日常开发中,我们经常会用到一些通用的方法,导致每次写新项目,都得复制粘贴。我们可以将这些通用处理逻辑封装成工具库,发布到 NPM 上。这样,每次只要 install 以下即可,其他开发者能共享这些库,也算是为开源做出一点贡献~
接下来,我们就尝试自己从零开始搭建一个工具库,成品可以参考 utils ,欢迎 star 🤞❤️
创建项目
首先在自己的 Github 上创建项目后,拉取到本地,然后 npm init -y
。
现在,项目中就有了最初的 package.json
文件。当然,这份文件还不够完善,在后续的各种配置中,我们会逐步修改这份文件。
喜欢用 pnpm
、 yarn
、bun
或是其他工具的也一样,此处无需赘述。
配置 TypeScript
安装 TypeScript
npm i typescript -D
安装 TypeScript 依赖。
npx --package typescript tsc --init
生成一个默认的配置文件。
在 TypeScript 项目中,最重要的就是这份 tsconfig.json
配置文件,它给项目提供了使用 TS 时的语法规范、编译标准以及环境配置等等选项,属性非常多,不过生成的默认文件已经给我们配置好了部分选项,并给所有的选项添加了注释,你也可以通过访问 aka.ms/tsconfig 来查阅相关配置。
拆分 tsconfig.json
我们将其中和语法相关的编译器选项单独提取出来,放在 tsconfig.base.json
中:
json
{
"compilerOptions": {
"incremental": true,
"target": "es5",
"lib": ["ESNext", "DOM", "ES2018"],
"jsx": "preserve",
"experimentalDecorators": true,
"jsxFragmentFactory": "Fragment",
"module": "esnext",
"moduleResolution": "node",
"baseUrl": "./",
"paths": {
"@/*": ["packages/*"]
},
"resolveJsonModule": true,
"allowJs": true,
"checkJs": true,
"declaration": true,
"sourceMap": true,
"importHelpers": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"strictFunctionTypes": false,
"skipLibCheck": true
}
}
再将工程环境配置提取到 tsconfig.json
文件中,并使用 extends
继承 tsconfig.base.json
的所有配置:
json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": "./packages",
},
"include": [
"packages/**/*.ts",
"packages/**/*.tsx"
],
"exclude": [
"packages/**/__test__/*.test.ts",
]
}
declaration: true
选项会自动从项目中的 ts 和 js 文件生成 .d.ts
声明文件。
rootDir: './packages'
指定了根目录,该目录下的文件结构会在打包的目录中得到保留,后续所有开发的 utils 都要放在这个文件夹中。
outDir: 'lib'
指定了输出目录,编译后的 js
、.d.ts
声明文件等都会打包到 lib
文件夹内。
include
指定需要编译处理的文件列表,解析路径相对于当前项目的 tsconfig.json
文件位置。
exclude
指定在解析 include
时应跳过的文件,所以该配置项只会对 include
包含的文件有影响。
FAQ
1. This syntax requires an imported helper but module 'tslib' cannot be found
这是因为 tsconfig 配置了 importHelpers: true
,开启该选项,一些低版本降级操作会从 tslib
中导入。如果你的 target
编译目标使用的是 ES5
这种较低版本,但语法中出现了 ES6
或更新的语法,那么该报错就会出现。
所以,你可以通过执行 npm install -D tslib@latest
来解决这个问题。tslib
是把一系列的降级代码(函数)抽离并合并导出的库。目的是降低编译后代码的数量,起到压缩代码体积的作用。
配置 Rollup
安装 Rollup 及其相关插件
bash
npm i rollup @rollup/plugin-node-resolve @rollup/plugin-typescript @rollup/plugin-commonjs @rollup/plugin-terser rollup-plugin-postcss -D
插件作用分别如下:
包名 | 作用 |
---|---|
@rollup/plugin-node-resolve | 处理路径 |
@rollup/plugin-typescript | 支持 TS |
@rollup/plugin-commonjs | 处理 CommonJS |
@rollup/plugin-terser | 压缩 UMD 规范的输出文件 |
rollup-plugin-postcss | 处理 CSS |
配置 rollup.config.js
js
const resolve = require('@rollup/plugin-node-resolve');
const typescript = require('@rollup/plugin-typescript');
const commonjs = require('@rollup/plugin-commonjs');
const terser = require('@rollup/plugin-terser');
const postcss = require('rollup-plugin-postcss');
module.exports = [
{
input: './packages/index.ts',
output: [
{
dir: 'lib',
format: 'cjs',
entryFileNames: '[name].cjs.js',
sourcemap: false, // 是否输出sourcemap
},
{
dir: 'lib',
format: 'esm',
entryFileNames: '[name].esm.js',
sourcemap: false, // 是否输出sourcemap
},
{
dir: 'lib',
format: 'umd',
entryFileNames: '[name].umd.js',
name: '$utils', // umd 模块名称,相当于一个命名空间,会自动挂载到window下面
sourcemap: false,
plugins: [terser()],
},
],
plugins: [
postcss({
minimize: true,
extensions: ['.css'],
extract: true,
}),
resolve(),
commonjs(),
typescript({ module: 'ESNext' }),
],
},
];
修改 package.json
我们修改其中部分配置:
json
{
"main": "lib/index.cjs.js",
"module": "lib/index.esm.js",
"jsnext:main": "lib/index.esm.js",
"browser": "lib/index.umd.js",
"scripts": {
"build": "rollup -c",
},
"files": ["lib"],
"types": "lib/index.d.ts",
}
配置说明如下:
配置项 | 说明 |
---|---|
main | Browser 和 Node 环境中指定的项目入口文件 |
module | 指定 ESModule 模块的入口文件 |
jsnext:main | 同上,不过这个是社区规范,上面是官方规范 |
browser | UMD 规范,当直接在浏览器中开发时,可下载 release 包并在浏览器中使用 script 导入 |
types | TS 类型声明文件路径 |
files | 约定 NPM 发包时包含的文件和文件夹 |
执行 npm run build
就会生成一份 lib 文件夹,里面会有 cjs
、esm
、umd
三种规范的 js 文件,以供不同方式引入。除此之外,还有自动生成的 .d.ts 类型声明文件,是不是很方便~
配置 Jest
安装测试框架 Jest 及其相关插件
bash
npm i jest jest-environment-jsdom ts-jest @types/jest -D
配置 jest.config.js
执行 npx jest --init
命令,并进行配置选择:
sql
jest --init
✔ Would you like to use Jest when running "test" script in "package.json"? ... yes
✔ Would you like to use Typescript for the configuration file? ... no
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? ... yes
✔ Which provider should be used to instrument code for coverage? › babel
✔ Automatically clear mock calls and instances between every test? ... no
此时会生成一份 jest.config.js
文件。
配置 Babel
执行以下命令安装 Babel 相关插件。
bash
npm i babel-jest @babel/core @babel/preset-env -D
在项目的根目录下创建 babel.config.js
注意不是 .babelrc
,通过配置 Babel 使其能够兼容当前的 Node 版本。
js
// only used by jest 不应该影响业务代码构建!
module.exports = {
presets: [
'@babel/preset-env'
],
};
接着修改 jest.config.js
部分配置:
js
const config = {
coverageProvider: 'babel',
testEnvironment: 'jsdom',
testEnvironmentOptions: {
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
},
transform: {
'^.+\\.(js|jsx)$': 'babel-jest',
},
}
module.exports = config;
最后在 package.json
中添加两条命令
json
{
"scripts": {
"test": "jest",
"coveralls": "jest --coverage",
}
}
test
会执行编写的单元测试,而 --coverage
则会在项目根目录下生成一份 coverage
文件夹,里面包含完整的测试报告
我们可以通过打开 index.html
在浏览器中查看报告:
coverage
文件夹无需上传,记得在 .gitignore
中忽略此文件。
编写单元测试
我们先在 packages/is
目录下写一个判断是否为 null 的方法:
ts
// packages/is/index.ts
export const isNull = (val: any) => val === null;
在入口中导入导出:
ts
// packages/index.ts
export { isNull } from './is';
在 packages/is
下新建一个 __test__
测试目录,创建 index.ts
文件,编写以下单元测试:
ts
// packages/__test__/index.ts
import { isNull } from '../index';
describe('isNull', () => {
it('should return true if the value is null', () => {
const result = isNull(null);
expect(result).toBe(true);
});
it('should return false if the value is not null', () => {
const result = isNull('some value');
expect(result).toBe(false);
});
});
执行 npm run test
,测试通过会有类似如下的提示:
FAQ
1. Error: Test environment jest-environment-jsdom cannot be found
从 jest 28.0.0 往后,jest-environment-jsdom
不再随 jest
内置,所以需要单独安装以支持 DOM 或 BOM 操作。
安装后,你可以在使用到 DOM 或 BOM 对象的测试文件的顶部加上一行注释即可运行:
js
/**
* @jest-environment jsdom
*/
或者在配置文件中修改运行环境为 testEnvironment: 'jsdom'
。
js
module.exports = {
// 支持测试环境访问dom
testEnvironment: 'jsdom',
// 配置测试环境 ua
testEnvironmentOptions: {
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
},
}
2. Cannot use import statement outside a module
Jest 本身支持 ESM,即单测可以如此写:
js
export const isNull = /** ...... */
但是当导入的文件本身也包含 ESM 模式的导入导出,就不会被 Jest 识别,此时就需要 Babel 进行编译,参见上述的有关 Babel 的配置。
3. SyntaxError: Unexpected token 'export'
如果你的文件中引入了三方依赖(比如 lodash-es),可能会出现以下错误:
Jest 默认情况下并不转换 node_modules
,但是 lodash-es 专门提供了 ESM 规范,所以在执行测试时,这些模块就需要 Jest 通过 Babel 进行转换。
我们可以通过 transformIgnorePatterns
配置项来配置 Jest 转换白名单,被匹配到的文件将会跳过转换:
js
module.exports = {
transformIgnorePatterns: ['<rootDir>/node_modules/(?!lodash-es)'],
}
4. Cannot find module '@/xxxx/xxxx'
项目中经常配置文件别名 alis 来简化路径,优化开发体验,但是 Jest 却无法识别这些代号。此时就需要配置 moduleNameMapper
,通过正则表达式建立别名到模块的映射。
js
module.exports = {
moduleNameMapper: {
'^@/(.*)': '<rootDir>/packages/$1',
},
}
总结
- TypeScript 的配置;
- Rollup 的配置;
- Jest 的配置;
- 单元测试的编写;
- 搭建问题的总结。
下篇我们将围绕开发中的代码质量,使用 ESLint 、Prettier、Husky、Commitlint 等工具对代码编写、提交进行规范约束。
参考资料
typescript-syntax-requires-imported-helper-but-module-tslib-cannot
stackoverflow | This syntax requires an imported helper but module 'tslib' cannot be found