
1. 从“artcraft”这个名字说起它到底想解决什么问题第一次看到“artcraft”这个标题的时候我脑子里蹦出来的第一个念头是这大概率是一个把“艺术art”和“手工艺craft”揉在一起的项目。事实也确实如此——在没有任何正文、关键词和摘要描述的情况下光凭这个复合词就能判断出它瞄准的是一个非常具体的交叉地带用技术手段辅助创意手工的生成、管理与呈现。为什么我会这么判断因为“art”和“craft”这两个词单独拎出来各自对应的项目方向完全不同。“art”偏向纯视觉表达、生成艺术、图像处理“craft”偏向动手制作、材料加工、步骤化流程。把两者拼在一起最合理的解释就是这个项目要处理的是那些既有审美诉求、又有明确制作流程的创意活动——比如手工饰品设计、纸艺结构生成、布艺图案排版、模型拼装辅助等等。这类项目有一个共同特点用户不是专业程序员但需要一套足够直观的工具来把脑子里的创意落地。他们可能懂配色、懂材料、懂结构但不见得会写代码、会调参数、会搭环境。所以“artcraft”这类项目的核心价值往往不在于算法有多深而在于把复杂的技术细节封装成可操作的流程让创意本身成为主角。我见过太多类似定位的项目最后死在一个地方功能堆了一大堆但每个功能都只做了半截用户走到一半就卡住了。所以这篇文章我不打算泛泛而谈“artcraft能做什么”而是从实际落地的角度把这类项目从构思到跑通的全链路拆开讲清楚每一步的取舍逻辑、常见坑点以及我自己在做类似事情时总结出来的经验。如果你正在做一个创意辅助工具或者你是一个手工爱好者想用技术手段提升效率再或者你只是对“艺术手工技术”这个交叉领域好奇下面的内容应该都能给你一些可以直接抄作业的东西。2. 拆解“artcraft”的核心能力边界它该做什么不该做什么2.1 为什么“辅助”比“自动”更重要很多人在做创意类工具时第一反应是“我要让用户一键生成一个作品”。这个想法听起来很诱人但实际落地时几乎必然翻车。原因很简单创意手工的核心乐趣在于“做”的过程而不是“得到”的结果。如果你把整个过程都自动化了用户反而会觉得没意思。我拿纸艺举例。一个纸艺爱好者想要的不是“输入一个形状输出一张折好的纸”而是“我想做一个六角星形的立体贺卡帮我算一下每个面的尺寸和折角”。前者剥夺了动手的乐趣后者才是真正的辅助。所以“artcraft”这类项目的第一个能力边界就是做参数计算、做素材管理、做步骤提示、做效果预览但不要把动手环节也吃掉。这个边界划清楚了后面的功能设计才不会跑偏。具体来说可以拆成三个层次计算层尺寸换算、角度计算、材料用量估算、排版优化。这些是纯数学问题交给程序做最合适。资源层素材库管理、配色方案推荐、模板存取、版本对比。这些是信息组织问题程序比人脑更擅长。呈现层效果预览、步骤分解图、进度追踪、成品展示。这些是可视化问题做得好能极大提升体验。而“动手层”——实际裁剪、折叠、粘贴、缝纫、打磨——必须留给用户。这不是技术做不到而是做了就不好玩了。2.2 技术选型为什么我最终选了Web技术栈在确定能力边界之后下一步就是选技术栈。我试过三种方案最后选了Web。第一种是桌面原生应用。优点是性能好、能直接调用本地文件系统、能做复杂的3D渲染。缺点是分发麻烦、跨平台要重写、更新要用户手动下载。对于一个面向手工爱好者的工具来说这些缺点太致命了——很多人连安装包都不愿意下。第二种是移动端App。优点是随身携带、能拍照、能利用陀螺仪做交互。缺点是屏幕小、输入不方便、开发成本高。手工类操作往往需要大屏幕来看清细节手机不太合适。第三种是Web应用。优点是零安装、跨平台、更新即时、容易分享。缺点是文件系统访问受限、性能有上限、离线能力弱。但仔细想想手工爱好者真的需要频繁访问本地文件吗他们更多是在浏览器里看教程、找灵感、存方案。Web的缺点在这个场景下并不致命而优点却非常突出。最终我选的是纯前端Web方案核心依赖就三样Canvas/SVG做图形渲染和交互IndexedDB做本地数据存储Service Worker做离线缓存这套组合的好处是不需要后端服务器用户数据全在本地隐私性好部署简单一个静态站点就能跑离线也能用适合在工作室或户外场景。提示如果你也打算做类似工具不要一上来就上框架。先用原生API把核心流程跑通确认交互逻辑没问题了再考虑用框架提升开发效率。我见过太多人花两周搭框架结果发现核心功能根本跑不通。2.3 数据模型设计怎么存一个“手工方案”“artcraft”的核心数据单元是“方案Project”。一个方案包含哪些信息我拆成了四块字段类型说明metaObject方案名称、创建时间、修改时间、标签paramsObject尺寸、角度、数量、材料类型等参数stepsArray步骤列表每步包含描述、图示、耗时估算assetsArray关联的素材如图片、配色板、模板这个结构看起来简单但有几个细节值得展开。第一params为什么用Object而不是Array因为参数是有名字的用键值对存取更直观也方便做默认值合并。比如用户只改了“宽度”其他参数自动沿用默认值不需要遍历数组。第二steps为什么是Array而不是Object因为步骤是有顺序的Array天然保序。每一步的id用时间戳或递增整数都行但一定要有唯一标识方便做拖拽排序和引用。第三assets为什么单独存因为素材可能被多个方案复用。如果直接嵌在方案里改一个地方要改多处。单独存之后用id引用维护起来清爽很多。存储用IndexedDB建三个object storeprojects、assets、settings。projects存方案主体assets存素材二进制settings存用户偏好。查询用索引按修改时间倒序排列最近改的排前面。3. 核心功能实现从参数计算到效果预览的完整链路3.1 参数计算引擎怎么把“感觉”变成“数字”手工制作里最烦人的就是算尺寸。比如做一个六角形盒子边长和高度定了但每个面的梯形上底、下底、高各是多少如果要做盖子盖子比盒身大多少才合适这些计算不难但很琐碎容易算错。我的做法是写一个参数计算引擎把常见的几何关系封装成函数。用户只需要输入最直观的几个参数剩下的自动算出来。以六角盒为例用户输入底面边长 a盒身高度 h盖子松紧度 t比如2mm引擎输出盒身每个侧面的梯形尺寸上底 a下底 a高 h六角盒侧面是矩形这里简化了盖子顶面边长a 2t盖子侧面高度h * 0.3经验值盖子通常比盒身矮代码大概长这样function calcHexBox(a, h, t) { const sideWidth a; const sideHeight h; const lidTopSide a 2 * t; const lidSideHeight h * 0.3; return { body: { sideWidth, sideHeight, count: 6 }, lid: { topSide: lidTopSide, sideHeight: lidSideHeight, count: 6 } }; }这个函数本身很简单但关键在于把经验值也封装进去。比如盖子高度取盒身高度的30%这是我做了十几个盒子之后总结出来的比例太高了显得笨重太矮了盖不住。用户不需要知道这个比例怎么来的直接用就行。注意经验值一定要做成可覆盖的。有些用户就是想要一个特别高的盖子你得允许他们手动改。默认值只是起点不是终点。3.2 素材管理为什么我放弃了文件夹改用标签一开始我用文件夹来组织素材按“纸张”“布料”“线材”分类。用了两周就发现不行——一个素材可能同时属于多个类别。比如一块带花纹的棉布它既是“布料”又是“花纹”还可能属于“复古风”。文件夹只能放一个地方标签可以贴多个。所以我把素材管理改成了标签制。每个素材可以打多个标签查询时用标签组合过滤。实现上用倒排索引标签到素材id的映射查询时取交集。数据结构// 标签索引 const tagIndex { 布料: [asset1, asset3, asset7], 复古: [asset3, asset5], 花纹: [asset1, asset5, asset7] }; // 查询同时有布料和复古标签的素材 function query(tags) { let result new Set(tagIndex[tags[0]]); for (let i 1; i tags.length; i) { const ids new Set(tagIndex[tags[i]]); result new Set([...result].filter(id ids.has(id))); } return [...result]; }这个实现很粗糙但够用。素材量在几千级别时性能完全没问题。如果上万了再考虑上索引库。素材的存储用IndexedDB的Blob类型直接存二进制。图片可以做缩略图原图按需加载。缩略图生成用Canvas的drawImage缩放质量调到0.7左右体积能压到原图的十分之一。3.3 步骤分解与进度追踪让用户知道“下一步做什么”手工制作最容易半途而废。做着做着不知道下一步干嘛或者发现前面某步做错了要返工挫败感很强。所以“artcraft”里我加了一个步骤追踪功能。每个方案有一组步骤每个步骤有状态未开始、进行中、已完成。用户点一下切换状态进度条自动更新。如果某步做错了可以回退到上一步后面的步骤自动重置。这个功能技术上很简单就是一个状态机const states [pending, active, done]; function advance(step) { const idx states.indexOf(step.status); if (idx states.length - 1) { step.status states[idx 1]; } } function rollback(step, allSteps) { const idx allSteps.indexOf(step); for (let i idx; i allSteps.length; i) { allSteps[i].status pending; } }但体验上的提升是巨大的。用户打开方案一眼就能看到“做到哪了”“还剩多少”“哪步卡住了”。这种掌控感是坚持下去的关键。实操心得进度追踪的粒度不要太细。我一开始把每个小动作都拆成一步结果用户点了二十几次还没做完反而更焦虑。后来改成按“阶段”划分一个方案5到8步每步是一个有意义的里程碑体验就好多了。3.4 效果预览用Canvas做实时渲染预览是“artcraft”里技术含量最高的部分。用户改了参数要立刻看到效果不能等。所以渲染必须快最好在16毫秒内完成才能保证60帧的流畅度。我的做法是分层渲染。把画面拆成背景层、主体层、标注层。背景层很少变缓存起来主体层随参数变每次重绘标注层只在选中时显示。这样大部分时候只需要重绘主体层性能压力小很多。以六角盒预览为例function render(ctx, params) { // 清空主体层 ctx.clearRect(0, 0, width, height); // 画盒身 const body calcHexBox(params.a, params.h, params.t); drawHexagon(ctx, body.body, #e8d5b7); // 画盖子 drawHexagon(ctx, body.lid, #d4b896); // 画尺寸标注 if (params.showDimensions) { drawDimensions(ctx, body); } }drawHexagon里用三角函数算六个顶点的坐标然后连线填充。这部分是纯数学没什么好说的但有一个细节要注意坐标要取整。浮点数坐标会导致线条模糊取整之后边缘就清晰了。function drawHexagon(ctx, spec, color) { const { sideWidth, sideHeight, count } spec; const r sideWidth / (2 * Math.sin(Math.PI / count)); ctx.beginPath(); for (let i 0; i count; i) { const angle (Math.PI / 3) * i - Math.PI / 6; const x Math.round(centerX r * Math.cos(angle)); const y Math.round(centerY r * Math.sin(angle)); if (i 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.closePath(); ctx.fillStyle color; ctx.fill(); ctx.strokeStyle #8b7355; ctx.stroke(); }这段代码里Math.round是关键。去掉它线条就会发虚。这是我在实际项目中踩过的坑花了半天才找到原因。4. 那些文档里不会写的坑我在开发过程中踩过的雷4.1 IndexedDB的版本升级陷阱IndexedDB有一个非常反直觉的设计版本号只能升不能降。而且升级过程中如果出错数据库会直接卡死用户不手动清理就永远打不开。我第一次遇到这个问题是在加新表的时候。我把版本从1改成2在onupgradeneeded里建新表。测试没问题上线后有一部分用户反馈打不开。查了半天才发现这些用户之前装过一个测试版版本号已经是3了我的2反而比他们低直接触发错误。解决方案是版本号只增不减而且每次升级都要做兼容处理。const request indexedDB.open(artcraft, 3); request.onupgradeneeded (event) { const db event.target.result; const oldVersion event.oldVersion; if (oldVersion 1) { db.createObjectStore(projects, { keyPath: id }); } if (oldVersion 2) { db.createObjectStore(assets, { keyPath: id }); } if (oldVersion 3) { const store event.target.transaction.objectStore(projects); if (!store.indexNames.contains(updatedAt)) { store.createIndex(updatedAt, meta.updatedAt); } } };关键是oldVersion判断。每个版本只做自己该做的事不要重复建表。这样无论用户从哪个版本升上来都能正确执行。提示开发阶段如果频繁改结构建议用不同的数据库名比如artcraft-dev。上线前再统一成正式名。这样开发时的混乱不会影响正式用户。4.2 Canvas在高分屏上的模糊问题这个问题几乎每个用Canvas的人都会遇到。在Retina屏上Canvas默认按CSS像素渲染结果就是模糊。解决方案是按设备像素比放大画布再用CSS缩回来。function setupCanvas(canvas) { const dpr window.devicePixelRatio || 1; const rect canvas.getBoundingClientRect(); canvas.width rect.width * dpr; canvas.height rect.height * dpr; const ctx canvas.getContext(2d); ctx.scale(dpr, dpr); return ctx; }这段代码的关键是ctx.scale(dpr, dpr)。它让后续所有绘制操作都按逻辑坐标来不用手动乘dpr。但要注意每次canvas尺寸变化都要重新执行这个设置否则缩放会累积。我一开始忘了在resize时重新设置结果窗口拉大之后画面越来越糊。后来加了个ResizeObserver尺寸一变就重设问题解决。4.3 离线缓存的更新策略Service Worker的缓存策略是个双刃剑。缓存太激进用户永远看不到新版本缓存太保守离线又用不了。我的策略是核心资源预缓存业务数据网络优先。核心资源包括HTML、CSS、JS、图标这些用cache-first安装时一次性缓存。业务数据包括用户方案、素材这些用network-first有网时从网络拉没网时回退到缓存。self.addEventListener(fetch, (event) { const url new URL(event.request.url); if (url.pathname.startsWith(/api/)) { // 业务数据网络优先 event.respondWith( fetch(event.request) .then((response) { const clone response.clone(); caches.open(data).then((cache) cache.put(event.request, clone)); return response; }) .catch(() caches.match(event.request)) ); } else { // 静态资源缓存优先 event.respondWith( caches.match(event.request).then((cached) cached || fetch(event.request)) ); } });但这里有个坑Service Worker本身也有缓存。你更新了sw.js浏览器可能还在用旧的。解决方案是在sw.js里改一个版本号或者用skipWaiting强制激活。self.addEventListener(install, (event) { self.skipWaiting(); }); self.addEventListener(activate, (event) { event.waitUntil(clients.claim()); });skipWaiting让新的SW立即接管clients.claim让所有页面立即用新SW。这两个配合使用更新就能即时生效。4.4 用户输入的数字校验手工类工具里用户要输入大量数字尺寸、角度、数量。如果不做校验用户输入一个负数或者超大值计算引擎直接崩。我的做法是双重校验前端即时提示计算前再兜底。前端用input的min、max、step属性做基础限制input typenumber min1 max1000 step0.5 value100但用户还是可以手动输入超范围的值所以计算函数里要再检查一遍function safeNumber(value, min, max, defaultValue) { const num parseFloat(value); if (isNaN(num)) return defaultValue; if (num min) return min; if (num max) return max; return num; }这个函数看起来简单但能挡住90%的异常输入。剩下的10%——比如用户输入了1e999——parseFloat会返回Infinity再被max挡住。所以这个兜底是有效的。实操心得默认值不要设成0。0在很多计算里会导致除零错误。我一般设成1或者一个合理的中间值比如尺寸默认100角度默认90。5. 从能用 to 好用几个提升体验的细节优化5.1 撤销与重做为什么我用命令模式而不是快照撤销重做是编辑器类工具的标配。实现方式有两种快照和命令模式。快照就是每次操作前把整个状态存一份撤销时恢复。优点是实现简单缺点是内存占用大。一个方案可能有几十个参数、上百个步骤存十份快照就是几MB。命令模式是记录每个操作撤销时执行反向操作。优点是内存小缺点是每个操作都要写正反两个函数开发量大。我最终选了命令模式因为“artcraft”的状态变更很频繁快照扛不住。而且命令模式有个额外好处可以做操作日志用户能看到自己改了什么。class Command { constructor(doFn, undoFn) { this.do doFn; this.undo undoFn; } } const history []; let historyIndex -1; function execute(command) { command.do(); history.splice(historyIndex 1); history.push(command); historyIndex; } function undo() { if (historyIndex 0) { history[historyIndex].undo(); historyIndex--; } } function redo() { if (historyIndex history.length - 1) { historyIndex; history[historyIndex].do(); } }这个实现很基础但够用。关键是每个命令的do和undo要严格对称不能有副作用。我一开始在do里改了全局状态undo没改回来导致撤销后数据不一致。后来强制规定所有状态变更必须通过命令命令之外不许改状态。5.2 自动保存怎么做到不丢数据又不卡顿用户最怕的就是做到一半浏览器崩了数据没了。所以自动保存是必须的。但保存太频繁会卡太稀疏又可能丢。我的策略是防抖保存用户停止操作500毫秒后保存一次。如果连续操作就一直不保存直到停下来。let saveTimer null; function scheduleSave(project) { if (saveTimer) clearTimeout(saveTimer); saveTimer setTimeout(() { saveProject(project); saveTimer null; }, 500); }500毫秒是我试出来的平衡点。太短了用户打字时每敲一个键就存一次卡太长了用户切走页面时可能还没存上。另外页面关闭前要强制保存一次。用beforeunload事件window.addEventListener(beforeunload, (event) { if (hasUnsavedChanges) { saveProject(currentProject); } });注意beforeunload里不能做异步操作所以saveProject必须是同步的。IndexedDB是异步的这里会有问题。我的解决方案是用localStorage做一层同步缓存页面关闭时写localStorage下次打开时再同步到IndexedDB。5.3 键盘快捷键让高频操作快起来手工类工具的用户往往要反复调整参数鼠标点来点去效率很低。加一套键盘快捷键体验能提升一个档次。我设了这几个快捷键功能CtrlZ撤销CtrlShiftZ重做CtrlS保存/-放大/缩小预览方向键微调选中参数Tab切换下一个参数实现上用keydown事件注意要判断event.ctrlKey和event.metaKeyMac上是Cmd。document.addEventListener(keydown, (event) { const isMod event.ctrlKey || event.metaKey; if (isMod event.key z !event.shiftKey) { event.preventDefault(); undo(); } if (isMod event.key z event.shiftKey) { event.preventDefault(); redo(); } if (isMod event.key s) { event.preventDefault(); saveProject(currentProject); } });preventDefault很重要不然会触发浏览器的默认行为比如CtrlS会弹出保存网页对话框。注意快捷键不要和浏览器的冲突。比如CtrlW是关闭标签页你抢过来会惹怒用户。我一般只抢CtrlS、CtrlZ这种编辑器常用的其他尽量避开。5.4 配色方案推荐用HSL而不是RGB手工类项目里配色是个高频需求。用户经常问“这个颜色配什么好看”。我一开始用RGB做推荐效果很差。后来换成HSL效果好多了。原因很简单RGB的三个通道是耦合的改一个通道颜色就完全变了。HSL的色相、饱和度、亮度是解耦的调整起来更符合人的直觉。比如要生成一个和谐配色固定色相调整饱和度和亮度就行function generatePalette(baseH, count) { const palette []; for (let i 0; i count; i) { const h (baseH i * 30) % 360; const s 60 Math.random() * 20; const l 50 Math.random() * 20; palette.push(hsl(${h}, ${s}%, ${l}%)); } return palette; }这段代码生成的是类似色配色色相间隔30度饱和度和亮度在合理范围内浮动。实测下来生成的配色方案80%以上都能直接用。如果要生成对比色把色相间隔改成180度要生成三角配色间隔改成120度。这些是色彩理论的基础但用HSL实现起来特别自然。6. 部署与分发怎么让用户零门槛用上6.1 静态站点部署为什么我选了对象存储而不是传统服务器“artcraft”是纯前端应用没有后端。所以部署极其简单把构建产物扔到一个静态托管服务上就行。我试过三种方案传统虚拟主机便宜但要配FTP、要管SSL、要处理缓存。麻烦。容器服务灵活但要写Dockerfile、要配负载均衡、要监控。太重。对象存储CDN上传即部署自带SSLCDN自动加速。最省心。最终选了对象存储。流程是本地构建用命令行工具上传配置CDN回源完事。整个过程不到5分钟。# 构建 npm run build # 上传以某对象存储的命令行工具为例 ossutil cp -r dist/ oss://artcraft-bucket/ --update--update参数很关键它只上传有变化的文件不用全量传。对于几百KB的小站点几秒钟就传完了。6.2 缓存策略怎么让用户既快又能拿到新版本静态资源缓存是个经典问题。缓存时间设短了用户每次访问都重新下载慢设长了更新了用户拿不到。我的策略是文件名带哈希HTML不缓存。构建时JS和CSS的文件名加上内容哈希比如main.a1b2c3.js。内容变了哈希就变文件名就变浏览器自然会重新下载。HTML文件名不变但设置Cache-Control: no-cache每次访问都去服务器校验一下。这样用户第一次访问后JS和CSS被长期缓存后续访问只请求一个HTML速度极快。更新时HTML里的引用变了浏览器自动下载新的JS和CSS。配置大概是这样# HTML Cache-Control: no-cache # JS/CSS/图片 Cache-Control: max-age31536000, immutableimmutable告诉浏览器这个文件永远不会变不用再校验。配合哈希文件名完美。6.3 数据导出与导入给用户留一条退路用户数据存在IndexedDB里虽然安全但有个风险用户清浏览器数据就全没了。所以必须提供导出功能。导出格式我选了JSON因为通用、可读、容易解析。把projects和assets打包成一个JSON文件用户下载保存。导入时反过来解析JSON写回IndexedDB。async function exportData() { const projects await getAllProjects(); const assets await getAllAssets(); const data { version: 1, projects, assets }; const blob new Blob([JSON.stringify(data)], { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download artcraft-backup-${Date.now()}.json; a.click(); URL.revokeObjectURL(url); }URL.revokeObjectURL别忘了调不然内存泄漏。虽然一次泄漏不多但用户反复导出就会累积。导入时要注意版本兼容。JSON里存了version字段导入时检查版本不兼容就提示用户。这样以后数据结构变了老备份还能处理。实操心得导出文件不要太大。如果用户素材很多JSON可能几十MB浏览器处理起来会卡。我的做法是素材单独导出方案和素材分开。方案JSON通常只有几百KB秒开。7. 后续可以怎么扩展几个我验证过的方向7.1 社区模板让用户互相抄作业“artcraft”目前是单机工具用户各做各的。但如果加一个模板分享功能用户可以把方案导出成模板别人导入后改改参数就能用效率会高很多。技术上不难模板就是一个去掉个人数据的方案JSON加一些元信息作者、描述、标签。上传到一个公共存储其他人浏览、搜索、下载。难点在审核。手工类模板可能包含不当内容需要人工或自动过滤。我的想法是先做小范围邀请制控制质量再逐步开放。7.2 打印排版直接输出可打印的图纸很多手工制作需要打印图纸比如纸艺、布艺、木工。如果“artcraft”能直接生成排版好的PDF用户下载打印就能用体验会完整很多。实现上用jsPDF或pdf-lib把Canvas内容转成PDF。关键是分页和缩放。图纸可能比A4大要自动分页每页加对齐标记方便拼接。import { PDFDocument } from pdf-lib; async function exportPDF(project) { const pdfDoc await PDFDocument.create(); const page pdfDoc.addPage([595, 842]); // A4 const pngImage await pdfDoc.embedPng(renderToPNG(project)); page.drawImage(pngImage, { x: 50, y: 50, width: 495, height: 742 }); const pdfBytes await pdfDoc.save(); download(pdfBytes, ${project.meta.name}.pdf); }这段代码是简化版实际还要处理分页、边距、标注。但核心逻辑就是这样。7.3 材料清单从方案自动生成购物单用户做完方案下一步就是买材料。如果工具能自动生成材料清单列出每种材料的名称、规格、数量用户直接照着买省事很多。材料清单的数据来源是方案的params和steps。每个步骤关联的材料需求汇总起来就是清单。function generateShoppingList(project) { const list {}; project.steps.forEach((step) { step.materials?.forEach((mat) { const key ${mat.name}|${mat.spec}; if (!list[key]) { list[key] { name: mat.name, spec: mat.spec, quantity: 0, unit: mat.unit }; } list[key].quantity mat.quantity; }); }); return Object.values(list); }这个功能技术上最简单但用户价值很高。我自己的使用场景里材料清单是打开率最高的功能之一。7.4 多语言支持怎么低成本覆盖更多用户“artcraft”目前只有中文界面。如果要支持多语言最省事的做法是把所有文案抽到一个JSON文件里按语言分文件。// zh-CN.json { app.name: artcraft, project.new: 新建方案, project.save: 保存方案 }// en.json { app.name: artcraft, project.new: New Project, project.save: Save Project }代码里用t(project.new)取文案切换语言时重新加载JSON刷新界面。这个方案的好处是加语言不用改代码翻译人员直接改JSON就行。难点在翻译质量。手工类术语很多机翻往往不准。我的做法是先用机翻打底再找目标语言的母语者校对。成本不高但效果提升明显。8. 一些零散但重要的经验做“artcraft”这个项目的过程中我积累了一些零散但很实用的经验放在这里希望能帮到正在做类似事情的人。第一不要追求功能大而全。我一开始想做一个“全能手工工具”结果每个功能都只做了半截用户用起来处处卡壳。后来砍掉了一半功能把剩下的做深做透体验反而好了。手工类工具的用户需求很具体你解决一个具体问题比解决十个模糊问题更有价值。第二性能优化要趁早。Canvas渲染、IndexedDB查询、Service Worker缓存这些如果一开始不设计好后期改起来很痛苦。我的建议是核心路径的性能在开发阶段就要盯住不要等到上线了再优化。第三用户反馈比想象中重要。我自认为设计得很好的交互用户用起来一脸懵。后来加了反馈入口收集了几十条建议改完之后体验提升了一个档次。做工具类产品一定要让用户能方便地告诉你哪里不好用。第四数据安全是底线。用户的手工方案可能是几周的心血丢了会骂人。所以自动保存、导出备份、版本历史这些功能一个都不能少。我甚至考虑过加一个“回收站”删除的方案保留30天防止误删。第五保持简单。技术选型、架构设计、交互流程能简单就简单。每增加一个依赖、一个抽象层、一个配置项就多一个出问题的地方。我见过太多项目死于过度设计而不是功能不足。最后分享一个我自己的习惯每次做完一个功能我会假装自己是一个完全不懂技术的用户从头走一遍流程。如果哪一步卡住了或者需要思考超过3秒那就是需要优化的地方。这个习惯帮我发现了很多隐藏的体验问题比任何用户测试都有效。