webpack 源码选读

书读百遍,其义自见。

  • 为了研读源码首先clone一份webpack的repo到本地。刚开始研读的时候一定是一头雾水,如果实在是难以下咽可以先尝试了解一下 tapablecompilercompilation 这些核心定义, 在进行阅读。
  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类(./lib/Compiler.js):webpack的主要引擎,在compiler对象记录了完整的webpack环境信息,在webpack从启动到结束,compiler只会生成一次。你可以在compiler对象上读取到webpack config信息,outputPath等;
  • Compilation类(./lib/Compilation.js):代表了一次单一的版本构建和生成资源。compilation编译作业可以多次执行,比如webpack工作在watch模式下,每次监测到源文件发生变化时,都会重新实例化一个compilation对象。一个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的初始化;在这个类中主要做了两件事;

  • new很多的Plugin,并且apply它们。
  • 根据options.xxx的配置项,做初始化工作。

提纲挈领 - 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

资源参考:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容