以下是我近期日常开发的一些心得

A

开发流程上,应该接口先行,以接口文档为契约,前后端并行开发,这样效率更高

项目上有什么风险要提前暴露出来,问题发现得越早,解决的成本越低

重复的功能封装成组件,文档注释要健全,不然就成了你的专属组件了

B

前期的一些准备工作可以很好的提升后期的开发速度,迭代0很重要,这一块可以重视起来

小项目组一起CodeReview可以很好的提升代码质量,让代码规范,设计模式行成一个统一的风格

有风险要提前预知,做好准备,找相应的人员反馈讨论出解决方案

C

项目中表单检验可以在提交的时候拿到FormDuck在里面把错误信息设置进去,并在页面展示出来,这一块文档示例做得不好,需要自己看源码,后期可以加强demo的建设

需求确定下来之后还是有很多可以调整的空间,并不是定死的,甚至有些需求是伪需求,问题发现得越早解决的代价越小

需求开发中有时需要造数据,这些数据有部分很难构造,如果有一个自动化测试数据平台,里面有很多现成的数据,那开发测试造数据的体验就会很好

D

目前一些独立版的交付都是将nginx配置和一些静态文件打包交付给运维,后续可以考虑直接通过Docker镜像来交付,让交付的统一性和稳定性有保障

E

TSF-TStack发布到现场环境就出问题,原因还是测试环境和现场环境不一致,这一块环境问题还是要尽量保证一致,可能一个配置不同,就会导致上线时出问题

TSF-Tea2升级后有大量警告,尽排查,关闭Tea2的HMR功能可消除警告,另外javascript:;也不要再使用,如果旧组件不好改,可将原组件拆成一个if/else写成两部分

F

当我们开发点击按钮发送异步请求,然后显示Loading状态,如果一点击就马上显示Loading,然后异步请求很快就完成了,就会造成一个视觉上的闪烁,这样的体验不是特别好,

我们可以让Loading状态在点击后500ms后再显示,如果异步请求很快完成,那么这个Loading状态就不会显示,体验上会好很多。

在redux-saga中,我们可以这样实现这个效果,伪代码如下:

const task = yield fork(showLoadingTask)

try {
    yield asyncTask()   
} catch() {
    cancel(task)
}
yield hideLoading()

在外层我们就不能使用takeLatest这种来监听到来的action,而要使用takeLeading,这个API在接收到一个action没有处理完成之间,不会再接收其它action,

就可以很好的实现我们需要的效果了,由于takeLeading是redux-saga较新版本才提供的API,如果我们的版本受到限制,也可以自己基于while 和take 自己实现一个,

一点都不复杂,就是要注意用tryCatch包装一下逻辑处理好异常

G

saga-duck如果子类要覆盖父类的一些行为,可以先将方法从takeLatest中抽出来,形成类方法,再在子类中重写此方法,比通过传参来控制有更高的灵活性和可扩展性

H

1.熔断需求的界面开发逻辑很复杂,有动态表单,需要根据不同的下拉类型,显示不同的表单,并且表单里面可以同时新增删除多个表单,相当于一个数组的结构。

还有一个API选择的功能,要在多个TagSelect组件间共用一份数据,这一个Select组件选择了某些选项,则下一个Select组件需要禁用掉这些选项。

还需要同时支持编辑、删除操作。一开始只是基于一个FormDuck实现的,于是在数据结构上就无法直接用类似<Input field={field} />这种写法,需要自己绑定value和onChange事件,

并将onChange事件自己实现field.setValue(xxxField, xxxValue),因为这里面的xxxValue是这个数组对象,所以实现出来之后可读性不是很好,后来想到了使用动态Duck。

于是在新增多个表单时就使用动态Duck,new出一个新的FormDuck,这样在Input组件中就能直接使用field了,代码的可读性提升了不少。

提交表单和派发时需要总是要遍历所有动态Duck,这些地方代码会有点啰嗦。和后续可以再封装一层,将action派发直接到所有动态Duck上,另再封装删除动态Duck的方法。

2.接入CI的时候,Tea build时遇到了内存溢出,1.4G的堆内存全被使用,GC无法回收内存,导致CI20分钟后才报超时。分析一直找不到原因,本地build也没有报错,

最后将CI的配置修改为image: node:12问题就自然解决了,神奇。下周再将本机的node装回10版本,build下,看会是什么效果。所以大家有时遇到一些奇怪的现象,

先将环境配置为一致,看问题能否解决,再寻找原因。

I

redux-saga中有三级联动需要请求接口时,有一种最佳实践,伪代码如下:

yield fork(function*() {
    yield* initData()
    yield fetchFirstLevelSelect()
    yield runAndWatchLatest([action], watchValueSelector, function*() {
        yield fetchSecondLevelSelect()
        yield runAndWatchLatest([action], watchValueSelector, function*() {
            yield fetchThirdLevelSelect()            
        })
    })
}

这样,在初始数据返回前,两级联动的action不会被触发,初始数据加载完后,自动run后续action,保证流程合理清晰

此外,当后请求的接口数据返回比先请求的接口响应早时,任务可以被自动取消,以此保证数据不会错乱。但要注意tryCatch cancel事件,以便清理未完成的状态,保证逻辑正确。

J

代码分支最好都基于master,拉出特性分支,最后再合回master,然后通过环境变量打包出各版本,这样可以保证代码的稳定性,不会出现有些全局修改只改了几个分支,而漏了一个分支没修改

Git分支管理想要提交线好看,有一种简单的最佳实践,本地开发分支基于rebase,合并时先rebase master 变基到master,然后提merge request 使用fast forward 合并,这样提交线也会是一条直线

K

项目代码model.ts 中有很多async方法,伪代码如下

async getData function() {
    const data = await ajax(params)
    return data
}

中间变量和await 完全是没必要的 可以简化成一个语句

return ajax(params)

更简洁可读性更好

L

项目代码model.ts 中有很多try catch代码,伪代码如下

async getData function() {
    try {
        const data = await ajax(params)
        return data
    } catch(e) {
        throw e
    }
}

如果catch 中完全不做处理只是向外throw,那这个try catch 块就是没有意义的 可以省略

M

TS3.8来了,仅导入、导出声明很好用,还有top-level await

重构移除了项目中的很多any,发现原有代码中使用any的地方有很多边际条件有bug

使用二分法定位了一个隐藏bug,最后发现原因是一个日期控件range是(29天前-当前),如果初始进入点29天前,然后切换Tab,再进入就会渲染错误,堆栈很难定位问题

原因是切换tab后 range会重新计算,而状态记录的还是之前的值不在range范围内

await new Promise(resolve => {
setTimeout(resolve, 1000)
})

N

控制台页面开发时,Dialog.show方法会新建一个Redux Store,这时如果想跟外层页面上的Redux Store通信,直接通过传回调方法dispath action 是行不通的

这个时候需要使用Redux Saga 的eventChannel 实现外部事件触发内部action

O

ORAGNGE.CI 配置时使用Yaml中DSL,可以大大简化繁琐的配置。&用来建立锚点,«表示合并到当前数据,*用来引用锚点。

http://nodeca.github.io/js-yaml/ 这个网站可以将YAML转化为JS代码,用来验证是否有语法错误和结果符合预期。

P

TS中 使用yield 从接口获取数据,其返回类型值是any,这个问题TS至今没有解决,

为了获取到正确的类型,我们可以声明一个推导类型动态获取到返回类型,代码如下:

// 获取Promise的泛型
type R<T> = T extends Promise<infer U> ? U : T
// 获取方法的返回值
type YieldReturn<T> = R<ReturnType<T extends (...args: any) => any ? T : any>>

使用方法如下:
export function fetchApi(regionId, params: Params) {
  return capiRequest<Result>(regionId, 'API', params)
}
const response = yield fetchApi(regionId)

这样获取到的response 变量类型是any
改为如下
const response: YieldReturn<typeof fetchApi> = yield fetchApi(regionId)

这样response 的类型可以正确获取到为Result,代码可能还是有些繁琐(需要手动指明类型),但可以动态获取到正确的类型了

Q

OCI现不支持并行任务,我们可以通过OCI 提供的OPEN API能力达到并行任务的效果,可以让两个串行的任务并行运行,从而大大减少时间。

具体细节如下:

需要在项目下添加.orange.ci.token 文件,里面内容填上一个随机字符串,然后通过HTTP POST方法将TOKEN内容放入HTTP请求头中即可主动触发CI构建。

我在CI的A任务下判断指定文件是否改变,决定是否触发B任务,使用Curl发送HTTP请求,通过CI提供的内置变量构建HTTP请求体发送到OCI提供的REST服务上,触发B任务开始。

因为Curl运行在A任务的开始,如果触发B任务,基本相当于A、B任务并行运行。

R

CI中可以使用npm ci 安装项目依赖的包,使用npm ci的好处是可以保证安装包的一致性,它在项目不存在package-lock.json 文件时使用会报错,

如果依赖包在package-lock.json文件和package.json 中定义的规则不匹配则会报错,而不是像npm install一样静默更新。

使用了package-lock.json文件后,npm install 就类似一个纯函数,固定的输入得到固定的输出,就算依赖的小版本更新了,也还是会继续使用lock的版本。

如果还想使用语义化版本得到较新的包版本,则需要删除lock文件,手动执行npm install,得到新的lock文件和node_modules目录。

npm的lock机制在5.7版本后稳定了,尽量使用6以上版本。