1. 程式人生 > 程式設計 >webpack高階配置與優化詳解

webpack高階配置與優化詳解

所謂打包多頁面,就是同時打包出多個 html 頁面,打包多頁面也是使用 html-webpack-plugin,只不過,在引入外掛的時候是建立多個外掛物件,因為一個html-webpack-plugin 外掛物件只能打包出一個 html 頁面。如:

module.exports = {
 entry: {
  index: "./src/index.js",// 指定打包輸出的chunk名為index
  foo: "./src/foo.js" // 指定打包輸出的chunk名為foo
 },plugins: [
  new HtmlWebpackPlugin({
   template: "./src/index.html",// 要打包輸出哪個檔案,可以使用相對路徑
   filename: "index.html",// 打包輸出後該html檔案的名稱
   chunks: ["index"] // 陣列元素為chunk名稱,即entry屬性值為物件的時候指定的名稱,index頁面只引入index.js
  }),new HtmlWebpackPlugin({
   template: "./src/index.html",// 要打包輸出哪個檔案,可以使用相對路徑
   filename: "foo.html",// 打包輸出後該html檔案的名稱
   chunks: ["foo"] // 陣列元素為chunk名稱,即entry屬性值為物件的時候指定的名稱,foo頁面只引入foo.js
  }),]
}

打包多頁面時,關鍵在於 chunks 屬性的配置,因為在沒有配置 chunks 屬性的情況下,打包輸出的 index.html 和 foo.html 都會同時引入 index.js 和 foo.js。

所以必須配置 chunks 屬性,來指定打包輸出後的 html 檔案中要引入的輸出模組,陣列的元素為 entry 屬性值為物件的時候指定的 chunk 名,如上配置,才能實現,index.html 只引入 index.js,foo.html 只引入 foo.js 檔案

二、配置 source-map

source-map 就是原始碼對映,主要是為了方便程式碼除錯,因為我們打包上線後的程式碼會被壓縮等處理,導致所有程式碼都被壓縮成了一行,如果程式碼中出現錯誤,那麼瀏覽器只會提示出錯位置在第一行,這樣我們無法真正知道出錯地方在原始碼中的具體位置。webpack 提供了一個 devtool 屬性來配置原始碼對映。

let foo = 1;
console.lg(`console物件的方法名log寫成了lg`); // 原始檔第二行出錯
index.js:1 Uncaught TypeError: console.lg is not a function
 at Object.<anonymous> (index.js:1)
 at o (index.js:1)
 at Object.<anonymous> (index.js:1)
 at o (index.js:1)
 at index.js:1
 at index.js:1

原始碼中出錯的位置明明是第二行程式碼,而瀏覽器中提示的錯誤確實在第一行,所以如果程式碼很複雜的情況下,我們就無法找到出錯的具體位置

devtool 常見的有 6 種配置:

1、source-map: 這種模式會產生一個.map檔案,出錯了會提示具體的行和列,檔案裡面保留了打包後的檔案與原始檔案之間的對映關係,打包輸出檔案中會指向生成的.map檔案,告訴js引擎原始碼在哪裡,由於原始碼與.map檔案分離,所以需要瀏覽器傳送請求去獲取.map檔案,常用於生產環境,如:

//# sourceMappingURL=index.js.map

2、eval: 這種模式打包速度最快,不會生成.map檔案,會使用eval將模組包裹,在末尾加入sourceURL,常用於開發環境,如:

//# sourceURL=webpack:///./src/index.js

