一、构建方式
Vue2 源码是基于 Rollup 构建的,适合构建 JavaScript 库,无法识别其它资源类型。如果需要识别其它资源类型,则需借助插件才能让 Rollup 识别。而对于 Webpack 构建工具,更适合在项目中应用。
二、构建流程
结合上面的流程图,来说说 Vue2 源码是如何构建的?
通常我们会在文件 package.json 配置 script 字段作为执行 npm 脚本,配置如下:
{
"script": {
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",
}
}
当我们执行命令:npm run build 时,实际上是执行目录 scripts 下文件 build.js 。
从上面流程图可知,文件 scripts/build.js 所做的工作主要有三点:
- 从文件
scripts/config.js加载构建配置(builds) - 根据命令行参数对构建配置进行过滤
- 调用函数
build执行构建
那么下面来说说这三点其内部是如何实现的?
加载构建配置(builds)
在文件 scripts/build.js 中,加载构建配置的代码如下:
let builds = require('./config').getAllBuilds()
从代码中可看出,调用文件 scripts/config.js 里函数 getAllBuilds ,代码实现如下:
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
函数的作用是返回构建配置,但是在返回之前对构建配置 builds 进行遍历,按照 Rollup 构建规范调用函数 genConfig 生成配置。
Vue 可构建出不同用途的版本,具体配置在变量 builds 里,先通过一张图来看可构建出哪些版本?
那么来看下具体的配置,然后举例子说明:
const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'web-runtime-cjs-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.dev.js'),
format: 'cjs',
env: 'development',
banner
},
'web-runtime-cjs-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.prod.js'),
format: 'cjs',
env: 'production',
banner
},
// Runtime+compiler CommonJS build (CommonJS)
'web-full-cjs-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.dev.js'),
format: 'cjs',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
'web-full-cjs-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.prod.js'),
format: 'cjs',
env: 'production',
alias: { he: './entity-decoder' },
banner
},
...
}
以 web-runtime-cjs-prod 为例说下配置参数:
entry:表示构建的入口JS文件地址dest:表示构建后的JS文件地址format:表示构建出来的文件遵循CommonJS格式env:指定构建环境banner:构建说明
enter 和 dest 用到函数 resolve ,那么该函数究竟是什么呢?代码实现如下:
const resolve = p => {
const base = p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}
函数的作用是根据传过来的参数 p 获取完整的文件路径。
对于 genConfig ,其内部实现也挺简单,主要是根据 Rollup 规范重新生成配置,用于后续打包构建,代码实现如下:
function genConfig(name) {
const opts = builds[name]
const config = {
input: opts.entry,
external: opts.external,
plugins: [
flow(),
alias(Object.assign({}, aliases, opts.alias))
].concat(opts.plugins || []),
output: {
file: opts.dest,
format: opts.format,
banner: opts.banner,
name: opts.moduleName || 'Vue'
},
onwarn: (msg, warn) => {
if (!/Circular/.test(msg)) {
warn(msg)
}
}
}
// built-in vars
const vars = {
__WEEX__: !!opts.weex,
__WEEX_VERSION__: weexVersion,
__VERSION__: version
}
// feature flags
Object.keys(featureFlags).forEach(key => {
vars[`process.env.${key}`] = featureFlags[key]
})
// build-specific env
if (opts.env) {
vars['process.env.NODE_ENV'] = JSON.stringify(opts.env)
}
config.plugins.push(replace(vars))
if (opts.transpile !== false) {
config.plugins.push(buble())
}
Object.defineProperty(config, '_name', {
enumerable: false,
value: name
})
return config
}
过滤构建配置
基于以上获取到的构建配置,根据命令行参数对构建配置进行过滤,其实现逻辑也挺简单的,代码实现如下:
// filter builds via command line arg
if (process.argv[2]) {
const filters = process.argv[2].split(',')
builds = builds.filter(b => {
return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
})
} else {
// filter out weex builds by default
builds = builds.filter(b => {
return b.output.file.indexOf('weex') === -1
})
}
进行构建
基础工作做好了,可开始构建,核心逻辑实现就一行代码:
build(builds)
所有的构建实现都在函数 build 里,来看看其代码实现:
function build(builds) {
let built = 0
const total = builds.length
const next = () => {
buildEntry(builds[built]).then(() => {
built++
if (built < total) {
next()
}
}).catch(logError)
}
next()
}
函数内部定义嵌套函数 next ,其作用是调用函数 buildEntry 对 builds 各个版本进行构建,最终打包构建出不同用途的 Vue.js 产物。buildEntry 实现如下:
function buildEntry(config) {
const output = config.output
const { file, banner } = output
const isProd = /(min|prod).js$/.test(file)
return rollup.rollup(config)
.then(bundle => bundle.generate(output))
.then(({ output: [{ code }] }) => {
if (isProd) {
const minified = (banner ? banner + '\n' : '') + terser.minify(code, {
toplevel: true,
output: {
ascii_only: true
},
compress: {
pure_funcs: ['makeMap']
}
}).code
return write(file, minified, true)
} else {
return write(file, code)
}
})
}
函数的作用是先调用 Rollup 方法 rollup 打包构建,其输出是 bundle ,然后再执行 bundle.generate(output) 生成产物,根据不同的环境对产物进行处理。
如果是生产环境,则使用 terser 对代码进行压缩,再调用函数 write 将产物写入到输出目录;如果是开发环境,则直接调用函数 write 将产物写入到输出目录,不作任何处理。
三、Runtime Only VS Runtime + Compiler
从上面的分析可知,Vue 可构建出 Runtime Only 和 Runtime + Compiler 版本,那么这两个版本有什么区别呢?
Runtime Only
仅运行时版本,只能通过 render 函数解析,无法识别 template 属性。
vue-cli 3.0+ 默认使用 Runtime Only,那么在 Vue 项目中之所以能跑起来,是因为借助 Webpack 的 vue-template-compiler 在编译阶段将 template 编译成 render 函数。这一层转换操作发生在编译阶段,所以只需包含运行时代码,其体积相对比较小。
对于使用 vue-cli 3.0+ 的项目,如果要使用 Runtime + Compiler 版本,则可在配置文件 vue.config.js 设置属性 runtimeCompiler 为 true :
module.exports = {
runtimeCompiler: true
}
Type:
booleanDefault:
false是否使用包含运行时编译器的 Vue 构建版本。设置为
true后你就可以在 Vue 组件中使用template选项了,但是这会让你的应用额外增加 10kb 左右。
Runtime + Compiler
如果项目中是通过 CDN 方式引入 Vue.js,并且没有使用 Webpack 等构建工具;同时又使用 Vue 属性 template 传入一个字符串,则需要在客户端编译模板,转换成 render 函数,浏览器才能解析。所以,需要使用带有编译器的 Vue 版本,即 Runtime + Compiler。
由于这时的编译过程是发生在运行时,对性能有一定的损耗,所以官方推荐使用我们使用 Runtime Only Vue.js 版本。