6
头图

This is the 101st original without water. If you want to get more original good articles, please search the public and follow us~ This article first appeared on the front-end blog of Zheng Caiyun: 160dada1f6deb9 Take you hands-on Webpack Plugin

About Webpack

Before talking about Plugin, let's first understand Webpack . Essentially, Webpack is a static module packaging tool for modern JavaScript applications. It can parse our code, generate corresponding dependencies, and then combine different modules into one or more bundles.

The basic concepts of Webpack

  1. Entry: Webpack's entry file refers to which module should be used as the entry to build the internal dependency graph.
  2. Output: Tell Webpack where to output the bundle files it creates, how to name the output bundle files, and which path to output to and other rules.
  3. Loader: A module code converter that enables Webpack to process other types of files besides JS and JSON.
  4. Plugin: Plugin provides functions to perform a wider range of tasks, including: packaging optimization, resource management, injecting environment variables, etc.
  5. Mode: Necessary parameters when performing different optimization parameters according to different operating environments.
  6. Browser Compatibility: Supports all ES5 standard browsers (IE8 and above).

After understanding the basic concepts of Webpack, let's take a look at why we need Plugin.

The role of Plugin

Let me first give an internal case of our Zheng Caiyun:

In the React project, our Router file is generally written in a project. If the project contains many pages, it is inevitable that all business modules Router will be coupled. Therefore, we developed a Plugin. When building and packaging, the Plugin will read index.js files in all folders and merge them together to form a unified Router file, which can easily solve business coupling problems. This is the application of Plugin (the specific implementation will be explained in the last section).

Take a look at the code structure of our pre-synthesis project:

├── 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

Let's take a look at the structure after the Router is synthesized by Plugin:

├── 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

In summary, the role of Plugin is as follows:

  1. Provides some other things that Loader cannot solve
  2. Provide a powerful extension method to perform a wider range of tasks

After understanding the general function of Plugin, let's talk about how to create a Plugin.

Create a Plugin

Hook

Before talking about creating Plugin, let's talk about what Hook is.

Webpack triggers a series of processes during the compilation process. In such a series of processes, Webpack exposes some key process nodes for developers to use. This is Hook, which can be analogous to React's lifecycle hook.

Plugin exposes methods on these Hooks for developers to do some additional operations. When writing Plugin, we also need to understand which Hook we should operate on.

How to create a Plugin

Let's take a look at the official case given by Webpack:

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

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

From the above code, we can summarize the following:

  • Plugin is actually a class.
  • The class needs an apply method to execute specific plug-in methods.
  • One thing the plug-in method does is to register a synchronous log printing method on the run hook.
  • The input parameter of the apply method is injected with a compiler instance. The compiler instance is the backbone engine of Webpack and represents all the configuration items passed by CLI and Node API.
  • The Hook callback method injects a compilation instance, and the compilation can access the module and corresponding dependencies at the current build time.
Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;

Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
                                              —— 摘自「深入浅出 Webpack」
  • There are many Hooks defined on the compiler instance and the compilation instance respectively, which can be accessed through instance.hooks. Specific Hooks. Hooks also expose 3 methods for use, namely tap, tapAsync and tapPromise. These three methods are used to define how to execute Hooks. For example, tap means registering synchronous hooks, tapAsync means registering asynchronous hooks in callback mode, and tapPromise means registering asynchronous hooks in Promise mode. You can look at the source code of these three types of implementation in Webpack. For easy reading, I added some notes.
// 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 provides the following ten kinds of Hooks, and all the specific Hooks in the code are one of the following ten kinds.

// 源码取自: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");

Give a few simple examples:

  • The run Hook in the official case above will be executed before starting to read the records. Its type is AsyncSeriesHook. Looking at the source code, you can find that run Hook can execute both the synchronous tap method and the asynchronous tapAsync and tapPromise methods, so The following writing is also possible:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
    apply(compiler) {
        compiler.hooks.run.tapAsync(pluginName, (compilation, callback) => {
            setTimeout(() => {
              console.log("webpack 构建过程开始!");
              callback(); // callback 方法为了让构建继续执行下去,必须要调用
            }, 1000);
        });
    }
}
  • To give another example, for example, the failed Hook will be executed after the compilation fails. Its type is SyncHook. If you look at the source code, you can find that when the tapAsync and tapPromise methods are called, an error will be thrown directly.

For some synchronous methods, it is recommended to use tap directly to register the method. For asynchronous solutions, tapAsync implements the callback by executing the callback method. If the executed method returns a Promise, it is recommended to use tapPromise to register the method.

The type of hook can be queried through the official API, address portal

// 源码取自: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;
}

After explaining the specific execution method, let's talk about the Webpack process and what Tapable is.

Webpack && Tapable

Webpack operating mechanism

