webpack自定义插件

14 3月

webpack的plugins系统本质上就是在webpack构建过程中,会广播出一些事件,插件可以监听这些事件搞点事情。项目中根据需求,可以自定义一些插件

  • 基础结构
  • compiler 和 compiler hooks
  • compilation 和 compilation hooks
  • 骨架屏插件

基础结构

plugin基础结构是这样的:(webpack v4的写法,v3的API有所不同,但概念都一样)

let CustomPlugin = function(options) {
    options = options || {};
    this.options = options;
}

CustomPlugin.prototype.apply = function(compiler) {
    compiler.hooks.someHook.tap(...});      // 基本写法
    compiler.hooks.entryOption.tap(...);    // 如果希望在entry配置完毕后执行某个功能 
    compiler.hooks.emit.tap(...);           // 如果希望在生成的资源输出到output指定目录之前执行某个功能
    ...
}

module.exports = CustomPlugin;

上述plugin定义好后,就可以在webpack.config.js中使用该插件:

const CustomPlugin = require('./CustomPlugin.js');

module.export = {
    ...
    plugins:[
        new CustomPlugin(options),
        ...
    ]
}

compiler 和 compiler hooks

webpack执行时,会生成插件的实例对象,并调用插件的apply(compiler)方法,参数compiler可以将它理解为webpack的实例对象,包含了webpack环境的配置信息,例如entry,loaders,plugins等。它是webpack和plugin间的桥梁。

compiler继承自事件流框架Tapable,暴露出了一系列事件的hooks

(事件机制用了观察者模式,类似Node里的EventEmitter,事件分同步和异步,这都是些通用概念,不赘述)

在插件的apply方法里,可以调用compiler hooks来监听事件,并搞些事情,例如:

CustomPlugin.prototype.apply = function(compiler) {
    // 同步钩子
    compiler.hooks.compilation.tap('MyPlugin', compilation => {
        console.log('以同步方式触及 compilation 钩子。');
    })

    // 异步钩子
    compiler.hooks.run.tapAsync('MyPlugin', (source, target, routesList, callback) => {
        console.log('以异步方式触及 run 钩子。');
        callback();
    });

    compiler.hooks.run.tapPromise('MyPlugin', (source, target, routesList) => {
        return new Promise(resolve => setTimeout(resolve, 1000)).then(() => {
	        console.log('以具有延迟的异步方式触及 run 钩子。');
        });
    });

    compiler.hooks.run.tapPromise('MyPlugin', async (source, target, routesList) => {
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log('以具有延迟的异步方式触及 run 钩子。');
    });
    ...
}

compilation 和 compilation hooks

compiler是整个webpack的实例,hooks里一个很重要的步骤就是compilation

webpack是基于模块的,模块会经历加载loaded,封存sealed,优化optimized,分块chunked,哈希hashed,重新创建restored这几个步骤。webpack运行时,每次检测到文件变化,就会创建一个新的compilation对象。

compilation对象包含了当前的模块信息,同样继承自事件流框架Tapable,暴露出了一系列事件的hooks

两者的区别是:compiler是整个webpack的实例,compilation是一次编译。compiler负责编译webpack配置对象并返回一个compilation实例,而compilation实例执行时,会创建所需的bundles。

另外注意,compiler和compilation这两个对象都是引用,不要在插件中直接修改这两个对象,会影响后面的插件。

如果你开发一个自定义插件,除了compilation官方提供的hooks外,你也可以暴露出一些自定义hook(下面的骨架屏例子中,就用了html-webpack-plugin的自定义的hook)供其他插件监听。

// CustomPlugin1.js
const SyncHook = require('tapable').SyncHook;

CustomPlugin1.prototype.apply = function(compiler) {
    if (compiler.hooks.myCustomHook) throw new Error('Already in use');
    compiler.hooks.myCustomHook = new SyncHook(['a', 'b', 'c']);    // 自定义hook
    ...
}

// CustomPlugin2.js
CustomPlugin2.prototype.apply = function(compiler) {
    compiler.hooks.myCustomHook.call(a, b, c);
    ...
}

骨架屏插件

React项目中,html模板里就一个DOM元素:

<div id="app"></div>

