Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【webpack进阶系列】Webpack源码断点调试:核心流程 #92

Open
amandakelake opened this issue Dec 30, 2019 · 4 comments
Open

Comments

@amandakelake
Copy link
Owner

amandakelake commented Dec 30, 2019

目录

  • 一、如何用vscode断点调试webpack
  • 二、webpack源码探索
    • 1、webpack入口
    • 2、WebpackOptionsApply.process:处理用户自定义的webpack配置
    • 3、compiler.run()
    • 4、compiler.compile()
    • 5、添加模块依赖+构建
    • 6、封装seal+打包输出
  • 三、总结

一、如何用vscode断点调试webpack

相信有不少小伙伴在膜拜别人的文章时,经常性的会被“断点调试”这四个字劝退,毕竟很多看源码写文章的作者都是大佬,默认断点必备技能,一笔带过,环境没搭起来,怎么跟着作者遨游源码世界呢?

今天,让我们手把手,环境没搭好,我们就不看这源码了好不好!!!

  • 把Github上的Webpack项目拉到本地
git clone https://github.com/webpack/webpack.git
cd webpack
# 切换到最新的webpack 4的某个线上版本(现在已经有5了)
git checkout 45ecebc

AE22C5D9-E465-4637-AB99-B88825D7C90C

  • 在根目录新建debug目录用于调试
# 新建debug目录
mkdir debug
cd debug
# 新建4个文件
touch index.js module.js start-debug.js webpack.config.js
# index和module 是要跑起来的文件
# webpack.config 是配置文件,跟我们日常使用的一样
# start-debug 调用webpack的入口文件,启动编译

# npm初始化debug目录,装一个插件
npm init -y
yarn add clean-webpack-plugin
# 退出外层,接下来的操作都在外面执行
cd ..
  • 4个基础文件的内容
// debug/start-debug.js

// 看根目录的package.json的main字段,可知入口在lib/webpack.js
const webpack = require('../lib/webpack');
const config = require('./webpack.config');

// compiler是webpack的启动入口,直接调用即可
const compiler = webpack(config);
compiler.run();
// debug/webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
	// 选择调试开发环境
	mode: 'development',
	devtool: 'source-map',
	entry: './index.js',
	// 在debug目录下会生成一个dist目录,打包完成后会生成一个main.js文件
	output: {
		path: path.join(__dirname, './dist'),
	},
	plugins: [new CleanWebpackPlugin()],
};
// debug/index.js
import { mudoleName } from './module';
console.log('hi, webpack, this is ' + mudoleName);

// debug/module.js
export const mudoleName = 'module';
  • 接下来我们先用全局的webpack-cli脚手架跑一下以上配置,看能否能正常运行,OK了我们再开始debug
# 如果已经全局安装过,忽略这一步
yarn global add webpack webpack-cli

# 在webpack根目录下执行
webpack debug/index.js --config debug/webpack.config.js

如果以上配置正确,应该会成功在debug/dist目录下打包出一个main.js文件,然后把里面的代码直接粘贴到浏览器控制台运行,应该能正确打印出hi, webpack, this is module
789EA41C-798C-4583-9568-3D447FF71139
400D9327-108D-483D-821B-336E0D1E6E8A

上面步骤通过后,我们来配置一下vscode的断点调试环境,用本地的webpack源码来跑编译,而不是上面全局安装的webpack-cli

打开vscode的调试界面,增加一个launch.json配置文件,选择Node.js环境,然后在program中选择webpack启动入口start-debug.js,可以顺便改个名字

360AC737-9AF4-49A3-84EB-47F6DF3930DA
DBD0BA15-40DB-4951-8966-B96023B3F549

配置完后,进webapck的lib目录内随便打几个断点,点击开始按钮
9F8A14A5-4FCA-47FD-843F-D1336F501B20

然后就会出现上面的断点工具栏(跟chrome的断点是不是很像?)
进入第一个断点
C99F37CA-96AE-4C29-AA9F-B873C13A9D4E

想了解更多可看具体vscode官方文档 Debugging in Visual Studio Code

