实现一个 webpack loader 和 webpack plugin

loader

官网上的定义:

例如 babel-loader 可以将 ES6 代码转换为 ES5 代码;sass-loadersass 代码转换为 css 代码。

一般 loader 的配置代码如下:

module: {

rules: [

{

test: /\.js$/,

use: [

// loader 的执行顺序从下到上

{

loader: path.resolve('./src/loader2.js'),

},

{

loader: path.resolve('./src/loader1.js'),

},

]

}

]

},

rules 数组包含了一个个匹配规则和具体的 loader 文件。

上述代码中的 test: /\.js$/ 就是匹配规则,表示对 js 文件使用下面的两个 loader。

而 loader 的处理顺序是自下向上的,即先用 loader1 处理源码,然后将处理后的代码再传给 loader2。

loader2 处理后的代码就是最终的打包代码。

loader 的实现

loader 其实是一个函数,它的参数是匹配文件的源码,返回结果是处理后的源码。下面是一个最简单的 loader,它什么都没做:

module.exports = function (source) {

return source

}

这么简单的 loader 没有挑战性,我们可以写一个稍微复杂一点的 loader,它的作用是将 var 关键词替换为 const

module.exports = function (source) {

return source.replace(/var/g, 'const')

}

写完之后,我们来测试一下,测试文件为:

function test() {

var a = 1;

var b = 2;

var c = 3;

console.log(a, b, c);

}

test()

wepback.config.js 配置文件为:

const path = require('path')

module.exports = {

mode: 'development',

entry: {

main: './src/index.js'

},

output: {

filename: 'bundle.js',

path: path.resolve(__dirname, 'dist')

},

module: {

rules: [

{

test: /\.js$/,

use: [

{

loader: path.resolve('./src/loader1.js'),

},

]

}

]

},

}

运行 npm run build,得到打包文件 bundle.js,我们来看一看打包后的代码:

eval("function test() {\r\n    const a = 1;\r\n    const b = 2;\r\n    const c = 3;\r\n    console.log(a, b, c);\r\n}\r\n\r\ntest()\n\n//# sourceURL=webpack:///./src/index.js?");

可以看到,代码中的 var 已经变成了 const

异步 loader

刚才实现的 loader 是一个同步 loader,在处理完源码后用 return 返回。

下面我们来实现一个异步 loader:

module.exports = function (source) {

const callback = this.async()

// 由于有 3 秒延迟,所以打包时需要 3+ 秒的时间

setTimeout(() => {

callback(null, `${source.replace(/;/g, '')}`)

}, 3000)

}

异步 loader 需要调用 webpack 的 async() 生成一个 callback,它的第一个参数是 error,这里可设为 null,第二个参数就是处理后的源码。当你异步处理完源码后,调用 callback 即可。

下面来试一下异步 loader 到底有没生效,这里设置了一个 3 秒延迟。我们来对比一下打包时间:

在这里插入图片描述
在这里插入图片描述
上图是调用同步 loader 的打包时间,为 141 ms;下图是调用异步 loader 的打包时间,为 3105 ms,说明异步 loader 生效了。

如果想看完整 demo 源码,请点击我的 github。

plugin

webpack 在整个编译周期中会触发很多不同的事件,plugin 可以监听这些事件,并且可以调用 webpack 的 API 对输出资源进行处理。

这是它和 loader 的不同之处,loader 一般只能对源文件代码进行转换,而 plugin 可以做得更多。plugin 在整个编译周期中都可以被调用,只要监听事件。

对于 webpack 编译,有两个重要的对象需要了解一下:

plugin 的实现

我们看一下官网的定义,webpack 插件由以下部分组成:

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

简单的说,一个具有 apply 方法的函数就是一个插件,并且它要监听 webpack 的某个事件。下面来看一个简单的示例:

function Plugin(options) { }

Plugin.prototype.apply = function (compiler) {

// 所有文件资源都被 loader 处理后触发这个事件

compiler.plugin('emit', function (compilation, callback) {

// 功能完成后调用 webpack 提供的回调

console.log('Hello World')

callback()

})

}

module.exports = Plugin

写完插件后要怎么调用呢?

先在 webpack 配置文件中引入插件,然后在 plugins 选项中配置:

const Plugin = require('./src/plugin')

module.exports = {

...

plugins: [

new Plugin()

]

}

这就是一个简单的插件了。

下面我们再来写一个复杂点的插件,它的作用是将经过 loader 处理后的打包文件 bundle.js 引入到 index.html 中:

function Plugin(options) { }

Plugin.prototype.apply = function (compiler) {

// 所有文件资源经过不同的 loader 处理后触发这个事件

compiler.plugin('emit', function (compilation, callback) {

// 获取打包后的 js 文件名

const filename = compiler.options.output.filename

// 生成一个 index.html 并引入打包后的 js 文件

const html = `<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Document</title>

<script></script>

</head>

<body>

</body>

</html>`

// 所有处理后的资源都放在 compilation.assets 中

// 添加一个 index.html 文件

compilation.assets['index.html'] = {

source: function () {

return html

},

size: function () {

return html.length

}

}

// 功能完成后调用 webpack 提供的回调

callback()

})

}

module.exports = Plugin

OK,执行一下,看看效果。

在这里插入图片描述
完美,和预测的结果一模一样。

完整 demo 源码,请看我的 github。

参考资料

  • 编写一个 loader
  • 编写一个插件

以上是 实现一个 webpack loader 和 webpack plugin 的全部内容, 来源链接: utcz.com/a/43040.html

回到顶部