文章目录一、端侧模型AI 不再只活在云端1.1 什么是端侧模型1.2 为什么端侧模型突然火了1.3 本项目的模型选择二、React TypeScript为什么是 AI 时代的首选2.1 React vs Vue选型的底层逻辑2.2 新建项目React TS ESLint 一步到位2.3 Vite 配置让 Tailwind 跑起来三、TailwindCSS告别手写 CSS 的原子化方案3.1 传统 CSS 的痛点3.2 Tailwind 的思路原子类3.3 Tailwind 运行原理3.4 为什么是 className 而不是 class四、React 组件函数就是积木4.1 Vue 组件 vs React 组件4.2 入口文件React 是怎么启动的五、代码详解App.tsx 逐段解析5.1 导入 Hooks5.2 数据状态响应式的核心5.3 WebGPU 检测一行代码判断浏览器能力5.4 组件生命周期useEffect 的执行时机5.5 JSX在 JavaScript 里写 HTML5.6 Tailwind 原子类实战解读5.7 模型信息展示区解析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架构专为本地轻量推理优化。模型格式为ONNXOpen Neural Network Exchange开放神经网络交换格式跨平台跨框架。托管在HuggingFace全球最大开源模型社区通过Transformers.js加载。关键理解蒸馏 用大模型教小模型。大模型生成高质量答案 → 小模型模仿学习 → 保留大部分推理能力但体积小很多。二、React TypeScript为什么是 AI 时代的首选2.1 React vs Vue选型的底层逻辑你可能会问Vue 上手更简单为什么 AI 项目偏爱 React维度ReactVue学习曲线较陡需要理解 JSX、Hooks、函数式编程平缓模板语法接近 HTML大型项目函数式编程天然适合抽象和复用生态更成熟中小项目效率极高AI/ML 生态Transformers.js、LangChain.js、Vercel AI SDK 都优先支持 React社区也在跟进但目前示例偏少招聘市场大厂、AI Startup 的首选国内中小企业用得更多一句话总结React 的上限更高Vue 的下限更低。做 AI 相关的复杂交互React 的函数式思想更适合。2.2 新建项目React TS ESLint 一步到位# 使用 Vite 创建项目最快的构建工具npmcreate vitelatest webgpu-demo ----templatereact-tscdwebgpu-demonpminstall创建完成后你会得到以下关键文件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 的核心依赖解读{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中统一配置。大公司必备否则代码合并时就是灾难。// eslint.config.js 关键配置exportdefaultdefineConfig([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 跑起来// vite.config.tsimport{defineConfig}fromviteimportreactfromvitejs/plugin-reactimporttailwindcssfromtailwindcss/viteexportdefaultdefineConfig({plugins:[react(),// 让 Vite 支持 React JSXtailwindcss(),// 让 Vite 处理 Tailwind 原子类],})Vite 插件机制很简单每个插件负责一块能力像搭积木一样拼起来。react()负责编译 JSXtailwindcss()负责扫描和注入 CSS。三、TailwindCSS告别手写 CSS 的原子化方案3.1 传统 CSS 的痛点回想一下你写 CSS 的流程想一个 class 名.my-cool-button找到对应文件或style块写选择器 规则color: red; font-size: 16px;反复调试样式冲突和优先级这个过程太低效了——你在两个文件之间来回切换还要想命名、管优先级。3.2 Tailwind 的思路原子类Tailwind 的做法是不写 CSS 规则直接写类名。!-- 传统方式 --buttonclassmy-button点击/buttonstyle.my-button{background:blue;color:white;padding:8px 16px;border-radius:4px;}/style!-- Tailwind 方式 --buttonclassNamebg-blue-500 text-white px-4 py-2 rounded点击/button每一个 class 名 一条 CSS 规则。bg-blue-500就是background-color: bluepx-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. 你写 classNamebg-blue-500 text-white ↓ 2. Tailwind Vite 插件扫描所有 .tsx/.jsx 文件 ↓ 3. 识别到 bg-blue-500 → 找到对应 CSS: background-color: #3b82f6; ↓ 4. 把这条 CSS 注入到最终构建的样式文件中 ↓ 5. 浏览器正确渲染蓝色背景核心原理一句话Tailwind 是一个类名到 CSS 规则的映射字典。插件在构建时扫描代码 → 查字典 → 生成最小化的 CSS 文件。你没有用到的类名不会出现在最终产物中。在项目中的体现/* src/index.css — 只需要一行 */importtailwindcss;/* 下面是项目自定义的 CSS 变量和全局样式 */:root{--text:#6b6375;--bg:#fff;/* ... */}import tailwindcss这一行就是 Tailwind 的入口插件会从这里开始注入扫描到的所有原子类。3.4 为什么是className而不是class这是一个非常经典的困惑。答案很简单JSX 中写div classxxx会出问题因为class是 JavaScript 的关键字用于定义类/面向对象编程。React 团队为了避免语法冲突用className替代了class// ❌ 错误class 是 JS 关键字 div classcontainer // ✅ 正确使用 className div classNamecontainer编译后div classNamecontainer→ 原生 DOM 的div classcontainer效果一模一样。四、React 组件函数就是积木4.1 Vue 组件 vs React 组件Vue 组件是三件套——HTML、CSS、JS 分块写在一个.vue文件里template div{{ message }}/div /template script setup const message Hello /script style scoped div { color: red; } /styleReact 组件就是一个函数返回 HTMLJSXfunction MyComponent() { const message Hello // JS 逻辑 // CSS 通过 import 或 Tailwind 引入 return div{message}/div // 返回 HTML }两者的本质区别VueReact组件形态.vue单文件模板逻辑样式函数JS JSX入门难度低模板接近原生 HTML中需要理解 JSX 和函数式编程抽象能力指令体系v-if, v-forJavaScript 原生能力, mapReact 的理念组件就是函数函数就是组件。所有 JavaScript 的能力条件判断、循环、解构都能直接在模板里用。4.2 入口文件React 是怎么启动的// 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 idroot把它变成 React 的根容器.render(...)— 把App /组件渲染到这个容器里StrictMode— 开发模式下的严格检查会帮你发现潜在问题比如不安全的生命周期生产环境自动失效五、代码详解App.tsx 逐段解析下面逐一解析App.tsx的每一部分代码确保你完全理解。5.1 导入 Hooksimport { useState, useEffect } from reactuseStateReact 的状态钩子。让你在函数组件中创建响应式数据——数据变了界面自动更新。useEffectReact 的副作用钩子。组件渲染完成后自动执行指定代码比如发请求、设置定时器。这两个函数都以use开头这是 React 的约定——所有 Hooks 都遵循useXxx命名模式。5.2 数据状态响应式的核心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语法详解const [值, 修改值的函数] useState(初始值)这是数组解构语法——useState返回一个长度为 2 的数组第一个元素是当前状态值只读不要直接修改第二个元素是更新函数想改状态调它// ❌ 错误直接修改不会触发界面更新 status ready // ✅ 正确调用更新函数 setStatus(ready) // 状态变了 → React 自动重新渲染组件为什么叫响应式数据状态和界面是绑定的。就像川剧变脸——你切换一张脸谱改状态观众看到的脸就变了界面更新。你不需要手动操作 DOMReact 帮你做好了。5.3 WebGPU 检测一行代码判断浏览器能力const IS_WEBGPU_AVAILABLE !!navigator.gpu这行代码值得拆开理解表达式含义navigator.gpu浏览器是否暴露 GPU 接口。支持 WebGPU → 返回对象不支持 →undefined!navigator.gpu取反。支持 →false不支持 →true!!navigator.gpu再取反双重否定等于肯定。支持 →true不支持 →false!!是一种将任意值强转为布尔值的 JS 技巧!!{}// true!!undefined// false!!null// false!!0// false!!hello// true5.4 组件生命周期useEffect 的执行时机useEffect(() { console.log(组件已经挂载完成) setTimeout(() { // setStatus(ready) // 1 秒后将状态改为 ready }, 1000) }, []) // ← 空数组只执行一次useEffect的第二个参数是关键第二个参数执行时机[]空数组组件首次渲染后执行一次[status]首次渲染后 status变化后执行不传每次渲染后都执行这里的[]意味着组件挂载完成时执行只此一次——非常适合做初始化操作加载模型、请求数据等。5.5 JSX在 JavaScript 里写 HTMLreturn ( IS_WEBGPU_AVAILABLE ? ( div classNameflex flex-col h-screen ... h1 classNametext-4xl font-bold mb-1DeepSeek-R1 WebGPU/h1 {/* ... */} /div ) : ( div您的浏览器还不支持WebGPU/div ) )JSXJavaScript XML是 React 最骄傲的特性之一——在 JS 代码中直接写 HTML 标签。几个 JSX 核心规则① 条件渲染三目运算符{condition ? ComponentA / : ComponentB /}② 列表渲染.map(){items.map(item li key{item.id}{item.name}/li)}③ 嵌入 JS 表达式{}大括号p计算结果{1 1}/p // → 计算结果2 p用户名{user.name}/p // → 用户名张三④ 注释大括号包裹{/* 这是 JSX 注释和 JS 多行注释一样的写法 */}⑤ 条件显示短路{error ( div classNametext-red-500 pUnable to load model due to the following error:/p p classNametext-sm{error}/p /div )}当error为空字符串或null时右边不执行整个div不渲染。这是 React 中极常用的条件渲染模式。5.6 Tailwind 原子类实战解读来看看项目中用到的关键原子类div classNameflex flex-col h-screen mx-auto items-center justify-end text-gray-800 bg-white类名对应 CSS含义flexdisplay: flex开启弹性布局flex-colflex-direction: column主轴方向为垂直从上到下h-screenheight: 100vh高度 整个屏幕高度mx-automargin-left: auto; margin-right: auto水平居中items-centeralign-items: center子元素垂直居中justify-endjustify-content: flex-end子元素靠底部对齐text-gray-800color: #1f2937文字颜色bg-whitebackground-color: white背景色自定义值的语法div classNamemax-w-[400px] {/* 方括号内是自定义值 */}[]允许你使用 Tailwind 预设之外的任意值。这里max-w-[400px]等价于max-width: 400px。1rem 4是 Tailwind 的默认尺寸单位映射p-1 4pxp-4 16px以此类推。5.7 模型信息展示区解析p classNamemx-w-[510px] mb-4 You are about to load a hrefhttps://huggingface.co/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX target_blank relnoreferrer classNamefont-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 hrefhttps://huggingface.co/docs/transformers.js target_blank relnoreferrer classNameunderline nbsp;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.jsHuggingFace 推出的 JavaScript 库让你在浏览器中加载和推理 Transformer 模型无需后端服务。ONNX Runtime Web微软的 ONNX 运行时浏览器版负责在 WebGPU 上高效执行模型推理。这两个库配合 WebGPU让浏览器跑大模型从不可能变成了现实。5.8 错误处理状态{error ( div classNametext-red-500 text-center mb-2 p classNamemb-1Unable to load model due to the following error:/p p classNametext-sm{error}/p /div )}当error有值时非空字符串显示红色错误提示。当错误被清除setError(null)或setError()错误提示自动消失。这就是响应式条件渲染——你只需要改数据界面自己会跟着变。六、全文总结本文从一个真实的浏览器端 AI 推理项目出发系统梳理了以下技术链路端侧模型模型从云端走向本地从服务器走向浏览器。核心理念是数据不出设备WebGPU 是浏览器端 AI 的关键基础设施。React TypeScriptAI 时代大型前端项目的首选技术栈。函数式组件 Hooks 模式提供了强大的抽象能力。TailwindCSS原子化 CSS 框架用堆类名替代写 CSS开发效率翻倍。Vite 插件在构建时按需注入样式。React 组件化函数 组件JSX 模板。所有 JavaScript 能力直接用于 UI 表达。状态驱动useStateuseEffect实现响应式数据绑定数据变化自动驱动界面更新。七、核心知识点复盘序号知识点一句话总结1端侧模型LLM 运行在用户设备上数据不出设备隐私安全2ONNX开放神经网络交换格式跨框架跨平台的模型标准3HuggingFace全球最大开源模型社区AI 界的 GitHub4WebGPU浏览器原生 GPU API替代 WebGL 做高性能计算5useStateReact 状态钩子创建响应式数据[值, 更新函数]6useEffectReact 副作用钩子组件渲染后执行第二个参数控制执行时机7!!双重否定强转布尔值!!undefinedfalse!!{}true8JSXJavaScript XML在 JS 中写 HTMLReact 的核心语法9classNameJSX 中替代class因为 class 是 JS 关键字10Tailwind原子化 CSS 框架类名即样式按需生成不写 CSS 文件11Vite 插件扩展 Vite 能力处理 JSX、Tailwind 等像搭积木12ESLint代码约束工具确保团队代码风格一致13条件渲染{condition Component /}或三目运算符14响应式数据变化 → 界面自动更新无需手动操作 DOM八、常见问题 / 避坑指南Q1!!navigator.gpu和Boolean(navigator.gpu)有区别吗没有本质区别效果一样。!!更简洁是 JS 社区的惯用写法。不推荐new Boolean()。Q2useEffect第二个参数传空数组[]时函数什么时候执行组件首次挂载到 DOM 后执行一次。类比 Vue 的mounted()生命周期钩子。Q3为什么不直接在useState里写useState(null) useState(出错了)会怎样不会怎样初始值只是第一次渲染时的状态。后续通过setError更新。这里给出错了是为了演示错误状态 UI。Q4Tailwindimport 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