手把手教你理解 React:从端侧 AI 到组件化思维

想象一下:你在浏览器里打开一个网页,下载一个 AI 模型,然后直接在浏览器里和它对话。整个过程数据不出你的电脑,不需要联网,不需要 API Key,完全免费。

这就是要做的事------在浏览器里运行 DeepSeek R1 推理模型,用 WebGPU 硬件加速。

一、端侧模型:为什么要在浏览器里跑 AI?

有别于 OpenAI / DeepSeek API 调用,LLM 在远程,和调用客户端不在一起。

  • 不安全(context 会随着请求发送到服务器)

这揭示了云 API 的两个核心痛点:

:每次调用都按 token 计费。一个重度用户一天轻松消耗上百万 token,一个月就是几百上千元。

不安全:你说的话、你传的文件、你的代码上下文,全部发到了第三方服务器。对于公司内部代码、用户隐私数据来说,这个风险不可接受。

于是有了端侧模型的概念------模型跑在用户自己的设备上:

markdown 复制代码
云模型(API 调用)                端侧模型
─────────────────              ─────────────────
你的电脑 ──数据──► 服务器        你的电脑(模型就在本地)
                  │              │
                  ▼              ▼
              模型推理           模型推理(数据不出设备)
                  │
                  ▼
你的电脑 ◄──结果──              结果直接显示

端侧模型可以跑在:

  • ollama 本地部署(开发者常用)
  • 手机端(iOS / Android)
  • 汽车端(车载 AI)
  • 浏览器端(WebGPU)------这就是本项目的方式

浏览器的优势是:无需安装,随时随地,打开网页就能用

二、WebGPU:浏览器的 GPU 加速能力

项目中检测 WebGPU 支持的代码:

javascript 复制代码
const IS_WEBGPU_AVALABLE = !!navigator.gpu;

navigator.gpu 是浏览器提供的 API。如果浏览器支持 WebGPU,它返回一个 GPU 对象;如果不支持,返回 undefined。

!! 是双重取反,作用是把任意值转成布尔值。如果支持 WebGPU 则 IS_WEBGPU_AVALABLE 为 true,不支持的浏览器显示提示信息。

WebGPU 是 WebGL 的下一代,允许网页直接调用 GPU 进行并行计算。AI 模型推理本质上是大量矩阵运算,GPU 天生擅长这个。有了 WebGPU,浏览器里的 AI 推理速度可以接近原生应用。

三、技术栈选型:为什么是 React + TypeScript?

AI 时代的大型项目首选前端技术

  • React 比 Vue 难入门
  • 大型项目
  • AI 训练代码 React 的偏多

这不是说 Vue 不好。事实上笔记也承认"Vue 好入门"。但在 AI 领域,React 生态系统更成熟------HuggingFace 的 Transformers.js、各种模型的 Demo 页面、社区组件库,几乎都是 React。学 React 不是为了"更好",而是为了"和整个生态站在同一边"。

项目依赖(package.json)的精简程度也值得关注:核心只有 React、Vite、Tailwind CSS、TypeScript。没有 Redux、没有 Router、没有 UI 组件库------因为现在还不需要。从最小的依赖开始,按需添加。

四、项目是怎么跑起来的?

4.1 HTML 入口

html 复制代码
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>web-gpu</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

这个文件非常干净。它只做一件事:提供一个空的 div(id="root"),然后加载 main.tsx。后面所有 React 生成的 HTML 都会塞进这个 div 里。

type="module" 告诉浏览器:"用 ES Module 的方式加载这个文件"。这意味着 main.tsx 里可以用 import 语法。

4.2 React 启动入口

javascript 复制代码
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

逐行解释:

import { StrictMode } from 'react'------StrictMode 是 React 的"严格模式",开发时会帮你发现潜在问题(比如过时的 API、意外的副作用),生产环境不会生效。

import { createRoot } from 'react-dom/client'------createRoot 是 React 18 引入的新 API。它的作用是创建"React 的根节点"。之前 React 17 用的是 ReactDOM.render,18 之后统一用 createRoot。

createRoot(document.getElementById('root')!)------找到 HTML 里那个空的 div(id="root"),用 React 接管它。感叹号是 TypeScript 语法,告诉编译器"这个元素一定存在,不会是 null"。

.render(<StrictMode><App /></StrictMode>)------在这个"根"里渲染 App 组件。

五、第一次理解"组件":函数就是组件

函数有封装特性,组件的 HTML、CSS、JS 封装成一个组件

Vue 的组件是一个 .vue 文件,里面 <template><script><style> 三个区域分开写,像三明治。

React 的思路完全不同:组件就是一个函数,这个函数返回 HTML

javascript 复制代码
function App() {
  return (
    <div>Hello World</div>
  )
}

这整个函数就是一个组件。它接受输入(props,暂时没用到)、返回界面(JSX)、可以有内部逻辑(数据、事件处理等)。

