_

阅读目录

一:webpack插件的基本原理

webpack构建工具大家应该不陌生了,那么下面我们来简单的了解下什么是webpack的插件。比如我现在写了一个插件叫 "kongzhi-plugin" 这个插件。那么这个插件在处理webpack编译过程中会处理一些特定的任务。

比如我们现在在webpack.config.js 中引入了一个如下插件:

// 引入打包html文件
const HtmlWebpackPlugin = require('html-webpack-plugin');

然后我们需要如下使用该插件:

module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html' // 模版文件
}),
]
};

如上就是一个 HtmlWebpackPlugin 插件 及在webpack中使用的方式了。现在我们需要实现一个类似的webpack的插件。

webpack打包是一种事件流的机制,它的原理是将各个插件串联起来。那么实现这一切的核心就是tapable,要想深入了解 tapable的知识可以看我之前的一篇文章.

tapable它可以暴露出挂载plugin的方法。可以让我们能将plugin控制在webpack事件流上运行。
tapable给我们暴露了很多钩子类,能为我们的插件提供挂载的钩子。
如下代码所示:

const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require('tapable');

如上各个钩子的含义及使用方式,可以看我之前这篇文章的介绍。

下面我们来看个简单的demo,我们会定义一个 KongZhiClass 类,在内部我们创建一个 hooks 这个对象,然后在该对象上分别创建同步钩子kzSyncHook及异步钩子 kzAsyncHook。 然后分别执行,代码如下:

const { SyncHook, AsyncParallelHook } = require('tapable');

// 创建类 

class KongZhiClass {
constructor() {
this.hooks = {
kzSyncHook: new SyncHook(['name', 'age']),
kzAsyncHook: new AsyncParallelHook(['name', 'age'])
}
}
} // 实例化
const myName = new KongZhiClass(); // 绑定同步钩子
myName.hooks.kzSyncHook.tap("eventName1", (name, age) => {
console.log(`同步事件eventName1: ${name} this year ${age} 周岁了, 可是还是单身`);
}); // 绑定一个异步Promise钩子
myName.hooks.kzAsyncHook.tapPromise('eventName2', (name, age) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`异步事件eventName2: ${name} this year ${age}周岁了,可是还是单身`);
}, 1000);
});
}); // 执行同步钩子
myName.hooks.kzSyncHook.call('空智', 31); // 执行异步钩子
myName.hooks.kzAsyncHook.promise('空智', 31).then(() => {
console.log('异步事件执行完毕');
}, (err) => {
console.log('异步事件执行异常:' + err);
})

执行结果如下:

如上是我们使用的 tapable 的使用方式,现在我们需要使用tapable的demo来和我们的webpack的插件相关联起来,我们要如何做呢?

我们可以将上面的代码来拆分成两个文件:compiler.js、main.js. (main.js 是入口文件)

假如我们的项目结构如下:

|--- tapable项目
| |--- node_modules
| |--- public
| | |--- js
| | | |--- main.js
| | | |--- compiler.js
| |--- package.json
| |--- webpack.config.js

compiler.js 需要做的事情如下:

1. 定义一个 Compiler 类,接收一个options对象参数,该参数是从main.js中的MyPlugin类的实列对象。该对象下有 apply函数。

2. 在该类中我们定义了run方法,我们在main.js 中执行该run函数就可以自动执行对应的插件了。

代码如下:

const { SyncHook, AsyncParallelHook } = require('tapable');

class Compiler {
constructor(options) {
this.hooks = {
kzSyncHook: new SyncHook(['name', 'age']),
kzAsyncHook: new AsyncParallelHook(['name', 'age'])
};
let plugins = options.plugins;
if (plugins && plugins.length > 0) {
plugins.forEach(plugin => plugin.apply(this));
}
}
run() {
console.log('开始执行了---------');
this.kzSyncHook('我是空智', 31);
this.kzAsyncHook('我是空智', 31);
}
kzSyncHook(name, age) {
this.hooks.kzSyncHook.call(name, age);
}
kzAsyncHook(name, age) {
this.hooks.kzAsyncHook.callAsync(name, age);
}
} module.exports = Compiler;

main.js 需要做的事情如下:

