深入解读vue源码(二)构建过程

基本介绍

通过上篇文章《深入解读vue源码(一)目录设计》了解到了vue的目录设计后,这篇文章要对vue的构建过程进行一些详细的介绍。

vue的构建过程是在根目录下的scripts目录中通过Rollup编译build.js文件。


构建脚本

通常一个基于node的项目在根目录下都会有一个package.json的文件,它是对项目的一个描述文件,描述的内容可以是包版本、启动脚本、项目名等等,它的内容是一个标准的JSON对象。

我们主要从build模式下分析项目的构建过程(打包编译到dist目录),在package.json中我们能看到这样一段代码:

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

上面的代码作用于构建vue应用程序,其中ssrweex是对第一条的环境拓展参数,当执行npm run build的时候实际执行的就是node scripts/build.js

对于有环境拓展参数的命令来讲,会将命令中的参数( – 符号后面的参数)传入process.argv数组中。


解读构建过程

scripts/build.js分析

我们知道了build过程实际是去读取这个文件,打开这个文件我们开始一段段分析:

1
2
3
if (!fs.existsSync('dist')) {
fs.mkdirSync('dist')
}

这段代码是在判断项目的根目录下是否存在dist目录,不存在则创建。

1
let builds = require('./config').getAllBuilds()

这里通过builds变量保存一个在scripts/config.js中返回的src/platforms路径下所有JavaScript文件的数组对象,该对象包括支持ssr、客户端渲染和weex的文件名和文件路径等信息。

1
2
3
4
5
6
7
8
9
10
11
12
if (process.argv[2]) { // 判断是否是ssr或者weex
const filters = process.argv[2].split(',') // 将数组中的第三条数据以逗号分隔形式保存到数组
builds = builds.filter(b => { // 遍历builds对象
// 对filters的每一项与builds对象中的文件名或者文件路径进行筛选,当b中包含f时返回true
return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
})
} else { // process.argv[2] = undefined,则不是ssr或者weex
builds = builds.filter(b => {
// 将b中不包含weex的数据设置为true
return b.output.file.indexOf('weex') === -1
})
}

这段代码判断的是build的编译类型,用类型去判断编译哪些文件,在这里通过判断process.argv[2]数组中是否存在第三条数据,存在第三条数据的情况下就是说明build的环境拓展参数是ssr或者weex

process.argv[2]的值在ssr的环境下是‘web-runtime-cjs,web-server-renderer’,在weex的环境下是‘weex’,在没有环境拓展参数的build下为undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
build(builds) // 将筛选后的builds对象传入build方法
function build (builds) {
let built = 0
const total = builds.length
const next = () => { // 定一个匿名函数,通过递归方式调用
buildEntry(builds[built]).then(() => { // 调用buildEntry方法,返回一个promise对象
built++
if (built < total) {
next()
}
}).catch(logError) // 调用logError,打印输出错误信息
}
next() // 调用
}

这段代码主要功能是通过递归的方式不断的调用buildEntry方法,该方法返回一个promise对象,之所以使用promise对象的原因是Rollup本身接收和返回的是promise对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function buildEntry (config) { // 接收builds数组中的一个对象
const output = config.output // 保存对象中的output的value
const { file, banner } = output // banner为scripts/config.js中定义的注释信息
const isProd = /(min|prod)\.js$/.test(file) // 正则匹配为min和prod的输出文件名
return rollup.rollup(config) // 将对象传入rollup,交给rollup进行打包
.then(bundle => bundle.generate(output))
.then(({ output: [{ code }] }) => {
if (isProd) { // 代码格式化形式
const minified = (banner ? banner + '\n' : '') + terser.minify(code, {
toplevel: true,
output: {
ascii_only: true
},
compress: {
pure_funcs: ['makeMap']
}
}).code
return write(file, minified, true) // 调用创建文件方法,创建文件方法再次不在赘述
} else {
return write(file, code)
}
})
}
scripts/config.js分析

scripts/build.js文件中我们调用了scripts/config.js这个方法,并且把数据保存在了builds的变量中,对于scripts/config.js文件的分析,会从getAllBuilds方法开始。

1
2
3
4
5
6
7
if (process.env.TARGET) { // 判断是否有TARGET,只有在dev环境下才有TARGET
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
// 通过枚举的形式将builds中的key名取出来形成新的数组,并对数组的每一项进行循环调用genConfig方法
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

上面这段代码通过判断启动参数来区分是否是编译环境,如果是编译环境的情况下,则把getAllBuilds映射出去。

1
2
3
4
5
6
7
8
9
10
const builds = {
'web-runtime-cjs-dev': {
entry: resolve('web/entry-runtime.js'), // 入口文件
dest: resolve('dist/vue.runtime.common.dev.js'), // 编译后的文件
format: 'cjs', // 编译文件的格式
env: 'development', // 指定环境
banner // 注释
}
......
}

这段代码是builds的基本结构,Object.keys(builds)获取到的值是[‘web-runtime-cjs-dev’, ……]这样的一个数组。之后对数据进行循环,去调用genConfig方法。

1
2
3
4
5
6
const banner =
'/*!\n' +
` * Vue.js v${version}\n` +
` * (c) 2014-${new Date().getFullYear()} Evan You\n` +
' * Released under the MIT License.\n' +
' */'

banner是一个注释,在打包编译的时候将其传入对应文件。

1
2
3
4
5
6
7
8
9
10
const resolve = p => {
const base = p.split('/')[0] // 对传入的路径进行截取形成新的数组,同时取得第一条数据,如weex、web、server等
if (aliases[base]) { // 判断aliases[base]是否为undefined
// 将aliases[base]目录和p路径从base.length + 1往后截取的字符串拼接成打包的entry入口的文件路径
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
// 为undefined的情况下将当前目录、返回上层目录和要输出的文件目录或者生成其它node包的目录传入path.resolve方法
return path.resolve(__dirname, '../', p)
}
}

通过上面的resolve这个方法已经对builds的结构进行了一系列的处理,并将新的结构返回给Object.keys(builds)中。

其中genConfig方法作用是将处理好的builds结构包装成符合Rollup打包渲染的结构,在这里就不再赘述。

scripts/alias.js分析

此文件主要用于包装出一个执行不同环境的命令时对应的源码相对路径,并且把相对路径包装成绝对路径。

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

runtime only and runtime compiler

基本介绍

说到构建过程,我们在vue官网能够看到两种构建方式,分别是runtime onlyruntime compiler。这两种构建方式在使用vue-cli创建一个项目的时候,会提供选择的操作,不过我们默认情况下推荐选择runtime only

二者区别
  • runtime only代表构建过程是在运行的时候,这种构建模式删除了模板编译的功能,因此无法支持带template属性的vue实例。使用此构建方式需要借助vue-loaderwebpackvueify等打包构建工具。
  • runtime compiler代表构建过程在运行的时候添加了编译器的功能,有了编译器就能够解析带有template属性的vue实例。
例子
1
2
3
4
5
6
7
8
9
10
11
// runtime compiler
new Vue({
template: '{{hello world!}}'
})
// runtime only
new Vue({
render(h) {
return h('div', 'hellow world!')
}
})

总结

到此一个完整的构建过程就已经介绍完了,构建过程主要就是在执行根目录下的scripts目录中的文件。

------本文结束,感谢您的阅读,如有问题请通过邮件方式联系作者------