从Node.js到eggjs:三天打通路由、控制器与服务层主链路

发布时间:2026/10/3 18:20:44

从Node.js到eggjs:三天打通路由、控制器与服务层主链路 2026年了居然还能看到 eggjs 这种老牌 Node 框架被放进学习清单这事儿本身就挺有意思。作为一个从 Express 时代写过来的后端我对 eggjs 的态度从最开始的不屑——约定这么多一点都不自由到现在的真香中间也就隔了一个大型项目的距离。今天是 15 天学习计划的第 3 天前两天我们刚把 Node.js 的异步模型和 Koa 的洋葱模型过完今天正式动手碰工程。目标很直接亲手初始化一个 eggjs 项目搞清楚它那套目录约定再把 路由 → 控制器 → 服务 这条请求主链路彻底跑通。这篇文章不是带你背文档而是一份真实操作记录我用了哪些命令、中间踩了什么坑、为什么框架这样设计。适合有 JS 基础、还没正经写过 Node 服务端的同学学完你可以直接拿这套结构去开新项目。1. 为什么第3天就开始动工程1.1 前两天到底铺垫了什么很多同学学 eggjs 上来就是npm init egg装完也不知道一堆目录是干嘛的遇到问题更是一脸懵。所以我刻意把 15 天计划的前两天留给了 Node.js 和 Koa 基础不碰 eggjs 本身。第 1 天过了一遍 Node 的事件循环、Buffer、Stream 这些底层概念不求精通但至少知道fs.readFile为什么是异步的、process.nextTick和setImmediate的区别在哪里。第 2 天写了个最小化的 Koa 服务手动注册了两个中间件看着next()前后的打印顺序把洋葱模型在实际代码里跑了一遍。这两天的铺垫在今天全用上了。eggjs 的核心是 Application、Context、Request、Response 这四类对象它们都继承自 Koa 的对应实现。只要你理解 Koa 里ctx.body hello是什么意思那 eggjs 里ctx.body {}返回 JSON 就是同一件事没有任何新概念。如果把 eggjs 比作一栋精装房Koa 就是毛坯房和施工规范前两天我们是在看施工规范今天开始直接搬进精装房但房间里哪儿是承重墙、哪儿能改心里有数。1.2 eggjs 到底解决了什么问题直接写 Koa 也不是不行但项目一大人就多了问题就来了路由文件堆成山控制器里写了三千行 SQL有人用req.query有人用ctx.request.query定时任务、日志、参数校验全都是各写各的。这不是代码能力问题是没有约束。eggjs 做的事就是约定优于配置把工程结构、代码分层、文件命名全部定死你不是在跟队友的代码风格博弈而是在跟框架的规则对齐。打个比方Koa 就像一间自由工位谁想坐哪儿都行eggjs 是一栋带工位牌的公司HR 一进门就知道你坐哪、你的岗位职责是什么。自由度降低换来的是团队协作的确定性。2026 年还聊这套是因为企业级 Node 服务里它依然能打尤其在需要多人维护、长期迭代的中后台系统里这种结构化带来的收益远超那点被限制的自由。1.3 第3天要完成的三件事今天内容我给自己定了三个验收标准不达标不算过第一能用脚手架新建一个 eggjs 项目并且通过npm run dev跑起来浏览器能访问到默认页面第二能说出来app/router.js、app/controller、app/service、config这几个目录各自负责什么为什么文件放对位置就能被自动加载第三能写一个真实的接口从路由到控制器再到服务层完整走通返回一段 JSON 数据并且知道参数从哪取、错误怎么处理。这三件事做完今天就算彻底值了。2. 初始化一个可运行的 eggjs 工程2.1 环境版本检查别一上来就敲命令先确认本机环境。我平时习惯先跑两条命令看一眼版本花不了十秒钟能避免后面一堆莫名其妙的兼容性问题。node -v npm -veggjs 官方要求 Node.js 版本不低于 16但 2026 年还在用 16 真说不过去建议至少 18最好是 20 以上的 LTS 版本。我本机用的是 Node 20 LTSnpm 10全程没遇到依赖安装的问题。如果你的 Node 版本过旧先升级再继续后面装依赖时遇到ERR_PNPM_UNSUPPORTED_ENGINE或者 node-gyp 编译报错大概率就是版本太老跟 eggjs 本身没关系。2.2 脚手架初始化eggjs 提供了官方脚手架执行npm init egg后会进入模板选择。我用的是 simple 模板这个模板足够干净不包含 sequelize、view 这类额外插件适合第 3 天用来理解主链路。mkdir egg-day3 cd egg-day3 npm init egg --typesimple npm i npm run devnpm init egg实际会下载create-egg这个初始化器--typesimple的意思是直接用 simple 模板免去交互选择。装依赖用npm i就行如果网络不太好也可以换成cnpm i但我不太推荐后面排查问题的时候 npm 官方源的错误信息往往更直观。跑完npm run dev后终端会打印日志最后一行会出现Egg application started on http://127.0.0.1:7001。浏览器打开这个地址看到页面就说明初始化成功了。我也见过有人这里卡住端口被占用会直接报EADDRINUSE我下面专门有一节讲排查。2.3 工程目录第一印象项目跑起来之后先别急着写代码花几分钟把目录结构过一遍。simple 模板生成的项目大概是这个形态egg-day3/ ├── app/ │ ├── controller/ │ │ └── home.js │ ├── public/ │ └── router.js ├── config/ │ ├── config.default.js │ ├── plugin.js └── package.json说实话这个模板目录很少容易让人产生就这的错觉其实它是刻意精简的。真正的 eggjs 项目里还会有app/service、app/middleware、app/extend、app/view、test这些目录今天后面都会用到。模板只是起点你按约定补目录上去框架会自动识别不需要改任何配置文件。2.4 开发模式和普通启动有什么区别npm run dev是 egg-bin dev开发专用支持代码热更新你改完controller里的文件保存进程会自动重启省得手动 CtrlC 再跑一遍。npm start是 egg-scripts start生产环境用它会做进程守护异常崩溃后自动拉起日志也会写到文件里。第 3 天只需要关心开发模式但这两个命令的区别最好现在就记住后面部署时才不会用混。3. 目录结构不是规矩是帮你写代码的3.1 核心目录逐个说清楚我见过太多人学 eggjs 卡在目录结构上不是因为难而是因为不知道为什么文件放这里就能生效。先把核心目录的职责一次性说清楚后面写代码就有方向了。路径职责访问方式app/router.js配置 URL 与处理函数的映射关系启动时自动加载app/controller/接收请求参数调用 Service返回响应app.controller.xxx.xxxapp/service/业务逻辑查数据库、调接口、算数据ctx.service.xxx.xxxapp/middleware/请求前后执行的通用逻辑鉴权、日志、跨域等config/config.default.js中启用app/extend/扩展 Application、Context、Request 等内建对象app.xxx或ctx.xxxconfig/配置文件端口、插件开关、自定义配置项代码里通过app.config读取这张表对应的是我后面几天会反复用到的地图。你需要形成肌肉记忆路由只负责转发控制器负责接客服务层负责干活。这个分层不是 eggjs 发明的但它用目录结构帮你强制固化下来了。3.2 Loader 自动加载机制刚才说的文件放对位置就自动生效背后是 eggjs 的 Loader 机制。框架启动的时候Loader 会扫描这些约定目录把文件加载进来并且自动挂载到 Application 或 Context 上。你写的app/controller/home.js会被挂成app.controller.homeapp/service/todo.js会被挂成ctx.service.todo。所以你在 router.js 里写controller.home.index不需要自己 require 这个文件。如果你在 controller 里需要调 service直接ctx.service.todo.list()中间没有任何手工 import。这套机制省掉的是整个项目里成百上千个维护文件依赖关系的代码代价是你必须严格遵循命名规则。文件名、方法名的大小写一旦对不上框架不会有任何报错你只会发现接口莫名其妙 404 或者返回 undefined。3.3 目录的约定细节约定不是光把文件放对目录就行命名规则也得注意。路由里controller.todo.list对应的是app/controller/todo.js文件里名为list的方法。如果控制器文件在子目录比如app/controller/admin/user.js路由里就要写controller.admin.user.add一层目录一个点。Service 同理ctx.service.todo.list对应app/service/todo.js里的list。类名用大驼峰还是小驼峰其实框架不挑但文件名和路由引用必须完全一致我习惯文件用小驼峰命名类名用大驼峰分得清楚。4. 打通主链路router → controller → service4.1 设计一个待办接口学框架最好的方式不是看文档而是给自己派一个业务需求。我今天的练习需求很简单做一个待办事项接口GET /api/todos返回一组待办列表。需求很简单但足够走完整条链路。后面如果想要扩展成增删改查也只是在这个基础上加路由和方法。4.2 先写路由打开app/router.js把默认注释清掉改成module.exports app { const { router, controller } app; router.get(/, controller.home.index); router.get(/api/todos, controller.todo.list); };第一行是模板自带的首页接口保留无妨。第二行是新增的URL 是/api/todosHTTP 方法是 GET处理函数指向controller.todo.list。看到controller.todo.list你应该条件反射地想到我需要去app/controller/todo.js里写一个名为list的方法。4.3 写控制器在app/controller目录下新建todo.js内容如下const Controller require(egg).Controller; class TodoController extends Controller { async list() { const { ctx } this; const list await ctx.service.todo.list(); ctx.body { code: 0, data: list, message: ok, }; } } module.exports TodoController;注意两个关键点。第一控制器类必须继承Controller这是 egg 提供的基础类你的方法里才能拿到this.ctx。第二方法必须用普通函数而不是箭头函数因为 egg 在调用时会把这个方法的 this 绑定到控制器实例上箭头函数是在定义时绑定 this会拿不到ctx这是新手最容易踩的坑下面有单独一节讲。控制器里我做了一件很重要的事没有让 controller 直接返回数据而是await ctx.service.todo.list()先拿去服务层处理。这看起来多绕了一步但这一步是 eggjs 一切分层思想的起点。4.4 写服务层在app/service目录下新建todo.jsconst Service require(egg).Service; class TodoService extends Service { async list() { return [ { id: 1, title: 学习 eggjs 第3天, done: false }, { id: 2, title: 打通 router-controller-service, done: true }, ]; } } module.exports TodoService;服务层同样继承Service基础类。这个类内部也可以通过this访问ctx、app、config这意味着你在 service 里可以拿到请求参数、读取配置、调用框架能力。今天只是一个模拟数据真实项目里这里会是数据库查询、缓存读取或者第三方接口调用但调用方式一模一样。为什么强烈推荐加这层最简单的原因是复用。一个查询待办列表的业务可能多个控制器里都要用直接写在 controller 里就会复制粘贴两份后面改逻辑要记得改两处。写在 service 里所有调用方都走同一个方法改一处就够了。第二个原因是可测试性service 是纯业务逻辑可以脱离 HTTP 上下文单独做单元测试controller 却要模拟整个请求链路。第三个原因是为后面几天的内容做准备学了数据库之后你会往 service 里塞 ORM 代码那时候就知道这层有多重要。4.5 验证接口保存文件后dev 模式会自动重启。用 curl 验证一下curl http://127.0.0.1:7001/api/todos正常会返回{code:0,data:[{id:1,title:学习 eggjs 第3天,done:false},{id:2,title:打通 router-controller-service,done:true}],message:ok}到这一步你就完成了第一个完整的 eggjs 接口。不是模板页面是你自己从路由到控制器再到服务层写的业务代码。这条链路后面会重复无数次今天走一遍的意义是把每个文件该写什么、方法怎么互相调用彻底焊在脑子里。5. 参数获取与响应处理的正确姿势5.1 GET 参数query 和 params只写一个不接收参数的接口说实话还停留在玩具阶段。真实业务里参数从三个地方来URL 的 query string、URL 里的路径参数、POST 请求的 body。eggjs 里分别对应ctx.query、ctx.params、ctx.request.body。举个例子。如果路由是router.get(/api/todos/:id, controller.todo.detail)请求地址是/api/todos/5?verbosetrueclass TodoController extends Controller { async detail() { const { ctx } this; const id ctx.params.id; // 5 const verbose ctx.query.verbose; // true ctx.body { id, verbose }; } }ctx.query拿到的是 query string 解析后的对象ctx.params.id是路由里:id通配符的实际值。要注意的是这两个值默认都是字符串。如果你需要数字类型要么在后续逻辑里转换要么用 eggjs 自带的参数校验插件来做类型转换和校验这一步先记住它们是字符串就够了。5.2 POST 参数body 从哪取POST 请求的 body 稍微麻烦一点。eggjs 内置了 bodyParser 中间件默认支持application/json和application/x-www-form-urlencoded。在控制器里这样取class TodoController extends Controller { async create() { const { ctx } this; const body ctx.request.body; // { title: 写博客, done: false } ctx.body { received: body }; } }新手最常见的错误是用ctx.body来取请求体但ctx.body是响应体ctx.request.body才是请求体。还有一次我排查了很久发现前端传的是text/plain格式bodyParser 默认不解析这种类型ctx.request.body就是空对象。所以遇到 post 接口收不到参数先看请求的 Content-Type 对不对。5.3 统一返回 JSON 结构我在第 4 节里已经用了{ code, data, message }这种返回结构。这不是 eggjs 强制的是企业实践里的惯例。好处很明显前端不管接口成不成功都能从约定好的字段里取数据错误类型通过 code 区分而不是靠解析 HTTP 状态码。实际项目里通常还会封装一个ctx.success(data)这样的辅助方法用 eggjs 的 extend 机制挂到 context 上后面我会专门讲 extend 的写法。第 3 天先手动写这个结构感受一下统一格式带来的舒适感。5.4 异常与错误处理真实接口不可能永远成功数据库可能查不到服务可能超时参数可能不合法。控制器的错误处理我推荐 try/catch ctx.throw的组合。class TodoController extends Controller { async list() { const { ctx } this; try { const list await ctx.service.todo.list(); ctx.body { code: 0, data: list, message: ok }; } catch (err) { ctx.logger.error(err); ctx.throw(500, 查询待办失败); } } }ctx.throw(500, 查询待办失败)会抛出一个状态码为 500 的异常eggjs 内部会统一处理成 HTTP 错误响应。这里有个重要的顺序问题先catch到错误再ctx.throwthrow 之后的代码不会继续执行。如果你在 try 里先给ctx.body赋值catch 里又 throw那ctx.body会被覆盖成错误信息吗答案是不会throw 直接中断了响应链框架会返回错误结构。所以别担心响应体被污染但要记住服务层抛出的异常一定要在 controller 这层接住否则用户会看到一行堆栈信息观感很差。6. 第3天常见问题与排查技巧实录6.1 端口占用解决EADDRINUSE 127.0.0.1:7001是第 3 天最可能出现的问题。你之前跑过别的 Node 服务或者上次 dev 进程没有完全退出端口被占用了。两个解决办法一是把旧进程杀掉重启 dev二是换一个端口启动eggjs 支持直接传参npm run dev -- --port7002然后访问http://127.0.0.1:7002。不过我更推荐找到占用端口的进程把它结束掉因为默认 7001 端口约定俗成日常交流时大家都默认这个地址。6.2 修改代码后没生效dev 模式大多数文件修改会触发自动重启但我遇到过几次例外修改config目录下的文件后进程看似重启了实际加载的还是旧配置。这时候最稳妥的方案是手动 CtrlC 停掉进程重新npm run dev。如果你的代码改了半天没生效别急着怀疑自己的代码先确认进程是不是真的重启了。观察终端日志重启时会出现新的启动信息如果只是文件刷新但没重启干脆手动重启顺便能排除很多玄学问题。6.3 this 指向问题控制器方法写成箭头函数是我见过最隐蔽的坑。代码看起来没问题一访问接口就报Cannot read properties of undefined (reading body)。原因前面说过egg 在调用 controller 方法时会把 this 绑定到控制器实例而箭头函数没有自己的 this会捕获定义时所在作用域的 this导致方法里的this.ctx不是控制器上的上下文。记住一条铁律controller 和 service 的所有业务方法都写成普通函数或者 async 普通函数永远不要用箭头函数。6.4 Service 找不到或方法不存在接口请求到了控制器但一调用ctx.service.todo.list()就报Cannot read properties of undefined大概率是三个原因。文件名对不上比如 service 文件名是Todo.js但代码里写的是todo方法名对不上类里写了listTodo但调用的是list或者目录名写错service写成了services。拿这个问题说回 Loader 的约定机制文件放错路径或者名字大小写不对都不会有编译报错只有运行时访问得到 undefined 才会暴露。排查的时候先去目录看一眼文件名再回代码看一眼引用名九成问题都出在这里。6.5 快速排查工具箱第 3 天不需要高深调试技巧掌握三个土办法就行。第一在 service 方法里console.log打印关键变量dev 模式下日志会直接出现在终端比断点更快第二用 curl 或者浏览器开发者工具里的 Network 面板看请求响应区分是 404、500 还是参数错误第三看 eggjs 启动日志每次启动都会打印加载了哪些 router、监听哪个端口URL 敲下去没反应的时候回去对比路由表格里的路径有没有写错。6.6 问题速查表现象可能原因处理方式启动报 EADDRINUSE7001 端口被占用换端口或杀掉旧进程改代码不生效dev 进程未自动重启手动重启 devctx.query为 undefined请求不是 GET检查路由方法ctx.request.body为空对象Content-Type 不是 JSON前端改请求头访问接口 404路由没写对或方法没定义检查 router.js 和 controller 文件名ctx.service.xxx报错文件名/方法名大小写不一致目录文件名对一遍返回了[object Promise]service 方法没有 await控制器里加await7. 最后说点我的实际感受按惯例每 3 天一个小节点。今天跑通这条主链路之后后面第 4 天学中间件、第 5 天学扩展都是在同一个骨架上做加法你会发现自己突然就看懂了那些项目里的每个文件为什么出现在那个目录。我给自己的一个小习惯是每天下午用十分钟把当天的接口写成 curl 命令存下来后面学测试用例的时候这些命令就是现成的测试素材。你在实际操作中如果今天卡了壳大部分时间都会花在环境问题和命名问题上这都很正常。框架本身的逻辑说白了就是那三张目录表记住就够。明天开始就可以往 service 里塞点有分量的东西了。
延伸阅读

更多相关文章

2026/10/3 18:15:43

ComfyUI一键整合包:8G显存跑SDXL的工程实践

1. 项目概述:为什么这个“秋叶ComfyUI一键整合包”值得你花5分钟认真读完我从2023年夏天开始在工作室带新人跑Stable Diffusion工作流,前前后后搭过不下20套环境——Windows上用原生PythonGit手动编译,Mac上折腾HomebrewConda多版本共存&…

2026/10/3 18:15:43

Hermes Agent企业级多智能体协同实战:Harness Engineering工程落地指南

1. 这不是又一个“AI Agent入门课”,而是一份能直接跑通企业级多智能体协同的工程实录你搜过“Hermes Agent”吗?我搜过——在B站、GitHub、Obsidian社区、甚至几个小众技术论坛里翻了整整三天。不是找教程,是找“有人真用它上线了什么”。结…

2026/10/3 19:00:45

GitHub日榜趋势速报:从抓取到技术雷达的实战指南

1. GitHub 日榜趋势速报的定位与价值 1.1 为什么值得每天花十分钟看日榜 GitHub 日榜趋势速报这件事,我从 2019 年就开始断断续续地跟,中间停过一阵,后来又捡起来,原因很简单:它是目前少有的、能让你在十分钟内感知到…

2026/10/3 19:00:45

