前一阵子想给自己的图片处理流程做点趁手的小工具。要求很土:双击就能用,不装软件,不联网,照片不上传。
做完之后是 10 个 html 文件,每个 7--12KB,加起来不到 90KB。它们不需要服务器,因为它们整个程序就是那个文件本身。
这篇不吹功能,只说技术上真正花时间的地方,以及我踩过的坑。
一、为什么坚持"单文件 + 本地处理"
在线工具的问题不是功能不够,而是你的数据要经过别人的服务器。压缩一张身份证照片、给合同加水印,这种事交给一个陌生网站,风险和收益完全不成比例。
浏览器其实自带全部能力:canvas 能重绘和编码图片,File API 能读本地文件,Blob 能直接给下载链接。这些都不需要网络。所以整件事可以在一台断网的电脑上完成。
二、坑 1:把"压缩"做成"压到指定大小",不能只调画质
最早的压缩工具只有一个质量滑杆。问题是有很多场景不是"越小越好",而是**"必须小于某个数"**:报名系统要求 100KB 以内、公众号要求 2MB 以内。
于是加了"目标大小"模式:二分搜索画质。第一版上线我就拿一张 1600×1200 的噪点图测,目标 20KB------
结果它输出 58.3 KB → 58.3 KB,什么都没做。
原因:画质降到 0.25,那张图还有 50KB 以上。单靠画质根本到不了目标,于是代码走了"压不动就保留原图"的兜底分支,用户看到的等于失败。
改成两条轴一起搜 :外层逐档缩小尺寸(1 → 0.8 → 0.65 → 0.5 → 0.4 → 0.32),内层在每档里二分画质,取"满足目标里质量最高的那个"。同一张图,同样目标 20KB,输出 19.9KB。
教训很直白:降画质和降分辨率是两个独立手段,只用一个,就一定有一类图压不动。
三、坑 2:圆角 + 阴影的绘制顺序
给商品图做圆角白底主图时,我先画了圆角路径、clip(),再给图片加 shadowBlur。
结果四个角外面渗出灰边。因为阴影是跟着图片的矩形边界绘制的,圆角裁剪只裁了图片本身,没裁阴影。
正确顺序是:先画一层"带阴影的圆角块"当影子,再在它上面画真正的图片。
js
// ① 影子:黑色圆角矩形 + blur,不画进最终图像的整体裁剪里
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,.45)';
ctx.shadowBlur = 26;
ctx.shadowOffsetY = 9;
ctx.fillStyle = '#000';
roundRect(ctx, x, y, w, h, r);
ctx.fill();
ctx.restore();
// ② 图片:同样的圆角路径,clip 后绘制
ctx.save();
roundRect(ctx, x, y, w, h, r);
ctx.clip();
ctx.drawImage(img, x, y, w, h);
ctx.restore();
验收方式我也换了:不看"看起来对不对",而是读像素------四个角取点应该是背景色(255,255,255),中心点应该等于原图颜色。这样改动前后能一眼对比。
四、坑 3:九宫格切图不能直接切
把一张图切九宫格发朋友圈,看起来是"分成 9 份",但手机拍的照片几乎都是 4:3 或 16:9,非方形直接切,每一格都是变形的。
做法是先把原图居中裁成正方形,再等分。多出来的部分会被丢掉,但九格拼回去仍然是一张正常的正方形图。
顺带做了个"切割线预览"------原图上叠一层网格,切之前就知道会切在哪。
五、坑 4:交付要加密,"base64 内嵌"不算加密
这是我自己最尴尬的一个发现。
一开始想把 4 个工具打包成"一个文件"发给别人,做法是把每个工具 base64 后塞进 iframe 的 srcdoc。它能用,看起来很优雅。
但它不是加密 。任何人打开开发者工具,一行 atob() 就把你的东西全拿走了。我自己用一行正则 + 解码就验证了这一点:atob 出来就是完整的 <!DOCTYPE html>。
改成了正经的做法:PBKDF2(20 万次)+ AES-256-GCM ,Python 侧用 cryptography 加密,浏览器侧用 WebCrypto 解密。好处是全程离线------密码在本地参与派生,密钥不会离开这台机器,也不需要任何后端。
配套我还放了两个密码:试用密码 (公开写在页面上,解锁一个小游戏)和正式密码。这样别人可以先玩上,再决定要不要。
六、坑 5:Monaco 编辑器没法用 textarea.value 填内容
这个是给在线代码平台粘贴代码时踩的。
我先找页面里的 textarea,把 value 设进去,事件也派发了。页面看起来接受了,textarea.value 读回来确实是我的内容。
但页面顶部显示"无更新",编辑器里还是原来的 hello world。
原因:Monaco 有自己的 model ,那个 textarea 只是给输入法用的隐藏输入框。更坑的是------它的 value 只有几十个字符(当前行的片段),我一度以为"内容只粘进去 60 个字符"。
正解只有一条:真实交互 。点进编辑器让它获得焦点,Ctrl+A 全选,然后真实按键输入或剪贴板粘贴。150KB 的内容用剪贴板一次性粘贴是可行的。
七、坑 6:把"页面显示了"当成"别人能用了"
我以为把文件上传到某个网盘、页面显示文件列表,就算交付完成了。
后来我用不带任何 Cookie 的请求 去抓那个下载接口,发现返回的是页面外壳;点开文件行,页面自己写着 This file could not be loaded.
"页面显示了"和"真的能用"是两件事。
现在我的验收标准固定成两条:①不带登录态 的请求能不能取回内容;②在真浏览器里真的跑一遍(图形类工具就直接读像素)。两条都过才算数。
八、坑 7:删除数据之前先确认它是不是你要删的
最后一个和技术无关。
我为了让页面"回到干净状态",在一次粘贴之后直接刷新了页面------粘进去的内容全没了,因为那个页面并没有自动保存草稿。
从那以后我的规矩是:在"有未保存内容"的页面上,刷新不是重置,是丢数据。
结果
10 个工具,总计 90KB 左右:两个小游戏(华容道、记忆翻牌),八个图片/文本工具(压缩、加水印、转格式并压到指定大小、九宫格切图、拼接、裁剪旋转、商品主图、文本工具箱)。
它们都在一个页面里,打开就能用,不需要下载:
在线试用(免费版):code.juejin.cn/pen/7690544...
如果你想自己改,思路就是把上面那些 canvas 和 File API 的组合拆开------每个工具本质上都只是"读入一个文件、在 canvas 上重画、toBlob 输出"这三步,剩下的都是细节。
细节才是花时间的地方。