Jest + React Testing Library 单测总结

大家好,我是若川。持续组织了6个月源码共读活动,感兴趣的可以点此加我微信 ruochuan02 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列

1、背景

以前还是学生的时候,有学习一门与测试相关的课程。那个时候,觉得测试就是写 test case,写断言,跑测试,以及查看 test case 的 coverage。整个流程和写法也不是特别难,所以就理所当然地觉得,写测试也不是特别难。

加上之前实际的工作中,也没有太多的写测试的经历,所以当自己需要对组件库补充单元测试的时候,发现并不能照葫芦画瓢来写单测。一时不知道该如何下手,也不知道如何编写有效的单测,人有点懵,于是就比较粗略地研究了一下前端组件单测。

1.1 单测的目的

  • 在频繁的需求变动中可控地保障代码变动的影响范围

  • 提升代码质量和开发测试效率

  • 保证代码的整洁清晰

  • ......

总之单测是一个保证产品质量的非常强大的手段。

1.2 测试框架和 UI 组件测试工具

而说起前端的测试框架和工具,比较主流的 JavaScript 测试框架有 Jest、Jasmine、Mocha 等等,并且还有一些 UI 组件测试工具,比如 testing-libraray,enzyme 等等。

测试框架和 UI 组件测试工具之间并不是相互依赖、非此即彼的,而是可以根据不同工具的性质做不同的搭配。目前腾讯课堂基于 Tdesign 开发的素材库组件的单测,就是使用 Jest + React Testing Library 来完成。

1.3 组件单测须知

在开始进行组件单测的时候,有几个因素我们需要考虑:

  • 组件是否按照既定的条件 / 逻辑进行渲染

  • 组件的事件回调是否正确

  • 异步接口如何校验

  • 异步执行完毕后的操作如何校验

  • ......

当然不止这些列举出来的,根据不同的业务场景,我们考虑的因素需要更全面更细致。

2、Jest 的使用

Jest 的安装这里就不赘述了,如果使用 create-react-app 来创建项目,Jest 和 React Testing Library(RTL) 都已经默认安装了。

如果想要看如何安装 Jest,可以参考:Jest 上手。

Jest 常用的配置项在根目录中的 jest.config.js 中,常用的配置可以参考:Jest 配置文件。

2.1 Jest 基础 API

Jest 的最基础,最常用的三个 API 是:describe、test 和 expect。

  • describe 是 test suite(测试套件)

  • test (也可以写成 it) 是 test case(测试用例)

  • expect 是断言

