├─ .github │ └─ workflows │ └─ ci.yml ├─ .yarn │ └─ ... ├─ node_modules │ └─ ... ├─ scripts │ ├─ e2e-tests │ │ └─ e2e-test-setup.sh │ └─ ... ├─ src │ ├─ api-server │ │ └─ ... │ ├─ back-for-front-server │ │ └─ ... │ └─ common-utils │ └─ ... ├─ .dockerignore ├─ .eslintrc.js ├─ .prettierrc.js ├─ .yarnrc.yml ├─ docker-compose.yml ├─ Dockerfile ├─ package.json ├─ README.md ├─ tsconfig.json └─ yarn.lock迁移之前的 Dockerfile(经过简化):
FROM node:16.16-alpine WORKDIR /backend COPY . . COPY .yarnrc.yml . COPY .yarn/releases/ .yarn/releases/ RUN yarn install RUN yarn build RUN chown node /backend USER node CMD exec node dist/api-server/start.js在共享存储库中维护多个服务器有以下好处:
├─ .github │ └─ workflows │ └─ ci.yml ├─ .yarn │ └─ ... ├─ node_modules │ └─ ... ├─ packages │ └─ common-utils │ └─ src │ └─ ... ├─ servers │ └─ monolith │ ├─ src │ │ ├─ api-server │ │ │ └─ ... │ │ └─ back-for-front-server │ │ └─ ... │ ├─ scripts │ │ ├─ e2e-tests │ │ │ └─ e2e-test-setup.sh │ │ └─ ... │ ├─ .eslintrc.js │ ├─ .prettierrc.js │ ├─ package.json │ └─ tsconfig.json ├─ .dockerignore ├─ .yarnrc.yml ├─ docker-compose.yml ├─ Dockerfile ├─ package.json ├─ README.md ├─ turbo.json └─ yarn.lock由于 Node.js 及其工具生态系统非常灵活,所以共享一个通用的方法会很复杂,因此请记住,为了让开发人员的体验至少与迁移前一样好,将需要进行大量的优化迭代。
3.在团队的帮助下,列出他们日常工作所需的所有工具、命令和工作流(包括 IDE 的特性,如代码导航、代码分析和自动补全)。这个需求列表(或验收标准)将帮助我们检查将开发体验迁移到 Monorepo 设置的步骤。这有助于确保在迁移时不会忘掉重要事项。
以下是我们决定满足的需求列表:
.yarn install 仍然安装依赖;# 这个脚本使用 Yarn 工作空间和 Turborepo 将存储库转换为 Monorepo set -e -o pipefail # stop in case of error, including for piped commands NEW_MONOLITH_DIR="servers/monolith" # 第一个工作空间的路径:"monolith" # 清理临时目录,即没有存储在 Git 中的那些 rm -rf ${NEW_MONOLITH_DIR} dist # 创建目标目录 mkdir -p ${NEW_MONOLITH_DIR} # 将文件和目录从 root 移动到 ${NEW_MONOLITH_DIR}目录 # ……除了那些绑定到 Yarn 和 Docker 的(目前) mv -f \ .eslintrc.js \ .prettierrc.js\ README.md \ package.json \ src \ scripts \ tsconfig.json \ ${NEW_MONOLITH_DIR} # 将新文件复制到 root 目录 cp -a migration-files/. . # 包括 turbo.json, package.json, Dockerfile, # 和 servers/monolith/tsconfig.json # 更新路径 sed -i.bak 's,docker\-compose\.yml,\.\./\.\./docker\-compose\.yml,g' \ ${NEW_MONOLITH_DIR}/scripts/e2e-tests/e2e-test-setup.sh find . -name "*.bak" -type f -delete # delete .bak files created by sed unset CI # to let yarn modify the yarn.lock file, when script is run on CI yarn add --dev turbo # 安装 Turborepo rm -rf migration-files/ echo "✅ You can now delete this script"我们在持续集成工作流中添加了一个作业(GitHub Actions),用于检查测试和其他常规 Yarn 脚本在迁移之后是否仍然可以正常工作:
jobs: monorepo-migration: timeout-minutes: 15 name: Test Monorepo migration runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: ./migrate-to-monorepo.sh env: YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # 允许 yarn.lock 变化 - run: yarn lint - run: yarn test:unit - run: docker build --tag "backend" - run: yarn test:e2e从单体的源代码转换生成第一个包
{ "name": "backend", "version": "0.0.0", "private": true, "scripts": { /* 所有 npm/yarn 脚本... */ }, "dependencies": { /* 所有运行时依赖 ... */ }, "devDependencies": { /* 所有开发依赖 ... */ } }以下片段摘自迁移之前 TypeScript 配置文件tsconfig.json :
{ "compilerOptions": { "target": "es2020", "module": "commonjs", "lib": ["es2020"], "moduleResolution": "node", "esModuleInterop": true, /* ... 多条让 TypeScript 更严谨的规则 */ }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist", "migration-files"]在将单体拆分成包时,我们必须:
nodeLinker: node-modules yarnPath: .yarn/releases/yarn-3.2.0.cjs根据 Yarn 迁移路径 的建议:
{ "name": "@myorg/backend", "version": "0.0.0", "private": true, "packageManager": "yarn@3.2.0", "workspaces": [ "servers/*" ] }从现在开始,每个工作空间必须有自己的package.json 文件,用于指定其包名和依赖。截至目前,我们只有一个工作空间“monolith”。在servers/monolith/package.json文件中使用组织名作为其名称的前缀,明确标明它现在是一个 Yarn 工作空间:
{ "name": "@myorg/monolith", /* ... */ }在运行完yarn install 之后,我们又修复了一些路径:
{ "name": "@myorg/common-utils", "version": "0.0.0", "private": true, "scripts": { "build": "swc src --out-dir dist --config module.type=commonjs --config env.targets.node=16", /* 其他脚本 ... */ }, "dependencies": { /* common-utils 的依赖 ... */ }, }注意:我们使用swc 将 TypeScript 转译为 JavaScript,但使用tsc 应该也可以获得类似的效果。此外,我们尽力让它的配置(使用命令行参数)与servers/monolith/package.json 中的配置一致。确保包会按预期构建:
$ cd packages/common-utils/ $ yarn $ yarn build $ ls dist/ # 应该包含 src/ 中所有文件的.js 构建接下来,更新根package.json 文件,将packages/ 的所有子目录(包括common-utils)也声明为工作空间:
{ "name": "@myorg/backend", "version": "0.0.0", "private": true, "packageManager": "yarn@3.2.0", "workspaces": [ "packages/*", "servers/*" ], /* ... */ }将common-utils 添加为服务器包monolith 的依赖:$ yarn workspace @myorg/monolith add @myorg/common-utils 。你可能已经注意到,Yarn 创建了一个到packages/common-utils/ (源代码就在这里)的符号链接node_modules/@myorg/common-utils 。
export { hasOwnProperty } from "@myorg/common-utils/src/index"更新服务器的Dockerfile ,以便构建包并包含在镜像中:
# 使用以下命令从项目根目录构建: # $ docker build -t backend -f servers/monolith/Dockerfile . FROM node:16.16-alpine WORKDIR /backend COPY . . COPY .yarnrc.yml . COPY .yarn/releases/ .yarn/releases/ RUN yarn install WORKDIR /backend/packages/common-utils RUN yarn build WORKDIR /backend/servers/monolith RUN yarn build WORKDIR /backend RUN chown node /backend USER node CMD exec node servers/monolith/dist/api-server/start.js这个Dockerfile 必须从根目录构建,那样它才能访问yarn 环境和那里的文件。注意:可以通过在Dockerfile 中将yarn install 替换为yarn workspaces focus --production来从 Docker 镜像中除去开发依赖,这要感谢 plugin-workspace-tools 插件,参考“使用 Yarn 3 和 Turborepo 编排和 Docker 化 Monorepo”一文中的介绍。
import { hasOwnProperty } from "@myorg/common-utils"即使我们在包的package.json 文件里指定"main": "src/index.ts" ,在运行转译构建时路径仍然会被破坏。作为补救使用 Node 的 条件导入,以使包的入口点可以适配运行时上下文:
{ "name": "@myorg/common-utils", "main": "src/index.ts", + "exports": { + ".": { + "transpiled": "./dist/index.js", + "default": "./src/index.ts" + } + }, /* ... */ }简而言之,增加一个exports配置项,关联包根目录的两个入口点:
- CMD exec node servers/monolith/dist/api-server/start.js + CMD exec node --conditions=transpiled servers/monolith/dist/api-server/start.js确保开发工作流和以前一样.
{ "pipeline": { "build": { "dependsOn": ["^build"] } } }这个管道定义的意思是,对于任何包,$ yarn turbo build 会从它依赖的包开始构建,以此类推。这样就可以简化Dockerfile:
# 使用以下命令从项目根目录构建: # $ docker build -t backend -f servers/monolith/Dockerfile . FROM node:16.16-alpine WORKDIR /backend COPY . . COPY .yarnrc.yml . COPY .yarn/releases/ .yarn/releases/ RUN yarn install RUN yarn turbo build # builds packages recursively RUN chown node /backend USER node CMD exec node --conditions=transpiled servers/monolith/dist/api-server/start.js注意:可以利用 Docker 多阶段构建和turbo prune 来优化构建时间和镜像大小,但在本文写作时,生成的yarn.lock 文件与 Yarn 3 还不兼容。(关于这个问题,可以查看 这个 pull 请求 了解最新进展。)借助 Turborepo,在定义好管道后(和构建时类似),只需一条命令(yarn turbo test:unit )就可以运行所有包的单元测试。
├─ packages │ ├─ config-eslint │ │ ├─ .eslintrc.js │ │ └─ package.json │ ├─ config-jest │ │ ├─ jest.config.js │ │ └─ package.json │ ├─ config-prettier │ │ ├─ .prettierrc.js │ │ └─ package.json │ └─ config-typescript │ ├─ package.json │ └─ tsconfig.json ├─ ...然后,把它们作为依赖项添加到每个包含源代码的包中,并创建配置文件扩展它们:
packages/*/.eslintrc.js: module.exports = { extends: ["@myorg/config-eslint/.eslintrc"], /* ... */ } packages/*/jest.config.js: module.exports = { ...require("@myorg/config-jest/jest.config"), /* ... */ } packages/*/.prettierrc.js: module.exports = { ...require("@myorg/config-prettier/.prettierrc.js"), /* ... */ } packages/*/tsconfig.json: { "extends": "@myorg/config-typescript/tsconfig.json", "compilerOptions": { "baseUrl": ".", "outDir": "dist", "rootDir": "." }, "include": ["src/**/*.ts"], /* ... */ }可以使用像 plop 这样的样板文件生成器来简化使用这些配置文件设置新包的过程,加快设置速度。
我们不打算讨论实现这一目标的详细步骤,但这里有一些关于如何做好拆分准备的建议:
.从提取小的实用程序包开始,例如类型库、日志记录、错误报告、API 封装器等;
.然后,提取计划跨所有服务器共享的代码的其他部分;