??作為一名踏足前端時間不長的小開發(fā)必須得聊一聊
webpack
谅摄,剛開始接觸webpack時第一反應(yīng)這是啥(⊙_⊙)? 怎么這么復(fù)雜,感覺好難呀娇妓,算了先不管這些肛根!時間是個好東西呀辫塌,隨著對前端工程化
的實(shí)踐和理解慢慢加深,跟webpack接觸越來越多派哲,最終還是被ta折服臼氨,不禁高呼一聲“webpack yyds(永遠(yuǎn)滴神)!
”
??去年年中就想寫一些關(guān)于webpack的文章芭届,由于各種原因耽擱了(主要是覺得對webpack理解還不夠储矩,不敢妄自下筆);臨近年節(jié)褂乍,時間也有些了持隧,與其 "摸魚"不如摸摸webpack,整理一些"年貨"分享給需要的xdm树叽!后續(xù)會繼續(xù)寫一些【 Webpack】系列文章舆蝴,xdm監(jiān)督···
導(dǎo)讀
??本文主要通過實(shí)現(xiàn)一個cdn優(yōu)化
的插件CdnPluginInject
介紹下webpack
的插件plugin
開發(fā)的具體流程谦絮,中間會涉及到html-webpack-plugin
插件的使用题诵、vue/cli3+
項目中webpack插件的配置以及webpack相關(guān)知識點(diǎn)的說明。全文大概2800+字层皱,預(yù)計耗時5~10分鐘性锭,希望xdm看完有所學(xué)、有所思叫胖、有所輸出草冈!
注意:文章中實(shí)例基于vue/cli3+
工程展開!
一瓮增、cdn常規(guī)使用
index.html:
<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:
module.exports = {
···
configureWebpack: {
···
externals: {
'vuex': 'Vuex',
'vue-router': 'VueRouter',
···
}
},
二怎棱、開發(fā)一個webpack plugin
webpack官網(wǎng)如此介紹到:插件向第三方開發(fā)者提供了 webpack 引擎中完整的能力。使用階段式的構(gòu)建回調(diào)绷跑,開發(fā)者可以引入它們自己的行為到 webpack 構(gòu)建流程中拳恋。創(chuàng)建插件比創(chuàng)建 loader 更加高級,因?yàn)槟銓⑿枰斫庖恍?webpack 底層的內(nèi)部特性來實(shí)現(xiàn)相應(yīng)的鉤子砸捏!
一個插件由以下構(gòu)成:
- 一個具名 JavaScript 函數(shù)谬运。
- 在它的原型上定義 apply 方法隙赁。
- 指定一個觸及到 webpack 本身的 事件鉤子。
- 操作 webpack 內(nèi)部的實(shí)例特定數(shù)據(jù)梆暖。
- 在實(shí)現(xiàn)功能后調(diào)用 webpack 提供的 callback伞访。
// 一個 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)建一個具名
JavaScript
函數(shù)(使用ES6
的class
實(shí)現(xiàn)); - 2、在它的原型上定義
apply
方法滑废; - 3蝗肪、指定一個觸及到 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
腊状;
實(shí)現(xiàn)步驟:
1诱咏、創(chuàng)建一個具名JavaScript
函數(shù)(使用ES6
的class
實(shí)現(xiàn))
??創(chuàng)建類cdnPluginInject
,添加類的構(gòu)造函數(shù)接收傳遞過來的參數(shù)缴挖;此處我們定義接收參數(shù)的格式如下:
modules:[
{
name: "xxx", //cdn包的名字
var: "xxx", //cdn引入庫在項目中使用時的變量名
path: "http://cdn.url/xxx.js" //cdn的url鏈接地址
},
···
]
定義類的變量modules
接收傳遞的cdn參數(shù)
的處理結(jié)果:
class CdnPluginInject {
constructor({
modules,
}) {
// 如果是數(shù)組袋狞,將this.modules變換成對象形式
this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules;
}
···
}
module.exports = CdnPluginInject;
2、在它的原型上定義 apply
方法
插件是由一個構(gòu)造函數(shù)(此構(gòu)造函數(shù)上的 prototype 對象具有
apply
方法)的所實(shí)例化出來的映屋。這個apply
方法在安裝插件時苟鸯,會被 webpack compiler 調(diào)用一次。apply
方法可以接收一個 webpack compiler 對象的引用棚点,從而可以在回調(diào)函數(shù)中訪問到 compiler 對象
cdnPluginInject.js
代碼如下:
class CdnPluginInject {
constructor({
modules,
}) {
// 如果是數(shù)組早处,將this.modules變換成對象形式
this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules;
}
//webpack plugin開發(fā)的執(zhí)行入口apply方法
apply(compiler) {
···
}
module.exports = CdnPluginInject;
3、指定一個觸及到 webpack 本身的事件鉤子
??此處觸及compilation
鉤子:編譯(compilation)創(chuàng)建之后瘫析,執(zhí)行插件砌梆。
??compilation
是compiler
的一個hooks函數(shù), compilation 會創(chuàng)建一次新的編譯過程實(shí)例贬循,一個 compilation 實(shí)例可以訪問所有模塊和它們的依賴
咸包,在獲取到這些模塊后,根據(jù)需要對其進(jìn)行操作處理杖虾!
class CdnPluginInject {
constructor({
modules,
}) {
// 如果是數(shù)組烂瘫,將this.modules變換成對象形式
this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules;
}
//webpack plugin開發(fā)的執(zhí)行入口apply方法
apply(compiler) {
//獲取webpack的輸出配置對象
const { output } = compiler.options;
//處理output.publicPath, 決定最終資源相對于引用它的html文件的相對位置
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
??這一步主要是要實(shí)現(xiàn) 將cdn
的script標(biāo)簽
插入到index.html
中 忱反;如何實(shí)現(xiàn)呢泛释?在vue項目中webpack進(jìn)行打包時其實(shí)是使用html-webpack-plugin生成.html
文件的,所以我們此處也可以借助html-webpack-plugin
對html文件進(jìn)行操作插入cdn的script標(biāo)簽温算。
// 4.1 引入html-webpack-plugin依賴
const HtmlWebpackPlugin = require("html-webpack-plugin");
class CdnPluginInject {
constructor({
modules,
}) {
// 如果是數(shù)組怜校,將this.modules變換成對象形式
this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules;
}
//webpack plugin開發(fā)的執(zhí)行入口apply方法
apply(compiler) {
//獲取webpack的輸出配置對象
const { output } = compiler.options;
//處理output.publicPath, 決定最終資源相對于引用它的html文件的相對位置
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配置項
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;
接下來逐步對上述實(shí)現(xiàn)進(jìn)行分析:
- 4.1、引入html-webpack-plugin依賴巩割,這個不用多說裙顽;
- 4.2、調(diào)用
html-webpack-plugin
中的hooks
函數(shù)宣谈,在html-webpack-plugin
中資源生成之前異步執(zhí)行愈犹;這里由衷的夸夸html-webpack-plugin
的作者了,ta在開發(fā)html-webpack-plugin
時就在插件中內(nèi)置了很多的hook函數(shù)供開發(fā)者在調(diào)用插件的不同階段嵌入不同操作闻丑;因此漩怎,此處我們可以使用html-webpack-plugin
的beforeAssetTagGeneration
對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
??在apply
方法執(zhí)行完之前還有一步必須完成:將cdn的參數(shù)
配置到外部擴(kuò)展externals
中;可以直接通過compiler.options.externals
獲取到webpack中externals屬性聊训,經(jīng)過操作將cdn配置中數(shù)據(jù)配置好就ok了抱究。
6、callback
带斑;
??返回callback鼓寺,告訴webpack CdnPluginInject
插件已經(jīng)完成勋拟;
// 4.1 引入html-webpack-plugin依賴
const HtmlWebpackPlugin = require("html-webpack-plugin");
class CdnPluginInject {
constructor({
modules,
}) {
// 如果是數(shù)組,將this.modules變換成對象形式
this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules;
}
//webpack plugin開發(fā)的執(zhí)行入口apply方法
apply(compiler) {
//獲取webpack的輸出配置對象
const { output } = compiler.options;
//處理output.publicPath妈候, 決定最終資源相對于引用它的html文件的相對位置
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配置項
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為項目中的使用命名
});
});
// 5.3 externals賦值
compiler.options.externals = externals; //配置externals
// 6 返回callback
callback();
}
}
module.exports = CdnPluginInject;
??至此苦银,一個完整的webpack插件CdnPluginInject
就開發(fā)完成了啸胧!接下來使用著試一試。
四幔虏、cdn優(yōu)化插件使用
??在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"
},
···
];
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
})
)
}
}
···
}
??通過使用CdnPluginInject
:
- 1纺念、通過配置實(shí)現(xiàn)對cdn優(yōu)化的管理和維護(hù);
- 2想括、實(shí)現(xiàn)針對不同環(huán)境做cdn優(yōu)化配置(開發(fā)環(huán)境直接使用本地安裝依賴進(jìn)行調(diào)試陷谱,生產(chǎn)環(huán)境適應(yīng)cdn方式優(yōu)化加載);
五瑟蜈、小結(jié)
??看完后肯定有webpack
大佬有一絲絲疑惑叭首,這個插件不就是 webpack-cdn-plugin 的乞丐版!CdnPluginInject
只不過是本人根據(jù)webpack-cdn-plugin
源碼的學(xué)習(xí)踪栋,結(jié)合自己項目實(shí)際所需修改的仿寫版本焙格,相較于webpack-cdn-plugin
將cdn鏈接的生成進(jìn)行封裝,CdnPluginInject
是直接將cdn鏈接進(jìn)行配置夷都,對于選擇cdn顯配置更加簡單眷唉。想要進(jìn)一步學(xué)習(xí)的xdm可以看看webpack-cdn-plugin
的源碼,經(jīng)過作者的不斷的迭代更新囤官,其提供的可配置參數(shù)更加豐富冬阳,功能更加強(qiáng)大(再次膜拜)。
重點(diǎn):整理不易党饮,覺得還可以的xdm記得 一鍵三連 喲肝陪!