手把手带你学习Midwayjs实战
前言
哈喽,大家好,我是migor,一个乐于分享工作中所用的一些知识的人,目前专注于前端和Node.js技术栈的分享,工作中目前负责提效平台的搭建和开发。
Node.js么
?我想这个问题,可能每个前端开发者,都会在工作到一定阶段思考这个问题。可以很明确的告诉大家,学习Node.js
可能是将来每个前端开发者必备的一项技能。在 Angular
发布的同一年(2009年),Node.js
也随之登台,Node.js
的出现带来的第一个好处就是前端工程化的成熟,前端构建工具开始百花齐放。这时的前端已经不再是一个简单编写几行 JavaScript
即可完成的事情,前端开发开始出现了前端工程师这个职位,专职前端研发人员开始在各个公司中普及,前后端协作问题也开始加剧。
BFF
随着 Node.js
的成熟,在2015年,基于BFF(Backgroud For Frontend, 服务于前端的后端)
的架构理念被提出,BFF
架构通过在UI
和服务端之间加入中间层,解决了前后端职责难以划分的问题。
如图所示,由于前端的逻辑复杂性不断增加,增加了专门用于处理用户界面逻辑的服务层,同时后端逻辑也完成下沉,基于微服务架构的后端服务逐渐成型,通过基于Node.js
的BFF
层,前后端形成了比较清晰的分工,也就是进入了前端工程师时代。
Node.js的基本原理
先看一下早期的Node.js
结构图,来自Node.js
之父 Ryan Dahl
的演讲稿,它简要的介绍了Node.js
是基于Chrome V8
引擎构建的,由于事件循环Event Loop
分发I/O
任务, 最终工作线程Work Thread
将任务丢到线程池Thread Pool
里去执行, 而事件循环只要等待执行结果就可以了
核心
Chrome V8 解释并执行 JavaScript 代码(这就是为什么浏览器能执行 JavaScript 原因) libuv
由事件循环和线程池组成,负责所有 I/O 任务的分发与执行
常用的框架
框架名称 | 特性 |
---|---|
Express | 简单、实用、路由中间件等俱全 |
Nest.js | 支持ts,易于拓展,结合了函数式编程等 |
Koa.js | 体积更小,代表现代和未来 |
egg.js | 基于Koa,在开发上有更大便利 |
Midway | 支持ts, 渐进式的Node框架,更接近与nest |
为什么选择Midway
如果说这两年那个语言在前端最火,我想
TypeScript
肯定有一席之地,强约束性的语言使得在构建Node.js
应用时,提供了类型检查等约束能力,使得Node.js
更安全等。Midway
基于TypeScript
开发,对于TypeScript
的支持更好一些。最近在深耕于公司的基础建设,使用的
Node.js
框架刚好是Midwayjs
。Midwayjs
提供了Web
中间件的能力。
Midway简介
Midway
是阿里巴巴 - 淘宝前端架构团队,基于渐进式理念研发的 Node.js 框架。
Midway
基于 TypeScript
开发,结合了面向对象(OOP + Class + IoC)
与函数式(FP + Function + Hooks)
两种编程范式,并在此之上支持了 Web / 全栈 / 微服务 / RPC / Socket / Serverless 等多种场景,致力于为用户提供简单、易用、可靠的 Node.js
服务端研发体验。
多编程范式
Midway 支持面向对象与函数式两种编程范式,你可以根据实际研发的需要,选择不同的编程范式来开发应用。
面向对象(OOP + Class + IoC)
Midway 支持面向对象的编程范式,为应用提供更优雅的架构。
下面是基于面向对象,开发路由的示例。
// 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)
Midway 也支持函数式的编程范式,为应用提供更高的研发效率。
下面是基于函数式,开发路由接口的示例。
// src/api/index.ts
import { useContext } from '@midwayjs/hooks'
import { Context } from '@midwayjs/koa';
export default async function home () {
const ctx = useContext()
return {
message: 'Hello Midwayjs!',
query: ctx.ip
}
}
环境准备
首先确保你已经安装了Node.js
,Node.js
安装会附带npx
和一个npm
包运行程序,Midway 3.0.0
最低版本要求12.x
。如果需要帮助,请参考如何安装Node.js环境[1]。
项目创建
使用npm init midway
来创建项目
npm init midway
我们这里使用3.0版本,因此我们这里选择koa-v3
,输入项目名称, 脚手架会帮我们创建一个简单的项目工程,等安装完成。
我们使用Vscode
打开项目。可以得到现在的工程目录
midway-demo
├── README.md
├── README.zh-CN.md
├── bootstrap.js
├── jest.config.js
├── package.json
├── src
│ ├── config
│ │ ├── config.default.ts
│ │ └── config.unittest.ts
│ ├── configuration.ts
│ ├── controller
│ │ ├── api.controller.ts
│ │ └── home.controller.ts
│ ├── filter
│ │ ├── default.filter.ts
│ │ └── notfound.filter.ts
│ ├── interface.ts
│ ├── middleware
│ │ └── report.middleware.ts
│ └── service
│ └── user.service.ts
├── test
│ └── controller
│ ├── api.test.ts
│ └── home.test.ts
└── tsconfig.json
整个项目包括了一些最基本的文件和目录
src
整个工程的源码目录,之后所有的开发代码都将放在这个文件夹下面test
测试目录,之后所有的代码测试文件都在这里package.json
Node.js
项目基础的包管理配置文件,这个想必大家都很熟悉tsconfig.json
TypeScript
编译配置文件.
在src
目录下面,常用的有:
config
业务的配置目录controller
web controller
目录filter
过滤器目录interface.ts
业务的ts定义文件middleware
中间件目录service
服务逻辑目录
启动项目
yarn dev
warning ../../../../../package.json: No license field
$ cross-env NODE_ENV=local midway-bin dev --ts
[ Midway ] Start Server at http://127.0.0.1:7001
在浏览器中输入127.0.0.1:7001
路由
我们来看一下代码中的controller
文件夹下面的home.controller.ts
文件
import { Controller, Get } from '@midwayjs/decorator';
@Controller('/')
export class HomeController {
@Get('/')
async home(): Promise<string> {
return 'Hello Midwayjs!';
}
}
我们找到了浏览器中的输出Hello Midwayjs!
路由装饰器
@controller
装饰器标注了控制器,装饰器有一个可选参数,用于进行路由前缀,这样控制器下面的所有路由都会带上这个前缀。
我们修改一下装饰器中的内容
import { Controller, Get } from '@midwayjs/decorator';
@Controller('/test')
export class HomeController {
@Get('/')
async home(): Promise<string> {
return 'Hello Midwayjs!';
}
}
在浏览器中输入127.0.0.1:7001
报错
报错信息告诉我们路由找不到,那么我们改一下浏览器中的路由127.0.0.1:7001/test
,我们得到了我们想要的结果,这里我们可以知道装饰器中的参数匹配我们的路由
Http装饰器
常见的 Http
装饰器, @Get
、 @Post
、 @Put()
、 @Del()
、 @Patch()
、 @Options()
、 @Head()
和 @All()
,表示各自的 HTTP 请求方法。
我们改写一下代码
import { Controller, Get, Post } from '@midwayjs/decorator';
@Controller('/test')
export class HomeController {
@Post('/')
async home(): Promise<string> {
return 'Hello Midwayjs!';
}
}
通过使用Postman
调用接口,将请求方式改为post
,可以看到我们拿到我们请求的接口了。
全局路由前缀
在工程项目中,我们常常使用一些路由前缀去区分不同服务之间的作用,那么相同的路由前缀,在每个controller
里面加入,显然很麻烦,如果要改变前缀名称,在后期工程相对较大,接口较多的时候,岂不是要一个个去改,在这里我们配置全局的路由前缀。
我们修改config/config.default.ts
文件,代码修改如下
import { MidwayConfig } from '@midwayjs/core';
export default {
// use for cookie sign key, should change to your own and keep security
keys: '1653223786698_4903',
koa: {
port: 7001,
globalPrefix: '/demo',
},
} as MidwayConfig;
保存文件之后,服务不需要我们手动重启,我们请求一下http://127.0.0.1/demo/test
,服务返回了我们的内容。
依赖注入
依赖注入(DI)、控制反转(IoC)等是Spring
的核心思想,那么在midwayjs
中通过装饰器的轻量特性,让依赖注入变得非常优雅.
举个例子🌰:
.
├── package.json
├── src
│ ├── controller # 控制器目录
│ │ └── api.controller.ts
│ └── service # 服务目录
│ └── user.service.ts
└── tsconfig.json
我们实现一下文件的代码
// api.controller.ts
import { Inject, Controller, Get, Query } from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';
import { UserService } from '../service/user.service';
@Controller('/api')
export class APIController {
@Inject()
ctx: Context;
@Inject()
userService: UserService;
@Get('/get_user')
async getUser(@Query('uid') uid) {
const user = await this.userService.getUser({ uid });
return { success: true, message: 'OK', data: user };
}
}
// user.service.ts
import { Provide } from '@midwayjs/decorator';
import { IUserOptions } from '../interface';
@Provide()
export class UserService {
async getUser(options: IUserOptions) {
return {
uid: options.uid,
username: 'mockedName',
phone: '12345678901',
email: 'xxx.xxx@xxx.com',
};
}
}
@Provide
的作用是告诉 依赖注入容器 ,我需要被容器所加载。@Inject
装饰器告诉容器,我需要将某个实例注入到属性上。
上面例子🌰上,我们实现了一个UserService
并通过@Provide
注入到容器中,在app.controller
中,我们通过@Inject
拿到了userService
的实例。
那么我们请求一下接口:
调试
我们在扩展里面搜索JavaScript Debugger
点击下拉箭头,选择JavaScript Debug Terminal
, .
输入命令yarn dev
,在需要debugger
的位置打上断点
在Postman
中请求接口,可以看到代码执行到断点位置
连接Mysql
前面我们已经实现了接口的请求,那么作为后端项目,必然会涉及到数据的CURD
,这里必须得使用数据库实现数据的持久化了,数据库我们这篇文章使用的是Mysql
, 如果是使用的Mongoose
可以参考笔者的另一篇文章MidwayJs多数据库配置,并实现Mongoose自增Id。
数据库安装
笔者使用的是Homebrew
来安装的Mysql
,如果没有安装Homebrew
,可以直接下载安装包安装,或者先安装Homebrew
,详细步骤参见Homebrew[2] 官网。
// 确认brew在正常工作
brew doctor
// 更新包
brew update
// 或者更新全局所有包
brew upgrade
// 安装mysql
brew install mysql
数据库服务启动
安装完成之后启动Mysql
服务
mysql.server start
启动完成。
Mysql可视化
我们使用可视化工具来管理数据库,这里笔者使用的是 Navicat Premium
,可视化工具相对比较多,你可以使用自己喜欢的可视化工具管理数据库。
我们创建一个Mysql
数据库连接,连接名称可以随意取自己喜欢的,输入默认的端口,输入自己数据库的密码。
连接成功之后,我们创建一个Midway
的数据表
创建成功之后
引入TypeORM
TypeORM[3] 是 node.js
现有社区最成熟的对象关系映射器(ORM
)。Midway 和 TypeORM 搭配,使开发更简单。
安装组件
安装ORM
组件,提供数据库ORM
能力
yarn add @midwayjs/orm typeorm --save
引入组件
在src/configuration.ts
引入ORM
组件,代码如下:
// configuration.ts
import { Configuration } from '@midwayjs/decorator';
import * as orm from '@midwayjs/orm';
import { join } from 'path';
@Configuration({
imports: [
// ...
orm // 加载 orm 组件
],
importConfigs: [
join(__dirname, './config')
]
})
export class ContainerConfiguratin {
}
安装数据库Driver
yarn add mysql mysql2 --save
配置数据库连接
在src/config/config.default.ts
中配置mysql
连接。
import { MidwayConfig } from '@midwayjs/core';
export default {
// use for cookie sign key, should change to your own and keep security
keys: '1653223786698_4903',
koa: {
port: 7001,
globalPrefix: '/demo',
},
orm: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'root',
password: '', // 数据库密码
database: 'midway', // 数据表
synchronize: true,
logging: false,
},
} as MidwayConfig;
保存之后重启,数据库连接成功
实现model
在src
文件夹下面创建model
文件夹,创建一个数据库表
声明一个实体table
// user.ts
import { EntityModel } from '@midwayjs/orm';
import { Column, PrimaryGeneratedColumn } from 'typeorm';
// 映射user table
@EntityModel({ name: 'user' })
export class UserModel {
// 声明主键
@PrimaryGeneratedColumn('increment') id: number;
// 映射userName和user表中的user_name对应
@Column({ name: 'user_name' }) userName: string;
@Column({ name: 'age' }) age: number;
@Column({ name: 'description' }) description: string;
}
修改src/user.service.ts
文件
import { Provide } from '@midwayjs/decorator';
import { InjectEntityModel } from '@midwayjs/orm';
import { Repository } from 'typeorm';
import { IUserOptions } from '../interface';
import { UserModel } from '../model/user';
@Provide()
export class UserService {
@InjectEntityModel(UserModel) userModel: Repository;
async getUser(options: IUserOptions) {
return {
uid: options.uid,
username: 'mockedName',
phone: '12345678901',
email: 'xxx.xxx@xxx.com',
};
}
async addUser() {
let record = new UserModel();
record = this.userModel.merge(record, {
userName: 'migor',
age: 18,
description: 'test',
});
try {
const created = await this.userModel.save(record);
return created;
} catch (e) {
console.log(e);
}
}
}
通过InjectEntityModel
装饰器,注入实例化userModel
,启动服务之后,我们在midway
数据表中增加user table
修改src/controller/api.controller.ts
import { Inject, Controller, Get, Query } from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';
import { UserService } from '../service/user.service';
@Controller('/api')
export class APIController {
@Inject()
ctx: Context;
@Inject()
userService: UserService;
@Get('/get_user')
async getUser(@Query('uid') uid) {
const user = await this.userService.getUser({ uid });
return { success: true, message: 'OK', data: user };
}
@Get('/add_user')
async addUser() {
const user = await this.userService.addUser();
return { success: true, message: 'OK', data: user };
}
}
在Postman
中调用add_user
接口
我们可以看到已经能正常返回我们保存的值了,那么我们去数据库看一下,数据是否保存了,刷新一下数据库,我们可以看到数据已经保存成功。
大功告成,至此我们完成数据的保存,那么后面我们可以进行数据的查询,删除,更新等。代码如下
在user.service.ts
中添加如下代码
// 删除用户
async deleteUser() {
const record = await this.userModel
.createQueryBuilder()
.delete()
.where({ userName: 'migor' })
.execute();
const { affected } = record || {};
return affected > 0;
}
// 更新用户信息
async updateUser() {
try {
const result = await this.userModel
.createQueryBuilder()
.update()
.set({
description: '测试更新',
})
.where({ userName: 'migor' })
.execute();
const { affected } = result || {};
return affected > 0;
} catch (e) {
console.log('接口更新失败');
}
}
// 查询
async getUserList() {
const users = await this.userModel
.createQueryBuilder()
.where({ userName: 'migor' })
.getMany();
return users;
}
在api.controller.ts
中增加相应的接口
@Get('/get_user_list')
async getUsers() {
const user = await this.userService.getUserList();
return { success: true, message: 'OK', data: user };
}
@Get('/update_user')
async updateUser() {
const user = await this.userService.updateUser();
return { success: true, message: 'OK', data: user };
}
@Get('/delete_user')
async deleteUser() {
const user = await this.userService.deleteUser()
return { success: true, message: 'OK', data: user };
}
接入Swagger
安装组件
接入swagger
组件和swagger ui
组件
yarn add @midwayjs/swagger swagger-ui-dist
开启组件
在configuration.ts
中增加组件
import { Configuration, App } from '@midwayjs/decorator';
import * as koa from '@midwayjs/koa';
import * as validate from '@midwayjs/validate';
import * as info from '@midwayjs/info';
import { join } from 'path';
import * as orm from '@midwayjs/orm';
import * as swagger from '@midwayjs/swagger';
// import { DefaultErrorFilter } from './filter/default.filter';
// import { NotFoundFilter } from './filter/notfound.filter';
import { ReportMiddleware } from './middleware/report.middleware';
@Configuration({
imports: [
koa,
validate,
{
component: info,
enabledEnvironment: ['local'],
},
orm,
swagger,
],
importConfigs: [join(__dirname, './config')],
})
export class ContainerLifeCycle {
@App()
app: koa.Application;
async onReady() {
// add middleware
this.app.useMiddleware([ReportMiddleware]);
// add filter
// this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
}
}
项目自动重启成功之后,访问地址
UI: http://127.0.0.1:7001/swagger-ui/index.html JSON: http://127.0.0.1:7001/swagger-ui/index.json
启用之后可以查看到对应的接口
swagger
组件会自动识别各个@Controller
中每个路由方法的@Body()
、@Query()
、@Param()
装饰器,提取路由方法参数和类型。
增加接口标签
我们希望给接口增加标签注释,这样才能更好的列举接口的定义
import { Inject, Controller, Get, Query } from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';
import { ApiOperation } from '@midwayjs/swagger';
import { UserService } from '../service/user.service';
@Controller('/api')
export class APIController {
@Inject()
ctx: Context;
@Inject()
userService: UserService;
@ApiOperation({ summary: '获取单个用户' })
@Get('/get_user')
async getUser(@Query('uid') uid) {
const user = await this.userService.getUser({ uid });
return { success: true, message: 'OK', data: user };
}
@ApiOperation({ summary: '增加单个用户' })
@Get('/add_user')
async addUser() {
const user = await this.userService.addUser();
return { success: true, message: 'OK', data: user };
}
@ApiOperation({ summary: '获取用户列表' })
@Get('/get_user_list')
async getUsers() {
const user = await this.userService.getUserList();
return { success: true, message: 'OK', data: user };
}
@ApiOperation({ summary: '更新单个用户' })
@Get('/update_user')
async updateUser() {
const user = await this.userService.updateUser();
return { success: true, message: 'OK', data: user };
}
@ApiOperation({ summary: '删除单个用户' })
@Get('/delete_user')
async deleteUser() {
const user = await this.userService.deleteUser();
return { success: true, message: 'OK', data: user };
}
}
重启之后,可以查看swagger ui
界面,标签增加成功。
总结
至此我们已经完成了Midwayjs
基本功能的学习,包括搭建,数据库的映射,简单的CRUD
,以及ORM
和Swagger
的接入了。
不知不觉搞到了12点,时间有点太晚了,关于接口传参,数据校验等问题,在后续的文章中会继续写,我们后面会进行一个博客前后端搭建的系列文章,后续带你继续学习midway
。
肝文不易,你的每个点赞和关注都是对我最大的鼓励,比心❤️。
如何安装Node.js环境: http://midwayjs.org/docs/how_to_install_nodejs
[2]Homebrew: https://brew.sh/
[3]TypeORM: https://github.com/typeorm/typeorm