3、eval-source-map: 每個 module 會通過 eval() 來執行,並且生成一個 DataUrl 形式的 SourceMap (即 base64 編碼形式內嵌到 eval 語句末尾),但是不會生成 .map 檔案,可以減少網路請求*,但是打包檔案會非常大*。

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXguanM/YjYzNSJdLCJuYW1lcyI6WyJmb28iLCJjb25zb2xlIiwibGciXSwibWFwcGluZ3MiOiJBQUFBLElBQUlBLEdBQUcsR0FBRyxDQUFWO0FBQ0FDLE9BQU8sQ0FBQ0MsRUFBUix1RSxDQUFxQyIsImZpbGUiOiIuL3NyYy9pbmRleC5qcy5qcyIsInNvdXJjZXNDb250ZW50IjpbImxldCBmb28gPSAxO1xuY29uc29sZS5sZyhgY29uc29sZeWvueixoeeahOaWueazleWQjWxvZ+WGmeaIkOS6hmxnYCk7IC8vIOa6kOaWh+S7tuesrOS6jOihjOWHuumUmVxuIl0sInNvdXJjZVJvb3QiOiIifQ==

//# sourceURL=webpack-internal:///./src/index.js

4、cheap-source-map: 加上 cheap,就只會提示到第幾行報錯,少了列資訊提示,同時不會對引入的庫做對映,可以提高打包效能,但是會產生 .map 檔案。

5、cheap-module-source-map: 和 cheap-source-map 相比,加上了 module,就會對引入的庫做對映,並且也會產生 .map 檔案,用於生產環境。

6、cheap-module-eval-source-map: 常用於開發環境,使用 cheap 模式可以大幅提高 souremap 生成的效率,加上 module 同時會對引入的庫做對映,eval 提高打包構建速度,並且不會產生 .map 檔案減少網路請求。

凡是帶 eval 的模式都不能用於生產環境,因為其不會產生 .map 檔案,會導致打包後的檔案變得非常大。通常我們並不關心列資訊,所以都會使用 cheap 模式,但是我們也還是需要對第三方庫做對映,以便精準找到錯誤的位置。

三、watch 和 watchOptions 配置

webpack 可以監聽檔案變化,當它們修改後會重新編譯,如果需要開啟該功能,那麼需要將 watch 設定為 true,具體監聽配置通過 watchOptions 進行相應的設定。

module.exports = {
 watch: true,watchOptions: {
  poll: 1000,// 每隔一秒輪詢一次檔案是否發生變化
  aggregateTimeout: 1000,// 當第一個檔案更改,會在重新構建前增加延遲。這個選項允許 webpack 將這段時間內進行的任何其他更改都聚合到一次重新構建裡
  ignored: /node_modules/ // 排除一些檔案的監聽
 }
}

四、三個常見小外掛的使用

1、clean-webpack-plugin: 其作用就是每次打包前先先將輸出目錄中的內容進行清空,然後再將打包輸出的檔案輸出到輸出目錄中。

const {CleanWebpackPlugin} = require("clean-webpack-plugin");
module.exports = {
 plugins: [
  new CleanWebpackPlugin() // 打包前清空輸出目錄
 ]
}