好了,万事俱备只欠东风,接下来让我们同步断点一步步来看webpack的工作流程吧

二、webpack源码探索

啰嗦一句,看代码之前,还是希望读者们有使用过webpack的经验,并且大致理解webpack的原理,不然有可能会雾里看花
对于这部分同学,这里有两道开胃菜
【webpack进阶系列】手撸一个mini-webpack(一) : 分析收集依赖
【webpack进阶系列】手撸一个mini-webpack(二) : 打包依赖代码

大纲
C85A4563-12BC-4271-A841-28BBF7413D91

1、webpack入口

一起从const compiler = webpack(config)开始

Webpack的起点在lib/webpack.js,除了导出webpack,还通过挂载对象属性的方式导出了很多内置插件

这里有个看源码的原则,我们只关注核心流程,只看核心代码,多余的信息暂且无视或者跳过,这是看源码的一种方法吧,你不可能一下子接受太多的细节信息,应该先把核心脉络弄清楚,建立起对整个库的宏观认知,对作者的主要思路弄清楚后,再回头慢慢品尝旁路分支和一些细枝末节

进入lib/webpack.js,核心代码如下

// lib/webpack.js
const webpack = (options, callback) => {
    compiler = createCompiler(options);
    // 调用compiler.run()手动启动编译,自动编译的代码这里省略了
    return compiler;
};

module.exports = webpack;

再回想我们上面start-debug.js写的启动代码,如何启动webpack是不是秒懂?

// debug/start-debug.js
const webpack = require('../lib/index');
const config = require('./webpack.config');

// compiler是webpack的启动入口,直接调用即可
const compiler = webpack(config);
compiler.run();

compiler在webpack中是一个非常重要的概念,这里先把概念铺出来,后面会慢慢深入
* compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
* compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用

我们先看下createCompiler的实现

// lib/webpack.js
const createCompiler = options => {
    const compiler = new Compiler(options.context);
    compiler.options = options;

    // options.plugins也就是用户自定义的外部插件,都在这里注册
    // 自定义写插件时,为何要定义一个apply方法,其实就是在这里被调用了
    if (Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                plugin.apply(compiler);
            }
        }
    }
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    compiler.options = new WebpackOptionsApply().process(options, compiler);
    return compiler;
};

compilerCompiler实例化而来,用户自定义的webpack.config配置通过options属性挂在compiler上,我们日常使用的plugins原来都是在这里注册的,如果自定义的插件是一个方法,就直接被call调用,如果插件是以类的方式编写,需要提供一个apply方法以供调用,同时会把compiler当做参数传入

调用钩子:compiler.hooks.environment.call()是一种钩子调用的方式,也是接下来我们会碰的最多的一种用法,而这种钩子注册于调用的方式来源于Tapable(含义,水龙头),Tapable是 Eventemitter 的升级版本,包含了 同步/异步 发布订阅模型, 在它的异步模型里又分为 串行/并行,Webpack的整个骨架基于Tapable,Webpack 可以认为是一种基于事件流的编程范例,内部的工作流程都是基于 插件 机制串接起来

再往下看WebpackOptionsApply().process(options, compiler)

2、WebpackOptionsApply.process:处理用户自定义的webpack配置

进入WebpackOptionsApply去看process方法的实现

// lib/WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply {
    // ... constructor() {}
    process(options, compiler){
        // 处理配置中的参数,调对应的plugin
        // if (options.param) new XXXPlugin().apply(compiler)
        if(options.target) {
            new XXXPlugin().apply(compiler)
        }
        // 或者直接初始化某些必备plugin 
        // new XXXPlugin().apply(compiler) ...
        new JavascriptModulesPlugin().apply(compiler);
        
        // 调用/注册某些钩子
        compiler.hooks.afterPlugins.call(compiler);
        compiler.hooks.afterResolvers.call(compiler);
        return options;
    }
}

拿到参数后,new很多的plugin,并apply他们

我们知道,webpack插件其实就是提供了apply方法的类编写一个插件 | webpack,这个 apply 方法在安装插件时,会被 compiler 调用一次。apply 方法可以接收一个 compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象

