阅读 677

NodeJS后端框架可以取代SpringBoot吗?尝试从0开始写了一套及感受

给框架一个名字,不想起一些怪异的名字,既然是按Spring Boot的设计思想,跟着Spring,我给这个框架起名Summer

Github:github.com/calidan-x/s…

编写参考了市面上成熟的 midwayjsnest

  ____  _   _ __  __ __  __ _____ ____  
 / ___|| | | |  \/  |  \/  | ____|  _ \ 
 \___ \| | | | |\/| | |\/| |  _| | |_) |
  ___) | |_| | |  | | |  | | |___|  _ < 
 |____/ \___/|_|  |_|_|  |_|_____|_| \_\
                                        
Ver 0.0.1

[2021-03-16 13:12:46] MySQL DB connected
[2021-03-16 13:12:46] Server started port:8080
复制代码

框架设计思路

loc.ts
反向控制的代码可以说是非常简单。只需要封装好实例存储和读取实例的方法即可。

route-mapping.ts
路由编写是其中最麻烦一块,需要读取controller中的方法传参,方法对应的请求方法,控制器的路由前缀,这些decorator数据最终会被拼装成一个routeMapping全局数据。

decorators/*.ts
所有装饰器(注解)
这里分为3类:

  1. Restful请求路由相关 @Controller @Get @Post @Put @Delete @Patch
  2. 请求参数相关 @RequestParam @PathParam @RequestBody
  3. Loc容器相关 @Service @Component @Autowired
  • 对于Controller中使用的装饰器例如@Controller @Get @PathParam,调用route-mapping中的方法,最终合成routeMapping数据。
  • 对于Controller,Service等默认Loc容器类装饰器例如@Controller @Service @Component,则调用loc.ts中的实例添加方法加入到loc容器中。

最终生成结构大致如下:

[
  '/persons': {
    GET: { callMethod: 'personList', params: [] },
    POST: { callMethod: 'addPerson', params: [Array] },
    controllerName: 'PersonController',
    pathRegExp: /^\/persons[\/#\?]?$/i,
    pathKeys: [],
    controller: PersonController { personService: [PersonService] }
  },
  '/persons/:id': {
    GET: { callMethod: 'personInfo', params: [Array] },
    controllerName: 'PersonController',
    pathRegExp: /^\/persons(?:\/([^\/#\?]+?))[\/#\?]?$/i,
    pathKeys: [ [Object] ],
    controller: PersonController { personService: [PersonService] }
  }
]
复制代码

server.ts
服务器主类,封装监听端口的方法。

  • 当路由请求过来的时候,为了加快匹配速度可以先找到完全匹配的路由,找不到再遍历从正则的route。
  • 找到合适的route后,要做的事就是分解方法传参,用请求发来的queryParam pathParam requestBody根据接口方法顺序组合成一个数组,传给调用方法,根据解析和方法执行结果设置http状态值,将方法返回数据写入请求返回数据。

database.ts
数据库连接类,在尝试Sequelize-Typescript和TypeORM后,觉得TypeORM更佳,所以集成了TypeORM

resouces目录
存放项目配置

logger.ts
日志打印类

summer.ts
框架主入口,扫描注入Loc单例,连接数据库,启动端口监听

开发中遇到的问题

  1. @PathParam()参数为空时是否可以简写成PathParam?因为TypeScript语言限制,没有重载方法的方式所以无法实现,考虑到大多数时候传参与变量名是一致的,所以最后设计成了。PathParam+PathParamFrom()的模式。
  2. 简写后ParaParam如何读取方法变量名?将方法toString()后从参数读取。
  3. 如何对调用接口的数据做验证?使用class-validator。
  4. 各种DTO与Entity的转换,在Java环境下往往需要做对象与Json格式的反复转换,在NodeJS环境下有没有便捷方法?经过研究,可以先设置tsconfig.json中的useDefineForClassFields为true,target至少要ES6,这样就可以导出类空字段,然后实例化后遍历导入数据。

实践框架,尝试构建工程得到了一些代码实践建议

创建了一个简单的项目,在数据库中创建了一个Person表,实现Person表的读取添加一系列接口

Controller类

import { Controller, Get, Autowired, PathParam, Post, RequestBody } from '../../lib/decorators';
import { PersonRequest } from '../dto/request/person-request';
import { PersonResource } from '../dto/resource/person-resource';
import { PersonService } from '../service/person-service';

@Controller('/persons')
export class PersonController {
  @Autowired
  personService: PersonService;

  @Get('')
  async personList() {
    const persons = await this.personService.getPersons();
    return persons;
  }

  @Post('')
  async addPerson(@RequestBody personRequest: PersonRequest) {
    const persons = await this.personService.savePerson(personRequest.toPerson());
    return persons;
  }

  @Get('/:id')
  async personInfo(@PathParam id: string) {
    const person = await this.personService.getPersonInfo(id);
    return PersonResource.from(person);
  }
}

复制代码

Service类

import { getRepository } from 'typeorm';
import { Service } from '../../lib/decorators';
import { Person } from '../entity/person';

@Service
export class PersonService {
  personRepository = getRepository(Person);

  async getPersons() {
    const person = await this.personRepository.find();
    return person;
  }

  async getPersonInfo(id: string) {
    const person = await this.personRepository.findOne(id);
    return person;
  }

  async savePerson(person: Person) {
    await this.personRepository.save(person);
  }
}
复制代码

Screen Shot 2021-03-17 at 00.37.09.png

代码实践建议:
  1. 使用class取代 interface / type,因为Entity与验证类都要求使用class,所以后端的typescript开发主要使用class而非前端常用的 interface / type。
  2. 代码内部可以通过TypeScript核查类型,对于外部输入数据要严防死守,因为TypeScript无法完成在运行时起作用。
  3. TypeORM比Sequelize-Typescript更好用些。

NodeJS与Java后端框架对比优势与劣势

优势:占用内存小,启动迅速,不用处理多线程问题,非阻塞高并发,少了Setter Getter和Java各种List/ArrayList/HashMap等...代码编写更简易。
劣势:运行时类型的检查,严谨的类型判断,TypeScript对反射与Decorator功能的支持不足导致框架无法更完善的封装高级功能。

总结

NodeJS后端已经有很好生态,各类工程化与微服务需要功能都有了npm版本,内部数据通过TypeScript验证保证类型无误,只要处理好输入数据的验证和转换问题,是可以在实际项目中发挥力量的。




离完整框架的距离,本框架中未实现的功能
  1. AOP
  2. 定时任务
  3. 消息队列
  4. 文件上传,与表单格式支持
  5. 验证与安全
  6. 跨域请求设置
  7. 框架测试/工程测试
  8. 申明式请求
  9. 模板渲染
  10. API文档生成
  11. 项目介绍文档
  12. ….

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情

文章分类
后端
文章标签