Webpack源码浅析

webpack启动方式

webpack有两种启动方式:

  1. 通过webpack-cli脚手架来启动,即可以在Terminal终端直接运行;
    webpack ./debug/index.js --config ./debug/webpack.config.js
    
  2. 通过require('webpack')引入包的方式执行;其实第一种方式最终还是会用require的方式来启动webpack,可以查看./bin/webpack.js文件

webpack编译的起点

const compiler = webpack(config)开始

webpack函数源码(./lib/webpack.js):

const webpack = (options, callback) => {let compiler = createCompiler(options)// 如果传入callback函数,则自启动if(callback){compiler.run((err, states) => {compiler.close((err2)=>{callbacl(err || err2, states)})})}return compiler
}

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

  • Compiler类(./lib/Compiler.js):webpack的主要引擎,在compiler对象记录了完整的webpack环境信息,在webpack从启动到结束,compiler只会生成一次。你可以在compiler对象上读取到webpack config信息,outputPath等;
  • Compilation类(./lib/Compilation.js):代表了一次单一的版本构建和生成资源。compilation编译作业可以多次执行,比如webpack工作在watch模式下,每次监测到源文件发生变化时,都会重新实例化一个compilation对象。一个compilation对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。

两者的区别?
compiler代表的是不变的webpack环境; compilation代表的是一次编译作业,每一次的编译都可能不同;

举个例子:
compiler就像一条手机生产流水线,通上电后它就可以开始工作,等待生产手机的指令; compliation就像是生产一部手机,生产的过程基本一致,但生产的手机可能是小米手机也可能是魅族手机。物料不同,产出也不同。


Compiler类在函数createCompiler中实例化(./lib/index.js):

const createCompiler = options => {const compiler = new Compiler(options.context)// 注册所有的自定义插件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)  // process中注册所有webpack内置的插件return compiler
}

 Compiler类实例化后,如果webpack函数接收了回调callback,则直接执行compiler.run()方法,那么webpack自动开启编译之旅。如果未指定callback回调,需要用户自己调用run方法来启动编译。

process(options, compiler)

WebpackOptionsApply类的工作就是对webpack options进行初始化。 打开源码文件lib/WebpackOptionsApply.js,你会发现前五十行都是各种webpack内置的Plugin的引入,那么可以猜想process方法应该是各种各样的new SomePlugin().apply()的操作,事实就是如此。

compiler.run()

先贴上源码吧(./lib/Compiler.js):

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) returnconst stats = new Stats(compilation);this.hooks.done.callAsync(stats, err => {if(err) returncallback(err, stats)this.hooks.afterDone.call(stats)})}this.hooks.beforeRun.callAsync(this, err => {if(err) returnthis.hooks.run.callAsync(this, err => {if(err) returnthis.compile(onCompiled)})})}
}

通读一遍run函数过程,你会发现它钩住了编译过程的一些阶段,并在相应阶段去调用已经提前注册好的钩子函数(this.hooks.xxxx.call(this)),效果与React中生命周期函数是一样的。在run函数中出现的钩子有:beforeRun --> run --> done --> afterDone。第三方插件可以钩住不同的生命周期,接收compiler对象,处理不同逻辑。

run函数钩住了webpack编译的前期和后期的阶段,那么中期最为关键的代码编译过程就交给了this.compile()来完成了。在this.comille()中,另一个主角compilation粉墨登场了。

compiler.compile()

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就是我们关心的编译过程。但在这里它仅是一个钩子触发,显然真正的编译执行是注册在这个钩子的回调上面。

webpack因为有Tapable的加持,代码编写非常灵活,node中流行的callback回调机制(说的就是回调地狱),webpack使用的炉火纯青。

this.parser其实就是JavascriptParser的实例对象,最终JavascriptParser会调用第三方包acorn提供的parse方法对JS源代码进行语法解析。

const result = this.parser.parse(source)parse(code, options){// 调用第三方插件`acorn`解析JS模块let ast = acorn.parse(code)// 省略部分代码if (this.hooks.program.call(ast, comments) === undefined) {this.detectStrictMode(ast.body)this.prewalkStatements(ast.body)this.blockPrewalkStatements(ast.body)// 这里webpack会遍历一次ast.body,其中会收集这个模块的所有依赖项,最后写入到`module.dependencies`中this.walkStatements(ast.body)}
}

有个线上小工具 AST explorer 可以在线将JS代码转换为语法树AST,将解析器选择为acorn即可。

