☰
软硬一体V1项目封装实战:从axios二次封装到PCB封装库
2026/9/27 4:40:44 网站建设 项目流程

1. 进入封装阶段,先把“封装”拆成三件事

V1 项目进入“封装”节点那天,我原本以为活儿不大:软件打几个包,硬件导几份光绘,再写一份总结文档就收工。真正开工后才发现,团队里每个人嘴里的“封装”根本不是同一个词——软件在说函数、接口、模块的收口,硬件在说原理图符号、PCB焊盘、封装库,而产线那边说的又是外壳、打样和装配。为了不让“封装”二字把大家带沟里,我先把这次 V1 涉及的封装对象拆了一遍,然后再分头推进。

这次 V1 项目是个软硬一体的设备端产品。硬件主控用了 Cortex-M 级别的 MCU,外围搭配 EMMC 存储、Type-C 16Pin 接口、电源用的 PWR2.5 座子,板上器件从 0603、0805 的阻容到 TSSOP-10、SOP-20W 这类小封装 IC 都有。软件侧则是“管理后台 + H5 端 + 设备端”三件套,管理后台要对接大模型做 AI 交互,H5 端要兼容两个正式域名。这里面的“封装”任务可细分成三类:

  • 代码封装:请求库的二次封装,接口的收口,AI 流式输出的模块封装,以及生成器/迭代器这类处理数据结构的封装。
  • 硬件封装库:建原理图符号、PCB焊盘、芯片封装,整理 AD/Allegro 的封装库,并处理跨软件转换。
  • 系统/协议层面的封装:把设备协议、分发方式、系统镜像做成统一可复用的“外壳”。

这样一分,事情就很清楚了。网上被频繁搜索的“封装”关键词也基本都是这三块:要么是“axios 二次封装”“uniapp 封装 H5”“SSE 流式输出配合 abort”这类软件话题,要么是“0603 封装尺寸”“AD 封装库”“Allegro 封装制作流程”这类硬件话题。很多朋友其实是被其中的某一块卡住了,但“封装”这个总关键词把他们聚集到了同一屏搜索结果里。这篇就以这次 V1 为主轴,把三类封装分别讲透,结尾再给一份可以直接拿去用的封装复盘清单。

2. 软件侧:从请求封装到迭代器封装的落地细节

2.1 axios 二次封装:拦截器比想象中重要

先说后台管理端。前端技术栈是 Vue3 + Vite,网络层我选了 axios,但没有直接在最外层写一堆业务逻辑,而是先做了一层“中间件”式的封装。直接给出骨架:

import axios from 'axios' import store from '@/store' import router from '@/router' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 15000, }) service.interceptors.request.use( (config) => { const token = store.getters.token if (token) { config.headers['Authorization'] = `Bearer ${token}` } return config }, (error) => Promise.reject(error) ) service.interceptors.response.use( (response) => { const res = response.data if (res.code !== 0) { if (res.code === 401) { store.dispatch('logout') router.push('/login') } return Promise.reject(new Error(res.message || '请求失败')) } return res.data }, (error) => { return Promise.reject(error) } ) export default service

这套封装的核心不是“少写几行请求代码”,而是把鉴权、错误码、超时处理统一收口。很多朋友第一次写 axios 封装时只顾着把 baseURL 和 token 放进去,却忽略了响应的“拆包”逻辑。实际用起来才发现,接口返回的数据结构如果不在这里统一拆掉,业务组件里就会到处出现res.data.data.data,而且一旦后端改了返回结构,你能体会到拆地雷式改文件的痛苦。所以我在 interceptor 里直接约定:后端统一返回{ code, message, data },code 非 0 视为异常,401 直接踢回登录页。这样业务层拿到的永远是最干净的 data。

提示:这里的 code 约定是 0,很多后端习惯用 200,不管哪种,前后端在接口文档里必须先统一,否则封装层再怎么写都是白搭。

2.2 小程序与 uniapp 场景下的请求封装差异

