├── README.md ├── app.ts ├── app ├── controller │ └── home.ts ├── extend │ └── application.ts ├── graphql │ ├── customBaseType.ts │ ├── index.ts │ └── schemaResolver │ │ ├── studentResolver.ts │ │ └── userResolver.ts ├── io │ ├── controller │ │ └── chat.ts │ └── middleware │ │ ├── connection.js │ │ └── packet.js ├── model │ ├── BaseModel.ts │ ├── Student.ts │ └── User.ts ├── router.ts ├── schedule │ └── addUserJob.ts └── service │ └── user.ts ├── config ├── config.default.ts ├── config.local.ts ├── config.prod.ts └── plugin.ts ├── package.json ├── test └── app │ ├── controller │ └── home.test.ts │ └── service │ └── Test.test.ts ├── tsconfig.json ├── typings └── index.d.ts └── yarn.lock /README.md: -------------------------------------------------------------------------------- 1 | # egg-ts-mongoose-template 2 | 3 | ### QuickStart && Development 4 | 5 | ```bash 6 | $ yarn install 7 | $ yarn dev 8 | $ open http://localhost:7001/ 9 | ``` 10 | 11 | # Node 使用 Egg 框架 之 上TS 的教程(一) 12 | 13 | ### Node + Egg + TS + Mongodb + Resetful 14 | 15 | 1. 作为一个从优美的、面向对象的、专业的:C、C++、C#、JAVA一路过来的程序员,开始让我写JS,我是拒绝的。这哪里是在写代码,明明是在写 **console.log()** 啊!!! 连少个参数、参数类型不对都不告诉我,我太难了。 16 | 17 | 2. 我那祖传的:**面向对象** 和 **23种设计模式**,在这JS的代码中失去了灵魂。 18 | 19 | 3. 顺便说句: **async** 和 **await** 真香。 20 | 21 | 3. 网上的教程都是egg少部分的结合,没有真正的做到俺们这篇的强。废话不多说,开始! 22 | 23 | 老规矩,本地教程的地址为:[https://github.com/liangwei0101/egg-demo/tree/egg-ts-mongoose-template](https://github.com/liangwei0101/egg-demo/tree/egg-ts-mongoose-template) 24 | 25 | ##### 运行环境: Node ,Yarn/NPM,MongoDB 26 | 27 | egg为node.js的一个框架,用起来还是挺简单的,大伙看看就会了。 28 | 29 | ## Node + egg + ts + Mongodb 示例 30 | ![数据库保存](https://user-gold-cdn.xitu.io/2019/11/7/16e465087f50e600?w=1240&h=275&f=png&s=54889) 31 | ![接口返回字段](https://user-gold-cdn.xitu.io/2019/11/7/16e4650887f3be79?w=1240&h=585&f=png&s=93903) 32 | 33 | 34 | ## 目录结构 35 | ![项目结构](https://user-gold-cdn.xitu.io/2019/11/7/16e465087f8814ca?w=290&h=446&f=png&s=22383) 36 | 37 | ``` 38 | egg-demo 39 | ├── app 40 | │ ├── controller (前端的请求会到这里来!) 41 | │ │ └── home.ts 42 | │ ├── model(数据库表结构抽象出来的模型) 43 | │ │ └── User.ts 44 | │ ├── service(controller 层不建议承载过多的业务,业务重时放在service层) 45 | │ │ └── user.ts 46 | │ └── router.ts (Url的相关映射) 47 | ├── config (框架的配置文件) 48 | │ ├── config.default.ts 49 | │ ├── config.local.ts 50 | │ ├── config.prod.ts 51 | │ └── plugin.ts 52 | ├── test (测试文件夹) 53 | │ └── **/*.test.ts 54 | ├── typings (目录用于放置 d.ts 文件) 55 | │ └── **/*.d.ts 56 | ├── README.md 57 | ├── package.json 58 | ├── tsconfig.json 59 | └── tslint.json 60 | ``` 61 | ## 配置 62 | demo配置了两处地方: 63 | + 数据库 64 | ``` 65 | config.mongoose = { 66 | url: process.env.EGG_MONGODB_URL || 'mongodb://127.0.0.1/egg-demo', 67 | options: {}, 68 | }; 69 | ``` 70 | + csrf(先关闭,要不然post报错) 71 | ``` 72 | config.security = { 73 | csrf: { 74 | enable: false, 75 | }, 76 | }; 77 | ``` 78 | ## router 79 | 80 | 对于resetful风格的接口来说,用http的关键字来标识动作,用名词来标识资源。已user为例子: 81 | 对于请求 '/user'的请求,在下方代码的指定映射到对应的函数中。 82 | ``` 83 | router.get('/user', controller.home.getUser); 84 | router.post('/user', controller.home.addUser); 85 | router.put('/user', controller.home.updateUser); 86 | router.delete('/user', controller.home.deleteUser); 87 | ``` 88 | ## controller 89 | 这里是请求对应的函数的类。 90 | ``` 91 | // 这里是get('/user')的处理函数 92 | public async getUser() { 93 | const { ctx } = this; 94 | 95 | // 这里就是随你怎么来。可以数据库查,或者别的。 96 | const user = { ... }; 97 | // 返回的值 98 | ctx.body = user; 99 | } 100 | 101 | // 下面类似,不再解释了啊 102 | public async addUser() { 103 | const { ctx } = this; 104 | 105 | // 模拟前端传递过来的数据(方便测试) 106 | const user = new UserModel(); 107 | user.userName = 'add user'; 108 | user.userNo = 99; 109 | 110 | const res = await ctx.model.User.create(user); 111 | ctx.body = res; 112 | } 113 | 114 | public async deleteUser() { 115 | const { ctx } = this; 116 | 117 | const user = new UserModel(); 118 | user.userNo = 99; 119 | 120 | const res = await UserModel.findOneAndRemove({ userNo: user.userNo }); 121 | 122 | ctx.body = res; 123 | } 124 | ``` 125 | ## service层 126 | 这里没有啥讲的,就是一些业务性的东西放这里,让被controller或者其他service调用。 127 | ``` 128 | /** 129 | * sayHi to you 130 | * @param name - your name 131 | */ 132 | public async sayHi(name: string) { 133 | return `hi, ${name}`; 134 | } 135 | ``` 136 | ## Model (画重点,用mongodb的注意啦) 137 | 1. 首先我们创建一个Schema 138 | ``` 139 | /** 140 | * 定义一个User的Schema 141 | */ 142 | const UserSchema: Schema = new Schema({ 143 | userNo: { 144 | type: Number, 145 | index: true, 146 | }, 147 | 148 | userName: String, 149 | }, 150 | { 151 | timestamps: true, 152 | }, 153 | ); 154 | ``` 155 | 2. 索引 156 | ``` 157 | // userNo 为索引 158 | UserSchema.index({ userNo: 1, }); 159 | ``` 160 | 3. 实例方法和静态方法 161 | ``` 162 | // UserSchema的实例方法 163 | UserSchema.methods.userInstanceTestMethods = function () { 164 | 165 | const user: IUser = new UserModel(); 166 | user.userName = '我是实例化方法测试'; 167 | user.userNo = 9527; 168 | 169 | return user; 170 | }; 171 | 172 | // UserSchema的实例方法 173 | UserSchema.statics.userStaticTestMethods = function () { 174 | 175 | const user: IUser = new UserModel(); 176 | user.userName = '我是静态方法测试'; 177 | user.userNo = 9528; 178 | 179 | return user; 180 | }; 181 | ``` 182 | 4. 创建User接口字段 183 | ``` 184 | /** 185 | * 用户字段接口 186 | */ 187 | export interface IUser { 188 | 189 | userNo: number; 190 | 191 | userName: string; 192 | } 193 | ``` 194 | 5. 实例方法和静态方法接口的定义,注意:这里的接口要和Schema中定义的函数的名称和返回值一致。 195 | ``` 196 | export interface IUserDocument extends IUser, Document { 197 | /** 198 | * 实例方法接口(名称需要和Schema的方法名一样) 199 | */ 200 | userInstanceTestMethods: () => IUser; 201 | } 202 | /** 203 | * 静态方法接口 204 | */ 205 | export interface IUserModel extends Model { 206 | 207 | /** 208 | * 静态方法 209 | */ 210 | userStaticTestMethods: () => IUser; 211 | } 212 | ``` 213 | 6. 导出model即可。 214 | ``` 215 | export const UserModel = model('User', UserSchema); 216 | ``` 217 | 7. 为了怕有需求使用到ctx.model.User,我们需要将UserSchema挂载到ctx中 218 | ``` 219 | // egg-mongoose注入 220 | export default (app: Application) => { 221 | 222 | const mongoose = app.mongoose; 223 | // 这里为了挂载到ctx中,让正常ctx.model.User也能使用 224 | mongoose.model('User', UserSchema); 225 | }; 226 | ``` 227 | ## 使用Model 228 | 使用mode能使用IUser字段接口,实例方法,静态方法。 229 | ``` 230 | // 这里的user是: IUser的类型。然后就能尽情的点点点啦! 231 | const user = await UserModel.findOne(); 232 | // 等价于 233 | const users = await this.ctx.model.User.find(); 234 | // 实例方法 235 | const newUser = new UserModel(); 236 | newUser.userInstanceTestMethods(); 237 | // 静态方法 238 | UserModel.userStaticTestMethods(); 239 | ``` 240 | ## 最后,单元测试!!! 241 | 可能很多人觉得单元测试不写就不写,写了浪费时间。但是等你发现你要重构的时候,没有足够的单元测试的时候,你会觉得,什么鬼,不敢动啊!!!所以,我觉得还是要写单元测试,这个东西是费点时间,但是后期好啊。 242 | 243 | ``` 244 | test/app/controller/home.test.ts 245 | √ should GET / (49ms) 246 | √ addUser (39ms) 247 | √ getUser 248 | √ getUsers 249 | √ updateUser 250 | √ deleteUser 251 | √ testStaticMethods 252 | √ testInstanceFunction 253 | 254 | test/app/service/Test.test.js 255 | √ sayHi 256 | √ testUserInstanceServiceMethods 257 | √ testUserInstanceServiceMethods 258 | 259 | 11 passing (4s) 260 | ``` 261 | 看见他打绿色的 √ √ 我就很开心。 262 | 263 | 今天有点晚了,后面给大家写:定时任务,GraphQL,redis,部署等内容。 264 | 265 | # Node 使用 Egg 框架 之 上TS 的教程(二) 266 | 267 | # Node + Egg + TS + Mongodb + Resetful + graphql 268 | 269 | **本节课内容: graphql 和 egg 定时任务** 270 | 271 | ##### 运行环境: Node ,Yarn/NPM,MongoDB 272 | 273 | 梁老师又开课啦!上节课讲到Node使用Mongodb上TS的一些操作,[上节课的链接]([https://www.jianshu.com/p/5fbade6874c1](https://www.jianshu.com/p/5fbade6874c1) 274 | )。 275 | 276 | 老规矩,本地教程的地址为:[https://github.com/liangwei0101/egg-demo/tree/egg-ts-mongoose-template](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fliangwei0101%2Fegg-demo%2Ftree%2Fegg-ts-mongoose-template) 277 | 278 | 279 | ``` 280 | egg-demo 281 | ├── app 282 | │ ├── controller (前端的请求会到这里来!) 283 | │ │ └── home.ts 284 | │ ├── model(数据库表结构抽象出来的模型) 285 | │ │ └── User.ts 286 | │ ├── graphql(graphql文件夹) 287 | │ │ └── mutation(所有的mutation声明文件夹) 288 | │ │ └── schema.graphql(所有的mutation声明文件) 289 | │ │ └── query(所有的query声明文件夹) 290 | │ │ └── schema.graphql(所有的query声明文件) 291 | │ │ └── user(user model的声明和实现) 292 | │ │ └── resolver.js(声明函数的实现) 293 | │ │ └── schema.graphql(user schema的字段声明) 294 | │ ├── service(controller 层不建议承载过多的业务,业务重时放在service层) 295 | │ │ └── user.ts 296 | │ ├── schedule(定时任务文件夹) 297 | │ │ └── addUserJob.ts 298 | │ └── router.ts (Url的相关映射) 299 | ├── config (框架的配置文件) 300 | │ ├── config.default.ts 301 | │ ├── config.local.ts 302 | │ ├── config.prod.ts 303 | │ └── plugin.ts 304 | ├── test (测试文件夹) 305 | │ └── **/*.test.ts 306 | ├── typings (目录用于放置 d.ts 文件) 307 | │ └── **/*.d.ts 308 | ├── README.md 309 | ├── package.json 310 | ├── tsconfig.json 311 | └── tslint.json 312 | ``` 313 | 本次教程增加了**schedule**文件夹和**graphql**文件夹。 314 | 315 | ##### egg 使用 graphql 316 | 317 | graphql 只是一种restful的一种不足的一种解决方案吧。它就是在 **数据库操作完以后,将字段的返回权交给了用户,意思是,用户想返回哪个字段就返回哪个字段,假如说,我pc端和移动端公用一套接口,pc端需要100个字段,移动端需要10个字段,使用resetful无论你需不需要,都会给你返回,而graphql很好的解决了这个问题,因为选择权在调用方。** 318 | 319 | 是不是感觉很神奇,js和ts可以共存。此教程,先上一个js和ts版本共存的,等下期教程将使用纯ts教程。使用js的问题是,在 resolver.js 中,将不能享受ts代码的代码提示和编译检查。但是很多项目可能都是之前存在的项目,此类js不能立马重构或者改动。共存也是有意义的,逐步重构或者重写吧。 320 | 321 | ``` 322 | // plugin.ts 323 | const plugin: EggPlugin = { 324 | // mongoose 325 | mongoose: { 326 | enable: true, 327 | package: 'egg-mongoose', 328 | }, 329 | // 添加 graphql 330 | graphql: { 331 | enable: true, 332 | package: 'egg-graphql', 333 | }, 334 | }; 335 | ``` 336 | ``` 337 | // config.default.ts 338 | config.graphql = { 339 | router: '/graphql', 340 | // 是否加载到 app 上,默认开启 341 | app: true, 342 | // 是否加载到 agent 上,默认关闭 343 | agent: false, 344 | // 是否加载开发者工具 graphiql, 默认开启。路由同 router 字段。使用浏览器打开该可见。 345 | graphiql: true, 346 | }; 347 | 348 | config.middleware = ['graphql']; 349 | ``` 350 | **使用user举例**: 351 | 定义User schema 的返回字段 352 | ``` 353 | // schema 354 | type User { 355 | _id: String 356 | userNo: String 357 | userName: String 358 | } 359 | ``` 360 | ``` 361 | // 查询所有声明 362 | type Query { 363 | // 函数名字为user 返回为 User的数组 364 | user: [User] 365 | } 366 | // Mutation 所有声明 367 | type Mutation { 368 | // 函数名字为user 返回为 User对象 369 | user: User 370 | } 371 | ``` 372 | Mutation 和 Query 声明函数的实现处: 373 | ``` 374 | 'use strict'; 375 | 376 | module.exports = { 377 | Query: { 378 | async user(root, { }, ctx) { 379 | return await ctx.model.User.find(); 380 | }, 381 | }, 382 | Mutation: { 383 | async user(root, { }, ctx) { 384 | return await ctx.model.User.create({userName: 'add user', userNo: 99}); 385 | }, 386 | } 387 | } 388 | ``` 389 | 然后打开浏览器输入:[http://127.0.0.1:7001/graphql](http://127.0.0.1:7001/graphql),没病,查询一个瞧瞧!! 390 | ![graphql使用截图](https://upload-images.jianshu.io/upload_images/9942046-cb2c88a96914a28b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 391 | 392 | ##### egg 定时任务 393 | 394 | 定时任务和我们一般定时任务差不多。定时任务一般分两种: 395 | + 一种是 **间隔若干时间执行** 某个任务 396 | + 另一种是 **某个时间点执行** 某个任务 397 | 398 | 1. 间隔若干时间执行(每隔60s将会执行一次) 399 | ``` 400 | static get schedule() { 401 | return { 402 | interval: '60s', // 60s 间隔 403 | type: 'all', // 指定所有的 worker 都需要执行 404 | }; 405 | } 406 | 407 | async subscribe() { 408 | const ctx = this.ctx; 409 | 410 | console.log('每60s执行一次增加User的定时任务!!' + new Date()) 411 | 412 | const test = await ctx.service.user.addUserByScheduleTest(); 413 | 414 | console.log(test) 415 | } 416 | ``` 417 | 2. 某个时间点执行(每个月的15号:00:00 分执行) 418 | ``` 419 | static get schedule() { 420 | return { 421 | cron: '0 0 0 15 * *', // 每个月的15号:00:00 分执行 422 | type: 'worker', // 只指定一个随机进程执行job 防止出现数据冲突 423 | disable: false, // 是否开启 424 | cronOptions: { 425 | tz: 'Asia/Shanghai', 426 | }, 427 | }; 428 | } 429 | 430 | async subscribe () { 431 | const ctx = this.ctx; 432 | 433 | console.log('每个月的15号:00:00 分执行!!' + new Date()) 434 | } 435 | ``` 436 | 时间的配置请看:[egg的官方文档]([https://eggjs.org/zh-cn/basics/schedule.html](https://eggjs.org/zh-cn/basics/schedule.html) 437 | )。又挺晚啦,下节课给大家上graphql 的ts版本。 438 | 439 | # Node 使用 Egg 框架 之 上TS 的教程(三) 440 | 441 | ##### Node + Egg + TS + typegoose + Resetful + schedule + type-graphql+ websocket 442 | 443 | 梁老师课堂,不间断,最终版教程来啦。 444 | 445 | 本次教材地址:[https://github.com/liangwei0101/egg-demo/tree/egg-ts-mongoose-graphql.ts-websocket](https://github.com/liangwei0101/egg-demo/tree/egg-ts-mongoose-graphql.ts-websocket) 446 | 447 | ## 本次教材讲解:typegoose 和 type-graphql 以及 websocket 448 | 449 | 网上找了一圈,都没有 egg + typegoose + type-graphql 的教程,还是我来吧。 450 | mongoose 和 graphql 一起使用,会有啥问题呢?当然,正常的问题是不存在,**但是很多废代码**。拿上节课的代码来说。 451 | ``` 452 | // schema.graphql(user schema的字段声明) 453 | type User { 454 | userNo: Number 455 | userName: String 456 | } 457 | ``` 458 | 459 | ``` 460 | const UserSchema: Schema = new Schema({ 461 | userNo: { 462 | type: Number, 463 | index: true, 464 | }, 465 | userName: String, 466 | }), 467 | ``` 468 | 在schema.graphql我们要声明如上的字段。我们可以发现,其实我们的字段都是一样的,却多写了一遍。当我们的model很多的时候,就会产生大量的这个问题。引出我们的主角:typegoose 和 type-graphql 。 469 | 470 | ## 使用 typegoose 和 type-graphql 471 | 472 | 1. typegoose 主要作用:就是将 *加了注解的类* 转化成 对应的 mongoose的属性、实例方法、静态方法等等。 473 | 474 | 2. type-graphql 的主要作用:也是讲 将 *加了注解的类* 转化成对应的 graphql 的声明字段、query、Mutation等。 475 | 476 | 看到这里我们应该可以理解到为啥代码比之前的写法少了重复的代码。 477 | ``` 478 | egg-demo 479 | ├── app 480 | │ ├── controller (前端的请求会到这里来!) 481 | │ │ └── home.ts 482 | │ ├── model(数据库表结构抽象出来的模型) 483 | │ │ └── User.ts 484 | │ ├── graphql(graphql文件夹) 485 | │ │ └── schemaResolver(所有Resolver) 486 | │ │ └── userResolver.ts(user的Resolver文件) 487 | │ ├── ├──index.ts (type-graphql的初始化) 488 | │ ├── service(controller 层不建议承载过多的业务,业务重时放在service层) 489 | │ │ └── user.ts 490 | │ ├── io(websocket文件夹) 491 | │ │ └── controller(前端通过websocket通信的请求) 492 | │ │ └── chat.ts(controller相应的处理) 493 | │ ├── schedule(定时任务文件夹) 494 | │ │ └── addUserJob.ts 495 | │ └── router.ts (Url的相关映射) 496 | ├── config (框架的配置文件) 497 | │ ├── config.default.ts 498 | │ ├── config.local.ts 499 | │ ├── config.prod.ts 500 | │ └── plugin.ts 501 | ├── test (测试文件夹) 502 | │ └── **/*.test.ts 503 | ├── typings (目录用于放置 d.ts 文件) 504 | │ └── **/*.d.ts 505 | ├── README.md 506 | ├── package.json 507 | ├── tsconfig.json 508 | └── tslint.json 509 | ``` 510 | 本次教程改动了model文件夹和graphql文件夹,以及增加了IO文件夹。 511 | 512 | ## Model 513 | 514 | ``` 515 | @ObjectType() 516 | export default class BaseModel extends Typegoose { 517 | 518 | @Field({ description: "id" }) 519 | _id?: string 520 | 521 | @prop() 522 | @Field({ description: "创建时间" }) 523 | createdAt: Date 524 | 525 | @prop() 526 | @Field({ description: "更新时间" }) 527 | updatedAt: Date 528 | } 529 | ``` 530 | 531 | ``` 532 | /** 533 | * 用户类 534 | */ 535 | @ObjectType() 536 | @index({ userNo: 1 }) 537 | export class User extends BaseModel { 538 | @prop({ required: true }) 539 | @Field(() => Int, { description: "编号" }) 540 | userNo: number; 541 | 542 | @prop({ required: true }) 543 | @Field({ nullable: true, description: "名称" }) 544 | userName?: string; 545 | 546 | //#region(实例方法 和 实例方法) 547 | @instanceMethod 548 | public async userInstanceTestMethods(this: InstanceType) { 549 | const user: User = new User(); 550 | user.userName = '我是实例化方法测试'; 551 | user.userNo = 9527; 552 | return user; 553 | } 554 | 555 | @staticMethod 556 | public static async userStaticTestMethods(this: ModelType & typeof User) { 557 | const user: User = new User(); 558 | user.userName = '我是静态方法测试'; 559 | user.userNo = 9527; 560 | return user; 561 | } 562 | //#endregion 563 | } 564 | export const UserModel = new User().getModelForClass(User) 565 | ``` 566 | 567 | + @prop({ required: true }) 这个注解是:这个是要转化成 mongoose 字段的,并且是必须填写。 568 | + @Field({ nullable: true, description: "名称" })这个注解是:这个字段将会转成 graphql的 schema,可选,描述是名称。 569 | + @instanceMethod 是 mongoose model 的实例方法。 570 | + @staticMethod 是 mongoose model 的静态方法。 571 | + 通过导出的 UserModel ,我们将能操作mongoose 的find,create、实例方法,静态方法,hook等。 572 | + 通过 model 继承BaseModel,我们每一个model将能少写三个正常js的graphql 的字段。 573 | 574 | ## graphql 对应schema 的 Resolver 575 | ``` 576 | @Resolver(User) 577 | export class UserResolver { 578 | 579 | @Query(() => [User], { description: '查询用户列表' }) 580 | async getUser() { 581 | return await UserModel.find(); 582 | } 583 | 584 | @Mutation(() => User, { description: '增加用户' }) 585 | async addUser() { 586 | 587 | let user = new UserModel(); 588 | user.userNo = 666; 589 | user.userName = 'liang'; 590 | 591 | return await UserModel.create(user); 592 | } 593 | } 594 | ``` 595 | + @Resolver(User) :意思为这个User model 对应的 Resolver。 596 | + @Query(() => [User], { description: '查询用户列表' }) :意思为:这个是函数是Query,返回的是一个数组,描述是XXX。 597 | + @Mutation(() => User, { description: '增加用户' }): 同理。 598 | 599 | 写到这里,我们已经可以运行啦。我们输入:http://127.0.0.1:7001/graphql 将能看到新的界面。我们的 Query 和 Mutation 还有相应的注解都在其中。 600 | ![graphql 界面](https://user-gold-cdn.xitu.io/2019/11/28/16eb25742212325b?w=1240&h=592&f=png&s=105135) 601 | 602 | 其实到这里,我这里不建议项目中原来用js的上这种方式。如果是存在许多js和ts共存的,我个人建议采用[教程二](https://www.jianshu.com/p/859006440e75)里面的方式去重构。 603 | 604 | ## websocket 教程 605 | 这个简单很多,我们只要: 606 | 607 | ``` 608 | // config.default.ts 配置一下 609 | config.io = { 610 | init: {}, 611 | namespace: { 612 | '/': { 613 | connectionMiddleware: ['connection'], 614 | packetMiddleware: ['packet'], 615 | }, 616 | '/chat': { 617 | connectionMiddleware: ['connection'], 618 | packetMiddleware: [], 619 | }, 620 | }, 621 | }; 622 | // plugin.ts 开启 623 | io: { 624 | enable: true, 625 | package: 'egg-socket.io', 626 | } 627 | ``` 628 | 加到路由中:访问 /的websocket将会到这个sendMsg函数处理。 629 | ``` 630 | io.of('/').route('chat', io.controller.chat.sendMsg); 631 | ``` 632 | 到了这里,ts版本会报错,因为controller中定义的chat他不认识。这里需要我们手写一下index.d.ts就好了。 633 | 634 | ``` 635 | declare module 'egg' { 636 | interface CustomController { 637 | chat: any; 638 | } 639 | 640 | interface EggSocketNameSpace { 641 | emit: any 642 | } 643 | } 644 | ``` 645 | 前端我这里用的vue框架,vue中的使用简单些。就是下载包,然后在main.js中使用一波就好了。在相应的页面去做这个事情就好了。 646 | ``` 647 | // main.js 648 | import VueSocketIO from 'vue-socket.io' 649 | 650 | Vue.use(new VueSocketIO({ 651 | debug: true, 652 | connection: 'http://127.0.0.1:7001', 653 | })) 654 | 655 | ``` 656 | ``` 657 | //接收服务端的信息 658 | this.sockets.subscribe("test", data => { 659 | alert(data) 660 | }); 661 | ``` 662 | 好啦,egg 上ts的教程,完美结束。愿大家写起来都开心啊。不用在写没有提示的点点点了。。。若是有帮助,给github里的项目start一下啊,我也是琢磨了很久的呢。看到网上又没有这方面教程,写这些个更希望希望能帮到大家。谢谢~~ 663 | ##### Node + Egg + TS + typegoose + Resetful + schedule + type-graphql+ websocket 664 | 665 | # Node 使用 Egg 框架 之 上TS 的教程(四) 666 | 667 | 在使用了之前教程的骨架代码一段时候,有同学说,typegoose出新包,大改了。现在写法变化了很多。于是就有了这个 typegoose新包的骨架代码教程。 668 | 669 | 本次教材地址:[https://github.com/liangwei0101/egg-demo.git](https://github.com/liangwei0101/egg-demo.git) 670 | 671 | 版本: 672 | + node:v12.14.0 673 | + @typegoose/typegoose: 6.4.0 674 | 675 | #### 先对比下前后typegoose之前的写法 676 | 677 | 1. 引入的包不一样了 678 | ``` 679 | // 之前是这样写的 680 | import { index, prop, instanceMethod, staticMethod } from 'typegoose'; 681 | 682 | // 现在是这样写的 683 | import { index, getModelForClass, prop } from '@typegoose/typegoose'; 684 | ``` 685 | 2. 实例方法和之前不一样了(简洁了许多) 686 | ``` 687 | // 之前实例方法是加注解就好了 688 | export default class User extends Typegoose { 689 | @instanceMethod 690 | public async userInstanceTestMethods(this: InstanceType) { 691 | 692 | } 693 | } 694 | 695 | // 现在直接写就好了不用注解 696 | export default class User extends Typegoose { 697 | public async userInstanceTestMethods() { 698 | 699 | } 700 | } 701 | ``` 702 | 3. 静态方法和之前不一样了(简洁了许多) 703 | ``` 704 | // 之前静态方法是加注解就好了 705 | export default class User extends Typegoose { 706 | @staticMethod 707 | public async userStaticTestMethods(this: InstanceType) { 708 | 709 | } 710 | } 711 | 712 | // 现在直接写就好了不用注解 713 | export default class User extends Typegoose { 714 | public static async userStaticTestMethods() { 715 | 716 | } 717 | } 718 | ``` 719 | 4. 导出Model, 现在getModelForClass直接使用即可 720 | ``` 721 | // 之前是这样写的 722 | export const UserModel = new User().getModelForClass(User); 723 | 724 | // 现在是这样写的 725 | export const UserModel = getModelForClass(User); 726 | ``` 727 | 5. buildSchema 函数 (单元测试的ctx.model可能会需要) 728 | ``` 729 | // 从class 获取 Schema 730 | const TradeSchema = buildSchema(User); 731 | // 如果需要Schema的话 732 | export default (app: Application) => { 733 | const mongoose = app.mongoose; 734 | const UserSchema = buildSchema(Order); 735 | UserSchema.loadClass(OrderClass); 736 | return mongoose.model('Order', UserSchema); 737 | } 738 | ``` 739 | #### type-graphql 使用JSON类型 740 | 找了很久type-graphql使用any或者json的文档或者例子,都是只支持type-graphql的基础类型,和class类型。然后,就自己定义一个JSON类型吧。 741 | + GraphQLScalarType 自定义(我封装了两个,一个查询用的,一个新增用的) 742 | ``` 743 | import { GraphQLScalarType } from "graphql"; 744 | // 自定义 名称为JSON的类型 745 | export const JosnScalar = new GraphQLScalarType({ 746 | name: "JSON", 747 | description: "验证 JSON 类型", 748 | parseValue(value: any) { 749 | if (Object.prototype.toString.call(value) !== '[object Object]') { 750 | throw new Error('亲,参数不是一个对象呢!'); 751 | } 752 | return value; 753 | }, 754 | serialize(value: any) { 755 | return value; 756 | }, 757 | }); 758 | ``` 759 | + 使用自定义JSON类型例子一 760 | ``` 761 | // 输入参数 762 | @ArgsType() 763 | export class DefaultMutation { 764 | 765 | @Field(() => JosnScalar, { nullable: true }) 766 | data: any; 767 | } 768 | 769 | @Mutation(() => User, { description: '增加用户' }) 770 | async addUser(@Args() { data }: DefaultMutation, @Ctx() ctx: Context) { 771 | // 这里的 data 指向 DefaultMutation 属性的data也就是any类型 772 | const user = await ctx.service.user.createUser(data); 773 | 774 | return user; 775 | } 776 | 777 | ``` 778 | + 使用自定义JSON类型例子二 779 | ``` 780 | @ArgsType() 781 | export class DefaultQuery { 782 | 783 | @Field(() => JosnScalar, { nullable: true }) 784 | filter: any; 785 | 786 | @Field(() => JosnScalar, { nullable: true }) 787 | order: any; 788 | 789 | @Field(() => JosnScalar, { nullable: true }) 790 | page: any; 791 | } 792 | 793 | // 使用 794 | @Query(() => [User], { description: '查询用户列表' }) 795 | async getUser(@Args() { filter, order, page }: DefaultQuery, @Ctx() ctx: Context) { 796 | return await ctx.service.user.filterUser(filter, order, page); 797 | } 798 | ``` 799 | #### egg中 mongoose 的 transaction 800 | + 提取公用函数,封装到上下文ctx中 801 | ``` 802 | // extend/context.ts 803 | import { Context } from 'egg'; 804 | 805 | export default { 806 | // 获取session,回滚事务 807 | async getSession(this: Context) { 808 | // Start a session. 809 | const session = await this.app.mongoose.startSession(); 810 | // Start a transaction 811 | // 这里因为获取到的session也是要startTransaction,所以,就一并在这写掉 812 | session.startTransaction(); 813 | return session 814 | } 815 | } 816 | ``` 817 | + 事务使用 818 | mongodb的事务比较low,开启事务,需要副本集,否则会报: [Transaction numbers are only allowed on a replica set member or mongos](https://stackoverflow.com/questions/51461952/mongodb-v4-0-transaction-mongoerror-transaction-numbers-are-only-allowed-on-a) 819 | 820 | ``` 821 | 822 | /** 823 | * 测试事务 824 | */ 825 | public async testErrorTransaction() { 826 | 827 | const session: any = await this.ctx.getSession(); 828 | try { 829 | const user = new UserModel(); 830 | user.userName = 'add user'; 831 | user.userNo = 103; 832 | (await UserModel.create(user)).$session(session) 833 | 834 | // 没有userNo将会报错 835 | const user1 = new UserModel(); 836 | (await UserModel.create(user)).$session(session) 837 | console.log(user1); 838 | 839 | // 提交事务 840 | await session.commitTransaction(); 841 | } catch (err) { 842 | // 事务回滚 843 | await session.abortTransaction(); 844 | throw err; 845 | } finally { 846 | await session.endSession(); 847 | } 848 | } 849 | ``` 850 | 这个骨架使用应该没有啥问题啦,有啥问题欢迎大家指出。谢谢!别忘记给我点星星哦,再次谢谢啦! 851 | -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import * as fs from 'fs-extra'; 3 | import { join, sep } from 'path'; 4 | import { find } from 'fs-jetpack'; 5 | import { watch } from 'chokidar'; 6 | import * as mongoose from 'mongoose'; 7 | import * as prettier from 'prettier'; 8 | import { Application, IBoot } from 'egg'; 9 | 10 | export default class FooBoot implements IBoot { 11 | private readonly app: Application; 12 | 13 | constructor(app: Application) { 14 | this.app = app; 15 | } 16 | 17 | async configWillLoad() { 18 | // Ready to call configDidLoad,` 19 | // Config, plugin files are referred,` 20 | // this is the last chance to modify the config. 21 | await this.connectDB(this.app) 22 | await this.customLoadModel(); 23 | if (this.app.config.env !== 'unittest') { 24 | await this.app.graphql.init() 25 | } 26 | } 27 | 28 | configDidLoad() { 29 | // Config, plugin files have loaded. 30 | } 31 | 32 | async didLoad() { 33 | // All files have loaded, start plugin here. 34 | } 35 | 36 | async willReady() { 37 | // All plugins have started, can do some thing before app ready. 38 | // await this.customLoadModel(); 39 | } 40 | 41 | async didReady() { 42 | // Worker is ready, can do some things 43 | // don't need to block the app boot. 44 | } 45 | 46 | async serverDidReady() { 47 | // Server is listening. 48 | } 49 | 50 | async beforeClose() { 51 | // Do some thing before app close. 52 | } 53 | 54 | //#region 手动挂载model,测试需要ctx.model 55 | 56 | public async connectDB(app: Application) { 57 | const { url, options } = app.config.mongoose 58 | if (url) { 59 | const connection = await mongoose.connect(url, options) 60 | app.context.connection = connection 61 | } 62 | } 63 | 64 | /** 65 | * 手动挂载model 66 | */ 67 | private async customLoadModel() { 68 | this.watchModel(this.app) 69 | await this.loadModel(this.app) 70 | } 71 | 72 | private capitalizeFirstLetter(str: string) { 73 | return str.charAt(0).toUpperCase() + str.slice(1) 74 | } 75 | 76 | private getModelName(file: string) { 77 | const filename = file.split(sep).pop() || '' 78 | const name = this.capitalizeFirstLetter(filename.replace(/\.ts$|\.js$/g, '')) 79 | return name 80 | } 81 | 82 | private async loadModel(app: Application) { 83 | const { baseDir } = app 84 | const modelDir = join(baseDir, 'app', 'model') 85 | if (!fs.existsSync(modelDir)) return 86 | 87 | // TODO: handle other env 88 | const matching = app.config.env === 'local' || app.config.env === 'unittest' ? '*.ts' : '*.js' 89 | 90 | const files = find(modelDir, { matching }) 91 | app.model = {} 92 | const modelWhitelist: String[] = app.config.modelWhitelist; 93 | 94 | try { 95 | for (const file of files) { 96 | const modelPath = join(baseDir, file) 97 | const Model = require(modelPath).default 98 | const name = this.getModelName(file) 99 | if (modelWhitelist.indexOf(name) === -1) { 100 | try { 101 | app.model[name] = new Model().getModelForClass(Model) 102 | } 103 | catch (e) { 104 | console.log('如果要挂载到model请将export default'); 105 | } 106 | } 107 | } 108 | } catch (e) { 109 | console.log(e) 110 | } 111 | } 112 | 113 | private watchModel(app: Application) { 114 | const { baseDir } = app 115 | const modelDir = join(baseDir, 'app', 'model') 116 | const typingsDir = join(baseDir, 'typings') 117 | 118 | if (!fs.existsSync(modelDir)) return 119 | 120 | fs.ensureDirSync(typingsDir) 121 | watch(modelDir).on('all', (eventType: string) => { 122 | if (['add', 'change'].includes(eventType)) { 123 | this.createTyingFile(app) 124 | } 125 | 126 | if (['unlink'].includes(eventType)) { 127 | this.createTyingFile(app) 128 | } 129 | }) 130 | } 131 | 132 | private createTyingFile(app: Application) { 133 | const { baseDir } = app 134 | const modelDir = join(baseDir, 'app', 'model') 135 | const files = find(modelDir, { matching: '*.ts' }) 136 | const typingPath = join(baseDir, 'typings', 'typegoose.d.ts') 137 | const pathArr = this.formatPaths(files) 138 | const importText = pathArr 139 | .map(i => `import ${i.name} from '${i.importPath}'`) 140 | .join('\n') 141 | const repoText = pathArr 142 | .map(i => `${i.name}: ModelType>`) 143 | .join('\n') 144 | 145 | const text = this.getTypingText(importText, repoText) 146 | this.writeTyping(typingPath, text) 147 | } 148 | 149 | private getTypingText(importText: string, modelText: string) { 150 | const tpl = ` 151 | import 'egg' 152 | import { InstanceType, ModelType } from 'typegoose' 153 | import * as mongoose from 'mongoose' 154 | 155 | ${importText} 156 | 157 | declare module 'egg' { 158 | interface Context { 159 | connection: mongoose.Collection 160 | model: { 161 | ${modelText} 162 | } 163 | } 164 | } 165 | ` 166 | return tpl 167 | } 168 | 169 | private writeTyping(path: string, text: string) { 170 | fs.writeFileSync(path, this.formatCode(text), { encoding: 'utf8' }) 171 | } 172 | 173 | private formatCode(text: string) { 174 | return prettier.format(text, { 175 | semi: false, 176 | tabWidth: 2, 177 | singleQuote: true, 178 | parser: 'typescript', 179 | trailingComma: 'all', 180 | }) 181 | } 182 | 183 | private formatPaths(files: string[]) { 184 | return files.map(file => { 185 | const name = this.getModelName(file) 186 | file = file.split(sep).join('/') 187 | const importPath = `../${file}`.replace(/\.ts$|\.js$/g, '') 188 | return { 189 | name, 190 | importPath, 191 | } 192 | }) 193 | } 194 | 195 | //#endregion 196 | } -------------------------------------------------------------------------------- /app/controller/home.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from 'egg'; 2 | import { UserModel } from '../model/User'; 3 | 4 | export default class HomeController extends Controller { 5 | 6 | public async index() { 7 | const ctx = this.ctx; 8 | 9 | ctx.body = await ctx.service.user.sayHi('egg'); 10 | } 11 | 12 | public async getUser() { 13 | const ctx = this.ctx; 14 | 15 | const users = await UserModel.findOne(); 16 | 17 | ctx.body = users; 18 | } 19 | 20 | public async getUsers() { 21 | const ctx = this.ctx; 22 | 23 | const users = await UserModel.find(); 24 | 25 | ctx.body = users; 26 | } 27 | 28 | public async addUser() { 29 | const ctx = this.ctx; 30 | 31 | // 模拟前端传递过来的数据(方便测试) 32 | const user = new UserModel(); 33 | user.userName = 'add user'; 34 | user.userNo = 99; 35 | 36 | const res = await UserModel.create(user); 37 | 38 | ctx.body = res; 39 | } 40 | 41 | public async updateUser() { 42 | const ctx = this.ctx; 43 | 44 | const user = new UserModel(); 45 | user.userNo = 99; 46 | 47 | const res = await UserModel.findOneAndUpdate({ userNo: user.userNo }, { userName: 'i am from update' }, { new: true }); 48 | 49 | ctx.body = res; 50 | } 51 | 52 | public async deleteUser() { 53 | const ctx = this.ctx; 54 | 55 | const user = new UserModel(); 56 | user.userNo = 99; 57 | 58 | const res = await UserModel.findOneAndRemove({ userNo: user.userNo }); 59 | 60 | ctx.body = res; 61 | } 62 | 63 | public async testInstanceFunction() { 64 | const ctx = this.ctx; 65 | 66 | const user = await ctx.service.user.testUserInstanceServiceMethods(); 67 | 68 | ctx.body = user; 69 | } 70 | 71 | public async testStaticMethods() { 72 | const ctx = this.ctx; 73 | 74 | const user = await ctx.service.user.testUserStaticServiceMethods(); 75 | 76 | ctx.body = user; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app/extend/application.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'egg'; 2 | import GraphQL from '../graphql'; 3 | 4 | const TYPE_GRAPHQL_SYMBOL = Symbol('Application#TypeGraphql'); 5 | 6 | export default { 7 | get graphql(this: Application): GraphQL { 8 | if (!this[TYPE_GRAPHQL_SYMBOL]) { 9 | this[TYPE_GRAPHQL_SYMBOL] = new GraphQL(this); 10 | } 11 | return this[TYPE_GRAPHQL_SYMBOL]; 12 | }, 13 | }; -------------------------------------------------------------------------------- /app/graphql/customBaseType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from "graphql"; 2 | import { ArgsType, Field } from 'type-graphql'; 3 | 4 | export const JosnScalar = new GraphQLScalarType({ 5 | name: "JSON", 6 | description: "验证 JSON 类型", 7 | parseValue(value: any) { 8 | checkIsObject(value) 9 | return value; 10 | }, 11 | serialize(value: any) { 12 | return value; 13 | }, 14 | }); 15 | 16 | // export const OrderScalar = new GraphQLScalarType({ 17 | // name: "Order", 18 | // description: "设置Order", 19 | // parseValue(value: any) { 20 | // checkIsObject(value) 21 | // if (value.createdAt === undefined) { 22 | // throw new Error('亲,createdAt 字段 才是排序哦'); 23 | // } 24 | // const orderObj = Object.assign(value, { createdAt: -1 }); 25 | // console.log('====================') 26 | // console.log(value) 27 | // console.log('====================') 28 | // return orderObj; 29 | // }, 30 | // serialize(value: any) { 31 | // return value; 32 | // }, 33 | // }); 34 | 35 | // export const PageScalar = new GraphQLScalarType({ 36 | // name: "Page", 37 | // description: "设置Page", 38 | // parseValue(value: any) { 39 | // checkIsObject(value); 40 | // if (value.num === undefined || value.size === undefined) { 41 | // throw new Error('亲,num 或者 size 这两个参数才能传呢'); 42 | // } 43 | // const orderObj = Object.assign(value, { num: 1, size: 10 }); 44 | // console.log('====================') 45 | // console.log(value) 46 | // console.log('====================') 47 | // return orderObj; 48 | // }, 49 | // serialize(value: any) { 50 | // return value; 51 | // }, 52 | // }); 53 | 54 | 55 | @ArgsType() 56 | export class DefaultQuery { 57 | 58 | @Field(() => JosnScalar, { nullable: true }) 59 | filter: any; 60 | 61 | @Field(() => JosnScalar, { nullable: true }) 62 | order: -1; 63 | 64 | @Field(() => JosnScalar, { nullable: true }) 65 | page: any; 66 | } 67 | 68 | /** 69 | * 验证 70 | */ 71 | function checkIsObject(value: any) { 72 | if (Object.prototype.toString.call(value) !== '[object Object]') { 73 | throw new Error('亲,参数不是一个对象呢!'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/graphql/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | 3 | import { ApolloServer } from 'apollo-server-koa' 4 | import { Application } from 'egg' 5 | import { GraphQLSchema, GraphQLFormattedError, SourceLocation } from 'graphql' 6 | import { buildSchema } from 'type-graphql' 7 | 8 | export interface GraphQLConfig { 9 | router: string 10 | dateScalarMode?: 'isoDate' | 'timestamp' 11 | graphiql: boolean 12 | } 13 | 14 | export default class GraphQL { 15 | private readonly app: Application 16 | private graphqlSchema: GraphQLSchema 17 | private config: GraphQLConfig 18 | 19 | constructor(app: Application) { 20 | this.app = app 21 | this.config = app.config.graphql 22 | } 23 | 24 | getResolvers() { 25 | const isLocal = this.app.config.env === 'local' 26 | return [path.resolve(this.app.baseDir, `app/graphql/schemaResolver/*.${isLocal ? "ts" : "js"}`)]; 27 | } 28 | 29 | async init() { 30 | this.graphqlSchema = await buildSchema({ 31 | resolvers: this.getResolvers(), 32 | dateScalarMode: "timestamp" 33 | }) 34 | const server = new ApolloServer({ 35 | schema: this.graphqlSchema, 36 | formatError: error => { 37 | const err = new FormatError() 38 | err.message = error.message 39 | err.path = error.path 40 | if (this.app.config.env === 'local') { 41 | err.extensions = error.extensions 42 | err.locations = error.locations 43 | } 44 | return err 45 | }, 46 | tracing: false, 47 | context: ({ ctx }) => ctx, // 将 egg 的 context 作为 Resolver 传递的上下文 48 | playground: { 49 | settings: { 50 | 'request.credentials': 'include' 51 | } 52 | } as any, 53 | introspection: true 54 | }) 55 | server.applyMiddleware({ 56 | app: this.app, 57 | path: this.config.router, 58 | cors: false 59 | }) 60 | this.app.logger.info('graphql server init') 61 | } 62 | 63 | // async query({query, var}) 64 | 65 | get schema(): GraphQLSchema { 66 | return this.graphqlSchema 67 | } 68 | } 69 | 70 | class FormatError implements GraphQLFormattedError { 71 | message: string 72 | locations?: readonly SourceLocation[] | undefined 73 | path?: readonly (string | number)[] | undefined 74 | extensions?: Record | undefined 75 | } -------------------------------------------------------------------------------- /app/graphql/schemaResolver/studentResolver.ts: -------------------------------------------------------------------------------- 1 | import Student from '../../model/Student'; 2 | import { Resolver, Query, Mutation } from 'type-graphql'; 3 | import { StudentModel } from '../../model/Student'; 4 | 5 | @Resolver(Student) 6 | export class StudentResolver { 7 | 8 | @Query(() => [Student], { description: '查询用户列表' }) 9 | async getStudent() { 10 | return await StudentModel.findOne(); 11 | } 12 | 13 | @Mutation(() => Student, { description: '增加用户' }) 14 | async addStudent() { 15 | let strudent = new StudentModel(); 16 | strudent.userNo = 666; 17 | strudent.userName = 'liang'; 18 | 19 | return await StudentModel.create(strudent); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/graphql/schemaResolver/userResolver.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'egg' 2 | import User from '../../model/User'; 3 | import { UserModel } from '../../model/User'; 4 | import { DefaultQuery } from '../customBaseType'; 5 | import { Resolver, Query, Mutation, Args, Ctx } from 'type-graphql'; 6 | 7 | @Resolver(User) 8 | export class UserResolver { 9 | 10 | @Query(() => [User], { description: '查询用户列表' }) 11 | async getUser(@Args() { filter, order, page }: DefaultQuery, @Ctx() ctx: Context) { 12 | return await ctx.service.user.filterUser(filter, order, page); 13 | } 14 | 15 | @Mutation(() => User, { description: '增加用户' }) 16 | async addUser() { 17 | 18 | let user = new UserModel(); 19 | user.userNo = 666; 20 | user.userName = 'liang'; 21 | 22 | return await UserModel.create(user); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/io/controller/chat.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'egg'; 2 | 3 | export default async (app: Application) => { 4 | class Controller extends app.Controller { 5 | async sendMsg() { 6 | const nsp = app.io.of('/'); 7 | nsp.emit('test', '我是测试'); 8 | } 9 | } 10 | return Controller; 11 | }; -------------------------------------------------------------------------------- /app/io/middleware/connection.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | module.exports = () => { 4 | return async (ctx, next) => { 5 | console.log('连接成功!!!'); 6 | // ctx.socket.emit('test', 'connected!'); 7 | await next(); 8 | }; 9 | }; -------------------------------------------------------------------------------- /app/io/middleware/packet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = app => { 3 | return async (ctx, next) => { 4 | ctx.socket.emit('res', 'packet received!'); 5 | console.log('packet:', ctx.packet); 6 | await next(); 7 | }; 8 | }; -------------------------------------------------------------------------------- /app/model/BaseModel.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from 'type-graphql'; 2 | import { prop, pre, Typegoose } from '@typegoose/typegoose'; 3 | 4 | /** 5 | * BaseModel 6 | */ 7 | @pre('save', function (next) { 8 | if (!this.createdAt || this.isNew) { 9 | this.createdAt = this.updatedAt = new Date() 10 | } else { 11 | this.updatedAt = new Date() 12 | } 13 | next() 14 | }) 15 | 16 | @ObjectType() 17 | export default class BaseModel extends Typegoose { 18 | 19 | @Field({ description: "id" }) 20 | _id?: string 21 | 22 | @prop() 23 | @Field({ description: "创建时间" }) 24 | createdAt: Date 25 | 26 | @prop() 27 | @Field({ description: "更新时间" }) 28 | updatedAt: Date 29 | } 30 | 31 | -------------------------------------------------------------------------------- /app/model/Student.ts: -------------------------------------------------------------------------------- 1 | 2 | import BaseModel from './BaseModel'; 3 | import { ObjectType, Field, Int } from 'type-graphql'; 4 | import { index, getModelForClass, prop } from '@typegoose/typegoose'; 5 | 6 | /** 7 | * 学生类 8 | */ 9 | @ObjectType() 10 | @index({ userNo: 1 }) 11 | export default class Student extends BaseModel { 12 | 13 | @prop({ required: true }) 14 | @Field(() => Int, { description: "编号" }) 15 | userNo: number; 16 | 17 | @prop({ required: true }) 18 | @Field({ nullable: true, description: "名称" }) 19 | userName: string; 20 | 21 | //#region(实例方法 和 实例方法) 22 | public async userInstanceTestMethods() { 23 | 24 | const user: Student = new Student(); 25 | user.userName = '我是实例化方法测试'; 26 | user.userNo = 9527; 27 | 28 | return user; 29 | } 30 | 31 | public static async userStaticTestMethods() { 32 | 33 | const user: Student = new Student(); 34 | user.userName = '我是静态方法测试'; 35 | user.userNo = 9527; 36 | 37 | return user; 38 | } 39 | 40 | //#endregion 41 | } 42 | 43 | export const StudentModel = getModelForClass(Student); -------------------------------------------------------------------------------- /app/model/User.ts: -------------------------------------------------------------------------------- 1 | 2 | import BaseModel from './BaseModel'; 3 | import { ObjectType, Field, Int } from 'type-graphql'; 4 | import { index, getModelForClass, prop } from '@typegoose/typegoose'; 5 | 6 | 7 | /** 8 | * 用户字段接口 9 | */ 10 | @ObjectType() 11 | @index({ userNo: 1 }) 12 | export default class User extends BaseModel { 13 | 14 | @prop({ required: true }) 15 | @Field(() => Int, { description: "编号" }) 16 | userNo: number; 17 | 18 | @prop({ required: true }) 19 | @Field({ nullable: true, description: "名称" }) 20 | userName?: string; 21 | 22 | 23 | //#region(实例方法 和 实例方法) 24 | public async userInstanceTestMethods() { 25 | 26 | const user: User = new User(); 27 | user.userName = '我是实例化方法测试'; 28 | user.userNo = 9527; 29 | 30 | return user; 31 | } 32 | 33 | public static async userStaticTestMethods() { 34 | 35 | const user: User = new User(); 36 | user.userName = '我是静态方法测试'; 37 | user.userNo = 9527; 38 | 39 | return user; 40 | } 41 | 42 | //#endregion 43 | } 44 | 45 | export const UserModel = getModelForClass(User); 46 | -------------------------------------------------------------------------------- /app/router.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'egg'; 2 | 3 | export default (app: Application) => { 4 | const { controller, router, io } = app; 5 | 6 | router.get('/', controller.home.index); 7 | router.get('/user', controller.home.getUser); 8 | router.get('/users', controller.home.getUsers); 9 | router.put('/user', controller.home.updateUser); 10 | router.post('/user', controller.home.addUser); 11 | router.delete('/user', controller.home.deleteUser); 12 | router.get('/testStaticMethods', controller.home.testStaticMethods); 13 | router.get('/testInstanceFunction', controller.home.testInstanceFunction); 14 | 15 | // app.io.of('/chat') 16 | io.of('/').route('chat', io.controller.chat.sendMsg); 17 | }; -------------------------------------------------------------------------------- /app/schedule/addUserJob.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Subscription } from 'egg' 3 | 4 | /** 5 | * 间隔时间段,定时任务测试 6 | */ 7 | export default class AddUserJob extends Subscription { 8 | static get schedule() { 9 | return { 10 | interval: '600000m', // 60s 间隔 11 | type: 'all', // 指定所有的 worker 都需要执行 12 | }; 13 | } 14 | 15 | async subscribe() { 16 | const ctx = this.ctx; 17 | 18 | console.log('每60s执行一次增加User的定时任务!!' + new Date()) 19 | 20 | this.ctx.app.io.of('/').emit('test', '我是定时任务,60s一次的主推的消息啊!!!'); 21 | 22 | const test = await ctx.service.user.addUserByScheduleTest(); 23 | 24 | console.log(test) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/service/user.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'egg'; 2 | import User from '../model/User'; 3 | import { UserModel } from '../model/User'; 4 | 5 | /** 6 | * 用户 Service 层 7 | */ 8 | export default class UserService extends Service { 9 | 10 | /** 11 | * sayHi to you 12 | * @param name - your name 13 | */ 14 | public async sayHi(name: string) { 15 | return `hi, ${name}`; 16 | } 17 | 18 | public async addUserByScheduleTest() { 19 | 20 | const user = new UserModel(); 21 | user.userName = 'add user'; 22 | user.userNo = 99; 23 | 24 | const res = await UserModel.create(user); 25 | return res; 26 | } 27 | 28 | /** 29 | * 测试用户的实例方法 30 | */ 31 | public async testUserInstanceServiceMethods(): Promise { 32 | const newUser = new UserModel(); 33 | 34 | return await newUser.userInstanceTestMethods(); 35 | } 36 | 37 | /** 38 | * 测试用户的方法 39 | */ 40 | public async testUserStaticServiceMethods(): Promise { 41 | return await UserModel.userStaticTestMethods(); 42 | } 43 | 44 | public async filterUser(filter: any, order: any, page: any) { 45 | order = Object.assign({ createdAt: -1 }, order); 46 | page = Object.assign({ num: 1, size: 10 }, page); 47 | filter = Object.assign({}, filter); 48 | 49 | return await UserModel.find(filter) 50 | .sort(order) 51 | .skip((page.num - 1) * page.size) 52 | .limit(page.size); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /config/config.default.ts: -------------------------------------------------------------------------------- 1 | import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg'; 2 | 3 | export default (appInfo: EggAppInfo) => { 4 | const config = {} as PowerPartial; 5 | 6 | // override config from framework / plugin 7 | // use for cookie sign key, should change to your own and keep security 8 | config.keys = appInfo.name + '_1572947163069_8100'; 9 | 10 | // add your egg config in here 11 | config.middleware = []; 12 | 13 | config.modelWhitelist = ['BaseModel', 'Fee']; 14 | 15 | config.security = { 16 | csrf: { 17 | enable: false, 18 | }, 19 | }; 20 | 21 | config.io = { 22 | init: {}, 23 | namespace: { 24 | '/': { 25 | connectionMiddleware: ['connection'], 26 | packetMiddleware: ['packet'], 27 | }, 28 | '/chat': { 29 | connectionMiddleware: ['connection'], 30 | packetMiddleware: [], 31 | }, 32 | }, 33 | }; 34 | 35 | config.cors = { 36 | origin: '*', 37 | allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH', 38 | credentials: true 39 | }; 40 | 41 | config.mongoose = { 42 | url: process.env.EGG_MONGODB_URL || 'mongodb://127.0.0.1/egg-demo', 43 | options: {}, 44 | }; 45 | 46 | config.graphql = { 47 | router: '/graphql', 48 | // 是否加载到 app 上,默认开启 49 | dateScalarMode: 'timestamp' 50 | }; 51 | 52 | // add your special config in here 53 | const bizConfig = { 54 | sourceUrl: `https://github.com/eggjs/examples/tree/master/${appInfo.name}`, 55 | }; 56 | 57 | // the return config will combines to EggAppConfig 58 | return { 59 | ...config, 60 | ...bizConfig, 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /config/config.local.ts: -------------------------------------------------------------------------------- 1 | import { EggAppConfig, PowerPartial } from 'egg'; 2 | 3 | export default () => { 4 | const config: PowerPartial = {}; 5 | return config; 6 | }; 7 | -------------------------------------------------------------------------------- /config/config.prod.ts: -------------------------------------------------------------------------------- 1 | import { EggAppConfig, PowerPartial } from 'egg'; 2 | 3 | export default () => { 4 | const config: PowerPartial = {}; 5 | return config; 6 | }; 7 | -------------------------------------------------------------------------------- /config/plugin.ts: -------------------------------------------------------------------------------- 1 | import { EggPlugin } from 'egg'; 2 | 3 | const plugin: EggPlugin = { 4 | static: true, 5 | // mongoose 6 | mongoose: { 7 | enable: true, 8 | package: 'egg-mongoose', 9 | }, 10 | // cors 11 | cors: { 12 | enable: true, 13 | package: 'egg-cors' 14 | }, 15 | // socket.io 16 | io: { 17 | enable: true, 18 | package: 'egg-socket.io', 19 | } 20 | }; 21 | 22 | export default plugin; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egg-ts-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "egg": { 7 | "typescript": true, 8 | "declarations": true 9 | }, 10 | "scripts": { 11 | "start": "egg-scripts start --daemon --title=egg-server-egg-ts-demo", 12 | "stop": "egg-scripts stop --title=egg-server-egg-ts-demo", 13 | "dev": "egg-bin dev", 14 | "debug": "egg-bin debug", 15 | "test-local": "egg-bin test", 16 | "test": "npm run test-local", 17 | "cov": "egg-bin cov", 18 | "tsc": "ets && tsc -p tsconfig.json", 19 | "ci": "npm run lint && npm run cov && npm run tsc", 20 | "autod": "autod", 21 | "lint": "tslint --project . -c tslint.json", 22 | "clean": "ets clean" 23 | }, 24 | "dependencies": { 25 | "@typegoose/typegoose": "^6.4.0", 26 | "apollo-server-koa": "^2.14.2", 27 | "egg": "^2.6.1", 28 | "egg-cors": "^2.2.3", 29 | "egg-graphql": "^2.7.0", 30 | "egg-mongoose": "^3.2.0", 31 | "egg-scripts": "^2.6.0", 32 | "egg-socket.io": "^4.1.6", 33 | "egg-type-graphql": "^1.8.1", 34 | "fs-extra": "^8.1.0", 35 | "graphql": "^14.5.8", 36 | "mongoose": "^5.9.3", 37 | "prettier": "^1.19.1", 38 | "reflect-metadata": "^0.1.13", 39 | "type-graphql": "^0.17.5" 40 | }, 41 | "devDependencies": { 42 | "@types/mocha": "^2.2.40", 43 | "@types/node": "^7.0.12", 44 | "@types/supertest": "^2.0.0", 45 | "autod": "^3.0.1", 46 | "autod-egg": "^1.1.0", 47 | "egg-bin": "^4.11.0", 48 | "egg-ci": "^1.8.0", 49 | "egg-mock": "^3.16.0", 50 | "tslib": "^1.9.0", 51 | "tslint": "^5.20.1", 52 | "tslint-config-egg": "^1.0.0", 53 | "typescript": "3.7.2" 54 | }, 55 | "engines": { 56 | "node": ">=8.9.0" 57 | }, 58 | "ci": { 59 | "version": "8" 60 | }, 61 | "repository": { 62 | "type": "git", 63 | "url": "" 64 | }, 65 | "eslintIgnore": [ 66 | "coverage" 67 | ], 68 | "author": "liangwei", 69 | "license": "MIT" 70 | } 71 | -------------------------------------------------------------------------------- /test/app/controller/home.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { app } from 'egg-mock/bootstrap'; 3 | import User from '../../../app/model/User'; 4 | 5 | describe('test/app/controller/home.test.ts', () => { 6 | 7 | it('should GET /', async () => { 8 | const result = await app.httpRequest().get('/').expect(200); 9 | assert(result.text === 'hi, egg'); 10 | }); 11 | 12 | it('addUser', async () => { 13 | const result = await app.httpRequest().post('/user').expect(200); 14 | const user: User = result.body; 15 | assert(user.userName === 'add user'); 16 | }); 17 | 18 | it('getUser', async () => { 19 | const result = await app.httpRequest().get('/user').expect(200); 20 | const user: User = result.body; 21 | assert(user !== null); 22 | }); 23 | 24 | it('getUsers', async () => { 25 | const result = await app.httpRequest().get('/users').expect(200); 26 | const users: User[] = result.body; 27 | assert(users === [] || users.length >= 0); 28 | }); 29 | 30 | it('updateUser', async () => { 31 | const result = await app.httpRequest().put('/user').expect(200); 32 | const user: User = result.body; 33 | assert(user.userName == 'i am from update'); 34 | }); 35 | 36 | it('deleteUser', async () => { 37 | const result = await app.httpRequest().delete('/user').expect(200); 38 | const user: User = result.body; 39 | assert(user.userNo == 99); 40 | }); 41 | 42 | it('testStaticMethods', async () => { 43 | const result = await app.httpRequest().get('/testStaticMethods').expect(200); 44 | const user: User = result.body; 45 | assert(user.userName == '我是静态方法测试'); 46 | }); 47 | 48 | it('testInstanceFunction', async () => { 49 | const result = await app.httpRequest().get('/testInstanceFunction').expect(200); 50 | const user: User = result.body; 51 | assert(user.userName == '我是实例化方法测试'); 52 | }); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /test/app/service/Test.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { Context } from 'egg'; 3 | import { app } from 'egg-mock/bootstrap'; 4 | 5 | describe('test/app/service/Test.test.js', () => { 6 | let ctx: Context; 7 | 8 | before(async () => { 9 | ctx = app.mockContext(); 10 | }); 11 | 12 | it('sayHi', async () => { 13 | const result = await ctx.service.user.sayHi('egg'); 14 | assert(result === 'hi, egg'); 15 | }); 16 | 17 | it('addUserByScheduleTest', async () => { 18 | const result = await ctx.service.user.addUserByScheduleTest(); 19 | assert(result.userNo === 99); 20 | }); 21 | 22 | it('testUserInstanceServiceMethods', async () => { 23 | const user = await ctx.service.user.testUserInstanceServiceMethods(); 24 | assert(user.userName == '我是实例化方法测试'); 25 | }); 26 | 27 | it('testUserStaticServiceMethods', async () => { 28 | const user = await ctx.service.user.testUserStaticServiceMethods(); 29 | assert(user.userName == '我是静态方法测试'); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "strict": true, 7 | "noImplicitAny": false, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "charset": "utf8", 11 | "allowJs": false, 12 | "pretty": true, 13 | "noEmitOnError": false, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "allowUnreachableCode": false, 17 | "allowUnusedLabels": false, 18 | "strictPropertyInitialization": false, 19 | "noFallthroughCasesInSwitch": true, 20 | "skipLibCheck": true, 21 | "skipDefaultLibCheck": true, 22 | "inlineSourceMap": true, 23 | "importHelpers": true 24 | }, 25 | "exclude": [ 26 | "app/public", 27 | "app/views", 28 | "node_modules*" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | import 'egg'; 3 | 4 | declare module 'egg' { 5 | interface CustomController { 6 | nsp: any; 7 | chat: any; 8 | } 9 | 10 | interface EggSocketNameSpace { 11 | emit: any 12 | } 13 | } --------------------------------------------------------------------------------