前后端对接: ESM配置与React Router

昨天一整天都在折腾项目前后端对接,后端的配置、数据库连接,全程状态就是一头雾水,从工程架构、环境配置到路由数据流,磕磕绊绊踩了无数坑。

今天把涉及到的知识,昨天一股脑生成的ai代码都读了一遍,相关文档也看了一遍。稍微明晰了一些。

所以写下这篇博客来复盘自己的开发过程,作为个人反思。

一、架构设计、ESM模块环境配置

最开始上手,我先重新梳理了一遍工程架构,光是琢磨后端服务怎么落地就纠结了好久...

原本打算做基于本地文件夹的存储架构(类似typora,支持本地修改且本地磁盘文件同步变更),做着做着发现逻辑不通,原本是打算做在线的web的。

改成了云端存储文档的模式,也重新定义了整个产品的核心业务路径。

接着开始做后端数据库配置,本以为之前做过类似配置能轻松搞定,结果又遇到了CommonJS和ES Module兼容这个问题上。

  • ESM与CommonJS核心区别

CommonJS是旧版规范,输出值的拷贝、运行时加载;ES Module是现代主流规范,输出值的引用、编译时加载,这是JS生态从CommonJS全面迁移到ES Modules的历史原因。

我简单地认为最大的区别是显式且静态地导入import依赖,是否支持tree shaking ,把js代码编译模块的依赖关系放在黑盒还是透明环境下。ESM的性能更好,支持tree shaking ,把"死代码"跳过编译过程,浏览器可以拿到比较小的bundle,对于网络带宽压力更小,首屏时间更快。(应该可以这么理解)

  • CommonJS 是动态的、运行时的:它的 require 是在代码执行到那一行时才去加载模块,导致依赖关系是'黑盒'的。因此它只能输出值的拷贝,且无法支持 Tree Shaking,因为它不知道你会用到哪些代码。
  • ESM 是静态的、编译时的:它的 import/export 是语法层面的,在代码解析阶段就能构建出完整的依赖关系图。这使得它能够输出值的引用(动态绑定),并且为打包工具提供了Tree Shaking 的基础------只打包真正用到的代码。

正是由于 ESM 的静态性带来了更小的打包体积(减少网络开销)和更好的运行时性能,它才成为了现代 JavaScript 生态的主流标准。"

还有一个细节,比较反直觉,明明文件是TS格式,用import导入的时候,必须写上.js后缀才能跑通,不写就直接报错,我当时完全不理解,这些错误都是黑盒,我不知道为什么错,怎么修复,只能硬着头皮做。

  • TS导入必须写.js后缀

TS在node next模块解析模式下,只会识别编译后的真实运行文件,源码是.ts格式,实际运行的是.js文件,所以import导入时必须补齐.js后缀,不然解析器找不到对应文件。

浏览器读到的其实就是js,尽管磁盘里只有ts原文件,但是源码时能认出来的对应是ts,只要是导实际运行的js

先把package.json里的type字段改成module,告诉Node和打包工具当前项目用ESM规范;接着在script脚本里配置dev的模块路径,把模块模式改成node next;另外还得把tsconfig.json里的target改成ESNext,适配现代语法。

json 复制代码
{
  "name": "server",
  "type": "module",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
   "dev": "node --loader ts-node/esm --watch src/index.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "packageManager": "pnpm@10.28.2",
  "dependencies": {
    "cors": "^2.8.6",
    "dotenv": "^17.3.1",
    "express": "^5.2.1",
    "mongoose": "^9.3.0"
  },
  "devDependencies": {
    "@types/cors": "^2.8.19",
    "@types/express": "^5.0.6",
    "@types/mongoose": "^5.11.97",
    "@types/node": "^24.11.0",
    "nodemon": "^3.1.14",
    "ts-node": "^10.9.2",
    "ts-node-dev": "^2.0.0",
    "tsx": "^4.21.0",
    "typescript": "~5.9.3"
  }
}

二、自顶向下梳理数据流

我认为的一个大概流程:

前端发起请求→打包数据传给后端→后端入口接收→路由分发到对应controller→controller写入数据库

