在企业中搭建 monorepo 工程,我们有多种方案可供选择,其中常见的有三种:

• Lerna:Lerna 官网

• Yarn+Workspace

• Pnpm+Workspace

综合考虑,我们选择了Pnpm方案来搭建工程。Pnpm 内置了对 monorepo 的支持,搭建过程简单快捷,门槛低,非常适合我们的需求。

工作区(Workspace)

在软件开发中,“工作区”是一个用于组织和管理项目文件、资源以及工具的逻辑容器。它就像一个工作空间,里面包含了工作所需的一切。

工作区的主要功能包括:

• 组织和管理项目文件:提供存储和组织项目文件的结构,如源代码、配置文件、测试文件等。

• 跨项目共享设置和工具:允许在多个项目间共享设置、依赖和工具,保持项目一致性,减少切换开销。

• 支持协同开发:团队成员可在同一工作区访问和修改项目文件,提高协同效率。

在许多编程语言、框架和开发工具中,都能看到工作区的概念。Pnpm 也提供了工作区功能,用于管理 monorepo 风格的多个项目。创建 Pnpm 工作区非常简单,只需创建一个名为pnpm-workspace.yaml的文件,并在其中定义包含的目录。例如:

1packages:
2  # packages/ 下所有子包,但是不包括子包下面的包
3  - 'packages/*'
4  # components/ 下所有的包,包含子包下面的子包
5  - 'components/**'
6  # 排除 test 目录
7  - '!**/test/**'

搭建 Monorepo 工程

• 创建新目录:

1mkdir frontend-projects2

• 初始化目录:

1pnpm init

• 创建工作空间文件:

1touch pnpm-workspace.yaml

• 配置工作空间:

1packages:
2  - 'components/*'
3  - 'utils/*'
4  - 'projects/*'

上述配置将componentsutilsprojects目录下的所有子包纳入工作空间,使项目间能相互引用。其中:

components:存放公共组件。

utils:存放工具库。

projects:存放各个项目。

封装公共函数库

utils下的tools目录为例,这是一个公共函数库,可正常打包、发布,并被工程中其他项目引用。首先,使用pnpm init初始化该目录。

接下来,考虑 TypeScript 的安装位置。由于 TypeScript 不仅tools会用到,其他项目大概率也会用到,因此选择将其安装到工作空间中:

1pnpm add typescript -D -w

-w表示安装到工作空间。

源码开发

源码开发如下:

1// src/index.ts
2export * from "./sum.js";
3export * from "./sub.js";
4
5// src/sum.ts
6export function sum(a: number, b: number) {
7  return a + b;
8}
9
10// src/sub.ts
11export function sub(a: number, b: number) {
12  return a - b;
13}

测试

为确保公共函数库的每个方法都可靠,需进行测试。选择 Jest 进行测试,同样将其安装到工作空间:

1pnpm add jest jest-environment-jsdom @types/jest -D -w

测试代码如下:

1// tests/sum.test.ts
2import { sum } from "../src/sum";
3
4test("测试 sum 方法", () => {
5  const result = sum(1, 2);
6  expect(result).toBe(3);
7});
8
9// tests/sub.test.ts
10import { sub } from "../src/sub";
11
12test("测试 sub 方法", () => {
13  const result = sub(10, 3);
14  expect(result).toBe(7);
15});

Jest 配置

创建 Jest 配置文件:

1npx jest --init

为使 Jest 识别 TS 文件,安装以下依赖:

1pnpm add ts-jest ts-node -D -w

并确保 Jest 配置文件中的preset设置为ts-jest

TypeScript 配置

创建 TS 配置文件:

1npx tsc --init

修改配置如下:

1{
2  "target": "ES6",
3  "module": "ES6",
4  "include": ["./src"],
5  "declaration": true,
6  "declarationDir": "./dist/types"
7}

打包与发布

选择 Rollup 进行打包,支持 CommonJS、Browser 和 ES Module 三种格式。安装以下依赖:

1pnpm add rollup rollup-plugin-typescript2 @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-json @rollup/plugin-babel @babel/preset-env -D -w

创建rollup.config.js配置文件:

1import typescript from "rollup-plugin-typescript2";
2import commonjs from "@rollup/plugin-commonjs";
3import resolve from "@rollup/plugin-node-resolve";
4import json from "@rollup/plugin-json";
5import babel from "@rollup/plugin-babel";
6
7const extensions = [".js", ".ts"];
8
9export default [
10  // CommonJS
11  {
12    input: "src/index.ts",
13    output: {
14      file: "dist/index.cjs",
15      format: "cjs",
16    },
17    plugins: [
18      typescript({
19        useTsconfigDeclarationDir: true,
20      }),
21      resolve({ extensions }),
22      commonjs(),
23      json(),
24    ],
25  },
26  // ESM
27  {
28    input: "src/index.ts",
29    output: {
30      file: "dist/index.js",
31      format: "es",
32    },
33    plugins: [
34      typescript({
35        useTsconfigDeclarationDir: true,
36      }),
37      resolve({ extensions }),
38      commonjs(),
39      json(),
40    ],
41  },
42  // Browser-compatible
43  {
44    input: "src/index.ts",
45    output: {
46      file: "dist/index.browser.js",
47      format: "iife",
48      name: "jsTools",
49    },
50    plugins: [
51      typescript({
52        useTsconfigDeclarationDir: true,
53      }),
54      resolve({ extensions }),
55      commonjs(),
56      json(),
57      babel({
58        exclude: "node_modules/**",
59        extensions,
60        babelHelpers: "bundled",
61        presets: [
62          [
63            "@babel/preset-env",
64            {
65              targets: "> 0.25%, not dead",
66            },
67          ],
68        ],
69      }),
70    ],
71  },
72];

修改package.json

重点配置如下:

1{
2  "main": "dist/index.cjs",
3  "module": "dist/index.js",
4  "type": "module",
5  "types": "dist/types/index.d.ts",
6  "exports": {
7    "require": "./dist/index.cjs",
8    "import": "./dist/index.js"
9  },
10  "scripts": {
11    "build": "rollup -c"
12  }
13}

运行pnpm build进行打包,完成后会在tools根目录下生成dist目录,包含打包后的文件。之后可将dist上传到 npm 或私服。

测试项目引用

projects下创建新项目tools-test-proj,使用pnpm init初始化。由于tools-test-proj需要使用tools中的工具方法,可直接从工作空间安装:

1pnpm add tools -w --filter tools-test-proj

安装完成后,package.json中会显示该依赖,且来自工作空间:

1"dependencies": {
2  "tools": "workspace:^"
3}

tools-test-proj目录下创建src源码目录,写入以下代码:

1import { sum, sub } from "tools";
2
3console.log(sum(1, 2));
4console.log(sub(10, 3));

调整package.json

1"type": "module"

调整 TS 配置文件:

1"target": "ESNext",
2"module": "ESNext",
3"moduleResolution": "node",
4"outDir": "./dist",
5"include": ["./src"]

配置脚本:

1"scripts": {
2  "start": "tsc && node ./dist/index.js"
3}

执行pnpm start,项目成功引入tools依赖