尧图精选

Egg.js实战:从配置分层到中间件鉴权,15天学习计划第14天全记录

🕒 发布时间:2026/10/2 9:35:53 📁 来源:尧图网络
1. 15天学习计划走到第14天先复盘整个路线按照“2026年15天学习完 Egg.js”的计划今天已经是第14天。走到这一步大多数知识点的学习和基础练习已经结束真正留给今天的任务只有一件把分散的技能点串成一个能交付、能上线、能拿得出手的完整项目。如果你也正在用15天这类极限周期学一个框架会发现真正拉开差距的从来不是“懂多少API”而是能不能在有限时间内把项目从“本地能跑”推进到“结构清晰、配置合理、接口安全、逻辑可测”的状态。先说清楚我这15天是怎么排的方便你在参考时自由调整第1天到第3天掌握 Egg.js 的目录结构、application/context/request/response 这些核心对象以及“约定优于配置”的开发模式。第4天到第5天中间件、路由、控制器、服务把一次完整请求的链路彻底跑通。第6天到第7天插件机制与扩展机制这周是 Egg 的灵魂包括如何用 app.xxx 扩展、如何封装自己的插件。第8天到第9天数据持久化选的是 Sequelize 方案覆盖模型定义、迁移、关联查询。第10天到第12天业务实战做一个带登录鉴权、权限校验、资源管理的小系统。第13天单元测试和接口自测把覆盖率和常见测试套路补齐。第14天则用来解决一个我看来所有框架学习中最后也最容易被忽略的问题怎么让项目在别人的机器、服务器以及生产环境下也能稳定运行。这一天的实战价值比前面任何一天都高。很多初学者学到这阶段容易陷入“教程抄完即毕业”的状态觉得接口能返回数据就万事大吉但实际工作中最难处理的往往是环境差异、配置隔离、部署流程、依赖版本这类比写业务更琐碎、又更容易让人连续加班的事情。所以这篇文章的记录重点我不打算再重复最基本的 Egg.js 入门示例而是完整还原我第14天的实操过程从配置分层开始到中间件与插件的搭配使用再到鉴权模块的实现、测试排错和部署准备把“能上线”这个目标拆开揉碎。你学完后如果手上正好有一个写到一半的 Egg.js 项目完全可以按同一条路径推进。2. 学会从应用配置到框架扩展的细节处理2.1 环境配置分层与 mixin 机制Egg.js 的配置设计非常顺手核心逻辑是用不同的文件区分运行环境。我第6天刚接触时只会改config.default.js把所有配置一股脑塞进去直到第9天做用户模块时才意识到如果不区分环境本地连的数据库地址和线上完全没法共存。Egg.js 配置规则是默认配置config.default.js永远加载环境变量EGG_SERVER_ENV或NODE_ENV决定叠加哪个具体环境的配置比如config.prod.js、config.local.js、config.test.js。取值时框架会把同名字段做合并数组是 concat对象是浅拷贝不是简单覆盖。以我当时项目里的数据库配置为例// config.default.js exports.sequelize { dialect: mysql, host: 127.0.0.1, port: 3306, database: egg_blog_dev, username: root, password: , timezone: 08:00, }; // config.prod.js exports.sequelize { host: 10.0.0.12, database: egg_blog_prod, username: deploy, password: process.env.DB_PASSWORD, };这里有两个细节我提醒你注意。第一线上密码不要写死用环境变量注入没人想看到你因为把数据库密码提交到 Git 仓库而被运维约谈。第二timezone: 08:00务必加上否则 Sequelize 读取 DATETIME 字段时会默认按 UTC 处理前端展示时间直接少8小时这种问题排查起来非常耗时而且坑得非常隐蔽。如果你用的是 egg-sequelize 插件还需要关注字段是否走 Sequelize 的同步生产环境建议用 migration 管理表结构而不是直接sequelize.sync()后者在你改字段类型时会干出什么我不敢想。2.2 配置内容的敏感字段与运行时取值光有环境配置文件还不够我第14天特意做了一次“配置体检”把所有写在配置里的敏感内容全部清理了一遍。判断标准很简单任何一个配置项如果换了环境或换了接手的人会不一样就不能硬编码在代码里。推荐的做法是在config.default.js中读取process.env并设置默认值// config.default.js exports.cluster { listen: { port: process.env.PORT || 7001, }, }; exports.redis { client: { port: process.env.REDIS_PORT || 6379, host: process.env.REDIS_HOST || 127.0.0.1, password: process.env.REDIS_PASSWORD || , db: 0, }, };有没有默认值这件事很重要它决定了你的项目会不会在别人本地拉下来之后因为缺少一个环境变量而直接启动失败。我在实际项目里吃过一次亏线上启动脚本给某个服务传了配置本地开发时没设置Egg 直接抛异常排查了大半个小时才发现是漏了环境变量。所以凡是牵扯到环境差异的配置务必三件套齐全默认值、环境变量读取、注释说明这个配置是干什么的以及哪类环境要覆盖。2.3 在扩展机制中注册自己的通用方法配置搞定之后今天上午我还优化了项目里一个最明显的坏味道很多公共逻辑散落在各个 Controller 里。比如响应格式统一处理、从 token 里解析用户信息、分页参数整理每个 Controller 各写一份改一处漏一处。Egg.js 提供了四类扩展点分别是对Application、Context、Request、Response的扩展。统一的响应结构我挂在Context上因为每次请求都是一个独立的 context 实例在中间件或者控制器里都能拿到// app/extend/context.js module.exports { success(data, message ok, code 0) { this.body { code, message, data, }; }, fail(message fail, code 1) { this.body { code, message, data: null, }; }, };用的时候在任何 Controller 里直接this.success(...)或者ctx.fail(...)就行。说实话这个方法很笨但它能强制整个项目所有接口返回结构一致前端联调的体验会提升非常明显。你也完全可以在这个基础上加类似pageInfo、parseToken之类的方法原则只有一个全局通用的能力优先考虑扩展局部通用的能力才放进 Service避免 Service 层变成垃圾箱。3. 中间件和插件的组合才是 Egg.js 真正出效果的地方3.1 理解洋葱模型与中间件执行顺序第5天初次接触中间件时我就发现大家常用 koa 洋葱模型来类比请求从最外层打进最内层响应从最内层逐层返回中间件可以同时在进入和离开时做两件事。如果你只把中间件当成“过滤请求”的工具很多高级玩法就错过了。我用一段典型代码说明中间件顺序为什么重要。假设你有三个中间件分别是日志、鉴权、耗时统计// config/config.default.js exports.middleware [logger, auth, cost];实际执行时请求会按照logger - auth - cost - 业务控制器 - cost - auth - logger的顺序走完。等于是入栈出栈的关系。那鉴权中间件必须放在 cost 前面否则“没登录的用户也跑完了统计逻辑”浪费性能倒在其次更严重的是某些中间件可能会因为拿不到用户信息直接抛错。我建议在你项目里保留一个记录响应耗时的中间件这对调优和排查问题非常有用// app/middleware/cost.js module.exports () { return async function cost(ctx, next) { const start Date.now(); await next(); const duration Date.now() - start; ctx.logger.info([reporter] ${ctx.method} ${ctx.url} cost${duration}ms); }; };记得中间件文件必须返回箭头函数或普通函数Egg 在加载时会调用它拿到真正的中间件函数如果直接导出函数本身大概率会在运行时报“middleware must be a function”之类的错误。3.2 从生态中挑选能力对口的插件Egg.js 的插件生态不算多但每个都很关键。我这15天实际用到的插件组合如下插件用途配置要点egg-sequelizeORM、数据库映射注意版本和 mysql2 匹配egg-jwt签发与校验 token统一配置 secretegg-cors跨域处理区分本地联调和线上域名egg-validate参数校验在控制器里做入参校验egg-redis分布式缓存与 session 存储生产环境必备egg-router-plus增强版路由支持命名空间与正则拿 egg-cors 举例这个插件配置不当会造成一个非常诡异的现象本地 axios 请求接口一切正常部署到测试环境后浏览器直接报 CORS 错误。原因在于我把允许的域名写死在了配置里而测试环境的访问域名根本没有加进白名单。exports.cors { origin: (ctx) { const origin ctx.get(Origin); if (/^https?:\/\/localhost(:\d)?$/.test(origin)) return origin; if (origin origin.endsWith(.yourdomain.com)) return origin; return ; }, credentials: true, allowMethods: [ GET, HEAD, PUT, POST, DELETE, PATCH ], };这里把origin配置成函数要比写死数组灵活很多。加上credentials: true后前端请求就必须明确携带withCredentials: true否则 Cookie 不会随请求发送登录态也就一直建立不起来。3.3 从“会用”到“能自己写一个插件”第14天我还做了一件事把项目里的统一鉴权逻辑抽成了一个本地插件复习了 Egg.js 插件机制。Egg 插件本质上就是一个“迷你应用”它有自己的app、ctx、middleware、config、extend并且能被多个项目复用。写插件时建议注意这几个要素包名必须以egg-开头如果是本地插件可以放lib/plugin下并命名为egg-xxx但那只是目录名正式发包时还是得按 npm 规则。package.json 里eggPlugin字段声明这个插件依赖了哪些插件。插件代码里可以通过app.config读取使用方传入的配置项并提供默认值。举个例子写一个给现有项目用的egg-auth插件// app/lib/plugin/egg-auth module.exports (app) { app.config.auth app.config.auth || { expires: 60 * 60 * 2, ignore: [ /api/login, /api/register ], }; app.beforeStart(async () { app.logger.info([egg-auth] 插件启动完成); }); };不过我这里先打住因为这个属于“进阶改造”如果你现在只是一个学了十几天的初学者老老实实先把中间件和扩展机制用透比一上来追求插件化更稳妥。我是在第13天的测试时反复复制一坨鉴权代码到多个中间件里才下定决心抽插件的。4. 鉴权模块实战从零写一个能过测试的登录接口4.1 模型定义与迁移文件的取舍登录鉴权是所有管理系统都绕不开的核心模块也是第14天项目实战的重头戏。我用的是 Sequelize 管理用户数据先定义一个用户模型// app/model/user.js module.exports (app) { const { STRING, INTEGER, DATE } app.Sequelize; const User app.model.define(user, { id: { type: INTEGER, primaryKey: true, autoIncrement: true }, username: { type: STRING(32), unique: true, allowNull: false }, password: { type: STRING(128), allowNull: false }, nickname: { type: STRING(64), allowNull: false }, createdAt: { type: DATE }, updatedAt: { type: DATE }, }); return User; };密码在数据库里绝对不能存明文这一点我是从第9天开始就严格执行的。用 bcrypt 对密码做哈希哪怕是同一个密码每次生成的哈希值也不同可以有效防止彩虹表攻击// app/service/user.js const bcrypt require(bcryptjs); async findOrCreate(username, password) { const hash bcrypt.hashSync(password, 10); const [ user, created ] await this.ctx.model.User.findOrCreate({ where: { username }, defaults: { username, password: hash, nickname: 新用户, }, }); return { user, created }; }关于建表我第9天图省事用app.beforeStart里执行sync()自动同步数据库的字段能创建出来但坑在于线上环境一旦运行sync()而某张表已经存在模型里字段名和数据库不一致时框架不会删字段却可能因为类型不一致引发奇怪报错。建议正式的开发流程里创建 migration 文件来管理表结构团队协作时也能用 migration 做表结构评审。4.2 Service 层负责业务Controller 只做转发写到这里必须强调一个 Egg.js 社区最看重的分层思想Controller 只负责接收请求、调用 Service 返回结果任何复杂的逻辑和数据库操作都要下沉到 Service。很多从 Express 转过来的人很习惯在路由回调里写完所有逻辑到了 Egg 依然把几十行代码堆在 Controller 里结果就是单元测试极其难写因为你很难 mock 掉一堆函数。我把登录和注册的 Controller 写得非常薄// app/controller/auth.js const Controller require(egg).Controller; class AuthController extends Controller { async register() { const { ctx, service } this; ctx.validate({ username: string, password: string }, ctx.request.body); const result await service.auth.register(ctx.request.body); ctx.success(result); } async login() { const { ctx, service } this; ctx.validate({ username: string, password: string }, ctx.request.body); const result await service.auth.login(ctx.request.body); ctx.success(result); } } module.exports AuthController;Service 里的实现// app/service/auth.js const Service require(egg).Service; const bcrypt require(bcryptjs); class AuthService extends Service { async register(body) { const { ctx } this; const exists await ctx.model.User.findOne({ where: { username: body.username } }); if (exists) { ctx.fail(用户名已存在, 10001); return; } const hash bcrypt.hashSync(body.password, 10); const user await ctx.model.User.create({ username: body.username, password: hash, nickname: body.username, }); const token await this.generateToken(user); return { token, user: { id: user.id, username: user.username, nickname: user.nickname } }; } async login(body) { const { ctx, app } this; const user await ctx.model.User.findOne({ where: { username: body.username } }); if (!user) { ctx.fail(用户名或密码错误, 10002); return; } const valid bcrypt.compareSync(body.password, user.password); if (!valid) { ctx.fail(用户名或密码错误, 10003); return; } const token await this.generateToken(user); return { token, user: { id: user.id, username: user.username, nickname: user.nickname } }; } async generateToken(user) { const { app } this; return app.jwt.sign( { id: user.id, username: user.username }, app.config.jwt.secret, { expiresIn: app.config.jwt.expires } ); } } module.exports AuthService;这里ctx.fail()是前面扩展 context 时定义的方法统一了错误的返回结构。你可能会问为什么 Service 里要用ctx.fail()而不是直接throw两者都可以但我个人偏好业务提示类错误用返回体处理系统异常类错误用ctx.throw()或boom处理。这样接口联调时状态码和业务码的语义清晰不会一个 500 把多个业务错误掩盖掉。4.3 自定义鉴权中间件保护接口有了 token还需要一个中间件去校验受保护接口的请求头。这里要留意 egg-jwt 的用法它自带一个jwt中间件但它的实现是比较通用化的我建议自己写一层薄封装方便之后扩展白名单、黑名单或刷新 token 的逻辑// app/middleware/auth.js module.exports () { return async function auth(ctx, next) { const token ctx.get(Authorization) || ; if (!token || !/^Bearer\s/.test(token)) { ctx.fail(未登录, 401); return; } try { const decoded ctx.app.jwt.verify(token.replace(Bearer , ), ctx.app.config.jwt.secret); ctx.state.user decoded; await next(); } catch (e) { ctx.fail(Token 无效或已过期, 401); } }; };注册时配合 ignore 参数把放行接口标出来// config/config.default.js exports.middleware [cost, auth]; exports.auth { ignore: [ /api/login, /api/register, /api/public, ], };这里我碰到过几次顺序问题如果我把 auth 放在 cost 前面那每次请求的成本统计里就包含了 token 校验的时间逻辑上没问题但如果你只想统计纯业务耗时就把它放在 auth 后面。中间件顺序一定要结合你的业务目标来决定没有绝对正确的套路。4.4 用 curl 实测整个链路代码写完不等于验完我一般会先用 curl 快速走一遍接口省得在调试工具里来回切换。下面是第14天实测登录接口的过程# 注册 curl -X POST http://127.0.0.1:7001/api/register \ -H Content-Type: application/json \ -d {username:testuser,password:123456} # 响应 # {code:0,message:ok,data:{token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...,user:{id:1,username:testuser,nickname:testuser}}} # 携带 token 访问受保护接口 curl http://127.0.0.1:7001/api/users/1 \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # 不带 token 访问应返回 401 curl http://127.0.0.1:7001/api/users/1 # {code:1,message:未登录,data:null}这就是一条完整的鉴权链路。整个流程跑通后我给项目的 jsdoc 补上关键注释一瞬间觉得这个项目至少可以拿给另一个后端同事接手了。5. 测试与排错第14天踩过的5个坑5.1 版本兼容问题egg 与 sequelize 匹配不当Egg.js 版本文档更新得不算频繁但 Sequelize 的迭代速度很快。我第8天按一份旧教程安装时直接装了个sequelize6随后版本冲突数据库连接建立了但查询时字段映射错乱。排查方法先看package.json里 egg 的版本再去查 egg-sequelize 插件对应的依赖范围最后确认 sequelize 主版本。不要因为 npm install 不报错就认为没版本问题。下面是一组经过验证的稳妥搭配Egg 3.x egg-sequelize 6.x sequelize 6.xEgg 2.x egg-sequelize 5.x sequelize 5.x安装命令尽量使用npm i egg-sequelize6 sequelize6 mysql2 --save5.2 ESLint 从入门到劝退第4天我第一次跑npm test时被 ESLint 一大片报错整懵了。Egg 官方模板自带一套严格配置包括强制分号、函数括号前空格、不允许未使用变量等。如果一开始不写规范代码最后会花大量时间改格式。我给你的建议是从第三天起就把 VS Code 的 ESLint 插件真实跑起来editor 里出现黄色波浪线立刻处理别等到最后再统一改。真的遇到整个文件已存在大量旧格式问题再考虑// eslint-disable-line这种临时的“还债”手段但千万别把它当成常规操作。5.3 egg-bin dev 偶发提示端口占用开发时间内反复重启进程偶尔会碰到端口无法启动。常见原因是旧的 worker 进程没被回收干净尤其 macOS 上egg-bin dev有时会留下 agent 进程。解决方式简单粗暴先查进程再手动清理lsof -i :7001 kill -9 PID同时也提醒你Egg 的 cluster 模式默认会启动 agent 和多个 worker日常开发时如果不需要多进程调试可以设置单进程跑能少踩很多“进程间调试信息不透明”的坑EGG_SERVER_ENVlocal egg-bin dev --workers15.4 session 存储不稳定接入 redisEgg 自带内存型 session但开发阶段随意重启就会丢登录态。一旦项目里有多实例部署的需求内存 session 几乎不可用。我在第11天就把 session 切换到了 egg-redisexports.session { key: EGG_SESS, maxAge: 7 * 24 * 3600 * 1000, renew: true, }; exports.redis { client: { port: 6379, host: 127.0.0.1, password: , db: 0, }, };前提是要先装好一个 Redis 服务否则应用启动时会直接往 Redis 写数据失败则抛异常。本地没有 Redis 的话直接用 Docker 起一个一分钟搞定docker run -d --name egg-redis -p 6379:6379 redis:7-alpine这里有一个细节session 换到 redis 后调试完记得观察 Redis 里是否堆积了大量过期 key建议给 key 统一设置 TTL否则等到线上流量上来你会发现 Redis 内存涨得吓人。5.5 部署环境 node 版本导致的兼容问题Egg 3.x 对 Node 版本有要求低于 14 基本跑不起来Egg 2.x 也需要 Node 8 或以上。我在第14天下午部署到一台旧服务器时发现 Node 版本是 12启动直接报SyntaxError。所以在配置 CI 或收集部署文档时最好明确写清推荐的 Node 版本。如果部署环境无法升级 Node有一种临时方案是先在本地用相同 Node 版本跑一次构建把产物传到服务器。但治本的方法还是给项目增加.nvmrc文件比如# .nvmrc 18.20.4然后在部署脚本里nvm use或 CI 阶段node-version统一指定这样至少不会因为本机和服务器 Node 版本差异出现“本地好好的一上线就崩”的灵异事件。6. 第15天收尾清单与长期扩展方向6.1 最后的查漏补缺文档与目录整理15天计划最后一天不可能再学新东西更应该做的是让项目像一份可交付的成果。我给自己列的清单是每个 Service 方法和 Controller 方法补上功能注释。README.md写明项目启动步骤、环境变量清单、测试命令。检查 package.json 里的 scripts 是否覆盖 dev、start、test、lint。确认.gitignore排除了 node_modules、logs、本地环境变量文件。把依赖做一次npm audit顺手处理中高危漏洞。很多初学阶段的人都会忽略 README但换位思考一下一个后端项目如果别人拿到手不知道跑起来需要哪些环境变量对你的评价会大打折扣。文档写得不求多么华丽清晰准确即可。6.2 压测内容不多但值得做一次Egg.js 本身性能基础不错但业务代码写得好不好会明显影响吞吐量。第15天我会用简单的ab或wrk快速做一次压测不追求压到瓶颈主要看接口压测时有没有明显的内存上涨、超时或连接异常。比如一个简单的 GET 接口最好能跑出每秒几百到上千 QPS如果明显很低优先查中间件是否太肥、SQL 是否有 N1 查询、日志是否同步写入阻塞了事件循环。这块需要单独一篇长文这里就点到为止。6.3 未来的方向Egg.js 之后该学什么严格说 Egg.js 是一个企业级框架掌握它之后你后续可以按这个顺序继续拓宽如果你所在团队偏向微服务可以了解 egg-grpc 或者把服务拆分成独立的 Egg 应用用消息队列解耦。如果前端学了 React/Vue可以把 Egg.js 作为纯 API 后端接上 Token 鉴权与权限模型做成完整的前后端分离项目。如果对框架内核感兴趣就去读 Egg.js 源码里 loader、plugin、router 的实现很多设计思想能直接用到其他语言框架里。Egg.js 还可以跟 TS 结合官方早就提供egg-ts-helper等工具做类型推导。如果15天让你产生了对 Node.js 服务端的兴趣往 TypeScript 方向走是性价比较高的选择。7. 写在最后一次极限学习计划带来的真实感受15天学完一个企业级框架听起来像口号真正做下来却比想象中更考验节奏管理。我的体会是每天不需要贪多但每天都要写代码哪怕只写十行也要让知识转化成项目里的实际产物。第14天之前我还在担心信息量太大记不住但当我把配置、中间件、鉴权、测试、部署这些环节全部串起来之后Egg.js 对我而言就从一个“有文档的框架”变成了“一堆能解决实际问题的工具”。它对约定式开发、插件扩展和请求链路管理的处理方式也影响了我后来看待其他后端框架的眼光。如果你也在按自己的节奏学 Egg.js无论处在第几天都建议尽早拿真实项目开刀试炼。别光看我的示例和别人的教程动手把登录注册、权限控制、数据校验这些模块在你自己本地跑通一次踩过几个坑之后才算真正入门。最后再分享一个小技巧第15天开始前把整份笔记按“配置 / 中间件 / 插件 / 测试 / 部署”五个标签重新归类一遍你回看时会发现框架学习最有价值的不是代码量而是形成了一套稳定、可迁移的排查与设计思路。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →