Tìm kiếm trang web

node.js - Làm thế nào để đọc các thuộc tính trong typescript sau khi sử dụng Object.defineProperty?


Tôi có đoạn mã sau trên playground TypeScript và có một vài câu hỏi nảy sinh mà tôi không chắc cách để làm cho nó hoạt động

class PathInfo {
    functionName: string;
    httpPath: string;
    httpMethod: string;

    constructor(functionName: string, httpPath: string, httpMethod: string) {
        this.functionName = functionName;
        this.httpPath = httpPath;
        this.httpMethod = httpMethod;
    }

    toString(): string {
        return "PathInfo["+this.functionName+","+this.httpPath+","+this.httpMethod+"]";
    }
}

class AuthRequest {}
class AuthResponse {}
class LoginRequest {}
class LoginResponse {}

const path: any = (thePath: string, type: any) => {
    return (target: Function, memberName: string, propertyDescriptor: PropertyDescriptor) => {
        const pathMeta = new PathInfo(memberName, path, type);

        Object.defineProperty(target, memberName+'pathInfo', {
            value: pathMeta,
            writable: false
        });

        //How do I access the stored pathMeta
        //console.log("target="+target.pathInfo);
        console.log("member="+memberName);
        console.log("props="+propertyDescriptor);
    }
}

class AuthApiImpl {
    @path("/authenticate", AuthResponse)
    authenticate(request: AuthRequest): Promise<AuthResponse> {
        throw new Error("all this is generated by factory.createApiImpl");
    }
    @path("/login", LoginResponse)
    login(request: LoginRequest): Promise<LoginResponse> {
        throw new Error("all this is generated by factory.createApiImpl");
    }
};

function printMethods(obj: any) {
    console.log("starting to print methods");

  for (var id in obj) {
      console.log("id="+id);
    try {
      //How do I access the stored pathMeta here FOR EACH METHOD ->
      //console.log("target="+target.pathInfo);
      if (typeof(obj[id]) == "function") {
          console.log(id+":"+obj[id].toString());
      }
    } catch (err) {
      console.log(id + ": inaccessible"+err);
    }
  }

}

console.log("starting to run")

const temp = new AuthApiImpl();
printMethods(temp);

console.log("done")
  • dòng 64-65, làm thế nào để đọc thuộc tính mà tôi đã đặt
  • dòng 40-41, làm thế nào để đọc thuộc tính mà tôi đã đặt
  • dòng 58-74, tại sao dòng này không in ra bất kỳ hàm nào? Tôi muốn in tất cả các hàm và tôi KHÔNG muốn in các thuộc tính (chỉ các hàm)
  • dòng 33, Tôi có thể truy cập tên lớp tại thời điểm này không?
  • dòng 35, tôi nghĩ target là một hàm và sẽ được authorize, sau đó login, NHƯNG nếu tôi định nghĩa thuộc tính chỉ là 'pathInfo', tôi nhận được lỗi rằng thuộc tính đã được định nghĩa trên target (Điều này ngụ ý rằng target là lớp chứ không phải hàm?). Tôi rất bối rối.

Rất xin lỗi khi tôi cố gắng tập trung vào một câu hỏi duy nhất, nhưng bài kiểm tra về viết decorator này đã mang lại cho tôi nhiều câu hỏi hơn là câu trả lời khi tôi khám phá thế giới TypeScript.

Làm thế nào tôi có thể chỉnh sửa mã để chơi nhiều hơn ở đây?

Một mục tiêu ở đây là khi các nhà phát triển định nghĩa các API của các microservices khác, tôi có thể ghi lại một loạt thông tin meta và lưu trữ nó Ở ĐÂU ĐÓ để tôi có thể sử dụng sau này trong mã khởi động. Tôi thực sự không quan tâm lưu trữ ở đâu, nhưng chỉ cần một cách rõ ràng để biết lớp mà tôi muốn mở rộng, các phương thức, kiểu dữ liệu trả về, đường dẫn http, v.v.

Câu trả lời #1

Tôi muốn đọc dữ liệu meta này trong Node.js và sử dụng nó để tạo động một client thực hiện API. Về cơ bản, các nhà phát triển không bao giờ phải viết client và chỉ viết API, còn việc triển khai được tạo ra cho họ.

Nếu tôi làm điều đó, tôi có lẽ sẽ không sử dụng decorators, mà sẽ sử dụng mapped types:

// library code
interface ApiMethodInfo {
    httpPath: string;
    httpMethod: string;
}

type ApiInfo<S extends object> = Record<keyof S, ApiMethodInfo>;
type Client<S extends object> = {[key in keyof S]: S[key] extends (req: infer Req) => infer Res ? (req: Req) => Promise<Res> : never};

function generateClient<S extends object>(apiInfo: ApiInfo<S>): Client<S> {
    const client = {} as Client<S>;
    for (const key in apiInfo) {
        const info = apiInfo[key as keyof S];
        client[key] = ((param: any) => invokeApi(info, param)) as any;
    }
    return client;
}

// application code

interface AuthRequest {}
interface AuthResponse {}
interface LoginRequest {
    username: string,
    password: string,
}
interface LoginResponse {}

interface MyServer {
    authenticate(request: AuthRequest): AuthResponse;
    login(request: LoginRequest): LoginResponse;
}

const myApiInfo: ApiInfo<MyServer> = { // compiler verifies that all methods of MyServer are described here
    authenticate: {
        httpPath: '/authenticate',
        httpMethod: 'POST'
    },
    login: {
        httpPath: '/login',
        httpMethod: 'POST'
    }
}

const myClient = generateClient(myApiInfo); // compiler derives the method signatures from the server implementation
const username = "joe";
const password = "secret";
const response = myClient.login({username, password}); // ... and can therefore check that this call is properly typed

(Để hiểu cách các định nghĩa kiểu này hoạt động, bạn có thể muốn đọc phần Tạo Kiểu từ Kiểu trong Sổ tay TypeScript)

Điểm yếu của phương pháp này là mặc dù trình biên dịch có thể suy ra các chữ ký của client từ các chữ ký của server, nó sẽ không sao chép bất kỳ JSDoc nào, vì vậy các lập trình viên client không thể dễ dàng truy cập vào tài liệu API.

Trong đoạn mã trên, tôi đã chọn chỉ định siêu dữ liệu trong một đối tượng riêng thay vì sử dụng decorators để trình biên dịch có thể kiểm tra tính toàn diện (decorators luôn là tùy chọn; trình biên dịch không thể được chỉ dẫn để yêu cầu sự hiện diện của chúng), và bởi vì decorators là một tính năng ngôn ngữ thử nghiệm có thể vẫn thay đổi trong các phiên bản tương lai của ngôn ngữ.

Hoàn toàn có thể để điền vào một đối tượng metadata như vậy bằng cách sử dụng các decorator nếu đó là điều bạn ưa thích. Đây là cách mà điều đó sẽ trông như:

// library code
interface ApiMethodInfo {
    httpPath: string;
    httpMethod: string;
}

const apiMethodInfo = Symbol("apiMethodInfo");

function api(info: ApiMethodInfo) {
    return function (target: any, propertyKey: string) {
        target[apiMethodInfo] = target[apiMethodInfo] || {};
        target[apiMethodInfo][propertyKey] = info;
    }
}

type ApiInfo<S extends object> = Record<keyof S, ApiMethodInfo>;
type Client<S extends object> = {[key in keyof S]: S[key] extends (req: infer Req) => infer Res ? (req: Req) => Promise<Res> : never};

function invokeApi(info: ApiMethodInfo, param: any) {
    console.log(info, param);
}

function generateClient<S extends object>(serverClass: new() => S): Client<S> {
    const infos = serverClass.prototype[apiMethodInfo]; // a decorator's target is the constructor function's prototype
    const client = {} as Client<S>;
    for (const key in infos) { // won't encounter apiMethodInfo because Symbol properties are not enumerable
        const info = infos[key];
        client[key as keyof S] = ((param: any) => invokeApi(info, param)) as any;
    }
    return client;
}

