实现一个 webpack loader 和 webpack plugin

loader

官网上的定义:css

loader 是一个转换器,用于对源代码进行转换。html

例如 babel-loader 能够将 ES6 代码转换为 ES5 代码;sass-loadersass 代码转换为 css 代码。webpack

通常 loader 的配置代码以下:git

module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    // loader 的执行顺序从下到上
                    {
                        loader: path.resolve('./src/loader2.js'),
                    },
                    {
                        loader: path.resolve('./src/loader1.js'),
                    },
                ]
            }
        ]
    },
复制代码

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

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

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

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

loader 的实现

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

module.exports = function (source) {
    return source
}
复制代码

这么简单的 loader 没有挑战性,咱们能够写一个稍微复杂一点的 loader,它的做用是将 var 关键词替换为 constsass

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 编译,有两个重要的对象须要了解一下:

Compiler 和 Compilation

在插件开发中最重要的两个资源就是 compiler 和 compilation 对象。理解它们的角色是扩展 webpack 引擎重要的第一步。

compiler 对象表明了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性创建,并配置好全部可操做的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。能够使用它来访问 webpack 的主环境。

compilation 对象表明了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会建立一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了不少关键时机的回调,以供插件作自定义处理时选择使用。

这两个组件是任何 webpack 插件不可或缺的部分(特别是 compilation),所以,开发者在阅读源码,并熟悉它们以后,会感到获益匪浅。

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 src="${filename}"></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

想了解更多的事件,请看官网介绍 compiler 钩子

参考资料

相关文章
相关标签/搜索