1. 为什么不用框架反而更值得折腾一遍1.1 从零搭一套前后端交互到底在练什么很多人第一次接触Web开发上来就是脚手架一跑路由、数据库、接口全给你配好了写几行业务逻辑就能跑通。爽是爽但一旦接口报错、跨域被拦、数据库连不上整个人就懵了因为根本不知道底下发生了什么。我当初带过几个刚入行的朋友他们能写业务代码但让他们从零搭一个“前端页面提交数据、后端接收并写进数据库”的最小闭环十有八九卡在某个环节上。这个项目的核心价值就在这儿前后端都不用框架前端就是原生HTML加JavaScript后端用最基础的服务端脚本或轻量HTTP服务数据库用常见的关系型数据库。整套东西没有任何黑盒每一行代码你都知道它在干嘛。它解决的问题不是“做一个能上线的产品”而是“让你彻底搞明白一次完整的请求是怎么从浏览器走到数据库再走回来的”。适合谁来参考我觉得有三类人特别值得动手做一遍。第一类是刚学完HTML、CSS、JavaScript基础想往全栈方向走但还没碰过后端的新手第二类是用惯了框架、想回头补一补底层原理的开发者第三类是要给学生或新人做技术分享的人需要一个足够简单又足够完整的演示项目。这套东西代码量不大但涉及的知识点非常密集HTTP请求、跨域、数据序列化、数据库连接、SQL执行、错误处理一个都不少。1.2 不用框架的代价与收益先算清楚这笔账不用框架意味着什么意味着路由要自己判断请求体要自己解析数据库连接要自己管理错误要自己捕获。听起来麻烦但恰恰是这些“麻烦”让你看清了框架帮你做了什么。我个人的体会是用框架开发像开自动挡汽车不用框架像开手动挡前者上手快后者让你真正理解离合和档位的配合。收益方面最直接的是排错能力。当你亲手写过Content-Type的解析逻辑以后再遇到“后端收不到前端发的数据”这种问题你第一反应就是去看请求头而不是到处搜索。其次是对性能瓶颈的直觉你知道每次请求背后到底建立了几次连接、执行了几条SQL优化的时候心里有数。代价也很明显没有框架的生态支持很多东西要手写代码复用性差安全性需要自己兜底。所以这套方案我从来不建议用在正式生产项目上它的定位就是学习与验证。把这一遍走通再去用框架你会用得比谁都明白。1.3 整体技术选型的思路既然是学习性质的项目选型的第一原则就是“依赖越少越好能跑起来就行”。前端不需要打包工具一个HTML文件加一个JS文件就够用浏览器自带的fetch发请求这是现代浏览器原生支持的不用引入任何库。后端我倾向于选一门自带HTTP服务能力的语言比如用某个轻量脚本语言直接起一个监听端口的服务或者用系统自带的运行环境写一个简单的请求处理器避免引入额外的Web框架。数据库方面选一个安装简单、社区资料多的关系型数据库即可本地装一个实例建一张测试表。整个链路就是浏览器里的JS用fetch把数据POST到后端监听的端口后端解析请求体拼一条SQL通过数据库驱动写进表里再把结果返回给前端。这条链路虽然短但每一环都有坑后面我会逐个拆开讲。提示选型时不要贪多先把一条链路跑通再考虑加功能。我见过太多人一上来就想做登录、做权限、做文件上传结果第一个接口都没调通就放弃了。2. 环境准备与项目骨架搭建2.1 运行环境与依赖清单动手之前先把环境理清楚。你需要的东西其实不多但每一样都要确认版本和可用性。下面这张表是我建议的准备清单按这个来基本不会缺东西。组件作用选择建议验证方式浏览器运行前端页面、发请求任意现代浏览器能打开开发者工具后端运行环境提供HTTP服务、处理请求系统自带或轻量脚本环境命令行能执行脚本数据库存储数据本地关系型数据库实例能连上并执行查询数据库驱动后端连接数据库的桥梁对应运行环境的官方驱动能成功建立连接文本编辑器写代码任意顺手的即可能保存UTF-8文件这里我要特别强调数据库驱动这一项。很多人环境装好了数据库也能用命令行连上但后端代码里就是连不上八成是驱动没装或者版本不匹配。驱动的版本要和数据库服务端的版本大致对应差太多容易出现协议不兼容的问题。2.2 目录结构怎么规划才不乱不用框架的时候目录结构全靠自己定。我的建议是保持极简但要有清晰的边界。前端的东西放一个目录后端的东西放一个目录数据库的建表脚本单独放一个文件。大概长这样project/ ├── frontend/ │ ├── index.html │ └── app.js ├── backend/ │ ├── server.js或对应语言的入口文件 │ └── db.js数据库连接封装 └── database/ └── init.sql建表语句为什么要把数据库连接单独抽一个文件因为连接配置地址、端口、用户名、密码、库名是容易变的东西抽出来之后改配置只改一个地方不用在业务代码里到处找。这也是我在实际项目里踩过坑之后养成的习惯——配置和逻辑分离哪怕项目再小。2.3 数据库初始化与建表实操数据库这一步先建库再建表。建库的时候注意字符集不然后面存中文会出现乱码这个问题新手遇到得特别多。建表语句我建议直接写成一个.sql文件方便重复执行。-- 建库指定字符集 CREATE DATABASE demo_db DEFAULT CHARACTER SET utf8mb4; -- 使用这个库 USE demo_db; -- 建一张最简单的用户表 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, email VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );字段设计上id用自增主键是最省事的做法created_at给个默认值插入的时候不用管它。username设成非空因为它是我们后面演示的核心字段。字符集用utf8mb4而不是utf8原因是前者能存更完整的字符包括一些特殊符号后者在某些情况下会出问题。这个细节很多人不注意等到存进去变成问号才回头改那时候表都建好了改起来麻烦。注意建库建表之后最好手动插一条测试数据确认数据库本身是通的再去写后端代码。这样能把“数据库问题”和“代码问题”分开排查。3. 后端服务与数据库连接的核心实现3.1 起一个能接收请求的后端服务后端的第一件事是监听一个端口能接收HTTP请求。不管你用什么语言核心逻辑都是一样的绑定端口、监听连接、读取请求、返回响应。我用伪代码的方式把关键步骤说清楚你对照自己选的语言实现即可。// 以Node.js原生http模块为例其他语言逻辑类似 const http require(http); const server http.createServer((req, res) { // 1. 设置响应头解决跨域 res.setHeader(Access-Control-Allow-Origin, *); res.setHeader(Access-Control-Allow-Methods, GET, POST, OPTIONS); res.setHeader(Access-Control-Allow-Headers, Content-Type); // 2. 处理预检请求 if (req.method OPTIONS) { res.writeHead(204); res.end(); return; } // 3. 根据路径和方法分发 if (req.url /api/user req.method POST) { let body ; req.on(data, chunk { body chunk; }); req.on(end, () { // 请求体接收完毕解析并处理 handleUser(body, res); }); } else { res.writeHead(404); res.end(Not Found); } }); server.listen(3000, () { console.log(服务已启动监听3000端口); });这段代码里有几个关键点必须说清楚。第一跨域响应头必须设置否则浏览器会拦截前端发来的请求这是新手最容易卡住的地方。第二预检请求OPTIONS方法要单独处理因为浏览器在发某些POST请求之前会先发一个OPTIONS探路你不处理它真正的请求就发不出去。第三请求体是流式接收的要监听data和end事件不能指望一次就读完。3.2 数据库连接的建立与参数详解数据库连接是后端和数据库之间的桥梁。连接的时候需要几个核心参数主机地址、端口、用户名、密码、数据库名。这几个参数任何一个错了都连不上而且报错信息有时候很模糊所以配置的时候要格外仔细。// 数据库连接封装示例 const dbConfig { host: 127.0.0.1, // 本地数据库 port: 3306, // 数据库默认端口 user: root, // 用户名 password: your_password, // 密码 database: demo_db, // 库名 charset: utf8mb4 // 字符集和建库时保持一致 };这里我重点讲两个容易出错的参数。主机地址用127.0.0.1比用localhost更稳因为在某些系统上localhost会优先走IPv6解析而数据库可能只监听了IPv4导致连接被拒。字符集要和建库时保持一致否则存进去的中文可能变成乱码。这两个坑我都踩过排查起来很费时间。连接建立之后建议做一次连接测试也就是执行一条最简单的查询比如SELECT 1确认连接是活的。很多驱动支持连接池但对于学习项目单连接就够了连接池反而增加理解成本。3.3 请求数据的解析与SQL执行前端发过来的数据通常是JSON格式后端收到的是一个字符串需要先解析成对象再取出字段拼SQL。这里有个安全要点绝对不要用字符串拼接的方式拼SQL要用参数化查询否则会有注入风险。// 解析请求体并执行插入 function handleUser(body, res) { let data; try { data JSON.parse(body); // 解析JSON } catch (e) { res.writeHead(400); res.end(JSON.stringify({ error: 请求体格式错误 })); return; } const { username, email } data; // 参数化查询用占位符代替直接拼接 const sql INSERT INTO users (username, email) VALUES (?, ?); connection.query(sql, [username, email], (err, result) { if (err) { res.writeHead(500); res.end(JSON.stringify({ error: 写入失败, detail: err.message })); return; } res.writeHead(200, { Content-Type: application/json }); res.end(JSON.stringify({ success: true, id: result.insertId })); }); }参数化查询的好处是驱动会自动帮你转义特殊字符即使用户输入里带了引号也不会破坏SQL结构。这个习惯从学习阶段就要养成不要觉得“反正是练习项目无所谓”。另外错误处理一定要做数据库操作失败是常有的事把错误信息返回给前端方便排查。提示返回给前端的错误信息不要包含数据库的完整堆栈暴露太多内部细节不安全。给一个简短的错误提示就够了详细的错误打到后端日志里。4. 前端页面与请求发送的完整流程4.1 页面结构与表单设计前端部分保持最简一个表单加一个提交按钮再加一个显示结果的区域。HTML不需要任何框架原生标签就够。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title前后端交互演示/title /head body h2用户注册/h2 form iduserForm label用户名input typetext idusername required/labelbr label邮箱input typeemail idemail/labelbr button typesubmit提交/button /form div idresult/div script srcapp.js/script /body /html表单这里有个细节input的type用email的时候浏览器会自动做格式校验省得自己写正则。required属性也是原生的能挡住空提交。这些原生能力不用白不用能少写不少JS。4.2 用fetch发送POST请求fetch是现代浏览器原生的请求API用法比老的XMLHttpRequest简洁很多。核心是配置好请求方法、请求头和请求体。document.getElementById(userForm).addEventListener(submit, async (e) { e.preventDefault(); // 阻止表单默认提交行为 const username document.getElementById(username).value; const email document.getElementById(email).value; try { const response await fetch(http://127.0.0.1:3000/api/user, { method: POST, headers: { Content-Type: application/json // 关键声明请求体是JSON }, body: JSON.stringify({ username, email }) // 转成JSON字符串 }); const result await response.json(); document.getElementById(result).textContent result.success ? 提交成功ID为 result.id : 提交失败 result.error; } catch (err) { document.getElementById(result).textContent 请求出错 err.message; } });这段代码里Content-Type请求头是重中之重。如果不设置它后端可能不知道请求体是什么格式解析就会出问题。JSON.stringify把对象转成字符串也是必须的fetch不会自动帮你转。e.preventDefault()阻止表单默认的页面跳转行为否则一提交页面就刷新了请求还没发出去。4.3 跨域问题的成因与解决跨域是前后端分离开发绕不开的话题。浏览器的同源策略规定协议、域名、端口三者只要有一个不同就属于跨域请求会被拦截。我们的前端页面可能是用file://打开的或者跑在另一个端口上而后端在3000端口这就跨域了。解决办法就是在后端设置响应头允许跨域。前面后端代码里已经写了Access-Control-Allow-Origin这里再强调几个细节。如果前端需要携带凭证比如CookieAccess-Control-Allow-Origin不能设为*必须指定具体的来源。另外Access-Control-Allow-Headers要包含前端实际发送的自定义请求头否则预检请求会失败。跨域报错关键词原因解决方向No Access-Control-Allow-Origin后端没设跨域头后端加响应头Preflight response is not successful预检请求没处理后端处理OPTIONSRequest header field not allowed请求头不在白名单补充Allow-Headers我个人的经验是跨域问题九成出在后端响应头没配对剩下一成是预检请求没处理。把这两个地方检查一遍基本都能解决。5. 常见报错与排查技巧实录5.1 数据库连接类报错速查数据库连不上是最常见的卡点报错信息五花八门但归类下来就那么几种。我整理了一张速查表遇到问题对照着看。报错信息关键词可能原因排查步骤ECONNREFUSED数据库没启动或端口不对确认服务在跑端口一致Access denied用户名或密码错误核对配置确认权限Unknown database库名写错或库没建确认库存在名字一致ER_NOT_SUPPORTED_AUTH_MODE认证方式不兼容调整用户认证插件Client does not support authentication驱动版本过旧升级驱动版本ECONNREFUSED这个报错我遇到得最多通常是数据库服务根本没启动或者端口号写错了。排查的时候先用命令行工具连一下命令行能连上说明服务没问题那就是代码里的配置有问题。Access denied一般是密码错了或者这个用户没有从当前主机连接的权限。5.2 请求发送与接收类报错前端请求发不出去或者后端收不到数据也是高频问题。这类问题的排查思路是先看浏览器开发者工具的Network面板看请求有没有发出去、状态码是多少、响应是什么。如果请求状态是(failed)或者CORS error那就是跨域问题回到上一节检查响应头。如果状态是400说明请求发出去了但格式不对检查Content-Type和请求体。如果状态是500说明后端处理出错了去看后端日志。如果请求根本没出现在Network面板里那可能是JS报错了去看Console面板。提示开发者工具的Network面板是排查前后端交互问题的第一现场学会看它比什么都重要。请求头、响应头、请求体、响应体四个地方都要看。5.3 数据写入异常与乱码处理数据能写进去但内容不对比如中文变成问号这是字符集问题。要保证三个地方的字符集一致数据库、表、连接。建库建表的时候指定utf8mb4连接配置里也指定utf8mb4三处对齐就不会乱码。还有一种情况是数据写进去了但字段是空的这通常是前端字段名和后端取值的字段名对不上。比如前端发的是userName后端取的是username大小写不一致取出来就是undefined。这种问题不报错但数据不对排查起来反而费劲。我的习惯是前后端字段名严格保持一致并且在后端加一层校验字段缺失就直接返回错误不要让它静默写入空值。5.4 我踩过的几个典型坑第一个坑是端口占用。后端服务启动时报EADDRINUSE意思是端口被别的程序占了。解决办法是换个端口或者找到占用端口的程序关掉。我一般习惯在启动脚本里加个端口检测被占了就自动换一个。第二个坑是请求体接收不完整。数据量小的时候没事数据量一大就发现只收到一部分。原因是流式接收的时候没有正确处理数据拼接或者没有等end事件就提前处理了。一定要等end事件触发后再解析。第三个坑是异步顺序问题。数据库查询是异步的如果在查询回调外面就返回了响应那响应里就没有查询结果。所有的响应发送都要放在回调里面确保数据拿到了再返回。6. 从跑通到跑稳的几点经验6.1 把配置抽出来别写死在代码里项目跑通之后第一件该做的事是把配置抽出来。数据库地址、端口、密码这些不要硬编码在代码里放到一个单独的配置文件或者环境变量里。这样做的好处是换环境的时候只改配置不动代码。我见过有人把密码写在代码里提交到了代码仓库后来改密码改得焦头烂额。配置文件的格式用什么都行JSON、环境变量、简单的键值对都可以。关键是要有一个统一的读取入口代码里用配置项的地方都从这个入口取不要东一个西一个。6.2 加日志让问题自己浮出来不用框架的时候日志要自己打。我的建议是在几个关键节点都加上日志服务启动时打一条收到请求时打一条包含路径和方法数据库操作前打一条操作后打一条包含影响行数出错时打详细的错误信息。日志不用搞得很复杂输出到控制台就行。但有了这些日志出问题的时候你一眼就能看出请求走到哪一步断了。没有日志的时候只能靠猜效率差很多。6.3 输入校验不能省前端做了校验不代表后端就不用做了因为请求可以绕过前端直接发。后端必须对收到的数据做校验字段是否存在、类型对不对、长度超没超。校验不通过就返回明确的错误不要让它进到数据库层。校验逻辑不用写得很复杂几个if判断就够。但这一步省不得它既是安全防线也是数据质量的保障。我在实际项目里见过因为没做后端校验导致数据库里塞了一堆脏数据清理起来非常麻烦。6.4 这套东西后续可以怎么扩展跑通最小闭环之后可以往上加东西。比如加一个查询接口前端提交后能列出所有用户加一个删除接口练习DELETE请求加一个更新接口练习PUT请求。每加一个接口都是对HTTP方法和SQL语句的一次练习。再往后可以练习分页查询、条件筛选、简单的登录验证。但每一步都要在跑通当前这一步的基础上加不要一次加太多。这套不用框架的方案最大的价值就是让你把每一步都看得清清楚楚加功能的时候也是同样的道理一步一步来每一步都搞明白。我个人在实际操作中的体会是这套东西看起来简单但真正动手做一遍遇到的坑比想象中多。而正是这些坑才是真正让你进步的地方。框架帮你把坑填平了但也让你失去了踩坑和填坑的机会。花一个下午把这条链路从零走通比看十篇教程都管用。