NodeJS框架对比 - Express、Koa、Egg、Nest等
Express.js
Error-First
的模式(第一个参数是error对象)一个简单的Express服务器
查看代码
const express = require('express');
const app = express();
/* 中间件 */
app.use((req, res, next) => {
console.log('middleware');
next();
console.log('middleware call');
});
/* 路由部分 */
const router = express.Router();
router.get('/', (req, res) => {
res.send('Home');
});
app.use(router);
/* 静态文件 */
app.use(express.static('./'));
app.listen(3000);
随着ECMAScript的发展,推出了generator yield 语法,JS向同步方式写异步代码迈出了一步,作为回应,TJ大神推出了Koa.js。
Koa.js
Async/Await
,也就是基于Promise,使用Try-Catch来捕获错误。koa与express 提供的API大致相同,express是大而全,内置了大多数的中间件,更让人省心,koa2不绑定任何的框架,干净简洁,小而精,更容易实现定制化,扩展性好。查看代码
const Koa = require('koa');
const Router = require('koa-router');
const serve = require('koa-static');
const app = new Koa();
const router = Router();
/* 中间件 */
app.use(async (ctx, next) => {
console.log('middleware');
next();
console.log('middleware call');
});
/* 路由部分 */
router.get('/', (ctx) => {
ctx.body = 'Home';
});
app.use(router.routes());
/* 静态文件 */
app.use(serve('./'));
app.listen(3000);
Koa 对比 Koa2
koa2与koa1的最大区别是koa2实现异步是通过async/await,使用await next()进入下一个中间件;koa1实现异步是通过使用generator函数,yield next进入下一个中间件,而express实现异步是通过回调函数的方式。
EGG.js
egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
| ├── router.js
│ ├── controller
│ | └── home.js
│ ├── service (可选)
│ | └── user.js
│ ├── middleware (可选)
│ | └── response_time.js
│ ├── schedule (可选)
│ | └── my_task.js
│ ├── public (可选)
│ | └── reset.css
│ ├── view (可选)
│ | └── home.tpl
│ └── extend (可选)
│ ├── helper.js (可选)
│ ├── request.js (可选)
│ ├── response.js (可选)
│ ├── context.js (可选)
│ ├── application.js (可选)
│ └── agent.js (可选)
├── config
| ├── plugin.js
| ├── config.default.js
│ ├── config.prod.js
| ├── config.test.js (可选)
| ├── config.local.js (可选)
| └── config.unittest.js (可选)
└── test
├── middleware
| └── response_time.test.js
└── controller
└── home.test.js
egg缺点:
- 单实例多进程模式,架构异常复杂,为了提升一点点性能,所增加的复杂度完全不可接受。如今serverless的时代,为了性能容器化部署,多实例才是主流。
- 开发体验,egg调试体验不太好,打个断点也很困难,调试需要依赖console打印。
- 插件机制,万物挂ctx,且不带类型提示(靠别人自觉写d.ts)。插件机制异常黑盒,只能靠配置文件。
- 约定大于配置,处处是约定,想整理目录比较困难。如果你更喜欢代码组织以模块为单位,可能会有些不适应。
- 生态维护力度很低,完善度也很低,很多插件使用方式很不优雅,不成体系,几乎是在hack。
Egg.js
没有原生提供的TypeScript支持, 开发时需要用egg-ts-helper 来帮助自动生成d.ts
文件
Nest.js
Nestjs 是一个将 Typescript 与 Nodejs Framework 结合的产物。
Nest
是基于Express
实现的,需要的话可以取到底层的对象,如request
和response
。Modules, Controllers, Components
Nestjs 开发围绕着这三个单词,Modules 是最大粒度的拆分,表示应用或者模块。Controllers 是传统意义的控制器,一个 Module 拥有多个 Controller。Components 一般用于做 Services,比如将数据库 CRUD 封装在 Services 中,每个 Service 就是一个 Component。
装饰器路由
装饰器路由是个好东西,路由直接标志在函数头上,做到了路由去中心化:
查看代码
@Controller()
export class UsersController {
@Get('users')
getAllUsers() {}
@Get('users/:id')
getUser() {}
@Post('users')
addUser() {}
}
装饰器的用法,几乎和java的Spring注解一模一样。以前用过 Go 语言框架 Beego,就是采用了中心化路由管理方式,虽然引入了 namespace 概念,但当协作者多、模块体量巨大时,路由管理成本直线上升。Nestjs 类似 namespace 的概念通过装饰器实现:
@Controller('users')
export class UsersController {
@Get()
getAllUsers(req: Request, res: Response, next: NextFunction) {}
}
访问 /users 时会进入 getAllUsers 函数。可以看到其 namespace 也是去中心化的。
模块间依赖注入
Modules, Controllers, Components 之间通过依赖注入相互关联,它们通过同名的 @Module @Controller @Component 装饰器申明,如:
查看代码
@Controller()
export class UsersController {
@Get('users')
getAllUsers() {}
}
@Component()
export class UsersService {
getAllUsers() {
return []
}
}
@Module({
controllers: [ UsersController ],
components: [ UsersService ],
})
export class ApplicationModule {}
在 ApplicationModule 申明其内部 Controllers 与 Components 后,就可以在 Controllers 中注入 Components 了:
查看代码
@Controller()
export class UsersController {
constructor(private usersService: UsersService) {}
@Get('users')
getAllUsers() {
return this.usersService.getAllUsers()
}
}
装饰器参数
与大部分框架从 this.req 或 this.context 等取请求参数不同,Nestjs 通过装饰器获取请求参数:
@Get('/:id')
public async getUser(
@Response() res,
@Param('id') id,
) {
const user = await this.usersService.getUser(id);
res.status(HttpStatus.OK).json(user);
}
@Response 获取 res,@Param 获取路由参数,@Query 获取 url query 参数,@Body 获取 Http body 参数。
Midway
面向对象(OOP + Class + IoC)
与函数式(FP + Function + Hooks)
两种编程范式,并在此之上支持了 Web / 全栈 / 微服务 / RPC / Socket / Serverless 等多种场景,致力于为用户提供简单、易用、可靠的 Node.js 服务端研发体验为什么要有 Midway
- Midway 是阿里内部一直持续在研发的框架,之前 egg 是作为底层框架,需要有面向应用层面的框架来和集团场景对接
- 全量使用 TypeScript 是未来一段时间的趋势,面向未来去迭代和研发是作为架构组创新的要求
- 虽然社区已经有 nest 这样的框架,但是这些产品的维护、协作、修改都会受到商业化产品的制约,也无法做到需求的快速迭代和安全性保障,整体的研发理念也和我们不同,为此,我们需要有一套自研的框架体系
优势
- Midway 框架是在内部已经使用 5 年以上的 Node.js 框架,有着长期投入和持续维护的团队做后盾。
- 已经在每年的大促场景经过考验,稳定性无须担心
- 丰富的组件和扩展能力,例如数据库,缓存,定时任务,进程模型,部署以及 Web,Socket 甚至 Serverless 等新场景的支持
- 一体化调用方案可以方便快捷和前端页面协同开发
- 良好的 TypeScript 定义支持
- 国产化文档和沟通容易简单
面向对象(OOP + Class + IoC)
查看代码
// src/controller/home.ts
import { Controller, Get } from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';
@Controller('/')
export class HomeController {
@Inject() ctx: Context
@Get('/') async home() {
return {
message: 'Hello Midwayjs!',
query: this.ctx.ip
}
}
}
函数式(FP + Function + Hooks)
查看代码
// src/api/index.ts
import { useContext } from '@midwayjs/hooks'
import { Context } from '@midwayjs/koa';
export default async function home () {
const ctx = useContext<Context>()
return {
message: 'Hello Midwayjs!',
query: ctx.ip
}
}
NodeJS后端与Java后端对比
优势与劣势
- 优势:占用内存小,启动迅速,不用处理多线程问题,非阻塞高并发,少了Setter Getter和Java各种List/ArrayList/HashMap等...代码编写更简易。
- 劣势:运行时类型的检查,严谨的类型判断,TypeScript对反射与Decorator功能的支持不足导致框架无法更完善的封装高级功能。
性能
总结
参考
https://blog.tangly1024.com/article/nodejs-framework-compare