1. 引入 compiler.js 文件。
2. 定义一个自己的插件,比如叫 MyPlugin 类,该类下有 apply 函数。该函数有一个 compiler 参数,该参数就是我们的 compiler.js 中的实列对象。然后我们会使用 compiler 实列对象去调用 compiler.js 里面的函数。因此就可以自动执行了。

代码如下所示:

const Compiler = require('./compiler');

class MyPlugin {
constructor() { }
apply(compiler) {
compiler.hooks.kzSyncHook.tap("eventName1", (name, age) => {
console.log(`同步事件eventName1: ${name} this year ${age} 周岁了, 可是还是单身`);
});
compiler.hooks.kzAsyncHook.tapAsync('eventName2', (name, age) => {
setTimeout(() => {
console.log(`异步事件eventName2: ${name} this year ${age}周岁了,可是还是单身`);
}, 1000)
});
}
} const myPlugin = new MyPlugin(); const options = {
plugins: [myPlugin]
}; const compiler = new Compiler(options);
compiler.run();

最后执行的效果如下所示:

如上就是我们仿照Compiler和webpack的插件原理逻辑实现的一个简单demo。也就是说在webpack源码里面也是通过类似的方式来做的。

上面只是一个简单实现的基本原理,但是在我们的webpack当中我们要如何实现一个插件呢?
在我们的webpack官网中会介绍编写一个插件要满足如下条件, 官网地址

从官网得知:编写一个webpack插件需要由以下组成:

1. 一个javascript命名函数。
2. 在插件函数的prototype上定义一个 apply 方法。
3. 指定一个绑定到webpack自身的钩子函数。
4. 处理webpack内部实列的特定数据。
5. 功能完成后调用webpack提供的回调函数。

一个最基础的插件代码像如下这个样子:

// 一个javascript命名函数
function MyExampleWebpackPlugin() { };
// 在插件函数的prototype上定义一个 apply 方法
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
// 指定一个挂载到webpack自身的事件钩子。
compiler.plugin('webpacksEventHook', function(compilation, callback) {
console.log('这是一个插件demo'); // 功能完成后调用 webpack 提供的回调
callback();
})
} // 导出plugin
module.exports = MyExampleWebpackPlugin;

在我们使用该plugin的时候,相关调用及配置代码如下:

const MyExampleWebpackPlugin = require('./MyExampleWebpackPlugin');
module.exports = {
plugins: [
new MyExampleWebpackPlugin(options)
]
};

webpack启动后,在读取配置的过程中会先执行 new MyExampleWebpackPlugin(options) 初始化MyExampleWebpackPlugin来获得一个实列。然后我们会把该实列当做参数传递给我们的Compiler对象,然后会实列化 Compiler类(这个逻辑可以结合看我们上面实现了一个简单的demo中 的main.js和compiler.js的代码结合起来理解)。在Compiler类中,我们会获取到options的这个参数,该参数是一个对象,该对象下有一个 plugins 这个属性。然后遍历该属性,然后依次执行 某项插件中的apply方法,即:myExampleWebpackPlugin.apply(compiler); 给插件传递compiler对象。插件实列获取该compiler对象后,就可以通过 compiler.plugin('事件名称', '回调函数'); 监听到webpack广播出来的事件.(这个地方我们可以看我们上面的main.js中的如下代码可以看到, 在我们的main.js代码中有这样代码:compiler.hooks.kzSyncHook.tap("eventName1", (name, age) => {}));
如上就是一个简单的Plugin的插件原理(切记:结合上面的demo中main.js和compiller.js来理解效果会更好)。

二:理解 Compiler对象 和 Compilation 对象

在开发Plugin时我们最常用的两个对象就是 Compiler 和 Compilation, 他们是Plugin和webpack之间的桥梁。

Compiler对象

Compiler 对象包含了Webpack环境所有的配置信息,包含options,loaders, plugins这些项,这个对象在webpack启动时候被实例化,它是全局唯一的。我们可以把它理解为webpack的实列。

基本源码可以看如下:

// webpack/lib/webpack.js
const Compiler = require("./Compiler") const webpack = (options, callback) => {
...
// 初始化 webpack 各配置参数
options = new WebpackOptionsDefaulter().process(options); // 初始化 compiler 对象,这里 options.context 为 process.cwd()
let compiler = new Compiler(options.context); compiler.options = options // 往 compiler 添加初始化参数 new NodeEnvironmentPlugin().apply(compiler) // 往 compiler 添加 Node 环境相关方法 for (const plugin of options.plugins) {
plugin.apply(compiler);
}
...
}

源码可以点击这里查看官网可以看这里

如上我们可以看到,Compiler对象包含了所有的webpack可配置的内容。开发插件时,我们可以从 compiler 对象中拿到所有和 webpack 主环境相关的内容。

compilation 对象

compilation 对象包含了当前的模块资源、编译生成资源、文件的变化等。当webpack在开发模式下运行时,每当检测到一个文件发生改变的时候,那么一次新的 Compilation将会被创建。从而生成一组新的编译资源。

Compiler对象 与 Compilation 对象 的区别是:Compiler代表了是整个webpack从启动到关闭的生命周期。Compilation 对象只代表了一次新的编译。
Compiler对象的事件钩子,我们可以看官网. 或者我们也可以查看它的源码也可以看得到,查看源码

我们可以了解常见的事件钩子:下面是一些比较常见的事件钩子及作用:

钩子               作用                     参数               类型
after-plugins 设置完一组初始化插件之后 compiler sync
after-resolvers 设置完 resolvers 之后 compiler sync
run 在读取记录之前 compiler async
compile 在创建新 compilation之前 compilationParams sync
compilation compilation 创建完成 compilation sync
emit 在生成资源并输出到目录之前 compilation async
after-emit 在生成资源并输出到目录之后 compilation async
done 完成编译 stats sync

理解webpack中的事件流

我们可以把webpack理解为一条生产线,需要经过一系列处理流程后才能将源文件转换成输出结果。
这条生产线上的每个处理流程的职责都是单一的,多个流程之间会存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。

我们的插件就像一个插入到生产线中的一个功能,在特定的时机对生产线上的资源会做处理。webpack它是通过 Tapable来组织这条复杂的生产线的。

webpack在运行的过程中会广播事件,插件只需要关心监听它的事件,就能加入到这条生产线中。然后会执行相关的操作。
webpack的事件流机制它能保证了插件的有序性,使整个系统的扩展性好。事件流机制使用了观察者模式来实现的。比如如下代码:

/*
* 广播事件
* myPlugin-name 为事件名称
* params 为附带的参数
*/ compiler.apply('myPlugin-name', params); /*
* 监听名称为 'myPlugin-name' 的事件,当 myPlugin-name 事件发生时,函数就会执行。
*/ compiler.hooks.myPlugin-name.tap('myPlugin-name', function(params) { });

三:插件中常用的API

1. 读取输出资源、模块及依赖

在我们的emit钩子事件发生时,表示的含义是:源文件的转换和组装已经完成了,在这里事件钩子里面我们可以读取到最终将输出的资源、代码块、模块及对应的依赖文件。并且我们还可以输出资源文件的内容。比如插件代码如下:

class MyPlugin {
apply(compiler) {
compiler.plugin('emit', function(compilation, callback) {
// compilation.chunks 是存放了所有的代码块,是一个数组,我们需要遍历
compilation.chunks.forEach(function(chunk) {
/*
* chunk 代表一个代码块,代码块它是由多个模块组成的。
* 我们可以通过 chunk.forEachModule 能读取组成代码块的每个模块
*/
chunk.forEachModule(function(module) {
// module 代表一个模块。
// module.fileDependencies 存放当前模块的所有依赖的文件路径,它是一个数组
module.fileDependencies.forEach(function(filepath) {
console.log(filepath);
});
});
/*
webpack 会根据chunk去生成输出的文件资源,每个chunk都对应一个及以上的输出文件。
比如在 Chunk中包含了css 模块并且使用了 ExtractTextPlugin 时,
那么该Chunk 就会生成 .js 和 .css 两个文件
*/
chunk.files.forEach(function(filename) {
// compilation.assets 是存放当前所有即将输出的资源。
// 调用一个输出资源的 source() 方法能获取到输出资源的内容
const source = compilation.assets[filename].source();
});
});
/*
该事件是异步事件,因此要调用 callback 来通知本次的 webpack事件监听结束。
如果我们没有调用callback(); 那么webpack就会一直卡在这里不会往后执行。
*/
callback();
})
}
}

2. 监听文件变化

webpack读取文件的时候,它会从入口模块去读取,然后依次找出所有的依赖模块。当入口模块或依赖的模块发生改变的时候,那么就会触发一次新的 Compilation。

在我们开发插件的时候,我们需要知道是那个文件发生改变,导致了新的Compilation, 我们可以添加如下代码进行监听。

// 当依赖的文件发生改变的时候 会触发 watch-run 事件
class MyPlugin {
apply(compiler) {
compiler.plugin('watch-run', (watching, callback) => {
// 获取发生变换的文件列表
const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
// changedFiles 格式为键值对的形式,当键为发生变化的文件路径
if (changedFiles[filePath] !== undefined) {
// 对应的文件就发生了变化了
}
callback();
}); /*
默认情况下Webpack只会监听入口文件或其依赖的模块是否发生变化,但是在有些情况下比如html文件发生改变的时候,那么webpack
就会去监听html文件的变化。因此就不会重新触发新的 Compilation。因此为了监听html文件的变化,我们需要把html文件加入到
依赖列表中。因此我们需要添加如下代码:
*/
compiler.plugin('after-compile', (compilation, callback) => {
/*
如下的参数filePath是html文件路径,我们把HTML文件添加到文件依赖表中,然后我们的webpack会去监听html模块文件,
html模板文件发生改变的时候,会重新启动下重新编译一个新的 Compilation.
*/
compilation.fileDependencies.push(filePath);
callback();
})
}
}

3. 修改输出资源

我们在第一点说过:在我们的emit钩子事件发生时,表示的含义是:源文件的转换和组装已经完成了,在这里事件钩子里面我们可以读取到最终将输出的资源、代码块、模块及对应的依赖文件。因此如果我们现在要修改输出资源的内容的话,我们可以在emit事件中去做修改。那么所有输出的资源会存放在 compilation.assets中,compilation.assets是一个键值对,键为需要输出的文件名,值为文件对应的内容。如下代码:

class MyPlugin {
apply(compiler) {
compiler.plugin('emit', (compilation, callback) => {
// 设置名称为 fileName 的输出资源
compilation.assets[fileName] = {
// 返回文件内容
source: () => {
// fileContent 即可以代表文本文件的字符串,也可以是代表二进制文件的buffer
return fileContent;
},
// 返回文件大小
size: () => {
return Buffer.byteLength(fileContent, 'utf8');
}
};
callback();
});
// 读取 compilation.assets 代码如下:
compiler.plugin('emit', (compilation, callback) => {
// 读取名称为 fileName 的输出资源
const asset = compilation.assets[fileName];
// 获取输出资源的内容
asset.source();
// 获取输出资源的文件大小
asset.size();
callback();
});
}
}

4. 判断webpack使用了哪些插件

在我们开发一个插件的时候,我们需要根据当前配置是否使用了其他某个插件,我们可以通过读取webpack某个插件配置的情况,比如来判断我们当前是否使用了 HtmlWebpackPlugin 插件。代码如下:

/*
判断当前配置使用了 HtmlWebpackPlugin 插件。
compiler参数即为 webpack 在 apply(compiler) 中传入的参数
*/ function hasHtmlWebpackPlugin(compiler) {
// 获取当前配置下所有的插件列表
const plugins = compiler.options.plugins;
// 去plugins中寻找有没有 HtmlWebpackPlugin 的实列
return plugins.find(plugin => plugin.__proto__.constructor === HtmlWebpackPlugin) !== null;
}

四:编写插件实战

假如现在我们的项目的目录结构如下:

|--- webpack-plugin-demo
| |--- node_modules
| |--- js
| | |--- main.js # js 的入口文件
| |--- plugins
| | |--- logWebpackPlugin.js # 编写的webpack的插件,主要作用是打印日志功能
| |--- styles
| |--- index.html
| |--- package.json
| |--- webpack.config.js

1. 实现一个打印日志的LogWebpackPlugin插件

代码如下:

class LogWebpackPlugin {
constructor(doneCallback, emitCallback) {
this.emitCallback = emitCallback
this.doneCallback = doneCallback
}
apply(compiler) {
compiler.hooks.emit.tap('LogWebpackPlugin', () => {
// 在 emit 事件中回调 emitCallback
this.emitCallback();
});
compiler.hooks.done.tap('LogWebpackPlugin', (err) => {
// 在 done 事件中回调 doneCallback
this.doneCallback();
});
compiler.hooks.compilation.tap('LogWebpackPlugin', () => {
// compilation('编译器'对'编译ing'这个事件的监听)
console.log("The compiler is starting a new compilation...")
});
compiler.hooks.compile.tap('LogWebpackPlugin', () => {
// compile('编译器'对'开始编译'这个事件的监听)
console.log("The compiler is starting to compile...")
});
}
} // 导出插件
module.exports = LogWebpackPlugin;

下面我们在webpack中引入该插件;如下代码:

// 引入LogWebpackPlugin 插件
const LogWebpackPlugin = require('./public/plugins/logWebpackPlugin'); module.exports = {
plugins: [
new LogWebpackPlugin(() => {
// Webpack 模块完成转换成功
console.log('emit 事件发生啦,所有模块的转换和代码块对应的文件已经生成好~')
} , () => {
// Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
console.log('done 事件发生啦,成功构建完成~')
})
]
}

然后执行结果如下所示:

可以看到我们执行成功了,执行了对应的回调函数。如上代码中的 compiler 这个我这边就不讲解了,上面已经讲过了。那么 compiler.hooks 代表的是对外 暴露了多少事件钩子,具体那个钩子是什么含义,我们可以来看下官网

如上面代码,我们使用两个钩子事件,分别是 compiler.hooks.emit 和 compiler.hooks.done, compiler.hooks.emit 钩子事件的含义是: 在生成资源并输出到目录之前。这个事件就会发生。 compiler.hooks.done 的含义是:编译完成,该事件就会发生。因此上面截图我们可以看到先触发 emit事件,因此会打印 'done 事件发生啦,成功构建完成~', 然后会触发 done事件,因此会打印 "emit 事件发生啦,所有模块的转换和代码块对应的文件已经生成好~" 执行这个回调函数。
github代码查看

2. 编写去除生成 bundle.js 中多余的注释的插件

项目结构如下:

|--- webpack-plugin-demo
| |--- node_modules
| |--- public
| | |--- js
| | | |--- main.js # 入口文件
| | |--- plugins # 存放所有的webpack插件
| | | |--- AsyncPlugin.js
| | | |--- AutoExternalPlugin.js
| | | |--- DonePlugin.js
| | | |--- FileListPlugin.js
| | | |--- MyPlugin.js
| | | |--- OptimizePlugin.js
| | |--- styles # 存放css样式文件
| | |--- index.html # index.html模板
| |--- package.json
| |--- webpack.config.js

项目结构如上所示;上面在 public/plugins 中一共有6个插件,我们分别来看下6个插件的代码:

1. public/plugins/AsyncPlugin.js 代码如下:

class AsyncPlugin {
constructor() { }
apply(compiler) {
// 监听emit事件,编译完成后,文件内容输出到硬盘上 触发该事件
compiler.hooks.emit.tapAsync('AsyncPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('文件将要被写入到硬盘中');
callback();
}, 2000)
})
}
} module.exports = AsyncPlugin;

如上该插件代码没有什么实际作用,无非就是监听 emit 异步事件钩子,emit事件钩子我们从官网

上可以看到具体的含义为:'在生成资源并输出到目录之前',会执行该事件钩子中函数代码,这边无非就是在控制台中打印一些提示信息的,没有什么实际作用的。
2. public/plugins/DonePlugin.js 代码如下:

class DonePlugin {
constructor() { }
apply(compiler) {
compiler.hooks.done.tapAsync('DonePlugin', (name, callback) => {
console.log('全部编译完成');
callback();
})
}
} module.exports = DonePlugin;

如上代码也是一个意思,当编译完成后,就会执行 done的事件钩子的回调函数,也是在命令中提示作用的。

3. public/plugins/OptimizePlugin.js 代码如下:

class OptimizePlugin {
constructor() { }
apply(compiler) {
// 监听 compilation 事件
compiler.hooks.compilation.tap('OptimizePlugin', (compilation) => {
compilation.hooks.optimize.tap('OptimizePlugin', () => {
console.log('compilation 完成,正在优化,准备输出');
});
});
}
} module.exports = OptimizePlugin;

也是一样监听 compilation 事件的,每当检测到一个文件发生改变的时候,那么一次新的 Compilation将会被创建。从而生成一组新的编译资源。

4. public/plugins/FileListPlugin.js 代码如下:

class FileListPlugin {
constructor() { }
apply(compiler) {
compiler.hooks.compilation.tap('FileListPlugin', (compilation) => {
compiler.hooks.emit.tap('FileListPlugin', () => {
let content = '生成的文件列表\r\n';
content = Object.keys(compilation.assets).reduce((current, prev) => current + '- ' + prev + '\r\n', content);
console.log(content);
compilation.assets['README.md'] = {
source() {
return content;
},
size() {
return content.length;
}
}
})
})
}
}
module.exports = FileListPlugin;

生成文件列表的时候,就会触发该文件的代码。

5. public/plugins/AutoExternalPlugin.js 代码如下:

const ExternalModules = require('webpack/lib/ExternalModule');

class AutoExternalPlugin {
constructor(options) {
this.options = options;
this.externalModules = {};
}
apply(compiler) {
compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', (normalModuleFactory) => {
// parser 将代码转换为语法书 判断有无 import
normalModuleFactory.hooks.parser.for('javascript/auto').tap('AutoExternalPlugin', (parser, parserOptions) => {
parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {
if (this.options[source]) {
this.externalModules[source] = true;
}
})
})
// factory 是创建模块的方法
// data 是创建模块的参数
normalModuleFactory.hooks.factory.tap('AutoExternalPlugin', factory => (data, callback) => {
const dependencies = data.dependencies;
const value = dependencies[0].request; // jquery
if (this.externalModules[value]) {
const varName = this.options[value].varName;
callback(null, new ExternalModules(varName, 'window'));
} else {
factory(data, callback);
}
})
});
compiler.hooks.compilation.tap('InlinePlugin', (compilation) => {
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('AutoExternalPlugin', (htmlPluginData, callback) => {
Object.keys(this.options).forEach(key => {
this.externalModules[key] = this.options[key];
htmlPluginData.body.unshift(this.processTags(compilation, htmlPluginData, this.options[key]))
});
callback(null, htmlPluginData);
});
});
}
processTags(compilation, htmlPluginData, value) {
var tag;
return tag = {
tagName: 'script',
closeTag: true,
attributes: {
type: 'text/javascript',
src: value.url
}
}
}
} module.exports = AutoExternalPlugin;

如上该插件的代码的作用是可以解决外部的js引用,比如我在webpack中如下使用该插件:

const AutoExternalPlugin = require('./public/plugins/AutoExternalPlugin');
module.exports = {
plugins:[
new AutoExternalPlugin({
jquery:{
varName:'jQuery',
url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js'
}
})
]
}

这样我就可以在页面中使用jquery插件了;如下代码所示:

import $ from 'jquery';
console.log($);

然后在我们的页面中引入的是 该 jquery库文件,它会把该库文件自动生成到 index.html 上去,如下index.html 代码变成如下了:

<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<link rel="manifest" href="/public/manifest.json" />
<link href="main.css" rel="stylesheet"></head>
<body>
<div id="app">222226666</div>
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.1.0/jquery.js"></script><script type="text/javascript" src="bundle.js"></script></body>
</html>

我们可以来简单的分析下 AutoExternalPlugin.js 的代码:

在apply方法内部会生成一个 compiler 实列,然后我们监听 normalModuleFactory 事件,该事件的作用我们可以看下官网就知道了。

compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', (normalModuleFactory) => {
// parser 将代码转换为语法书 判断有无 import
normalModuleFactory.hooks.parser.for('javascript/auto').tap('AutoExternalPlugin', (parser, parserOptions) => {
parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {
if (this.options[source]) {
this.externalModules[source] = true;
}
})
})
}

