普通视图

发现新文章,点击刷新页面。
昨天 — 2025年7月1日首页

入坑node.js全面指南(三)

作者 温暖前端
2025年6月30日 21:15

前两篇文章详细讲解了node.js的知识点,本篇文章来实现一个经典的全栈待办事项(Todo List)应用,采用前后端分离架构。后端基于 Node.js + Express + TypeScript,前端基于 Vue 3 + TypeScript,数据持久化采用 MongoDB。项目结构清晰,适合学习和实践全栈开发。

项目源码地址链接: todolist-express: 一个基于 Vue3 + TypeScript + Node.js + Express + MongoDB 的全栈 ToDoList 项目。

项目截图

image.png

image.png

image.png

一、后端技术栈与实现

1. 技术选型

  • Node.js:高性能的 JavaScript 运行环境,适合 I/O 密集型应用。
  • Express:简洁灵活的 Web 框架,便于快速搭建 RESTful API。
  • TypeScript:为 JavaScript 提供类型系统,提升代码可维护性和可读性。
  • Mongoose:MongoDB 的对象建模工具,简化数据库操作。

2. 目录结构

backend/
  ├── src/
  │   ├── app.ts         // Express 应用主入口
  │   ├── index.ts       // 启动服务
  │   ├── models/        // Mongoose 数据模型
  │   └── routes/        // 路由模块

3. 主要知识点

a) TypeScript 在后端的应用

  • 类型定义提升了开发效率,减少了运行时错误。
  • 接口(interface)用于定义数据结构,如 Todo、User。
import mongoose, { Schema, Document } from 'mongoose';

export interface ITodo extends Document {
  content: string;
  status: 'active' | 'completed' | 'deleted';
  priority: number;
  tags: string[];
  category: string;
  user: mongoose.Types.ObjectId;
  createdAt: Date;
  completedAt?: Date;
}

b) 路由与中间件

  • 路由模块化,routes/auth.ts 处理认证相关接口,routes/todo.ts 处理待办事项相关接口。
  • 使用中间件进行请求体解析、鉴权等操作。
// 获取所有 ToDo
router.get('/', auth, async (req: AuthRequest, res: Response) => {
  const todos = await Todo.find({ user: req.user.id, status: { $ne: 'deleted' } }).sort({ createdAt: -1 });
  res.json(todos);
});

c) Mongoose 数据建模

  • models/Todo.tsmodels/User.ts 定义了数据结构和约束。
  • 通过 Schema 约束字段类型、必填项、默认值等。

d) 用户认证

  • 通常采用 JWT(JSON Web Token)进行用户身份验证(可在 auth.ts 路由中实现)。
  • 注意密码加密存储(如 bcrypt),避免明文存储。
// 鉴权中间件
function auth(req: AuthRequest, res: Response, next: NextFunction) {
  const token = req.headers['authorization']?.split(' ')[1];
  if (!token) return res.status(401).json({ msg: '未登录' });
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    req.user = decoded;
    next();
  } catch {
    res.status(401).json({ msg: '无效token' });
  }
}

e) 错误处理

  • 统一的错误处理机制,保证接口返回一致的错误格式,便于前端处理。

  • Vue 3:响应式、组件化的前端框架。

  • TypeScript:提升代码可维护性。

  • Vite:新一代前端构建工具,极快的热更新体验。

  • Pinia/Vuex:状态管理。

二、前端技术栈与实现

2. 目录结构

frontend/
  ├── src/
  │   ├── api/           // 封装后端请求
  │   ├── components/    // 复用组件
  │   ├── store/         // 状态管理
  │   ├── views/         // 页面视图
  │   └── router/        // 路由配置

3. 主要知识点

a) 组件化开发

  • 复用性强,易于维护,如 TodoItem.vueTodoList.vue
  • 父子组件通信(props、emit)、兄弟组件通信(状态管理)。

b) 状态管理

  • store/todo.tsstore/user.ts 管理全局状态,避免多层组件传参。
  • 响应式数据驱动视图更新。

c) 路由管理

  • router/index.ts 配置页面路由,实现页面跳转与权限控制。

d) API 封装

  • api/request.ts 封装 axios/fetch,统一处理请求和响应。
  • api/todo.tsapi/user.ts 封装具体业务接口,便于维护。

e) 样式与动画

  • assets/animate.css 提供动画效果,提升用户体验。

三、项目运行以及MongoDB启动指南

项目运行

  1. 安装 Node.js、MongoDB
  2. 分别进入 backend、frontend 目录执行 npm install
  3. 启动 MongoDB 服务
  4. 启动后端:npm run dev
  5. 启动前端:npm run dev
  6. 访问前端页面(如 http://localhost:5173)

MongoDB启动指南

  • 访问 MongoDB 官方下载页面
  • 选择 Windows 平台,版本建议选择最新稳定版(如 6.0)
  • 下载 MSI 安装程序(64 位)

在mongodb官网安装mongodb后,启动命令行输入mongod,仍旧显示 image.png

打开电脑的高级系统设置,配置mongodb的环境变量,将mongodb的bin目录添加到环境变量中

image.pngimage.png

启动mongodb服务的时候提示

image.png

可以通过在mongod/bin目录下cmd,输入mongod --dbpath D:\app\MongoDB\data启动数据库服务

数据库启动后,可以通过mongodb compass进行数据库连接

image.png

数据库启动并连接成功后,再运行服务端代码,服务端代码运行端如下图所示,则代表服务端代码成功运行

image.png

昨天以前首页

Express 子路由、静态资源与错误处理详解

作者 coderklaus
2025年6月30日 18:12

在实际开发中,随着项目规模扩大,路由和中间件的数量会迅速增长。如果所有路由都写在同一个文件里,代码将变得难以维护。Express 提供了子路由(Router)中间件机制,可以帮助我们实现模块化开发、静态资源托管和高效的错误处理。下面详细介绍这些内容。

一、子路由(Router)

1. 为什么要使用子路由?

当路由数量较多时,推荐将不同业务模块的路由拆分到不同文件或目录(如 routerroutes 文件夹),每个模块对应一个子路由。这样可以做到高内聚、低耦合,提升项目可维护性和可扩展性。

2. express.Router 的原理

express.Router() 本质上是一个“迷你版”的 Express 应用,拥有完整的中间件和路由系统。你可以为它单独定义路由、中间件、参数处理等,最后将其挂载到主应用的某个路径下。

3. 子路由的使用示例

import express from 'express'

const app = express()

// 创建子路由对象
const userRouter = express.Router()

// 定义子路由
userRouter.get('/list', (req, res) => {
  // req.url 是子路由基准路径之后的部分
  // req.baseUrl 是子路由的基准路径
  // req.originalUrl 是原始请求路径
  res.json({
    url: req.url,
    path: req.path,
    baseUrl: req.baseUrl,
    originalUrl: req.originalUrl,
    params: req.params,
    query: req.query
  })
})

userRouter.post('/create', (req, res) => res.end(req.url))
userRouter.patch('/:id', (req, res) => res.end(req.url))

// 挂载子路由到主应用
app.use('/users', userRouter)

app.listen(3000)

小结:

  • 子路由路径必须以 / 开头。
  • 请求到 /users/list 时,userRouter 内的 /list 路由会被匹配。
  • 这样拆分后,主文件只需负责路由挂载,具体逻辑分散在各自模块,结构清晰。

二、静态资源托管

Web 项目通常需要对外暴露静态文件(如图片、CSS、JS、上传文件等)。Express 提供了内置中间件 express.static(),可将指定目录设为静态资源目录。

1. 使用方法

import express from 'express'

const app = express()

// 设置静态资源目录(可多次调用,支持多个目录)
app.use(express.static('./uploads'))
app.use(express.static('./build'))

app.listen(3000)
  • 访问 http://localhost:3000/头像.jpg,会自动查找 uploadsbuild 目录下的 头像.jpg 文件。
  • 静态资源中间件会优先返回第一个匹配到的文件。

建议:
将上传目录、前端打包目录等都设置为静态资源目录,便于统一管理和访问。

三、错误处理机制

后端服务不可避免会遇到各种异常情况。Express 提供了灵活的错误处理机制,主要包括两种方式:

1. HTTP 状态码 + 错误信息

这是最常见的处理方式。直接返回标准 HTTP 错误码(如 404、500),并在响应体中说明错误原因。

res.status(404).send('资源未找到')
  • 优点:符合 HTTP 标准,前端可直接根据状态码判断。
  • 缺点:有时无法满足复杂业务需求。

2. 业务自定义错误码

有些团队喜欢所有接口都返回 200 状态码,通过响应体中的自定义 code 字段区分业务成功/失败。

res.json({
  code: 404,
  message: '用户不存在',
  data: null
})
  • 优点:前后端约定灵活,业务错误粒度可控。
  • 缺点:不符合 RESTful 风格,需前端配合解析。

3. 错误处理中间件

Express 允许通过特殊格式的中间件统一处理错误(即错误处理中间件)。只要在 next() 里传递参数,Express 就会跳过后续普通中间件,直接进入错误处理中间件。

import express from 'express'

const app = express()

app.use(express.json())

app.post('/login', (req, res, next) => {
  const { name, password } = req.body
  if (!name || !password) {
    // 传递错误码到错误处理中间件
    next(-1001)
  } else {
    res.send('登录成功')
  }
})

// 错误处理中间件(参数比普通中间件多一个 err)
app.use((err, req, res, next) => {
  switch (err) {
    case -1001:
      res.status(400).send('用户名或密码不能为空')
      break
    case -1002:
      res.status(401).send('用户名或密码错误')
      break
    case -1003:
      res.status(404).send('用户名不存在')
      break
    default:
      res.status(500).send('服务器内部错误')
  }
})

app.listen(3000)

建议:

  • 错误处理中间件应放在所有路由之后。
  • 可结合日志系统,记录错误详情,便于排查问题。
  • 复杂项目可对错误码、错误信息进行统一管理。

入坑node.js全面指南(二)

作者 温暖前端
2025年6月30日 08:21

1. Express.js 架构哲学

作为 Node.js 生态中应用最广泛的 Web 框架,Express.js 以"最小化设计原则"为核心,通过模块化架构实现灵活扩展。

核心设计理念

  • 简约而不简单:仅提供 Web 开发的基础功能,其他能力通过中间件扩展
  • 中间件驱动架构:采用洋葱模型实现请求处理流水线
  • 无强制约束:不限定项目结构,开发者拥有完全自由
  • 渐进式增强:可根据需求添加功能模块,避免过度设计

关键架构组件

  • 中间件系统:请求处理流水线,每个中间件可访问请求/响应对象
  • 路由分发器:基于 HTTP 方法和 URL 的高效路由匹配
  • 视图引擎接口:支持多种模板引擎(EJS、Pug等)
  • 扩展接口:通过 app.set()/app.get() 管理应用配置

安装

npm install express

基本应用结构

const express = require('express');
const app = express();
const port = 3000;

// 中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 路由
app.get('/', (req, res) => {
  res.send('Hello Express!');
});

// 启动服务器
app.listen(port, () => {
  console.log(`服务器运行在 http://localhost:${port}`);
});

2. 中间件

中间件类型

  1. 应用级中间件:绑定到 app 实例

    // 记录请求时间的应用级中间件
    app.use((req, res, next) => {
      req.requestTime = Date.now();
      console.log(`请求时间: ${req.requestTime}`);
      next(); // 必须调用next传递控制权
    });
    
  2. 路由级中间件:绑定到路由实例

    const router = express.Router();
    router.use((req, res, next) => {
      console.log('路由中间件执行');
      next();
    });
    
  3. 错误处理中间件:四个参数的特殊中间件

    app.use((err, req, res, next) => {
      console.error(err.stack);
      res.status(500).send('服务器错误!');
    });
    
  4. 内置中间件

    app.use(express.json()); // 解析JSON请求体
    app.use(express.static('public')); // 静态文件服务
    
  5. 第三方中间件

    const helmet = require('helmet');
    app.use(helmet()); // 安全头部设置
    

中间件执行顺序

Express 中间件按照声明顺序执行,理解这一点对于正确配置应用至关重要:

  1. 请求到达服务器
  2. 按顺序执行匹配的中间件
  3. 每个中间件可以修改 req/res 对象
  4. 中间件通过 next() 传递控制权
  5. 当某个中间件发送响应时,链终止

3. 路由系统深度解析

路由基础

// 基础路由
app.get('/products', (req, res) => {
  res.json([{ id: 1, name: '手机' }]);
});

// 路由参数
app.get('/products/:id', (req, res) => {
  const id = req.params.id;
  res.json({ id, name: `商品${id}` });
});

// 查询参数
app.get('/search', (req, res) => {
  const q = req.query.q;
  res.send(`搜索: ${q}`);
});

4. 请求与响应处理

请求对象 (req)

  • req.params:路由参数
  • req.query:查询字符串参数
  • req.body:请求体内容(需要中间件解析)
  • req.cookies:客户端 cookies
  • req.headers:HTTP 请求头
  • req.ip:客户端 IP 地址

响应对象 (res)

  • res.status(code):设置状态码
  • res.send(body):发送响应
  • res.json(obj):发送 JSON 响应
  • res.render(view, locals):渲染模板
  • res.redirect(path):重定向
  • res.set(field, value):设置响应头
// 综合使用示例
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  
  if (!username || !password) {
    return res.status(400).json({ error: '用户名和密码必填' });
  }
  
  // 认证逻辑...
  res.cookie('token', 'abc123', { httpOnly: true });
  res.redirect('/dashboard');
});

