2024年最新[全文高能] 一文吃透 Webpack 核心原理(1),2024年最新web前端面试上机题

最后

我可以将最近整理的前端面试题分享出来,其中包含HTML、CSS、JavaScript、服务端与网络、Vue、浏览器、数据结构与算法等等,还在持续整理更新中,希望大家都能找到心仪的工作。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

篇幅有限,仅展示部分截图:

回顾章节开始时提到的问题:

  • Webpack 编译过程会将源码解析为 AST 吗?webpack 与 babel 分别实现了什么?

    • 构建阶段会读取源码,解析为 AST 集合。
  • Webpack 读出 AST 之后仅遍历 AST 集合;babel 则对源码做等价转换

  • Webpack 编译过程中,如何识别资源对其他资源的依赖?

    • Webpack 遍历 AST 集合过程中,识别 require/ import 之类的导入语句,确定模块对其他资源的依赖关系
  • 相对于 grant、gulp 等流式构建工具,为什么 webpack 会被认为是新一代的构建工具?

    • Grant、Gulp 仅执行开发者预定义的任务流;而 webpack 则深入处理资源的内容,功能上更强大

生成阶段


基本流程

构建阶段围绕 module 展开,生成阶段则围绕 chunks 展开。经过构建阶段之后,webpack 得到足够的模块内容与模块关系信息,接下来开始生成最终资源了。代码层面,就是开始执行 compilation.seal 函数:

// 取自 webpack/lib/compiler.js

compile(callback) {

const params = this.newCompilationParams();

this.hooks.beforeCompile.callAsync(params, err => {

// …

const compilation = this.newCompilation(params);

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

// …

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

// …

process.nextTick(() => {

compilation.finish(err => {

compilation.seal(err => {…});

});

});

});

});

});

}

seal 原意密封、上锁,我个人理解在 webpack 语境下接近于 “将模块装进蜜罐”seal 函数主要完成从 modulechunks 的转化,核心流程:

简单梳理一下:

  1. 构建本次编译的 ChunkGraph 对象;

  2. 遍历 compilation.modules 集合,将 moduleentry/动态引入 的规则分配给不同的 Chunk 对象;

  3. compilation.modules 集合遍历完毕后,得到完整的 chunks 集合对象,调用 createXxxAssets 方法

  4. createXxxAssets 遍历 module/chunk ,调用 compilation.emitAssets 方法将 assets 信息记录到 compilation.assets 对象中

  5. 触发 seal 回调,控制流回到 compiler 对象

这一步的关键逻辑是将 module 按规则组织成 chunks ,webpack 内置的 chunk 封装规则比较简单:

  • entry 及 entry 触达到的模块,组合成一个 chunk

  • 使用动态引入语句引入的模块,各自组合成一个 chunk

chunk 是输出的基本单位,默认情况下这些 chunks 与最终输出的资源一一对应,那按上面的规则大致上可以推导出一个 entry 会对应打包出一个资源,而通过动态引入语句引入的模块,也对应会打包出相应的资源,我们来看个示例。

示例:多入口打包

假如有这样的配置:

const path = require(“path”);

module.exports = {

mode: “development”,

context: path.join(__dirname),

entry: {

a: “./src/index-a.js”,

b: “./src/index-b.js”,

},

output: {

filename: “[name].js”,

path: path.join(__dirname, “./dist”),

},

devtool: false,

target: “web”,

plugins: [],

};

实例配置中有两个入口,对应的文件结构:

index-a 依赖于c,且动态引入了 e;index-b 依赖于 c/d 。根据上面说的规则:

  • entry 及entry触达到的模块,组合成一个 chunk

  • 使用动态引入语句引入的模块,各自组合成一个 chunk

生成的 chunks 结构为:

也就是根据依赖关系,chunk[a] 包含了 index-a/c 两个模块;chunk[b] 包含了 c/index-b/d 三个模块;chunk[e-hash] 为动态引入 e 对应的 chunk。

不知道大家注意到没有,chunk[a]chunk[b] 同时包含了 c,这个问题放到具体业务场景可能就是,一个多页面应用,所有页面都依赖于相同的基础库,那么这些所有页面对应的 entry 都会包含有基础库代码,这岂不浪费?为了解决这个问题,webpack 提供了一些插件如 CommonsChunkPluginSplitChunksPlugin,在基本规则之外进一步优化 chunks 结构。

SplitChunksPlugin 的作用

