閱讀目錄
一:webpack插件的基本原理
webpack構建工具大家應該不陌生了,那么下面我們來簡單的了解下什么是webpack的插件。比如我現在寫了一個插件叫 "kongzhi-plugin" 這個插件。那么這個插件在處理webpack編譯過程中會處理一些特定的任務。
比如我們現在在webpack.config.js 中引入了一個如下插件:
// 引入打包html文件 const HtmlWebpackPlugin = require('html-webpack-plugin');
然后我們需要如下使用該插件:
module.exports = { plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' // 模版文件 }), ] };
如上就是一個 HtmlWebpackPlugin 插件 及在webpack中使用的方式了。現在我們需要實現一個類似的webpack的插件。
webpack打包是一種事件流的機制,它的原理是將各個插件串聯起來。那么實現這一切的核心就是tapable,要想深入了解 tapable的知識可以看我之前的一篇文章.
tapable它可以暴露出掛載plugin的方法。可以讓我們能將plugin控制在webpack事件流上運行。
tapable給我們暴露了很多鈎子類,能為我們的插件提供掛載的鈎子。
如下代碼所示:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require('tapable');
如上各個鈎子的含義及使用方式,可以看我之前這篇文章的介紹。
下面我們來看個簡單的demo,我們會定義一個 KongZhiClass 類,在內部我們創建一個 hooks 這個對象,然后在該對象上分別創建同步鈎子kzSyncHook及異步鈎子 kzAsyncHook。 然后分別執行,代碼如下:
const { SyncHook, AsyncParallelHook } = require('tapable'); // 創建類 class KongZhiClass { constructor() { this.hooks = { kzSyncHook: new SyncHook(['name', 'age']), kzAsyncHook: new AsyncParallelHook(['name', 'age']) } } } // 實例化 const myName = new KongZhiClass(); // 綁定同步鈎子 myName.hooks.kzSyncHook.tap("eventName1", (name, age) => { console.log(`同步事件eventName1: ${name} this year ${age} 周歲了, 可是還是單身`); }); // 綁定一個異步Promise鈎子 myName.hooks.kzAsyncHook.tapPromise('eventName2', (name, age) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(`異步事件eventName2: ${name} this year ${age}周歲了,可是還是單身`); }, 1000); }); }); // 執行同步鈎子 myName.hooks.kzSyncHook.call('空智', 31); // 執行異步鈎子 myName.hooks.kzAsyncHook.promise('空智', 31).then(() => { console.log('異步事件執行完畢'); }, (err) => { console.log('異步事件執行異常:' + err); })
執行結果如下:
如上是我們使用的 tapable 的使用方式,現在我們需要使用tapable的demo來和我們的webpack的插件相關聯起來,我們要如何做呢?
我們可以將上面的代碼來拆分成兩個文件:compiler.js、main.js. (main.js 是入口文件)
假如我們的項目結構如下:
|--- tapable項目 | |--- node_modules | |--- public | | |--- js | | | |--- main.js | | | |--- compiler.js | |--- package.json | |--- webpack.config.js
compiler.js 需要做的事情如下:
1. 定義一個 Compiler 類,接收一個options對象參數,該參數是從main.js中的MyPlugin類的實列對象。該對象下有 apply函數。
2. 在該類中我們定義了run方法,我們在main.js 中執行該run函數就可以自動執行對應的插件了。
代碼如下:
const { SyncHook, AsyncParallelHook } = require('tapable'); class Compiler { constructor(options) { this.hooks = { kzSyncHook: new SyncHook(['name', 'age']), kzAsyncHook: new AsyncParallelHook(['name', 'age']) }; let plugins = options.plugins; if (plugins && plugins.length > 0) { plugins.forEach(plugin => plugin.apply(this)); } } run() { console.log('開始執行了---------'); this.kzSyncHook('我是空智', 31); this.kzAsyncHook('我是空智', 31); } kzSyncHook(name, age) { this.hooks.kzSyncHook.call(name, age); } kzAsyncHook(name, age) { this.hooks.kzAsyncHook.callAsync(name, age); } } module.exports = Compiler;
main.js 需要做的事情如下:
1. 引入 compiler.js 文件。
2. 定義一個自己的插件,比如叫 MyPlugin 類,該類下有 apply 函數。該函數有一個 compiler 參數,該參數就是我們的 compiler.js 中的實列對象。然后我們會使用 compiler 實列對象去調用 compiler.js 里面的函數。因此就可以自動執行了。
代碼如下所示:
const Compiler = require('./compiler'); class MyPlugin { constructor() { } apply(compiler) { compiler.hooks.kzSyncHook.tap("eventName1", (name, age) => { console.log(`同步事件eventName1: ${name} this year ${age} 周歲了, 可是還是單身`); }); compiler.hooks.kzAsyncHook.tapAsync('eventName2', (name, age) => { setTimeout(() => { console.log(`異步事件eventName2: ${name} this year ${age}周歲了,可是還是單身`); }, 1000) }); } } const myPlugin = new MyPlugin(); const options = { plugins: [myPlugin] }; const compiler = new Compiler(options); compiler.run();
最后執行的效果如下所示:
如上就是我們仿照Compiler和webpack的插件原理邏輯實現的一個簡單demo。也就是說在webpack源碼里面也是通過類似的方式來做的。
上面只是一個簡單實現的基本原理,但是在我們的webpack當中我們要如何實現一個插件呢?
在我們的webpack官網中會介紹編寫一個插件要滿足如下條件, 官網地址
從官網得知:編寫一個webpack插件需要由以下組成:
1. 一個javascript命名函數。
2. 在插件函數的prototype上定義一個 apply 方法。
3. 指定一個綁定到webpack自身的鈎子函數。
4. 處理webpack內部實列的特定數據。
5. 功能完成后調用webpack提供的回調函數。
一個最基礎的插件代碼像如下這個樣子:
// 一個javascript命名函數 function MyExampleWebpackPlugin() { }; // 在插件函數的prototype上定義一個 apply 方法 MyExampleWebpackPlugin.prototype.apply = function(compiler) { // 指定一個掛載到webpack自身的事件鈎子。 compiler.plugin('webpacksEventHook', function(compilation, callback) { console.log('這是一個插件demo'); // 功能完成后調用 webpack 提供的回調 callback(); }) } // 導出plugin module.exports = MyExampleWebpackPlugin;
在我們使用該plugin的時候,相關調用及配置代碼如下:
const MyExampleWebpackPlugin = require('./MyExampleWebpackPlugin'); module.exports = { plugins: [ new MyExampleWebpackPlugin(options) ] };
webpack啟動后,在讀取配置的過程中會先執行 new MyExampleWebpackPlugin(options) 初始化MyExampleWebpackPlugin來獲得一個實列。然后我們會把該實列當做參數傳遞給我們的Compiler對象,然后會實列化 Compiler類(這個邏輯可以結合看我們上面實現了一個簡單的demo中 的main.js和compiler.js的代碼結合起來理解)。在Compiler類中,我們會獲取到options的這個參數,該參數是一個對象,該對象下有一個 plugins 這個屬性。然后遍歷該屬性,然后依次執行 某項插件中的apply方法,即:myExampleWebpackPlugin.apply(compiler); 給插件傳遞compiler對象。插件實列獲取該compiler對象后,就可以通過 compiler.plugin('事件名稱', '回調函數'); 監聽到webpack廣播出來的事件.(這個地方我們可以看我們上面的main.js中的如下代碼可以看到, 在我們的main.js代碼中有這樣代碼:compiler.hooks.kzSyncHook.tap("eventName1", (name, age) => {}));
如上就是一個簡單的Plugin的插件原理(切記:結合上面的demo中main.js和compiller.js來理解效果會更好)。
二:理解 Compiler對象 和 Compilation 對象
在開發Plugin時我們最常用的兩個對象就是 Compiler 和 Compilation, 他們是Plugin和webpack之間的橋梁。
Compiler對象
Compiler 對象包含了Webpack環境所有的配置信息,包含options,loaders, plugins這些項,這個對象在webpack啟動時候被實例化,它是全局唯一的。我們可以把它理解為webpack的實列。
基本源碼可以看如下:
// webpack/lib/webpack.js const Compiler = require("./Compiler") const webpack = (options, callback) => { ... // 初始化 webpack 各配置參數 options = new WebpackOptionsDefaulter().process(options); // 初始化 compiler 對象,這里 options.context 為 process.cwd() let compiler = new Compiler(options.context); compiler.options = options // 往 compiler 添加初始化參數 new NodeEnvironmentPlugin().apply(compiler) // 往 compiler 添加 Node 環境相關方法 for (const plugin of options.plugins) { plugin.apply(compiler); } ... }
如上我們可以看到,Compiler對象包含了所有的webpack可配置的內容。開發插件時,我們可以從 compiler 對象中拿到所有和 webpack 主環境相關的內容。
compilation 對象
compilation 對象包含了當前的模塊資源、編譯生成資源、文件的變化等。當webpack在開發模式下運行時,每當檢測到一個文件發生改變的時候,那么一次新的 Compilation將會被創建。從而生成一組新的編譯資源。
Compiler對象 與 Compilation 對象 的區別是:Compiler代表了是整個webpack從啟動到關閉的生命周期。Compilation 對象只代表了一次新的編譯。
Compiler對象的事件鈎子,我們可以看官網. 或者我們也可以查看它的源碼也可以看得到,查看源碼
我們可以了解常見的事件鈎子:下面是一些比較常見的事件鈎子及作用:
鈎子 作用 參數 類型 after-plugins 設置完一組初始化插件之后 compiler sync after-resolvers 設置完 resolvers 之后 compiler sync run 在讀取記錄之前 compiler async compile 在創建新 compilation之前 compilationParams sync compilation compilation 創建完成 compilation sync emit 在生成資源並輸出到目錄之前 compilation async after-emit 在生成資源並輸出到目錄之后 compilation async done 完成編譯 stats sync
理解webpack中的事件流
我們可以把webpack理解為一條生產線,需要經過一系列處理流程后才能將源文件轉換成輸出結果。
這條生產線上的每個處理流程的職責都是單一的,多個流程之間會存在依賴關系,只有完成當前處理后才能交給下一個流程去處理。
我們的插件就像一個插入到生產線中的一個功能,在特定的時機對生產線上的資源會做處理。webpack它是通過 Tapable來組織這條復雜的生產線的。
webpack在運行的過程中會廣播事件,插件只需要關心監聽它的事件,就能加入到這條生產線中。然后會執行相關的操作。
webpack的事件流機制它能保證了插件的有序性,使整個系統的擴展性好。事件流機制使用了觀察者模式來實現的。比如如下代碼:
/* * 廣播事件 * myPlugin-name 為事件名稱 * params 為附帶的參數 */ compiler.apply('myPlugin-name', params); /* * 監聽名稱為 'myPlugin-name' 的事件,當 myPlugin-name 事件發生時,函數就會執行。 */ compiler.hooks.myPlugin-name.tap('myPlugin-name', function(params) { });
三:插件中常用的API
1. 讀取輸出資源、模塊及依賴
在我們的emit鈎子事件發生時,表示的含義是:源文件的轉換和組裝已經完成了,在這里事件鈎子里面我們可以讀取到最終將輸出的資源、代碼塊、模塊及對應的依賴文件。並且我們還可以輸出資源文件的內容。比如插件代碼如下:
class MyPlugin { apply(compiler) { compiler.plugin('emit', function(compilation, callback) { // compilation.chunks 是存放了所有的代碼塊,是一個數組,我們需要遍歷 compilation.chunks.forEach(function(chunk) { /* * chunk 代表一個代碼塊,代碼塊它是由多個模塊組成的。 * 我們可以通過 chunk.forEachModule 能讀取組成代碼塊的每個模塊 */ chunk.forEachModule(function(module) { // module 代表一個模塊。 // module.fileDependencies 存放當前模塊的所有依賴的文件路徑,它是一個數組 module.fileDependencies.forEach(function(filepath) { console.log(filepath); }); }); /* webpack 會根據chunk去生成輸出的文件資源,每個chunk都對應一個及以上的輸出文件。 比如在 Chunk中包含了css 模塊並且使用了 ExtractTextPlugin 時, 那么該Chunk 就會生成 .js 和 .css 兩個文件 */ chunk.files.forEach(function(filename) { // compilation.assets 是存放當前所有即將輸出的資源。 // 調用一個輸出資源的 source() 方法能獲取到輸出資源的內容 const source = compilation.assets[filename].source(); }); }); /* 該事件是異步事件,因此要調用 callback 來通知本次的 webpack事件監聽結束。 如果我們沒有調用callback(); 那么webpack就會一直卡在這里不會往后執行。 */ callback(); }) } }
2. 監聽文件變化
webpack讀取文件的時候,它會從入口模塊去讀取,然后依次找出所有的依賴模塊。當入口模塊或依賴的模塊發生改變的時候,那么就會觸發一次新的 Compilation。
在我們開發插件的時候,我們需要知道是那個文件發生改變,導致了新的Compilation, 我們可以添加如下代碼進行監聽。
// 當依賴的文件發生改變的時候 會觸發 watch-run 事件 class MyPlugin { apply(compiler) { compiler.plugin('watch-run', (watching, callback) => { // 獲取發生變換的文件列表 const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes; // changedFiles 格式為鍵值對的形式,當鍵為發生變化的文件路徑 if (changedFiles[filePath] !== undefined) { // 對應的文件就發生了變化了 } callback(); }); /* 默認情況下Webpack只會監聽入口文件或其依賴的模塊是否發生變化,但是在有些情況下比如html文件發生改變的時候,那么webpack 就會去監聽html文件的變化。因此就不會重新觸發新的 Compilation。因此為了監聽html文件的變化,我們需要把html文件加入到 依賴列表中。因此我們需要添加如下代碼: */ compiler.plugin('after-compile', (compilation, callback) => { /* 如下的參數filePath是html文件路徑,我們把HTML文件添加到文件依賴表中,然后我們的webpack會去監聽html模塊文件, html模板文件發生改變的時候,會重新啟動下重新編譯一個新的 Compilation. */ compilation.fileDependencies.push(filePath); callback(); }) } }
3. 修改輸出資源
我們在第一點說過:在我們的emit鈎子事件發生時,表示的含義是:源文件的轉換和組裝已經完成了,在這里事件鈎子里面我們可以讀取到最終將輸出的資源、代碼塊、模塊及對應的依賴文件。因此如果我們現在要修改輸出資源的內容的話,我們可以在emit事件中去做修改。那么所有輸出的資源會存放在 compilation.assets中,compilation.assets是一個鍵值對,鍵為需要輸出的文件名,值為文件對應的內容。如下代碼:
class MyPlugin { apply(compiler) { compiler.plugin('emit', (compilation, callback) => { // 設置名稱為 fileName 的輸出資源 compilation.assets[fileName] = { // 返回文件內容 source: () => { // fileContent 即可以代表文本文件的字符串,也可以是代表二進制文件的buffer return fileContent; }, // 返回文件大小 size: () => { return Buffer.byteLength(fileContent, 'utf8'); } }; callback(); }); // 讀取 compilation.assets 代碼如下: compiler.plugin('emit', (compilation, callback) => { // 讀取名稱為 fileName 的輸出資源 const asset = compilation.assets[fileName]; // 獲取輸出資源的內容 asset.source(); // 獲取輸出資源的文件大小 asset.size(); callback(); }); } }
4. 判斷webpack使用了哪些插件
在我們開發一個插件的時候,我們需要根據當前配置是否使用了其他某個插件,我們可以通過讀取webpack某個插件配置的情況,比如來判斷我們當前是否使用了 HtmlWebpackPlugin 插件。代碼如下:
/* 判斷當前配置使用了 HtmlWebpackPlugin 插件。 compiler參數即為 webpack 在 apply(compiler) 中傳入的參數 */ function hasHtmlWebpackPlugin(compiler) { // 獲取當前配置下所有的插件列表 const plugins = compiler.options.plugins; // 去plugins中尋找有沒有 HtmlWebpackPlugin 的實列 return plugins.find(plugin => plugin.__proto__.constructor === HtmlWebpackPlugin) !== null; }
四:編寫插件實戰
假如現在我們的項目的目錄結構如下:
|--- webpack-plugin-demo | |--- node_modules | |--- js | | |--- main.js # js 的入口文件 | |--- plugins | | |--- logWebpackPlugin.js # 編寫的webpack的插件,主要作用是打印日志功能 | |--- styles | |--- index.html | |--- package.json | |--- webpack.config.js
1. 實現一個打印日志的LogWebpackPlugin插件
代碼如下:
class LogWebpackPlugin { constructor(doneCallback, emitCallback) { this.emitCallback = emitCallback this.doneCallback = doneCallback } apply(compiler) { compiler.hooks.emit.tap('LogWebpackPlugin', () => { // 在 emit 事件中回調 emitCallback this.emitCallback(); }); compiler.hooks.done.tap('LogWebpackPlugin', (err) => { // 在 done 事件中回調 doneCallback this.doneCallback(); }); compiler.hooks.compilation.tap('LogWebpackPlugin', () => { // compilation('編譯器'對'編譯ing'這個事件的監聽) console.log("The compiler is starting a new compilation...") }); compiler.hooks.compile.tap('LogWebpackPlugin', () => { // compile('編譯器'對'開始編譯'這個事件的監聽) console.log("The compiler is starting to compile...") }); } } // 導出插件 module.exports = LogWebpackPlugin;
下面我們在webpack中引入該插件;如下代碼:
// 引入LogWebpackPlugin 插件 const LogWebpackPlugin = require('./public/plugins/logWebpackPlugin'); module.exports = { plugins: [ new LogWebpackPlugin(() => { // Webpack 模塊完成轉換成功 console.log('emit 事件發生啦,所有模塊的轉換和代碼塊對應的文件已經生成好~') } , () => { // Webpack 構建成功,並且文件輸出了后會執行到這里,在這里可以做發布文件操作 console.log('done 事件發生啦,成功構建完成~') }) ] }
然后執行結果如下所示:
可以看到我們執行成功了,執行了對應的回調函數。如上代碼中的 compiler 這個我這邊就不講解了,上面已經講過了。那么 compiler.hooks 代表的是對外 暴露了多少事件鈎子,具體那個鈎子是什么含義,我們可以來看下官網
如上面代碼,我們使用兩個鈎子事件,分別是 compiler.hooks.emit 和 compiler.hooks.done, compiler.hooks.emit 鈎子事件的含義是: 在生成資源並輸出到目錄之前。這個事件就會發生。 compiler.hooks.done 的含義是:編譯完成,該事件就會發生。因此上面截圖我們可以看到先觸發 emit事件,因此會打印 'done 事件發生啦,成功構建完成~', 然后會觸發 done事件,因此會打印 "emit 事件發生啦,所有模塊的轉換和代碼塊對應的文件已經生成好~" 執行這個回調函數。
github代碼查看
2. 編寫去除生成 bundle.js 中多余的注釋的插件
項目結構如下:
|--- webpack-plugin-demo | |--- node_modules | |--- public | | |--- js | | | |--- main.js # 入口文件 | | |--- plugins # 存放所有的webpack插件 | | | |--- AsyncPlugin.js | | | |--- AutoExternalPlugin.js | | | |--- DonePlugin.js | | | |--- FileListPlugin.js | | | |--- MyPlugin.js | | | |--- OptimizePlugin.js | | |--- styles # 存放css樣式文件 | | |--- index.html # index.html模板 | |--- package.json | |--- webpack.config.js
項目結構如上所示;上面在 public/plugins 中一共有6個插件,我們分別來看下6個插件的代碼:
1. public/plugins/AsyncPlugin.js 代碼如下:
class AsyncPlugin { constructor() { } apply(compiler) { // 監聽emit事件,編譯完成后,文件內容輸出到硬盤上 觸發該事件 compiler.hooks.emit.tapAsync('AsyncPlugin', (compilation, callback) => { setTimeout(() => { console.log('文件將要被寫入到硬盤中'); callback(); }, 2000) }) } } module.exports = AsyncPlugin;
如上該插件代碼沒有什么實際作用,無非就是監聽 emit 異步事件鈎子,emit事件鈎子我們從官網
上可以看到具體的含義為:'在生成資源並輸出到目錄之前',會執行該事件鈎子中函數代碼,這邊無非就是在控制台中打印一些提示信息的,沒有什么實際作用的。
2. public/plugins/DonePlugin.js 代碼如下:
class DonePlugin { constructor() { } apply(compiler) { compiler.hooks.done.tapAsync('DonePlugin', (name, callback) => { console.log('全部編譯完成'); callback(); }) } } module.exports = DonePlugin;
如上代碼也是一個意思,當編譯完成后,就會執行 done的事件鈎子的回調函數,也是在命令中提示作用的。
3. public/plugins/OptimizePlugin.js 代碼如下:
class OptimizePlugin { constructor() { } apply(compiler) { // 監聽 compilation 事件 compiler.hooks.compilation.tap('OptimizePlugin', (compilation) => { compilation.hooks.optimize.tap('OptimizePlugin', () => { console.log('compilation 完成,正在優化,准備輸出'); }); }); } } module.exports = OptimizePlugin;
也是一樣監聽 compilation 事件的,每當檢測到一個文件發生改變的時候,那么一次新的 Compilation將會被創建。從而生成一組新的編譯資源。
4. public/plugins/FileListPlugin.js 代碼如下:
class FileListPlugin { constructor() { } apply(compiler) { compiler.hooks.compilation.tap('FileListPlugin', (compilation) => { compiler.hooks.emit.tap('FileListPlugin', () => { let content = '生成的文件列表\r\n'; content = Object.keys(compilation.assets).reduce((current, prev) => current + '- ' + prev + '\r\n', content); console.log(content); compilation.assets['README.md'] = { source() { return content; }, size() { return content.length; } } }) }) } } module.exports = FileListPlugin;
生成文件列表的時候,就會觸發該文件的代碼。
5. public/plugins/AutoExternalPlugin.js 代碼如下:
const ExternalModules = require('webpack/lib/ExternalModule'); class AutoExternalPlugin { constructor(options) { this.options = options; this.externalModules = {}; } apply(compiler) { compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', (normalModuleFactory) => { // parser 將代碼轉換為語法書 判斷有無 import normalModuleFactory.hooks.parser.for('javascript/auto').tap('AutoExternalPlugin', (parser, parserOptions) => { parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => { if (this.options[source]) { this.externalModules[source] = true; } }) }) // factory 是創建模塊的方法 // data 是創建模塊的參數 normalModuleFactory.hooks.factory.tap('AutoExternalPlugin', factory => (data, callback) => { const dependencies = data.dependencies; const value = dependencies[0].request; // jquery if (this.externalModules[value]) { const varName = this.options[value].varName; callback(null, new ExternalModules(varName, 'window')); } else { factory(data, callback); } }) }); compiler.hooks.compilation.tap('InlinePlugin', (compilation) => { compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('AutoExternalPlugin', (htmlPluginData, callback) => { Object.keys(this.options).forEach(key => { this.externalModules[key] = this.options[key]; htmlPluginData.body.unshift(this.processTags(compilation, htmlPluginData, this.options[key])) }); callback(null, htmlPluginData); }); }); } processTags(compilation, htmlPluginData, value) { var tag; return tag = { tagName: 'script', closeTag: true, attributes: { type: 'text/javascript', src: value.url } } } } module.exports = AutoExternalPlugin;
如上該插件的代碼的作用是可以解決外部的js引用,比如我在webpack中如下使用該插件:
const AutoExternalPlugin = require('./public/plugins/AutoExternalPlugin'); module.exports = { plugins:[ new AutoExternalPlugin({ jquery:{ varName:'jQuery', url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js' } }) ] }
這樣我就可以在頁面中使用jquery插件了;如下代碼所示:
import $ from 'jquery';
console.log($);
然后在我們的頁面中引入的是 該 jquery庫文件,它會把該庫文件自動生成到 index.html 上去,如下index.html 代碼變成如下了:
<html lang="en"> <head> <meta charset="UTF-8"> <title></title> <link rel="manifest" href="/public/manifest.json" /> <link href="main.css" rel="stylesheet"></head> <body> <div id="app">222226666</div> <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.1.0/jquery.js"></script><script type="text/javascript" src="bundle.js"></script></body> </html>
我們可以來簡單的分析下 AutoExternalPlugin.js 的代碼:
在apply方法內部會生成一個 compiler 實列,然后我們監聽 normalModuleFactory 事件,該事件的作用我們可以看下官網就知道了。
compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', (normalModuleFactory) => { // parser 將代碼轉換為語法書 判斷有無 import normalModuleFactory.hooks.parser.for('javascript/auto').tap('AutoExternalPlugin', (parser, parserOptions) => { parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => { if (this.options[source]) { this.externalModules[source] = true; } }) }) }
如上 parser 實例,是用來解析由 webpack 處理過的每個模塊。parser 也是擴展自 tapable 的 webpack 類,並且提供多種 tapable 鈎子,插件作者可以使用它來自定義解析過程。官網解釋可以看這里
如上代碼,我們調用 parser.hooks.import 鈎子函數, 然后返回的 source 就是我們的在 我們的main.js 中調用插件名。如main.js 代碼如下:
import $ from 'jquery';
因此在我們的webpack.config.js 中會如下初始化插件
new AutoExternalPlugin({ jquery:{ varName:'jQuery', url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js' } });
因此 source 返回的值 就是 'jquery'; 其他的代碼可以自己稍微看看就行了。這里暫時先不講了,由於時間問題。
6. public/plugins/MyPlugin.js 代碼如下:
class MyPlugin { constructor(options) { this.options = options; this.externalModules = {}; } apply(compiler) { var reg = /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)|(\/\*\*\*\*\*\*\/)/g; compiler.hooks.emit.tap('CodeBeautify', (compilation) => { Object.keys(compilation.assets).forEach((data) => { console.log(data); let content = compilation.assets[data].source(); // 獲取處理的文本 content = content.replace(reg, function (word) { // 去除注釋后的文本 return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word; }); compilation.assets[data] = { source() { return content; }, size() { return content.length; } } }); }); } } module.exports = MyPlugin;
這個js代碼的真正的含義才是我們今天要講到的,這個插件最主要作用是 去除注釋后的文本。
1. 第一步,我們使用 compiler.hooks.emit 鈎子函數。在生成資源並輸出到目錄之前觸發該函數,也就是說將編譯好的代碼發射到指定的stream中就會觸發,然后我們從回調函數返回的 compilation 對象上可以拿到編譯好的 stream.
2. 訪問compilation對象,compilation內部會返回很多內部對象,這邊先不打印了,因為打印的話直接會卡死掉,要等很長時間才會打印出來,你們自己可以試試;然后我們遍歷 assets.
Object.keys(compilation.assets).forEach((data) => { console.log(compilation.assets); console.log(8888) console.log(data); });
如下圖所示:
1) assets 數組對象中的key是資源名。在如上代碼,我們通過 Object.key()方法拿到了。如下所示:
main.css
bundle.js
index.html
2) 然后我們調用 compilation.assets[data].source(); 可以獲取資源的內容。
3) 使用正則,去掉注釋,如下代碼:
Object.keys(compilation.assets).forEach((data) => { let content = compilation.assets[data].source(); // 獲取處理的文本 content = content.replace(reg, function (word) { // 去除注釋后的文本 return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word; }); });
4) 更新 compilation.assets[data] 對象,如下代碼:
compilation.assets[data] = { source() { return content; }, size() { return content.length; } }
然后我們就可以在webpack中引入該所有的插件:
const DonePlugin = require('./public/plugins/DonePlugin'); const OptimizePlugin = require('./public/plugins/OptimizePlugin'); const AsyncPlugin = require('./public/plugins/AsyncPlugin'); const FileListPlugin = require('./public/plugins/FileListPlugin'); const AutoExternalPlugin = require('./public/plugins/AutoExternalPlugin'); const MyPlugin = require('./public/plugins/MyPlugin');
調用方式如下:
module.exports = { plugins:[ new DonePlugin(), new OptimizePlugin(), new AsyncPlugin(), new FileListPlugin(), new MyPlugin(), new AutoExternalPlugin({ jquery:{ varName:'jQuery', url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js' } }) ] }
然后我們進行打包運行效果如下所示: