# 前言
本来打算学习lerna的,但是考虑到停止维护一段时间了其对workspace的支持不行,而且各大主流框架开始弃用了,可以看下babel的解释:
- 不再支持在保持版本同步的同时只发布更改的包
- Yarn workspaces are way faster than lerna bootstrap in this repository
- We needed to use Yarn's workspace: protocol, but lerna doesn't support it
- We were using a very small subset of Lerna's features
当然随着nx收购了lerna之后新出来的lerna5已经投入使用,解决了部分问题,但是以目前项目来说,只要使用pnpm来管理多个包已经够了,上回 已经介绍了pnpm的大概原理,这次将实际介绍pnpm如何在项目中使用来解决多个js包的问题
pnpm可以去官网查下相应基础知识
# 子包需要启动服务
所谓子包全是服务,其实就是子包中每个都是一个项目端口,这比较适用于微前端时候,我们首先新建一个文件夹,名为 vue3-pnpm-monorepo,进
入 vue3-pnpm-monorepo 文件夹,初始化一个默认的 package.json 文件,执行命令:
## npm install -g pnpm 全局安装pnpm
pnpm init -y
这时 package.json 的文件内部应该是这样的:
{
"name": "vue3-pnpm-monorepo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
先将一些没用的配置项删掉,再新增以下配置:
{
"private": true
}
"private": true:私有的,不会被发布,是管理整个项目,与要发布的 npm 包解耦。详细可参考这里。
配置完成之后是是这个样子:
{
"name": "vue3-pnpm-monorepo",
"version": "1.0.0",
"private": true,
"scripts": {},
"license": "ISC"
}
接下来再新建 packages 文件夹,来存放项目。进入 packages 目录,我直接初始化三个 vue3 + ts 的项目进行演示:
为了保持大家和我的代码同步,创建命令如下:
npm init vite vue-demo1
npm init vite vue-demo2
npm init vite vue-demo3
目前项目结构如下
├── packages
| ├── vue-demo1
| ├── vue-demo2
| └── vue-demo3
├── package.json
接下来进入到刚才创建的项目中,项目内部结构应该是这样的:
├── packages
| ├── vue-demo1
| | ├── .vscode
| | ├── public
| | ├── src
| | ├── .gitignore
| | ├── index.html
| | ├── package.json
| | ├── README.md
| | ├── tsconfig.json
| | ├── tsconfig.node.json
| | └── vite.config.ts
| ├── vue-demo2
| └── vue-demo3
├── package.json
进入到项目的目录下,打开 package.json 文件,是这样的:
{
"name": "vue-demo1",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.25"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.2.0",
"typescript": "^4.5.4",
"vite": "^2.8.0",
"vue-tsc": "^0.29.8"
}
}
我们要知道,目前这三个项目是完全一样的,需要的依赖也是完全一样的,所以这些依赖项就可以直接抽离出来,变成公共的依赖项,添加上版本号,另外调试的话也不需要在这里进行调试,也直接删掉,稍加修改这个文件,最后变成这样:
{
"name": "vue-demo1",
"private": true,
"version": "1.0.0"
}
将三个项目都按照上面的方式进行修改即可,接下来就需要将三个公共的依赖项,进行配置到根目录,使用全局的依赖包提供这三个项目使用:
在 根目录下的 package.json 新增之前抽离出来的公共配置项,都添加到公共的配置文件中:
{
"name": "vue3-pnpm-monorepo",
"version": "1.0.0",
"private": true,
"dependencies": {
"vue": "^3.2.25"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.2.0",
"typescript": "^4.5.4",
"vite": "^2.8.0",
"vue-tsc": "^0.29.8"
},
"license": "ISC"
}
那么现在还没有调试的方式,可以新增调试的命令,一般启动项目可以使用 dev:项目名 来进行分别启动项目,后面跟上需要启动的路径即可
{
"name": "vue3-pnpm-monorepo",
"version": "1.0.0",
"private": true,
"scripts": {
"dev:vue-demo1": "vite packages/vue-demo1",
"dev:vue-demo2": "vite packages/vue-demo2",
"dev:vue-demo3": "vite packages/vue-demo3"
},
"dependencies": {
"vue": "^3.2.25"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.2.0",
"typescript": "^4.5.4",
"vite": "^2.8.0",
"vue-tsc": "^0.29.8"
},
"license": "ISC"
}
这样配置之后,就可以根据不同的命令,来启动不同的项目了。接下来就是需要安装依赖进行测试了,不过安装前还需要配置一个特殊的文件 pnpm-workspace.yaml,这个文件可以帮
助我们在安装公共依赖的情况下,也将 packages 下的项目所需要的依赖也同时进行安装。
在根目录创建 pnpm-workspace.yaml 文件,内容为:
packages:
- 'packages/*'
配置好之后,就可以在根目录执行:
pnpm i
来安装依赖,安装好了之后,我们就会发现,在项目的根目录和分别每个项目中,都会有了 node_modules 文件夹。通过命令启动一下项目:
pnpm run dev:vue-demo1
pnpm run dev:vue-demo2
pnpm run dev:vue-demo3
局部安装依赖项比如说,我的 vue-demo1 的项目中需要安装 tyh-ui,而其它的两个项目是不需要的,那么这样的话,就可以将 tyh-ui 单独安装
到 vue-demo1 的项目中,而另外两个项目是不需要的,所以就没必要安装到全局,直接安装到 vue-demo1 内部,安装的方式有两种:
- 进入到指定目录去安装
可以直接进入到指定需要安装的目录进行安装,那么进入到 packages/vue-demo1 中,执行:
npm i tyh-ui2
完成安装,这样 vue-demo1 中就会单独多出一个依赖项进行使用了。
--filter安装
使用 --filter 修饰符可以实现在根目录指定某个目录进行安装,具体命令为:
pnpm i tyh-ui2 --filter vue-demo1
添加全局的依赖项的时候,需要在命令后面加上 -W,比如所有的组件都需要使用到 lodash,就可以执行:
pnpm i lodash -W
# 子包是依赖的js
很多比较著名的包,例如vue3,babel等都是采用该方式,这里给个大概的使用方法,先安装一些依赖
# 源码采用 typescript 编写
pnpm add -D -w typescript
# 构建工具,命令行参数解析工具
pnpm add -D -w esbuild rollup rollup-plugin-typescript2 @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-commonjs minimist execa
- -D:作为开发依赖安装
- -w:pnpm 将依赖安装到 workspace-root,也就是项目的根目录
初始化Typescript
pnpm tsc --init
# pnpm 的使用基本和 npm 一致。这里的用法就相当于 npm 中的 npx:
npx tsc --init
意思是,去 node_modules 下的 .bin 目录中找到tsc 命令,并执行它。执行完该命令,会在项目根目录生成一个 tsconfig.json 文件,进行一些配置:
{
"compilerOptions": {
"outDir": "dist", // 输出的目录
"sourceMap": true, // 开启 sourcemap
"target": "es2016", // 转译的目标语法
"module": "esnext", // 模块格式
"moduleResolution": "node", // 模块解析方式
"strict": false, // 关闭严格模式,就能使用 any 了
"resolveJsonModule": true, // 解析 json 模块
"esModuleInterop": true, // 允许通过 es6 语法引入 commonjs 模块
"jsx": "preserve", // jsx 不转义
"lib": ["esnext", "dom"], // 支持的类库 esnext及dom
"baseUrl": ".", // 当前目录,即项目根目录作为基础目录
"paths": { // 路径别名配置
"@my-vue/*": ["packages/*/src"] // 当引入 @my-vue/时,去 packages/*/src中找
},
}
}
然后在根目录下新建pnpm-workspace.yaml文件,用于告诉pnpm需要打包的路径,例如下面代码所示,表示 pnpm 打包的文件的位置在根目录下的 packages 下
packages:
- 'packages/*'
我们先在 packages 目录下新建两个模块,分别是 reactivity 响应式模块 和 shared 工具库模块。然后编写构建脚本进行第一次的开发调试
在 shared 文件中初始化 shared 项目,但是 package.json有一点不一样,因为 shared 作为一个引入对象,不需要再浏览器中使用,所以不需要打包成global
{
"name": "@vue/shared",
"version": "1.0.0",
"description": "",
"main": "index.js",
"buildOptions":{
"formats":[
"cjs",
"esm-boundler"
]
}
}
// 编写该模块的入口文件: src/index.ts
/**
* 判断对象
*/
export const isObject = (value) =>{
return typeof value === 'object' && value !== null
}
/**
* 判断函数
*/
export const isFunction= (value) =>{
return typeof value === 'function'
}
/**
* 判断字符串
*/
export const isString = (value) => {
return typeof value === 'string'
}
/**
* 判断数字
*/
export const isNumber =(value)=>{
return typeof value === 'number'
}
/**
* 判断数组
*/
export const isArray = Array.isArray
然后进入 reactivity 目录,pnpm 项目,生成 package.json文件,然后修改 package.json文件
{
"name": "@vue/reactivity", //项目的名称
"version": "1.0.0",
"description": "",
"main": "index.js",
"buildOptions":{ //自定义属性
"name":"VueReactivity", //打包的名称,
"formats":[ //打包的格式
"global", //浏览器中使用
"cjs",
"esm-boundler"
]
}
}
编写该模块的入口文件:
// src/index.ts
import { isObject } from '@my-vue/shared'
const obj = {name: 'Vue3'}
console.log(isObject(obj))
在 reactivity 包中用到了另一个包 shared ,需要安装才能使用:
pnpm add @my-vue/shared@workspace --filter @my-vue/reactivity
意思是,将本地 workspace 内的 @my-vue/shared 包,安装到 @my-vue/reactivity包中去。此时,查看 reactivity 包的依赖信息:
"dependencies": {
"@my-vue/shared": "workspace:^1.0.0"
}
编写构建脚本,在根目录下新建 scripts 目录,存放项目构建的脚本。新建 dev.js,作为开发阶段的构建脚本
// scripts/dev.js
// 使用 minimist 解析命令行参数
const args = require('minimist')(process.argv.slice(2))
const path = require('path')
// 使用 esbuild 作为构建工具
const { build } = require('esbuild')
// 需要打包的模块。默认打包 reactivity 模块
const target = args._[0] || 'reactivity'
// 打包的格式。默认为 global,即打包成 IIFE 格式,在浏览器中使用
const format = args.f || 'global'
// 打包的入口文件。每个模块的 src/index.ts 作为该模块的入口文件
const entry = path.resolve(__dirname, `../packages/${target}/src/index.ts`)
// 打包文件的输出格式
const outputFormat = format.startsWith('global') ? 'iife' : format === 'cjs' ? 'cjs' : 'esm'
// 文件输出路径。输出到模块目录下的 dist 目录下,并以各自的模块规范为后缀名作为区分
const outfile = path.resolve(__dirname, `../packages/${target}/dist/${target}.${format}.js`)
// 读取模块的 package.json,它包含了一些打包时需要用到的配置信息
const pkg = require(path.resolve(__dirname, `../packages/${target}/package.json`))
// buildOptions.name 是模块打包为 IIFE 格式时的全局变量名字
const pgkGlobalName = pkg?.buildOptions?.name
console.log('模块信息:\n', entry, '\n', format, '\n', outputFormat, '\n', outfile)
// 使用 esbuild 打包
build({
// 打包入口文件,是一个数组或者对象
entryPoints: [entry],
// 输入文件路径
outfile,
// 将依赖的文件递归的打包到一个文件中,默认不会进行打包
bundle: true,
// 开启 sourceMap
sourcemap: true,
// 打包文件的输出格式,值有三种:iife、cjs 和 esm
format: outputFormat,
// 如果输出格式为 IIFE,需要为其指定一个全局变量名字
globalName: pgkGlobalName,
// 默认情况下,esbuild 构建会生成用于浏览器的代码。如果打包的文件是在 node 环境运行,需要将平台设置为node
platform: format === 'cjs' ? 'node' : 'browser',
// 监听文件变化,进行重新构建
watch: {
onRebuild (error, result) {
if (error) {
console.error('build 失败:', error)
} else {
console.log('build 成功:', result)
}
}
}
}).then(() => {
console.log('watching ...')
})
使用该脚本,会使用 esbuild 对 packages 下的包进行构建,打包的结果放到各个包的 dist 目录下。在开发阶段,我们默认打包成 IIFE 格式,方便在浏览器中使用 html 文件进行 测试。在生产阶段,会分别打包成 CommonJS,ES Module 和 IIFE 的格式。完成第一次调试给项目增加一条 scripts 命令:
// package.json
"scripts": {
"dev": "node scripts/dev.js reactivity -f global"
}
意思是,以 IIFE 的格式,打包 reactivity 模块,打包后的文件可以运行在浏览器中。在终端中执行:pnpm dev会生成dist文件,我们建立个html文件进行测试
// packages/reactivity/test/index.html
<body>
<div id="app"></div>
<script src="../dist/reactivity.global.js"></script>
</body>