你需要了解的Vue3分包的流程

1,852 阅读4分钟

背景

学习Vue3的同时,也需要关注Vue3的分包机制。Vue3将各个模块分包管理,包与包之见关系大大的解耦,使得每个包之间的维护性提高。因为每个包是一个独立的存在,也就使每个包可以独立的运行,并不完全依赖于vue环境。例如@vue/reactivity,它的响应式非常不错,使它的运行环境也有不同,社区有人将它运行于react环境。当我阅读到的时候并不惊讶,因为我之前阅读# Vue3 源码深入解析 响应式原理 --> Reactivity就发现它不依赖于任何环境,以至于可以适当的运行任何环境。

package.json

当我拿到项目时,最先看的可能就是package.json文件。因为这个文件可以清晰的看到项目的依赖、脚本命令···,我们先来分析一下scripts

image.png

其实vuedevbuild等多个命令都是用node去执行的js的文件,于是我们便可以知道vue的打包流程了

build

当我们cd到build.js文件内,我们可以看到入口函数run只有它在执行,其余的都是些功能函数

image.png

run

看看run函数做了什么事情

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))
  }
}

主要可以看到run函数主要针对 isReleasetargets做了判断

isRelease === true,说明是发布的版本,则会去删除打包的缓存避免过期的枚举值

targets是一个空数组的时候,说明是全量打包,即打包所有的模块。

这时我们来看看allTargets是什么

allTargets

从上面我们可以知道 allTargets是从utils文件引入 看看源码

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模块读取packages文件夹并对文件类型进行过滤

image.png

我们可以看到每个包都是以文件夹格式存放的,所以targets也是针对isDirectory()来区别当前文件是否是一个vue的包

然后针对每个包的package.json文件内容在进行区分

是否是一个私有包 是否具有buildOptions字段

image.png

image.png

例如上面两个包则会有不同的判断逻辑

于是我们最终得到我们需要进行打包的包名数组

image.png

buildAll

先看源码

async function buildAll(targets) {
  await runParallel(require('os').cpus().length, targets, build)
}

很简单的一段代码,将功能全部交给了runParallel

runParallel

逐步递进,我们再看runParallel的源码

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(() => executing.splice(executing.indexOf(e), 1))
      executing.push(e)
      if (executing.length >= maxConcurrency) {
        await Promise.race(executing)
      }
    }
  }
  return Promise.all(ret)
}

我们可以看到runParallel其实就是循环我们的包数组(即上文的allTargets),然后将打包行为分发给iteratorFn函数,即接下来要讲的build函数,先将他们用promise包裹,利用Promise.all进行集中打包

build === iteratorFn

build函数才是真正的打包动作。我们可以看到runParallel中,

const p = Promise.resolve().then(() => iteratorFn(item, source))

于是我们的build函数的形参便是需要打包的包名

我们看看源码

async function build(target) {
  const pkgDir = path.resolve(`packages/${target}`)
  const pkg = require(`${pkgDir}/package.json`)

  // if this is a full build (no specific targets), ignore private packages
  if ((isRelease || !targets.length) && pkg.private) {
    return
  }

  // if building a specific format, do not remove dist.
  if (!formats) {
    await fs.remove(`${pkgDir}/dist`)
  }

  const env =
    (pkg.buildOptions && pkg.buildOptions.env) ||
    (devOnly ? 'development' : 'production')
  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' } //把子进程打包的信息共享给父进程
  )
}

最后通过execa命令去执行我们的rollup命令进行打包

rollup.config.js

rollup打包自然要有我们的配置文件。

const packageConfigs = process.env.PROD_ONLY
  ? []
  : packageFormats.map(format => createConfig(format, outputConfigs[format]))

if (process.env.NODE_ENV === 'production') {
  packageFormats.forEach(format => {
    if (packageOptions.prod === false) {
      return
    }
    if (format === 'cjs') {
      packageConfigs.push(createProductionConfig(format))
    }
    if (/^(global|esm-browser)(-runtime)?/.test(format)) {
      packageConfigs.push(createMinifiedConfig(format))
    }
  })
}

export default packageConfigs

主要分析一下 packageConfigs ,它就是rollup.config.js的导出内容

packageFormats.map(format => createConfig(format, outputConfigs[format]))

通过这段代码创建出配置文件

packageFormats 就是每个包的buildOptions的打包格式

image.png

formats的类型也有对应的枚举,即rollup打包输出文件类型

const outputConfigs = {
  'esm-bundler': {
    file: resolve(`dist/${name}.esm-bundler.js`),
    format: `es`
  },
  'esm-browser': {
    file: resolve(`dist/${name}.esm-browser.js`),
    format: `es`
  },
  cjs: {
    file: resolve(`dist/${name}.cjs.js`),
    format: `cjs`
  },
  global: {
    file: resolve(`dist/${name}.global.js`),
    format: `iife`
  },
  // runtime-only builds, for main "vue" package only
  'esm-bundler-runtime': {
    file: resolve(`dist/${name}.runtime.esm-bundler.js`),
    format: `es`
  },
  'esm-browser-runtime': {
    file: resolve(`dist/${name}.runtime.esm-browser.js`),
    format: 'es'
  },
  'global-runtime': {
    file: resolve(`dist/${name}.runtime.global.js`),
    format: 'iife'
  }
}

知道了这两类参数的值,我们便可以很清晰的去阅读createConfig函数


function createConfig(format, output, plugins = []) {
  
  // 此处省去很多代码,详情请阅读具体源码
 
  return {
    input: resolve(entryFile),
    // Global and Browser ESM builds inlines everything so that they can be
    // used alone.
    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
    }
  }
}

配置文件中间有太多的判断,详情请阅读具体源码。

我们只关心它的返回,创建了一个怎样的配置文件

看到它的入口其实是

let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`

output在省略的代码中有配置,可以查看

然后就是应用了一些插件,一些其他的配置

总结

Vue3的分包流程大概就是这样,本文可能描述的不太细致,希望可以给大家一个思路参考,还是希望大家能够自己动手进行debug进行详细流程查看。

本人也在尝试手写Vue3源码,git地址:github.com/chris-zhu/r…

如有错误,请指正