La vérité sur la gestion des dépendances - Explorer les gestionnaires de packages frontaux

ad23d22c10dc451488e6517d32cb4cb6.png

Dachang Technology Nœud Frontal Avancé Avancé

Cliquez sur le guide de croissance du meilleur programmeur, faites attention au numéro public

Répondre 1, rejoignez le groupe d'échange avancé Node

avant-propos

npm est un outil de gestion de packages pour Node.JS. De plus, la communauté dispose d'outils de gestion de packages similaires tels que yarn, pnpm et cnpm, ainsi que tnpm utilisés au sein du groupe. Dans le processus de développement de projet, nous utilisons généralement les gestionnaires de packages traditionnels ci-dessus pour générer le répertoire node_modules afin d'installer les dépendances et d'effectuer la gestion des dépendances. Cet article explore principalement le principe de gestion des dépendances du gestionnaire de packages frontal, dans l'espoir d'aider les lecteurs.

au dessus du niveau de la mer

Lorsque nous exécutons la npm installcommande, npm nous aide à télécharger le package de dépendance correspondant et à l'extraire dans le cache local, puis à construire la structure de répertoire node_modules et à écrire le fichier de dépendance. Alors, quelle est la structure du package correspondant dans le répertoire node_modules ? npm a principalement subi les modifications suivantes.

1. Imbrication de dépendances npm v1/v2

Les premières versions de npm utilisaient un modèle imbriqué très simple pour la gestion des dépendances. Par exemple, nous dépendons du module A et du module C dans le projet, et le module A et le module C dépendent de différentes versions du module B. Le répertoire node_modules généré est le suivant :
2c06aa89c0ad95a20e0c91080d12cc0d.png6ff0cb7890bc92d2efad943abcc09924.png

L'enfer des dépendances

On peut voir qu'il s'agit d'une structure node_modules imbriquée Il y aura également un répertoire node_modules sous les dépendances de chaque module pour stocker les dépendances des dépendances du module. Bien que cette méthode soit simple et claire, il y a quelques gros problèmes. Si nous ajoutons un module D qui dépend également de la version 2.0 B dans le projet, le répertoire node_modules généré sera le suivant. Bien que les modules A et D dépendent de la même version B, B a été téléchargé et installé deux fois, ce qui entraîne un gaspillage d'espace répété. C'est le problème de l'enfer de la dépendance.

node_modules
├── [email protected]
│   └── node_modules
│       └── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
└── [email protected]
    └── node_modules
        └── [email protected]

Quelques mèmes célèbres :
e081d74d391647f11bc89dda4ca63968.png141c5cf3b29cbab93542d1c1387c10fb.png

2. aplatissement npm v3

npm v3 termine la réécriture du programme d'installation des dépendances. npm3 installe les sous-dépendances dans le même répertoire que la dépendance principale de manière aplatie (promotion de levage) pour réduire l'arborescence profonde et la redondance causées par l'imbrication des dépendances. Le répertoire node_modules généré à ce moment est le suivant :
83f1383082d28131c1b2012a9e77871f.png7a44902babd34f820ec893184d2ee6a1.png
Afin d'assurer le chargement correct des modules, npm implémente également un algorithme de recherche de dépendances supplémentaire, le cœur étant de rechercher récursivement node_modules vers le haut. Lors de l'installation d'un nouveau package, il continuera à rechercher les node_modules supérieurs. Si un paquet de la même version est trouvé, il ne sera pas réinstallé. Lorsqu'un conflit de version est rencontré, la sous-dépendance du module sera stockée dans le répertoire node_modules sous le module, ce qui résout le problème d'installation répétée d'un grand nombre de packages, et le niveau de dépendances ne sera pas trop profond. .

Le modèle plat résout le problème de l'enfer des dépendances, mais introduit également de nouveaux problèmes supplémentaires.

Dépendance fantôme

Les dépendances Spectre se produisent principalement lorsqu'un package n'est pas défini dans package.json, mais peut toujours être référencé dans le projet. Considérez le cas précédent, dont package.json est affiché à droite.


0bc7f8b27fec9f5ba3ebc6d75612f51a.pngb8521494565a9f9cc4b46beb8f08761f.png

Dans index.js, nous pouvons exiger directement A, car la dépendance est déclarée dans package.json, mais notre demande B peut également fonctionner correctement.

var A = require('A');
var B = require('B'); // ???

Étant donné que B est une dépendance de A, pendant le processus d'installation, npm placera la dépendance B sous node_modules, afin que la fonction require puisse la trouver. Mais cela peut entraîner des problèmes inattendus :

  • Incompatibilité de dépendance : La bibliothèque my-library ne déclare pas la version qui dépend de B, donc la mise à jour majeure de B est tout à fait légale pour le système SemVer, ce qui amène les autres utilisateurs à télécharger une version incompatible avec la dépendance actuelle lors de l'installation.

  • Dépendances manquantes : nous pouvons également nous référer directement aux sous-dépendances de devDepdency dans le projet, mais les autres utilisateurs n'installeront pas devDepdency, ce qui peut entraîner le signalement immédiat d'une erreur lors de l'exécution.

Dépendances multiples (doppelgangers)

9a0e2db9e42111ccbbe4fcccbff18d77.png

Considérant que le module D qui dépend de la version 2.0 B et le module E qui dépend de la version 1.0 B continuent d'être introduits dans le projet, peu importe que B 2.0 ou 1.0 soit promu au niveau supérieur, cela causera des problèmes de doublons dans un autre version, comme la duplication ici 2.0. À ce stade, les problèmes suivants existeront :

  • Destruction du mode singleton : Un objet singleton exporté dans le module B est introduit dans les modules C et D. Même si le code semble charger la même version du même module, c'est en fait un module différent qui est analysé et chargé, et l'objet importé ceux-ci sont également différents. Des problèmes peuvent survenir si des opérations secondaires sont effectuées sur l'objet en même temps.

  • Conflits de type : bien que le code de chaque package ne se pollue pas, leurs types peuvent toujours s'affecter, de sorte que la duplication des versions peut entraîner des conflits de nommage de type global.

Non-déterminisme

Dans le contexte de la gestion frontale des packages, le déterminisme signifie que sous un package.json donné, la même structure de répertoires node_modules peut être obtenue en exécutant la commande npm install dans n'importe quel environnement. Cependant, npm v3 est non déterministe, son répertoire node_modules et sa structure d'arborescence de dépendances dépendent de l'ordre d'installation de l'utilisateur.

Considérez que le projet a l'arborescence de dépendances suivante et que la structure de répertoires node_modules générée par npm install est affichée à droite.
54b9e4fdb53ad39a2b5c5ceafbe59452.pngf07b07333849197bb3c049d4a24e9059.png
Supposons que lorsqu'un utilisateur met à niveau manuellement le module A vers la version 2.0 à l'aide de npm, entraînant la mise à niveau de son module dépendant B vers la version 2.0, la structure de l'arborescence des dépendances à ce moment est la suivante.
15fef88b8c19bee5609d45f185b414e3.png942bd30e9ecb2aaf603045475320d8de.png
À ce stade, le développement est terminé, le projet est déployé sur le serveur et l'installation de npm est réexécutée. À ce stade, la version mise à niveau de la sous-dépendance B a changé et la structure de répertoire node_modules générée sera différente de la structure générée par le développement local de l'utilisateur, comme le montre la figure suivante. Si la structure du répertoire node_modules doit être cohérente, vous devez supprimer la structure node_modules et réexécuter npm install lorsque package.json est modifié.
3862d857fed8acd380c08705fc487c83.pngce5386fb06640686517426d073973086.png

3. aplatissement npm v5 + verrouillage

Ajout de package-lock.json dans npm v5. Lorsque le projet a un fichier package.json et que l'installation de npm est exécutée pour la première fois, un fichier package-lock.json est automatiquement généré, qui enregistre les modules dont dépend package.json et les sous-dépendances des modules. Et chaque dépendance est marquée avec une version, une adresse d'accès et une valeur de hachage pour vérifier l'intégrité du module. Grâce à package-lock.json, la certitude et la compatibilité de l'installation du package dépendant sont garanties, de sorte que le même résultat apparaîtra à chaque fois que vous l'installerez.

cohérence