用户自定义的插件在上面createCompiler的时候就已经全部注册完成了,这里的全部是webpack内置的插件,它们的职责其实是一样的,都是勾住compiler.hooks上的一个生命周期(或者说webpack的事件钩子),一旦进入该生命周期(或者说注册的事件被触发),插件上定义的callback事件也会被触发,执行插件的功能

再次回到上面说的Webpack 可以认为是一种基于事件流的编程范例,内部的工作流程都是基于 **插件** 机制串接起来,是不是好理解一点了

通过注册用户自定义以及webpack内置的插件,在合适的钩子事件触发后执行插件的功能,而插件可以通过compilercompilation拿到完整的 webpack 环境配置以及编译过程的状态,那么通过插件,我们就几乎可以控制整个编译过程的任一节点,做任意的事情。

好了好了,扯远了
以上process流程完成了用户配置的初始化,把对应的插件都注册好了,webpack将所有它关心的hook消息都注册完成,就等着后续编译过程中一一触发

3、compiler.run()

compiler是实例化了一个Compiler类,启动webpack编译是通过compiler.run()方法,我们去类上面找一下这个方法

// lib/Compiler.js
run(callback) {
    const onCompiled = (err, compilation) => {
        if (this.hooks.shouldEmit.call(compilation) === false) {
            this.hooks.done.callAsync(stats, err => {...});
        }
        this.emitAssets(compilation, err => {
            if (compilation.hooks.needAdditionalPass.call()) {
                this.hooks.done.callAsync(stats, err => {
                    this.compile(onCompiled);
                });
            }

            this.emitRecords(err => {
                this.hooks.done.callAsync(stats, err => {...});
            });
        });
    };

    this.hooks.beforeRun.callAsync(this, err => {
        this.hooks.run.callAsync(this, err => {
            this.compile(onCompiled);
        });
    });
}

先看后面那段,在beforeRunrun两个钩子中应该是先做完一些前置预备工作(beforeRun中绑定读取文件的对象,run中处理缓存的模块,减少编译的模块,加速编译速度),然后进入Compiler.compile()编译环节

// beforeRun -> run -> this.compile(onCompiled);
this.hooks.beforeRun.callAsync(this, err => {
    this.hooks.run.callAsync(this, err => {
        this.compile(onCompiled);
    });
});

Compiler.compile()编译完成后调用上面的onCompiled回调,看shouldEmitdone这些钩子的字面意思,应该将编译后的输出结果生成文件

const onCompiled = (err, compilation) => {
	  // 编译失败,直接done
    if (this.hooks.shouldEmit.call(compilation) === false) {
        this.hooks.done.callAsync(stats, err => {...});
    }
	  // 编译成功,emitAssets生成文件,然后done, 如果有递归,可以继续
    this.emitAssets(compilation, err => {
        if (compilation.hooks.needAdditionalPass.call()) {
            this.hooks.done.callAsync(stats, err => {
                this.compile(onCompiled);
            });
        }

        this.emitRecords(err => {
            this.hooks.done.callAsync(stats, err => {...});
        });
    });
};

虽然上面的代码已经够精简了,但还是还有点复杂,而且有点callback hell的意思,我们再捋一捋顺序,本质上是这么一条链路
hooks.beforeRun -> hooks.run -> this.compile(onCompiled); -> hooks.done -> hooks.afterDone

this.compiler(onCompiled)编译过程稍等一下,先看其他4个钩子,beforeRun -> run -> doned -> afterDone

字面来看,run方法勾住了编译了整个阶段的前期和后期,在编译过程中的不同阶段,处处都有compiler对象的存在,触发了不同的生命周期钩子,那么前面根据钩子注册的插件的回调也会被相应调用

举个例子,我随便搜了个beforeRun的钩子,发现一个叫NodeEnvironmentPlugin的内置插件,勾住了beforeRun

compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
    if (compiler.inputFileSystem === inputFileSystem) {
        inputFileSystem.purge();
    }
});

