Vue.js 源码是基于 Rollup 构建的,他的构建相关配置都在 scripts 目录下。
构建脚本
通常一个基于 NPM 托管的项目都会有一个 package.json 文件,他的内容实际上是一个标准的 JSON 对象。
我们通常会配置 script 字段作为 NPM 的执行脚本,Vue.js 源码构建脚本如下:
1 2 3 4 5 6 7
| { "script": { "build": "node scripts/build.js", "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer", "build:weex": "npm run build -- weex" } }
|
构建过程
基于源码分析构建过程,构建入口文件在 script/build.js 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| let builds = require('./config').getAllBuilds()
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 { builds = builds.filter(b => { return b.output.file.indexOf('weex') === -1 })} build(builds)
|
这段代码作用是:先从配置文件读取配置,在通过命令行参数对构建配置做过滤,这样就可以构建出不同用途的 Vue.js 。
代码构建配置文件
源码地址:srcripts/config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| const builds = { 'web-runtime-cjs': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.common.js'), format: 'cjs', banner }, 'web-full-cjs': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.common.js'), format: 'cjs', alias: { he: './entity-decoder' }, banner }, 'web-runtime-esm': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.esm.js'), format: 'es', banner }, 'web-full-esm': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.esm.js'), format: 'es', alias: { he: './entity-decoder' }, banner }, 'web-runtime-dev': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.js'), format: 'umd', env: 'development', banner }, 'web-runtime-prod': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.min.js'), format: 'umd', env: 'production', banner }, '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 }, '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 }, }
|
对于单个配置,他是遵循 Rollup 的构建规则的。
entry
属性表示构建的入口 JS 文件地址
dest
属性表示构建后的 JS 文件地址。
format
属性表示构建的格式
通过上面的配置可以看出来,每种模块形式分别输出了 运行时版 以及 完整版。
通过名字可以知道,完整版比运行时版多了一个 compiler, 而 compiler 的作用是:将template 编译为 render 函数。
以 web-runtime-cjs
配置为例,他的 entry
是 resolve('web/entry-runtime-js')
。
resolve 函数的定义
源码地址:scripts/config.js
1 2 3 4 5 6 7 8 9
| 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) } }
|
这里的 resolve
函数实现的非常简单,他先把 resolve
函数传入的参数 p 通过 / 分割成了数组,然后取数组第一个元素设置为 base
。
在这个例子中 , 参数 p 是 web/entry-runtime.js
,那么 base
则为 web
。
base 并不是实际的路径,他的真实路径借助了别名的配置。
别名配置定义
源码地址:scripts/alias
1 2 3 4 5 6 7 8 9 10 11 12 13
| const path = require('path') module.exports = { vue: path.resolve(__dirname, '../src/platforms/web/entry-runtime-with-compiler'), compiler: path.resolve(__dirname, '../src/compiler'), core: path.resolve(__dirname, '../src/core'), shared: path.resolve(__dirname, '../src/shared'), web: path.resolve(__dirname, '../src/platforms/web'), weex: path.resolve(__dirname, '../src/platforms/weex'), server: path.resolve(__dirname, '../src/server'), entries: path.resolve(__dirname, '../src/entries'), sfc: path.resolve(__dirname, '../src/sfc') }
|
这里的 web 对应的真实路径是 path.resolve(__dirname,'../src/platform/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
。
Runtime Only VS Runtime + Compiler
通常我们利用 vue-cli 去初始化我们的 Vue.js 项目的时候会询问我们用 Runtime Only 版本还是 Runtime + Compiler 版本。
不同构建输出的作用
为什么要分 运行时版 与 完整版?
什么是完整版:完整版 = 运行时版 + Compiler
也就是说完整版比运行时版多了一个 Compiler, 一个将字符串模板编译为 render 函数的编译器。
将字符串模板编译为 render 函数这个过程在构建时完成,这样真正运行代码时就免去了一个步骤,提升了性能。
同时,将 Compiler 抽离为单独的包,还减小了库的体积。
那为什么还需要完整版呢?
因为 Vue 是渐进式 JavaScript 框架,就是说如果你已经有一个现成的服务端应用,也就是非单页应用,可以将 Vue.js 作为该应用的一部分嵌入其中,带来更丰富的交互体验。
说白了就是允许你在代码运行时编译模板,再不配合构建工具的情况下可以直接使用,但是更多情况下推荐你配合构建工具使用运行时版本。
Runtime Only
我们在使用 Runtime Only 版本的 Vue.js 的时候,通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript, 因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因为代码体积也会更轻量。
Runtime + Compiler
我们如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入了一个字符串,则需要在客户端编译模板。
1 2 3 4 5 6 7 8 9 10 11
| new Vue({ template: '<div>{{ hi }}</div>' })
new Vue({ render(h) { return h('div', this.hi) } })
|
因为在 Vue.js 2.0 中,最终渲染都是通过 render 函数,如果写template 属性,则需要编译成 render 函数,那么这个编译过程会发生运行时,所以需要带有编译器的版本。
很显然,这个编译过程对性能有一定的损耗,所以通常我们更推荐使用 Runtime-Only 的 Vue.js。