Dans le cas ci-dessus, l'installation initiale génère package-lock.json comme indiqué à gauche, les dépendances répertoriées dans l'objet dépendances sont toutes promues et l'objet requirements dans chaque dépendance est une sous-dépendance. À ce stade, la mise à jour de la dépendance A vers la version 2.0, comme illustré dans la figure de droite, ne modifiera pas la version de la sous-dépendance mise à niveau. Ainsi, la structure de répertoires node_modules régénérée ne changera pas.
effddb3908ce314ec4f28a18d778c2bc.png54496bb781430d910edf6ffba52a163f.png

compatibilité

Versionnement sémantique

En fonction de la compatibilité de la version, vous devez mentionner la spécification de version SemVer utilisée par npm. Le format de version est le suivant :

  • Numéro de version majeur : modifications d'API incompatibles

  • Numéro de version mineur : ajouts fonctionnels pour la rétrocompatibilité

  • Numéro de révision : corrections de bugs pour la rétrocompatibilité

dfc5a20c8d65cd8091f98c225d93b9c5.png
Lors de l'utilisation de dépendances tierces, nous spécifions généralement la plage de version des dépendances dans package.json. La plage de version sémantique spécifie :

  • ~ : uniquement les numéros de révision de mise à jour

  • ^ : Mettre à niveau le numéro de version mineure et le numéro de révision

  • * : Mise à niveau vers la dernière version

Les règles de gestion des versions sémantiques définissent une règle idéale de mise à jour du numéro de version. On espère que toutes les mises à jour des dépendances pourront suivre cette règle, mais il existe souvent de nombreuses dépendances qui ne suivent pas strictement ces règles. Par conséquent, la mise à niveau par inadvertance de certaines sous-dépendances de modules dépendants peut entraîner des problèmes d'incompatibilité. Par conséquent, package-lock.json indique une certaine version pour chaque sous-dépendance de module afin d'éviter les problèmes d'incompatibilité.

Fil

Yarn était open source en 2016. Yarn semblait résoudre certains problèmes dans npm v3, avant la sortie de npm v5. Yarn est défini comme une gestion des dépendances rapide, sécurisée et fiable.

1、Fichier de verrouillage de fil v1

La structure du répertoire node_modules générée par Yarn est la même que celle de npm v5, et un fichier yarn.lock est généré par défaut. Pour l'exemple ci-dessus, le fichier yarn.lock généré est le suivant :

A@^1.0.0:
  version "1.0.0"
  resolved "uri"
 dependencies:
    B "^1.0.0"

B@^1.0.0:
  version "1.0.0"
  resolved "uri"

B@^2.0.0:
  version "2.0.0"
  resolved "uri"

C@^2.0.0:
  version "2.0.0"
  resolved "uri"
 dependencies:
    B "^2.0.0"

D@^2.0.0:
  version "2.0.0"
  resolved "uri"
  dependencies:
    B "^2.0.0"

E@^1.0.0:
  version "1.0.0"
  resolved "uri"
  dependencies:
    B "^1.0.0"

Vous pouvez voir que yarn.lock utilise un format personnalisé au lieu de JSON et place toutes les dépendances au niveau supérieur, étant donné les raisons d'être plus facile à lire et à réviser, et de réduire les conflits de fusion.

Verrou de fil vs verrou npm

  • Le format de fichier est différent, npm v5 utilise le format json, yarn utilise le format personnalisé

  • Les versions des dépendances enregistrées dans le fichier package-lock.json sont toutes déterministes et il n'y aura pas de symboles de plage de version sémantique (~ ^ *), tandis que les symboles de plage de version sémantique apparaîtront toujours dans le fichier yarn.lock.

  • Le fichier package-lock.json est plus riche en contenu et implémente un fichier de verrouillage plus dense, y compris les informations de promotion des sous-dépendances

    • npm v5 n'a besoin que du fichier package.lock pour déterminer la structure du répertoire node_modules

    • yarn.lock ne peut pas déterminer les dépendances de niveau supérieur et deux fichiers, package.json et yarn.lock, sont nécessaires pour déterminer la structure du répertoire node_modules. L'emplacement des packages dans le répertoire node_modules est calculé en interne dans yarn, ce qui peut entraîner une incertitude lors de l'utilisation de différentes versions de yarn.

2、Fil v2 Plug'n'Play

Dans la version 2.x de Yarn, le mode zéro installation Plug'n'Play (PnP) a été introduit et node_modules a été abandonné, ce qui a assuré la fiabilité des dépendances et amélioré la vitesse de construction.

Étant donné que Node s'appuie sur node_modules pour trouver des dépendances, la génération de node_modules impliquera une série d'opérations lourdes en E/S telles que le téléchargement de packages de dépendances, la décompression dans le cache et la copie dans des répertoires de fichiers locaux, y compris la recherche de dépendances et le traitement des dépendances en double, qui sont très opérations chronophages. Le gestionnaire de packages pour node_modules n'a pas beaucoup de place pour l'optimisation. Par conséquent, yarn fait le contraire. Puisque le gestionnaire de packages a déjà la structure de l'arborescence des dépendances du projet, le gestionnaire de packages peut directement informer l'interpréteur de l'emplacement du package sur le disque et gérer la version et les sous-dépendances du package dépendant.

Exécutez yarn --pnple mode pour activer le mode PnP. En mode PnP, yarn générera un fichier .pnp.cjs au lieu de node_modules. Ce fichier maintient un mappage des packages dépendants vers les emplacements de disque et les listes de sous-dépendances. Dans le même temps, .pnp.js implémente également la méthode resolveRequest pour traiter la demande requise. Cette méthode déterminera directement l'emplacement de la dépendance dans le système de fichiers en fonction de la table de mappage, évitant ainsi l'opération d'E/S consistant à trouver le dépendance dans node_modules.

16f40fdf9a2c928dd94206aff21f29a5.png
Les avantages et les inconvénients du mode pnp sont également très évidents :

  • Excellent : débarrassez-vous de node_modules, installez et chargez les modules rapidement ; tous les modules npm seront stockés dans le répertoire de cache global pour éviter les dépendances multiples ; les sous-dépendances ne seront pas promues en mode strict et les dépendances fantômes seront évitées (mais cela peut conduire à certains packages Un problème survient, donc le mode détendu qui repose sur le boost est également pris en charge :<).

  • Inconvénient : le résolveur auto-construit gère la méthode Node require, et l'exécution des fichiers Node doit être exécutée via l'interpréteur de nœuds de fil, qui est séparé de l'écologie existante de Node, et la compatibilité n'est pas très bonne.

pnpm

pnpm1.0 a été officiellement publié en 2017. pnpm présente les avantages d'une vitesse d'installation rapide, d'une économie d'espace disque et d'une bonne sécurité. Il semble également résoudre les problèmes de npm et de fil.

Parce que sous la structure de node_modules aplatis basée sur npm ou yarn, bien que les problèmes d'enfer de dépendance, de cohérence et de compatibilité soient résolus, il n'y a pas de bonne solution pour les dépendances multiples et les dépendances fantômes. Parce que sans tenir compte des dépendances circulaires, le graphe de structure de dépendance réel est un graphe acyclique dirigé (DAG), mais ce que npm et yarn simulent via le répertoire de fichiers et l'algorithme de résolution de nœud est en fait un sur-ensemble du graphe acyclique dirigé (beaucoup de liens entre les mauvais ancêtres et frères et sœurs), ce qui a causé beaucoup de problèmes. pnpm utilise également une combinaison de liens physiques et de liens symboliques pour simuler plus précisément DAG afin de résoudre les problèmes de fil et de npm.

1. Node_modules non plats

Les liens physiques économisent de l'espace disque

Un lien physique peut être compris comme une copie du fichier source, permettant aux utilisateurs de trouver un fichier à travers différentes références de chemin, qui est de la même taille que le fichier source mais n'occupe en fait aucun espace. pnpm stockera les liens physiques vers le fichier node_modules du projet dans le répertoire du magasin global. Les liens durs permettent à différents projets de trouver la même dépendance à partir du magasin global, ce qui économise considérablement de l'espace disque.

Les liens symboliques créent des structures imbriquées