SplitChunksPlugin 是 webpack 架构高扩展的一个绝好的示例,我们上面说了 webpack 主流程里面是按 entry / 动态引入 两种情况组织 chunks 的,这必然会引发一些不必要的重复打包,webpack 通过插件的形式解决这个问题。

回顾 compilation.seal 函数的代码,大致上可以梳理成这么4个步骤:

  1. 遍历 compilation.modules ,记录下模块与 chunk 关系

  2. 触发各种模块优化钩子,这一步优化的主要是模块依赖关系

  3. 遍历 module 构建 chunk 集合

  4. 触发各种优化钩子

上面 1-3 都是预处理 + chunks 默认规则的实现,不在我们讨论范围,这里重点关注第4个步骤触发的 optimizeChunks 钩子,这个时候已经跑完主流程的逻辑,得到 chunks 集合,SplitChunksPlugin 正是使用这个钩子,分析 chunks 集合的内容,按配置规则增加一些通用的 chunk :

module.exports = class SplitChunksPlugin {

constructor(options = {}) {

// …

}

_getCacheGroup(cacheGroupSource) {

// …

}

apply(compiler) {

// …

compiler.hooks.thisCompilation.tap(“SplitChunksPlugin”, (compilation) => {

// …

compilation.hooks.optimizeChunks.tap(

{

name: “SplitChunksPlugin”,

stage: STAGE_ADVANCED,

},

(chunks) => {

// …

}

);

});

}

};

理解了吗?webpack 插件架构的高扩展性,使得整个编译的主流程是可以固化下来的,分支逻辑和细节需求“外包”出去由第三方实现,这套规则架设起了庞大的 webpack 生态,关于插件架构的更多细节,下面 plugin 部分有详细介绍,这里先跳过。

写入文件系统

经过构建阶段后,compilation 会获知资源模块的内容与依赖关系,也就知道“输入”是什么;而经过 seal 阶段处理后, compilation 则获知资源输出的图谱,也就是知道怎么“输出”:哪些模块跟那些模块“绑定”在一起输出到哪里。seal 后大致的数据结构:

compilation = {

// …

modules: [

/* … */

],

chunks: [

{

id: “entry name”,

files: [“output file name”],

hash: “xxx”,

runtime: “xxx”,

entryPoint: {xxx}

// …

},

// …

],

};

seal 结束之后,紧接着调用 compiler.emitAssets 函数,函数内部调用 compiler.outputFileSystem.writeFile 方法将 assets 集合写入文件系统,实现逻辑比较曲折,但是与主流程没有太多关系,所以这里就不展开讲了。

资源形态流转


OK,上面已经把逻辑层面的构造主流程梳理完了,这里结合资源形态流转的角度重新考察整个过程,加深理解:

  • compiler.make 阶段:

    • entry 文件以 dependence 对象形式加入 compilation 的依赖列表,dependence 对象记录有 entry 的类型、路径等信息
  • 根据 dependence 调用对应的工厂函数创建 module 对象,之后读入 module 对应的文件内容,调用 loader-runner 对内容做转化,转化结果若有其它依赖则继续读入依赖资源,重复此过程直到所有依赖均被转化为 module

  • compilation.seal 阶段:

    • 遍历 module 集合,根据 entry 配置及引入资源的方式,将 module 分配到不同的 chunk
  • 遍历 chunk 集合,调用 compilation.emitAsset 方法标记 chunk 的输出规则,即转化为 assets 集合

  • compiler.emitAssets 阶段:

    • assets 写入文件系统

Plugin 解析

=========

网上不少资料将 webpack 的插件架构归类为“事件/订阅”模式,我认为这种归纳有失偏颇。订阅模式是一种松耦合架构,发布器只是在特定时机发布事件消息,订阅者并不或者很少与事件直接发生交互,举例来说,我们平常在使用 HTML 事件的时候很多时候只是在这个时机触发业务逻辑,很少调用上下文操作。而 webpack 的钩子体系是一种强耦合架构,它在特定时机触发钩子时会附带上足够的上下文信息,插件定义的钩子回调中,能也只能与这些上下文背后的数据结构、接口交互产生 side effect,进而影响到编译状态和后续流程。

学习插件架构,需要理解三个关键问题:

  • WHAT: 什么是插件

  • WHEN: 什么时间点会有什么钩子被触发

  • HOW: 在钩子回调中,如何影响编译状态

What: 什么是插件


从形态上看,插件通常是一个带有 apply 函数的类:

class SomePlugin {

apply(compiler) {

}

}

apply 函数运行时会得到参数 compiler ,以此为起点可以调用 hook 对象注册各种钩子回调,例如:compiler.hooks.make.tapAsync ,这里面 make 是钩子名称,tapAsync 定义了钩子的调用方式,webpack 的插件架构基于这种模式构建而成,插件开发者可以使用这种模式在钩子回调中,插入特定代码。webpack 各种内置对象都带有 hooks 属性,比如 compilation 对象:

class SomePlugin {

apply(compiler) {

compiler.hooks.thisCompilation.tap(‘SomePlugin’, (compilation) => {

compilation.hooks.optimizeChunkAssets.tapAsync(‘SomePlugin’, ()=>{});

})

}

}

钩子的核心逻辑定义在 Tapable 仓库,内部定义了如下类型的钩子:

const {

SyncHook,

SyncBailHook,

SyncWaterfallHook,

SyncLoopHook,

AsyncParallelHook,

AsyncParallelBailHook,

AsyncSeriesHook,

AsyncSeriesBailHook,

AsyncSeriesWaterfallHook

} = require(“tapable”);

不同类型的钩子根据其并行度、熔断方式、同步异步,调用方式会略有不同,插件开发者需要根据这些的特性,编写不同的交互逻辑,这部分内容也特别多,回头展开聊聊。

When: 什么时候会触发钩子


了解 webpack 插件的基本形态之后,接下来需要弄清楚一个问题:webpack 会在什么时间节点触发什么钩子?这一块我认为是知识量最大的一部分,毕竟源码里面有237个钩子,但官网只介绍了不到100个,且官网对每个钩子的说明都太简短,就我个人而言看完并没有太大收获,所以有必要展开聊一下这个话题。先看几个例子:

  • compiler.hooks.compilation

    • 时机:启动编译创建出 compilation 对象后触发
  • 参数:当前编译的 compilation 对象

  • 示例:很多插件基于此事件获取 compilation 实例

  • compiler.hooks.make

    • 时机:正式开始编译时触发
  • 参数:同样是当前编译的 compilation 对象

  • 示例:webpack 内置的 EntryPlugin 基于此钩子实现 entry 模块的初始化

  • compilation.hooks.optimizeChunks

    • 时机:seal 函数中,chunk 集合构建完毕后触发
  • 参数:chunks 集合与 chunkGroups 集合

  • 示例:SplitChunksPlugin 插件基于此钩子实现 chunk 拆分优化

  • compiler.hooks.done

    • 时机:编译完成后触发
  • 参数:stats 对象,包含编译过程中的各类统计信息

  • 示例:webpack-bundle-analyzer 插件基于此钩子实现打包分析

这是我总结的钩子的三个学习要素:触发时机、传递参数、示例代码。

触发时机

触发时机与 webpack 工作过程紧密相关,大体上从启动到结束,compiler 对象逐次触发如下钩子:

compilation 对象逐次触发:

所以,理解清楚前面说的 webpack 工作的主流程,基本上就可以捋清楚“什么时候会触发什么钩子”。

参数

传递参数与具体的钩子强相关,官网对这方面没有做出进一步解释,我的做法是直接在源码里面搜索调用语句,例如对于 compilation.hooks.optimizeTree ,可以在 webpack 源码中搜索 hooks.optimizeTree.call 关键字,就可以找到调用代码:

// lib/compilation.js#2297

this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {

});

结合代码所在的上下文,可以判断出此时传递的是经过优化的 chunksmodules 集合。

找到示例

Webpack 的钩子复杂程度不一,我认为最好的学习方法还是带着目的去查询其他插件中如何使用这些钩子。例如,在 compilation.seal 函数内部有 optimizeModulesafterOptimizeModules 这一对看起来很对偶的钩子,optimizeModules 从字面上可以理解为用于优化已经编译出的 modules ,那 afterOptimizeModules 呢?

从 webpack 源码中唯一搜索到的用途是 ProgressPlugin ,大体上逻辑如下:

compilation.hooks.afterOptimizeModules.intercept({

name: “ProgressPlugin”,

call() {

handler(percentage, “sealing”, title);

},

done() {

progressReporters.set(compiler, undefined);

handler(percentage, “sealing”, title);

},

result() {

handler(percentage, “sealing”, title);

},

error() {

handler(percentage, “sealing”, title);

},

tap(tap) {

// p is percentage from 0 to 1

// args is any number of messages in a hierarchical matter

progressReporters.set(compilation.compiler, (p, …args) => {

handler(percentage, “sealing”, title, tap.name, …args);

});

handler(percentage, “sealing”, title, tap.name);

}

});

基本上可以猜测出,afterOptimizeModules 的设计初衷就是用于通知优化行为的结束。

apply 虽然是一个函数,但是从设计上就只有输入,webpack 不 care 输出,所以在插件中只能通过调用类型实体的各种方法来或者更改实体的配置信息,变更编译行为。例如:

  • compilation.addModule :添加模块,可以在原有的 module 构建规则之外,添加自定义模块

  • compilation.emitAsset:直译是“提交资产”,功能可以理解将内容写入到特定路径

到这里,插件的工作机理和写法已经有一个很粗浅的介绍了,回头单拎出来细讲吧。

How: 如何影响编译状态


解决上述两个问题之后,我们就能理解“如何将特定逻辑插入 webpack 编译过程”,接下来才是重点 —— 如何影响编译状态?强调一下,webpack 的插件体系与平常所见的 订阅/发布 模式差别很大,是一种非常强耦合的设计,hooks 回调由 webpack 决定何时,以何种方式执行;而在 hooks 回调内部可以通过修改状态、调用上下文 api 等方式对 webpack 产生 side effect

比如,EntryPlugin 插件:

class EntryPlugin {

apply(compiler) {

compiler.hooks.compilation.tap(

“EntryPlugin”,

(compilation, { normalModuleFactory }) => {

compilation.dependencyFactories.set(

EntryDependency,

normalModuleFactory

);

}

);

compiler.hooks.make.tapAsync(“EntryPlugin”, (compilation, callback) => {

const { entry, options, context } = this;

const dep = EntryPlugin.createDependency(entry, options);

compilation.addEntry(context, dep, options, (err) => {

callback(err);

});

});

}

}

上述代码片段调用了两个影响 compilation 对象状态的接口:

  • compilation.dependencyFactories.set

  • compilation.addEntry

操作的具体含义可以先忽略,这里要理解的重点是,webpack 会将上下文信息以参数或 this (compiler 对象) 形式传递给钩子回调,在回调中可以调用上下文对象的方法或者直接修改上下文对象属性的方式,对原定的流程产生 side effect。所以想纯熟地编写插件,除了要理解调用时机,还需要了解我们可以用哪一些api,例如:

  • compilation.addModule:添加模块,可以在原有的 module 构建规则之外,添加自定义模块

  • compilation.emitAsset:直译是“提交资产”,功能可以理解将内容写入到特定路径

  • compilation.addEntry:添加入口,功能上与直接定义 entry 配置相同

  • module.addError:添加编译错误信息

Loader 介绍

=========

结尾

正式学习前端大概 3 年多了,很早就想整理这个书单了,因为常常会有朋友问,前端该如何学习,学习前端该看哪些书,我就讲讲我学习的道路中看的一些书,虽然整理的书不多,但是每一本都是那种看一本就秒不绝口的感觉。

以下大部分是我看过的,或者说身边的人推荐的书籍,每一本我都有些相关的推荐语,如果你有看到更好的书欢迎推荐呀。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

前端学习书籍导图-1

片段调用了两个影响 compilation 对象状态的接口:

  • compilation.dependencyFactories.set

  • compilation.addEntry

操作的具体含义可以先忽略,这里要理解的重点是,webpack 会将上下文信息以参数或 this (compiler 对象) 形式传递给钩子回调,在回调中可以调用上下文对象的方法或者直接修改上下文对象属性的方式,对原定的流程产生 side effect。所以想纯熟地编写插件,除了要理解调用时机,还需要了解我们可以用哪一些api,例如:

  • compilation.addModule:添加模块,可以在原有的 module 构建规则之外,添加自定义模块

  • compilation.emitAsset:直译是“提交资产”,功能可以理解将内容写入到特定路径

  • compilation.addEntry:添加入口,功能上与直接定义 entry 配置相同

  • module.addError:添加编译错误信息

Loader 介绍

=========

结尾

正式学习前端大概 3 年多了,很早就想整理这个书单了,因为常常会有朋友问,前端该如何学习,学习前端该看哪些书,我就讲讲我学习的道路中看的一些书,虽然整理的书不多,但是每一本都是那种看一本就秒不绝口的感觉。

以下大部分是我看过的,或者说身边的人推荐的书籍,每一本我都有些相关的推荐语,如果你有看到更好的书欢迎推荐呀。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

[外链图片转存中…(img-oqR7fSTo-1715717834094)]

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值