前端包管理器的依赖管理原理

发表于 2年以前  | 总阅读数:421 次

前言

npm是Node.JS的包管理工具,除此之外,社区有一些类似的包管理工具如yarn、pnpm和cnpm,以及集团内部使用的tnpm。我们在项目开发过程中通常使用以上主流包管理器生成node_modules目录安装依赖并进行依赖管理。本文主要探究前端包管理器的依赖管理原理,希望对读者有所帮助。

npm

当我们执行npm install命令后,npm会帮我们下载对应依赖包并解压到本地缓存,然后构造node_modules 目录结构,写入依赖文件。那么,对应的包在node_modules目录内部是怎样的结构呢,npm主要经历了以下几次变化。

n**pm v1/v2 依赖嵌套**

npm最早的版本中使用了很简单的嵌套模式进行依赖管理。比如我们在项目中依赖了A模块和C模块,而A模块和C模块依赖了不同版本的B模块,此时生成的node_modules目录如下:

  • 依赖地狱(Dependency Hell)

可以看到这种是嵌套的node_modules结构,每个模块的依赖下面还会存在一个 node_modules 目录来存放模块依赖的依赖。这种方式虽然简单明了,但存在一些比较大的问题。如果我们在项目中增加一个同样依赖2.0版本B的模块D,此时生成的node_modules目录便会如下所示。虽然模块A、D依赖同一个版本B,但B却重复下载安装了两遍,造成了重复的空间浪费。这便是依赖地狱问题。

node_modules
├── A@1.0.0
│   └── node_modules
│       └── B@1.0.0
├── C@1.0.0
│   └── node_modules
│       └── B@2.0.0
└── D@1.0.0
    └── node_modules
        └── B@1.0.0

一些著名的梗图:

npm v3 扁平化

npm v3完成重写了依赖安装程序,npm3通过扁平化的方式将子依赖项安装在主依赖项所在的目录中(hoisting提升),以减少依赖嵌套导致的深层树和冗余。此时生成的node_modules目录如下:

为了确保模块的正确加载,npm也实现了额外的依赖查找算法,核心是递归向上查找node_modules。在安装新的包时,会不停往上级node_modules中查找。如果找到相同版本的包就不会重新安装,在遇到版本冲突时才会在模块下的 node_modules 目录下存放该模块子依赖,解决了大量包重复安装的问题,依赖的层级也不会太深。

扁平化的模式解决了依赖地狱的问题,但也带来了额外的新问题。

  • 幽灵依赖(Phantom dependency)

幽灵依赖主要发生某个包未在package.json中定义,但项目中依然可以引用到的情况下。考虑之前的案例,它的package.json如右图所示。

在index.js中我们可以直接require A,因为在package.json声明了该依赖,但是,我们require B也是可以正常工作的。

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

因为B是A的依赖项,在安装过程中,npm会将依赖B平铺到node_modules下,因此require函数可以查找到它。但这可能会导致意想不到的问题:

  1. 依赖不兼容:my-library库中并没有声明依赖B的版本,因此B的major更新对于SemVer体系是完全合法的,这就导致其他用户安装时可能会下载到与当前依赖不兼容的版本。
  2. 依赖缺失:我们也可以直接引用项目中devDepdency的子依赖,但其他用户安装时并不会devDepdency,这就可能导致运行时会立刻报错。
  • 多重依赖(doppelgangers)

考虑在项目中继续引入的依赖2.0版本B的模块D与而1.0版本B的模块E,此时无论是把B 2.0还是1.0提升放在顶层,都会导致另一个版本存在重复的问题,比如这里重复的2.0。此时就会存在以下问题:

  1. 破坏单例模式:模块C、D中引入了模块B中导出的一个单例对象,即使代码里看起来加载的是同一模块的同一版本,但实际解析加载的是不同的module,引入的也是不同的对象。如果同时对该对象进行副作用操作,就会产生问题。
  2. types冲突:虽然各个package的代码不会相互污染,但是他们的types仍然可以相互影响,因此版本重复可能会导致全局的types命名冲突。
  • 不确定性(Non-Determinism)

在前端包管理的背景下,确定性指在给定package.json下,无论在何种环境下执行npm install命令都能得到相同的node_modules目录结构。然而npm v3是不确定性的,它node_modules目录以及依赖树结构取决于用户安装的顺序。

考虑项目拥有以下依赖树结构,其npm install产生的node_modules目录结构如右图所示。

假设当用户使用npm手动升级了模块A到2.0版本,导致其依赖的模块B升级到了2.0版本,此时的依赖树结构如下。

此时完成开发,将项目部署至服务器,重新执行npm install,此时提升的子依赖B版本发生了变化,产生的node_modules目录结构将会与用户本地开发产生的结构不同,如下图所示。如果需要node_modules目录结构一致,就需要在package.json修改时删除node_modules结构并重新执行npm install。

npm v5 扁平化+lock

在npm v5中新增了package-lock.json。当项目有package.json文件并首次执行npm install安装后,会自动生成一个package-lock.json文件,该文件里面记录了package.json依赖的模块,以及模块的子依赖。并且给每个依赖标明了版本、获取地址和验证模块完整性哈希值。通过package-lock.json,保障了依赖包安装的确定性与兼容性,使得每次安装都会出现相同的结果。

  • 一致性

考虑上文案例,初始时安装生成package-lock.json如左图所示,depedencies对象中列出的依赖都是提升的,每个依赖项中的requires对象中为子依赖项。此时更新A依赖到2.0版本,如右图所示,并不会改变提升的子依赖版本。因此重新生成的node_modules目录结构将不会发生变化。

  • 兼容性

语义化版本(Semantic Versioning)

依赖版本兼容性就不得不提到npm使用的SemVer版本规范,版本格式如下:

  1. 主版本号:不兼容的 API 修改
  2. 次版本号:向下兼容的功能性新增
  3. 修订号:向下兼容的问题修正

在使用第三方依赖时,我们通常会在package.json中指定依赖的版本范围,语义化版本范围规定:

  1. ~:只升级修订号
  2. ^:升级次版本号和修订号
  3. *:升级到最新版本

语义化版本规则定义了一种理想的版本号更新规则,希望所有的依赖更新都能遵循这个规则,但是往往会有许多依赖不是严格遵循这些规定的。因此一些依赖模块子依赖不经意的升级,可能就会导致不兼容的问题产生。因此package-lock.json给每个模块子依赖标明了确定的版本,避免不兼容问题的产生。

Yarn

Yarn 是在2016年开源的,yarn 的出现是为了解决 npm v3 中的存在的一些问题,那时 npm v5 还没发布。Yarn 被定义为快速、安全、可靠的依赖管理。

Yarn v1 lockfile

Yarn 生成的 node_modules 目录结构和 npm v5 是相同的,同时默认生成一个 yarn.lock 文件。对于上文例子,生成的yarn.lock文件如下:

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"

可以看到yarn.lock使用自定义格式而不是JSON,并将所有依赖都放在顶层,给出的理由是便于阅读和审查,减少合并冲突。

  • Yarn lock vs. npm lock

  1. 文件格式不同,npm v5 使用的是 json 格式,yarn 使用的是自定义格式
  2. package-lock.json 文件里记录的依赖的版本都是确定的,不会出现语义化版本范围符号(~ ^ *),而 yarn.lock 文件里仍然会出现语义化版本范围符号
  3. package-lock.json 文件内容更丰富,实现了更密集的锁文件,包括子依赖的提升信息

npm v5 只需要 package.lock 文件就可以确定 node_modules 目录结构yarn.lock 无法确定顶层依赖,需要 package.json 和 yarn.lock 两个文件才能确定 node_modules 目录结构。node_modules 目录中 package 的位置是在 yarn 的内部计算出来的,在使用不同版本的 yarn 时可能会引起不确定性。

Yarn v2 Plug'n'Play

在Yarn 的2.x版本重点推出了Plug'n'Play(PnP)零安装模式,放弃了node_modules,更加保证依赖的可靠性,构建速度也得到更大的提升。因为Node依赖于node_modules查找依赖,node_modules的生成会涉及到下载依赖包、解压到缓存、拷贝到本地文件目录等一系列重IO的操作,包括依赖查找以及处理重复依赖都是非常耗时操作,基于node_modules的包管理器并没有很多优化的空间。

因此yarn反其道而行之,既然包管理器已经拥有了项目依赖树的结构,那也可以直接由包管理器通知解释器包在磁盘上的位置并管理依赖包版本与子依赖关系。

执行yarn --pnp模式即可开启PnP模式。在PnP模式,yarn 会生成 .pnp.cjs 文件代替node_modules。该文件维护了依赖包到磁盘位置与子依赖项列表的映射。同时 .pnp.js 还实现了resolveRequest方法处理require请求,该方法会直接根据映射表确定依赖在文件系统中的位置,从而避免了在node_modules查找依赖的 I/O 操作。 pnp模式优缺点也非常明显:

  1. 优:摆脱node_modules,安装、模块速度加载快;所有 npm 模块都会存放在全局的缓存目录下,避免多重依赖;严格模式下子依赖不会提升,也避免了幽灵依赖(但这可能会导致某些包出现问题,因此也支持了依赖提升的宽松模式:<)。
  2. 缺:自建resolver 处理Node require方法,执行Node文件需要通过yarn node解释器执行,脱离Node现存生态,兼容性不太好

pnpm

pnpm1.0于2017年正式发布,pnpm具有安装速度快、节约磁盘空间、安全性好等优点,它的出现也是为了解决npm和yarn存在的问题。

因为在基于npm或yarn的扁平化node_modules的结构下,虽然解决了依赖地狱、一致性与兼容性的问题,但多重依赖和幽灵依赖并没有好的解决方式。因为在不考虑循环依赖的情况下,实际的依赖结构图为有向无环图(DAG),但是npm和yarn通过文件目录和node resolve算法模拟的实际上是有向无环图的一个超集(多出了很多错误祖先节点和兄弟节点之间的链接),这导致了很多的问题。pnpm也是通过硬链接与符号链接结合的方式,更加精确的模拟DAG来解决yarn和npm的问题。

非扁平化的node_modules

  • 硬链接(hard link)节约磁盘空间

硬链接可以理解为源文件的副本,使得用户可以通过不同的路径引用方式去找到某个文件,他和源文件一样的大小但是事实上却不占任何空间。pnpm 会在全局 store 目录里存储项目 node_modules 文件的硬链接。硬链接可以使得不同的项目可以从全局 store 寻找到同一个依赖,大大节省了磁盘空间。

  • 符号链接(symbolic link)创建嵌套结构

软链接可以理解为快捷方式,pnpm在引用依赖时通过符号链接去找到对应磁盘目录(.pnpm)下的依赖地址。考虑在项目中安装依赖于foo模块的bar模块,生成的node_modules目录如下所示。

可以看到node_modules下的bar目录下并没有node_modules,这是一个符号链接,实际真正的文件位于.pnpm目录中对应的 <package-name>@version/node_modules/<package-name>目录并硬链接到全局store中。而bar的依赖存在于.pnpm目录下<package-name>@version/node_modules目录下,而这也是软链接到<package-name>@version/node_modules/<package-name>目录并硬链接到全局store中。

而这种嵌套node_modules结构的好处在于只有真正在依赖项中的包才能访问,避免了使用扁平化结构时所有被提升的包都可以访问,很好地解决了幽灵依赖的问题。此外,因为依赖始终都是存在store目录下的硬链接,相同的依赖始终只会被安装一次,多重依赖的问题也得到了解决。

官网上的这张图清晰地解释了pnpm的依赖管理机制

局限性

看起来pnpm似乎很好地解决了问题,但也存在一些局限。

  1. 忽略了 package-lock.json。npm 的锁文件旨在反映平铺的 node_modules 布局,但是 pnpm 默认创建隔离布局,无法由 npm 的锁文件格式反映出来,而是使用自身的锁文件pnpm-lock.yaml。
  2. 符号链接兼容性。存在符号链接不能适用的一些场景,比如 Electron 应用、部署在 lambda 上的应用无法使用 pnpm。
  3. 子依赖提升到同级的目录结构,虽然由于 Node.js 的父目录上溯寻址逻辑,可以实现兼容。但对于类似 Egg、Webpack 的插件加载逻辑,在用到相对路径的地方,需要去适配。
  4. 不同应用的依赖是硬链接到同一份文件,如果在调试时修改了文件,有可能会无意中影响到其他项目。

ccnpm 和 tnpm

