前言
作为 vue
玩家,一路走来在 vue3
源码中收获了很多,无论是项目构建
还是TS
方面vue3
源码都是最好的学习教程。
本课程打算作为系列课程,主要包括以下几个章节:
- 从vue3源码构建中你能学到什么
- 从vue3源码中你能学到的TS
- 从Vue3源码中你能学到的工具类
感兴趣的话关注一波吧🤑
不是大佬,文章内容如有错误,欢迎留言。
Vue3 架构总览
Vue3
源码架构相比于 vue2
最大的变化就是 Vue3
中 使用了 monorepo
架构,很多UI库采用的也是这种架构。
从表象上来看 monorepo
架构是把项目中文件夹(功能模块)变成了包,每个包可以独立发布也可相互依赖。
以下是vue3
源码中packages
目录
我们点击 compiler-core
文件夹看看
可以看出是一个标准的包结构,再来看看package.json
,如下所示(关键部分以给出注释):
{
"name": "@vue/compiler-core", // 包的名称 以@开头说明是个范围包
"version": "3.1.2",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
"types": "dist/compiler-core.d.ts",
"files": [
"index.js",
"dist"
],
"buildOptions": { // 打包时需要的自定义属性
"name": "VueCompilerCore",
"compat": true,
"formats": [
"esm-bundler",
"cjs"
]
},
"dependencies": { // 包的依赖项
"@vue/shared": "3.1.2",
"@babel/parser": "^7.12.0",
"@babel/types": "^7.12.0",
"estree-walker": "^2.0.1",
"source-map": "^0.6.1"
}
}
这个时候你可能会有些疑问?为什么vue3
要使用这种架构?以下纯属个人猜想:
- 更有利于团队合作
我们知道vue3
中的模块非常多,同时每个模块都需要相对应的人员来开发,使用monorepo
项目架构可以吧每个模块都作为一个个的包,这样不同的人员维护不同的包,逻辑上很清晰。
- 解决每个模块的依赖关系
比如 compiler-core
这个模块会使用到 shared
模块里面的方法,如果使用monorepo
项目架构,我们可以直接在 compiler-core
中通过 import
的方式导入 shared
,且不需要使用npm link
进行关联(yarn 的 workspaces已经帮我们做了)
- 分包发布
比如我们只是使用了vue
中的某个模块,这个时候我们只需要下载对应的模块包即可,无需下载整个vue
。
如何搭建 monorepo
项目
业界上大概有三种搭建方式
- 使用
yarn
的workspaces
- 使用
lerna
- 使用
pnpm
最新版element-plus
就是采用这种架构
下面我们参考vue3
源码架构使用 yarn
的 workspaces
进行项目搭建
- 新建文件夹
monorepo-project
进入文件夹执行yarn init -y
命令,生成package.json
文件
新增 private:true
和 workspaces
{
"name": "monorepo-project",
"private": "true",
"workspaces": [
"packages/*"
],
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
配置完这两个属性后其实我们的项目就变成了monorepo
项目了
- 在
packages
文件夹下新建两个文件夹module1
和module2
作为我们的两个包,然后分别进入module1
和module2
里面进行yarn init -y
,这个时候的目录结构如下所示:
分别更改module1
和module2
中package.json
中的 name 名称,把他都变成范围包
{
"name": "@monorepo-project/module1",
"version": "1.0.0",
"main": "src/index.ts",
"license": "MIT"
}
- 回到根目录执行
yarn install
命令
你会神奇的发现yarn自动给我们的module1``module2
模块创建了软链
这个时候我们在module1
中使用module2
就可以直接import
了
import module2 from '@monorepo-project/module2'
console.log(module2);
如何让项目跑起来
现在根目录的 package.json
里面还十分的朴实,没有配置 scripts
脚本,现在我们需要配置个dev
让项目先跑起来。
{
"name": "monorepo-project",
"private": "true",
"workspaces": [
"packages/*"
],
"scripts": { //新增 dev 脚本
"dev": "node ./scripts dev.js"
},
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
dev脚本其实运行的就是 script
文件夹下面的 dev.js
文件。首先我们看下vue3源码中dev.js
里面都干了什么事情(关键部分已给出注释)。
const execa = require('execa')
const { fuzzyMatchTarget } = require('./utils')
const args = require('minimist')(process.argv.slice(2))
const target = args._.length ? fuzzyMatchTarget(args._)[0] : 'vue' // 目标模块
const formats = args.formats || args.f
const sourceMap = args.sourcemap || args.s
const commit = execa.sync('git', ['rev-parse', 'HEAD']).stdout.slice(0, 7)
execa(
'rollup', // 打包命令
[
'-wc', // 文件变化时就执行
'--environment', // 设置环境变量
[
`COMMIT:${commit}`,
`TARGET:${target}`,
`FORMATS:${formats || 'global'}`,
sourceMap ? `SOURCE_MAP:true` : ``
]
.filter(Boolean)
.join(',')
],
{
stdio: 'inherit' // 输出到主进程中
}
)
可以看出dev.js
中主要干的就是使用 execa 去执行 rollup
打包命令,那接下来我们就去瞅瞅 rollup.config.json
文件。
把里面的函数全部折叠后看出这个配置文件最后导出了一个 packageConfigs
配置,也是rollup
的最终配置项,我们就从这个配置看起。
const packageConfigs = process.env.PROD_ONLY
? []
: packageFormats.map(format => createConfig(format, outputConfigs[format]))
上面代码可以看出主要是使用 map 循环了 packageFormats
通过 createConfig
方法生成配置文件。
我们先来看看 packageFormste
是怎么来的。
// 如果通过命令行有设置就用命令行中的设置,否则使用package.json只中的formats配置 如果都没有 就使用默认
const packageFormats = inlineFormats || packageOptions.formats || defaultFormats
通过上面的代码,我们可以清楚的看到,其实最终的配置项是在每个模块的 package.json
中 buildOptions
配置了,下面我们来说说这些配置项都是什么意思:
"buildOptions": {
"name": "Vue", // 模块最总暴露出来的变量名称,打包为global时需要
"formats": [ // 需要打包成的类型
"esm-bundler", // es
"esm-bundler-runtime", // es
"cjs", //cjs
"global", // iife
"global-runtime", // iife
"esm-browser", // es
"esm-browser-runtime" // es
]
}
es
用于 ES6 语法,cjs
用于 node,iife
用于浏览器环境
createConfig
函数接受两个参数,分别是:
format
支持的打包类型outputConfigs[format]
根据打包类型获取对应 rollup 的 output 部分配置
下面我们来看下 createConfig
函数返回的配置项信息
function createConfig(format,output,plugins=[]){
...
return {
input: resolve(entryFile), // 入口 src/index.ts
external, // 一些外部依赖不打包
plugins: [ // 插件配置
json({
namedExports: false
}),
tsPlugin,
createReplacePlugin(
isProductionBuild,
isBundlerESMBuild,
isBrowserESMBuild,
// isBrowserBuild?
(isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) &&
!packageOptions.enableNonBrowserBranches,
isGlobalBuild,
isNodeBuild,
isCompatBuild
),
...nodePlugins,
...plugins
],
output, // 输入文件
onwarn: (msg, warn) => {
if (!/Circular/.test(msg)) {
warn(msg)
}
},
treeshake: {
moduleSideEffects: false
}
}
}
至此 npm run dev
打包逻辑已经全部看完,下面我们来梳理下整个流程
npm run dev
只是打包的是 vue
文件夹,vue 文件也是对外输出vue的所有功能。应为在 dev.js
中执行 rollup
的时候配置了 --wc
即文件变化后就执行这个文件的打包,所以在开发环境中我们可以使用 npm run dev 进行代码的调试。
如果我们想每个模块都打包 就的使用 npm run build
命令。
打包所有模块
npm run build
和 npm run dev
要干的事情本质来说是一样的,只不过在 执行 build.js
的时候需要打包所有的模块,所以 targets
变量需要去读取而不是像 dev.js
中的直接写死。
首先我们来看看 utils
中是如何获取 targets 的
const targets = (exports.targets = fs.readdirSync('packages').filter(f => {
// 过滤掉不是文件夹的文件
if (!fs.statSync(`packages/${f}`).isDirectory()) {
return false
}
const pkg = require(`../packages/${f}/package.json`)
// 过滤掉 package 中 private=4 以及没有设置 buildOptions 属性的文件
if (pkg.private && !pkg.buildOptions) {
return false
}
return true
}))
最后 targets 返回的是一个需要打包的模块名
获得打包列表后,我们就可以进行循环并行打包了
async function buildAll(targets) {
await runParallel(require('os').cpus().length - 7, targets, build)
}
//maxConcurrency 最大CPU核数 source 需要打包的模块数组 iteratorFn执行的打包命令
async function runParallel(maxConcurrency, source, iteratorFn) {
const ret = []
const executing = []
for (const item of source) {
const p = Promise.resolve().then(() => iteratorFn(item, source))
ret.push(p)
if (maxConcurrency <= source.length) {
const e = p.then(() => {
return executing.splice(executing.indexOf(e), 1)
})
executing.push(e)
if (executing.length >= maxConcurrency) {
await Promise.race(executing)
}
}
}
return Promise.all(ret)
}
我们再来看看 build 函数中的主要代码
async function build(target){
...
await execa(
'rollup',
[
'-c',
'--environment',
[
`COMMIT:${commit}`,
`NODE_ENV:${env}`,
`TARGET:${target}`,
formats ? `FORMATS:${formats}` : ``,
buildTypes ? `TYPES:true` : ``,
prodOnly ? `PROD_ONLY:true` : ``,
sourceMap ? `SOURCE_MAP:true` : ``
]
.filter(Boolean)
.join(',')
],
{ stdio: 'inherit' }
)
}
...
其实和 dev 中的逻辑一样,都是使用 execa 执行 rollup 命令去打包
下面我们来梳理下 npm run build
执行的打包流程
最后
最新的 vue3
代码已经使用 pnpm
进行模块管理了