通常我们会使用一些类似于babel-loader等 loader 预处理源文件,那么webpack 在这里的parse具体作用是什么呢?parse的最大作用就是收集模块依赖关系,比如调试代码中出现的import {is} from 'object-is'const xxx = require('XXX')的模块引入语句,webpack会记录下这些依赖项,记录在module.dependencies数组中。

compilation.seal()

至此,从入口文件开始,webpack收集完整了该模块的信息和依赖项,接下来就是如何进一步打包封装模块了。

compiler.hooks.emit.callAsync()

在seal执行后,关于模块所有信息以及打包后源码信息都存在内存中,是时候将它们输出为文件了。接下来就是一连串的callback回调,最后我们到达了compiler.emitAssets方法体中。在compiler.emitAssets中会先调用this.hooks.emit生命周期,之后根据webpack config文件的output配置的path属性,将文件输出到指定的文件夹。至此,你就可以在./debug/dist中查看到调试代码打包后的文件了。

this.hooks.emit.callAsync(compilation, () => {outputPath = compilation.getPath(this.outputPath, {})mkdirp(this.outputFileSystem, outputPath, emitFiles)})

简单总结一下 webpack 编译模块的基本流程:

  1. 调用webpack函数接收config配置信息,并初始化compiler,在此期间会apply所有 webpack 内置的插件;
  2. 调用compiler.run进入模块编译阶段;
  3. 每一次新的编译都会实例化一个compilation对象,记录本次编译的基本信息;
  4. 进入make阶段,即触发compilation.hooks.make钩子,从entry为入口: a. 调用合适的loader对模块源码预处理,转换为标准的JS模块; b. 调用第三方插件acorn对标准JS模块进行分析,收集模块依赖项。同时也会继续递归每个依赖项,收集依赖项的依赖项信息,不断递归下去;最终会得到一颗依赖树🌲;
  5. 最后调用compilation.seal render 模块,整合各个依赖项,最后输出一个或多个chunk

以下为时序图:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/669562.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

zabbix server/agent源码编译成rpm包(通用版-小白教程)

前言 工作环境需要用到很多信创的操作系统,zabbix agent2的官方没有现成的包可用,网上巴拉了一下找到zabbix agent2通用版编译成rpm包的方法 思路:假如当你有一批ky10_x86的机器需要配套的zabbix agent的rpm包,那就找一台ky10_x…

计组学习笔记2024/2/5

记录每天学到了什么,同时在挪移图片过程中再次理解加深印象 学计算机最重要的是理解,而不是整齐的笔记,不要主次搞混,所以以后记笔记的模式也要改一下(主要还是自己太菜,还达不到一边做到整齐笔记的同时还能够有时间做到理解,所以只能舍弃整齐时间保留理解时间)(不过如果有现成…

针对物联网应用优化 Cortex-M0+ 微控制器的功耗消耗”

为了针对物联网应用优化 Cortex-M0 微控制器的功耗消耗,我们可以采取一系列措施,包括优化代码、使用低功耗模式、优化外设配置等。以下是对 Cortex-M0 微控制器功耗消耗优化的详细解释,并提供示例代码以演示如何在物联网应用中优化功耗。 ✅作…

敏捷开发的INVEST原则

很久没来这个社区发点文章了,是因为工作的变动很大。 上一篇文章,我还在讨论专项测试领域,如何在金融投资领域进行测试,如何把控测试管理。 现在我要做的是质量体系建设的咨询,上升的高度和领域发生了变化。 我现在…

centos系统初始配置

centos 7网络配置、主机名配置、修改hosts文件、ssh服务和远程登录。 静态网络配置 主机名配置 ssh服务和远程登陆

Nginx: a little source code

Nginx被称为C程序员必学的源码之一,我觉得名副其实,它的事件机制、内存管理、进程通信都可以说是顶级实践,非常值得学习。 Nginx源码比较多,本文只看几个重要的模块,更详细的内容请参考《深入理解nginx模块开发与架构》…

6.s081 学习实验记录(五)traps

文章目录 一、RISC-V assembly简介问题 二、Backtrace简介注意实验代码实验结果 三、Alarm简介注意实验代码实验结果 一、RISC-V assembly 简介 git checkout traps,切换到traps分支user/call.c 文件在我们输入 make fs.img 之后会被汇编为 call.asm 文件&#xf…

网络原理TCP/IP(5)

文章目录 IP协议IP协议报头地址管理网段划分特殊的IP地址路由选择以太网认识MAC地址对比理解MAC地址和IP地址DNS(域名服务器) IP协议 IP协议主要完成的工作是两方面: 地址管理,使用一套地址体系,来描述互联网上每个设…

day20网页基本标签

网页基本标签 标题标签段落标签换行标签水平线标签字体样式标签注释和特殊符号 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>网页基本标签</title> </head> <body> <!--标题…

CTFshow web(php特性 105-108)

web105 <?php /* # -*- coding: utf-8 -*- # Author: Firebasky # Date: 2020-09-16 11:25:09 # Last Modified by: h1xa # Last Modified time: 2020-09-28 22:34:07 */ highlight_file(__FILE__); include(flag.php); error_reporting(0); $error你还想要flag嘛&…

BVH动画绑骨蒙皮并在Unity上展示

文章目录 Blender绑定骨骼Blender蒙皮Blender中导入bvh文件将FBX导入Unity Blender绑定骨骼 先左上角红框进入model模式&#xff0c;选中要绑定的模型&#xff0c;然后进入Edit模式把骨骼和关节对齐。 &#xff08;选中骨骼&#xff0c;G移动&#xff0c;R旋转&#xff09; 为…

如何使用NimExec通过无文件命令执行实现横向移动

关于NimExec NimExec是一款功能强大的无文件远程命令执行工具&#xff0c;该工具专为红队研究人员设计&#xff0c;使用Nim语言开发&#xff0c;基于服务控制管理器远程协议&#xff08;MS-SCMR&#xff09;实现其功能&#xff0c;可以帮助广大研究人员在目标网络系统中实现横…

【算法与数据结构】583、72、LeetCode两个字符串的删除操作+编辑距离

文章目录 一、583、两个字符串的删除操作二、72、编辑距离三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、583、两个字符串的删除操作 思路分析&#xff1a;本题的思路和115、不同的子序列差不多&#xff0c;只是变成…

【Java EE初阶十】多线程进阶二(CAS等)

1. 关于CAS CAS: 全称Compare and swap&#xff0c;字面意思:”比较并交换“&#xff0c;且比较交换的是寄存器和内存&#xff1b; 一个 CAS 涉及到以下操作&#xff1a; 下面通过语法来进一步进项说明&#xff1a; 下面有一个内存M&#xff0c;和两个寄存器A,B; CAS(M,A,B)&am…

synchronized内部工作原理

作者简介&#xff1a; zoro-1&#xff0c;目前大二&#xff0c;正在学习Java&#xff0c;数据结构&#xff0c;javaee等 作者主页&#xff1a; zoro-1的主页 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f496; synchronized内部工作原理 syn…

矿泉水市场调研:预计2029年将达到83亿美元

矿泉水为国民饮水消费升级的方向&#xff0c;估算我国矿泉水市场规模约472亿元&#xff0c;成长性好。我们按照水种将包装水划分为矿泉水、纯净水、天然水及其他&#xff0c;根据多个第三方数据来源数据&#xff0c;我们估算矿泉水2017年瓶装与桶装合计市场销售规模约472亿元&a…

深入理解指针(3)

⽬录 1. 字符指针变量 2. 数组指针变量 3. ⼆维数组传参的本质 4. 函数指针变量 5. 函数指针数组 6. 转移表 1. 字符指针变量 在指针的类型中我们知道有⼀种指针类型为字符指针 char* ; ⼀般使⽤: int main() {char ch w;char *pc &ch;*pc w;return 0; } 还有…

属性“xxxx”在类型“ArrayConstructor”上不存在。是否需要更改目标库? 请尝试将 “lib” 编译器选项更改为“es2015”或更高版本。

使用vscode编写vue&#xff0c;在使用elementUI时&#xff0c;发现代码中的form报错如下&#xff1a; 属性“form”在类型“ArrayConstructor”上不存在。是否需要更改目标库? 请尝试将 “lib” 编译器选项更改为“es2015”或更高版本。 解决方法&#xff1a; 打开jsconfig.…

TOP100-二叉数

1.94. 二叉树的中序遍历 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3,2]示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[]示例 3&#xff1a; 输入&#xf…

第8章 多线程

8.1 线程概述 人们在日常生活中&#xff0c;很多事情都是可以同时进行的。例如&#xff0c;一个人可以一边听音乐&#xff0c;一边打扫房间&#xff0c;可以一边吃饭&#xff0c;一边看电视。在使用计算机时&#xff0c;很多任务也是可以同时进行的。例如&#xff0c;可以一边…