Vite解析内建模块错误

1、背景

这两年我所在的技术团队受到教育和互联网双重 Debuff 加持,团队人数从300+减少到20+,期间接手了很多前端项目,也遇到很多问题。比如有些项目本地启动速度很慢,有些项目配置复杂、过度封装,一系列问题使得开发体验很差。

为了提升开发体验,我在项目中引入 Vite 用于开发调试(不用于构建)。配置好之后启动项目,页面白屏了,浏览器控制台输出这个错误:Module "crypto" has been externalized for browser compatibility

Vite 配置与项目原先使用的 Webpack 配置基本一致,但是 Webpack 启动不会报错,这是什么情况。

2、问题分析

2.1、Vite 为什么报错

排查问题首先从错误栈看起,错误提示看起来不太直观,以前也没见过类似错误。然后顺着错误栈找到出错的主要代码:

typescript 复制代码
import { createHmac } from 'crypto';
createHmac('sha1', key).update(str2Sign).digest('base64');

可以看到这段代码使用了 Node.js 的内建模块 crypto,跟错误提示所述一致,其他没啥问题。一套操作下来没看出问题所在,好在错误提示里挂了 Vite官网说明链接中文版),官网说明如下:

当你在浏览器中使用一个 Node.js 模块时,Vite 会输出以下警告: Module "fs" has been externalized for browser compatibility. Cannot access "fs.readFile" in client code.

这是因为 Vite 不会自动 polyfill Node.js 的内建模块。 我们推荐你不要在浏览器中使用 Node.js 模块以减小包体积,尽管你可以为其手动添加 polyfill。如果该模块是被某个第三方库(这里意为某个在浏览器中使用的库)导入的,则建议向对应库提交一个 issue。

一句话概括就是:浏览器中使用 Node.js 的内建模块会报错,因为 Vite 不会自动 polyfill 内建模块,解决方案是不要使用内建模块或者手动 polyfill

如果有踩坑经验,这个问题基本就到此结束了。而我只觉得这好像说了什么又好像什么都没说。到底为什么会报错?

实际上 Vite Server 要把一份 JS 类的资源响应给浏览器且能正常运行,至少需要满足两个条件:

  • 1、原始代码能转译成 JavaScript
  • 2、转译后的代码能在浏览器环境下正常运行

以实际的 Vite 项目为例,下图中 index.tsx 的文件内容是转译后的 JavaScript 代码而非原始代码,另外这份代码在浏览器中可正常运行。

对内建模块而言无法完全满足上述条件。首先,内建模块底层调用的是 C++,转译为 JavaScript 的开发成本较高,甚至可能无法转译;其次,即使可以全部转译,内建模块需要 Node.js 运行时环境才能正常运行。

所以 Vite 能咋办,要么自动 polyfill 内建模块让开发者无感,要么抛错提示开发者进行处理。Vite 选择了抛错,原因应该是时代不断进步没必要一直背着历史包袱,可参考 Webpack官网博客中文版)。

之后进一步查阅 Vite 源码,发现处理内建模块时有一段特殊逻辑。其中,解析逻辑 是返回一个特殊的虚拟资源标识,加载逻辑 是返回一段定制的抛错代码(错误内容与开篇的抛错截图一致)。

需要注意的是,Vite 解析内建模块的错误不是在系统终端抛出,而是把抛错代码返回给浏览器,由浏览器抛出错误。这点与开篇的抛错截图相符。

2.2、Webpack 为什么不报错

文章开头提到项目原先使用 Webpack 启动开发服务不会报错,原因是项目原先使用的是 Webpack4,而 Webpack5 之前会自动 polyfill 内建模块,因此不报错。

随着时代发展情况发生了变化,从 Webpack官网博客中文版)可以看到,Webpack5Vite 采取一样的策略,不再自动 polyfill Node.js 内建模块。具体说明如下:

在早期,Webpack 的目的是为了让大多数的 Node.js 模块运行在浏览器中,但如今模块的格局已经发生了变化,现在许多模块主要是为前端而编写。Webpack <= 4 的版本中提供了许多 Node.js 核心模块的 polyfills,一旦某个模块引用了任何一个核心模块(如 cypto 模块),webpack 就会自动引用这些 polyfills。

尽管这会使得使用为 Node.js 编写模块变得容易,但它在构建时给 bundle 附加了庞大的 polyfills。在大部分情况下,这些 polyfills 并非必须。

从 Webpack 5 开始不再自动填充这些 polyfills,而会专注于前端模块兼容。我们的目标是提高 web 平台的兼容性。

