本教程是rollup.js系列教程的最后一篇,我将基于Vue.js框架,深度分析Vue.js源码打包过程,让你们深刻理解复杂的前端框架是如何利用rollup.js进行打包的。经过这一篇教程的学习,相信你们能够更好地应用rollup.js为本身的项目服务。前端
要理解Vue.js
的打包源码,须要掌握如下知识点:vue
fs
模块:Node.js
内置模块,用于本地文件系统处理;path
模块:Node.js
内置模块,用于本地路径解析;buble
模块:用于ES6+
语法编译;flow
模块:用于Javascript
源码静态检查;zlib
模块:Node.js
内置模块,用于使用gzip
算法进行文件压缩;terser
模块:用于Javascript
代码压缩和美化。我将这些基础知识点整理成一篇前置学习教程:《10分钟快速精通rollup.js——前置学习之基础知识篇》,感兴趣的小伙伴能够看看。node
rollup.js
进阶教程中讲解了rollup.js
的部分经常使用插件:git
rollup-plugin-resolve
:集成外部模块代码;rollup-plugin-commonjs
:支持CommonJS
模块;rollup-plugin-babel
:编译ES6+
语法为ES2015
;rollup-plugin-json
:支持json
模块;rollup-plugin-uglify
:代码压缩(不支持ES
模块);为了理解Vue.js
的打包源码,咱们还须要学习如下rollup.js
插件及知识:github
rollup-plugin-buble
插件:编译ES6+
语法为ES2015
,无需配置,比babel
更轻量;rollup-plugin-alias
插件:替换模块路径中的别名;rollup-plugin-flow-no-whitespace
插件:去除flow
静态类型检查代码;rollup-plugin-replace
插件:替换代码中的变量为指定值;rollup-plugin-terser
插件:代码压缩,取代uglify
,支持ES
模块。intro
和outro
配置:在代码块内添加代码注释。我为还不熟悉这些插件的小伙伴准备了另外一篇前置学习教程:《10分钟快速精通rollup.js——前置学习之rollup.js插件篇》。web
Vue.js
的打包过程并不复杂,首先要将Vue.js
源码clone到本地:算法
git clone https://github.com/vuejs/vue.git
复制代码
安装依赖:npm
cd vue
npm i
复制代码
打开package.json查看scripts:json
"scripts": {
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",
}
复制代码
咱们先经过build指令进行打包:数组
$ npm run build
> vue@2.5.17-beta.0 build /Users/sam/WebstormProjects/vue
> node scripts/build.js
dist/vue.runtime.common.js 209.20kb
dist/vue.common.js 288.22kb
dist/vue.runtime.esm.js 209.18kb
dist/vue.esm.js 288.20kb
dist/vue.runtime.js 219.55kb
dist/vue.runtime.min.js 60.24kb (gzipped: 21.62kb)
dist/vue.js 302.27kb
dist/vue.min.js 85.19kb (gzipped: 30.86kb)
packages/vue-template-compiler/build.js 121.88kb
packages/vue-template-compiler/browser.js 228.17kb
packages/vue-server-renderer/build.js 220.73kb
packages/vue-server-renderer/basic.js 304.00kb
packages/vue-server-renderer/server-plugin.js 2.92kb
packages/vue-server-renderer/client-plugin.js 3.03kb
复制代码
打包成功后会在dist目录下建立下列打包文件:
build:ssr
和
build:weex
,先尝试
build:ssr
指令:
$ npm run build:ssr
> vue@2.5.17-beta.0 build:ssr /Users/sam/WebstormProjects/vue
> npm run build -- web-runtime-cjs,web-server-renderer
> vue@2.5.17-beta.0 build /Users/sam/WebstormProjects/vue
> node scripts/build.js "web-runtime-cjs,web-server-renderer"
dist/vue.runtime.common.js 209.20kb
packages/vue-server-renderer/build.js 220.73kb
packages/vue-server-renderer/basic.js 304.00kb
packages/vue-server-renderer/server-plugin.js 2.92kb
packages/vue-server-renderer/client-plugin.js 3.03kb
复制代码
再尝试build:weex
:
$ npm run build:weex
> vue@2.5.17-beta.0 build:weex /Users/sam/WebstormProjects/vue
> npm run build -- weex
> vue@2.5.17-beta.0 build /Users/sam/WebstormProjects/vue
> node scripts/build.js "weex"
packages/weex-vue-framework/factory.js 193.79kb
packages/weex-vue-framework/index.js 5.68kb
packages/weex-template-compiler/build.js 109.11kb
复制代码
经过命令行日志能够看出这两个指令和build指令没有本质区别,都是经过node
执行scripts/build.js源码,只是附带的参数不一样:
node scripts/build.js # build
node scripts/build.js "web-runtime-cjs,web-server-renderer" # build:ssr
node scripts/build.js "weex" # build:weex
复制代码
可见scripts/build.js是解读Vue.js
源码打包的关键。下面咱们就来分析Vue.js
的源码打包流程。
Vue.js
源码打包基于rollup.js
的API,大体可分为五步,以下图所示:
rollup
配置文件。经过scripts/config.js生成rollup
的配置文件;rollup
配置文件过滤。根据传入的参数,对rollup
配置文件的内容进行过滤,排除没必要要的打包项目。rollup
的API进行打包,并生成打包后的源码。terser
进行最小化压缩并经过zlib
进行gzip压缩测试,并在控制台输出测试结果,最后将源码内容输出到指定文件中,完成打包。下面咱们将深刻Vue.js
打包源码,解析打包的原理和细节。
友情提示:建议阅读源码以前先将以前提供的四份教程所有看完:
执行npm run build
时,会从scripts/build.js开始执行:
// scripts/build.js
const fs = require('fs')
const path = require('path')
const zlib = require('zlib')
const rollup = require('rollup')
const terser = require('terser')
if (!fs.existsSync('dist')) {
fs.mkdirSync('dist')
}
复制代码
前5行分别导入了5个模块,这5个模块的用途在前置学习教程中已经详细过。第7行经过同步方法判断dist目录是否存在,若是不存在则经过同步方法建立dist目录。
生成dist目录后,经过如下代码生成了rollup
的配置文件:
// scripts/build.js
let builds = require('./config').getAllBuilds()
复制代码
代码虽然只有短短一句,可是作了不少事情。首先它加载了scripts/config.js模块,而后调用其中的getAllBuilds()
方法。下面咱们来分析scripts/config.js的加载过程,加载config.js时先执行了如下内容:
// scripts/config.js
const path = require('path')
const buble = require('rollup-plugin-buble')
const alias = require('rollup-plugin-alias')
const cjs = require('rollup-plugin-commonjs')
const replace = require('rollup-plugin-replace')
const node = require('rollup-plugin-node-resolve')
const flow = require('rollup-plugin-flow-no-whitespace')
复制代码
这些插件的用途和用法在进阶教程和前置教程中都有介绍。
const version = process.env.VERSION || require('../package.json').version
const weexVersion = process.env.WEEX_VERSION || require('../packages/weex-vue-framework/package.json').version
复制代码
上述代码是从package.json中获取Vue
的版本号和Weex
的版本号。
const banner =
'/*!\n' +
` * Vue.js v${version}\n` +
` * (c) 2014-${new Date().getFullYear()} Evan You\n` +
' * Released under the MIT License.\n' +
' */'
复制代码
上述代码生成了banner文本,在Vue
代码打包后,会写在文件顶部。
const weexFactoryPlugin = {
intro () {
return 'module.exports = function weexFactory (exports, document) {'
},
outro () {
return '}'
}
}
复制代码
上述代码仅用于打包weex-factory
源码时使用:
// Weex runtime factory
'weex-factory': {
weex: true,
entry: resolve('weex/entry-runtime-factory.js'),
dest: resolve('packages/weex-vue-framework/factory.js'),
format: 'cjs',
plugins: [weexFactoryPlugin]
}
复制代码
接下来导入了scripts/alias.js模块:
const aliases = require('./alias')
复制代码
alias.js模块输出了一个对象,这个对象中定义了全部的别名及其对应的绝对路径:
// scripts/alias.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'),
entries: resolve('src/entries'),
sfc: resolve('src/sfc')
}
复制代码
这个模块中定义了resolve()
方法,用于生成绝对路径:
const resolve = p => path.resolve(__dirname, '../', p)
复制代码
__dirname
为当前模块对应的路径,即scripts/
目录,../
表示上一级目录,即项目的根目录,而后经过path.resolve()
方法将项目的根目录与传入的相对路径结合起来造成最终结果。回到scripts/config.js模块,咱们继续向下执行:
// scripts/config.js
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)
}
}
复制代码
config.js也定义了一个resolve()
方法,该方法接收一个路径参数p,假设p为web/entry-runtime.js
,则第一步获取的base为web,而后到alias模块输出的对象aliases中寻找对应的别名是否存在,web模块对应的别名是存在的,它的值为:
web: resolve('src/platforms/web')
复制代码
因此会将别名的实际路径与文件名进行拼接,获取文件的真实路径。文件名的获取方法是:
p.slice(base.length + 1)
复制代码
若是传入的路径为:dist/vue.runtime.common.js
,则会查找别名dist,该别名是不存在的,因此会执行另一条路径,将项目根路径与传入的参数路径进行拼接,即执行下面这段代码:
return path.resolve(__dirname, '../', p)
复制代码
这与scripts/alias.js模块的实现是相似的。接下来config.js模块中定义了builds变量,代码节选以下:
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
}
}
复制代码
这个变量中调用resolve()
方法生成了文件的真实路径,因为配置项采用的是rollup.js
老版本的配置名称,在新版本中已经被废弃,因此紧接着config.js模块又定义了一个genConfig(name)
方法来解决这个问题:
function genConfig (name) {
const opts = builds[name]
const config = {
input: opts.entry,
external: opts.external,
plugins: [
replace({
__WEEX__: !!opts.weex,
__WEEX_VERSION__: weexVersion,
__VERSION__: version
}),
flow(),
buble(),
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)
}
}
}
if (opts.env) {
config.plugins.push(replace({
'process.env.NODE_ENV': JSON.stringify(opts.env)
}))
}
Object.defineProperty(config, '_name', {
enumerable: false,
value: name
})
return config
}
复制代码
这个方法的用途是将老版本的rollup.js
配置转为新版本的格式。对于插件部分,每个打包项目都会采用replace
、flow
、buble
和alias
插件,其他自定义的插件会合并到plugins中,经过如下代码实现:
plugins: [].concat(opts.plugins || []),
复制代码
genConfig()
方法还判断了环境变量NODE_ENV
是否须要被替换:
if (opts.env) {
config.plugins.push(replace({
'process.env.NODE_ENV': JSON.stringify(opts.env)
}))
}
复制代码
上述代码判断了传入的opts中是否存在env参数,若是存在,则会将代码中的process.env.NODE_ENV
部分替换为JSON.stringify(opts.env)
: ,如传入的env值为development,则生成的结果为带双引号的development
"development"
复制代码
除此以外,genConfig()
方法还将builds对象的key保存在config对象中:
Object.defineProperty(config, '_name', {
enumerable: false,
value: name
})
复制代码
若是builds的key为web-runtime-cjs
,则生成的config为:
config = {
'_name': 'web-runtime-cjs'
}
复制代码
最后config.js模块定义了getAllBuilds()
方法:
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
复制代码
该方法首先判断环境变量TARGET是否认义,在build的三种方法中没有定义TARGET环境变量,因此会执行else中的逻辑,else逻辑中会暴露一个getBuild()
方法和getAllBuilds()
方法,getAllBuilds()
方法会获取builds对象的key数组,进行遍历并调用genConfig()
方法生成配置对象,这样rollup
的配置就生成了。
咱们回到scripts/build.js模块,配置生成完毕后,将对配置项进行过滤,由于每一种打包模式都将输出不一样的结果,过滤部分的源码以下:
// scripts/build.js
// 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命令,该命令实际执行指令为:
node scripts/build.js
复制代码
因此process.argv的内容为:
[ '/Users/sam/.nvm/versions/node/v11.2.0/bin/node',
'/Users/sam/WebstormProjects/vue/scripts/build.js' ]
复制代码
不存在process.argv[2],因此会执行else中的内容:
builds = builds.filter(b => {
return b.output.file.indexOf('weex') === -1
})
复制代码
这段代码的用途是排除weex
的代码打包,经过output.file是否包含weex
字符串判断是否为weex
代码。build:ssr
命令实际执行指令为:
node scripts/build.js "web-runtime-cjs,web-server-renderer"
复制代码
此时process.argv的值为:
[ '/Users/sam/.nvm/versions/node/v11.2.0/bin/node',
'/Users/sam/WebstormProjects/vue/scripts/build.js',
'web-runtime-cjs,web-server-renderer' ]
复制代码
process.argv[2]的值为web-runtime-cjs,web-server-renderer
,因此会执行if中的逻辑:
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)
复制代码
这个方法首先将参数经过逗号分隔为一个filters数组,而后遍历builds数组,寻找output.file或_name中任一个包含filters中任一个的配置项。好比filters的第一个元素为:web-runtime-cjs,则会寻找output.file或_name中包含web-runtime-cjs
的配置项,_name以前分析过,它指向配置项的key,此时会找到下面的配置项符合条件:
'web-runtime-cjs': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.js'),
format: 'cjs',
banner
}
复制代码
那么该配置就会被保留,并最终被打包。
配置过滤完以后就会调用打包函数:
build(builds)
复制代码
build函数定义以下:
function build (builds) {
let built = 0 // 当前打包项序号
const total = builds.length // 须要打包的总次数
const next = () => {
buildEntry(builds[built]).then(() => {
built++ // 打包完成后序号加1
if (built < total) {
next() // 若是打包序号小于打包总次数,则继续执行next()函数
}
}).catch(logError) // 输出错误信息
}
next() // 调用next()函数
}
复制代码
build()
函数接收builds参数,进行遍历,并调用buildEntry()
函数执行实际的打包逻辑,buildEntry()
函数返回一个Promise对象,若是出错,会调用logError(e)
函数打印报错信息:
function logError (e) {
console.log(e)
}
复制代码
打包的核心函数是buildEntry(config)
function buildEntry (config) {
const output = config.output // 获取config的output配置项
const { file, banner } = output // 获取output中的file和banner
const isProd = /min\.js$/.test(file) // 判断file中是否以min.js结尾,若是是则标记isProd为true
return rollup.rollup(config) // 执行rollup打包
.then(bundle => bundle.generate(output)) // 将打包的结果生成源码
.then(({ code }) => { // 获取打包生成的源码
if (isProd) { // 判断是否为isProd
const minified = (banner ? banner + '\n' : '') + terser.minify(code, { // 执行代码最小化打包,并在代码标题处手动添加banner,由于最小化打包会致使注释被删除
output: {
ascii_only: true // 只支持ascii字符
},
compress: {
pure_funcs: ['makeMap'] // 过滤makeMap函数
}
}).code // 获取最小化打包的代码
return write(file, minified, true) // 将代码写入输出路径
} else {
return write(file, code) // 将代码写入输出路径
}
})
}
复制代码
若是理解了rollup
的原理及terser
的使用方法,理解上述代码并不难,这里与咱们以前使用rollup
打包不一样之处在于采用了手动添加banner注释和手动输出代码文件,而以前都是rollup
自动输出。以前咱们采用的方法为:
const bundle = await rollup.rollup(input) // 获取打包对象bundle
bundle.write(output) // 将打包对象输出到文件
复制代码
而Vue.js
采用的方法是:
const bundle = await rollup.rollup(input) // 获取打包对象bundle
const { code, map } = await bundle.generate(output) // 根据bundle生成源码和source map
复制代码
经过bundle获取源码,而后手动输出到文件中。
源码输出主要是调用write()
函数,这里须要提供3个参数:
bundle.generate()
获取;function write (dest, code, zip) {
return new Promise((resolve, reject) => {
function report (extra) { // 输出日志函数
console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || '')) // 打印文件名称、文件容量和gzip压缩测试结果
resolve()
}
fs.writeFile(dest, code, err => {
if (err) return reject(err) // 若是报错则直接调用reject()方法
if (zip) { // 若是isProd则进行gzip测试
zlib.gzip(code, (err, zipped) => { // 经过gzip对源码进行压缩测试
if (err) return reject(err)
report(' (gzipped: ' + getSize(zipped) + ')') // 测试成功后获取gzip字符串长度并输出gizp容量
})
} else {
report() // 输出日志
}
})
})
}
复制代码
这里有几个细节须要注意,第一是获取当前命令行路径到最终生成文件的相对路径:
path.relative(process.cwd(), dest)
复制代码
第二是调用blue()
函数生成命令行蓝色的文本:
function blue (str) {
return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m'
}
复制代码
第三是获取文件容量的方法:
function getSize (code) {
return (code.length / 1024).toFixed(2) + 'kb'
}
复制代码
这三个方法不难理解,可是都很是实用,你们在开发过程当中能够多多借鉴。
你们能够发现当咱们具有了基础知识后,再分析Vue.js
的源码打包过程并不复杂,因此建议你们工做中能够借鉴这种学习方式,将基础知识点先抽离出来,单独搞明白后再攻克复杂的源码。rollup.js
10分钟系列教程到此完结,对本教程有任何建议很是欢迎你们给我留言,教程内容较多,谢谢你们耐心看完。