依據 webpack 官方文檔,webpack 是一個 module bundler (模塊打包器)。初次聽到這個概念的時候,可能會一臉矇蔽:這是個啥?對個人開發有啥影響麼?javascript
爲了更好理解 webpack,咱們須要先了解模塊 Module 與打包 Bundle 的具體含義。只有將這兩個概念理清楚了纔會更清楚 webpack 的做用。css
不管使用何種編程語言開發大型應用,最關鍵的特性就是代碼模塊化。模塊化的必要性在於:提升代碼的開發效率,方便代碼/功能的維護與重構。在C++裏爲命名空間,Java中爲包,名稱不同但解決的是同一問題。html
可是,最初的 JavaScript 並非用來編寫大規模代碼應用的,因而它的規範裏面並無模塊化這個標準。對於此,開源開發者提出了一些標準,如 CommoneJs 模塊模型、異步模塊定義(AMD)以及一些庫,來實現模塊化。java
幸運的是:ES6 爲 JavaScript 帶來了模塊特性。但瀏覽器實現這一特性還須要一段時間。node
接着模塊化的思路。在 JavaScript 程序開發過程當中,模塊化會產生不少不一樣的代碼文件(如 js、css等)。舉個栗子, SPA 頁面 index.html 用到了三個JS文件 和 一個 CSS 文件,那麼其經過script標籤引入這些文件webpack
//文件結構
|- index.html
|- main.css
| - a.js
| - b.js
| - c.js
//代碼演示
// index.html
<!doctype html>
<html>
<head><link href="main.css" rel="stylesheet"></head>
<body>
<div>hello world</div>
<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
<script type="text/javascript" src="c.js"></script>
</body>
</html>
複製代碼
由於有3 個 js 文件,因此瀏覽器須要發送三次 http 請求來獲取這三個文件。當咱們的項目逐漸變大,有幾十個到上百個 JavaScript 文件的時候,那問題會更嚴重。諸多問題都會暴露無遺(如網絡延遲等)。es6
是否是把全部 JavaScript 文件合成一個文件就行了呢?是的。咱們確實能夠這樣作。web
可是,矛盾點來了:在開發階段,咱們使用模塊化開發;在實際應用中,咱們但願可以將多個文件合併爲一個文件。這該怎麼辦呢?chrome
很顯然,在開發結束以後,咱們須要一個合併的過程。在開發完成後的這個合併過程就是打包。npm
能夠說,webpack 所作的一切工做,都是爲了實現模塊打包。
將 webpack 理解爲模塊打包器,將 webpack 工做的過程理解爲模塊打包的過程。
webpack 的靈活性在於:整個過程的大部分因子咱們都是能夠配置的,極爲個性化。咱們先來認識整個過程的頭與尾:Entry屬性、Output屬性。
Entry 屬性指示 webpack 應該使用哪一個模塊,來做爲構建其內部依賴圖的開始。進入入口起點後,webpack 會找出有哪些模塊和庫是入口起點(直接和間接)依賴的。入口文件能夠有多個。根據項目特色,能夠以多種方式來配置 Entry。
Output 屬性告訴 webpack 在哪裏輸出它所建立的 bundles,以及如何命名這些文件。它表示的是打包後輸出文件的路徑。
這時,咱們來了解一下 webpack 的基本工做機制。
在默認狀況下,webpack 只可以識別 .js
.json
格式的文件,其餘的文件它是沒法識別的。對於更多格式的文件,webpack 爲咱們提供了 Loader 選項,Loader 能夠當作是 webpack 不一樣格式文件的解析器。針對不一樣的文件格式,咱們能夠配置不一樣的 Loader。
更進一步,咱們須要瞭解的是:webpack 的運行過程存在一個生命週期的過程。詳細的,能夠安裝lifecycle-webpack-plugin 插件來查看生命週期信息。
plugin 插件,能夠在webpack運行到某個階段的時候(構建流程中的特定時機),幫你作某些事情(注⼊擴展邏輯來改變構建結果),相似於生命週期鉤子的做用。
瞭解了上面的這些概念以後,咱們來看 webpack 的基本配置。默認的配置文件是項目目錄下的 webpack.config.js 文件。
對 web 開發而言,經常使用的須要解析的文件無非是這幾種:CSS文件、圖片文件、字體文件。那麼對應的 loader 配置爲:
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: "file-loader",
options: {
name: "[name]_[hash:6].[ext]",
outputPath: "images/"
}
}
},
{
test: /\.(eot|ttf|woff|woff2)$/,
use: {
loader: "file-loader",
options: {
name: "[name].[ext]"
}
}
}
}
]
},
複製代碼
Plugin 配置,就很是個性化了。隨着後期優化的不斷增強,咱們使用的插件會隨之增多。在此只介紹兩個插件:
有時候咱們會使用新的 ECMAScript 規範語法,但瀏覽器對這個新的語法規範可能不支持。因而須要降級處理。這個時候 Babel 就出現了。
Babel 是 JavaScript 編譯器,能將 ES6(或更新的)代碼轉換成 ES5 代碼,讓咱們開發過程當中放⼼使⽤ JS 新特性而不⽤擔憂兼容性問題。而且還能夠經過插件機制根據需求靈活的擴展。
Babel 配置會由於 Babel 版本不一樣而發生變化。最新的 Babel 7 配置,相比於 Babel 6 簡化了很多。
Babel 在執行編譯的過程當中,會從項目根⽬錄下的 .babelrc JSON⽂件中讀取配置。沒有該文件會從對應 loader 的 options 地⽅讀取配置。
npm i babel-loader @babel/core @babel/preset-env -D
這幾個依賴包的做用以下:
//配置方式一:直接在webpack.config.js 對應 loader 的 options 地⽅配置
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
}
//配置方式二:項目根⽬錄下的 .babelrc 文件配置
//推薦方式二
{
presets: ["@babel/preset-env"]
}
複製代碼
經過上面的幾步還不夠,默認的 babel 只支持 let 等一些基礎的特性轉換(只轉換語法,不轉換新的 API),promise 等高級特性尚未轉換過來。這個時候還須要藉助 @babel/polyfill,將es的新特性都裝進來,來彌補低版本瀏覽器中缺失的特性。
npm install --save @babel/polyfill
值得注意的是 @babel/polyfill 是運行時依賴。
咱們能夠嘗試全局引入 polyfill 。
// index.js 頂部
import "@babel/polyfill";
複製代碼
會發現打包的體積大了不少。這是由於 polyfill 默認會把全部特性注入進來。我但願當我使用到了 es 6+ 特性的時候,才注入,不用到的不注入,從而減小打包的體積。且對於現代的已經支持高級特性的瀏覽器,不須要用到 polyfill。所以咱們要按需引入 polyfill。
修改.babelrc 文件配置
{
presets: [
[
"@babel/preset-env",
{
targets: { //編譯後的代碼支持的運行環境對象
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1"
},
corejs: 2, // 指定核心庫版本
useBuiltIns: "usage" //按需注入
}
}
複製代碼
重點是 useBuiltIns
這個選項。 useBuiltIns
選項是 babel 7 的新功能,這個選項告訴 babel 如何配置 @babel/polyfill 。 它有三個參數可使⽤:
咱們使用useBuiltIns: usage
即知足咱們的需求。
通過上面的操做,咱們已經能夠作到了對 webpack 的基本配置,並讓其可以運行 ES6 的代碼了。在講 webpack 的性能優化配置以前,咱們來嘗試一下手寫 Loader 與 Plugin 兩個模塊,以更好地理解這兩個模塊的做用。別太驚訝,這其實不難。往下看吧:
咱們知道,webpack 中的 loader 是各類格式文件的解析器。簡單看待 loader,就能夠理解爲它是一個處理器。對輸入進行一番處理(解析)以後,輸出結果。
本身嘗試編寫一個 Loader,這個過程咱們能夠更好理解 Loader 的工做原理。其過程是比較簡單的。
在 webpack 中,Loader 就是一個函數(聲明式函數,不能用箭頭函數)。它經過參數項獲取到源代碼,作進一步的修飾處理後,返回處理過的源代碼。
就讓咱們來看看一個最簡單的替換源碼中文字符串的 loader (它的名字就叫 replaceLoader)是如何寫出來的吧:
建立原材料(帶放入 loader 的源代碼,以及 loader):
//index.js
console.log("hello 親愛的");
//replaceLoader.js
module.exports = function(source){
console.log(source, this, this.query);
return source.replace("親愛的", "dear");
}
複製代碼
在配置文件中使用 loader
//webpack.config.js
//node核⼼模塊path來處理路徑
const path = require('path')
···
module: {
rules: [ {
test: /\.js$/,
use: path.resolve(__dirname,"./loader/replaceLoader.js"),
options: {
name : "frank"
}
} ]
},
···
複製代碼
如何給 loader 配置參數? loader 如何接收參數?
正如上面的 loader 中,咱們能夠寫入對應的參數放入到 options 中,那麼 loader中如何接收到呢?有兩種方式:
讓 loader 返回多個信息
有的時候咱們不只僅但願 loader 可以返回一個信息,而是多是更多的信息,好比:報錯信息、source-map 信息等。webpack 中的 loader 能夠經過調用 this.callback 來返回多個信息。
//replaceLoader.js
module.exports = function(source) {
const result = source.replace("親愛的", this.query.name);
this.callback(null, result);
};
//callback中能夠包含的參數項
//this.callback (
err: Error | null,
content: String | Buffer,
sourceMap?: SourceMap,
meta?: any
)
複製代碼
讓 loader 異步返回
//replaceLoader.js
//使用 setTimeout 3sec 再返回
module.exports = function(source) {
console.log(this, this.query);
const callback = this.async();
setTimeout(() => {
const result = source.replace("親愛的", this.query.name);
callback(null, result);
}, 3000);
};
複製代碼
經歷了本身手寫 loader 以後,咱們也能夠試試本身手寫一個簡單的 plugin。來加深對於 webpack 的認識。
前面講過:plugin 插件能夠在 webpack 運行到某個階段的時候(構建流程中的特定時機),幫你作某些事情(注⼊擴展邏輯來改變構建結果)。
在 webpack 中, plugin 必須是一個類(class),裏面必須包含一個 apply 函數,該函數接收一個參數:compiler。就這麼簡單,沒有更多的要求了。因此讓咱們來試試吧:
咱們來嘗試書寫一個在 webpack 打包完成後,往 dist 文件夾增長一個 js 文件的 plugin。很簡單沒啥真實做用,只是爲了演示生成 plugin 的過程而已。
建立 ./plugin/add-js-file-webpack-plugin.js
class AddJsFileWebpackPlugin {
constructor(){
}
apply(compiler){
}
}
module.exports = AddJsFileWebpackPlugin;
複製代碼
配置文件裏使用
const AddJsFileWebpackPlugin = require('./plugin/add-js-file-webpack-plugin.js');
...
plugins: [new AddJsFileWebpackPlugin({name:"frank"})]
...
複製代碼
如何應用
在上面的配置中,咱們看到插件傳入了參數{name:"frank"}
,咱們能夠在構造器中捕獲,並保存起來。
而後咱們如何使用apply
函數呢?這個時候就須要結合 webpack 的生命週期函數鉤子了。在官網上,能夠看到大量的鉤子能夠供咱們使用(有同步執行的鉤子,也有異步執行的鉤子)。
爲了演示的目的,咱們使用了 compiler.emit
異步鉤子和compiler.compile
同步鉤子:
class AddJsFileWebpackPlugin {
constructor(options) {
this.name = options.name;
console.log(options);
}
apply(compiler) {
compiler.hooks.compile.tap("AddJsFileWebpackPlugin", compilation => {
console.log("執行了, ");
});
compiler.hooks.emit.tapAsync(
"AddJsFileWebpackPlugin",
(compilation, cb) => {
compilation.assets["finish.js"] = {
source: function() {
return "hello dear";
},
size: function() {
return 20;
}
};
cb();//異步鉤子中,必須調用 cb
}
);
}
}
module.exports = AddJsFileWebpackPlugin;
複製代碼
在這篇文章中,咱們對 webpack 作了一個基本的認識與瞭解,並經過簡單的配置,讓其可以簡單運行代碼。最後,經過本身手寫 Loader 與 Plugin ,更好地認識其中的模塊與工做原理。接下來,咱們來看看具體如何利用 webpack 來進行優化配置吧!