Skip to main content

Monorepo 工程结构

Monorepo 风格

中小型的工程使用常规目录机构就可以,但是随着时间的推移工程会变得越来越大,可能需要考虑将工程分为多个模块,这时就适合采用monorepo风格的目录结构来开发,有两种情况适合monorepo风格。

大型工程

在大型工程中将所有的代码放在一个编译单元时构建速度会非常慢,严重影响开发和维护效率,所以最好将整个工程拆分成多个可独立部署的模块。

阿里云控制台就是典型的大型工程,他们的每个业务都作为独立的模块来部署。

多个终端APP

开发多个终端APP (比如:需要开发一个桌面web端 + 移动web端 + 运营管理端) 时可能想把通用的模块分出来,并且还想采用统一的样式风格,这时也可以考虑使用这种风格。

虽然我们主要在用 angular 开发,但是 nx 工具支持非 angular 工程的项目,所以每个终端的工程类型不同也能一起协作

创建工作区

项目的构建我们使用 nwrlnx 命令行工具来构建。

npm

npm create nx-workspace my-workspace

yarn

yarn create nx-workspace my-workspace

最后我们的工作区目录大概是这样:

monorepo风格的目录结构

📁 my-workspace                                       // 根目录
├─ 📁 apps // 多个应用工程
│ ├─ 📁 first-app // 第一个应用
│ │ ├─ 📁 src // 源代码目录
│ │ │ ├─ 📁 app
│ │ │ ├─ 📁 assets
│ │ │ ├─ 📁 environments
│ │ │ ├─ 📃 browserslist
│ │ │ ├─ 📃 index.html
│ │ │ ├─ 📃 main.ts
│ │ │ ├─ 📃 polyfills.ts
│ │ │ ├─ 📃 styles.scss
│ │ │ └─...
│ │ │
│ │ ├─ 📃 tsconfig.app.json
│ │ ├─ 📃 tsconfig.json
│ │ ├─ 📃 tslint.json
│ │ └─ ...
│ │
│ └─ 📁 second-app


├─ 📁 dist // 打包结果输出目录
│ ├─ 📁 first-app
│ ├─ 📁 second-app
│ └─ ...

├─ 📁 libs // 工作区复用模块
│ ├─ 📁 api // api 模块
│ │ ├─ 📁 src // 源代码
│ │ ├─ 📃 README.md
│ │ ├─ 📃 tsconfig.json
│ │ ├─ 📃 tsconfig.lib.json
│ │ ├─ 📃 tslint.json
│ │ └─ ...
│ │
│ ├─ 📁 auth // 通用验证模块
│ │ ├─ 📁 src // 源代码
│ │ │ └─ ...
│ │ │
│ │ ├─ 📃 README.md
│ │ ├─ 📃 tsconfig.json
│ │ ├─ 📃 tsconfig.lib.json
│ │ ├─ 📃 tslint.json
│ │ └─ ...
│ │
│ ├─ 📁 ui // 可复用的业务ui组件
│ └─ ...

├─ 📁 styles // 全局样式文件管理
│ ├─ 📃 _variables.scss // 主题样式参数定义文件
│ └─ ...

├─ 📃 .editorconfig
├─ 📃 .gitignore
├─ 📃 angular.json
├─ 📃 nx.json // nx 构建配置文件
├─ 📃 package.json
├─ 📃 README.md
├─ 📃 tsconfig.json
├─ 📃 tslint.json
└─ ...

创建应用

根目录执行创建命令会在 apps 目录下增加新的工程目录并且在配置文件中增加此配置项。

ng g @nrwl/angular:application my-app

更多详情阅读 https://nx.dev/angular/tutorial/01-create-application

创建库

根目录执行创建命令会在 libs 目录下增加新的工程目录并且在配置文件中增加此配置项。

ng g @nrwl/angular:library my-lib

更多详情阅读 https://nx.dev/angular/tutorial/08-create-libs

优化代码导入路径

因为 monorepo风格的目录比较复杂,可能会经常遇到需要写复杂相对路径的情况。

如:

import { LibFeatureModule } from '../../../../libs/ui/src/xxx';

这种代码并不好阅读,通过 tsconfig.compilerOptions.pashs 定义别名来优化导入代码。

{
"compilerOptions": {
"paths": {
"@my-workspace/first-app": ["apps/first-app/src/app"],
"@my-workspace/first-app/*": ["apps/first-app/src/app/*"],
"@my-workspace/second-app": ["apps/second-app/src/app"],
"@my-workspace/second-app/*": ["apps/second-app/src/app/*"],
"@my-workspace/api/*": ["libs/api/src/*"],
"@my-workspace/api": ["libs/api/src/index"],
"@my-workspace/ui/*": ["libs/ui/src/*"],
"@my-workspace/ui": ["libs/ui/src/index.ts"],
"@my-workspace/auth/*": ["libs/auth/src/*"],
"@my-workspace/auth": ["libs/auth/src/index.ts"],
}
}
}

这样我们就可以通过别名导入。

import { LibFeatureModule } from '@my-workspace/ui/xxx';

要规避的问题

每个 app 是可独立部署的块,不应该产生依赖性代码。

// apps/second-app/src/app/xxxx.ts

/* 糟糕的代码,如果出现这种情况,应该考虑将相关代码移动到 libs */
import { FirstAppFeatureModule } from '@my-workspace/first-app/feature';

这种场景要通过 url 访问的方式解决。

<a routerLink="//first-app.domain.com/xxxxx">
访问 fist-app 的 router
</a>

angular.json

可以看到 app 的工程和 lib 工程类型分别是 applicationlibrary

{
"version": 1,
"projects": {
"api": {
"projectType": "library",
},
"fisrt-app": {
"projectType": "application"
},
"second-app": {
"projectType": "application"
},
"ui": {
"projectType": "library",
},
},
"defaultProject": "fisrt-app"
}

应用之间的依赖关系

nx.json 文件记录着 app 和 lib 之间的依赖关系,随着工程和代码的增长,后期会很难理解每个 app 和 lib 的含义。

https://nx.dev/angular/tutorial/09-dep-graph

{
"npmScope": "softeaming-workspace",
"implicitDependencies": {
"angular.json": "*",
"package.json": "*",
"tsconfig.json": "*",
"tslint.json": "*",
"nx.json": "*"
},
"projects": {
"api": {
"tags": ["scope:shared"]
},
"first-app": {
"tags": ["scope:client"],
"implicitDependencies": ["api", "ui", "auth"]
},
"second-app": {
"tags": ["scope:client"],
"implicitDependencies": ["api", "ui", "auth"]
},
"ui": {
"tags": ["scope:shared"],
"implicitDependencies": ["api"]
},
"auth": {
"tags": [],
"implicitDependencies": ["api"]
}
}
}

```-->