midway是阿里巴巴开源的,基于TypeScript语言开发的Nodejs后端框架。 本教程指导大家从0开始搭建一个midway项目。
https://github.com/bestaone/midway-boot
http://midway-boot.hiauth.cn/swagger-ui/index.html
我们这里使用 IntelliJ IDEA
>npm init midway
>cd midway-boot
>npm run dev
启动后浏览器访问:http://127.0.0.1:7001
为了保证代码分隔统一,我们调整下ESLint配置
// .prettierrc.js
module.exports = {
...require('mwts/.prettierrc.json'),
endOfLine: "lf", // 换行符使用 lf
printWidth: 120, // 一行最多 120 字符
proseWrap: "preserve", // 使用默认的折行标准
semi: true, // 行尾需要有分号
}
在windows中代码的首行、尾行不能有空行,否则ESLint提示格式错误,可能是bug。
├─src # 源码目录
│ ├─config # 配置
│ ├─controller # 控制器
│ ├─entity # 数据对象模型
│ ├─filter # 过滤器
│ ├─middleware # 中间件
│ ├─service # 服务类
│ ├─configurations.ts # 服务生命周期管理及配置
│ └─interface.ts # 接口定义
├─test # 测试类目录
├─bootstrap.js # 启动入口
├─package.json # 包管理配置
├─tsconfig.json # TypeScript 编译配置文件
TypeORM是Object Relation Mapping工具,提供的数据库操作能力。
>npm i @midwayjs/typeorm@3 typeorm --save
安装完后package.json
文件中会多出如下配置
{
"dependencies": {
"@midwayjs/typeorm": "^3.4.4",
"typeorm": "^0.3.7"
}
}
在src/configuration.ts
中引入 orm 组件
// 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 { ReportMiddleware } from './middleware/report.middleware';
import * as orm from '@midwayjs/typeorm';
@Configuration({
imports: [
orm, // 引入orm组件
koa,
validate,
{
component: info,
enabledEnvironment: ['local'],
},
],
importConfigs: [join(__dirname, './config')],
})
export class ContainerLifeCycle {
@App()
app: koa.Application;
async onReady() {
this.app.useMiddleware([ReportMiddleware]);
}
}
修改配置src/config/config.default.ts
// src/config/config.default.ts
import { MidwayConfig } from '@midwayjs/core';
export default {
keys: '1657707214114_9253',
koa: {
port: 7001,
},
// 添加orm配置
typeorm: {
dataSource: {
default: {
type: 'mysql',
host: '127.0.0.1', // 改成你的mysql数据库IP
port: 3306, // 改成你的mysql数据库端口
username: 'root', // 改成你的mysql数据库用户名(需要有创建表结构权限)
password: '123456', // 改成你的mysql数据库密码
database: 'midway_boot',// 改成你的mysql数据库IP
synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true
logging: true,
entities: [User],
}
}
},
} as MidwayConfig;
注意:首次启动没有创建表结构的,需要设置自动创建表接口
synchronize: true
>npm install mysql2 --save
安装完后package.json
文件中会多出如下配置
{
"dependencies": {
"mysql2": "^2.3.3"
}
}
src/entity
;user.ts
;// src/entity/user.ts
import { Entity } from '@midwayjs/typeorm';
import {
Column,
CreateDateColumn,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('user')
export class User {
@PrimaryColumn({ type: 'bigint' })
id: number;
@Column({ length: 100, nullable: true })
avatarUrl: string;
@Column({ length: 20, unique: true })
username: string;
@Column({ length: 200 })
password: string;
@Column({ length: 20 })
phoneNum: string;
@Column()
regtime: Date;
@Column({ type: 'bigint' })
updaterId: number;
@Column({ type: 'bigint' })
createrId: number;
@CreateDateColumn()
createTime: Date;
@UpdateDateColumn()
updateTime: Date;
@Column({ type: 'int', default: 1 })
status: number;
}
@EntityModel
用来定义一个实体类;@Column
用来描述类的一个熟悉,对应数据库就是一个数据列;@PrimaryColumn
用来定义一个主键,每个实体类必须要要主键;@PrimaryGeneratedColumn
用来定义一个自增主键;@CreateDateColumn
定义创建时,自动设置日期;@UpdateDateColumn
定义更新时,自动设置日期;对应的数据库结构
CREATE TABLE `user` (
`id` bigint NOT NULL,
`avatarUrl` varchar(100) DEFAULT NULL,
`username` varchar(20) NOT NULL,
`password` varchar(200) NOT NULL,
`phoneNum` varchar(20) NOT NULL,
`regtime` datetime NOT NULL,
`updaterId` bigint NOT NULL,
`createrId` bigint NOT NULL,
`createTime` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updateTime` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`status` int NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `IDX_78a916df40e02a9deb1c4b75ed` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
创建或者修改src/service/user.service.ts
文件。
// src/service/user.service.ts
import { Provide } from '@midwayjs/decorator';
import { User } from '../eneity/user';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { DeleteResult } from 'typeorm/query-builder/result/DeleteResult';
@Provide()
export class UserService {
@InjectEntityModel(User)
userModel: Repository<User>;
async create(user: User): Promise<User> {
return this.userModel.save(user);
}
async findById(id: number): Promise<User> {
return this.userModel.findOneBy({ id });
}
async delete(id: number): Promise<DeleteResult> {
return this.userModel.delete(id);
}
}
@Provide
表示这个类将会由系统自动实例化,在使用的时候,只需要使用@Inject
注入就可以了;@InjectEntityModel
注入实体模型数据库操作工具;注意:由于调整了UserService,
src/controller/api.controller.ts
、test/controller/api.test.ts
会报错,直接删掉即可
创建或者修改src/controller/user.controller.ts
文件。
// src/controller/user.controller.ts
import { Inject, Controller, Query, Post, Body } from '@midwayjs/decorator';
import { User } from '../eneity/user';
import { UserService } from '../service/user.service';
import { DeleteResult } from 'typeorm/query-builder/result/DeleteResult';
@Controller('/api/user')
export class UserController {
@Inject()
userService: UserService;
@Post('/create', { description: '创建' })
async create(@Body() user: User): Promise<User> {
Object.assign(user, {
id: new Date().getTime(),
regtime: new Date(),
updaterId: 1,
createrId: 1,
});
return this.userService.save(user);
}
@Post('/findById', { description: '通过主键查找' })
async findById(@Query('id') id: number): Promise<User> {
return this.userService.findById(id);
}
@Post('/delete', { description: '删除' })
async delete(@Query('id') id: number): Promise<DeleteResult> {
return this.userService.delete(id);
}
}
@Inject()
装饰类指定该对象会被自动注入;添加文件test/controller/user.test.ts
// test/controller/user.test.ts
import {close, createApp, createHttpRequest} from '@midwayjs/mock';
import {Application, Framework} from '@midwayjs/koa';
import {User} from '../../src/eneity/user'
describe('test/controller/user.test.ts', () => {
let app: Application;
let o: User;
beforeAll(async () => {
try {
app = await createApp<Framework>();
} catch(err) {
console.error('test beforeAll error', err);
throw err;
}
});
afterAll(async () => {
await close(app);
});
// create
it('should POST /api/user/create', async () => {
o = new User();
Object.assign(o, {
username: new Date().getTime().toString(),
password: new Date().getTime().toString(),
phoneNum: new Date().getTime().toString(),
});
const result = await createHttpRequest(app).post('/api/user/create')
.send(o);
expect(result.status).toBe(200);
// 将创建好的数据存起来,以供后面测试使用(返回的数据会有id)
o = result.body;
});
// findById
it('should POST /api/user/findById', async () => {
const result = await createHttpRequest(app).post('/api/user/findById?id=' + o.id);
expect(result.status).toBe(200);
});
// delete
it('should POST /api/user/delete', async () => {
const result = await createHttpRequest(app).post('/api/user/delete?id=' + o.id);
expect(result.status).toBe(200);
});
});
beforeAll
、afterAll
分别会在测试开始前、后执行;createApp<Framework>()
BeforeAll阶段的error会忽略,需要手动处理异常;单元测试的详细文档,见:http://www.midwayjs.org/docs/testing
>npm run test
如果测试时间过长,会导致测试失败,那么我们需要修改超时时间
jest.setup.js
;// jest.setup.js
// 只需要一行代码
// 设置单元测试超时时间
jest.setTimeout(60000);
jest
配置文件jest.config.js
;module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
// 添加如下一行代码,引入jest初始化文件
setupFilesAfterEnv: ['<rootDir>/jest.setup.js']
};
common
;src/common/BaseEntity.ts
;// src/common/BaseEntity.ts
import { Column, CreateDateColumn, PrimaryColumn, UpdateDateColumn } from 'typeorm';
export class BaseEntity {
@PrimaryColumn({ type: 'bigint' })
id: number;
@Column({ type: 'bigint' })
updaterId: number;
@Column({ type: 'bigint' })
createrId: number;
@CreateDateColumn()
createTime: Date;
@UpdateDateColumn()
updateTime: Date;
}
src/entity/user.ts
;继承BaseEntity
,并删除user.ts
中的通用字段。
// src/entity/user.ts
import { Entity } from '@midwayjs/typeorm';
import { Column } from 'typeorm';
import { BaseEntity } from '../common/BaseEntity';
@Entity('user')
export class User extends BaseEntity {
@Column({ length: 100, nullable: true })
avatarUrl: string;
@Column({ length: 20, unique: true })
username: string;
@Column({ length: 200 })
password: string;
@Column({ length: 20 })
phoneNum: string;
@Column()
regtime: Date;
@Column({ type: 'int', default: 1 })
status: number;
}
src/common/BaseService.ts
;// src/common/BaseService.ts
import { In, Repository } from 'typeorm';
import { BaseEntity } from './BaseEntity';
import { FindOptionsWhere } from 'typeorm/find-options/FindOptionsWhere';
export abstract class BaseService<T extends BaseEntity> {
abstract getModel(): Repository<T>;
async save(o: T) {
if (!o.id) o.id = new Date().getTime();
return this.getModel().save(o);
}
async delete(id: number) {
return this.getModel().delete(id);
}
async findById(id: number): Promise<T> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return this.getModel().findOneBy({ id });
}
async findByIds(ids: number[]): Promise<T[]> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return this.getModel().findBy({ id: In(ids) });
}
async findOne(where: FindOptionsWhere<T>): Promise<T> {
return this.getModel().findOne({ where });
}
}
abstract
,并添加抽象接口abstract getModel()
;<T extends BaseEntity>
泛型用法,定义T
为BaseEntity
的子类;src/service/user.service.ts
;import { Provide } from '@midwayjs/decorator';
import { User } from '../eneity/user';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { BaseService } from '../common/BaseService';
@Provide()
export class UserService extends BaseService<User> {
@InjectEntityModel(User)
model: Repository<User>;
getModel(): Repository<User> {
return this.model;
}
}
UserService extends BaseService<User>
;getModel()
,并返回Repository
;src/common/BaseController.ts
;// src/common/BaseController.ts
import { BaseService } from './BaseService';
import { BaseEntity } from './BaseEntity';
import { Body, Post, Query } from '@midwayjs/decorator';
/**
* Controller基础类,由于类继承不支持装饰类@Post、@Query、@Body等,
* 所以这里的装饰类不生效,否则实现类就不需要再写多余代码了,
* 这里保留在这里,以备以后可能会支持继承的装饰类
*/
export abstract class BaseController<T extends BaseEntity> {
abstract getService(): BaseService<T>;
@Post('/create')
async create(@Body() body: T): Promise<T> {
return this.getService().save(body);
}
@Post('/delete')
async delete(@Query('id') id: number): Promise<boolean> {
await this.getService().delete(id);
return true;
}
@Post('/update')
async update(@Body() body: T): Promise<T> {
return this.getService().save(body);
}
@Post('/findById')
async findById(@Query('id') id: number): Promise<T> {
return this.getService().findById(id);
}
@Post('/findByIds')
async findByIds(@Query('ids') ids: number[]): Promise<T[]> {
return this.getService().findByIds(ids);
}
}
abstract
,并添加抽象接口abstract getService()
;<T extends BaseEntity>
泛型用法,定义T
为BaseEntity
的子类;src/controller/user.controller.ts
;// src/controller/user.controller.ts
import { Inject, Controller, Query, Post, Body } from '@midwayjs/decorator';
import { User } from '../eneity/user';
import { UserService } from '../service/user.service';
import { BaseController } from '../common/BaseController';
import { BaseService } from '../common/BaseService';
@Controller('/api/user')
export class UserController extends BaseController<User> {
@Inject()
userService: UserService;
getService(): BaseService<User> {
return this.userService;
}
@Post('/create', { description: '创建' })
async create(@Body() user: User): Promise<User> {
Object.assign(user, {
id: new Date().getTime(),
regtime: new Date(),
updaterId: 1,
createrId: 1,
});
return super.create(user);
}
@Post('/findById', { description: '通过主键查找' })
async findById(@Query('id') id: number): Promise<User> {
return super.findById(id);
}
@Post('/delete', { description: '删除' })
async delete(@Query('id') id: number): Promise<boolean> {
return super.delete(id);
}
}
UserController extends BaseController
;getService()
;super.xxx()
;>npm run test
Test Suites: 2 passed, 2 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 10.686 s
web中间件是在控制器调用之前
和之后
调用的函数方法,我们可以利用中间件在接口执行前或者后,加一些逻辑。
比如:统一返回格式、接口鉴权。
src/common/ErrorCode.ts
;// src/common/ErrorCode.ts
export class ErrorCode {
/**
* 100000 正常
*/
static OK = 100000;
/**
* 400000-500000 平台异常
*/
static SYS_ERROR = 400000;
/**
* 50000 未知异常
*/
static UN_ERROR = 500000;
/**
* 60000-69999 基本的业务异常
*/
static BIZ_ERROR = 600000;
}
src/common/CommonException.ts
;// src/common/CommonException.ts
import { MidwayError } from '@midwayjs/core';
export class CommonException extends MidwayError {
code: number;
msg: string;
data: any;
constructor(code: number, msg: string) {
super(msg, code.toString());
this.code = code;
this.msg = msg;
}
}
src/middleware/format.middleware.ts
// src/middleware/format.middleware.ts
import { IMiddleware } from '@midwayjs/core';
import { Middleware } from '@midwayjs/decorator';
import { NextFunction, Context } from '@midwayjs/koa';
import { ErrorCode } from '../common/ErrorCode';
/**
* 对接口返回的数据统一包装
*/
@Middleware()
export class FormatMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
const result = await next();
return { code: ErrorCode.OK, msg: 'OK', data: result };
};
}
match(ctx) {
return ctx.path.indexOf('/api') === 0;
}
static getName(): string {
return 'API_RESPONSE_FORMAT';
}
}
@Middleware()
标识此类是一个中间件;match(ctx)
方法确定哪些路径会被拦截;详细的中间件使用说明见:http://www.midwayjs.org/docs/middleware
注册中间件,需要修改src/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 { ReportMiddleware } from './middleware/report.middleware';
import * as orm from '@midwayjs/typeorm';
import { FormatMiddleware } from './middleware/format.middleware';
@Configuration({
imports: [
orm, // 引入orm组件
koa,
validate,
{
component: info,
enabledEnvironment: ['local'],
},
],
importConfigs: [join(__dirname, './config')],
})
export class ContainerLifeCycle {
@App()
app: koa.Application;
async onReady() {
// 注册中间件 FormatMiddleware
this.app.useMiddleware([FormatMiddleware, ReportMiddleware]);
}
}
此时返回结果已经被重新包装了。
统一的异常处理使用异常过滤器,可以在这里进行异常的封装处理。
src/filter/default.filter.ts
;// src/filter/default.filter.ts
import { Catch } from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';
import { ErrorCode } from '../common/ErrorCode';
@Catch()
export class DefaultErrorFilter {
async catch(err: Error, ctx: Context) {
return { code: ErrorCode.UN_ERROR, msg: err.message };
}
}
src/filter/notfound.filter.ts
;// src/filter/notfound.filter.ts
import { Catch } from '@midwayjs/decorator';
import { httpError, MidwayHttpError } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Catch(httpError.NotFoundError)
export class NotFoundFilter {
async catch(err: MidwayHttpError, ctx: Context) {
// 404 错误会到这里
ctx.redirect('/404.html');
}
}
// src/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 { ReportMiddleware } from './middleware/report.middleware';
import * as orm from '@midwayjs/typeorm';
import { FormatMiddleware } from './middleware/format.middleware';
import { NotFoundFilter } from './filter/notfound.filter';
import { DefaultErrorFilter } from './filter/default.filter';
@Configuration({
imports: [
orm, // 引入orm组件
koa,
validate,
{
component: info,
enabledEnvironment: ['local'],
},
],
importConfigs: [join(__dirname, './config')],
})
export class ContainerLifeCycle {
@App()
app: koa.Application;
async onReady() {
this.app.useMiddleware([FormatMiddleware, ReportMiddleware]);
// 注册异常过滤器
this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
}
}
由于调整了返回值,此时单元测试会报错,我们需要调整下单元。修改test/controller/user.test.ts
。
o = result.body;
# 改为
o = result.body.data;
>npm run test
Test Suites: 2 passed, 2 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 6.525 s, estimated 9 s
断言工具
;我们使用Snowflake主键生成算法。 其优点是:高性能,低延迟;独立的应用;按时间有序。 缺点是:需要独立的开发和部署。 我们这里把算法迁移到本地,测试开发没有问题,生产使用需要配置数据中心和服务器。
utils
;src/utils/Snowflake.ts
;// src/utils/Snowflake.ts
import { Provide } from '@midwayjs/decorator';
/**
* Snowflake主键生成算法
* 完整的算法是生成的ID长度为20位
* 但是由于js最大值9007199254740991,再多就会溢出,再多要特殊处理。
* 所以这里设置长度为16位id。将数据中心位调小到1位,将服务器位调小到1位,将序列位调小到10位
* 这意味着最多支持两个数据中心,每个数据中心最多支持两台服务器
*/
@Provide('idGenerate')
export class SnowflakeIdGenerate {
private twepoch = 0;
private workerIdBits = 1;
private dataCenterIdBits = 1;
private maxWrokerId = -1 ^ (-1 << this.workerIdBits); // 值为:1
private maxDataCenterId = -1 ^ (-1 << this.dataCenterIdBits); // 值为:1
private sequenceBits = 10;
private workerIdShift = this.sequenceBits; // 值为:10
private dataCenterIdShift = this.sequenceBits + this.workerIdBits; // 值为:11
// private timestampLeftShift =
// this.sequenceBits + this.workerIdBits + this.dataCenterIdBits; // 值为:12
private sequenceMask = -1 ^ (-1 << this.sequenceBits); // 值为:4095
private lastTimestamp = -1;
private workerId = 1; //设置默认值,从环境变量取
private dataCenterId = 1;
private sequence = 0;
constructor(_workerId = 0, _dataCenterId = 0, _sequence = 0) {
if (this.workerId > this.maxWrokerId || this.workerId < 0) {
throw new Error('config.worker_id must max than 0 and small than maxWrokerId-[' + this.maxWrokerId + ']');
}
if (this.dataCenterId > this.maxDataCenterId || this.dataCenterId < 0) {
throw new Error(
'config.data_center_id must max than 0 and small than maxDataCenterId-[' + this.maxDataCenterId + ']',
);
}
this.workerId = _workerId;
this.dataCenterId = _dataCenterId;
this.sequence = _sequence;
}
private timeGen = (): number => {
return Date.now();
};
private tilNextMillis = (lastTimestamp): number => {
let timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
};
private nextId = (): number => {
let timestamp: number = this.timeGen();
if (timestamp < this.lastTimestamp) {
throw new Error('Clock moved backwards. Refusing to generate id for ' + (this.lastTimestamp - timestamp));
}
if (this.lastTimestamp === timestamp) {
this.sequence = (this.sequence + 1) & this.sequenceMask;
if (this.sequence === 0) {
timestamp = this.tilNextMillis(this.lastTimestamp);
}
} else {
this.sequence = 0;
}
this.lastTimestamp = timestamp;
// js 最大值 9007199254740991,再多就会溢出
// 超过 32 位长度,做位运算会溢出,变成负数,所以这里直接做乘法,乘法会扩大存储
const timestampPos = (timestamp - this.twepoch) * 4096;
const dataCenterPos = this.dataCenterId << this.dataCenterIdShift;
const workerPos = this.workerId << this.workerIdShift;
return timestampPos + dataCenterPos + workerPos + this.sequence;
};
generate = (): number => {
return this.nextId();
};
}
>npm i bcryptjs --save
src/utils/PasswordEncoder.ts
// src/utils/PasswordEncoder.ts
const bcrypt = require('bcryptjs');
/**
* 加密。加上前缀{bcrypt},为了兼容多种加密算法,这里暂时只实现bcrypt算法
*/
export function encrypt(password) {
const salt = bcrypt.genSaltSync(5);
const hash = bcrypt.hashSync(password, salt, 64);
return '{bcrypt}' + hash;
}
/**
* 解密
*/
export function decrypt(password, hash) {
if (hash.indexOf('{bcrypt}') === 0) {
hash = hash.slice(8);
}
return bcrypt.compareSync(password, hash);
}
// src/common/Assert.ts
import { CommonException } from './CommonException';
export class Assert {
/**
* 不为空断言
*/
static notNull(obj: any, errorCode: number, errorMsg: string) {
if (!obj) {
throw new CommonException(errorCode, errorMsg);
}
}
/**
* 空字符串断言
*/
static notEmpty(obj: any, errorCode: number, errorMsg: string) {
if (!obj || '' === obj.trim()) {
throw new CommonException(errorCode, errorMsg);
}
}
/**
* 布尔断言
*/
static isTrue(expression: boolean, errorCode: number, errorMsg: string) {
if (!expression) {
throw new CommonException(errorCode, errorMsg);
}
}
}
很多时候,后端接口需要登录后才能进行访问,甚至有的接口需要拥有相应的权限才能访问。
这里实现bearer
验证方式(bearerFormat 为 JWT)。
>npm i @midwayjs/jwt@3 --save
>npm i @types/jsonwebtoken --save-dev
安装完后package.json
文件中会多出如下配置
{
"dependencies": {
"@midwayjs/jwt": "^3.3.11"
},
"devDependencies": {
"@types/jsonwebtoken": "^8.5.8"
}
}
src/config/config.default.ts
,添加如下内容;// src/config/config.default.ts
jwt: {
secret: 'setscrew',
expiresIn: 60 * 60 * 24,
}
JWT
组件;// src/configuration.ts
import * as jwt from '@midwayjs/jwt';
@Configuration({
imports: [
jwt,
//...
],
})
export class ContainerLifeCycle {
//...
}
关于JWT的详细使用文档,见:http://www.midwayjs.org/docs/extensions/jwt
>npm i @midwayjs/redis@3 --save
>npm i @types/ioredis --save-dev
安装完后package.json
文件中会多出如下配置
{
"dependencies": {
"@midwayjs/redis": "^3.0.0"
},
"devDependencies": {
"@types/ioredis": "^4.28.7"
}
}
// src/configuration.ts
import * as redis from '@midwayjs/redis';
@Configuration({
imports: [
redis,
// ...
],
})
export class ContainerLifeCycle {
// ...
}
修改src/config/config.default.ts
,添加如下内容:
// src/config/config.default.ts
redis: {
client: {
host: 127.0.0.1,
port: 6379,
db: 0,
},
}
关于Redis的详细使用文档,见:http://www.midwayjs.org/docs/extensions/redis
// src/config/config.default.ts
app: {
security: {
prefix: '/api', # 指定已/api开头的接口地址需要拦截
ignore: ['/api/login'], # 指定该接口地址,不需要拦截
},
}
// src/common/Constant.ts
export class Constant {
// 登陆验证时,缓存用户登陆状态KEY的前缀
static TOKEM = 'TOKEN';
}
// src/common/UserContext.ts
/**
* 登陆后存储访问上下文的状态数据,同时也会存在redis缓存中
*/
export class UserContext {
userId: number;
username: string;
phoneNum: string;
constructor(userId: number, username: string, phoneNum: string) {
this.userId = userId;
this.username = username;
this.phoneNum = phoneNum;
}
}
src/interface.ts
,将UserContext
注册到ApplecationContext
中// src/interface.ts
import '@midwayjs/core';
import { UserContext } from './common/UserContext';
declare module '@midwayjs/core' {
interface Context {
userContext: UserContext;
}
}
src/middleware/security.middleware.ts
// src/middleware/security.middleware.ts
import { Config, Inject, Middleware } from '@midwayjs/decorator';
import { Context, NextFunction } from '@midwayjs/koa';
import { httpError } from '@midwayjs/core';
import { JwtService } from '@midwayjs/jwt';
import { UserContext } from '../common/UserContext';
import { RedisService } from '@midwayjs/redis';
import { Constant } from '../common/Constant';
/**
* 安全验证
*/
@Middleware()
export class SecurityMiddleware {
@Inject()
jwtUtil: JwtService;
@Inject()
cacheUtil: RedisService;
@Config('app.security')
securityConfig;
resolve() {
return async (ctx: Context, next: NextFunction) => {
if (!ctx.headers['authorization']) {
throw new httpError.UnauthorizedError('缺少凭证');
}
const parts = ctx.get('authorization').trim().split(' ');
if (parts.length !== 2) {
throw new httpError.UnauthorizedError('无效的凭证');
}
const [scheme, token] = parts;
if (!/^Bearer$/i.test(scheme)) {
throw new httpError.UnauthorizedError('缺少Bearer');
}
// 验证token,过期会抛出异常
const jwt = await this.jwtUtil.verify(token, { complete: true });
// jwt中存储的user信息
const payload = jwt['payload'];
const key = Constant.TOKEM + ':' + payload.userId + ':' + token;
const ucStr = await this.cacheUtil.get(key);
// 服务器端缓存中存储的user信息
const uc: UserContext = JSON.parse(ucStr);
if (payload.username !== uc.username) {
throw new httpError.UnauthorizedError('无效的凭证');
}
// 存储到访问上下文中
ctx.userContext = uc;
return next();
};
}
public match(ctx: Context): boolean {
const { path } = ctx;
const { prefix, ignore } = this.securityConfig;
const exist = ignore.find((item) => {
return item.match(path);
});
return path.indexOf(prefix) === 0 && !exist;
}
public static getName(): string {
return 'SECURITY';
}
}
@Config('app.security')
装饰类,指定加载配置文件src/config/config.**.ts
中对应的配置信息;JwtService
进行JWT编码校验;
jwt token
将用户信息编码在token中,解码后可以获取对应用户数据,通常情况下,不需要存储到redis中; 但是有个缺点就是,不能人为控制分发出去的token失效。所以,有时人们会使用缓存中的用户信息; 这里使用了JWT+Redis的方式,是为了演示两种做法;
// src/configuration.ts
this.app.useMiddleware([SecurityMiddleware, FormatMiddleware, ReportMiddleware]);
// src/api/dto/CommonDTO.ts
export class LoginDTO {
username: string;
password: string;
}
// src/api/vo/CommonVO.ts
export class LoginVO {
accessToken: string;
expiresIn: number;
}
src/service/user.service.ts
,添加通过用户名查找用户接口;import { Provide } from '@midwayjs/decorator';
import { User } from '../eneity/user';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { BaseService } from '../common/BaseService';
@Provide()
export class UserService extends BaseService<User> {
@InjectEntityModel(User)
model: Repository<User>;
getModel(): Repository<User> {
return this.model;
}
async findByUsername(username: string): Promise<User> {
return this.model.findOne({ where: { username } });
}
}
src/controller/common.controller.ts
;// src/controller/common.controller.ts
import { Body, Config, Controller, Inject, Post } from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';
import { UserService } from '../service/user.service';
import { RedisService } from '@midwayjs/redis';
import { LoginDTO } from '../api/dto/CommonDTO';
import { LoginVO } from '../api/vo/CommonVO';
import { SnowflakeIdGenerate } from '../utils/Snowflake';
import { JwtService } from '@midwayjs/jwt';
import { Assert } from '../common/Assert';
import { ErrorCode } from '../common/ErrorCode';
import { UserContext } from '../common/UserContext';
import { Constant } from '../common/Constant';
import { ILogger } from '@midwayjs/core';
import { decrypt } from '../utils/PasswordEncoder';
import { Validate } from '@midwayjs/validate';
import { ApiResponse, ApiTags } from '@midwayjs/swagger';
@ApiTags(['common'])
@Controller('/api')
export class CommonController {
@Inject()
logger: ILogger;
@Inject()
ctx: Context;
@Inject()
userService: UserService;
@Inject()
cacheUtil: RedisService;
@Inject()
jwtUtil: JwtService;
@Inject()
idGenerate: SnowflakeIdGenerate;
@Config('jwt')
jwtConfig;
@ApiResponse({ type: LoginVO })
@Validate()
@Post('/login', { description: '登陆' })
async login(@Body() body: LoginDTO): Promise<LoginVO> {
const user = await this.userService.findByUsername(body.username);
Assert.notNull(user, ErrorCode.UN_ERROR, '用户名或者密码错误');
const flag = decrypt(body.password, user.password);
Assert.isTrue(flag, ErrorCode.UN_ERROR, '用户名或者密码错误');
const uc: UserContext = new UserContext(user.id, user.username, user.phoneNum);
const at = await this.jwtUtil.sign({ ...uc });
const key = Constant.TOKEM + ':' + user.id + ':' + at;
const expiresIn = this.jwtConfig.expiresIn;
this.cacheUtil.set(key, JSON.stringify(uc), 'EX', expiresIn);
const vo = new LoginVO();
vo.accessToken = at;
vo.expiresIn = expiresIn;
return vo;
}
}
Swagger是一个集成在系统内部,能够通过装饰类描述接口文档的工具,可以方便的测试接口
>npm install @midwayjs/swagger@3 --save
>npm install swagger-ui-dist --save
// src/configuration.ts
import * as swagger from '@midwayjs/swagger';
@Configuration({
imports: [
swagger,
// ...
],
})
export class ContainerLifeCycle {
// ...
}
访问:http://127.0.0.1:7001/swagger-ui/index.html
验证接口,提示缺少凭证
,需要Swagger支持bearer
验证
bearer
支持// src/config/config.default.ts
swagger: {
auth: {
authType: 'bearer',
},
},
@ApiBearerAuth()
;// src/controller/user.controller.ts
@ApiBearerAuth()
@Controller('/api/user')
export class UserController extends BaseController<User> {
// ...
}
@ApiTags()
通常用于Controller
,将其分类标记;@ApiResponse()
用于标注API的返回值;@ApiProperty()
用于标注返回DTO、VO,实体类的属性;common.controller.ts
、user.controller.ts
、user.ts
、CommonDTO.ts
、CommonVO.ts
、BaseEntity.ts
;
关于Swagger的详细使用文档,见:http://www.midwayjs.org/docs/extensions/swagger
>npm install midwayjs-kinfe4j2 --save
@midwayjs/swagger": "^3.3.14
midwayjs-kinfe4j2": "^0.0.2
import ??? from '@midwayjs/swagger';
改为
import ??? from 'midwayjs-knife4j2';
通常我们不希望将生产环境的相关配置写在项目代码中,而希望在不同的环境中启动时自动读取环境中设置的配置; 在本教程中,我也不希望将自己的数据库、缓存IP提交到代码仓库,所以可以使用环境变量+host;
npm install dotenv --save
// src/configuration.ts
import * as dotenv from 'dotenv';
// 初始化环境变量
dotenv.config();
@Configuration({
imports: [
// ...
]
})
export class ContainerLifeCycle {
// ...
}
// .env
MYSQL_HOST=devserver
MYSQL_USERNAME=dev
MYSQL_PASSWORD=123456
MYSQL_PORT=3306
REDIS_HOST=devserver
REDIS_PORT=6379
// windows电脑
// C:\Windows\System32\drivers\etc\hosts
// xx.xx.xx.xx 为你自己mysql、redis的ip,如果在一台机器上的话
xx.xx.xx.xx devserver
// src/config/config.default.ts
typeorm: {
dataSource: {
default: {
type: 'mysql',
host: process.env.MYSQL_HOST,
port: process.env.MYSQL_PORT,
username: process.env.MYSQL_USERNAME,
password: process.env.MYSQL_PASSWORD,
database: 'midway_boot',
synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true
logging: true,
entities: [User],
}
}
},
// redis配置
redis: {
client: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
db: 0,
},
},
在生产环境中使用,你可以将环境变量配置到系统中,如果你是Docker启动,可以指定环境变量文件。
Docker是基于Go语言进行开发实现,一个开源的应用容器引擎。
在项目根目录中添加Dockerfile构建配置文件;
FROM node:16.14.2-alpine
WORKDIR /app
ENV TZ="Asia/Shanghai"
COPY . .
RUN npm install --registry=https://registry.npm.taobao.org
RUN npm run build
# 移除开发环境的依赖
RUN npm prune --production
# 暴露端口(内部)
EXPOSE 7001
# 设定容器启动时第一个运行的命令及其参数
ENTRYPOINT ["npm", "run", "start"]
Jenkins是一个基于java开发的一个开源的自动化工具,它能够帮我们快速的完成测试、编译、构建、打包、发布等一系列部署任务;
可以网上查找下安装配置方法,这里不赘述了。
#!/bin/sh
#./deploy.sh -n hi-mall -v 1.0 -R hiauth -p 8182:8182
url='bestaone'
name=""
version="1.0"
registry="hlll"
portMapping=""
opts=""
envFile=""
function useage () {
echo "Usage: -n name -v version -p portMapping [-R registry] [-e envFile] [-o opts]"
echo "-n name, app name"
echo "-v version, deploy image version"
echo "-p portMapping, container port mapping"
echo "-R registry, image registry"
echo "-e envFile, Env File"
echo "-o opts, JVM OPST"
exit 1
}
while getopts "h:n:v:R:V:p:N:e:o:P:" option
do
case "${option}" in
n)
name=${OPTARG} ;;
v)
version=${OPTARG} ;;
p)
portMapping+=" -p "${OPTARG} ;;
R)
registry=${OPTARG} ;;
e)
envFile=${OPTARG} ;;
o)
opts=${OPTARG} ;;
\\?)
useage ;;
esac
done
if [ "${name}" == "" ] ; then
useage ;
fi
if [ "${portMapping}" == "" ] ; then
useage ;
fi
echo "--------------------------------------------------------------------------"
echo "Name = ${name}"
echo "Version = ${version}"
echo "Opts = ${opts}"
echo "Registry = ${registry}"
echo "PortMapping = ${portMapping}"
echo "EnvFile = ${envFile}"
runcommand=""
echo "Deploy ${name}:${version}"
echo "step 1 : shoutdown and remove container"
docker ps -a --filter "name=$name" | awk '{print $1}'| while read cid
do
if [ $cid != 'CONTAINER' ];then
echo docker rm -f $cid
docker rm -f $cid
fi
done
echo "step 2 : remove image"
echo docker rmi $url/$registry/$name:$version
docker rmi $url/$registry/$name:$version
echo "step 3 : build new image"
echo docker build -t $url/$registry/$name:$version /opt/services/$name
docker build -t $url/$registry/$name:$version /opt/services/$name
echo "step 4 : run container"
if [ "${name}" != "" ] ; then
runcommand="${runcommand} --name ${name} "
fi
if [ "${portMapping}" != "" ] ; then
runcommand="${runcommand} ${portMapping} "
fi
if [ "${volume}" != "" ] ; then
runcommand="${runcommand} -v ${volume} "
fi
if [ "${envFile}" != "" ] ; then
runcommand="${runcommand} --env-file ${envFile} "
fi
if [ "${opts}" != "" ] ; then
runcommand="${runcommand} -e 'JAVA_OPTS=${opts}' "
fi
echo docker run -d $runcommand $url/$registry/$name:$version
docker run -d $runcommand $url/$registry/$name:$version
echo "step 5 : check deploy"
echo docker images
docker images
echo docker ps -a
docker ps -a
echo "${name}:${version} deploy over!"
添加一个自由风格的任务,添加好源码地址(git),然后添加执行shell。
# 切换目录
cd /root/.jenkins/workspace/midway-boot
# 删除旧文件
rm -rf /opt/services/midway-boot/*
# 复制新文件
cp -rf /root/.jenkins/workspace/midway-boot/* /opt/services/midway-boot/
# 发布
/opt/services/deploy.sh -n midway-boot -R midway -p 10100:7001 -e /opt/services/.env
在这之前需要先创建目录 /opt/services/midway-boot,以及添加环境变量配置文件 /opt/services/.env
等Jenkins构建任务之心完成之后,我们可以输入主机地址(域名或IP)进行访问了
service:
name: midway_boot
provider:
name: aliyun # aliyun(cn-zhangjiakou)、tencent(ap-shanghai)
region: cn-zhangjiakou
runtime: nodejs14
memorySize: 128
timeout: 5
environment:
MYSQL_HOST: devserver # 需要修改
MYSQL_USERNAME: root
MYSQL_PASSWORD: 123456
MYSQL_PORT: 3306
REDIS_HOST: devserver
REDIS_PORT: 6379
deployType:
type: koa
version: 3.0.0
custom:
customDomain:
domainName: auto # auto:需要使用自动域名
functions:
apis:
handler: index.handler
events:
- http:
path: /*
在项目根目录下添加启动文件app.js
const WebFramework = require('@midwayjs/koa').Framework;
const { Bootstrap } = require('@midwayjs/bootstrap');
/**
* serverless 部署是需要 添加此启动文件
*/
module.exports = async () => {
console.log('启动服务');
// 加载框架并执行
await Bootstrap.run();
// 获取依赖注入容器
const container = Bootstrap.getApplicationContext();
// 获取 koa framework
const framework = container.get(WebFramework);
// 返回 app 对象
return framework.getApplication();
};
npm run deploy
控制台输出:
Install production dependencies...
- Dependencies install complete
Package artifact...
- Artifact file serverless.zip
There is auto config in the service: midway_boot
Auto Domain: http://app-index.midway-boot.1480563473081285.cn-zhangjiakou.fc.devsapp.net/
Function 'app_index' deploy success
http://app-index.midway-boot.1480563473081285.cn-zhangjiakou.fc.devsapp.net 就是对应的服务地址
时间较长,观察命令行输出,耐心等待会。
第一次执行需要配置云平台账号,如果没有出现配置提示,可以主动执行:
npx midway-bin deploy --resetConfig
如何配置,请参考:点击这里
访问:https://app-index.midway-boot.1480563473081285.cn-zhangjiakou.fc.devsapp.net/swagger-ui/index.html
provider:
name: aliyun
region: cn-zhangjiakou
# 改为
provider:
name: tencent
region: ap-shanghai
npm run deploy
控制台会输出二维码,使用微信扫码授权。
使用控制台输入的域名进行测试。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。