1. 先搞清楚 makefsdata 在整条链路里干什么
1.1 从"网页到底放哪"这个争论说起
做嵌入式 Web 服务的人几乎都遇到过同一个问题:设备上跑了一个 HTTP 服务器,能返回"Hello World",但真正要把一个带样式、带图标、带几张小图的配置页面放上去时,就开始纠结了。放 SD 卡?很多板子根本没卡槽,就算有,量产的机器也不希望你为了改一个按钮文字去拆机。挂外部 SPI Flash 做个简易文件系统?那就得再引入一套文件系统代码,还要处理擦写、坏块、掉电一致性,为了几个静态页面不值当。直接在代码里写sprintf拼 HTML?一百行以内还行,超过三百行就没人愿意维护了。
makefsdata就是在这个节点上出现的工具。它的定位非常单一:把你本地一个文件夹里的静态网页资源,全部转成 C 语言数组,直接编进固件里。设备跑起来之后,这套数组就相当于一个只读文件系统,HTTP 服务器从里面读文件、取长度、发数据,全程不需要任何外部存储、不需要动态内存分配、不需要文件系统。
我第一次用它是在一个只有 128KB Flash 的小板子上,硬是塞进了一个 40KB 左右的配置界面,包含 HTML、CSS、两个 JS、几个小图标。客户端浏览器打开的速度比我用 SD 卡方案时还快一截,因为读取就是一次指针偏移,连寻道都省了。
所以这篇文章解决的问题很具体:怎么把 makefsdata 用对、用稳,而不是生成一个编译报错或者浏览器白屏的 fsdata.c。适合的读者包括正在做嵌入式 Web 配置页的固件工程师、需要给设备加一个本地管理界面的产品开发者,以及刚接触这套轻量级 TCP/IP 协议栈、被fsdata.c里那堆十六进制数字吓到的人。哪怕你只是想让设备在浏览器里显示一个能改 WiFi 密码的页面,这篇里的流程也够用。
1.2 静态资源三种存放方案的横向对比
在动手之前,值得把可选方案摆在一起比一比。选型错了,后面所有功夫都是白费。我把常见的三种做法列成表,你可以直接对号入座。
| 方案 | 存储位置 | 更新方式 | RAM 占用 | 适用体量 | 主要代价 |
|---|---|---|---|---|---|
| 编进固件(makefsdata) | 片内 Flash 的只读段 | 重新烧录固件 | 几乎为零 | 几十 KB 到几百 KB | 改一个标点也要重新出固件 |
| 外部存储 + 文件系统 | SD 卡或外部 Flash | 拷卡、OTA 分区写入 | 需要缓存缓冲 | 几 MB 以上 | 文件系统代码、掉电风险、成本 |
| 运行时动态拼接 | RAM | 改代码 | 高,且易碎片 | 极小页面 | 难维护、易注入、不便于前端调试 |
这张表里最关键的一列是"RAM 占用"。用 makefsdata 生成的数组,标准写法是static const char data_xxx[],const让编译器把它放进只读段,链接脚本一般会把它映射到片内 Flash,运行时完全不占 RAM。这一条对只有几 KB RAM 的芯片来说是决定性的——同样一个 40KB 的页面,走文件系统方案可能要额外准备 512 字节到几 KB 的读写缓冲,而 makefsdata 方案是零。
代价也很明确:静态资源被冻结在固件版本里。你改一个按钮颜色,就得走一遍完整的编译、烧录、验证流程。所以我的习惯是,在项目早期就把页面结构定下来,把需要频繁变动的内容(比如设备名称、实时状态)交给 AJAX 接口去拿,静态部分才交给 makefsdata,两边分工清楚,后面就不会天天为了改文案重新出固件。
1.3 哪些场景别硬上 makefsdata
工具是好工具,但它有明确的边界。如果你的页面上限在 500KB 以上、需要用户上传图片、需要保存日志文件、或者需要 OTA 增量替换网页资源,那这套方案会很别扭。特别是当页面里包含大体积的字体文件(woff2 动辄几百 KB)时,编进固件的做法会显著拉长编译时间,某些编译器处理超大数组时还会明显变慢。
还有一个容易被忽略的点:makefsdata 生成的是只读文件系统。它没有写入接口,没有目录遍历接口(除了内部链表),也没有删除。如果业务上需要"用户上传一个配置文件到设备",那必须另外挂一个可写分区或者外部存储,静态页面从固件读,动态数据走另一条路,两者不要混为一谈。
我的经验判断标准是三句话:页面总量在一两百 KB 以内、更新频率低于固件迭代频率、没有写入需求——三条都满足就用 makefsdata;有任何一条不满足,就该认真考虑外部存储方案了。
2. 生成出来的 fsdata.c 到底是个什么东西
2.1 一个 C 数组就是一份完整的 HTTP 响应
很多人第一次打开生成的 fsdata.c 会懵:为什么数组开头不是<html>,而是一串HTTP/1.0 200 OK?原因很简单,这个数组里存的不是"文件内容",而是"HTTP 响应报文"——响应头加正文一起打包。
static const char data_index_html[] = { /* Header */ "HTTP/1.0 200 OK\r\n" "Server: lwIP/2.1.0\r\n" "Content-Type: text/html\r\n" "Content-Length: 1024\r\n" "\r\n" /* Content */ "<!DOCTYPE html>..." };服务器在处理请求时,做的事情就是把这段字节流原样塞进 TCP 发送队列,中间不做任何拼接。这样做的好处是零运行时开销——不需要在响应时动态生成头部、不需要查 MIME 表、不需要算长度,指针指过去,长度也是编译期常量。对一个要在几十毫秒内响应请求的小设备来说,这个设计很划算。
代价是灵活性:Content-Length在生成时就被写死了,所以你后期用脚本去改动 html 内容而不重新跑一遍工具,长度就对不上,浏览器会一直转圈等到超时。这是新手最容易踩的坑之一,后面第五节会细说。
对应地,结构体里有一个len字段记录正文长度(不含头部),还有一个offset概念用来标记头部占了多少字节。早期版本是直接在链表节点里存data + 头长度这样的指针偏移,新一点的版本拆成独立字段,但本质一样:头部和正文是同一块内存,靠偏移量区分。理解了这一点,你就能明白为什么不能用文本编辑器手工改 fsdata.c 里的内容。
2.2 链表和 FS_ROOT:服务器是怎么"找到"文件的
单个文件好办,多个文件怎么组织?答案是一条单向链表加上一个入口宏。生成文件的末尾通常长这样:
const struct fsdata_file file__index_html[] = { { NULL, data__index_html, data__index_html + 108, sizeof(data__index_html) - 1, FS_FILE_FLAGS_HEADER_INCLUDED }, }; const struct fsdata_file file__style_css[] = { { file__index_html, data__style_css, data__style_css + 96, sizeof(data__style_css) - 1, FS_FILE_FLAGS_HEADER_INCLUDED }, }; #define FS_ROOT file__style_css #define FS_NUMFILES 2FS_ROOT指向链表头,fs_open()从表头开始逐个比较name字段(注意每个节点的name实际是指向data里文件名那一小段的指针,不是独立字符串),匹配上了就返回句柄。所以查找是线性扫描,文件越多、排得越靠后就越多比较次数。文件数量在二三十个以内时,这点开销完全可以忽略;如果你有上百个文件,那就要考虑把最常用的那个放到链表尾部(也就是生成顺序里靠后),或者干脆检查是不是该换方案了。
链表节点的顺序由工具扫描目录的顺序决定,不同文件系统下(Windows 和 Linux)跑出来的顺序可能不一样。这点在你做 diff 对比固件版本时会有点烦,但功能上无所谓。
还有一个经常被忽略的宏FS_NUMFILES,某些版本的 HTTP 服务实现会用它做静态检查。如果你手工裁剪了某个文件,记得把它一起改掉,否则编译时可能出现数组越界的警告。
2.3 文件名到符号名的转换规则
C 语言标识符不能带.和/,所以工具会把文件名里的这些字符替换成下划线。index.html变成data__index_html(注意是两个下划线,第一个来自开头的斜杠或前缀,不同版本略有差异),img/logo.png变成data__img_logo_png。
这个规则带来两个实际后果。第一,目录层级会被压平:a/b/index.html和a_b_index.html有可能撞名字。虽然概率不高,但我在一个项目里就遇到过,两个不同目录下同名的style.css,生成后符号冲突,编译直接报重复定义。解决办法是给每个子目录换一个独特的前缀,或者在生成前就把目录结构设计得清爽一些。
第二,中文文件名、空格、特殊符号会造成麻烦。工具本身对这类名字的处理不统一,有些版本会直接跳过,有些会生成奇怪的符号名导致编译失败。我的做法是硬性规定:Web 资源目录下只用小写字母、数字、下划线、连字符和点,长度控制在 30 字符以内。这条规矩看起来小题大做,但能省掉很多莫名其妙的排查时间。
另外要说清楚一点:符号名只是编译期的标识,浏览器访问时用的还是转换前的原始路径。也就是说img/logo.png在浏览器里请求的是/img/logo.png,data__img_logo_png只存在于 C 代码里,用户看不见。
3. 命令行参数与一次完整的生成流程
3.1 拿到工具,或者自己编一个
这个工具通常随协议栈源码一起分发,一般躺在源码树的应用目录下,可能是一份 C 源码,也可能附带一个 Python 版本。获取方式无非三种:直接用现成的可执行文件、用系统自带的编译器现编一个、或者跑 Python 脚本。
用现成可执行文件最省事,但要注意版本匹配——老版本生成的代码结构和新版本的服务端实现可能对不上,尤其是结构体字段增减的情况,编译时会报成员不存在的错。
自己编也不难,本质就是一个不依赖第三方库的单文件程序:
gcc -O2 -o makefsdata makefsdata.c如果编译时报"找不到某个头文件",通常是因为源码里 include 了协议栈的其他头文件。这种情况最简单的办法是把需要的头文件路径一起加进命令行,或者干脆找一个已经整理好的独立版本。别去改源码里的 include 路径,改完后面升级又得重新来一遍。
Python 版本的好处是跨平台、改起来方便,缺点是生成速度比 C 版慢一些,几十个文件的目录大概要几秒钟。日常开发用它完全够,正式出固件前我一般还是用 C 版跑一遍,保持产物一致。
提示:不管用哪个版本,第一次运行时先不带参数跑一次,让它打印 usage。不同分支的参数集不完全一样,照着本地输出的帮助敲最靠谱,别照抄网上教程。
3.2 参数逐个过一遍,以及为什么要这么用
下面这张表是我常用的几个参数,基于常见的版本行为整理,具体以本地 usage 为准。
| 参数 | 作用 | 什么时候必须用 | 坑点 |
|---|---|---|---|
-i <文件名> | 指定默认首页名 | 首页不叫 index.html 时 | 名字写错会导致访问根路径 404 |
-d <目录> | 指定输出目录 | 想把产物和源码分开时 | 目录不存在时部分版本不自动创建 |
-r | 递归处理子目录 | 页面有 css、js、img 子目录时 | 不开的话子目录里的文件全丢 |
-f | 强制覆盖已有产物 | 重复生成时 | 不加可能提示文件已存在并跳过 |
逐个说清楚使用意图。-i决定的是"用户访问/时返回哪个文件",服务端会用它去匹配根路径。默认值通常是index.html,但如果你习惯用default.htm,就必须显式指定,否则浏览器打开首页就是 404,而你在目录里明明能看到文件,这种"看得见打不开"的错觉最容易让人怀疑人生。
-r是新手最容易漏的一个。不开递归时,工具只处理指定目录下的第一层文件,子目录里的内容会被静默忽略——注意是静默,不报错、不警告,生成过程一切正常,产物里就是少了那些文件。然后你打开网页,样式全丢、图标全裂,回头查代码查半天。我的习惯是只要目录里有子目录,就无脑加上-r,宁可多处理也不会漏。
-d用来控制产出位置。工程实践里我强烈建议把"网页源文件"和"生成的 fsdata.c"分开放在两个目录,用脚本串起来生成。原因很实际:fsdata.c 动辄几千行、几百 KB,如果和网页源码混在一起,你每次在编辑器里搜索都会命中一堆乱码,而且版本管理里这个文件会频繁产生巨大 diff,把真正有价值的改动淹没了。
-f属于"防止自己被自己坑"的参数。有些版本在目标文件已存在时会询问或跳过,非交互环境下就会卡住或者不更新产物。加上强制覆盖,每次都是干净重建,避免出现"明明改了网页但固件里还是旧的"这种诡异现象。
3.3 一次完整的生成流程,附目录规划
假设我要做一个设备配置页面,规划如下结构:
web_source/ ├── index.html ├── style.css ├── app.js ├── img/ │ ├── logo.png │ └── favicon.ico └── fonts/ └── icon.ttf生成命令大致是:
makefsdata -i index.html -r -f -d ../generated web_source执行完之后,../generated目录下会出现fsdata.c,少数版本还会附带头文件或者一个自定义版本的文件。这里有个细节值得提醒:输出文件名在不同版本里可能是 fsdata.c,也可能是 fsdata_custom.c,后者一般用于对接自定义的文件系统接口。集成前先看一眼文件名,别拿着 fsdata_custom.c 往标准工程里塞,然后对着"FS_ROOT 未定义"的报错发愣。
生成完成后,别急着集成,先做三步自检:
- 用文本编辑器打开产物,看数组数量对不对,
FS_NUMFILES是不是等于你的文件总数; - 搜索一下你最长的那个文件(比如字体),确认它的
Content-Length是个合理的数字,不是 0; - 搜一下
.css、.png这些扩展名的 Content-Type,确认没被识别成application/octet-stream。
第三步尤其重要。工具内部有一张 MIME 映射表,覆盖常见类型,但像.webp、.woff2、.svg这类相对新的格式,老版本可能不认识,于是统一按二进制流返回。浏览器收到二进制流,大概率不会渲染——图片不显示、字体不生效。发现这种情况,要么换格式(ico 换成 png,woff2 换成 ttf),要么在生成后手动把那几行Content-Type改掉,但要记得同步改回来,别下次重新生成又忘了。
4. 集成进固件:从一堆数组到浏览器里能打开的页面
4.1 工程里该放哪,怎么避免重复定义
集成的核心只有一件事:让 fsdata.c 参与编译,且只参与一次。
常见做法有两种。第一种是把生成的 fsdata.c 直接加入工程的文件列表,交给构建系统编译。第二种是把它放在 HTTP 服务实现的源文件旁边,通过包含方式引入(有些版本的实现里就有一行把 fsdata.c 包含进来)。两种方式都能跑,但绝对不要同时用,否则所有符号都定义两遍,链接阶段会报一堆multiple definition of data__index_html。
放好之后还有两个依赖要检查。一是生成文件里通常会 include 一个描述文件系统结构的头文件,这个头文件在协议栈源码的应用目录里,不在你的工程路径下,需要手动拷过来或者把路径加进 include 搜索目录。二是如果服务实现里有类似"是否使用内置文件系统"的开关,确认它是打开的,否则代码编进去了但服务端压根不去查表,表现为所有请求都 404。
我踩过一次比较隐蔽的坑:工程里同时存在一个旧的 fsdata.c 和一个手工建的占位文件,两个人各自加进工程了,编译能过(因为弱符号),但运行起来永远返回旧页面。这种情况用grep -r "FS_ROOT"全局搜一遍就能发现,建议集成完就搜一次,图个安心。
4.2 启动服务器与最小验证
服务启动通常只要一行初始化调用,然后就等请求了。启动前后可以做几个检查:
/* 伪代码示意,具体接口名以所用实现为准 */ httpd_init(); /* 挂上 80 端口 */ printf("httpd started\r\n");验证顺序我建议从底层往上走,一层层排除:
| 层级 | 验证方法 | 失败意味着 |
|---|---|---|
| 链路 | ping 通设备 IP | 网络配置或 ARP 有问题 |
| 端口 | 用命令行工具请求一次,看是否有响应 | HTTP 服务没起来或端口不对 |
| 路由 | 请求/index.html而不是/ | 默认首页配置不对 |
| 内容 | 看响应头里的 Content-Length 和 Content-Type | 生成配置或 MIME 有问题 |
| 渲染 | 浏览器打开看样式、图标 | 子目录路径或压缩问题 |
这个顺序的价值在于不要一上来就用浏览器。浏览器会缓存、会发一堆并行请求、会自动请求 favicon,干扰因素太多。先用一条最简单的请求拿到响应头,信息量远比想象中大:
curl -i http://192.168.1.100/index.html返回的Content-Length如果和你生成时的正文长度一致,说明文件系统查表正常;如果返回的是 404 但你确信文件在里面,那就是路径匹配的问题,大概率是前面的斜杠或者大小写;如果连接建立了但一直没数据返回,检查一下发送缓冲是不是被更大的文件塞满了。
4.3 体积核算:别让网页吃掉你的 Flash
这一步很多人跳过,直到某天编译提示空间不足才发现问题。核算方法很简单,但数字会吓你一跳。
C 数组的文本形式,每个字节大约占 5 到 6 个字符(0xNN,加上空格换行),所以一个 100KB 的资源,生成出来的 .c 文件大概 500KB 到 600KB。但这不代表固件会增大 500KB,经过编译后回到二进制,实际占用仍然接近原始的 100KB,再加上少量结构体开销。
我按经验给一组参考值:
| 原始资源总量 | 生成的 .c 文本大小 | 编入固件后的近似增量 | 编译耗时感受 |
|---|---|---|---|
| 20KB | 约 110KB | 约 21KB | 秒级,无感 |
| 100KB | 约 550KB | 约 105KB | 十几秒,可接受 |
| 500KB | 约 2.7MB | 约 520KB | 明显变慢,编辑器会卡 |
所以规划页面时,心里要有一条线:单个页面资源总量控制在芯片 Flash 的三分之一以内,留够余量给代码和 OTA 分区。超过这个比例,删图、压图、延迟加载,能省则省。我一般会把背景图从 PNG 换成高度压缩的 JPG,把图标合并成雪碧图甚至直接内联成 base64 的小 SVG,效果立竿见影。
还有一个容易忽略的点:编译器的段分配。如果const忘了写,或者链接脚本没有把只读数据段指向 Flash,这些数组会被搬到 RAM 里。一个 40KB 的数组搬进 64KB RAM 的芯片是什么后果,不用我多说。集成完之后一定要看 map 文件,确认data__xxx这些符号落在 Flash 区域,这一步只需要看一次,但能救整块板子。
注意:某些编译器对大数组有默认上限,超过之后会静默截断或者报错。如果你的字体文件比较大,建议拆成多个文件分别生成,或者在构建脚本里加一段体积检查逻辑。
5. 常见问题与排查实录
5.1 网页打不开的五类原因
这一类问题的排查,我总结了一张速查表,基本能覆盖九成情况。
| 现象 | 最可能原因 | 快速验证 | 处理 |
|---|---|---|---|
| 访问根路径 404 | 默认首页名不匹配 | 直接请求/index.html | 生成时加-i,或改服务端配置 |
| 所有路径都 404 | fsdata.c 没编进工程 | 查 map 文件有无data_符号 | 加入编译,检查 include 开关 |
| 请求后无响应 | 文件太大,发送缓冲不够 | 换成小文件请求试试 | 加大发送缓冲,或分块发送 |
| 偶尔能通、偶尔超时 | 多处同时访问,缓冲竞争 | 单线程串行请求复测 | 减少并发,检查缓冲复用逻辑 |
| 首页正常,二级页 404 | 未递归处理子目录 | 看产物里文件数量 | 加-r重新生成 |
第一类特别值得展开说。默认首页的名字是工具和服务器两边约定的,一边叫index.html、另一边期待index.htm,结果就是访问/返回 404,但你手工敲/index.html又能打开——这个现象极具迷惑性,我第一次遇到时还以为是自己把文件放错位置了。确认方法很简单:把目录里所有候选首页名都列出来,和生成参数、服务端配置三方对齐。
第三类问题通常出现在页面里有一张比较大的图片时。TCP 发送缓冲不够大,服务端一次发不完,如果发送逻辑没有处理"部分发送"的情况,就会卡在那里不动。判断方法是用命令行请求那个大文件,看是不是刚好在某个字节数上停住。解决要么加大缓冲,要么确认发送逻辑支持续发——这属于服务端实现层面的问题,跟 makefsdata 没关系,但现象很容易归错因。
5.2 内容异常:乱码、裂图、样式丢失
页面能打开但显示不对,问题集中在三个地方。
乱码:响应头里没有charset=utf-8,浏览器按本地编码猜,中文就成了乱码。解决有两条路,一是在 HTML 里加<meta charset="utf-8">,让浏览器自己认;二是让生成出来的响应头直接带 charset。后者需要工具支持,或者手工改。我一般两条都做,双保险。
裂图、样式丢失:多半是 Content-Type 不对,或者路径不对。区分方法很清楚——如果资源请求的地址正确(开发者工具的 Network 面板能看到 200),但显示不出来,就是 MIME 问题;如果根本是 404,那就是路径或递归问题。
样式部分生效:这种情况我遇到过一次,原因是页面里引用了 CDN 上的字体文件,设备访问不到外网。做离线页面时一定要把外部依赖全部本地化,别留任何外链。
排查这类问题,浏览器开发者工具是最好的朋友。Network 面板里每个请求的状态码、响应头、大小一目了然,比在串口里打日志快十倍。
5.3 体积和编译相关的坑
这几条属于"没遇到就觉得没事,遇到了特别费时间"的类型。
第一条,改了网页但设备上没变化。原因可能是浏览器缓存(按 F5 强刷或者加时间戳参数)、也可能是固件没重新烧(确认烧录日志里 fsdata 那个段确实更新了),还可能是老版本文件残留(构建目录里同时存在两份 fsdata.c)。排查顺序按这个来,别一上来就怀疑代码。
第二条,编译时间长到让人怀疑人生。几百 KB 的数组会让编译器明显变慢,尤其是开优化的时候。缓解办法是把这个文件单独编成静态库,让它不参与增量编译;或者把大资源拆成几个文件分别生成,改哪个文件就只重编哪个。
第三条,符号冲突。前面提过,同名文件在不同子目录下会撞。还有一种情况是产品有两个版本,各自的网页目录里有同名文件,最后链接到一起就冲突了。给每个版本加个前缀是最省事的解法。
第四条,生成产物的内容对不上源码。有一次我改了 HTML,跑工具,集成,烧录,结果页面还是旧的。查了半天发现生成命令里的源目录写成了备份目录,工具老老实实处理了那个目录,全程没有任何提示。从那以后我的生成脚本第一行就是打印源目录的绝对路径,眼睛能看到,心里才踏实。
5.4 一个动手查问题的通用套路
遇到任何显示异常,我用的固定流程是这样:先用命令行请求目标地址拿到原始响应头,看看 Content-Length 和 Content-Type 是否符合预期;如果响应头对,那就看响应体开头几个字节是不是正常 HTML(有时候会看到一堆?,说明压缩或者编码搞错了);如果响应体也对,就说明问题在浏览器端,去看控制台的报错。
这套流程的关键在于把问题切成"服务端给了什么"和"客户端怎么理解"两半,而不是一上来就打开浏览器刷新。多数时候看两行响应头就有答案了,比反复刷新页面高效得多。
6. 几个能让方案更耐用的做法
6.1 压缩与预压缩资源的处理
静态资源里最容易膨胀的是文本类文件(HTML、CSS、JS)和字体。文本类的压缩率通常能到 70% 以上,所以很多版本的工具支持把同名.gz文件一并打包,运行时根据请求头里的编码声明返回压缩版本。
要注意的是,这条路能不能走通取决于服务端实现是否支持,光有压缩文件不够。判断方法很简单:在浏览器开发者工具里看那个请求的响应头有没有Content-Encoding: gzip,有就是生效了,没有就说明服务端没走压缩分支。不生效也别硬扛,直接接受未压缩版本,然后在资源本身下手——删注释、去掉没用的 CSS 规则、图片转高质量 JPG,这些手段的效果往往比压缩更直接。
还有一个坑:预压缩文件的内容和原文件必须严格一致。我曾经出现过改完 HTML 忘了重新压缩的情况,结果设备返回的是压缩版本的旧内容,而源文件是新的,两边对不上,排查了很久才发现。所以生成脚本里一定要把压缩步骤和打包步骤绑在一起,做成一键执行,别留手工环节。
6.2 动态内容怎么和静态页面衔接
静态资源编进固件,动态数据怎么办?答案是接口分离,页面骨架走文件系统,数据走单独的路径。服务端实现里一般有一个回调注册机制,你注册一个处理函数,匹配到某个路径前缀时就不再查文件系统,而是交给你的代码处理。
这样做的好处是两边互不干扰:页面可以整块更新,数据接口按需实现。典型划分是/和/assets/*走静态,/api/*走动态。这套划分我已经在好几个项目里用,比较稳。
要注意的是,动态接口返回的 JSON 也要自己拼完整的响应头,别指望服务端帮你补。头部格式写错(少个空行、Content-Length 算错)会导致浏览器一直等到超时,而串口日志里一切正常,非常迷惑。我的习惯是封装一个极小的辅助函数,负责拼头部和算长度,所有接口都走它,避免手写出错。
6.3 我在实际项目里攒下的几条经验
最后分享几条不成体系的体会,都是踩过之后才记住的。
目录里只放最终产物,不要放源文件(PSD、SVG 源、未压缩的图片)。工具不认文件用途,它会把目录里所有东西都打包,我一个项目里因为目录里躺了一张 2MB 的设计稿,固件直接爆了。
文件名全部小写。有些文件系统大小写不敏感,工具处理时也没做归一化,但浏览器请求是区分大小写的,混用大小写迟早出事。
每次生成后看一眼文件总数。这是我现在的固定动作,FS_NUMFILES的数字对不对,一眼就知道有没有漏文件。
把生成命令写进脚本,别靠手敲。参数一多,手敲必然出错,而且下次换台机器又得回忆一遍。脚本里顺手加上体积统计和源目录打印,长期看非常值。
页面的入口控制在三层目录以内。目录太深,符号名容易长且容易撞,排查时看着也累。
预留一个"关于"页面。里面写上固件版本号和生成时间,后期现场出问题时,让用户打开这个页面截个图,能省掉很多来回确认的沟通成本。这个小页面的价值,比它占的那几 KB 空间高得多。