import aFunction from'./function.js';// 假设 aFunction 读取一个 bool 参数,并返回该 bool 参数
describe('a example test suite', () => {test('function return true', () => {expect(aFunction(true)).toBe(true);// 测试通过});test('function return false', () => {expect(aFunction(false)).toBe(false);// 测试通过});
});

通过运行 npm run jest (运行所有的 test suite 和 test case,以及断言),或者 npm run jest -t somefile.test.tsx(运行指定文件中的测试用例),就可以得到测试结果,如:

073ef1eed299fa624a6a579b190fe17e.png

当然,如果想要看到覆盖率的报告,可以使用 jest --coverage,或者 jest-report

在 VS Code 中,我们也可以安装插件:Jest Runner

aa9bc7b6efb6876de11b7a7a5eafc48d.png

在代码中,就可以快速跑测试用例,可以说非常的方便了。

617c5603afb2be7fe65ab6bd031940e5.png

如果在使用 Jest runner 的时候出现 Node.js 相关的报错,可以查看一下当前 Node.js 的使用版本,切换到 14.17.0 版本即可。

39e55ac7e5170c8145753f523352c022.png

2.2 Jest 匹配器

Jest 匹配器是在 expect 断言时,用来检查值是否满足一定的条件。例如上面的例子中:

expect(aFunction(true)).toBe(true)

其中 toBe () 就是用来比较 aFunction (true) 的值是否为 true。

完整的 Jest 匹配器可以在 这里 查看,下面也列举一些常用的匹配器:

匹配器说明
.toBe(value)相等性,检查规则为 === + Object.is
.toEqual(value)相等性,递归对比对象字段
.toBeInstanceOf(Class)检查是否属于某一个 Class 的 instance
.toHaveProperty(keyPath, value)检查断言中的对象是否包含 keyPath 字段,或可以检查该对象的值是否等于 value
.toBeGreaterThan(number)大于 number
.toBeGreaterThanOrEqual(number)大于等于 number
.toBeNaN()值是否是 NaN
.toMatch(regexp or String)字符串的相等性,可以填入 string 或者一个正则
.toContain(item)substring
.toHaveLength(number)字符串长度

其实在 Testing Library 库中,还提供了一些匹配器专门用来测试前端组件,这些扩展的匹配器会让前端组件的测试变得更灵活。除了前端组件的匹配器,一些扩展库也依据不同的测试场景衍生出了很多其他的匹配器。

2.3 Jest Mock

在查看官方文档的时候,Jest 匹配器中还有一类匹配器专门用来检查 Jest Mock 函数的。在组件单测中,有的时候我们可能只关注一个函数是否被正确地调用了,或者只想要某个函数的返回值来支持该组件渲染逻辑是否正确,而并不关心这个函数本身的逻辑。正如官方文档中强调的那样:

Testing Library encourages you to avoid testing implementation details like the internals of a component you're testing.

测试库鼓励您避免测试实现细节,例如您正在测试的组件的内部结构。

所以,Jest Mock 的意义就在于可以帮助我们完成下面这些事情:

  1. 有些模块可能在测试环境中不能很好地工作,或者对测试本身不是很重要,使用虚拟数据来 mock 这些模块,可以使你为代码编写测试变得更容易;

  2. 如果不想在测试中加载这个组件,我们可以将依赖 mock 到一个虚拟组件;

  3. 测试组件处于不同状态下的表现;

  4. mock 一些子组件,可以帮助减小快照的大小,并使它们在代码评审中保持可读性;

  5. ......

Jest Mock 的常用 API 是:jest.fn () 和 jest.mock ()。

2.3.1 jest.fn()

通过 jest.fn(implementation) 可以创建 mock 函数。如果没有定义函数内部的实现,mock 函数会返回 undefined。

// 定义一个 mock 的函数,因为没有函数体,所以 mockFn 会 return undefined
const mockFn = jest.fn();// mockFn 调用
mockFn();
// 虽然没有定义函数体,但是 mockFn 被调用过了
expect(mockFn).toHaveBeenCalled();const res = mockFn('a','b','c');// 断言 mockFn 的执行后返回 undefined
expect(res).toBeUndefined();// 断言mockFn被调用了两次
expect(mockFn).toBeCalledTimes(2);// 断言mockFn传入的参数为a,b,c
expect(mockFn).toHaveBeenCalledWith('a','b','c');// 定义implementation,自定义函数体:
const returnsTrue = jest.fn(() =>true); // 定义了函数体
console.log(returnsTrue()); // true// 可以给mock的函数设置返回值
const returnSomething = jest.fn().mockReturnValue('hello world'); 
expect(returnSomething()).toBe('hello world');// mock也可以返回一个Promise
const promiseFn = jest.fn().mockResolvedValue('hello promise');
const promiseRes = await promiseFn();
expect(promiseRes).toBe('hello promise');

2.3.2 jest.mock(moduleName, factory, options)

jest.mock() 可以帮助我们去 mock 一些 ajax 请求,作为前端只需要去确认这个异步请求发送成功就好了,至于后端接口返回什么内容我们就不关注了,这是后端自动化测试要做的事情。

// users.js 获取所有user信息
import axios from'axios';class Users {staticall() {return axios.get('.../users.json').then(resp => resp.data);}
}exportdefault Users;
// user.test.js
import axios from'axios';
import Users from'./users';jest.mock('axios');test('should fetch users', () => {const users = [{name: 'Bob'}];const resp = {data: users};axios.get.mockResolvedValue(resp);// or you could use the following depending on your use case:// axios.get.mockImplementation(() => Promise.resolve(resp))return Users.all().then(data => expect(data).toEqual(users));
});

2.3.3 Jest Mock 的匹配器

Jest 匹配器中还有一类匹配器专门用来检查 jest mock() 的,比如:

  • 名字

    • mockFn.mockName(value)

    • mockFn.getMockName()

  • 运行情况

    • mockFn.mock.calls:传的参数

    • mockFn.mock.results:得到的返回值

    • mockFn.mock.instances:mock 包装器实例

  • 模拟函数

    • mockFn.mockImplementation(fn):重新声明被 mock 的函数

    • mockFn.mockImplementationOnce(fn)

  • 模拟结果

    • mockFn.mockReturnThis()

    • mockFn.mockReturnValue(value)

    • mockFn.mockReturnValueOnce(value)

    • mockFn.mockResolvedValue(value)

    • mockFn.mockResolvedValueOnce(value)

    • mockFn.mockRejectedValue(value)

    • mockFn.mockRejectedValueOnce(value)

2.4 Jest 的扩展阅读材料

  • Jest 学习指南

  • 那些年错过的 React 组件单元测试

  • 使用 Jest 测试 JavaScript (Mock 篇)

3、React Testing Library

testing library 是一个测试 React 组件的测试库,它的核心理念就是:

The more your tests resemble the way your software is used, the more confidence they can give you.

测试越类似于软件使用方式,就越能给测试信心。

3.1 render & debug

在测试用例中渲染内容,可以使用 RTL 库中的 render,render 函数可以为我们在测试用例中渲染 React 组件。

被渲染的组件,可以通过 debug 函数或者 screen 的 debug 函数在控制台输出组件的 HTML 结构。例如下面的 Dropdown 组件的例子:

import { render, screen } from '@testing-library/react';
import Dropdown from '../index'; // 要测试的组件describe('dropdown test', () => {it('render Dropdown', () => {// 渲染 Dropdown 组件const comp = render(<Dropdown />);comp.debug();screen.debug();// 这两种都可以打印出来渲染组件的结构});
});

其实,在我们编写组件测试用例时,都可以通过 debug 函数把组件渲染结果打印出来,这可以提高我们编写用例时的效率,同时,这一特点也很符合 RTL 的设计观念。

3.2 screen

在上面的例子中,其实我们也使用到了库中的 screen。screen 为测试用例提供了一个全局 DOM 环境,通过这个环境,我们就可以去使用库中提供的不同函数去定位元素,定位后的元素可以用于断言判断或者用户交互。

3.3 定位元素

3.3.1 Query 类型

定位元素的方法在 RTL 中称为 Query,Query 帮助我们去找到页面上的元素。RTL 提供了三种 Query 的类型:"get", "find", "query"。

Query 类型未找到元素找到 1 个元素找到多个元素Retry (Async/Await)
Single Element



getBy...Throw errorReturn elementThrow errorNo
queryBy...Return nullReturn elementThrow errorNo
findBy...Throw errorReturn elementThrow errorYes
Multiple Elements



getAllBy...Throw errorReturn arrayReturn arrayNo
queryAllBy...Return []Return arrayReturn arrayNo
findAllBy...Throw errorReturn arrayReturn arrayYes

从上面的表格可以看出来,定位的方法在找单个元素时和多个元素时会做了一些区别,比如 getBy... 如果找到了多个元素就会 throw error,这时就需要使用 getAllBy...。

get 和 query 的区别主要是在未找到元素时,queryBy 会返回 null,这对于我们测试一个元素是否存在时非常有帮助。

