跳到主要内容

目录和构建思路

Vue.js 源码目录设计​

Vue.js 的源码都在 src 目录下,其目录结构如下。

src
├── compiler # 编译相关
├── core # 核心代码
├── platforms # 不同平台的支持
├── server # 服务端渲染
├── sfc # .vue 文件解析
├── shared # 共享代码

compiler​

compiler 目录包含 Vue.js 所有编译相关的代码。它包括把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能。

编译的工作可`以在构建时做(借助 webpack、vue-loader 等辅助插件);也可以在运行时做,使用包含构建功能的 Vue.js。显然,编译是一项耗性能的工作,所以更推荐前者——离线编译。

core​

core 目录包含了 Vue.js 的核心代码,包括==内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数==等等。

这里的代码可谓是 Vue.js 的灵魂,也是我们之后需要重点分析的地方。

platform​

Vue.js 是一个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 native 客户端上。platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。

我们会重点分析 web 入口打包后的 Vue.js,对于 weex 入口打包的 Vue.js,感兴趣的同学可以自行研究。

server​

Vue.js 2.0 支持了服务端渲染,所有服务端渲染相关的逻辑都在这个目录下。注意:这部分代码是跑在服务端的 Node.js,不要和跑在浏览器端的 Vue.js 混为一谈。

服务端渲染主要的工作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。

sfc​

通常我们开发 Vue.js 都会借助 webpack 构建, 然后通过 .vue 单文件来编写组件。

这个目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象。

shared​

Vue.js 会定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的。

Vue.js 源码构建​

构建脚本​

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

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

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

{
"script": {
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex"
}
}

这里总共有 3 条命令,作用都是构建 Vue.js,后面 2 条是在第一条命令的基础上,添加一些环境参数。

当在命令行运行 npm run build 的时候,实际上就会执行 node scripts/build.js,接下来我们来看看它实际是怎么构建的。

构建过程​

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

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 了。接下来我们看一下配置文件,在 scripts/config.js 中:

const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
"web-runtime-cjs": {
entry: resolve("web/entry-runtime.js"),
dest: resolve("dist/vue.runtime.common.js"),
format: "cjs",
banner,
},
// Runtime+compiler CommonJS build (CommonJS)
"web-full-cjs": {
entry: resolve("web/entry-runtime-with-compiler.js"),
dest: resolve("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": {
entry: resolve("web/entry-runtime.js"),
dest: resolve("dist/vue.runtime.esm.js"),
format: "es",
banner,
},
// Runtime+compiler CommonJS build (ES Modules)
"web-full-esm": {
entry: resolve("web/entry-runtime-with-compiler.js"),
dest: resolve("dist/vue.esm.js"),
format: "es",
alias: { he: "./entity-decoder" },
banner,
},
// runtime-only build (Browser)
"web-runtime-dev": {
entry: resolve("web/entry-runtime.js"),
dest: resolve("dist/vue.runtime.js"),
format: "umd",
env: "development",
banner,
},
// runtime-only production build (Browser)
"web-runtime-prod": {
entry: resolve("web/entry-runtime.js"),
dest: resolve("dist/vue.runtime.min.js"),
format: "umd",
env: "production",
banner,
},
// Runtime+compiler 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 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,
},
// ...
};

这里列举了一些 Vue.js 构建的配置,关于还有一些服务端渲染 webpack 插件以及 weex 的打包配置就不列举了。

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

以 web-runtime-cjs 配置为例,它的 entry 是 resolve('web/entry-runtime.js'),先来看一下 resolve 函数的定义。

源码目录: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);
}
};

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

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/platforms/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 版本。下面我们来对比这两个版本。

  • Runtime Only

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

  • Runtime + Compiler

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

// 需要编译器的版本
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