NutUI-React 底层架构工具升级之从 npm 到 pnpm

2,261 阅读11分钟

NutUI 是一款京东风格的移动端组件库。NutUI 目前支持 Vue 和 React 技术栈,支持 Taro 多端适配。本次架构升级主要介绍 NutUI-React 在 npm 到 pnpm 的升级迁移、Taro 升级到 3.6 版本和底层构建工具 Vite2 升级到 Vite4 的过程中所经历的踩坑之旅。作为此系列的第一篇文章,希望可以给家人们带来一些帮助。

期待您早日成为我们共建大军中的一员!

GitHub:NutUI-VueNutUI-React

欢迎共建、使用!

一、pnpm 的优势

在进行工具迁移之前,我觉得首先我们应该思考的是,我们为什么要迁移升级?迁移的成本是否很高?迁移后的工具是否社区活跃、成熟稳定并且不埋雷?以及迁移升级后能给我们的组件库带来什么?

好,带着这些疑问,我们开始进入到 pnpm 的世界中进行探索~

pnpm 官网以及很多文章都对 pnpm 的优势进行了介绍,但我总结使用下来觉得 pnpm 相比于 npm、yarn 最牛*的几个杀手锏就是:

  • 安装速度极快;
  • 硬链接高效节约磁盘空间;
  • 软链接极致优化依赖管理;

下面简单介绍一下其杀手锏特性,详细的大家自行阅读 pnpm 官方中文文档哈~ pnpm 官方文档

二、pnpm 特性概览

1. 安装速度快

pnpm 安装速度到底有多快呢,废话不多说,直接上图:

clean install 可以理解为全新安装、with cache 带缓存

可以看到与 npm 和 yarn 相比,在绝大多数的场景中 pnpm 的安装速度都是极快的,甚至比 npm/yarn 快到2倍以上。

2. 硬链接高效节约磁盘空间

简单来说:pnpm 就是通过基于内容寻址的的方式来存储磁盘上的所有文件,这里我们引用官方文档对其中的一段介绍:

当使用 npm 时,如果你有 100 个项目,并且所有项目都有一个相同的依赖包,那么, 你在硬盘上就需要保存 100 份该相同依赖包的副本。然而,如果是使用 pnpm,依赖包将被存放在一个统一的位置,因此:

  1. 如果你对同一依赖包需要使用不同的版本,则仅有版本之间不同的文件会被存储起来。例如,如果某个依赖包包含 100 个文件,其发布了一个新版本,并且新版本中只有一个文件有修改,则 pnpm update 只需要添加一个新文件到存储中,而不会因为一个文件的修改而保存依赖包的 所有文件。
  2. 所有文件都保存在硬盘上的统一的位置。当安装软件包时, 其包含的所有文件都会硬链接自此位置,而不会占用额外的硬盘空间。这让你可以在项目之间方便地共享相同版本的依赖包。

所以基于这种方式,我们的安装速度才会如此之高,因为我们节省了大量的磁盘空间。

3. 软链接极致优化依赖管理

这里我们用 NutUI-React 实践结果来举例,首先我们要全局安装 pnpm 包:

npm i -g pnpm

在使用 npm 包安装我们的 NutUI-React 项目是这样的:

可以看到由于 npm 包的扁平化管理,node_modules 下安装的 npm 包巨多,你往下滑能滑到手抽筋,才能滑到 node_modules 目录下的结尾。

这里拓展下:有的家人们可能对 npm 包扁平化管理不太了解,这里我简单介绍下。就是我们在安装一个 npm 包的时候,npm 包也会有它的依赖,也就是我们平时说的深层依赖。那 npm 和 yarn 等上一代包管理器为了解决这种依赖层级过深的情况,于是采用了扁平化的方式全部平铺到了 node_modules 的目录下。

扁平化是有好处的,就比如我们要安装一个 npm 包,包管理器会在 node_modules 中去找,如果找到相同版本的包就不会重新安装,因而解决了大量包重复安装的问题。

但是扁平化也是有一些弊端的:

  • 比如扁平化依赖一些复杂的算法,性能消耗比较严重
  • 不同的项目对于相同的依赖包会重复下载,磁盘空间的利用率不足,且大量的解压、IO操作也会进一步降低执行效率。
  • 代码中可以使用一些并没有在 package.json 中定义的包,造成依赖非法访问的问题

这个时候我们再看一下用 pnpm 包安装我们的 NutUI-React 项目是这样的:

可以看到 node_modules 下安装的依赖包特别少,简单滑一下就到头了,特别清爽不油腻。

那有的家人们可能有疑问了?就比如说我安装的 npm 包 inquirer,inquirer 目录下也没有 node_modules 啊,它的依赖哪里去了呢?给整丢了?

别急哈,家人们,这正是 pnpm 的神奇之处!我们展开 .pnpm 目录就会发现原来依赖都藏在这里了啊!

这里我简单总结下,node_modules 下的 inquirer 包在这里其实就是一个软链接(常用 Windows 的家人们可以理解为快捷方式),那这个软链接真正指向的位置就是 .pnpm 目录中对应的包。

可见 .pnpm 中的 inquirer 才是其”真身”,而 node_modules 中的 inquirer 只不过是它的一个”傀儡“。

同理 .pnpm中inquirer 包需要的依赖真身也是以相同的结构安装在 .pnpm 中。于是现在所有包的依赖文件结构,都是与 package.json 中声明保持的一致了。

最后我们以这张图做一个收尾,看看在我啰嗦了这么多,大家能不能看看懂这张图。

三、NutUI-React 从 npm 到 pnpm 的迁移之旅

好了,介绍了这么多,我们要开始正式的迁移了:

首先在迁移成本上我觉得还是相当友好的,官方文档上也有对其迁移的详细介绍。于是我们先把 package-lock.json 文件和 node_modules 目录删除掉,然后开始 pnpm install 进行依赖的全部安装。

pnpm 安装的命令和 npm 的安装命令基本没差别哈

1. pnpm install 安装

要是不出意外的话,这个时候应该报错了,果然没有让我失望:

其实看一下报错,也好理解,因为 pnpm 没有自动为我们安装 peerDependencies。那按照它给的提示,把这些包安装一下就好啦~

注意: 如果大家在自己的项目中启动依旧报错的话,可以检查下是否使用了没有在 package.json 中声明的包。导致出现了依赖非法访问的问题。

2. pnpm run build 打包

安装没有问题了,这个时候我们要继续验证下 pnpm run dev 启动和 pnpm run build 打包是否存在问题

果然另我比较”欣慰“的是,在 pnpm run build 打包的时候......

这又是因为什么呢?经过排查发现,我们在打包的过程中用到了 rollup-plugin-dts 插件,导致将 TypeScript 代码转换为 .d.ts 声明文件的时候出现了错误。

2.1 rollup-plugin-dts 插件

这里我们简单说下 rollup-plugin-dts 这个插件,它是一个用于 Rollup 构建系统的插件。

在 TypeScript 项目中,.d.ts 文件是声明文件,用于描述 JavaScript 模块、类、变量、接口、函数等的类型信息。这些声明文件通常不会被 JavaScript 引擎解释执行,而是由编辑器和开发者工具使用,以提供更好的代码补全、类型检查和错误提示。

使用 rollup-plugin-dts,可以将 TypeScript 代码构建为包含 .d.ts 声明文件的 JavaScript 模块,使得其他开发者可以轻松地使用你的代码,并且获得完整的类型信息。

总的来说,rollup-plugin-dts 可以帮助开发者更好地管理和共享 TypeScript 代码的类型信息,提高代码的可重用性和可维护性。

详细写法如下:

import dts from 'rollup-plugin-dts'

const config = [
  {
    input: './dist/types/nutui.react.build.ts',
    output: [{ file: 'dist/types/index.d.ts', format: 'es' }],
    plugins: [
      dts({
        compilerOptions: {
          baseUrl: '.',
          paths: {
            '@/*': ['src/*'],
          },
        },
      }),
    ],
  },
]

export default config

那到底是哪里出现问题,导致这个 TypeScript 代码打包转换成 .d.ts 出现问题了呢?不急,让我们进入到源码中来看一下!!

在 rollup-plugin-dts 的源码 rollup-plugin-dts.mjs 文件中我们经过仔细排查,发现了问题:preserveSymlinks 这个初始值竟然是 true 呀

怪不得在编译类型定义文件时,很多文件的内容都找不到。那有的家人们估计有疑问了,这个 preserveSymlinks 属性值干啥的嘞,为啥用了 pnpm 安装就被它给影响了呢?接下来让我们一起看个透:

2.2 preserveSymlinks 属性值

