Vue.js 源码构建
1 )rollup 和 webpack 的对比
- vuejs的源码呢是基于rollup构建的
- rollup 和 webpack 都是一个构建工具
- webpack 它会更强大一些, 会把像图片, css等静态资源通通编译成javascript
- rollup 更适合一种javscript库的一个编译
- 它只出了js部分,而其他资源它是不管的,所以它更轻量
- 在编译后代码也是更友好的
- 所以 vuejs 就选了rollup做构建
2 )rollup的构建设计
- vuejs 是发布到 npm 上的一个包, 每个包都是需要一个package.json文件来做描述
- 它是对项目的描述文件,它的内容实际上是一个标准的 JSON 对象
- 比如说,常用的属性, name, version, description, main, module, ...
- main 是vue的入口,在 import vue 时,通过这个 main 来查找入口, 后缀是.js
- module 和 main 非常类似的,在webpack2以上把 module作为默认入口, 后缀是 .esm.js
- vuejs 源码是基于 rollup 构建的,它的构建相关配置都在 scripts 目录下
- npm 提供了一个叫 npm scripts 的东西
- 之前早期构建, 可能会用到gulp或者grunt
- 它们两个都是一个以任务为基准的
- 也就是说可以定义一系列任务
- npm scripts 就是完成了这部分的功能
- 也就是说它定义了很多脚本, 每个脚本都是一个任务
- 通过 npm run xxx 可以执行不同的任务
- 构建相关的任务,就是这三个
- build 构建web平台相关
- build:ssr 构建服务端渲染相关
- build:weex 构建weex平台相关
- 我们的源码是托管在 src 目录下,通过构建生成的目标代码在 dist 目录下
- 在vue的仓库下,它已经默认帮我们构建出来很多版本的vuejs
- 那为什么我们能构建如此多版本的vuejs呢?
3 )rollup构建vuejs的过程
package.json
js
{
"script": {
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex"
}
}
- 当我们去执行的这个 npm scripts 的时候,比如说,执行 npm run build
- 它实际上就是执行了这样一个脚本
node scripts/build.js
- 我们来看一下 scripts/build.js
scripts/build.js
js
const fs = require('fs')
const path = require('path')
const zlib = require('zlib')
const rollup = require('rollup')
const terser = require('terser')
if (!fs.existsSync('dist')) {
fs.mkdirSync('dist')
}
let builds = require('./config').getAllBuilds()
// 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)
function build (builds) {
let built = 0
const total = builds.length
const next = () => {
buildEntry(builds[built]).then(() => {
built++
if (built < total) {
next()
}
}).catch(logError)
}
next()
}
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)
}
})
}
function write (dest, code, zip) {
return new Promise((resolve, reject) => {
function report (extra) {
console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || ''))
resolve()
}
fs.writeFile(dest, code, err => {
if (err) return reject(err)
if (zip) {
zlib.gzip(code, (err, zipped) => {
if (err) return reject(err)
report(' (gzipped: ' + getSize(zipped) + ')')
})
} else {
report()
}
})
})
}
function getSize (code) {
return (code.length / 1024).toFixed(2) + 'kb'
}
function logError (e) {
console.log(e)
}
function blue (str) {
return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m'
}
- 前面声明读取的模块
let builds = require('./config').getAllBuilds()
是从配置文件中,读取配置- 之后,再通过命令行参数对构建配置做过滤,最终调用 build() 函数 进行真正的构建
- 所以它整个的构建的流程是非常清晰的
- 那我们首先来分析一下这个这就是怎么拿到的
打开 .config 文件
js
const path = require('path')
const buble = require('rollup-plugin-buble')
const alias = require('rollup-plugin-alias')
const cjs = require('rollup-plugin-commonjs')
const replace = require('rollup-plugin-replace')
const node = require('rollup-plugin-node-resolve')
const flow = require('rollup-plugin-flow-no-whitespace')
const version = process.env.VERSION || require('../package.json').version
const weexVersion = process.env.WEEX_VERSION || require('../packages/weex-vue-framework/package.json').version
const featureFlags = require('./feature-flags')
const banner =
'/*!\n' +
` * Vue.js v${version}\n` +
` * (c) 2014-${new Date().getFullYear()} Evan You\n` +
' * Released under the MIT License.\n' +
' */'
const weexFactoryPlugin = {
intro () {
return 'module.exports = function weexFactory (exports, document) {'
},
outro () {
return '}'
}
}
const aliases = require('./alias')
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)
}
}
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
},
// Runtime only ES modules build (for bundlers)
'web-runtime-esm': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.esm.js'),
format: 'es',
banner
},
// Runtime+compiler ES modules build (for bundlers)
'web-full-esm': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.js'),
format: 'es',
alias: { he: './entity-decoder' },
banner
},
// Runtime+compiler ES modules build (for direct import in browser)
'web-full-esm-browser-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.browser.js'),
format: 'es',
transpile: false,
env: 'development',
alias: { he: './entity-decoder' },
banner
},
// Runtime+compiler ES modules build (for direct import in browser)
'web-full-esm-browser-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.browser.min.js'),
format: 'es',
transpile: false,
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// runtime-only build (Browser)
'web-runtime-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.js'),
format: 'umd',
env: 'development',
banner
},
// runtime-only production build (Browser)
'web-runtime-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.min.js'),
format: 'umd',
env: 'production',
banner
},
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
// Runtime+compiler production build (Browser)
'web-full-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.min.js'),
format: 'umd',
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// Web compiler (CommonJS).
'web-compiler': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/build.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
},
// Web compiler (UMD for in-browser use).
'web-compiler-browser': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/browser.js'),
format: 'umd',
env: 'development',
moduleName: 'VueTemplateCompiler',
plugins: [node(), cjs()]
},
// Web server renderer (CommonJS).
'web-server-renderer-dev': {
entry: resolve('web/entry-server-renderer.js'),
dest: resolve('packages/vue-server-renderer/build.dev.js'),
format: 'cjs',
env: 'development',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-prod': {
entry: resolve('web/entry-server-renderer.js'),
dest: resolve('packages/vue-server-renderer/build.prod.js'),
format: 'cjs',
env: 'production',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-basic': {
entry: resolve('web/entry-server-basic-renderer.js'),
dest: resolve('packages/vue-server-renderer/basic.js'),
format: 'umd',
env: 'development',
moduleName: 'renderVueComponentToString',
plugins: [node(), cjs()]
},
'web-server-renderer-webpack-server-plugin': {
entry: resolve('server/webpack-plugin/server.js'),
dest: resolve('packages/vue-server-renderer/server-plugin.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-webpack-client-plugin': {
entry: resolve('server/webpack-plugin/client.js'),
dest: resolve('packages/vue-server-renderer/client-plugin.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
// Weex runtime factory
'weex-factory': {
weex: true,
entry: resolve('weex/entry-runtime-factory.js'),
dest: resolve('packages/weex-vue-framework/factory.js'),
format: 'cjs',
plugins: [weexFactoryPlugin]
},
// Weex runtime framework (CommonJS).
'weex-framework': {
weex: true,
entry: resolve('weex/entry-framework.js'),
dest: resolve('packages/weex-vue-framework/index.js'),
format: 'cjs'
},
// Weex compiler (CommonJS). Used by Weex's Webpack loader.
'weex-compiler': {
weex: true,
entry: resolve('weex/entry-compiler.js'),
dest: resolve('packages/weex-template-compiler/build.js'),
format: 'cjs',
external: Object.keys(require('../packages/weex-template-compiler/package.json').dependencies)
}
}
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
}
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
-
这个config文件在最后一行暴露了一个方法,叫 getAllBuilds,它是一个函数
- 这个函数做了什么事情呢?
Object.keys(builds).map(genConfig)
- 拿到一个keys的数组,然后我们再通过map方法调用这个 genConfig 函数
- 这个函数做了什么事情呢?
-
我们来看这个 builds 参数
- 上面代码大篇幅定义了 builds 对象
- 里面每个key对应的也都是一个对象
- 通过注释可知,是不同版本的vuejs的编译配置
- 每一个编译配置它都会有一个 entry 顾名思义就是入口
- dest 顾名思义就是目标
- 还有一个format(输出格式) 和 banner(头部注释)
-
这个 entry 它是通过 resolve 这个函数,然后传一个字符串(文件地址)
jsconst aliases = require('./alias') 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) } }
-
resolve 函数,它就是接收一个参数。这个字符参数会通过split('/') 拿到第一个值作为base
-
之后判断 aliases 这个 aliases 也是 require 进来的,看下这个 alias 文件
jsconst path = require('path') const resolve = p => path.resolve(__dirname, '../', p) module.exports = { vue: resolve('src/platforms/web/entry-runtime-with-compiler'), compiler: resolve('src/compiler'), core: resolve('src/core'), shared: resolve('src/shared'), web: resolve('src/platforms/web'), weex: resolve('src/platforms/weex'), server: resolve('src/server'), sfc: resolve('src/sfc') }
-
可见,alias 文件提供别名到真实地址文件的映射
-
回到最上面的 resolve 方法内部,最终返回了从参数到真实地址的字符串
-
-
dest 这个key也走的 resolve, 只不过走到了 最终 else 的环节里
-
format 是构建出来的文件格式,cjs 对应的就是 xxx.common.js
- cjs 最终生成的js文件是 module.exports = Vue
- es 最终生成的文件是 export default Vue
- umd 最终生成的文件是 符合 umd 规范的 vuejs 文件
- ...
-
所以,很显然
- 上面 web 对应的真实的路径是 path.resolve(__dirname, '.../src/platforms/web'),这个路径就找到了 Vue.js 源码的 web 目录
- 然后 resolve 函数通过 path.resolve(aliases[base], p.slice(base.length + 1)) 找到了最终路径
- 它就是 Vue.js 源码 web 目录下的 entry-runtime.js。因此,web-runtime-cjs 配置对应的入口文件就找到了
- 它经过 Rollup 的构建打包后,最终会在 dist 目录下生成 vue.runtime.common.js
-
banner 是自己设计的头部注释
- 里面可以 写八本,日期,作者,license 等信息
-
再回到我们的config,最终通过
Object.keys(builds).map(genConfig)
- 拿到这个所有keys 的一个数组,然后这个这个数组我们去调用这个 genConfig 函数
- genConfig 就拿到每个key,然后它就会拿到这个对象,再通过 build[name]
- name 是key对应的这个对象,构造出一个新的 config 对象
- 里面有 input, external, plugins, output, ...
- 这个最终的 config 对象才是 rollup 打包所需要的配置结构
- 这是个适配器来进行的转换工作
-
返回到
let builds = require('./config').getAllBuilds()
- 这里的 builds 是一个数组
-
之后进行
if (process.argv[2]) {}
的判断- 提取到命令行输出的参数,如 -- weex 等
- 如果有参数,则会通过 filter 来过滤一些不需要的流程
-
最终编译的时候,就调用
build(builds)
在里面进行一个个的编译- 里面定义一个 next() 函数,里面调用 buildEntry, 之后递归,里面有个 built 计数器
- 而 buildEntry 传入最终的config, config作为最终rollup的参数进行构建
- 编译完之后得到了 bundle, 通过了 bundle.generate 传入 output
- 之后拿到 code, 判断环境,分别做处理,最终通过 write 方法,得到构建好的文件
- 在生成的过程中,打下一些 log 信息
-
这是整个的构建流程
4 ) Runtime Only VS Runtime + Compiler
- 通常我们利用 vue-cli 去初始化我们的 Vue.js 项目的时候
- 会询问我们用 Runtime Only 版本的还是 Runtime + Compiler 版本
- 下面我们来对比这两个版本
4.1 ) Runtime Only
- 我们在使用 Runtime Only 版本的 Vue.js 的时候,通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript
- 在浏览器里,是不认识 .vue文件的,所以需要一个编译的过程,在编译阶段,会把 template 模板编译成 render 函数
- 最终编译后的,就是一个 render 函数的版本,所以vue是不带编译的,也就是运行时不带编译
- 所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量
4.2 )Runtime + Compiler
-
我们如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板,如下所示:
js// 需要编译器的版本 new Vue({ template: '<div>{{ hi }}</div>' })
-
这种一定要选择 Runtime + Compiler 版本
js// 这种情况不需要 new Vue({ render (h) { return h('div', this.hi) } })
-
这种就不需要
-
而 .vue文件 是在编译过程中,通过 vue-loader 处理的
-
所以我们写的 .vue 文件在运行的时候已经编译成 js 函数了,并且模板部分已经编译成 render 函数了
-
在 Vue.js 2.0 中,最终渲染都是通过 render 函数,如果写 template 属性,则需要编译成 render 函数
-
那么这个编译过程会发生运行时,需要带有编译器的版本
-
很显然,这个编译过程对性能会有一定损耗,所以通常开发阶段更推荐使用 Runtime-Only 的 Vue.js
- 一种是运行时的性能优化,一种是编译出来的体积会更轻量
5 )总结
- 我们可了解到 Vue.js 的构建打包过程,也知道了不同作用和功能的 Vue.js 它们对应的入口以及最终编译生成的 JS 文件
- 在实际开发过程中我们会用 Runtime Only 版本开发比较多
- 但为了分析 Vue 的编译过程,我们重点分析的源码是