webpack loader和plugin編寫
// 入口檔案 entry: { app: './src/js/index.js', }, // 輸出檔案 output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: '/'//確保檔案資源能夠在 http://localhost:3000 下正確訪問 }, // 開發者工具 source-map devtool: 'inline-source-map', // 建立開發者伺服器 devServer: { contentBase: './dist', hot: true// 熱更新 }, plugins: [ // 刪除dist目錄 new CleanWebpackPlugin(['dist']), // 重新穿件html檔案 new HtmlWebpackPlugin({ title: 'Output Management' }), // 以便更容易檢視要修補(patch)的依賴 new webpack.NamedModulesPlugin(), // 熱更新模組 new webpack.HotModuleReplacementPlugin() ], // 環境 mode: "development", // loader配置 module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /\.(png|svg|jpg|gif)$/, use: [ 'file-loader' ] } ] } 複製程式碼
這裡面我們重點關注 module和plugins屬性,因為今天的重點是編寫loader和plugin,需要配置這兩個屬性。
1.2 打包原理
- 識別入口檔案
- 通過逐層識別模組依賴。(Commonjs、amd或者es6的import,webpack都會對其進行分析。來獲取程式碼的依賴)
- webpack做的就是分析程式碼。轉換程式碼,編譯程式碼,輸出程式碼
- 最終形成打包後的程式碼
這些都是webpack的一些基礎知識,對於理解webpack的工作機制很有幫助。
2 loader
OK今天第一個主角登場
2.1 什麼是loader?
loader是檔案載入器,能夠載入資原始檔,並對這些檔案進行一些處理,諸如編譯、壓縮等,最終一起打包到指定的檔案中
- 處理一個檔案可以使用多個loader,loader的執行順序是和本身的順序是相反的,即最後一個loader最先執行,第一個loader最後執行。
- 第一個執行的loader接收原始檔內容作為引數,其他loader接收前一個執行的loader的返回值作為引數。最後執行的loader會返回此模組的JavaScript原始碼
2.2 手寫一個loader
需求:
- 處理.txt檔案
- 對字串做反轉操作
- 首字母大寫
例如:abcdefg轉換後為Gfedcba
OK,我們開始
1)首先建立兩個loader(這裡以本地loader為例)
為什麼要建立兩個laoder?理由後面會介紹

reverse-loader.js
module.exports = function (src) { if (src) { console.log('--- reverse-loader input:', src) src = src.split('').reverse().join('') console.log('--- reverse-loader output:', src) } return src; } 複製程式碼
uppercase-loader.js
module.exports = function (src) { if (src) { console.log('--- uppercase-loader input:', src) src = src.charAt(0).toUpperCase() + src.slice(1) console.log('--- uppercase-loader output:', src) } // 這裡為什麼要這麼寫?因為直接返回轉換後的字串會報語法錯誤, // 這麼寫import後轉換成可以使用的字串 return `module.exports = '${src}'` } 複製程式碼
看,loader結構是不是很簡單,接收一個引數,並且return一個內容就ok了。
然後建立一個txt檔案

2)mytest.txt
abcdefg 複製程式碼
3)現在開始配置webpack
module.exports = { entry: { index: './src/js/index.js' }, plugins: [...], optimization: {...}, output: {...}, module: { rules: [ ..., { test: /\.txt$/, use: [ './loader/uppercase-loader.js', './loader/reverse-loader.js' ] } ] } } 複製程式碼
這樣就配置完成了
4)我們在入口檔案中匯入這個指令碼
為什麼這裡需要匯入呢,我們不是配置了webapck處理所有的.txt檔案麼?
因為webpack會做過濾,如果不引用該檔案的話,webpack是不會對該檔案進行打包處理的,那麼你的loader也不會執行
import _ from 'lodash'; import txt from '../txt/mytest.txt' import '../css/style.css' function component() { var element = document.createElement('div'); var button = document.createElement('button'); var br = document.createElement('br'); button.innerHTML = 'Click me and look at the console!'; element.innerHTML = _.join('【' + txt + '】'); element.className = 'hello' element.appendChild(br); element.appendChild(button); // Note that because a network request is involved, some indication // of loading would need to be shown in a production-level site/app. button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => { var print = module.default; print(); }); return element; } document.body.appendChild(component()); 複製程式碼
package.json配置
{ ..., "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack --config webpack.prod.js", "start": "webpack-dev-server --open --config webpack.dev.js", "server": "node server.js" }, ... } 複製程式碼
然後執行命令
npm run build 複製程式碼

