前言
如今的Web,早已不再是前后端混合的 PHP 世界。我们早已进入了前后端分离的,Web3.0的 – 未来。
欢迎你阅读本文 – 「现代网页开发入门指南书 – 从数据库设计,到后端,再到前端」
由 Dontalk 会员 – 【葵】 耗时4天编写 – 如有不对,敬请原谅和指正。
如果你希望下载本文 PDF 以及配套代码,请前往「dl.dontalk.org」找到「现代网页开发入门指南书 – 从数据库设计,到后端,再到前端」即可下载。或者博文提到的 GitHub地址 也可以下载到副本 (包括本指南书的PDF档)。
Dontalk 地址:dontalk.org
Github 仓库地址(25年5月中):https://github.com/infoabcd/Modern-Web-Development-Beginner-s-Guide/
但是请注意,本作品采用 [署名-非商业性使用 4.0 国际 (CC BY-NC 4.0)] 协议发布。
你可以自由转载、分发、分享,但必须注明作者及出处,不得用于任何商业用途。
否则在沟通无果后,Dontalk 团队不介意采用法律武器捍卫权益。
协议详情请见:https://creativecommons.org/licenses/by-nc/4.0/deed.zh
评价:
这份《现代网页开发入门指南书 v1.3》作为一份旨在引导初学者进入全栈Web开发的材料,展现了其覆盖广泛知识领域的雄心。它试图带领读者从后端的数据库设计,经过API开发,最终到前端的实现,并采用了当前较为流行的技术栈(如Node.js, Express, Sequelize, React, Vite)。文档通过代码示例来阐述概念,这对于初学者动手实践具有一定的积极意义。
然而,从技术严谨性和最佳实践指导的角度来看,这份指南在一些关键环节存在较为重要的改进空间(缺少动机阐述和不建议声明)(下面声明),初学者若不加辨别地采纳,可能在实际项目中引入风险或不良实践。
值得肯定的方面:
1. 内容覆盖面广:指南尝试覆盖Web开发的整个生命周期,从数据存储到用户交互,有助于初学者建立整体概念。
2. 技术栈选择现代:采用了Node.js、Express、React、Vite等现代Web开发中常用的技术和工具,使学习者能够接触到当前主流的开发模式。
3. 实践性导向:提供了大量的代码片段,涉及数据库模型定义、API路由、JWT认证、前端组件等,鼓励读者通过实践学习。
4. 核心概念涉及:介绍了诸如ORM(Sequelize)、RESTful API设计、JWT认证机制、前端路由(React Router)、组件化开发等Web开发的核心概念。
5. 安全意识初显:在某些方面体现了对安全性的关注,例如提到了使用bcrypt
进行密码哈希,以及在JWT存储中讨论了HttpOnly
Cookie的应用。
现代网页开发入门指南书 v1.3 存在的问题
你可以粗略阅读此处,之后再回看
“现代网页开发入门指南书 – 从数据库设计,到后端,再到前端 v1.3版本.pdf”的摘录内容,我来分析其中可能存在的严重技术性问题,并提供相应的解读。
从整体上看,这份指南覆盖了从数据库设计到前后端实现的关键环节,并提供了一些实践代码。然而,在一些技术细节和最佳实践方面,确实存在一些可以被认为是“严重”或至少是“重要”的技术问题或改进点。
存在的一些问题(下文不好更改-此处声明(填坑)):
1. 数据库设计与 SQL 问题
- 问题 1.1:
article
表中不必要的article_id
字段 (用户已指出并解决)- 原文描述: 用户提供的 SQL 中
article
表同时有id
(主键) 和article_id
。 - 严重性: 中等。这会导致数据冗余和潜在的逻辑混乱。外键应该引用主键
id
。 - 状态: 用户在后续的交流中已经指出了这个问题,并通过移除
article.article_id
并让article_image.article_id
引用article.id
来修正。
- 原文描述: 用户提供的 SQL 中
- 问题 1.2: 使用
CHAR(6)
作为自定义关联键 (Release level
)- 原文描述: 提出使用名为 “Release level” (在
article
表) 和 “level” (在article_image
表) 的CHAR(6)
字段,通过随机6位数进行关联。 - 严重性: 中高。
- 唯一性风险: “随机6位数”如果生成算法不够健壮,有碰撞(重复)的风险,尤其是在数据量大时。
UNIQUE
约束可以防止插入重复值,但不能解决生成算法本身的问题。 - 性能: 字符串类型的键通常比整数类型的键在索引和连接操作上性能稍差。
- 可读性和维护性: 虽然在某些特定场景下自定义关联键有其用途,但通常情况下,使用自增整数主键 (
id
) 作为外键引用目标更简单、高效且符合常规实践。 - SQL字段名:
Release level
这样的带空格的字段名在 SQL 中需要用反引号`Release level`
包裹,容易出错且不推荐。
- 唯一性风险: “随机6位数”如果生成算法不够健壮,有碰撞(重复)的风险,尤其是在数据量大时。
- 改进建议: 坚持使用
article.id
(主键) 和article_image.article_id
(外键) 进行关联。如果确实需要一个人类可读的、唯一的发布标识,可以额外增加一个release_code
字段,并确保其唯一性,但不一定用它作为主外键关联。
- 原文描述: 提出使用名为 “Release level” (在
- 问题 1.3:
ON UPDATE CASCADE
的普遍使用- 原文描述: 在外键定义中同时使用了
ON DELETE CASCADE
和ON UPDATE CASCADE
。 - 严重性: 低到中等。
ON DELETE CASCADE
是常见的,表示删除主表记录时级联删除从表相关记录。ON UPDATE CASCADE
表示更新主表主键值时,级联更新从表外键值。虽然在某些数据库和场景下支持,但主键本身通常是不应该被频繁更新的。如果主键是自增整数,几乎永远不会更新。如果主键是可能变更的业务字段(不推荐),ON UPDATE CASCADE
才显得更有意义。过度依赖此特性可能掩盖了主键设计不当的问题。
- 改进建议: 仔细评估是否真的需要
ON UPDATE CASCADE
。对于自增主键,它几乎没有作用。
- 原文描述: 在外键定义中同时使用了
2. Sequelize ORM 使用问题
- 问题 2.1:
DataTypes.STRING(255)
的写法 (用户已提问)- 原文描述: 代码中使用了
DataTypes.STRING(255)
。 - 严重性: 低。这不是一个“错误”,但正如用户指出的,Sequelize v6+ 中
DataTypes.STRING
默认映射到数据库的VARCHAR(255)
(或数据库的默认字符串长度),所以显式写(255)
通常不是必需的,除非有特定长度要求。 - 状态: 已在交流中澄清。
- 原文描述: 代码中使用了
- 问题 2.2:
timestamps: false
与手动created_at
- 原文描述: 模型定义中设置
timestamps: false
,然后手动定义created_at
字段。 - 严重性: 低。这是一种可行的做法,但 Sequelize 的
timestamps: true
(默认) 会自动管理createdAt
和updatedAt
字段,通常更方便。如果只需要createdAt
,可以配置timestamps: true, updatedAt: false
。手动管理意味着在更新操作时也需要手动更新updatedAt
(如果需要的话)。 - 改进建议: 除非有特殊理由,否则利用 Sequelize 内建的时间戳管理功能可能更简洁。
- 原文描述: 模型定义中设置
- 问题 2.3:
sequelize.sync({ alter: true })
的滥用- 原文描述: 在
app.js
或入口文件中使用await sequelize.sync({ alter: true });
来同步数据库。 - 严重性: 高(在生产环境中)。
{ alter: true }
会尝试修改现有表以匹配模型定义,这在开发初期可能很方便,但在生产环境中非常危险,可能导致数据丢失或表结构意外更改。{ force: true }
(未在示例中直接出现,但与sync
相关) 会先删除表再重建,同样会导致数据丢失。
- 改进建议: 生产环境中应使用数据库迁移 (Migrations) 工具 (如 Sequelize CLI 提供的迁移功能) 来管理数据库模式的变更。开发环境中
sync({ alter: true })
可酌情使用,但需谨慎。
- 原文描述: 在
- 问题 2.4: 模型关联中
sourceKey
和targetKey
的理解- 原文描述: 在
Article.hasMany(ArticleImage, { foreignKey: 'article_id', sourceKey: 'id' })
和ArticleImage.belongsTo(Article, { foreignKey: 'article_id', targetKey: 'id' })
中,sourceKey
和targetKey
的使用。 - 严重性: 低。这里的用法是正确的,因为它们都指向了
Article
表的id
字段。sourceKey
是源模型(Article
)中与外键关联的键,targetKey
是目标模型(Article
)中被外键引用的键。当外键引用的不是目标模型的主键时,或者源模型的关联键不是其主键时,这些选项才更有区分度。对于标准的主键-外键关联,有时可以省略。 - 说明: 当关联字段名与默认规则一致时 (例如,
ArticleImage
有articleId
字段引用Article
的id
),很多配置可以省略。显式配置更清晰,但需要确保理解每个选项的含义。
- 原文描述: 在
3. 后端 API 与安全问题 (基于 JWT 部分)
- 问题 3.1: JWT 密钥管理
- 原文描述:
const JWT_SECRET = 'your_secret_key';
- 严重性: 高 (如果直接用于生产)。
- 密钥硬编码在代码中是非常不安全的。
- 改进建议: 密钥必须通过环境变量 (如
.env
文件配合dotenv
库) 或安全的配置服务来管理,并且密钥本身应该是强随机字符串。
- 原文描述:
- 问题 3.2: 错误处理和信息泄露
- 原文描述: 登录失败时返回
res.status(404).json({ error: '用户未找到' });
或res.status(401).json({ error: '无效的凭证' });
。 - 严重性: 中等。
- 区分“用户未找到”和“密码错误”会给攻击者提供枚举有效用户名的信息。
- 改进建议: 对于登录失败,应返回统一的、模糊的错误信息,例如
res.status(401).json({ error: '用户名或密码错误' });
。
- 原文描述: 登录失败时返回
- 问题 3.3:
HttpOnly
Cookie 的secure
和sameSite
属性- 原文描述: 提到了将 JWT 存储在
HttpOnly
Cookie 中,并给出了配置示例。res.cookie("authToken", token, { httpOnly: true, // 防止客户端JS访问 secure: process.env.NODE_ENV === 'production', // 仅在HTTPS下传输 maxAge: 3600000 * 1, // Cookie有效期 (例如1小时) sameSite: "Strict", // 防CSRF });
- 严重性: 低 (因为示例代码考虑到了这些)。这是一个好的实践。
- 关键点: 确保
secure: process.env.NODE_ENV === 'production'
的逻辑正确,并且生产环境强制 HTTPS。sameSite: "Strict"
或"Lax"
是防止 CSRF 的重要措施。
- 原文描述: 提到了将 JWT 存储在
- 问题 3.4: 权限校验的粒度
- 原文描述:
authorizeRole
中间件示例if (!req.user || req.user.role !== requiredRole)
。 - 严重性: 中等 (取决于应用复杂度)。
- 这种基于单一角色的校验对于简单应用尚可,但复杂应用可能需要更细粒度的权限控制(例如,基于操作的权限,或者用户同时拥有多个角色)。
- 改进建议: 考虑引入更完善的权限管理方案,如 RBAC (Role-Based Access Control) 或 ABAC (Attribute-Based Access Control)。
- 原文描述:
4. 前端 React 与 Vite 问题
- 问题 4.1: API 地址硬编码
- 原文描述:
fetch("http://localhost:3000/data")
- 严重性: 中等 (在需要部署到不同环境时)。
- API 地址硬编码不利于在不同环境(开发、测试、生产)中部署。
- 改进建议: 使用环境变量配置 API 基地址。例如,在 Vite 中通过
.env
文件和import.meta.env.VITE_API_URL
。
- 原文描述:
- 问题 4.2: 错误处理和用户反馈
- 原文描述:
catch (error) { console.error("Failed to fetch announcement:", error); setAnnouncement(" "); }
- 严重性: 中等。
- 仅在控制台打印错误对用户不友好。设置为空字符串可能也不是最佳的用户体验。
- 改进建议: 应该向用户显示明确的错误信息(例如,使用 Toast 通知、错误提示组件),并考虑重试机制。
- 原文描述:
- 问题 4.3: 状态管理选择
- 原文描述: 提到了 Context API 和 Redux/Zustand 等。
- 严重性: 低 (这是一个架构选择问题)。
- 说明: 指南中提到了多种状态管理方案是好的。选择哪种取决于应用规模和团队偏好。对于小型应用,
useState
和useReducer
+ Context API 可能足够。大型应用则可能从 Redux、Zustand 等库中受益。
总结
指南中提供的内容基本覆盖了现代Web开发的一些核心概念和技术栈,并且在一些地方(如 HttpOnly Cookie 的使用)体现了较好的安全意识。
主要的“严重”技术性问题集中在:
- 生产环境中
sequelize.sync({ alter: true })
的使用:这是最危险的一点,可能导致生产数据丢失。 - JWT 密钥的硬编码:严重的安全隐患。
- 登录错误信息过于具体:可能泄露用户信息。
- 自定义关联键
CHAR(6)
的设计:相比标准整数主外键,风险更高,性能可能更差。
其他问题更多是关于“最佳实践”、“代码健壮性”和“可维护性”的改进点。这份指南作为一个“入门指南”,在简化概念的同时,也需要在关键的生产安全和最佳实践方面给予更明确的警告和指导。
数据库 – MariaDB
-- 用户管理表
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY, -- 用户唯一标识
username VARCHAR(50) NOT NULL UNIQUE, -- 用户名,唯一
password VARCHAR(255) NOT NULL, -- 用户密码(加密存储)
email VARCHAR(100) NOT NULL UNIQUE, -- 用户邮箱,唯一
is_member TINYINT(1) DEFAULT 1, -- 用户是否会员(1=激活,0=未激活)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 创建时间
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 更新时间
);
-- 文章管理表
CREATE TABLE articles (
id INT AUTO_INCREMENT PRIMARY KEY, -- 文章唯一标识
publisher_id INT NOT NULL, -- 发布文章的用户ID
title VARCHAR(255) NOT NULL, -- 文章标题
content TEXT NOT NULL, -- 文章内容
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 创建时间
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间
FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE -- 用户删除时删除其文章
);
-- 文章图片表
CREATE TABLE article_images (
id INT AUTO_INCREMENT PRIMARY KEY, -- 图片唯一标识
article_id INT NOT NULL, -- 关联文章的ID
image_url VARCHAR(255) NOT NULL, -- 图片路径或URL
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 创建时间
FOREIGN KEY (article_id) REFERENCES articles (id) ON DELETE CASCADE -- 文章删除时级联删除图片
);
用户管理表的 最后更新时间
最后更新时间的意义和应用场景是什么呢?
意义
updated_at
字段的作用是记录一条数据的最后更新时间。它能够自动更新为数据被修改时的时间戳。这是一个非常实用的字段,尤其在需要追踪数据变化的场景中,可以轻松知道数据最近一次被操作的时间。
在设计数据库表结构中,updated_at
通常用于以下场景:
– 数据变更的记录: 用来标记一条数据最后被修改的时间。
– 数据同步: 在需要同步不同系统或服务器上的数据时,可以通过 updated_at
找到比某时间戳之后更新的数据。
– 调试和审计: 跟踪数据的更新历史,快速定位问题或分析系统行为。
应用场景
在用户表中,updated_at
可用于记录用户信息的最后修改时间:
- 用户修改资料: 当用户修改个人信息(如用户名、邮箱等)时,自动更新
updated_at
字段。 - 会员续费: 当会员续费或到期时间更改时,
updated_at
会更新为最新时间,方便追踪会员状态的变更。 - 调试问题: 如果用户报告登录或账户问题,可以检查
updated_at
了解最近的变更时间,协助排查问题。
一般网站数据库如何存储图片呢?
方案 1:文件系统/云存储 + 数据库记录路径或 URL
- 优点:
- 性能更高: 图片文件不存储在数据库中,数据库的读取和写入性能不会因图片数据增大。
- 易于扩展: 图片可以通过 CDN 或云存储加速分发,提高访问速度。
- 维护简单: 图片可以独立于数据库进行备份和管理。
- 存储成本低: 文件存储(尤其是云存储)比数据库存储更加经济。
- 缺点:
- 数据一致性问题: 图片文件和数据库记录可能不同步(如文件被意外删除,但数据库记录仍然存在)。
- 额外的存储配置: 需要搭建文件服务器或对接云存储服务。
方案 2:将图片直接存储在数据库中
- 描述:
图片文件以二进制数据的形式存储在数据库中,使用BLOB
(Binary Large Object)或TEXT
类型字段。 - 优点:
- 数据一致性: 图片和其他文章数据存储在同一数据库中,管理和备份更加方便。
- 易于迁移: 数据库迁移时,图片数据会一并迁移,无需额外操作。
- 安全性更高: 图片数据直接存储在数据库中,比文件系统更容易保护敏感信息。
- 缺点:
- 性能较差: 数据库的读写性能可能会因存储大量图片而显著下降。
- 备份和恢复速度慢: 数据库变得非常庞大后,备份和恢复会更加耗时。
- 存储成本高: 数据库通常使用更昂贵的存储方案,存储大量图片会增加成本。
为什么不把图片放在和文章一个表里呢
**之后,用 逗号 隔开它们。
在一个表中保存多张图片是可以做到的,但这取决于实际需求和场景。如果直接在一个表的字段中保存多张图片路径(例如用英文逗号分隔),虽然实现简单,但在扩展性、维护性和性能方面存在一些限制,因此通常不推荐。
下面是详细的分析,为什么不建议在一个字段中保存多张图片路径,以及可以这样做的场景。
1. 数据库设计原则
- 数据库设计遵循 “第一范式(1NF)” 的范式规则:
- 每个字段应该只存储一个值,而不是一组值。
- 如果一个字段存储了多个值(如用逗号分隔的图片路径),会违反第一范式,使得数据结构冗杂,难以管理和查询。
2. 查询复杂性
- 当数据存储为逗号分隔的形式时,查询和操作会变得困难。例如:
- 查询某篇文章是否包含某张特定的图片路径需要用
LIKE
或FIND_IN_SET
,效率较低。 - 统计或筛选包含特定图片的记录时,无法利用数据库的索引。
示例:查询包含/uploads/images/image2.jpg
的文章
- 查询某篇文章是否包含某张特定的图片路径需要用
SELECT *
FROM articles
WHERE FIND_IN_SET('/uploads/images/image2.jpg', image_urls);
- 这种查询无法利用索引,性能较差。
3.维护困难
- 当需要对单张图片进行操作时(比如删除、更新或替换图片路径),操作会变得复杂,容易出错。
- 例如,如果某张图片路径需要更新,必须解析整个字段并重新构建路径字符串。
示例:删除某张图片路径 – 如果字段
image_urls
的值为:
/uploads/images/image1.jpg,/uploads/images/image2.jpg,/uploads/images/image3.jpg
需要删除 /uploads/images/image2.jpg
:
- 必须在应用层解析字符串,删除对应路径后再更新字段内容。
- 操作繁琐,容易遗漏或出错。
4.扩展性不足
- 如果以后需要为每张图片添加额外信息(如图片的标题、描述、排序顺序等),逗号分隔的形式很难支持。
- 在单个字段下,图片的额外信息没法单独存储,无法满足复杂业务需求。
使用场景:
1. 图片数量较少
– 如果每篇文章的图片数量固定且较少(如 2-3 张),存储在一个字段中可能是可接受的。
2. 简单项目
– 在小型项目或实验性开发中,数据结构简单且无需复杂查询时,可以使用逗号分隔的形式存储图片路径。
3. 仅用于展示
– 如果图片仅用于简单展示,不需要复杂的查询、筛选或操作,逗号分隔的形式可以满足需求。
发表回复