5. Express vs Koa vs Node 深度对比

5.1 Express 与 Node.js 的关系

关系解析

  • Node.js 是运行时环境,提供 HTTP 模块等基础能力
  • Express 是基于 Node.js HTTP 模块的封装框架
  • Express 简化了路由、中间件等常见 Web 开发任务

5.2 Express 与 Koa 的深度对比

  • 设计理念:Express.js 设计较为灵活,提供了丰富的功能和插件,适合快速开发各种类型的 Web 应用。而 Koa 是由 Express.js 的原班人马打造,它更注重简洁和优雅,采用了更现代化的异步编程方式(如 async/await),并且没有捆绑任何中间件,开发者可以根据需求自由选择和组合中间件。

  • 中间件机制:Express.js 的中间件采用传统的回调函数形式,中间件之间通过next()方法传递控制权。在处理复杂的异步操作时,可能会出现回调地狱的问题。Koa 的中间件基于 async/await 语法,通过await next()实现中间件的执行和传递,代码更加简洁、易读,避免了回调地狱。

  • 错误处理:在 Express.js 中,错误处理通常通过在中间件或路由处理函数中捕获错误,并传递给错误处理中间件。而 Koa 使用 try/catch 块或者在 async 函数中返回错误,通过onerror事件来统一处理错误,错误处理更加直观和方便。