這樣我們的loader就寫完了。
現在回答為什麼要寫兩個loader?
看到執行的順序沒,我們的配置的是這樣的
use: [ './loader/uppercase-loader.js', './loader/reverse-loader.js' ] 複製程式碼
正如前文所說, 處理一個檔案可以使用多個loader,loader的執行順序是和本身的順序是相反的
我們也可以自己寫loader解析自定義模板,像vue-loader是非常複雜的,它內部會寫大量的對.vue檔案的解析,然後會生成對應的html、js和css。
我們這裡只是講述了一個最基礎的用法,如果有更多的需要,可以檢視《loader官方文件》
3 plugin
3.1 什麼是plugin?
在 Webpack 執行的生命週期中會廣播出許多事件,Plugin 可以監聽這些事件,在合適的時機通過 Webpack 提供的 API 改變輸出結果。
plugin和loader的區別是什麼?
對於loader,它就是一個轉換器,將A檔案進行編譯形成B檔案,這裡操作的是檔案,比如將A.scss或A.less轉變為B.css,單純的檔案轉換過程
plugin是一個擴充套件器,它豐富了wepack本身,針對是loader結束後,webpack打包的整個過程,它並不直接操作檔案,而是基於事件機制工作,會監聽webpack打包過程中的某些節點,執行廣泛的任務。
3.2 一個最簡的外掛
/plugins/MyPlugin.js(本地外掛)
class MyPlugin { // 構造方法 constructor (options) { console.log('MyPlugin constructor:', options) } // 應用函式 apply (compiler) { // 繫結鉤子事件 compiler.plugin('compilation', compilation => { console.log('MyPlugin') )) } } module.exports = MyPlugin 複製程式碼
webpack配置
const MyPlugin = require('./plugins/MyPlugin') module.exports = { entry: { index: './src/js/index.js' }, plugins: [ ..., new MyPlugin({param: 'xxx'}) ], ... }; 複製程式碼
這就是一個最簡單的外掛(雖然我們什麼都沒幹)
- webpack 啟動後,在讀取配置的過程中會先執行 new MyPlugin(options) 初始化一個 MyPlugin 獲得其例項。
- 在初始化 compiler 物件後,再呼叫 myPlugin.apply(compiler) 給外掛例項傳入 compiler 物件。
- 外掛例項在獲取到 compiler 物件後,就可以通過 compiler.plugin(事件名稱, 回撥函式) 監聽到 Webpack 廣播出來的事件。
- 並且可以通過 compiler 物件去操作 webpack。
看到這裡可能會問compiler是啥,compilation又是啥?
-
Compiler 物件包含了 Webpack 環境所有的的配置資訊,包含 options,loaders,plugins 這些資訊,這個物件在 Webpack 啟動時候被例項化,它是全域性唯一的,可以簡單地把它理解為 Webpack 例項;
-
Compilation 物件包含了當前的模組資源、編譯生成資源、變化的檔案等。當 Webpack 以開發模式執行時,每當檢測到一個檔案變化,一次新的 Compilation 將被建立。Compilation 物件也提供了很多事件回撥供外掛做擴充套件。通過 Compilation 也能讀取到 Compiler 物件。
Compiler 和 Compilation 的區別在於:
Compiler 代表了整個 Webpack 從啟動到關閉的生命週期,而 Compilation 只是代表了一次新的編譯。
3.3 事件流
- webpack 通過 Tapable 來組織這條複雜的生產線。
- webpack 的事件流機制保證了外掛的有序性,使得整個系統擴充套件性很好。
- webpack 的事件流機制應用了觀察者模式,和 Node.js 中的 EventEmitter 非常相似。
繫結事件
compiler.plugin('event-name', params => { ... }); 複製程式碼
觸發事件
compiler.apply('event-name',params) 複製程式碼
3.4 需要注意的點
- 只要能拿到 Compiler 或 Compilation 物件,就能廣播出新的事件,所以在新開發的外掛中也能廣播出事件,給其它外掛監聽使用。
- 傳給每個外掛的 Compiler 和 Compilation 物件都是同一個引用。也就是說在一個外掛中修改了 Compiler 或 Compilation 物件上的屬性,會影響到後面的外掛。
- 有些事件是非同步的,這些非同步的事件會附帶兩個引數,第二個引數為回撥函式,在外掛處理完任務時需要呼叫回撥函式通知 webpack,才會進入下一處理流程 。例如:
compiler.plugin('emit',function(compilation, callback) { ... // 處理完畢後執行 callback 以通知 Webpack // 如果不執行 callback,執行流程將會一直卡在這不往下執行 callback(); }); 複製程式碼
關於complier和compilation,webpack定義了大量的鉤子事件。開發者可以根據自己的需要在任何地方進行自定義處理。
ofollow,noindex">《compiler鉤子文件》
3.5 手寫一個plugin
場景:
小程式mpvue專案,通過webpack編譯,生成子包(我們作為分包引入到主程式中),然後考入主包當中。生成子包後,裡面的公共靜態資源wxss引用地址需要加入分包的字首:/subPages/enjoy_given。
在未編寫外掛前,生成的資源是這樣的,這個路徑如果作為分包引入主包,是沒法正常訪問資源的。

所以需求來了:
修改dist/static/css/pages目錄下,所有頁面的樣式檔案(wxss檔案)引入公共資源的路徑。
因為所有頁面的樣式都會引用通用樣式vender.wxss
那麼就需要把@import "/static/css/vendor.wxss"; 改為:@import "/subPages/enjoy_given/static/css/vendor.wxss"; 複製程式碼
OK 開始!
1)建立外掛檔案 CssPathTransfor.js

CssPathTransfor.js
class CssPathTransfor { apply (compiler) { compiler.plugin('emit', (compilation, callback) => { console.log('--CssPathTransfor emit') // 遍歷所有資原始檔 for (var filePathName in compilation.assets) { // 檢視對應的檔案是否符合指定目錄下的檔案 if (/static\/css\/pages/i.test(filePathName)) { // 引入路徑正則 const reg = /\/static\/css\/vendor\.wxss/i // 需要替換的最終字串 const finalStr = '/subPages/enjoy_given/static/css/vendor.wxss' // 獲取檔案內容 let content = compilation.assets[filePathName].source() || '' content = content.replace(reg, finalStr) // 重寫指定輸出模組內容 compilation.assets[filePathName] = { source () { return content; }, size () { return content.length; } } } } callback() }) } } module.exports = CssPathTransfor 複製程式碼
看著挺多,實際就是遍歷compilation.assets模組。對符合要求的檔案進行正則替換。
2)修改webpack配置
var baseWebpackConfig = require('./webpack.base.conf') var CssPathTransfor = require('../plugins/CssPathTransfor.js') var webpackConfig = merge(baseWebpackConfig, { module: {...}, devtool: config.build.productionSourceMap ? '#source-map' : false, output: {...}, plugins: [ ..., // 配置外掛 new CssPathTransfor(), ] }) 複製程式碼
外掛編寫完成後,執行編譯命令

搞定~
如果有更多的需求可以參考《如何寫一個外掛》