昨天一整天都在折腾项目前后端对接,后端的配置、数据库连接,全程状态就是一头雾水,从工程架构、环境配置到路由数据流,磕磕绊绊踩了无数坑。
今天把涉及到的知识,昨天一股脑生成的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测试接口通断。
四、前后端完整交互流程
-
UI层:负责绑定点击、提交等事件,不直接写fetch请求,统一调用service层方法,实现UI与请求逻辑解耦;
-
Service层:作为API客户端,封装所有请求逻辑,统一处理请求头、异常和响应,把数据打包成HTTP请求体发送给后端;
-
后端路由层:接收前端请求,按照路由规则分发到对应的controller处理;
-
Controller层:解析请求参数,调用model层执行业务逻辑;
-
Model层:基于JSON Schema校验数据合法性,过滤脏数据,执行数据库增删改查操作;
-
响应与回显:后端处理完毕返回结果,前端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