从零开发自己的工具库(一)配置 TS + Rollup + Jest

日常开发中,我们经常会用到一些通用的方法,导致每次写新项目,都得复制粘贴。我们可以将这些通用处理逻辑封装成工具库,发布到 NPM 上。这样,每次只要 install 以下即可,其他开发者能共享这些库,也算是为开源做出一点贡献~

接下来,我们就尝试自己从零开始搭建一个工具库,成品可以参考 utils ,欢迎 star 🤞❤️

创建项目

首先在自己的 Github 上创建项目后,拉取到本地,然后 npm init -y

现在,项目中就有了最初的 package.json 文件。当然,这份文件还不够完善,在后续的各种配置中,我们会逐步修改这份文件。

喜欢用 pnpmyarnbun 或是其他工具的也一样,此处无需赘述。

配置 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 文件夹,里面会有 cjsesmumd 三种规范的 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',
    },
}

总结

  1. TypeScript 的配置;
  2. Rollup 的配置;
  3. Jest 的配置;
  4. 单元测试的编写;
  5. 搭建问题的总结。

下篇我们将围绕开发中的代码质量,使用 ESLint 、Prettier、Husky、Commitlint 等工具对代码编写、提交进行规范约束。

参考资料

使用Typescript和Rollup从零开发一个工具库

typescript 之 tslib 是什么,你需要它吗

typescript-syntax-requires-imported-helper-but-module-tslib-cannot

stackoverflow | This syntax requires an imported helper but module 'tslib' cannot be found

让 Jest 支持测试 ESM 业务代码

使用Jest进行React单元测试

test-environment-jest-environment-jsdom-cannot-be-found

Jest setup "SyntaxError: Unexpected token export"

相关推荐
安冬的码畜日常7 小时前
【The Art of Unit Testing 3_自学笔记06】3.4 + 3.5 单元测试核心技能之:函数式注入与模块化注入的解决方案简介
笔记·学习·单元测试·jest
王解9 小时前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
鸿蒙开天组●12 小时前
鸿蒙进阶篇-网格布局 Grid/GridItem(二)
前端·华为·typescript·harmonyos·grid·mate70
zhizhiqiuya12 小时前
第二章 TypeScript 函数详解
前端·javascript·typescript
初遇你时动了情1 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
王解1 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
_jiang1 天前
nestjs 入门实战最强篇
redis·typescript·nestjs
清清ww1 天前
【TS】九天学会TS语法---计划篇
前端·typescript
努力变厉害的小超超2 天前
TypeScript中的类型注解、Interface接口、泛型
javascript·typescript
王解3 天前
Jest进阶知识:整合 TypeScript - 提升单元测试的类型安全与可靠性
前端·javascript·typescript·单元测试