webpack 源码选读

2020-03-26  本文已影响0人  pengji

书读百遍,其义自见。

  git clone https://github.com/webpack/webpack.git

从入口看起 - webpack.js

/* path - ./lib/webpack.js */
const Compiler = require("./Compiler");
const webpack = (options, callback) => {
    let compiler 
    // 非必要的watch 参数就暂时忽略
    // 根据options 来判断使用 createCompiler / createMultiCompiler 来实例化
    compiler = createCompiler(options)
    // 如果传入callback函数,则自启动
    if(callback){
        compiler.run((err, states) => {
            compiler.close((err2)=>{
                callbacl(err || err2, states)
            })
        })
    }
    return compiler
}

webpack函数执行后返回compiler对象,在webpack中存在两个非常重要的核心对象,分别为compiler和compilation,它们在整个编译过程中被广泛使用。

Compiler 在上面的 createCompiler 中被实例化,其中对于plugins的操作需要特别注意一下;这也是为什么plugins配置时要保持为函数,或者一个有apply字段的对象且apply是函数。

const createCompiler = rawOptions => {
    // 对于一些options 的操作直接过滤
    const compiler = new Compiler(options.context);
    compiler.options = options;
    if (Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                plugin.apply(compiler);
            }
        }
    }

    // 调用相关hook
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    // 对webpack options的初始化
    new WebpackOptionsApply().process(options, compiler);
    compiler.hooks.initialize.call();
    // 最终返回compiler
    return compiler;
};

WebpackOptionsApply 主要完成了对options的初始化;在这个类中主要做了两件事;


提纲挈领 - tapable

Tapable 是一个小型的库,允许你对一个 javascript 模块添加和应用插件。看到很多文章把它形容为webpack的管家或者骨架。把它放在第一个来了解主要是为了防止后面的阅读过程中由于这几个API带来的一头雾水。

Tapable 通过工厂类 HookCodeFactory,释放出以下几个API:

const {
    SyncHook, 
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");
  1. Hook 类型
    每个hook都可以触发一个或多个功能。 它们如何执行取决于hook类型:

    • Basic hook.: 按照事件注册顺序,依次执行handler,handler之间互不干扰;
    • Waterfall:按照事件注册顺序,依次执行handler,前一个handler的返回值将作为下一个handler的入参;
    • Bail: 按照事件注册顺序,依次执行handler,若其中任一handler返回值不为undefined,则剩余的handler均不会执行;
    • Loop:按照事件注册顺序,依次执行handler,若任一handler的返回值不为undefined,则该事件链再次从头开始执行,直到所有handler均返回undefined

    hook可以是同步的或异步的。 为了反映这一点,提供了“ Sync”,“ AsyncSeries”和“ AsyncParallel” hook类:

    • Sync.
    • AsyncSeries.
    • AsyncParallel.

    hook类型反映在其类名称中。例如,AsyncSeriesWaterfallHook允许异步函数并依次运行它们,将每个函数的返回值传递给下一个函数。

  2. Interception

    • call: (...args) => void 当hook被触发时,向拦截器添加呼叫将被触发。 可以访问hooks参数。
    • tap: (tap: Tap) => void 将插件添加到拦截器中时,将在插件点击钩子时触发。 提供的是Tap对象。 点击对象无法更改。
    • loop: (...args) => void 在拦截器中添加循环将为循环钩子的每个循环触发。
    • register: (tap: Tap) => Tap | undefined 将寄存器添加到拦截器将为每个添加的Tap触发并允许对其进行修改。
  3. 其他
    还有一些其他的具体参数,详细可以看 https://github.com/webpack/tapable


Compiler

参考了下面文章中的伪代码部分;

class Compiler {
    constructor(context){
    // 所有钩子都是由`Tapable`提供的,不同钩子类型在触发时,调用时序也不同
    this.hooks = {
            beforeRun: new AsyncSeriesHook(["compiler"]),
            run: new AsyncSeriesHook(["compiler"]),
            done: new AsyncSeriesHook(["stats"]),
            // ...
        }
    }
  
    // ...
    
    run(callback){
        const onCompiled = (err, compilation) => {
            if(err) return
            const stats = new Stats(compilation);
            this.hooks.done.callAsync(stats, err => {
                if(err) return
                callback(err, stats)
                this.hooks.afterDone.call(stats)
            })
        }
        this.hooks.beforeRun.callAsync(this, err => {
            if(err) return
            this.hooks.run.callAsync(this, err => {
                if(err) return
                this.compile(onCompiled)
            })
        })
    }
}

run 这个阶段hook住编译的一些阶段并在不同阶段执行一些准备好的hook;在run函数中出现的钩子有:beforeRun --> run --> done --> afterDone。第三方插件可以钩住不同的生命周期,接收compiler对象,处理不同逻辑。

在this.compile中引出了另外一个重要的阶段 compilation;


Compilation

compile(callback){
    const params = this.newCompilationParams()  // 初始化模块工厂对象
    this.hooks.beforeCompile.callAsync(params, err => {
        this.hooks.compile.call(params)
        // compilation记录本次编译作业的环境信息 
        const compilation = new Compilation(this)
        this.hooks.make.callAsync(compilation, err => {
            compilation.finish(err => {
                compilation.seal(err=>{
                    this.hooks.afterCompile.callAsync(compilation, err => {
                        return callback(null, compilation)
                    })
                })
            })
        })
    })
}

compile函数和run一样,触发了一系列的钩子函数,在compile函数中出现的钩子有:beforeCompile --> compile --> make --> afterCompile

我们关心的make过程,在compile过程中暴露出来的仅仅是一个hook; 探究具体: this.addEntry --> this.addModuleChain --> this.handleModuleCreation --> this.addModule --> this.buildModule --> this._buildModule --> module.build(this指代compiliation)

在build时 会优先执行doBuild,选用合适的 loader 去加载对应的资源;webpack对处理标准的JS模块很在行,但处理其他类型文件(css, scss, json, jpg)等就无能为力了,此时它就需要loader的帮助。loader的作用就是转换源代码为JS模块,这样webpack就可以正确识别了。

TODO: parse、 seal

资源参考:

上一篇下一篇

猜你喜欢

热点阅读