做爲一名踏足前端時間不長的小開發必須得聊一聊webpack
,剛開始接觸webpack時第一反應這是啥(⊙_⊙)? 怎麼這麼複雜,感受好難呀,算了先無論這些!時間是個好東西呀,隨着對前端工程化
的實踐和理解慢慢加深,跟webpack接觸愈來愈多,最終仍是被ta折服,不由高呼一聲「webpack yyds(永遠滴神)!
」javascript
去年年中就想寫一些關於webpack的文章,因爲各類緣由耽擱了(主要是以爲對webpack理解還不夠,不敢妄自下筆);臨近年節,時間也有些了,與其 "摸魚"不如摸摸webpack,整理一些"年貨"分享給須要的xdm!後續會繼續寫一些【 Webpack】系列文章,xdm監督···css
本文主要經過實現一個cdn優化
的插件CdnPluginInject
介紹下webpack
的插件plugin
開發的具體流程,中間會涉及到html-webpack-plugin
插件的使用、vue/cli3+
項目中webpack插件的配置以及webpack相關知識點的說明。全文大概2800+字,預計耗時5~10分鐘,但願xdm看完有所學、有所思、有所輸出!html
注意:文章中實例基於vue/cli3+
工程展開!前端
index.html:vue
<head> ··· </head> <body> <div id="app"></div> <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script> <script src="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js"></script> ··· </body>
vue.config.js:java
module.exports = { ··· configureWebpack: { ··· externals: { 'vuex': 'Vuex', 'vue-router': 'VueRouter', ··· } },
webpack官網如此介紹到:插件向第三方開發者提供了 webpack 引擎中完整的能力。使用階段式的構建回調,開發者能夠引入它們本身的行爲到 webpack 構建流程中。建立插件比建立 loader 更加高級,由於你將須要理解一些 webpack 底層的內部特性來實現相應的鉤子!webpack
一個插件由如下構成:git
- 一個具名 JavaScript 函數。
- 在它的原型上定義 apply 方法。
- 指定一個觸及到 webpack 自己的 事件鉤子。
- 操做 webpack 內部的實例特定數據。
- 在實現功能後調用 webpack 提供的 callback。
// 一個 JavaScript class class MyExampleWebpackPlugin { // 將 `apply` 定義爲其原型方法,此方法以 compiler 做爲參數 apply(compiler) { // 指定要附加到的事件鉤子函數 compiler.hooks.emit.tapAsync( 'MyExampleWebpackPlugin', (compilation, callback) => { console.log('This is an example plugin!'); console.log('Here’s the `compilation` object which represents a single build of assets:', compilation); // 使用 webpack 提供的 plugin API 操做構建結果 compilation.addModule(/* ... */); callback(); } ); } }
思路:github
JavaScript
函數(使用ES6
的class
實現);apply
方法;compilation
鉤子:編譯(compilation)建立以後,執行插件);index.html
(將cdn
的script標籤
插入到index.html
中);apply
方法執行完以前將cdn的參數
放入webpack
的外部擴展externals
中;webpack
提供的callback
;實現步驟:web
JavaScript
函數(使用ES6
的class
實現) 建立類cdnPluginInject
,添加類的構造函數接收傳遞過來的參數;此處咱們定義接收參數的格式以下:
modules:[ { name: "xxx", //cdn包的名字 var: "xxx", //cdn引入庫在項目中使用時的變量名 path: "http://cdn.url/xxx.js" //cdn的url連接地址 }, ··· ]
定義類的變量modules
接收傳遞的cdn參數
的處理結果:
class CdnPluginInject { constructor({ modules, }) { // 若是是數組,將this.modules變換成對象形式 this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; } ··· } module.exports = CdnPluginInject;
apply
方法插件是由一個構造函數(此構造函數上的 prototype 對象具備
apply
方法)的所實例化出來的。這個apply
方法在安裝插件時,會被 webpack compiler 調用一次。apply
方法能夠接收一個 webpack compiler 對象的引用,從而能夠在回調函數中訪問到 compiler 對象
cdnPluginInject.js
代碼以下:
class CdnPluginInject { constructor({ modules, }) { // 若是是數組,將this.modules變換成對象形式 this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; } //webpack plugin開發的執行入口apply方法 apply(compiler) { ··· } module.exports = CdnPluginInject;
此處觸及compilation
鉤子:編譯(compilation)建立以後,執行插件。
compilation
是compiler
的一個hooks函數, compilation 會建立一次新的編譯過程實例,一個 compilation 實例能夠訪問全部模塊和它們的依賴
,在獲取到這些模塊後,根據須要對其進行操做處理!
class CdnPluginInject { constructor({ modules, }) { // 若是是數組,將this.modules變換成對象形式 this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; } //webpack plugin開發的執行入口apply方法 apply(compiler) { //獲取webpack的輸出配置對象 const { output } = compiler.options; //處理output.publicPath, 決定最終資源相對於引用它的html文件的相對位置 output.publicPath = output.publicPath || "/"; if (output.publicPath.slice(-1) !== "/") { output.publicPath += "/"; } //觸發compilation鉤子函數 compiler.hooks.compilation.tap("CdnPluginInject", compilation => { ··· } } module.exports = CdnPluginInject;
index.html
這一步主要是要實現 將cdn
的script標籤
插入到index.html
中 ;如何實現呢?在vue項目中webpack進行打包時實際上是使用html-webpack-plugin生成.html
文件的,因此咱們此處也能夠藉助html-webpack-plugin
對html文件進行操做插入cdn的script標籤。
// 4.1 引入html-webpack-plugin依賴 const HtmlWebpackPlugin = require("html-webpack-plugin"); class CdnPluginInject { constructor({ modules, }) { // 若是是數組,將this.modules變換成對象形式 this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; } //webpack plugin開發的執行入口apply方法 apply(compiler) { //獲取webpack的輸出配置對象 const { output } = compiler.options; //處理output.publicPath, 決定最終資源相對於引用它的html文件的相對位置 output.publicPath = output.publicPath || "/"; if (output.publicPath.slice(-1) !== "/") { output.publicPath += "/"; } //觸發compilation鉤子函數 compiler.hooks.compilation.tap("CdnPluginInject", compilation => { // 4.2 html-webpack-plugin中的hooks函數,當在資源生成以前異步執行 HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration .tapAsync("CdnPluginInject", (data, callback) => { // 註冊異步鉤子 //獲取插件中的cdnModule屬性(此處爲undefined,由於沒有cdnModule屬性) const moduleId = data.plugin.options.cdnModule; // 只要不是false(禁止)就行 if (moduleId !== false) { // 4.3獲得全部的cdn配置項 let modules = this.modules[ moduleId || Reflect.ownKeys(this.modules)[0] ]; if (modules) { // 4.4 整合已有的js引用和cdn引用 data.assets.js = modules .filter(m => !!m.path) .map(m => { return m.path; }) .concat(data.assets.js); // 4.5 整合已有的css引用和cdn引用 data.assets.css = modules .filter(m => !!m.style) .map(m => { return m.style; }) .concat(data.assets.css); } } // 4.6 返回callback函數 callback(null, data); }); } } module.exports = CdnPluginInject;
接下來逐步對上述實現進行分析:
html-webpack-plugin
中的hooks
函數,在html-webpack-plugin
中資源生成以前異步執行;這裏由衷的誇誇html-webpack-plugin
的做者了,ta在開發html-webpack-plugin
時就在插件中內置了不少的hook函數供開發者在調用插件的不一樣階段嵌入不一樣操做;所以,此處咱們可使用html-webpack-plugin
的beforeAssetTagGeneration
對html進行操做;beforeAssetTagGeneration
中,獲取獲得全部的須要進行cdn引入的配置數據;data.assets.js
能夠獲取到compilation
階段全部生成的js資源
(最終也是插入index.html中)的連接/路徑,而且將須要配置的cdn的path數據(cdn的url)
合併進去;data.assets.css
能夠獲取到compilation
階段全部生成的css資源
(最終也是插入index.html中)的連接/路徑,而且將須要配置的css類型cdn的path數據(cdn的url)
合併進去;webpack
該操做已經完成,能夠進行下一步了;webpack
的外部擴展externals
在apply
方法執行完以前還有一步必須完成:將cdn的參數
配置到外部擴展externals
中;能夠直接經過compiler.options.externals
獲取到webpack中externals屬性,通過操做將cdn配置中數據配置好就ok了。
callback
; 返回callback,告訴webpack CdnPluginInject
插件已經完成;
// 4.1 引入html-webpack-plugin依賴 const HtmlWebpackPlugin = require("html-webpack-plugin"); class CdnPluginInject { constructor({ modules, }) { // 若是是數組,將this.modules變換成對象形式 this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; } //webpack plugin開發的執行入口apply方法 apply(compiler) { //獲取webpack的輸出配置對象 const { output } = compiler.options; //處理output.publicPath, 決定最終資源相對於引用它的html文件的相對位置 output.publicPath = output.publicPath || "/"; if (output.publicPath.slice(-1) !== "/") { output.publicPath += "/"; } //觸發compilation鉤子函數 compiler.hooks.compilation.tap("CdnPluginInject", compilation => { // 4.2 html-webpack-plugin中的hooks函數,當在資源生成以前異步執行 HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration .tapAsync("CdnPluginInject", (data, callback) => { // 註冊異步鉤子 //獲取插件中的cdnModule屬性(此處爲undefined,由於沒有cdnModule屬性) const moduleId = data.plugin.options.cdnModule; // 只要不是false(禁止)就行 if (moduleId !== false) { // 4.3獲得全部的cdn配置項 let modules = this.modules[ moduleId || Reflect.ownKeys(this.modules)[0] ]; if (modules) { // 4.4 整合已有的js引用和cdn引用 data.assets.js = modules .filter(m => !!m.path) .map(m => { return m.path; }) .concat(data.assets.js); // 4.5 整合已有的css引用和cdn引用 data.assets.css = modules .filter(m => !!m.style) .map(m => { return m.style; }) .concat(data.assets.css); } } // 4.6 返回callback函數 callback(null, data); }); // 5.1 獲取externals const externals = compiler.options.externals || {}; // 5.2 cdn配置數據添加到externals Reflect.ownKeys(this.modules).forEach(key => { const mods = this.modules[key]; mods .forEach(p => { externals[p.name] = p.var || p.name; //var爲項目中的使用命名 }); }); // 5.3 externals賦值 compiler.options.externals = externals; //配置externals // 6 返回callback callback(); } } module.exports = CdnPluginInject;
至此,一個完整的webpack插件CdnPluginInject
就開發完成了!接下來使用着試一試。
在vue項目的vue.config.js
文件中引入並使用CdnPluginInject
:
cdn配置文件CdnConfig.js:
/* * 配置的cdn * @name: 第三方庫的名字 * @var: 第三方庫在項目中的變量名 * @path: 第三方庫的cdn連接 */ module.exports = [ { name: "moment", var: "moment", path: "https://cdn.bootcdn.net/ajax/libs/moment.js/2.27.0/moment.min.js" }, ··· ];
const CdnPluginInject = require("./CdnPluginInject"); const cdnConfig = require("./CdnConfig"); module.exports = { ··· configureWebpack: config => { //只有是生產山上線打包才使用cdn配置 if(process.env.NODE.ENV =='production'){ config.plugins.push( new CdnPluginInject({ modules: CdnConfig }) ) } } ··· }
const CdnPluginInject = require("./CdnPluginInject"); const cdnConfig = require("./CdnConfig"); module.exports = { ··· chainWebpack: config => { //只有是生產山上線打包才使用cdn配置 if(process.env.NODE.ENV =='production'){ config.plugin("cdn").use( new CdnPluginInject({ modules: CdnConfig }) ) } } ··· }
經過使用CdnPluginInject
:
看完後確定有webpack
大佬有一絲絲疑惑,這個插件不就是 webpack-cdn-plugin 的乞丐版!CdnPluginInject
只不過是本人根據webpack-cdn-plugin
源碼的學習,結合本身項目實際所需修改的仿寫版本,相較於webpack-cdn-plugin
將cdn連接的生成進行封裝,CdnPluginInject
是直接將cdn連接進行配置,對於選擇cdn顯配置更加簡單。想要進一步學習的xdm能夠看看webpack-cdn-plugin
的源碼,通過做者的不斷的迭代更新,其提供的可配置參數更加豐富,功能更增強大(再次膜拜)。
重點:整理不易,以爲還能夠的xdm記得 一鍵三連 喲!