Maison > interface Web > js tutoriel > Augmenter la vitesse et les performances avec la mise en cache avancée dans NestJS : comment utiliser les arborescences AVL et Redis

Augmenter la vitesse et les performances avec la mise en cache avancée dans NestJS : comment utiliser les arborescences AVL et Redis

DDD
Libérer: 2024-12-26 12:15:19
original
597 Les gens l'ont consulté

Boosting Speed and Performance with Advanced Caching in NestJS: How to Use AVL Trees and Redis

Dans le monde d'aujourd'hui, la rapidité et l'efficacité de la réponse aux demandes sont d'une importance primordiale pour les systèmes à grande échelle et à fort trafic. Les plateformes en ligne telles que les sites de commerce électronique, les réseaux sociaux et les services bancaires sont confrontées à un volume massif de données et de demandes des utilisateurs. Cette forte demande impose non seulement une charge importante sur les serveurs et les bases de données, mais peut également avoir un impact significatif sur l'expérience utilisateur. Dans ce contexte, la mise en œuvre d'un système de mise en cache peut être une solution efficace pour améliorer les performances et réduire la charge des ressources.

Dans cet article, nous explorons la mise en œuvre d'un système de mise en cache avancé qui combine les arborescences AVL et Redis. Ce système comprend des mécanismes de sécurité, une gestion TTL (Time to Live) et une intégration avec Redis pour améliorer les performances et la flexibilité. L'objectif est de tirer parti des avantages des deux technologies tout en atténuant leurs faiblesses.

Remarque importante : cet article a été développé avec l'aide de l'intelligence artificielle.


Avantages et inconvénients de la combinaison d'un système de mise en cache arborescente AVL avec Redis

Avantages :

  1. Efficacité de la mémoire améliorée :

    • Gestion intelligente du TTL : En utilisant une arborescence AVL pour gérer l'expiration des données, la consommation de mémoire peut être optimisée et la conservation des données obsolètes peut être évitée. Ceci est particulièrement utile dans les scénarios où les données changent rapidement et où une expiration précise est requise.
  2. Sécurité renforcée :

    • Validation des jetons : L'ajout d'un mécanisme de validation basé sur des jetons améliore la sécurité de Redis. Cette couche de sécurité supplémentaire empêche tout accès non autorisé au cache, renforçant ainsi la sécurité globale du système.
  3. Gestion TTL avancée :

    • Politiques d'expiration personnalisées : Les arborescences AVL permettent la mise en œuvre de politiques d'expiration plus complexes et personnalisées que Redis pourrait ne pas prendre en charge par défaut.
  4. Structures de données diverses :

    • Structure arborescente équilibrée : En tant que structure de données équilibrée, les arborescences AVL peuvent offrir de meilleures performances pour certains cas d'utilisation qui nécessitent des recherches et un tri rapides par rapport aux structures de données par défaut de Redis.
  5. Flexibilité et personnalisation accrues :

    • Une plus grande personnalisation : La combinaison des deux systèmes permet une personnalisation plus étendue, permettant le développement de solutions plus précises et spécifiques à l'application.

Inconvénients :

  1. Complexité architecturale accrue :

    • Gestion de deux systèmes de mise en cache : L'utilisation simultanée de Redis et d'un système de mise en cache arborescente AVL augmente la complexité architecturale et nécessite une gestion coordonnée entre les deux systèmes.
  2. Augmentation des frais généraux :

    • Latence supplémentaire : L'ajout d'une couche de mise en cache supplémentaire peut introduire des retards. Il est essentiel de s’assurer que les avantages en termes de performances l’emportent sur ces retards potentiels.
  3. Maintenance et synchronisation des données :

    • Cohérence des données : Le maintien de la cohérence et de la synchronisation entre Redis et l'arborescence AVL est crucial pour éviter les divergences de données, nécessitant des mécanismes de synchronisation complexes.
  4. Coûts de développement et de maintenance plus élevés :

    • Augmentation des dépenses : Le développement et la maintenance de deux systèmes de mise en cache nécessitent davantage de ressources et une expertise diversifiée, ce qui peut augmenter les coûts globaux du projet.
  5. Complexité de sécurité :

    • Coordonner les politiques de sécurité : Garantir que les politiques de sécurité sont mises en œuvre correctement et de manière cohérente dans les deux systèmes peut être un défi.

