zoukankan      html  css  js  c++  java
  • YYDS: Webpack Plugin开发



      作为一名踏足前端时间不长的小开发必须得聊一聊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)创建之后,执行插件。

      compilation compiler 的一个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记得 一键三连 哟!

    文章参考

  • 相关阅读:
    HLG 1522 子序列的和【队列的应用】
    POJ 3273 Monthly Expense【二分】
    HDU 4004 The Frog's Games 【二分】
    POJ 2001 Shortest Prefixes【第一棵字典树】
    POJ 2823 Sliding Window【单调对列经典题目】
    HDU 1969 Pie 【二分】
    POJ 3125 Printer Queue【暴力模拟】
    POJ 3250 Bad Hair Day【单调栈】
    字典树【模板】
    验证码 Code
  • 原文地址:https://www.cnblogs.com/wawoweb/p/14299978.html
Copyright © 2011-2022 走看看