Cómo usar genéricos para escribir una función que solicite automáticamente métodos y parámetros de API

archivo

Este artículo se publicó por primera vez en: https://github.com/bigo-frontend/blog/ Bienvenido a seguirlo y volver a publicarlo.

Cómo usar genéricos para escribir una función que solicite automáticamente métodos y parámetros de API

Recientemente, estoy usando ts para desarrollar aplicaciones Vue y encontré el concepto de genéricos durante el proceso de desarrollo. Basado en la comprensión y la comprensión de los genéricos, tuve un capricho: si es posible utilizar las características de los genéricos
para implementar una función de solicitud automática de API, no solo recordará a otros desarrolladores en el mismo proyecto, sino que también ahorrará la necesidad de verificar. Esfuerzo de documentación; también puede incluir este conjunto de métodos en la plantilla de proyecto mecanografiado de nuestra empresa, que es conveniente para que otros colegas lo desarrollen y utilicen y mejoren la eficiencia de I + D de la empresa. Hagámoslo, hablemos de cómo hacerlo.
Primero tenemos que entender algunos conceptos.

genérico

Veamos primero un fragmento de código.

class Stack {
    
    
  private data = [];
  pop () {
    
    
    return this.data.pop()
  }
  push (item) {
    
    
    this.data.push(item)
  }
}
const stack = new Stack();
stack.push(1);
stack.push('string');
stack.pop();

Lo anterior es una implementación de JavaScript de una pila de primero en entrar, último en salir. Al llamar, los datos pueden ser de cualquier tipo. Pero cuando lo implementamos con mecanografiado, debemos pasar el tipo especificado, de la siguiente manera:

class Stack {
    
    
  private data:number = [];
  pop (): number {
    
    
    return this.data.pop();
  }
  push (item: number) {
    
    
    this.data.push(item);
  }
}
const stack = new Stack();
stack.push(1);
stack.push('string'); // Error: type error

En el código anterior, especificamos que los elementos que se insertan y extraen de la pila son de tipo número. Si insertamos un tipo que no sea numérico en la pila, el compilador mecanografiado informará un error. Sin embargo, una clase que implementamos a menudo se usa en más de un lugar y los tipos de elementos involucrados en ella también pueden ser diferentes. Entonces, ¿cómo podemos llamar a esta clase Stack en diferentes lugares en este momento, para que los elementos de datos internos puedan ser del tipo deseado? Mira el código a continuación

class Stack<T> {
    
    
  private data:T = [];
  pop (): T {
    
    
    return this.data.pop();
  }
  push (item: T) {
    
    
    this.data.push(item);
  }
}
const stack = new Stack<string>();
stack.push(1); // Error: type error

En el código anterior, agregamos corchetes angulares a la clase Stack y le pasamos una T. Esta T es un tipo genérico, lo que indica que nuestra clase puede pasar diferentes tipos al llamar. De manera similar a cómo las funciones pueden pasar parámetros, T en genéricos es un parámetro en la función, que puede considerarse como una variable de tipo. De esta forma, los genéricos nos dan la oportunidad de pasar tipos.

función genérica

Los genéricos se dividen en genéricos de interfaz, genéricos de clase y genéricos de funciones. El tipo genérico mencionado anteriormente significa que al definir una clase, el valor relacionado no especifica un tipo específico, pero usa una clase genérica para pasar un tipo específico cuando se usa, para definir de manera flexible su tipo. Por ejemplo, nuestra clase Promise común en mecanografiado es una clase genérica típica. Cuando se usa, se debe pasar un tipo para especificar el tipo de valor en la devolución de llamada de promesa. Una función genérica es una función que implementa una interfaz genérica. Veamos el siguiente código.

function getDataFromUrl<T>(url: sting): Promise<T> {
    
    
  return new Promise((resolve) => {
    
    
    setTimeout(() => {
    
    
      resolve(data); //
    });
  });
}

En este código, simulamos un método para pasar una URL para obtener datos. Este método devuelve una promesa y el valor de resolución de la promesa se especifica mediante el tipo genérico T. El método de escritura anterior se ve a menudo en nuestra solicitud ajax, porque el tipo de datos en el valor de respuesta devuelto por la solicitud asincrónica no es estático, sino que cambia según las diferentes interfaces.

restricciones genéricas

En 泛型函数el código de ahora, pasamos Teste tipo genérico, que se puede usar así

getDataFromUrl<number>('/userInfo').then(res => {
    
    
  constole.log(res);
})

