前端工程化
ES+ Git Node.js Express MongoDB Ajax Axios
ES6+
ECMAScript是标准,JavaScript是其主要实现。
ES5的严格模式
1 |
◆严格模式下八进制的限制
严格模式下不允许使用0开头的八进制数字表示法(如012)。会抛出Uncaught SyntaxError: Octal literals are not allowed in strict mode。不过允许使用十六进制表示法(如0x12)。
设计原因: 数字前导零存在语义歧义(数学上012应与12等价),属于历史遗留问题。
◆严格模式下变量声明的限制
不允许未声明直接使用变量(如username = 100),会抛出username is not defined错误。
非严格模式对比: 普通模式下会自动创建全局变量,属于隐式变量声明。
let 与 const
◆ES6新增了let命令。
- 不存在变量提升:let不像var那样会发生“变量提升”现象。所以,变量一定要在声明后使用,否则报错。
- 暂时性死区:只要块级作用域内存在let命令,它所声明的变量就“绑定”这个区域,不再受外部的影响。
- 不允许重复声明:let不允许在相同作用域内重复声明同一个变量。
ES6规定暂时性死区和不存在变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。 这样的错误在ES5中是很常见的,现在有了这种规定,避免此类错误就很容易了。
◆ES5只有全局作用域和函数作用域,ES6增加了块级作用域。
这其实带来了很多不合理的场景,内层变量可能会覆盖外层变量,用来计数的循环变量泄露为全局变量。
立即执行函数设计目的:隔离变量,避免污染全局作用域。 ES5 中 var 声明的变量无块级作用域,即使写在 {} 里,也会泄漏到外层(甚至全局)。IIFE 的解决方式就是用函数作用域包裹变量,强制隔离,避免泄漏。
所以,块级作用域的出现,实际上使得获得广泛应用的立即执行匿名函数(IIFE)不再必要了。
◆const与let不同的是,常量不可变,声明时必须立即赋值。
解构赋值
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这就是解构(Destructuring)。
数组解构赋值
只要具有索引结构(通过下标访问元素),就可以解构。所以伪数组也可以。(字符串,arguments,DOM元素集合等)
1 | /** 错误 易混点 |
◆关于默认值
解构赋值允许指定默认值。.ES6内部使用严格相等运算符(===)判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。
1 | var [f = true] = []; // f = true |
对象的解构赋值
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
1 | var {foo, bar} = {foo: "aaa", bar: "bbb"}; |
所以,对象的解构赋值是以下形式的简写:
1 | var {foo: foo, bar: bar} = {foo: "aaa", bar: "world"}; |
对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
指定默认值
1 | var {n1,n2:n2=22,n3=33,n4} = {n1:11}; |
◆解构数组为对象
1 | var {length, push, map} = [1, 2, 3, 4, 5]; |
数组本质也是对象,具有 length 等内置属性。它可以解构出数组的内置方法和属性。
◆解构字符串为对象
1 | var {length, indexOf, forEach} = "hello world"; |
字符串新增特性
◆模板字符串
特点:
- 内部可以直接换行
- 内部可以直接插入变量或表达式 例如:
${name},${age + 10}
典型应用:用模板字符串动态生成HTML内容很方便
◆ES5 方法
charAt()
charCodeAt() 字符 -> 索引
indexOf() 返回子串首次出现的位置
lastIndexOf() 返回子串最后出现的位置
slice()
substring()
substr()
toLowerCase()
toUpperCalse()
split() 将字符串分割为数组
search() 正则搜索
match() 正则匹配
replace() 正则替换
◆ES6 + 方法
repeat(n) 字符串重复,返回新字符串
includes(‘a’) 判断是否包含a,返回布尔值
startsWith() 判断是否以某个值开始,返回布尔值
endsWith() 判断是否以某个值结尾,返回布尔值
trim() 去掉两端的空格
trimStart() 去掉前面的空格(ES2019)
trimEnd() 去掉后面的空格(ES2019)
padStart() 前面填充字符串(ES2017)
padEnd() 后面填充字符串(ES2017)
replaceAll() 替换字符串中指定内容,替换所有(ES2021)
1 | 'Hello World'.padStart(20) // 在前面补9个空格使总长度达20 |
数值新增特性
二进制:使用0b前缀(如0b10表示二进制的2)
八进制:使用0o前缀(如0o10表示八进制的8)
十六进制仍然使用0x前缀。
输出特性:无论使用何种进制表示,控制台输出都会转换为十进制形式
Number 构造函数本身新增的方法和属性:
ES5
1 | Number.MAX_VALUE; |
ES6+
Number.MAX_SAFE_INTEGER 读取最大的安全整数
Number.MIN_SAFE_INTEGER 读取最小的安全整数
Number.EPSILION 两个数字间最小差值,就是JS的数字精度
Number.isNaN() 同全局对象的 isNaN()
Number.isFinite() 同全局对象的 isFinite()
Number.parseInt() 同全局对象的 parseInt()
Number.parsetFloat() 同全局对象的 parsetFloat()
Number.isInteger() 判断参数是否是整数,返回布尔值
Number.isSafeInteger() 判断参数是否是安全整数,返回布尔值
Git
- github https://github.com/
- 码云 https://gitee.com/
开始
Git 是一款开源免费的分布式的版本控制系统。是 Linux 之父 Linus Torvalds(林纳斯·托瓦兹)为了方便管理 linux 代码代码而开发的。
功能:
- 代码备份
- 版本回退
- 多人协作
- 权限控制
Git 官方文档地址: https://git-scm.com/book/zh/v2
◆常用Linux命令ls 将当前目录下所有的文件或子目录列举 ls -l 详细信息
rm -rf 删除(小心rm -rf /,全部格式化删除clear 删除当前的执行记录
cd 进入到指定的目录
◆常用快捷键
ctrl + c 终止当前命令
ctrl + l 清除当前命令行
tab 自动补全路径
键盘上下方向键 调取历史命令
◆Vim的使用
1 | vim <文件名.txt> |
Git基础命令
初始化:每个新项目需要执行一次git init初始化,初始化会在项目目录下创建.git隐藏目录。
.git目录 :
- hooks 目录包含客户端或服务端的钩子脚本,在特定操作下自动执行。
- info 包含一个全局性排除文件,可以配置文件忽略。
- logs 保存日志信息。
- objects 目录存储所有数据内容,本地的版本库存放位置。
- refs 目录存储指向数据的提交对象的指针(分支)。
- config 文件包含项目特有的配置选项。
- description 用来显示对仓库的描述信息。
- HEAD 文件指示目前被检出的分支。
- index 暂存区数据。
不要手动去修改 .git 文件夹中的内容。
- 工作区:代码编辑区,开发者直接编辑文件的地方。所有修改首先在工作区进行。
- 暂存区:修改待提交区。通过
git add命令将工作区修改添加到暂存区。 - 版本库:存储项目历史版本。通过
git commit命令将暂存区内容提交到版本库。除了.git文件,其他都是工作区。
添加暂存区:
1 | git add <file> # 添加指定文件到暂存区 |
提交版本库:
1 | git commit -m "提交日志" # 把暂存区的东西提交到版本库 |
查看状态和变化:
1 | git status; |
如果 git status 命令的输出对于你来说过于简略,而你想知道具体修改了什么地方,可以用 git diff 命令。
1 | git diff # 查看当前工作区和版本库的差异 (不包括新增的文件) |
撤销修改和撤销暂存
◆工作区的修改没有添加到暂存区
1 | git restore <文件名> # 恢复工作区指定文件 |
◆工作区的修改已经添加到暂存区
如果工作区的修改已经添加到暂存区,先清除暂存区,再恢复工作区。
1 | git restore --staged <文件名> # 把指定文件从暂存区移除 |
历史版本回滚
查看历史版本号:
1 | git log # 查看提交记录 |
通过指定版本号回滚:
1 | git reset --hard <commitID> # 版本号前七位即可 |
如果需要查看被回滚掉的提交的版本号:
1 | git reflog |
快捷回滚:
1 | git reset --hard HEAD^ # 恢复到上个版本 |
Git忽略文件
哪些文件需要被 git 忽略:
- 忽略操作系统自动生成的文件,比如缩略图等;
- 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如 Java 编译产生的
.class文件; - 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。
◆设置忽略文件 .gitignore
忽略文件的文件名是 .gitignore 的文件, 文件内可以设置项目的忽略规则。
忽略文件可以放在项目中的任意目录中,放在哪个目录作用范围就是哪个目录; 一般忽略文件会放在项目的根目录下。
Node与模块化
官方网站地址: https://nodejs.org/en/
中文网站地址 : http://nodejs.cn/
开始
Node.js,也称 Node,是一个基于 Chrome V8 引擎的 JavaScript 运行环境(宿主),与浏览器是等价的。
Node.js不是一种独立的语言,也不是一个JS框架或库,运行在Node.js上的JS不能使用DOM,BOM,但是可以使用Node.js提供的各种API。
有什么用?
- 基于Node进行后端开发
- 前端工程化支持(Wepack/Gulp等工具链)
- 开发桌面应用(框架Electron、ReactNative等)
- 开发小工具(自动化脚本、爬虫程序等)
特点:
- 单线程
- 非阻塞
- 事件驱动
前端是指运行在客户端上的代码,特指WEB前端时即运行在浏览器上的代码,如HTML、CSS和JavaScript构建的页面。
后端是指运行在服务器端的程序,主要负责业务逻辑实现、数据库操作等功能,也称为服务端。
◆后端开发的组成
- 后端编程语言(PHP、Java、C#、Go、Python、JavaScript)
- Web服务器程序(Tomcat、Apache、Nginx、IIS等)
- 数据库程序(MySQL、Oricle、MongoDB)
后端架构分层
- 表现层:Node.js
- 业务层:Java等
- 底层:C/C++
cmd :不支持ls等Linux命令
PowerShell : 比cmd强大
GitBash : 支持完整Linux命令
◆REPL方式运行
(临时计算或快速测试)
进入REPL:命令行或终端运行 node ,就进入了 repl 模式
退出REPL:.exit 或者 按两下 ctrl+c 或者 ctrl+d
常用REPL命令:
ctrl + c- 按下两次 - 退出 Node REPL。ctrl + d- 退出 Node REPL.- 向上/向下键 - 查看输入的历史命令
tab键 - 列出当前变量(对象)
◆脚本运行
◆内置全局常量
__dirname:当前文件文件夹绝对路径__filename:当前文件完整绝对路径 + 文件名module:当前模块对象exports:导出对象require:导入方法
Buffer
Buffer 是 Node.js 提供的用于存储二进制数据的类数组对象(长得像数组,但本质是底层内存的直接映射,速度快),它的大小在创建时固定,无法动态调整,且存储的是 0~255 之间的整数值(对应字节)。
普通字符串只能存文字。图片、视频、文件、网络数据流都是二进制,JS原生不好处理,所以Node专门搞了个Buffer用来装这些二进制数据。
类数组:写法像数组,可以buf[0]、遍历、有 length,但不是普通数组。
1 Byte = 8 Bit (1Bit对应的是一个二进制位) = 2个十六进制位
UTF-8 下:
1 个英文字符 = 1 字节,比如 H e l l o 每个字母都是 1 字节
1 个中文汉字 (UTF-8) = 3 字节,汉字复杂,编码内容多。
存储容量换算:
- 1024 Byte = 1KB
- 1024 KB = 1 MB
- 1024 MB = 1 GB
豆包再讲: https://www.doubao.com/thread/wa1b9734c49035aaf
◆创建Buffer
1 | // 创建空的Buffer -> 申请一段干净的内存,自动全部填0 |
◆读取Buffer
1 | const b = Buffer.from("hello 嘻嘻嘻"); // 总长度为hello(5) + 空格(1) + 嘻×3(9) = 15字节 |
buffer 每个元素能表示的最大数字是 255,如果超过 255 的数字,会舍去高位(二进制)。
超过256,前面多出来的高位,都是完整的 256 倍数。取余,就是把所有「完整的 256 倍数」全部扔掉,只剩不足 256 的尾数。
1 | buff3[0] = 365; // 365的二进制为0001 0110 1101 |
内置模块
Node 当中的模块分为三种:内置模块,第三方模块(npm 下载的,比如express、axios)以及自定义模块(自己写的 js 文件)。 不论哪一种模块,在使用时都必须先引入模块。
1 | const 变量 = require('模块'); |
关于抛错:
- try 里面的错误会被catch捕获,不论是代码错误还是主动抛出,捕获到错误之后由程序员处理,系统不会报错。
- try-catch 不论是否抛出错误,都不影响后面的语句的执行。
- try 内部,错误后面的语句不会执行。
1 | try { |
Path模块
path.join([path1][, path2][, ...])用于连接路径。该方法的主要用途在于,会正确使用当前系统的路径分隔符,Unix系统是”/“,Windows系统是”\“。path.isAbsolute(path)判断参数path是否是绝对路径。path.dirname(p)返回路径中目录的部分 。path.basename(p)返回路径中的最后一部分,文件名部分。path.extname(p)返回路径中文件的后缀名。path.resolve()将路径或者路径片段序列化为绝对路径 (常用)。
fs模块(文件系统模块)
文件读取:
1 | // 引入模块 |
写入文件:
1 | // 导入模块 |
文件重命名:
1 | // 导入模块 |
删除文件:
1 | const fs = require('fs'); |
创建目录:
1 | // 异步方式 创建目录 递归方式创建多级目录 |
URL模块
querystring 模块
JSON 格式的处理
JSON全称是 JavaScript Object Notation (JavaScript 对象表示法) ,是一种轻量级的数据交换格式。
JSON 的语法与 JS 定义数组和对象的语法区别:
- json 中的字符串必须使用双引号。
- json 中的属性名必须使用双引号包裹。
- json 中的最后一个属性不能有逗号。
- json 中的属性值不能是表达式。
1 | // 将JSON格式的字符串转为对象或数组! |
模块规范概述
四种模块化规范:CommonJS规范、AMD规范、CMD规范、ES6 Module规范。
- CommonJS规范是Node.js 默认规范。Node 后端专用。
同步加载:加载完模块,代码才继续往下走。
运行时加载:代码执行到 require 才引入。
适合服务端、本地文件(本地读取快,同步没问题)。
浏览器原生不支持,必须打包(webpack)。 - ES6 Module规范,现在浏览器 + Node.js + Vue/React/ 小程序 全都用它。
编译时加载(静态):代码没运行,就先分析模块。
支持异步、按需导入。
原生浏览器直接支持。
Node 开启:package.json 加 “type”:”module”。
未来唯一主流规范。
CommonJS模块规范
引入模块
1)使用require()引入模块,内置模块直接写模块名,自定义模块需要些相对路径。
2)自定义模块的地址必须以 ./、 ../ 开头。如果模块文件的地址没有以 ./、 ../ 开头,会被认为是内置模块或第三方模块的模块名。
3)扩展名可以省略。如.js或.json。无扩展名时会依次查找同名js文件、json文件和目录。
1.扩展名是
.js的模块文件: 读取文件内容并编译执行并获取模块中暴露的数据。
2.扩展名是.json的模块文件: 读取文件时会自动用JSON.parse()解析返回结果作为获取的数据。
整个目录作为一个模块:
1)会默认加载该目录下 package.json 文件中 main 属性定义的入口文件。
2)如果没有package.json, 或者 main 属性对应的文件不存在,则自动找 index.js 、 index.json 作为入口文件。
- 模块作用域隔离
每个模块里的变量、函数,默认只能自己用。
不导出,外面绝对访问不到,不会和其他文件变量冲突。 - 模块缓存机制
同一个文件,require引入多次,只会执行一次。
后面再次引入,直接读取缓存,提升性能。 - 模块执行顺序
引入模块时,会从头到尾执行一遍被引入文件的代码。
导出数据(模块暴露数据)
模块中定义了变量但未暴露,外部就无法访问。require返回的是空对象{},无法访问模块内部变量。
本质原理:module.exports实际上是模块对外暴露的接口对象,require()的返回值就是这个对象。
1 | require() 的返回值 = module.exports 的值 |
模块中通过module.exports赋值暴露数据,绝大多数暴露的是对象类型,因为信息承载量更大。当模块暴露的是对象时,可以使用解构赋值直接获取属性。要注意解构出的方法如果直接调用,this会指向全局对象而非原模块对象。
通过给module.exports添加getMessage和setMessage两个方法,可以实现暴露多个数据。
1 | module.exports.getMessage = () => { |
关于exports赋值的原理 :对象存储在堆中,变量名(如exports)存储在栈中,然后指向堆中的对象。module.exports默认指向一个空对象,exports是它的别名,也指向这个空对象。给exports添加属性实际上是给这个空对象添加属性。如果修改exports的值(重新赋值),则exports会指向新的对象,与module.exports的引用关系断开,导致无法暴露数据。
1 | module.exports = 真正出口 |
ES6 模块规范
ES6 模块怎么开启?
方式 1:文件后缀改成 .mjs
方式 2:package.json 加一句
1 | { |
引入模块
1 | // 解构导入 |
导出数据(模块暴露数据)
使用 export default 可以在模块中暴露单个数据,注意文件中 export default 语句只能出现一次。
而使用 export 可以暴露多个数据。
1 | // 第一种写法 一边定义一边暴露 |
export:
多份导出。
导入:import { 原名 }。
不能乱改名。
export default:
单个唯一。
导入:import 随便起名。
不用花括号。
NPM
NPM官网:npm | Home
npm类似于手机应用商店或电脑软件管家,是一个集中管理各种开发包的工具平台。官网上托管了大量开发者共享的包,包括流行框架如Vue、React等。此时需要通过命令行工具进行下载,而非直接从网站下载。
基础命令行
1 | npm -v |
package.json
1 | { |
当删除node_modules后运行程序会报错,因为缺少依赖模块。package.json中记录了项目所需的所有依赖包及其版本。通过npm install命令可以自动安装所有依赖。
◆版本锁定规则
^3.0.0:锁定大版本(3.x.x)
~3.1.0:锁定小版本(3.1.x)
3.1.1:锁定完整版本package-lock.json:精确锁定安装时的具体版本。
◆依赖包列表
依赖类型:
- dependencies:生产依赖(开发和运行都需要,项目本身需要)
- devDependencies:开发依赖(仅开发过程需要,项目本身不需要)
安装方式: npm install 包名 --save:安装为生产依赖npm install 包名 --save-dev:安装为开发依赖
模块的查找过程
- 看到 ./ 或 ../ → 自定义文件模块。
- 不是相对路径 → 先找内置模块。
- 不是内置 → 找第三方模块。
- 第三方模块加载过程:
① 先从脚本就所在的目录中查找有没有 node_modules 目录,如果有进入查找模块。
② 如果脚本同级目录没有 node_modules 目录,去上级目录查找 node_modules 目录,如果有进入查找模块。
③ 以此类推,一直查找到根目录。
远程仓库与npm
克隆的项目不包含node_modules目录,必须执行npm install才能正常运行项目,依赖安装应在项目根目录下执行。
配置命令别名
配置 package.json 中的 scripts 属性:
1 | { |
配置完成之后,可以使用别名执行命令:
1 | npm run server |
不过 start 别名比较特别,使用时可以省略 run:
1 | npm start |
所以,对于陌生的项目,可以通过查看 scripts 属性来参考项目的一些操作。
cnpm
使用npm下载包时,会去npm官网下载,而npm服务器在国外,可能不稳定。
cnpm使用国内镜像作为npm源,解决官方源在国外导致的下载不稳定问题。淘宝镜像每10分钟同步一次npm官方仓库,提供国内高速下载。
总结特点:
- 与npm命令完全兼容,仅需替换命令前缀。
- 支持所有npm操作(安装、初始化、发布等)。
- 镜像服务器位于阿里云,网络稳定性更高。
全局安装 cnpm 命令,安装完成后使用 cnpm 命令代替 npm 命令。
1 | npm install -g cnpm --registry=https://registry.npmmirror.com |
yarn
Yarn 由 Meta 推出,核心优势是缓存、并行下载、精确版本锁定。
全局安装yarn:
1 | npm install -g yarn |
基础命令:
1 | yarn --version #查看版本 |
发布npm包
- 准备项目
npm init生成 package.json;写好代码,用module.exports导出。 - 必须切回官方源
1 | npm config set registry https://registry.npmjs.org/ |
用淘宝镜像无法发布,必须换回 npm 官方源!
3. 登录并发布
1 | npm login // 输入:用户名、密码、邮箱 |
- 更新包
必须改版本号 才能再次发布,改完 package.json 的 version,再执行npm publish。
HTTP协议
基础
◆IP地址
IP地址是IP协议提供的统一地址格式,为互联网上每个网络和主机分配的唯一标识。每个联网设备(电脑、手机、智能家居等)都必须有唯一IP地址。采用分层结构解决地址不足问题(局域网使用私有IP,对外共用公网IP)。当前主流IPv4格式为点分十进制,地址组合数为256^4种。IPv6是为解决地址不足问题推出的新格式。
◆域名
DNS:把网址翻译成 IP 地址。
◆端口号
客户端通过IP地址找到服务器后,需要通过特定端口号访问服务器上的具体应用程序。每个应用程序对应一个端口号,这是客户端与服务器应用程序通信的最终通道。
HTTP(hypertext transport protocol)协议,中文叫超文本传输协议,是一种基于TCP/IP的应用层通信协议,这个协议详细规定了浏览器和万维网服务器之间互相通信的规则。主要规定了两个方面的内容:
- 客户端:用来向服务器发送数据,可以被称之为请求报文。
- 服务端:向客户端返回数据,可以被称之为响应报文。
通信过程:
1.用户在浏览器地址栏输入域名并回车后,会发送请求报文到服务器。
2.服务器接收请求报文后返回响应报文(包含HTML、CSS、JS等资源)。
3.浏览器解析响应报文并渲染显示给用户。
请求报文与响应报文
◆请求报文
- 请求行(请求方式+URL+协议版本)
- 请求头
- 空行
- 请求体(POST才有数据)
◆响应报文
- 响应行(协议版本+响应状态码+响应状态描述)
- 响应头
- 空行
- 响应体
响应状态码:
1xx:指示信息–表示请求已接收,继续处理。2xx:成功–表示请求已被成功接收、理解、接受。3xx:重定向–要完成请求必须进行更进一步的操作。4xx:客户端错误–请求有语法错误或请求无法实现。(你写错了)5xx:服务器端错误–服务器未能实现合法的请求。(后端挂了)
更多响应状态码: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Status
使用Node创建http服务
◆创建服务
写一个程序,该程序可以接收到客户端浏览器的请求,并能为客户端浏览器做出响应; 该程序是后端程序,运行在服务器上,需要 node 的支持。
1 | // 导入内置模块 |
获取请求报文信息
常见的获取请求的信息:
1 | req.method // 请求方式 GET/POST |
◆获取请求报文信息
1 | // 导入模块 |
◆获取url中的查询字符串
1 | // 第一种方式 解析url |
1 | // 第二种方式 解析url |
◆GET 和 POST 区别
HTML表单属性设置:
action:提交到哪个地址。method:GET / POST。name:参数名,必须写才能提交。
表单请求方式设置:
表单默认使用GET方式提交,通过method="post"属性可将请求方式改为POST。
数据位置差异:- GET请求:表单数据会拼接在URL查询字符串中。
- POST请求:表单数据会放在请求体中传输。
◆获取POST请求体信息
因为 HTTP 请求POST数据是流式传输的,所以要一段一段接收,最后拼起来。每一块数据叫chunk(块),类型是Buffer(二进制数据)。
所以必须监听。
1 | // 给请求对象监听data事件 请求对象本质上就是以读取流 |
只有 POST/PUT 等请求方式才能有请求体,get 、delete 等方式没有请求体!
设置响应报文
◆设置响应行
1 | response.statusCode = 200; // 设置响应状态码 |
◆设置响应头
1 | // response.setHeader('响应头名字', '响应头内容') |
1 | // 同时设置 响应状态码、设置响应状态描述、响应头 |
◆设置响应体
1 | // resposne 本质上是一个可写流对象 可以分段写内容 |
◆结束响应
1 | res.end(); // 结束 |
Express
- 官网: http://expressjs.com/
- 中文网: https://www.expressjs.com.cn/
- Github: https://github.com/expressjs/express
Express 是 基于 Node.js 封装好的后端极简框架。Express 中的路由和中间件为我们的开发带来极大便利。简单来说, Express 就是一个第三方模块,专门用来创建 HTTP服务。
原生 http = 自己砍柴、生火、做饭(麻烦、底层、累、容易错)
Express = 全自动电饭煲(开箱即用、简单、高效、企业直接用)
安装:
1 | npm init |
核心:
1 | const app = express(); // 创建网站服务 |
创建http服务
- 模块导入: 使用
const express = require('express')导入Express模块 - 服务创建: 直接调用
express()函数即可创建服务实例,通常命名为app - 服务启动: 通过
app.listen()方法启动服务,指定端口号和回调函数
1 | // 导入模块 |
只要你把文件放进 public 文件夹:
- public/index.html
- public/css/style.css
- public/images/1.jpg
浏览器就可以直接访问: - http://localhost:8080/index.html
- http://localhost:8080/css/style.css
- http://localhost:8080/images/1.jpg
路由
地址 ↔ 路由(app.get / app.post) ↔ 处理函数
◆路由方法
1 | app.get() // 查(获取数据、页面) |
◆路径匹配
1 | // 精确匹配,只能匹配 /home/index |
把同一个地址的 GET 和 POST 写在一起:
1 | app.route('/login') |
请求对象与响应对象
◆请求对象
1 | req.ip // 获取客户端IP |
◆响应对象
1 | res.status(200) // 设置状态码 |
中间件
请求流程:浏览器请求 ➜ 中间件 ➜ 路由 ➜ 响应给浏览器
1 | app,use('/log', (req, res, next)) => { |
- 不写
next():请求卡住、页面一直转圈,后面路由永远不执行 - 写了
next():正常放行,继续走下一个中间件 / 路由
◆应用级中间件
例1:要定义一个访问日志中间件
第一步,创建文件accesslog.js,作为单独模块,定义中间件的代码。
1 | // accesslog.js |
第二步,在应用的入口文件挂载中间件。
1 | // 导入自定义中间件 |
例2:要定义一个错误处理中间件
错误处理中间件有4个参数,定义错误处理中间件时必须使用这 4 个参数。即使不需要 next 对象,也必须声明它,否则中间件会被识别为一个常规中间件,不能处理错误。
错误处理中间件需要挂载在所有路由和中间件的后面,如果路由回调函数或前面的中间件中出现错误,会自动进入错误处理中间件!
第一步:创建文件 catcherror.js,作为一个模块。
1 | const moment = require('moment'); |
第二步:在应用的入口文件挂载中间件。
1 | // 导入自定义中间件 |
应用级中间件(app)管全部,路由级中间件(router)管自己的模块。
路由模块化
可使用 express.Router 类创建模块化、可挂载的路由系统。Router实例是一个完整的中间件和路由系统,因此常称其为一个 mini-app。
创建一个路由模块,加载中间件,定义一些路由,将它们挂载至应用的路径上。
第一步:创建路由文件 index.js
1 | // 导入模块 |
可以继续创建路由文件 login.js
1 | // 导入模块 |
第二步:在入口文件中,将路由文件挂载到应用上:
1 | // 导入路由模块 |
模板引擎
项目生成器 express-generator
全局安装:
1 | npm install -g express-generator |
运行命令生成目录结构并指定模板引擎为 ejs:
1 | express --view=ejs |
安装依赖:
1 | npm install |
启动项目:
1 | npm start |
MongoDB
开始
- 下载地址: https://www.mongodb.com/try/download/community
- 配置环境变量:C:\Program Files\MongoDB\Server\5.0\bin (默认)
- 启动:
(1)系统启动:此电脑->管理->服务->找到MongoDB启动
(2)命令行启动:
1 | mongod --dbpath "D:\CodeApp\关于前端\MongoDB\data" |
- 可视化工具Compass: https://www.mongodb.com/try/download/compass
◆三个重要概念
数据库(database): 数据库是一个仓库,一个 MongoDB 服务可以创建多个数据库。
集合(collection): 集合类似于 JavaScript 中的数组,一个数据库中可以创建多个集合。
文档(document): 文档是数据库中的最小单位,类似于 JavaScript 中的对象,表示一条数据信息,一个集合中可以有多个文档。
命令行工具
◆连接 MongoDB 服务:
1 | # 本地无密码连接 |
◆数据库操作:
1 | # 显示所有的数据库 |
◆集合操作:
1 | # 创建集合 |
◆文档操作:
数据库的基本操作包括四个,即CURD,增加(create),删除(delete),修改(update),查询(read)。
1 | # 创建文档并插入到集合中 |
◆条件控制:
- 运算符
在 mongodb 不能使用运算符,需要使用替代符号:>使用$gt<使用$lt>=使用$gte<=使用$lte!==使用$ne
- 逻辑或
$in满足其中一个即可,$or逻辑或的情况,$and逻辑与的情况。 - 正则匹配
条件中可以直接使用 JS 的正则语法。
1 | db.集合名.find({age:{$in:[18,24,26]}}) |
使用Node操作数据库
Mongoose:是 Node.js 操作 MongoDB 的工具库。之前你要写一堆 MongoDB 原生命令,麻烦、容易错。有了 Mongoose,你写 JS,它帮你搞定数据库。
1 | npm install mongoose |
Schema定义,模型相当于实例化。
◆基础操作
1 | // 1. 导入 mongoose 模块 |
Schema 的类型:
| 类型 | 描述 |
|---|---|
| String | 字符串 |
| Number | 数字 |
| Boolean | 布尔值 |
| Array | 数组,也可以使用 [] 来标识 |
| Date | 日期 |
| Buffer | Buffer 对象 |
| Mixed | 任意类型,需要使用 mongoose.Schema.Types.Mixed 指定 |
| ObjectId | 对象 ID,需要使用 mongoose.Schema.Types.ObjectId 指定 |
| Decimal128 | 高精度数字,需要使用 mongoose.Schema.Types.Decimal128 指定 |
◆插入数据
1 | SongModel.deleteOne({_id:'5dd65f32be6401035cb5b1ed'}, function(err, data){ |
◆删除数据
1 | SongModel.deleteOne({_id:'5dd65f32be6401035cb5b1ed'}, function(err, data){ |
◆更新数据
1 | // 修改符合条件的第一个数据 |
◆查询数据
1 | // 1.查询一条数据 |
会话控制
HTTP协议特性: HTTP是一个无状态协议,无法区分多次请求是否来自同一客户端。服务器端只能接收和响应请求,但不会记录请求来源。
常见的会话控制解决方案有
- Cookie: 存储在客户端的小型数据片段。
- Session: 存储在服务器端的用户会话信息。
- Token: 开发者自定义的认证机制(非HTTP原生)。
会话控制主要在后端实现,前端开发需要理解原理并配合后端要求进行开发。
cookie
cookie本质是一个存储在浏览器的文本键值对,随着http请求自动传递给服务器(浏览器每次给服务器发请求,都会自动把 Cookie 带上)。cookie 通过请求头和响应头实现。
完整的工作流程:
① 服务器以响应头的形式将如何设置 cookie 发送给浏览器。
② 浏览器收到以后会设置 cookie 并保存。
③ 浏览器再次访问服务器时,会以请求头的形式将 cookie 发送。
④ 服务器就可以通过检查浏览器发送的 cookie 来识别出不同的浏览器。
特点:
- 以网站域名为单位隔离存储
- 不同浏览器间cookie不共享
删除 Cookie 只会让你失去当前的登录状态,不会影响你的账号密码本身。只要你输入正确的账号密码,服务器会重新验证并给你发一个新的 Cookie,让你正常登录。
在 express 中,通过配置 cookie-parser 中间件,可以将 cookie 解析为一个对象,并为 request 对象添加了一些操作 cookie 属性方法。
安装:
1 | npm install cookie-parser |
引入:
1 | const cookieParser = require("cookie-parser"); |
挂载中间件:
1 | app.use(cookieParser()); |
使用:
1 | // 响应体设置 cookie (添加或修改) 注意:cookie的属性设置使用小驼峰 |
cookie的缺点:
- 各个浏览器对 cookie 的数量和大小都有不同的限制,一般数量不超过 50 个,单个大小不超过 4KB。
- cookie 由服务器发给浏览器,再由浏览器发回,如果 cookie 较大会导致发送速度变慢,降低用户体验。
- cookie 内容存储在客户端浏览器,客户端可以对 cookie 内容进行修改,安全性低。
实际影响:不能用 Cookie 存大量数据,比如用户信息、配置、列表数据,这些都得放数据库或 localStorage。Cookie 是存在你自己电脑里的,用户可以直接在开发者工具里修改、伪造 Cookie,甚至通过恶意脚本(XSS)偷取别人的 Cookie,所以也不能在 Cookie 里存敏感数据。
session
Session 是一个对象,存储特定用户会话所需的属性及配置信息。Session是保存在服务器端的数据,保存介质可以是文件、数据库或者内存。
session运行流程:
- 服务器中为每一次会话创建一个对象,然后每个对象都设置一个唯一的 ID。
- 通过设置响应头让浏览器设置 cookie 用于保存该 ID。
- 将会话中产生的数据统一保存到 session 对象中,这样我们就可以将用户的数据全都保存到服务器中,而不需要保存到客户端,客户端只需要保存一个 ID 即可。
![[Pasted image 20260511213526.png]]
![[Pasted image 20260511214210.png]]
完整工作流程:
- 用户第一次访问网站
服务器发现你没有登录,会:- 在服务器上创建一个 Session 对象。
- 给这个 Session 分配一个唯一的
Session ID。 - 把
Session ID通过Set-Cookie响应头发给浏览器。
- 浏览器保存 Cookie
浏览器收到响应后,把这个包含Session ID的 Cookie 存在本地。 - 用户再次访问网站
浏览器会自动把 Cookie 里的Session ID发给服务器。 - 服务器根据 ID 找到 Session
服务器拿到Session ID,在自己的 Session 池里找到对应的 Session 对象,读取里面的用户数据(比如 “这是已登录的用户March”),然后返回对应的页面。
豆包讲session与cookie的关系 https://www.doubao.com/thread/w28f1678377a5dfeb
在 express 中,通过配置 express-session 中间件,可以将 cookie 解析为一个对象,并为 request 对象添加了一些操作 cookie 属性方法。
安装:
1 | npm install express-session |
引入:
1 | var session = require("express-session"); |
挂载中间件:
1 | app.use(session({ |
(豆包讲httpOnly: https://www.doubao.com/thread/wef0a665fd824bd83 )
使用:
1 | app.get("/setSession",function (req,res){ |
◆修改session存储位置
session对象的store属性可以设置session保存在内存、文件或其他存储介质。(默认session会存储在服务器的内存中,重启服务器 session 就没了)
1.保存在文件中: 需要实现文件的读写操作,包括创建文件、写入session数据、读取时查找对应客户端的session。
安装包: 会话-文件-存储 - NPM
1 | npm install session-file-store |
1 | // 导入包 |
2.保存在MongoDB中
安装包: Connect-Mongodb-Session - NPM
1 | npm install connect-mongodb-session |
1 | // 导入包 |
登录与注册
记账本项目:
用户路由
① 注册页面
GET /users/reg
② 执行注册
POST /users/reg
③ 登录页面
GET /users/login
③ 执行登录
POST /users/login
④ 退出登录
GET /users/logout注册用户
数据库创建 users 集合,创建对应的 schema、model,将用户信息添加到集合中。用户名是唯一的,否则无法注册成功。密码经过md5加密之后存储在数据库。登录
根据提交的用户名和密码从数据库查找,如果找到说明存在用户,登录成功,如果找不到,登录失败
登录成功之后,将用户信息存储在session中全局登录验证
有些页面只有登录之后才能访问,如果没有登录,跳转到登录页。哪些必须登录之后才允许访问?
GET /account
GET /account/create
POST /account/create 在account路由前面加中间件如果有session信息执行next()
GET /account/delete/:id 没有的话重定向。
哪些不需登录就可以访问?
GET /users/reg
POST /users/reg
GET /users/login
POST /users/login
GET /users/logout
如何验证是否登录?
看能否获取到session 如果没有就重定向到登录页
将用户信息显示在账单首页上退出登录
删除session账单
账单集合,每个文档添加一个属性,记录用户id
添加账单的时候,将用户id添加进去
这样查询账单只会查询该用户的账单
Ajax
Ajax 全称为 Asynchronous Javascript And XML,就是异步的 JavaScrript 和 XML。
Ajax 是浏览器自带的技术,不用刷新整个网页,就能在后台异步偷偷和服务器收发数据,实现网页局部更新。
◆静态网页与动态网页
静态网页:内容提前写死在 HTML 里,所有人打开看到的都一样,不连数据库、不跟服务器实时运算。
动态网页:内容是服务器实时临时生成的,可以连数据库、判断登录身份,不同人看到的内容可以不一样。
原生Ajax四步流程
◆原生Ajax四步流程:
- new XMLHttpRequest()
创建浏览器自带的请求工具对象,相当于造一个通信小助手。 - 绑定 onload 事件
提前说好:等后端数据返回来之后,要做什么操作(拿数据、改页面)。 - xhr.open (请求方式,接口地址)
给请求设置规则:用 GET/POST、访问哪个后端接口,只是配置,还没发请求。 - xhr.send()
真正发送请求,去找后端要数据。
1 | <h1>ajax 基本流程</h1> |
后端路由匹配,只看 ? 前面的路径,不看 ? 后面的参数。
?前面:路由路径 → 用来匹配后端app.get('/addData')?后面:参数 → 不算路由路径,只是附带的数据
req.query 干一件事:自动解析 ? 后面的键=值&键=值,变成一个 JS 对象。
前端->后端 后端接收使用
GET 数据拼在 URL 上 =====> req.query
POST 数据放请求体里 =====> req.body
后端返回统一 res.send → 前端 responseText 接收。
1 | // 设置请求头 请求头一定要在 open() 之后,send() 之前设置 |
1 | // 点击按钮向后端发送 GET 请求 |
data 是 JS 对象,网络传输只能发字符串,不能直接发对象。
JSON.stringify(对象)就是把 JS 对象转成 JSON 格式字符串。
1 | // 配置中间件 |
后端路由代码:
1 | // 接收 get 请求 |
FormData
请求体(POST/PUT)(send()的方法的参数)除了是字符串,也可以是 formData 对象。
如果请求体是 FormData 对象,浏览器会自动设置请求头字段 Content-type 为 multipart/form-data。
FormData是浏览器原生自带的表单数据对象,不用自己拼接key=value、也不用手动转JSON。它可以快速组装表单键值对,发AJAX不用手动设置请求头,浏览器自动处理。最大用处是支持文件/图片上传,日常提交表单、传文件都用它,比手写参数简单太多。
使用 FormData 对象作为请求体
1 | // 方式一 创建 空的 FormData,再添加数据,可以添加文件数据或者字符串数据 |
FormData 对象的方法
append()
set()
delete()
get()
getAll()
FormData 实现文件上传
1 | // server.js |
multer 是 Express 里专门用来处理 multipart/form-data 类型(也就是文件上传)的中间件。{ dest: 'uploads/' } 意思是:上传的文件,先临时存到 uploads/ 文件夹里,这里的这个文件夹要自己手动创建,不然会报错!
✅ 非 Ajax:
HTML 写 form + enctype=”multipart/form-data”
浏览器自动封装文件,不用 JS,但是会刷新页面。
✅ Ajax:
自己 new FormData () + append 添加文件
不用写 enctype,无刷新上传。
读取响应报文
1 | // 响应行 |
响应JSON数据
◆服务端设置
设置响应头,告知浏览器响应体的内容类型是 json 格式。
1 | Content-type: application/json;charset=utf-8 |
◆客户端处理接收到的 json 数据
方式一 使用 JSON.parse。
1 | // 监听响应结束的回调函数、 |
方式二 设置 xhr.responseType 属性 ,通过 xhr.response获取。
1 | xhr.responseType = 'json'; |
响应超时
1 | // 监听响应超时的事件 |
异步与同步请求
- 异步:发请求后代码继续往下跑,响应回来再执行回调,页面不卡死,日常开发全用它
- 同步:发请求后代码暂停卡住,必须等响应完毕才继续执行,页面冻结卡顿,极少使用
- 开关写法
1 | xhr.open('GET','地址',true) // 异步(默认) |
XMLHttpRequest 对象总结
① XHR 对象概述
1)XMLHttpRequest 对象简称 XHR 对象。
2)XMLHttpRequest 对象提供了对 HTTP 协议的完全的访问,包括做出 POST 和 HEAD 请求以及普通的 GET 请求的能力。
3)XMLHttpRequest 可以同步或异步地返回 Web 服务器的响应,并且能够以文本或者一个 DOM 文档的形式返回内容。
4)尽管名为 XMLHttpRequest,它并不限于和 XML 文档一起使用,它可以接收任何形式的文本文档。
② 创建 XHR 对象
使用构造函数 XMLHttpRequest 就可以创建一个 XHR 对象。
1 | const xhr = new XMLHttpRequest(); |
注意:在古老的 IE 浏览器中(如:IE6),需要使用其他方式来创建 XHR 对象。
1 | // IE5、IE6 |
③ XHR 对象的属性
| 属性名 | 含义 |
|---|---|
| readyState | 返回一个数字,表示请求的状态: 0 – UNSET – XHR对象已创建或已被 abort() 方法重置。 1 – OPENDED – open() 方法已经被调用。2 – HEADERS_RECEIVED – send() 方法已经被调用,并且响应头和响应状态已经可获得。 3 – LOADING – 下载中, responseText 属性已经包含部分数据。 4 – DONE – 所有响应数据接收完毕。 |
| status | 响应状态码,如 404、200 等。 |
| statusText | 响应状态码的文本描述,如 200 对应的是 “OK”。 |
| responseXML | 接收格式为 XML 的响应数据,返回一个 document 对象。 |
| responseText | 获取响应文本,返回一个字符串。 |
| responseType | 用于设置响应内容的类型 xhr2 |
| response | 返回的类型取决于 responseType 的设置。 xhr2 |
| timeout | 设置超时时间。xhr2 |
④ XHR 对象的方法
| 方法名 | 含义 |
|---|---|
| open() | 初始化 HTTP 请求,用来指定请求方式和 URL。 xhr.open(method, url, [async], [user], [password]) |
| send() | 发送 HTTP 请求,参数可以设置请求体,没有请求体无需设置参数。 |
| setRequestHeader() | 设置 HTTP 请求头的值。必须在 open() 之后、send() 之前调用。 |
| abort() | 如果请求已被发出,则立刻中止请求。 |
| getAllResponseHeaders() | 以字符串形式返回所有的响应头。 |
| getResponseHeader() | 返回指定的响应头。 |
⑤ XHR 对象的事件
| 事件名 | 含义 |
|---|---|
| readystatechange | readyState 属性值发生变化触发该事件。 |
| abort | 请求终止时触发。 |
| error | 请求遇到错误时触发。注意,只有发生了网络层级别的异常才会触发此事件。 |
| loadstart | 接收到响应数据时触发。 xhr2 |
| load | 请求成功完成时触发。xhr2 |
| loadend | 当请求结束时触发, 无论请求成功 ( load) 还是失败 (abor 或 error)。xhr2 |
| progress | 当请求接收到更多数据时,周期性地触发。xhr2 |
| timeout | 在预设时间内没有接收到响应时触发。xhr2 |
跨域
浏览器同源策略规定:域名、端口、协议三者任一不同,就是跨域。它是浏览器的安全防护规则。这个策略要求ajax代码所在的页面URL中的协议、域名、端口号与ajax请求的URL中协议、域名、端口号保持一致。
同源三要素:协议 + 域名 + 端口。
✅ 完全一致,不跨域
页面:http://127.0.0.1:5500/index.html
请求:http://127.0.0.1:5500/api/data
❌ 端口不同,跨域
页面:http://127.0.0.1:5500
请求:http://127.0.0.1:3000
❌ 域名不同,跨域
页面:http://localhost
请求:http://192.168.1.100
❌ 协议不同,跨域
页面:http://xxx
请求:https://xxx
请求发过去了,服务端不在乎同源还是不同源。数据返程抵达浏览器后,浏览器才会执行同源校验,不符规则就拦截数据,前端代码拿不到结果,会报错。
那么如何进行跨域资源访问?
CORS跨域资源共享
一句话,让后端服务器在响应头里加一句 “允许跨域”,浏览器就放行!
在服务端进行设置,添加一个响应头,设置允许的域名。
1 | res.set("Access-Control-Allow-Origin", "http://localhost:8080") |
JSONP
JSONP(JSON with Padding),是一个非官方的跨域解决方案,纯粹凭借程序员的聪明才智开发出来,只支持 GET 请求方式。
在网页有一些标签天生具有跨域能力,比如:img link iframe script,JSONP就是利用 script 标签的跨域能力来发送请求的。
浏览器不拦截 <script> 跨域。只创建 script 不插入页面 = 不请求,插入 body 瞬间浏览器才请求 src。
后端不能返回普通 JSON,必须返回 函数名(数据) 格式。
◆JSONP 使用步骤
1 | // 1.动态的创建一个script标签 |
如果请求回来后,函数还没定义 → 报错
- 你插入 script → 请求瞬间飞出去
- 服务器超快返回:
getData(数据) - 浏览器立刻执行:
getData(...) - 但此时 getData 函数还没定义的话!
- 浏览器懵逼:
getData is not defined→ 报错!
◆服务端的处理
服务端需要将 js 代码作为响应体:
1 | // 1. 获取前端传过来的回调函数名称 |
案例:搜索框实时提示
Promise
Promise基础
- 实例化 Promise 类,需要传一个回调函数作为参数。
- Promise 类的回调函数(参数),在实例化 Promise 的时候会自动调用,是同步任务。
- Promise 类的回调函数(参数),在被调用的时候,会接收两个参数,两个参数都是函数。
第一点什么意思??
① 使用 Promise 构造函数创建 promise 对象
1 | new Promise((res, rej) => { |
第三点怎么理解??
② 修改 promise 对象的状态
JS 会自动给你传进来两个函数:
- 第一个叫
resolve - 第二个叫
reject
你可以随便改名,但作用固定: resolve()→ 代表成功reject()→ 代表失败
1 | new Promise((resolve, reject) => { |
③ 为 promise 对象设置回调函数
- promise对象的then方法第第一个参数(回调函数),状态变为成功会执行,可以通过形参得到 PromiseResult。
- promise对象的then方法第第二个参数(回调函数),状态变为失败会执行,可以通过形参得到 PromiseResult。
- then 方法的两个回调函数都是异步执行!
你在 Promise 里写:
1 | new Promise((resolve, reject) => { |
就会在 then 里收到:
1 | .then( |
Promise核心的两个方法
- .then() → 成功时执行
- .catch() → 失败时执行
写法超级干净:
1 | 请求() |
then 方法
◆参数
- 第一个参数,是一个回调函数,当
promise对象的状态改为成功的时候,会被调用,并接收到参数 PromiseResult。 - 第一个参数,是一个回调函数,当
promise对象的状态改为失败的时候,会被调用,并接收到参数 PromiseResult。
◆返回值then() 方法的返回值是一个 Promise 对象,该 Promise 对象的状态取决于 then() 方法回调函数的返回值(then 可以设置两个回调函数,哪个回调函数执行就取决于谁)。
- 情况一:回调函数没有返回值,
then()返回的Promise对象改为成功状态,PromiseResult是undefined - 情况二:返回非Promise类型的对象或原始类型数据,
then()返回的Promise对象改为成功状态,PromiseResult是该回调的返回值 - 情况三:返回Promise对象,
then()返回的Promise对象与该回调返回的Promise对象,状态和PromiseResult保持一致 - 情况四:出现代码运行错误,
then()返回的Promise对象,状态改为失败,PromiseResult是错误对象
◆链式调用
以前的回调地狱:
1 | 请求1(function(){ |
嵌套太深,像金字塔,难看难维护。
由于then()方法返回的仍然是一个 promise 对象,所以支持链式调用,then() 的链式调用可以解决回调地狱的问题。
- 每一个 .then () 都会返回一个新的 Promise。
- 在 .then () 里 return 一个值 → 下一个 .then () 直接收到。
- 任何一步报错,都会直接跳到最近的 .catch (),后面的 then 都不执行!(异常穿透)
catch 方法
◆参数
需要一个回调函数作为参数,Promise对象的状态改为失败的的时候,执行该回调函数。
finally 方法
1 | .finally( () => { |
Promise构造函数本身的方法
先分清两类东西:
1 | // 1. 构造函数:Promise |
Promise 构造函数本身的方法就是静态方法。
async 与 await
async 和 await 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用。
① 定义一个 async 函数
任何形式的函数,声明的时候,添加 async 关键字就可以变为 async 函数
1 | // 1. function 关键字方式 |



