本教程是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指令對Vue.js源碼進行打包的過程,除此以外,Vue.js還提供了另外兩種打包方式: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分鐘系列教程到此完結,對本教程有任何建議很是歡迎你們給我留言,教程內容較多,謝謝你們耐心看完。