Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下。

构建脚本


通常一个基于 NPM 托管的项目都会有一个 package.json 文件,它是一个项目描述的文件,它的内容实际上是一个标准的 JSON 对象。

我们通常会配置 script 字段作为 NPM 的执行脚本,Vue.js 源码构建的脚本如下

  1. {
  2. "script": {
  3. "build": "node scripts/build.js",
  4. "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
  5. "build:weex": "npm run build -- weex"
  6. }
  7. }

这里总共有 3 条命令,作用都是构建 Vue.js,后面 2 条是在第一条命令的基础上,添加一些环境参数。
当在命令行运行 npm run build 的时候,实际上就会执行 node scripts/build.js,接下来我们来看看它实际是怎么构建的。

构建过程


我们对于构建过程分析是基于源码的,先打开构建的入口 JS 文件,在 scripts/build.js 中。

  1. let builds = require('./config').getAllBuilds();
  2. // filter builds via command line arg
  3. if (process.argv[2]) {
  4. const filters = process.argv[2].split(',');
  5. builds = builds.filter((b) => {
  6. return filters.some((f) => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1);
  7. });
  8. } else {
  9. // filter out weex builds by default
  10. builds = builds.filter((b) => {
  11. return b.output.file.indexOf('weex') === -1;
  12. });
  13. }
  14. build(builds);

这段代码的逻辑主要是通过 scripts/config.js 获取所有的打包配置,然后通过命令行传入的参数进行过滤。最后执行 build 构建函数。接下来我们看一下配置文件。

  1. const builds = {
  2. // Runtime+compiler CommonJS build (CommonJS)
  3. 'web-full-cjs-dev': {
  4. entry: resolve('web/entry-runtime-with-compiler.js'),
  5. dest: resolve('dist/vue.common.dev.js'),
  6. format: 'cjs',
  7. env: 'development',
  8. alias: { he: './entity-decoder' },
  9. banner,
  10. },
  11. // Runtime only ES modules build (for bundlers)
  12. 'web-runtime-esm': {
  13. entry: resolve('web/entry-runtime.js'),
  14. dest: resolve('dist/vue.runtime.esm.js'),
  15. format: 'es',
  16. banner,
  17. },
  18. // Runtime+compiler ES modules build (for bundlers)
  19. 'web-full-esm': {
  20. entry: resolve('web/entry-runtime-with-compiler.js'),
  21. dest: resolve('dist/vue.esm.js'),
  22. format: 'es',
  23. alias: { he: './entity-decoder' },
  24. banner,
  25. },
  26. // runtime-only build (Browser)
  27. 'web-runtime-dev': {
  28. entry: resolve('web/entry-runtime.js'),
  29. dest: resolve('dist/vue.runtime.js'),
  30. format: 'umd',
  31. env: 'development',
  32. banner,
  33. },
  34. // Runtime+compiler development build (Browser)
  35. 'web-full-dev': {
  36. entry: resolve('web/entry-runtime-with-compiler.js'),
  37. dest: resolve('dist/vue.js'),
  38. format: 'umd',
  39. env: 'development',
  40. alias: { he: './entity-decoder' },
  41. banner,
  42. },
  43. // ...
  44. };

这里列举了一些 Vue.js 的一些构建配置,其他配置就不一一列举了。

对于单个配置,它是遵循 Rollup 的构建规则的。其中 entry 属性表示构建的入口 JS 文件地址,dest 属性表示构建后的 JS 文件地址。format 属性表示构建的格式,cjs 表示构建出来的文件遵循 CommonJS 规范,es 表示构建出来的文件遵循 ES Module 规范。umd 表示构建出来的文件遵循 UMD 规范。

web-full-esm 配置为例,它的 entryresolve ( ‘web/entry-runtime-with-compiler.js’ ),先来看一下 resolve 函数的定义。

  1. const aliases = require('./alias');
  2. const resolve = (p) => {
  3. const base = p.split('/')[0];
  4. if (aliases[base]) {
  5. return path.resolve(aliases[base], p.slice(base.length + 1));
  6. } else {
  7. return path.resolve(__dirname, '../', p);
  8. }
  9. };

这里的 resolve 函数实现非常简单,它先把 resolve 函数传入的参数 p 通过 / 做了分割成数组,然后取数组第一个元素设置为 base。在我们这个例子中,参数 p web/entry-runtime-with-compiler.js,那么 base 则为 webbase 并不是实际的路径,它的真实路径借助了别名的配置,我们来看一下别名配置的代码,在 scripts/alias 中。

  1. const path = require('path');
  2. const resolve = (p) => path.resolve(__dirname, '../', p);
  3. module.exports = {
  4. vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  5. compiler: resolve('src/compiler'),
  6. core: resolve('src/core'),
  7. shared: resolve('src/shared'),
  8. web: resolve('src/platforms/web'),
  9. weex: resolve('src/platforms/weex'),
  10. server: resolve('src/server'),
  11. sfc: resolve('src/sfc'),
  12. };

很显然,这里 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-with-compiler.js。因此,web-full-esm 配置对应的入口文件就找到了。
它经过 Rollup 的构建打包后,最终会在 dist 目录下生成 vue.esm.js

Runtime Only VS Runtime + Compiler


通常我们利用 vue-cli 去初始化我们的 Vue.js 项目的时候会询问我们用 Runtime Only 版本的还是 Runtime + Compiler 版本。下面我们来对比这两个版本。

Runtime Only
我们在使用 Runtime Only 版本的 Vue.js 的时候,通常需要借助如 webpackvue-loader 工具把 .vue 文件编译成 JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量。

Runtime + Compiler
我们如果没有对代码做预编译,但又使用了 Vuetemplate 属性并传入一个字符串,则需要在客户端编译模板,如下所示:

  1. // 需要编译器的版本
  2. new Vue({
  3. template: '<div>{{ hi }}</div>',
  4. });
  5. // 这种情况不需要
  6. new Vue({
  7. render(h) {
  8. return h('div', this.hi);
  9. },
  10. });

因为在 Vue.js 2.0 中,最终渲染都是通过 render 函数,如果写 template 属性,则需要编译成 render 函数,所以需要带有编译器的版本。

很显然,这个编译过程对性能会有一定损耗,所以通常我们更推荐使用 Runtime-Only Vue.js