让Webpack Plugin开发更加单

image

  作为一名踏足前端时间不长的小开发必须得聊一聊webpack,刚开始接触webpack时第一反应这是啥(⊙_⊙)? 怎么这么复杂,感觉好难呀,算了先不管这些!时间是个好东西呀,随着对前端工程化的实践和理解慢慢加深,跟webpack接触越来越多,最终还是被ta折服,不禁高呼一声“webpack yyds(永远滴神)!

  去年年中就想写一些关于webpack的文章,由于各种原因耽搁了(主要是觉得对webpack理解还不够,不敢妄自下笔);临近年节,时间也有些了,与其 "摸鱼"不如摸摸webpack,整理一些"年货"分享给需要的xdm!后续会继续写一些【 Webpack】系列文章,xdm监督···

导读

  本文主要通过实现一个cdn优化的插件CdnPluginInject介绍下webpack的插件plugin开发的具体流程,中间会涉及到html-webpack-plugin插件的使用、vue/cli3+项目中webpack插件的配置以及webpack相关知识点的说明。全文大概2800+字,预计耗时5~10分钟,希望xdm看完有所学、有所思、有所输出!

注意:文章中实例基于vue/cli3+工程展开!

一、cdn常规使用

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',
      ···
    }
  },

二、开发一个webpack plugin

webpack官网如此介绍到:插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。创建插件比创建 loader 更加高级,因为你将需要理解一些 webpack 底层的内部特性来实现相应的钩子!

一个插件由以下构成:

  • 一个具名 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();
       }
     );
 }
}

三、cdn优化插件实现

思路:

  • 1、创建一个具名JavaScript 函数(使用ES6class实现);
  • 2、在它的原型上定义 apply 方法;
  • 3、指定一个触及到 webpack 本身的事件钩子(此处触及compilation钩子:编译(compilation)创建之后,执行插件);
  • 4、在钩子事件中操作index.html(将cdnscript标签插入到index.html中);
  • 5、在apply方法执行完之前将cdn的参数放入webpack外部扩展externals中;
  • 6、在实现功能后调用webpack 提供的callback

实现步骤:

1、创建一个具名JavaScript 函数(使用ES6class实现)

  创建类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;

2、在它的原型上定义 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;

3、指定一个触及到 webpack 本身的事件钩子

  此处触及compilation钩子:编译(compilation)创建之后,执行插件。

image

  compilationcompiler 的一个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;

4、在钩子事件中操作index.html

  这一步主要是要实现 cdnscript标签插入到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;

接下来逐步对上述实现进行分析:

  • 4.1、引入html-webpack-plugin依赖,这个不用多说;
  • 4.2、调用html-webpack-plugin中的hooks函数,在html-webpack-plugin中资源生成之前异步执行;这里由衷的夸夸html-webpack-plugin的作者了,ta在开发html-webpack-plugin时就在插件中内置了很多的hook函数供开发者在调用插件的不同阶段嵌入不同操作;因此,此处我们可以使用html-webpack-pluginbeforeAssetTagGeneration对html进行操作;
  • 4.3、 在beforeAssetTagGeneration中,获取得到所有的需要进行cdn引入的配置数据;
  • 4.4、 整合已有的js引用和cdn引用;通过data.assets.js可以获取到compilation阶段所有生成的js资源(最终也是插入index.html中)的链接/路径,并且将需要配置的cdn的path数据(cdn的url)合并进去;
  • 4.5、 整合已有的css引用和cdn引用;通过data.assets.css可以获取到compilation阶段所有生成的css资源(最终也是插入index.html中)的链接/路径,并且将需要配置的css类型cdn的path数据(cdn的url)合并进去;
  • 4.6、 返回callback函数,目的是告诉webpack该操作已经完成,可以进行下一步了;

5、设置webpack外部扩展externals

  在apply方法执行完之前还有一步必须完成:将cdn的参数配置到外部扩展externals中;可以直接通过compiler.options.externals获取到webpack中externals属性,经过操作将cdn配置中数据配置好就ok了。

6、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就开发完成了!接下来使用着试一试。

四、cdn优化插件使用

  在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 => {
    //只有是生产山上线打包才使用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 => {
    //只有是生产山上线打包才使用cdn配置
    if(process.env.NODE.ENV =='production'){
      config.plugin("cdn").use(
        new CdnPluginInject({
          modules: CdnConfig
        })
      )
    }
  }
  ···
}

  通过使用CdnPluginInject

  • 1、通过配置实现对cdn优化的管理和维护;
  • 2、实现针对不同环境做cdn优化配置(开发环境直接使用本地安装依赖进行调试,生产环境适应cdn方式优化加载);

五、小结

  看完后肯定有webpack大佬有一丝丝疑惑,这个插件不就是 webpack-cdn-plugin 的乞丐版!CdnPluginInject只不过是本人根据webpack-cdn-plugin源码的学习,结合自己项目实际所需修改的仿写版本,相较于webpack-cdn-plugin将cdn链接的生成进行封装,CdnPluginInject是直接将cdn链接进行配置,对于选择cdn显配置更加简单。想要进一步学习的xdm可以看看webpack-cdn-plugin的源码,经过作者的不断的迭代更新,其提供的可配置参数更加丰富,功能更加强大(再次膜拜)。

重点:整理不易,觉得还可以的xdm记得 一键三连 哟!

文章参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容