⚛️ React 19 + TypeScript 深度学习笔记 ------ 从组件化思维到 WebGPU 端侧 AI 落地(续)
前言 :在上一篇文章中,我们系统学习了 React 19 的技术栈全景------从 JSX 语法、Hooks 原理到 TailwindCSS 工程化。本文作为续篇,将以
deepseek-r1-webgpu项目的教学笔记为主线,深入端侧 AI 架构、组件封装实战、组件树思维三大主题。阅读本文前,建议先理解基础的 React Hooks 和 JSX 语法。
📋 目录
- [端侧 AI:为什么要在浏览器里跑大模型](#端侧 AI:为什么要在浏览器里跑大模型 "#1-%E7%AB%AF%E4%BE%A7-ai%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E9%87%8C%E8%B7%91%E5%A4%A7%E6%A8%A1%E5%9E%8B")
- [React 组件 = 函数,函数就是组件](#React 组件 = 函数,函数就是组件 "#2-react-%E7%BB%84%E4%BB%B6--%E5%87%BD%E6%95%B0%E5%87%BD%E6%95%B0%E5%B0%B1%E6%98%AF%E7%BB%84%E4%BB%B6")
- [TailwindCSS:原子 CSS 的运行原理](#TailwindCSS:原子 CSS 的运行原理 "#3-tailwindcss%E5%8E%9F%E5%AD%90-css-%E7%9A%84%E8%BF%90%E8%A1%8C%E5%8E%9F%E7%90%86")
- [JSX 再探:className、{} 表达式与事件绑定](#JSX 再探:className、{} 表达式与事件绑定 "#4-jsx-%E5%86%8D%E6%8E%A2classname-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E4%B8%8E%E4%BA%8B%E4%BB%B6%E7%BB%91%E5%AE%9A")
- [React 合成事件:框架高手的代码洁癖](#React 合成事件:框架高手的代码洁癖 "#5-react-%E5%90%88%E6%88%90%E4%BA%8B%E4%BB%B6%E6%A1%86%E6%9E%B6%E9%AB%98%E6%89%8B%E7%9A%84%E4%BB%A3%E7%A0%81%E6%B4%81%E7%99%96")
- [组件封装实战:从 App.tsx 中提取 Progress](#组件封装实战:从 App.tsx 中提取 Progress "#6-%E7%BB%84%E4%BB%B6%E5%B0%81%E8%A3%85%E5%AE%9E%E6%88%98%E4%BB%8E-apptsx-%E4%B8%AD%E6%8F%90%E5%8F%96-progress")
- [组件树:React 的页面组织哲学](#组件树:React 的页面组织哲学 "#7-%E7%BB%84%E4%BB%B6%E6%A0%91react-%E7%9A%84%E9%A1%B5%E9%9D%A2%E7%BB%84%E7%BB%87%E5%93%B2%E5%AD%A6")
- 总结:六大核心认知
1. 端侧 AI:为什么要在浏览器里跑大模型
🌐 云端 vs 端侧
┌──────────────────────────────────────────────┐
│ 云端 API(OpenAI / DeepSeek) │
│ 浏览器 ──网络请求──→ 远程服务器 │
│ 问题:💰 贵、🔓 上下文发送到远程不安全 │
├──────────────────────────────────────────────┤
│ 端侧模型(浏览器内推理) │
│ 浏览器 ──本地 GPU 加速──→ 结果直接返回 │
│ 优势:🆓 免费、🔒 数据不出浏览器、📡 离线可用 │
└──────────────────────────────────────────────┘
端侧模型的典型应用场景:
- 📱 手机端:本地语音助手、实时翻译
- 🚗 汽车端:离线语音指令
- 🤖 Agent:任务划分与本地决策
- 🔒 隐私计算:敏感数据不出设备
🧠 本项目使用的模型
markdown
DeepSeek-R1-Distill-Qwen-1.5B-ONNX
DeepSeek-R1(671B 参数,数据中心级)
└── Distill(知识蒸馏,大模型 → 小模型)
└── Qwen-1.5B(千问架构,15 亿参数)
└── ONNX(跨平台推理格式,WebGPU 加速)
🏗️ 浏览器推理流水线
markdown
HuggingFace(模型托管)
↓
ONNX 格式模型文件
↓
Transformers.js(HuggingFace 官方 JS SDK,浏览器中加载模型)
↓
ONNX Runtime Web(WebGPU 后端,比 CPU 快 5-10 倍)
↓
用户看到推理结果(全程无网络请求!)
🎯 核心认知 :不是所有 AI 任务都需要 GPT-5。开源小参数模型(1B~7B)已经能出色完成分类、提取、摘要等任务。端侧模型 = 便宜 + 安全 + 离线可用。
2. React 组件 = 函数,函数就是组件
🧱 React 和 Vue 最根本的差异
xml
Vue 组件(SFC 单文件组件):
┌──────────────────┐
│ <template> │ ← HTML 模板
│ </template> │
│ <script> │ ← JS 逻辑
│ </script> │
│ <style> │ ← CSS 样式
│ </style> │
└──────────────────┘
一个 .vue 文件 = 一个组件
React 组件(纯函数):
┌──────────────────┐
│ function App() {│ ← JS 函数 = 组件
│ // JS 逻辑 │
│ const [x,setX] │
│ return ( │
│ <div>...</div>│ ← JSX (HTML in JS)
│ ) │
│ } │
│ // CSS 通过 import 引入
└──────────────────┘
一个函数 = 一个组件
🎯 核心认知 :React 组件就是返回 HTML 的 JavaScript 函数 。
return之前是逻辑层,return里面是 UI 层。CSS 通过 import 引入,不在组件函数内部。函数即组件------这是 React 最简洁的设计哲学。
🏗️ 搭积木的方式搭建页面
jsx
function App() { // 🏠 根组件
return (
<div>
<Header /> // 📐 头部
<TodoInput /> // ✍️ 输入
<TodoList // 📋 列表
todos={todos}
onToggle={handleToggle}
/>
<Footer /> // 📊 底部
</div>
);
}
每个组件是一个独立的功能单元(HTML + CSS + JS 的封装体),组合在一起就是完整的应用。
3. TailwindCSS:原子 CSS 的运行原理
💡 传统 CSS vs 原子 CSS
css
传统方式:
1. 想 class 名字(最头疼的事)
2. 打开 .css 文件写样式
3. 回到 HTML 加 class
→ 上下文频繁切换,效率低下
TailwindCSS:
直接在标签上写类名:
<div className="flex items-center gap-4">
→ 几乎不需要写 .css 文件!
🧬 原子类的命名规则
jsx
<div className="flex flex-col h-screen mx-auto items-center">
{/*
flex → display: flex
flex-col → flex-direction: column
h-screen → height: 100vh
mx-auto → margin-left/right: auto(水平居中)
items-center → align-items: center
*/}
</div>
间距系统:1 单位 = 0.25rem = 4px ,m-4 = 16px,p-6 = 24px。方括号自定义:max-w-[400px]、text-[#667eea]。
⚙️ 运行原理
markdown
1. 你写 className="flex items-center"
2. @tailwindcss/vite 插件扫描所有 .tsx 文件
3. 提取用到的原子类名
4. 从 Tailwind 库中查找对应的 CSS 规则
5. 只把你用到的 CSS 注入最终产物
→ 生产环境 CSS 通常 < 10KB!
在 index.css 中只需一行:
css
@import "tailwindcss";
🎯 核心认知 :TailwindCSS 已成为 Vibe UI 的基本构成。原子类名天然语义化,特别适合自然语义编程------你告诉 AI "这个 div 需要水平居中、蓝色背景",AI 就能精准生成对应的类名。
4. JSX 再探:className、{} 表达式与事件绑定
🔑 为什么是 className 而不是 class?
jsx
// ✅ React 中必须用 className
<div className="container">
// ❌ class 是 JavaScript 的关键字(声明类),不能在 JSX 中使用
<div class="container">
你在写 JSX 时实际是在写 JavaScript,class 已经被 JS 的 OOP 语法占用了。
{} 大括号:从 JSX 回到 JS 世界
jsx
const name = 'React';
<h1>Hello, {name}</h1> // Hello, React
<h1>Hello, {1 + 1}</h1> // Hello, 2
// 条件渲染
{error && <div className="error">{error}</div>}
// 列表渲染------React 不发明新语法,直接用原生 map
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
🎯 核心认知 :Vue 用
v-for做循环,React 绝对不去发明新语法 ------直接用 JS 原生的map。这是 React 的设计哲学:能用已有概念的,绝不发明新的。
🆚 React 事件 vs Vue 事件
jsx
// Vue:发明了 @ 语法(v-on 的简写)
<button @click="handleClick">
// React:直接用已有的概念,零学习成本
<button onClick={handleClick}>
5. React 合成事件:框架高手的代码洁癖
📜 DOM 事件进化史
html
<!-- event.html ------ 一个文件看尽三代事件 -->
<!-- ① DOM 0 级:onclick 属性(HTML/JS 耦合,不推荐) -->
<button id="btn" onclick="console.log('DOM 0')">按钮</button>
<script>
// ② DOM 2 级:addEventListener(W3C 标准,推荐)
document.getElementById('btn').addEventListener('click', () => {
console.log('DOM 2');
});
</script>
📖 冷知识:DOM 1 级没有更新事件相关内容,所以直接从 DOM 0 跳到了 DOM 2。
🎭 React 的合成事件(SyntheticEvent)
React 中的事件不是原生 DOM 事件,而是 React 封装后的"合成事件":
arduino
用户点击按钮
↓
浏览器触发原生 click 事件
↓
React 包装为 SyntheticEvent
├── 抹平浏览器差异(跨浏览器行为统一)
├── 内置事件委托(性能优化)
└── 调用你写的 onClick 回调
| 维度 | DOM 0 (onclick) | DOM 2 (addEventListener) | React (onClick) |
|---|---|---|---|
| 绑定 | HTML 属性中 | JS 代码中 | JSX 属性中 |
| 多次绑定 | 覆盖 | 追加 | 追加 |
| 模块化 | ❌ 耦合 | ✅ 分离 | ✅ 组件内聚 |
| 跨浏览器 | 自行处理 | 自行处理 | ✅ 自动抹平 |
6. 组件封装实战:从 App.tsx 中提取 Progress
🧩 为什么要拆组件?
当 App.tsx 越来越臃肿时,需要把独立、可复用的功能模块提取出来。
📦 Progress 组件完整拆解
typescript
// src/components/Progress.tsx
// 组件 = 函数,接受 Props(属性),返回 JSX
const Progress = ({ text, percentage, total }) => {
// ① return 之前 = JS 逻辑
percentage ??= 0; // 空值合并:null/undefined 时取 0
// ② return = UI 渲染
return (
<div>
<p>{text}</p> {/* 文件名 */}
<p>{percentage}%</p> {/* 下载进度 */}
<p>{total}</p> {/* 文件大小 */}
</div>
);
};
export default Progress;
🔗 父组件中调用
typescript
// App.tsx
{status === "loading" && (
<div>
{progressItems.map(({ text, percentage, total }, i) => (
<Progress
key={i} // React 列表必须加 key
text={text} // Props 传参
percentage={percentage}
total={total}
/>
))}
</div>
)}
🎯 组件提取的判断标准
| 信号 | 说明 |
|---|---|
| 可复用 | 同一个 UI 模式在多处出现 |
| 逻辑独立 | 有自己独立的数据流,不依赖父组件内部状态 |
| 职责单一 | 只做一件事(Progress 只展示进度,不负责下载) |
| 行数过多 | 单文件超过 150 行,是时候拆了 |
7. 组件树:React 的页面组织哲学
🌳 DOM 树 → 组件树
xml
传统前端(关注 HTML 标签): React(关注功能单元):
<html> <App>
<body> ├── <Header>
<header> │ ├── <h1>
<h1>标题</h1> │ └── <h2>
</header> ├── <MainContent>
<main> │ ├── <p>
<p>描述</p> │ ├── <a>
<button>按钮</button> │ ├── <ErrorMsg>
</main> │ └── <LoadButton>
</body> └── <ProgressArea>
</html> ├── <p>
└── <Progress /> × N
看到的是「HTML 结构」 看到的是「功能构成」
🏗️ 组件树的四大价值
- 一眼看出页面构成:新人接手,看组件树就懂结构
- 粒度清晰:太粗 → 一个组件做太多;太细 → 文件太多
- 团队协作友好:每人负责不同子树,Props 接口清晰
- 好维护、好复用:改一个组件不影响其他组件
🔄 本项目的数据流
ini
<App> ← 根组件,掌管所有状态
│
├── 标题区(h1 + h2)
├── 链接区(HuggingFace → Transformers.js)
├── 错误区(error && <红色提示>) ← 条件渲染
├── LoadButton ← 状态驱动 disabled
└── ProgressArea ← status === "loading" &&
└── progressItems.map(item => ← 列表渲染
<Progress ← 子组件
text={item.text} ← Props:父→子单向数据流
/>
)
🎯 核心认知:前端发展的必然趋势------页面越来越复杂,组件成为开发的最小单元。组件树替代 DOM 树,标志着前端从"操作页面元素"进化到"组合功能模块"。
8. 总结:六大核心认知
csharp
React 技术栈 ── 六大核心认知:
1️⃣ 端侧 AI
浏览器内推理 = 免费 + 安全 + 离线
小参数模型能完成大部分特定任务
2️⃣ 组件化
函数就是组件 → function App() { return <JSX>; }
搭积木方式搭建页面
3️⃣ TailwindCSS
原子 CSS 类 → 几乎不再需要写 .css 文件
Vite 插件按需生成,生产环境 < 10KB
4️⃣ JSX
JavaScript + XML → 在 JS 中写 HTML
className 不是 class、{} 表达式、React 不发明新语法
5️⃣ 合成事件
React 封装原生事件 → 跨浏览器统一 + 事件委托
不发明 @click 新语法,直接用 onClick
6️⃣ 组件树
从「操作 DOM 元素」到「组合功能模块」
组件是开发的最小单元,树形组合构建完整应用
📖 参考资源
💡 写在最后 :本文与上一篇文章形成互补------上篇侧重 Hooks、状态管理等编码技能,本篇侧重端侧 AI 思维、组件封装和架构设计。两篇文章合在一起,构成了从 React 初学者到 AI 时代前端工程师的完整学习路径。共勉 🚀