Les liens symboliques peuvent être compris comme des raccourcis. pnpm utilise des liens symboliques pour trouver l'adresse de dépendance sous le répertoire de disque correspondant (.pnpm) lors du référencement des dépendances. Envisagez d'installer le module bar qui dépend du module foo dans votre projet, le répertoire node_modules résultant ressemble à ceci.
dc834506cb0acadfbb410834eab9c799.png8a13426354f77ba2a3f99af85ff81dfa.png
On peut voir qu'il n'y a pas de node_modules dans le répertoire bar sous node_modules. Il s'agit d'un lien symbolique. Le fichier réel se trouve dans le répertoire correspondant dans le <package-name>@version/node_modules/<package-name>répertoire et est lié en dur au magasin global. Les dépendances de bar existent dans le répertoire .pnpm <package-name>@version/node_modules, qui est également un lien symbolique vers le <package-name>@version/node_modules/<package-name>répertoire et un lien physique vers le magasin global.

L'avantage de cette structure node_modules imbriquée est que seuls les packages qui sont réellement dans les dépendances sont accessibles, ce qui évite que tous les packages promus soient accessibles lorsque la structure plate est utilisée, et résout bien le problème des dépendances fantômes. De plus, comme les dépendances sont toujours des liens physiques dans le répertoire du magasin, les mêmes dépendances ne sont toujours installées qu'une seule fois et le problème des dépendances multiples a également été résolu.

Cette image sur le site officiel explique clairement le mécanisme de gestion des dépendances de pnpm
5e62db94610859fdd430f88aea07608c.png

2. Limites

Il semble que pnpm résolve bien le problème, mais il y a quelques limitations.

  • package-lock.json est ignoré. Le fichier de verrouillage de npm est conçu pour refléter la disposition en mosaïque node_modules, mais pnpm crée une disposition isolée par défaut, qui ne peut pas être reflétée par le format de fichier de verrouillage de npm, et utilise à la place son propre fichier de verrouillage pnpm-lock.yaml.

  • Compatibilité des liens symboliques. Il existe certains scénarios où les liens symboliques ne fonctionnent pas, comme les applications Electron, les applications déployées sur lambda ne peuvent pas utiliser pnpm.

  • Les sous-dépendances sont promues à la structure de répertoire de même niveau, bien que la compatibilité puisse être obtenue grâce à la logique de suivi du répertoire parent de Node.js. Mais pour la logique de chargement des plugins comme Egg et Webpack, où des chemins relatifs sont utilisés, ils doivent être adaptés.

  • Les dépendances de différentes applications sont des liens physiques vers le même fichier. Si le fichier est modifié pendant le débogage, cela peut affecter par inadvertance d'autres projets.

cnpm et tnpm

164ec0954a99f3d9d0b34f0de9bd1664.png
cnpm est une source miroir nationale npm maintenue et open source par Ali, et prend en charge la synchronisation miroir du registre officiel npm. Basé sur cnpm, tnpm est conçu pour servir les étudiants dans l'économie d'Alibaba. Il fournit un entrepôt npm privé et accumule de nombreuses pratiques d'ingénierie Node.js.

La gestion des dépendances de cnpm/tnpm est basée sur pnpm, qui crée une structure node_modules non plate via des liens symboliques, ce qui maximise la vitesse d'installation. Les packages de dépendance installés sont nommés avec le nom du package dans le dossier node_modules, puis des liens symboliques sont créés vers le répertoire numéro de version@nom du package. Contrairement à pnpm, cnpm n'utilise pas de liens physiques et n'établit pas de lien symbolique entre les sous-dépendances pour séparer les répertoires à des fins d'isolation.
0417a54d0a97026e301f8113edc360ca.png22063f0233c2ba3db842673fabb004e1.png
De plus, le nouveau mode rapide de tnpm utilise le système de fichiers de l'espace utilisateur (FUSE) pour effectuer de nouvelles optimisations pour la gestion des dépendances. FUST est similaire à la version du système de fichiers de ServiceWorker. FUSE peut prendre en charge la logique de fonctionnement du système de fichiers d'un répertoire. L'implémentation d'une structure node_modules non plate basée sur cela peut résoudre le problème de compatibilité des liens symboliques. En raison du manque d'espace, je n'entrerai pas dans les détails ici. Si vous êtes intéressé, vous pouvez passer au mode rapide tnpm - comment être 10 secondes plus rapide que pnpm.

autre

Déno