需要注意的是,require("clean-webpack-plugin)的結果是一個物件而不是類,這個物件中的 CleanWebpackPlugin 屬性才是一個類,我們就是用這個類去建立外掛物件。

2、copy-webpack-plugin: 其作用就是打包的時候帶上一些 readMe.md、history.md 等等一起輸出到輸出目錄中。

module.exports = {
 plugins: [
  new CopyWebpackPlugin([
   {
    from: "./readMe.md",// 將專案根目錄下的readMe.md檔案一起拷貝到輸出目錄中
    to: "" // 屬性值為空字串則表示是輸出目錄
   }
  ])
 ]
}

3、BannerPlugin: 其作用就是在打包輸出的 js 檔案的頭部新增一些文字註釋,比如版權說明等等,BannerPlugin 是 webpack 內建的外掛,如:

module.exports = {
 plugins: [
  new webpack.BannerPlugin("Copyright © 2019") // 在js檔案頭部新增版權說明
 ]
}

五、webpack 跨域問題

為什麼 webpack 會存在跨域問題?因為 webpack 打包的是前端程式碼,其最終會被部署到前端伺服器上,而前後端程式碼通常部署在不同的伺服器上,即使是部署在同一個伺服器上,所使用的埠也是不一樣的,當前端程式碼通過 ajax 等手段向後端伺服器獲取資料的時候,由於前後端程式碼不在同一個域中,故存在跨域問題。

比如,我們通過 webpack 的 devServer 來執行部署我們的前端應用程式碼,devServer 啟動在 8080 埠上,而前端應用程式碼中會通過 ajax 請求後端資料,後端伺服器啟動在 3000 埠上。

// index.js
const xhr = new XMLHttpRequest();
// xhr.open("get","http://localhost:3000/api/test"); // 由於跨域問題無法直接訪問到http://localhost:3000下的資源
xhr.open("get","/api/test"); // 本來是要訪問http://localhost:3000/api/test
xhr.onreadystatechange = () => {
 if (xhr.readyState === 4) {
  console.log(xhr.responseText);
 }
}
xhr.send();

由於前端程式碼是執行在瀏覽器中的,如果在前端程式碼中直接通過 ajax 向http://localhost:3000/api/test 發起請求獲取資料,那麼由於瀏覽器同源策略的影響,會存在跨域的問題,所以必須訪問 /api/test。但是這樣訪問又會出現 404 問題,因為其實訪問的是 http://localhost:8080/api/test,8080 伺服器上是沒有該資源的,解決辦法就是通過 devServer 配置一個代理伺服器

module.exports = {
 devServer: {
  proxy: {
   "/api": "http://localhost:3000" // 路徑以/api開頭則代理到localhost:3000上
  }
 }
}

訪問 http://localhost:8080/api/test

就會被代理到http://localhost:3000/api/test 上,proxy 還支援路徑的重寫,如果 3000 埠伺服器上並沒有 /api/test 路徑,只有 /test 路徑,那麼就可以對路徑進行重寫,將 /api 替換掉

module.exports = {
 devServer: {
  proxy: {
   "/api": {
    target: "http://localhost:3000",pathRewrite: {"/api": ""} // 將/api替換掉
   }
  }
 }
}

訪問 http://localhost:8080/api/test

就會被代理到 http://localhost:3000/test 上

如果前端只是想 mock 一些資料,並不需要真正的去訪問後臺伺服器,那麼我們可以通過 devServer 提供的 before 鉤子函式獲取到內建的伺服器物件進行處理請求,這個內建的伺服器物件就是 webpack 的 devServer 即 8080 埠的 server,因為是在同一個伺服器中請求資料所以也不會出現跨域問題。

before(app) { // 此app即webpack的devServer
   app.get("/api/test",(req,res,next) => {
    res.json({name: "even"});
   })
  }

我們還可以不通過 webpack 提供的 devServer 來啟動 webpack,而是使用自己伺服器來啟動 webapck。

const express = require("express");
const app = express();
const webpack = require("webpack"); // 引入webpack
const config = require("./webpack.config.js"); // 引入配置檔案
const compiler = webpack(config); // 建立webpack的編譯器
const middleWare = require("webpack-dev-middleware"); //引入webpack的中介軟體
app.use(middleWare(compiler)); // 將compiler編譯器交給中介軟體處理
app.get("/api/test",next) => {
 res.json({name: "lhb"});
});
app.listen(3000);

通過自定義伺服器啟動 webpack,這樣 webpack 中的前端程式碼請求資料就和伺服器的資源在同一個域中了。

六、resolve 配置

resolve 用於配置模組的解析相關引數的,其屬性值為一個物件。

1、modules: 告訴 webpack 解析模組時應該搜尋的目錄,即 require 或 import 模組的時候,只寫模組名的時候,到哪裡去找,其屬性值為陣列,因為可配置多個模組搜尋路徑,其搜尋路徑必須為絕對路徑,比如,src 目錄下面有一個 foo.js 檔案和 index.js 檔案:

// index.js
const foo = require("./foo"); // 必須寫全foo.js模組的路徑
// const foo = require("foo"); // resolve.modules中配置了模組解析路徑為.src目錄,則可用只寫foo即可搜尋到foo.js模組
console.log(foo);
module.exports = {
 resolve: {
  modules: [path.resolve(__dirname,"./src/"),"node_modules"]
 },}

