【Vuejs】1241- Vue 是如何用 Rollup 打包的?
Rollup 是一個 JavaScript 模塊打包器,它將小塊的代碼編譯并合并成更大、更復(fù)雜的代碼,比如打包一個庫或應(yīng)用程序。它使用的是 ES Modules 模塊化標準,而不是之前的模塊化方案,如 CommonJS 和 AMD。ES 模塊可以讓你自由、無縫地使用你最喜愛庫中那些最有用的獨立函數(shù),而讓你的項目無需包含其他未使用的代碼。
近期在團隊內(nèi)組織學(xué)習(xí) Rollup 專題,在著重介紹了 Rollup 核心概念和插件的 Hooks 機制后,為了讓小伙伴們能夠深入了解 Rollup 在實際項目中的應(yīng)用。我們就把目光轉(zhuǎn)向了優(yōu)秀的開源項目,之后就選擇了尤大的 Vue/Vite/Vue3 項目,接下來本文將先介紹 Rollup 在 Vue 中的應(yīng)用。
dev 命令
在 vue-2.6.14 項目根目錄下的 package.json 文件中,我們可以找到 scripts 字段,在該字段內(nèi)定義了如何構(gòu)建 Vue 項目的相關(guān)腳本。
{
??"name":?"vue",
??"version":?"2.6.14",
??"sideEffects":?false,
??"scripts":?{
????"dev":?"rollup?-w?-c?scripts/config.js?--environment?TARGET:web-full-dev",
????"dev:cjs":?"rollup?-w?-c?scripts/config.js?--environment?TARGET:web-runtime-cjs-dev",
????...
}
這里我們以 dev 命令為例,來介紹一下與 rollup 相關(guān)的配置項:
-c:指定rollup打包的配置文件;-w:開啟監(jiān)聽模式,當文件發(fā)生變化的時候,會自動打包;--environment:設(shè)置環(huán)境變量,設(shè)置后可以通過process.env對象來獲取已配置的值。
由 dev 命令可知 rollup 的配置文件是 scripts/config.js:
//?scripts/config.js
//?省略大部分代碼
if?(process.env.TARGET)?{
??module.exports?=?genConfig(process.env.TARGET)
}?else?{
??exports.getBuild?=?genConfig
??exports.getAllBuilds?=?()?=>?Object.keys(builds).map(genConfig)
}
觀察以上代碼可知,當 process.env.TARGET 有值的話,就會根據(jù) TARGET 的值動態(tài)生成打包配置對象。
//?scripts/config.js
function?genConfig?(name)?{
??const?opts?=?builds[name]
??const?config?=?{
????input:?opts.entry,
????external:?opts.external,
????plugins:?[
??????flow(),
??????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)
??????}
????}
??}
??//?省略部分代碼
??return?config
}
在 genConfig 函數(shù)內(nèi)部,會從 builds 對象中獲取當前目標對應(yīng)的構(gòu)建配置對象。當目標為 'web-full-dev' 時,它對應(yīng)的配置對象如下所示:
//?scripts/config.js
const?builds?={?
??'web-runtime-cjs-dev':?{?...?},
??'web-runtime-cjs-prod':?{?...?},
??//?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
??},
}
在每個構(gòu)建配置對象中,會定義 entry(入口文件)、dest (輸出文件)、format(輸出格式)等信息。當獲取構(gòu)建配置對象后,就根據(jù) rollup 的要求生成對應(yīng)的配置對象。
需要注意的是,在 Vue 項目的根目錄中是沒有 web 目錄的,該項目的目錄結(jié)構(gòu)如下所示:
├──?BACKERS.md
├──?LICENSE
├──?README.md
├──?benchmarks
├──?dist
├──?examples
├──?flow
├──?package.json
├──?packages
├──?scripts
├──?src
├──?test
├──?types
└──?yarn.lock
那么 web/entry-runtime-with-compiler.js 入口文件的位置在哪呢?其實是利用了 rollup 的 @rollup/plugin-alias 插件為地址取了個別名。具體的映射規(guī)則被定義在 scripts/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'),
??sfc:?resolve('src/sfc')
}
根據(jù)以上的映射規(guī)則,我們可以定位到 web 別名對應(yīng)的路徑,該路徑對應(yīng)的文件結(jié)構(gòu)如下:
├──?compiler
├──?entry-compiler.js
├──?entry-runtime-with-compiler.js
├──?entry-runtime.js
├──?entry-server-basic-renderer.js
├──?entry-server-renderer.js
├──?runtime
├──?server
└──?util
到這里結(jié)合前面介紹的 builds 對象,相信你也知道了 Vue 是如何打包不同類型的文件,以滿足不同場景的需求,比如含有編譯器和不包含編譯器的版本。分析完 dev 命令的處理流程,下面我來分析 build 命令。
build 命令
同樣,在根目錄下 package.json 的 scripts 字段,我們可以找到 build 命令的定義:
{
??"name":?"vue",
??"version":?"2.6.14",
??"sideEffects":?false,
??"scripts":?{
????"build":?"node?scripts/build.js",
????...
}
當你運行 build 命令時,會使用 node 應(yīng)用程序執(zhí)行 scripts/build.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)
在 scripts/build.js 文件中,會先獲取所有的構(gòu)建目標,然后根據(jù)進行過濾操作,最后再調(diào)用 build 函數(shù)進行構(gòu)建操作,該函數(shù)的處理邏輯也很簡單,就是遍歷構(gòu)建列表,然后調(diào)用 buildEntry 函數(shù)執(zhí)行構(gòu)建操作。
//?scripts/build.js
function?build?(builds)?{
??let?built?=?0
??const?total?=?builds.length
??const?next?=?()?=>?{
????buildEntry(builds[built]).then(()?=>?{
??????built++
??????if?(built?????????next()
??????}
????}).catch(logError)
??}
??next()
}
當 next 函數(shù)執(zhí)行時,就會開始調(diào)用 buildEntry 函數(shù),在該函數(shù)內(nèi)部就是根據(jù)傳入了配置對象調(diào)用 rollup.rollup API 進行構(gòu)建操作:
//?scripts/build.js
function?buildEntry?(config)?{
??const?output?=?config.output
??const?{?file,?banner?}?=?output
??const?isProd?=?/(min|prod)\.js$/.test(file)
??return?rollup.rollup(config)
????.then(bundle?=>?bundle.generate(output))
????.then(({?output:?[{?code?}]?})?=>?{
??????if?(isProd)?{?//?若為正式環(huán)境,則進行壓縮操作
????????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)
??????}
????})
}
當打包完成后,下一個環(huán)節(jié)就是生成文件。在 buildEntry 函數(shù)中是通過調(diào)用 write 函數(shù)來生成文件:
//?scripts/build.js
const?fs?=?require('fs')
function?write?(dest,?code,?zip)?{
??return?new?Promise((resolve,?reject)?=>?{
????function?report?(extra)?{
??????console.log(blue(path.relative(process.cwd(),?dest))?
?????+?'?'?+?getSize(code)?+?(extra?||?''))
??????resolve()
????}
????fs.writeFile(dest,?code,?err?=>?{
??????if?(err)?return?reject(err)
??????if?(zip)?{
????????zlib.gzip(code,?(err,?zipped)?=>?{
??????????if?(err)?return?reject(err)
??????????report('?(gzipped:?'?+?getSize(zipped)?+?')')
????????})
??????}?else?{
????????report()
??????}
????})
??})
}
write 函數(shù)內(nèi)部是通過 fs.writeFile 函數(shù)來生成文件,該函數(shù)還支持 zip 參數(shù),用于輸出經(jīng)過 gzip 壓縮后的大小?,F(xiàn)在我們已經(jīng)分析完了 dev 和 build 命令,最后我們來簡單介紹一下構(gòu)建過程中所使用的一些核心插件。
rollup 插件
在 package.json ?文件中,我們可以看到 Vue2 項目中用到的 rollup 插件:
//?package.json
{
??"name":?"vue",
??"version":?"2.6.14",
??"devDependencies":?{
????"rollup-plugin-alias":?"^1.3.1",
????"rollup-plugin-buble":?"^0.19.6",
????"rollup-plugin-commonjs":?"^9.2.0",
????"rollup-plugin-flow-no-whitespace":?"^1.0.0",
????"rollup-plugin-node-resolve":?"^4.0.0",
????"rollup-plugin-replace":?"^2.0.0",
??}
}
其中,"rollup-plugin-alias" 插件在前面我們已經(jīng)知道它的作用了。而其他插件的作用如下:
rollup-plugin-buble:該插件使用 buble 轉(zhuǎn)換 ES2015 代碼,它已經(jīng)被移到新的倉庫 @rollup/plugin-buble; rollup-plugin-commonjs:該插件用于把 CommonJS 模塊轉(zhuǎn)換為 ES6 Modules,它已經(jīng)移到新的倉庫 @rollup/plugin-commonjs; rollup-plugin-flow-no-whitespace:該插件用于移除 flow types 中的空格; rollup-plugin-node-resolve:該插件用于支持使用 node_modules中第三方模塊,會使用 Node 模塊解析算法來定位模塊。它也被移動到新的倉庫 @rollup/plugin-node-resolve;rollup-plugin-replace:該插件用于在打包時執(zhí)行字符串替換操作,它也被移動到新的倉庫 @rollup/plugin-replace。
除了以上的插件,在實際的項目中,你也可以使用 Rollup 官方倉庫提供的插件,來實現(xiàn)對應(yīng)的功能,具體如下圖所示(僅包含部分插件):

(來源:https://github.com/rollup/plugins)
總結(jié)
本文只是簡單介紹了 Rollup 在 Vue 2 中的應(yīng)用,很多細節(jié)并沒有展開介紹,感興趣的小伙伴可以自行學(xué)習(xí)一下。如果遇到問題的話,歡迎跟我一起交流哈。另外,你們也可以自行分析一下在 Vue 3 和 Vite 項目中是如何利用 Rollup 進行打包的。

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點擊“閱讀原文”查看 130+ 篇原創(chuàng)文章
