Vue 打包配置

124 阅读3分钟

做了那么久 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 中查找对应文件就行了。