工程化治理主要分为以下几个方面:
- 静态检查:typescript + ESLint
- 开发体验: 打包工具和Mono-repo管理
- 代码质量: 测试
静态检查:
TS 和ESLint这些工具本质上是对代码做静态检查,尽早发现隐藏的bug
TS和ESLint的区别: TS有ESLint没有的类型检查
相同点:都具有语法错误检查的能力
TS + ES相结合:ES对代码风格做一个规范,TS主要负责对代码语法和语义上的错误进行静态检查
从AnyScript到TypeScript
用TS,一个很重要的区别就是有没有在配置中打开strict 选项
没有的话,其实使用的是AnyScript,在类型上没有约束,和JS没有太大区别,如果是从 JS 迁移到 TS 的项目,这个选项应该关闭,因为老的 JS 代码没有写类型。但如果是全新的纯 TS 项目,strict 是一定要打开的。现在 CRA 这样的脚手架创建的项目也是默认开启了 strict 模式的。
下面说说一些 strict 模式下的常见问题以及一些类型的技巧:
noImplicitAny
场景1: 函数的入参没有定义类型,含有隐式any, strict模式不允许
场景2: 没有显示声明对象的index signiture
const props = {
foo: "bar"
}
props['foo'] = 'bar';
// Element implicitly has an 'any' type because expression
// of type 'string' can't be used to index type
修改
interface Props {
foo: string;
[key:string]:Props[keyof Props];
// 在key值确定的情况下,可以用keyof来获取一个接口的所有key 组成的联合类型
}
const props: Props = {
foo: "bar",
}
props["foo"] = "bar"; // ok
props["bar"] = 'bar"; // error
strictNullChecks
场景: 获取一个可能是null或undefined变量上面的方法或属性
导致的原因: GUI场景(全局变量),如:this.graph,很多的成员变量是会在组件初始化之后才有值的,初始值这个时候就是undefined
解决方案:对于 strict 模式下的 strictNullChecks,我们可以用 type guards(if(this.graph)this.graph.on()
),type assertion(!),optional chaining(?) 三种办法去告诉编译器,这里的操作是安全的。
关键是,Optional 的值在 GUI 编程中是很正常的,我们要学会去处理和面对这些情况,把 undefined 和 null 作为一个单独的类型来对待。
如何查看第三方库是否有类型定义
方法1: 查找项目中的package.json中是否有types字段
方法2: 类型定义可能是一个单独的类型包,如: @types/react
没有类型的库最好不要在TS项目中使用
高级类型
除了 Type Guard,交叉类型,联合类型和上文提到的可以为 null 的类型之外,最关键的是: Mapped types(映射类型)、Conditional Types(条件类型)、Index types(索引类型)
在使用泛型时,这些技巧可以让我们对类型进行“编程”,想象一下对类型变量使用 ?三元表达式或者 Array.prototype.map 这样的方法。
exp: 条件类型
T extends U ? X : Y // 如果T包含U所以的属性,这个类型是X, 否则就是Y
function process(text:string|null):string|null{
return text && text.replace(/f/g, "p")
}
process("foo").toUpperCase(); // Type error
// 解决方案: 条件类型
function process<T extends string | null >(
text: T
): T extends string ? string : null {
return text && text.replace(/f/g, "p")
}
ESLint 和 Prettier
ESLint + TypeScript: 取代TSLint的新方案
TSLint 在 2019 年宣布未来项目将会废弃。TS 官方推荐 ESLint 作为 Linter
如何配置让ESLint支持解析TS文件?
@typescript-eslint/parser
配套还有 @typescript/eslint-plugin 作为 ESLint 下针对 TS 订制的 Lint 规则
JS + TS混合项目 ESLint 配置
在又有 JS 又有 TS 文件的情况下,ESLint 需要只在 TS 文件上,执行 TS 相关规则的校验,不然在校验 JS 时很多 TS 规则也会生效,这样就造成了困扰。
解决方案就是使用 ESLint 的 override:
"overrides": [
{
"files": "**/*.ts",
"extends": [
"eslint-config-airbnb",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"prettier",
"prettier/react"
],
}
]
// 只有处理TS文件时才加入TS的相关规则
// 另外,有一些 JS 规则在 TS 文件上使用时也会出现问题,比如:
// https://github.com/eslint/eslint/issues/8813。解决方案也是使用 override。
Pre commit hook
Pre commit hook: 是指设置一个 Git hook,在提交之前运行。前端项目一般利用这个机会运行静态代码检查和代码格式化,比如 ESLint,Prettier。也可以运行测试或者 TS 编译等等检查。
具体设置的流程可以参考:Configuring Pre-commit Hooks for Prettier and Linting on a TypeScript Project。
commit hook 也可以用 -n 跳过,所以还应该在 CI 时加上 ESLint,来保证不规范的代码提交被立刻发现。
开发体验
目标:一键配置,一键升级
打包工具
关于模块的格式,我们听过 AMD,CommonJS,UMD,ES Module 等等。
Nodejs 的 CommonJS
Webpack 这样的打包工具也只兼容 CommonJS 模块
ES Module,这个标准是未来浏览器支持的标准,Nodejs 也会支持。
目前的构建工具都支持原生的 ES Module 格式(之前需要用 babel 转为 CommonJS)
构建 ES module:Rollup/Babel
我们只需要把 package.json 的 module 字段指向打包出的 es module 格式的文件,构建工具就会使用 module 字段而不是 main 字段进行构建了。
打包工具选择:
Webpack 目前不支持输出 ES module,排除
Rollup 和 Babel 是可行的两种方案。
Rollup 是目前最流行 JS 库打包工具,React,Vue 之类的开源项目都在使用 Rollup。Rollup 支持输出 CommonJS,UMD,ES Module 在内的主流格式,并可以通过插件支持 CSS 等静态资源的处理。
Rollup 和 Webpack 的主要区别就是 Rollup 是以构建 JS 为核心的,并且从一开始就是基于 ES Module 的,如果要兼容 CommonJS 代码,需要引入额外的插件。Webpack 更关注的是所有资源的构建,并且强调 Code Splitting 的能力,专注于 Web 应用的打包。Rollup 更轻量和专注,而且支持 ES Module 的输出,所有在 JS 库打包这个方面 Rollup 是首选。
Babel其实本身只是一个转译工具。但 Babel 可以通过插件支持 TS 代码的转译,还有 JSX 的转译(老本行),所以如果是简单的 TS 库,可以直接用 Babel 进行转译,输出的就是原汁原味的 ES Module(因为 Babel 压根没有去解析模块,只是单纯的转译代码)。需要注意的是 Babel 的 TS 转译只是转译,不是编译,所以类型错误是不会报出的,需要额外跑 tsc 来对 TS 代码进行类型校验。其他的静态资源也是一样的,需要单独跑 task。
专注与 JS 库打包的 Father
Father 可以简单理解为是 JS 库领域的 CRA 或者 Umi。Father 封装了 Rollup 和 Babel 两套工具链。
在最简单的情况下:我们只需要告诉 Father 需要什么格式的输出就可以构建成功,比如:
father build --esm --cjs --umd --file bar src/foo.js
mono-repo 管理:Lerna
Lerna 是用于管理拥有多个 npm package 的 mono repo 的工具。mono repo 就是指多个项目的源码放在同一个仓库下进行管理。
简单的说,Lerna 的功能就是一键在多个 package 中同时运行一些命令。而且运行的时候还会根据 package 之间的依赖拓扑关系,对命令的启动顺序进行编排。同时 Lerna 的 bootstrap 命令可以把 package 之间相互的依赖,自动 link 到 package 自己的 node_modules 里面。这可以说是最大的一个卖点。Lerna 之前如果要在本地开发多个相互依赖的 npm 包,那就要敲一堆的 npm link,而且还容易出问题。
mono-repo 这种方式本身也是为了提升多个 npm package 的情况下,管理源代码的效率,以及共享基础设施。因此 Lerna 其实是提升了开发者开发基于 mono repo 的前端项目的体验。
前端的组件库一类的项目,用 Lerna 是非常合适的。
代码质量
这里的代码质量主要是指测试
React 组件测试技术选型: jest 测试框架 + Enzyme/ react-testing-library DOM Util(用于组建渲染和DOM操作)
在 React 16 下,Enzyme 有一些问题,比如 shallow 模式下不支持 useEffect。
react testing library 是在 React 官方的 test util 基础上包装的,要更轻量一些。
常见的测试技巧
react testing library 的测试套路
我们只需要调用 render ,把组件渲染出来就行了:
asTragment
queryByText
rerender
queryByTextId
act()
fireEvent
Mock 浏览器事件
Canvas 测试
如果测试的目标中有 Canvas,情况分两种:
Canvas 上的内容是组件用到的图表库一类的渲染结果,和组件本身的正确性无关
Canvas 上的内容就是测试的目标,比如给图表库写测试
如果是前者,我们可以 Mock 掉 Canvas,使用 jest-canvas-mock 可以很方便的一键 Mock。
如果是后者,我们可以用 jest-electron 去运行一个真实的浏览器,来测试 Canvas 的绘制结果。
覆盖率
Jest 配置了 collectCoverage: true 之后就会在本地生成测试覆盖率报表。用 http-server 起一个本地服务器就可以看到
补充:
ESLint + TS 这方面的资料很多,Using ESLint and Prettier in a TypeScript Project 这篇文章讲了如何从 TSLint 迁移到 ESLint。
还有以下的文章,都讲解了相关的配置(ESLint + TS):
Integrating Prettier + ESLint + Airbnb Style Guide in VSCode
Setting up ESLint with Prettier, TypeScript, and Visual Studio Code
From ESLint to TSLint and Back Again
关于 TSLint 到 ESLint 的切换的背景,可以看 typescript-eslint 这个项目的 README,讲的非常详细
使用 ESLint 的好处就是:可以背靠 ESLint 的生态,像 Airbnb 这样的规则集就可以直接用于 TS 项目。上面列举的博客就有讲如何配置 Airbnb + typescript-eslint + prettier 三种规则集。让项目可以用 typescript-eslint 来规范 TS 代码(TS 特有的 Lint 规则),用 Airbnb 来规范 React 和 JS 代码(TS 是 JS 的超集),用 Prettier 相关规则来关闭前两个规则中和 Prettier 代码风格冲突的规则。三者集合就是目前比较完善,好用的 Lint 规则了。
Airbnb 中有一些规则,比如要求 React 组件声明 PropTypes,是不适用于 TS 项目的,所以需要在 ESLint 配置文件里关掉。其他类似的配置有很多,我们不用死板的遵守 Lint 规则,而是关闭不合适的规则,只取其精华
在 React 16 下,Enzyme 有一些问题,比如 shallow 模式下不支持 useEffect。详见:https://github.com/airbnb/enzyme/issues/2086
如果对 react testing library 不熟悉,可以看官网和这篇教程。
https://testingjavascript.com/ 这个测试教程网站,可以了解到测试相关技术的大图,如果对测试的分类和作用不太清楚可以看一下这个网站。