实战webpack-YYDS:Webpack插件开发

第一次接触webpack的第一反应是什么(⊙_⊙)? 为什么这么复杂,感觉这么难实战webpack,算了! 时间是个好东西。 随着对后端工程的实践和理解逐渐深入,我接触webpack也越来越多,但最终还是被ta吓倒了,忍不住大喊“webpack yyds(永远)!”

去年年中实战webpack,本想写一些关于webpack的文章,但由于种种原因而被耽搁了(主要是对webpack了解不够,所以不敢自己写); 快过年了,有时间了,不如去“钓鱼”一下Touch webpack,整理一些“年货”分享给需要的xdm吧! 以后会继续写一些[Webpack]系列文章,由xdm监督...

指导

本文主要介绍通过实现一个cdn优化插件CdnPluginInject来进行webpack插件插件开发的具体流程,其中会涉及到html-webpack-plugin插件的使用、vue/cli3+项目中webpack插件的配置以及webpack相关知识的讲解点。 全文约2800+字,预计时长5~10分钟。 希望xdm读完之后能学到一些东西,思考一下,输出一些东西!

注:文章中的例子基于vue/cli3+项目!

1.cdn常规使用

索引.html:


  ···


  <div id="app">

  <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js">
  <script src="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js">
  ···

vue.config.js:

module.exports = {
  ···
  configureWebpack: {
    ···
    externals: {
      'vuex''Vuex',
      'vue-router''VueRouter',
      ···
    }
  },

webpack官网_实战webpack

2. 开发webpack插件

webpack官网是这样介绍的:该插件为第三方开发者提供了webpack引擎中完整的能力。 使用分阶段构建反弹,开发人员可以将自己的行为引入到 webpack 构建过程中。 创建插件比创建加载器更中间,因为您需要了解 webpack 的一些底层内部结构才能实现相应的钩子!

插件包括:

// 一个 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();
       }
     );
 }
}

3、cdn优化插件的实现

想法:

实施步骤:

1.创建一个命名的JavaScript函数(使用ES6类实现)

实战webpack_webpack官网

创建一个类cdnPluginInject,添加该类的构造函数来接收传入的参数; 这里我们定义接收参数的格式如下:


modules:[
  {
    name: "xxx",    //cdn包的名字
    var: "xxx",    //cdn引入库在项目中使用时的变量名
    path: "http://cdn.url/xxx.js" //cdn的url链接地址
  },
  ···
]

定义该类的变量模块,用于接收传入的cdn参数的处理结果:

class CdnPluginInject {
  constructor({
    modules,
  }) {
    // 如果是数组,将this.modules变换成对象形式
    this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; 
  }
 ···
}
module.exports = CdnPluginInject;

2.在其原型上定义apply方法

插件由构造函数实例化,其原型对象具有 apply 方法。 当插件安装时,webpack 编译器会调用一次 apply 方法。 apply方法可以接收一个webpack编译器对象的引用,这样就可以在回调函数中访问该编译器对象

cdnPluginInject.js的代码如下:

class CdnPluginInject {
  constructor({
    modules,
  }) {
    // 如果是数组,将this.modules变换成对象形式
    this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; 
  }
  //webpack plugin开发的执行入口apply方法
  apply(compiler) {
    ···
  }

module.exports = CdnPluginInject;

实战webpack_webpack官网

3.指定一个接触webpack本身的storm hook

这里接触到编译钩子:编译(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

这一步主要是将CDN的script标签插入到index.html中; 如何实现? 虽然webpack在vue项目中打包时使用html-webpack-plugin生成.html文件,但是我们也可以使用html-webpack-plugin来操作html文件,在这里插入cdn脚本标签。

// 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;

接下来逐步分析上面的实现:

5、设置webpack externals的外部扩展

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

6. 回调;

返回回调告诉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优化插件的使用

实战webpack_webpack官网

在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:

五、总结

一些webpack大鳄看完之后肯定有一点疑惑。 这个插件不就是webpack-cdn-plugin的乞丐版吗! CdnPluginInject 只是我在研究 webpack-cdn-plugin 源码的基础上,结合我的项目实际需要修改的扩展版本。 相比于 webpack-cdn-plugin 封装了 cdn 链接的生成,CdnPluginInject 直接进行 cdn 链接配置,选择 CDN 显示配置更加简单。 如果你想了解更多关于xdm的知识,可以查看webpack-cdn-plugin的源码。 经过作者的不断迭代更新,它提供的可配置参数更加丰富,功能更加强大(再次膜拜)。