這篇是我看《webpack實(shí)戰(zhàn) 入門、進(jìn)階與調(diào)優(yōu)》這本書的一個(gè)筆記,也相應(yīng)擴(kuò)充了部分內(nèi)容,可以算是給沒(méi)讀過(guò)的人做個(gè)引子。這本書比較系統(tǒng)地介紹了webpack的基礎(chǔ),閱讀量也不大,讓我弄清楚了很多以前模糊的點(diǎn)。
1、安裝webpack
安裝webpack建議本地安裝(不使用全局),因?yàn)槿职惭b的話項(xiàng)目在不同機(jī)器下可能出現(xiàn)版本不一(本地安裝能保證團(tuán)隊(duì)的版本一致),并且使用時(shí)可能出現(xiàn)本地和全局webpack版本混亂的情況。所以干脆就本地安裝。
安裝:npm i webpack webpack-cli --save-dev
webpack為核心庫(kù),webpack-cli是命令行工具。
由于是安裝于本地,所以可以使用 npx webpack 來(lái)使用。(npx是nodejs自帶的自動(dòng)執(zhí)行本地模塊的一個(gè)命令,具體可以參考npx 使用教程;
2、JS的模塊管理
在es6 module未成為標(biāo)準(zhǔn)前,有2個(gè)比較多人使用的模塊管理方案:AMD和commonJS。這兩者都是通過(guò)編譯后生成runtime,在代碼運(yùn)行過(guò)程中動(dòng)態(tài)引入。目前commonJS是nodejs的模塊管理標(biāo)準(zhǔn)。
- AMD(Asynchronous Module Definition 異步模塊定義):通過(guò)聲明回調(diào)函數(shù)異步加載模塊,將其他模塊以依賴注入的方式加入進(jìn)當(dāng)前模塊,由于是異步加載所以不會(huì)阻塞當(dāng)前模塊加載。(參考:RequireJS和AMD規(guī)范;
// foo.js export
define({
method1: function() {},
method2: function() {},
});
// import
require(['foo'], function ( foo ) {
foo.method1();
});
- commonJS:通過(guò)一個(gè)模塊對(duì)象引出、引入其他模塊,引入的值為拷貝值,相較AMD語(yǔ)法更簡(jiǎn)單方便;
// export
module.exports = {name: 123}
// import
var name = require('./export.js').name
- es6 module:通過(guò)語(yǔ)法靜態(tài)引出、引入其他模塊,是在編譯期間進(jìn)行,引入的值只讀的變量映射(因此可以解決循環(huán)引用的問(wèn)題,通過(guò)包裹一個(gè)函數(shù)的方式,參考:JavaScript 模塊的循環(huán)加載),所以原始值改變會(huì)影響到引用者。因?yàn)槭庆o態(tài)引用,所以不像前兩者可以將import語(yǔ)句寫在任意地方,如if判斷內(nèi),需要在編譯過(guò)程就確定是否引入。
// export
export const a = 123;
// import
import {a} from './export'
-
三個(gè)模塊引入的方式,都在第一次引入時(shí)將該目標(biāo)模塊代碼執(zhí)行一遍。
PS:因?yàn)榍皟烧卟皇枪俜綐?biāo)準(zhǔn),所以都需要借助webpack等模塊打包工具進(jìn)行編譯打包后才可以在瀏覽器上運(yùn)行,而es6 module可以直接在瀏覽器上運(yùn)行,只需要把script標(biāo)簽的type設(shè)置為module。但如果你在本地想試驗(yàn)一下es6 module則會(huì)發(fā)現(xiàn),本地引入模塊時(shí)用的是file協(xié)議,因?yàn)樵跒g覽器引入js資源時(shí)需要域名、協(xié)議、端口一致,所以在file協(xié)議下沒(méi)有域名會(huì)觸發(fā)跨域限制(chrome、firefox會(huì),ie不會(huì)),導(dǎo)致引入失敗。 - UMD(Universal Module Definition 通用模塊標(biāo)準(zhǔn)):這不是一個(gè)模塊管理方案,是一個(gè)統(tǒng)一所有模塊管理方案的解決。webpack中則應(yīng)用了這個(gè)方案。由于會(huì)通過(guò)全局是否有define函數(shù)來(lái)判斷AMD環(huán)境,但在AMD的規(guī)則下是無(wú)法使用commonJS和es6 module的,所以如果項(xiàng)目全使用commonJS卻因?yàn)槟承┰虺霈F(xiàn)了define函數(shù),則可能導(dǎo)致全部模塊失效,需要手動(dòng)去修改webpack UMD模塊的判斷順序。
/*
UMD判斷模塊管理方案的源碼
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['b'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require('b'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.b);
}
}(this, function (b) {
//use b in some fashion.
// Just return a value to define the module export.
// This example returns an object, but the module
// can return a function as the exported value.
return {};
}));
3、chunk、entry、bundle的基本概念
chunk:打包的模塊
entry:打包的入口文件
bundle:每個(gè)模塊打包好后的文件1
圖片來(lái)自《Webpack實(shí)戰(zhàn):入門、進(jìn)階與調(diào)優(yōu)》
4、配置資源入口
// webpack.config.js
module.exports = {
/*
* context配置entry的路徑前綴,可以理解為入口的文件上下文,所以是絕對(duì)路徑,這樣在多個(gè)entry的時(shí)候?qū)懫饋?lái)比較方便
*/
context: path.join(__dirname, './src'),
/*
* 打包入口文件
* 入口可為多個(gè),entry的值可以是數(shù)組、字典,用函數(shù)或promise返回這兩種數(shù)據(jù)結(jié)構(gòu)也可以
*/
entry: './index.js'
}
5、提取公共模塊
像loadsh、jquery這些第三方的庫(kù),如果都跟業(yè)務(wù)代碼一起打進(jìn)一個(gè)bundle文件就會(huì)很大,并且每次代碼更新都需要更新整個(gè)文件。這時(shí)候可將一些公共模塊抽出來(lái),就不用跟業(yè)務(wù)代碼混雜在一起了。
module.exports = {
entry: {
app: './index.js', // 主入口
vender: ['react', 'lodash', 'jquery'] // vender是‘提供商’的意思,這里理解為第三方模塊
}
}
6、配置資源出口
資源出口的配置在output對(duì)象種配置。
module.exports = {
entry: './src/app.js',
output: {
// bundle文件名
filename: 'bundle.js',
// bundle導(dǎo)出路徑
path: path.join(__dirname, 'assets'),
// 資源訪問(wèn)上下文
publicPath: '/dist/',
},
};
output詳解:
- filename的書寫形式:
// 直接寫bundle名
filename: 'bundle.js'
// 相對(duì)路徑,webpack會(huì)自動(dòng)幫你創(chuàng)建src文件夾
filename: './src/bundle.js'
// 動(dòng)態(tài)指定文件名,具體看下圖
filename: '[name].js'
圖片來(lái)自《Webpack實(shí)戰(zhàn):入門、進(jìn)階與調(diào)優(yōu)》
- path:打包資源輸出路徑,默認(rèn)為項(xiàng)目根目錄的dist文件夾,需要配置絕對(duì)路徑;
- publicPath:資源的請(qǐng)求位置,html直接請(qǐng)求的資源如sript標(biāo)簽里的js,和js或css的間接請(qǐng)求的資源資源如引入模塊和css加載背景/圖,這些都屬于資源請(qǐng)求,他們的路徑都會(huì)在前面加上publicPath;
// 相對(duì)路徑則會(huì)從當(dāng)前請(qǐng)求的文件路徑開始銜接
publicPath: './js'
// 在app目錄下的html直接請(qǐng)求的資源index.js => www.example.com/app/js/index.js
// 以 / 開頭,則直接從域名后開始銜接
publicPath: '/js'
// 請(qǐng)求資源index.js => www.example.com/js/index.js
// 絕對(duì)路徑,一般用CDN的場(chǎng)景
publicPath: 'www.cdn.com/js'
// 請(qǐng)求資源index.js => www.cdn.com/js/index.js
- output.path建議跟devServer里面的publicPath保持一致,這樣在開發(fā)和生產(chǎn)環(huán)境才不會(huì)搞混,具體原因可以看2、webpack-dev-server的解釋;
7、webpack模塊打包的簡(jiǎn)單原理(理解地比較粗淺)
通過(guò)聲明一個(gè)installedModules字典來(lái)存儲(chǔ)每個(gè)模塊,給每個(gè)模塊設(shè)置一個(gè)唯一key,全部傳入一個(gè)立即執(zhí)行的匿名函數(shù),有個(gè)入口模塊,在里面執(zhí)行所有模塊并存儲(chǔ)進(jìn)installedModules,已經(jīng)執(zhí)行過(guò)的模塊會(huì)直接拿緩存。
webpack編譯打包后的代碼,在瀏覽器中是這么運(yùn)行的:1、初始化環(huán)境和一些數(shù)據(jù)結(jié)構(gòu);2、執(zhí)行入口模塊代碼;3、執(zhí)行模塊代碼,記錄export和導(dǎo)出import(遞歸);4、所有模塊代碼執(zhí)行完畢,控制權(quán)回到入口模塊;
8、loader
loader可以譯為裝載機(jī),在webpack中一切皆模塊,這也是為什么引入css需要在js中import,因?yàn)閣ebpack只能識(shí)別js,而一個(gè)組件或者頁(yè)面的js+css就是一個(gè)模塊,所以通過(guò)在js中引用css的方式來(lái)將其綁定成一個(gè)模塊。
// app.js
import './style.css';
// style.css
body {
text-align: center;
padding: 100px;
color: #fff;
background-color: #09c;
}
而loader其實(shí)一個(gè)函數(shù),它的輸入和輸出是源碼或上一個(gè)loader的輸出(字符串、source map、AST),所以loader的調(diào)用是鏈?zhǔn)降?,像一個(gè)流水線一樣將模塊打包出去。這也意味著loader的聲明是需要注意順序的。
ps:source map是一個(gè)json文件,用來(lái)解決代碼編譯前后的映射問(wèn)題
loader配置:
module.exports = {
module: {
rules: [{
// 正則匹配需要進(jìn)入loader的文件
test: /\.css$/,
// 用到的loader數(shù)組(loader的執(zhí)行順序從后到前,所以這里是css-loader先執(zhí)行)
use: [
'style-loader',
// loader除了上面'style-loader'這種直接聲明字符串
// 還可以像下面'css-loader'這樣聲明一些配置項(xiàng)
{
loader: 'css-loader',
options: {
// css-loader 配置項(xiàng)
}
}
],
// loader處理文件的排除范圍
exclude: /node_modules/, // 正則
// 處理范圍,exclude優(yōu)先于include,意味著如果兩個(gè)配置有重疊,include是不能覆蓋exclude的
include: /src/, // 正則
/*
* 在Webpack中,我們認(rèn)為被加載模塊是resource,而加載者是issuer。
* 比如在這個(gè)例子里,css文件是加載模塊(resource),js文件則是加載者(issuer)
* 所以下面是配置js文件,則是配置加載者
* 前面的loader則是配置加載模塊
*/
issuer: {
test: /\.js$/,
include: /src/pages/ // 正則
},
// loader執(zhí)行順序:
// normal(默認(rèn),按排列順序)、pre(在所有正常loader前)、post(在所有正常lodaer后)
enforce: 'normal',
}],
},
};
9、寫一個(gè)最簡(jiǎn)單的loader
上面說(shuō)了loader其實(shí)就是一個(gè)有輸入輸出的函數(shù),所以最簡(jiǎn)單的loader其實(shí)只要寫一個(gè)函數(shù)就行。
- 用 npm init 初始化一個(gè)項(xiàng)目;
- 創(chuàng)建一個(gè)index.js寫入以下代碼;
// 這個(gè)loader可以在js文件頭部加上 “這是我加上去的代碼” 這句注釋
// content 則是loader的輸入即源碼或上一個(gè)loader的輸出字符串
module.exports = function(content) {
var useStrictPrefix = `
// 這是我加上去的代碼
`;
return useStrictPrefix + content;
}
- 在另一個(gè)項(xiàng)目通過(guò) npm install <絕對(duì)路徑> 來(lái)安裝loader;
- 在webpack配置文件中寫入loader配置;
- 執(zhí)行編譯;
10、webpack-dev-server
開啟一個(gè)熱更新的服務(wù),可以修改代碼后通過(guò)websocket通知瀏覽器更新。devServer會(huì)對(duì)代碼進(jìn)行編譯打包,但不會(huì)生成文件,打包后的代碼會(huì)放進(jìn)內(nèi)存訪問(wèn),當(dāng)瀏覽器對(duì)這個(gè)服務(wù)發(fā)起請(qǐng)求,它會(huì)先校驗(yàn)請(qǐng)求的url是不是配置文件里devServer的publicPath。
安裝:npm install --save-dev webpack-dev-server
配置:
devServer: {
/*
devServer.contentBase
決定了 webpackDevServer 啟動(dòng)時(shí)服務(wù)器資源的根目錄,默認(rèn)是項(xiàng)目的根目錄。
在有靜態(tài)文件需要 serve 的時(shí)候必填,contentBase 不會(huì)影響 path 和 publicPath,
它唯一的作用就是指定服務(wù)器的根目錄來(lái)引用靜態(tài)文件。
可以這么理解 contentBase 與 publicPath 的關(guān)系:contentBase 是服務(wù)于引用靜態(tài)文件的路徑,
而 publicPath 是服務(wù)于打包出來(lái)的文件訪問(wèn)的路徑,兩者是不互相影響的。
*/
contentBase: './dist',
/*
devServer.publicPath
在開啟 webpackDevServer 時(shí)瀏覽器中可通過(guò)這個(gè)路徑訪問(wèn) bundled 文件,
靜態(tài)文件會(huì)加上這個(gè)路徑前綴,若是devServer里面的publicPath沒(méi)有設(shè)置,
則會(huì)認(rèn)為是output里面設(shè)置的publicPath的值。
(如果有使用htmlWebpackPlugin,建議devServer.publicPath不填或者跟output.publicPath一致,
因?yàn)樵陂_啟devServer后,htmlWebpackPlugin插入js會(huì)使用devServer.publicPath)
和 output.publicPath 非常相似,都是為瀏覽器制定訪問(wèn)路徑的前綴。
但是不同的是 devServer.publicPath 只影響于 webpackDevServer(一般來(lái)說(shuō)就是 html),
但各種 loader 打出來(lái)的路徑還是根據(jù) output.publicPath。
*/
publicPath: './dist'
}
參考:https://github.com/fi3ework/blog/issues/39
11、代碼分片
考慮到緩存和減少請(qǐng)求時(shí)間等原因,需要將公共代碼分塊。不同于之前使用的CommonsChunk-Plugin插件,webpack4有了改進(jìn)版的代碼分片配置optimization.SplitChunks。
不像CommonsChunk-Plugin需要去將特定的模塊提取出來(lái),使用SplitChunks只需要配置提取條件,webpack就會(huì)將符合條件的模塊打包出來(lái)。下面是默認(rèn)配置:
optimization: {
splitChunks: {
// chunks: async(默認(rèn),只提取異步模塊) | initial(只提取入口) | all(前兩者都提取)
chunks: 'all',
// 按cacheGroups的提取規(guī)則,并以automaticNameDelimiter為分隔符命名chunks
// eg: vendors~a~b~c.js意思是該chunk為vendors規(guī)則所提取,并且該chunk是由a、b、c三個(gè)入口chunk所產(chǎn)生的。
name: true,
// chunk命名的分隔符
automaticNameDelimiter: '~',
/*
* 根據(jù)chunk資源本身情況配置規(guī)則
*/
// 提取后的Javascript chunk體積大于30kB(壓縮和gzip之前),CSS chunk體積大于50kB
minSize: {
javascript: 30000,
style: 50000,
},
// 在按需加載過(guò)程中,并行請(qǐng)求的資源最大值小于等于5
maxAsyncRequests: 5,
// 在首次加載時(shí),并行請(qǐng)求的資源數(shù)最大值小于等于3
maxInitialRequests: 3,
// 備注:設(shè)置maxAsyncRequests和maxInitialRequests是因?yàn)椴幌M麨g覽器一次發(fā)出過(guò)多請(qǐng)求,
// 所以希望把一次加載的模塊限定規(guī)定次數(shù);
/*
* 根據(jù)chunk來(lái)源配置提取規(guī)則
*/
cacheGroups: {
// 模塊來(lái)自node_modules目錄,vendors只是chunk命名,可靈活調(diào)整;
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10, // 優(yōu)先級(jí),這里vendors優(yōu)先
},
// chunk被至少兩個(gè)模塊引用則重用
default: {
minChunks: 2,
reuseExistingChunk: true,
priority: -20,
},
}
},
}
// 正常只需要像下面這樣聲明即可
optimization: {
splitChunks: {
chunks: 'all'
}
}
12、webpack異步模塊加載
// 異步地將b.js加載進(jìn)來(lái)
import('./b.js').then((b) => {
...
})
這個(gè)異步import,webpack是通過(guò)動(dòng)態(tài)插入script標(biāo)簽來(lái)實(shí)現(xiàn)的,因?yàn)橹疤徇^(guò),通過(guò)script加載進(jìn)來(lái)的屬于間接資源請(qǐng)求,這個(gè)資源位置需要通過(guò)output.publicPath來(lái)確定,所以需要配置號(hào)output.publicPath;
13、環(huán)境區(qū)分
在開發(fā)過(guò)程中,需要區(qū)分開發(fā)環(huán)境和生產(chǎn)環(huán)境,開發(fā)環(huán)境一般完成基本的編譯打包工作,讓代碼能在瀏覽器運(yùn)行就好,而生產(chǎn)環(huán)境為了更小的包體通常還會(huì)進(jìn)行壓縮、tree-shaking等操作。將這兩種環(huán)境的操作區(qū)分開來(lái),一般有兩種方案:
- 通過(guò)命令傳入環(huán)境變量
// package.json
{ ...
"scripts": {
"dev": "ENV=development webpack-dev-server",
"build": "ENV=production webpack"
},
}
// webpack.config.js
const ENV = process.env.ENV;
const isProd = ENV === 'production';
module.exports = {
output: {
filename: isProd ? 'bundle@[chunkhash].js' : 'bundle.js',
},
// mode模式如果為production,webpack會(huì)默認(rèn)添加一些配置,幫助壓縮代碼
mode: ENV,
};
- 為兩種環(huán)境分別寫一個(gè)配置文件,公用的部分可以通過(guò)webpack-merge來(lái)合并
{ ...
"scripts": {
"dev": " webpack-dev-server --config=webpack.development.config.js",
"build": " webpack --config=webpack.production.config.js"
},
}
- 通過(guò)webpack中可以通過(guò)DefinePlugin插件將環(huán)境信息注入到j(luò)s運(yùn)行環(huán)境:
// webpack.config.js
plugins: [
new webpack.DefinePlugin({
ENV: JSON.stringify('production'),
})
]
// app.js
document.write(ENV);
14、tree-shaking
tree-shaking能使webpack在打包過(guò)程中,將一些沒(méi)引用到的多余包體剔除,但有兩點(diǎn)需要注意,一是tree-shaking只在es6 moudle下生效(依靠es6 moudle靜態(tài)引用實(shí)現(xiàn)),意味著commonjs的模塊管理是不可行的;二是tree-shaking只是做多余包體的標(biāo)記工作,實(shí)際剔除代碼還是需要借助壓縮插件如terser-web-pack-plugin,但在webpack4只需要將mode設(shè)置為production即可。
針對(duì)上面第一點(diǎn),需要注意在babel-loader中設(shè)置module=false,禁止bable將模塊轉(zhuǎn)為commonjs。
15、模塊熱替換(hot module replace)
監(jiān)聽(tīng)文件變化,不同于live reload(刷新頁(yè)面,全量更新),熱替換是增量修改,不刷新網(wǎng)頁(yè),只更改局部。
開啟HMR:
const webpack = require('webpack');
module.exports = { // ...
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: { hot: true, },
};
HMR原理:
- 首先瀏覽器端會(huì)有HMR runtime,webpack會(huì)起一個(gè)webpack-dev-server(WDS),兩者依靠websocket通信;
- 當(dāng)WDS監(jiān)聽(tīng)到文件變化,會(huì)向客戶端推送更新事件,并帶上構(gòu)建的hash??蛻舳烁鶕?jù)這個(gè)hash和之前資源的對(duì)比,判斷是否需要更新;
- 當(dāng)需要更新,客戶端就會(huì)向WDS請(qǐng)求更改的資源列表。WDS會(huì)返回需要構(gòu)建的chunk name和資源版本hash??蛻舳嗽俑鶕?jù)這些信息向WDS請(qǐng)求增量更新的資源;
- 拿到更新的資源,HMR runtime就會(huì)開始決定哪些地方需要替換。webpack會(huì)暴露一個(gè)module.hot接口,用于給使用者知道熱替換的時(shí)機(jī),module.accept則是設(shè)置需要替換的模塊。一般loader都會(huì)設(shè)置相應(yīng)的模塊熱替換的補(bǔ)丁操作,對(duì)替換模塊進(jìn)行操作。如果runtime對(duì)某個(gè)模塊沒(méi)有檢測(cè)到對(duì)HMR的update handler,則會(huì)將替換操作冒泡到父級(jí)模塊,以此類推。(參考:webpack中文文檔)