各位观众老爷,晚上好!我是你们的老朋友,今天咱们聊聊JavaScript项目中的Monorepo
架构,以及两位大咖:Lerna
和Nx
。保证让你们听完之后,感觉自己立马就能搞一个宇宙级的Monorepo出来。
Monorepo:一个筐里装所有鸡蛋
啥叫Monorepo?简单来说,就是把多个项目、库、工具等等,全都放到同一个代码仓库里管理。 想象一下,你以前是每个项目一个仓库,现在把它们都塞到一个巨大的、豪华的仓库里。
为什么要用Monorepo?
你可能会问,这么做图个啥?好处可多了去了:
- 代码复用更方便: 不同的项目之间共享代码,简直不要太容易。改个底层组件,所有项目都能受益,妈妈再也不用担心我到处复制粘贴了。
- 依赖管理更简单: 统一管理依赖,避免版本冲突。再也不用为了解决依赖问题,头发都掉光了。
- 原子性变更: 修改一个底层库,可以同时更新所有依赖它的项目。测试、发布一条龙服务,避免出现版本不一致的情况。
- 协作更高效: 所有开发者都在同一个仓库里工作,更容易了解整个系统的架构,协作起来更加流畅。
- 构建和测试更高效: 可以利用工具来分析代码依赖关系,只构建和测试受影响的部分,大大提高效率。
当然,Monorepo也不是完美的。它也有缺点:
- 仓库体积大: 所有代码都在一起,仓库体积肯定不小。不过现在网络速度这么快,硬盘这么大,这都不是事儿。
- 权限管理复杂: 需要更精细的权限管理,避免有人不小心改了别人的代码。
- 构建和测试配置复杂: 需要花点心思配置构建和测试流程,才能充分发挥Monorepo的优势。
Lerna:Monorepo的开路先锋
Lerna
是Monorepo领域的元老级人物,专门用来管理包含多个package的JavaScript仓库。它主要解决两个问题:
- 版本管理: 自动更新所有package的版本号,并发布到npm。
- 依赖管理: 自动安装和链接所有package的依赖。
Lerna的基本用法:
-
安装Lerna:
npm install --global lerna
-
初始化Lerna仓库:
lerna init
这会在你的仓库里生成一个
lerna.json
文件和一个packages
目录。lerna.json
是Lerna的配置文件,packages
目录用来存放所有的package。 -
创建package:
在
packages
目录下创建你的package,比如packages/component-a
和packages/component-b
。每个package都应该有自己的package.json
文件。 -
安装依赖:
lerna bootstrap
这会安装所有package的依赖,并创建符号链接,让它们可以互相引用。
-
发布package:
lerna publish
这会自动更新所有package的版本号,并发布到npm。
一个简单的Lerna Monorepo示例:
假设我们有一个Monorepo,包含两个package:component-a
和component-b
。component-a
依赖于component-b
。
-
目录结构:
my-monorepo/ ├── lerna.json ├── package.json └── packages/ ├── component-a/ │ ├── index.js │ └── package.json └── component-b/ ├── index.js └── package.json
-
lerna.json
:{ "packages": [ "packages/*" ], "version": "independent", "npmClient": "npm", "useWorkspaces": true }
packages
指定了package的路径,version
指定了版本管理模式(independent
表示每个package独立管理版本),npmClient
指定了使用的包管理器,useWorkspaces
是否使用 workspaces ,使用之后依赖安装速度更快。 -
package.json
(根目录):{ "name": "my-monorepo", "private": true, "devDependencies": { "lerna": "^4.0.0" }, "workspaces": [ "packages/*" ] }
private: true
表示这是一个私有仓库,不会被发布到npm。devDependencies
包含了Lerna的依赖。workspaces
用于配置 workspaces 。 -
packages/component-a/package.json
:{ "name": "@my-monorepo/component-a", "version": "1.0.0", "description": "Component A", "main": "index.js", "dependencies": { "@my-monorepo/component-b": "^1.0.0" } }
-
packages/component-b/package.json
:{ "name": "@my-monorepo/component-b", "version": "1.0.0", "description": "Component B", "main": "index.js" }
-
packages/component-a/index.js
:import componentB from '@my-monorepo/component-b'; function componentA() { console.log('Component A'); componentB(); } export default componentA;
-
packages/component-b/index.js
:function componentB() { console.log('Component B'); } export default componentB;
现在,你可以使用lerna bootstrap
来安装依赖,然后使用lerna publish
来发布package。
Lerna的进阶用法:
lerna run
: 在所有package中运行指定的npm脚本。例如,lerna run test
会在所有package中运行npm test
。lerna exec
: 在所有package中执行指定的命令。例如,lerna exec -- rm -rf node_modules
会删除所有package的node_modules
目录。lerna changed
: 列出自上次发布以来发生更改的package。lerna diff
: 显示自上次发布以来每个package的更改。
Nx:Monorepo的瑞士军刀
Nx
是新一代的Monorepo工具,它不仅仅是一个版本管理工具,更是一个强大的构建和测试工具。Nx的核心思想是计算缓存和依赖分析。
- 计算缓存: Nx会缓存每次构建和测试的结果,如果代码没有发生变化,Nx会直接使用缓存,避免重复构建和测试。
- 依赖分析: Nx会分析代码的依赖关系,只构建和测试受影响的部分。
Nx的基本用法:
-
安装Nx:
npm install --global nx
-
创建Nx workspace:
npx create-nx-workspace@latest my-nx-monorepo --preset=npm --package-manager=npm
这会创建一个新的Nx workspace,并选择
npm
作为包管理器。 -
创建application或library:
nx generate @nx/react:application my-react-app nx generate @nx/js:library my-js-lib
这会创建一个新的React application和一个JavaScript library。
-
构建和测试:
nx build my-react-app nx test my-js-lib
这会构建React application和测试JavaScript library。
一个简单的Nx Monorepo示例:
假设我们有一个Nx Monorepo,包含一个React application和一个JavaScript library。React application依赖于JavaScript library。
-
目录结构:
my-nx-monorepo/ ├── apps/ │ └── my-react-app/ │ ├── src/ │ │ └── app/ │ │ └── app.tsx │ └── project.json ├── libs/ │ └── my-js-lib/ │ ├── src/ │ │ └── index.ts │ └── project.json ├── nx.json ├── package.json └── tsconfig.base.json
-
nx.json
:{ "npmScope": "my-nx-monorepo", "affected": { "defaultBase": "main" }, "implicitDependencies": { "package.json": { "dependencies": "*", "devDependencies": "*" }, ".eslintrc.json": "*" }, "tasksRunnerOptions": { "default": { "runner": "@nrwl/nx-cloud", "options": { "cacheableOperations": [ "build", "lint", "test", "e2e" ], "accessToken": "YOUR_NX_CLOUD_ACCESS_TOKEN" } } }, "targetDefaults": { "build": { "dependsOn": [ "^build" ], "inputs": [ "default", "{projectRoot}/.eslintrc.json", "{projectRoot}/tsconfig.json", "{projectRoot}/tslint.json" ], "outputs": [ "{projectRoot}/dist" ] } }, "namedInputs": { "default": [ "{{implicitDependencies}}", "{projectRoot}/**/*", "!{projectRoot}/**/?(*.)+(spec|test).ts?(x)?(.snap)", "{projectRoot}/.babelrc", "{projectRoot}/.swcrc" ], "production": [ "default" ] } }
npmScope
指定了npm scope,affected
指定了受影响的代码的默认分支,tasksRunnerOptions
指定了任务运行器,targetDefaults
指定了目标的默认配置,namedInputs
指定了命名的输入。 -
package.json
(根目录):{ "name": "my-nx-monorepo", "version": "0.0.0", "license": "MIT", "scripts": { "start": "nx serve", "build": "nx build", "test": "nx test" }, "private": true, "devDependencies": { "@nrwl/cli": "14.0.2", "@nrwl/eslint-plugin-nx": "14.0.2", "@nrwl/jest": "14.0.2", "@nrwl/linter": "14.0.2", "@nrwl/react": "14.0.2", "@nrwl/node": "14.0.2", "@nrwl/web": "14.0.2", "@nrwl/workspace": "14.0.2", "@testing-library/react": "13.0.0", "@types/jest": "27.4.1", "@types/node": "16.11.7", "@types/react": "18.0.0", "@types/react-dom": "18.0.0", "@typescript-eslint/eslint-plugin": "~5.24.0", "@typescript-eslint/parser": "~5.24.0", "eslint": "~8.15.0", "eslint-config-prettier": "8.1.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-jsx-a11y": "6.6.1", "eslint-plugin-react": "7.30.0", "eslint-plugin-react-hooks": "4.6.0", "jest": "27.5.1", "prettier": "^2.6.2", "ts-jest": "27.1.4", "ts-node": "10.9.1", "typescript": "~4.7.2" }, "dependencies": { "react": "18.2.0", "react-dom": "18.2.0" } }
-
apps/my-react-app/project.json
:{ "name": "my-react-app", "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "apps/my-react-app/src", "projectType": "application", "targets": { "build": { "executor": "@nrwl/web:webpack", "outputs": [ "{options.outputPath}" ], "defaultConfiguration": "production", "options": { "compiler": "babel", "outputPath": "dist/apps/my-react-app", "index": "apps/my-react-app/src/index.html", "baseHref": "/", "main": "apps/my-react-app/src/main.tsx", "polyfills": "apps/my-react-app/src/polyfills.ts", "tsConfig": "apps/my-react-app/tsconfig.app.json", "assets": [ "apps/my-react-app/src/favicon.ico", "apps/my-react-app/src/assets" ], "styles": [], "scripts": [], "webpackConfig": "apps/my-react-app/webpack.config.js" }, "configurations": { "production": { "fileReplacements": [ { "replace": "apps/my-react-app/src/environments/environment.ts", "with": "apps/my-react-app/src/environments/environment.prod.ts" } ], "optimization": true, "outputHashing": "all", "sourceMap": false, "extractCss": true, "namedChunks": false, "extractLicenses": true, "vendorChunk": false, "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ] } } }, "serve": { "executor": "@nrwl/web:dev-server", "defaultConfiguration": "development", "options": { "buildTarget": "my-react-app:build", "hmr": true }, "configurations": { "development": { "buildTarget": "my-react-app:build:development" }, "production": { "buildTarget": "my-react-app:build:production", "hmr": false } } }, "lint": { "executor": "@nrwl/linter:eslint", "outputs": [ "{options.outputFile}" ], "options": { "lintFilePatterns": [ "apps/my-react-app/**/*.{ts,tsx,js,jsx}" ] } }, "test": { "executor": "@nrwl/jest:jest", "outputs": [ "coverage/apps/my-react-app" ], "options": { "jestConfig": "apps/my-react-app/jest.config.ts", "passWithNoTests": true } } }, "tags": [] }
-
libs/my-js-lib/project.json
:{ "name": "my-js-lib", "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/my-js-lib/src", "projectType": "library", "targets": { "build": { "executor": "@nrwl/js:tsc", "outputs": [ "{options.outputPath}" ], "options": { "outputPath": "dist/libs/my-js-lib", "tsConfig": "libs/my-js-lib/tsconfig.lib.json", "packageJson": "libs/my-js-lib/package.json", "main": "libs/my-js-lib/src/index.ts", "assets": [ "libs/my-js-lib/*.md" ] } }, "lint": { "executor": "@nrwl/linter:eslint", "outputs": [ "{options.outputFile}" ], "options": { "lintFilePatterns": [ "libs/my-js-lib/**/*.ts" ] } }, "test": { "executor": "@nrwl/jest:jest", "outputs": [ "coverage/libs/my-js-lib" ], "options": { "jestConfig": "libs/my-js-lib/jest.config.ts", "passWithNoTests": true } } }, "tags": [] }
-
apps/my-react-app/src/app/app.tsx
:import React from 'react'; import { myJsLib } from '@my-nx-monorepo/my-js-lib'; function App() { return ( <div> <h1>My React App</h1> <p>{myJsLib()}</p> </div> ); } export default App;
-
libs/my-js-lib/src/index.ts
:export function myJsLib(): string { return 'Hello from my-js-lib!'; }
现在,你可以使用nx build my-react-app
来构建React application,使用nx test my-js-lib
来测试JavaScript library。如果修改了my-js-lib
的代码,再次构建my-react-app
时,Nx会自动检测到依赖关系,并重新构建my-react-app
。
Nx的进阶用法:
- Nx Console: 一个VS Code插件,可以方便地生成代码、运行命令和查看依赖关系。
- Nx Cloud: 一个云服务,可以缓存构建和测试结果,并提供分布式构建和测试功能。
- Nx Plugins: 可以扩展Nx的功能,支持更多的技术栈和工具。
Lerna vs Nx:选择困难症?
Lerna和Nx都是优秀的Monorepo工具,选择哪个取决于你的需求。
特性 | Lerna | Nx |
---|---|---|
核心功能 | 版本管理、依赖管理 | 构建、测试、依赖分析、计算缓存 |
学习曲线 | 简单易上手 | 功能强大,学习曲线稍高 |
适用场景 | 简单的Monorepo,只需要版本管理和依赖管理 | 复杂的Monorepo,需要高效的构建和测试流程,以及强大的依赖分析能力 |
生态系统 | 相对简单 | 丰富,支持各种技术栈和工具 |
总结:
Monorepo是一种强大的代码管理模式,可以提高代码复用率、简化依赖管理、提高协作效率。Lerna和Nx都是优秀的Monorepo工具,可以帮助你更好地管理Monorepo。选择哪个取决于你的需求。
希望今天的讲座能帮助大家更好地理解Monorepo架构,并选择合适的工具来管理你的JavaScript项目。 谢谢大家!