如上 parser 实例,是用来解析由 webpack 处理过的每个模块。parser 也是扩展自 tapable 的 webpack 类,并且提供多种 tapable 钩子,插件作者可以使用它来自定义解析过程。官网解释可以看这里

如上代码,我们调用 parser.hooks.import 钩子函数, 然后返回的 source 就是我们的在 我们的main.js 中调用插件名。如main.js 代码如下:

import $ from 'jquery';

因此在我们的webpack.config.js 中会如下初始化插件

new AutoExternalPlugin({
jquery:{
varName:'jQuery',
url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js'
}
});

因此 source 返回的值 就是 'jquery'; 其他的代码可以自己稍微看看就行了。这里暂时先不讲了,由于时间问题。

6. public/plugins/MyPlugin.js 代码如下:

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) => {
console.log(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;

这个js代码的真正的含义才是我们今天要讲到的,这个插件最主要作用是 去除注释后的文本。

1. 第一步,我们使用 compiler.hooks.emit 钩子函数。在生成资源并输出到目录之前触发该函数,也就是说将编译好的代码发射到指定的stream中就会触发,然后我们从回调函数返回的 compilation 对象上可以拿到编译好的 stream.

2. 访问compilation对象,compilation内部会返回很多内部对象,这边先不打印了,因为打印的话直接会卡死掉,要等很长时间才会打印出来,你们自己可以试试;然后我们遍历 assets.

Object.keys(compilation.assets).forEach((data) => {
console.log(compilation.assets);
console.log(8888)
console.log(data);
});

如下图所示:

1) assets 数组对象中的key是资源名。在如上代码,我们通过 Object.key()方法拿到了。如下所示:

main.css
bundle.js
index.html

2) 然后我们调用 compilation.assets[data].source(); 可以获取资源的内容。

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;
});
});

4) 更新 compilation.assets[data] 对象,如下代码:

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

然后我们就可以在webpack中引入该所有的插件:

const DonePlugin = require('./public/plugins/DonePlugin');
const OptimizePlugin = require('./public/plugins/OptimizePlugin');
const AsyncPlugin = require('./public/plugins/AsyncPlugin');
const FileListPlugin = require('./public/plugins/FileListPlugin');
const AutoExternalPlugin = require('./public/plugins/AutoExternalPlugin');
const MyPlugin = require('./public/plugins/MyPlugin');

调用方式如下:

module.exports = {
plugins:[
new DonePlugin(),
new OptimizePlugin(),
new AsyncPlugin(),
new FileListPlugin(),
new MyPlugin(),
new AutoExternalPlugin({
jquery:{
varName:'jQuery',
url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js'
}
})
]
}

然后我们进行打包运行效果如下所示:

github源码查看

如何编写一个WebPack的插件原理及实践的更多相关文章

  1. Viper 微服务框架 编写一个hello world 插件-02

    1.Viper是什么? Viper 是.NET平台下的Anno微服务框架的一个示例项目.入门简单.安全.稳定.高可用.全平台可监控.底层通讯可以随意切换thrift grpc. 自带服务发现.调用链追 ...

  2. Hexo+NexT(六):手把手教你编写一个Hexo过滤器插件

    Hexo+NexT介绍到这里,我认为已经可以很好地完成任务了.它所提供的一些基础功能及配置,都已经进行了讲解.你已经可以随心所欲地配置一个自己的博客环境,然后享受码字的乐趣. 把博客托管到Github ...

  3. WordPress插件制作教程(二): 编写一个简单的插件

    上一篇说到了如何创建一个插件,我想大家看了之后一定会有所收获,这一篇简单给大家写一个插件样例,让大家有一个基本的印象.这个插件的样例就是当你激活这个插件后会在你的每篇文章中插入一段自己定义好的内容,比 ...

  4. 新书介绍 -- 《Redis核心原理与实践》

    大家好,今天给大家介绍一下我的新书 -- <Redis核心原理与实践>. 后端开发的同学应该对Redis都不陌生,Redis由于性能极高.功能强大,已成为业界非常流行的内存数据库. < ...

  5. webpack编写一个plugin插件

    插件向第三方开发者提供了 webpack 引擎中完整的能力.使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中.创建插件比创建 loader 更加高级,因为你将需要理解一 ...

  6. 从零开始编写一个vue插件

    title: 从零开始编写一个vue插件 toc: true date: 2018-12-17 10:54:29 categories: Web tags: vue mathjax 写毕设的时候需要一 ...

  7. 编写一个Open Live Writer的VSCode代码插件

    起因 又是一年多没有更新过博客了,最近用Arduino做了一点有意思的东西,准备写一篇博客.打开尘封许久的博客园,发现因为Windows Live Writer停止更新,博客园推荐的客户端变为了Ope ...

  8. 80行代码教你写一个Webpack插件并发布到npm

    1. 前言 最近在学习 Webpack 相关的原理,以前只知道 Webpack 的配置方法,但并不知道其内部流程,经过一轮的学习,感觉获益良多,为了巩固学习的内容,我决定尝试自己动手写一个插件. 这个 ...

  9. [转]nopcommerce商城系统--如何编写一个插件

    本文转自:http://www.cnblogs.com/ganqiyin/p/3680771.html 原址:http://www.nopcommerce.com/docs/77/how-to-wri ...