Express vs Koa 核心差异

特性 Express Koa
中间件模型 线性链式调用 洋葱模型
异步处理 回调函数 原生Async/Await
错误处理 集中式错误中间件 Try/Catch捕获
路由系统 内置Router 需koa-router
请求/响应封装 原生对象扩展 统一Context对象
体积大小 约4.5MB 约0.6MB

6. Express 实战案例:RESTful API 实现

const express = require('express');
const app = express();
app.use(express.json());

// 内存数据库
let users = [
  { id: 1, name: '张三', email: 'zhang@example.com' },
  { id: 2, name: '李四', email: 'li@example.com' }
];

// 获取所有用户
app.get('/api/users', (req, res) => {
  res.json({
    status: 'success',
    count: users.length,
    data: users
  });
});

// 创建新用户
app.post('/api/users', (req, res) => {
  const { name, email } = req.body;
  const newUser = {
    id: users.length + 1,
    name,
    email
  };
  
  users.push(newUser);
  
  res.status(201).json({
    status: 'success',
    data: newUser
  });
});

});

// 全局404处理
app.use((req, res) => {
  res.status(404).json({
    status: 'error',
    message: '路由不存在'
  });
});

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    status: 'error',
    message: '服务器内部错误'
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
});

7. 总结

Express.js的优势:

  1. 简洁核心+丰富扩展:保持核心轻量,通过中间件提供无限扩展
  2. 渐进式采用路径:从简单路由到复杂应用均可胜任
  3. 社区驱动生态:庞大的中间件生态系统解决各种场景需求
  4. 文档与稳定性:优秀的文档质量和稳定的API设计

最佳实践建议

  1. 使用 express.Router 实现路由模块化
  2. 中间件按功能拆分为独立模块
  3. 统一错误处理机制
  4. 使用 helmet 增强安全性
  5. 结合 morgan 记录访问日志
  6. 使用 express-validator 进行输入验证

✨✨ECMAScript2025 有哪些新特性?✨✨

2025年6月28日 23:31

2025 年 6 月 25 日,第 129 届 Ecma 大会批准了 ECMAScript 2025 语言规范。

本文将介绍 ECMAScript 2025 新增的语法和特性。

导入属性和 JSON 模块

导入属性提供了一个新的导入语法,可以用来指导入的模块类型,为导入非 JavaScript 模块提供了支持,第一个被支持的类型就是 JSON 模块。

// 静态导入
import configData1 from './config-data.json' with { type: 'json' };

// 动态导入
const configData2 = await import(
  './config-data.json', { with: { type: 'json' } }
);

with 关键字后面跟一个对象,这个对象包含 type 属性,用来指定导入的模块类型。

迭代器辅助方法

ECMAScript 2025 引入了 Iterator 类:

  • Iterator.from() 方法可以创建一个迭代器。
  • Iterator.prototype 身上有很多迭代器的辅助方法,可以用来操作迭代器。

下面这些方法和同名的数组方法类似:

  • 返回新的迭代器

    • iterator.filter(filterFn)
    • iterator.map(mapFn)
    • iterator.flatMap(mapFn)
  • 返回 boolean

    • iterator.some(someFn)
    • iterator.every(everyFn)
  • 返回其他值

    • iterator.find(findFn)
    • iterator.reduce(reduceFn, initialValue)
  • 无返回值

    • iterator.forEach(forEachFn)

还有一些迭代器独有的方法:

  • iterator.drop():返回一个新的迭代器,跳过指定数量的元素。
  • iterator.take():返回一个新的迭代器,只取指定数量的元素。
  • iterator.toArray():返回一个数组,包含迭代器的所有元素。

任何可迭代数据都可以使用上面的迭代器方法。迭代器方法不创建中间数组,也不增量计算数据,对于大量的数据迭代很有用:

迭代器方法与数组方法不同的是,使用迭代器方法时,所有方法都应用于迭代器的第一个值,然后应用于第二个值,以此类推。使用数组方法时,每一个方法都应用于所有值。其实可以理解为迭代器方法内部自动调了一次 next() 方法。

Set 的新方法

  • 组合 Set
    • Set.prototype.intersection(otherSet):返回一个新的 Set,包含两个 Set 的交集。
    • Set.prototype.union(otherSet):返回一个新的 Set,包含两个 Set 的并集。
    • Set.prototype.difference(otherSet):返回一个新的 Set,包含第一个 Set 中不属于第二个 Set 的元素。
    • Set.prototype.symmetricDifference(otherSet):返回一个新的 Set,包含两个 Set 的差集。
  • 判断 Set 的关系
    • Set.prototype.isSubsetOf(otherSet):返回一个 boolean,表示当前 Set 是否是另一个 Set 的子集。
    • Set.prototype.isSupersetOf(otherSet):返回一个 boolean,表示当前 Set 是否是另一个 Set 的超集。
    • Set.prototype.isDisjointFrom(otherSet):返回一个 boolean,表示当前 Set 是否与另一个 Set 无交集。
const set1 = new Set([1, 2, 3]);
const set2 = new Set([2, 3, 4]);

const intersection = set1.intersection(set2); // Set(2) {2, 3}
const union = set1.union(set2); // Set(4) {1, 2, 3, 4}
const difference = set1.difference(set2); // Set(1) {1}
const symmetricDifference = set1.symmetricDifference(set2); // Set(2) {1, 4}

const isSubsetOf = set1.isSubsetOf(set2); // false
const isSupersetOf = set1.isSupersetOf(set2); // false
const isDisjointFrom = set1.isDisjointFrom(set2); // false

RegExp.escape()

