前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket

# 数据库设计

30 秒速记

  • 从业务规则和查询方式出发设计表,不是把前端对象字段一股脑平铺
  • 必须始终成立的规则交给数据库约束:主键、NOT NULL、UNIQUE、外键
  • 「先查有没有再插入」在并发下会漏,唯一约束才是最后一道防线
  • 索引按实际查询建,用 EXPLAIN 验证,每多一个索引写入就慢一点
  • 时间用 DATETIME、金额用 DECIMAL 或整数分、状态用明确的枚举值,别全用 varchar

设计表的时候先问两个问题:哪些规则任何时候都不能被打破,以及页面会怎么查这些数据。 第一个问题决定约束,比如邮箱不能重复,就加唯一索引,别只靠代码里先查一下,两个请求同时进来都能查到「没有」,然后各插一条。第二个问题决定索引,列表页按状态筛选、按时间倒序,就建对应的联合索引,再用 EXPLAIN 看有没有真的用上。字段类型也要讲究,时间存字符串、金额存浮点,后面排序和对账都会出问题。

理解「数据库设计」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 database 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

用一张用户表对比一下「平铺字段」和「按业务规则设计」的差别:

-- 只是把对象字段平铺
CREATE TABLE user_info (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255),
  password VARCHAR(255),
  create_time VARCHAR(20),
  level INT
);

-- 按业务规则设计
CREATE TABLE user_info (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255) NOT NULL,
  password_hash VARCHAR(100) NOT NULL,          -- 存 bcrypt 摘要,不存明文
  status ENUM('active', 'disabled') NOT NULL DEFAULT 'active',
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted_at DATETIME NULL,                     -- 软删除
  UNIQUE KEY uk_email (email),                  -- 邮箱唯一由数据库保证
  KEY idx_status_created (status, created_at)   -- 服务后台列表页查询
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@前端进阶之旅: 代码已经复制到剪贴板

每一处改动背后都是一个具体问题:UNIQUE 解决并发注册重复;NOT NULL 防止脏数据;DATETIME 让排序和范围查询正确;utf8mb4 能存 emoji;联合索引对应 WHERE status = ? ORDER BY created_at DESC。

建完索引用 EXPLAIN 验证:

EXPLAIN SELECT * FROM user_info WHERE status = 'active' ORDER BY created_at DESC LIMIT 20;
-- key: idx_status_created,Extra 里没有 Using filesort,说明索引起作用了
@前端进阶之旅: 代码已经复制到剪贴板

软删除要留意一点:加了 deleted_at 之后,email 的唯一约束会让「注销后用同一邮箱重新注册」失败,这时要么把唯一键改成包含删除标记的组合,要么注销时改写邮箱。

面试官追问

追问 1注册时代码里先查了邮箱没被占用,为什么还会出现重复账号?
参考回答

两个请求几乎同时进来,都查到「没有」,然后各插一条。给 email 加 UNIQUE 约束,第二条插入会报 ER_DUP_ENTRY,代码捕获后提示「邮箱已注册」。

追问 2订单列表按 status 筛选、created_at 倒序,索引怎么建?
参考回答

建联合索引 (status, created_at),等值条件的列放前面。EXPLAIN 看 key 列用上了这个索引,Extra 里没有 Using filesort 就对了。

追问 3状态要加一个 pending,表上有 CHECK 约束,先改代码还是先改库?
参考回答

先改库放宽约束,再发会写 pending 的代码。反过来,新代码一上线写入就被约束拒绝。注意 MySQL 到 8.0.16 才真正执行 CHECK,之前只解析不生效。

追问 4索引越多查询越快,能不能把常用字段都加上索引?
参考回答

不行。每个索引都要在插入、更新时同步维护,写多的表加一堆索引写入会明显变慢,还占空间。按真实的慢查询来加,用不上的索引定期清掉。

追问 5创建时间存成 varchar(20),有什么问题?
参考回答