En este punto, limitamos el tipo de datos en la respuesta a número. Por supuesto, también podemos especificarlo como cadena, matriz o cualquier otro tipo que cumpla con el estándar ts. ¿Qué pasa si queremos especificar el rango de T? Luego use restricciones genéricas, como

function getDataFromUrl<T extends keyof string|number>(url: sting): Promise<T> {
    
    
  return new Promise((resolve) => {
    
    
    setTimeout(() => {
    
    
      resolve(data); //
    });
  });
}

Usamos la palabra clave extends para limitar el alcance de T hacia stringy numberdentro. Al llamar, el valor de tipo de T solo puede ser estos dos tipos; de lo contrario, se informará un error.
Después de comprender los conceptos anteriores, podemos comenzar a realizar la operación automática. función que queremos Solicitar función ahora. Primero veamos cómo se ve la función que queremos lograr.

import api from '@/service';
private async getUserInfo () {
    
    
  const res = await api('getUserInfo', {
    
    uid: 'xxxxx'})
  // 省略若干代码
}

Lo que queremos lograr es que cuando ingresamos y llamamos a este método API anterior, podamos solicitar automáticamente getUserInfoel nombre de la interfaz y, al mismo tiempo, podemos establecer un límite en nuestros parámetros. Y nuestro servicio se ve así:
el objetivo es Claro, podemos bajar.

El primer paso es cómo habilitar el servicio para que solicite automáticamente el nombre del método o el nombre de la interfaz.

Definimos una interfaz que contiene los métodos de interfaz deseados:

interface Service {
    
    getUserInfo: (arg:any) => any}
const service: Service = {
    
    getUserInfo: (params) => {
    
    return get('/usrInfo', params)}}

El código anterior ha realizado el nombre del método de solicitud automática, pero no es suficiente. Nuestro objetivo no es solo solicitar automáticamente el nombre del método, sino también solicitar el tipo de parámetro que debe pasar el método correspondiente. Luego primero definimos los parámetros de diferentes métodos,
aquí creamos un archivo de declaración de tipo de parámetro llamado params.d.ts, que se usa para definir un módulo Params, que contiene diferentes interfaces o tipos de parámetros correspondientes a los métodos
params.d .ts;

export interface getUserInfoParams {
    
    
  name: string;
  uid: string;
}

export default interface Params {
    
    
    getUserInfo: getUserInfoParams
}

Bueno, creemos otro archivo llamado service.d.ts, que se usa para declarar nuestra clase de servicio, que contiene la interfaz correspondiente.

import {
    
     AxiosPromise } from "axios";
import Params from './params';
import {
    
     AnyObj } from "COMMON/interface/common";

interface ResponseValue {
    
    
    code: number;
    data: any;
    msg?: string;
}

type ServiceType = (params: AnyObj) => Promise<ResponseValue>;
export type Service = {
    
    
  getUserInfo: ServiceType
}

De esta manera, tenemos dos archivos y dos módulos grandes (tipo Servicio y Params), entonces, ¿cómo vinculamos el método en servicio con el tipo de parámetro del método correspondiente en params?
Podemos pensar en ello de esta manera: en primer lugar, el método en nuestro servicio, es decir, la clave debe ser la misma que la clave en los parámetros, ¿podemos definir la interfaz del servicio de esta manera?

interface Service extends Params {
    
    }

Obviamente, esto permite que la interfaz de Servicio tenga la misma clave que en params, pero esto no solo hereda la clave de params, sino que también hereda el tipo de clave. Sin embargo, lo que queremos es solo
la clave en params. El tipo de clave en Service es un método cuyo tipo de retorno es promesa, lo cual no cumple con nuestra intención original. Mencionamos
la clase de herramienta genérica de mecanografiado anteriormente, una de las cuales se llama Registro, y la función de este es convertir el tipo de clave del tipo T en otro tipo, y este otro tipo lo especifica el usuario.
Aquí, podemos aprovechar esta característica, no solo podemos obtener la clave correspondiente, sino también satisfacer lo mencionado anteriormente, el tipo de clave del Servicio es un método cuyo tipo de retorno es promesa.
De la siguiente manera podemos lograr

type Service = Record<keyof Params, ServiceType>

Aquí, extraemos la clave en Params y la pasamos a Record, y al mismo tiempo especificamos el tipo de clave como ServiceType, de modo que se implemente un tipo de Servicio, sus atributos son los mismos que los de Params y el tipo de atributo es ServiceType.
. Después de cambiarlo queda así

import {
    
     AxiosPromise } from "axios";