而 findby 的作用主要用于那些最终会显示在页面当中的异步元素。

3.3.2 Query 内容

那么,getBy...、queryBy... 和 findBy... 后面具体可以查询什么内容呢?

  • 主要

    • ByLabelText:用于表单的 label

    • ByPlaceholderText:用于表单

    • ByText:查询 TextNode

    • ByDisplayValue:输入框等当前值

  • 语义

    • ByAltText:img 的 alt 属性

    • ByTitle:title 属性或元素

    • ByRole:ARIA role,可以定位到辅助树中的元素

  • Id

    • getByTestId:函数需要在源代码中添加 data-testid 属性才能使用

一般而言,getByText 和 getByRole 应该是元素的首选定位类型。

import { render, screen } from'@testing-library/react';
import Dropdown from'../index'; // 要测试的组件const propsRender = {commonStyle: {},data: {btnTheme: 'default',btnVariant: 'text',btnText: 'test', // 给 dropdown 的 button 设置文字 'test'trigger: 'click',},style: {},meta: {previewMode: true,isEditor: false},on: jest.fn(),off: jest.fn(),emit: jest.fn(), 
};describe('dropdown test', () => {it('render Dropdown', () => {// 渲染 Dropdown 组件const comp = render(<Dropdown />);// 使用 queryByText("test") 定位这个 button 的文字内容,然后使用断言+匹配做测试expect(screen.queryByText("test")).toBeInTheDocument();});
});

findBy 的使用方法

假如在 Component 组件中定义一行文字 “hello world” 和一个定时器,在组件渲染 3 秒后再显示这行字。

describe('test hello world', () => {test('renders component', async () => {render(<Component />);// 在组件的初始化渲染中,我们在 HTML 中无法通过 queryBy 找到 “hello world”,因为它三秒后才能出现expect(screen.queryByText(/hello world/)).toBeNull();// await 一个新的元素被找到,并且最终确实被找到当 promise resolves 并且组件重新渲染之后。expect(await screen.findByText(/hello world/)).toBeInTheDocument();});
});

对于任何开始不显示、但迟早会显示的元素,要使用 findBy。如果你想要验证一个元素不在页面中,使用 queryBy,否则默认使用 getBy。

RTL 所有定位方法可 点击 查看。

3.4 RTL + Jest 匹配器

在 2.2 Jest 匹配器 中可以看到 Jest 提供了一些匹配器,然而 Jest 自己提供的匹配器很难去实现组件测试的一些特殊条件,所以 RTL 自己实现了一个 Jest 匹配器的扩展包:jest-dom

  • Custom matchers

    • toBeDisabled

    • toBeEnabled

    • toBeEmptyDOMElement

    • toBeInTheDocument

    • toBeInvalid

    • toBeRequired

    • toBeValid

    • toBeVisible

    • toContainElement

    • toContainHTML

    • toHaveAccessibleDescription

    • toHaveAccessibleName

    • toHaveAttribute

    • toHaveClass

    • toHaveFocus

    • toHaveFormValues

    • toHaveStyle

    • toHaveTextContent

    • toHaveValue

    • toHaveDisplayValue

    • toBeChecked

    • toBePartiallyChecked

    • toHaveErrorMessage

  • Deprecated matchers

    • toBeEmpty

    • toBeInTheDOM

    • toHaveDescription

3.5 事件:FireEvent

实际的用户交互可以通过 RTL 的 fireEvent 函数去模拟。

fireEvent(node: HTMLElement, event: Event)
fireEvent[eventName](node: HTMLElement, eventProperties: Object)// <button>Submit</button>
fireEvent(getByText(container, 'Submit'),new MouseEvent('click', {bubbles: true,cancelable: true,}),
);// 两种写法
fireEvent(element, new MouseEvent('click', options?));
fireEvent.click(element, options?);

fireEvent 函数需要两个参数,一个参数是定位的元素 node,另一个参数是 event。这个例子中就模拟了用户点击了 button,同时 fireEvent 有两种写法。

事件 options 描述

属性 / 方法描述
bubbles返回特定事件是否为冒泡事件。
cancelBubble设置或返回事件是否应该向上层级进行传播。
cancelable返回事件是否可以阻止其默认操作。
composed指示该事件是否可以从 Shadow DOM 传递到一般的 DOM。
composedPath()返回事件的路径。
createEvent()创建新事件。
currentTarget返回其事件侦听器触发事件的元素。
defaultPrevented返回是否为事件调用 preventDefault () 方法。
eventPhase返回当前正在评估事件流处于哪个阶段。
isTrusted返回事件是否受信任。
target返回触发事件的元素。
timeStamp返回创建事件的时间(相对于纪元的毫秒数)。
type返回事件名称。

常用 fireEvent:

键盘:

  • keyDown

  • keyPress

  • keyUp

聚焦:

  • focus

  • blur

表单:

  • change

  • input

  • invalid

  • submit

  • reset

鼠标:

  • click

  • dblClick

  • drag

fireEvent API 列表可 点击 查看。

4、写在最后

测试在整个需求开发的流程中起着重要作用,它对于需求产品的质量提供了强而有力的保障。但是在实际的工作中,产品的迭代、需求的变更以及各种不确定的因素,我们经常会陷入“bug的轮回” —— 关上一个bug,点亮另一个bug。

随着业务复杂度的提升,测试的人力成本也会越来越高。面对这些痛点,作为“懒而聪明”的前端开发,我也常常在思考有什么方法可以在解放双(ren)手(li)的同时,又能保证产品的质量,也不必在每次需求上线时紧张兮兮地盯着告警看板,生怕发的版本影响了其他的功能。所以,我相信借助于测试的力量,这些痛点终有一天会逐个击破。

就像开头提到的,本文只是“比较粗略”地浏览了 Jest + RTL,相较于整个前端单测来说只是冰山一角。希望在日后工作的每一天能不断地探索这个领域,也希望在不久的将来,我也能 “快乐编码,自信发布”。

c70e7e99d82633433f6d5094c2400556.gif

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动,帮助3000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

9e7fe2d0397d8e301efe4f10ad695d3e.png

识别方二维码加我微信、拉你进源码共读

今日话题

略。分享、收藏、点赞、在看我的文章就是对我最大的支持~

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

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

相关文章

着迷英语900句_字体令人着迷

着迷英语900句I’m crazy about fonts. My favorite part of any text editing software is the drop down menu for picking fonts. When I look at any text, I try to identify the font. Roboto is my favorite font.我为字体疯狂。 在任何文本编辑软件中&#xff0c;我最喜…

推荐一个大佬,文章适合偷偷读!

大家好&#xff0c;我是若川。周末愉快。也许你看到这篇文章是周一的上午~我不得不推荐一位大佬给你&#xff01;这位大佬的文章很硬&#xff0c;却一直在「抱怨没有粉丝&#xff0c;没人愿意分享」我去读了读&#xff0c;尼玛这个「谁TM敢分享啊」&#xff0c;文章太「违规」了…

PERFORMANCE-MONITORING(转)

Performance-Monitoring 是Intel提供的可以监测统计CPU内部所产生事件的一组方法。在Intel的手册上介绍了两类CPU事件监测方法&#xff1a;architectural performance monitoring 和 non-architectural performance monitoring。Architectural performance monitoring与平台&am…

ux设计_为企业UX设计更好的数据表

ux设计重点 (Top highlight)If you have worked on enterprise products, you must have noticed the use of lots of data tables. Therefore, I am writing this article to collect the most common use cases and discuss how elegantly we can handle them.如果您使用过企…

狼叔直播 Reaction《学习指北:Node.js 2022 全解析》

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan02 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列本文是…

figma下载_Figma中的高级图像处理

figma下载Figma is not exactly suited for image manipulation, and that’s completely fine. While it does provide an ample amount of tools that let you apply some basic changes to your raster images, for anything more complex you need to look someplace else.…

指针和指针的指针_网络上的iPad指针

指针和指针的指针a week ago I saw a new IPad Pointer presentation and was very excited about what they did. It was very interesting to see how they design different pointer modes and attention to details. Here is the presentation:一周前&#xff0c;我看到了一…

Vue 是如何用 Rollup 打包的?

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列Rollu…

leetcode 207课程表

class Solution { public:bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {//验证是否为DAG&#xff0c;每次验证指向的是否已经存在于当前图中//建图vector<int> indegree(numCourses,0);//入度vector<vector<int>> …

sketch怎么传到ps_2020年从Sketch移植到Figma的详细指南

sketch怎么传到psAs we’re locked up in our homes due to COVID-19 pandemic, many of us are working remotely and Figma is a go-to tool for designers for the same.由于COVID-19流行病使我们被关在家里&#xff0c;我们中的许多人都在远程工作&#xff0c;而Figma是设计…

还没搭建过Vue3.x项目?几行代码搞定~

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列相信现…

一步步创建 边栏 Gadget(二)

相信使用上篇中创建的边栏Gadget之后&#xff0c;大家会很郁闷。难道视频窗口就那么小吗&#xff1f;看起来真费劲。我能通过该Gadget看着一部电视剧。而不能够定制自己需要的或者想要看的电视剧。 在上一篇一步步创建 边栏 Gadget&#xff08;一&#xff09;中&#xff0c;我们…

tableau 自定义图表_一种新的十六进制美国地图布局的案例-Tableau中的自定义图表

tableau 自定义图表For whatever reason, 无论出于什么原因 maps are cool. Even though the earth has mostly been the same since those 地图很酷 。 即使自Pangaea days, we humans make and remake maps constantly. It might be that old maps remind us of how things …

2022,前端工具链十年盘点

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列2021 …

书籍排版学习心得_为什么排版是您可以学习的最佳技能

书籍排版学习心得重点 (Top highlight)I was introduced to design in a serpentine fashion. I don’t have any formal training. Instead, I’ve learned everything through the Web, books, and by interacting with designers daily.我被介绍为蛇形设计。 我没有任何正规…

若川的 2021 年度总结,弹指之间

1前言从2014年开始&#xff0c;每一年都会写年度总结&#xff0c;已经坚持了7个年头。7年的光阴就是弹指之间&#xff0c;转瞬即逝。正如孔子所说&#xff1a;逝者如斯夫&#xff0c;不舍昼夜。回顾2014&#xff0c;约定2015&#xff08;QQ空间日志&#xff09;2015年总结&…

线框图用什么软件_为什么要在线框中着色?

线框图用什么软件I was recently involved in a debate around why some wireframes (which were definitely not UI screens) were not 100% greyscale. This got me thinking — when is it ok to use colour in wireframes, and when is it going to cause you problems fur…

Linux 内核

Linux 内核是一个庞大而复杂的操作系统的核心&#xff0c;不过尽管庞大&#xff0c;但是却采用子系统和分层的概念很好地进行了组织。通过本专题&#xff0c;我们可以学习 Linux 的分层架构、内核配置和编译、内核性能调试和 Linux 2.6 中的许多提升功能。Linux 内核组成 Linux…

给asterisk写app供CLI调用

环境&#xff1a;CentOS6.2 Asterisk 1.8.7.1 一、添加源文件 复制app_verbose.c为app_testApp.c 复制app_verbose.exports为app_testApp.exports 主要是修改一些标识&#xff0c;编译不会出错就行&#xff0c;这里列出我进行的主要修改。 1、添加头文件 #include "aster…

前端,校招,面淘宝,指南

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列虽然是…