GD32三种低功耗模式详解:从睡眠到待机,续航从月变年

前阵子帮朋友改一个温湿度记录仪,主控是GD32F303。原来装3节AAA电池,一个月出头就得换,朋友怀疑单片机漏电,拿万用表一测,正常运行电流确实只有十几毫安,问题在于它几乎从不睡觉,偶尔进一下低功…

2026/10/3 18:55:45

Hadoop伪分布式搭建与电商商品推荐实战

简介:本资源是一套基于Hadoop生态构建的轻量级商品推荐系统实践项目,面向大数据初学者、高校课程设计学生及分布式计算入门开发者,聚焦电商场景下的用户行为分析与个性化推荐落地。项目依托HDFS分布式存储与MapReduce批处理框架,完…

2026/10/2 8:16:46

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/10/2 18:20:53

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/10/3 15:02:19

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/10/3 0:04:31

国内大学生必备的AI写作辅助软件是哪款?

国内高校学生在论文写作过程中,越来越依赖AI辅助工具提升效率,主流方案以本土化全流程工具为核心,结合通用大模型与专业插件,覆盖选题构思、框架搭建、初稿撰写、查重降重、格式调整等关键环节,本文将深入解析当前主流…

2026/10/3 0:04:31

Codex接入Jev模型完整指南:配置方法、本地部署与踩坑排查

最近不少人在讨论 Codex 搭配 Jev 这套玩法,我一开始没太当回事,直到自己把 Jev 接进 Codex跑了几轮编码任务之后,才明白那些说“直接起飞”的人是怎么想的。Codex 作为工具本身已经够能打了,但模型固定、上下文策略固定&#xff…

2026/10/3 0:04:31

GitHub 热门: NVIDIA/Model-Optimizer

👋 Hi,我擅长 AI 大模型应用落地、意识解码与 AI 开发工具链 。 💡 创业路上,用技术换时间,一起把 AI 变成生产力 🚀 >GitHub 热门: NVIDIA/Model-Optimizer 凌晨两点,你刚把跑通了的 Qwen3.…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