那么,在触发beforeRun钩子时,也即运行到this.hooks.beforeRun.callAsync时,NodeEnvironmentPlugin插件的下面这段回调逻辑就会执行,这里也就是上面说的beforeRun中绑定读取文件的对象

if (compiler.inputFileSystem === inputFileSystem) {
    inputFileSystem.purge();
}

以上可以理解,所有注册了beforeRun、run、doned、afterDone钩子的插件,都会在compiler.run()这个过程中被一一触发调用

捋完compiler的钩子,我们再看看核心的this.compiler(onCompiled)

终于要到compilation要出场了,再po一次概念
* compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
* compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用

4、compiler.compile()

先看精简版代码

// lib/Compiler.js

// callback:compiler.run()传入的onCompiled方法
compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {

        this.hooks.compile.call(params);

        const compilation = this.newCompilation(params);

        this.hooks.make.callAsync(compilation, err => {

            compilation.finish(err => {

                compilation.seal(err => {

                    this.hooks.afterCompile.callAsync(compilation, err => {

                        return callback(null, compilation);
                    });
                });
            });
        });
    });
}

同样是一个回调层次比较深的方法,仿照上面的分析方法抽一下钩子出来看看,beforeCompile -> compile -> make -> afterCompile

make钩子之间实例化了一个compilation对象,实例化之前通过newCompilationParams实例化了两个核心的工厂对象normalModuleFactorycontextModuleFactory,用来创建normalModulecontextModule实例,然后将整个模块工厂对象传入 compilercompilation中,在compilation中能够拿到当前的模块类型,最常用的就是normalModule
A063CF76-2086-4E09-B927-13ACEF7AAF7A

真正的编译构建过程注册在make这个钩子的回调上,终于到你了啊,老哥

虽然知道了是make这个钩子,但它是在哪里被哪个插件注册的呢?

我们写过业务代码都知道,发布订阅模式虽然好用,但是它的代码可以到处飞,任何地方都可能出现订阅代码,而基于tapable的webpack同样有这个特点,我们反向查找一下看看,它是用callAsync触发的,先试试hooks.make.tapAsync,如果不行,就试试tapPromise

一共也没几个选项,根据插件的名字,其实很容易就能区分出来,就是SingleEntryPluginMultiEntryPlugin这两插件

其他几个插件的名字明显不符合要求,什么自动预加载、DLL、动态入口、预加载、测试子编译进程失败,只有动态入口DynamicEntryPlugin有个干扰性,大不了都看一下嘛
8513DA8E-7D8E-49B2-871E-71C402040DB0

go go go,进lib/SingleEntryPlugin.js看一下
5CD3DB68-1A7E-443C-8728-6FBE5A4201AB

找到了插件,那么在哪里调用的呢?
再反向查一下哪里new XXXPlugin,搜一发new SingleEntryPlugin,再用一下排除法,原来是个叫EntryOptionPlugin的插件,果然是同时调用了SingleEntryPlugin、MultiEntryPlugin两个插件,上面的猜测没错
CC971069-12A8-4168-B808-C4A496281F61

等一下,这里是不是有点似曾相识,原来是对配置的entry字段根据不同类型进行分发,分别调用SingleEntryPlugin、MultiEntryPlugin、DynamicEntryPlugin等插件

// entry是字符串、数组、对象等类型时的处理,默认入口是main
if (typeof entry === "string" || Array.isArray(entry)) {
    itemToPlugin(context, entry, "main").apply(compiler);
} else if (typeof entry === "object") {
    for (const name of Object.keys(entry)) {
        itemToPlugin(context, entry[name], name).apply(compiler);
    }
// entry是函数时,实例化一个动态入口插件
} else if (typeof entry === "function") {
    new DynamicEntryPlugin(context, entry).apply(compiler);
}

再往下找调用处,搜new EntryOptionPlugin,nice,只有一个结果
AE0C2F31-9B53-466B-8FFC-EEB0973DDBCD

咦,回到了WebpackOptionsApply.process()方法里,这不就是上面第2点的用户配置初始化吗,bingo,看来上面的判断没错,WebpackOptionsApply.process里已经把所有的插件都注册好了,make就在这里等你触发呢

整体梳理一下,围绕make钩子的核心关系,如下图
60904800-2800-41E6-B223-E430638F86BD

现在我们再回到最初注册make的地方,先看看lib/SingleEntryPlugin.js注册了什么回调

// lib/SingleEntryPlugin.js
compiler.hooks.make.tapAsync(
    "SingleEntryPlugin",
    (compilation, callback) => {
        const { entry, name, context } = this;

        const dep = SingleEntryPlugin.createDependency(entry, name);
        compilation.addEntry(context, dep, name, callback);
    }
);

再看看lib/MultiEntryPlugin.js

// /lib/MultiEntryPlugin.js
compiler.hooks.make.tapAsync(
    "MultiEntryPlugin",
    (compilation, callback) => {
        const { context, entries, name } = this;

        const dep = MultiEntryPlugin.createDependency(entries, name);
        compilation.addEntry(context, dep, name, callback);
    }
);

两份代码类似,重点在compilation.addEntry,继续往下看

5、添加模块依赖+构建

compilation.addEntry开始,这里的方法嵌套比较深,而且参数有可能就是关键方法,直接读不是容易理解,我们先把调用链理出来
addEntry -> _addModuleChain -> onModule(module) -> buildModule -> module.build

_addModuleChain,构建依赖核心方法,同时保存模块与模块之间的依赖

const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
// ...
moduleFactory.create()

// ...
onModule(module);

moduleFactory是当前模块类型的创造工厂

moduleFactory.create()生成新模块

onModule(module)最终调用了_addModuleChain的第三个参数,将入口模块添加到compilation.entries

这里虽然方法名写的很清楚,但是要理解的话,可能要认真读一读几个方法的参数是怎么传递的,才能知道整个调用的链路,比如onModule整个方法其实是传进来的参数,并不是在compilation里直接定义的方法属性

module => {
	this.entries.push(module);
},

A2155AF6-16A4-463C-ABCC-400822A0E024

再往下走,buildModule -> this.hooks.buildModule.call(module); 给出一个可以对module进行操作的hook,即允许自定义插件勾住这个地方对模块进行操作

module.build自行创建模块,找到build整个方法需要根据类的继承一直往上查找,最终会走到NormalModuledoBuild里面,然后runLoaders选择合适的loader把所有的resource全部转换为标准JS模块(webpack只认识JS,对css、vue、react、jpg等等文件全都不认识,需要loader把它们转成标准JS模块)
F506765E-7074-4F80-A97E-8DA1E2F1ADE4

经过build -> doBuild拿到所有模块后,开始编译生成AST

// lib/NormalModule.js

build(options, compilation, resolver, fs, callback) {
    // ...定义很多内部变量

    return this.doBuild(options, compilation, resolver, fs, err => {
        // ...
        try {
            const result = this.parser.parse(
                this._ast || this._source.source(),
                {
                    current: this,
                    module: this,
                    compilation: compilation,
                    options: options
                },
                (err, result) => {
                    if (err) {
                        handleParseError(err);
                    } else {
                        handleParseResult(result);
                    }
                }
            );
            if (result !== undefined) {
                // parse is sync
                handleParseResult(result);
            }
        } catch (e) {
            handleParseError(e);
        }
    });
}

其中const result = this.parser.parse()方法,本质调用链是new JavascriptModulesPlugin() -> new Parser() -> Parse.parse() -> acorn.parse ,用第三方包acorn提供的parse方法对JS源代码进行语法解析生成AST

其实生成AST最大的作用的是收集模块依赖关系,比如代码中出现了以下语法的话,都可以统一处理收集,试想通过写正则表达式或者其他ifelse条件来处理这部分依赖的话,头都要炸了

// ES6 module
import Module from 'XX-npm-module';
// commonjs
const Module = require('XX-npm-module')
// AMD/CMD
require(Module, callback)

本段生成module相关内容可参考详细版本 【webpack进阶系列】构建module流程
B62435CD-345F-4A58-A86D-0811FD4ADFC8

