背景
学习Vue3
的同时,也需要关注Vue3
的分包机制。Vue3
将各个模块分包管理,包与包之见关系大大的解耦,使得每个包之间的维护性提高。因为每个包是一个独立的存在,也就使每个包可以独立的运行,并不完全依赖于vue
环境。例如@vue/reactivity
,它的响应式非常不错,使它的运行环境也有不同,社区有人将它运行于react
环境。当我阅读到的时候并不惊讶,因为我之前阅读# Vue3 源码深入解析 响应式原理 --> Reactivity就发现它不依赖于任何环境,以至于可以适当的运行任何环境。
package.json
当我拿到项目时,最先看的可能就是package.json
文件。因为这个文件可以清晰的看到项目的依赖、脚本命令···,我们先来分析一下scripts
其实vue
的dev
、build
等多个命令都是用node
去执行的js
的文件,于是我们便可以知道vue
的打包流程了
build
当我们cd到build.js
文件内,我们可以看到入口函数run
只有它在执行,其余的都是些功能函数
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
函数主要针对 isRelease
和 targets
做了判断
当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
文件夹并对文件类型进行过滤
我们可以看到每个包都是以文件夹格式存放的,所以targets
也是针对isDirectory()
来区别当前文件是否是一个vue
的包
然后针对每个包的package.json
文件内容在进行区分
是否是一个私有包 是否具有buildOptions
字段
例如上面两个包则会有不同的判断逻辑
于是我们最终得到我们需要进行打包的包名数组
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
的打包格式
而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…
如有错误,请指正