迁移:

  • 尽量使用前端兼容的模块。
  • 可以手动为 Node.js 核心模块添加 polyfill。错误提示会告诉你如何实现。
  • Package 作者:在 package.json 中添加 browser 字段,使 package 与前端兼容。为浏览器提供其他的实现/dependencies。

为了验证自动 polyfill 内建模块这个变更是否属实,分别使用 Webpack4Webpack5 构建下面的 src/index.js 代码:

javascript 复制代码
// src/index.js

import { createHmac } from "crypto";
createHmac("sha1", "secret").update("data").digest("base64");

Webpack 使用如下相同配置:

javascript 复制代码
// webpack.config.js

const path = require("path");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
  },
};

Webpack4 构建成功,结果如下:

Webpack5 构建失败,结果如下:

可以看到 Webpack5 之后确实不再自动 polyfill Node.js 内建模块。我只是恰好在引入 Vite 时踩坑了,如果项目要升级到 Webpack5 必然也会踩到这个坑。

2.3、如何 polyfill

既然 Webpack 早就实现了这个功能,那这里就以 Webpack 为例简单说明如何 polyfill

Webpack 在编译代码时借助 node-libs-browser 把内建模块替换成兼容浏览器的第三方包(具体代码参见 Github),使其既能在 Node.js 中运行又能在浏览器中运行。简单点说就是,如果项目中使用了内建模块 cryptoWebpack 会在编译时使用兼容浏览器的包 crypto-browserify 去替换它。

node-libs-browserWebpack 团队开发的工具包,下图是这个包为内建模块与第三方兼容包建立的对应关系:

3、解决方案

Vite 官网给出的方案主要有两个方向:不使用内建模块或者手动 polyfill。这里以 crypto 这个内建模块为例,整理出4个解决方案,其中第一个是不使用内建模块,其他则是手动 polyfill

方案1、不使用内建模块

修改项目代码,将代码中引用的内建模块 crypto 全部替换为第三方兼容包 crypto-browserify。改动如下:

  • 安装依赖:npm install crypto-browserify --save
  • 替换依赖:将代码中引用的 crypto 全部替换为 crypto-browserify

如果条件允许,推荐使用这个方案。

方案2、编译时替换依赖

Vite 配置文件的 resolve.alias 选项中将 crypto 配置为 crypto-browserify,达到编译时替换依赖的效果。改动如下:

  • 安装依赖:npm install crypto-browserify --save
  • 指定 alias:在 Vite 配置文件的 resolve.alias 选项中添加配置 {crypto:'crypto-browserify'}

方案3、编译时替换依赖 + ESM 服务

这是编译时替换依赖方案的升级版,利用 ESM 特性将 alias 目标配置为 ESM 服务提供的在线文件。改动如下:

  • 指定 alias:在 Vite 配置文件的 resolve.alias 选项中添加配置 {crypto:'https://jspm.dev/crypto-browserify'}

这里省去了安装依赖的步骤,ESM 服务也有比较多选择,比如:jsdelivresm.sh 等,只是国内访问这些服务都不容易。

方案4、安装依赖使用别名

在项目根目录下安装 crypto-browserify,并将其目录名改为 crypto,编译时就会使用 node_modules/crypto 下的文件,实测可以正常启动项目。这里有很多改法,提供最简单的一个:

  • 安装依赖:npm install crypto@npm:crypto-browserify --save

这行命令的效果是:安装名为 crypto-browserifynpm 依赖,并且使用 crypto 作为目录名。

除了以上方案还有其他方案,比如定制 Vite 插件修改依赖的解析结果,还可以结合 import maps。玩法很多,可以自由发挥。

4、如何寻找兼容包

以上方案其实本质上都是使用兼容浏览器的第三方包替代 Node.js 内建模块,那如何找到合适的兼容包呢?有3个途径:

5、总结

Vite 不支持自动 polyfill Node.js 内建模块,浏览器端代码中引用内建模块可能会导致其报错。解决方案有:不使用内建模块、编译时替换依赖、编译时替换依赖 + ESM 服务、安装依赖使用别名等。这些方案本质上都是使用兼容浏览器的第三方包替代内建模块。寻找兼容包可以参考 Webpack5 之前的官方 polyfill 方案、Rollup 插件的 polyfill 方案、以及 browserify

此外,如果老项目需要升级到 Webpack5,可能也会遇到这个问题,解决方案可以参考本文与 Webpack 官网说明。

相关推荐
加班是不可能的,除非双倍日工资3 分钟前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi37 分钟前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip1 小时前
vite和webpack打包结构控制
前端·javascript
excel1 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国2 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼2 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy2 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT2 小时前
promise & async await总结
前端
Jerry说前后端2 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天2 小时前
A12预装app
linux·服务器·前端