前言
Vue3 源码阅读系列,计划从环境搭建开始,将 Vue3
的响应式模块,运行时模块和编译器模块,以及状态库 Pinia
、路由库 Vue-Router
的核心原理做一个梳理。这大概是一个漫长的过程。祝自己不要烂尾,祝大家有所收获。
Pnpm 和 Monorepo
Pnpm
是新一代的 nodejs
包管理工具。第一个 “P”
意为 Performance
,代表着更佳的性能。
它的主要优点有两个,一是采用了 hard-link
机制,避免了包的重复安装,节省了空间,同时能提高项目依赖的安装速度。二是对monorepo
的支持非常友好,只需要一条配置就能实现。
Monorepo
是一种新的仓库管理方式。过去的项目,大多采用一个仓库维护一个项目的方案。对于一个庞大复杂的项目,哪怕只进行一处小小的修改,影响的也是整体。而采用 monorepo
的形式,我们可以在一个仓库中管理多个包。每个包都可以单独发布和使用,就好像是一个仓库中又有若干个小仓库。
Vue3 源码采用 monorepo 方式进行管理,将众多模块拆分到 packages 目录中。
这带来的最直观的好处,就是方便管理和维护。而且,它不像 Vue2
那样将源码整体打包对外暴露。Vue3
的这种组织形式,方便的实现了 Tree-shaking
,需要哪个功能就引入对应的模块,能大大减少打包后项目的体积。
搭建开发环境
创建项目
首先全局安装 pnpm
:
npm install -g pnpm
新建一个目录并进行初始化:
mkdir vue3-learn cd vue3-learn pnpm init mkdir packages
配置 monorepo
在项目根目录下新建 pnpm-workspace.yaml
文件:
packages: - 'packages/*'
意思是,将 packages
目录下所有的目录都作为单独的包进行管理。
通过这样一个简单的配置,Monorepo
开发环境搭建好了。
如果大家之前接触过 lerna yarn workspace
的方案,就会深有体会,使用 pnpm
的确方便。Vue3
,Element Plus
以前采用的方案就是前者,现在都已经改用后者了。
安装依赖
如果你使用过 Vite
,就一定体验过它的快。因为 Vite
内置了 esbuild
作为开发阶段的构建工具。esbuild
的特点就是快。
Vue3
采用了和 vite
一致的选择,开发阶段使用 esbuild 作为构建工具,在生产阶段采用 rollup 进行打包。
我们先安装一些依赖:
# 源码采用 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
:monorepo
环境默认会认为应该将依赖安装到具体的 package
中。使用 -w 参数,告诉 pnpm 将依赖安装到 workspace-root,也就是项目的根目录。
依赖说明:
依赖 | 描述 |
---|---|
typescript | 项目使用 typescript 进行开发 |
esbuild | 开发阶段的构建工具 |
rollup | 生产阶段的构建工具 |
rollup-plugin-typescript2 | rollup 编译 ts 的插件 |
@rollup/plugin-json | rollup 默认采用 esm 方式解析模块,该插件将 json 解析为 esm 供 rollup 处理 |
@rollup/plugin-node-resolve | rollup 默认采用 esm 方式解析模块,该插件可以解析安装在 node_modules 下的第三方模块 |
@rollup/plugin-commonjs | 将 commonjs 模块 转化为 esm 模块 |
minimist | 解析命令行参数 |
execa | 生产阶段开启子进程 |
初始化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中找 }, } }
准备两个模块
我们先在 packages
目录下新建两个模块,分别是 reactivity 响应式模块 和 shared 工具库模块。然后编写构建脚本进行第一次的开发调试。
shared
在 packages
下新建 shared
目录,并初始化:
pnpm init
然后修改 package.json
:
{ "name": "@my-vue/shared", "version": "1.0.0", "description": "@my-vue/shared", "main": "dist/shared.cjs.js", "module": "dist/shared.esm-bundler.js" }
注意 name
字段的值,我们使用了一个 @scope
作用域,它相当于 npm
包的命名空间,可以使项目结构更加清晰,也能减少包的重名。
编写该模块的入口文件:
// 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
在packages
下新建 reactivity
目录,并初始化:
pnpm init
然后修改 package.json
:
{ "name": "@my-vue/reactivity", "version": "1.0.0", "description": "@my-vue/reactivity", "main": "dist/reactivity.cjs.js", "module": "dist/reactivity.esm-bundler.js", "buildOptions": { "name": "VueReactivity" } }
在浏览器中以 IIFE
格式使用响应式模块时,需要给模块指定一个全局变量名字,通过 buildOptions.name
进行指定,将来打包时会作为配置使用。
main
指定的文件支持 commonjs
规范进行导入,也就是说在nodejs
环境中,通过 require
方法导入该模块时,会导入 main
指定的文件。
同理,module
指定的是使用 ES Module
规范导入模块时的入口文件。
编写该模块的入口文件:
// 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
输出:
PS D:\vue3-learn> pnpm dev
> vue3-learn@1.0.0 dev D:\vue3-learn
> node scripts/dev.js reactivity -f global
模块信息:
D:\vue3-learn\packages\reactivity\src\index.ts
global
iife
D:\demo3\vue3-learn\packages\reactivity\dist\reactivity.global.js
watching ...
编写一个 html
文件进行测试:
// packages/reactivity/test/index.html <body> <div id="app"></div> <script src="../dist/reactivity.global.js"></script> </body>
打开浏览器控制台:
小结
到此,一个基本的 monorepo 开发环境就搭建完毕了。
代码已上传至 Github
,点击访问。
更多关于Vue3 pnpm搭建monorepo的资料请关注Devmax其它相关文章!