// application code

interface AuthRequest {}
interface AuthResponse {}
interface LoginRequest {
    username: string,
    password: string,
}
interface LoginResponse {}

class MyServer {
    @api({
        httpPath: '/authenticate',
        httpMethod: 'POST'
    })
    authenticate(request: AuthRequest): AuthResponse {
        throw new Error("Not implemented yet");
    }

    @api({
        httpPath: '/login',
        httpMethod: 'POST'
    })
    login(request: LoginRequest): LoginResponse {
        throw new Error("Not implemented yet");
    }
}

const myClient = generateClient(MyServer); // compiler derives the method signatures from the server implementation
const username = "joe";
const password = "secret";
const response = myClient.login({username, password}); // ... and can therefore check that this call is properly typed

Hãy lưu ý cách sử dụng Symbol ngăn ngừa xung đột tên, và đảm bảo rằng các đoạn mã khác không nhận thấy thuộc tính này (trừ khi chúng tìm kiếm chính Symbol đó), và do đó không bị vướng phải bởi sự xuất hiện không mong đợi của nó.

Cũng hãy chú ý cách MyServer, khi chạy, chứa constructor của lớp, mà prototype của nó giữ các phương thức instance đã khai báo, và nó được truyền làm target cho bất kỳ decorator nào của chúng.

Lời khuyên chung

Tôi có thể kết thúc bằng một số lời khuyên cho lập trình viên Java đang hồi phục không? ;-)

EcmaScript không phải Java. Mặc dù cú pháp có thể trông giống nhau, EcmaScript có nhiều tính năng hữu ích mà Java không có, điều này thường cho phép viết mã ít hơn nhiều. Ví dụ, nếu bạn cần một DTO, hoàn toàn không cần thiết phải khai báo một lớp với một hàm khởi tạo sao chép từng tham số vào một thuộc tính. Bạn chỉ cần khai báo một giao diện, và tạo đối tượng bằng cách sử dụng literal đối tượng. Tôi khuyên bạn nên xem qua Hướng dẫn JavaScript Hiện đại để làm quen với những tính năng hữu ích của ngôn ngữ này.

Ngoài ra, một số tính năng hoạt động khác nhau trong EcmaScript. Cụ thể, sự phân biệt giữa class và interface khá khác biệt: Class dùng để kế thừa các phương thức từ một prototype, interface dùng để truyền dữ liệu. Thật phi lý khi khai báo một class cho Response sẽ được giải tuần tự từ JSON, vì các prototype không tồn tại sau khi tuần tự hóa.

Câu trả lời #2

Làm thế nào để lấy các phương thức của một lớp

Bạn vẫn không thể lấy tên phương thức ngay cả khi bạn gỡ bỏ decorator. Đây không phải là một câu hỏi riêng về TypeScript.

Bạn cần lấy các thuộc tính của prototype, không chỉ của chính đối tượng đó.

function printMethods(obj: any) {
    console.log("starting to print methods");

    const objProto = Object.getPrototypeOf(obj);
    console.log(Object.getOwnPropertyNames(objProto));
}

Cách truy cập tên lớp

Hiện tại đừng nghĩ rằng điều này có thể thực hiện với các decorator, nhưng nên khá đơn giản chỉ cần truyền tên lớp của bạn dưới dạng chuỗi.

Vấn đề tương tự: Trình trang trí lớp TypeScript lấy tên lớp

Mở vấn đề trên GitHub: https://github.com/microsoft/TypeScript/issues/1579

"thuộc tính đã được định nghĩa trên mục tiêu"

Lưu ý rằng nếu bạn chạy mã trên, bạn sẽ nhận được những điều sau trong console.log:

["constructor", "authenticate", "login", "authenticatepathInfo", "loginpathInfo"]

Tôi cũng muốn chỉ ra rằng nếu bạn thậm chí không khởi tạo một thể hiện của lớp, bạn vẫn sẽ nhận được cùng một lỗi.