Vue2 源码构建

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()  

// 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)

这段代码作用是:先从配置文件读取配置,在通过命令行参数对构建配置做过滤,这样就可以构建出不同用途的 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 = {  
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs': {
    entryresolve('web/entry-runtime.js'),
    destresolve('dist/vue.runtime.common.js'),
    format'cjs',
    banner
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs': {
    entryresolve('web/entry-runtime-with-compiler.js'),
    destresolve('dist/vue.common.js'),
    format'cjs',
    alias: { he'./entity-decoder' },
    banner
  },
  // Runtime only (ES Modules). Used by bundlers that support ES Modules,
  // e.g. Rollup & Webpack 2
  'web-runtime-esm': {
    entryresolve('web/entry-runtime.js'),
    destresolve('dist/vue.runtime.esm.js'),
    format'es',
    banner
  },
  // Runtime+compiler CommonJS build (ES Modules)
  'web-full-esm': {
    entryresolve('web/entry-runtime-with-compiler.js'),
    destresolve('dist/vue.esm.js'),
    format'es',
    alias: { he'./entity-decoder' },
    banner
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {
    entryresolve('web/entry-runtime.js'),
    destresolve('dist/vue.runtime.js'),
    format'umd',
    env'development',
    banner
  },
  // runtime-only production build (Browser)
  'web-runtime-prod': {
    entryresolve('web/entry-runtime.js'),
    destresolve('dist/vue.runtime.min.js'),
    format'umd',
    env'production',
    banner
  },
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entryresolve('web/entry-runtime-with-compiler.js'),
    destresolve('dist/vue.js'),
    format'umd',
    env'development',
    alias: { he'./entity-decoder' },
    banner
  },
  // Runtime+compiler production build  (Browser)
  'web-full-prod': {
    entryresolve('web/entry-runtime-with-compiler.js'),
    destresolve('dist/vue.min.js'),
    format'umd',
    env'production',
    alias: { he'./entity-decoder' },
    banner
  },
  // ...
}

对于单个配置,他是遵循 Rollup 的构建规则的。

  • entry 属性表示构建的入口 JS 文件地址

  • dest 属性表示构建后的 JS 文件地址。

  • format 属性表示构建的格式

    • cjs 表示构建出来的文件遵循 CommonJS 规范

    • es 表示构建出来的文件遵循 ES Module 规范,

    • umd 表示构建出来的文件遵循 UMD 规范

通过上面的配置可以看出来,每种模块形式分别输出了 运行时版 以及 完整版。

  • 运行时的入口文件名:entry-runtime.js

  • 完整版的入口文件名:entry-runtime-with-compiler.js

通过名字可以知道,完整版比运行时版多了一个 compiler, 而 compiler 的作用是:将template 编译为 render 函数

web-runtime-cjs 配置为例,他的 entryresolve('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。


Vue2 源码构建
https://blog.pangcy.cn/2022/05/02/前端编程相关/前端框架与库/Vue/Vue2 源码分析/Vue2 源码构建/
作者
子洋
发布于
2022年5月2日
许可协议