Webpack 再深入再總結

我是你的超級英雄發表於2020-01-10

前言

本文為作者第二次專門對 Webpack 的知識點進行深入和實踐,根據理解和實踐的結果進行總結的;

文章內容參考書籍《深入淺出 Webpack》,因為該書籍基於 Webpack 3.4.0 版本,本文的實踐基於 Webpack 4.28.2 版本,所以也踩了不少由於模組版本問題出現的坑,已經彙總到第 6 章節 踩坑彙總,大家記得避免踩坑;也印證了那句哲理:紙上得來終覺淺,絕知此事要躬行 ...

部落格 github地址為:github.com/fengshi123/… ,彙總了作者的所有部落格,也歡迎關注及 star ~

本文實踐 demo 的 github地址

一、Webpack 原理

1、構建作用

構建工具就是將原始碼轉換成可執行的 JavaScript、CSS、HTML 程式碼,包括以下內容:

  • 程式碼轉換:將 TypeScript 編譯成 JavaScript、將 SCSS 編譯成 CSS 等;

  • 檔案優化:壓縮 JavaScript、CSS、HTML 程式碼,壓縮合並圖片等;

  • 程式碼分割:提取多個頁面的公共程式碼,提取首屏不需要執行部分的程式碼,讓其非同步載入;

  • 模組合併:在採用模組化的專案裡會有很多個模組和檔案,需要通過構建功能將模組分類合併成一個檔案;

  • 自動重新整理:監聽本地原始碼的變化,自動重新構建、重新整理瀏覽器;

  • 程式碼校驗:在程式碼被提交到倉庫前需要校驗程式碼是否符合規範,以及單元測試是否通過;

  • 自動釋出:更新程式碼後,自動構建出線上釋出程式碼並傳輸給釋出系統;

2、核心概念

Webpack 有以下幾個核心概念:

  • Entry :入口,Webpack 執行構建的第一步將從 entry 開始,可抽象成輸入;

  • Module:模組,配置處理模組的規則;在 Webpack 裡一切皆模組,一個模組對應一個檔案;Webpack 會從配置的 Entry 開始遞迴找出所有依賴的模組;

  • Loader:模組轉換器,用於將模組的原內容按照需求轉換成新內容;

  • Resolve:配置尋找模組的規則;

  • Plugin:擴充套件外掛,在 Webpack 構建流程中的特定時機會廣播對應的事件,外掛可以監聽這些事情的發生,在特定的時機做對應的事情;

  • Output:輸出結果,在 Webpack 經過一系列處理並得出最終想要的程式碼後輸出結果;

  • Chunk:程式碼塊,一個 Chunk 由多個模組組合而成,用於程式碼合併與分割;

3、流程概述

(1)初始化引數:從配置檔案和 Shell 語句中讀取與合併引數,得出最終的引數;

(2)開始編譯:用上一步得到的引數初始化 Compiler 物件,載入所有配置的外掛,通過執行物件的 run 方法開始執行編譯;

(3)確定入口:根據配置中的 entry 找出所有入口檔案;

(4)編譯模組:從入口檔案出發,呼叫所有配置的 Loader 對模組進行翻譯,再找出該模組依賴的模組,再遞迴本步驟直到所有入口依賴的檔案都經過了本步驟的處理;

(5)完成模組編譯:在經過第 4 步使用 Loader 翻譯完所有模組後,得到了每個模組被翻譯後的最終內容及它們之間的依賴關係;

(6)輸出資源:根據入口和模組之間的依賴關係,組裝成一個個包含多個模組的 Chunk,再將每個 Chunk 轉換成一個單獨的檔案加入輸出列表中,這是可以修改輸出內容的最後機會;

(7)輸出完成:在確定好輸出內容後,根據配置確定輸出的路徑和檔名,將檔案的內容寫入檔案系統中;

在以上過程中,Webpack 會在特定的時間點廣播特定的事件,外掛在監聽到感興趣的事件後會執行特定的邏輯,並且外掛可以呼叫 Webpack 提供的 API 改變 Webpack 的執行結果;

二、Webpack 配置

1、Webpack 專案初始化

1、新建 Web 專案

新建一個目錄,再進入專案根目錄執行 npm init 來初始化最簡單的採用了模組化開發的專案;最終生成 package.json 檔案;

$ npm init
複製程式碼

2、安裝 Webpack 到本專案

(1)檢視 Webpack 版本

執行以下命令可以檢視 Webpack 的版本號

$ npm view webpack versions
複製程式碼

(2)安裝 Webpack

可以選擇(1)步驟羅列得到的 Webpack 版本號,也可以安裝最新穩定版、最新體驗版本,相關命令如下所示,我選擇安裝 4.28.2 版本(沒有為什麼,就想裝個 4.x 的版本);

// 安裝指定版本
npm i -D webpack@4.28.2

// 安裝最新穩定版
npm i -D webpack

// 安裝最新體驗版本
npm i -D webpack@beta
複製程式碼

(3)安裝 Webpack 腳手架

需要安裝 Webpack 腳手架,才能在命令視窗執行 Webpack 命令,執行以下命令安裝 Webpack 腳手架;

$ npm i -D webpack-cli
複製程式碼

3、使用 Webpack

使用 Webpack 構建一個採用 CommonJS 模組化編寫的專案;

(1)新建頁面入口檔案 index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Webpack</title>
</head>
<body>
<!--匯入 Webpack 輸出的 JavaScript 檔案-->
<script src="./dist/bundle.js"></script>  
</body>
</html>
複製程式碼

(2)新建需要用到的 JS 檔案

show.js 檔案

// 操作 DOM 元素,把 content 顯示到網頁上
function show(content) {
  window.document.getElementById('app').innerText = 'Hello,' + content;
}

// 通過 CommonJS 規範匯出 show 函式
module.exports = show;
複製程式碼

main.js 檔案

// 通過 CommonJS 規範匯入 show 函式
const show = require('./show.js');
// 執行 show 函式
show('Webpack');
複製程式碼

(3)新建 Webpack 配置檔案 webpack.config.js

const path = require('path');

module.exports = {
  // JavaScript 執行入口檔案
  entry: './main.js',
  output: {
    // 把所有依賴的模組合併輸出到一個 bundle.js 檔案
    filename: 'bundle.js',
    // 輸出檔案都放到 dist 目錄下
    path: path.resolve(__dirname, './dist'),
  }
};
複製程式碼

(4)執行 webpack 命令進行構建

在 package.json 檔案中配置編譯命令,如下所示:

  "scripts": {
    "build": "webpack --config webpack.config.js",
  },
複製程式碼

執行以下命令進行專案的 Webpack 編譯,成功後會在專案根目錄下生成編譯目錄 dist ;

$ npm run build
複製程式碼

(5)執行 index.html

編譯成功後,我們用瀏覽器開啟 index.html 檔案,能看到頁面成功顯示 “Hello Webpack”;

2、Loader 配置

本節通過為之前的例子新增樣式,來嘗試使用 Loader;

(1)新建樣式檔案 main.css

#app{
  text-align: center;
  color:'#999';
}
複製程式碼

(2)將 main.css 檔案引入入口檔案 main.js 中,如下所示:

// 通過 CommonJS 規範匯入 CSS 模組
require('./main.css');

// 通過 CommonJS 規範匯入 show 函式
const show = require('./show.js');
// 執行 show 函式
show('Webpack');
複製程式碼

(3)Loader 配置

以上修改後去執行 Webpack 構建是會報錯的,因為 Webpack 不原生支援解析 CSS 檔案。要支援非 JavaScript 型別的檔案,需要使用 Webpack 的 Loader 機制;

(3.1)執行以下命令,安裝 style-loader 和 css-loader,其中:

  • css-loader 用於讀取 CSS 檔案;
  • style-loader 把 CSS 內容注入到 JavaScript 中;
$ npm i -D style-loader css-loader
複製程式碼

(3.2)進行以下配置

module:{
rules:[
  {
	// 用正則去匹配要用該 loader 轉換的 CSS 檔案
	test:/\.css$/,
	use:['style-loader','css-loader']
  }
 ]
}
複製程式碼

(4)檢視結果

編譯後,重新整理 index.html ,檢視剛剛的樣式 loader 已經起作用;

1

3、Plugin 配置

(1)安裝樣式提取外掛 extract-text-webpack-plugin

$ npm i -D extract-text-webpack-plugin@next
複製程式碼

(2)plugin 檔案配置如下

  module:{
    rules:[
      {
        // 用正則去匹配要用該 loader 轉換的 CSS 檔案
        test:/\.css$/,
        use:ExtractTextPlugin.extract({
          use:['css-loader']
        }),
      }
    ]
  },
  plugins:[
    new ExtractTextPlugin({
       // 從 .js 檔案中提取出來的 .css 檔案的名稱
       filename:`[name]_[hash:8].css`
    }),
  ]
複製程式碼

(3)檢視結果

通過以上配置後,執行 Webapack 的執行命令,發現在 dist 目錄下,生成對應的 css 檔案;存在的坑點:

  • 我們需要手動將生成的 css 檔案引入到 index.html 中;
  • 修改 css 檔案後,會生成新的 css 檔案,原先的不會刪除;

4、使用 DevServer

