web 前端培训带你学习 Midwayjs 实战
经常有人问,现在都 2022 年了,还要学习 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 任务的分发与执行
常用的框架
编辑
为什么选择 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<Context>()
return {
message: 'Hello Midwayjs!',
query: ctx.ip
}
}
环境准备
首先确保你已经安装了 Node.js,Node.js 安装会附带 npx 和一个 npm 包运行程序,Midway 3.0.0 最低版本要求 12.x。
项目创建
使用 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_web前端培训。
编辑
// 确认 brew 在正常工作
brew doctor
// 更新包
brew update
// 或者更新全局所有包
brew upgrade
// 安装 mysql
brew install mysql
数据库服务启动
安装完成之后启动 Mysql 服务
mysql.server start
编辑
启动完成。
Mysql 可视化
我们使用可视化工具来管理数据库,这里笔者使用的是 Navicat Premium,可视化工具相对比较多,你可以使用自己喜欢的可视化工具管理数据库。
我们创建一个 Mysql 数据库连接,连接名称可以随意取自己喜欢的,输入默认的端口,输入自己数据库的密码。
编辑
连接成功之后,我们创建一个 Midway 的数据表
编辑
创建成功之后
编辑
引入 TypeORM
TypeORM 是 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<UserModel>;
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 界面,标签增加成功。
编辑
文章来源于程序员成长指北
评论