随机推荐

  1. 源码阅读 - java.util.concurrent (一)

    java.util.concurrent这个包大致可以分为五个部分: Aomic数据类型 这部分都被放在java.util.concurrent.atomic这个包里面,实现了原子化操作的数据类型,包 ...

  2. 小代介绍Spring Boot

    想要获取更多文章可以访问我的博客 - 代码无止境. 小代很顺利的完成了陈BOSS交代给他搭建Spring Boot示例程序的任务.但是小代是一个乐于学习的程序员,他通过一番学习总结了一篇简单介绍Spr ...

  3. 探究Hybrid-APP技术原理

    探究Hybrid-APP技术原理 author: @TiffanysBear 背景 随着Web技术的发展和移动互联网的发展,Hybrid技术已经成为一种前端开发的主流技术方案.那什么是Hybrid A ...

  4. c++学习书籍推荐《Exceptional C++ Style》下载

    百度云及其他网盘下载地址:点我 编辑推荐 软件“风格”所要讨论的主题是如何在开销与功能之间.优雅与可维护性之间.灵活.性与过分灵活之间寻找完美的平街点.在本书中,著名的C++大师Herb Sutter ...

  5. Golang 高效实践之并发实践

    前言 在我前面一篇文章Golang受欢迎的原因中已经提到,Golang是在语言层面(runtime)就支持了并发模型.那么作为编程人员,我们在实践Golang的并发编程时,又有什么需要注意的点呢?下面 ...

  6. Dapper学习笔记

    听说有个轻量化的orm Dapper,我就去了解下.试着对Sql Server和Mysql进行增删改查,体验不错.它不如EF臃肿,也比一般的封装灵活,比如我们封装了一个映射类.利用反射,在Execut ...

  7. 为什么说是时候拥抱.NET CORE了?

    微软和社区已经做了大量艰苦的工作,使.Net Core成为市场上具有竞争力的框架,帮助开发人员快速开发具有最佳性能和可扩展性的强大应用程序.做的最棒的事情是.Net Framework开发人员不需要任 ...

  8. 个人永久性免费-Excel催化剂功能第39波-DotNet版的正则处理函数

    在很久之前已经开发过的一些自定义函数,未能有详细的讲解,接下来几波中着重对之前开发过的自定义函数进行详细讲解及示例说明,希望能够帮助到普通用户们能顺利使用,此篇介绍为正则表达式函数. 文章出处说明 原 ...

  9. VUE v-for循环中每个item节点动态绑定不同函数方法

    一. 业务场景: 一个title 处 可能有 一个或多个按钮,  按钮对应不同的响应事件 二. 思路 : 按钮个数 根据传入的数据length 来循环渲染,  每条数据对应的事件名称 通过动态绑定 三 ...

  10. 问题解决:Maven execution terminated abnormally (exit code 1)

    Maven execution terminated abnormally (exit code 1) 修改setting.xml中的镜像位置 如下就可以了 <mirror> <id ...