乐趣区

关于前端:手把手带你入门Webpack-Plugin

这是第 101 篇不掺水的原创,想获取更多原创好文,请搜寻公众号关注咱们吧~ 本文首发于政采云前端博客:手把手带你入门 Webpack Plugin

对于 Webpack

在讲 Plugin 之前,咱们先来理解下 Webpack。实质上,Webpack 是一个用于古代 JavaScript 应用程序的动态模块打包工具。它可能解析咱们的代码,生成对应的依赖关系,而后将不同的模块达成一个或多个 bundle。

Webpack 的基本概念包含了如下内容:

  1. Entry:Webpack 的入口文件,指的是应该从哪个模块作为入口,来构建外部依赖图。
  2. Output:通知 Webpack 在哪输入它所创立的 bundle 文件,以及输入的 bundle 文件该如何命名、输入到哪个门路下等规定。
  3. Loader:模块代码转化器,使得 Webpack 有能力去解决除了 JS、JSON 以外的其余类型的文件。
  4. Plugin:Plugin 提供执行更广的工作的性能,包含:打包优化,资源管理,注入环境变量等。
  5. Mode:依据不同运行环境执行不同优化参数时的必要参数。
  6. Browser Compatibility:反对所有 ES5 规范的浏览器(IE8 以上)。

理解完 Webpack 的基本概念之后,咱们再来看下,为什么咱们会须要 Plugin。

Plugin 的作用

我先举一个咱们政采云外部的案例:

在 React 我的项目中,个别咱们的 Router 文件是写在一个我的项目中的,如果我的项目中蕴含了许多页面,未免会呈现所有业务模块 Router 耦合的状况,所以咱们开发了一个 Plugin,在构建打包时,该 Plugin 会读取所有文件夹下的 index.js 文件,再合并到一起造成一个对立的 Router 文件,轻松解决业务耦合问题。这就是 Plugin 的利用(具体实现会在最初一大节阐明)。

来看一下咱们合成前我的项目代码构造:

├── package.json
├── README.md
├── zoo.config.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .stylelintrc
├── build(Webpack 配置目录)│   └── webpack.dev.conf.js
├── src
│   ├── index.hbs
│   ├── main.js(入口文件)│   ├── common(通用模块,包权限,对立报错拦挡等)│       └── ...
│   ├── components(我的项目公共组件)│       └── ...
│   ├── layouts(我的项目顶通)│       └── ...
│   ├── utils(公共类)│       └── ...
│   ├── routes(页面路由)│   │   ├── Hello(对应 Hello 页面的代码)│   │   │   ├── config(页面配置信息)│   │   │       └── ...
│   │   │   ├── models(dva 数据中心)│   │   │       └── ...
│   │   │   ├── services(申请相干接口定义)│   │   │       └── ...
│   │   │   ├── views(申请相干接口定义)│   │   │       └── ...
│   │   │   └── index.js(router 定义的路由信息)├── .eslintignore
├── .eslintrc
├── .gitignore
└── .stylelintrc

再看一下通过 Plugin 合成 Router 之后的构造:

├── package.json
├── README.md
├── zoo.config.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .stylelintrc
├── build(Webpack 配置目录)│   └── webpack.dev.conf.js
├── src
│   ├── index.hbs
│   ├── main.js(入口文件)│   ├── router-config.js(合成后的 router 文件)│   ├── common(通用模块,包权限,对立报错拦挡等)│       └── ...
│   ├── components(我的项目公共组件)│       └── ...
│   ├── layouts(我的项目顶通)│       └── ...
│   ├── utils(公共类)│       └── ...
│   ├── routes(页面路由)│   │   ├── Hello(对应 Hello 页面的代码)│   │   │   ├── config(页面配置信息)│   │   │       └── ...
│   │   │   ├── models(dva 数据中心)│   │   │       └── ...
│   │   │   ├── services(申请相干接口定义)│   │   │       └── ...
│   │   │   ├── views(申请相干接口定义)│   │   │       └── ...
├── .eslintignore
├── .eslintrc
├── .gitignore
└── .stylelintrc

总结来说 Plugin 的作用如下:

  1. 提供了 Loader 无奈解决的一些其余事件
  2. 提供弱小的扩大办法,能执行更广的工作

理解完 Plugin 的大抵作用之后,咱们来聊一聊如何创立一个 Plugin。

创立一个 Plugin

Hook

在聊创立 Plugin 之前,咱们先来聊一下什么是 Hook。

Webpack 在编译的过程中会触发一系列流程,而在这样一连串的流程中,Webpack 把一些要害的流程节点裸露进去供开发者应用,这就是 Hook,能够类比 React 的生命周期钩子。

Plugin 就是在这些 Hook 上暴露出办法供开发者做一些额定操作,在写 Plugin 的时候,也须要先理解咱们应该在哪个 Hook 上做操作。

如何创立 Plugin

咱们先来看一下 Webpack 官网给的案例:

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {apply(compiler) {
        // 代表开始读取 records 之前执行
        compiler.hooks.run.tap(pluginName, compilation => {console.log("webpack 构建过程开始!");
        });
    }
}

从下面的代码咱们能够总结如下内容:

  • Plugin 其实就是一个类。
  • 类须要一个 apply 办法,执行具体的插件办法。
  • 插件办法做了一件事件就是在 run 这个 Hook 上注册了一个同步的打印日志的办法。
  • apply 办法的入参注入了一个 compiler 实例,compiler 实例是 Webpack 的支柱引擎,代表了 CLI 和 Node API 传递的所有配置项。
  • Hook 回调办法注入了 compilation 实例,compilation 可能拜访以后构建时的模块和相应的依赖。
Compiler 对象蕴含了 Webpack 环境所有的的配置信息,蕴含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局惟一的,能够简略地把它了解为 Webpack 实例;Compilation 对象蕴含了以后的模块资源、编译生成资源、变动的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变动,一次新的 Compilation 将被创立。Compilation 对象也提供了很多事件回调供插件做扩大。通过 Compilation 也能读取到 Compiler 对象。—— 摘自「深入浅出 Webpack」
  • compiler 实例和 compilation 实例上别离定义了许多 Hooks,能够通过 实例.hooks. 具体 Hook 拜访,Hook 上还裸露了 3 个办法供应用,别离是 tap、tapAsync 和 tapPromise。这三个办法用于定义如何执行 Hook,比方 tap 示意注册同步 Hook,tapAsync 代表 callback 形式注册异步 hook,而 tapPromise 代表 Promise 形式注册异步 Hook,能够看下 Webpack 中对于这三种类型实现的源码,为不便浏览,我加了些正文。
// tap 办法的 type 是 sync,tapAsync 办法的 type 是 async,tapPromise 办法的 type 是 promise
// 源码取自 Hook 工厂办法:lib/HookCodeFactory.js
create(options) {this.init(options);
  let fn;
  // Webpack 通过 new Function 生成函数
  switch (this.options.type) {
    case "sync":
      fn = new Function(this.args(), // 生成函数入参
        '"use strict";\n' +
        this.header() + // 公共办法,生成一些须要定义的变量
        this.contentWithInterceptors({ // 生成理论执行的代码的办法
          onError: err => `throw ${err};\n`, // 谬误回调
          onResult: result => `return ${result};\n`, // 失去值的时候的回调
          resultReturns: true,
          onDone: () => "",
          rethrowIfPossible: true
        })
      );
      break;
    case "async":
      fn = new Function(
        this.args({after: "_callback"}),
        '"use strict";\n' +
        this.header() + // 公共办法,生成一些须要定义的变量
        this.contentWithInterceptors({onError: err => `_callback(${err});\n`, // 谬误时执行回调办法
          onResult: result => `_callback(null, ${result});\n`, // 失去后果时执行回调办法
          onDone: () => "_callback();\n" // 无后果,执行实现时
        })
      );
      break;
    case "promise":
      let errorHelperUsed = false;
      const content = this.contentWithInterceptors({
        onError: err => {
          errorHelperUsed = true;
          return `_error(${err});\n`;
        },
        onResult: result => `_resolve(${result});\n`,
        onDone: () => "_resolve();\n"
      });
      let code = "";
      code += '"use strict";\n';
      code += this.header(); // 公共办法,生成一些须要定义的变量
      code += "return new Promise((function(_resolve, _reject) {\n"; // 返回的是 Promise
      if (errorHelperUsed) {
        code += "var _sync = true;\n";
        code += "function _error(_err) {\n";
        code += "if(_sync)\n";
        code +=
          "_resolve(Promise.resolve().then((function() {throw _err;})));\n";
        code += "else\n";
        code += "_reject(_err);\n";
        code += "};\n";
      }
      code += content; // 判断具体执行_resolve 办法还是执行_error 办法
      if (errorHelperUsed) {code += "_sync = false;\n";}
      code += "}));\n";
      fn = new Function(this.args(), code);
      break;
  }
  this.deinit(); // 清空 options 和 _args
  return fn;
}

