webpack进阶:理解webpack

954 阅读6分钟

上一篇文章中我们说了下作为小白,我们要掌握好那些基本内容才能基本开发出一个能够进行项目开发的脚手架。这篇文章,我将继续秉承刨根问底的思路,看一下webpack到底是怎么实现打包功能以及如何正确的认识和编写loader和plugin。

Tapable

要想知道什么是Webpack,首先要知道什么是Tapable。这是一个类似Node.js的EventEmitter的库,定义了一系列钩子,通过钩子函数的发布和订阅,控制者Webpack的插件系统。

EventEmitter

在介绍Tapable之前,有必要先了解下EventEmitter。这是Node.js的一个原生API,它实现了js的发布-订阅设计模式。如果没有了解过发布订阅模式的小伙伴,最好提前了解一下,也可以比较一下发布订阅和观察者之间的区别。

我们先来实现一下一个最简单的Event类,方便我们理解Event。我们实现这样几个钩子:emit,on,off。

class Test{
    events = {};
    // 注册观察者
    on(eventName,listerer){
        if(!events[eventName]){
            events[eventName] = [];
        }
        events[eventName].push(listerer);
    }
    emit(eventName,...args){
        if(events[eventName]){
            events[eventName].forEach(listener=>{
                listener(...args);
            });
            return true;
        }else{
            return false;
        }
    }
    // 取消观察
    off(eventName,listerer){
        if(this.events[eventName]&&this.events[eventName].length>0){
            if(this.events[eventName].indexOf(listener)>-1){
              this.events[eventName]
              .splice(this.events[eventName].indexOf(listener),1);
            }
        }
        // 取消成功与否不关注,保证能够正常执行
        return this;
    }
}

可以看到,实现原理不难理解。

Tapable正是通过这个模式设计出了Webpack打包的基础架构。

Tapable的特点

在EventEmitter的基础上,Tapable扩展了同步和异步两个类钩子。 对于监听和触发方法,也做了一些扩展,比如将emit方法扩展为tap和tapPromise等。

class Compiler {
    constructor(){
        this.hook = {
            accelerate: new SyncHook(['newspeed']),
            break: new SyncHook(),
            calculateRoutes: new AsyncSeriesHook(['source','target','routesList'])
        }
    }
    run(){
        this.accelerate(10)
        this.break()
        this.caculareteRoutes('Async','hook','demo')
    }
    accelate(speed){
        this.hooks.accelerate.call(speed)
    }
    break(){
        this.hooks.break.call()
    }
    caculateRoutes(){
        this.hooks.calulateRoutes.promise(...arguments).then(()=>{
            console.log('demo')
        },error=>{
            console.log('error')
        })
    }
}

我们通过上面的代码知道了Tapable的用法,其实在webpack中,也是直接饮用了Tapable的众多钩子类,通过一个compiler完成了对各种钩子的集合,然后通过compiler定义了在打包的不同阶段应该调用的钩子,我们开发自己的插件的时候,就是根据这些钩子进行响应的行为,来完成在不同的打包阶段我们自己需要完成的逻辑。

Compiler

通过刚才的描述我们已经对这个api有了一定的了解了。上面一个demo代码就是compiler模拟代码。 compiler和webpack是什么关系呢

class webpack{
    constructor(){
        const compiler = new Compiler()
    }
}

compiler是webpack内部实现插件和loader调用的核心打包实现工具。这个地方非常值得我们学习,将功能解耦,为以后的架构升级或者重构都提供了强力支撑。

在webpack内部,当compiler实例被构建出来之后,webpack会首先将用户默认配置和用户在webpcak.config.js中配置的选项结合起来生成一份配置再把它转化成内部插件,这些内部插件会注册在不同的compiler钩子上,结合打包流程在合适的时机被调用。

这里提到内部插件,用户自己提供的插件也是在这个时候被注册到compiler的钩子上的。这里用户提供的插件和内部插件没有任何优先级或者其他的不同,区别完全来源于开发者的不同。

webpack控制整体打包的流程,也就是在什么时候应该使用compiler的哪个钩子。

webpack打包流程

上文我们分析了compiler,也写了一个demo来展示compiler。在webpack的打包过程中,会根据需要进行compiler的钩子的触发。在compiler中有这么几个钩子非常重要: entryOption -> run -> make -> before-resolve -> build-module -> nomore-module-loader -> program -> seal -> emit

这也是打包的步骤,其中在make阶段会进行loader的调用,loader会将我们的源代码进行一些过滤和处理。

compilation

在make阶段,真正开始进行模块的过滤和打包是另一个组件完成:compilation。compilation是基于tapable实现的另一个接口,会被compiler用来创建新的compilation对象(或者叫做新的build对象)。

compilation这个对象能够访问所有的模块和他们的依赖。他会对应用程序依赖图中所有模块进行字面上的编译。在编译阶段,模块会被加载(load) ,封存(seal),优化(optimize),分块(chunk),哈希(hash)和重新创建(restore)。

可以看出,在整个打包过程中,compilation负责的这部分工作任务最为艰巨。

loader

上面介绍了原理和打包过程,我们在看下与打包过程紧密结合的两个功能,一个是loader一个是plugin。

先说loader,loader是在make阶段,compilation调用build相关的逻辑的时候起的作用。他的调用是一个递归过程,所以调用顺序是配置顺序的反方向,也就是从右向左。

同步loader

loader除了链式从右向左调用外,还有一个特点,那就是可以分同步和异步调用,接下来就分别看一下。

首先我们先看看同步loader:

function loaderTest(source){
    // 获取配置项
    const loaderUtil = require('loader-utils')
    const pageWidth = loaderUtil.getOptions('pageWidth')
    const num = source.match(/\d*px/g)
    for(let i=0;i<num.length;i++){
        source.replace(num[i],num[i]/pageWidth)
    }
    return source
    // 也可以这么写
   // this.callback(null,source)
}

以上,就是一个同步loader的编写方法。

异步loader

下面我们在看一下异步loader应该怎么写,还是上面的例子我们来改造一下

function loaderTest(source){
   // 获取配置项
   const loaderUtil = require('loader-utils')
   const pageWidth = loaderUtil.getOptions('pageWidth')
   // 获取异步的callback
   const callback = this.async()
   const num = source.match(/\d*px/g)
   new Promise((resolve,reject)=>{
       setTime(()=>{
           for(let i=0;i<num.length;i++){
               source.replace(num[i],num[i]/pageWidth)
           }
           resolve(source)
       },1000)
   }).then((data)=>{
       callback(null,data)
   },(err)=>{
       callback(err,null)
   })
}

其实就是使用了async保证了异步返回处理数据而已。loader的用法里还有更多实用的特点比如缓存,比如输出文件,这些在本篇就暂时先不介绍,可能会单独在写一篇loader的文章来详细介绍。

plugin

在webpack中,另一个更加强大的功能就是plugin,webpack中正是通过一个个内置插件完成的整个打包功能。

我们先看一下一个插件的基本结构

class PluginTest{
    apply(compiler){
        compiler.hook.done.tap('my plugin',(stats)=>{
            console.log(stats)
        })
    }
}
module.export = PluginTest

上面就是一个plugin的基本结构,也就是说通过监听当前打包实例compiler上面的钩子,实现各种功能。 我们在开发过程中不可避免要从配置项给plugin传一些参数,以及想文件夹输出我们的打包结果。我们接下来看一下怎么进行这一部分的实现。

class PluginTest{
    constructor(options){
        this.options = options;
    }
    apply(compiler){
        const {name} = this.options
        compiler.hook.done.tap('my plugin',(compilation,callback)=>{
            compilation.assets[name] = new RawSource('demo')
            callback()
        })
    }
}
module.export = PluginTest

再结合前面了解的compiler和compilation相关的知识,我们就可以开发出各种功能强大的plugin了,复杂plugin的开发我也会在其他文章中编写。

总结

这篇文章我分享了webpack的原理和loader以及plugin的编写相关的知识。因为一篇文章篇幅和读者阅读体验的问题,没有把webpack的实现和更复杂loader和plugin的编写demo放在本篇文章。

下一篇实现一个简单的webpack

下一篇,我们会实现一个简单的webpack,接下来尽量使用小篇幅,多篇文章的形式输出,以提高读者体验和降低理解难度。最后,还是希望小伙伴们天天进步。