12k
All articles

使用 preact/compat 在 Preact 中运行 React 库

使用 preact/compat 在 Preact 中运行 React 库,并提供 Vite、webpack、Rollup、Jest、TypeScript 的 alias 配置及常见故障说明。

OpenReplay Team
OpenReplay Team
使用 preact/compat 在 Preact 中运行 React 库

preact/compat 是一个兼容层,自 Preact X 起随主 preact 包一同发布。它将 React 的公共 API 映射到 Preact 上,使得大多数 React 库无需修改即可运行,而你的应用只需承载约 9.5KB 的框架体积,而非 React 那个大得多的运行时。

配置别名只是五分钟的活儿。但三天后才发现某个日期选择器从 node_modules 深处抛出错误——这部分没人会提前提醒你。启用方式是在打包工具中将 reactreact-dom 别名指向 preact/compat:无需改动组件代码,也无需安装额外的包。本文将给出各主流工具链的确切别名配置、一份带有包体积收益数据的迁移演练,以及关于哪些库会出问题的坦诚说明。

核心要点

  • preact/compat 内置于 preact 包中。兼容层现已进入核心代码库,因此独立的 preact-compat 包已被废弃,你只需执行 npm install preact
  • 整套机制就是为四条导入路径设置别名:reactreact-domreact-dom/test-utilsreact/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、createPortalforwardRefmemo、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 页面列出了确切的版本号。

别名就是全部诀窍

整套机制就是设置别名:把 reactreact-domreact-dom/test-utilsreact/jsx-runtime 指向 Preact,这样所有现有的导入语句——包括 node_modules 深处的第三方库——都会解析到 preact/compat 而非 React。你的组件代码无需任何改动。import { useState } from 'react' 这行语句原样保留;改变的只是打包工具对 react 的解析目标。

以下是 Preact 官方指南 将 React 别名为 Preact 中给出的四条标准条目:

导入路径别名目标原因
reactpreact/compatReact 核心 API
react-dom/test-utilspreact/test-utils测试工具
react-dompreact/compatDOM 渲染器(必须位于 test-utils 之下
react/jsx-runtimepreact/jsx-runtime自动 JSX 转换

只为 reactreact-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,只需四步:

  1. 替换依赖。 移除 reactreact-dom 及其 @types 包(Preact 自带 TypeScript 类型),然后执行 npm install preactnpm install -D @preact/preset-vite
  2. 添加预设。 在 Vite 的 plugins 中加入 preact()。它会同时处理 JSX 转换和 react → preact/compat 别名,因此可以删掉旧版 Vite 2 时代遗留的手动 esbuild.jsxInject / jsxFactory 配置。
  3. 修改渲染入口。 把 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'));
  1. 构建并检查体积。 收益才是重点: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 运行它自带的测试套件。

DevTools for the frontend

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

We use cookies to improve your experience. By using our site, you accept cookies.