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
相关推荐
Jiaberrr几秒前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy25 分钟前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白25 分钟前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、26 分钟前
Web Worker 简单使用
前端
web_learning_32128 分钟前
信息收集常用指令
前端·搜索引擎
tabzzz36 分钟前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百1 小时前
Vuex详解
前端·javascript·vue.js
滔滔不绝tao1 小时前
自动化测试常用函数
前端·css·html5
码爸1 小时前
flink doris批量sink
java·前端·flink
深情废杨杨1 小时前
前端vue-父传子
前端·javascript·vue.js