mcpField
E
expo-use-dom-v55

expo-use-dom-v55

github.com/blockmatic开发1221 次安装5 周前更新
代码仓库

关于 expo-use-dom-v55

面向 Expo 应用开发者的技能指南,介绍如何用 DOM 组件在原生平台运行 Web 代码,并在 Web 上直接渲染。适用于需要复用 Web 库或复杂 HTML/CSS 的场景。

主要功能

  1. 01在原生平台的 WebView 中运行 Web 代码
  2. 02支持使用 Web 专属库
  3. 03通过属性向 WebView 传递原生操作
  4. 04配置 DOM 组件样式与路由
  5. 05说明平台差异及使用限制

适用场景

  1. 01在 Expo 应用中加入图表或语法高亮
  2. 02将现有 React Web 组件用于原生应用
  3. 03嵌入依赖浏览器环境的内容
  4. 04使用 Canvas 或 WebGL 图形

安装

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

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

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

附带文件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 组件规则

  1. 必须在文件顶部添加 'use dom'; 指令
  2. 只能有一个默认导出 — 每个文件对应一个 React 组件
  3. 必须单独放在一个文件中 — 不能内联定义,也不能与原生组件合并
  4. 仅支持可序列化的属性 — 字符串、数字、布尔值、数组、普通对象
  5. 在组件文件中包含 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 submit-expo-feedback@latest --category skills --subject "expo-dom" "<actionable feedback>"

仅在有具体且可执行的内容需要报告时提交。请尽可能提供相关上下文。 如果 AI Agent 反复失败,或用户不得不接手 Expo 任务,请加载 expo-skill-feedback 技能,并按照其 eval-candidate 流程操作,而不是重复使用上面的命令。

评论

支持 Markdown