YYDS: Webpack Plugin開發(fā)

??
剛開始接觸webpack時(shí)第一反應(yīng)這是啥(⊙_⊙)? 怎么這么復(fù)雜,感覺好難呀,算了先不管這些!時(shí)間是個(gè)好東西呀,隨著對(duì)前端工程化的實(shí)踐和理解慢慢加深,跟webpack接觸越來越多,最終還是被ta折服,不禁高呼一聲“webpack yyds(永遠(yuǎn)滴神)!”
??
去年年中就想寫一些關(guān)于webpack的文章,由于各種原因耽擱了(主要是覺得對(duì)webpack理解還不夠,不敢妄自下筆);臨近年節(jié),時(shí)間也有些了,與其 "摸魚"不如摸摸webpack,整理一些"年貨"分享給需要的xdm!后續(xù)會(huì)繼續(xù)寫一些【 Webpack】系列文章,xdm監(jiān)督···
導(dǎo)讀
??
本文主要通過實(shí)現(xiàn)一個(gè)cdn優(yōu)化的插件CdnPluginInject介紹下webpack的插件plugin開發(fā)的具體流程,中間會(huì)涉及到html-webpack-plugin插件的使用、vue/cli3+項(xiàng)目中webpack插件的配置以及webpack相關(guān)知識(shí)點(diǎn)的說明。全文大概2800+字,預(yù)計(jì)耗時(shí)5~10分鐘,希望xdm看完有所學(xué)、有所思、有所輸出!
注意:文章中實(shí)例基于vue/cli3+工程展開!
一、cdn常規(guī)使用
index.html:
??···
??"app">
??
??
??···
vue.config.js:
module.exports?=?{
??···
??configureWebpack:?{
????···
????externals:?{
??????'vuex':?'Vuex',
??????'vue-router':?'VueRouter',
??????···
????}
??},
二、開發(fā)一個(gè)webpack plugin
webpack官網(wǎng)如此介紹到:插件向第三方開發(fā)者提供了 webpack 引擎中完整的能力。使用階段式的構(gòu)建回調(diào),開發(fā)者可以引入它們自己的行為到 webpack 構(gòu)建流程中。創(chuàng)建插件比創(chuàng)建 loader 更加高級(jí),因?yàn)槟銓⑿枰斫庖恍?webpack 底層的內(nèi)部特性來實(shí)現(xiàn)相應(yīng)的鉤子!
一個(gè)插件由以下構(gòu)成:
一個(gè)具名 JavaScript 函數(shù)。 在它的原型上定義 apply 方法。 指定一個(gè)觸及到 webpack 本身的?事件鉤子。 操作 webpack 內(nèi)部的實(shí)例特定數(shù)據(jù)。 在實(shí)現(xiàn)功能后調(diào)用 webpack 提供的 callback。 //?一個(gè)?JavaScript?class
class?MyExampleWebpackPlugin?{
//?將?`apply`?定義為其原型方法,此方法以?compiler?作為參數(shù)
?apply(compiler)?{
???//?指定要附加到的事件鉤子函數(shù)
?????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?操作構(gòu)建結(jié)果
?????????compilation.addModule(/*?...?*/);
?????????callback();
???????}
?????);
?}
}
三、cdn優(yōu)化插件實(shí)現(xiàn)
1、創(chuàng)建一個(gè)具名?JavaScript?函數(shù)(使用ES6的class實(shí)現(xiàn)); 2、在它的原型上定義 apply 方法; 3、指定一個(gè)觸及到 webpack 本身的事件鉤子(此處觸及compilation鉤子:編譯(compilation)創(chuàng)建之后,執(zhí)行插件); 4、在鉤子事件中操作index.html(將cdn的script標(biāo)簽插入到index.html中); 5、在apply方法執(zhí)行完之前將cdn的參數(shù)放入webpack的外部擴(kuò)展externals中; 6、在實(shí)現(xiàn)功能后調(diào)用 webpack 提供的 callback;
1、創(chuàng)建一個(gè)具名?JavaScript?函數(shù)(使用ES6的class實(shí)現(xiàn))
modules:[
??{
????name:?"xxx",????//cdn包的名字
????var:?"xxx",????//cdn引入庫在項(xiàng)目中使用時(shí)的變量名
????path:?"http://cdn.url/xxx.js"?//cdn的url鏈接地址
??},
??···
]
class?CdnPluginInject?{
??constructor({
????modules,
??})?{
????//?如果是數(shù)組,將this.modules變換成對(duì)象形式
????this.modules?=?Array.isArray(modules)???{?["defaultCdnModuleKey"]:?modules?}?:?modules;?
??}
?···
}
module.exports?=?CdnPluginInject;
2、在它的原型上定義?apply?方法
插件是由一個(gè)構(gòu)造函數(shù)(此構(gòu)造函數(shù)上的 prototype 對(duì)象具有 apply 方法)的所實(shí)例化出來的。這個(gè) apply 方法在安裝插件時(shí),會(huì)被 webpack compiler 調(diào)用一次。apply 方法可以接收一個(gè) webpack compiler 對(duì)象的引用,從而可以在回調(diào)函數(shù)中訪問到 compiler 對(duì)象
class?CdnPluginInject?{
??constructor({
????modules,
??})?{
????//?如果是數(shù)組,將this.modules變換成對(duì)象形式
????this.modules?=?Array.isArray(modules)???{?["defaultCdnModuleKey"]:?modules?}?:?modules;?
??}
??//webpack?plugin開發(fā)的執(zhí)行入口apply方法
??apply(compiler)?{
????···
??}
module.exports?=?CdnPluginInject;
3、指定一個(gè)觸及到 webpack 本身的事件鉤子

class?CdnPluginInject?{
??constructor({
????modules,
??})?{
????//?如果是數(shù)組,將this.modules變換成對(duì)象形式
????this.modules?=?Array.isArray(modules)???{?["defaultCdnModuleKey"]:?modules?}?:?modules;?
??}
??//webpack?plugin開發(fā)的執(zhí)行入口apply方法
??apply(compiler)?{
????//獲取webpack的輸出配置對(duì)象
????const?{?output?}?=?compiler.options;
????//處理output.publicPath,?決定最終資源相對(duì)于引用它的html文件的相對(duì)位置
????output.publicPath?=?output.publicPath?||?"/";
????if?(output.publicPath.slice(-1)?!==?"/")?{
??????output.publicPath?+=?"/";
????}
????//觸發(fā)compilation鉤子函數(shù)
????compiler.hooks.compilation.tap("CdnPluginInject",?compilation?=>?{?
?????···
??}
}
module.exports?=?CdnPluginInject;
4、在鉤子事件中操作index.html
//?4.1?引入html-webpack-plugin依賴
const?HtmlWebpackPlugin?=?require("html-webpack-plugin");
class?CdnPluginInject?{
??constructor({
????modules,
??})?{
????//?如果是數(shù)組,將this.modules變換成對(duì)象形式
????this.modules?=?Array.isArray(modules)???{?["defaultCdnModuleKey"]:?modules?}?:?modules;?
??}
??//webpack?plugin開發(fā)的執(zhí)行入口apply方法
??apply(compiler)?{
????//獲取webpack的輸出配置對(duì)象
????const?{?output?}?=?compiler.options;
????//處理output.publicPath,?決定最終資源相對(duì)于引用它的html文件的相對(duì)位置
????output.publicPath?=?output.publicPath?||?"/";
????if?(output.publicPath.slice(-1)?!==?"/")?{
??????output.publicPath?+=?"/";
????}
????//觸發(fā)compilation鉤子函數(shù)
????compiler.hooks.compilation.tap("CdnPluginInject",?compilation?=>?{?
??????//?4.2?html-webpack-plugin中的hooks函數(shù),當(dāng)在資源生成之前異步執(zhí)行
??????HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration
???????.tapAsync("CdnPluginInject",?(data,?callback)?=>?{???//?注冊異步鉤子
????????????//獲取插件中的cdnModule屬性(此處為undefined,因?yàn)闆]有cdnModule屬性)
??????????const?moduleId?=?data.plugin.options.cdnModule;??
??????????//?只要不是false(禁止)就行
??????????if?(moduleId?!==?false)?{????
?????????????//?4.3得到所有的cdn配置項(xiàng)
????????????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函數(shù)
??????????callback(null,?data);
????????});
??}
}
module.exports?=?CdnPluginInject;
4.1、引入html-webpack-plugin依賴,這個(gè)不用多說; 4.2、調(diào)用html-webpack-plugin中的hooks函數(shù),在html-webpack-plugin中資源生成之前異步執(zhí)行;這里由衷的夸夸html-webpack-plugin的作者了,ta在開發(fā)html-webpack-plugin時(shí)就在插件中內(nèi)置了很多的hook函數(shù)供開發(fā)者在調(diào)用插件的不同階段嵌入不同操作;因此,此處我們可以使用html-webpack-plugin的beforeAssetTagGeneration對(duì)html進(jìn)行操作; 4.3、 在beforeAssetTagGeneration中,獲取得到所有的需要進(jìn)行cdn引入的配置數(shù)據(jù); 4.4、 整合已有的js引用和cdn引用;通過data.assets.js可以獲取到compilation階段所有生成的js資源(最終也是插入index.html中)的鏈接/路徑,并且將需要配置的cdn的path數(shù)據(jù)(cdn的url)合并進(jìn)去; 4.5、 整合已有的css引用和cdn引用;通過data.assets.css可以獲取到compilation階段所有生成的css資源(最終也是插入index.html中)的鏈接/路徑,并且將需要配置的css類型cdn的path數(shù)據(jù)(cdn的url)合并進(jìn)去; 4.6、 返回callback函數(shù),目的是告訴webpack該操作已經(jīng)完成,可以進(jìn)行下一步了;
5、設(shè)置webpack的外部擴(kuò)展externals
6、 callback;
//?4.1?引入html-webpack-plugin依賴
const?HtmlWebpackPlugin?=?require("html-webpack-plugin");
class?CdnPluginInject?{
??constructor({
????modules,
??})?{
????//?如果是數(shù)組,將this.modules變換成對(duì)象形式
????this.modules?=?Array.isArray(modules)???{?["defaultCdnModuleKey"]:?modules?}?:?modules;?
??}
??//webpack?plugin開發(fā)的執(zhí)行入口apply方法
??apply(compiler)?{
????//獲取webpack的輸出配置對(duì)象
????const?{?output?}?=?compiler.options;
????//處理output.publicPath,?決定最終資源相對(duì)于引用它的html文件的相對(duì)位置
????output.publicPath?=?output.publicPath?||?"/";
????if?(output.publicPath.slice(-1)?!==?"/")?{
??????output.publicPath?+=?"/";
????}
????//觸發(fā)compilation鉤子函數(shù)
????compiler.hooks.compilation.tap("CdnPluginInject",?compilation?=>?{?
??????//?4.2?html-webpack-plugin中的hooks函數(shù),當(dāng)在資源生成之前異步執(zhí)行
??????HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration
???????.tapAsync("CdnPluginInject",?(data,?callback)?=>?{???//?注冊異步鉤子
????????????//獲取插件中的cdnModule屬性(此處為undefined,因?yàn)闆]有cdnModule屬性)
??????????const?moduleId?=?data.plugin.options.cdnModule;??
??????????//?只要不是false(禁止)就行
??????????if?(moduleId?!==?false)?{????
?????????????//?4.3得到所有的cdn配置項(xiàng)
????????????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函數(shù)
??????????callback(null,?data);
????????});
??????
??????//?5.1?獲取externals
?????????const?externals?=?compiler.options.externals?||?{};
??????//?5.2?cdn配置數(shù)據(jù)添加到externals
??????Reflect.ownKeys(this.modules).forEach(key?=>?{
????????const?mods?=?this.modules[key];
????????mods
??????????.forEach(p?=>?{
??????????externals[p.name]?=?p.var?||?p.name;?//var為項(xiàng)目中的使用命名
????????});
??????});
??????//?5.3?externals賦值
??????compiler.options.externals?=?externals;?//配置externals
??????
??????//?6?返回callback
??????callback();
??}
}
module.exports?=?CdnPluginInject;
四、cdn優(yōu)化插件使用
/*
?*?配置的cdn
?*?@name:?第三方庫的名字
?*?@var:第三方庫在項(xiàng)目中的變量名
?*?@path:第三方庫的cdn鏈接
?*/
module.exports?=?[
??{
????name:?"moment",
????var:?"moment",
????path:?"https://cdn.bootcdn.net/ajax/libs/moment.js/2.27.0/moment.min.js"
??},
??···
];
configureWebpack中配置:
const?CdnPluginInject?=?require("./CdnPluginInject");
const?cdnConfig?=?require("./CdnConfig");
module.exports?=?{
??···
??configureWebpack:?config?=>?{
????//只有是生產(chǎn)山上線打包才使用cdn配置
????if(process.env.NODE.ENV?=='production'){
??????config.plugins.push(
????????new?CdnPluginInject({
??????????modules:?CdnConfig
????????})
??????)
??????}
??}
??···
}
chainWebpack中配置:
const?CdnPluginInject?=?require("./CdnPluginInject");
const?cdnConfig?=?require("./CdnConfig");
module.exports?=?{
??···
??chainWebpack:?config?=>?{
????//只有是生產(chǎn)山上線打包才使用cdn配置
????if(process.env.NODE.ENV?=='production'){
??????config.plugin("cdn").use(
????????new?CdnPluginInject({
??????????modules:?CdnConfig
????????})
??????)
??????}
??}
??···
}
1、通過配置實(shí)現(xiàn)對(duì)cdn優(yōu)化的管理和維護(hù); 2、實(shí)現(xiàn)針對(duì)不同環(huán)境做cdn優(yōu)化配置(開發(fā)環(huán)境直接使用本地安裝依賴進(jìn)行調(diào)試,生產(chǎn)環(huán)境適應(yīng)cdn方式優(yōu)化加載);
五、小結(jié)

