导语
本文记录我在搭建企业级 HRMS 人事管理系统 Monorepo 模板时的学习笔记,涵盖第一步初始化基础结构、第二步创建第一个应用、第三步配置共享 TypeScript 配置。内容包含核心概念、操作步骤和避坑指南,适合刚开始接触 Monorepo 和 Vue3 + TS 工程化的开发者参考。
第一步:初始化 Monorepo 基础结构
1.1 初始化 Git
-
作用 :在当前目录创建一个空的 Git 仓库,生成
.git文件夹,用于版本控制。 -
解决问题:让项目能够被 Git 跟踪,后续可以提交代码、回滚、协作等。
-
操作步骤:
-
在项目根目录执行
git init。 -
关联远程仓库,在终端中执行以下命令(以 Gitee 为例):
bashgit remote add origin https://gitee.com/xxxx/hrms-monorepo-2.git git push -u origin "master"
-
-
我的理解:Git 是项目的地基,没有它后续的版本管理无从谈起,是搭建任何项目的第一步。
1.2 项目初始化(创建根 package.json)
-
作用 :在项目根目录生成
package.json文件,用于声明项目名称、版本、脚本、依赖等元信息,是 Node.js 项目的核心配置文件。 -
解决问题 :没有
package.json,后续无法使用 pnpm 安装依赖、执行脚本命令;同时通过定制字段(如private、packageManager、engines)可以约束项目属性和环境。 -
操作步骤:
-
在项目根目录执行
pnpm init(会自动生成一个基础的package.json)。 -
打开
package.json,将其内容修改为以下结构(可直接复制粘贴):json{ "name": "hrms-monorepo", "version": "1.0.0", "private": true, "description": "企业 HRMS 人事管理系统,采用 Monorepo 工程化架构,包含后台管理端、数据可视化大屏与公共工具包,配套完整代码规范、安全及无障碍校验体系,覆盖组织、员工、权限、薪资、考勤、审批等全量人事业务模块", "author": "咖啡无伴侣", "license": "UNLICENSED", "type": "module", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "engines": { "node": ">= 22.14.0", "pnpm": ">=11.24.0" }, "packageManager": "pnpm@11.24.0" } -
修改说明:
- 修改
description为项目描述信息。 - 添加
"private": true防止项目被误发布到 npm。 - 删除
main字段:monorepo 多包项目的入口文件位于各子包中,根目录无需main。 - 添加
"type": "module":表示项目采用 ES Module 规范(使用import和export),而不是 CommonJS 模式。 - 删除
keywords字段:根包不发布,无需提供搜索关键词。 - 删除
devEngines对象:该字段尚未被广泛支持,且已通过engines和packageManager约束版本,避免冗余。 - 添加
engines对象:配置 Node.js 和 pnpm 的版本要求。
- 修改
-
-
我的理解 :
package.json是 Node.js 项目的配置文件,其中包含项目的基本信息与脚本配置。只有当项目中包含这个文件时,才能运行npm、pnpm等脚本工具。
1.3 配置 pnpm Workspace
-
概念 :pnpm workspace 允许在一个仓库中管理多个相互独立的包(packages),并通过
pnpm-workspace.yaml声明哪些目录是包。这样 pnpm 能自动通过符号链接关联本地依赖,避免重复安装相同版本的包,节省磁盘空间。 -
操作步骤:
-
在项目根目录创建
pnpm-workspace.yaml,内容如下:yamlpackages: - "apps/*" - "packages/*" -
创建目录
apps(如果尚未存在)。
-
-
我的理解 :pnpm 通过
pnpm-workspace.yaml文件来识别哪些目录是子项目,从而统一管理整个 monorepo 的依赖和脚本。
1.4 总结
-
项目创建之初就要初始化 Git 仓库,便于团队开发协同。
-
修改根目录
package.json:- 添加
"private": true,防止根包被误发布到 npm。 - 删除
main字段:monorepo 的入口文件位于各子包中,根目录无需main。 - 删除
keywords字段:根包不发布,无需关键词。 - 删除
devEngines字段:该字段尚未被广泛支持,且已通过engines和packageManager约束版本。 - 添加
"type": "module":声明使用 ES Modules 规范(import/export)。 - 添加
engines字段:要求 Node 和 pnpm 的最低版本。 - 添加
packageManager字段:指定使用的包管理器及精确版本,便于配合 corepack 统一环境。
- 添加
第二步:创建第一个应用(hrms-admin)
2.1 理解 monorepo 的工作原理
核心问题:为什么
vue放在子项目,typescript放在根目录?在 Monorepo 中,依赖不是随意放置的,而是遵循职责分离 和依赖隔离的原则。理解这一点,是掌握 Monorepo 工程化的关键。
1. 运行时依赖 vs 开发时依赖
-
运行时依赖 :应用在浏览器运行时必需的包,例如
vue、vue-router、pinia。- 特征 :会被
import到源码中,参与最终打包,应用离开它无法运行。 - 放置位置 :子项目的
dependencies。
- 特征 :会被
-
开发时依赖 :只在开发、构建、检查阶段使用的工具,例如
typescript、eslint、prettier。- 特征:不参与最终打包,只辅助开发流程(类型检查、代码规范、格式化)。
- 放置位置 :根目录的
devDependencies,因为工具链应当全仓库统一,避免版本不一致。
重点 :运行时依赖 放在子项目,开发工具 放在根目录。
原因:应用需要独立可控的框架版本,而工具链需要全仓库统一。
2. pnpm 的依赖隔离机制
pnpm 采用严格的依赖隔离 ,子项目只能访问自己 package.json 中显式声明的依赖。
import语句 :必须在子项目声明对应依赖,否则报错。
例如hrms-admin中写import { createApp } from 'vue',那么vue必须出现在hrms-admin/package.json的dependencies中。- 终端命令 :如
tsc、vue-tsc,执行时会向上查找node_modules/.bin。
因此根目录安装的typescript能被所有子项目直接使用,无需子项目重复声明。
重点 :
import看声明,命令看查找路径 。子项目必须声明自己要
import的包;而命令工具可以从根目录继承。
3. pnpm 的符号链接与全局 Store
pnpm 安装依赖时,不会将依赖文件复制到每个子项目的 node_modules,而是使用符号链接 和全局 Store。
- 全局 Store :所有依赖包的实际文件存储在一个统一位置(如
~/.pnpm-store)。同一个版本的依赖只会保存一份,不同项目共享。 - 虚拟存储
.pnpm:在项目node_modules/.pnpm下创建硬链接,指向全局 Store,并构建依赖关系图。 - 子项目
node_modules:只包含符号链接,指向.pnpm中的实际包。
例如hrms-admin/node_modules/vue是一个链接,指向.pnpm/vue@3.5.31/node_modules/vue。
重点 :依赖只有一份真实文件,项目通过链接共享 。
好处:节省磁盘空间、安装速度快、避免重复下载。
4. workspace 协议与跨包引用
pnpm 通过 pnpm-workspace.yaml 声明哪些目录是包,并支持 workspace 协议进行本地包引用。
-
例如在
hrms-admin/package.json中写:json{ "dependencies": { "@hrms/utils": "workspace:*" } }表示引用本地
packages/utils包,pnpm 会自动链接它,无需发布到 npm。 -
修改
packages/utils后,依赖它的子项目立即生效,可以在一次 Git 提交中完成共享包和应用的联动更新。
重点 :workspace 协议让本地包像 npm 包一样被引用,实现跨包即时联动,这是 Monorepo 的核心优势。
5. 总结:Monorepo 依赖管理的黄金法则
| 对比维度 | 示例 | 放置位置 | 原因 |
|---|---|---|---|
| 运行时依赖 | vue、vue-router、pinia |
子项目 dependencies |
谁用谁声明,依赖隔离 |
| 应用级构建工具 | vite、@vitejs/plugin-vue |
子项目 devDependencies |
不同应用构建需求可能不同 |
| 通用开发工具 | typescript、eslint、prettier |
根目录 devDependencies |
全仓库统一版本,避免冲突 |
理解这些原则,你就知道了在 Monorepo 中每个依赖应该放在哪里,以及 pnpm 如何通过隔离 + 链接实现高效、安全的依赖管理。
2.2 创建 hrms-admin 应用的核心知识点
1. 子包 package.json 的关键字段
| 字段 | 作用 | 关键点 |
|---|---|---|
name |
定义包名 | 使用作用域命名,如 @hrms/admin,区分不同子包 |
private |
防止包被发布 | 设为 true,仅用于内部应用 |
type |
声明模块规范 | "module" 使 import/export 语法生效 |
scripts |
定义常用命令 | dev 启动开发服务器,build 先类型检查再构建,preview 预览产物 |
dependencies |
运行时依赖 | 如 vue,浏览器运行必需 |
devDependencies |
开发/构建依赖 | 如 vite、@vitejs/plugin-vue、typescript、vue-tsc,不参与最终打包 |
重点 :Monorepo 中谁用谁声明 。子包必须声明自己
import的依赖;通用开发工具(如typescript)可提升到根目录,但vite和 Vue 插件通常留在子包,因为它们与应用强绑定。
2. index.html 的作用
- Vite 的入口文件,浏览器直接加载。
- 包含
<div id="app">,Vue 应用挂载点。 - 通过
<script type="module" src="/src/main.ts">引入入口模块。 type="module":让浏览器按 ES Module 加载脚本,支持import语法,并具有延迟执行和作用域隔离特性。
3. src/main.ts 的挂载流程
ts
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.mount("#app");
createApp(App):创建 Vue 应用实例。.mount('#app'):将应用挂载到index.html中 id 为app的元素。- 这是 Vue3 启动应用的标准写法。
4. App.vue 中的响应式基础
vue
<script setup lang="ts">
import { ref } from "vue";
const msg = ref("Hello HRMS");
function changeMsg() {
msg.value = "Hello Vue3 + TypeScript";
}
</script>
<template>
<h1>{{ msg }}</h1>
<button @click="changeMsg">点击我改变文字</button>
</template>
- 使用
<script setup lang="ts">语法糖,更简洁。 ref创建响应式数据,模板中自动解包(直接写msg,无需.value)。@click绑定事件,修改ref值会触发视图更新。
5. vite.config.mts 的作用
ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
});
defineConfig用于获得类型提示。- 必须从
@vitejs/plugin-vue导入插件 ,不能从'vue'导入(否则报错)。 - 插件让 Vite 能编译
.vue文件。
6. tsconfig.json 的核心作用(理解即可,不深究所有字段)
- 告诉 TypeScript 如何检查类型。
"strict": true:开启严格模式,是 TS 的核心价值。"include":指定要检查的文件(如src/**/*)。tsconfig.node.json专门用于 Node 环境下的配置文件(如vite.config.mts)。
重点:现在不需要背所有字段,遇到类型错误再反查即可。
7. pnpm 安装与依赖链接
-
执行
pnpm install后,pnpm 会:- 识别 workspace 中的子包。
- 将依赖下载到全局 store。
- 在子包
node_modules中创建符号链接,指向全局 store 中的真实文件。
-
同一个依赖版本只存储一份,多个项目共享,节省磁盘空间。
-
若出现
Ignored build scripts提示(如esbuild),执行pnpm approve-builds批准构建脚本。
8. 总结表格
| 文件 | 作用 | 关键点 |
|---|---|---|
package.json |
声明依赖和脚本 | type: "module", private: true |
index.html |
浏览器入口 | <div id="app">, type="module" |
src/main.ts |
应用启动 | createApp(App).mount('#app') |
src/App.vue |
根组件 | <script setup>, ref, @click |
vite.config.mts |
Vite 配置 | 导入 @vitejs/plugin-vue |
tsconfig.json |
TS 检查配置 | strict: true, include |
pnpm install |
依赖安装 | 全局 store + 符号链接,去重节省空间 |
第三步:共享 TypeScript 配置
前置知识:TypeScript 是什么?
TypeScript 是 JavaScript 的超集,它在 JavaScript 的基础上添加了静态类型系统。简单说,TypeScript 允许你在写代码时声明变量、参数、返回值的类型,编译器会在开发阶段检查类型错误,提前发现潜在问题,而不是等到运行时才报错。它最终会被编译成纯 JavaScript 运行在浏览器或 Node.js 中。
为什么需要 TypeScript?
- 提升代码可读性和可维护性,类型即文档。
- 减少低级错误(如拼写错误、类型不匹配、空值访问)。
- 更好的编辑器支持(智能提示、重构、跳转)。
- 适合大型项目和团队协作。
3.1 创建共享配置包 packages/typescript-config
1. 在 packages/typescript-config 下新建 package.json 文件
json
{
"name": "@hrms/typescript-config",
"version": "0.0.1",
"private": true,
"type": "module",
"exports": {
"./base.json": "./base.json",
"./vue.json": "./vue.json",
"./node.json": "./node.json"
}
}
2. 标量字段说明
| 字段 | 值 | 作用 |
|---|---|---|
name |
@hrms/typescript-config |
包名,@hrms 是作用域,用于在 monorepo 中引用该配置包 |
version |
0.0.1 |
包版本,目前为初始版本 |
private |
true |
防止误发布到 npm,仅内部使用 |
type |
module |
声明包使用 ES Module 规范 |
3. exports 字段说明
exports 定义了导出映射 ,允许使用者通过 @hrms/typescript-config/子路径 的方式导入配置文件。
json
perl
"exports": {
"./base.json": "./base.json", // 导入 @hrms/typescript-config/base.json 时,返回包内根目录的 base.json
"./vue.json": "./vue.json", // 导入 @hrms/typescript-config/vue.json 时,返回包内根目录的 vue.json
"./node.json": "./node.json" // 导入 @hrms/typescript-config/node.json 时,返回包内根目录的 node.json
}
3.2 子项目继承与路径别名配置
1. base.json 文件
json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
字段说明
| 字段 | 值 | 作用 |
|---|---|---|
$schema |
https://json.schemastore.org/tsconfig |
指定 JSON Schema 地址,让编辑器(如 VS Code)提供智能提示和校验 |
compilerOptions |
对象 | TypeScript 编译选项的容器,所有具体配置都写在这个对象内 |
target |
ES2020 |
指定编译后 JavaScript 的语法版本,确保代码能在支持 ES2020 的环境中运行 |
module |
ESNext |
指定模块系统标准,使用最新的 ES 模块语法(即 import/export) |
moduleResolution |
bundler |
模块解析策略,告诉 TS 使用打包器(如 Vite、Webpack)的方式解析模块路径,支持 exports 字段和省略扩展名 |
strict |
true |
开启所有严格类型检查选项,是 TS 类型安全的核心保障 |
jsx |
preserve |
保留 JSX 语法,不进行转换,交给后续工具(如 Vite)处理。在 Vue 项目中用于支持 TSX |
resolveJsonModule |
true |
允许导入 .json 文件,并自动推断其类型 |
isolatedModules |
true |
确保每个文件可以独立编译,兼容 Babel 等单文件转译工具,防止某些不安全语法 |
esModuleInterop |
true |
允许用 ESM 默认导入 CommonJS 模块,避免 import react from 'react' 这类写法报错 |
lib |
["ES2020", "DOM", "DOM.Iterable"] |
指定编译时可用的全局类型库,包含 ES2020 API、DOM 接口和 DOM 可迭代对象类型 |
skipLibCheck |
true |
跳过对所有 .d.ts 声明文件的类型检查,加快编译速度,避免第三方库类型冲突 |
noEmit |
true |
不生成输出文件,仅做类型检查(通常配合 vue-tsc --noEmit 使用) |
noUnusedLocals |
true |
报告未使用的局部变量错误,保持代码整洁 |
noUnusedParameters |
true |
报告未使用的函数参数错误 |
noFallthroughCasesInSwitch |
true |
禁止 switch 语句中 case 出现贯穿(fallthrough),必须显式 break 或 return |
2. node.json 文件
在 Monorepo 共享 TypeScript 配置中,node.json 没有继承 base.json 是有意为之,主要基于以下原因:
-
运行环境不同
base.json面向浏览器 + Vue 应用,包含"lib": ["ES2020", "DOM", "DOM.Iterable"]和"jsx": "preserve"等浏览器/前端相关选项。node.json用于 Node.js 环境(例如vite.config.mts),它不需要 DOM 类型,也不应该包含 JSX 配置。如果继承base.json,就会把这些不必要的选项带入 Node 配置,可能引发类型错误或混淆。
-
noEmit与项目引用的冲突-
base.json中设置了"noEmit": true,目的是让vue-tsc --noEmit只做类型检查,不输出文件。 -
但 TypeScript 的项目引用(Project References) 要求被引用的项目(如
tsconfig.node.json)必须设置"composite": true,且不能同时设置"noEmit": true,否则会报错:textReferenced project may not disable emit. -
因此
node.json必须覆盖noEmit为false。如果它继承了base.json,就需要显式覆盖,容易遗漏且逻辑混乱。
-
-
独立性更清晰
将
node.json独立定义,可以让每个配置文件职责单一:base.json:前端通用基础配置(浏览器、Vue、严格模式等)vue.json:继承base.json并添加 Vue 相关类型(如vite/client)node.json:专门针对 Node 工具链配置,不继承,避免耦合
-
代码块
json{ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "composite": true, "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "lib": ["ES2020"], "skipLibCheck": true, "allowSyntheticDefaultImports": true, "strict": true, "noEmit": false } } -
字段说明
字段 值 作用 skipLibChecktrue跳过对所有 .d.ts声明文件的类型检查,加速编译,避免第三方库类型冲突allowSyntheticDefaultImportstrue允许从没有默认导出的模块中使用默认导入(例如 import path from 'path'),方便在 Node 环境中使用 CommonJS 模块lib["ES2020"]指定编译时可用的全局类型库。这里只引入 ES2020,不包含 DOM,避免 Node 环境下误用浏览器 API noEmitfalse允许输出文件。由于项目引用(references)要求被引用项目不能禁用 emit,这里设置为 false以满足构建规则(实际vue-tsc --noEmit检查时不会真正输出)
3. vue.json
json
json
{
"extends": "./base.json",
"compilerOptions": {
"types": ["vite/client"]
}
}
-
字段说明
字段 值 作用 extends./base.json继承同目录下的基础 TypeScript 配置,避免重复定义通用编译选项 types["vite/client"]引入 Vite 客户端类型声明,使 import.meta.env、静态资源导入(如.vue、.svg)等获得类型支持 -
补充说明
-
extends:允许当前配置文件复用另一个配置文件的设置,同时可以覆盖或增加特定选项。这里vue.json继承base.json的所有浏览器相关配置,只需额外声明 Vite 客户端类型即可。 -
types:默认情况下 TypeScript 会加载node_modules/@types下的所有类型包。但通过types字段可以显式限制要包含的类型声明包,避免无关类型干扰。vite/client是 Vite 提供的类型定义文件,包含:import.meta.env的环境变量类型- 对
.vue、.svg、.css等资源的模块声明 - Vite 特有的 HMR API 类型
-
3.3 避坑指南(常见错误与解决方案)
坑 1:baseUrl 已弃用警告
现象 :
TypeScript 编译时提示:
text
arduino
选项"baseUrl"已弃用,并将停止在 TypeScript 7.0 中运行。
原因 :
TypeScript 6.0 开始弃用 baseUrl,因为 paths 已经可以独立工作,不再需要基准目录。
解决方案 :
删除 baseUrl,将 paths 中的路径改为相对于 tsconfig.json 文件的相对路径。
json
json
// 错误写法
{
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
// 正确写法
{
"paths": {
"@/*": ["./src/*"]
}
}
预防 :
配置路径别名时,不要依赖 baseUrl,直接使用以 ./ 或 ../ 开头的相对路径。
坑 2:项目引用要求 composite 和 noEmit 冲突
现象 :
执行 vue-tsc --noEmit 时报错:
text
bash
Referenced project 'tsconfig.node.json' may not disable emit.
原因 :
TypeScript 项目引用(references)要求被引用项目必须设置 "composite": true,并且不能同时设置 "noEmit": true。而 base.json 中为了类型检查设置了 "noEmit": true,被继承后导致冲突。
解决方案 :
在子项目的 tsconfig.node.json 中显式覆盖 "noEmit": false,并确保 "composite": true 存在。
json
json
{
"extends": "@hrms/typescript-config/node.json",
"compilerOptions": {
"composite": true,
"noEmit": false
},
"include": ["vite.config.mts"]
}
同时建议在共享配置 node.json 中直接设置 "noEmit": false,避免每个子项目都要重复覆盖。
预防 :
在设计共享配置时,node.json 最好不继承 base.json,而是独立定义,从根本上避免环境差异带来的冲突。
坑 3:路径别名在 Vite 中无效
现象 :
TypeScript 能识别 @/ 别名,但 Vite 构建或开发服务器报错找不到模块。
原因 :
TypeScript 的 paths 只影响类型检查,不会影响构建工具。Vite 需要单独配置 resolve.alias。
解决方案 :
在 vite.config.mts 中添加与 tsconfig.json 一致的别名配置。
ts
javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
预防 :
修改路径别名时,记得同步更新 TypeScript 和 Vite 两处配置,保持一致性。
坑 4:忘记安装 @types/node 导致 Node 模块类型缺失
现象 :
在 vite.config.mts 中使用 import { fileURLToPath } from 'node:url' 时,TypeScript 报错找不到模块。
原因 :
node:url 是 Node.js 内置模块,但 TypeScript 需要对应的类型声明包 @types/node 才能识别。
解决方案 :
在根目录安装 @types/node 作为开发依赖(因为多个子包可能都需要)。
bash
sql
pnpm add -D -w @types/node
预防 :
在 Monorepo 中,如果某个配置文件运行在 Node 环境,提前安装 @types/node 并放在根目录,统一管理。
坑 5:共享配置包 exports 未正确导出导致 extends 失败
现象 :
子项目 tsconfig.json 中写 "extends": "@hrms/typescript-config/vue.json" 时,TypeScript 报错找不到模块。
原因 :
共享配置包的 package.json 中没有正确配置 exports 字段,或者路径映射错误。
解决方案 :
确保共享包的 package.json 包含:
json
perl
{
"name": "@hrms/typescript-config",
"exports": {
"./base.json": "./base.json",
"./node.json": "./node.json",
"./vue.json": "./vue.json"
}
}
并且子项目的 devDependencies 中通过 workspace:* 引用了该包。
预防 :
创建共享配置包后,先在子项目测试 extends 是否能正确解析,再继续其他配置。
写在最后
以上就是我搭建 Monorepo 模板前三步的完整记录,涵盖基础结构初始化、第一个 Vue3 应用创建、共享 TypeScript 配置包的设计与坑点。后续我还会继续完善 ESLint、Prettier、Husky 等工程化配置,并逐步引入 Turborepo 和 CI/CD,感兴趣的朋友可以持续关注。
如果本文对你有帮助,欢迎点赞、收藏、评论,也欢迎指出不足之处,一起交流进步。