5-1 React 实战之从零到一的后端项目、埋点 SDK(完)

第一篇:# 5-1 React 实战之从零到一的项目环境搭建(一)

第二篇:# 5-1 React 实战之从零到一的项目开发(二)

接上文:# 5-1 React 实战之从零到一的组件库、插件开发(三)

6、后端项目搭建(koa 框架)

nodejs 后端项目使用场景:BFF(backend-for-fontend 给前端用的后端)、内部小系统等

一、初始化配置

  1. 创建文件夹(react-actual-combat 下)
arduino 复制代码
mkdir packages/apps/back-end
  1. 初始化项目
bash 复制代码
cd packages/apps/back-end && pnpm init
  1. 安装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
  1. 安装dependencies依赖(apps/back-end 下)
csharp 复制代码
pnpm add jsonwebtoken koa koa-bodyparser koa-router
  1. 编写 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,
		}),
	],
};
  1. 编写 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 }]
	]
}
  1. 新建入口文件src/index.js
arduino 复制代码
mkdir src && touch src/index.js

// 写如下代码:
import { random } from "./test";

console.log("[ hello word ] >", random);

export default random;
  1. 新建测试文件src/test.js
arduino 复制代码
touch src/test.js

// 写如下代码:
export const random = Math.random();
  1. 本地运行下src/index.js
scss 复制代码
node src/index.js

// 会发现报错,因为代码里面写了 esm 的 import、export 语法
// 但我们的 node环境或 package.json 没有指定支持该语法,所以会报错
  1. package.json新增build命令,然后运行pnpm build
javascript 复制代码
{
  ......
  
 "scripts": {
    "test": "echo "Error: no test specified" && exit 1",
    "build": "rollup --config rollup.config.js" // ++++++
  },

  ......
}
  1. 打包后的文件如下(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;

}));
  1. 然后可以运行下node dist/bundle.js

二、本地环境搭建

初始化完项目后,可以发现构建产物是能直接运行的,那可以这样处理本地开发

  1. 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", // ++++++`
  },

  ......
}
  1. 运行pnpm start,这样每次更改代码后,就会自动打包
  2. 新起终端,运行nodemon dist/bundle.js,没有的话全局安装下npm install -g nodemon,这样每次变更时可自动运行
  3. 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" // ++++++
  },

  ......
}

三、练一下手

  1. 删除之前的构建测试代码,新建空的入口文件
bash 复制代码
rm src/index.js src/test.js && touch src/index.js
  1. 项目保持运行pnpm dev
  2. 先写个 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}`);
});
  1. 命令行里面点击网址,可以看到页面

访问 http://localhost:3001/list,也能看到数据

四、实际开发

后端项目,一般是基于 MVC 形式来组织代码的

所以我们的后端项目也会进行分层创建文件夹:controllers、services

核心技术:基于TS 的装饰器去组装我们的代码

  1. 新建controllers文件夹
bash 复制代码
mkdir src/controllers && touch src/controllers/book.js
  1. 新建辅助函数:装饰器、等等
bash 复制代码
mkdir src/utils && touch src/utils/decorator.js
  1. 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 = "") {}
  1. 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,也能生成路由

// ⭐️ 但核心的在于如何实现达到更高的扩展性、稳定性
  1. 完善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);
	};
}
  1. 新建src/controllers/index.js,集中导出controllers下面的文件
javascript 复制代码
touch src/controllers/index.js

// 写如下代码:
import BookController from "./book.js";

export default [BookController];
  1. 现在这几个文件还毫无关系,所以我们简单点强行关联(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}`);
});
  1. 页面效果如下:

(一) 处理跨域

首先跨域是浏览器限制的,为了网页的安全。

所以我们本地开发前端时,前端浏览器直接访问服务器会出现跨域,一般是本地加个 devServer 配置就能解决,那是因为服务端直接无跨域的说法。

当我们加了 devServer 后,浏览器就直接访问 devServer,然后 devServer 再去访问服务端,这样就走通了。

若我们是服务端,则可以通过 cors(跨源资源共享) 解决

  1. 更改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. 模拟一下

本次通过模拟生成来演示

  1. 创建文件
bash 复制代码
touch src/utils/mockJWT.js
  1. 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 模块

  1. 新建文件
bash 复制代码
touch src/utils/jwt.js
  1. 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,
				};
			}
		}
	};
  1. index.js里面进行app.use注册
javascript 复制代码
import { jwtVerify } from "./utils/jwt.js";


// ......


// 使用 jwt 验证中间件
app.use(jwtVerify(["/", "/api/user/login", "/api/user/register"]));
  1. 刷新下页面,就能看到无法访问了,因为没带token
  1. 新增user模块
bash 复制代码
touch src/controllers/user.js && mkdir src/services && touch src/services/user.js
  1. 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 = "登录成功";
	}
}
  1. src/services/user.js初始化编码
arduino 复制代码
export default class UserService {}
  1. src/controllers/index.js引入
javascript 复制代码
import BookController from "./book.js";
import UserController from "./user.js"; // ++++++

export default [BookController, UserController];
  1. 因为是post请求,就只有打开postman调用,发现能调通
  1. 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 };
	}
}
  1. 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,
			};
		}
	}
}
  1. 完善一下index.js,支持body参数的获取
  1. 重新启动下后端项目,然后用postman调用下,正常 ok 了

  1. 然后拿着这个 token,去调用其他接口

7、埋点实现(react-master 下)

  1. 新建目录
bash 复制代码
mkdir src/utils/lib && touch src/utils/lib/track.ts && touch src/utils/lib/async-track-queue.ts
  1. 安装依赖pnpm add lodash
  2. 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);
			};
		});
	}
}
  1. 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));
	}
}
  1. 新建上报文件
bash 复制代码
touch src/utils/lib/apis.ts
  1. 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);
};
  1. 找个地方使用下上报的,我在react-master/src/pages/home/commandList/index.tsx里面调用
  1. 再找个地方使用下上报的,我在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
相关推荐
崔庆才丨静觅39 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax