英文:
How make method decorators instance the object in Typescript
问题
I have been developing a lambda project and we are using lambda-api package. Then I have defined some decorators called Get
and Post
to map a route into the lambda API object. With these decorators, I have defined a class called ProductApi
to hold methods that can be configured using those decorators and passing a route path. It works fine.
The problem is that when I have a class like ProductApi
, the constructor is never called, and if I want to add some dependencies (like a Service or a Repository), they will never be defined. In this example, the /health
route works fine because it does not use anything from the object instance, but other routes do not.
How can I make sure that the constructor will be called and define the service instance?
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 healthCheck(req: Request, res: Response) {
console.log(`Executing -- GET /health`);
// this.someValue does not exist here
return res.status(200).json({ ok: true });
}
@Get('/products')
async getProducts(req: Request, res: Response) {
console.log(`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.log(`Executing -- POST /products`);
const product = this.productService.saveProduct('Drums', 1200); // error: Cannot read properties of undefined (reading 'saveProduct')
return res.status(201).json(product);
}
}
export const lambdaHandler = async (event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> => {
console.log('SCOPE lambda');
return await api.run(event, context);
};
Note: I don't want to use frameworks; I just want an easy way to configure routes on the lambda API instance.
英文:
I have been developing a lambda project and we are using lambda-api package. Then I have defined some decorators called Get
and Post
to map a route into lambda api object. With these decorators I have defined a class called ProductApi to hold methods that can be configured using those decorators and passing a route path. It works fine.
The problem is that when I have a class like ProductApi
the constructor is never called and if I want to add some dependencies (like a Service or a Repository) it will never be defined. In this example, the /health
route works fine because it does not use anything from the object instance, but other routes does not.
How can I make sure that the constructor will be called and define the service instance?
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.log(`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.log(`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.log(`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.log('SCOPE lambda');
return await api.run(event, context);
};
Note: I don't want to use frameworks, I just want a easy way to configure routes on the lamda api instanmce.
答案1
得分: 1
你需要首先存储有关如何绑定路由的元数据,然后在类创建时应用它。
https://tsplay.dev/mbvA4m(可运行)
import type { Request, Response } from 'lambda-api'
type LambdifiedProto = {
'_lambdifiedGetters'?: Record< /* 路径 */ string, /* 属性键 */ 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')) {
// 创建或从原型克隆
lproto._lambdifiedGetters = { ...lproto._lambdifiedGetters }
}
lproto._lambdifiedGetters[path] = propertyKey
console.log(`在路径 ${path} 上注册 getter ${propertyKey}`)
}
}
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.log('注册 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.log(`执行 -- GET /health`);
// 这里不存在this.someValue
return res.status(200).json({ ok: true });
}
}
console.log('...创建开始...')
new ProductApi()
(注意:代码中可能存在一些拼写错误,需要根据需要进行修复。)
英文:
You need to first store metadata about how to bind routes, and then apply that on class creation
https://tsplay.dev/mbvA4m (runnable)
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.log(`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.log('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.log(`Executing -- GET /health`);
// this.someValue does not exists here
return res.status(200).json({ ok: true });
}
}
console.log('...create start...')
new ProductApi()
答案2
得分: 1
不同于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("你需要将这个装饰器与一个方法一起使用。");
}
const Class = target.constructor;
api.get(path, (req: Request, res: Response) => {
const instance = diContainer.getInstance(Class); // 或者 new Class();
return instance[methodName](req, res);
});
};
}
附注:Dimava提出了这个话题;这些是旧的装饰器。TS在JS有装饰器规范之前就已经采用了它们。现在已经有了一个规范,并且与这些旧装饰器有很大不同,TS终于在V5中实现了该规范。你(和我)应该了解新的语法并采用它,因为这个语法可能很快就会被弃用。
英文:
Unlike in C#, in JS a "method" is just a function stuck to an object. You can easily put it in a variable or stick it to another object. This basically defines what this
is inside that "method".
And a "class" constructor is just a function that creates a new object and tells it, "if someone's looking for some property that you don't have, forward them to my prototype
object over here." Then it executes the code inside the constructor with that object as this
.
That is JS' prototypal inheritance in a nutshell, and even if JS has received a class
keyword in the meantime, that's what still happens behind the scenes.
Why am I explaining this?
Because decorator are working on that prototype object. This line here api.get(path, descriptor.value.bind(target));
takes the method from that prototype, permanently binds the prototype object as this
(so the resulting function will only know the prototype object and never ever see any real instance) and uses the bound function as a callback for that route.
So currently, even if that class would magically be instantiated (by whom; I don't know) the function that you've passed to the route will have no knowledge of that.
imo. Your decorator should look more like 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);
});
};
}
Sidenote: Dimava brought up this topic; these are legacy decorators. TS adapted them long before there was a spec for decorator in JS. Now there is one and it significantly differs from these legacy decorators and TS has finally implemented the spec in V5. You (and me) should get updated on the new syntax and adopt it, because this syntax will probably be deprecated pretty soon.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论