至此,webpack已经收集完整了从入口开始所有模块的信息和依赖项,那么接下来就是如何封装打包代码了 -> compilation.seal

6、封装seal+打包输出

回到前面的compiler.compile()
32A0FA86-6CE5-4954-9215-F080023DAB43

compilation完成构建过程后,接下来进入compilation.seal钩子,点进去瞧一眼,可怕。。。大量的性能优化插件都在此被调用,简化起来就是生成chunk资源

// lib/Compilation.js
seal() {
    // ...many optimize plugins being called
    buildChunkGraph()
    // ...many optimize plugins being called
    this.createHash();
    // ...
    this.createModuleAssets();
    // ...
    this.createChunkAssets();
    // ...
}

this.createChunkAssets()会调用emitAsset,即将生成最终js并输出到output的path,在这里我们打个断点,会看到第二个参数 source._source的内容已经初具雏形,基本就是我们打包完成后所看见的内容了
49254156-273D-4742-899F-2FCBC07472DA

然后会调用emit钩子,根据配置文件的output.path属性,将文件输出到指定的文件夹,然后就可以查看最后的打包文件了

this.hooks.emit.callAsync(compilation, err => {
    if (err) return callback(err);
    outputPath = compilation.getPath(this.outputPath);
    this.outputFileSystem.mkdirp(outputPath, emitFiles);
});

下面以一张完美的gif图来收个尾,看一看打包文件在最后一步从无到有的神圣一刻
2020-01-21 20-49-24 2020-01-21 20_51_17

三、核心流程总结

90900D00-8AD4-4CA4-B1BB-BF90CB96BB50

这里只是一条主线,我们剔除了很多的细节,只是为了搞清楚webpack是怎么跑起来到生成打包文件的

举个例子,假如我们以watch的模式运行webpack,其实走的不是run任务,而是watch-run任务

然后还有tapable、模块工厂、Parse如何生成AST、seal过程如何生成chunk、如何调用模板生成最终代码,性能优化插件是怎么处理的等等,还有很多方面的东西值得我们去研究学习,后面笔者有时间的话会一块块写文章进行分析讲解,敬请期待。

然后再啰嗦一句,为何我们要花时间去看源码了解webapck的原理,做这些看起来没啥业务价值,甚至有点【枯燥】的事情?

个人看法,前端变化快是公认的,但是技术这东西,应该是万变不离其宗的,比如我们现在在研究webpack4,但其实webpack5已经出来了,源码里已经在写webpack6了,那么我们要怎么跟得上,怎么避免’学不动’?其实无论它发展到6、7、8都好,本质上webpack就是tapable模型扩展开来的插件架构,把核心的几个钩子串起来的流程弄清楚后,再怎么变也只是一些API和一些细节,使用起来或者说掌握webpack的使用会更加得心应手,当然我的意思不是说你看过源码懂原理就一定能把webpack用的很好,实践还是很吃项目的,但你懂了就能很快上手基于webpack生态的前端工程化,而且有举一反三,开发插件的能力。

掌握技术的核心底层原理,不能立刻产生效果,能让我们学的更快,走的更远,这绝对是稳赚不赔的投资。

参考

Webpack author Tobias Koppers: How Webpack works - YouTubewebpack作者亲自给你讲webpack原理,还要啥自行车
Understanding webpack from the inside out - YouTube
Webpack源码解读:理清编译主流程 - 掘金
从Webpack源码探究打包流程,萌新也能看懂~ - 掘金

@amandakelake amandakelake changed the title 【webpack进阶系列】webpack核心原理 【webpack进阶系列】Webpack源码断点调试:核心流程 Jan 20, 2020
@ghost
Copy link

ghost commented Mar 30, 2021

前辈好厉害,看懂了一部分流程,后面越看越懵,自己断点弄的头都快晕了,再好好吸收一下,点个赞👍

@Inchill
Copy link

Inchill commented Apr 9, 2021

👍

@zhangzs000
Copy link

./debug/index.js

@hellozhangran
Copy link

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants