Первый проект egg также является первым бэкенд-фреймворком для разработки egg+typescript с собственной архитектурой.
В 2019 году я постепенно перешел от фронтенда к бэкенду, на самом деле я не хотел переходить на бэкенд, но хотел начать с полного стека, потому что я думаю, что изучение большего количества технологий может привести к более широкому мышление. В начале компании back end был koa.После написания некоторое время я инкапсулировал версию koa+ decorator (автоматическая идентификация роутинга), а потом потихоньку подключался к egg+ts, так что у меня были вот такие леса.
адрес проекта
GitHub.com/CB-early-month/egg-…
Функции
- Поддержка маршрутизации с автоматической настройкой (на основе пути к каталогу и внедрения декоратора)
- Поддержка нескольких параметров для перехвата и фильтрации
- Генерация маршрутных и служебных файлов одним щелчком мыши
- Поддержка конфигурации модели с несколькими базами данных (автоматическое создание файла x.d.ts)
- Поддержка нескольких конфигурации Redis
- Поддавливание путей поддержки, отклонить долго
../../../
вводить
Структура каталогов проекта
egg-ts-base
├─.autod.conf.js
├─.dockerignore
├─.editorconfig
├─.travis.yml
├─Jenkinsfile // Jenkinsfile构建文件
├─README.md
├─appveyor.yml
├─package.json
├─plopfile.js // 用于生成文件
├─tsconfig.json
├─tshelper.js // 自动生成数据库对应model
├─tslint.json
├─example // 测试用的数据库
| ├─egg_test.sql
| └egg_test2.sql
├─docker // docker文件
| └Dockerfile.api.prod
├─dev-scripts // 开发用的脚本,主要用来生成文件
| ├─plop-templates
| | ├─util.js
| | ├─service // 生成service文件
| | | ├─index.hbs
| | | └prompt.js
| | ├─router // 生成路由文件
| | | ├─index.hbs
| | | └prompt.js
├─config // 项目配置文件
| ├─config.default.ts // 默认配置
| ├─config.local.ts // 本地配置
| ├─config.prod.ts // 生产环境配置
| └plugin.ts // 插件配置
├─app
| ├─router.ts // 入口
| ├─util // 存放一些工具
| | ├─error.ts // 统一管理错误信息
| | ├─redisKey.ts // 统一管理rediskey
| | └sessionKey.ts // 统一管理cookiekey
| ├─service // 存放service
| | ├─test
| | | └test.ts
| | ├─lib
| | | └Oss.ts
| ├─public
| ├─model // 数据库模型
| | ├─test2
| | | ├─Admin.ts
| | | └User.ts
| | ├─test
| | | └User.ts
| ├─middleware // 中间件
| | └response.ts
| ├─lib // 放一些第三方包之类的
| | ├─auth
| | | └authUser.ts
| | ├─aRouter // 路由识别的代码在这里面
| | | ├─index.ts
| | | └install.ts
| ├─extend // 扩展
| | ├─application.ts
| | ├─context.ts
| | └helper.ts
| ├─controller // 路由
| | ├─index.ts
| | ├─example
| | | ├─index.ts
| | | └test.ts
| ├─base // 存放一些基础类
| | ├─baseController.ts // 如果要能自动识别路由,需要继承该类
| | └baseService.ts
Автоматически определять и настраивать функцию маршрутизации
Здесь используется декоратор, если вы не разбираетесь в декораторе, то можете прочитать другую мою статью:Использование декораторов javascript
Файл находится по адресу:GitHub.com/CB-early-month/egg-…
В основном см.:
ARouter: // 入口,用户引入app以及做一些配置
AController: // 装饰器,使用这个的类才会自动配置路由
POST、GET、PUT、DEL、PATCH、ALL // 各种路由请求方法装饰器
ARouterHelper类 // 前面几个只是声明,这个类才是处理那些数据自动生成路由
Давайте сначала посмотрим на функцию ARouter:
/**
* 抛出hwrouter,在router.ts中直接使用ARouter(app);即可完成自动注入路由
* @param app application
* @param options 参数,目前只有prefix,就是所有路由的前缀
*/
export function ARouter(app: Application, options?: {prefix?: string}) {
const { router } = app; // 获取router
if (options && options.prefix) {
router.prefix(options.prefix); // 配置路由的前缀
}
aRouterHelper.injectRouter(router); // 注入路由
}
класс RouterHelper:
/**
* 路由注入
*/
class ARouterHelper {
/**
* 临时存放controller以及路由
*/
controllers: {
[key: string]: {
prefix?: string, // 前缀
target?: any, // 对应的class
routers: Array<{ // controller下的路由
handler: string, // 方法名
path: string, // 路由路径
method: RequestMethods // 请求方法
}>
}} = {};
/**
* 注入路由
* @param router egg的路由
*/
public injectRouter(router: Router) {
const keys = Object.keys(this.controllers);
keys.forEach(key => {
const controller = this.controllers[key];
controller.routers.forEach(r => {
// 以前的写法是router.get('/xxx', xxx, controller.xxx.xxx);
// 这里直接批量注入,controller.prefix + r.path拼接公共前缀于路由路径
router[r.method](controller.prefix + r.path, async (ctx: Context) => {
// 得到class实例
const instance = new controller.target(ctx);
// 获取class中使用的装饰器中间件
const middlewares = controller.target.prototype._middlewares;
if (middlewares) {
// all是绑定在class上的,也就是下面所有的方法都需先经过all中间件
const all = middlewares.all;
for (let i = 0; i < all.length; ++i) {
const func = all[i];
await func(ctx);
}
// 这是方法自带的中间件
const self = middlewares[r.handler] || [];
for (let i = 0; i < self.length; ++i) {
const func = self[i];
await func(ctx);
}
}
// 经过了所有中间件,最后才真正执行调用的方法
await instance[r.handler]();
});
});
});
}
}
Видно, что вышеуказанная инъекция настраивается через контроллеры в aRouterHelper, так откуда данные контроллеров? По сути, это сначала создать новый экземпляр ARouterHelper, а затем сохранить данные в контроллерах при реализации декораторов, таких как AController, GET и POST. Среди них AController используется в классе, указывая, что класс необходимо использовать в качестве маршрута, и такие методы, как GET, используются в методе класса, указывая, что этот метод необходимо использовать в качестве записи маршрутизации.
// 先创建一个helper实例
const aRouterHelper = new ARouterHelper();
/**
* controller装饰器
* @param prefix 前缀
*/
function AController (prefix: string) {
prefix = prefix ? prefix.replace(/\/+$/g, '') : '/';
if (prefix === '/') {
prefix = '';
}
return (target: any) => {
// 获取class名
const key = target.aRouterGetName(); // class继承自BaseController,里面就有提供aRouterGetName方法,用于获取类名
if (!aRouterHelper.controllers[key]) {
aRouterHelper.controllers[key] = {
target,
prefix,
routers: []
};
} else {
aRouterHelper.controllers[key].target = target; // target为使用装饰器的类。
aRouterHelper.controllers[key].prefix = prefix;
}
};
}
/**
* 路由装饰器
* @param path 路径
* @param method 请求方法(get,post等)
*/
function request(path: string, method: RequestMethods) {
// 装饰器作用于函数上,会多出几个参数
return function (target: any, value: any, des: PropertyDescriptor & ThisType<any>) {
const key = target.constructor.toString().split(' ')[1];
if (!aRouterHelper.controllers[key]) {
aRouterHelper.controllers[key] = {
routers: []
};
}
aRouterHelper.controllers[key].routers.push({
handler: value, // 所作用的方法名,暂存起来,用于后面执行所有中间件后调用
path, // 设置的路由路径
method // 请求方法(get、post等)
});
};
}
function POST (path: string) {
return request(path, RequestMethods.POST);
}
Идеи кода для автоматической генерации маршрутов перечислены выше.Как использовать промежуточное ПО после завершения автоматической генерации маршрута? Декоратор также используется здесь для временного хранения промежуточного программного обеспечения.Когда запрашивается соответствующий маршрут, промежуточное программное обеспечение выполняется первым, а затем целевой метод выполняется последним.Вы можете обратиться к реализации метода injectRouter выше. Итак, вопрос в том, откуда взялось промежуточное ПО? Здесь я написал метод установки, который можно использовать для привязки обработки, соответствующей декоратору, к промежуточному программному обеспечению.
Если декоратор воздействует на класс, он долженmiddlewares.allВыше это означает, что все маршруты в классе должны быть обработаны промежуточным программным обеспечением в первую очередь.Если декоратор воздействует на функцию в классе, она будет привязана к соответствующему классу, то естьmiddlewares[r.handler], r.handler — это имя функции.
установить реализацию:
/**
* 封装路由中间件装饰器注入,支持class和methods
*/
export default (target: any, value: any, des: PropertyDescriptor & ThisType<any> | undefined, fn: Function) => {
// 没有value,说明是作用于class
if (value === undefined) {
const middlewares = target.prototype._middlewares;
if (!middlewares) {
target.prototype._middlewares = { all: [ fn ] };
} else {
target.prototype._middlewares.all.push(fn);
}
} else {
const source = target.constructor.prototype;
if (!source._middlewares) {
source._middlewares = { all: [] };
}
const middlewares = source._middlewares;
if (middlewares[value]) {
middlewares[value].push(fn);
} else {
middlewares[value] = [ fn ];
}
}
};
установить использовать:
/**
* 用于过滤header参数,并挂在context的filterHeaders上
*/
function Headers (opt: {[key: string]: Function[]}) {
return (target: any, value?: any, des?: PropertyDescriptor & ThisType<any> | undefined) => {
return install(target, value, des, async (ctx: Context) => {
ctx.filterHeaders = getValue(ctx.headers, opt);
});
};
}
// 基本新增中间件都是这样的写法
// XXXXXX就是装饰器名称
function XXXXXX (opt: {[key: string]: Function[]}) {
return (target: any, value?: any, des?: PropertyDescriptor & ThisType<any> | undefined) => {
return install(target, value, des, async (ctx: Context) => {
// ...自己的处理
});
};
}
// 再来一个权限验证中间件例子
const auth = async (ctx: Context) => {
const authorizeHeader = ctx.get('Authorization');
if (!authorizeHeader) {
throw ctx.customError.USER.UNAUTHORIZED;
}
const token = authorizeHeader.split(' ').pop();
if (!token) {
throw ctx.customError.USER.UNAUTHORIZED;
}
ctx.jwtInfo = ctx.helper.jwtVerify(token, ctx.app.config.jwtSecret, ctx.customError.USER.UNAUTHORIZED);
if (!ctx.jwtInfo || !ctx.jwtInfo.userInfo) {
throw ctx.customError.USER.UNAUTHORIZED;
}
};
export default function (allowNull: boolean = false) {
return (target: any, value?: any, des?: PropertyDescriptor & ThisType<any> | undefined) => {
return install(target, value, des, async (ctx: Context) => {
try {
await auth(ctx); // 先进行权限验证,不通过会报错
} catch (e) {
if (allowNull) { // 如果运行验证不通过,则设置userInfo为null,并且不再报错(这种需求用于部分接口在有用户身份的时候和没身份的时候会返回不同的信息)
ctx.jwtInfo = {
userInfo: null
};
} else {
throw e;
}
}
});
};
}
// 使用
@GET('/auth')
@authUser() // 验证权限,失败直接抛错误
public async auth() {
const {
service: { test },
jwtInfo: { userInfo } // 权限验证成功,这里就有用户信息
} = this.ctx;
this.returnSuccess(await test.test.showData());
}
@GET('/auth2')
@authUser(true) // 验证权限,失败可跳过
public async auth2() {
const {
service: { test },
jwtInfo: { userInfo } // 验证成功有信息,否则为null
} = this.ctx;
this.returnSuccess(await test.test.showData());
}
Конфигурация модели с несколькими базами данных и функция распознавания ts
Конфигурация с несколькими базами данных уже поддерживается egg-sequelize-ts, но egg-ts-helper не будет генерировать соответствующий файл x.d.ts в соответствии с именем каталога и псевдонимом, установленным по умолчанию, поэтому его нельзя использовать правильно.
Здесь мы начнем с egg-ts-helper, К счастью, эта библиотека предоставляет функцию реализации генератора сама по себе и может генерировать соответствующие файлы x.d.ts в соответствии с вашими потребностями.
Я хочу реализовать:
// config文件:
config.sequelize = {
datasources: [
getSqlConfig({
delegate: 'model.testModel',
baseDir: 'model/test',
database: 'egg_test'
}),
getSqlConfig({
delegate: 'model.test2Model',
baseDir: 'model/test2',
database: 'egg_test2'
})
]
};
// service文件
async showData() {
const {
model: {
testModel,
test2Model,
}
} = this.ctx;
const [[ userTest ]] = await testModel.query('select * from `user`');
const admin = await test2Model.Admin.findByPk(1);
const userTest2 = await test2Model.User.findByPk(1);
return { userTest, admin, userTest2 };
}
Вы можете видеть, что выше настроены две модели, использующие базы данных egg_test и egg_test2 соответственно и висящие на context.model. Кстати, здесь также реализована привязка метода запроса к модели, перед вызовом этого метода будет сообщено об ошибке, так как ts не может его распознать. Для конкретной реализации добавьте файл tshelper.js в корневую директорию, подробности смотрите в официальной документации:GitHub.com/Я скучаю по топорам/яйцу…
// 把query挂在model上
const sequelizeModelContent = `
interface Model {
query(sql: string, options?: any): function;
}
`
function selfGenerator(config) {
if (!config.modelMap) {
throw 'modelMap must not be undefined'
}
const modelMap = {};
Object.entries(config.modelMap).forEach(item=> {
modelMap[item[0]] = {
name: item[1],
pathList: []
}
})
const modelList = config.fileList.map(item=> ({name: item.split('/')[0], path: item}));
for (let i = 0;i < modelList.length;++i) {
const map = modelMap[modelList[i].name];
if (!map) {
throw 'modelName must not be null';
}
map.pathList.push(modelList[i].path);
}
const importContent = [];
const modelInterface = [];
const modelContent = [];
Object.entries(modelMap).forEach(([k, v])=> {
const item = {name: v.name, contentList: []};
modelContent.push(item);
v.pathList.forEach(path=> {
const name = path[0].toUpperCase() + path.replace('/', '').slice(1, -3);
importContent.push(`import Export${name} from '../../../${config.directory}/${path.slice(0, -3)}'`);
item.contentList.push(` ${path.split('/')[1].slice(0, -3)}: ReturnType<typeof Export${name}>;`);
})
modelInterface.push(` ${v.name}: T_${v.name} & Model;`)
})
const content = `
${importContent.join('\n')}
${sequelizeModelContent}
declare module 'egg' {
interface Context {
model: {
${modelInterface.join('\n')}
}
}
${modelContent.map(item=> {
return ` interface T_${item.name} {
${item.contentList.join('\n')}
}`
}).join('\n')}
}
`;
return {
dist: config.dtsDir + '/index.d.ts',
content
}
}
module.exports = {
watchDirs: {
model: {
directory: 'app/model',
modelMap: {
test: 'testModel', // 每增加一个model目录,就在这里新增一个对应的映射关系,key为目录名,value为前面config里设置的对应的delegate
test2: 'test2Model'
},
generator: selfGenerator
}
}
}
Конфигурация с несколькими Redis
Плагин egg-redis изначально поддерживает несколько конфигураций Redis, но он не монтируется в приложении, поэтому он должен быть таким каждый раз, когда он используется:
(this.redis as Singleton<Redis>).get('test').xxx()
Вот способ смонтировать его в приложении и добавить файл расширения приложения.
import { Context, Singleton, Application } from 'egg';
import { Redis } from 'ioredis';
/**
* 扩展application
*/
export default {
get testRedis(this: Application) {
return (this.redis as Singleton<Redis>).get('test');
},
get test2Redis(this: Application) {
return (this.redis as Singleton<Redis>).get('test2');
}
};
// 这样就可以直接在service里这样使用:
const {
app: {
testRedis,
test2Redis
}
} = this.ctx;
const set1 = await testRedis.set('test', 1);
const set2 = await test2Redis.set('test', 2);
// 当然,还需要在config里配置:
config.redis = {
clients: {
test: getRedisConfig({ db: 13 }),
test2: getRedisConfig({ db: 14 })
}
};
Тут есть неприятный момент, то есть для добавления библиотеки redis нужно прописать приобретение соответствующей библиотеки в расширении приложения, а потом посмотреть модифицировать egg-redis, чтобы он автоматически монтировался на приложение.
псевдоним пути
В проектах мы обычно ссылаемся на файлы и часто сталкиваемся с длинными и длинными путями, такими как **../../../../xxx**, Мне это не очень нравится, потому что если вы скопируете в Для другого файла путь может измениться, и если вы не будете осторожны, будет сообщено об ошибке, поэтому исследование вводит псевдонимы пути, которые можно использовать следующим образом:
require('module-alias/register');
import BaseService from '@base/baseService';
// @base指向app/base目录。
Конфигурация, в первую очередь ts поддерживает распознавание псевдонима пути конфигурации, настроенного в tsconfig.json:
"compilerOptions": {
....
"baseUrl": ".", // 一定要设置,项目的路径
"paths": { // 这里配置相应的别名
"@base/*": ["./app/base/*"],
"@util/*": ["./app/util/*"],
"@lib/*": ["./app/lib/*"],
}
},
После настройки ts его может распознать только ts, но это бесполезно, в конце концов, у нас все еще работает js, и нам тоже нужно настроить js, чтобы он его распознал. Заимствуйте стороннюю библиотеку здесьmodule-alias
Настройте псевдонимы в package.json
"_moduleAliases": {
"@base": "./app/base",
"@lib": "./app/lib",
"@util": "./app/util"
},
В то же время добавьте предложение в файл, который должен использовать псевдоним
require('module-alias/register');
На этом этапе вы можете использовать псевдонимы путей и попрощаться с длинными путями.
Но здесь нужно каждый раз писать на одну строчку большеrequire('module-alias/register');, все еще довольно громоздко, я надеюсь, что более поздние версии ts могут автоматически преобразовывать путь в соответствии с настроенным псевдонимом во время компиляции, что идеально, ха-ха.
Однострочная команда для создания маршрутов и сервисов
Поскольку проект был переработан, каждый класс контроллера имеет много общих частей.
require('module-alias/register');
import BaseController from '@base/baseController'; // 引入baseController
import { AController, GET } from '@lib/aRouter'; // 引入aRouter
// 自动识别路径
const __CURCONTROLLER = __filename.substr(__filename.indexOf('/app/controller')).replace('/app/controller', '').split('.')[0].split('/').filter(item => item !== 'index').join('/').toLowerCase();
// 配置路由
@AController(__CURCONTROLLER)
export default class indexController extends BaseController {
}
Если вам нужно копировать его каждый раз, когда вы его создаете, это слишком хлопотно (я слишком ленив, ха-ха), поэтому я подумал об использовании команд для генерации файлов.
Первоначальной идеей было написать js-файл для его генерации, а потом я узнал о plop. Так что этот проект использует это. В каталоге проекта dev-script.
Маршрутизатор является шаблоном маршрутизации, то есть генерируется контроллер.
service — это шаблон службы.
Вам также необходимо добавить файл plopfile.js в корневой каталог.
const routerGenerator = require('./dev-scripts/plop-templates/router/prompt');
const serviceGenerator = require('./dev-scripts/plop-templates/service/prompt');
module.exports = function (plop) {
plop.setGenerator('router', routerGenerator);
plop.setGenerator('service', serviceGenerator);
};
Добавьте новую команду в package.json
"scripts": {
"new": "plop"
},
Таким образом, вы можете напрямую сгенерировать файл с помощью одной строки команды
// 在app/controller/test目录下新建一个index.ts文件
npm run new router test/index
Как использовать синтаксис plop здесь не рассматривается, вы можете проверить это онлайн.
Наконец
Эти леса использовались в нескольких проектах компании, и в настоящее время проблем нет.
Приветствуем ваш опыт и комментарии~.
Оригинальная ссылка:бэкэнд-проект сборки egg+typescript