Implémentation du système de mise en cache à l'aide des arbres AVL et Redis

Ci-dessous, nous présentons la mise en œuvre professionnelle de ce système de mise en cache. Cette implémentation comprend une arborescence AVL pour gérer les données avec des capacités TTL et Redis pour un stockage rapide des données.

1. Arbre AVL avec TTL

Tout d'abord, nous implémentons l'arborescence AVL avec des capacités de gestion TTL.

// src/utils/avltree.ts

export class AVLNode {
  key: string;
  value: any;
  ttl: number; // Expiration time in milliseconds
  height: number;
  left: AVLNode | null;
  right: AVLNode | null;

  constructor(key: string, value: any, ttl: number) {
    this.key = key;
    this.value = value;
    this.ttl = Date.now() + ttl;
    this.height = 1;
    this.left = null;
    this.right = null;
  }

  isExpired(): boolean {
    return Date.now() > this.ttl;
  }
}

export class AVLTree {
  private root: AVLNode | null;

  constructor() {
    this.root = null;
  }

  private getHeight(node: AVLNode | null): number {
    return node ? node.height : 0;
  }

  private updateHeight(node: AVLNode): void {
    node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
  }

  private rotateRight(y: AVLNode): AVLNode {
    const x = y.left!;
    y.left = x.right;
    x.right = y;
    this.updateHeight(y);
    this.updateHeight(x);
    return x;
  }

  private rotateLeft(x: AVLNode): AVLNode {
    const y = x.right!;
    x.right = y.left;
    y.left = x;
    this.updateHeight(x);
    this.updateHeight(y);
    return y;
  }

  private getBalance(node: AVLNode): number {
    return node ? this.getHeight(node.left) - this.getHeight(node.right) : 0;
  }

  insert(key: string, value: any, ttl: number): void {
    this.root = this.insertNode(this.root, key, value, ttl);
  }

  private insertNode(node: AVLNode | null, key: string, value: any, ttl: number): AVLNode {
    if (!node) return new AVLNode(key, value, ttl);

    if (key < node.key) {
      node.left = this.insertNode(node.left, key, value, ttl);
    } else if (key > node.key) {
      node.right = this.insertNode(node.right, key, value, ttl);
    } else {
      node.value = value;
      node.ttl = Date.now() + ttl;
      return node;
    }

    this.updateHeight(node);
    const balance = this.getBalance(node);

    // Balancing the tree
    if (balance > 1 && key < node.left!.key) return this.rotateRight(node);
    if (balance < -1 && key > node.right!.key) return this.rotateLeft(node);
    if (balance > 1 && key > node.left!.key) {
      node.left = this.rotateLeft(node.left!);
      return this.rotateRight(node);
    }
    if (balance < -1 && key < node.right!.key) {
      node.right = this.rotateRight(node.right!);
      return this.rotateLeft(node);
    }

    return node;
  }

  search(key: string): any {
    let node = this.root;
    while (node) {
      if (node.isExpired()) {
        this.delete(key);
        return null;
      }
      if (key === node.key) return node.value;
      node = key < node.key ? node.left : node.right;
    }
    return null;
  }

  delete(key: string): void {
    this.root = this.deleteNode(this.root, key);
  }

  private deleteNode(node: AVLNode | null, key: string): AVLNode | null {
    if (!node) return null;

    if (key < node.key) {
      node.left = this.deleteNode(node.left, key);
    } else if (key > node.key) {
      node.right = this.deleteNode(node.right, key);
    } else {
      if (!node.left || !node.right) return node.left || node.right;
      let minLargerNode = node.right;
      while (minLargerNode.left) minLargerNode = minLargerNode.left;
      node.key = minLargerNode.key;
      node.value = minLargerNode.value;
      node.ttl = minLargerNode.ttl;
      node.right = this.deleteNode(node.right, minLargerNode.key);
    }

    this.updateHeight(node);
    const balance = this.getBalance(node);

    if (balance > 1 && this.getBalance(node.left!) >= 0) return this.rotateRight(node);
    if (balance < -1 && this.getBalance(node.right!) <= 0) return this.rotateLeft(node);
    if (balance > 1 && this.getBalance(node.left!) < 0) {
      node.left = this.rotateLeft(node.left!);
      return this.rotateRight(node);
    }
    if (balance < -1 && this.getBalance(node.right!) > 0) {
      node.right = this.rotateRight(node.right!);
      return this.rotateLeft(node);
    }

    return node;
  }
}
Copier après la connexion
Copier après la connexion

2. Service de cache (CacheService) avec intégration Redis

Dans cette section, nous implémentons le service de cache qui utilise à la fois l'arborescence AVL et Redis pour la gestion du cache. De plus, nous intégrons un mécanisme de validation de jeton.

// src/cache/cache.service.ts

import { Injectable, UnauthorizedException, InternalServerErrorException } from '@nestjs/common';
import { AVLTree } from '../utils/avltree';
import { InjectRedis, Redis } from '@nestjs-modules/ioredis';

@Injectable()
export class CacheService {
  private avlTree: AVLTree;
  private authorizedTokens: Set<string> = new Set(['your_authorized_token']); // Authorized tokens

  constructor(@InjectRedis() private readonly redis: Redis) {
    this.avlTree = new AVLTree();
  }

  validateToken(token: string): void {
    if (!this.authorizedTokens.has(token)) {
      throw new UnauthorizedException('Invalid access token');
    }
  }

  async set(key: string, value: any, ttl: number, token: string): Promise<void> {
    this.validateToken(token);
    try {
      // Store in Redis
      await this.redis.set(key, JSON.stringify(value), 'PX', ttl);
      // Store in AVL Tree
      this.avlTree.insert(key, value, ttl);
    } catch (error) {
      throw new InternalServerErrorException('Failed to set cache');
    }
  }

  async get(key: string, token: string): Promise<any> {
    this.validateToken(token);
    try {
      // First, attempt to retrieve from Redis
      const redisValue = await this.redis.get(key);
      if (redisValue) {
        return JSON.parse(redisValue);
      }

      // If not found in Redis, retrieve from AVL Tree
      const avlValue = this.avlTree.search(key);
      if (avlValue) {
        // Re-store in Redis for faster access next time
        // Assuming the remaining TTL is maintained in AVL Tree
        // For simplicity, we set a new TTL
        const newTtl = 60000; // 60 seconds as an example
        await this.redis.set(key, JSON.stringify(avlValue), 'PX', newTtl);
        return avlValue;
      }

      return null;
    } catch (error) {
      throw new InternalServerErrorException('Failed to get cache');
    }
  }

  async delete(key: string, token: string): Promise<void> {
    this.validateToken(token);
    try {
      // Remove from Redis
      await this.redis.del(key);
      // Remove from AVL Tree
      this.avlTree.delete(key);
    } catch (error) {
      throw new InternalServerErrorException('Failed to delete cache');
    }
  }
}
Copier après la connexion
Copier après la connexion

3. Contrôleur API (CacheController)

Le contrôleur gère les requêtes API vers le service de cache.

// src/cache/cache.controller.ts

import { Controller, Get, Post, Delete, Body, Param, Query, HttpCode, HttpStatus } from '@nestjs/common';
import { CacheService } from './cache.service';

class SetCacheDto {
  key: string;
  value: any;
  ttl: number; // milliseconds
  token: string;
}

@Controller('cache')
export class CacheController {
  constructor(private readonly cacheService: CacheService) {}

  @Post('set')
  @HttpCode(HttpStatus.CREATED)
  async setCache(@Body() body: SetCacheDto) {
    await this.cacheService.set(body.key, body.value, body.ttl, body.token);
    return { message: 'Data cached successfully' };
  }

  @Get('get/:key')
  async getCache(@Param('key') key: string, @Query('token') token: string) {
    const value = await this.cacheService.get(key, token);
    return value ? { value } : { message: 'Key not found or expired' };
  }

  @Delete('delete/:key')
  @HttpCode(HttpStatus.NO_CONTENT)
  async deleteCache(@Param('key') key: string, @Query('token') token: string) {
    await this.cacheService.delete(key, token);
    return { message: 'Key deleted successfully' };
  }
}
Copier après la connexion
Copier après la connexion

4. Module de cache (CacheModule)

Définit le module de cache qui connecte le service et le contrôleur et injecte Redis.

// src/cache/cache.module.ts

import { Module } from '@nestjs/common';
import { CacheService } from './cache.service';
import { CacheController } from './cache.controller';
import { RedisModule } from '@nestjs-modules/ioredis';

@Module({
  imports: [
    RedisModule.forRoot({
      config: {
        host: 'localhost',
        port: 6379,
        // Other Redis configurations
      },
    }),
  ],
  providers: [CacheService],
  controllers: [CacheController],
})
export class CacheModule {}
Copier après la connexion
Copier après la connexion

5. Configuration Redis

Pour utiliser Redis dans le projet NestJS, nous utilisons le package @nestjs-modules/ioredis. Tout d’abord, installez le package :

npm install @nestjs-modules/ioredis ioredis
Copier après la connexion
Copier après la connexion

Ensuite, configurez Redis dans le CacheModule comme indiqué ci-dessus. Si vous avez besoin de configurations plus avancées, vous pouvez utiliser des fichiers de configuration distincts.

6. Mécanisme de validation des jetons

Pour gérer et valider les jetons, diverses stratégies peuvent être utilisées. Dans cette implémentation simple, les jetons sont conservés dans un ensemble fixe. Pour les projets plus importants, il est recommandé d'utiliser JWT (JSON Web Tokens) ou d'autres méthodes de sécurité avancées.

7. Gestion des erreurs et validation des entrées

Dans cette implémentation, les classes DTO (Data Transfer Object) sont utilisées pour la validation des entrées et la gestion des erreurs. De plus, le service de cache utilise la gestion générale des erreurs pour éviter les problèmes inattendus.

8. Module d'application principal (AppModule)

Enfin, nous ajoutons le module de cache au module d'application principal.

// src/utils/avltree.ts

export class AVLNode {
  key: string;
  value: any;
  ttl: number; // Expiration time in milliseconds
  height: number;
  left: AVLNode | null;
  right: AVLNode | null;

  constructor(key: string, value: any, ttl: number) {
    this.key = key;
    this.value = value;
    this.ttl = Date.now() + ttl;
    this.height = 1;
    this.left = null;
    this.right = null;
  }

  isExpired(): boolean {
    return Date.now() > this.ttl;
  }
}

export class AVLTree {
  private root: AVLNode | null;

  constructor() {
    this.root = null;
  }

  private getHeight(node: AVLNode | null): number {
    return node ? node.height : 0;
  }

  private updateHeight(node: AVLNode): void {
    node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
  }

  private rotateRight(y: AVLNode): AVLNode {
    const x = y.left!;
    y.left = x.right;
    x.right = y;
    this.updateHeight(y);
    this.updateHeight(x);
    return x;
  }

  private rotateLeft(x: AVLNode): AVLNode {
    const y = x.right!;
    x.right = y.left;
    y.left = x;
    this.updateHeight(x);
    this.updateHeight(y);
    return y;
  }

  private getBalance(node: AVLNode): number {
    return node ? this.getHeight(node.left) - this.getHeight(node.right) : 0;
  }

  insert(key: string, value: any, ttl: number): void {
    this.root = this.insertNode(this.root, key, value, ttl);
  }

  private insertNode(node: AVLNode | null, key: string, value: any, ttl: number): AVLNode {
    if (!node) return new AVLNode(key, value, ttl);

    if (key < node.key) {
      node.left = this.insertNode(node.left, key, value, ttl);
    } else if (key > node.key) {
      node.right = this.insertNode(node.right, key, value, ttl);
    } else {
      node.value = value;
      node.ttl = Date.now() + ttl;
      return node;
    }

    this.updateHeight(node);
    const balance = this.getBalance(node);

    // Balancing the tree
    if (balance > 1 && key < node.left!.key) return this.rotateRight(node);
    if (balance < -1 && key > node.right!.key) return this.rotateLeft(node);
    if (balance > 1 && key > node.left!.key) {
      node.left = this.rotateLeft(node.left!);
      return this.rotateRight(node);
    }
    if (balance < -1 && key < node.right!.key) {
      node.right = this.rotateRight(node.right!);
      return this.rotateLeft(node);
    }

    return node;
  }

  search(key: string): any {
    let node = this.root;
    while (node) {
      if (node.isExpired()) {
        this.delete(key);
        return null;
      }
      if (key === node.key) return node.value;
      node = key < node.key ? node.left : node.right;
    }
    return null;
  }

  delete(key: string): void {
    this.root = this.deleteNode(this.root, key);
  }

  private deleteNode(node: AVLNode | null, key: string): AVLNode | null {
    if (!node) return null;

    if (key < node.key) {
      node.left = this.deleteNode(node.left, key);
    } else if (key > node.key) {
      node.right = this.deleteNode(node.right, key);
    } else {
      if (!node.left || !node.right) return node.left || node.right;
      let minLargerNode = node.right;
      while (minLargerNode.left) minLargerNode = minLargerNode.left;
      node.key = minLargerNode.key;
      node.value = minLargerNode.value;
      node.ttl = minLargerNode.ttl;
      node.right = this.deleteNode(node.right, minLargerNode.key);
    }

    this.updateHeight(node);
    const balance = this.getBalance(node);

    if (balance > 1 && this.getBalance(node.left!) >= 0) return this.rotateRight(node);
    if (balance < -1 && this.getBalance(node.right!) <= 0) return this.rotateLeft(node);
    if (balance > 1 && this.getBalance(node.left!) < 0) {
      node.left = this.rotateLeft(node.left!);
      return this.rotateRight(node);
    }
    if (balance < -1 && this.getBalance(node.right!) > 0) {
      node.right = this.rotateRight(node.right!);
      return this.rotateLeft(node);
    }

    return node;
  }
}
Copier après la connexion
Copier après la connexion

9. Fichier de candidature principal (main.ts)

Le fichier d'application principal qui démarre NestJS.

// src/cache/cache.service.ts

import { Injectable, UnauthorizedException, InternalServerErrorException } from '@nestjs/common';
import { AVLTree } from '../utils/avltree';
import { InjectRedis, Redis } from '@nestjs-modules/ioredis';

@Injectable()
export class CacheService {
  private avlTree: AVLTree;
  private authorizedTokens: Set<string> = new Set(['your_authorized_token']); // Authorized tokens

  constructor(@InjectRedis() private readonly redis: Redis) {
    this.avlTree = new AVLTree();
  }

  validateToken(token: string): void {
    if (!this.authorizedTokens.has(token)) {
      throw new UnauthorizedException('Invalid access token');
    }
  }

  async set(key: string, value: any, ttl: number, token: string): Promise<void> {
    this.validateToken(token);
    try {
      // Store in Redis
      await this.redis.set(key, JSON.stringify(value), 'PX', ttl);
      // Store in AVL Tree
      this.avlTree.insert(key, value, ttl);
    } catch (error) {
      throw new InternalServerErrorException('Failed to set cache');
    }
  }

  async get(key: string, token: string): Promise<any> {
    this.validateToken(token);
    try {
      // First, attempt to retrieve from Redis
      const redisValue = await this.redis.get(key);
      if (redisValue) {
        return JSON.parse(redisValue);
      }

      // If not found in Redis, retrieve from AVL Tree
      const avlValue = this.avlTree.search(key);
      if (avlValue) {
        // Re-store in Redis for faster access next time
        // Assuming the remaining TTL is maintained in AVL Tree
        // For simplicity, we set a new TTL
        const newTtl = 60000; // 60 seconds as an example
        await this.redis.set(key, JSON.stringify(avlValue), 'PX', newTtl);
        return avlValue;
      }

      return null;
    } catch (error) {
      throw new InternalServerErrorException('Failed to get cache');
    }
  }

  async delete(key: string, token: string): Promise<void> {
    this.validateToken(token);
    try {
      // Remove from Redis
      await this.redis.del(key);
      // Remove from AVL Tree
      this.avlTree.delete(key);
    } catch (error) {
      throw new InternalServerErrorException('Failed to delete cache');
    }
  }
}
Copier après la connexion
Copier après la connexion

10. Test et exécution de l'application

Après avoir implémenté tous les composants, vous pouvez exécuter l'application pour garantir sa fonctionnalité.

// src/cache/cache.controller.ts

