2

YYDS: Webpack Plugin开发

 3 years ago
source link: http://www.cnblogs.com/wawoweb/p/14299978.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

6R7nMfv.png!mobile

目录

  • 二、开发一个webpack plugin
    • 1、创建一个具名 JavaScript 函数(使用ES6的class实现)
    • 2、在它的原型上定义 apply 方法
    • 3、指定一个触及到 webpack 本身的事件钩子
    • 4、在钩子事件中操作index.html
    • 5、设置webpack的外部扩展externals
    • configureWebpack中配置:
作为一名踏足前端时间不长的小开发必须得聊一聊 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)创建之后,执行插件。

r2YRFrn.png!mobile

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

文章参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK