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。