在一篇文章中掌握使用 npm、yarn 和 pnpm 的工作区!
workspaces(工作区)是一个通用术语,它指的是从单个*根包中管理本地文件系统中的多个包。
npm、yarn、pnpm都提供了对workspaces的支持,在使用上会有些微的不同,今天就分享一下这几个包管理器分别使用workspaces的方式
前段时间写几个独立的组件,当时因为没考虑要做成啥样,就把demo文档和组件放在一个包管理了,随着需求的增多,不得不把文档抽离出来,由于前期都是用的npm(因为要使用新发布的包),就直接使用npm的workspaces了
npm的workspaces介绍及使用
npm官网的介绍
以一个简单的组件库demo为例
项目初始化
创建vangle目录为项目的根目录,初始化package.json
cd vangle
npm init -y
package.json
{
"name": "vangle",
"version": "1.0.0",
"license": "ISC"
}
手动添加子包
创建packages存放所有的子包,然后在该目录下新建docs和components目录,分别初始化子包的package.json
packages/components/package.json
{
"name": "components",
"version": "1.0.0",
"license": "ISC"
}
packages/docs/package.json
{
"name": "docs",
"version": "1.0.0",
"license": "ISC"
}
- 目录结构为
|-- vangle
|-- package.json
|-- packages
|-- components
| |-- package.json
|-- docs
|-- package.json
在根项目的package.json中添加workspaces,其实就是包的路径数组,支持Glob通配符,这里的路径指向指的是package.json所在文件夹文件夹名。
{
"name": "vangle",
"version": "1.0.0",
"license": "ISC",
"workspaces": [
"packages/components",
"packages/docs"
]
}
这时执行npm install后node_modules下就会有components和docs的依赖了
上面是手动添加子包的方式,npm也提供了命令行的方式添加,所做的功能和上面手动类似,-w
就是 --workspace
的简写,但用法稍有区别,如下:
npm init -w ./packages/a -y
# or
npm init --workspace=./packages/b -y
没有层级目录会自动创建,生成pckage.json并在根目录package.json中添加workspace路径
- workspaces也支持glob通配符,例如像下面这样配置表示匹配packages目录下所有的一级子包
{
"workspaces": [
"packages/*"
]
}
为子包添加、移除、更新依赖
- 如果想为components包添加dayjs依赖,可以使用一下命令操作,使用
-w [packageName]
来告诉npm为哪个子包添加依赖
npm install dayjs -w components
npm uninstall dayjs -w components
npm update dayjs -w components
# or
npm install dayjs --workspace=components
注意:如果在项目根目录运行
npm install
会同时将子包及子包的依赖一起安装到根node_modules下
使用子包
为components、docs分别添加如下代码,由于docs依赖components建议位docs包添加依赖
npm install components -w docs
// packages/components/index.js
export const button = 'button'
// packages/docs/index.js
import { button } from 'components'
console.log(button)
在docs/package.json中添加启动脚本
{
"name": "docs",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"license": "ISC",
"scripts": {
"dev": "node index.js"
}
}
因为使用了ES module 需要添加"type": "module"
,在docs/index.js中导入了components,默认回去加载components/index.js,建议加上"main": "index.js"
,不然会有提示
- 运行启动命令 dev
npm run dev -w docs
# run many
npm run dev -w docs -w components
# or
npm run dev --workspace=docs
npm run test --workspace=docs --workspace=components
- 如果我们想启动子包的所有dev脚本可以使用
--workspaces
参数,如果有的子包没有dev脚本会报错,使用--if-present
参数可以避免
npm run dev --workspaces
# 有就运行
npm run dev --workspaces --if-present
在根package.json中运行工作区脚本
和上面一模一样,好处是可以简写运行的命令
"scripts": {
"dev": "npm run dev --workspaces",
"docs:dev": "npm run dev -w docs"
}
上面就是npm工作区的基本使用,这里快速总结一下几个常用命令
# 新增子包
npm init -w ./packages/a -y
# 为子包添加依赖
npm install dayjs -w components
# 运行子包的dev脚本
npm run dev -w docs
# 运行所有子包dev脚本,注意 --if-present 的使用时机
npm run dev --workspaces
yarn 使用 workspaces
yarn官网的介绍
其实和npm差不多,这里还是按上面的方式操作一遍
工作区是一种设置包架构的新方法,从 Yarn 1.0 开始默认可用。 它允许你设置多个包,这样你只需要运行 yarn install 就可以一次性安装所有包。
使用npm安装yarn
npm install -g yarn
项目初始化
创建vangle目录为项目的根目录,初始化package.json
cd vangle
yarn init -y
package.json
{
"name": "vangle",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"workspace": [
"packages/docs",
"packages/components"
]
}
private:根目录一般是项目的脚手架,无需发布,"private": true会确保根目录不被发布出去
workspaces:其实就是包的路径数组,支持Glob通配符,这里的路径指向指的是package.json所在文件夹文件夹名。
相关命令
显示你当前项目的工作空间依赖树
yarn workspaces info
{
"docs": {
"location": "packages/docs",
"workspaceDependencies": [],
"mismatchedWorkspaceDependencies": []
},
"components": {
"location": "packages/components",
"workspaceDependencies": [],
"mismatchedWorkspaceDependencies": []
}
}
选定的工作空间(即包)中运行所选的yarn命令 yarn workspace <package-name> <command>
# 添加依赖
yarn workspace docs add dayjs
# 移除依赖
yarn workspace docs remove dayjs
如果你想为所有的包添加一个共同的依赖关系,进入项目的根目录并使用-W (或-ignore-workspace-root-check) 标志
yarn add dayjs -W -D
在docs包中安装components包(安装本地包),运行下面的命令,必须加上版本号
yarn workspace docs add components@1.0.0
# or
cd .\packages\docs
yarn add components@1.0.0
注意:如果远程仓库有相同名称相同版本的包会优先下载远程的,注意命名
hoist 依赖提升问题
如果我们直接切换到子包如:cd .\packages\docs
,然后添加依赖 yarn add -D typescript
,这时依赖还是会添加到根级的node_modules里
这是因为yarn默认会把子包安装的依赖会提升到根级,如果你想在子包中安装它,需要在根package.json中设置不需要提升的包nohoist
,注意workspaces的结构
{
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": ["**/docs", "**/docs/**"]
}
}
npm 原始好像没有nohoist的实现,所以上面没讲 github.com/npm/rfcs/is…
运行工作区npm脚本
- 运行docs的dev脚本
yarn workspace docs dev
- 在根package.json中运行工作区脚本
yarn docs:dev
"scripts": {
"docs:dev": "yarn workspace docs dev"
}
pnpm 使用 workspaces
pnpm官网的介绍
相比于npm和yarn,pnpm就是专门为menorepo而生的,提供了一些高级的workspaces用法。一些知名的开源项目如:Next.js、Vue3.0、Vite、Nuxt等都陆续使用pnpm作为脚手架。
使用pnpm有以下几个优势,这也是pnpm项目的初衷。
- 节省磁盘空间
- 提高安装速度
- 创建一个非扁平的 node_modules 目录
使用npm安装pnpm
npm install -g pnpm
pnpm和npm、yarn的用法相似,这里就不作过多介绍了,主要讲讲与workspaces
相关的内容
项目初始化
创建vangle目录为项目的根目录,初始化package.json
mkdir vangle
cd vangle
pnpm init -y
pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持, 你可以创建一个 workspace 以将多个项目合并到一个仓库中。
与npm、yarn不同,pnpm的workspaces的配置都放在pnpm-workspace.yaml文件里
一个 workspace 的根目录下必须有 pnpm-workspace.yaml 文件, 也可能会有 .npmrc 文件。
pnpm-workspace.yaml 定义了 工作空间 的根目录,并能够使您从工作空间中包含 / 排除目录 。 默认情况下,包含所有子目录。
# pnpm-workspace.yaml
packages:
# all packages in direct subdirs of packages/
- 'packages/*'
# # all packages in subdirs of components/
# - 'components/**'
# # exclude packages that are inside test directories
# - '!**/test/**'
初始化子包命令
和 npm 类似
pnpm init -w ./packages/docs -y
pnpm init -w ./packages/components -y
- 在项目根目录安装依赖
# 使用 --ignore-workspace-root-check 或 -w 来标记,否则在 root workspace 包添加依赖项时会失败。
pnpm add dayjs -w
- 为子包添加依赖,
--filter
或-F
的使用,--filter 后面跟的是包名
pnpm add dayjs --filter docs
- 安装工作区里的子包,例如docs安装components作为依赖
pnpm add components --filter docs
我们看看docs/package.json中写入的是什么"components": "workspace:^1.0.0"
,前面有workspace:
说明这个依赖来自于本地工作区
{
"dependencies": {
"components": "workspace:^1.0.0",
"dayjs": "^1.11.7"
}
}
workspace:
是pnpm Workspace的协议,这里是官网的详细解释
默认情况下,如果可用的 packages 与已声明的可用范围相匹配,pnpm 将从工作区链接这些 packages。 例如, 如果bar引用"foo": "^1.0.0"
并且foo@1.0.0
存在工作区,那么pnpm会从工作区将foo@1.0.0
链接到bar。 但是,如果 bar 的依赖项中有 "foo": "2.0.0"
,而 foo@2.0.0
在工作空间中并不存在,则将从 npm registry 安装 foo@2.0.0
。 这种行为带来了一些不确定性。
幸运的是,pnpm 支持 workspace 协议 workspace:
。 当使用此协议时,pnpm 将拒绝解析除本地 workspace 包含的 package 之外的任何内容。 因此,如果您设置为 "foo": "workspace:2.0.0"
时,安装将会失败,因为 "foo@2.0.0"
不存在于此 workspace 中。
当 link-workspace-packages 选项被设置为 false 时,这个协议将特别有用。 在这种情况下,仅当使用 workspace: 协议声明依赖,pnpm 才会从此 workspace 链接所需的包。
详细内容请参考 pnpm.io/zh/workspac…
hoist相关配置
pnpm 提升方式,我们可以在 .npmrc
对其进行配置,.npmrc
文件都遵循 INI-formatted 列表,包含 key = value 参数。
更详细的配置请参考 pnpm.io/zh/npmrc
hoist
- 默认值: true
- 类型: boolean
当 hoist 为 true 时,所有依赖项都会被提升到 node_modules/.pnpm/node_modules。 这使得 node_modules所有包都可以访问 未列出的依赖项。
hoist = true
hoist-pattern
- 默认值: ['*']
- 类型: string[]
告诉 pnpm 哪些包应该被提升到 node_modules/.pnpm/node_modules。 默认情况下,所有包都被提升 —— 但是,如果您知道只有某些有缺陷的包具有幻影依赖,您可以使用此选项专门提升幻影依赖(推荐做法)。
例如:
hoist-pattern[]=*eslint*
hoist-pattern[]=*babel*
还可以使用排除模式,过滤不需要提升的依赖
例如:
hoist-pattern[]=*types*
hoist-pattern[]=!@types/react
shamefully-hoist
- 默认值: false
- 类型:Boolean
默认情况下,pnpm 创建一个半严格的 node_modules,这意味着依赖项可以访问未声明的依赖项,但 node_modules 之外的模块不行。 通过这种布局,生态系统中的大多数的包都可以正常工作。 但是,如果某些工具仅在提升的依赖项位于根目录的 node_modules 时才有效,您可以将其设置为 true 来为您提升它们。
shamefully-hoist = true
运行工作区npm脚本
- 运行docs的dev脚本
pnpm --filter docs dev
- 在根package.json中运行工作区脚本
pnpm docs:dev
"scripts": {
"docs:dev": "pnpm --filter docs dev"
}
最后
本文介绍了在npm、yarn、pnpm中使用workspaces的方式,基本功能都有实现,但pnpm确实提供了npm和yarn所没有的功能,可以根据项目的需要具体选择使用哪个
下一篇: BIMSpace2021 自动扶梯布局
推荐阅读
-
在一篇文章中掌握 godoc 的使用和规范
-
在一篇文章中掌握使用 npm、yarn 和 pnpm 的工作区!
-
windows下进程间通信的(13种方法)-摘 要 本文讨论了进程间通信与应用程序间通信的含义及相应的实现技术,并对这些技术的原理、特性等进行了深入的分析和比较。 ---- 关键词 信号 管道 消息队列 共享存储段 信号灯 远程过程调用 Socket套接字 MQSeries 1 引言 ---- 进程间通信的主要目的是实现同一计算机系统内部的相互协作的进程之间的数据共享与信息交换,由于这些进程处于同一软件和硬件环境下,利用操作系统提供的的编程接口,用户可以方便地在程序中实现这种通信;应用程序间通信的主要目的是实现不同计算机系统中的相互协作的应用程序之间的数据共享与信息交换,由于应用程序分别运行在不同计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换。进程间通信和应用程序间通信及相应的实现技术有许多相同之处,也各有自己的特色。即使是同一类型的通信也有多种的实现方法,以适应不同情况的需要。 ---- 为了充分认识和掌握这两种通信及相应的实现技术,本文将就以下几个方面对这两种通信进行深入的讨论:问题的由来、解决问题的策略和方法、每种方法的工作原理和实现、每种实现方法的特点和适用的范围等。 2 进程间的通信及其实现技术 ---- 用户提交给计算机的任务最终都是通过一个个的进程来完成的。在一组并发进程中的任何两个进程之间,如果都不存在公共变量,则称该组进程为不相交的。在不相交的进程组中,每个进程都独立于其它进程,它的运行环境与顺序程序一样,而且它的运行环境也不为别的进程所改变。运行的结果是确定的,不会发生与时间相关的错误。 ---- 但是,在实际中,并发进程的各个进程之间并不是完全互相独立的,它们之间往往存在着相互制约的关系。进程之间的相互制约关系表现为两种方式: ---- (1) 间接相互制约:共享CPU ---- (2) 直接相互制约:竞争和协作 ---- 竞争——进程对共享资源的竞争。为保证进程互斥地访问共享资源,各进程必须互斥地进入各自的临界段。 ---- 协作——进程之间交换数据。为完成一个共同任务而同时运行的一组进程称为同组进程,它们之间必须交换数据,以达到协作完成任务的目的,交换数据可以通知对方可以做某事或者委托对方做某事。 ---- 共享CPU问题由操作系统的进程调度来实现,进程间的竞争和协作由进程间的通信来完成。进程间的通信一般由操作系统提供编程接口,由程序员在程序中实现。UNIX在这个方面可以说最具特色,它提供了一整套进程间的数据共享与信息交换的处理方法——进程通信机制(IPC)。因此,我们就以UNIX为例来分析进程间通信的各种实现技术。 ---- 在UNIX中,文件(File)、信号(Signal)、无名管道(Unnamed Pipes)、有名管道(FIFOs)是传统IPC功能;新的IPC功能包括消息队列(Message queues)、共享存储段(Shared memory segment)和信号灯(Semapores)。 ---- (1) 信号 ---- 信号机制是UNIX为进程中断处理而设置的。它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。例如在发生浮点错、非法内存访问、执行无效指令、某些按键(如ctrl-c、del等)等都会产生一个信号,操作系统就会调用有关的系统调用或用户定义的处理过程来处理。 ---- 信号处理的系统调用是signal,调用形式是: ---- signal(signalno,action) ---- 其中,signalno是规定信号编号的值,action指明当特定的信号发生时所执行的动作。 ---- (2) 无名管道和有名管道 ---- 无名管道实际上是内存中的一个临时存储区,它由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出方式管理,并严格按顺序操作,例如不能对管道进行搜索,管道中的信息只能读一次。 ---- 无名管道只能用于两个相互协作的进程之间的通信,并且访问无名管道的进程必须有共同的祖先。 ---- 系统提供了许多标准管道库函数,如: pipe——打开一个可以读写的管道; close——关闭相应的管道; read——从管道中读取字符; write——向管道中写入字符; ---- 有名管道的操作和无名管道类似,不同的地方在于使用有名管道的进程不需要具有共同的祖先,其它进程,只要知道该管道的名字,就可以访问它。管道非常适合进程之间快速交换信息。 ---- (3) 消息队列(MQ) ---- 消息队列是内存中独立于生成它的进程的一段存储区,一旦创建消息队列,任何进程,只要具有正确的的访问权限,都可以访问消息队列,消息队列非常适合于在进程间交换短信息。 ---- 消息队列的每条消息由类型编号来分类,这样接收进程可以选择读取特定的消息类型——这一点与管道不同。消息队列在创建后将一直存在,直到使用msgctl系统调用或iqcrm -q命令删除它为止。 ---- 系统提供了许多有关创建、使用和管理消息队列的系统调用,如: ---- int msgget(key,flag)——创建一个具有flag权限的MQ及其相应的结构,并返回一个唯一的正整数msqid(MQ的标识符); ---- int msgsnd(msqid,msgp,msgsz,msgtyp,flag)——向队列中发送信息; ---- int msgrcv(msqid,cmd,buf)——从队列中接收信息; ---- int msgctl(msqid,cmd,buf)——对MQ的控制操作; ---- (4) 共享存储段(SM) ---- 共享存储段是主存的一部分,它由一个或多个独立的进程共享。各进程的数据段与共享存储段相关联,对每个进程来说,共享存储段有不同的虚拟地址。系统提供的有关SM的系统调用有: ---- int shmget(key,size,flag)——创建大小为size的SM段,其相应的数据结构名为key,并返回共享内存区的标识符shmid; ---- char shmat(shmid,address,flag)——将当前进程数据段的地址赋给shmget所返回的名为shmid的SM段; ---- int shmdr(address)——从进程地址空间删除SM段; ---- int shmctl (shmid,cmd,buf)——对SM的控制操作; ---- SM的大小只受主存限制,SM段的访问及进程间的信息交换可以通过同步读写来完成。同步通常由信号灯来实现。SM非常适合进程之间大量数据的共享。 ---- (5) 信号灯 ---- 在UNIX中,信号灯是一组进程共享的数据结构,当几个进程竞争同一资源时(文件、共享内存或消息队列等),它们的操作便由信号灯来同步,以防止互相干扰。 ---- 信号灯保证了某一时刻只有一个进程访问某一临界资源,所有请求该资源的其它进程都将被挂起,一旦该资源得到释放,系统才允许其它进程访问该资源。信号灯通常配对使用,以便实现资源的加锁和解锁。 ---- 进程间通信的实现技术的特点是:操作系统提供实现机制和编程接口,由用户在程序中实现,保证进程间可以进行快速的信息交换和大量数据的共享。但是,上述方式主要适合在同一台计算机系统内部的进程之间的通信。 3 应用程序间的通信及其实现技术 ---- 同进程之间的相互制约一样,不同的应用程序之间也存在竞争和协作的关系。UNIX操作系统也提供一些可用于应用程序之间实现数据共享与信息交换的编程接口,程序员可以通过自己编程来实现。如远程过程调用和基于TCP/IP协议的套接字(Socket)编程。但是,相对普通程序员来说,它们涉及的技术比较深,编程也比较复杂,实现起来困难较大。 ---- 于是,一种新的技术应运而生——通过将有关通信的细节完全掩盖在某个独立软件内部,即底层的通讯工作和相应的维护管理工作由该软件内部来实现,用户只需要将通信任务提交给该软件去完成,而不必理会它的具体工作过程——这就是所谓的中间件技术。 ---- 我们在这里分别讨论这三种常用的应用程序间通信的实现技术——远程过程调用、会话编程技术和MQSeries消息队列技术。其中远程过程调用和会话编程属于比较低级的方式,程序员参与的程度较深,而MQSeries消息队列则属于比较高级的方式,即中间件方式,程序员参与的程度较浅。 ---- 4.1 远程过程调用(RPC)