mcpField
U
use-dom

use-dom

github.com/expo开发3.5k30.3k 次安装今天更新
代码仓库

关于 use-dom

use-dom 指导开发 Expo DOM Components:网页代码在原生平台的 WebView 中运行,在 Web 上直接渲染。它帮助 React Native 开发者复用网页库和现有 React 组件,无需重写。

主要功能

  1. 01在原生 WebView 中运行网页代码
  2. 02在 Web 上直接渲染
  3. 03使用仅限 Web 的 React 库
  4. 04向组件传递可序列化属性
  5. 05通过异步属性暴露原生操作

适用场景

  1. 01添加基于网页库的图表
  2. 02复用现有 React 网页组件
  3. 03嵌入依赖浏览器环境的内容
  4. 04渲染复杂 HTML 和 CSS 布局

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/expo/skills --skill use-dom

安装后在 Claude Code 中输入/use-dom确认已加载。

附带文件agents/openai.yaml

详细介绍

什么是 DOM 组件?

DOM 组件允许 Web 代码在原生平台的 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 组件规则

  1. 必须在文件顶部添加 'use dom'; 指令
  2. 单个默认导出 — 每个文件只能有一个 React 组件
  3. 独立文件 — 不能内联定义,也不能与原生组件合并
  4. 仅限可序列化的属性 — 字符串、数字、布尔值、数组、普通对象
  5. 在组件文件中包含 CSS — DOM 组件在隔离的上下文中运行

dom 属性

每个 DOM 组件都会接收一个特殊的 dom 属性,用于配置 WebView。请始终在属性类型中声明它:

"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 组件文件中:

// 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

这些 Hook 无法直接在 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 上下文 — 无法与原生端直接共享状态

评论

支持 Markdown