RegExp.escape() 方法可以用来转义文本,使其可以在正则表达式中使用。

function removeUnquotedText(str, text) {
  const regExp = new RegExp(`(?<!“)${RegExp.escape(text)}(?!”)`, 'gu');
  return str.replaceAll(regExp, '•');
}

console.log(removeUnquotedText('“yes” and yes and “yes”', 'yes')); //  '“yes” and • and “yes”'

正则表达式的模式修饰符

正则表达式模式修饰符可以将正则标志应用于需要正则匹配的字符的某个部分。

例如

console.log(/^x(?i:HELLO)x$/.test('xHELLOx')); // true
console.log(/^x(?i:hello)x$/.test('xHELLOx')); // true
console.log(/^x(?i:HELLO)x$/.test('XhelloX')); // false

i 表示忽略大小写,上面的例子仅将i 作用于 hello 字符串,字符 x 则是不受影响。

重复命名的捕获组

命名捕获组(Named Capture Group) 是正则表达式中用来给捕获的子串起一个名字,便于后续读取。

const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result = regex.exec('2025-06-28');
console.log(result.groups); // { year: '2025', month: '06', day: '28' }

重复命名捕获组则可以给多个捕获组起相同的名字,这样就可以在匹配时,一次性获取多个捕获组的值。

const RE = /(?<chars>a+)|(?<chars>b+)/v;
console.log(RE.exec('aaa').groups); // { chars: 'aaa' }
console.log(RE.exec('bbb').groups); // { chars: 'bbb' }

Promise.try()

Promise.try() 可以创建一个包含非异步功能的 Promise 链。

参数传递一个回调函数:

  • 如果函数抛出异常,将返回带有异常的 reject
  • 如果函数返回一个值,Promise.try() 将该值解析为 Promise(如果该值已经是 Promise,则直接返回)。
function computeAsync() {
  return Promise.try(() => {
    const value = syncFuncMightThrow(); // (A)
    return asyncFunc(value); // (B)
  });
}

如果我们在 A 行调用的同步函数抛出异常,Promise.try() 捕获该异常并将其转换为被拒绝的 Promise

支持 16 位的浮点数

  • Math.f16round()
  • 类型化数组的新元素类型
    • Float16Array
    • DataView.prototype.getFloat16()
    • DataView.prototype.setFloat16()

定时同步订单信息

作者 Jimmy
2025年6月28日 19:34

有这么一个场景👇

由于网络的问题,我们创建订单并付费成功了。但是,在调用同步订单接口的时候,出现了网络的问题等不可抗拒的因素而同步不成功。这个时候,就会造成订单的状态不正确,那么,我们有什么措施解决或者减少这种情况的?

嗯~

我们可以采用下面的方案:

  1. 使用浏览器存储,将订单信息存储在 IndexedDB
  2. 将信息存储在电脑本地

使用 IndexedDB 在之前的文章中,我们已经提及 前端使用 IndexedDB 存储

这里,我们采取第二种方法 - 将信息存储在电脑磁盘。下面是我们的思路👇

这里我们使用的开发环境 👇

版本:Windows 10 专业版

版本号:22H2

电脑上拆分了多个磁盘 C, D, E, F

  1. 在同步订单的接口执行前,先将订单机器信息记录到本地
  2. 在应用的首页,每隔 * 分钟轮询本地的记录,同步到远程服务
  3. 如果同步成功,则删除本地的信息记录;同步失败的订单下次轮询

👌。思路有了,我们来实现下,这里假设你已经安装了对应的 Node 环境。

写入文件

我们将订单信息记录在 D 盘下:

export const LOCAL_DISK_ORDER_INFO_PATH = "d:\\app-name\order-info"; // 订单信息存放的路径

export interface DiskSaveOrderInfo {
  billNo: string;
  billPayTime: string;
  payWay: string
  // ... other properties
}

我们将执行下面的操作进行保存。

// 获取订单的文件路径
export const getOrderFilePath = (orderId: string) => {
  return path.join(LOCAL_DISK_ORDER_INFO_PATH, `${orderId}.json`); // 存储为 json 文件
};

// 保存订单到文件
export const saveOrderToFile = async (orderId: string, order: DiskSaveOrderInfo) => {
  const filePath = getOrderFilePath(orderId);
  // 创建文件路径
  if (!fs.existsSync(LOCAL_DISK_ORDER_INFO_PATH)) {
    try {
      // 创建对应的文件夹
      await fs.promises.mkdir(LOCAL_DISK_ORDER_INFO_PATH, { recursive: true });
    } catch (error) {
      console.error("fs.promises.mkdir", "创建目录失败", error);
    }
  } else {
    fs.writeFileSync(filePath, JSON.stringify(order, null, 2)); // 写入数据
  }
};

读取文件

我们从磁盘中读取保存的订单信息👇

// 从文件中读取订单 - 读取单个文件
export const readOrderFromFile = (orderId: string): DiskSaveOrderInfo | null => {
  const filePath = getOrderFilePath(orderId);
  try {
    const data = fs.readFileSync(filePath, "utf8");
    return JSON.parse(data);
  } catch (error) {
    console.error("fs.readFileSync", `readOrderFromFile - 读取订单 ${orderId} 失败`, error);
    return null;
  }
}

// 读取指定文件夹下的所有订单信息
export const getAllOrderFiles = (): DiskSaveOrderInfo[] => {
  try {
    const orderFiles = fs.readdirSync(LOCAL_DISK_ORDER_INFO_PATH);
    const orderFilesData = orderFiles
      .filter((file) => file.endsWith(".json"))
      .map((file) => {
        // 获取订单号
        const orderId: string = file.replace(/\.json$/, "");
        const data: DiskSaveOrderInfo | null = readOrderFromFile(orderId);
          return data;
        });
    const result = orderFilesData.filter((data) => data !== null);
    return [...result] as DiskSaveOrderInfo[];
  } catch (error) {
    console.error("getAllOrderFiles", "获取所有订单文件失败", error);
    return [];
  }
}

删除文件

我们删除磁盘上保存的订单,如下👇

// 删除订单文件
export const deleteOrderFile = (orderId: string): boolean => {
  const filePath = getOrderFilePath(orderId);
  try {
    if (fs.existsSync(filePath)) {
      fs.unlinkSync(filePath);
        return true;
    }
    return false;
  } catch (error) {
    console.error("fs.unlinkSync", `删除订单 ${orderId} 文件失败`, error);
    return false;
  }
};

执行保存

在同步订单接口之前,我们将相关的信息记录下

export function usePayDone() {
  const billNo = useStore(SelectorBillNo);
  
  const notifyPaySuccessFn = async (payWay: PayWayEnum) => {
    try {
      // 保存订单信息到本地
      await saveOrderToFile(billNo, {
        billNo,
        billPayTime: `${new Date().getTime()}`,
        payWay
      })
    } catch (error) {
      console.error("saveOrderToFile()", `保存订单 ${billNo} 信息到本地失败`, error);
    }
    // 执行同步订单到远程的代码 ...
  }
  
  return {
    notifyPaySuccessFn
  }
}

同步到远程

我们将本地的订单信息读取出来,然后依次向远程发起请求同步👇

// 定时同步本地订单的信息 - 这里可以自行实现是间隔多久触发下面的代码
export const useSyncOrders = () => {
  const handleSyncOrderFn = async () => {
    // 获取本地磁盘所有的订单信息
    const orderFiles = getAllOrderFiles();
    
    if (orderFiles.length === 0) {
      return Promise.resolve();
    }
    
    const syncPromises = orderFiles.map((file) => {
      return new Promise(async (resolve, reject) => {
        try {
          // 同步订单信息接口
          await postNotifyPaySuccess({
            billNo: file.billNo,
            payWay: file.payWay,
          });
          // 如果同步成功,则删除订单
          deleteOrderFile(file.billNo);
          resolve(true);
        } catch (error){
          console.error("deleteOrderFile()", `删除本地订单 ${file.billNo} 失败`, error);
          reject(error);
        }
      });
    });
    
    return Promise.all(sysnPromises);
  }
  
  return {
    hanleSyncOrderFn,
  };
}

至此,我们已经完成了订单信息的存储,订单的读取和订单的同步。

拜拜,Happy Coding!

参考

❌
❌