Grâce au mécanisme de gestion des dépendances du gestionnaire de packages grand public exploré ci-dessus, nous avons constaté que peu importe si la structure plate ou non plate de node_modules est parfaite, le mode PnP d'abandon de node_modules n'est pas compatible avec l'écologie actuelle des nœuds, et il n'y a pas de solution. Il semble que Node ait un problème avec node_modules lui-même (?). L'auteur de Node.JS, Ryan, a également admis à JSConf que node_modules était l'un de ses dix principaux regrets à propos de Node, mais c'était irréversible, puis il a recommandé son nouveau travail Deno. Voyons donc comment Deno, un autre grand environnement d'exécution pour JS, gère les dépendances.

e289d91f378472c30fa51c4ac4c30718.png
Dans Deno, au lieu d'utiliser npm, package.json et node_modules, la source importée, le nom du package, le numéro de version et le nom du module sont tous insérés dans l'URL. Les dépendances sont importées via l'URL et mises en cache globalement, ce qui non seulement économise du disque d'espace, mais économise également de l'espace disque Structure de projet optimisée.

import * as log from "https://deno.land/[email protected]/log/mod.ts";

Par conséquent, il n'y a pas de concept de gestionnaire de packages dans Deno.Pour la gestion des dépendances dans les projets, Deno fournit une telle solution. Créées par le développeur dep.ts, toutes les dépendances distantes requises sont référencées dans ce fichier, et les méthodes et classes requises sont réexportées. Les modules locaux dep.tsimportent les méthodes et les classes requises à partir de l'unité, évitant ainsi les incohérences pouvant être causées par l'importation de dépendances externes à l'aide de l'URL seule.

// dep.ts
export {
  assert,
  assertEquals,
  assertStringIncludes,
} from "https://deno.land/[email protected]/testing/asserts.ts";

// index.ts
import { assert } from './dep.ts';

Bien que la façon dont Deno gère les dépendances résolve divers problèmes causés par node_modules, l'expérience actuelle n'est pas très bonne. Tout d'abord, la façon dont l'URL introduit les dépendances est redondante et encombrante, et la sécurité du référencement direct des fichiers sur le réseau est également discutable ; et les développeurs sont tenus de maintenir manuellement les dep.tsfichiers, les sources des dépendances ne sont pas claires et les changements dans les dépendances nécessitent également des modifications des fichiers locaux qui introduisent des dépendances ; en outre, l'écosystème de packages dépendants est également bien inférieur à Node.

Mais Deno offre une autre façon de penser. Le gestionnaire de packages de Node semble être juste un "outil pur" qui installe des dépendances et génère des node_modules. La logique pour trouver les dépendances de résolution est toujours effectuée dans Node, il n'y a donc pas grand-chose au niveau du gestionnaire de packages. .espace optimisé. Le modèle Pnp de Yarn a tenté de changer le statut du gestionnaire de paquets, mais il n'est pas invincible face au puissant écosystème Node. Par conséquent, Deno redémarre le poêle, fusionne les dépendances intall et resolve, et les node_modules et gestionnaires de packages redondants sont inutiles. C'est juste que la méthode actuelle de Deno n'est pas assez mature, et nous attendons avec impatience l'évolution ultérieure.

Épilogue

Bien qu'il n'existe pas de solution parfaite de gestion des dépendances, en regardant le développement historique du gestionnaire de packages, il s'agit d'un processus d'apprentissage mutuel et d'optimisation continue entre les bibliothèques et les développeurs, et ils promeuvent constamment le développement du domaine de l'ingénierie frontale. regard vers l'avenir, une meilleure solution émerge.

faire référence à

  • Le dilemme node_modules (https://zhuanlan.zhihu.com/p/137535779) npm :

  • Comment fonctionne Npm (https://npm.github.io/how-npm-works-docs/index.html)

  • Fil : Plug'n'Play (https://yarnpkg.com/features/pnp)

  • pnpm : structure node_modules liée symboliquement (https://pnpm.io/en/symlinked-node-modules-structure)

  • tnpm : mode rapide tnpm vrai et simple - comment être 10 secondes plus rapide que pnpm (https://zhuanlan.zhihu.com/p/455809528)

  • deno : Lien vers un code tiers (https://deno.land/[email protected]/linking_to_external_code)


Node 社群


我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:

1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长

点赞和在看就是最大的支持❤️

Je suppose que tu aimes

Origine blog.csdn.net/xgangzai/article/details/123700324
conseillé
Classement