现代网页开发入门指南书 – MariaDB(数据库)

前言

如今的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 来修正。
  • 问题 1.2: 使用 CHAR(6) 作为自定义关联键 (Release level)
    • 原文描述: 提出使用名为 “Release level” (在 article 表) 和 “level” (在 article_image 表) 的 CHAR(6) 字段,通过随机6位数进行关联。
    • 严重性: 中高。
      • 唯一性风险: “随机6位数”如果生成算法不够健壮,有碰撞(重复)的风险,尤其是在数据量大时。UNIQUE 约束可以防止插入重复值,但不能解决生成算法本身的问题。
      • 性能: 字符串类型的键通常比整数类型的键在索引和连接操作上性能稍差。
      • 可读性和维护性: 虽然在某些特定场景下自定义关联键有其用途,但通常情况下,使用自增整数主键 (id) 作为外键引用目标更简单、高效且符合常规实践。
      • SQL字段名: Release level 这样的带空格的字段名在 SQL 中需要用反引号 `Release level` 包裹,容易出错且不推荐。
    • 改进建议: 坚持使用 article.id (主键) 和 article_image.article_id (外键) 进行关联。如果确实需要一个人类可读的、唯一的发布标识,可以额外增加一个 release_code 字段,并确保其唯一性,但不一定用它作为主外键关联。
  • 问题 1.3: ON UPDATE CASCADE 的普遍使用
    • 原文描述: 在外键定义中同时使用了 ON DELETE CASCADEON 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 (默认) 会自动管理 createdAtupdatedAt 字段,通常更方便。如果只需要 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: 模型关联中 sourceKeytargetKey 的理解
    • 原文描述: 在 Article.hasMany(ArticleImage, { foreignKey: 'article_id', sourceKey: 'id' })ArticleImage.belongsTo(Article, { foreignKey: 'article_id', targetKey: 'id' }) 中,sourceKeytargetKey 的使用。
    • 严重性: 低。这里的用法是正确的,因为它们都指向了 Article 表的 id 字段。sourceKey 是源模型(Article)中与外键关联的键,targetKey 是目标模型(Article)中被外键引用的键。当外键引用的不是目标模型的主键时,或者源模型的关联键不是其主键时,这些选项才更有区分度。对于标准的主键-外键关联,有时可以省略。
    • 说明: 当关联字段名与默认规则一致时 (例如,ArticleImagearticleId 字段引用 Articleid),很多配置可以省略。显式配置更清晰,但需要确保理解每个选项的含义。

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 的 securesameSite 属性
    • 原文描述: 提到了将 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 的重要措施。
  • 问题 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 等。
    • 严重性: 低 (这是一个架构选择问题)。
    • 说明: 指南中提到了多种状态管理方案是好的。选择哪种取决于应用规模和团队偏好。对于小型应用,useStateuseReducer + Context API 可能足够。大型应用则可能从 Redux、Zustand 等库中受益。

总结

指南中提供的内容基本覆盖了现代Web开发的一些核心概念和技术栈,并且在一些地方(如 HttpOnly Cookie 的使用)体现了较好的安全意识。

主要的“严重”技术性问题集中在:

  1. 生产环境中 sequelize.sync({ alter: true }) 的使用:这是最危险的一点,可能导致生产数据丢失。
  2. JWT 密钥的硬编码:严重的安全隐患。
  3. 登录错误信息过于具体:可能泄露用户信息。
  4. 自定义关联键 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

  • 优点:
    1. 性能更高: 图片文件不存储在数据库中,数据库的读取和写入性能不会因图片数据增大。
    2. 易于扩展: 图片可以通过 CDN 或云存储加速分发,提高访问速度。
    3. 维护简单: 图片可以独立于数据库进行备份和管理。
    4. 存储成本低: 文件存储(尤其是云存储)比数据库存储更加经济。
  • 缺点:
    1. 数据一致性问题: 图片文件和数据库记录可能不同步(如文件被意外删除,但数据库记录仍然存在)。
    2. 额外的存储配置: 需要搭建文件服务器或对接云存储服务。

方案 2:将图片直接存储在数据库中

  • 描述:
    图片文件以二进制数据的形式存储在数据库中,使用 BLOB(Binary Large Object)或 TEXT 类型字段。
  • 优点:
    1. 数据一致性: 图片和其他文章数据存储在同一数据库中,管理和备份更加方便。
    2. 易于迁移: 数据库迁移时,图片数据会一并迁移,无需额外操作。
    3. 安全性更高: 图片数据直接存储在数据库中,比文件系统更容易保护敏感信息。
  • 缺点:
    1. 性能较差: 数据库的读写性能可能会因存储大量图片而显著下降。
    2. 备份和恢复速度慢: 数据库变得非常庞大后,备份和恢复会更加耗时。
    3. 存储成本高: 数据库通常使用更昂贵的存储方案,存储大量图片会增加成本。

为什么不把图片放在和文章一个表里呢

**之后,用 逗号 隔开它们。

在一个表中保存多张图片是可以做到的,但这取决于实际需求和场景。如果直接在一个表的字段中保存多张图片路径(例如用英文逗号分隔),虽然实现简单,但在扩展性、维护性和性能方面存在一些限制,因此通常不推荐。

下面是详细的分析,为什么不建议在一个字段中保存多张图片路径,以及可以这样做的场景。

1. 数据库设计原则

  • 数据库设计遵循 “第一范式(1NF)” 的范式规则:
    • 每个字段应该只存储一个值,而不是一组值。
    • 如果一个字段存储了多个值(如用逗号分隔的图片路径),会违反第一范式,使得数据结构冗杂,难以管理和查询。

2. 查询复杂性

  • 当数据存储为逗号分隔的形式时,查询和操作会变得困难。例如:
    • 查询某篇文章是否包含某张特定的图片路径需要用 LIKEFIND_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. 仅用于展示
– 如果图片仅用于简单展示,不需要复杂的查询、筛选或操作,逗号分隔的形式可以满足需求。


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注