node.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > node.js > 前端pnpm workspace

前端pnpm workspace架构实例详解

作者:珑墨

pnpm作为新一代包管理工具,解决了npm和yarn在依赖管理中的效率问题,它采用独特的网状+平铺结构,显著提升安装速度并优化磁盘空间,这篇文章主要介绍了前端pnpm workspace架构的相关资料,需要的朋友可以参考下

前言

一篇帮你搞懂 pnpm workspace 的实战向教程,从「为啥要用」到「怎么配」全给你捋清楚;每个知识点都会讲清是什么、为什么、怎么用、注意啥,方便你系统学习、随时查阅、直接落地。

一、先聊聊:我们到底遇到了啥问题?

做前端久了,多包、monorepo、组件库联调这些事一多,就会踩到一堆具体又磨人的坑。下面把这些痛点拆开说:具体表现 → 典型场景 → 对你有啥影响。搞清楚这些,后面再看 pnpm workspace 解决啥就一目了然。

1.1node_modules膨胀,磁盘和时间都遭殃

具体表现:用 npm 搞 monorepo 时,根目录一个 node_modules,每个子包再来一个;或者多个独立项目各自一份。每个 node_modules 里,npm 会做扁平化:把子依赖提升到顶层,同一份包可能在不同项目的 node_modules 里各存一份,重复拷贝

典型场景:比如你有一个 monorepo,里面 5 个 app、3 个共享库,都用 React、lodash、一堆 Babel/Webpack 相关包。单项目 node_modules 可能就 400~600MB,monorepo 里再乘上包数量、加上提升带来的重复,轻松破 2GB。npm install 第一次全量装要几分钟,以后每次 npm ci 或清缓存重装,体感也很慢。

影响:占磁盘、拉代码慢、CI 缓存大、流水线耗时增加;本机多开几个项目,node_modules 动不动几十 GB。

1.2 依赖版本乱成一锅粥:幽灵依赖与冲突

幽灵依赖的定义:某个包没有在你自己的 package.jsondependencies / devDependencies 里声明,你却能在代码里 importrequire 到它。常见原因就是 npm 的扁平化:你装了 A,A 依赖 B,B 被提升到了项目根 node_modules,于是你的代码「意外」地能直接用 B。

典型场景:你习惯性 import _ from 'lodash',但从没在 package.json 里加过 lodash,因为它是某个依赖的子依赖,被提升上来了。后来你升级了那个依赖,人家不再依赖 lodash,或者换了版本,你这边没改一行业务代码就报错:找不到 lodash。更坑的是「本地能跑、CI 挂」:本地可能还有别的路径残留或缓存,CI 干净安装就炸。同理,删了某个你以为没用的依赖,结果别的地方一直隐式用着,一删就挂。

版本冲突:A 包要 React 18,B 包要 React 17,扁平化之后只能满足一边,另一边可能用了「不对」的版本,运行时才暴露问题,调试成本很高。

1.3 本地包联调贼麻烦:npm link的坑

典型场景:你维护一个业务组件库,要在另一个前端项目里联调。通常做法是 npm link:在组件库目录 npm link,在业务项目里 npm link your-components。但经常会遇到:

总之,改一下组件库就要反复 link、unlink、重装,体验很差,也容易忘步骤导致联调结果不可靠。

1.4 CI 又慢又占空间

典型场景:每次 CI 全量 npm install,没有跨项目或跨 job 的 store 复用;缓存 key 设计不当(例如只按 package.json 不按 lockfile),导致缓存命中率低,每次都几乎全量装。加上前面说的 node_modules 巨大,流水线耗时长、占用空间大,体验和成本都不好。

上面这些,本质都可以归为两类问题:一是多包怎么组织、怎么一起开发、怎么发布(项目结构 + 工作流);二是依赖怎么存、怎么解析、怎么隔离(存储与解析策略)。pnpm 的 workspace 就是在这两方面同时发力的方案之一:多包管理 + 更合理的依赖存储与解析。下面先把你可能最关心的——pnpm 底层是怎么干的——讲清楚,再回头看 workspace 具体解决了啥。

二、pnpm 底层原理:为啥能省空间、装得快、依赖还干净?

很多人只记住结论:「pnpm 省磁盘、快、没幽灵依赖」,但不知道它到底咋做到的。这一节把存储模型node_modules 结构说透,你后面看配置、看优缺点都会更有数。

2.1 全局 store:content-addressable + 硬链接

pnpm 有一个全局 store,所有安装过的包都会先放进这里,再通过硬链接挂到各个项目的 node_modules 里。

结果:同 monorepo、同样依赖,用 pnpm 时磁盘占用往往只有 npm 的一半左右(常见 benchmark 结论),二次安装时大量命中 store,pnpm install 明显更快。

2.2node_modules的真实结构:非扁平 + 严格依赖

npm 会把依赖扁平化提升到顶层,所以你能「意外」用到子依赖;pnpm 不这么做,结构是非扁平的。

目录结构示意(精简版):

严格依赖就是这样实现的:
只有package.json 里显式声明的包,才会出现在你项目的 node_modules 顶层(或子包自己的 node_modules 里)。未声明的包根本不在你可访问的路径下,require / import 会直接报错,从根上杜绝幽灵依赖

有些老旧工具会假设「所有依赖都在根 node_modules 扁平展开」,在 pnpm 默认结构下会找不到包。这时可以用 public-hoist-patternnode-linker=hoisted有限提升,相当于在「兼容旧工具」和「严格依赖」之间做权衡;提升多了,幽灵依赖风险又回来了,所以能窄就窄。

2.3 workspace 包怎么被链接进来?

当你在 package.json 里写 "@my/ui": "workspace:*" 时,pnpm 会:

  1. pnpm-workspace.yaml 定义的目录里找到对应包(如 packages/ui);
  2. 该包所在目录(源码目录)链接到 node_modules 里对应位置,不拷贝、不先打包

所以,你改 packages/ui 的源码,消费方(例如 apps/web立即可见,不用 npm link,也没有双实例、路径错乱那些破事。这就是 workspace 协议 带来的「本地包即源码」的联调体验。

2.4 和 npm / Yarn 的存储对比(简要)

差异主要在存储与解析策略,而不是「有没有 workspace」这个概念。

三、pnpm workspace 解决了什么问题?(深化版)

有了第二节的原理打底,这里直接说 workspace 在「多包管理」场景下,具体帮你解决了啥;每个点都往「能用、能查」上靠。

3.1 磁盘与安装

3.2 依赖隔离与一致性

3.3 多包协作与发布

四、pnpm workspace 架构长什么样?

4.1 目录树与职责

下面是一个常见的 pnpm workspace 根目录结构,以及各部分的职责。

项目根目录
├── pnpm-workspace.yaml    # 声明哪些目录是 workspace 包(唯一、仅根目录)
├── package.json           # 根包:公共 devDependencies、批量脚本、overrides 等
├── pnpm-lock.yaml         # 全 workspace 唯一 lockfile,所有人、CI 共用一个
├── .npmrc                 # 可选:store-dir、node-linker、hoist 等
├── packages/
│   ├── ui/                # 如:组件库
│   ├── utils/             # 公共工具
│   ├── config-eslint/     # 共享 ESLint 配置
│   └── ...
└── apps/
    ├── web/               # 前端应用
    ├── docs/              # 文档站
    └── ...

有的项目还会加 tools/* 放脚本、CLI 等,本质上一样:在 pnpm-workspace.yaml 里写上对应 glob 即可。

4.2 命名与布局约定

4.3 workspace 包的解析与匹配机制

靠啥匹配?

pnpm 解析 workspace:* 时,只看 package.json 里的 name,和目录名、路径都无关。你写 "@my/ui": "workspace:*",pnpm 就会在 pnpm-workspace.yaml 声明的那堆目录里,找 name 为 @my/ui 的包;找到就把该包所在目录链进 node_modules,找不到就直接报错,不会悄悄去 npm 装一个。

具体流程

  1. pnpm-workspace.yaml,收集所有匹配 packages 的目录(如 packages/*apps/*);
  2. 逐个读这些目录下的 package.json,拿到 name,建成一张 「name → 目录」 的映射;
  3. 解析依赖时,遇到 workspace:*workspace:^ 等,用依赖里的包名去这张表里查;
  4. 查到了 → 用该包所在目录做链接目标,链到当前包的 node_modules 里;
  5. 查不到 → 报错(例如 ERR_PNPM_NO_MATCHING_PACKAGE),安装中止。

所以:包名必须和依赖里写的一模一样packages/uiname 要是 @my/ui,别的地方才能 "@my/ui": "workspace:*";写成 @my/components 就匹配不上。

几种写法

别名

可以用 "别名": "workspace:真实包名@*" 把 workspace 包挂到另一个名字下,例如 "react": "workspace:my-react@*"。发布时同样会替换成普通依赖形式。

找不到会怎样?
只会报错,不会回退到 npm 装。这样你才能确定:用的一定是本地的 workspace 包,没有误用远端的。

4.4 依赖图与构建顺序

workspace 里包和包之间的依赖关系,会形成一张有向图:谁依赖谁,一目了然。pnpm 跑 pnpm -r run build 这类递归命令时,默认按这张图的拓扑顺序执行:先跑被依赖的,再跑依赖别人的,避免「还没 build 完就被别人 require」的坑。

拓扑顺序是啥?

简单说:若 A 依赖 B,则一定执行 B 的 build执行 A 的 build。例如 utilsuiweb,顺序就是 utilsuiweb。同一层之间(比如多个 app 互不依赖)谁先谁后不保证,但层级不会乱。

默认行为

怎么知道谁依赖谁?

循环依赖

若出现 A → B → C → A,依赖图成环,拓扑排序搞不定,pnpm 会报错;安装、-r 执行都可能挂。所以必须保证 workspace 内无环,设计时就要避免「包互相依赖」。

4.5 安装与打包:workspace 如何工作

安装(pnpm install
根目录执行 pnpm install 时,大致会做这几步:

  1. 读 workspace 定义:解析 pnpm-workspace.yaml,得到所有 workspace 包目录(如 packages/*apps/*)。
  2. 收集包信息:逐个读这些目录下的 package.json,建 name → 目录 映射,并算出整棵依赖树(含对 npm 包的依赖)。
  3. 解析 workspace:*:遇到 workspace:* 等,按 4.3 的规则匹配到本地包目录,从 registry 拉包。
  4. 链接 workspace 包:把匹配到的本地包目录链到各包的 node_modules 里(符号链接或 junction),不拷贝、不往 store 塞;改源码立即生效。
  5. 装外部依赖:对 npm 上的包,按平时那套来:store + 硬链接,装到 node_modules/.pnpm 等位置。
  6. 写 lockfile:把所有依赖(含 workspace:* 的解析结果)写入根目录的 pnpm-lock.yaml

所以:workspace 包只做链接,不占 store;占磁盘、耗时的主要是外部依赖,而它们仍走 store 复用。

打包 / 构建(pnpm -r run build

构建改依赖安装方式,只是按依赖图顺序跑各包的 build 脚本:

  1. 算依赖图:根据各包 package.json 的依赖关系,得到有向图。
  2. 拓扑排序:排出「被依赖的在前、依赖别人的在后」的顺序(pnpm 内部用类似 graph-sequencer 的方式处理)。
  3. 依次执行:按该顺序对每个 workspace 包执行 pnpm run build(或你配的其它 script)。
  4. 若某包没有 build 脚本,pnpm 会报错或跳过该包,视配置而定。

因此:先装依赖,再构建;装依赖保证 node_modules 里 workspace 包、npm 包都就位,构建则按依赖顺序生成各包产物。
若用 --parallel,pnpm 会忽略拓扑顺序,所有包一起跑;适合 devtest 等不严格要求「被依赖的先跑」的场景,但 build 一般别开 --parallel,否则可能用到尚未 build 的依赖。

和 Turborepo / Nx 的关系

pnpm 只负责依赖安装 + 按拓扑序跑 script缓存、增量构建、远程缓存等,可交给 Turborepo、Nx。通常做法是:pnpm 管 install 和 workspace 链接,Turbo/Nx 管 build / test 的调度与缓存,两者一起用没问题。

五、优缺点一览(够直白版)+ 逐条详解

5.1 优点总览

说明
省磁盘、安装快全局 store + 硬链接,避免重复存包;workspace 包用链接,不复制。
依赖干净严格依赖,无幽灵依赖;lockfile 唯一,版本一致。
本地联调友好workspace:* 直接链到源码,改即生效,无需 npm link
monorepo 友好内建 workspace 支持,-r--filter 过滤、并行跑脚本很方便。
易于做权限与发布配合 pnpm publish -r、changesets 做按包发布、权限控制。

详细说明

5.2 缺点 / 注意点总览

说明
和 npm 不完全兼容部分工具假设「所有依赖扁平在根 node_modules」,可能报错,需适配。
学习与迁移成本团队要搞懂 workspace、workspace:*pnpm-workspace.yaml--filter 等。
部分旧工具兼容性极端老旧的构建/调试工具对 pnpm 的 node_modules 结构可能不友好。
需统一包管理全 repo 必须用 pnpm,不能混用 npm/yarn,否则 lockfile、链接会乱。

详细说明

适合:中大型前端项目、组件库 + 多应用、多包复用的 monorepo。

不大适合:单应用、没有多包复用需求的小项目;用 pnpm 单仓也能受益,但 workspace 收益有限。

六、应用场景(什么时候上 workspace?)

下面按场景拆:谁用、解决啥问题、推荐结构、关键配置、日常工作流。你对照自己项目,能直接套用或微调。

6.1 UI 组件库 + 多个业务项目

场景:你们有一个业务组件库,要同时支撑 2~3 个前端项目;组件库频繁迭代,需要在各项目里即时验证,而不是先发 npm 再装。

推荐结构

packages/
  ui/           # 组件库
apps/
  web-admin/
  web-h5/
  web-docs/     # 组件文档

web-adminweb-h5web-docs 都依赖 @my/ui,用 workspace:*

关键配置

工作流

packages/ui → 在 apps/web-docs 或任意 app 里直接看效果;要发版时用 changesets 给 @my/ui 打 version、写 changelog、publish,各 app 再决定何时把 workspace:* 换成固定版本(若你们发 npm 的话)。

6.2 多应用 + 公共 utils / config

场景:多条产品线、多个前端应用,共享 utilsapi-clienteslint-config 等,希望统一版本、统一升级

推荐结构

packages/
  utils/
  api-client/
  config-eslint/
apps/
  app-a/
  app-b/

apps 按需依赖 @my/utils@my/api-clientconfig-eslint 被各 app 的 devDependencies 引用。

关键配置

工作流
公共逻辑在 packages/* 改,各 app 自动用到;发版用 changesets 按包发布,各 app 通过 workspace:* 或固定版本消费。

6.3 文档站 + 组件库

场景:组件库配套一个文档站(如 VitePress、Docusaurus),文档站要直接引用源码里的组件做 Demo,而不是已发布的 npm 包。

推荐结构

packages/
  ui/
apps/
  docs/

docs 依赖 @my/uiworkspace:*

关键配置

工作流

改组件 → 跑 docs 的 dev,文档里实时看效果;发版时先发 @my/ui,再更新文档站里对版本的说明(若文档站自己也要发)。

6.4 全栈 monorepo(前后端同仓)

场景:前端 + Node 服务同仓,共享类型、常量或少量 utils,用同一套依赖管理。

推荐结构

packages/
  types/
  shared-utils/
apps/
  web/
  api/          # Node 服务

apiweb 都依赖 @my/types@my/shared-utilsworkspace:*

关键配置

工作流

typesshared-utils,前后端同时生效;各自部署时只构建对应 app,公共逻辑通过 workspace 链进去。

只要你存在「多个包 + 互相依赖 + 要一起开发」的需求,workspace 就很值得上;上面四种可以组合,比如「组件库 + 多应用 + 文档站」一起做。

七、详细教程:从零搭一个 pnpm workspace

下面按步骤做一遍,每步会写操作、预期结果、常见报错与排查。路径、包名和上文保持一致,你照抄就能跑通。

7.1 环境准备

7.2 初始化根项目

mkdir my-workspace && cd my-workspace
pnpm init

会生成根目录 package.json。编辑成类似:

{
  "name": "my-workspace",
  "version": "1.0.0",
  "private": true,
  "packageManager": "pnpm@9.0.0"
}

7.3 配置pnpm-workspace.yaml

项目根目录新建 pnpm-workspace.yaml

packages:
  - 'packages/*'
  - 'apps/*'

预期:保存后暂无输出;之后 pnpm install 时 pnpm 会扫描这些目录。

7.4 创建子包目录并初始化

mkdir -p packages/ui packages/utils apps/web

然后逐个初始化(Windows 用户可用 PowerShell,mkdir -p 若不可用就分步 mkdir):

cd packages/utils && pnpm init && cd ../..
cd packages/ui   && pnpm init && cd ../..
cd apps/web      && pnpm init && cd ../..

Windows:若 mkdir -p 报错,可改为 mkdir packages\uimkdir packages\utilsmkdir apps\web 等分步创建;cd ../.. 在 PowerShell 中同样适用。)

每个子包会多一个 package.json。接下来改包名、入口、exports

packages/utils/package.json

{
  "name": "@my/utils",
  "version": "0.0.1",
  "main": "index.js",
  "exports": {
    ".": "./index.js"
  }
}

packages/ui/package.json

{
  "name": "@my/ui",
  "version": "0.0.1",
  "main": "index.js",
  "exports": {
    ".": "./index.js"
  }
}

apps/web/package.json

{
  "name": "web",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "dev": "echo \"dev placeholder\"",
    "build": "echo \"build placeholder\""
  }
}

7.5 用workspace:*做包间依赖

packages/ui/package.json 里加依赖 @my/utils

{
  "name": "@my/ui",
  "version": "0.0.1",
  "main": "index.js",
  "exports": { ".": "./index.js" },
  "dependencies": {
    "@my/utils": "workspace:*"
  }
}

apps/web/package.json 里加依赖 @my/ui

{
  "name": "web",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "dev": "echo \"dev placeholder\"",
    "build": "echo \"build placeholder\""
  },
  "dependencies": {
    "@my/ui": "workspace:*"
  }
}

workspace:* 表示「用当前 workspace 里的同名包,追踪源码」;装完依赖后会链接到对应包目录,改代码即时生效。

7.6 根目录执行pnpm install

务必在根目录执行(若不在根目录,先 cd 到项目根):

pnpm install

预期

packages:
  '@my/utils@workspace:*':
    resolution: { directory: packages/utils, type: directory }
  '@my/ui@workspace:*':
    resolution: { directory: packages/ui, type: directory }

(省略其他字段;实际 lockfile 还有 nameversion 等。)

若报 ERR_PNPM_NO_MATCHING_PACKAGE:检查 pnpm-workspace.yamlpackages 是否包含对应目录,以及子包 name 是否和依赖里写的一致。

7.7 根package.json里加批量脚本

根目录 package.json 增加:

{
  "name": "my-workspace",
  "version": "1.0.0",
  "private": true,
  "packageManager": "pnpm@9.0.0",
  "scripts": {
    "dev": "pnpm -r --parallel run dev",
    "build": "pnpm -r run build",
    "build:web": "pnpm --filter web run build"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

Windows:若使用 PowerShell,scripts 里的双引号、&& 等和 Unix 略有差异,一般上述写法没问题;若遇解析错误,可改为 node 跑一小段脚本封装命令。

7.8 验证 workspace 链路

module.exports = { add: (a, b) => a + b };
const { add } = require('@my/utils');
module.exports = { add, hello: 'from ui' };
"scripts": {
  "dev": "echo \"dev placeholder\"",
  "build": "echo \"build placeholder\"",
  "run:check": "node -e \"const x=require('@my/ui'); console.log(x.add(1,2), x.hello)\""
}

保存后,在根目录执行:

pnpm --filter web run run:check

预期输出3 'from ui'

Cannot find module '@my/ui'

ENOENT 等路径类错误:

验证通过后,可以把 webdev / build 换成真实命令(如 Vite、Next 等),继续开发。

八、配置说明(可查阅手册)

这一节把 pnpm workspace 相关配置 拆开讲:每项是啥、怎么配、适用场景、注意点。方便你以后查。

8.1pnpm-workspace.yaml

示例

packages:
  - 'packages/*'
  - 'apps/*'
  - 'tools/*'

8.2 根目录package.json

catalog 示例pnpm-workspace.yaml):

packages:
  - 'packages/*'
  - 'apps/*'

catalog:
  react: ^18.3.1
  react-dom: ^18.3.1

子包 package.json

{
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:"
  }
}

升级时只改 catalog 即可,所有用 catalog: 的包一起变。

8.3workspace:协议

日常开发 workspace:* 就够用;若你们有严格的 semver 约束再考虑 ^ / ~

8.4pnpm-lock.yaml

8.5.npmrc(项目级)

放在项目根目录,只影响当前仓库。

常见项:

配置项含义示例
store-dir全局 store 路径store-dir=D:\pnpm-store
node-linker链接方式isolated(默认)/ hoisted
hoisted已废弃,用 node-linker
public-hoist-pattern哪些包提升到根 node_modulespublic-hoist-pattern[]=*eslint*
shamefully-hoist全部提升,类似 npmtrue,易幽灵依赖,慎用
auto-install-peers自动装 peerDependenciestrue
strict-peer-dependenciespeer 未满足时报错true

示例(只提升部分工具):

public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*

8.6--filter完整语法

--filter 用来限定要对哪些 workspace 包执行命令,常与 pnpm -rpnpm add 等一起用。

写法含义示例
--filter <pkg>指定包(按 name 或路径)pnpm --filter web run build
--filter <pkg>...pkg 以及依赖了 pkg 的所有包(dependents)pnpm -r --filter '@my/ui...' run build
--filter ...<pkg>pkg 以及 pkg 依赖的所有包(dependencies)pnpm -r --filter '...web' run build
--filter ...^<pkg>依赖了 pkg 的包,不含 pkg 自身pnpm -r --filter '...^@my/ui' run test
@scope/*通配,所有 @scope 下包pnpm -r --filter '@my/*' run build

示例

# 只给 web 装 lodash
pnpm add lodash --filter web

# 只给名字匹配 @my/* 的包跑 build
pnpm -r --filter '@my/*' run build

# 只给依赖了 @my/ui 的包跑 test(不含 @my/ui 自身,例如 web、docs)
pnpm -r --filter '...^@my/ui' run test

# 只给 web 及其依赖的 workspace 包跑 build(含 web 自身)
pnpm -r --filter '...web' run build

多 filter 可组合,例如 --filter '@my/ui...' --filter web 表示满足任一条件的包。仅要「依赖了某包」的包且排除该包本身时,用 ...^<pkg>

8.7 依赖提升(hoisting)

对比

8.8 只用 pnpm / 锁包管理

九、和 npm / Yarn workspace 的简单对比

能力npm workspacesYarn workspacepnpm workspace
磁盘占用高,多份拷贝一般低,store+硬链接
安装速度一般较快
node_modules 结构扁平扁平或 PnP非扁平,.pnpm
幽灵依赖易出现默认严格,无
lockfile 格式package-lock.jsonyarn.lockpnpm-lock.yaml
workspace 协议workspace:*workspace:*workspace:*
配置方式package.json workspacespackage.json workspacespnpm-workspace.yaml
filter/scripts无内置 filter有 workspaces 脚本-r--filter
CI 缓存友好度一般较好好(store 可复用)

何时选 pnpm workspace

何时继续用 npm / Yarn

pnpm 的差异主要来自存储与解析策略,而不是「有没有 workspace」本身。

十、进阶与延伸

10.1 发版:按包发布 + changesets

10.2 任务编排:Turborepo / Nx

10.3 参考

十一、小结与 FAQ

11.1 小结

11.2 FAQ

Q:子包的依赖装到根还是装到各自包?
A:各自 package.json 里声明,各自装;pnpm 会把实体放在 store、在对应包的 node_modules/.pnpm 下链接。根 package.json 只放全仓库共用的 devDependencies(如 TS、ESLint)和脚本。

Q:workspace:* 发布到 npm 前要改吗?
A:不用pnpm publish 时会把 workspace:* 等替换成实际版本号再发布,发布出去的 package.json 里是普通版本范围。

Q:Windows 下路径或脚本有问题怎么办?
A:

如果你有具体的目录结构或 package.json 想优化,可以贴出来,按你现在的项目一步步改也行。

到此这篇关于前端pnpm workspace架构的文章就介绍到这了,更多相关前端pnpm workspace内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文