技术探索与实践踩坑记录:从前端恐惧症患者到能写 React 的嵌入式老狗
去年双11前夕,我还在公司仓库里帮硬件团队调一个 Zigbee 网关的固件。手里的 J-Link 还没拔下来,手机就震了——是我们后端组的老张:“兄弟,你不是会点 Go 吗?能不能顺手把前端那块也搞了?产品说要赶在大促前上线管理后台……”
我当时差点把示波器探头甩出去。
我是谁?一个从 STM32 裸机编程一路摸爬滚打出来的嵌入式工程师,Vim 是我的第二皮肤,Makefile 比女朋友还熟。三年前转 Go 开发,本以为终于告别了寄存器手册和时序图,结果现在让我碰前端?
但现实很骨感——我们团队只有 4 个人,两个 Go 写业务逻辑,一个运维(兼 DBA 兼客服),剩下一个就是我,负责“其他所有”。产品经理已经把 PRD 发群里三天了,UI 设计稿都交了,前端没人写,项目卡住,deadline 就在眼前。领导拍我肩膀:“你不是学得快嘛?Go 都啃下来了,前端不就是 HTML + JS 嘛,能难到哪去?”
行吧,为了年终奖,干!
初识前端:我以为的 vs 实际的
一开始我真以为前端就是 <div>Hello World</div> 加点 onclick。于是我在网上搜了个“React 入门教程”,跟着敲了几个组件,本地跑起来还挺像那么回事。我甚至在 Vim 里装了 vim-jsx 插件,美滋滋地觉得:“这不比 C++ 容易多了?”
然而,当我试图把 UI 设计稿变成代码时,噩梦开始了。
第一个坑:模块打包。
我按照教程用 create-react-app 初始化项目,结果 npm start 直接报错:
Error: Cannot find module 'webpack'
我:???我不是刚装的吗?
后来才知道,Node 版本太低(我还在用 v12,因为公司 CI 环境卡死了)。升级到 v18,又遇到 node-sass 编译失败——因为 ARM 架构(我用的是 M1 Mac,别问,问就是公司配的)。折腾了整整一天,最后换 sass(Dart Sass)才搞定。
第二个坑:CSS-in-JS vs Tailwind vs 原生 CSS。
UI 给的设计稿用的是 Tailwind 风格,但我看教程都是用 styled-components。我傻乎乎地两个都装了,结果样式冲突,按钮忽大忽小,hover 效果乱飞。测试同事提了个 bug:“这个按钮点一下会闪红光,像警报似的”,我查了半天才发现是两个 class 名重复了。
第三个坑:状态管理。
页面有表格、筛选器、分页、弹窗,数据还要和后端 API 对接。我一开始全塞进 useState,结果组件嵌套三层之后,状态传递像俄罗斯套娃,改一个字段要传五层 props。某次不小心删了个 prop,整个页面白屏,控制台一行红字:
Cannot read properties of undefined (reading 'map')
那一刻我真的想砸电脑。
转折点:从“能跑就行”到“有点讲究”
事情的转机发生在一个周五晚上。我加班到十点,正对着满屏的 useEffect 回调抓狂,突然想起之前看 Go 项目时用的 状态机 思路。前端的状态,不也可以抽象成“加载中 / 成功 / 失败 / 空”几种状态吗?
于是我重构了数据获取逻辑,引入了 useReducer:
// 简化版的数据加载 hook
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return { ...state, isLoading: true, isError: false };
case 'FETCH_SUCCESS':
return { ...state, isLoading: false, isError: false, data: action.payload };
case 'FETCH_FAILURE':
return { ...state, isLoading: false, isError: true };
default:
throw new Error();
}
};
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await fetch(url);
const data = await result.json();
if (!didCancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: data });
}
} catch (error) {
if (!didCancel) {
dispatch({ type: 'FETCH_FAILURE' });
}
}
};
fetchData();
return () => { didCancel = true; };
}, [url]);
return [state, setUrl];
};
这段代码其实抄了 Dan Abramov(React 核心团队)的一个博客示例,但关键是我理解了它的意图:把副作用和状态更新解耦,避免竞态条件(race condition)。以前我总在 useEffect 里直接 setData,结果快速切换 tab 时,旧请求覆盖新数据,用户看到的是上一页的内容。
用了这个 hook 之后,Bug 少了一半。
与后端协作:API 联调的血泪史
前端写完,该对接 Go 后端了。我们的 API 用的是 Gin 框架,返回 JSON。按理说很简单,但实际联调时问题一堆。
最离谱的一次:前端 POST 提交表单,后端收不到 body。
我检查了请求头、Content-Type、CORS,全对。Wireshark 抓包发现数据确实发出去了。最后发现是 Gin 默认不解析 application/json 以外的格式,而我的前端(用 Axios)默认发的是 application/x-www-form-urlencoded。
解决方法?要么前端改:
// Axios 全局配置
axios.defaults.headers.post['Content-Type'] = 'application/json';
要么后端加中间件:
// Gin 注册 Form 解析
r.Use(gin.BindJSON)
但更根本的问题是:前后端契约不清晰。产品经理只给了 UI,没给 API 文档。我俩(我和另一个 Go 开发)靠口头约定字段名,结果他写 user_id,我写 userId,对不上。
后来我们痛定思痛,引入了 OpenAPI (Swagger)。用 Go 的 swag 工具自动生成文档,前端根据 schema 写类型定义。我还用 openapi-typescript 自动生成 TypeScript 接口:
npx openapi-typescript http://localhost:8080/swagger/doc.json -o src/api/types.ts
从此,字段名拼写错误、类型不匹配的问题基本消失。测试同事都说:“最近提的前端 bug 少多了。”
选型权衡:为什么最终选了 Vite + React + TypeScript
一开始我用的是 create-react-app(CRA),但越用越觉得慢。热更新要 3 秒,改个颜色都要等半天。我们嵌入式出身的人,对“延迟”特别敏感——以前烧录固件都要看进度条,现在写代码也要等?
后来我试了 Vite。第一次跑 vite dev,热更新快到我以为没生效。官方说“毫秒级”,我信了。
| 工具 | 热更新时间 | 构建速度 | 配置复杂度 |
|---|---|---|---|
| CRA | ~3s | 慢 | 低(黑盒) |
| Vite | <200ms | 快 | 中(可配置) |
| Webpack 手动 | 可优化 | 可优化 | 高 |
我们选 Vite,不仅因为快,还因为它原生支持 TypeScript、JSX、CSS Modules,不用额外装 loader。而且它用 ESBuild 做依赖预构建,启动飞快。
至于为什么用 TypeScript?被逼的。有一次我把 user.name 写成 user.nmae,页面直接崩了。Go 写习惯了,没有类型检查真的睡不着觉。TS 虽然学习曲线陡,但配合 VS Code(我破天荒装了一次,因为 Vim 插件对 TS 支持太弱),智能提示+类型推导,效率反而更高。
踩坑总结:给硬件转前端的朋友几点建议
- 别轻视前端生态。它不是“简单网页”,而是一个完整的工程体系。打包、构建、类型、状态、测试……每个环节都有坑。
- 从真实需求出发,别一上来就学 Redux、Next.js。我们只是做个内部管理后台,用 React + Vite + TS + Axios 足够了。
- 善用工具链。ESLint + Prettier 自动格式化,Husky + lint-staged 提交前检查,CI 跑测试——这些我们在 Go 项目里用的,前端一样需要。
- 和后端共建契约。API 文档不是可有可无,而是协作基石。Swagger/OpenAPI 是底线。
- 保持嵌入式思维的优势:注重性能、资源占用、稳定性。比如我们禁用了 React DevTools 生产构建,首屏加载控制在 1s 内。
最后:关于 Rust 和未来的思考
最近我在刷 LeetCode 准备跳槽,顺便研究 Rust。不得不说,Rust 的所有权模型让我想起了嵌入式开发时对内存的敬畏。Go 虽然好写,但 GC 偶尔还是会卡顿;而 Rust 既能写高性能服务,又能编译成 WASM 跑在前端——说不定下次我就用 Rust + Yew 写前端了(开玩笑的,Yew 生态还不成熟)。
但这次前端之旅让我明白:技术没有高低贵贱,只有合适不合适。以前我鄙视前端是“切图仔”,现在我知道,一个复杂的交互系统,其状态管理和用户体验设计,丝毫不比写一个 RTOS 简单。
上周五,产品验收通过,管理后台正式上线。虽然 UI 还有点丑(毕竟不是专业前端),但功能完整、稳定运行。领导请我们吃了顿火锅,席间笑着说:“没想到你这硬件老狗,还能写出像样的前端。”
我夹了片毛肚,心想:下个项目,我要试试用 Rust 写个 CLI 工具,再用 Go 写 API,前端继续 React —— 全栈老狗,正在进化。
(完)
附:关键依赖版本参考(2024年中)
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"axios": "^1.6.0",
"vite": "^5.0.0",
"typescript": "^5.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"eslint": "^8.50.0",
"prettier": "^3.0.0"
}
}
如果你也是从底层转应用开发,别怕前端。它可能不如寄存器手册严谨,但足够有趣。毕竟,能让用户点按钮时笑出来,也是一种成就感,对吧?

评论 0