格式一不统一排序就乱了,'2024-9-1' 会排到 '2024-10-1' 后面;按时间范围查也用不好索引。直接用 DATETIME,默认值写 CURRENT_TIMESTAMP。

# 数据库设计

# 初始化数据库脚本

30 秒速记

  • 初始化脚本 = 建表 + 建约束和索引 + 插默认数据,一条命令跑完
  • 任何一条 SQL 失败都要以非零退出码结束,别打印「完成」然后退出码 0
  • 能重复执行:CREATE TABLE IF NOT EXISTS,默认数据用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE
  • 成功和失败分支都要关连接池,不然脚本跑完进程挂着不退
  • 表结构上线后还要改,就别再改初始化脚本,用迁移工具一版一版加

初始化数据库脚本要做到三点:一条命令建好表和默认数据,出错时明确失败,重复执行不会搞坏数据。 我见得最多的问题是脚本 catch 了错误打个日志就结束,退出码还是 0,CI 以为成功了,结果库只建了一半。另一个是连接池没关,脚本跑完进程一直挂着。表结构一旦上线,后续改动我会用迁移工具管理,每次变更一个文件,有版本号能回溯,而不是反复改那个初始化脚本。

理解「初始化数据库脚本」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 database 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

一个靠谱的 Node 初始化脚本骨架:

const fs = require('fs')
const path = require('path')
const mysql = require('mysql2/promise')

async function main() {
  const pool = mysql.createPool({
    host: process.env.DB_HOST || 'localhost',
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    multipleStatements: true, // 允许一次执行多条 SQL
  })
  try {
    const dir = path.join(__dirname, 'sql')
    const files = fs.readdirSync(dir).filter(f => f.endsWith('.sql')).sort()
    for (const file of files) {
      await pool.query(fs.readFileSync(path.join(dir, file), 'utf8'))
      console.log(`✔ ${file}`)
    }
    const [rows] = await pool.query('SHOW TABLES')
    console.log('当前表:', rows.map(r => Object.values(r)[0])) // 例如 [ 'user_info' ]
  } finally {
    await pool.end() // 成功失败都关,进程才能正常退出
  }
}

main().catch(err => {
  console.error('初始化失败:', err.message)
  process.exitCode = 1 // CI 靠这个判断失败
})
@前端进阶之旅: 代码已经复制到剪贴板

几个要点:

  • 文件按名字排序执行,比如 001-user.sql、002-seed.sql,顺序可控
  • 配置从环境变量读,密码不进仓库
  • 默认数据写成 INSERT IGNORE INTO ...,前提是表上有对应的唯一键,否则 IGNORE 起不到去重作用
  • 表上线之后的结构变更交给 Knex、Prisma Migrate 这类迁移工具,每次变更一个带版本号的文件

面试官追问

追问 1
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

数据库设计|Nodejs专题系列

# 数据库设计

30 秒速记

  • 从业务规则和查询方式出发设计表,不是把前端对象字段一股脑平铺
  • 必须始终成立的规则交给数据库约束:主键、NOT NULL、UNIQUE、外键
  • 「先查有没有再插入」在并发下会漏,唯一约束才是最后一道防线
  • 索引按实际查询建,用 EXPLAIN 验证,每多一个索引写入就慢一点
  • 时间用 DATETIME、金额用 DECIMAL 或整数分、状态用明确的枚举值,别全用 varchar

设计表的时候先问两个问题:哪些规则任何时候都不能被打破,以及页面会怎么查这些数据。 第一个问题决定约束,比如邮箱不能重复,就加唯一索引,别只靠代码里先查一下,两个请求同时进来都能查到「没有」,然后各插一条。第二个问题决定索引,列表页按状态筛选、按时间倒序,就建对应的联合索引,再用 EXPLAIN 看有没有真的用上。字段类型也要讲究,时间存字符串、金额存浮点,后面排序和对账都会出问题。

理解「数据库设计」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 database 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

用一张用户表对比一下「平铺字段」和「按业务规则设计」的差别:

-- 只是把对象字段平铺
CREATE TABLE user_info (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255),
  password VARCHAR(255),
  create_time VARCHAR(20),
  level INT
);

-- 按业务规则设计
CREATE TABLE user_info (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255) NOT NULL,
  password_hash VARCHAR(100) NOT NULL,          -- 存 bcrypt 摘要,不存明文
  status ENUM('active', 'disabled') NOT NULL DEFAULT 'active',
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted_at DATETIME NULL,                     -- 软删除
  UNIQUE KEY uk_email (email),                  -- 邮箱唯一由数据库保证
  KEY idx_status_created (status, created_at)   -- 服务后台列表页查询
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@前端进阶之旅: 代码已经复制到剪贴板

每一处改动背后都是一个具体问题:UNIQUE 解决并发注册重复;NOT NULL 防止脏数据;DATETIME 让排序和范围查询正确;utf8mb4 能存 emoji;联合索引对应 WHERE status = ? ORDER BY created_at DESC。

建完索引用 EXPLAIN 验证:

EXPLAIN SELECT * FROM user_info WHERE status = 'active' ORDER BY created_at DESC LIMIT 20;
-- key: idx_status_created,Extra 里没有 Using filesort,说明索引起作用了
@前端进阶之旅: 代码已经复制到剪贴板

软删除要留意一点:加了 deleted_at 之后,email 的唯一约束会让「注销后用同一邮箱重新注册」失败,这时要么把唯一键改成包含删除标记的组合,要么注销时改写邮箱。

面试官追问

追问 1注册时代码里先查了邮箱没被占用,为什么还会出现重复账号?
参考回答

两个请求几乎同时进来,都查到「没有」,然后各插一条。给 email 加 UNIQUE 约束,第二条插入会报 ER_DUP_ENTRY,代码捕获后提示「邮箱已注册」。

追问 2订单列表按 status 筛选、created_at 倒序,索引怎么建?
参考回答

建联合索引 (status, created_at),等值条件的列放前面。EXPLAIN 看 key 列用上了这个索引,Extra 里没有 Using filesort 就对了。

追问 3状态要加一个 pending,表上有 CHECK 约束,先改代码还是先改库?
参考回答

先改库放宽约束,再发会写 pending 的代码。反过来,新代码一上线写入就被约束拒绝。注意 MySQL 到 8.0.16 才真正执行 CHECK,之前只解析不生效。

追问 4索引越多查询越快,能不能把常用字段都加上索引?
参考回答

不行。每个索引都要在插入、更新时同步维护,写多的表加一堆索引写入会明显变慢,还占空间。按真实的慢查询来加,用不上的索引定期清掉。

追问 5创建时间存成 varchar(20),有什么问题?
参考回答

格式一不统一排序就乱了,'2024-9-1' 会排到 '2024-10-1' 后面;按时间范围查也用不好索引。直接用 DATETIME,默认值写 CURRENT_TIMESTAMP。

# 数据库设计

# 初始化数据库脚本

30 秒速记

  • 初始化脚本 = 建表 + 建约束和索引 + 插默认数据,一条命令跑完
  • 任何一条 SQL 失败都要以非零退出码结束,别打印「完成」然后退出码 0
  • 能重复执行:CREATE TABLE IF NOT EXISTS,默认数据用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE
  • 成功和失败分支都要关连接池,不然脚本跑完进程挂着不退
  • 表结构上线后还要改,就别再改初始化脚本,用迁移工具一版一版加

初始化数据库脚本要做到三点:一条命令建好表和默认数据,出错时明确失败,重复执行不会搞坏数据。 我见得最多的问题是脚本 catch 了错误打个日志就结束,退出码还是 0,CI 以为成功了,结果库只建了一半。另一个是连接池没关,脚本跑完进程一直挂着。表结构一旦上线,后续改动我会用迁移工具管理,每次变更一个文件,有版本号能回溯,而不是反复改那个初始化脚本。

