关于 expo-dom
expo-dom 是一项 Expo 技能,指导如何在原生应用的 WebView 中运行 Web React 组件,并在 Web 上直接渲染。适合需要使用仅限 Web 的库或将现有 Web 界面带入 Expo 的团队。
主要功能
- 01在原生 WebView 中运行 Web React 代码
- 02使用仅限 Web 的 React 库
- 03通过异步属性传递原生操作
- 04使用 DOMProps 配置 WebView
- 05在 DOM 组件中使用 Expo Router
适用场景
- 01在 Expo 应用中添加图表或语法高亮
- 02在原生应用中复用现有 React Web 组件
- 03嵌入需要浏览器上下文的内容
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ expo/ skills --skill expo-dom安装后在 Claude Code 中输入/expo-dom确认已加载。
附带文件agents/openai.yaml
详细介绍
什么是 DOM 组件?
DOM 组件允许网页代码在原生平台的 WebView 中原样运行,同时在 Web 上保持原样渲染。这样一来,你可以在 Expo 应用中使用 recharts、react-syntax-highlighter 等仅限 Web 的库,或任何 React Web 库,无需修改。
何时使用 DOM 组件
在以下情况下使用 DOM 组件:
- 仅限 Web 的库 — 图表(recharts、chart.js)、语法高亮工具、富文本编辑器,或任何依赖 DOM API 的库
- 迁移 Web 代码 — 将现有的 React Web 组件移植到原生平台,无需重写
- 复杂的 HTML/CSS 布局 — React Native 不支持所需 CSS 功能时
- iframe 或嵌入内容 — 嵌入需要浏览器上下文的外部内容
- Canvas 或 WebGL — 原生平台不支持的 Web 图形 API
何时不应使用 DOM 组件
在以下情况下避免使用 DOM 组件:
- 原生性能至关重要 — WebView 会增加开销
- 简单 UI — 对于基本布局,React Native 组件效率更高
- 深度原生集成 — 原生 API 应使用本地模块
- 布局路由 —
_layout文件不能是 DOM 组件
基本 DOM 组件
新建文件,并在顶部添加 'use dom'; 指令:
// components/WebChart.tsx
"use dom";
export default function WebChart({
data,
}: {
data: number[];
dom: import("expo/dom").DOMProps;
}) {
return (
<div style={{ padding: 20 }}>
<h2>Chart Data</h2>
<ul>
{data.map((value, i) => (
<li key={i}>{value}</li>
))}
</ul>
</div>
);
}
DOM 组件规则
- 必须在文件顶部添加
'use dom';指令 - 只能有一个默认导出 — 每个文件对应一个 React 组件
- 必须单独放在一个文件中 — 不能内联定义,也不能与原生组件合并
- 仅支持可序列化的属性 — 字符串、数字、布尔值、数组、普通对象
- 在组件文件中包含 CSS — DOM 组件在隔离的上下文中运行
dom 属性
每个 DOM 组件都会接收一个用于配置 WebView 的特殊 dom 属性。请始终在属性类型中声明它:
"use dom";
interface Props {
content: string;
dom: import("expo/dom").DOMProps;
}
export default function MyComponent({ content }: Props) {
return <div>{content}</div>;
}
常见的 dom 属性选项
// Disable body scrolling
<DOMComponent dom={{ scrollEnabled: false }} />
// Flow under the notch (disable safe area insets)
<DOMComponent dom={{ contentInsetAdjustmentBehavior: "never" }} />
// Control size manually
<DOMComponent dom={{ style: { width: 300, height: 400 } }} />
// Combine options
<DOMComponent
dom={{
scrollEnabled: false,
contentInsetAdjustmentBehavior: "never",
style: { width: '100%', height: 500 }
}}
/>
向 WebView 暴露原生操作
将异步函数作为属性传入,即可向 DOM 组件暴露原生功能:
// app/index.tsx (native)
import { Alert } from "react-native";
import DOMComponent from "@/components/dom-component";
export default function Screen() {
return (
<DOMComponent
showAlert={async (message: string) => {
Alert.alert("From Web", message);
}}
saveData={async (data: { name: string; value: number }) => {
// Save to native storage, database, etc.
console.log("Saving:", data);
return { success: true };
}}
/>
);
}
// components/dom-component.tsx
"use dom";
interface Props {
showAlert: (message: string) => Promise<void>;
saveData: (data: {
name: string;
value: number;
}) => Promise<{ success: boolean }>;
dom?: import("expo/dom").DOMProps;
}
export default function DOMComponent({ showAlert, saveData }: Props) {
const handleClick = async () => {
await showAlert("Hello from the webview!");
const result = await saveData({ name: "test", value: 42 });
console.log("Save result:", result);
};
return <button onClick={handleClick}>Trigger Native Action</button>;
}
使用 Web 库
DOM 组件可以使用任何 Web 库:
// components/syntax-highlight.tsx
"use dom";
import SyntaxHighlighter from "react-syntax-highlighter";
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";
interface Props {
code: string;
language: string;
dom?: import("expo/dom").DOMProps;
}
export default function SyntaxHighlight({ code, language }: Props) {
return (
<SyntaxHighlighter language={language} style={docco}>
{code}
</SyntaxHighlighter>
);
}
// components/chart.tsx
"use dom";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
} from "recharts";
interface Props {
data: Array<{ name: string; value: number }>;
dom: import("expo/dom").DOMProps;
}
export default function Chart({ data }: Props) {
return (
<LineChart width={400} height={300} data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="value" stroke="#8884d8" />
</LineChart>
);
}
DOM 组件中的 CSS
由于 CSS 导入的代码会在隔离的上下文中运行,因此必须在 DOM 组件文件中导入 CSS:
// components/styled-component.tsx
"use dom";
import "@/styles.css"; // CSS file in same directory
export default function StyledComponent({
dom,
}: {
dom: import("expo/dom").DOMProps;
}) {
return (
<div className="container">
<h1 className="title">Styled Content</h1>
</div>
);
}
或者使用内联样式 / CSS-in-JS:
"use dom";
const styles = {
container: {
padding: 20,
backgroundColor: "#f0f0f0",
},
title: {
fontSize: 24,
color: "#333",
},
};
export default function StyledComponent({
dom,
}: {
dom: import("expo/dom").DOMProps;
}) {
return (
<div style={styles.container}>
<h1 style={styles.title}>Styled Content</h1>
</div>
);
}
DOM 组件中的 Expo Router
expo-router 的 <Link /> 组件和路由器 API 可在 DOM 组件中使用:
"use dom";
import { Link, useRouter } from "expo-router";
export default function Navigation({
dom,
}: {
dom: import("expo/dom").DOMProps;
}) {
const router = useRouter();
return (
<nav>
<Link href="/about">About</Link>
<button onClick={() => router.push("/settings")}>Settings</button>
</nav>
);
}
需要通过属性传入的路由器 API
这些钩子无法直接在 DOM 组件中使用,因为它们需要同步访问原生路由状态:
useLocalSearchParams()useGlobalSearchParams()usePathname()useSegments()useRootNavigation()useRootNavigationState()
解决方案: 在原生父组件中读取这些值,并作为属性传入:
// app/[id].tsx (native)
import { useLocalSearchParams, usePathname } from "expo-router";
import DOMComponent from "@/components/dom-component";
export default function Screen() {
const { id } = useLocalSearchParams();
const pathname = usePathname();
return <DOMComponent id={id as string} pathname={pathname} />;
}
// components/dom-component.tsx
"use dom";
interface Props {
id: string;
pathname: string;
dom?: import("expo/dom").DOMProps;
}
export default function DOMComponent({ id, pathname }: Props) {
return (
<div>
<p>Current ID: {id}</p>
<p>Current Path: {pathname}</p>
</div>
);
}
检测 DOM 环境
检查代码是否正在 DOM 组件中运行:
"use dom";
import { IS_DOM } from "expo/dom";
export default function Component({
dom,
}: {
dom?: import("expo/dom").DOMProps;
}) {
return <div>{IS_DOM ? "Running in DOM component" : "Running natively"}</div>;
}
资源
优先使用 require 加载资源,而不是使用 public 目录:
"use dom";
// Good - bundled with the component
const logo = require("../assets/logo.png");
export default function Component({
dom,
}: {
dom: import("expo/dom").DOMProps;
}) {
return <img src={logo} alt="Logo" />;
}
在原生组件中使用
像常规组件一样导入和使用 DOM 组件:
// app/index.tsx
import { View, Text } from "react-native";
import WebChart from "@/components/web-chart";
import CodeBlock from "@/components/code-block";
export default function HomeScreen() {
return (
<View style={{ flex: 1 }}>
<Text>Native content above</Text>
<WebChart data={[10, 20, 30, 40, 50]} dom={{ style: { height: 300 } }} />
<CodeBlock
code="const x = 1;"
language="javascript"
dom={{ scrollEnabled: true }}
/>
<Text>Native content below</Text>
</View>
);
}
平台行为
| 平台 | 行为 |
|---|---|
| iOS | 在 WKWebView 中渲染 |
| Android | 在 WebView 中渲染 |
| Web | 原样渲染(不使用 WebView 包装器) |
在 Web 上,由于不需要 WebView,因此会忽略 dom 属性。
提示
- DOM 组件支持在开发期间热重载
- 让 DOM 组件专注于特定功能 — 不要将整个屏幕放入 WebView
- 导航界面使用原生组件,专用内容使用 DOM 组件
- 在所有平台上测试 — Web 渲染效果与原生 WebView 可能略有不同
- 大型 DOM 组件可能影响性能 — 必要时进行性能分析
- WebView 拥有独立的 JavaScript 上下文 — 无法直接与原生端共享状态
提交反馈
如果你在此技能中发现错误、误导性信息或过时信息,请提交反馈,以便 Expo 改进:
npx --yes @expo/agent-cli@latest feedback --category skills --subject "expo-dom" "<actionable feedback>"
只有在有具体且可执行的反馈内容时才提交。请尽可能提供相关背景信息。
如果 Agent 反复无法完成 Expo 任务,或用户不得不介入,请使用 --category evals,并将相关 Expo 包或命令作为 --subject,简要报告任务内容、预期结果以及出现的问题。