等js下载好后,真实的DOM节点会动态插入html。导致加载js时,页面会有一段白屏时间。如果能在这段时间给用户一个骨架屏,会让前端体验好很多。

如果就一个页面,直接将骨架屏代码写在html模板里就行了。如果项目中有多个页面,每个页面的骨架屏又不太一样,需要根据url来判断究竟显示哪个骨架屏,示意代码:

<div id="app">
    <div style="background-color: yellowgreen;height: 500px;display: none;" id="firstPage">第一页 骨架屏</div>
    <div style="background-color: hotpink;height: 500px;display: none;" id="secondPage">第二页 骨架屏</div>
</div>
<script type=text/javascript>
    var hPath = location.pathname;

    if(hPath === '/entry/entry1.html') {
        document.getElementById('firstPage').style.display = 'block';
    } else if(hPath === '/entry/entry2.html') {
        document.getElementById('secondPage').style.display = 'block';
    }
</script>

上面都是示意代码,写在模板里,逼格er不高。你说我项目改了template,怎么好意思和人打招呼,可以写个webpack插件。(注意,这只是个玩笑,解决问题是第一位的。优化永远只是加分项,不是必须的。甚至“优化”到极致,导致维护的人看不懂,这种优化其实是减分的)

因为webpack构建过程中,依靠html-webpack-plugin插件处理模板文件,所以思路是在此过程中动态替换DOM节点。

找到插件官网,根据事件的执行图,选择beforeAssetTagGeneration事件。注意,webpack 3.x和4.x,包括html-webpack-plugin插件的3.x和4.x版本的API和hook名都有变化,开发插件时可以做一些兼容性处理:

let HtmlSkeletonScreenPlguin = function(options) {
    options = options || {};
    this.options = options;
}

HtmlSkeletonScreenPlguin.prototype.injectSkeletonScreen = function (htmlData, callback) {
    htmlData.html = htmlData.html.replace(`<div id="app"></div>`, 
    `<div id="app">
        <div style="background-color: yellowgreen;height: 500px;display: none;" id="firstPage">第一页 骨架屏</div>
        <div style="background-color: hotpink;height: 500px;display: none;" id="secondPage">第二页 骨架屏</div>
    </div>
    <script type=text/javascript>
        var hPath = location.pathname;

        if(hPath === '/entry/entry1.html') {
            document.getElementById('firstPage').style.display = 'block';
        } else if(hPath === '/entry/entry2.html') {
            document.getElementById('secondPage').style.display = 'block';
        }
    </script>`);
    callback(null, htmlData);            
};

HtmlSkeletonScreenPlguin.prototype.apply = function(compiler) {
    if (compiler.hooks) {
        // webpack 4 support
        compiler.hooks.compilation.tap('HtmlSkeletonScreen', compilation => {
            if (compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing) {
                // HtmlWebPackPlugin 3.x
                compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tapAsync('HtmlSkeletonScreen', (htmlData, callback) => {
                    this.injectSkeletonScreen(htmlData, callback);
                });
            } else {
                // HtmlWebPackPlugin 4.x
                const HtmlWebpackPlugin = require('html-webpack-plugin');
                HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync('HtmlSkeletonScreen', (htmlData, callback) => {
                    this.injectSkeletonScreen(htmlData, callback);
                });
            }
          });
    } else {
        // webpack 3 support
        compiler.plugin('compilation', compilation => {
            compilation.plugin('html-webpack-plugin-before-html-processing', (htmlData, callback) => {
                this.injectSkeletonScreen(compilation, htmlData, callback);
            });
        });
    }
}

module.exports = HtmlSkeletonScreenPlguin;

在webpack.config.js里引用自定义插件即可:

const htmlSkeletonScreenPlugin = require('./htmlSkeletonScreenPlugin');

module.export = {
    ...
    plugins:[
        new htmlSkeletonScreenPlugin(),
        ...
    ]
}

评论(2)

    • 不是固定的,可以根据业务代码画出不同页面的骨架屏,例子中只是示意。另外我觉得骨架屏用dom模拟也不算最优解,一大坨dom代码看着就烦。骨架屏最好美工做个像素少,体积小的打码图,然后直接显示图片。

发表评论

电子邮件地址不会被公开。 必填项已用*标注