资讯动态

HTML连接数据库实战:基于Node.js+Express+MySQL的全栈开发

发布时间:2026/10/1 17:27:27 来源:尧图企业网站定制
1. 先把架构想清楚谁在连接数据库谁在画页面1.1 HTML/CSS到底扮演什么角色三个需要纠正的误区我做前端开发这么多年几乎每隔一段时间就会收到类似的问题为什么我在HTML里写了表格还是没法把数据库里的数据显示出来或者CSS能不能直接调用数据库接口这里必须先纠正几个根深蒂固的误区。第一个误区把HTML当成能独立干活的程序。实际上HTML全称是HyperText Markup Language它是标记语言不是编程语言。它负责描述页面上有什么——一个标题、一张表格、一个按钮但完全不具备运算能力更不可能去读取文件、访问网络服务。你可以把HTML想象成一栋房子的户型图图纸再精细它自己也不会通水通电。第二个误区觉得CSS是美化工具就不重要。CSS确实负责样式但它和数据库之间隔着一道天然的墙——CSS连浏览器内部的JavaScript都接触不到更不用说直接和后端服务通信。不过后面你会看到CSS虽然不是数据链路上的一环却决定了数据展示出来以后用户愿不愿意看。同一个查询结果样式粗糙和样式规整给人的专业感完全不同。第三个误区以为连接数据库这件事本身是某一行神秘代码。很多同学在网上搜HTML连接数据库的代码搜出来一堆乱糟糟的代码片段贴进去根本跑不通原因就是这些片段来自不同的技术栈——有的是PHP写的有的是Java写的有的是ASP写的它们和HTML根本没有直接关系。1.2 连接的本质请求-响应-渲染这条链路真正需要理解的是这套东西在实际项目中怎么协作。前两天我帮一个学员排查他课程设计的登录功能他用了整整一天都没搞定我打开他的代码目录一看里面只有一个login.html里面写了一句类似数据库密码是123456的注释然后一直在问为什么页面报错。这就是对链路的理解出了问题。标准的链路是这个样子的浏览器输入网址服务器把HTML、CSS、JavaScript文件返回给浏览器浏览器渲染出页面。这时候用户在页面上输入用户名、点击登录按钮前端JavaScript用fetch或者XMLHttpRequest发一个HTTP请求到后端服务Node.js、PHP、Java Spring、Python Flask等后端服务收到请求后通过数据库驱动与数据库服务器建立连接执行SQL语句拿到结果把结果整理成JSON返回给前端前端再把数据渲染到页面上。这条链路里HTML和CSS在最外层它们负责人机交互的部分后端技术是中间桥梁负责业务逻辑的部分数据库在最底层负责数据存取的部分。用一句大白话总结HTML负责长什么样浏览器JavaScript负责什么时候发请求、怎么处理响应后端技术负责和数据库打交道、把数据带回给前端。1.3 为什么说这是个典型的全栈最小闭环项目使用HTML与后端技术连接数据库这个标题本质上就是一个全栈最小闭环项目。它的价值在于一套代码里同时覆盖了前端页面设计HTMLCSS、前端交互JavaScript发请求、后端接口开发接收请求、调用数据库、返回数据、数据库设计与操作建库、建表、增删改查四个层面。这篇博文里我会用一个非常典型的实战场景——做一个简单的用户管理页面把用户列表显示在HTML表格中支持新增、修改、删除操作带你把这个闭环完整地跑通。技术栈我选择Node.js/Express MySQL原因后面会说清楚。下面开始动手。2. 起步准备本地环境搭建与第一个后端接口2.1 工具清单其实没你想的那么复杂很多人一想到后端技术就头皮发麻觉得要装一堆东西。其实一个最小可运行的组合只需要四样Node.js运行环境自带npm包管理器MySQL数据库也可以用MariaDB、PostgreSQL但课程设计和生产环境里MySQL最常见VS Code或其他编辑器写代码用顺手装一个MySQL扩展方便看数据一个能发HTTP请求的工具比如Postman但前期直接用浏览器也行安装Node.js的时候有一点要提醒不要图省事一路Next。装完之后打开命令行输入node -v和npm -v两个命令都有版本号输出才算装好。如果提示node不是内部或外部命令说明安装时没有把路径加进环境变量重新安装时勾选Add to PATH选项就行。MySQL安装的时候版本号、端口号默认3306、root账号密码这些信息记下来后面连接数据库全靠它们。建议装MySQL 8.0以上的版本不仅性能更好认证方式也更安全网上排错资料多。2.2 初始化项目从一行命令开始打开命令行找个干净的目录执行mkdir user-manage cd user-manage npm init -ynpm init -y会自动生成一个package.json文件这个文件记录项目的依赖信息。接下来安装今天的主角npm install express mysql2 cors这里出现了三个包每个都不是白装的express是Node.js领域最流行的web框架负责处理HTTP请求和路由mysql2是MySQL的官方驱动负责让Node.js能连上MySQL数据库cors是处理跨域资源共享的中间件解决前端页面在5500端口、后端接口在3000端口浏览器不允许直接通信的问题。安装完成后创建一个server.js文件先写一个最简单的接口验证环境没问题const express require(express); const app express(); app.use(express.json()); app.get(/, (req, res) { res.send(后端服务运行中); }); app.listen(3000, () { console.log(服务已启动http://localhost:3000); });在命令行执行node server.js然后浏览器打开http://localhost:3000看到后端服务运行中几个字说明后端这半边已经通了。这个看似简陋的接口后面会一路成长为完整的用户管理API。2.3 数据库那边要做什么建库、建表和插入测试数据后端环境就绪后轮到数据库登场。打开MySQL的命令行客户端或者在VS Code的MySQL扩展里新建查询执行CREATE DATABASE IF NOT EXISTS user_manage DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE user_manage; CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); INSERT INTO users (username, email) VALUES (zhangsan, zhangsanexample.com), (lisi, lisiexample.com), (wangwu, wangwuexample.com);这一小段SQL做了三件事创建数据库、创建用户表、插入三条测试数据。有个细节值得单独强调一下建库的时候我特意指定了CHARACTER SET utf8mb4而不是老的utf8。utf8mb4是MySQL中真正的四字节UTF-8实现能完整支持中文、日文、emoji等字符而utf8在MySQL里最多只能存三字节遇到某些生僻字会直接报错或者变成乱码。这是我当年踩过的坑建库时偷懒没指定字符集上线后用户昵称里带个生僻字整条记录都读不出来。输入SELECT * FROM users;能看到三条数据说明数据库侧准备完成。3. 打通链路后端接口怎么读数据库前端怎么接数据3.1 后端连接MySQL核心代码拆解现在到了整个项目最关键的一步——让后端代码真正连上MySQL。在server.js中加入数据库连接逻辑const mysql require(mysql2); // 创建数据库连接池 const pool mysql.createPool({ host: localhost, user: root, password: your_password, // 改成你自己的数据库密码 database: user_manage, waitForConnections: true, connectionLimit: 10, queueLimit: 0 });注意这里用的是mysql2的连接池pool而不是单个连接connection。为什么要这样如果你用mysql.createConnection创建一个连接那么每一次请求都要经历建立连接-使用-关闭的完整过程。而建立连接本身是需要网络握手、认证的非常耗时。如果并发量一大几十个请求同时进来每一个都现连现建数据库很容易被打垮。连接池的思路是提前创建一批连接放在池子里谁要用谁拿用完不销毁而是放回去下一次请求可以直接复用。这个类比就是图书馆的座位你每次去都现找座位还是图书馆预留了一批固定座位随到随坐显然是后者效率高。上面代码里的connectionLimit: 10就是池子里最多同时存在10个连接超过10个连接还在忙时新的请求会在queueLimit: 0的情况下排队等待0表示不对排队长度做限制。3.2 做一个查询接口把数据库数据变成JSON有了连接池就可以写业务接口了。下面这个接口负责返回用户列表app.get(/api/users, (req, res) { pool.query(SELECT id, username, email, created_at FROM users ORDER BY id DESC, (err, results) { if (err) { console.error(查询失败, err); return res.status(500).json({ message: 服务器内部错误 }); } res.json(results); }); });这个过程看起来就几行代码但里面有几个点值得说道说道第一SQL语句写成SELECT id, username, email, created_at而不是SELECT *。这是经验之谈明确列出字段的好处是多方面的——减少不必要的数据传输、让代码可读性更强、未来表结构变更时更容易定位问题。尤其是当users表里以后加了密码字段、手机号字段SELECT *会把不该返回的敏感信息也一股脑交给前端。第二错误处理不能省。这里用了if (err)分支把错误情况区分开。一旦数据库连接断开或者SQL语句写错前端会收到一个500状态码而不是浏览器转圈圈等半天。前端也能根据状态码给出明确的提示。第三res.json(results)是express提供的方法自动把JavaScript数组或对象序列化成JSON字符串返回给前端。现在重启服务CtrlC停掉然后重新node server.js浏览器访问http://localhost:3000/api/users就能看到类似这样的输出[ { id: 3, username: wangwu, email: wangwuexample.com, created_at: 2025-01-12T08:30:00.000Z }, { id: 2, username: lisi, email: lisiexample.com, created_at: 2025-01-12T08:30:00.000Z }, { id: 1, username: zhangsan, email: zhangsanexample.com, created_at: 2025-01-12T08:30:00.000Z } ]到这一步后端和数据库已经连通了数据变成了JSON在浏览器里可以直接看到。3.3 前端页面HTML结构、CSS样式、JavaScript取数据后端通了现在回到今天标题里的另一主角——HTML和CSS。在项目目录下新建一个public文件夹里面放index.html、style.css、app.js三个文件。index.html的核心结构!DOCTYPE html html langzh-cn head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title用户管理 - HTML连接数据库实战/title link relstylesheet hrefstyle.css /head body div classcontainer h1用户列表/h1 div classtoolbar input typetext idsearchInput placeholder搜索用户名... button idloadBtn刷新数据/button button idaddBtn新增用户/button /div table classuser-table thead tr thID/th th用户名/th th邮箱/th th创建时间/th th操作/th /tr /thead tbody iduserTableBody !-- 数据由JavaScript动态填充 -- /tbody /table /div script srcapp.js/script /body /html注意meta charsetUTF-8这行经常有人忽略了它然后发现页面上中文全是乱码。HTML文件本身的编码、服务器返回的Content-Type头、数据库那边存储的字符集三处都统一了中文展示才没问题这也是一个排查乱码的老经验。CSS方面我写一套简洁但不失专业的样式让表格在视觉上立得住* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Segoe UI, Microsoft YaHei, sans-serif; background: #f0f2f5; color: #333; padding: 40px 20px; } .container { max-width: 900px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 30px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); } h1 { font-size: 24px; margin-bottom: 20px; border-left: 4px solid #2563eb; padding-left: 12px; } .toolbar { display: flex; gap: 12px; margin-bottom: 20px; align-items: center; } .toolbar input { flex: 1; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; } .toolbar input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); } .toolbar button { padding: 8px 16px; background: #2563eb; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; } .toolbar button:hover { background: #1d4ed8; } .user-table { width: 100%; border-collapse: collapse; margin-top: 10px; } .user-table th, .user-table td { padding: 12px 14px; text-align: left; border-bottom: 1px solid #e5e7eb; } .user-table th { background: #f9fafb; font-weight: 600; color: #4b5563; } .user-table tr:hover { background: #f9fafb; } .empty-tip { text-align: center; padding: 30px; color: #9ca3af; }这套样式里有几个我认为值得模仿的细节box-sizing: border-box全局设置避免padding撑破宽度输入框聚焦时的焦点环效果不突兀又清楚按钮的悬停色微变交互反馈很轻量。表格的border-collapse: collapse让边框不出现双线这些都是前端日常里的基本功。真正把数据和页面连起来的是app.jsconst tableBody document.getElementById(userTableBody); async function loadUsers() { try { const response await fetch(http://localhost:3000/api/users); if (!response.ok) { throw new Error(接口返回错误 response.status); } const users await response.json(); renderTable(users); } catch (error) { console.error(加载用户失败, error); tableBody.innerHTML trtd colspan5 classempty-tip加载失败请确认后端服务已启动/td/tr; } } function renderTable(users) { if (!users.length) { tableBody.innerHTML trtd colspan5 classempty-tip暂无数据/td/tr; return; } tableBody.innerHTML users.map(user { return tr td${user.id}/td td${user.username}/td td${user.email}/td td${new Date(user.created_at).toLocaleString()}/td td button classbtn-edit onclickeditUser(${user.id})编辑/button button classbtn-delete onclickdeleteUser(${user.id})删除/button /td /tr; }).join(); } document.getElementById(loadBtn).addEventListener(click, loadUsers); loadUsers();这段代码里用了fetch的async/await写法比起早期的回调函数、promise链可读性高很多。renderTable函数用模板字符串生成HTML行再一次性赋值给innerHTML比逐行createElement要简洁不少数据量不大的场景完全够用。有个小细节new Date(user.created_at).toLocaleString()把数据库返回的UTC时间格式转换成用户本地时间格式。这一点如果不做页面上会显示一串带Z结尾的ISO时间字符串对你的用户来说不友好。3.4 把前后端黏合起来现在遇到一个问题index.html在public文件夹下但如果直接用浏览器打开file://协议fetch请求跨域、路径解析都会很麻烦。正确的做法是让express把public文件夹托管为静态资源并在server.js里加一行app.use(express.static(public));这样访问http://localhost:3000express会自动找到public目录下的index.html返回给浏览器。前后端在同一个端口下规避了CORS的麻烦。这也是为什么之前安装cors中间件时我说解决跨域问题——如果你把HTML放在另一个端口比如用VS Code的Live Server跑在5500端口那cors中间件就派上用场了。但更省心的是现在这种同一端口托管的方案。到这里这个HTML页面显示数据库数据的最小闭环已经打通了浏览器请求index.html页面渲染出来app.js里的fetch向后端 /api/users 发请求后端查询数据库返回JSON前端把JSON渲染成表格行CSS再让表格变得好看。刷新页面你会看到三条测试数据稳当地躺在表格里。这一刻整个链路就通了。4. 把增删改查补齐一个能用的用户管理才算数4.1 后端四个接口的设计不只是能跑这么简单只读数据不算完一个用户管理页面至少得支持增、删、改。后端要新增三个接口// 新增用户 app.post(/api/users, (req, res) { const { username, email } req.body; if (!username || !email) { return res.status(400).json({ message: 用户名和邮箱不能为空 }); } pool.query( INSERT INTO users (username, email) VALUES (?, ?), [username, email], (err, result) { if (err) { // 处理唯一键冲突用户名重复 if (err.code ER_DUP_ENTRY) { return res.status(409).json({ message: 用户名已存在 }); } console.error(新增失败, err); return res.status(500).json({ message: 服务器内部错误 }); } res.status(201).json({ id: result.insertId, username, email }); } ); }); // 更新用户 app.put(/api/users/:id, (req, res) { const { id } req.params; const { username, email } req.body; pool.query( UPDATE users SET username ?, email ? WHERE id ?, [username, email, id], (err, result) { if (err) { console.error(更新失败, err); return res.status(500).json({ message: 服务器内部错误 }); } if (result.affectedRows 0) { return res.status(404).json({ message: 用户不存在 }); } res.json({ message: 更新成功 }); } ); }); // 删除用户 app.delete(/api/users/:id, (req, res) { const { id } req.params; pool.query( DELETE FROM users WHERE id ?, [id], (err, result) { if (err) { console.error(删除失败, err); return res.status(500).json({ message: 服务器内部错误 }); } if (result.affectedRows 0) { return res.status(404).json({ message: 用户不存在 }); } res.json({ message: 删除成功 }); } ); });这段代码看似简单里面藏了几个我认为新手必须要学会的范式参数校验放在第一道关卡。在POST /api/users里先检查username和email是否为空空就直接返回400。这样既避免了后面的SQL执行报错也让接口的调用方清楚地知道你传的参数不合规。数据合法性校验永远应该在入口处做而不是等数据库报错了才追悔莫及。SQL参数使用占位符而不是字符串拼接。注意我写的是VALUES (?, ?)而不是VALUES (${username}, ${email})。前者叫参数化查询是防止SQL注入的第一道防线。如果你用字符串拼接用户把; DROP TABLE users; --这样的字符串传进来后果不堪设想。参数化查询让mysql2驱动负责转义这个习惯从我写第一行SQL到今天从来没有动摇过。状态码的使用。新增成功返回201Created复制成功返回200参数错误返回400资源不存在返回404服务器内部错误返回500用户名重复返回409Conflict。这些语义化的状态码让前端处理逻辑变得非常简单——不用解析响应体光看状态码就能做对应的提示。影响行数的判断。在更新和删除里我检查了result.affectedRows如果为0说明传入的id在数据库里找不到对应记录。这也是为何很多人写的接口删了不报错也没反应的原因——SQL执行成功了但没有任何行被影响你如果不检查这个值前端还以为删掉了。4.2 前端把新增和删除做进去前端app.js新增一段处理新增用户的逻辑const addBtn document.getElementById(addBtn); addBtn.addEventListener(click, () { const username prompt(请输入用户名); if (!username) return; const email prompt(请输入邮箱); if (!email) return; fetch(http://localhost:3000/api/users, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ username, email }) }) .then(res { if (!res.ok) return res.json().then(err { throw new Error(err.message); }); return res.json(); }) .then(() { alert(新增成功); loadUsers(); }) .catch(err alert(新增失败 err.message)); });用prompt做输入框虽然不高级但胜在简洁。真实的项目里应该用弹窗组件或者跳转到一个表单页面但作为最小可运行的教学项目prompt足够演示整个数据流向。deleteUser函数挂载在全局这样HTML里的onclickdeleteUser(3)才能正确调用async function deleteUser(id) { if (!confirm(确定要删除该用户吗)) return; try { const res await fetch(http://localhost:3000/api/users/${id}, { method: DELETE }); if (!res.ok) { const err await res.json(); throw new Error(err.message); } alert(删除成功); loadUsers(); } catch (error) { alert(删除失败 error.message); } }一个完整的数据链路就此形成浏览器发起各种HTTP方法GET、POST、PUT、DELETE后端通过不同的路由分发到对应的处理函数处理函数操作数据库后再把结果返回。4.3 三个高频问题与排查技巧实录现在这个闭环能跑通了但实际动手时你会遇到各种问题。我把这些年带新手最常见的三个问题拿出来逐个排雷。问题一中文乱码。表现是数据库里能看到中文但接口返回的中文变成???或者页面显示一堆问号。排查顺序先看数据库连接的字符集对不对上面建库时指定的utf8mb4是源头再看server.js的连接配置里有没有加上charset: utf8mb4mysql2默认不一定使用最后看HTML文件本身的meta标签。三个地方都一致了乱码基本能解决。问题二fetch请求报错 NetworkError或者CORS错误。如果你把HTML放在了Live Server的5500端口而后端跑在3000端口就触发了跨域。解决方式server.js里加上app.use(cors())这行。顺带说一句cors中间件默认允许所有源这在开发环境没问题生产环境里应该配置成一个白名单列表只允许你自己的前端域名访问。问题三端口被占用。运行node server.js时提示EADDRINUSE这是3000端口被其他进程占用了。在macOS/Linux上执行lsof -i :3000找到占用进程的PID然后kill -9 PID结束它Windows上用netstat -ano | findstr :3000查看PID。或者更简单把server.js里的端口改成3001。4.4 顺带讲讲搜索这个功能怎么实现用户列表基本功能齐了我建议你再顺手加一个搜索因为这是面试和课程设计里最常被追问的扩展点。后端接口改成app.get(/api/users, (req, res) { const keyword req.query.keyword || ; const sql SELECT id, username, email, created_at FROM users WHERE username LIKE ? ORDER BY id DESC; const likeParam %${keyword}%; pool.query(sql, [likeParam], (err, results) { if (err) { console.error(查询失败, err); return res.status(500).json({ message: 服务器内部错误 }); } res.json(results); }); });前端监听输入框的input事件并调用接口就实现了输入即搜索的体验。注意SQL里的LIKE配合%通配符可以实现模糊匹配但%也要作为参数传进去不能直接拼进SQL里。5. 安全与规范上线前必须补齐的几个细节5.1 SQL注入是怎么发生的以及参数化查询为什么能防住它我见过太多课程设计代码SQL语句拿字符串拼接一看到就头大。有人可能觉得自己做的只是个管理页面没什么攻击价值但SQL注入的危害不是有没有人攻击而是一旦被攻击后果能不能承受。假设你写的是const sql SELECT * FROM users WHERE username ${username} AND password ${password};一个懂行的人在用户名框输入 OR 11密码随便填最终拼接出来的SQL变成SELECT * FROM users WHERE username OR 11 AND password xxx11恒为真整个WHERE条件被绕过这个人就能以任意用户身份登录系统。这就是SQL注入。参数化查询为什么安全因为mysql2驱动会把传入的值和SQL语句分开处理先让数据库编译SQL模板占位符的地方是固定的参数位置再单独把值作为数据传进去。数据库严格区分哪些是代码、哪些是数据恶意字符串就丧失了变成代码的机会。5.2 不该出现的底线错误密码不进数据库的明文如果你的用户表有密码字段一定不能明文存储。业界最低标准是用bcrypt做哈希加盐const bcrypt require(bcrypt); const saltRounds 10; const hash bcrypt.hashSync(password, saltRounds);bcrypt的加盐具体说就是把一段随机字符串混进密码里一起做哈希这样就算两个用户设置了相同密码存储的哈希值也不同有效防止彩虹表破解。saltRounds是计算成本参数取10表示约2的10次方轮迭代既能保证安全又不会让登录响应慢得离谱。5.3 连接池参数怎么调别照抄一个配置用到底前面说的connectionLimit: 10是最基础的参数但实际场景里需要结合业务量调优。有一个原则连接池最大连接数不是越大越好。连接数是和数据库服务器的内存、并发能力挂钩的。MySQL默认的最大连接数是151如果每个后端实例配了10个连接池上限那3个后端实例就可能占满整个数据库。我在一个真实项目里把连接池配置调成了这样pool: { connectionLimit: 20, waitForConnections: true, queueLimit: 0, idleTimeout: 60000 }idleTimeout让空闲连接1分钟没有任务就自动释放避免低峰期占着数据库连接不松手。如果你在做课程设计10个连接完全够用但如果你打算部署到一个有实际用户访问的网站上就要认真做压力测试观察连接池的实际使用率再调整。6. 从课程设计到真实项目的差距三个值得继续深挖的方向6.1 把数据接口从能用做到好用现在我们做出来的接口能查、能增、能删、能改但距离生产级接口还有差距。一个典型的差距是分页目前 /api/users 会把表里所有数据一次返回数据量到一千条、一万条的时候这个接口的响应时间和传输量都会恶化。实现分页的标准做法是让接口支持page和pageSize参数app.get(/api/users, (req, res) { const page parseInt(req.query.page) || 1; const pageSize parseInt(req.query.pageSize) || 10; const offset (page - 1) * pageSize; pool.query( SELECT id, username, email, created_at FROM users LIMIT ? OFFSET ?, [pageSize, offset], (err, results) { if (err) { console.error(查询失败, err); return res.status(500).json({ message: 服务器内部错误 }); } res.json({ data: results, page, pageSize, total: results.length }); } ); });同时还可以加一个总数查询让前端能够计算总页数并渲染页码按钮。这个能力做出来接口的好用程度立刻提升一个档次。6.2 告别回调地狱用async/await重写数据库操作目前代码里用的还是回调函数风格pool.query(sql, params, (err, results) { ... });这在小项目里没毛病一旦接口数量变多、业务逻辑嵌套变深回调嵌套会变成传说中的回调地狱。mysql2/promise提供了一套Promise API配合async/await可以写成const mysql require(mysql2/promise); const pool mysql.createPool({ host: localhost, user: root, password: your_password, database: user_manage, waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); app.get(/api/users, async (req, res) { try { const [rows] await pool.query(SELECT id, username, email, created_at FROM users ORDER BY id DESC); res.json(rows); } catch (error) { console.error(查询失败, error); res.status(500).json({ message: 服务器内部错误 }); } });这种写法的可读性和错误处理都舒服得多也是现在Node.js后端开发的主流风格。新项目建议直接用mysql2/promise别走回头路。6.3 前端工程化把HTML/CSS/JS从页面升级为应用如果你已经能完整跑通这个项目下一步可以考虑使用Vue或React把前端重写一遍。说句实话原生JavaScript做演示足够了但真实的项目里DOM操作遍地都是、状态管理混乱、代码难以维护这时候框架的价值就体现出来了。以Vue为例它的响应式数据绑定意味着你只需要维护一个users数组更新数组前端表格自动渲染不用再手动拼接innerHTML。组件化还能把用户表格、搜索框、新增弹窗拆分成独立组件代码结构比散装HTML干净得多。不过这些都是后话前提是先把原生链路理解透彻。我今天讲的所有原理——HTTP请求、后端处理、数据库操作、数据渲染——到了框架时代依然适用框架只是帮你在更上层把这些操作封装起来而已。7. 最后想对你说的一些实话做一个HTMLCSS连接数据库的项目最大的门槛从来不是某一种具体技术而是理解前后端分离协作这个思维方式。很多人卡住的共同原因都是试图用一个HTML文件完成所有事情这就像一个餐厅试图把厨房和服务员合并成一个人菜单看着再漂亮菜就是端不上来。我个人的建议是把今天这个项目从头到尾自己敲一遍不要复制粘贴。敲的过程中遇到报错第一时间看清错误消息里的关键信息再回来看对应环节的代码。等你亲手完成了新增一个用户让它显示在表格里这个闭环再回头看待浏览器里那些花花绿绿的页面你会发现自己已经看到了它们背后那条完整的河流。动手吧数据库那边有几条数据正在等你把它们捞上来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