Webpack 共提供了以下十种 Hooks,代码中所有具体的 Hook 都是以下这 10 种中的一种。

// 源码取自:lib/index.js
"use strict";

exports.__esModule = true;
// 同步执行的钩子,不能解决异步工作
exports.SyncHook = require("./SyncHook");
// 同步执行的钩子,返回非空时,阻止向下执行
exports.SyncBailHook = require("./SyncBailHook");
// 同步执行的钩子,反对将返回值透传到下一个钩子中
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
// 同步执行的钩子,反对将返回值透传到下一个钩子中,返回非空时,反复执行
exports.SyncLoopHook = require("./SyncLoopHook");
// 异步并行的钩子
exports.AsyncParallelHook = require("./AsyncParallelHook");
// 异步并行的钩子,返回非空时,阻止向下执行,间接执行回调
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
// 异步串行的钩子
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
// 异步串行的钩子,返回非空时,阻止向下执行,间接执行回调
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
// 反对异步串行 && 并行的钩子,返回非空时,反复执行
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
// 异步串行的钩子,下一步依赖上一步返回的值
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
// 以下 2 个是 hook 工具类,别离用于 hooks 映射以及 hooks 重定向
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");

举几个简略的例子:

  • 下面官网案例中的 run 这个 Hook,会在开始读取 records 之前执行,它的类型是 AsyncSeriesHook,查看源码能够发现,run Hook 既能够执行同步的 tap 办法,也能够执行异步的 tapAsync 和 tapPromise 办法,所以以下写法也是能够的:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {apply(compiler) {compiler.hooks.run.tapAsync(pluginName, (compilation, callback) => {setTimeout(() => {console.log("webpack 构建过程开始!");
              callback(); // callback 办法为了让构建继续执行上来,必须要调用}, 1000);
        });
    }
}
  • 再举一个例子,比方 failed 这个 Hook,会在编译失败之后执行,它的类型是 SyncHook,查看源码能够发现,调用 tapAsync 和 tapPromise 办法时,会间接抛错。

对于一些同步的办法,举荐间接应用 tap 进行注册办法,对于异步的计划,tapAsync 通过执行 callback 办法实现回调,如果执行的办法返回的是一个 Promise,举荐应用 tapPromise 进行办法的注册

Hook 的类型能够通过官网 API 查问,地址传送门

// 源码取自:lib/SyncHook.js
const TAP_ASYNC = () => {throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {throw new Error("tapPromise is not supported on a SyncHook");
};

function SyncHook(args = [], name = undefined) {const hook = new Hook(args, name);
  hook.constructor = SyncHook;
  hook.tapAsync = TAP_ASYNC;
  hook.tapPromise = TAP_PROMISE;
  hook.compile = COMPILE;
  return hook;
}

解说完具体的执行办法之后,咱们再聊一下 Webpack 流程以及 Tapable 是什么。

Webpack && Tapable

Webpack 运行机制

要了解 Plugin,咱们先大抵理解 Webpack 打包的流程

  1. 咱们打包的时候,会先合并 Webpack config 文件和命令行参数,合并为 options。
  2. 将 options 传入 Compiler 构造方法,生成 compiler 实例,并实例化了 Compiler 上的 Hooks。
  3. compiler 对象执行 run 办法,并主动触发 beforeRun、run、beforeCompile、compile 等要害 Hooks。
  4. 调用 Compilation 构造方法创立 compilation 对象,compilation 负责管理所有模块和对应的依赖,创立实现后触发 make Hook。
  5. 执行 compilation.addEntry() 办法,addEntry 用于剖析所有入口文件,逐级递归解析,调用 NormalModuleFactory 办法,为每个依赖生成一个 Module 实例,并在执行过程中触发 beforeResolve、resolver、afterResolve、module 等要害 Hooks。
  6. 将第 5 步中生成的 Module 实例作为入参,执行 Compilation.addModule() 和 Compilation.buildModule() 办法递归创立模块对象和依赖模块对象。
  7. 调用 seal 办法生成代码,整顿输入主文件和 chunk,并最终输入。

Tapable

Tapable 是 Webpack 外围工具库,它提供了所有 Hook 的抽象类定义,Webpack 许多对象都是继承自 Tapable 类。比方下面说的 tap、tapAsync 和 tapPromise 都是通过 Tapable 进行裸露的。源码如下(截取了局部代码):

// 第二节“创立一个 Plugin”中说的 10 种 Hooks 都是继承了这两个类
// 源码取自:tapable.d.ts
declare class Hook<T, R, AdditionalOptions = UnsetAdditionalOptions> {tap(options: string | Tap & IfSet<AdditionalOptions>, fn: (...args: AsArray<T>) => R): void;
}

declare class AsyncHook<T, R, AdditionalOptions = UnsetAdditionalOptions> extends Hook<T, R, AdditionalOptions> {
  tapAsync(
    options: string | Tap & IfSet<AdditionalOptions>,
    fn: (...args: Append<AsArray<T>, InnerCallback<Error, R>>) => void
  ): void;
  tapPromise(
    options: string | Tap & IfSet<AdditionalOptions>,
    fn: (...args: AsArray<T>) => Promise<R>
  ): void;
}

常见 Hooks API

能够参考 Webpack

本文列举一些罕用 Hooks 和其对应的类型:

Compiler Hooks

Hook type 调用
run AsyncSeriesHook 开始读取 records 之前
compile SyncHook 一个新的编译 (compilation) 创立之后
emit AsyncSeriesHook 生成资源到 output 目录之前
done SyncHook 编译 (compilation) 实现

Compilation Hooks

Hook type 调用
buildModule SyncHook 在模块构建开始之前触发
finishModules SyncHook 所有模块都实现构建
optimize SyncHook 优化阶段开始时触发

Plugin 在我的项目中的利用

讲完这么多理论知识,接下来咱们来看一下 Plugin 在我的项目中的实战:如何将各个子模块中的 router 文件合并到 router-config.js 中。

背景:

在 React 我的项目中,个别咱们的 Router 文件是写在一个我的项目中的,如果我的项目中蕴含了许多页面,未免会呈现所有业务模块 Router 耦合的状况,所以咱们开发了一个 Plugin,在构建打包时,该 Plugin 会读取所有文件夹下的 Router 文件,再合并到一起造成一个对立的 Router Config 文件,轻松解决业务耦合问题。这就是 Plugin 的利用。

实现:

const fs = require('fs');
const path = require('path');
const _ = require('lodash');

function resolve(dir) {return path.join(__dirname, '..', dir);
}

function MegerRouterPlugin(options) {// options 是配置文件,你能够在这里进行一些与 options 相干的工作}

MegerRouterPlugin.prototype.apply = function (compiler) {
  // 注册 before-compile 钩子,触发文件合并
  compiler.plugin('before-compile', (compilation, callback) => {
    // 最终生成的文件数据
    const data = {};
    const routesPath = resolve('src/routes');
    const targetFile = resolve('src/router-config.js');
    // 获取门路下所有的文件和文件夹
    const dirs = fs.readdirSync(routesPath);
    try {dirs.forEach((dir) => {const routePath = resolve(`src/routes/${dir}`);
        // 判断是否是文件夹
        if (!fs.statSync(routePath).isDirectory()) {return true;}
        delete require.cache[`${routePath}/index.js`];
        const routeInfo = require(routePath);
        // 多个 view 的状况下,遍历生成 router 信息
        if (!_.isArray(routeInfo)) {generate(routeInfo, dir, data);
        // 单个 view 的状况下,间接生成
        } else {routeInfo.map((config) => {generate(config, dir, data);
          });
        }
      });
    } catch (e) {console.log(e);
    }

    // 如果 router-config.js 存在,判断文件数据是否雷同,不同删除文件后再生成
    if (fs.existsSync(targetFile)) {delete require.cache[targetFile];
      const targetData = require(targetFile);
      if (!_.isEqual(targetData, data)) {writeFile(targetFile, data);
      }
    // 如果 router-config.js 不存在,间接生成文件
    } else {writeFile(targetFile, data);
    }

    // 最初调用 callback,继续执行 webpack 打包
    callback();});
};
// 合并以后文件夹下的 router 数据,并输入到 data 对象中
function generate(config, dir, data) {
  // 合并 router
  mergeConfig(config, dir, data);
  // 合并子 router
  getChildRoutes(config.childRoutes, dir, data, config.url);
}
// 合并 router 数据到 targetData 中
function mergeConfig(config, dir, targetData) {const { view, models, extraModels, url, childRoutes, ...rest} = config;
  // 获取 models,并去除 src 字段
  const dirModels = getModels(`src/routes/${dir}/models`, models);
  const data = {...rest,};
  // view 拼接到 path 字段
  data.path = `${dir}/views${view ? `/${view}` : ''}`;
  // 如果有 extraModels,就拼接到 models 对象上
  if (dirModels.length || (extraModels && extraModels.length)) {data.models = mergerExtraModels(config, dirModels);
  }
  Object.assign(targetData, {[url]: data,
  });
}
// 拼接 dva models
function getModels(modelsDir, models) {if (!fs.existsSync(modelsDir)) {return [];
  }
  let files = fs.readdirSync(modelsDir);
  // 必须要以 js 或者 jsx 结尾
  files = files.filter((item) => {return /\.jsx?$/.test(item);
  });
  // 如果没有定义 models,默认取 index.js
  if (!models || !models.length) {if (files.indexOf('index.js') > -1) {
      // 去除 src
      return [`${modelsDir.replace('src/', '')}/index.js`];
    }
    return [];}
  return models.map((item) => {if (files.indexOf(`${item}.js`) > -1) {
      // 去除 src
      return `${modelsDir.replace('src/', '')}/${item}.js`;
    }
  });
}
// 合并 extra models
function mergerExtraModels(config, models) {return models.concat(config.extraModels ? config.extraModels : []);
}
// 合并子 router
function getChildRoutes(childRoutes, dir, targetData, oUrl) {if (!childRoutes) {return;}
  childRoutes.map((option) => {
    option.url = oUrl + option.url;
    if (option.childRoutes) {
      // 递归合并子 router
      getChildRoutes(option.childRoutes, dir, targetData, option.url);
    }
    mergeConfig(option, dir, targetData);
  });
}

// 写文件
function writeFile(targetFile, data) {fs.writeFileSync(targetFile, `module.exports = ${JSON.stringify(data, null, 2)}`, 'utf-8');
}

module.exports = MegerRouterPlugin;

后果:

合并前的文件:

module.exports = [
  {
    url: '/category/protocol',
    view: 'protocol',
  },
  {
    url: '/category/sync',
    models: ['sync'],
    view: 'sync',
  },
  {
    url: '/category/list',
    models: ['category', 'config', 'attributes', 'group', 'otherSet', 'collaboration'],
    view: 'categoryRefactor',
  },
  {
    url: '/category/conversion',
    models: ['conversion'],
    view: 'conversion',
  },
];

合并后的文件:

module.exports = {
  "/category/protocol": {"path": "Category/views/protocol"},
  "/category/sync": {
    "path": "Category/views/sync",
    "models": ["routes/Category/models/sync.js"]
  },
  "/category/list": {
    "path": "Category/views/categoryRefactor",
    "models": [
      "routes/Category/models/category.js",
      "routes/Category/models/config.js",
      "routes/Category/models/attributes.js",
      "routes/Category/models/group.js",
      "routes/Category/models/otherSet.js",
      "routes/Category/models/collaboration.js"
    ]
  },
  "/category/conversion": {
    "path": "Category/views/conversion",
    "models": ["routes/Category/models/conversion.js"]
  },
}

最终我的项目就会生成 router-config.js 文件

结尾

心愿大家看完本章之后,对 Webpack Plugin 有一个初步的意识,可能上手写一个本人的 Plugin 来利用到本人的我的项目中。

文章中如有不对的中央,欢送斧正。

举荐浏览

通过自定义 Vue 指令实现前端曝光埋点

H5 页面列表缓存计划

开源作品

  • 政采云前端小报

开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交换群)

招贤纳士

政采云前端团队(ZooTeam),一个年老富裕激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员形成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端利用、数据分析及可视化等方向进行技术摸索和实战,推动并落地了一系列的外部技术产品,继续摸索前端技术体系的新边界。

如果你想扭转始终被事折腾,心愿开始能折腾事;如果你想扭转始终被告诫须要多些想法,却无从破局;如果你想扭转你有能力去做成那个后果,却不须要你;如果你想扭转你想做成的事须要一个团队去撑持,但没你带人的地位;如果你想扭转既定的节奏,将会是“5 年工作工夫 3 年工作教训”;如果你想扭转原本悟性不错,但总是有那一层窗户纸的含糊… 如果你置信置信的力量,置信平凡人能成就不凡事,置信能遇到更好的本人。如果你心愿参加到随着业务腾飞的过程,亲手推动一个有着深刻的业务了解、欠缺的技术体系、技术发明价值、影响力外溢的前端团队的成长历程,我感觉咱们该聊聊。任何工夫,等着你写点什么,发给 ZooTeam@cai-inc.com

退出移动版