由於 resolve.modules 中配置了 ./src 目錄作為模組的搜尋目錄,所以 index.js 中可以只寫模組名即可搜尋到 foo.js 模組

2、alias: 用於給路徑或者檔案取別名,當 import 或者 require 的模組的路徑非常長時,我們可以給該模組的路徑或者整個路徑名+檔名都設定成一個別名,然後直接引入別名即可找到該模組,比如,有一個模組位置非常深

// const foo = require("./a/b/c/foo"); // foo.js在./src/a/b/c/foo.js
// const foo = require("foo"); // foo被對映成了./src/a/b/c/foo.js檔案
const foo = require("bar/foo.js"); // bar被對映成了./src/a/b/c/路徑
console.log(foo);
module.exports = {
 resolve: {
  alias: {
   "foo": path.resolve(__dirname,"./src/a/b/c/foo.js"),"bar": path.resolve(__dirname,"./src/a/b/c/")
  }
 },}

需要注意的就是,alias 可以對映檔案也可以對映路徑

3、mainFields: 我們的 package.json 中可以有多個欄位,用於決定優先使用哪個欄位來匯入模組,比如 bootstrap 模組中含有 js 也含有 css,其 package.json 檔案中 main 欄位對應的是"dist/js/bootstrap",style 欄位中對應的是"dist/css/bootstrap.css",我們可以通過設定 mainFields 欄位來改變預設引入,如:

module.exports = {
 resolve: {
  mainFields: ["style","main"]
 },}

4、extensions: 用於設定引入模組的時候,如果沒有寫模組字尾名,webpack 會自動新增字尾去查詢,extensions 就是用於設定自動新增字尾的順序,如:

module.exports = {
 resolve: {
  extensions: ["js","vue"]
 },}

如果專案中引入了 foo 模組,require("./foo"),其會優先找 ./foo.js,如果沒有找到 ./foo.js 則會去找 ./foo.vue 檔案

七、設定環境變數

設定環境變數需要用到 webpack 提供的一個內建外掛 DefinePlugin 外掛,其作用是將一個字串值設定為全域性變數,如:

module.exports = {
 plugins: [
  new webpack.DefinePlugin({
   DEV_MODE: JSON.stringify('development') // 將'development'設定為全域性變數DEV_MODE
  }),]
}

這樣配置之後任何一個模組中都可以直接使用 DEV_MODE 變量了,並且其值為'development',與 ProvidePlugin 有點相似,ProvidePlugin 是將一個模組注入到所有模組中,實現模組不需要引入即可直接使用。

八、webpack 優化

1、noParse: 該配置是作為 module 的一個屬性值,即不解析某些模組,所謂不解析,就是不去分析某個模組中的依賴關係,即不去管某個檔案是否 import(依賴)了某個檔案,對於一些獨立的庫,比如 jquery,其根本不存在依賴關係,jquery 不會去引入其他的庫(要根據自己對某個模組的瞭解去判斷是否要解析該模組),所以我們可以讓 webpack 不去解析 jquery 的依賴關係,提高打包速度,如:

module.exports = {
 module: {
  noParse:/jquery/,//不去解析jquery中的依賴庫
 }
}

noParse 是 module 配置中的一個屬性,其屬性值為一個正則表示式,填入不被解析的模組名稱。

為了更清楚的展示 noParse 的作用,假設我們在入口檔案 index.js 中引入 bar.js 模組,同時這個 bar.js 模組中也引入了 foo.js 模組,foo.js 不再依賴其他模組了,那麼在不使用 noParse 的情況下,webpack 打包的時候,會先去分析 index.js 模組,發現其引入了 bar.js 模組,然後接著分析 bar.js 模組,發現其引入了 foo.js 模組,接著分析 foo.js 模組。

Entrypoint index = index.js

[./src/bar.js] 55 bytes {index} [built]

[./src/foo.js] 21 bytes {index} [built]

[./src/index.js] 81 bytes {index} [built]

