讓W(xué)ebpack Plugin開發(fā)更加單

image

??作為一名踏足前端時間不長的小開發(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ù)(使用ES6class實(shí)現(xiàn));
  • 2、在它的原型上定義 apply 方法滑废;
  • 3蝗肪、指定一個觸及到 webpack 本身的事件鉤子(此處觸及compilation鉤子:編譯(compilation)創(chuàng)建之后,執(zhí)行插件)蠕趁;
  • 4薛闪、在鉤子事件中操作index.html(將cdnscript標(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ù)(使用ES6class實(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í)行插件砌梆。

image

??compilationcompiler 的一個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) cdnscript標(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-pluginbeforeAssetTagGeneration對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記得 一鍵三連 喲肝陪!

文章參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市刑顺,隨后出現(xiàn)的幾起案子氯窍,更是在濱河造成了極大的恐慌,老刑警劉巖蹲堂,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狼讨,死亡現(xiàn)場離奇詭異,居然都是意外死亡柒竞,警方通過查閱死者的電腦和手機(jī)政供,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人布隔,你說我怎么就攤上這事离陶。” “怎么了衅檀?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵枕磁,是天一觀的道長。 經(jīng)常有香客問我术吝,道長计济,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任排苍,我火速辦了婚禮沦寂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘淘衙。我一直安慰自己传藏,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布彤守。 她就那樣靜靜地躺著毯侦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪具垫。 梳的紋絲不亂的頭發(fā)上侈离,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機(jī)與錄音筝蚕,去河邊找鬼卦碾。 笑死,一個胖子當(dāng)著我的面吹牛起宽,可吹牛的內(nèi)容都是我干的洲胖。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼坯沪,長吁一口氣:“原來是場噩夢啊……” “哼绿映!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起腐晾,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤叉弦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后赴魁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卸奉,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钝诚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年颖御,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡潘拱,死狀恐怖疹鳄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情芦岂,我是刑警寧澤瘪弓,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站禽最,受9級特大地震影響腺怯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜川无,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一呛占、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧懦趋,春花似錦晾虑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至诫咱,卻和暖如春笙隙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背坎缭。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工逃沿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人幻锁。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓凯亮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哄尔。 傳聞我的和親對象是個殘疾皇子假消,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容