TEGG学习总结
我也是初次接触到TEGG,下面内容是根据GITHUB上的npmmirror 项目总结而出,仅代表个人理解,如有错误,请指出。
tegg
将任务组件化,每个组件负责一个任务模块,在每个组件文件夹中需要定义个json
文件夹。
{
"name": "cnpmcore-port",
"eggModule": {
"name": "cnpmcorePort"
}
}
@SingletonProto()
语法糖
全局单例语法糖,整个应用单例。
@SingletonProto(params: {
// 原型的实例化名称
// 默认行为:会把 Proto 的首字母转为小写
// 如 UserAdapter 会转换为 userAdapter
// 如果有不符合预期的可以手动指定,比如
// @SingletonProto({ name: 'mistAdapter' })
// MISTAdapter
// MISTAdapter 的实例名称即为 mistAdapter
name?: string;
// 对象是在 module 内可访问还是全局可访问
// PRIVATE: 仅 module 内可访问
// PUBLIC: 全局可访问
// 默认值为 PRIVATE
accessLevel?: AccessLevel;
})
@ContextProto()
语法糖
每次请求都会实例化一个ContextProto
,并且只会实例化一次。
@ContextProto(params: {
// 原型的实例化名称
// 默认行为:会把 Proto 的首字母转为小写
// 如 UserAdapter 会转换为 userAdapter
// 如果有不符合预期的可以手动指定,比如
// @ContextProto({ name: 'mistAdapter' })
// MISTAdapter
// MISTAdapter 的实例名称即为 mistAdapter
name?: string;
// 对象是在 module 内可访问还是全局可访问
// PRIVATE: 仅 module 内可访问
// PUBLIC: 全局可访问
// 默认值为 PRIVATE
accessLevel?: AccessLevel;
})
@Inject()
语法糖
注入变量,向控制器类中注入变量。
@Inject(param?: {
// 注入对象的名称,在某些情况下一个原型可能有多个实例
// 比如说 egg 的 logger
// 默认为属性名称
name?: string;
// 注入原型的名称
// 在某些情况不希望注入的原型和属性使用一个名称
// 默认为属性名称
proto?: string;
})
{root}/app/ceshi/controller/home.ts
import { Inject } from '@eggjs/tegg';
import { EggLogger } from 'egg';
export class DownloadController extends AbstractController {
@Inject()
private readonly logger: EggLogger;
}
@Middleware()
语法糖
注入中间件,向控制器类中注入中间件。
import { Middleware } from '@eggjs/tegg';
import { traceMethod } from 'app/middleware/trace_method'; // 中间件
@Middleware(traceMethod)
export class HelloController {}
@HTTPController()
语法糖
控制器的类型为HTTP类型,可以实现HTTP请求。
import {
HTTPController,
HTTPMethod,
HTTPMethodEnum,
Context,
EggContext,
HTTPQuery,
Middleware,
Inject,
} from '@eggjs/tegg';
import { traceMethod } from 'app/middleware/trace_method';
@HTTPController()
@Middleware(traceMethod)
export class HelloController {
@HTTPMethod({
method: HTTPMethodEnum.GET,
path: '/hello',
})
async hello(@Context() ctx: EggContext, @HTTPQuery() name: string) {
return {
success: true,
data: { message }
};
}
}
@Context()
语法糖
注入CTX
对象语法糖,在@HTTPController()
控制器语法糖中使用。
import { Context, EggContext } from '@eggjs/tegg';
export class HelloController {
async hello(@Context() ctx: EggContext) {
const message = await this.helloService.hello(name);
return {
success: true,
data: {message}
};
}
}
@HTTPMethod()
语法糖
HTTP请求方法语法糖。
import { HTTPMethod } from '@eggjs/tegg';
export class HelloController {
@HTTPMethod({
method: HTTPMethodEnum.GET, // HTTP请求类型
path: '/hello',
})
}
@HTTPQuery()
语法糖
HTTP请求query
参数语法糖。
import { HTTPQuery } from '@eggjs/tegg';
export class HelloController {
async hello(@HTTPQuery() name: string) {
return {
success: true,
data: { message }
};
}
}
@HTTPBody()
语法糖
HTTP请求的请求体。
import { Context, EggContext, HTTPBody} from '@eggjs/tegg';
import { E400 } from 'egg-errors';
export class ScopeController extends AbstractController {
async createScope(@Context() ctx: EggContext, @HTTPBody() scopeOptions: Static<typeof ScopeCreateOptions>) {
await this.scopeManagerService.createScope({
name,
registryId,
operatorId: authorizedUser.userId,
});
return { ok: true };
}
}
@HTTPParam()
语法糖
HTTP请求的Param
参数语法糖。
import {
Context,
EggContext,
HTTPParam,
} from '@eggjs/tegg';
@HTTPController()
export class ScopeController extends AbstractController {
async removeScope(@Context() ctx: EggContext, @HTTPParam() id: string) {
const authorizedUser = await this.userRoleManager.requiredAuthorizedUser(ctx, 'setting');
await this.scopeManagerService.remove({ scopeId: id, operatorId: authorizedUser.userId });
return { ok: true };
}
}
HTTPMethodEnum
类
HTTP请求的枚举类型。
import { HTTPController, HTTPMethod, HTTPMethodEnum } from '@eggjs/tegg';
@HTTPController()
export class BinarySyncController extends AbstractController {
@HTTPMethod({
path: '/-/binary/:binaryName(@[^/]{1,220}\/[^/]{1,220}|[^@/]{1,220})',
method: HTTPMethodEnum.GET,
})
async showBinaryIndex(@Context() ctx: EggContext, @HTTPParam() binaryName: BinaryName) {
// check binaryName valid
try {
ctx.tValidate(BinaryNameRule, binaryName);
} catch (e) {
throw new NotFoundError(`Binary "${binaryName}" not found`);
}
return await this.showBinary(ctx, binaryName, '/');
}
}
EGG
中类
EggLogger、EggAppConfig
EGG-ERRORS
中类
NotFoundError、UnavailableForLegalReasonsError、UnprocessableEntityError、ForbiddenError、UnauthorizedError、BadRequestError、E400
backgroundTaskHelper
类
异步任务函数类。
import { BackgroundTaskHelper } from '@eggjs/tegg';
@HTTPController()
export class PackageSyncController extends AbstractController {
async createSyncTask(@Context() ctx: EggContext, @HTTPParam() fullname: string, @HTTPBody() data: SyncPackageTaskType) {
if (data.force) {
if (isAdmin) {
// set background task timeout to 5min
this.backgroundTaskHelper.timeout = 1000 * 60 * 5;
this.backgroundTaskHelper.run(async () => {
ctx.logger.info('[PackageSyncController.createSyncTask:execute-immediately] taskId: %s',
task.taskId);
// execute task in background
await this._executeTaskAsync(task);
});
}
}
ctx.status = 201;
return {
ok: true,
id: task.taskId,
type: task.type,
state: task.state,
};
}
}
生命周期 hook
由于对象的生命周期交给了容器来托管,代码中无法感知什么时候对象初始化,什么时候依赖被注入了。所以提供了生命周期 hook 来实现这些通知的功能。
/**
* lifecycle hook interface for egg object
*/
interface EggObjectLifecycle {
/**
* call after construct
*/
postConstruct?(): Promise<void>;
/**
* call before inject deps
*/
preInject?(): Promise<void>;
/**
* call after inject deps
*/
postInject?(): Promise<void>;
/**
* before object is ready
*/
init?(): Promise<void>;
/**
* call before destroy
*/
preDestroy?(): Promise<void>;
/**
* destroy the object
*/
destroy?(): Promise<void>;
}
{root}/app/ceshi/controller/home.ts
import {
HTTPController,
HTTPMethod,
HTTPMethodEnum,
Inject,
HTTPQuery,
Context,
EggContext,
EggObjectLifecycle
} from '@eggjs/tegg';
import { EggLogger } from 'egg';
import {Name} from '../typings/ceshi';
@HTTPController({
controllerName: 'HomeController',
path: '/'
})
export class HomeController implements EggObjectLifecycle {
@Inject()
logger: EggLogger;
@HTTPMethod({
path: '/',
method: HTTPMethodEnum.GET
})
async index(@Context() ctx: EggContext, @HTTPQuery() name: string) {
this.logger.info("hello egg info");
ctx.tValidate(Name, ctx.query);
const res = await ctx.model.Users.findOne({
where: {
id: 1
}
});
return {
res,
name
};
}
async postConstruct(): Promise<void> {
console.log('对象构造完成');
}
async preInject(): Promise<void> {
console.log('依赖将要注入');
}
async postInject(): Promise<void> {
console.log('依赖注入完成');
}
async init(): Promise<void> {
console.log('执行一些异步的初始化过程');
}
async preDestroy(): Promise<void> {
console.log('对象将要释放了');
}
async destroy(): Promise<void> {
console.log('执行一些释放资源的操作');
}
}
组件内原型名称冲突
一个组件内,有两个原型,原型名相同,实例化不同,这时直接Inject
是不行的,组件无法理解具体需要哪个对象。这时就需要告知组件需要注入的对象实例化方式是哪种。
@InitTypeQualifier(initType: ObjectInitType)
{root}/app/ceshi/controller/home.ts
import {
HTTPController,
HTTPMethod,
HTTPMethodEnum,
Inject,
HTTPQuery,
Context,
EggContext,
EggObjectLifecycle,
ObjectInitType,
ContextProto,
InitTypeQualifier
} from '@eggjs/tegg';
import { EggLogger } from 'egg';
import {Name} from '../typings/ceshi';
@ContextProto()
@HTTPController({
controllerName: 'HomeController',
path: '/'
})
export class HomeController implements EggObjectLifecycle {
@Inject()
@InitTypeQualifier(ObjectInitType.CONTEXT)
logger: EggLogger;
@HTTPMethod({
path: '/',
method: HTTPMethodEnum.GET
})
async index(@Context() ctx: EggContext, @HTTPQuery() name: string) {
this.logger.info("hello egg info");
ctx.tValidate(Name, ctx.query);
const res = await ctx.model.Users.findOne({
where: {
id: 1
}
});
return {
res,
name
};
}
}
组件间原型名称冲突
可能多个组件都实现了名称为HelloAdapter
的原型,且accessLevel = AccessLevel.PUBLIC
,需要明确的告知组件需要注入的原型来自哪个组件。
@ModuleQualifier(moduleName: string)
{root}/app/ceshi/controller/home.ts
import {
HTTPController,
HTTPMethod,
HTTPMethodEnum,
Inject,
HTTPQuery,
Context,
EggContext,
EggObjectLifecycle,
ContextProto,
ModuleQualifier
} from '@eggjs/tegg';
import { EggLogger } from 'egg';
import {Name} from '../typings/ceshi';
@ContextProto()
@HTTPController({
controllerName: 'HomeController',
path: '/'
})
export class HomeController implements EggObjectLifecycle {
@Inject()
@ModuleQualifier('foo')
logger: EggLogger;
@HTTPMethod({
path: '/',
method: HTTPMethodEnum.GET
})
async index(@Context() ctx: EggContext, @HTTPQuery() name: string) {
this.logger.info("hello egg info");
ctx.tValidate(Name, ctx.query);
const res = await ctx.model.Users.findOne({
where: {
id: 1
}
});
return {
res,
name
};
}
}
egg
内ctx/app
命名冲突
egg
内可能出现ctx
和app
上有同名对象的存在,我们可以通过使用 EggQualifier
来明确指定注入的对象来自ctx
还是app
。不指定时,默认注入`app上的对象。
@EggQualifier(eggType: EggType)
{root}/app/ceshi/controller/home.ts
import {
HTTPController,
HTTPMethod,
HTTPMethodEnum,
Inject,
HTTPQuery,
Context,
EggContext,
EggObjectLifecycle,
ContextProto,
EggQualifier,
EggType
} from '@eggjs/tegg';
import { EggLogger } from 'egg';
import {Name} from '../typings/ceshi';
@ContextProto()
@HTTPController({
controllerName: 'HomeController',
path: '/'
})
export class HomeController implements EggObjectLifecycle {
@Inject()
@EggQualifier(EggType.CONTEXT)
logger: EggLogger;
@HTTPMethod({
path: '/',
method: HTTPMethodEnum.GET
})
async index(@Context() ctx: EggContext, @HTTPQuery() name: string) {
this.logger.info("hello egg info");
ctx.tValidate(Name, ctx.query);
const res = await ctx.model.Users.findOne({
where: {
id: 1
}
});
return {
res,
name
};
}
}
MiddleWare
结构
import { EggContext, Next } from '@eggjs/tegg';
export async function Tracing(ctx: EggContext, next: Next) {
// headers: {
// 'user-agent': 'npm/8.1.2 node/v16.13.1 darwin arm64 workspaces/false',
// 'npm-command': 'adduser',
// 'content-type': 'application/json',
// accept: '*/*',
// 'content-length': '124',
// 'accept-encoding': 'gzip,deflate',
// host: 'localhost:7001',
// connection: 'keep-alive'
// }
ctx.set('request-id', ctx.tracer.traceId);
if (ctx.method !== 'HEAD') {
ctx.logger.info('[Tracing] auth: %s, npm-command: %s, referer: %s, user-agent: %j',
ctx.get('authorization') ? 1 : 0,
ctx.get('npm-command') || '-',
ctx.get('referer') || '-',
ctx.get('user-agent'));
}
await next();
}
Schedule
结构
通过运行指令npm i @eggjs/tegg-schedule-plugin -S
来安装插件。
{root}/config/plugin.ts
import { EggPlugin } from 'egg';
const plugin: EggPlugin = {
...
teggSchedule: {
enable: true,
package: '@eggjs/tegg-schedule-plugin',
}
};
export default plugin;
{root}/app/ceshi/schedule/task.ts
import { IntervalParams, Schedule, ScheduleType } from '@eggjs/tegg/schedule';
import { Inject } from '@eggjs/tegg';
@Schedule<IntervalParams>({
type: ScheduleType.WORKER,
scheduleData: {
interval: 60000,
// cron: '0 2 * * *'
},
})
export class ChangesStreamWorker {
async subscribe() {
}
}
egg-typebox-validate
插件
用于tegg
的参数验证。
通过运行指令npm i egg-typebox-validate -S
来安装插件。
{root}/config/plugin.ts
import { EggPlugin } from 'egg';
const plugin: EggPlugin = {
typeboxValidate: {
enable: true,
package: 'egg-typebox-validate',
},
};
export default plugin;
基本使用
import { Static, Type } from 'egg-typebox-validate/typebox';
const paramsSchema = Type.Object({
id: Type.String(),
name: Type.String(),
timestamp: Type.Integer(),
});
export type ParamsType = Static<typeof paramsSchema>;
ctx.tValidate(paramsSchema, ctx.params);
ioredis
和egg-redis
插件
用于操作redis
数据库。
通过运行指令npm i ioredis -D
来安装插件。通过运行指令npm i egg-redis -S
来安装插件。
{root}/config/plugin.ts
import { EggPlugin } from 'egg';
const plugin: EggPlugin = {
redis: {
enable: true,
package: 'egg-redis',
}
};
export default plugin;
{root}/config/config.default.ts
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
...
config.redis = {
client: {
port: 6379,
host: '127.0.0.1',
password: '',
db: 2
}
};
// the return config will combines to EggAppConfig
return {
...config,
...bizConfig,
};
};
简单使用
import { SingletonProto, AccessLevel, Inject } from '@eggjs/tegg';
// FIXME: egg-redis should use ioredis v5
// https://github.com/eggjs/egg-redis/issues/35
import type { Redis } from 'ioredis';
const ONE_DAY = 3600 * 24;
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
})
export class CacheAdapter {
@Inject()
private readonly redis: Redis; // 由 redis 插件引入
async setBytes(key: string, bytes: Buffer) {
await this.redis.setex(key, ONE_DAY, bytes);
}
async getBytes(key: string) {
return await this.redis.getBuffer(key);
}
async set(key: string, text: string) {
await this.redis.setex(key, ONE_DAY, text);
}
async get(key: string) {
return await this.redis.get(key);
}
async delete(key: string) {
await this.redis.del(key);
}
async lock(key: string, seconds: number) {
const lockName = this.getLockName(key);
const existsTimestamp = await this.redis.get(lockName);
if (existsTimestamp) {
if (Date.now() - parseInt(existsTimestamp) < seconds * 1000) {
return null;
}
// lock timeout, delete it
await this.redis.del(lockName);
}
const timestamp = `${Date.now() + seconds * 1000}`;
const code = await this.redis.setnx(lockName, timestamp);
// setnx fail, lock fail
if (code === 0) return null;
// expire
await this.redis.expire(lockName, seconds);
return timestamp;
}
}
egg-sequelize
和mysql2
插件
通过运行指令npm install egg-sequelize mysql2 -S
来安装插件和库。
{root}/config/plubin.ts
import { EggPlugin } from 'egg';
const plugin: EggPlugin = {
...
sequelize: {
enable: true,
package: 'egg-sequelize'
}
};
export default plugin;
{root}/config.default.ts
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
...
config.sequelize = {
dialect: 'mysql',
host: '127.0.0.1',
username: 'root',
password: 'xxx',
port: 3306,
database: 'ceshi',
// 中国时区
timezone: '+08:00',
define: {
// 取消数据表名复数
freezeTableName: true,
// 自动写入时间戳 created_at updated_at
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
// 所有驼峰命名格式化
underscored: true
}
};
// the return config will combines to EggAppConfig
return {
...config,
...bizConfig,
};
};
通过运行指令npm install sequelize-cli -D
来安装数据库管理工具。
通过在根目录下新建一个名为.sequelizerc
的文件。
{root}/.sequelizerc
'use strict';
const path = require('path');
module.exports = {
config: path.join(__dirname, 'database/config.json'),
'migrations-path': path.join(__dirname, 'database/migrations'),
'seeders-path': path.join(__dirname, 'database/seeders'),
'models-path': path.join(__dirname, 'app/model'),
};
通过运行指令npx sequelize init:config
和npx sequelize init:migrations
来初始化配置文件。
{root}/database/config.json
{
"development": {
"username": "root",
"password": "xxx",
"database": "lovers",
"host": "127.0.0.1",
"dialect": "mysql"
},
"test": {
"username": "root",
"password": "xxx",
"database": "lovers",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": "xxx",
"database": "lovers",
"host": "127.0.0.1",
"dialect": "mysql"
}
}
通过运行指令npx sequelize db:create
来创建数据库。
通过执行指令npx sequelize migration:generate --name=表名
,来创建迁移文件。
{root}/database/migrations/表名.js
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
const { INTEGER, STRING, DATE, ENUM } = Sequelize;
// 创建表
await queryInterface.createTable('表名', {
id:
{
type: INTEGER(20).UNSIGNED,
primaryKey: true,
autoIncrement: true
},
username:
{
type: STRING(30),
allowNull: false,
comment: '用户名称',
defaultValue: ''
},
email:
{
type: STRING(160),
allowNull: false,
comment: '用户邮箱',
unique: true,
defaultValue: ''
},
password:
{
type: STRING(200),
allowNull: false,
comment: '用户密码',
defaultValue: ''
},
avatar:
{
type: STRING(200),
allowNull: true,
comment: '用户头像',
defaultValue: ''
},
birthday:
{
type: DATE,
allowNull: false,
comment: '用户生日',
defaultValue: '2000-12-20'
},
gender:
{
type: ENUM,
values: ['男', '女'],
allowNull: false,
comment: '用户性别',
defaultValue: '男'
},
status:
{
type: ENUM,
values: ['1', '0'],
allowNull: false,
comment: '1:正常; 0:禁用',
defaultValue: '1'
},
created_at: DATE,
updated_at: DATE
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('users');
}
};
通过运行指令npx sequelize db:migrate
来更新升级数据表。
## 升级数据库
npx sequelize db:migrate
## 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更
## npx sequelize db:migrate:undo
## 可以通过 `db:migrate:undo:all` 回退到初始状态
## npx sequelize db:migrate:undo:all
通过在app
文件夹下新建一个文件夹model
,来实现在框架的app
对象上挂载。
注意:文件夹model下的文件名一定要与数据库迁移文件的文件名一致。
{root}/app/model/表名.ts
'use strict';
const CryptoJS = require('crypto-js');
module.exports = app => {
const { INTEGER, STRING, DATE, ENUM } = app.Sequelize;
const Users = app.model.define('users',
{
id:
{
type: INTEGER(20).UNSIGNED,
primaryKey: true,
autoIncrement: true
},
username:
{
type: STRING(30),
allowNull: false,
comment: '用户名称',
defaultValue: ''
},
email:
{
type: STRING(160),
allowNull: false,
comment: '用户邮箱',
unique: true,
defaultValue: ''
},
birthday:
{
type: DATE,
allowNull: false,
comment: '用户生日',
defaultValue: '2000-12-20'
},
password:
<any>{
type: STRING(200),
allowNull: false,
comment: '用户密码',
defaultValue: '',
set(val: string) {
let hmac = CryptoJS.SHA256(`${val}${app.config.crypto.secret}`);
const hash = hmac.toString(CryptoJS.enc.Hex);
this.setDataValue('password', hash);
}
},
avatar:
{
type: STRING(200),
allowNull: true,
comment: '用户头像',
defaultValue: ''
},
gender:
{
type: ENUM,
values: ['男', '女'],
allowNull: false,
comment: '用户性别',
defaultValue: '男'
},
status:
{
type: ENUM,
values: ['1', '0'],
allowNull: false,
comment: '1:正常; 0:禁用',
defaultValue: '1'
},
created_at: DATE,
updated_at: DATE
});
return Users;
}
简单使用
{root}/app/ceshi/controller/home.ts
import {
HTTPController,
HTTPMethod,
HTTPMethodEnum,
Inject,
HTTPQuery,
Context,
EggContext
} from '@eggjs/tegg';
import { EggLogger } from 'egg';
import {Name} from '../typings/ceshi';
@HTTPController({
controllerName: 'HomeController',
path: '/'
})
export class HomeController {
@Inject()
logger: EggLogger;
@HTTPMethod({
path: '/',
method: HTTPMethodEnum.GET
})
async index(@Context() ctx: EggContext, @HTTPQuery() name: string) {
this.logger.info("hello egg info");
ctx.tValidate(Name, ctx.query);
const res = await ctx.model.Users.findOne({
where: {
id: 1
}
})
return {
res,
name
};
}
@HTTPMethod({
path: '/set',
method: HTTPMethodEnum.GET
})
async setKey(){
return {
data: 'ok'
}
}
}