Как сделать декораторы методов экземпляром объекта в Typescript

Я разрабатываю лямбда-проект, и мы используем пакет 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.

Используете ли вы устаревшие декораторы или черновые декораторы?

Dimava 31.03.2023 16:17

@Thomas Идея декораторов состоит в том, чтобы настраивать маршруты API, а не выполнять их. Он не выполняется, я вижу журналы. Он выполняется, когда api.run сталкивается с лямбда-обработчиком, и, используя аргумент события, он знает, какой маршрут он будет выполнять. Асинхронность будет добавлена ​​в будущем — пока просто игнорируйте.

Felipe Oriani 31.03.2023 16:56

@Dimava Я не уверен, я просто включил ExperimentDecorators в своих скриптах и ​​сделал этот код.

Felipe Oriani 31.03.2023 17:00

@FelipeOriani, какая у тебя версия машинописного текста? Последняя версия 5.0?

Dimava 31.03.2023 17:17

У вас есть DI-контейнер или как вы хотите создать экземпляр этого класса и внедрить зависимости?

Thomas 31.03.2023 17:30

Нет, в данный момент у меня его нет, я просто хочу убедиться, что конструктор будет выполнен перед вызовом метода. Я постараюсь подготовить рабочий пример и опубликовать ссылку.

Felipe Oriani 31.03.2023 17:40
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
6
111
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вам нужно сначала сохранить метаданные о том, как связывать маршруты, а затем применить их при создании класса.

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-контейнер вы порекомендуете в этом случае?

Felipe Oriani 31.03.2023 20:34

@FelipeOriani Я не использую их в JS/TS. Но я знаком с этими шаблонами/конструкциями из .NET; контроллер, DI в конструкторе, атрибуты маршрута. Я просто предположил, что это то, с чем вы знакомы, это то, что вы пытаетесь воспроизвести в node. имо. Я не вижу преимущества @Get("path") someFunc(rec, res) { ... } перед api.get("path", (rec, res) => { ... });, и если вы действительно хотите ioc, это может быть так же просто, как общий Map<T, ()=>T>.

Thomas 31.03.2023 21:10

То же самое, я 20-летний разработчик C#, работающий с TS с прошлого месяца. Я пытаюсь упростить использование и тестирование с помощью декораторов get/post и экспортировать класс Api. Тогда я мог бы настроить lambda-api и протестировать его через DI. Спасибо за ответ @Thomas, я проведу несколько экспериментов.

Felipe Oriani 31.03.2023 23:33

Это сработало, как я и ожидал, большое спасибо. Я не уверен, что это хорошая практика для лямбда-проекта в nodejs, но он работает аналогично asp.net. Для каждого запроса у меня есть новый экземпляр моего класса ProductApi, разрешающий все зависимости. Теперь мне просто нужен DI-контейнер, но я пока не уверен, что буду его использовать. Спасибо @Томас.

Felipe Oriani 01.04.2023 19:09

А также спасибо за объяснение прототипа, теперь я понимаю, почему он не работал раньше.

Felipe Oriani 01.04.2023 19:14

Другие вопросы по теме