# webpack Plugin实现

本文来源于干货!撸一个 webpack 插件(内含 tapable 详解+webpack 流程) (opens new window)


Webpack 可以将其理解是一种基于事件流的编程范例,一个插件合集。

而将这些插件控制在 webapck 事件流上的运行的就是 webpack 自己写的基础类Tapable

Tapable 暴露出挂载plugin的方法,使我们能 将 plugin 控制在 webapack 事件流上运行(如下图)。后面我们将看到核心的对象  CompilerCompilation等都是继承于Tabable类。(如下图所示)

# Tabable 是什么?

tapable 库 (opens new window)暴露了很多 Hook(钩子)类,为插件提供挂载的钩子。

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook,
} = require("tapable");
1
2
3
4
5
6
7
8
9
10
11

# Tabable 用法

  • 1.new Hook 新建钩子
    • tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子。
    • class 接受数组参数 options,非必传。类方法会根据传参,接受同样数量的参数。
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
1
  • 2.使用 tap/tapAsync/tapPromise 绑定钩子

tabpack 提供了同步&异步绑定钩子的方法,并且他们都有绑定事件执行事件对应的方法。

Async* Sync*
绑定:tapAsync/tapPromise/tap 绑定:tap
执行:callAsync/promise 执行:call
  • 3.call/callAsync 执行绑定事件
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
//绑定事件到webapck事件流
hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3
//执行绑定的事件
hook1.call(1,2,3)

1
2
3
4
5
6

  • 举个栗子
    • 定义一个 Car 方法,在内部 hooks 上新建钩子。分别是同步钩子 accelerate、break(accelerate 接受一个参数)、异步钩子calculateRoutes
    • 使用钩子对应的绑定和执行方法
    • calculateRoutes 使用tapPromise可以返回一个promise对象。
//引入tapable
const { SyncHook, AsyncParallelHook } = require("tapable");
//创建类
class Car {
  constructor() {
    this.hooks = {
      accelerate: new SyncHook(["newSpeed"]),
      break: new SyncHook(),
      calculateRoutes: new AsyncParallelHook([
        "source",
        "target",
        "routesList",
      ]),
    };
  }
}
const myCar = new Car();
//绑定同步钩子
myCar.hooks.break.tap("WarningLampPlugin", () =>
  console.log("WarningLampPlugin")
);
//绑定同步钩子 并传参
myCar.hooks.accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log(`Accelerating to ${newSpeed}`)
);
//绑定一个异步Promise钩子
myCar.hooks.calculateRoutes.tapPromise(
  "calculateRoutes tapPromise",
  (source, target, routesList, callback) => {
    // return a promise
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log(`tapPromise to ${source}${target}${routesList}`);
        resolve();
      }, 1000);
    });
  }
);
//执行同步钩子
myCar.hooks.break.call();
myCar.hooks.accelerate.call("hello");
console.time("cost");
//执行异步钩子
myCar.hooks.calculateRoutes.promise("i", "love", "tapable").then(
  () => {
    console.timeEnd("cost");
  },
  (err) => {
    console.error(err);
    console.timeEnd("cost");
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

运行结果

WarningLampPlugin
Accelerating to hello
tapPromise to ilovetapable
cost: 1003.898ms

1
2
3
4
5

calculateRoutes 也可以使用tapAsync绑定钩子,注意:此时用callback结束异步回调。

myCar.hooks.calculateRoutes.tapAsync(
  "calculateRoutes tapAsync",
  (source, target, routesList, callback) => {
    // return a promise
    setTimeout(() => {
      console.log(`tapAsync to ${source}${target}${routesList}`);
      callback();
    }, 2000);
  }
);
myCar.hooks.calculateRoutes.callAsync("i", "like", "tapable", (err) => {
  console.timeEnd("cost");
  if (err) console.log(err);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

运行结果

WarningLampPlugin
Accelerating to hello
tapAsync to iliketapable
cost: 2007.850ms

1
2
3
4
5

# 进阶一下

到这里可能已经学会使用 tapable 了,但是它如何与 webapck/webpack 插件关联呢?

我们将刚才的代码稍作改动,拆成两个文件:Compiler.js、Myplugin.js

Compiler.js

  • 把 Class Car 类名改成 webpack 的核心Compiler
  • 接受 options 里传入的 plugins
  • 将 Compiler 作为参数传给 plugin
  • 执行 run 函数,在编译的每个阶段,都触发执行相对应的钩子函数。
const { SyncHook, AsyncParallelHook } = require("tapable");
class Compiler {
  constructor(options) {
    this.hooks = {
      accelerate: new SyncHook(["newSpeed"]),
      break: new SyncHook(),
      calculateRoutes: new AsyncParallelHook([
        "source",
        "target",
        "routesList",
      ]),
    };
    let plugins = options.plugins;
    if (plugins && plugins.length > 0) {
      plugins.forEach((plugin) => plugin.apply(this));
    }
  }
  run() {
    console.time("cost");
    this.accelerate("hello");
    this.break();
    this.calculateRoutes("i", "like", "tapable");
  }
  accelerate(param) {
    this.hooks.accelerate.call(param);
  }
  break() {
    this.hooks.break.call();
  }
  calculateRoutes() {
    const args = Array.from(arguments);
    this.hooks.calculateRoutes.callAsync(...args, (err) => {
      console.timeEnd("cost");
      if (err) console.log(err);
    });
  }
}
module.exports = Compiler;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

MyPlugin.js

  • 引入 Compiler

  • 定义一个自己的插件。

  • apply 方法接受 compiler 参数。

    webpack 插件是一个具有  apply  方法的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。

  • 给 compiler 上的钩子绑定方法。

  • 仿照 webpack 规则,向 plugins 属性传入 new 实例

const Compiler = require("./Compiler");
class MyPlugin {
  constructor() {}
  apply(conpiler) {
    //接受 compiler参数
    conpiler.hooks.break.tap("WarningLampPlugin", () =>
      console.log("WarningLampPlugin")
    );
    conpiler.hooks.accelerate.tap("LoggerPlugin", (newSpeed) =>
      console.log(`Accelerating to ${newSpeed}`)
    );
    conpiler.hooks.calculateRoutes.tapAsync(
      "calculateRoutes tapAsync",
      (source, target, routesList, callback) => {
        setTimeout(() => {
          console.log(`tapAsync to ${source}${target}${routesList}`);
          callback();
        }, 2000);
      }
    );
  }
}
//这里类似于webpack.config.js的plugins配置
//向 plugins 属性传入 new 实例
const myPlugin = new MyPlugin();
const options = {
  plugins: [myPlugin],
};
let compiler = new Compiler(options);
compiler.run();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

运行结果

Accelerating to hello
WarningLampPlugin
tapAsync to iliketapable
cost: 2015.866ms

1
2
3
4
5

改造后运行正常,仿照 Compiler 和 webpack 插件的思路慢慢得理顺插件的逻辑成功。

# Tabable 的其他方法

type function
Hook 所有钩子的后缀
Waterfall 同步方法,但是它会传值给下一个函数
Bail 熔断:当函数有任何返回值,就会在当前执行函数停止
Loop 监听函数返回 true 表示继续循环,返回 undefine 表示结束循环
Sync 同步方法
AsyncSeries 异步串行钩子
AsyncParallel 异步并行执行钩子

我们可以根据自己的开发需求,选择适合的同步/异步钩子。

# webpack 流程

通过上面的阅读,我们知道了如何在 webapck 事件流上挂载钩子。
假设现在要自定义一个插件更改最后产出资源的内容,我们应该把事件添加在哪个钩子上呢?哪一个步骤能拿到 webpack 编译的资源从而去修改?
所以接下来的任务是:了解 webpack 的流程。
贴一张淘宝团队分享的经典 webpack 流程图,再慢慢分析~

# 1. webpack 入口(webpack.config.js+shell options)

从配置文件 package.json 和 Shell 语句中读取与合并参数,得出最终的参数;

每次在命令行输入 webpack 后,操作系统都会去调用  ./node_modules/.bin/webpack  这个 shell 脚本。这个脚本会去调用  ./node_modules/webpack/bin/webpack.js  并追加输入的参数,如 -p , -w 。

# 2. 用 yargs 参数解析(optimist)

yargs.parse(process.argv.slice(2), (err, argv, output) => {});
1

源码地址 (opens new window)

# 3.webpack 初始化

(1)构建 compiler 对象

let compiler = new Webpack(options);
1

源码地址 (opens new window)

(2)注册 NOdeEnvironmentPlugin 插件

new NodeEnvironmentPlugin().apply(compiler);
1

源码地址 (opens new window)

(3)挂在 options 中的基础插件,调用WebpackOptionsApply库初始化基础插件。

if (options.plugins && Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    if (typeof plugin === "function") {
      plugin.apply(compiler);
    } else {
      plugin.apply(compiler);
    }
  }
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler);
1
2
3
4
5
6
7
8
9
10
11
12

源码地址 (opens new window)

# 4. run  开始编译

if (firstOptions.watch || options.watch) {
  const watchOptions =
    firstOptions.watchOptions || firstOptions.watch || options.watch || {};
  if (watchOptions.stdin) {
    process.stdin.on("end", function (_) {
      process.exit(); // eslint-disable-line
    });
    process.stdin.resume();
  }
  compiler.watch(watchOptions, compilerCallback);
  if (outputOptions.infoVerbosity !== "none")
    console.log("\nwebpack is watching the files…\n");
} else compiler.run(compilerCallback);
1
2
3
4
5
6
7
8
9
10
11
12
13

这里分为两种情况:
1)Watching:监听文件变化
2)run:执行编译
源码地址 (opens new window)

# 5.触发compile

(1)在 run 的过程中,已经触发了一些钩子:beforeRun->run->beforeCompile->compile->make->seal (编写插件的时候,就可以将自定义的方挂在对应钩子上,按照编译的顺序被执行)
(2)构建了关键的  Compilation对象
在 run()方法中,执行了 this.compile()
this.compile()中创建了 compilation

this.hooks.beforeRun.callAsync(this, err => {
    ...
	this.hooks.run.callAsync(this, err => {
        ...
		this.readRecords(err => {
            ...
			this.compile(onCompiled);
		});
	});
});
...
compile(callback) {
	const params = this.newCompilationParams();
	this.hooks.beforeCompile.callAsync(params, err => {
		...
		this.hooks.compile.call(params);
		const compilation = this.newCompilation(params);
		this.hooks.make.callAsync(compilation, err => {
            ...
			compilation.finish();
			compilation.seal(err => {
                ...
				this.hooks.afterCompile.callAsync(compilation, err
				    ...
					return callback(null, compilation);
				});
			});
		});
	});
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

源码地址 (opens new window)

const compilation = this.newCompilation(params);
1

Compilation负责整个编译过程,包含了每个构建环节所对应的方法。对象内部保留了对 compiler 的引用。
当 Webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。
划重点:Compilation 很重要!编译生产资源变换文件都靠它。

# 6.addEntry() make 分析入口文件创建模块对象

compile 中触发make事件并调用addEntry
webpack 的 make 钩子中, tapAsync 注册了一个DllEntryPlugin, 就是将入口模块通过调用 compilation。
这一注册在 Compiler.compile()方法中被执行。
addEntry 方法将所有的入口模块添加到编译构建队列中,开启编译流程。
DllEntryPlugin.js

compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => {
  compilation.addEntry(
    this.context,
    new DllEntryDependency(
      this.entries.map((e, idx) => {
        const dep = new SingleEntryDependency(e);
        dep.loc = {
          name: this.name,
          index: idx,
        };
        return dep;
      }),
      this.name
    ),
    this.name,
    callback
  );
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

源码地址 (opens new window)
流程走到这里让我觉得很奇怪:刚刚还在 Compiler.js 中执行 compile,怎么一下子就到了 DllEntryPlugin.js?
这就要说道之前WebpackOptionsApply.process()初始化插件的时候,执行了compiler.hooks.entryOption.call(options.context, options.entry);
WebpackOptionsApply.js

class WebpackOptionsApply extends OptionsApply {
	process(options, compiler) {
	    ...
	    compiler.hooks.entryOption.call(options.context, options.entry);
	}
}

1
2
3
4
5
6
7

process (opens new window)
entryOption (opens new window)
DllPlugin.js

compiler.hooks.entryOption.tap("DllPlugin", (context, entry) => {
  const itemToPlugin = (item, name) => {
    if (Array.isArray(item)) {
      return new DllEntryPlugin(context, item, name);
    }
    throw new Error("DllPlugin: supply an Array as entry");
  };
  if (typeof entry === "object" && !Array.isArray(entry)) {
    Object.keys(entry).forEach((name) => {
      itemToPlugin(entry[name], name).apply(compiler);
    });
  } else {
    itemToPlugin(entry, "main").apply(compiler);
  }
  return true;
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

DllPlugin (opens new window)
其实 addEntry 方法,存在很多入口,SingleEntryPlugin 也注册了 compiler.hooks.make.tapAsync 钩子。这里主要再强调一下WebpackOptionsApply.process()流程(233)。
入口有很多,有兴趣可以调试一下先后顺序~

# 7. 构建模块

compilation.addEntry中执行  _addModuleChain()这个方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。
通过 *ModuleFactory.create 方法创建模块,(有 NormalModule , MultiModule , ContextModule , DelegatedModule 等)对模块使用的 loader 进行加载。调用 acorn 解析经 loader 处理后的源文件生成抽象语法树 AST。遍历 AST,构建该模块所依赖的模块

addEntry(context, entry, name, callback) {
	const slot = {
		name: name,
		request: entry.request,
		module: null
	};
	this._preparedEntrypoints.push(slot);
	this._addModuleChain(
		context,
		entry,
		module => {
			this.entries.push(module);
		},
		(err, module) => {
			if (err) {
				return callback(err);
			}
			if (module) {
				slot.module = module;
			} else {
				const idx = this._preparedEntrypoints.indexOf(slot);
				this._preparedEntrypoints.splice(idx, 1);
			}
			return callback(null, module);
		}
	);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

addEntry addModuleChain()源码地址 (opens new window)

# 8. 封装构建结果(seal)

webpack 会监听 seal 事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。

template.getRenderMainfest.render()

1
2

通过模板(MainTemplate、ChunkTemplate)把 chunk 生产_webpack_requie()的格式。

# 9. 输出资源(emit)

把 Assets 输出到 output 的 path 中。

# 总结

webpack 是一个插件合集,由 tapable 控制各插件在 webpack 事件流上运行。主要依赖的是 compilation 的编译模块和封装。
webpack 的入口文件其实就实例了 Compiler 并调用了 run 方法开启了编译,webpack 的主要编译都按照下面的钩子调用顺序执行。

  • Compiler:beforeRun 清除缓存
  • Compiler:run 注册缓存数据钩子
  • Compiler:beforeCompile
  • Compiler:compile 开始编译
  • Compiler:make 从入口分析依赖以及间接依赖模块,创建模块对象
  • Compilation:buildModule 模块构建
  • Compiler:normalModuleFactory 构建
  • Compilation:seal 构建结果封装, 不可再更改
  • Compiler:afterCompile 完成构建,缓存数据
  • Compiler:emit 输出到 dist 目录

一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。
Compilation 对象也提供了很多事件回调供插件做扩展。
Compilation 中比较重要的部分是 assets 如果我们要借助 webpack 帮你生成文件,就要在 assets 上添加对应的文件信息。
compilation.getStats()能得到生产文件以及 chunkhash 的一些信息。等等

# 实战!写一个插件

这次尝试写一个简单的插件,帮助我们去除 webpack 打包生成的 bundle.js 中多余的注释

# 怎么写一个插件?

参照 webpack 官方教程Writing a Plugin (opens new window)
一个 webpack plugin 由一下几个步骤组成:

  1. 一个 JavaScript 类函数。
  2. 在函数原型 (prototype)中定义一个注入compiler对象的apply方法。
  3. apply函数中通过 compiler 插入指定的事件钩子,在钩子回调中拿到 compilation 对象
  4. 使用 compilation 操纵修改 webapack 内部实例数据。
  5. 异步插件,数据处理完后使用 callback 回调

# 完成插件初始架构

在之前说 Tapable 的时候,写了一个 MyPlugin 类函数,它已经满足了 webpack plugin 结构的前两点(一个 JavaScript 类函数,在函数原型 (prototype)中定义一个注入compiler
现在我们要让 Myplugin 满足后三点。首先,使用 compiler 指定的事件钩子。

class MyPlugin {
  constructor() {}
  apply(conpiler) {
    conpiler.hooks.break.tap("WarningLampPlugin", () =>
      console.log("WarningLampPlugin")
    );
    conpiler.hooks.accelerate.tap("LoggerPlugin", (newSpeed) =>
      console.log(`Accelerating to ${newSpeed}`)
    );
    conpiler.hooks.calculateRoutes.tapAsync(
      "calculateRoutes tapAsync",
      (source, target, routesList, callback) => {
        setTimeout(() => {
          console.log(`tapAsync to ${source}${target}${routesList}`);
          callback();
        }, 2000);
      }
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 插件的常用对象

对象 钩子
Compiler run,compile,compilation,make,emit,done
Compilation buildModule,normalModuleLoader,succeedModule,finishModules,seal,optimize,after-seal
Module Factory beforeResolver,afterResolver,module,parser
Module
Parser program,statement,call,expression
Template hash,bootstrap,localVars,render

# 编写插件

class MyPlugin {
  constructor(options) {
    this.options = options;
    this.externalModules = {};
  }
  apply(compiler) {
    var reg =
      /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)|(\/\*\*\*\*\*\*\/)/g;
    compiler.hooks.emit.tap("CodeBeautify", (compilation) => {
      Object.keys(compilation.assets).forEach((data) => {
        let content = compilation.assets[data].source(); // 欲处理的文本
        content = content.replace(reg, function (word) {
          // 去除注释后的文本
          return /^\/{2,}/.test(word) ||
            /^\/\*!/.test(word) ||
            /^\/\*{3,}\//.test(word)
            ? ""
            : word;
        });
        compilation.assets[data] = {
          source() {
            return content;
          },
          size() {
            return content.length;
          },
        };
      });
    });
  }
}
module.exports = MyPlugin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

第一步,使用 compiler 的 emit 钩子
emit 事件是将编译好的代码发射到指定的 stream 中触发,在这个钩子执行的时候,我们能从回调函数返回的 compilation 对象上拿到编译好的 stream。

compiler.hooks.emit.tap('xxx',(compilation)=>{})

1
2

第二步,访问 compilation 对象,我们用绑定提供了编译 compilation 引用的 emit 钩子函数,每一次编译都会拿到新的 compilation 对象。这些 compilation 对象提供了一些钩子函数,来钩入到构建流程的很多步骤中。
compilation 中会返回很多内部对象,不完全截图如下所示:

其中,我们需要的是compilation.assets

assetsCompilation {
  assets:
   { 'js/index/main.js':
      CachedSource {
        _source: [Object],
        _cachedSource: undefined,
        _cachedSize: undefined,
        _cachedMaps: {} } },
  errors: [],
  warnings: [],
  children: [],
  dependencyFactories:
   ArrayMap {
     keys:
      [ [Object],
        [Function: MultiEntryDependency],
        [Function: SingleEntryDependency],
        [Function: LoaderDependency],
        [Object],
        [Function: ContextElementDependency],
     values:
      [ NullFactory {},
        [Object],
        NullFactory {} ] },
  dependencyTemplates:
   ArrayMap {
     keys:
      [ [Object],
        [Object],
        [Object] ],
     values:
      [ ConstDependencyTemplate {},
        RequireIncludeDependencyTemplate {},
        NullDependencyTemplate {},
        RequireEnsureDependencyTemplate {},
        ModuleDependencyTemplateAsRequireId {},
        AMDRequireDependencyTemplate {},
        ModuleDependencyTemplateAsRequireId {},
        AMDRequireArrayDependencyTemplate {},
        ContextDependencyTemplateAsRequireCall {},
        AMDRequireDependencyTemplate {},
        LocalModuleDependencyTemplate {},
        ModuleDependencyTemplateAsId {},
        ContextDependencyTemplateAsRequireCall {},
        ModuleDependencyTemplateAsId {},
        ContextDependencyTemplateAsId {},
        RequireResolveHeaderDependencyTemplate {},
        RequireHeaderDependencyTemplate {} ] },
  fileTimestamps: {},
  contextTimestamps: {},
  name: undefined,
  _currentPluginApply: undefined,
  fullHash: 'f4030c2aeb811dd6c345ea11a92f4f57',
  hash: 'f4030c2aeb811dd6c345',
  fileDependencies: [ '/Users/mac/web/src/js/index/main.js' ],
  contextDependencies: [],
  missingDependencies: [] }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

优化所有 chunk 资源(asset)。资源(asset)会以 key-value 的形式被存储在  compilation.assets

第三步,遍历 assets。
1)assets 数组对象中的 key 是资源名,在 Myplugin 插件中,遍历 Object.key()我们拿到了

main.css
bundle.js
index.html

1
2
3
4

2)调用 Object.source() 方法,得到资源的内容

compilation.assets[data].source()

1
2

3)用正则,去除注释

Object.keys(compilation.assets).forEach((data)=> {
    let content = compilation.assets[data].source()
    content = content.replace(reg, function (word) {
        return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word;
    })
});

1
2
3
4
5
6
7

第四步,更新 compilation.assets[data]对象

compilation.assets[data] = {
    source(){
        return content
    },
    size(){
        return content.length
    }
}

1
2
3
4
5
6
7
8
9

第五步 在 webpack 中引用插件
webpack.config.js

const path  = require('path')
const MyPlugin = require('./plugins/MyPlugin')
module.exports = {
    entry:'./src/index.js',
    output:{
        path:path.resolve('dist'),
        filename:'bundle.js'
    },
    plugins:[
        ...
        new MyPlugin()
    ]
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

插件地址 (opens new window)