我们先看定义:

  • 当 preserveSymlinks 设置为 true 时,rollup-plugin-dts 会保留符号链接,并将其视为链接本身。这意味着在编译类型定义文件时,将使用链接本身的内容,而不是链接目标文件的内容。
  • 当 preserveSymlinks 设置为 false 时,rollup-plugin-dts 会将符号链接解析为其所指向的文件,并将编译器输出写入这个文件中。这意味着在编译类型定义文件时,将使用链接目标文件的内容,而不是链接本身的内容。

不太理解的话,我们再看一个例子:

大家可以先自行花费 2 分钟阅读一下,根据刚才的定义猜测下 preserveSymlinks 为 false 或者 true 时,控制台分别打印什么值呢?

// /main.js
import { x } from './linked.js';
console.log(x);

// /linked.js
// this is a symbolic link to /nested/file.js

// /nested/file.js
export { x } from './dep.js';

// /dep.js
export const x = 'next to linked';

// /nested/dep.js
export const x = 'next to original';

好了,我们来揭晓谜底。preserveSymlinks 为 false 的话,则控制台打印 'next to original'。preserveSymlinks 为 true 的话,则控制台打印 'next to linked'。

为什么嘞?按照刚才的定义,preserveSymlinks 为 false 的话,解析文件时将遵循符号链接。当我们解析到 ./linked.js 文件时,发现里面有 symbolic link 指向 /nested/file.js,所以遵循符号链接,自然而然就指向到了 /nested/file.js 文件中:

// /nested/file.js
export { x } from './dep.js';

// /nested/dep.js
export const x = 'next to original';

/nested/file.js 文件中的 x 值来自同级目录 './dep.js',所以就自然而然指向了 /nested/dep.js 文件中,最后打印了 'next to original'。

我们再来看 preserveSymlinks 为 true 的情况,当设置为 true 时,符号链接将被视为文件就在链接所在的位置,而不是被跟随。也就是说下面这个文件就被视为在链接所在的位置,也就是在 /linked.js 这个位置。

export { x } from './dep.js';

那这个时候引用 './dep.js' 这个文件,就是和 './linked.js' 同级目录的 './dep.js' 文件,于是最后打印了 'next to linked'。

这个时候我们再回过头来看 rollup-plugin-dts 中这个 preserveSymlinks 值,再结合上 pnpm 软链接的特性,是不是脑袋瓜一下子就畅通了~

只有将 preserveSymlinks 默认值改为 false,才能根据 pnpm 软链接指向文件真正所在的位置,才不至于找不到对应的文件内容导致报错,于是将我们的代码修改如下就好咯~

import dts from 'rollup-plugin-dts'

const config = [
  {
    input: './dist/types/nutui.react.build.ts',
    output: [{ file: 'dist/types/index.d.ts', format: 'es' }],
    plugins: [
      dts({
        compilerOptions: {
          preserveSymlinks: false,
          baseUrl: '.',
          paths: {
            '@/*': ['src/*'],
          },
        },
      }),
    ],
  },
]

export default config

pnpm run build 通过 rollup-plugin-dts 打包 .d.ts 的问题也迎刃而解了。

四、pnpm 迁移之旅总结

pnpm 的团队目前也是正在积极发展中,未来发展趋势也更值得我们期待:

  • 更广泛的采用: 随着 pnpm 越来越受欢迎,它将被更广泛地采用,并成为 Node.js 生态系统的一部分。许多公司和组织已经在使用 pnpm,并且随着 pnpm 越来越成熟,它将被更多人所接受和使用。

  • 更快的安装速度: pnpm 采用硬链接的方式来安装包,这使得安装速度非常快。但是,pnpm 仍在寻求改进安装速度的方法。例如,PNPM 团队正在研究将包的元数据存储在 SQLite 数据库中的方法,以进一步提高安装速度。

  • 更多的功能: PNPM 团队正在考虑添加更多的功能,例如支持多个存储库的混合安装、支持多线程下载、增强包的完整性验证等等。这些功能将进一步提高 pnpm 的可用性和安全性。

  • 更好的兼容性: PNPM 团队正在努力使 pnpm 更好地与其他工具和生态系统兼容。例如,他们正在积极参与 Node.js Ecosystem Working Group(Node.js 生态系统工作组),与其它包管理器团队合作制定标准,以提高各种包管理器之间的兼容性。

总之,pnpm 是一个快速、安全、易于使用的包管理器,并且在未来将继续发展和改进,以更好地满足 Node.js 生态系统中开发者和用户的需求。