大家好,我是若川。持續組織了6個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列
Rollup 是一個 JavaScript 模塊打包器,它將小塊的代碼編譯并合并成更大、更復雜的代碼,比如打包一個庫或應用程序。它使用的是 ES Modules 模塊化標準,而不是之前的模塊化方案,如 CommonJS 和 AMD。ES 模塊可以讓你自由、無縫地使用你最喜愛庫中那些最有用的獨立函數,而讓你的項目無需包含其他未使用的代碼。
近期在團隊內組織學習 Rollup 專題,在著重介紹了 Rollup 核心概念和插件的 Hooks 機制后,為了讓小伙伴們能夠深入了解 Rollup 在實際項目中的應用。我們就把目光轉向了優秀的開源項目,之后就選擇了尤大的 Vue/Vite/Vue3 項目,接下來本文將先介紹 Rollup 在 Vue 中的應用。
dev 命令
在 vue-2.6.14
項目根目錄下的 package.json
文件中,我們可以找到 scripts
字段,在該字段內定義了如何構建 Vue 項目的相關腳本。
{"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
相關的配置項:
-c
:指定rollup
打包的配置文件;-w
:開啟監聽模式,當文件發生變化的時候,會自動打包;--environment
:設置環境變量,設置后可以通過process.env
對象來獲取已配置的值。
由 dev
命令可知 rollup 的配置文件是 scripts/config.js
:
//?scripts/config.js
//?省略大部分代碼
if?(process.env.TARGET)?{module.exports?=?genConfig(process.env.TARGET)
}?else?{exports.getBuild?=?genConfigexports.getAllBuilds?=?()?=>?Object.keys(builds).map(genConfig)
}
觀察以上代碼可知,當 process.env.TARGET
有值的話,就會根據 TARGET
的值動態生成打包配置對象。
//?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
函數內部,會從 builds
對象中獲取當前目標對應的構建配置對象。當目標為 'web-full-dev'
時,它對應的配置對象如下所示:
//?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},
}
在每個構建配置對象中,會定義 entry
(入口文件)、dest
(輸出文件)、format
(輸出格式)等信息。當獲取構建配置對象后,就根據 rollup 的要求生成對應的配置對象。
需要注意的是,在 Vue 項目的根目錄中是沒有 web
目錄的,該項目的目錄結構如下所示:
├──?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 插件為地址取了個別名。具體的映射規則被定義在 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')
}
根據以上的映射規則,我們可以定位到 web
別名對應的路徑,該路徑對應的文件結構如下:
├──?compiler
├──?entry-compiler.js
├──?entry-runtime-with-compiler.js
├──?entry-runtime.js
├──?entry-server-basic-renderer.js
├──?entry-server-renderer.js
├──?runtime
├──?server
└──?util
到這里結合前面介紹的 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 應用程序執行 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?defaultbuilds?=?builds.filter(b?=>?{return?b.output.file.indexOf('weex')?===?-1})
}build(builds)
在 scripts/build.js
文件中,會先獲取所有的構建目標,然后根據進行過濾操作,最后再調用 build
函數進行構建操作,該函數的處理邏輯也很簡單,就是遍歷構建列表,然后調用 buildEntry
函數執行構建操作。
//?scripts/build.js
function?build?(builds)?{let?built?=?0const?total?=?builds.lengthconst?next?=?()?=>?{buildEntry(builds[built]).then(()?=>?{built++if?(built?<?total)?{next()}}).catch(logError)}next()
}
當 next
函數執行時,就會開始調用 buildEntry
函數,在該函數內部就是根據傳入了配置對象調用 rollup.rollup
API 進行構建操作:
//?scripts/build.js
function?buildEntry?(config)?{const?output?=?config.outputconst?{?file,?banner?}?=?outputconst?isProd?=?/(min|prod)\.js$/.test(file)return?rollup.rollup(config).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']}}).codereturn?write(file,?minified,?true)}?else?{return?write(file,?code)}})
}
當打包完成后,下一個環節就是生成文件。在 buildEntry
函數中是通過調用 write
函數來生成文件:
//?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
函數內部是通過 fs.writeFile
函數來生成文件,該函數還支持 zip
參數,用于輸出經過 gzip
壓縮后的大小。現在我們已經分析完了 dev
和 build
命令,最后我們來簡單介紹一下構建過程中所使用的一些核心插件。
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"
插件在前面我們已經知道它的作用了。而其他插件的作用如下:
rollup-plugin-buble:該插件使用 buble 轉換 ES2015 代碼,它已經被移到新的倉庫 @rollup/plugin-buble;
rollup-plugin-commonjs:該插件用于把 CommonJS 模塊轉換為 ES6 Modules,它已經移到新的倉庫 @rollup/plugin-commonjs;
rollup-plugin-flow-no-whitespace:該插件用于移除 flow types 中的空格;
rollup-plugin-node-resolve:該插件用于支持使用
node_modules
中第三方模塊,會使用 Node 模塊解析算法來定位模塊。它也被移動到新的倉庫 @rollup/plugin-node-resolve;rollup-plugin-replace:該插件用于在打包時執行字符串替換操作,它也被移動到新的倉庫 @rollup/plugin-replace。
除了以上的插件,在實際的項目中,你也可以使用 Rollup 官方倉庫提供的插件,來實現對應的功能,具體如下圖所示(僅包含部分插件):
(來源:https://github.com/rollup/plugins)
總結
本文只是簡單介紹了 Rollup 在 Vue 2 中的應用,很多細節并沒有展開介紹,感興趣的小伙伴可以自行學習一下。如果遇到問題的話,歡迎跟我一起交流哈。另外,你們也可以自行分析一下在 Vue 3 和 Vite 項目中是如何利用 Rollup 進行打包的。
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。分享、收藏、點贊、在看我的文章就是對我最大的支持~