1. 为什么第一次接 MongoDB 总卡在“连不上”这一步
如果你刚开始学 egg,大概率会遇到这样一个场景:项目脚手架跑起来了,npm run dev也能看到localhost:7001的欢迎页,但一旦想接本地 MongoDB,就开始报错——要么是MongooseError: connect ECONNREFUSED 127.0.0.1:27017,要么是Cannot read property 'Student' of undefined,要么是启动日志里压根看不到数据库连接成功的提示。
egg 本身是基于 Koa 封装的企业级 Node.js 框架,它把约定优于配置这件事做到了极致:目录结构、插件加载、配置合并都有固定套路。egg-mongoose 则是官方生态里用来对接 MongoDB 的插件,它做的事情很单纯——把 mongoose 实例挂到app.mongoose上,再通过app.model暴露给 controller 和 service 使用。听起来简单,但第一次配的时候,config.default.js里写什么、plugin.js里要不要显式声明、model 目录怎么命名、启动后怎么确认真的连上了,这几个点任何一个没对齐,都会让你在“明明照着文档写了却跑不通”的状态里耗掉一晚上。
这篇内容聚焦的就是这个落地场景:从零搭一个 egg 项目,装上 egg-mongoose,把本地 MongoDB 的连接配置写进config.default.js,在plugin.js里启用插件,按约定建好 model 目录,最后用一个最小的读写请求验证连接是否成功,并且能在终端日志里看到明确反馈。目标很直接——你复制配置、改一下数据库名,就能跑通一次完整的写入和查询。
适合谁看:刚接触 egg、本地已经装好 MongoDB、想用 mongoose 做数据层但不想在配置环节反复试错的人。下面所有步骤都是可复制的,命令和配置我会给全,踩过的坑也会标出来。
2. 前置准备:TaoToken 与本地环境确认
在动 egg 之前,先把两件事确认好,不然后面报错会分不清是环境问题还是配置问题。
第一件是本地 MongoDB 是否真的在跑。macOS 上用brew services list看 mongodb-community 的状态,Windows 上在服务里找 MongoDB Server,Linux 用systemctl status mongod。更直接的验证方式是开一个终端执行:
mongosh --eval "db.runCommand({ ping: 1 })"返回{ ok: 1 }就说明数据库活着。如果这一步就失败,先去把 MongoDB 服务起起来,别往下走。
第二件是 egg 脚手架。全局装一次就行:
npm i egg-init -g然后初始化项目,这里用 simple 模板,够用且干净:
egg-init egg-mongo-demo --type=simple cd egg-mongo-demo npm i装完先跑一次npm run dev,看到egg started on http://127.0.0.1:7001说明骨架没问题。这时候再装 egg-mongoose:
npm install egg-mongoose -S关于 TaoToken,如果你后续想用统一的 API Key 管理来调试模型对话或者跑 coding plan,可以把它当成一个入口:官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。不过这一篇的重点是本地 MongoDB 连接,TaoToken 的部分放在最后 CTA 里说,现在先把数据库跑通。
注意:egg-mongoose 装完后不要急着改代码,先确认
package.json的 dependencies 里出现了egg-mongoose,版本号记一下,后面排查兼容性问题会用到。
3. 可复制配置:plugin.js 与 config.default.js 骨架
egg 的插件机制是这样的:config/plugin.js决定“要不要启用某个插件”,config/config.default.js决定“这个插件用什么参数”。egg-mongoose 两个文件都要动,缺一个都会导致app.mongoose是 undefined。
先改config/plugin.js,加上:
'use strict'; exports.mongoose = { enable: true, package: 'egg-mongoose', };这里enable: true是必须的,simple 模板默认不会帮你开。package的值就是 npm 包名,别写错。
然后改config/config.default.js。原始文件里有一堆默认配置,你只需要在return config之前插入 mongoose 段:
'use strict'; module.exports = appInfo => { const config = exports = {}; config.keys = appInfo.name + '_1690000000000_1234'; config.middleware = []; // egg-mongoose 连接配置 config.mongoose = { url: 'mongodb://127.0.0.1:27017/egg_mongo_demo', options: { useNewUrlParser: true, useUnifiedTopology: true, }, }; return config; };几个参数说明一下。url里的egg_mongo_demo是数据库名,MongoDB 在没有这个库的时候会在第一次写入时自动创建,所以不用提前手动建。options里那两个参数在新版 mongoose 里其实已经默认开启,但显式写上去能避免不同版本之间的行为差异,尤其是你本地 mongoose 版本和 egg-mongoose 依赖的版本不一致时。
如果你本地 MongoDB 开了认证,url 要写成mongodb://用户名:密码@127.0.0.1:27017/egg_mongo_demo?authSource=admin,authSource指向存用户信息的库,通常是 admin。没开认证就保持上面那样。
提示:数据库名不要用中文、不要带空格,用下划线分隔最稳。我第一次用
mongoTest这种驼峰命名,在 mongoose 的某些版本里会被转成小写,导致连的库和你以为的不是同一个。
配置写完后,egg 会在启动时自动加载插件并建立连接。但这时候还没有 model,app.model是空的,所以下一步要建 model 目录。
4. 模型目录约定与一次读写验证
egg 的约定是:model 文件放在app/model/下,文件名小写,导出一个函数,函数接收app参数,返回 mongoose model。这个 model 会自动挂到app.model上,命名规则是文件名首字母大写。比如app/model/student.js对应app.model.Student。
先建文件app/model/student.js:
'use strict'; module.exports = app => { const mongoose = app.mongoose; const Schema = mongoose.Schema; const StudentSchema = new Schema({ name: { type: String, required: true }, age: { type: Number, default: 0 }, gender: { type: String, enum: ['男', '女'] }, createdAt: { type: Date, default: Date.now }, }); return mongoose.model('Student', StudentSchema, 'student_info'); };第三个参数student_info是实际写入 MongoDB 的集合名。如果不写,mongoose 会把Student转成复数students作为集合名。我习惯显式指定,避免以后查数据时找不到表。
接着建 service,app/service/student.js:
'use strict'; const Service = require('egg').Service; class StudentService extends Service { async list() { return this.ctx.model.Student.find({}); } async add(payload) { try { const res = await this.ctx.model.Student.create(payload); return { success: true, data: res, code: 0 }; } catch (err) { return { success: false, err: err.message, code: -1 }; } } } module.exports = StudentService;再建 controller,app/controller/student.js:
'use strict'; const Controller = require('egg').Controller; class StudentController extends Controller { async list() { const ctx = this.ctx; ctx.body = await ctx.service.student.list(); } async add() { const ctx = this.ctx; const payload = ctx.request.body; ctx.body = await ctx.service.student.add(payload); } } module.exports = StudentController;最后配路由,app/router.js:
'use strict'; module.exports = app => { const { router, controller } = app; router.get('/student/list', controller.student.list); router.post('/student/add', controller.student.add); };启动项目:
npm run dev如果配置正确,终端里会看到 egg 的启动日志,并且没有 mongoose 报错。这时候用 curl 发一个写入请求:
curl -X POST http://127.0.0.1:7001/student/add \ -H "Content-Type: application/json" \ -d '{"name":"张三","age":20,"gender":"男"}'返回{"success":true,"data":{...},"code":0}就说明写入成功。再查一次:
curl http://127.0.0.1:7001/student/list能看到刚才写入的那条数据,整个链路就通了。这时候打开 MongoDB Compass,连上mongodb://127.0.0.1:27017,在egg_mongo_demo库里找到student_info集合,数据应该在里面。
注意:POST 请求如果报
missing csrf token,是因为 egg 默认开了 CSRF 防护。本地调试阶段可以在config.default.js里加config.security = { csrf: { enable: false } };,上线前再按需开启。
5. 本篇常见错排查
连接阶段的报错基本集中在下面几种,按出现频率排:
ECONNREFUSED 127.0.0.1:27017:MongoDB 没启动,或者端口不是 27017。先用mongosh确认能连上,再检查config.mongoose.url里的端口。
app.mongoose is undefined:config/plugin.js里没启用 egg-mongoose,或者package名写错了。检查plugin.js的exports.mongoose段,确认enable: true。
app.model.Student is undefined:model 文件没放在app/model/下,或者文件名和调用名对不上。app/model/student.js对应app.model.Student,首字母大写是约定。
MongooseError: model already compiled:热重载时 model 被重复注册。egg 的 dev 模式会监听文件变化,如果你在 model 文件里用了mongoose.model('Student', ...)而没有先判断是否已存在,就会报这个。用mongoose.models.Student || mongoose.model('Student', ...)可以规避,但正常情况下 egg-mongoose 会处理好,出现这个多半是手动引入了 mongoose 实例。
写入成功但 Compass 里看不到数据:集合名不对。检查 model 第三个参数,或者直接在 Compass 里刷新一下,有时候需要手动刷新才显示新集合。
ValidationError: gender is not a valid enum value:写入的 gender 不在['男','女']里。这是 model 层校验生效了,说明配置是对的,改数据就行。
DeprecationWarning: useNewUrlParser:mongoose 版本较新时这两个参数已经废弃,警告不影响运行,想消掉就把options里的这两个键删掉。
排查顺序建议:先看终端启动日志有没有 mongoose 相关报错,再用mongosh确认数据库活着,然后检查plugin.js和config.default.js,最后看 model 命名。大部分问题在前两步就能定位。
6. 跑通之后:把调试入口固定下来
本地读写跑通只是第一步。实际开发里你会反复做三件事:改 model 后重启验证、用 Postman 或 curl 测接口、查数据库里的数据对不对。把这三件事的入口固定下来,能省很多时间。
接口调试我习惯用 curl 存成脚本,或者用 Postman 建一个 collection,把add和list两个请求存进去,环境变量设成http://127.0.0.1:7001。数据库查看用 MongoDB Compass 连本地,收藏egg_mongo_demo这个库,每次启动项目后直接点开看。
如果你后面要接模型对话或者跑 coding plan 来辅助写 egg 的 service 层逻辑,可以用 TaoToken 的 API Key 管理入口:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,模型对话入口是 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。长期做编码和 Agent 的话,coding plan 在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。这些和本地 MongoDB 连接是两条线,但调试阶段有个顺手的模型入口,写 model 校验逻辑和排错会快一些。
最后留一个实用技巧:在config.default.js里把config.mongoose.options加上debug: true,mongoose 会把所有实际执行的数据库操作打印到终端。第一次接的时候打开它,你能清楚看到find和create到底发了什么、返回了什么,比猜快得多。跑通之后关掉就行,不然日志会刷屏。