vue源码分析(二) 源码构建

10/2/2018 vue源码分析面试

# 1. 概述

vue 源码选择了rollup (opens new window) 进行构建,rollup 相比于 webpack,更加轻量,编译后的代码更加干净,更适合javascript库的构建,除了vue以外,像ReactEmberD3Three.js 以及其他很多开源库也选择了Rollup 进行构建。

# 2. 版本输出

# 2.1 版本

​ 根据 format 构建格式可分为三个版本;再根据有无 compiler ,每个版本中又可分出两个版本。

类型 说明
cjs 表示构建出来的文件遵循 CommonJS 规范
es 构建出来的文件遵循 ES Module 规范
umd 构建出来的文件遵循 UMD 规范

# 2.2 区别

类型 区别
Runtime Only 通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量
Runtime + Compiler 我们如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板;Vue.js 2.0 中,最终渲染都是通过 render 函数,如果写 template 属性,则需要编译成 render 函数,那么这个编译过程会发生运行时,所以需要带有编译器的版本

​ 通过上面的说明,我们对 Vue 三种不同的构建输出的概念有了一个了解,对每种模块形式又分别输出了 Runtime Only(运行时版) 以及 完整版(Runtime + Compiler) 的概念也有了一个了解,接下来我们从源码的角度进行分析。

# 3. package.json

​ 我们都知道 rollupwebpackscripts 是中配置的节点是执行 npm 脚本命令简写,比如 "start": "react-scripts start", 执行 npm start 就是运行 "react-scripts start" ,其中 build 节点为构建vue的脚本代码。具体配置如下:

​ 源码目录: package.json

{
  // ...
  "scripts": {
    // 构建完整版 umd 模块的 Vue
    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
    "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
    "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
    "dev:test": "karma start test/unit/karma.dev.config.js",
    "dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
    "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
    "dev:weex": "rollup -w -c scripts/config.js --environment TARGET:weex-framework",
    "dev:weex:factory": "rollup -w -c scripts/config.js --environment TARGET:weex-factory",
    "dev:weex:compiler": "rollup -w -c scripts/config.js --environment TARGET:weex-compiler ",
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
    "test": "npm run lint && flow check && npm run test:types && npm run test:cover && npm run test:e2e -- --env phantomjs && npm run test:ssr && npm run test:weex",
    "test:unit": "karma start test/unit/karma.unit.config.js",
    "test:cover": "karma start test/unit/karma.cover.config.js",
    "test:e2e": "npm run build -- web-full-prod,web-server-basic-renderer && node test/e2e/runner.js",
    "test:weex": "npm run build:weex && jasmine JASMINE_CONFIG_PATH=test/weex/jasmine.js",
    "test:ssr": "npm run build:ssr && jasmine JASMINE_CONFIG_PATH=test/ssr/jasmine.js",
    "test:sauce": "npm run sauce -- 0 && npm run sauce -- 1 && npm run sauce -- 2",
    "test:types": "tsc -p ./types/test/tsconfig.json",
    "lint": "eslint src scripts test",
    "flow": "flow check",
    "sauce": "karma start test/unit/karma.sauce.config.js",
    "bench:ssr": "npm run build:ssr && node benchmarks/ssr/renderToString.js && node benchmarks/ssr/renderToStream.js",
    "release": "bash scripts/release.sh",
    "release:weex": "bash scripts/release-weex.sh",
    "release:note": "node scripts/gen-release-note.js",
    "commit": "git-cz"
  },
  // ...
}
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

# 4. 构建过程

# 4.1 入口文件

​ 从上面的 package.json 中的配置我们可以看出 vue 构建输出最终执行的 "build": "node scripts/build.js",所以执行的 js 文件是 scripts/build.js,如下:

​ 源码目录: scripts/build.js

// ...

// 获取构建需要的配置
let builds = require('./config').getAllBuilds()

build(builds)
// ...
1
2
3
4
5
6
7

​ 我们从 build.js 中的源码可以看出,首先从配置文件 ./config.js 中读取配置信息,然后进行过滤,最后通过 build(builds) 方法构建 vue.js ,接下来我们分析一下 config.js

# 4.2 配置文件

​ 源码目录: scripts/config.js

const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs-dev': { /* ...*/ }
  'web-runtime-cjs-prod': { /* ...*/ },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs-dev': { /* ...*/ }
  'web-full-cjs-prod': { /* ...*/ },
  // Runtime only ES modules build (for bundlers)
  'web-runtime-esm': { /*...*/ },
  // Runtime+compiler ES modules build (for bundlers)
  'web-full-esm': { /*...*/ },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-dev': { /*...*/ },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-prod': { /*...*/ },
  // runtime-only umd build (Browser)
  'web-runtime-dev': { /*...*/ },
  // runtime-only umd production build (Browser)
  'web-runtime-prod': { /*...*/ },
  // Runtime+compiler umd 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 umd 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': { /*...*/ },
  // Web compiler (UMD for in-browser use).
  'web-compiler-browser': { /*...*/ },
  // Web server renderer (CommonJS).
  'web-server-renderer-dev': { /*...*/ },
  'web-server-renderer-prod': { /*...*/ },
  'web-server-renderer-basic': { /*...*/ },
  'web-server-renderer-webpack-server-plugin': { /*...*/ },
  'web-server-renderer-webpack-client-plugin': { /*...*/ },
  // Weex runtime factory
  'weex-factory': { /*...*/  },
  // Weex runtime framework (CommonJS).
  'weex-framework': { /*...*/ },
  // Weex compiler (CommonJS). Used by Weex's Webpack loader.
  'weex-compiler': { /*...*/ }
}
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

​ 配置文件遵循 Rollup 的构建规则的。其中:

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

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

  • format:属性表示构建的格式

    • cjs : 表示构建出来的 文件遵循 CommonJS 规范
    • es: 表示构建出来的文件遵循 ES Module 规范
    • umd :表示构建出来的文 件遵循 UMD 规范

​ 以 web-full-dev 配置为例,它的 entryresolve('web/entry-runtime-with-compiler.js') ,先来看一下函数的定义。

​ 源码目录: scripts/config.js

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)
  }
}
1
2
3
4
5
6
7
8
9

resolve 函数接受一个参数 p ,以 web-full-dev 为例。

​ 此处的参数是 web/entry-runtime-with-compiler.jsdist/vue.jsresolve 函数中以 / 分割获取到一个数组,此例子中分别为 ['web', 'entry-runtime-with-compiler.js']['dist', 'vue.js'] ,拿到数组的第一项 webdist 然后判断第一项对应的 key 能否在 aliases 中找到,最终拼接获得路径字符串: src/platforms/web/entry-runtime-with-compiler.js../dist/vue.js

​ 其中 aliases 定义在文件 aliases.js 中:

​ 源码目录: scripts/aliases.js

const 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')
}
1
2
3
4
5
6
7
8
9
10
11
12

​ 这里定义了一些项目的正式路径所对应的键值对,上面例子对应的键值为:web:resolve('src/platforms/web')

​ 最终,通过函数 genConfig(name) 生成 rollup 所需的配置项,经过 Rollup 的构建打包后,最终会在 dist 目录下生成不同版本的 vue.jsgenConfig 函数的定义如下:

​ 源码目录: scripts/config.js

// npm run dev ===> TARGET:web-full-dev
function genConfig (name) {
  const opts = builds[name] // // builds 里面 web-full-dev ===> 对应的配置
  const config = {
    sourceMap: true,
    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
}
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

# 5. 构建结果

|── dist
│   ├── README.md
│   ├── vue.common.dev.js
│   ├── vue.common.js
│   ├── vue.common.prod.js
│   ├── vue.esm.browser.js
│   ├── vue.esm.browser.min.js
│   ├── vue.esm.js
│   ├── vue.js
│   ├── vue.min.js
│   ├── vue.runtime.common.dev.js
│   ├── vue.runtime.common.js
│   ├── vue.runtime.common.prod.js
│   ├── vue.runtime.esm.js
│   ├── vue.runtime.js
│   └── vue.runtime.min.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 6. 总结

UMD CommonJS ES Module (基于构建工具使用) ES Module (直接用于浏览器)
完整版 vue.js vue.common.js vue.esm.js vue.esm.browser.js
只包含运行时版 vue.runtime.js vue.runtime.common.js vue.runtime.esm.js -
完整版 (生产环境) vue.min.js - - vue.esm.browser.min.js
只包含运行时版 (生产环境) vue.runtime.min.js - - -

# 7. 参考资料

对不同构建版本的解释