import { Controller, Get, Post, Delete, Body, Param, Query, HttpCode, HttpStatus } from '@nestjs/common';
import { CacheService } from './cache.service';

class SetCacheDto {
  key: string;
  value: any;
  ttl: number; // milliseconds
  token: string;
}

@Controller('cache')
export class CacheController {
  constructor(private readonly cacheService: CacheService) {}

  @Post('set')
  @HttpCode(HttpStatus.CREATED)
  async setCache(@Body() body: SetCacheDto) {
    await this.cacheService.set(body.key, body.value, body.ttl, body.token);
    return { message: 'Data cached successfully' };
  }

  @Get('get/:key')
  async getCache(@Param('key') key: string, @Query('token') token: string) {
    const value = await this.cacheService.get(key, token);
    return value ? { value } : { message: 'Key not found or expired' };
  }

  @Delete('delete/:key')
  @HttpCode(HttpStatus.NO_CONTENT)
  async deleteCache(@Param('key') key: string, @Query('token') token: string) {
    await this.cacheService.delete(key, token);
    return { message: 'Key deleted successfully' };
  }
}
Copier après la connexion
Copier après la connexion

11. Exemples de demandes

Définir le cache :

// src/cache/cache.module.ts

import { Module } from '@nestjs/common';
import { CacheService } from './cache.service';
import { CacheController } from './cache.controller';
import { RedisModule } from '@nestjs-modules/ioredis';

@Module({
  imports: [
    RedisModule.forRoot({
      config: {
        host: 'localhost',
        port: 6379,
        // Other Redis configurations
      },
    }),
  ],
  providers: [CacheService],
  controllers: [CacheController],
})
export class CacheModule {}
Copier après la connexion
Copier après la connexion

Obtenir le cache :

npm install @nestjs-modules/ioredis ioredis
Copier après la connexion
Copier après la connexion

Supprimer le cache :

// src/app.module.ts

import { Module } from '@nestjs/common';
import { CacheModule } from './cache/cache.module';

@Module({
  imports: [CacheModule],
  controllers: [],
  providers: [],
})
export class AppModule {}
Copier après la connexion

Cas d'utilisation appropriés pour combiner les systèmes de mise en cache basés sur l'arborescence Redis et AVL

  1. Systèmes bancaires et financiers :

    • Gestion des sessions et des transactions sensibles : Une haute sécurité et une gestion précise du TTL sont essentielles pour les données financières sensibles. La combinaison de la sécurité des jetons et de la gestion intelligente du TTL est très bénéfique dans ce domaine.
  2. Plateformes de commerce électronique à fort trafic :

    • Stockage des données sur les produits et gestion des paniers : L'optimisation de la mémoire et l'augmentation de la vitesse d'accès aux données sont essentielles pour améliorer l'expérience utilisateur dans les grands magasins en ligne comme Amazon.
  3. Applications de messagerie et de réseaux sociaux :

    • Stockage des statuts des utilisateurs en temps réel : Un accès rapide et une gestion précise des données sont nécessaires pour afficher les statuts et les messages en ligne/hors ligne des utilisateurs.
  4. Applications météo et de change :

    • Mise en cache API pour réduire la charge des requêtes : Stockage des résultats de calculs complexes et des données en direct avec une gestion précise de l'expiration pour fournir des informations à jour et rapides aux utilisateurs.
  5. Systèmes de gestion de contenu et plateformes multimédias :

    • Mise en cache des pages et du contenu à fort trafic : Optimisation de l'accès au contenu très consulté et réduction de la charge du serveur pour offrir une expérience utilisateur plus fluide.
  6. Applications analytiques et tableaux de bord en temps réel :

    • Stockage des résultats d'analyse immédiats :Fournir des données analytiques rapides et à jour à l'aide de plusieurs caches pour améliorer les performances et la précision des résultats.

Conclusion

Dans cet article, nous avons implémenté un système de mise en cache avancé utilisant des arborescences AVL et Redis dans le framework NestJS. Ce système, offrant une gestion TTL avancée, une sécurité basée sur des jetons et une intégration Redis, fournit une solution robuste et flexible pour les applications à forte demande. La combinaison de ces deux technologies exploite les atouts des deux, corrigeant les faiblesses de Redis et améliorant les performances globales de mise en cache.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal