技术探索与实践踩坑记录:从前端恐惧症患者到能写 React 的嵌入式老狗

Debug到怀疑人生
2025-12-17 08:16
阅读 2985

去年双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 支持太弱),智能提示+类型推导,效率反而更高。


踩坑总结:给硬件转前端的朋友几点建议

  1. 别轻视前端生态。它不是“简单网页”,而是一个完整的工程体系。打包、构建、类型、状态、测试……每个环节都有坑。
  2. 从真实需求出发,别一上来就学 Redux、Next.js。我们只是做个内部管理后台,用 React + Vite + TS + Axios 足够了。
  3. 善用工具链。ESLint + Prettier 自动格式化,Husky + lint-staged 提交前检查,CI 跑测试——这些我们在 Go 项目里用的,前端一样需要。
  4. 和后端共建契约。API 文档不是可有可无,而是协作基石。Swagger/OpenAPI 是底线。
  5. 保持嵌入式思维的优势:注重性能、资源占用、稳定性。比如我们禁用了 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

最热最新
暂无评论
Debug到怀疑人生Lv.1
0
影响力
0
文章
0
粉丝