1. JavaScript模块化简述👦
1.1 为什么需要模块化
- 没有模块化前的项目,常常在一个JS文件中会有很多功能的代码,这使得文件很大,分类性不强,自然而然不易维护;
- 那么我们将一个大的JS文件根据一定的规范拆分成几个小的文件的话将会便于管理,可以提高复用性,随之,可以起到分治的效果;
- 一个复杂的项目肯定有很多相似的功能模块,如果每次都需要重新编写模块肯定既费时又耗力。同样,某个功能别人已经造好了轮子,我们就调来用用就好,这时就要引用别人编写模块,引用的前提是要有统一的「打开姿势」,如果每个人有各自的写法,那么肯定会乱套,所以会引出模块化规范;
- 现在常用的JavaScript模块化规范有四种:
Commonjs
,AMD
,CMD
,ES6模块化
。个人理解,ES6模块化才是主流。
1.2 模块的定义
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据相对而言是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
所以,我们发现学习或建立模块就是抓住两点:如何引入模块?如何暴露模块?
1.3 模块化的定义
编码时是按照模块一个一个编码的, 整个项目就是一个模块化的项目
1.4 模块化的优势
- 方便维护代码,更好的分离,按需加载
- 提高代码复用性
- 降低代码耦合度(降偶)
- 分治思想——模块化不仅仅只是复用,不管你将来是否要复用某段代码,你都有充分的理由将其分治为一个模块。(我们在开发中有时候经常会出现一个模块,实则只用到了一次,但还是抽离出来作为单个独立的模块,这就是分而治之的软件工程的思想,在前端模块化同样适用)
2. 模块化的进化史👺
2.1 全局Function模式
module1.js
(定义一个模块1)
//数据
let data1 = 'module one data'
//操作数据的函数
function foo() {
console.log(`foo() ${data1}`)
}
function bar() {
console.log(`bar() ${data1}`)
}
module2.js
(定义一个模块2)
let data2 = 'module two data';
function foo() { //与模块1中的函数冲突了
console.log(`foo() ${data2}`)
}
test.html
(去使用定义好的模块1和模块2)
//同步引入,若函数冲突,则后面覆盖前面
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript">
foo() //foo() module two data
bar() //bar() module one data
</script>
说明:
- 全局函数模式: 将不同的功能封装成不同的全局函数
- 问题: Global被污染了, 很容易引起命名冲突(比如模块中的
data1
data2
都是全局变量)
2.2 namespace模式
module1.js
(定义一个模块1)
let moduleOne = {
data: 'module one data',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
module2.js
(定义一个模块2)
let moduleTwo = {
data: 'module two data',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
test.html
(去使用定义好的模块1和模块2)
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript">
moduleOne.foo() //foo() module one data
moduleOne.bar() //bar() module one data
moduleTwo.foo() //foo() module two data
moduleTwo.bar() //bar() module two data
moduleOne.data = 'update data' //能直接修改模块内部的数据
moduleOne.foo() //foo() update data
</script>
说明:
- namespace模式: 简单对象封装
- 作用: 减少了全局变量 (如两个模块的
data
都不是全局变量了,而是对象的某一个属性 ) - 问题: 不安全,可以直接修改模块内部的数据
2.3 IIFE模式
module1.js
(定义一个模块1)
(function (window) {
//数据
let data = 'IIFE module data'
//操作数据的函数
function foo() { //用于暴露的函数
console.log(`foo() ${data}`)
}
function bar() {//用于暴露的函数
console.log(`bar() ${data}`)
otherFun() //内部调用
}
function otherFun() { //内部私有的函数
console.log('privateFunction go otherFun()')
}
//暴露foo函数和bar函数
window.moduleOne = {foo, bar}
})(window)
test.html
(去使用定义好的模块1)
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript">
moduleOne.foo() //foo() IIFE module data
moduleOne.bar() //bar() IIFE module data privateFunction go otherFun()
//moduleOne.otherFun() //报错,moduleOne.otherFun is not a function
console.log(moduleOne.data) //undefined 因为我暴露的moduleOne对象中无data
moduleOne.data = 'xxxx' //不是修改的模块内部的data,而是在moduleOne新增data属性
moduleOne.foo() //验证内部的data没有改变 还是会输出 foo() IIFE module data
</script>
说明:
- IIFE模式: 匿名函数自调用(闭包)
- IIFE : immediately-invoked function expression(立即调用函数表达式)
- 作用: 数据是私有的, 外部只能通过暴露的方法操作
- 问题: 如果当前这个模块依赖另一个模块怎么办? 见下面IIFE增强版的(模块依赖于jQuery)
2.4 IIFE模式增强
引入jquery
到项目中
module1.js
(定义一个模块1)
(function (window,$) {
//数据
let data = 'IIFE Strong module data'
//操作数据的函数
function foo() { //用于暴露的函数
console.log(`foo() ${data}`)
$('body').css('background', 'red')
}
function bar() {//用于暴露的函数
console.log(`bar() ${data}`)
otherFun() //内部调用
}
function otherFun() { //内部私有的函数
console.log('privateFunction go otherFun()')
}
//暴露foo函数和bar函数
window.moduleOne = {foo, bar}
})(window,jQuery)
test.html
(去使用定义好的模块1)
<!--引入的js必须有一定顺序-->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript">
moduleOne.foo() //foo() IIFE Strong module data 而且页面背景会变色
</script>
说明:
-
IIFE模式增强 : 引入依赖
-
这就是现代模块实现的基石。其实很像了,有引入和暴露两个方面。
-
存在的问题:一个页面需要引入多个JS的问题
<script type="text/javascript" src="module1.js"></script> <script type="text/javascript" src="module2.js"></script> <script type="text/javascript" src="module3.js"></script> <script type="text/javascript" src="module4.js"></script>
请求过多:一个
script
标签就是一次请求
依赖模糊:看不出来谁依赖着谁?哪些模块是有依赖关系的,很难看出来。
难以维护:内部依赖关系混乱也就难以维护啦
3. 现代模块化方案🏆
3.1 CommonJS
CommonJS
是服务器端模块的规范,Node.js
就是采用了这个规范。但目前也可用于浏览器端,需要使用 Browserify
进行提前编译打包。
CommonJS
模块化的引入方式使用require
; 暴露的方式使用module.exports
或exports
。

CommonJS基于服务器端
-
下载安装
node.js
-
创建项目结构
|-modules |-module1.js |-module2.js |-module3.js |-index.js
-
模块化编码
module1.js
(定义一个模块1)
定义一个没有依赖的模块,此模块用来定义配置常量const newsUrl = 'http://localhost:3000/news'; const commentsUrl = 'http://localhost:3000/comments'; //通过exports暴露出去 exports.newsUrl = newsUrl; exports.commentsUrl = commentsUrl;
module2.js
(定义一个模块2)
定义一个有依赖的模块(这个模块2又依赖模块1,故需要引入模块1),用来模拟发送请求获取数据的一个模块//引入依赖 const m1 = require('./module1'); //定义发送请求的方法 function getNews(url) { console.log('发送请求获取数据,请求地址为:' + url); return 'newsData'; } function getComments(url) { console.log('发送请求获取数据,请求地址为:' + url); return 'commentsData'; } const newsData = getNews(m1.newsUrl); const commentsData = getComments(m1.commentsUrl); //通过module.exports暴露模块 module.exports = { newsData, commentsData }
module3.js
(定义一个模块3)
定义一个模块,用来显示用户数据//定义显示内容的方法 function showData(data) { console.log('要显示的信息:' + data); } //通过module.exports暴露模块 module.exports = showData;
index.js
(主模块,用来启动整个项目)
需要引入所有需要启动的模块const m2 = require('./modules/module2'); const showData = require('./modules/module3'); showData(m2.newsData); showData(m2.commentsData)
结果输出:
发送请求获取数据,请求地址为:http://localhost:3000/news 发送请求获取数据,请求地址为:http://localhost:3000/comments 要显示的信息:newsData 要显示的信息:commentsData
-
通过
node
运行index.js
执行命令:node index.js
CommonJS基于浏览器端
-
创建项目结构
|-dist //打包生成文件的目录 |-src //源码所在的目录 |-module1.js |-module2.js |-module3.js |-index.js //应用主源文件(只需打包主模块) |-index.html //引入dist里面的打包好的js文件,[需要在html文件中引入就是基于浏览器端咯]
-
下载
browserify
全局安装下载:npm install browserify -g
-
定义模块代码
module1.js
定义一个没有依赖的模块,此模块用来定义配置常量//定义配置常量 const newsUrl = 'http://localhost:3000/news'; const commentsUrl = 'http://localhost:3000/comments'; //暴露出去 exports.newsUrl = newsUrl; exports.commentsUrl = commentsUrl;
module2.js
定义一个有依赖的模块(依赖模块1),用来模拟发送请求获取数据的一个模块//引入依赖 const m1 = require('./module1'); //定义发送请求的方法 function getNews(url) { console.log('发送请求获取数据,请求地址为:' + url); return 'newsData'; } function getComments(url) { console.log('发送请求获取数据,请求地址为:' + url); return 'commentsData'; } const newsData = getNews(m1.newsUrl); const commentsData = getComments(m1.commentsUrl); //暴露模块 module.exports = { newsData, commentsData }
module3.js
定义一个模块,用来显示用户数据//定义显示内容的方法 function showData(data) { console.log('要显示的信息:' + data); } //暴露模块 module.exports = showData;
index.js
(应用的主模块JS)
主模块,用来启动整个项目。需要引入所有需要启动的模块。const m2 = require('./module2'); const showData = require('./module3'); showData(m2.newsData); showData(m2.commentsData);
打包处理
index.js
执行命令:browserify src/index.js -o dist/bundle.js
src/index.js
表示就是src
目录下的index
主模块
-o
表示outfile
dist/bundle.js
表示打包处理结果生成到dist/bundle.js
在主页面
index.html
中使用引入:
直接引入主模块就可以了,因为主模块上就有各种依赖,他会自动去解析打包处理。<script type="text/javascript" src="dist/bundle.js"></script>
结果输出:
发送请求获取数据,请求地址为:http://localhost:3000/news 发送请求获取数据,请求地址为:http://localhost:3000/comments 要显示的信息:newsData 要显示的信息:commentsData
如果直接引用未打包处理的
index.js
则会报错:引入方式:<script src="src/index.js"></script> 报错信息为:Uncaught ReferenceError: require is not defined-->
我们现在是基于浏览器端的使用。只有在
node
环境下才可以直接使用未打包的index.js
引入,因为在node
环境下有exports
,modular
,require
这些全局方法。node
函数中是这样的:function (exports, require, module, filename, dirname) {}
,所以我们引入一个browserify
就会自动配置好这些参数。
彻底说明白module.exports
和exports
的区别:
在nodejs
中,module
是一个全局变量,类似于在浏览器端的window
也是一个全局变量一样的道理。
module.exports
初始的时候置为{}
, exports
也指向这个空对象。
内部的代码实现是:
var module = {
id: 'xxxx', // 我总得知道怎么去找到他吧
exports: {}, // exports 就是个空对象
}
var exports = module.exports; //exports是对module.exports的引用
//也就是exports现在指向的内存地址和module.exports指向的内存地址是一样的
上面的代码可以看出我们平常使用的exports
是对module.exports
的一个引用,两者都是指向同一个对象。
用一句话来说明就是,模块的require
(引入)能看到的只有module.exports
这个对象,它是看不到exports
对象的,而我们在编写模块时用到的exports
对象实际上只是对module.exports
的引用。(exports = module.exports
)。
我们可以使用exports.a = ‘xxx’
或 exports.b = function () {}
添加方法或属性,本质上它也添加在module.exports
所指向的对象身上。但是你不能直接exports = { a: 'xxx'}
这样子的意义就是将exports
重新指向新的对象!它和module.exports
就不是指向同一个对象,也就这两者已经失去了关系,而nodejs
中require
(引入)能看到的是module.exports
指向的对象。

module.exports
。再举例说明两者区别:
function foo() {
console.log('foo');
}
function bar() {
console.log('bar');
}
想要将这两个函数暴露出去,可以直接使用exports
exports.foo = foo;
exports.bar = bar;
也可以对module.exports
赋值
module.exports = {
foo: foo,
bar: bar
}
但是不能直接对exports
赋值
// 错误
exports = {
foo: foo,
bar: bar
}
因为这样做仅仅改变了exports
的引用,而不改变module.exports
。
好,剧终。这个问题讲明白了吧。
总结CommonJS
特点:同步加载,有缓存
用法:(抓住引入和暴露)
- 暴露模块
exports
module.exports
- 引入模块
require(路径参数)
路径: 自定义模块:路径必须以./
或者../
开头
第三方模块/内置模块/核心模块:路径直接使用模块名称
主要是在服务器端使用的,但是也能在浏览器端运行,需要借助browserify
进行编译。
3.2 AMD
CommonJS
规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。由于Node.js
主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,所以同步加载没有问题。但是如果是浏览器端,同步加载很容易阻塞,这时候AMD规范就出来了。AMD规范则是非同步加载模块,允许指定回调函数。故浏览器端一般会使用AMD规范。
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出 。

-
下载
require.js
, 并引入官网: http://www.requirejs.cn/
github : https://github.com/requirejs/requirejs
将require.js
导入项目: js/libs/require.js -
创建项目结构
|-libs |-require.js |-modules |-alerter.js |-dataService.js |-main.js |-index.html
-
定义
require.js
的模块代码
dataService.js
(定义一个无依赖的模块)define(function () { let msg = 'hello world lyuya'; function dataServer() { return msg.toUpperCase(); } //暴露这个模块 return dataServer; });
alerter.js
(定义一个有依赖的模块)
定义方法:define(['模块1', '模块2', '模块3'], function (m1, m2,m3) {})
注意前后一一对应//一定要注意一一对应,前面有,后面一定要有,别忘记后面的传参 define(['dataServer'],function (dataServer) { let msg = dataServer(); function alerter() { alert(msg); } return alerter; });
-
应用主(入口):
main.js
(主模块)//配置模块的路径 requirejs.config({ baseUrl:'./', //配置所有引入模块的公共路径(基本路径) //模块标识名与模块路径映射 paths : { // 模块名称(一定要与引入的模块名称一一对应): 模块的路径 dataServer: 'modular/dataServer', //一定不能写文件的后缀名,它会自动补全 alerter: 'modular/alerter', //库/框架自己实现模块化的功能,定义了暴露模块的名称 jquery: 'libs/jquery-1.10.1' } }) //主模块,下面requirejs可以用require代替,require是异步可缓存的 requirejs(['alerter','jquery'],function (alerter,$) { alerter(); $('body').css('background','pink') });
-
在页面
index.html
中使用模块<!--src引入requirejs模块去用这个模块解析主模块--> <script data-main="./main" src="./libs/require.js"></script>
总结requireJS
特点:异步加载,有缓存
用法:(抓住引入和暴露)
- 暴露模块
在模块内部使用return
- 定义模块
define(['模块名'], function (模块暴露内容) {})
require(['模块名'], function (模块暴露内容) {})
在模块内部可以使用require
定义异步模块 - 主模块:
requirejs.config({}) 配置使用的模块路径
requirejs(['模块名'], function (模块暴露内容) {})
- html文件引入script标签
<script data-main='app.js' src='require.js'></script>
AMD(通用模块定义)主要是在浏览器使用的。
3.3 CMD
CMD是根据CommonJS和AMD基础上提出的。
CMD(通用模块定义)和AMD(异步模块定)是比较相似的。
RequireJS 遵循的是 AMD(异步模块定义)规范,SeaJS 遵循的是 CMD (通用模块定义)规范。
seaJS
是国人阿里建立的,代表着海纳百川之意。

-
下载
sea.js
, 并引入官网: http://seajs.org/
github : https://github.com/seajs/seajs
将sea.js
导入项目: libs/sea.js -
创建项目结构
|-libs |-sea.js |-modules |-module1.js |-module2.js |-module3.js |-module4.js |-main.js |-index.html
-
定义
sea.js
的模块代码
module1.js
define(function (require, exports, module) { /* require: 引入依赖模块 exports: 暴露模块 module: 暴露模块 */ const msg = 'moduleone'; function getMsg() { console.log('module1 getMsg() ' + msg); return msg; } //暴露模块 module.exports = getMsg; })
module2.js
define(function (require, exports, module) { exports.msg1 = 'lyuya'; exports.msg2 = 'hello'; })
module3.js
define(function (require, exports, module) { //同步引入模块 const getMsg = require('./module1'); let msg = getMsg(); msg = msg.toUpperCase(); module.exports = { msg } })
module4.js
//异步引入模块 require.async('./module2', function (m2) { console.log(m2.msg1, m2.msg2); }) console.log('module4执行了~~~'); })
main.js
:主(入口)模块define(function (require) { const m3 = require('./module3'); require('./module4'); console.log(m3.msg); })
index.html:
<script type="text/javascript" src="libs/sea.js"></script> <script type="text/javascript"> seajs.use('./modules/main') </script>
结果输出:
module1 getMsg() moduleone =====module1.js:12 module4执行了~~~ =====module4.js:9 MODULEONE =====main.js:9 lyuya hello =====module4.js:7
总结seaJS
特点:异步加载,有缓存
用法:
-
定义模块
define(function (require, exports, module) {})
-
引入模块
同步加载require()
异步加载require.async(['模块名'], function (模块暴露内容) {})
-
暴露模块
exports
module.exports
-
html文件引入script标签
<script src='sea.js'></script>
<script>seajs.use('app.js')</script>
seajs
和requirejs
一样主要在浏览器中使用。其实这两个一般都很少使用。用的比较多的是commonjs
和马上要介绍的es6模块化
。
3.4 ES6模块化⭐⭐
ES6模块化的出现,给前端更大的方便。旨在成为浏览器和服务器通用的模块解决方案,但还是主要专门针对浏览器端。其模块功能主要由两个命令构成:export
和import
。现在很多项目都在使用ES6模块化规范。

-
定义
package.json
文件 -
安装
babel-cli
,babel-preset-es2015
和browserify
npm install babel-cli browserify -g
npm install babel-preset-es2015 --save-dev
preset 预设(将es6
转换成es5
的所有插件打包) -
定义
.babelrc
文件{ "presets": ["es2015"] }
-
编码
module1.js
分别暴露 后面需要完整的定义(变量或函数定义)export function foo() { console.log('module1 foo()'); } export function bar() { console.log('module1 bar()'); } export const DATA_ARR = [1, 3, 5, 1]
module2.js
统一暴露 暴露的是一个对象,要暴露的数据添加为对象的属性/方法let data = 'module2 data' function fun1() { console.log('module2 fun1() ' + data); } function fun2() { console.log('module2 fun2() ' + data); } export {fun1, fun2}
module3.js
静默暴露 只能暴露一个内容,默认暴露的本质:定义了default
变量,将后面的值赋值给default
变量,暴露出去export default { name: 'Tom', setName: function (name) { this.name = name } }
app.js
主模块用import
引入模块import {foo, bar} from './module1' import {DATA_ARR} from './module1' import {fun1, fun2} from './module2' import person from './module3' import $ from 'jquery' //引入第三方jQuery模块 npm install jquery@1 --save $('body').css('background', 'red') foo() bar() console.log(DATA_ARR); fun1() fun2() person.setName('JACK') console.log(person.name);
输出结果:
module1 foo() module1 bar() [1, 3, 5, 1] module2 fun1() module2 fun2() JACK
-
编译
使用Babel将ES6编译为ES5代码(但包含CommonJS
语法):babel src -d build
使用Browserify
编译js:browserify build/app.js -o dist/bundle.js
-
在页面
index.html
中引入测试
<script type="text/javascript" src="lib/bundle.js"></script>
总结ES6
特点:动态引入(按需加载),没有缓存
用法:(抓住引入和暴露)
- 引入模块使用
import
- 对于统一暴露/分别暴露
import {模块暴露的内容} from '模块路径';
或import * as m1 from './module1'
这两者暴露的本质是对象,接收的时候只能以对象的解构赋值的方式来接收值 - 对于默认暴露
直接使用import 模块暴露的内容 from '模块路径'
默认暴露,暴露任意数据类型,暴露什么数据类型,接收什么数据类型
- 对于统一暴露/分别暴露
- 暴露模块使用
export
- 分别暴露 (基本不用)
- 统一暴露 (暴露多个内容)
- 默认暴露 (暴露单个内容)
主要是用在浏览器,服务器端也使用。但是现在浏览器和服务器均不支持ES6的模块化语法,所以要借助工具来编译运行
babel
将ES6 - ES5 (ES6的模块化语法 编译成commonjs
)browserify
将commonjs
语法编译成能让浏览器识别的语法
3. 模块化的扩展阅读
前端模块化开发那点历史
Javascript模块化编程@阮一峰
知乎专栏 | AMD和CMD的区别
4. 本片我想说的话 🙈 🙈
既然说到模块化,其实我更想说说模块化与组件化。这两个概念在前端领域已经十分普遍。
先有模块化后有组件化。组件化是建立在模块化思想上的一次演进,一个变种。所以,我们会在软件工程体系中看过一句话:模块化是组件化的基石。
组件化和模块化的思想都是分而治之的思想。但还是有细小的区分,他们的侧重点有所不同。
组件化更加倾向于UI层面上,是一个可以独立展示内容的「积木」,比如一个页面的头部组件,包含结构HTML、样式CSS、逻辑JS、以及静态资源图片组合一个集合体。一个页面是由众多组件组成的,就像由众多「积木」搭成的「城堡」一样; 模块化更加倾向于功能或者数据的封装,一般由几个组件或1个组件构成的带有一定功能的集合体;
引用一下@张云龙「👈大神」对组件化的理解:

title
组件,包含了结构HTML、样式CSS、逻辑JS、以及静态资源图片,往往组件的组成就是以上四个方面。这个header
文件夹我们可以拿到其他项目中使用,它具有可以独立展示内容的特点。
结合前面提到的模块化开发,整个前端项目可以划分为这么几种开发概念:


不仅仅如此,多终端也已经成为时下以及未来的一个必然趋势,移动端、PC端、触摸屏、智能设备、物联网等等,相信前端在跨端的领域下肯定会有更好的解决方案。
但是,如果从整个软件工程来看,我们就会意识到一个惨痛的事实:前端工程师在整个系统工程中的地位太低了。前端是处于系统软件的上游(用户入口),因此没有其他系统会来调取前端系统的服务。而后端它在软件开发中处于下游,后端一方面要为前端提供接口服务,一方面要向中后台以及数据层索取服务,对接层次会更多,地位也就更高了。由此导致,感觉每次需求评估前端往往是最后一道坎,因为上游依托下游,就只能是下游先行了,整体上就会感觉前端对业务的参与度太低了。
甚至,2019了。现在还是有很多团队会把前端开发归类为产品或设计岗位底下,嗯,我不好说什么,唉···。
你在的公司前端的组织架构是肿么样呐??? 🙋🙋
前端未来一定不会差,就像在人工智能和大数据领域下,不止于前端,前端完全可以融合和细化下去。
引用一位蚂蚁伙伴的话来说:前两年的前端主要矛盾是日益爆发的前端新技术同前端程序猿学不动之间的矛盾,而现在主要矛盾发生了变化,变成了前端日益增长的工程地位诉求同前端工程局限性之间的矛盾。(这人考研政治绝对高分!)
在这样新的矛盾下,我们就要化被动为主动,改接受为影响。
好啦,好好学习吧,做一个π
型人。打铁还需自身硬。Confidence~🙌
今天是2019年3月8日,农历二月二(龙抬头),星期五,阴阴天气。我在深圳祝福各位女同胞唷节日快乐永远美丽,祝福男同胞单身幸福~biubiu💖💗💙💚💛💜💝
去泡个澡,今晚早点休息,明天还要去北京大学深圳医院。
此文档作者:吕涯
CSDN主页:https://blog.csdn.net/LY_code
掘金主页:https://juejin.cn/user/3667626521532855
若有错误,及时提出,一起学习,共同进步。谢谢。 😝😝😝