ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

9.1 KiB
Raw Blame History

React + Babel 项目规范

用HTML+React+Babel做原型时必须遵守的技术规范。不遵守会炸。

Pinned Script Tags必须用这些版本

在HTML的<head>里放这三个script tag固定版本+integrity hash

<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>

不要react@18react@latest这种unpinned版本——会出现版本漂移/缓存问题。

不要省略integrity——CDN一旦被劫持或篡改这是防线。

文件结构

项目名/
├── index.html               # 主HTML
├── components.jsx           # 组件文件type="text/babel"加载)
├── data.js                  # 数据文件
└── styles.css               # 额外CSS可选

HTML里加载方式

<!-- 先React+Babel -->
<script src="https://unpkg.com/react@18.3.1/..."></script>
<script src="https://unpkg.com/react-dom@18.3.1/..."></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/..."></script>

<!-- 然后你的组件文件 -->
<script type="text/babel" src="components.jsx"></script>
<script type="text/babel" src="pages.jsx"></script>

<!-- 最后主入口 -->
<script type="text/babel">
  const root = ReactDOM.createRoot(document.getElementById('root'));
  root.render(<App />);
</script>

不要type="module"——会和Babel冲突。

三条不可违反的规矩

规矩1styles 对象必须用唯一命名

错误(多组件时必炸):

// components.jsx
const styles = { button: {...}, card: {...} };

// pages.jsx  ← 同名覆盖!
const styles = { container: {...}, header: {...} };

正确每个组件文件的styles用唯一前缀。

// terminal.jsx
const terminalStyles = { 
  screen: {...}, 
  line: {...} 
};

// sidebar.jsx
const sidebarStyles = { 
  container: {...}, 
  item: {...} 
};

或者用inline styles(小组件推荐):

<div style={{ padding: 16, background: '#111' }}>...</div>

这条是非协商的。每次写const styles = {...}都必须replace成specific命名否则多组件加载时全栈报错。

规矩2Scope 不共享需手动export

关键认知:每个<script type="text/babel">被Babel独立编译它们之间scope不通components.jsx里定义的Terminal组件,在pages.jsx默认是undefined

解决方式:在每个组件文件末尾,把要共享的组件/工具export到window

// components.jsx 末尾
function Terminal(props) { ... }
function Line(props) { ... }
const colors = { green: '#...', red: '#...' };

Object.assign(window, {
  Terminal, Line, colors,
  // 所有你要在别处用的都列在这里
});

然后pages.jsx就能直接用<Terminal />因为JSX会去window.Terminal找。

规矩3不要用 scrollIntoView

scrollIntoView会把整个HTML容器往上推搞坏web harness的布局。永远不要用

替代方案:

// 滚到容器内某个位置
container.scrollTop = targetElement.offsetTop;

// 或者用element.scrollTo
container.scrollTo({
  top: targetElement.offsetTop - 100,
  behavior: 'smooth'
});

调 Claude APIHTML内

部分原生 design-agent 环境(如 Claude.ai Artifacts有免配置的 window.claude.complete,但大部分 agent 环境Claude Code / Codex / Cursor / Trae / etc.)本地里没有

如果你的 HTML 原型需要调用 LLM 做 demo比如做个聊天 interface两个选项

选项A不真调用mock

Demo场景推荐。写一个假helper返回预设的response

window.claude = {
  async complete(prompt) {
    await new Promise(r => setTimeout(r, 800)); // 模拟延迟
    return "这是一个mock响应。真部署时请替换为真API。";
  }
};

选项B真调Anthropic API

需要API key用户必须在HTML里填入自己的key才能跑。永远不要把key硬编码在HTML里

<input id="api-key" placeholder="粘贴你的Anthropic API key" />
<script>
window.claude = {
  async complete(prompt) {
    const key = document.getElementById('api-key').value;
    const res = await fetch('https://api.anthropic.com/v1/messages', {
      method: 'POST',
      headers: {
        'x-api-key': key,
        'anthropic-version': '2023-06-01',
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        model: 'claude-haiku-4-5',
        max_tokens: 1024,
        messages: [{ role: 'user', content: prompt }]
      })
    });
    const data = await res.json();
    return data.content[0].text;
  }
};
</script>

注意浏览器直接调Anthropic API会遇到CORS问题。如果用户给你的预览环境不支持CORS bypass这条路不通。这时候用选项A mock或者告诉用户需要一个proxy后端。

选项 C用 agent 侧的 LLM 能力生成 mock 数据

如果只是本地演示用,可以在当前 agent 会话里临时调用该 agent 的 LLM 能力(或用户装的 multi-model 类 skill先生成 mock 响应数据,再硬编码写进 HTML。这样 HTML 运行时完全不依赖任何 API。

典型 HTML 起手模板

拷贝这个模板作为React原型的骨架

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Your Prototype Name</title>

  <!-- React + Babel pinned -->
  <script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
  <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
  <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>

  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    html, body { height: 100%; width: 100%; }
    body { 
      font-family: -apple-system, 'SF Pro Text', sans-serif;
      background: #FAFAFA;
      color: #1A1A1A;
    }
    #root { min-height: 100vh; }
  </style>
</head>
<body>
  <div id="root"></div>

  <!-- 你的组件文件 -->
  <script type="text/babel" src="components.jsx"></script>

  <!-- 主入口 -->
  <script type="text/babel">
    const { useState, useEffect } = React;

    function App() {
      return (
        <div style={{padding: 40}}>
          <h1>Hello</h1>
        </div>
      );
    }

    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(<App />);
  </script>
</body>
</html>

常见报错及解决

styles is not definedCannot read property 'button' of undefined → 你在一个文件里定义了const styles另一个文件覆盖了。给每个改成specific命名。

Terminal is not defined → 跨文件引用时scope不通。在定义Terminal的文件末尾加Object.assign(window, {Terminal})

整个页面白屏,控制台没错误 → 多半是JSX语法错误但Babel没报在控制台。把babel.min.js临时换成babel.js非压缩版,错误信息更清晰。

ReactDOM.createRoot is not a function → 版本不对。确认用了react-dom@18.3.1而不是17或其他

Objects are not valid as a React child → 你渲染了一个对象而不是JSX/字符串。通常是{someObj}写成了{someObj.name}

大项目怎么拆文件

>1000行的单文件难维护。分拆思路:

项目/
├── index.html
├── src/
│   ├── primitives.jsx      # 基础元素Button、Card、Badge...
│   ├── components.jsx      # 业务组件UserCard、PostList...
│   ├── pages/
│   │   ├── home.jsx        # 首页
│   │   ├── detail.jsx      # 详情页
│   │   └── settings.jsx    # 设置页
│   ├── router.jsx          # 简单路由React state切换
│   └── app.jsx             # 入口组件
└── data.js                 # mock data

HTML里按顺序加载

<script type="text/babel" src="src/primitives.jsx"></script>
<script type="text/babel" src="src/components.jsx"></script>
<script type="text/babel" src="src/pages/home.jsx"></script>
<script type="text/babel" src="src/pages/detail.jsx"></script>
<script type="text/babel" src="src/pages/settings.jsx"></script>
<script type="text/babel" src="src/router.jsx"></script>
<script type="text/babel" src="src/app.jsx"></script>

每个文件末尾都要Object.assign(window, {...})导出要共享的东西。