除了后台管理端,这次 V1 还有一个 uniapp 编写的 H5 端。如果你真的用过 uniapp,会发现它默认没有 axios,而是自带的 uni.request。直接把 axios 那套搬过来是不行的,因为 uni.request 的回调风格、拦截器实现都不太一样。做 uniapp 的请求封装,我习惯包一层 Promise:

const request = (options = {}) => { return new Promise((resolve, reject) => { uni.request({ url: options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', ...(options.header || {}), }, success: (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { const body = res.data if (body.code === 0) { resolve(body.data) } else { uni.showToast({ title: body.message || '请求失败', icon: 'none' }) reject(new Error(body.message)) } } else { reject(new Error(`HTTP ${res.statusCode}`)) } }, fail: (err) => reject(err), }) }) } export default request

注意这里把鉴权 token 的注入放在 header 里,没额外写拦截器。原因是 uniapp 的拦截器在不同平台(H5、小程序、App)上的表现不完全一致,早期版本还有不少兼容问题。为了让 V1 能按时交付,我宁愿用最朴素的 Promise 包裹,也不引入额外一层不确定性。这里也给新手一个建议:封装不是越复杂越好,复杂封装带来的抽象成本和你自己的调试成本要成正比。

2.3 生成器与迭代器封装函数:处理树形数据的利器

代码封装里另一个容易被忽略的是数据结构处理。这次 V1 的权限菜单是树形结构,后端返回的是一份扁平列表,需要前端自己加工成嵌套树。如果每次都在业务组件里递归,不仅代码冗余,而且很容易因为深拷贝不及时把原数组改了。我封装了一个用生成器实现的树遍历函数:

function* walkTree(nodes, childrenKey = 'children') { for (const node of nodes) { yield node if (Array.isArray(node[childrenKey]) && node[childrenKey].length) { yield* walkTree(node[childrenKey], childrenKey) } } } // 使用示例:扁平列表转树,保留原数据不变 const buildTree = (flatList, pidKey = 'pid', idKey = 'id') => { const map = new Map() const roots = [] flatList.forEach((item) => map.set(item[idKey], { ...item, children: [] })) flatList.forEach((item) => { const node = map.get(item[idKey]) if (item[pidKey] && map.has(item[pidKey])) { map.get(item[pidKey]).children.push(node) } else { roots.push(node) } }) return roots }

这段代码看着简单,它其实已经把手头两个问题各自封装好了:扁平数据转树、按生成器协议遍历树。生成器的好处是惰性执行——你只在需要遍历的时候一点一点取,而不是一次性把整棵树的递归结果全塞进内存。配合迭代器协议,后续要对菜单做权限过滤、节点搜索、展开状态还原,都直接调用同一个函数,业务代码会干净很多。

封装继承多态这些老生常谈,放到真实项目里其实就是“把变化隔离出去、把公共逻辑收敛回来”。你不用背一堆名词,动手写一两次自然就理解了。

3. AI 交互逻辑封装:SSE 流式输出与 abort 的配合实战

3.1 为什么 AI 对话不能用普通 fetch 一把梭

V1 项目的管理后台需要接入大模型做智能助手,核心体验是“回答要实时渲染”。一开始团队有人提议直接用普通 fetch 请求,把完整的 JSON 拿回来再一次渲染,结果用户反馈很直接:“点完发送按钮,两三秒屏幕没动静,以为卡死了。” 这其实是所有流式交互的通病:人在等待超过 1 秒时就会开始焦虑。

要解决实时渲染,最主流的方案是 SSE(Server-Sent Events)。大模型的接口基本都支持流式返回 token,服务端会把内容一段一段推下来。这里的“封装”不是说调一个流式接口那么简单,而是要把连接建立、数据解析、异常退出、手动停止都包成一个可复用的模块。如果每个页面都自己写 fetch + ReadableStream,代码会以肉眼可见的速度腐烂。

3.2 封装一个带 abort 控制的 SSE 模块

我最终封装了一个SSEChannel类,核心代码如下:

class SSEChannel { private controller: AbortController | null = null private reader: ReadableStreamDefaultReader<Uint8Array> | null = null private buffer = '' private onMessage: (text: string) => void private onDone: () => void private onError: (err: unknown) => void constructor( private url: string, handlers: { onMessage: (text: string) => void onDone?: () => void onError?: (err: unknown) => void } ) { this.onMessage = handlers.onMessage this.onDone = handlers.onDone || (() => {}) this.onError = handlers.onError || (() => {}) } async start() { this.controller = new AbortController() try { const resp = await fetch(this.url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream', }, body: JSON.stringify({ /* 你的请求参数 */ }), signal: this.controller.signal, }) if (!resp.ok || !resp.body) { throw new Error('SSE 连接建立失败') } this.reader = resp.body.getReader() const decoder = new TextDecoder('utf-8') while (true) { const { value, done } = await this.reader.read() if (done) break this.buffer += decoder.decode(value, { stream: true }) const lines = this.buffer.split('\n') this.buffer = lines.pop() || '' for (const line of lines) { if (!line.startsWith('data:')) continue const data = line.slice(5).trim() if (data === '[DONE]') { this.onDone() return } try { const json = JSON.parse(data) const content = json.choices?.[0]?.delta?.content || '' if (content) this.onMessage(content) } catch { // 忽略无法解析的中间帧 } } } } catch (err) { if ((err as Error).name === 'AbortError') { // 主动停止,不视为异常 return } this.onError(err) } } abort() { this.controller?.abort() this.reader?.cancel().catch(() => {}) } }

这里有几个关键点很多人会忽略。第一,为什么用 fetch 而不用 EventSource?因为 EventSource 不支持自定义请求头,也不支持 POST 带 body,而现在多数大模型接口都是 POST。第二,为什么要在 while 循环里手动split('\n')?因为 SSE 是按事件流一帧一帧推下来的,底层 ReadableStream 每次返回的 chunk 长度不固定,可能在任意字符处断掉。如果不做缓冲区处理,经常会出现半截 JSON 解析失败。第三,abort 的作用是人工停止生成,用户点击“停止生成”按钮时调用this.controller.abort(),底层连接立即断开,前端 UI 马上停住。

3.3 配合 abort 时踩到的几个坑

封装模块已经写出来了,真正跑起来才发现坑都在细节里。

  • 缓冲字符问题。上面代码里的this.buffer必须保留不完整的尾行,否则会出现每帧都解析失败的情况。我第一次写的时候忘了做 buffer,结果屏幕上只出字,但 console 里全是 parse error,查了半天才发现是流分片导致的。
  • abort 和 onError 的竞态。调用abort()后浏览器会抛一个AbortError,如果 catch 里不加判断,会把主动停止也算成异常,弹一个“连接中断”的错误提示。后面我改成判断err.name === 'AbortError',主动停止就静默处理。
  • 大模型输出的 markdown 渲染。流式输出拿到的是 token 片段,不能每收到一段就整段重新渲染,否则用户滚动时会看到光标跳动;我最终采用了“增量追加到缓冲区 + requestAnimationFrame 节流渲染”的方案,一帧内只渲染一次,体验明显顺滑。
  • 后端 SSE 与 nginx 的缓冲。服务端跑在 nginx 后面,nginx 默认会缓冲响应,导致你在前端等半天收不到第一个 token。需要把X-Accel-Buffering设为 no,并在 nginx 中关闭相关缓冲,这些要在部署文档里写清楚,否则接手运维的人会很痛苦。

这节内容放在整个 V1 的角度看,就是把 AI 交互逻辑封装成了一个黑盒:业务组件只需要new SSEChannel(url, handlers)然后start(),所有协议细节都藏在模块内部。后续如果要加重新生成、历史记录、流式输出开关,改动范围都被锁在这一个文件里,这就是封装带来的直接收益。

4. 硬件侧的大头:PCB 封装库整理与转换的真实经历

4.1 跟着一堆封装尺寸和型号做斗争

硬件这块,V1 项目电路板上的器件不算多,但器件类型很杂:电源部分的 PWR2.5 座子、卧贴 4.5×4.5mm 轻触开关、Type-C 16Pin 连接器、EMMC 存储芯片、STM32H7 系列主控,还有一批 0603/0805 的阻容、TSSOP-10 的器件、SOP-20W 的驱动芯片。说到建封装库,第一个要面对的就是“尺寸地狱”。

以最常见的 0603 和 0805 为例:

  • 0603 封装:外形 1.6mm × 0.8mm,焊盘建议宽度 0.8mm,焊盘间距约 0.8mm(以厂商手册为准)。
  • 0805 封装:外形 2.0mm × 1.25mm,焊盘尺寸相应放大到 1.0mm 左右。

这些数字如果只凭记忆乱填,生产时很容易和钢网、回流焊工艺打架。正确做法是每个封装都去查原厂数据手册的 recommended land pattern,而不是抄别的板子上的现成封装。

还有一些容易让人迷惑的型号。比如“2.5×3mm 是什么封装型号”,这个问题单独看没法回答,因为外形尺寸并不能唯一确定封装型号,可能是 SOT 系列、晶振封装、或者某些特殊二极管封装,必须结合引脚数量和功能手册才能判断。我的习惯是:凡是遇到叫不出名字的封装,先把手册里的三视图截下来,标上外形、间距、焊盘尺寸,放进一个“待确认封装”文件夹,等确认后再转移到正式库。如果后续业务做到 SiP、3D 堆叠这类东西,还需要专门补半导体先进封装技术的知识,那和普通 PCB 封装完全是两套体系。

4.2 AD/Allegro 封装制作与转换流程

V1 项目原先在 Altium Designer(AD)里画原理图,后来部分信号完整性仿真要迁到 Cadence Allegro 环境,封装库必须跟着转。这一步是最容易翻车的。AD 的封装文件用的是.PcbLib,Allegro 用的是.dra/.psm,两者本质上是两种完全不同的数据模型。我试过直接导入,结果焊盘形状、丝印层、参考点全都错位,最后老老实实按标准流程重新做:

  1. 在 Allegro 中设置好用户环境变量:padpath、psmpath,指向自定义封装库目录。
  2. 用 Pad Designer 制作焊盘,先做 flash symbol(热焊盘)和 regular pad,再设置钻孔尺寸、通孔属性。
  3. 用 Package Symbol 编辑器创建封装,放置焊盘、添加装配层位号、丝印外框、约束区域,并设置 refdes 和 value 的字体大小。
  4. 检查 Pin Number 顺序。这一步非常关键,AD23 里如果焊盘顺序需要重新按顺序编号,我一般先按原理图 symbol 的管脚顺序列出映射表,再在封装编辑器里逐个检查,避免用“自动编号”产生隐蔽错位。

封装转换同样要考虑 AD 导入 PADS、AD 转 Allegro 这类场景。常规做法是用 ASCII 格式作为中间桥梁,但无论哪条路径,转完后必须做一个“3D 模型比对”:让封装工程师把两种软件里的封装都导出成 STEP 模型,放进同一坐标系叠一下,看丝印、焊盘、实体高度是否一致。V1 项目里就是因为漏了这一步,一个连接器的封装从 AD 转 Allegro 后焊盘中心偏移了 0.2mm,打样回来才发现,硬生生浪费了一版板子。

4.3 常用封装尺寸参数表与识别引脚的技巧

整理封装文档时,我会把常用的封装参数做成表格,方便团队直接对照。

封装名称常见外形/间距备注
06031.6mm × 0.8mm焊盘宽度约 0.8mm
08052.0mm × 1.25mm功率稍大的阻容
TSSOP-10引线间距 0.5mm本体宽约 3.0mm
SOP-20W引线间距 1.27mm宽体封装
EMMC BGA-153球距约 0.5mm需要看芯片规格书球排列
Type-C 16Pin引脚间距 0.5mm注意电源/信号引脚定义
PWR2.5适配 2.5mm 电源座孔径按端子规格确认

“封装怎么识别引脚”也是新手常问的问题。我总结的经验是四看:一看位号旁的丝印圆点或斜角(Pin 1 标记);二看芯片手册的 TOP VIEW 图;三看底视图和顶视图是否镜像(BGA 往往容易搞反脚位);四看原理图 symbol 和 PCB 封装的 Pin Number 映射。一句话,所有引脚识别最后都落到“数据手册”四个字上。

还有一个特别容易踩的坑:只看外形相似就套用封装。比如 DB9 和 DB15 接口,远看都是 D-sub 形状,但如果直接套用封装,轻则插不进去,重则烧板子。DB9 和 DB15 的引脚数量、引脚间距、外壳尺寸都不一样,不能混用。连接器类封装我强烈建议去封装库网站下载原厂推荐封装,或者按官方结构图重建,不要凭着“目测差不多”来做。如果是 XCZU19EG-2FFVC1760 这类大型 BGA FPGA,更是建议直接找官网封装文件再校验,手工慢慢画很容易出错。

5. 协议与系统级封装:DL645 电表接入和多域名 H5 分发

5.1 像封装接口一样封装设备协议

这次 V1 项目里还涉及一个工业场景:现场要采集 DL645-2007 电能表的数据,上层平台要拿到统一的电表读数。如果直接在业务系统里写一套 DL645 的报文解析代码,那么以后换一块支持其他规约的电表,代码又要重写。我的做法是把它当作一次“协议封装”来处理:维持上层接口不变,底层适配不同的设备规约。

具体到技术栈,我用了 Kepware 作为中间连接件。Kepware 是工业协议网关软件,可以把各种各样的设备协议(Modbus、DL645、OPC UA 等)统一变成上层可访问的数据项。这里要做的封装分两层:

  • 驱动层:在 Kepware 里建一个 Channel,配置以太网连接,填写电表的 IP 地址和端口。
  • 数据映射层:把 DL645-2007 的数据标识(如电能量、瞬时电压、电流)映射成统一的变量名,比如CurrentEnergy、VoltageA、CurrentA。

这样一来,业务系统只需要订阅 Kepware 暴露的标准接口,完全不用关心底层是 DL645 还是 Modbus。演示效果就是:一套软件,今天接 DL645 电表,明天接 Modbus 电表,只要改 Kepware 内部的驱动配置,业务代码一行不改。这个过程本质上是“把设备的方言封装成普通话”,和代码里的接口封装思路完全一致。

实际踩坑是地址格式。DL645 的报文里数据地址是四个字节十六进制,很多驱动文档里写地址时要倒序或者按规约的偏移量换算,第一次配的时候怎么都读不到数据,最后是抓包看规约帧,才确认地址映射需要从数据标识低字节开始填。强烈建议在联调前先准备一个 DL645 模拟器,把报文打印出来和实际设备比对,能省大量现场排查时间。

5.2 uniapp 封装 H5 如何指向两个域名

H5 端这次要支持两个正式域名:一个面向内部环境,一个面向公网客户。直接在某一个页面里写死 API 地址肯定不行,正确做法是把域名配置提升到“构建环境”层面。

我在 uniapp 项目根目录放了.env.development、.env.production两组变量,里面定义VITE_API_BASE_URL和VITE_WEB_BASE_URL。打包时按目标环境分别构建:

# 构建 OA 环境包 VITE_API_BASE_URL=https://oa-api.example.com VITE_BUILD_TARGET=oa npm run build:h5 # 构建公网环境包 VITE_API_BASE_URL=https://public-api.example.com VITE_BUILD_TARGET=public npm run build:h5

打包脚本里用 cross-env 设置环境变量,代码里统一通过import.meta.env.VITE_API_BASE_URL读取。这样 H5 项目虽然是同一套代码,但生成的两个静态包指向不同 API 域名,两个域名互不干扰。如果将来要上微信公众号或者 App 内嵌,只要在 H5 封装分发平台上传对应构建包就行,代码不用再动。

这里还有一个容易被忽略的体验问题:H5 封装分发平台的更新机制。如果 H5 包被分发到 App 的 WebView 里,最好的更新方式是让 App 每次冷启动时请求一次最新版本号,与本地缓存的包版本做对比。这个“版本检查 + 拉新包”的逻辑,最好也封装成一个独立模块,因为一旦要做灰度发布或者限流,你只需要改这一个模块。

5.3 系统封装的一个类比

如果项目涉及 Windows 设备交付,还会碰到 sysprep 这类系统封装工具。sysprep 的本质是把一台电脑上的驱动、用户配置、软件授权信息“抽象掉”,生成一个干净且可复制的镜像,然后分发给同型号的几十台设备。其实这和代码封装是同一个道理:把个性剥离、把共性固化。我每次跟团队讲“为什么要封装”时,都会搬这个类比出来,大家一下子就懂了。

6. V1 复盘:封装规范化清单与踩坑记录

6.1 封装前先回答五个问题

V1 忙完之后,我把封装相关的工作复盘了一遍,最大的收获是在大规模封装之前,应该先让团队回答五个问题:

  1. 封装的边界在哪里?这个模块/库应该对外暴露什么,隐藏什么?
  2. 命名规范是否已经定义?AD/Allegro 库、接口函数、环境变量,都必须有统一前缀或命名法则。
  3. 版本怎么管理?封装库和代码库一样需要 git 管理,封装库的变更记录也要可以 diff 回溯。
  4. 有没有测试用例?软件封装要有单元测试,硬件封装要有 3D 模型比对和试产验证。
  5. 谁负责维护?没有 owner 的封装库,三个月后就会变成一群谁都不敢动的陈旧代码。

其中命名规范和 git 版本管理最容易被省掉。V1 里我坚持把所有 PCB 封装库放进 git 仓库,配合 diff 工具比对封装库的版本差异,每次改动都能看到是哪个焊盘、哪层丝印发生了变化。软件侧也一样,vue 项目中封装函数的 git 版本差异比对,可以快速定位“以前能用现在不能用”是哪个 commit 引起的。很多项目到后期混乱,就是因为没有版本管理这个“后悔药”。

提示:硬件封装库最好把“封装名 + 创建日期 + 作者”写进自定义属性里,导出 BOM 和坐标文件时这些信息会跟着带出来,排查问题时非常有用。

6.2 一份可以直接抄走的检查清单

最后分享一份我在 V1 项目里整理出来的检查清单,可以当作团队评审的模板:

检查项说明状态
数据库/接口字段命名统一 camelCase 或 snake_case[ ]
请求封装含鉴权与错误码401 处理、HTTP 错误统一拦截[ ]
AI 流式输出支持 abort停止生成按钮和断线重连[ ]
PCB 封装焊盘编号顺序Pin1 与丝印一致,BGA 注意方向[ ]
封装库 3D 模型比对AD/Allegro/PADS 互转后确认尺寸[ ]
连接器封装选用官方推荐不凭目测套用相近封装[ ]
协议接入的可配置化切换设备规约不改业务代码[ ]
H5 多域名构建配置环境变量进入构建产物[ ]
封装对象纳入 git 管理可 diff、可回溯、有 owner[ ]

坦白讲,V1 项目并不是每个封装都做得完美。数据库那层的字段命名,因为一开始没定死,后期花了不少时间重构;AI 流式模块的 abort 竞态,也差点上线前没测出来。但正是这些问题让我认识到:封装不是某个时间节点的临时动作,而是贯穿整个开发周期的设计意识。每次动手封装前多想一步“这东西将来会怎样被复用”,养成了习惯,V1 的教训就会变成 V2 的本能。

以上是这次 V1 项目封装细节里能完整公开的部分。个人实际体会是,最具价值的不是封装本身,而是封装迫使你把边界理清楚——软件模块的边界、硬件焊盘的边界、协议兼容的边界。边界清楚了,项目后续的扩展和交接都会轻松很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询