使用 preact/compat 在 Preact 中运行 React 库
使用 preact/compat 在 Preact 中运行 React 库,并提供 Vite、webpack、Rollup、Jest、TypeScript 的 alias 配置及常见故障说明。
preact/compat 是一个兼容层,自 Preact X 起随主 preact 包一同发布。它将 React 的公共 API 映射到 Preact 上,使得大多数 React 库无需修改即可运行,而你的应用只需承载约 9.5KB 的框架体积,而非 React 那个大得多的运行时。
配置别名只是五分钟的活儿。但三天后才发现某个日期选择器从 node_modules 深处抛出错误——这部分没人会提前提醒你。启用方式是在打包工具中将 react 和 react-dom 别名指向 preact/compat:无需改动组件代码,也无需安装额外的包。本文将给出各主流工具链的确切别名配置、一份带有包体积收益数据的迁移演练,以及关于哪些库会出问题的坦诚说明。
核心要点
preact/compat内置于preact包中。兼容层现已进入核心代码库,因此独立的preact-compat包已被废弃,你只需执行npm install preact。- 整套机制就是为四条导入路径设置别名:
react、react-dom、react-dom/test-utils和react/jsx-runtime全部指向 Preact。 - 使用
@preact/preset-vite时,别名会自动配置,无需手写resolve.alias。 - 在 webpack 中,
react-dom别名必须列在react-dom/test-utils之下,否则范围更宽的规则会遮蔽 test-utils 的映射。 - 兼容层覆盖的是 React 的公共 API,而非其内部实现。那些深入访问
react-dom内部路径,或依赖最新 React 19 API 的库,仍可能出问题。
preact/compat 是什么?为何存在?
preact/compat 将 React 的公共 API 表面(React.Component、hooks、createPortal、forwardRef、memo、JSX 运行时)转译为 Preact 的对应实现,从而让针对 React 编写的第三方组件在构建期解析到 Preact。Preact 官方的 npm 页面正是以”只需一个别名即可获得广泛的 React 支持”作为卖点,而这种兼容性正是你无需重写即可复用 React 生态的关键。
现在已经没有名为 preact-compat 的包需要安装了。官方升级指南说明,该兼容层曾作为独立包发布,后被合并进核心仓库以简化协同维护,因此升级者需要把旧的 preact-compat 导入和别名替换为 preact/compat。那个非作用域包已是死路一条:它的 GitHub 仓库自 2021 年 12 月起已归档并设为只读,其 npm 页面也提示你卸载它,因为 Preact X 默认自带兼容层。Preact 10.x 是当前的稳定线,11.0.0 尚处于 release candidate 阶段而非正式发布;Preact releases 页面列出了确切的版本号。
别名就是全部诀窍
Discover how at OpenReplay.com.
整套机制就是设置别名:把 react、react-dom、react-dom/test-utils 和 react/jsx-runtime 指向 Preact,这样所有现有的导入语句——包括 node_modules 深处的第三方库——都会解析到 preact/compat 而非 React。你的组件代码无需任何改动。import { useState } from 'react' 这行语句原样保留;改变的只是打包工具对 react 的解析目标。
以下是 Preact 官方指南 将 React 别名为 Preact 中给出的四条标准条目:
| 导入路径 | 别名目标 | 原因 |
|---|---|---|
react | preact/compat | React 核心 API |
react-dom/test-utils | preact/test-utils | 测试工具 |
react-dom | preact/compat | DOM 渲染器(必须位于 test-utils 之下) |
react/jsx-runtime | preact/jsx-runtime | 自动 JSX 转换 |
只为 react 和 react-dom 设置别名——这是老教程里常见的偷懒做法——会导致那些导入 JSX 运行时或 react-dom/test-utils 的库仍然解析到 React,从而把你本想削减的字节又带了回来。
各工具链的别名配置
Vite(推荐默认方案)
使用 @preact/preset-vite 时,别名会自动配置,你不应再手写 resolve.alias。该预设会替你启用 React 别名:由 reactAliasesEnabled 选项控制,除非显式关闭,否则默认为 true。它同时也会为你配置好 JSX 转换。
// vite.config.ts
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
export default defineConfig({
plugins: [preact()], // JSX + react→preact/compat aliasing handled automatically
});
如果你在不使用该预设的情况下运行 Vite,则需添加手动回退配置:
export default defineConfig({
resolve: {
alias: {
react: 'preact/compat',
'react-dom/test-utils': 'preact/test-utils',
'react-dom': 'preact/compat',
'react/jsx-runtime': 'preact/jsx-runtime',
},
},
});
Webpack
在 webpack 中,react-dom 别名必须列在 react-dom/test-utils 之下,否则范围更宽的 react-dom 规则会遮蔽 test-utils 的映射,导致测试工具悄无声息地解析到错误的模块。
const config = {
resolve: {
alias: {
react: 'preact/compat',
'react-dom/test-utils': 'preact/test-utils',
'react-dom': 'preact/compat', // Must be below test-utils
'react/jsx-runtime': 'preact/jsx-runtime',
},
},
};
Rollup
安装 @rollup/plugin-alias 并将其注册在 @rollup/plugin-node-resolve 之前,以确保路径重写发生在 Rollup 解析模块之前。
import alias from '@rollup/plugin-alias';
export default {
plugins: [
alias({
entries: [
{ find: 'react', replacement: 'preact/compat' },
{ find: 'react-dom/test-utils', replacement: 'preact/test-utils' },
{ find: 'react-dom', replacement: 'preact/compat' },
{ find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' },
],
}),
],
};
Node / Next.js(无打包工具别名)
Node 运行时会忽略打包工具的别名配置,Next.js 也不例外,因此别名要写在 package.json 中,使用已发布的 @preact/compat 包。这个带作用域的包存在的唯一目的,就是给 npm 内置的别名机制提供一个指向目标;它的全部职责就是原样再导出 preact/compat。注意:带作用域的 @preact/compat 与已废弃的非作用域 preact-compat 不是一回事。
{
"dependencies": {
"react": "npm:@preact/compat",
"react-dom": "npm:@preact/compat"
}
}
Jest
Jest 通过 moduleNameMapper 下的正则条目重写模块路径:
{
"moduleNameMapper": {
"^react$": "preact/compat",
"^react-dom/test-utils$": "preact/test-utils",
"^react-dom$": "preact/compat",
"^react/jsx-runtime$": "preact/jsx-runtime"
}
}
TypeScript
TypeScript 的类型解析独立于打包工具,因此需要在 tsconfig.json 中映射路径,并启用 skipLibCheck。之所以要开启 skipLibCheck,是因为少数 React 库依赖了兼容层未提供的类型,而对 node_modules 中每个 .d.ts 做完整检查会在这些声明上失败。
{
"compilerOptions": {
"skipLibCheck": true,
"baseUrl": "./",
"paths": {
"react": ["./node_modules/preact/compat/"],
"react/jsx-runtime": ["./node_modules/preact/jsx-runtime"],
"react-dom": ["./node_modules/preact/compat/"],
"react-dom/*": ["./node_modules/preact/compat/*"]
}
}
}
一次最小化迁移演练
在现代工具链上将现有 React 应用迁移到 Preact,只需四步:
- 替换依赖。 移除
react、react-dom及其@types包(Preact 自带 TypeScript 类型),然后执行npm install preact和npm install -D @preact/preset-vite。 - 添加预设。 在 Vite 的 plugins 中加入
preact()。它会同时处理 JSX 转换和react → preact/compat别名,因此可以删掉旧版 Vite 2 时代遗留的手动esbuild.jsxInject/jsxFactory配置。 - 修改渲染入口。 把 React DOM 的挂载调用换成 Preact 的
render:
// Before
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
// After
import { render } from 'preact';
render(<App />, document.getElementById('root'));
- 构建并检查体积。 收益才是重点:Preact 核心加上
preact/compat约为 9.5KB(min+gzip),而 React 19 加 React DOM 接近 60KB,其中绝大部分来自react-dom/client入口。对于组件已经通过兼容层解析的应用来说,这个差距几乎是白捡的。
preact/compat 什么时候会出问题?
兼容层覆盖的是 React 的公共 API,而非其内部实现。那些深入访问 react-dom 私有内部路径的库,或依赖某些较新 React 19 API 的库,即便别名配置正确也可能出问题,因此上线前请逐一验证每个依赖。来自 React 类型库的类型不匹配是预期之内的,用 skipLibCheck 即可处理;真正需要警惕的是运行时故障,而这类集成问题在会话回放中通常表现为从依赖内部(而非你自己的代码)抛出的控制台错误。
在正式引入某个库之前,可做如下快速预检:
- grep 该包,查找深层的
react-dom/内部导入,这是最常见的故障信号。 - 检查该库依赖的 React 19 独有 API;请对照当前 Preact 版本核实覆盖情况,不要想当然。
- 用上面的 Jest
moduleNameMapper运行该库自带的测试套件,尽早捕获失败。 - 在开发环境做冒烟测试,留意控制台中源自该依赖内部的错误。
SSR 和框架用户会遇到另一类问题:由于打包工具的别名在 Node 中不生效,Next.js 及类似运行时需要在 package.json 中配置别名;此外 Vite 的 ssrLoadModule 路径可能绕过部分别名配置,因此请确认客户端和服务端都解析到了 preact/compat。
为你的工具链配好这四条别名,用同一套映射跑一遍依赖的测试,你就能以极小的字节代价复用大部分 React 生态。诚实地说,例外情况是那些耦合了 React 内部实现(而非公共 API)的库。不妨先在一个分支上加入 @preact/preset-vite,然后对比前后的生产包体积。
常见问题
@preact/compat 与旧的 preact-compat 包有什么区别?
带作用域的 @preact/compat 是一个仍在维护的 npm 包,它再导出 preact/compat,仅用于在 Next.js 等打包工具别名不生效的 Node 运行时中通过 package.json 为 react 设置别名。非作用域的 preact-compat 则是另一个已归档的包,其仓库自 2021 年 12 月起即为只读;它面向的是 Preact 8.x,而 Preact X 现已将兼容层内置于核心。切勿安装非作用域版本。
如果我使用 @preact/preset-vite,还需要手动写 resolve.alias 吗?
不需要。使用 @preact/preset-vite 时,将 react 和 react-dom 别名指向 preact/compat 的工作会自动完成,由 reactAliasesEnabled 选项控制,除非显式关闭否则默认开启。在 Vite plugins 中加入 preact() 即可同时处理 JSX 转换和别名映射,手写 resolve.alias 是多余的,还可能产生冲突。只有在不使用该预设运行 Vite 时,才需要手写那四条别名。
为什么在 webpack 中设置别名后,我的测试工具解析到了错误的模块?
因为在你的 webpack 配置中,react-dom 别名列在了 react-dom/test-utils 之上。webpack 会先匹配范围更宽的 react-dom 规则,从而遮蔽更具体的 test-utils 映射,导致测试工具悄无声息地解析到 preact/compat 而非 preact/test-utils。把 react-dom 条目放到 react-dom/test-utils 之下即可修复。Rollup 也有类似的顺序规则:把 @rollup/plugin-alias 放在 @rollup/plugin-node-resolve 之前。
为什么 preact/compat 别名配置正确,某个 React 库仍然会出问题?
因为兼容层映射的是 React 的公共 API,而非其内部实现。那些导入了深层 react-dom 内部路径,或依赖最新 React 19 API 的库,即便别名正确也可能在运行时失败。这类问题表现为从依赖内部(而非你自己的代码)抛出的控制台错误。在正式引入某个库之前,请 grep 其中的深层 react-dom/ 导入,并用 Jest 的 moduleNameMapper 运行它自带的测试套件。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k