写在前面:今天学了一个让我"惊掉下巴"的知识------端侧模型 。以前我们用 AI,都得调远程 API,数据发出去,等半天,还得担心隐私泄露。老师说,现在小参数模型已经可以在浏览器里跑了!对,你没看错------浏览器里跑 LLM。我们亲手做了一个 React + TypeScript + TailwindCSS 的项目,在浏览器端用 WebGPU 加载 DeepSeek-R1 的 1.5B 蒸馏模型。当看到模型在本地加载完毕、聊天框可以打字的那一刻,我整个人都兴奋了------以后再也不用把数据发给第三方了!
一、端侧模型:AI 也要"本地化"
1.1 远程 API 调用的痛点
老师说:
"有别于 OpenAI/DeepSeek API 调用------LLM 在远程,和调用客户端不在一起。贵、不安全。Context 会随着请求发送到服务器。"
远程调用的三大问题:
| 问题 | 说明 | 比喻 |
|---|---|---|
| 贵 | 百万 Token 几块钱,频繁调用钱包疼 | 每次打车去吃饭,不如楼下小吃店 |
| 不安全 | 你的 Prompt 和数据会发到别人服务器上 | 你把日记本寄给陌生人帮你读 |
| 依赖网络 | 没网就不能用 | 断网就成了砖头 |
1.2 端侧模型的好处
老师说:
"Ollama 本地开源模型部署,在客户端------端侧模型。手机端、汽车端、Agent 端。开源小参数模型就能完成这些任务。浏览器端,随时下载,随时使用。WebGPU。"
端侧模型 = 模型在用户本地运行,不联网,不出门。
我们的项目用的就是 DeepSeek-R1-Distill-Qwen-1.5B------一个 15 亿参数的蒸馏版推理模型,专门优化到可以在浏览器里用 WebGPU 加速运行。
二、React + TypeScript + TailwindCSS:AI 时代的"基建"
2.1 为什么选这套技术栈?
老师说:
"React + TS ------ AI 时代的大型项目首选前端技术。React 比 Vue 难入门,但大型项目用得多,AI 训练代码 React 的偏多。"
TailwindCSS 更是成了 Vibe Coding 的标配:
老师说:
"几乎不再需要写 CSS------原子类 CSS。TailwindCSS 已经成为 Vibe UI 的基本构成。写类名就行,自然语义编程。"
以前写 CSS:
css
.container {
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
background-color: white;
}
用 TailwindCSS:
jsx
<div className="flex flex-col items-center h-screen bg-white">
这就是"自然语义编程"------在 JSX 里用英文单词描述样式,不用再写选择器和 CSS Rules。
2.2 React 组件:函数就是组件
"搭积木的方式搭建页面。React 封装一个组件------函数就是组件。函数返回 HTML 就是组件。"
Progress 组件------一个进度条:
tsx
function formatBytes(size) {
const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return (
+(size / Math.pow(1024, i)).toFixed(2) * 1 +
["B", "kB", "MB", "GB", "TB"][i]
);
}
const Progress = ({ text, percentage, total }) => {
percentage ??= 0; // ES12 空值合并运算符
return (
<div className="w-full bg-gray-100 text-left rounded-lg overflow-hidden mb-0.5">
<div
style={{ width: `${percentage}%` }}
className="bg-blue-400 whitespace-nowrap px-1"
>
{text}
{percentage.toFixed(2)}%
{isNaN(total) ? "" : `of ${formatBytes(total)}`}
</div>
</div>
);
};
export default Progress;
几个关键点:
| 概念 | 说明 | 代码 |
|---|---|---|
| Props | 父组件传过来的属性,子组件只展示不修改 | { text, percentage, total } |
| 空值合并 | ES12 语法,没传参时默认 0 | percentage ??= 0 |
| JSX | 在 JS 里写 HTML | <div className="...">{text}</div> |
| className | 因为 class 是 JS 关键字,改用 className |
className="w-full bg-gray-100" |
老师说:
"封装者多考虑,使用者用得爽。
percentage ??= 0------当percentage为空时赋值为 0。初始化的时候,没有下载进度这个概念。如果传了呢?传入的值不赋值。"
三、React 的两种数据:State 和 Props
3.1 State:组件自己的状态
看 App.tsx 中的状态管理:
tsx
const [input, setInput] = useState(''); // 输入框内容
const [status, setStatus] = useState(null); // 模型状态 null | loading | ready
const [error, setError] = useState(null); // 错误信息
const [loadingMessage, setLoadingMessage] = useState('开始加载...');
const [progressItems, setProgressItems] = useState([]); // 下载进度列表
老师特别强调了一种设计思路:
"数据状态驱动界面状态。数据有不同的状态,界面不同的状态------川剧变脸。"
| status 的值 | 界面的表现 |
|---|---|
null |
显示"Load Model"按钮 |
"loading" |
显示下载进度条,按钮禁用 |
"ready" |
显示聊天输入框 |
这种"状态驱动"的思维,让 UI 逻辑变得非常清晰。
3.2 Props:父组件传给子组件
tsx
// 父组件 App 中循环渲染 Progress
progressItems.map(({ text, percentage, total }, i) => (
<Progress
key={i}
text={text}
percentage={percentage}
total={total}
/>
))
老师说:
"Props 从父组件传递到子组件的属性,不能在子组件里修改。子组件主要负责展示------父组件给我什么 Props,我显示成什么样子。报告父组件,才可以修改。"
State = 组件自己的"私房钱"。 Props = 父母给的"零花钱",只能花不能改。
四、React 合成事件和原生事件的区别
4.1 DOM 事件的发展史
看 event.html:
html
<button id="btn" onclick="console.log('点击了按钮')">按钮</button>
<script>
document.getElementById('btn')
.addEventListener('click', function() {
console.log('点击了按钮')
})
</script>
老师说:
"
onclick是最原始的 DOM0 级事件监听。addEventListener是 DOM2 级事件监听------同一个 DOM 元素可以多次监听同一事件。"
4.2 React 的合成事件
"React 代码洁癖------能不发明新概念就不发明。
@事件绑定是 Vue 的做法。React 直接用已有的概念,onClick作为高手没有学习成本。"
Vue:
html
<button @click="handleClick">点我</button>
React:
tsx
<button onClick={() => { setStatus("loading"); }}>点我</button>
老师说:
"React 里的事件并不是原生事件,是合成事件(SyntheticEvent)。它封装了浏览器的原生事件,解决了跨浏览器兼容性问题。"
用 onClick 而不是 @click,用 className 而不是 class------React 的原则就是"尽量不发明新概念"。
五、检测 WebGPU + 状态驱动的 UI
tsx
const IS_WEBGPU_AVALABLE = !!navigator.gpu;
"
!取反------navigator.gpu不支持的时候是undefined。!!再取反,一定可以转成true | false。双重否定等于肯定。"
然后整个 App 的返回根据这个值分为两种:
tsx
return IS_WEBGPU_AVALABLE ? (
// 支持 WebGPU ------ 显示完整的 LLM 界面
<div className="flex flex-col h-screen mx-auto items-center ...">
{/* 标题、按钮、进度条、聊天框 */}
</div>
) : (
// 不支持 ------ 提示用户
<div>您的浏览器还不支持 WebGPU</div>
);
这个三元表达式就是"条件渲染"------根据不同的条件,渲染不同的 UI。
六、总结:端侧 AI 是未来
| 概念 | 说明 |
|---|---|
| 端侧模型 | 在客户端本地运行 LLM,不依赖远程 API |
| WebGPU | 浏览器端 GPU 加速 API,支持本地模型推理 |
| React 组件 | 函数返回 JSX,组件化搭建页面 |
| JSX | JavaScript + XML,在 JS 里写 HTML |
| State | 组件自身的状态,useState 管理 |
| Props | 父组件传给子组件的属性,只读 |
| 合成事件 | React 封装的跨浏览器事件系统 |
| TailwindCSS | 原子类 CSS,自然语义写样式 |
空值合并 ??= |
ES12 语法,变量为空时才赋值 |
端侧模型的优势:不用花钱调 API、数据不出本地、断网也能用。React + TypeScript + TailwindCSS 是 AI 时代前端的标准基建,这套技术栈让你能快速构建出 AI 应用的界面。
写在最后
今天最大的收获,是亲眼看着一个 LLM 在浏览器里跑了起来。以前觉得"在本地跑 AI"是算法工程师的事,现在发现前端也能做------用 WebGPU + Transformers.js + ONNX Runtime,前端工程师也能让 AI 在用户浏览器里"原地运行"。这种感觉太酷了。
下次面试官问你:"端侧模型和 API 调用有什么区别?React 的合成事件是什么?"
你可以淡定地说:
"端侧模型是在客户端本地运行 LLM,比如在浏览器里通过 WebGPU 加载 DeepSeek-R1 的蒸馏模型。相比远程 API 调用,它的优势是免费、数据安全(不出本地)、离线可用。React 的合成事件(SyntheticEvent)是对浏览器原生事件的跨浏览器封装,解决了不同浏览器事件模型的兼容性问题。React 用 onClick 等驼峰命名绑定事件,而不是 Vue 的 @click------基本原则是尽量不发明新概念。在项目中我们还用了 TailwindCSS 的原子类来写样式,不需要手写 CSS,直接在 className 里用自然语义描述样式。"
然后看着面试官满意的表情,心里默念:这波,又稳了。
本文所有代码示例均来自课堂学习资料,真实可运行。