我先写好了ui按钮,哈哈哈最终自己能独立做的还是只会切图()。。

数据模型这块用Mongoose Schema来定义,先把schema设计好、数据模型确定下来。

Mongoose Schema是前后端约定的数据格式标准,既能让前端做校验提升用户体验,又能让后端做校验保证数据安全,避免不符合规则的脏数据进入数据库。

ts 复制代码
import mongoose, { Document, Schema } from 'mongoose';

// 定义文档在代码中的类型接口
export interface IDocument extends Document {
  title: string;
  content: any; // BlockNote 的 JSON 数组,暂时用 any,后续可细化
  createdAt?: Date; 
  updatedAt?: Date; 
}

const DocumentSchema: Schema = new Schema({
  title: { type: String, default: 'Untitled' },
  content: { type: Schema.Types.Mixed }, 
}, { 
  timestamps: true 
});

export default mongoose.model<IDocument>('Document', DocumentSchema);

后端routes

ts 复制代码
import express from "express";
import {
  createDocument,
  getAllDocuments,
  getDocumentById,
  updateDocument,
} from "../controllers/documentController.js";

const router: express.Router = express.Router();

router.get("/", getAllDocuments);
router.get("/:id", getDocumentById);
router.post("/", createDocument);
router.put("/:id", updateDocument);

export default router;

对接后端controller层,数据经过model层校验后再落地到数据库。

ts 复制代码
import Document from "../models/document.js";

export const createDocument = async (req: any, res: any) => {
  try {
    const { title, content } = req.body;
    const newDoc = await Document.create({ title, content });
    res.status(201).json(newDoc);
  } catch (error) {
    res.status(500).json({ message: "服务器内部错误" });
  }
};

export const getAllDocuments = async (req: any, res: any) => {
  try {
    const docs = await Document.find({}, "title _id updatedAt createdAt").sort({
      updatedAt: -1,
    });
    res.json(docs);
  } catch (error) {
    res.status(500).json({ message: "服务器内部错误" });
  }
};

export const getDocumentById = async (req: any, res: any) => {
  try {
    const doc = await Document.findById(req.params.id);
    if (!doc) return res.status(404).json({ message: "文档不存在" });
    res.json(doc);
  } catch (error) {
    res.status(500).json({ message: "服务器内部错误" });
  }
};

export const updateDocument = async (req: any, res: any) => {
  try {
    const { title, content } = req.body;
    const doc = await Document.findByIdAndUpdate(
      req.params.id,
      {
        ...(title !== undefined && { title }),
        ...(content !== undefined && { content }),
      },
      { new: true },
    );
    if (!doc) return res.status(404).json({ message: "文档不存在" });
    res.json(doc);
  } catch (error) {
    res.status(500).json({ message: "服务器内部错误" });
  }
};
  • 请求语义规范

POST请求对应新建操作,用于提交新增数据;PUT请求对应更新操作,用于修改已有数据;GET请求对应查询操作,专门用来做数据回显,前后端对接要严格遵循这个语义规则。

三、React Router路由与数据回显

React Router DOM编程式导航管理路由到底是什么意思?路径参数、查询参数该怎么理解?为什么是前端管理 react-router-dom 跳转?

  • 编程式导航核心逻辑

编程式导航就是用代码实现路由跳转,常用useNavigate钩子实现,本质是封装浏览器History API,只修改地址栏、不刷新页面,再触发React组件重渲染,比声明式更适合业务逻辑跳转场景。

  • 后端无需处理页面跳转

页面跳转是前端React Router的专属职责,后端只负责接收请求、处理数据、返回状态和结果,前后端分工更明确,职能更纯粹。

实际做数据回显的时候,我用了useNavigate、useLocation这两个核心钩子,搭配useEffect做渲染优化:通过useEffect判断组件是否初次挂载,从location.pathname的wiki路径里提取当前激活的文档ID,还特意加了silent参数做静默刷新。