(1)執行以下命令安裝 webpack-dev-server

$ npm i -D  webpack-dev-server
複製程式碼

在 package.json 中配置啟動命令

  "scripts": {
    "build": "webpack --config webpack.config.js",
    "dev": "webpack-dev-server",
  },
複製程式碼

執行命令後,就可以啟動 HTTP 服務

$ npm run dev
複製程式碼

啟動結果如下所示,我們可以通過 http://localhost:8080/ 訪問我們的 index.html 的demo

1

(2)實時預覽

我們在執行命令後面新增引數 --watch 實現實時預覽,配置如下所示:

  "scripts": {
    "dev": "webpack-dev-server --watch"
  },
複製程式碼

然後我們修改 main.js 的傳入引數,發現並不能實時預覽,也沒有報錯!!! why?

踩坑:

在 index.html 中需要將 js 的路徑修改為:

<script src="bundle.js"></script>  
複製程式碼

而不能是之前的(因為這個是編譯生成的,並不是通過 devServer 生成放在記憶體的)

<script src="./dist/bundle.js"></script> 
複製程式碼

(3)模組熱替換

可以通過配置 -- hot 進行模組熱替換;

三、Webpack 優化

關於優化的實踐之前有進行過實踐了,這裡不再累述,感興趣的童鞋可以檢視作者寫的另一篇文章《Vue專案Webpack優化實踐,構建效率提高50%

四、編寫 Loader

1、Loader 要點總結

(1)Loader 為模組轉換器,用於將模組的原內容按照需求轉換成新內容;

(2)Loader 的職責是單一的,只需要完成一種轉換,遵守單一職責原則;

(3)Webpack 為 Loader 提供了一系列 API 供 Loader 呼叫,例如:

  • loader-utils.getOptions( this ) 獲取使用者傳入的 options,
  • this.callback( ) 自定義返回結果,
  • this.async( ) 支援非同步操作;
  • this.context 當前檔案所在的目錄;
  • this.resource 當前處理檔案的完整請求路徑;
  • 其它等等

2、編寫 loader 原始碼

手寫一個 loader 原始碼,其功能是將 /hello/gi 轉換成 HELLO,當然這個 loader 其實沒啥實際意義,純碎是為了寫 loader 而寫 loader;當然如果你實際業務有需要編寫 loader 需求,那就要反思這個業務的合理性,因為龐大的社群,一般合理的需求都能找到對應的 loader。

(1)原始碼編寫

在原有的專案底下,新建目錄 custom-loader 作為我們編寫 loader 的名稱,執行 npm init 命令,新建一個模組化專案,然後新建 index.js 檔案,相關原始碼如下:

function convert(source){
  return source && source.replace(/hello/gi,'HELLO');
}

module.exports = function(content){
  return convert(content);
}
複製程式碼

(2)Npm link 模組註冊

正常我們安裝 Loader 是從 Npm 公有倉庫安裝,也即將 Loader 釋出到 Npm 倉庫,然後再安裝到本地使用;但是我們可以使用 Npm link 做到在不釋出模組的情況下,將本地的一個正在開發的模組的原始碼連結到專案的 node_modules 目錄下,讓專案可以直接使用本地的 Npm 模組;

在 custom-loader 目錄底下,執行以下命令,將本地模組註冊到全域性:

$ npm link
複製程式碼

成功結果如下:

1

然後在專案根目錄執行以下命令,將註冊到全域性的本地 Npm 模組連結到專案的 node_modules 下:

$ npm link custom-loader
複製程式碼

成功結果如下,並且在 node_modules 目錄下能查詢到對應的 loader;

1

3、Webpack 中配置編寫的 loader

該配置跟第一章節的 Webpack 配置並沒有任何區別,這裡不再詳述,配置參考如下:

  module:{
    rules:[
      {
        test:/\.js/,
        use:['custom-loader'],
        include:path.resolve(__dirname,'show')
      }
    ]
  }
複製程式碼

執行執行 or 編譯命令,就能看到我們的 loader 起作用了。

五、編寫 Plugin

Webpack 就像一條生產線,要經過一系列處理流程後才能將原始檔轉換成輸出結果,這條生產線上的每個處理流程的職責都是單一的,多個流程之間存在依賴關係,只有在完成當前處理後才能提交給下一個流程去處理。外掛就像生產線中的某個功能,在特定的時機對生產線上的資源進行處理。

Webpack 通過 Tapable 來組織這條複雜的生產線。 Webpack 在執行過程中會廣播事件,外掛只需要監聽它所關心的事件,就能加入到這條生產線中,去改變生產線的運作。 Webpack 的事件流機制保證了外掛的有序性,使得整個系統擴充套件性很好。

1、Plugin 要點總結

  • Webpack 在編譯過程中,會廣播很多事件,例如 run、compile、done、fail 等等,可以檢視官網;
  • Webpack 的事件流機制應用了觀察者模式,我們編寫的外掛可以監聽 Webpack 事件來觸發對應的處理邏輯;
  • 外掛中可以使用很多 Webpack 提供的 API,例如讀取輸出資源、程式碼塊、模組及依賴等;

2、編寫 Plugin 原始碼

手寫一個 plugin 原始碼,其功能是在 Webpack 編譯成功或者失敗時輸出提示;當然這個 plugin 其實沒啥實際意義,純碎是為了寫 plugin 而寫 plugin;當然如果你實際業務有需要編寫 plugin 需求,那就要反思這個業務的合理性,因為龐大的社群,一般合理的需求都能找到對應的 plugin。

(1)原始碼編寫

在原有的專案底下,新建目錄 custom-plugin 作為我們編寫 plugin 的名稱,執行 npm init 命令,新建一個模組化專案,然後新建 index.js 檔案,相關原始碼如下:

class CustomPlugin{
  constructor(doneCallback, failCallback){
     // 儲存在建立外掛例項時傳入的回撥函式
     this.doneCallback = doneCallback;
     this.failCallback = failCallback;
  }
  apply(compiler){
    // 成功完成一次完整的編譯和輸出流程時,會觸發 done 事件
    compiler.plugin('done',(stats)=>{
      this.doneCallback(stats);
    })
    // 在編譯和輸出的流程中遇到異常時,會觸發 failed 事件
    compiler.plugin('failed',(err)=>{
      this.failCallback(err);
    })
  }
}
module.exports = CustomPlugin;
複製程式碼

(2)Npm link 模組註冊

跟 Loader 註冊一樣,我們使用 npm link 進行註冊;

在 custom-plugin 目錄底下,執行以下命令,將本地模組註冊到全域性:

$ npm link
複製程式碼

然後在專案根目錄執行以下命令,將註冊到全域性的本地 Npm 模組連結到專案的 node_modules 下:

$ npm link custom-plugin
複製程式碼

如果一切順利,可以在 node_modules 目錄下能查詢到對應的 plugin;

3、Webpack 中配置編寫的 plugin

該配置跟第一章節的 Webpack 配置並沒有任何區別,這裡不再詳述,配置參考如下:

  plugins:[
    new CustomPlugin(
     stats => {console.info('編譯成功!')},
     err => {console.error('編譯失敗!')}
   ),
  ],
複製程式碼

執行執行 or 編譯命令,就能看到我們的 plugin 起作用了。

六、踩坑彙總

1、css-loader 以下配置

rules:[
  {
	// 用正則去匹配要用該 loader 轉換的 CSS 檔案
	test:/\.css$/,
	use:['style-loader','css-loader?minimize']
  }
]
複製程式碼

報以下錯誤:

 - options has an unknown property 'minimize'. These properties are valid:
   object { url?, import?, modules?, sourceMap?, importLoaders?, localsConventio               n?, onlyLocals?, esModule? }
複製程式碼

原因:

minimize 屬性在新版本已經被移除,

解決:

先去掉 minimize 選項;

2、ExtractTextPlugin 編譯以下錯誤:

1

原因:

extract-text-webpack-plugin 版本號問題

參考連結:github.com/webpack/web…

解決:

重新安裝 extract-text-webpack-plugin

$ npm i -D extract-text-webpack-plugin@next
複製程式碼

3、修復第2個坑之後,ExtractTextPlugin 編譯繼續報以下錯誤:

1

原因:

不存在 contenthash 這個變數

解決:

更改 extract-text-webpack-plugin 的配置:

  plugins:[
    new ExtractTextPlugin({
       // 從 .js 檔案中提取出來的 .css 檔案的名稱
       filename:`[name]_[hash:8].css`
    }),
  ]
複製程式碼

4、新增 HappyPack 後,編譯 CSS 檔案時報以下錯誤:

1

原因:

css-loader 版本的問題

解決:

重新安裝 css-loader@3.2.0

七、總結

本文主要基於 Webpack 的作用、核心概念、流程,Webpack 的基礎配置,Webpack 優化,編寫 Loader,編寫 Plugin ,從理論到實踐,從基礎到較難,對 Webpack 進行總結掌握,希望對你也有幫助。還是那句話:紙上得來終覺淺,絕知此事要躬行 ...,如果你沒有手敲過,一定要多動動手 !

部落格 github地址為:github.com/fengshi123/… ,彙總了作者的所有部落格,也歡迎關注及 star ~

本文實踐 demo 的 github地址

相關文章