六、JSX:在 JavaScript 里写 HTML

JSX 是 React 专业语法(模板),能在 JS 代码里直接写 HTML 标签,编译后转为原生 DOM 操作。React 最为骄傲的一大特性之一,非常方便地表达 UI 界面。

javascript 复制代码
return (
  <div className="flex flex-col h-screen">
    <h1>DeepSeek R1 WebGPU</h1>
  </div>
)

这段代码既不是 HTML,也不是纯 JavaScript。它是 JSX------一种"看起来像 HTML 的 JavaScript 扩展语法"。Vite 在编译时会把它转成真正的 JavaScript:

javascript 复制代码
// 编译前(JSX)
<div className="flex">Hello</div>

// 编译后(JavaScript 等价代码)
React.createElement('div', { className: 'flex' }, 'Hello')

为什么是 className 而不是 class?

class 是 JavaScript 里的关键字(用来声明类)。在 JSX 里写 class 会让编译器困惑------你到底是在写 HTML 的 class 属性,还是在声明一个 JavaScript 类?所以 React 统一用 className。这是历史原因,记住就行。

JSX 的本质 :它不是模板语言(像 Vue 的 {{ }} 或 Jinja 的 {% %})。JSX 就是 JavaScript,所以可以直接写 {变量名} 来嵌入表达式,可以直接写三元运算符,可以直接调用函数。

七、数据驱动界面:告别 DOM 编程

数据状态驱动界面状态,不需要 DOM 编程。数据(数据绑定 && 数据驱动)→ 数据状态(响应式,修改状态界面会跟着变)。数据有不同的状态,界面有不同的状态,川剧变脸一样。

传统写法是直接操作 DOM:

javascript 复制代码
document.getElementById('status').innerText = '加载中...'
document.getElementById('error').style.display = 'block'
document.getElementById('progress').style.width = '35%'

React 的写法完全不同:

javascript 复制代码
// 声明数据状态
const [status, setStatus] = useState(null)
const [error, setError] = useState(null)
const [progressItems, setProgressItems] = useState(...)

// 只修改数据,界面自动更新
setStatus('loading')
setError('模型加载失败')

// JSX 中根据状态显示不同界面
{status === 'loading' && <div>加载中...</div>}
{error && <div className="text-red-500">{error}</div>}

打个比方:DOM 编程就像你用手直接调整舞台上的每个演员------你走过去把演员 A 推到左边,把演员 B 的衣服换成红色。React 的数据驱动就像你改变剧本里的角色设定------"这个角色现在是受伤状态",然后舞台自动呈现受伤的效果。

八、useState:数据的三种状态

项目中定义了多个状态变量,每个代表界面的一个"变脸可能":

javascript 复制代码
const [status, setStatus] = useState(null);
// null     → 初始状态
// 'loading'→ 加载中
// 'ready'  → 模型就绪

const [error, setError] = useState(null);
// null    → 没有错误
// '出错了'→ 显示错误信息

const [loadingMessage, setLoadingMessage] = useState("");
// ''                   → 无消息
// '正在下载模型...'    → 显示提示

const [progressItems, setProgressItems] = useState([{
  file: 'model.onnx',      // 文件名
  progress: 0,             // 已下载字节数
  total: 147896325         // 总大小(约141MB)
}]);
// progress 从 0 到 total → 下载进度
// 用于显示进度条

useState 是 React 最基础的 Hook。调用它时会返回一个数组,第一项是当前值,第二项是修改值的函数。

为什么叫 Hook(钩子)? 因为它"钩住"了 React 的内部机制。当你调用 setStatus('loading') 时,React 知道你修改了状态,会自动重新渲染组件,界面随之更新。你不需要手动操作 DOM。

九、useEffect:组件什么时候做什么事

javascript 复制代码
useEffect(() => {
  console.log('组件已经挂载完成')
  // setTimeout(() => {
  //   setStatus('ready')
  // }, 2000)
}, [])

useEffect 叫"副作用 Hook"。副作用指的是那些与渲染无关、但需要在特定时机执行的操作------发网络请求、设置定时器、订阅事件等。

第二个参数 [](空数组)表示"只在组件第一次挂载时执行一次"。如果写成 [status],则每次 status 变化时都会执行。如果不传第二个参数,则每次渲染都执行。

三种情况对比:

第二个参数 执行时机
[] 只在组件首次渲染后执行一次
[status] 首次渲染 + 每次 status 变化时执行
不传 每次渲染后都执行(通常要避免)

十、Tailwind CSS:不写 CSS 的 CSS 方案

原子类 CSS 框架,提供一堆 CSS 类名(原子类)。不用写 CSS 了,选择器、CSS rules 太低效了。Vite 插件就可以使用,将我们声明的类名对应的样式提取出来加到代码里。原子类名,简单语义化很好,特别适合自然语言编程。

