mcpField
U
use-dom

use-dom

github.com/midudev开发7.0k58 次安装2 周前更新
代码仓库

关于 use-dom

面向 Expo 应用开发者的 DOM 组件使用指南,说明如何在原生平台 WebView 中运行 Web 代码,并在 Web 上直接渲染。

主要功能

  1. 01通过 'use dom' 声明 DOM 组件
  2. 02在 DOM 组件中使用 Web 专属库
  3. 03以可序列化属性传递数据
  4. 04通过异步函数向 WebView 暴露原生操作
  5. 05在组件内管理 CSS 与资源
  6. 06区分原生路由状态并通过属性传递

适用场景

  1. 01在 Expo 应用中使用图表库
  2. 02将现有 React Web 组件迁移到原生平台
  3. 03在 WebView 中呈现复杂 HTML/CSS
  4. 04嵌入需要浏览器环境的内容

安装

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

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

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

详细介绍

什么是 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