之所以加静默刷新,是因为不加的话,每次路由变化都会重新渲染骨架屏,页面卡顿特别严重。我设定的逻辑是:组件第一次挂载时全量加载数据,后续路由发生变化时只做按需刷新,这套逻辑能有效解决卡顿问题,但初始数据到底怎么获取呢?

类似/wiki/:docId的路径里,冒号后的docId就是文档唯一标识,从路径提取这个ID,再用ID调用后端接口查询数据,渲染到页面上,这是前端路由数据回显的标准玩法,路径就是数据的"定位钥匙"。

这里后来做了优化,useLocation的暴力取路径中的id,改成了useParams,并且前端做好route配置,后续还有待优化数据的加密和安全性管理。

React Router在根组件外层做包裹配置,不然路由无法正常生效。

写路由的时候用Postman测试接口通断。

四、前后端完整交互流程

  1. UI层:负责绑定点击、提交等事件,不直接写fetch请求,统一调用service层方法,实现UI与请求逻辑解耦;

  2. Service层:作为API客户端,封装所有请求逻辑,统一处理请求头、异常和响应,把数据打包成HTTP请求体发送给后端;

  3. 后端路由层:接收前端请求,按照路由规则分发到对应的controller处理;

  4. Controller层:解析请求参数,调用model层执行业务逻辑;

  5. Model层:基于JSON Schema校验数据合法性,过滤脏数据,执行数据库增删改查操作;

  6. 响应与回显:后端处理完毕返回结果,前端service层接收响应,更新页面数据;数据更新后,要么重新请求当前ID数据,要么用后端返回的新数据覆盖前端状态,实现实时同步。

五、小结

昨天一天都在做跑环境,做配置,强行拉逻辑链路,觉得很忙但是没有任何收获,所有概念都太陌生,之前虽然也有全站的项目但是自己参与度比较低,没有看懂代码也就放过去了,没有深究为什么。

今天花了大块时间去思考为什么这样写,这样写的好处是是什么,诚然现在的代码依旧存在很多漏洞和待改进的地方,之前粗略看一遍的esm和commonjs这次又重复地加深一下理解,感觉项目的确要多做,说白了代码一定要多敲,而且是自己实际参与的代码量要多敲。并且,多敲很重要,敲完以后复盘也很重要。

复盘 >>> 有思考地敲代码 >>> 只敲不思考,全盘ai 或者 cv大法 >只看文档

提升代码量,见多识广以后,多思考,多问问为什么。

参考文档

TypeScript: Documentation - Modules - Theory

JavaScript 模块 - JavaScript | MDN

JavaScript modules · V8

Tree Shaking | webpack 中文文档 | webpack中文文档 | webpack中文网

Tree-shaking 详解一. 什么是 tree-shaking 前端中的 tree-shaking 可以理解为通过 - 掘金

Main Concepts v6.30.3 | React Router

拦截机 |Axios 文档 --- Interceptors | Axios Docs

SPA(单页应用) - MDN Web 文档术语表:Web 相关术语的定义 | MDN

相关推荐
学且思2 小时前
使用import.meta.url实现传递路径动态加载资源
前端·javascript·vue.js
楼田莉子2 小时前
Linux网络:应用层HTTP网络协议
网络·c++·后端·网络协议·学习·http
1234567890@world2 小时前
FFmpeg | Day1 FFmpege音视频开发与学习
学习·ffmpeg·音视频
problc2 小时前
OpenClaw 的前端用的React还是Vue?
前端·vue.js·react.js
冰暮流星2 小时前
javascript里面的return语句讲解
开发语言·前端·javascript
弓.长.2 小时前
ReactNative for OpenHarmony项目鸿蒙化三方库:react-native-image-gallery — 图片画廊组件
react native·react.js·harmonyos
sensen_kiss2 小时前
CPT306 Principles of Computer Games Design 电脑游戏设计原理 Pt.2 游戏引擎
学习·游戏引擎
步步为营DotNet2 小时前
使用.NET 11的Native AOT提升应用性能
java·前端·.net
天若有情6732 小时前
用编程思维重构学习:从IoC到响应式,打造高效知识体系
学习·算法·重构·ioc·学习方法·依赖注入·响应式数据