传统写 CSS 需要:给元素起类名 → 写选择器 → 写属性 → 写值。一个简单的水平居中要写三行 CSS。

Tailwind 的方式:flex items-center justify-center 一个类名一行搞定。它把每个 CSS 属性都拆成了独立的"原子类"------flex 就是 display: flex,text-center 就是 text-align: center

Tailwind 已经成为 vibe UI 的基本构成,自然语义编程

"自然语义编程"的意思是:你看到 flex items-center justify-center 就能直接理解"弹性布局、子元素水平居中、垂直居中",不需要去 CSS 文件里找对应的样式。

十一、App.tsx 完整结构拆解

11.1 状态声明区

javascript 复制代码
const [status, setStatus] = useState(null);
const [error, setError] = useState(null);
const [loadingMessage, setLoadingMessage] = useState("");
const [progressItems, setProgressItems] = useState([{...}]);

组件的第一部分:声明所有需要追踪的数据状态。这些状态决定了界面长什么样。

11.2 WebGPU 检测

javascript 复制代码
const IS_WEBGPU_AVALABLE = !!navigator.gpu;

不需要 useState,因为浏览器支不支持 WebGPU 在页面加载时就确定了,不会变。

11.3 生命周期

javascript 复制代码
useEffect(() => {
  console.log('组件已经挂载完成');
}, []);

组件挂载后执行。这里将来会放模型加载的逻辑。

11.4 条件渲染

javascript 复制代码
return (
  IS_WEBGPU_AVALABLE ? (
    // 支持的界面:标题、描述、模型链接
  ) : (
    <div>您的浏览器还不支持 WebGPU 加速</div>
  )
)

根据 WebGPU 是否可用,显示完全不同的界面。这是 React 最强大的能力之一------用一个变量控制整个页面的结构。

11.5 错误状态

javascript 复制代码
{error && (
  <div className="text-red-500 text-center mb-2">
    <p className="mb-1">Unable to load mode due to the</p>
    <p className="text-sm">{error}</p>
  </div>
)}

error && (...) 是短路运算符:如果 error 有值(不为 null),就显示错误框。没有错误时,这部分完全不渲染。

11.6 模型链接

javascript 复制代码
<a href="https://huggingface.co/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX"
   target="_blank" rel="noreferrer"
   className="font-medium underline">
  DeepSeek-R1-Distill-Qwen-1.5B
</a>

target="_blank" 在新标签页打开,rel="noreferrer" 是安全措施------防止新页面通过 window.opener 操控原页面。

链接指向的模型是 DeepSeek R1 的 1.5B 蒸馏版本,用 Qwen 架构,转成了 ONNX 格式。ONNX(Open Neural Network Exchange,开放神经网络交换格式)可以跨框架运行,让模型在浏览器里也能推理。

十二、两种界面状态完整对照

十三、关键概念速查

概念 一句话解释 本项目体现
JSX 在 JS 里写 HTML <div className="flex">...</div>
className 替代 class,因为 class 是 JS 关键字 className="flex"
useState 声明响应式数据 const [status, setStatus] = useState(null)
useEffect 在特定时机执行代码 useEffect(() => {...}, [])
条件渲染 根据数据决定渲染什么 {error && <div>错误</div>}
三元渲染 二选一的界面 IS_WEBGPU_AVALABLE ? 界面A : 界面B
数据驱动 改数据 → 界面自动变 setStatus('ready') 无需操作 DOM
Tailwind 原子类 CSS,不用写 CSS flex flex-col items-center

从 0 到 1 理解 React 确实比 Vue 更难------Vue 的三明治结构(template / script / style)更符合直觉。但 React 的"一切皆函数"在大型项目中能提供更强的抽象能力。正如笔记所说,AI 时代的趋势已经摆在这里,值得投入时间去掌握。

相关推荐
SimonKing9 小时前
Agnes AI出桌面版了,可图可视频,免费用
java·后端·程序员
CodeSheep9 小时前
有这4个迹象,你就该离职了!
前端·后端·程序员
极客小俊10 小时前
我熬夜3天!终于搞定了VMware 2026官网免费下载的终极避坑指南
程序员
这个DBA有点耶1 天前
索引碎片与统计信息维护:执行计划突然变差的隐形杀手
mysql·程序员·架构
爱勇宝1 天前
AI不会淘汰所有人,但会淘汰这6种人
前端·后端·程序员
摸鱼师moko1 天前
AI 来了之后,我的心流状态去哪了
人工智能·程序员
wing981 天前
通往全干之路之:被迫成为全栈
前端·后端·程序员
Hilaku1 天前
为什么很多人觉得前端很简单?
前端·javascript·程序员
不简说1 天前
JS 代码技巧 vol.6 — 20 个性能优化野路子,从渲染到网络全栈提速
前端·javascript·程序员