前言
本文為作者第二次專門對 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 已經起作用;
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
(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
複製程式碼
成功結果如下:
然後在專案根目錄執行以下命令,將註冊到全域性的本地 Npm 模組連結到專案的 node_modules 下:
$ npm link custom-loader
複製程式碼
成功結果如下,並且在 node_modules 目錄下能查詢到對應的 loader;
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 編譯以下錯誤:
原因:
extract-text-webpack-plugin 版本號問題
解決:
重新安裝 extract-text-webpack-plugin
$ npm i -D extract-text-webpack-plugin@next
複製程式碼
3、修復第2個坑之後,ExtractTextPlugin 編譯繼續報以下錯誤:
原因:
不存在 contenthash 這個變數
解決:
更改 extract-text-webpack-plugin 的配置:
plugins:[
new ExtractTextPlugin({
// 從 .js 檔案中提取出來的 .css 檔案的名稱
filename:`[name]_[hash:8].css`
}),
]
複製程式碼
4、新增 HappyPack 後,編譯 CSS 檔案時報以下錯誤:
原因:
css-loader 版本的問題
解決:
重新安裝 css-loader@3.2.0
七、總結
本文主要基於 Webpack 的作用、核心概念、流程,Webpack 的基礎配置,Webpack 優化,編寫 Loader,編寫 Plugin ,從理論到實踐,從基礎到較難,對 Webpack 進行總結掌握,希望對你也有幫助。還是那句話:紙上得來終覺淺,絕知此事要躬行 ...,如果你沒有手敲過,一定要多動動手 !
部落格 github地址為:github.com/fengshi123/… ,彙總了作者的所有部落格,也歡迎關注及 star ~
本文實踐 demo 的 github地址