文章目录
-
- [一、端侧模型:AI 不再只活在云端](#一、端侧模型:AI 不再只活在云端)
-
- [1.1 什么是端侧模型?](#1.1 什么是端侧模型?)
- [1.2 为什么端侧模型突然火了?](#1.2 为什么端侧模型突然火了?)
- [1.3 本项目的模型选择](#1.3 本项目的模型选择)
- [二、React + TypeScript:为什么是 AI 时代的首选?](#二、React + TypeScript:为什么是 AI 时代的首选?)
-
- [2.1 React vs Vue:选型的底层逻辑](#2.1 React vs Vue:选型的底层逻辑)
- [2.2 新建项目:React + TS + ESLint 一步到位](#2.2 新建项目:React + TS + ESLint 一步到位)
- [2.3 Vite 配置:让 Tailwind 跑起来](#2.3 Vite 配置:让 Tailwind 跑起来)
- [三、TailwindCSS:告别手写 CSS 的原子化方案](#三、TailwindCSS:告别手写 CSS 的原子化方案)
-
- [3.1 传统 CSS 的痛点](#3.1 传统 CSS 的痛点)
- [3.2 Tailwind 的思路:原子类](#3.2 Tailwind 的思路:原子类)
- [3.3 Tailwind 运行原理](#3.3 Tailwind 运行原理)
- [3.4 为什么是 `className` 而不是 `class`?](#3.4 为什么是
className而不是class?)
- [四、React 组件:函数就是积木](#四、React 组件:函数就是积木)
-
- [4.1 Vue 组件 vs React 组件](#4.1 Vue 组件 vs React 组件)
- [4.2 入口文件:React 是怎么启动的?](#4.2 入口文件:React 是怎么启动的?)
- [五、代码详解:App.tsx 逐段解析](#五、代码详解:App.tsx 逐段解析)
-
- [5.1 导入 Hooks](#5.1 导入 Hooks)
- [5.2 数据状态:响应式的核心](#5.2 数据状态:响应式的核心)
- [5.3 WebGPU 检测:一行代码判断浏览器能力](#5.3 WebGPU 检测:一行代码判断浏览器能力)
- [5.4 组件生命周期:useEffect 的执行时机](#5.4 组件生命周期:useEffect 的执行时机)
- [5.5 JSX:在 JavaScript 里写 HTML](#5.5 JSX:在 JavaScript 里写 HTML)
- [5.6 Tailwind 原子类实战解读](#5.6 Tailwind 原子类实战解读)
- [5.7 模型信息展示区解析](#5.7 模型信息展示区解析)
- [5.8 错误处理状态](#5.8 错误处理状态)
- 六、全文总结
- 七、核心知识点复盘
- [八、常见问题 / 避坑指南](#八、常见问题 / 避坑指南)
一份保姆级技术复盘,覆盖端侧模型、React + TypeScript、TailwindCSS、JSX 等核心技能点,适合学习复盘和技术分享。
一、端侧模型:AI 不再只活在云端
1.1 什么是端侧模型?
平时我们使用 ChatGPT、DeepSeek、Kimi 等 AI 助手,流程是这样的:
用户输入 → 网络请求 → 远程服务器(GPU集群) → 推理计算 → 返回结果
这种方式叫云端推理,模型跑在厂商的服务器上。它有两个绕不开的问题:
- 贵:厂商需要采购大量 GPU,成本最终转嫁给你(API 按 token 计费)。
- 不安全:你的输入内容(context)会随着请求发送到远端服务器,数据隐私无法完全掌控。
而端侧模型(On-Device Model)指的是模型直接运行在你的设备上------手机、电脑、汽车、甚至浏览器。数据不出设备,推理在本地完成。
1.2 为什么端侧模型突然火了?
关键推动力来自两点:
| 推动因素 | 说明 |
|---|---|
| 开源小参数模型成熟 | Llama、Qwen、Gemma 等 1B~7B 参数模型,在特定任务上表现已经不输大模型 |
| WebGPU 的到来 | 浏览器可以直接调用 GPU 做并行计算,不再依赖 WebGL 的"曲线救国" |
Ollama 就是典型的端侧方案------你下载模型到本地,通过命令行或 API 调用。而本项目的更进一步:模型直接在浏览器里下载、加载、推理,用户打开网页就能用,用完即走,不占用磁盘。
1.3 本项目的模型选择
项目使用的是 DeepSeek-R1-Distill-Qwen-1.5B:
- 这是 DeepSeek-R1(推理模型)的蒸馏版,参数量压缩到 15 亿。
- 基于 Qwen 架构,专为本地轻量推理优化。
- 模型格式为 ONNX(Open Neural Network Exchange,开放神经网络交换格式),跨平台跨框架。
- 托管在 HuggingFace (全球最大开源模型社区),通过
Transformers.js加载。
关键理解:蒸馏 = 用大模型"教"小模型。大模型生成高质量答案 → 小模型模仿学习 → 保留大部分推理能力但体积小很多。
二、React + TypeScript:为什么是 AI 时代的首选?
2.1 React vs Vue:选型的底层逻辑
你可能会问:Vue 上手更简单,为什么 AI 项目偏爱 React?
| 维度 | React | Vue |
|---|---|---|
| 学习曲线 | 较陡(需要理解 JSX、Hooks、函数式编程) | 平缓(模板语法接近 HTML) |
| 大型项目 | 函数式编程天然适合抽象和复用,生态更成熟 | 中小项目效率极高 |
| AI/ML 生态 | Transformers.js、LangChain.js、Vercel AI SDK 都优先支持 React | 社区也在跟进,但目前示例偏少 |
| 招聘市场 | 大厂、AI Startup 的首选 | 国内中小企业用得更多 |
一句话总结:React 的上限更高,Vue 的下限更低。做 AI 相关的复杂交互,React 的函数式思想更适合。
2.2 新建项目:React + TS + ESLint 一步到位
bash
# 使用 Vite 创建项目(最快的构建工具)
npm create vite@latest webgpu-demo -- --template react-ts
cd webgpu-demo
npm install
创建完成后,你会得到以下关键文件:
webgpu-demo/
├── src/
│ ├── App.tsx # 主组件(你写代码的地方)
│ ├── App.css # 组件样式
│ ├── main.tsx # 入口文件(挂载 React 到页面)
│ └── index.css # 全局样式 + Tailwind 导入
├── eslint.config.js # ESLint 代码约束配置
├── vite.config.ts # Vite 构建配置
├── tsconfig.json # TypeScript 配置
└── package.json # 依赖管理
package.json 的核心依赖解读:
json
{
"dependencies": {
"@tailwindcss/vite": "^4.3.3", // TailwindCSS Vite 插件
"react": "^19.2.6", // React 核心库
"react-dom": "^19.2.6", // React DOM 渲染(浏览器端)
"tailwindcss": "^4.3.3" // TailwindCSS 框架本体
},
"devDependencies": {
"typescript": "~6.0.2", // TypeScript 编译器
"eslint": "^10.3.0", // 代码规范检查
"vite": "^8.0.12" // 构建工具
}
}
ESLint 的作用是什么?
ESLint 是代码"纪律委员"------约束团队写出一致风格的代码。比如用单引号还是双引号?结尾要不要分号?这些规则在 eslint.config.js 中统一配置。大公司必备,否则代码合并时就是灾难。
js
// eslint.config.js 关键配置
export default defineConfig([
globalIgnores(['dist']), // 忽略构建产物
{
files: ['**/*.{ts,tsx}'], // 对 TS 和 TSX 文件生效
extends: [
js.configs.recommended, // JS 基础规则
tseslint.configs.recommended, // TypeScript 规则
reactHooks.configs.flat.recommended, // React Hooks 规则
],
},
])
2.3 Vite 配置:让 Tailwind 跑起来
ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(), // 让 Vite 支持 React JSX
tailwindcss(), // 让 Vite 处理 Tailwind 原子类
],
})
Vite 插件机制很简单 :每个插件负责一块能力,像搭积木一样拼起来。react() 负责编译 JSX,tailwindcss() 负责扫描和注入 CSS。
三、TailwindCSS:告别手写 CSS 的原子化方案
3.1 传统 CSS 的痛点
回想一下你写 CSS 的流程:
- 想一个 class 名(
.my-cool-button) - 找到对应文件(或
<style>块) - 写选择器 + 规则(
color: red; font-size: 16px;) - 反复调试样式冲突和优先级
这个过程太低效了------你在两个文件之间来回切换,还要想命名、管优先级。
3.2 Tailwind 的思路:原子类
Tailwind 的做法是:不写 CSS 规则,直接写类名。
html
<!-- 传统方式 -->
<button class="my-button">点击</button>
<style>
.my-button {
background: blue;
color: white;
padding: 8px 16px;
border-radius: 4px;
}
</style>
<!-- Tailwind 方式 -->
<button className="bg-blue-500 text-white px-4 py-2 rounded">
点击
</button>
每一个 class 名 = 一条 CSS 规则。bg-blue-500 就是 background-color: blue,px-4 就是 padding-left: 1rem; padding-right: 1rem;。
为什么这更好?
- 不用命名 :不用再纠结 class 叫
btn-primary还是btn-main - 所见即所得:看到类名就知道样式,不用跳转到 CSS 文件
- 自然语言友好:类名是用英文单词组合的,和 AI 编程(Vibe Coding)天然契合
- 按需生成:Vite 插件只提取你用到的类名,打包体积很小
3.3 Tailwind 运行原理
Tailwind 不是原生 CSS------浏览器不认识 bg-blue-500。它的工作流程是:
1. 你写 className="bg-blue-500 text-white"
↓
2. Tailwind Vite 插件扫描所有 .tsx/.jsx 文件
↓
3. 识别到 bg-blue-500 → 找到对应 CSS: background-color: #3b82f6;
↓
4. 把这条 CSS 注入到最终构建的样式文件中
↓
5. 浏览器正确渲染蓝色背景
核心原理一句话:Tailwind 是一个"类名到 CSS 规则"的映射字典。插件在构建时扫描代码 → 查字典 → 生成最小化的 CSS 文件。你没有用到的类名不会出现在最终产物中。
在项目中的体现:
css
/* src/index.css --- 只需要一行! */
@import "tailwindcss";
/* 下面是项目自定义的 CSS 变量和全局样式 */
:root {
--text: #6b6375;
--bg: #fff;
/* ... */
}
@import "tailwindcss" 这一行就是 Tailwind 的"入口",插件会从这里开始注入扫描到的所有原子类。
3.4 为什么是 className 而不是 class?
这是一个非常经典的困惑。答案很简单:
JSX 中写 <div class="xxx"> 会出问题,因为 class 是 JavaScript 的关键字(用于定义类/面向对象编程)。
React 团队为了避免语法冲突,用 className 替代了 class:
tsx
// ❌ 错误:class 是 JS 关键字
<div class="container">
// ✅ 正确:使用 className
<div className="container">
编译后 <div className="container"> → 原生 DOM 的 <div class="container">,效果一模一样。
四、React 组件:函数就是积木
4.1 Vue 组件 vs React 组件
Vue 组件 是"三件套"------HTML、CSS、JS 分块写在一个 .vue 文件里:
vue
<template>
<div>{{ message }}</div>
</template>
<script setup>
const message = 'Hello'
</script>
<style scoped>
div { color: red; }
</style>
React 组件 就是一个函数,返回 HTML(JSX):
tsx
function MyComponent() {
const message = 'Hello' // JS 逻辑
// CSS 通过 import 或 Tailwind 引入
return <div>{message}</div> // 返回 HTML
}
两者的本质区别:
| Vue | React | |
|---|---|---|
| 组件形态 | .vue 单文件(模板+逻辑+样式) |
函数(JS + JSX) |
| 入门难度 | 低(模板接近原生 HTML) | 中(需要理解 JSX 和函数式编程) |
| 抽象能力 | 指令体系(v-if, v-for) | JavaScript 原生能力(&&, map) |
React 的理念:组件就是函数,函数就是组件。所有 JavaScript 的能力(条件判断、循环、解构)都能直接在"模板"里用。
4.2 入口文件:React 是怎么启动的?
tsx
// src/main.tsx --- React 应用的"点火开关"
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css' // 全局样式(含 Tailwind)
import App from './App.tsx' // 导入根组件
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
执行流程:
createRoot(...)--- 找到index.html中的<div id="root">,把它变成 React 的"根容器".render(...)--- 把<App />组件渲染到这个容器里<StrictMode>--- 开发模式下的"严格检查",会帮你发现潜在问题(比如不安全的生命周期),生产环境自动失效
五、代码详解:App.tsx 逐段解析
下面逐一解析 App.tsx 的每一部分代码,确保你完全理解。
5.1 导入 Hooks
tsx
import { useState, useEffect } from 'react'
useState:React 的"状态钩子"。让你在函数组件中创建响应式数据------数据变了,界面自动更新。useEffect:React 的"副作用钩子"。组件渲染完成后自动执行指定代码(比如发请求、设置定时器)。- 这两个函数都以
use开头,这是 React 的约定------所有 Hooks 都遵循useXxx命名模式。
5.2 数据状态:响应式的核心
tsx
function App() {
// status: 当前加载状态
// null = 初始 / 'loading' = 加载中 / 'ready' = 模型就绪
const [status, setStatus] = useState(null)
// error: 错误信息(演示用 "出错了" 作为初始值)
const [error, setError] = useState("出错了")
// loadingMessage: 加载提示文本
const [loadingMessage, setLoadingMessage] = useState("")
// progressItems: 模型文件下载进度
const [progressItems, setProgressItems] = useState([{
file: 'model.onnx', // 模型文件名
progress: 0, // 当前已下载字节数
total: 5465458632 // 模型总大小(约 5.5GB)
}])
useState 语法详解:
tsx
const [值, 修改值的函数] = useState(初始值)
这是数组解构语法------useState 返回一个长度为 2 的数组:
- 第一个元素是当前状态值(只读,不要直接修改)
- 第二个元素是更新函数(想改状态?调它!)
tsx
// ❌ 错误:直接修改不会触发界面更新
status = 'ready'
// ✅ 正确:调用更新函数
setStatus('ready') // 状态变了 → React 自动重新渲染组件
为什么叫"响应式"? 数据(状态)和界面是绑定的。就像川剧变脸------你切换一张脸谱(改状态),观众看到的脸就变了(界面更新)。你不需要手动操作 DOM,React 帮你做好了。
5.3 WebGPU 检测:一行代码判断浏览器能力
tsx
const IS_WEBGPU_AVAILABLE = !!navigator.gpu
这行代码值得拆开理解:
| 表达式 | 含义 |
|---|---|
navigator.gpu |
浏览器是否暴露 GPU 接口。支持 WebGPU → 返回对象;不支持 → undefined |
!navigator.gpu |
取反。支持 → false;不支持 → true |
!!navigator.gpu |
再取反(双重否定等于肯定)。支持 → true;不支持 → false |
!! 是一种将任意值强转为布尔值的 JS 技巧:
js
!!{} // true
!!undefined // false
!!null // false
!!0 // false
!!'hello' // true
5.4 组件生命周期:useEffect 的执行时机
tsx
useEffect(() => {
console.log('组件已经挂载完成')
setTimeout(() => {
// setStatus('ready') // 1 秒后将状态改为 ready
}, 1000)
}, []) // ← 空数组,只执行一次
useEffect 的第二个参数是关键:
| 第二个参数 | 执行时机 |
|---|---|
[](空数组) |
组件首次渲染后执行一次 |
[status] |
首次渲染后 + status 变化后执行 |
| 不传 | 每次渲染后都执行 |
这里的 [] 意味着"组件挂载完成时执行,只此一次"------非常适合做初始化操作(加载模型、请求数据等)。
5.5 JSX:在 JavaScript 里写 HTML
tsx
return (
IS_WEBGPU_AVAILABLE ? (
<div className="flex flex-col h-screen ...">
<h1 className="text-4xl font-bold mb-1">DeepSeek-R1 WebGPU</h1>
{/* ... */}
</div>
) : (
<div>您的浏览器还不支持WebGPU</div>
)
)
JSX(JavaScript XML) 是 React 最骄傲的特性之一------在 JS 代码中直接写 HTML 标签。
几个 JSX 核心规则:
① 条件渲染:三目运算符
tsx
{condition ? <ComponentA /> : <ComponentB />}
② 列表渲染:.map()
tsx
{items.map(item => <li key={item.id}>{item.name}</li>)}
③ 嵌入 JS 表达式:{} 大括号
tsx
<p>计算结果:{1 + 1}</p> // → 计算结果:2
<p>用户名:{user.name}</p> // → 用户名:张三
④ 注释:大括号包裹
tsx
{/* 这是 JSX 注释,和 JS 多行注释一样的写法 */}
⑤ 条件显示:&& 短路
tsx
{error && (
<div className="text-red-500">
<p>Unable to load model due to the following error:</p>
<p className="text-sm">{error}</p>
</div>
)}
当 error 为空字符串或 null 时,&& 右边不执行,整个 <div> 不渲染。这是 React 中极常用的条件渲染模式。
5.6 Tailwind 原子类实战解读
来看看项目中用到的关键原子类:
tsx
<div className="flex flex-col h-screen mx-auto items-center justify-end text-gray-800 bg-white">
| 类名 | 对应 CSS | 含义 |
|---|---|---|
flex |
display: flex |
开启弹性布局 |
flex-col |
flex-direction: column |
主轴方向为垂直(从上到下) |
h-screen |
height: 100vh |
高度 = 整个屏幕高度 |
mx-auto |
margin-left: auto; margin-right: auto |
水平居中 |
items-center |
align-items: center |
子元素垂直居中 |
justify-end |
justify-content: flex-end |
子元素靠底部对齐 |
text-gray-800 |
color: #1f2937 |
文字颜色 |
bg-white |
background-color: white |
背景色 |
自定义值的语法:
tsx
<div className="max-w-[400px]"> {/* 方括号内是自定义值 */}
[] 允许你使用 Tailwind 预设之外的任意值。这里 max-w-[400px] 等价于 max-width: 400px。
1rem = 4 是 Tailwind 的默认尺寸单位映射:p-1 = 4px,p-4 = 16px,以此类推。
5.7 模型信息展示区解析
tsx
<p className="mx-w-[510px] mb-4">
You are about to load
<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>
, a 1.5B parameter reasoning LLM optimized for in-browser
inference. Everything runs entirely in your browser with
<a
href="https://huggingface.co/docs/transformers.js"
target="_blank"
rel="noreferrer"
className="underline"
>
🤗 Transformers.js
</a>
and ONNX Runtime Web, meaning no data is sent to a server.
</p>
两个关键链接指向的技术:
- DeepSeek-R1-Distill-Qwen-1.5B-ONNX:模型托管在 HuggingFace。HuggingFace 是全球最大的开源模型社区,被称为 AI 界的 GitHub。
- Transformers.js:HuggingFace 推出的 JavaScript 库,让你在浏览器中加载和推理 Transformer 模型,无需后端服务。
- ONNX Runtime Web:微软的 ONNX 运行时浏览器版,负责在 WebGPU 上高效执行模型推理。
这两个库配合 WebGPU,让"浏览器跑大模型"从不可能变成了现实。
5.8 错误处理状态
tsx
{error && (
<div className="text-red-500 text-center mb-2">
<p className="mb-1">Unable to load model due to the following error:</p>
<p className="text-sm">{error}</p>
</div>
)}
当 error 有值时(非空字符串),显示红色错误提示。当错误被清除(setError(null) 或 setError('')),错误提示自动消失。这就是"响应式条件渲染"------你只需要改数据,界面自己会跟着变。
六、全文总结
本文从一个真实的浏览器端 AI 推理项目出发,系统梳理了以下技术链路:
- 端侧模型:模型从云端走向本地,从服务器走向浏览器。核心理念是"数据不出设备",WebGPU 是浏览器端 AI 的关键基础设施。
- React + TypeScript:AI 时代大型前端项目的首选技术栈。函数式组件 + Hooks 模式提供了强大的抽象能力。
- TailwindCSS:原子化 CSS 框架,用"堆类名"替代"写 CSS",开发效率翻倍。Vite 插件在构建时按需注入样式。
- React 组件化:函数 = 组件,JSX = 模板。所有 JavaScript 能力直接用于 UI 表达。
- 状态驱动 :
useState+useEffect实现响应式数据绑定,数据变化自动驱动界面更新。
七、核心知识点复盘
| 序号 | 知识点 | 一句话总结 |
|---|---|---|
| 1 | 端侧模型 | LLM 运行在用户设备上,数据不出设备,隐私安全 |
| 2 | ONNX | 开放神经网络交换格式,跨框架跨平台的模型标准 |
| 3 | HuggingFace | 全球最大开源模型社区,AI 界的 GitHub |
| 4 | WebGPU | 浏览器原生 GPU API,替代 WebGL 做高性能计算 |
| 5 | useState |
React 状态钩子,创建响应式数据 [值, 更新函数] |
| 6 | useEffect |
React 副作用钩子,组件渲染后执行,第二个参数控制执行时机 |
| 7 | !! |
双重否定强转布尔值,!!undefined = false,!!{} = true |
| 8 | JSX | JavaScript XML,在 JS 中写 HTML,React 的核心语法 |
| 9 | className |
JSX 中替代 class(因为 class 是 JS 关键字) |
| 10 | Tailwind | 原子化 CSS 框架,类名即样式,按需生成,不写 CSS 文件 |
| 11 | Vite 插件 | 扩展 Vite 能力(处理 JSX、Tailwind 等),像搭积木 |
| 12 | ESLint | 代码约束工具,确保团队代码风格一致 |
| 13 | 条件渲染 | {condition && <Component />} 或三目运算符 |
| 14 | 响应式 | 数据变化 → 界面自动更新,无需手动操作 DOM |
八、常见问题 / 避坑指南
Q1:!!navigator.gpu 和 Boolean(navigator.gpu) 有区别吗?
没有本质区别,效果一样。!! 更简洁,是 JS 社区的惯用写法。不推荐 new Boolean()。
Q2:useEffect 第二个参数传空数组 [] 时,函数什么时候执行?
组件首次挂载 到 DOM 后执行一次。类比 Vue 的 mounted() 生命周期钩子。
Q3:为什么不直接在 useState 里写 useState(null) => useState("出错了") 会怎样?
不会怎样,初始值只是"第一次渲染时"的状态。后续通过 setError 更新。这里给 "出错了" 是为了演示错误状态 UI。
Q4:Tailwind @import "tailwindcss" 报错怎么办?
检查 vite.config.ts 中是否添加了 tailwindcss() 插件。Tailwind v4 通过 Vite 插件工作,不需要手动安装 PostCSS。
Q5:为什么组件函数里 console.log 会执行多次?
React 在开发模式(StrictMode)下会故意渲染两次来帮你发现副作用问题。生产环境不会。这是正常的,不用担心。
Q6:模型文件 5.5GB,浏览器怎么存得下?
模型通过 Transformers.js 分片下载后会缓存在浏览器的 Cache Storage 中。第二次访问时直接从缓存加载,不需要重新下载。离线也能用。
项目地址 :github.com/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX
技术栈:React 19 + TypeScript 6 + Vite 8 + TailwindCSS 4 + WebGPU + Transformers.js + ONNX Runtime Web