To understand Plugin, we first have a general understanding of the Webpack packaging process

  1. When we pack, we will first merge the Webpack config file and the command line parameters into options.
  2. Pass options to the Compiler construction method, generate a compiler instance, and instantiate Hooks on the Compiler.
  3. The compiler object executes the run method and automatically triggers key Hooks such as beforeRun, run, beforeCompile, and compile.
  4. The Compilation construction method is called to create a compilation object. The compilation is responsible for managing all modules and corresponding dependencies. After the creation is completed, make Hook is triggered.
  5. Execute the compilation.addEntry() method, addEntry is used to analyze all entry files, recursively resolve step by step, call the NormalModuleFactory method, generate a Module instance for each dependency, and trigger key Hooks such as beforeResolve, resolver, afterResolve, and module during execution .
  6. Take the Module instance generated in step 5 as an input parameter, and execute the Compilation.addModule() and Compilation.buildModule() methods to recursively create module objects and dependent module objects.
  7. Call the seal method to generate code, organize and output the main file and chunk, and finally output.

Tapable

Tapable is the core tool library of Webpack. It provides abstract class definitions for all hooks. Many objects of Webpack inherit from the Tapable class. For example, the tap, tapAsync and tapPromise mentioned above are all exposed through Tapable. The source code is as follows (some code is intercepted):

// 第二节 “创建一个 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;
}

Common Hooks API

You can refer to Webpack

This article lists some commonly used Hooks and their corresponding types:

Compiler Hooks

Hooktypetransfer
runAsyncSeriesHookBefore starting to read records
compileSyncHookAfter a new compilation is created
emitAsyncSeriesHookBefore generating resources to the output directory
doneSyncHookCompilation completed

Compilation Hooks

Hooktypetransfer
buildModuleSyncHookTriggered before the start of the module build
finishModulesSyncHookAll modules are built
optimizeSyncHookTriggered at the beginning of the optimization phase

Plugin application in the project

After talking about so much theoretical knowledge, let's take a look at the actual combat of Plugin in the project: how to merge the router files in each sub-module into router-config.js.

background:

In the React project, our Router file is generally written in a project. If the project contains many pages, it is inevitable that all business modules Router will be coupled. Therefore, we developed a Plugin. When building and packaging, the Plugin will read the Router files in all folders and merge them together to form a unified Router Config file, which can easily solve the business coupling problem. This is the application of Plugin.

achieve:

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;

result:

Files before merging:

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',
  },
];

The merged file:

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"
    ]
  },
}

The final project will generate router-config.js file

end

I hope that after reading this chapter, you will have a preliminary understanding of Webpack Plugin and be able to write your own Plugin to apply it to your project.

If there is something wrong in the article, please correct me.

Recommended reading

realize the front-end exposure buried point

H5 page list caching scheme

Open source works

  • Front-end tabloid of Zheng Caiyun

open source address www.zoo.team/openweekly/ (WeChat exchange group on the homepage of the tabloid official website)

Recruitment

ZooTeam, a young, passionate and creative front-end team, is affiliated to the product development department of Zheng Caiyun. The Base is located in picturesque Hangzhou. The team now has more than 40 front-end partners with an average age of 27 years old. Nearly 30% are full-stack engineers, a proper youth storm troupe. The membership consists of not only “veteran” soldiers from Ali and Netease, as well as newcomers from Zhejiang University, University of Science and Technology of China, Hangzhou Electric Power and other schools. In addition to the daily business docking, the team also conducts technical exploration and actual combat in the material system, engineering platform, building platform, performance experience, cloud application, data analysis and visualization, and promotes and implements a series of internal technical products. Explore the new boundaries of the front-end technology system.

If you want to change that you have been tossed by things, hope to start to toss things; if you want to change and have been warned, you need more ideas, but you can’t break the game; if you want to change you have the ability to make that result, but you don’t need you; if If you want to change what you want to accomplish, you need a team to support it, but there is no position for you to lead people; if you want to change the established rhythm, it will be "5 years of work time and 3 years of work experience"; if you want to change the original The comprehension is good, but there is always the ambiguity of the window paper... If you believe in the power of belief, believe that ordinary people can achieve extraordinary things, believe that you can meet a better self. If you want to participate in the process of business take-off, and personally promote the growth of a front-end team with in-depth business understanding, complete technical system, technology to create value, and influence spillover, I think we should talk. Anytime, waiting for you to write something, send it to ZooTeam@cai-inc.com


政采云前端团队
3.8k 声望4k 粉丝

Z 是政采云拼音首字母,oo 是无穷的符号,结合 Zoo 有生物圈的含义。寄望我们的前端 ZooTeam 团队,不论是人才梯队,还是技术体系,都能各面兼备,成长为一个生态,卓越且持续卓越。