☰
Egg.js第4天实战:路由、控制器与中间件搞定鉴权接口
2026/10/8 2:42:12 网站建设 项目流程

2026年这个学习计划里,我给自己排了一个“15天学完eggjs”的打卡清单。今天是第4天,进度条刚好走到四分之一。回头看看之前几天,其实都在打地基:装环境、跑通Hello World、理解目录结构。但从今天开始,画风要变了——今天要真正用eggjs写出带业务逻辑的接口,动手实现路由、控制器和中间件。如果你也在学Node.js后端,或者想找个企业级框架落地一个能上线的项目,这个系列应该能帮上忙。文章里的代码都是我今天现敲现跑的,没有滤镜,直接复制就能跑。

1. 学习路径全览:eggjs 的15天应该怎么拆

1.1 先回答一个关键问题:15天真的够吗

很多人看到“15天学完”第一反应是不靠谱,我一开始也这么想。但后来我给自己定了一条规则:不追求把每个API都背下来,只追求“每天产出一个可运行的小成果”。比如第1天装环境,第2天跑通一个接口,第3天理解目录规范,这样一天一个进度,15天下来其实足够覆盖一个后端框架从入门到能独立做项目的核心路径。

Egg.js不是一个大到没边的框架。它基于Koa封装,核心思想是“约定优于配置”,框架把目录结构、加载机制、插件生态都帮你定好了,你要学的东西其实是一套组织代码的规则,加上中间件、插件、Service这类扩展机制。这比从零学一个底层库要快得多。以我目前每天投入两小时左右的节奏来看,第10天左右就能独立写一个带数据库的API服务,后面的时间主要用来打磨部署和测试。

当然,15天能不能学完,取决于你怎么定义“学完”。如果你想达到“文档倒背如流、源码全看懂”的水平,那15天肯定不够。但如果目标是“给我一个需求,我能用eggjs把接口写出来,并且知道去哪里查文档”,那15天是一个非常合理的周期。

1.2 15天逐日计划表:每天都有可验证的产出

下面这份计划表是我在开跑之前就排好的,第4天正好卡在“基础”和“核心机制”的分界线上。前3天解决环境与项目骨架,第4天到第7天集中啃路由、中间件、Service和参数处理,第8天开始接数据库,后面进入真实项目实战。

天数主题当天可验证的产出
第1天Node.js环境、npm基础终端能正常执行node和npm命令
第2天创建Egg项目、跑通Hello World访问7001端口能看到页面
第3天目录结构、配置管理修改配置并观察生效,理解默认环境差异
第4天路由、控制器、中间件协作写一个带鉴权的接口并验证
第5天Service层与统一响应把业务逻辑抽到Service,返回统一结构
第6天参数校验与异常处理非法参数能返回友好错误提示
第7天模板渲染和静态资源做一个简单的列表页面
第8天接入MySQL与ORM接口能读写数据库数据
第9天JWT登录与完整鉴权用户登录后拿到token访问受保护接口
第10天文件上传与第三方接口对接实现头像上传、调用外部API
第11天定时任务与队列场景每天定时执行一次报表任务
第12天单元测试与基础脚本对核心接口写单测并跑通
第13天日志、监控与性能分析每个请求打印耗时日志,定位慢接口
第14天线上部署与进程守护部署到服务器并能稳定运行
第15天项目复盘与面试整理整理一份eggjs核心知识清单

这张表里,第4天的定位非常明确:前3天你只能“让框架跑起来”,今天开始你要“让框架为你干活”。中间件和控制器是之后几乎所有功能的基础,今天不打牢,后面做登录鉴权、日志统计、参数校验都会很吃力。

1.3 前3天结束时的基础:一条最简接口长什么样

学第4天之前,先确认一个地基:一条最简Route是什么样子。Egg.js项目的入口目录是app,路由写在app/router.js,控制器放在app/controller。下面这个例子在第三天就能跑通。

// app/controller/home.js const { Controller } = require('egg'); class HomeController extends Controller { async index() { this.ctx.body = { message: 'Hello Egg.js' }; } } module.exports = HomeController;
// app/router.js module.exports = app => { const { router, controller } = app; router.get('/', controller.home.index); };

这里最让新手困惑的是:为什么router.js里的controller.home.index不需要require?因为Egg.js在启动时会自动加载app/controller目录下的所有文件,并且按文件名挂载到app.controller对象上。你不需要手动管理依赖关系,只需要把文件放到正确的位置。这就是“约定优于配置”的直观体现。

前3天还有一个容易被忽视的点:this.ctx.body = xxx是同步返回响应的方式,而且必须在controller方法内部直接赋值。很多人第一次写的时候会在return里写数据,发现接口返回空,原因就在这里。Egg.js的controller方法不需要return,所有响应内容都通过this.ctx来设置。

2. 第4天核心任务:搞懂路由、控制器和中间件

2.1 今天的学习定位:从“骨架”切换到“业务开发”

前3天学的目录结构、配置文件,都只是骨架,接口逻辑放在controller里固然能跑,但如果所有东西都堆在controller里,项目很快就会变成一锅粥。第4天要解决的,就是“一个请求从浏览器进来,到拿到响应,中间到底经过了多少层、每一层负责干什么”。

Egg.js处理一次请求的链路大致是:请求先进路由,路由把请求交给对应的controller,controller在需要时调用service处理业务逻辑,最后把结果写到ctx.body返回给客户端。而中间件就像流水线上的关卡,在请求进入controller前后都可以插入逻辑。比如判断用户是否登录、打印请求耗时、设置跨域头,这些都是中间件的典型使用场景。

你可以把中间件理解成商场入口的一排安检口:每个安检口只负责一件事,有的看身份证,有的查背包,全部通过才能进入商场核心区域。Egg.js的中间件也是这个逻辑,一个请求要经过多个中间件之后才会到达controller,而每个中间件都有机会在请求继续往下走之前,或者在controller处理完之后做一些事情。

2.2 控制器与路由:先让接口立起来

先看一眼今天会频繁接触的controller完整写法。下面这个UserController有两个方法:list返回一个用户列表,profile返回单个用户信息。

// app/controller/user.js const { Controller } = require('egg'); class UserController extends Controller { async list() { const users = [ { id: 1, name: '小明', age: 20 }, { id: 2, name: '小红', age: 22 }, ]; this.ctx.body = { code: 0, data: users }; } async profile() { this.ctx.body = { code: 0, data: { id: 1, name: '小明', age: 20, role: 'admin' }, }; } } module.exports = UserController;

对应的路由需要这样写:

// app/router.js module.exports = app => { const { router, controller } = app; router.get('/api/user/list', controller.user.list); router.get('/api/user/profile', controller.user.profile); };

这里有一个非常实用的知识点:this.ctx身上挂载了几乎所有请求相关信息。ctx.query能拿到URL上的查询参数,ctx.params是路由参数,ctx.request.body是POST请求体,ctx.headers是请求头。第4天至少要记住这几个,后面写接口几乎每天都在用。

关于route的写法,需要注意路径别冲突。比如先注册/api/user/list,再注册/api/user/:id,如果两个路由都能匹配到同一个地址,Egg会按注册顺序优先匹配先定义的那条。所以把精确路由写在参数路由前面是一种好习惯。

2.3 第一个中间件:算一下每个请求的耗时

前面一直说中间件重要,不如直接写一个。在app/middleware下新建文件cost.js,文件名会被自动注册为中间件名称。

// app/middleware/cost.js module.exports = () => { return async function cost(ctx, next) { const start = Date.now(); await next(); const cost = Date.now() - start; ctx.logger.info(`${ctx.method} ${ctx.url} 耗时 ${cost}ms`); }; };

然后在config/config.default.js里加上以下配置,就能让这个中间件对所有请求生效。

// config/config.default.js module.exports = () => { const config = {}; config.middleware = [ 'cost' ]; return config; };

有两点值得展开说一下。

第一,中间件导出的必须是一个函数,这个函数返回另一个异步函数。外层函数用来接收options配置,内层函数才是真正处理请求的中间件逻辑。新手最容易犯的错是少包一层,写成module.exports = async function(ctx, next) {...},这样框架加载的时候会直接报错。

第二,config.middleware数组里的顺序就代表执行顺序。cost这个中间件里,await next()前面的代码会在请求进入controller之前执行,后面的代码会在controller处理完之后执行。这种先进入后返回的模式就是常说的“洋葱模型”,中间件越多,一层套一层,但核心逻辑清晰。

换个场景,如果只想让某个路由走这个中间件,可以不配置config.middleware,而是直接在路由里加:

const { router, controller, middleware } = app; router.get('/api/user/list', middleware.cost(), controller.user.list);

把middleware.cost()放在路由处理函数前面,只有这个接口会执行耗时统计。全局配置和局部配置各有用处,今天先掌握两种方式,后面用起来就不会局限。

3. 实操项目:从零写一个带鉴权校验的用户接口

3.1 今天这个小目标具体是什么

单看知识点还是容易飘,所以我给第4天安排了一个小项目:做一个用户接口,其中列表接口可以直接访问,但用户详情接口必须带着正确的token才能访问,否则返回401。这个场景非常常见,它同时用上了controller、router、中间件三种能力,又把“鉴权”这个核心需求落地了一次。

需要额外说明的是,这里为了专注中间件写法,token直接写死在代码里,真实项目绝对不能用这种方式。到第9天我们会用JWT做完整的登录鉴权链路,今天只为了让中间件的触发逻辑看得见、摸得着。

3.2 初始化项目:推荐先用官方脚手架

如果前3天已经创建过项目,直接用旧项目就行。如果今天想从头建一个,推荐用egg官方脚手架,一条命令搞定。

mkdir egg-day4 && cd egg-day4 npm init egg --type=simple npm install npm run dev

npm init egg --type=simple会生成一个最小的项目模板。安装依赖完成后,npm run dev启动,默认端口是7001,浏览器访问http://127.0.0.1:7001能看到默认页面。

如果你之前装了其他目录结构比较乱的项目,建议今天统一用脚手架重新初始化一遍。Egg.js对目录结构很敏感,文件放错位置不会直接报错,但路由和控制器的自动加载会失效,排查起来非常痛苦。

3.3 核心代码:用户列表、详情与鉴权中间件

首先新建app/controller/user.js,写入用户controller。这里用内存数组模拟数据库,好处是不需要安装任何东西,思路也直观,后续第8天把数据源换成MySQL时,只改Service层就行。

// app/controller/user.js const { Controller } = require('egg'); class UserController extends Controller { async list() { const users = [ { id: 1, name: '小明', age: 20 }, { id: 2, name: '小红', age: 22 }, ]; this.ctx.body = { code: 0, data: users }; } async profile() { this.ctx.body = { code: 0, data: { id: 1, name: '小明', age: 20, role: 'admin' }, }; } } module.exports = UserController;

然后在app/middleware目录下新建auth.js,这就是今天的主角——鉴权中间件。

// app/middleware/auth.js module.exports = () => { return async function auth(ctx, next) { const token = ctx.headers.token; if (!token || token !== 'egg-2026') { ctx.status = 401; ctx.body = { code: 401, message: '未授权,请携带正确的 token' }; return; } await next(); }; };

最后修改app/router.js,把接口和中间件串起来。list接口不需要鉴权,profile接口在controller之前插入auth中间件。

// app/router.js module.exports = app => { const { router, controller, middleware } = app; router.get('/api/user/list', controller.user.list); router.get('/api/user/profile', middleware.auth(), controller.user.profile); };

这里有一个细节值得注意:middleware.auth()写的是调用,不是引用。auth定义的时候是一个函数,它返回真正处理请求的中间件函数,所以必须加括号调用。如果写成middleware.auth,框架只会把函数本身当路由处理逻辑传入,而不会执行它,接口会直接报错。

3.4 启动验证:三条curl命令看懂全流程

代码写完,启动项目:

npm run dev

启动日志如果提示egg started on http://127.0.0.1:7001,说明一切正常。接下来用三条命令验证效果。

第一条命令请求列表接口,不需要任何额外header,应该正常返回数据。

curl http://127.0.0.1:7001/api/user/list

响应:

{"code":0,"data":[{"id":1,"name":"小明","age":20},{"id":2,"name":"小红","age":22}]}

第二条命令直接请求用户详情接口,因为没有携带token,中间件会把请求拦下来,返回401。

curl http://127.0.0.1:7001/api/user/profile

响应:

{"code":401,"message":"未授权,请携带正确的 token"}

第三条命令带上正确的token头再次请求,这次中间件放行,controller里的profile方法正常执行。

curl http://127.0.0.1:7001/api/user/profile -H "token: egg-2026"

响应:

{"code":0,"data":{"id":1,"name":"小明","age":20,"role":"admin"}}

这三条命令基本把第4天的核心链路走通了:不带token被中间件拦截,带token放行到controller,最终返回业务数据。你在浏览器里也能看到同样的效果,只是浏览器设置header比较麻烦,推荐用curl或者postman做这类验证。

4. 第4天常见问题与排查技巧

4.1 高频报错与解决速查表

第4天开始涉及多个文件联动,报错的概率明显比前3天高。归纳一下今天最容易遇到的几个问题和排查方向。

现象可能原因排查与解决
访问接口返回404路由没有注册,或注册路径和访问路径不一致检查app/router.js路径,确认文件和导出结构
端口被占用,启动失败上一个dev进程没关干净lsof -i:7001找到PID后kill,或改config.port
接口返回404但路由存在controller返回了undefined,或没有赋值ctx.body检查controller方法结尾有没有this.ctx.body = ...
中间件完全没有执行没有在config或router中声明中间件确认config.middleware数组和文件名匹配
中间件执行报错middleware.auth少了括号写成middleware.auth()调用,不能只传函数引用
修改代码后行为不变dev进程崩了或文件没保存重启npm run dev,再看启动日志有没有语法错误

这些坑看起来琐碎,但每一条都是我实际踩过或者帮别人排查过的。尤其是404问题,新手很容易在controller、router、文件路径三者之间绕晕,排查顺序就是:先看启动日志有没有加载文件,再看路由注册对不对,再看controller是否执行。

4.2 调试技巧:让日志替你说话

写后端和写前端最大的差异是,后端没法随时打开控制台看状态,很多问题要靠日志。第4天开始,建议有意识地把调试习惯从console.log切换到框架日志。

在controller和中间件里,可以直接用ctx.logger打印日志:

ctx.logger.debug('这是调试日志'); ctx.logger.info('这是普通日志'); ctx.logger.warn('这是一个警告'); ctx.logger.error('这是一个错误');

ctx.logger会按照级别输出到终端,同时写入logs目录下的日志文件。这个习惯要尽早养成,因为后面项目大了,线上环境根本看不到终端,只能靠日志排查问题。

如果启动阶段就想看框架内部加载过程,可以带DEBUG环境变量启动:

DEBUG=egg* npm run dev

启动日志会打印很多内部信息,包括路由注册、中间件加载、插件初始化等。新手第一次看到那么多日志可能会懵,但只需要重点关注有没有类似router: register和middleware: use的输出。

4.3 复盘:今天值得记下来的三个点

最后对第4天做一个简短复盘,我认为有三个点是最值得写进笔记的。

第一,中间件函数的结构是“箭头式”的:外层函数接收参数,内层异步函数处理请求,await next()前后分别是请求前和响应后逻辑。这个结构理解了,以后写任何中间件都能一眼看懂。

第二,controller不负责“思考”,只负责“响应”。这只是一个开始,今天数据写在controller里看起来还凑合,但后面一旦引入数据库查询、第三方接口调用,controller就会变得臃肿。第5天会把业务逻辑抽到Service层,今天先记住这个演变方向。

第三,路由层面可以做鉴权,但真正的鉴权往往会落在更底层。今天我们是在每个需要保护的接口前面手动加中间件,实际项目里可能有几十个接口都要保护,这时候就会用到全局中间件加路由白名单机制,这个内容到第9天做JWT时会完整展开。

最后再分享一个个人体会。我之前第一次用Egg.js的时候,喜欢在一个中间件里写好多逻辑,既想校验token,又想记录日志,还想处理异常,结果后来每次改动都心惊胆战。中间件这个东西,功能越单一越好维护。今天做完这个带鉴权的小项目,最大的收获不是学会了写那个auth.js,而是理解了“每个中间件只干一件事”这种设计习惯。带着这个感觉继续学后面的Service、插件、数据库,你会发现Egg.js真正厉害的地方不是帮你写好了代码,而是帮你养成了一套还算不坏的后端组织方式。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询