而此時如果使用了 noParse: /bar/,那麼 webpack 打包的時候,會先去分析 index.js 模組,發現其引入了 bar.js 模組,但是由於 noParse 的作用,將不再繼續解析 bar.js 模組了,即不會去分析 bar.js 中引入的 foo.js 模組了。

Entrypoint index = index.js
[./src/bar.js] 55 bytes {index} [built]
[./src/index.js] 81 bytes {index} [built]

2、exclude: 在 loader 中使用 exclude 排除對某些目錄中的檔案處理,即引入指定目錄下的檔案時候,不使用對應的 loader 進行處理,exclude 是 loader 配置中的一個屬性,屬性值為正則表示式,如:

module.exports = {
 module: {
  rules: [
   {
    test: /.js$/,use: [
     {
      loader: "babel-loader",options: {
       presets: ["@babel/preset-env"],plugins: ["@babel/plugin-transform-runtime"]
      }
     }
    ],exclude: /node_modules/
   }
  ]
 }
}

3、使用 IgnorePlugin 來忽略某個模組中某些目錄中的模組引用,比如在引入某個模組的時候,該模組會引入大量的語言包,而我們不會用到那麼多語言包,如果都打包進專案中,那麼就會影響打包速度和最終包的大小,然後再引入需要使用的語言包即可,如:

專案根目錄下有一個 time 包,其中有一個 lang 包,lang 包中包含了各種語言輸出對應時間的 js 檔案,time 包下的 index.js 會引入 lang 包下所有的 js 檔案,那麼當我們引入 time 模組的時候,就會將 lang 包下的所有 js 檔案都打包進去,新增如下配置:

const webpack = require("webpack");
module.exports = {
 plugins: [
  new webpack.IgnorePlugin(/lang/,/time/)
 ]
}

引入 time 模組的時候,如果 time 模組中引入了其中的 lang 模組中的內容,那麼就忽略掉,即不引入 lang 模組中的內容,需要注意的是,這 /time/ 只是匹配資料夾和 time 模組的具體目錄位置無關,即只要是引入了目錄名為 time 中的內容就會生效。

4、使用 HappyPack:由於在打包過程中有大量的檔案需要交個 loader 進行處理,包括解析和轉換等操作,而由於 js 是單執行緒的,所以這些檔案只能一個一個地處理,而 HappyPack 的工作原理就是充分發揮 CPU 的多核功能,將任務分解給多個子程序去併發執行,子程序處理完後再將結果傳送給主程序,happypack 主要起到一個任務劫持的作用,在建立 HappyPack 例項的時候要傳入對應檔案的 loader,即 use 部分,loader 配置中將使用經過 HappyPack 包裝後的 loader 進行處理,如:

const HappyPack = require("happypack"); // 安裝並引入happypack模組
module.exports = {
 plugins: [
  new HappyPack({ // 這裡對處理css檔案的loader進行包裝
   id: "css",// 之前的loader根據具體的id進行引入
   use: ["style-loader","css-loader"],threads: 5 // 設定開啟的程序數
  })
 ],module: {
  rules: [
   {
    test: /.css$/,// 匹配以.css結尾的檔案
    use: ["happypack/loader?id=css"] //根據happypack例項中配置的id引入包裝後的laoder,這裡的happyPack的h可以大寫也可以小寫
   }
  ]
 }
}

webpack 要打包的檔案非常多的時候才需要使用 happypack 進行優化,因為開啟多程序也是需要耗時間的,所以檔案少的時候,使用 happypack 返回更耗時

5、抽離公共模組: 對於多入口情況,如果某個或某些模組,被兩個以上檔案所依賴,那麼可以將這個模組單獨抽離出來,不需要將這些公共的程式碼都打包進每個輸出檔案中,這樣會造成程式碼的重複和流量的浪費,即如果有兩個入口檔案 index.js 和 other.js,它們都依賴了 foo.js,那麼如果不抽離公共模組,那麼 foo.js 中的程式碼都會打包進最終輸出的 index.js 和 other.js 中去,即有兩份 foo.js 了。抽離公共模組也很簡單,直接在 optimization 中配置即可,如:

module.exports = {
  splitChunks: { // 分割程式碼塊,即抽離公共模組
   cacheGroups: { // 快取組
    common: { // 組名為common可自定義
     chunks: "initial",minSize: 0,// 檔案大小為0位元組以上才抽離
     minChunks: 2,// 被引用過兩次才抽離
     name: "common/foo",// 定義抽離出的檔案的名稱
    }
   }
  }
}

這樣就會將公共的 foo.js 模組抽離到 common 目錄下 foo.js 中了,但是如果我們也有多個檔案依賴了第三方模組如 jquery,如果按以上配置,那麼 jquery 也會被打包進 foo.js 中,會導致程式碼混亂,所以我們希望將 jquery 單獨抽出來,即與 foo.js 分開,我們可以複製一份以上配置,並通過設定抽離程式碼權重的方式來實現,即優先抽離出 jquery,如:

module.exports = {
  splitChunks: { // 分割程式碼塊,即抽離公共模組
   cacheGroups: { // 快取組
    common: { // 組名為common可自定義
     chunks: "initial",// 定義抽離出的檔案的名稱
    },verdor: {
     test: /node_modules/,priority: 1,// 設定打包權重,即優先抽離第三方模組
     chunks: "initial",// 被引用過兩次才抽離
     name: "common/jquery",// 定義抽離出的檔案的名稱
    }
   }
  }
}

這樣就會在 common 目錄下同時抽離出 foo.js 和 jquery.js 了,需要注意的是,程式碼的抽離必須是該模組沒有被排除打包,即該模組會被打包進輸出 bundle 中,如果第三方模組已經通過 externals 排除打包,則以上 vendor 配置無效。

6、按需載入,即在需要使用的時候才打包輸出,webpack 提供了 import() 方法,傳入要動態載入的模組,來動態載入指定的模組,當 webpack 遇到 import()語句的時候,不會立即去載入該模組,而是在用到該模組的時候,再去載入,也就是說打包的時候會一起打包出來,但是在瀏覽器中載入的時候並不會立即載入,而是等到用到的時候再去載入,比如,點選按鈕後才會載入某個模組,如:

const button = document.createElement("button");
button.innerText = "點我"
button.addEventListener("click",() => { // 點選按鈕後加載foo.js
 import("./foo").then((res) => { // import()返回的是一個Promise物件
  console.log(res);
 });
});
document.body.appendChild(button);

從中可以看到,import() 返回的是一個 Promise 物件,其主要就是利用 JSONP 實現動態載入,返回的 res 結果不同的 export 方式會有不同,如果使用的 module.exports 輸出,那麼返回的 res 就是 module.exports 輸出的結果;如果使用的是 ES6 模組輸出,即 export default 輸出,那麼返回的 res 結果就是 res.default,如:

// ES6模組輸出,res結果為
{default: "foo",__esModule: true,Symbol(Symbol.toStringTag): "Module"}

7、開啟模組熱更新: 模組熱更新可以做到在不重新整理網頁的情況下,更新修改的模組,只編譯變化的模組,而不用全部模組重新打包,大大提高開發效率,在未開啟熱更新的情況下,每次修改了模組,都會重新打包。

要開啟模組熱更新,那麼只需要在 devServer 配置中新增 hot:true 即可。當然僅僅開啟模組熱更新是不夠的,我們需要做一些類似監聽的操作,當監聽的模組發生變化的時候,重新載入該模組並執行,如:

module.exports = {
 devServer: {
  hot: true // 開啟熱更新
 }
}
----------
import foo from "./foo";
console.log(foo);
if (module.hot) {
 module.hot.accept("./foo",() => { // 監聽到foo模組發生變化的時候
  const foo = require("./foo"); // 重新引入該模組並執行
  console.log(foo);
 });
}

更多關於webpack配置車優化的文章請點選下面的相關文章