理解「初始化数据库脚本」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 database 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

一个靠谱的 Node 初始化脚本骨架:

const fs = require('fs')
const path = require('path')
const mysql = require('mysql2/promise')

async function main() {
  const pool = mysql.createPool({
    host: process.env.DB_HOST || 'localhost',
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    multipleStatements: true, // 允许一次执行多条 SQL
  })
  try {
    const dir = path.join(__dirname, 'sql')
    const files = fs.readdirSync(dir).filter(f => f.endsWith('.sql')).sort()
    for (const file of files) {
      await pool.query(fs.readFileSync(path.join(dir, file), 'utf8'))
      console.log(`✔ ${file}`)
    }
    const [rows] = await pool.query('SHOW TABLES')
    console.log('当前表:', rows.map(r => Object.values(r)[0])) // 例如 [ 'user_info' ]
  } finally {
    await pool.end() // 成功失败都关,进程才能正常退出
  }
}

main().catch(err => {
  console.error('初始化失败:', err.message)
  process.exitCode = 1 // CI 靠这个判断失败
})
@前端进阶之旅: 代码已经复制到剪贴板

几个要点:

  • 文件按名字排序执行,比如 001-user.sql、002-seed.sql,顺序可控
  • 配置从环境变量读,密码不进仓库
  • 默认数据写成 INSERT IGNORE INTO ...,前提是表上有对应的唯一键,否则 IGNORE 起不到去重作用
  • 表上线之后的结构变更交给 Knex、Prisma Migrate 这类迁移工具,每次变更一个带版本号的文件

面试官追问

追问 1脚本中途一条建表语句报错,最后还是打印了「初始化完成」,退出码 0,怎么改?
参考回答

每条 SQL 都 await,外层 catch 里 console.error 之后 process.exitCode = 1。CI 靠退出码判断成败,日志写什么它不看。

追问 2脚本跑完了,日志也打了,终端却一直不退出,什么原因?
参考回答

连接池还开着,事件循环里还有活跃的句柄。在 finally 里 await pool.end(),成功和失败都会关。

追问 3测试库里已经有部分表了,再跑一次脚本就报错,要不要直接忽略错误?
参考回答

不要无脑忽略。建表用 IF NOT EXISTS,默认数据用 INSERT IGNORE,让重复执行是安全的;但如果发现同名表结构不一样,应该报错让人去处理。

追问 4建表、默认数据、检查查询全写在一个文件里好,还是拆开好?
参考回答

拆开。建表会一直演进,适合交给迁移工具按版本管;默认数据单独一个种子脚本。可以用一个入口按顺序调用,阶段清楚,哪步挂了一眼能看出来。

# 脚本目录

30 秒速记

  • 脚本在 init/sql/ 下:建 user_info 表 + 插一个默认管理员 admin001
  • IF NOT EXISTS 让建表能重复执行,但 INSERT 没有唯一键兜底,每跑一次就多一个管理员
  • 密码 123456 明文入库是演示写法,真实项目存 bcrypt 摘要
  • 改进点:email 加 UNIQUE、时间用 DATETIME、字符集用 utf8mb4
  • int(11) 里的 11 只是显示宽度,MySQL 8.0.17 起已废弃,不影响能存多大的数

这份脚本建了一张 user_info 用户表,再插入一条默认管理员数据,项目跑起来就能直接登录后台。 表用自增 id 做主键,存邮箱、密码、昵称、权限级别这些字段。作为演示没问题,但真要上线有几处要改:密码明文存是大忌,要存 bcrypt 这类摘要;email 没有唯一约束,INSERT 又没做去重,脚本多跑一次就多一个 admin001;时间存成 varchar 不好排序;utf8 在 MySQL 里其实是三字节的,存不了 emoji,新表直接用 utf8mb4。

← 12.3 分层操作12.5 路由设计 →

fe