Я разрабатываю лямбда-проект, и мы используем пакет lambda-api. Затем я определил несколько декораторов, называемых Get
и Post
, чтобы сопоставить маршрут с объектом лямбда-апи. С помощью этих декораторов я определил класс ProductApi для хранения методов, которые можно настроить с помощью этих декораторов и передачи пути маршрута. Это работает нормально.
Проблема в том, что когда у меня есть такой класс, как ProductApi
, конструктор никогда не вызывается, и если я хочу добавить некоторые зависимости (например, службу или репозиторий), он никогда не будет определен. В этом примере маршрут /health
работает нормально, потому что он ничего не использует из экземпляра объекта, а другие маршруты — нет.
Как я могу убедиться, что конструктор будет вызываться и определять экземпляр службы?
const api = createAPI();
function Get(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
api.get(path, descriptor.value.bind(target));
};
}
function Post(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
api.post(path, descriptor.value.bind(target));
};
}
class ProductApi {
private someValue: string;
constructor(private readonly productService: IProductService = new ProductService()) {
// this scope does not execute
this.someValue = "some value";
}
@Get('/health')
async healthCheckr(req: Request, res: Response) {
console.info(`Executing -- GET /health`);
// this.someValue does not exists here
return res.status(200).json({ ok: true });
}
@Get('/products')
async getProducts(req: Request, res: Response) {
console.info(`Executing -- GET /products`);
const data = this.productService.getProductsFromService(); // error: Cannot read properties of undefined (reading 'getProductsFromService')
return res.status(200).json(data);
}
@Post('/products')
async postProducts(req: Request, res: Response) {
console.info(`Executing -- POST /products`);
const product = this.productService.saveProduct('Drums', 1200); // erro: Cannot read properties of undefined (reading 'saveProduct')
return res.status(201).json(product);
}
}
export const lambdaHandler = async (event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> => {
console.info('SCOPE lambda');
return await api.run(event, context);
};
Примечание. Я не хочу использовать фреймворки, мне просто нужен простой способ настройки маршрутов в экземпляре лямбда API.
@Thomas Идея декораторов состоит в том, чтобы настраивать маршруты API, а не выполнять их. Он не выполняется, я вижу журналы. Он выполняется, когда api.run сталкивается с лямбда-обработчиком, и, используя аргумент события, он знает, какой маршрут он будет выполнять. Асинхронность будет добавлена в будущем — пока просто игнорируйте.
@Dimava Я не уверен, я просто включил ExperimentDecorators в своих скриптах и сделал этот код.
@FelipeOriani, какая у тебя версия машинописного текста? Последняя версия 5.0?
У вас есть DI-контейнер или как вы хотите создать экземпляр этого класса и внедрить зависимости?
Нет, в данный момент у меня его нет, я просто хочу убедиться, что конструктор будет выполнен перед вызовом метода. Я постараюсь подготовить рабочий пример и опубликовать ссылку.
Вам нужно сначала сохранить метаданные о том, как связывать маршруты, а затем применить их при создании класса.
https://tsplay.dev/mbvA4m (работает)
import type { Request, Response } from 'lambda-api'
type LambdifiedProto = {
'_lambdifiedGetters'?: Record< /* path */ string, /* propertyKey */ string>
}
function Get(path: string) {
return function <K extends string, T extends (req: Request, res: Response) => Promise<any>>(
proto: Record<K, T>, propertyKey: K, descriptor: TypedPropertyDescriptor<T>
): void {
let lproto = proto as LambdifiedProto;
if (!Object.hasOwn(lproto, '_lambdifiedGetters')) {
// create or clone from protoproto
lproto._lambdifiedGetters = { ...lproto._lambdifiedGetters }
}
lproto._lambdifiedGetters![path] = propertyKey
console.info(`registered getter ${propertyKey} on path ${path}`)
}
}
function Lambda<C extends new (...a: any[]) => any>(klass: C) {
return class Lambdified extends klass {
constructor(...a: any[]) {
super(...a);
let getters = (klass.prototype as Lambdified)._lambdifiedGetters
for (let [path, propertyKey] of Object.entries(getters)) {
console.info('register api: ', { path, propertyKey, this: this })
// api.register(path, (q, s) => this[propertyKey](q, s))
}
}
}
}
@Lambda
class ProductApi {
me = 'ProductApi'
@Get('./health')
@Get('./v1/health')
async healthCheckr(req: Request, res: Response) {
console.info(`Executing -- GET /health`);
// this.someValue does not exists here
return res.status(200).json({ ok: true });
}
}
console.info('...create start...')
new ProductApi()
В отличие от C#, в JS «метод» — это просто функция, привязанная к объекту. Вы можете легко поместить его в переменную или привязать к другому объекту. Это в основном определяет, что this
находится внутри этого «метода».
А конструктор «класса» — это просто функция, которая создает новый объект и сообщает ему: «Если кто-то ищет какое-то свойство, которого у вас нет, перенаправьте его на мой prototype
объект здесь». Затем он выполняет код внутри конструктора с этим объектом как this
.
В двух словах, это прототипное наследование JS, и даже если JS тем временем получил ключевое слово class
, это то, что все еще происходит за кулисами.
Почему я это объясняю?
Потому что декоратор работает над этим объектом-прототипом. Эта строка здесь api.get(path, descriptor.value.bind(target));
берет метод из этого прототипа, постоянно связывает объект-прототип как this
(поэтому результирующая функция будет знать только объект-прототип и никогда не увидит реальный экземпляр) и использует связанную функцию в качестве обратного вызова для этого маршрута.
Итак, в настоящее время, даже если этот класс будет волшебным образом создан (кем; я не знаю), функция, которую вы передали маршруту, не будет знать об этом.
имо. Ваш декоратор должен выглядеть примерно так:
function Get(path: string) {
return function (target: any, methodName: string) {
if (typeof target[methodName] !== "function"){
throw new Error("you need to use this decorator with a method.");
}
const Class = target.constructor;
api.get(path, (req: Request, res: Response) => {
const instance = diContainer.getInstance(Class); // or new Class();
return instance[methodName](req, res);
});
};
}
Примечание: Димава поднял эту тему; это унаследованные декораторы. TS адаптировал их задолго до того, как в JS появилась спецификация декоратора. Теперь есть один, и он значительно отличается от этих устаревших декораторов, и TS наконец-то реализовала спецификацию в V5. Вы (и я) должны быть в курсе нового синтаксиса и принять его, потому что этот синтаксис, вероятно, скоро устареет.
Спасибо за ответ @Томас. Какой DI-контейнер вы порекомендуете в этом случае?
@FelipeOriani Я не использую их в JS/TS. Но я знаком с этими шаблонами/конструкциями из .NET; контроллер, DI в конструкторе, атрибуты маршрута. Я просто предположил, что это то, с чем вы знакомы, это то, что вы пытаетесь воспроизвести в node. имо. Я не вижу преимущества @Get("path") someFunc(rec, res) { ... }
перед api.get("path", (rec, res) => { ... });
, и если вы действительно хотите ioc, это может быть так же просто, как общий Map<T, ()=>T>
.
То же самое, я 20-летний разработчик C#, работающий с TS с прошлого месяца. Я пытаюсь упростить использование и тестирование с помощью декораторов get/post и экспортировать класс Api. Тогда я мог бы настроить lambda-api и протестировать его через DI. Спасибо за ответ @Thomas, я проведу несколько экспериментов.
Это сработало, как я и ожидал, большое спасибо. Я не уверен, что это хорошая практика для лямбда-проекта в nodejs, но он работает аналогично asp.net. Для каждого запроса у меня есть новый экземпляр моего класса ProductApi
, разрешающий все зависимости. Теперь мне просто нужен DI-контейнер, но я пока не уверен, что буду его использовать. Спасибо @Томас.
А также спасибо за объяснение прототипа, теперь я понимаю, почему он не работал раньше.
Используете ли вы устаревшие декораторы или черновые декораторы?