cnpm是由阿里维护并开源的npm国内镜像源,支持官方 npm registry 的镜像同步。tnpm是在cnpm基础之上,专为阿里巴巴经济体的同学服务,提供了私有的 npm 仓库,并沉淀了很多 Node.js 工程实践方案。

cnpm/tnpm的依赖管理是借鉴了pnpm ,通过符号链接方式创建非扁平化的node_modules结构,最大限度提高了安装速度。安装的依赖包都是在 node_modules 文件夹以包名命名,然后再做符号链接到 版本号 @包名的目录下。与pnpm不同的是,cnpm没有使用硬链接,也未把子依赖符号链接到单独目录进行隔离。

此外,tnpm新推出的rapid模式使用用户态文件系统(FUSE)对依赖管理做了一些新的优化。FUST类似于文件系统版的 ServiceWorker,通过 FUSE 可以接管一个目录的文件系统操作逻辑。基于此实现非扁平化的node_modules结构可以解决软链接的兼容性问题。限于篇幅原因这里不再详述,感兴趣可以移步真·深入浅出 tnpm rapid 模式 - 如何比 pnpm 快 10 秒。

其他

Deno

通过上文探究的主流包管理器依赖管理机制,我们发现无论扁平化或非扁平化node_modules结构似乎都不完美,抛弃node_modules的PnP模式又不兼容当前Node的生态,无解。看起来似乎是Node与node_modules自身有点问题(?)。Node.JS作者Ryan也在JSConf上承认node_modules是他对Node的十大遗憾之一,但已经无法挽回了,随后他推荐了自己的新作Deno。那让我们看看JS的另一大运行时环境Deno是如何进行依赖管理的。

在Deno不使用npm、package.json以及node_modules,而是将引入源、包名、版本号、模块名全部塞进了 URL 里,通过URL导入依赖并进行全局统一缓存,不仅节省了磁盘空间,也优化了项目结构。

import * as log from "https://deno.land/std@0.125.0/log/mod.ts";

因此Deno中没有包管理器的概念,对于项目中的依赖管理,Deno提供了这样一种方案。由开发者创建dep.ts,此文件中引用了所有必需的远程依赖关系,并且重新导出了所需的方法和类。本地模块从dep.ts统一导入所需方法和类,避免单独使用URL导入外部依赖可能造成的不一致的问题。

// dep.ts
export {
  assert,
  assertEquals,
  assertStringIncludes,
} from "https://deno.land/std@0.125.0/testing/asserts.ts";

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

Deno处理依赖的方式虽然解决了node_modules带来的种种问题,但目前体验也并不是很好。首先URL引入依赖的方式写法比较冗余繁琐,直接引用网络上文件的安全性也值得商榷;而且需要开发者手动维护dep.ts文件,依赖来源不清晰,依赖变更还需要更改引入依赖的本地文件;此外,依赖包的生态也远远不及Node。

但Deno确实提供了另外一种思路,Node的包管理器似乎只是安装依赖、生成node_modules的“纯工具人”,真正查找resolve依赖的逻辑还是在Node做的,所以包管理器层面也没有太多优化的空间。Yarn的Pnp模式曾试图改变包管理器的地位,但也不敌强大的Node生态。因此Deno重启炉灶,将intall和resolve依赖过程合并,多余的node_modules与包管理器也就没什么存在的必要了。只是Deno当前的方式还不够成熟,期待后续的演进。

结语

虽然目前还没有完美的依赖管理方案,但纵观包管理器的历史发展,是库与开发者互相学习和持续优化的过程,并且都在不断推动着前端工程化领域的发展,我们期待未来会出现更好的解决方案。

参考

  1. node_modules困境(https://zhuanlan.zhihu.com/p/137535779)npm:
  2. How Npm Works(https://npm.github.io/how-npm-works-docs/index.html)
  3. Yarn: Plug'n'Play(https://yarnpkg.com/features/pnp)
  4. pnpm: 基于符号链接的 node_modules 结构(https://pnpm.io/zh/symlinked-node-modules-structure)
  5. tnpm: 真·深入浅出 tnpm rapid 模式 - 如何比 pnpm 快 10 秒(https://zhuanlan.zhihu.com/p/455809528)
  6. deno: Linking to third party code(https://deno.land/manual@v1.18.2/linking_to_external_code)

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/BfijbUe017ZzlINnCIda8g

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237227次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录