做了那么久 Vue,试着看一看源码,我拉取的版本是 3.2.31。
根目录下看到了 pnpm-workspace.yaml,明显包管理工具使用的是 pnpm,不同于 element-plus,这里没有指定 packageManager。
monorepo 的根目录一般用来配置一些通用内容,会设置 "private": true,不会发布。只想了解 Vue 核心,可以看 packages 目录下对应的包。
Vue 3 做了很多 tree-shaking 方面的优化,拆分的比较细致。也给阅读源码,带来了一些难度。代码不再像之前一样,脉络清晰,有一个统一的入口,很多内容变成了可选的。阅读时多思考平常使用遇到的问题,带着目的去读会好一点。
打包
第一步了解下打包的相关操作。"build": "node scripts/build.js",打包用的是 node,也学习一下。
build.js 中引入了 utils.js 的两个变量:targets 和 fuzzyMatchTarget。
// 读取 packages 下面的文件
const targets = (exports.targets = fs.readdirSync('packages').filter(f => {
// 过滤不是目录的文件
if (!fs.statSync(`packages/${f}`).isDirectory()) {
return false
}
// 过滤私有库,和不用打包的库
const pkg = require(`../packages/${f}/package.json`)
if (pkg.private && !pkg.buildOptions) {
return false
}
return true
}))
使用的都是同步的 fs 方法,当前的目录,就是运行代码的目录,也就是在根目录下。readdirSync 会将目录下的所有文件,文件夹都读取出来,需要过滤掉不是文件夹的。JSON 是一种序列化数据的格式,node 可以直接读取,根据对应 package 是否私有、有打包选项,这里做了一个过滤。
fuzzyMatchTarget 用来过滤打包情况,打包命令中有获取参数 const buildAllMatching = args.all || args.a,如果带 all 或者 a 参数是打包多个库的。没有涉及到 node API,就不写下来了。
回到 build.js,看主体实现:
async function run() {
if (isRelease) {
// remove build cache for release builds to avoid outdated enum values
await fs.remove(path.resolve(__dirname, '../node_modules/.rts2_cache'))
}
if (!targets.length) {
await buildAll(allTargets)
checkAllSizes(allTargets)
} else {
await buildAll(fuzzyMatchTarget(targets, buildAllMatching))
checkAllSizes(fuzzyMatchTarget(targets, buildAllMatching))
}
}
这里使用了 fs.remove 清除缓存。remove 这个方法,不是 node 自身支持的,依赖于一个库 fs-extra,接下来是打包文件的过滤。
async function buildAll(targets) {
// require('os').cpus().length 获取 cpu 核心数
await runParallel(require('os').cpus().length, targets, build)
}
os 是 node 中的一个模块,获取操作系统相关信息。通过获取 cpu 核心数,控制并发数。
async function runParallel(maxConcurrency, source, iteratorFn) {
const ret = []
const executing = []
for (const item of source) {
// 使用 Promise 完成异步任务,防止阻塞主进程
const p = Promise.resolve().then(() => iteratorFn(item, source))
ret.push(p)
// 处理并发数
if (maxConcurrency <= source.length) {
// 异步任务完成后,移除对应任务
const e = p.then(() => executing.splice(executing.indexOf(e), 1))
executing.push(e)
// 如果超出并发限制需要等待
if (executing.length >= maxConcurrency) {
await Promise.race(executing)
}
}
}
return Promise.all(ret)
}
这里给我一种豁然开朗的感觉,一直不清楚 Promise.race 的应用场景,终于看到了。Promise.race 会等待第一个异步任务完成,这样就可以在保证并发数的情况下,执行下一个操作。const e = p.then(() => executing.splice(executing.indexOf(e), 1)) 更是让我体会到异步编程,和同步上的区别。如果是同步操作的话,大概需要轮询,直到完成。这里放入了异步回调里面,非常简洁。也不是多么难的操作,业务写久了,异步用的不多。
打包使用的是 rollup,需要了解 rollup 的打包配置。Vue packages 中的包,入口文件引入的是打包后的文件,需要了解打包的入口文件。
一个基本的 rollup.config.js,结构是这样的:
export default {
input: 'src/main.js',
output: {
file: 'bundle.js',
format: 'cjs'
}
};
其中 input 指定了入库文件,Vue 中是通过 config 中的一个方法 createConfig,来生成对应的 config(Vue 会区分 runtime-only 和 full-build,对应不同的打包配置),其中一行代码:
let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`
这里指定了入库文件,直接去对应的 package 中查找对应文件就行了。