import Params from './params';
import {
    
     AnyObj } from "COMMON/interface/common";

interface ResponseValue {
    
    
    code: number;
    data: any;
    msg?: string;
}

type ServiceType = (params: AnyObj) => Promise<ResponseValue>;
export type Service = Record<keyof Params, ServiceType>

Hasta ahora, la interfaz del Servicio tiene una cierta relación con el tipo de Params, es decir, la clave que aparece en el Servicio debe aparecer en los Params; de lo contrario, la detección de tipo fallará, lo que garantiza que se agregue un método de interfaz cada vez durante el desarrollo
. , primero debe definir el tipo de parámetro del método en Params.

El segundo paso es solicitar automáticamente el tipo de parámetro de la interfaz al llamar al método de servicio.

Tenemos que encontrar una manera de asociar el tipo de parámetro del método con el nombre del método al llamar al método (es decir, la clave del servicio).
De hecho, existe una forma sencilla de lograr esta asociación, es decir, al definir el método del atributo en el servicio, definir directamente el tipo de parámetro del método correspondiente en el parámetro , pero esto no cumple con el propósito de nuestro uso. de genéricos. Dado que se utilizan genéricos, creemos que los genéricos tienen la característica de pasar parámetros. Si también podemos extraer el tipo de parámetro correspondiente al nombre del método de Params al llamar al método en el servicio, ¿podremos lograr nuestro objetivo? Primero definimos una función que puede pasar la clave en nuestro servicio como parámetro, llamar al método en el servicio y devolver el valor de retorno del método.

const api = (method) {
    
    return service[method]()};

Este método debe poder pasar parámetros al llamar al servicio, por lo que se convierte en el siguiente

const api = (method, parmas) {
    
    return service[method](params)};

De acuerdo con nuestro propósito anterior, establezca el tipo de parámetro de la función API en genérico y debemos restringir este parámetro genérico para que sea el nombre del método en la clase de Servicio. Según lo dicho anteriormente, las restricciones pueden utilizar la palabra clave extends.
Entonces alli esta

const api<T extends keyof Service> = (method: T, params: Params[T]){
    
    return service[method](parmas)};

De esta manera, podemos llamar al servicio a través de la API y obtener sugerencias sobre el nombre del método y el tipo de parámetro.
Hasta ahora, nuestra pequeña función de solicitar automáticamente el método y los parámetros de la API se ha realizado. Durante el proceso de desarrollo, siempre que se llame al método de la API, aparecerá automáticamente el método de API opcional. En proyectos complejos, los desarrolladores solo necesitan definir las interfaces correspondientes y los tipos de parámetros en los parámetros, y habrá mensajes cada vez que se llamen, lo que elimina la molestia de revisar constantemente los documentos de la interfaz y mejora en gran medida la eficiencia del desarrollo. Por supuesto, esta pequeña función también puede agregar una función para solicitar automáticamente los datos de respuesta, que no se mencionará aquí, y lo dejaré para que todos piensen en ello.
Código completo:
params.d.ts

export interface getUserInfoParams {
    
    }

export default interface Params {
    
    
    getUserInfo: getUserInfoParams
}

servicio.d.ts:

import {
    
     AxiosPromise } from "axios";
import Params from './params';
import API from './api';
import {
    
     AnyObj } from "COMMON/interface/common";

interface ResponseValue {
    
    
    code: number;
    data: any;
    msg?: string;
}

type ServiceType = (params: AnyObj) => Promise<ResponseValue>;
export type Service = Record<keyof Params, ServiceType>

servicio/index.ts:

import {
    
     get, post } from 'COMMON/xhr-axios';
import {
    
    Service} from './types/service';
import Params from './types/params';

const service:Service = {
    
    
    getUserInfo: (params) => {
    
    
        return get('/usrinfo', params);
    }
}


const api = <FN extends keyof Params>(fn: FN, params: Params[FN]) => {
    
    
    return service[fn](params)
}

// 用法
// import api from '@/service/index'
// api('getUserInfo', {})

export default api;

usar:

private async getUserInfo () {
    
    
  const res = await api('getUserInfo', {
    
    uid: 'xxxxx'})
  // 省略若干代码
}

Bienvenidos a todos a dejar un mensaje para discutir. ¡Les deseo un buen trabajo y una vida feliz!

Soy el front-end de bigo, nos vemos en el próximo número.

Supongo que te gusta

Origin blog.csdn.net/yeyeye0525/article/details/121329384
Recomendado
Clasificación