到目前,Webpack已發(fā)布到v3.8.1,網(wǎng)上有很多Webpack入門到精通的教程及文檔,此文就從問答的角度梳理下Webpack的知識(shí)脈絡(luò)。
基礎(chǔ)篇
1. 為什么要使用Webpack?
在面對(duì)復(fù)雜的業(yè)務(wù)需求和前端性能優(yōu)化的場(chǎng)景時(shí),會(huì)存在以下需要解決的問題:
- 前端模塊化工具
- 異步模塊加載及管理
- 樣式(Style)預(yù)處理:Scss、Less、Postcss等
- 樣式(Style)后處理:autoprefixer、img資源內(nèi)聯(lián)、原子css、css module
- ES6語(yǔ)法、TypeScript、CoffeeScript支持
- 專有代碼處理:JSX、*.vue
- 外部依賴模塊:Handlerbars等模板、gzip、text、json等
- 公共模塊提取
- 開模模式自動(dòng)刷新瀏覽器
- 長(zhǎng)效緩存處理
- 代碼壓縮混淆
- ...
等等以上如果人肉處理則會(huì)相當(dāng)麻煩,按照Webpack的模塊打包思路,以上問題都能很好的解決。
2. 什么是Webpack?
Webpack是模塊打包器:它做的事情是,分析你的項(xiàng)目結(jié)構(gòu),找到JavaScript模塊以及其它的一些瀏覽器不能直接運(yùn)行的拓展語(yǔ)言(Scss,TypeScript等),并將其轉(zhuǎn)換和打包為合適的格式供瀏覽器使用。
3. Webpack和Browserify、Rollup、Gulp、Grunt、Seajs、Requirejs的區(qū)別?
以上工具的功能可分為兩類:
1. 任務(wù)管理器(Task Runner)
Gulp / Grunt是一種任務(wù)管理工具,能夠優(yōu)化前端工作流程。比如自動(dòng)刷新頁(yè)面、combo、壓縮css、js、編譯less等等。簡(jiǎn)單來(lái)說,就是使用Gulp/Grunt,然后配置你需要的插件,就可以把以前需要手工做的事情讓它幫你做了。
2. 模塊化解決方案
2.1 在線模塊方案
Seajs/Requirejs是一種在線"編譯" 模塊的方案,相當(dāng)于在頁(yè)面上加載一個(gè) CMD/AMD 解釋器。這樣瀏覽器就認(rèn)識(shí)了 define、exports、module 這些東西。也就實(shí)現(xiàn)了模塊化。
2.2 模塊打包器
Browserify / Webpack / Rollup是一個(gè)預(yù)編譯模塊的方案,相比于上面 ,這個(gè)方案更加智能。沒用過browserify,這里以webpack為例。首先,它是預(yù)編譯的,不需要在瀏覽器中加載解釋器。另外,你在本地直接寫JS,不管是 AMD / CMD / ES6 風(fēng)格的模塊化,它都能認(rèn)識(shí),并且編譯成瀏覽器認(rèn)識(shí)的JS。以上三個(gè)工具都能完成對(duì)lib庫(kù)的打包,React,Vue,Ember,Preact,D3,Three.js,Moment 以及其他許多知名的庫(kù)都使用 Rollup 。Rollup使用Tree Shaking技術(shù)精簡(jiǎn)Lib庫(kù),這個(gè)最新的Webpack也能做到,但是從最終的打包代碼中分析,Rollup做好的包會(huì)更純凈一些,性能更好一些。
4. Webpack的構(gòu)建思路?
一切都是模塊
就像JS文件可以視作“模塊”一樣,其他所有的一切(CSS,圖片,HTML)都可以被視作模塊。也就是說,你可以require("myJSfile.js")或者require("myCSSfile.css")。這意味著我們可以把任何靜態(tài)資源分割成可控的模塊,以供重復(fù)使用等不同的操作。
只加載“你需要的”和“你何時(shí)需要”的
典型的模塊加載器會(huì)把所有的模塊最終打包生成一個(gè)巨大的“bundle.js”文件。但在很多實(shí)際的項(xiàng)目當(dāng)中,這個(gè)“bundle.js”文件體積可能會(huì)達(dá)到10MB~15MB,并且會(huì)一直不停進(jìn)行加載!所以Webpack通過大量的特性去分割你的代碼,生成多個(gè)“bundle”片段,并且異步地加載項(xiàng)目的不同部分,因此只會(huì)為你加載“你需要的”和“你何時(shí)需要”的部分。
入門篇
入門主要是對(duì)Webpack配置的解讀及常見問題點(diǎn)解答。
1. 如何區(qū)分Webpack構(gòu)建形式(開發(fā)環(huán)境 VS 生產(chǎn)環(huán)境)?
按照Webpack文檔中說明的,使用配置對(duì)象編寫配置文檔,一般Webpack構(gòu)建分為兩個(gè)環(huán)境:開發(fā)、發(fā)布。因此需要三個(gè)配置:
-
webpack.base.config.js:基礎(chǔ)公共 -
webpack.dev.config.js:開發(fā)模式 -
webpack.prod.config.js:發(fā)布模式
通過webpack-merge將配置合并,使用pkg.script管理構(gòu)建任務(wù)。
為了在window和max上都能無(wú)縫兼容,這里使用了 cross-env 配置環(huán)境變量,例如:
"scripts": {
"clear": "rm -rf build&& mkdir build",
"start": "npm run clear&& cross-env NODE_ENV=development webpack-dev-server --host 0.0.0.0 --devtool eval --progress --color --profile",
"deploy": "npm run pre&& npm run clear&& cross-env NODE_ENV=production webpack -p --progress"
}
代碼中可使用如下方式判斷:
if (process.env.NODE_ENV === 'development') {
// ...
}
2. Entry在MPA下如何寫?
MPA是Muti Page Application的縮寫,指多頁(yè)應(yīng)用程序。這種需求常見于:
- 多頁(yè)靜態(tài)項(xiàng)目編寫
- 多頁(yè)后端渲染模板編寫
如果頁(yè)面不多,且模板路徑無(wú)規(guī)律的話,使用對(duì)象語(yǔ)法定義Entry配置,具體Entry配置參考文檔。
如果頁(yè)面結(jié)構(gòu)已約定,且頁(yè)面眾多,這里建議使用JS代碼獲取入口路徑,比如像這個(gè)項(xiàng)目express-hbs-webpack-demo,約定了chunkName和入口JSindex: .../views/index/main.js
| ├── views // 前端各個(gè)頁(yè)面
| | |── common // 公共Partials
| | |── layout.hbs
| | |── index // 主頁(yè)
| | | |── partials
| | | | |── content.hbs // 頁(yè)面Partials
| | | | |── xxxxxxxx.hbs // 其余頁(yè)面Partials
| | | | |── resource.ejs // webpack的 HTMLHtmlWebpackPlugin 插件需要這個(gè)模板
| | | | └── resource.hbs // 最終生成的hbs資源片段
| | | |── index.hbs // 頁(yè)面主結(jié)構(gòu)
| | | |── main.js // 頁(yè)面入口js,約定只能是index.js或者main.js
| | | └── style.less // 當(dāng)前頁(yè)面的樣式,支持scss/less/css等
| | └── xxxxx // 其余頁(yè)面
| └── app.js // 頁(yè)面公共部分,比如全局樣式及腳本,或者頁(yè)面初始化時(shí)的動(dòng)作
代碼示例如下(源碼):
/**
* read path of js entry files(for webpack entry)
* notice: the entry file name must be main.js or index.js, and only one exist
*/
exports.webpackEntry = function () {
var webpackEntry = {} // for webpack entry
glob.sync(`${config.viewsPath}/**/{main,index}.js`).forEach(function (entry) {
var tmp = entry.split('/').splice(-3)
var moduleName = tmp.slice(1, 2)[0]
// eg: entry -> {about: '/Users/xxx/xxx/express-here/client/views/about/main.js'}
webpackEntry[moduleName] = entry
})
return webpackEntry
4. 如何在移動(dòng)配置文件的同時(shí)不影響構(gòu)建結(jié)果?
因?yàn)樵谂渲弥行枰褂孟鄬?duì)路徑獲取某些文件/模塊的位置,因此當(dāng)移動(dòng)Webpack配置時(shí)會(huì)影響資源引用,這個(gè)很好處理,使用context即可,這使得你的配置獨(dú)立于 CWD(current working directory - 當(dāng)前執(zhí)行路徑)。
context: path.resolve(__dirname, "app")
4. 靜態(tài)資源托管到CDN時(shí),構(gòu)建時(shí)資源如何改寫?
需要看下文檔中對(duì)output.publicPath的說明。這里需要對(duì)三個(gè)常用屬性進(jìn)行解讀:
- filename:決定了每個(gè)輸出 bundle 的名稱
- path:這些 bundle 將寫入到 output.path 選項(xiàng)指定的目錄下。
- publicPath:將上面兩個(gè)準(zhǔn)備的路徑字符串前面加上外部路徑,比如某個(gè)CDN:
http://xx.xx.com/public/,此時(shí)原資源位置:js/xx.xxxxx.bundle.js-->http://xx.xx.com/public/js/xx.xxxxx.bundle.js
因此,增加publicPath屬性就可改寫資源名稱外部路徑。
5. 構(gòu)建Lib庫(kù)使用Rollup還是Webpack?
建議使用Rollup打包,編譯最終生成的模塊干凈清晰。
6. 引入模塊時(shí),rules的匹配順序是?
module是對(duì)import/require引入的資源進(jìn)行匹配處理的配置項(xiàng)。
module.noParse
首先,如果定義了module.noParse且匹配到時(shí),則不解析匹配的模塊,因此模塊內(nèi)部不應(yīng)該有:import, require, define 的調(diào)用,或任何其他導(dǎo)入機(jī)制,這樣做可以忽略大型的 library 可以提高構(gòu)建性能。比如:
noParse: /jquery|lodash/
// 從 webpack 3.0.0 開始
noParse: function(content) {
return /jquery|lodash/.test(content);
}
module.rules
其次,如果上述未匹配到,則進(jìn)行module.rules匹配。數(shù)組中對(duì)象可通過屬性:test, include, exclude 和 resource對(duì)資源匹配。
這里需要注意,Webpack執(zhí)行最后一次匹配到的rule配置。因此,rule配置順序影響構(gòu)建,無(wú)用的rule全部去掉。
7. 代碼中import/require找模塊的過程?
這個(gè)問題也是對(duì)“模塊解析(Module Resolution)”過程的提問。
Webpack使用enhanced-resolve來(lái)解析文件路徑。
1. 絕對(duì)路徑
不需要解析
2. 相對(duì)路徑
由當(dāng)前文件的上下文生成絕對(duì)路徑,之后轉(zhuǎn)到第一種情況。
3. 模塊路徑
import "module"; // 文件
import "module/lib/file"; // 文件夾
模塊將在 resolve.modules 中指定的所有目錄內(nèi)搜索。
- 在
resolve.alias查找有沒匹配到的別名,如果匹配到則替換路徑,繼續(xù)向下 - 如果路徑指向文件:
- 如果文件自帶拓展名,則直接引用
- 否則,使用
resolve.extensions作為拓展名解析
- 如果路徑指向文件夾:
- 如果文件夾中包含 package.json 文件,則按照順序查找
resolve.mainFields配置選項(xiàng)中指定的字段(默認(rèn)為index)。并且 package.json 中的第一個(gè)這樣的字段確定文件路徑。 - 如果 package.json 文件不存在或者 package.json 文件中的 main 字段沒有返回一個(gè)有效路徑,則按照順序查找
resolve.mainFields配置選項(xiàng)中指定的文件名,看是否能在import/require目錄下匹配到一個(gè)存在的文件名。 - 文件擴(kuò)展名通過
resolve.mainFields選項(xiàng)采用類似的方法進(jìn)行解析。
- 如果文件夾中包含 package.json 文件,則按照順序查找
8. 外部依賴如何處理?
比如公共模塊不希望打包到vendor中,而是通過CDN的方式引入的時(shí)候。這里參考下externals的配置。
配置:
externals: {
jquery: 'jQuery'
}
代碼中:
import $ from 'jquery';
說明:
jQuery 為<script>中的引入全局變量window.jQuery。jquery為代碼中的引入名稱。
9. Webpack管理的模塊如何掛載到window上?
參考這個(gè)loader:expose-loader。
8. 如何選擇SourceMap生成類型?
SourceMap 在devtool 屬性下設(shè)置:
- 開發(fā)模式時(shí),使用
source-map,他提供全套支持 - 發(fā)布模式時(shí),使用
cheap-module-eval-source-map,不影響構(gòu)建速度
進(jìn)階篇
1. 長(zhǎng)效緩存怎么處理?
原則
一般來(lái)說,我們發(fā)布到線上的資源都會(huì)再次進(jìn)行迭代開發(fā),或者增加新需求。為了保證用戶瀏覽的流暢性,服務(wù)端會(huì)對(duì)靜態(tài)資源進(jìn)行長(zhǎng)效緩存。這里我們希望:
- 新需求上線對(duì)緩存變更影響越小越好,最小化用戶本地更新,減少服務(wù)器請(qǐng)求帶寬
- 用戶本地緩存不能和新上線的資源產(chǎn)生沖突
- 資源和頁(yè)面在進(jìn)行升級(jí)時(shí),不能出現(xiàn)資源引用的問題(404)
傳統(tǒng)的方式使用資源名后面加query的方式處理緩存,比如:
var sourcePath = `http://xx.xx.com/public/js/index.js?time=${new Date().getTime()}`
var sourcePath = `http://xx.xx.com/public/js/index.js?v=${version()}`
類似的方式進(jìn)行防緩存處理,但是這樣的做法不便于管理,且頁(yè)面和資源部署先后會(huì)出現(xiàn)沖突。
因此,最暴力直接的方式是根據(jù)資源內(nèi)容改寫文件名的方式處理長(zhǎng)效緩存的問題。當(dāng)資源發(fā)生變化時(shí)修改資源名稱,不變則不處理。使用這種方式,可以先替換靜態(tài)資源,之后替換頁(yè)面,不會(huì)造成沖突。
以上的介紹建議閱讀這篇文章:用 webpack 實(shí)現(xiàn)持久化緩存。我這里總結(jié)最終執(zhí)行的部分。
Webpack解讀
Webpack使用內(nèi)建模塊系統(tǒng)管理模塊引用,為了保證模塊內(nèi)容穩(wěn)定,這里需要將webpack runtime/chunk清單(經(jīng)常改變)和模塊(相對(duì)固定)分離。
因此,長(zhǎng)效緩存的核心就是:生成穩(wěn)定的模塊及模塊ID,只有模塊內(nèi)容變動(dòng)才會(huì)改變模塊及模塊ID,只進(jìn)行必要的模塊及模塊ID變更。
措施
- 合理劃分公共模塊
- 提取vendor模塊
- 使用
CommonsChunkPlugin提取公共模塊 - 提取manifest,將模塊與webpack runtime/Manifest分離
- 使用chunkhash改寫js模塊的文件名, 使模塊唯一化
- 使用
HashedModuleIdsPlugin穩(wěn)定模塊ID - 使用
import()加載異步模塊 - 使用
inline-manifest-webpack-plugin將 manifest文件 inline到html中處理
2. 模塊熱替換的思路是?
3. 如何優(yōu)化構(gòu)建性能?
4. 如何在編譯后的代碼中添加作者信息, 在文件后面添加版權(quán)信息?
使用new webpack.BannerPlugin('版權(quán)所有,翻版必究')處理
(完)