第一篇:# 5-1 React 实战之从零到一的项目环境搭建(一)
第二篇:# 5-1 React 实战之从零到一的项目开发(二)
接上文:# 5-1 React 实战之从零到一的组件库、插件开发(三)
6、后端项目搭建(koa 框架)
nodejs 后端项目使用场景:BFF(backend-for-fontend 给前端用的后端)、内部小系统等
一、初始化配置
- 创建文件夹(react-actual-combat 下)
arduino
mkdir packages/apps/back-end
- 初始化项目
bash
cd packages/apps/back-end && pnpm init
- 安装
devDependencies
依赖(apps/back-end 下)
sql
pnpm add @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env babelrc-rollup core-js rollup rollup-plugin-babel -D
- 安装
dependencies
依赖(apps/back-end 下)
csharp
pnpm add jsonwebtoken koa koa-bodyparser koa-router
- 编写 Rollup 脚本
php
touch rollup.config.js
// 写如下代码:
const babel = require("rollup-plugin-babel");
module.exports = {
input: "./src/index.js",
output: {
name: "hzqServer", // 自定义命名
file: "./dist/bundle.js",
format: "umd",
},
treeshake: false,
plugins: [
babel({
runtimeHelpers: true,
extensions: [".js", ".ts"],
exclude: "node_modules/**",
externalHelpers: true,
}),
],
};
- 编写 Babel 配置
json
touch .babelrc
// 写如下代码:
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"loose": true,
"targets": "node 16",
"useBuiltIns": "usage",
"corejs": {
"version": "3.36", // 跟 package.json 里面的 core-js 版本一致
"proposals": true
}
}
]
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}
- 新建入口文件
src/index.js
arduino
mkdir src && touch src/index.js
// 写如下代码:
import { random } from "./test";
console.log("[ hello word ] >", random);
export default random;
- 新建测试文件
src/test.js
arduino
touch src/test.js
// 写如下代码:
export const random = Math.random();
- 本地运行下
src/index.js
scss
node src/index.js
// 会发现报错,因为代码里面写了 esm 的 import、export 语法
// 但我们的 node环境或 package.json 没有指定支持该语法,所以会报错
package.json
新增build
命令,然后运行pnpm build
javascript
{
......
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "rollup --config rollup.config.js" // ++++++
},
......
}
- 打包后的文件如下(
back-end/dist/bundle.js
)
javascript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.hzqServer = factory());
})(this, (function () { 'use strict';
const random = Math.random();
console.log("[ hello word ] >", random);
return random;
}));
- 然后可以运行下
node dist/bundle.js
二、本地环境搭建
初始化完项目后,可以发现构建产物是能直接运行的,那可以这样处理本地开发
package.json
新增start
命令
javascript
{
......
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "rollup --config rollup.config.js",
"start": "rollup --config rollup.config.js -w", // ++++++`
},
......
}
- 运行
pnpm start
,这样每次更改代码后,就会自动打包 - 新起终端,运行
nodemon dist/bundle.js
,没有的话全局安装下npm install -g nodemon
,这样每次变更时可自动运行
package.json
新增dev
命令,以后本地开发就运行pnpm dev
即可
javascript
{
......
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "rollup --config rollup.config.js",
"start": "rollup --config rollup.config.js -w",
"dev": "pnpm start & nodemon dist/bundle.js" // ++++++
},
......
}
三、练一下手
- 删除之前的构建测试代码,新建空的入口文件
bash
rm src/index.js src/test.js && touch src/index.js
- 项目保持运行
pnpm dev
哦 - 先写个 Hello Koa,练练手
ini
import Koa from "koa";
import Router from "koa-router";
const app = new Koa();
const router = new Router();
router.get("/", async (ctx) => {
ctx.body = "hello koa 3";
});
router.get("/list", async (ctx) => {
ctx.body = ["1", "2"];
});
app.use(router.routes());
const port = 3001;
app.listen(port, () => {
console.log(`server is running at http://localhost:${port}`);
});
- 命令行里面点击网址,可以看到页面
访问 http://localhost:3001/list,也能看到数据
四、实际开发
后端项目,一般是基于 MVC 形式来组织代码的
所以我们的后端项目也会进行分层创建文件夹:controllers、services
核心技术:基于TS 的装饰器
去组装我们的代码
- 新建
controllers
文件夹
bash
mkdir src/controllers && touch src/controllers/book.js
- 新建辅助函数:装饰器、等等
bash
mkdir src/utils && touch src/utils/decorator.js
src/utils/decorator.js
写如下代码
javascript
export const RequestMethod = {
GET: "get",
POST: "post",
PUT: "put",
DELETE: "delete",
};
export function Controller(perfix = "") {}
export function RequestMapping(method = "", url = "") {}
src/controllers/book.js
写代码
typescript
import { Controller, RequestMapping, RequestMethod } from "../utils/decorator";
@Controller("/book")
export default class BookController {
@RequestMapping(RequestMethod.GET, "/all")
async getAll(ctx) {
ctx.body = ["1", "2"];
}
}
// 我们期望这样写后,就能当做接口被调用,这需要处理成 router
// 比如:router.get('/book/all', ctx => ctx.body = ["1", "2"])
// 核心为:方法(get)、地址('/book/all')、函数(ctx => ctx.body = ["1", "2"])
// 所以只要想办法能根据配置生成对应的路由就行了,即 router[method](path, fn)
// 这一步其实也很简单,自己写个 JSON,有method、path、fn,然后循环 JSON,也能生成路由
// ⭐️ 但核心的在于如何实现达到更高的扩展性、稳定性
- 完善
src/utils/decorator.js
代码
javascript
export const RequestMethod = {
GET: "get",
POST: "post",
PUT: "put",
DELETE: "delete",
};
export const controllers = [];
export function Controller(prefix = "") {
return function (constructor) {
constructor.prefix = prefix;
};
}
export function RequestMapping(method = "", url = "") {
return function (target, propertyKey, decriptor) {
let path = url || `/${propertyKey}`;
const item = {
method,
path,
handler: decriptor.value, // 函数自身
constructor: target.constructor, // 构造函数
};
controllers.push(item);
};
}
- 新建
src/controllers/index.js
,集中导出controllers
下面的文件
javascript
touch src/controllers/index.js
// 写如下代码:
import BookController from "./book.js";
export default [BookController];
- 现在这几个文件还毫无关系,所以我们简单点强行关联(import),更改
src/index.js
ini
import Koa from "koa";
import Router from "koa-router";
import Routers from "./controllers/index"; // 导入 controllers 作为路由
import { controllers } from "./utils/decorator.js"; // 导入 controllers,里面有具体的 method, path, handler
const app = new Koa();
const router = new Router();
const allPath = [];
Routers.forEach((route) => {
const currRoute = controllers.find((item) => item.constructor === route);
if (!currRoute) return;
let { method, path, handler } = currRoute;
const { prefix } = route;
if (prefix) path = prefix + path;
allPath.push({ method, path });
router[method](path, handler);
});
router.get("/", async (ctx) => {
let body = "";
allPath.forEach((item) => {
body += `<a href='${item.path}'>${item.method}: ${item.path}</a><br>`;
});
ctx.body = body;
});
app.use(router.routes());
const port = 3001;
app.listen(port, () => {
console.log(`server is running at http://localhost:${port}`);
});
- 页面效果如下:
(一) 处理跨域
首先跨域是浏览器限制的,为了网页的安全。
所以我们本地开发前端时,前端浏览器直接访问服务器会出现跨域,一般是本地加个 devServer 配置就能解决,那是因为服务端直接无跨域的说法。
当我们加了 devServer 后,浏览器就直接访问 devServer,然后 devServer 再去访问服务端,这样就走通了。
若我们是服务端,则可以通过 cors(跨源资源共享) 解决
- 更改
src/index.js
dart
// 找地方加上这一段手写代码,也可以使用 @koa/cors 库来处理跨域
app.use(async (ctx, next) => {
ctx.set("Acess-Control-Allow-Origin", "*"); // 允许与给定的来源(origin)共享。
ctx.set(
"Access-Control-Allow-Headers",
"Content-Type,Content-Length,Authorization,Accept,X-Requested-With",
); // 允许的请求头。
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, DELETE"); // 允许使用的方法或方法列表。
ctx.set("Content-Type", "application/json"); // 设置响应的 Content-Type 头,与跨域无关,只是放在一起写了
if (ctx.request.method === "OPTIONS") {
ctx.status = 200; // 状态码为 200,表示请求成功
} else await next();
});
(二) 登录鉴权
1. JWT 是什么?
JSON Web Token,由三段通过.
连接组成:
- header:类型,通常是 jwt
- payload:主体内容,可以包含一些用户信息等
- signature:签名结果,一般是 header、payload 加密后的结果
类似于:eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIkpvaG4gRG9lIiwgImlhdCI6IDE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
2. 模拟一下
本次通过模拟生成来演示
- 创建文件
bash
touch src/utils/mockJWT.js
src/utils/mockJWT.js
编码
php
const crypto = require("crypto");
/**
* 生成JWT令牌
*
* @param payload 令牌负载
* @param salt 加密盐
* @returns 返回JWT令牌字符串
*/
function sign(payload, salt) {
// 定义头部信息
const header = { type: "JWT", alg: "HS256" };
// 创建一个空数组用于存储令牌
const tokenArr = [];
// 第一段:将头部信息编码为Base64Url格式并添加到令牌数组中
tokenArr.push(base64UrlEncode(JSON.stringify(header)));
// 第二段:将负载信息编码为Base64Url格式并添加到令牌数组中
tokenArr.push(base64UrlEncode(JSON.stringify(payload)));
// 第三段:将前两段拼接后的字符串进行加密,并将加密结果添加到令牌数组中
tokenArr.push(encryption(tokenArr.join("."), salt));
// 将令牌数组中的元素用"."连接并返回
return tokenArr.join(".");
}
/**
* 将字符串进行base64Url编码
*
* @param str 待编码的字符串
* @returns 返回base64Url编码后的字符串
*/
function base64UrlEncode(str) {
return Buffer.from(str).toString("base64");
}
function encryption(value, salt) {
return crypto.createHmac("sha256", salt).update(value).digest("base64");
}
/**
* 验证token是否有效
*
* @param token 需要验证的token
* @param salt 加密时使用的盐值
* @returns 返回布尔值,表示token是否有效
*/
function verify(token, salt) {
// 将token按"."分割成header、payload和signature三部分
const [header, payload, signature] = token.split(".");
// 将header和payload拼接成字符串,并使用salt进行加密
// 返回加密后的结果是否与signature相等
return encryption([header, payload].join("."), salt) === signature;
}
const salt = "<huangzq>";
const token = sign({ user: "hzq" }, salt);
console.log("[ token ] >", token);
console.log("[ verify() ] >", verify(token, salt));
// 终端进入该文件夹,运行 node ./mockJWT.js
// 打印结果:
// [ token ] > eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ==.eyJ1c2VyIjoiaHpxIn0=.WOckAZBwACMtmAFXTBb3vRsY0J2Lef1S80WMU/RJUvg=
// [ verify() ] > true
3. 正式开搞
基于三方库,完成 JWT 模块
- 新建文件
bash
touch src/utils/jwt.js
src/utils/jwt.js
编码,实现验证、加密逻辑
javascript
import jwt from "jsonwebtoken";
const SALT = "<Huangzq666>";
export const verify = async (token) => {
return new Promise((resolve) => {
if (token) {
jwt.verify(token, SALT, (err, data) => {
if (err) {
if (err.name === "TokenExpiredError") {
resolve({
status: "failed",
error: "token 已过期",
});
} else {
resolve({
status: "failed",
error: " 认证识别",
});
}
} else {
resolve({
status: "success",
data,
});
}
});
} else {
resolve({
status: "failed",
error: "token 不能为空",
});
}
});
};
// 加密
export const signature = (data) => {
return jwt.sign(data, SALT, {
expiresIn: "10h", // 秒
});
};
export const jwtVerify =
(whiteList = []) =>
async (ctx, next) => {
if (whiteList.includes(ctx.path)) {
return next(ctx);
} else {
// 不是白名单,则需要进行校验
let token;
try {
token = ctx.header.authorization.split("Bearer ")[1];
} catch (error) {
// todo
}
const res = await verify(token);
if (res.status === "success") {
return next(ctx);
} else {
ctx.body = {
...res,
code: 401,
};
}
}
};
- 在
index.js
里面进行app.use
注册
javascript
import { jwtVerify } from "./utils/jwt.js";
// ......
// 使用 jwt 验证中间件
app.use(jwtVerify(["/", "/api/user/login", "/api/user/register"]));
- 刷新下页面,就能看到无法访问了,因为没带
token
- 新增
user
模块
bash
touch src/controllers/user.js && mkdir src/services && touch src/services/user.js
src/controllers/user.js
初始化编码
typescript
import { Controller, RequestMapping, RequestMethod } from "../utils/decorator";
@Controller("/user")
export default class UserController {
@RequestMapping(RequestMethod.POST, "/login")
async login(ctx) {
ctx.body = "登录成功";
}
}
src/services/user.js
初始化编码
arduino
export default class UserService {}
src/controllers/index.js
引入
javascript
import BookController from "./book.js";
import UserController from "./user.js"; // ++++++
export default [BookController, UserController];
- 因为是
post
请求,就只有打开postman
调用,发现能调通
src/controllers/user.js
正式编码
typescript
import UserService from "../services/user";
import { Controller, RequestMapping, RequestMethod } from "../utils/decorator";
@Controller("/user")
export default class UserController {
@RequestMapping(RequestMethod.POST, "/login")
async login(ctx) {
const userService = new UserService();
const res = await userService.validate(ctx.request.body || {});
ctx.body = { ...res };
}
}
src/services/user.js
正式编码
javascript
import { signature } from "../utils/jwt";
const mockUserTable = [
{ username: "zhangsan", password: "123456" },
{ username: "lisi", password: "654321" },
{ username: "admin", password: "111111" },
{ username: "huangzq", password: "hzq666" },
];
export default class UserService {
async validate({ username, password }) {
if (username && password) {
let findValue = mockUserTable.find((item) => item.username === username);
if (findValue) {
let findValue = mockUserTable.find(
(item) => item.username === username && item.password === password,
);
if (findValue) {
// 登录成功
return {
code: 200,
msg: "登录成功",
status: "success",
data: { token: signature({ username }) },
};
} else {
return {
code: 200,
msg: "密码错误",
status: "failed",
data: void 0,
};
}
} else {
return {
code: 200,
msg: "用户名错误",
status: "failed",
data: void 0,
};
}
} else {
return {
code: 200,
msg: "用户名或密码不能为空",
status: "failed",
data: void 0,
};
}
}
}
- 完善一下
index.js
,支持body
参数的获取
- 重新启动下后端项目,然后用
postman
调用下,正常 ok 了
- 然后拿着这个 token,去调用其他接口
7、埋点实现(react-master 下)
- 新建目录
bash
mkdir src/utils/lib && touch src/utils/lib/track.ts && touch src/utils/lib/async-track-queue.ts
- 安装依赖
pnpm add lodash
src/utils/lib/track.ts
编码
typescript
// 这个是埋点 API
import { AsyncTrackQueue } from "./async-track-queue";
export interface TrackQueue {
seqId: number;
id: number;
timestamp: number;
}
export interface UserTrackData {
type: string;
data: any;
}
// 思考 1:每次调用时是否立马发起请求?
// 答案 1:不一定,比如滚动了页面,那可能存在几十个埋点请求,所以应该先收集一波,然后统一发送。这样服务器的 QPS 会减少
export class BaseTrack extends AsyncTrackQueue<TrackQueue> {
private seq = 0;
/**
* 埋点请求收集
*
* @param data 用户轨迹数据
*/
track(data: UserTrackData) {
// 埋点请求收集
this.addTask({
id: Math.random(),
seqId: this.seq++,
timestamp: Date.now(),
...data,
});
}
/**
* 消费埋点请求任务队列
*
* @param data 任务队列数据,类型为任意类型数组
* @returns 返回一个 Promise,当 img 标签加载完成后 resolve 为 true
*/
comsumeTaskQuene(data: Array<TrackQueue>) {
return new Promise((resolve) => {
// 通过构建一个 img 标签,然后设置 src 属性,来发送请求
const image = new Image();
image.src = "http://localhost:3001/track?data=" + JSON.stringify(data);
console.log("[ comsumeTaskQuene data ] >", data);
image.onload = () => {
resolve(true);
};
});
}
}
src/utils/lib/sync-track-queue.ts
编码
kotlin
// 第二层:AsyncTrackQueue 是专门处理收集工作的
import { debounce } from "lodash";
interface RequiredData {
timestamp: number | string;
}
// 思考 2:如何收集?收集多少?怎么发请求?
export abstract class AsyncTrackQueue<T extends RequiredData> {
_queueData: Array<T> = [];
// 获取本次存储服务
private get storageService() {
return TaskQueueStorableHelper.getInstance();
}
private get queueData(): Array<T> {
return this.storageService.queueData;
}
private set queueData(data: Array<T>) {
this.storageService.queueData = data;
if (data.length) {
this.debounceRun();
}
}
// 添加任务
addTask(data: T | Array<T>) {
this.queueData = this.queueData.concat(data);
}
// 消费埋点请求任务队列,需要子类去实现
protected abstract comsumeTaskQuene(data: Array<T>): Promise<unknown>;
// 上报策略:当一段时间内,没有新增的任务时,可以去上报一波
// 通过 debounce 防抖,来控制上报频率
protected debounceRun = debounce(this.run.bind(this), 500);
private run() {
const currentDataList = this.queueData;
if (currentDataList.length) {
// 清空任务
this.queueData = [];
// 执行任务
this.comsumeTaskQuene(currentDataList);
}
}
}
// 思考 3:当还有数据未上报时,用户关闭了浏览器,那就会丢失一部分待上报的埋点数据,如何解决这个问题?
// 答案 3:使用 localStorage 存储,当用户关闭浏览器时,将数据存到 localStorage 中,下次打开浏览器时,再从 localStorage 中读取数据上报
class TaskQueueStorableHelper<T extends RequiredData = any> {
// 一个单例模式
private static instance: TaskQueueStorableHelper | null = null;
static getInstance<T extends RequiredData = any>() {
if (!this.instance) {
this.instance = new TaskQueueStorableHelper();
}
return this.instance;
}
private STORAGE_KEY = "track-queue";
protected store: any = null;
// 打开浏览器时,是要执行 constructor
constructor() {
const localStorageVal = localStorage.getItem(this.STORAGE_KEY);
if (localStorageVal) {
// 说明有待上报的数据,则存储一些
try {
this.store = JSON.parse(localStorageVal);
} catch (error: any) {
throw new Error(error);
}
}
}
get queueData() {
return this.store?.queueData || [];
}
set queueData(data: Array<T>) {
this.store = {
...this.store,
queueData: data.sort((a, b) => Number(a.timestamp) - Number(b.timestamp)),
};
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.store));
}
}
- 新建上报文件
bash
touch src/utils/lib/apis.ts
src/utils/lib/apis.ts
编码
typescript
import { BaseTrack, UserTrackData } from "./track";
// 性能上报
export class Performance {
static readonly timing = window.performance && window.performance.timing;
static init() {
if (!this.timing) {
console.warn("performance is not support");
}
window.addEventListener("load", () => {
const data = this.getTiming();
new BaseTrack().track(data);
});
}
static getTiming(): UserTrackData {
const timing = this.timing;
return {
type: "performance",
data: {
loadTime: timing.loadEventEnd - timing.navigationStart,
domReadyTime: timing.domComplete - timing.domLoading,
readyTime: timing.domContentLoadedEventEnd - timing.navigationStart,
requestTime: timing.responseEnd - timing.requestStart,
},
};
}
}
// 主动上报
const t = new BaseTrack();
export const sendLog = <T>(data: T) => {
t.track(data as T & UserTrackData);
};
- 找个地方使用下上报的,我在
react-master/src/pages/home/commandList/index.tsx
里面调用
- 再找个地方使用下上报的,我在
react-master/src/app.tsx
里面调用
8、总结
本篇文章从项目搭建开始讲解,讲了前端 React 项目的环境搭建:Webpack 配置、Babel 配置等一系列真实开发可用的;然后手动搭建 React 的路由体系,并使用 tailwindcss 仿照知乎进行了代码书写;最后讲了一些业务"亮点":use* API 的封装、极致的本地存储工具
之后深入组件库领域,基于 Rollup 自己搭建了一套构建逻辑
之后了解了前端微内核架构,并手写了 Webpack、Babel、postcss 插件
之后进入后端领域,基于 koa 手撸了一套装饰器模式的"MVC"架构,并且自己处理了项目打包,写了具有代表意义的登录鉴权的接口(其他接口自然也会了)
最后讲了前端埋点的一种实现方式,采用"分层设计",讲了埋点收集、本地存储、埋点上报等逻辑
所以以上的内容,有很多是可以作为"亮点"去包装一下的
本项目的代码已全部上传 github:github.com/MrHzq/react...
补充知识
什么是亮点?
具备一定的思考、技术难度、并实际解决了关键问题的事情
符合下面三个之一的可以称为"亮点"
- 基于业务,封装公共能力,解决业务中的标准问题,并推广使用。
-
- 为什么要封装?具体解决了什么问题?如何实现推广的?
- 基于工程化手段,解决流程或研发问题
-
- 比如:自研 Webpack 插件,解决 xxx 问题;提供 cli 脚手架解决项目创建问题等
- 创造性的弄了一个东西(这个行业之前没有的),并解决了某个问题
若是:基于 axios、vueRouter、防抖、节流封装 xxx 组件,这种已经不算什么亮点了。
构建知识
在 Webpack 的前端项目中,我们一般如下引入依赖
import A from "A";
const A = require("A");
那为什么可以这样写?为什么这两个写法都行?require 不是 node 端的写法吗?
解答:是构建工具来决定你的语法,你在 Webpack 里面这样写,这里的 import、require 与 esm、cjs 没有任何的关系,这样写为了让你写的更方便、好记,并且也是沿用了大家熟悉的 esm、cjs 规范写法。
Webpack 的本质就是"翻译",从你写的入口文件开始,找到所有的文件,每个不同类型的文件使用对应的"语言包"进行翻译,最后只输出一篇浏览器能看懂的文章。
常见"语言包":less 翻译为 css、react 翻译为 js、图片地址(./xx.png)翻译为最终的地址((./public/img/xx.png).
所以 Webpack 强大在于插件,你需要特定翻译什么,就可以自己去开发对应的语言包,装到 Webpack 上就能用
一般 Webpack 的产物为:.html、.js、.css,用 html 去 script 加载 js,用 link 加载 css
React 的闭包陷阱
从react hooks"闭包陷阱"切入,浅谈react hooks - 掘金
如何学习
流派划分:连接主义、符号主义
连接主义:吸收到新知识时,会用老的知识去连接它,让新知识不那么容易遗忘
符号主义:通过自己的逻辑,将新知识变大,这样也不容易遗忘
如何连接?
输出:找别人讨论、写文章、教别人等等(费曼技巧)。
在这个过程中,你会用你熟悉的知识(大球)去解释它(小球)
如何变大?
总结:将关键词记下来,确保看到这几个关键词后就能解释清楚
并且在不同的场合都温习一下(通勤、吃饭等)
当你看到一个知识时,脑海有印象但就是想不起来,则说明这个知识正在和你的老知识在连接,但就还差一些。所以就立马去看,把连接建立起来
结合运用:将新知识总结出关键词,自己能通过关键词解释清楚,让知识变大,然后去输出,用自己的语言描述一边,建立连接,最后时常温习关键词,记不清楚了则立马去看。
这样脑海里面的知识才能形成体系,否则只能死记硬背。
如何串行 Promise?
javascript
const PromiseArray = [
Promise.resolve(1),
Promise.resolve(2),
new Promise((resolve) => setTimeout(() => resolve(3), 1000)),
new Promise((resolve) => setTimeout(() => resolve(4), 2000)),
new Promise((resolve) => setTimeout(() => resolve(5), 1000)),
new Promise((resolve) => setTimeout(() => resolve(6), 1000)),
];
// ⭐️ 核心代码在这
const lastPromise = PromiseArray.reduce((prev, next) =>
prev.then((res) => {
console.log("[ prev promise res ] >", res);
return next;
}),
);
// 最后一个的结果,需要等待所有 promise 都执行完然后自行 then
lastPromise.then((res) => {
console.log("[ last promise res ] >", res);
});
// 打印结果
[ prev promise res ] > 1
[ prev promise res ] > 2
[ prev promise res ] > 3
[ prev promise res ] > 4
[ prev promise res ] > 5
[ last promise res ] > 6