用 Excalidraw 打造中文手写风格画板
在技术团队的日常协作中,一张随手勾勒的草图往往比千言万语更高效。无论是架构讨论、流程推演,还是产品原型构思,可视化表达始终是沟通的核心工具。而 Excalidraw 正是这一场景下的明星选手——它以极简界面和“手绘风”视觉效果著称,让数字白板拥有了纸笔般的亲和力。
但当我们真正投入中文环境使用时,却总感觉哪里不对劲:英文文本用了 Virgil 这类手写字体,线条歪歪扭扭很自然;可一旦输入中文,立刻变成规整冷峻的黑体或宋体,像是一张温柔的手稿里突然插入了打印标签,风格割裂感扑面而来。
有没有办法让中文也“写”得随意一点?答案是肯定的。本文将带你从零开始,深度定制 Excalidraw,引入一款优雅的开源中文字体,实现真正的“全手写风”统一体验,并部署为可共享的 Web 应用。
整个过程不仅解决了一个具体问题,更是一次对现代前端项目架构的实战演练:如何理解 Monorepo 结构、干预字体加载机制、处理服务端渲染兼容性,以及完成静态站点发布。
准备工作:获取并运行 Excalidraw 源码
要定制一个开源项目,第一步永远是跑通它的本地开发环境。
前往 Excalidraw 官方仓库,点击右上角Fork按钮复制到你的 GitHub 账户下,然后克隆到本地:
git clone git@github.com:yourname/excalidraw.git cd excalidraw建议创建一个独立分支用于本次功能开发:
git checkout -b feature/chinese-handwriting-font这样后续即使官方更新主干代码,也能通过 rebase 或 merge 平滑合并,保留我们的修改痕迹。
接下来安装依赖并启动服务:
yarn install yarn run startExcalidraw 使用 Yarn 管理依赖,基于 Vite 构建。几分钟后浏览器会自动打开 http://localhost:3000,你应该能看到熟悉的绘图界面,可以自由添加形状、文本、箭头等元素。
✅ 提示:若启动失败,请确认 Node.js 版本是否为 v16+,这是项目推荐的基础运行环境。
字体选择:寻找一款“会呼吸”的中文字体
我们想要的不是普通的书法字,而是一种带有轻微抖动、笔画粗细变化、仿佛真实书写留下的痕迹——这种质感才能与 Excalidraw 的“手绘风”融为一体。
关键要求如下:
- 支持常用汉字(至少覆盖 GBK)
- 视觉风格自然流畅,避免机械感
- 免费可商用,规避版权风险
目前市面上有不少优秀开源字体平台可供筛选:
-猫啃网(maoken.com):专注免费可商用字体分享,标注清晰
-字由(hellofont.cn):设计师常用资源库
-Google Fonts 中文镜像:部分字体支持
本文选用“平方雨桐体”(YuTong Type Foundry 出品),这是一款圆润自然、略带连笔感的开源字体,其轻重变化恰到好处,非常适合模拟手写情境下的非均匀运笔效果。
下载.ttf格式的文件后,重命名为Yutong.ttf,便于后续引用。
集成字体:打通客户端与服务端的渲染链路
Excalidraw 采用 Monorepo 架构,字体系统分布在多个子包中。我们需要同时配置前端显示和服务端导出能力,否则会出现“页面看着正常,导出图片变黑体”的尴尬情况。
1. 放置字体文件
将Yutong.ttf复制到两个关键目录:
cp Yutong.ttf packages/excalidraw/fonts/assets/ cp Yutong.ttf public/前者用于构建时打包进字体模块,后者供 Node.js 服务直接提供静态资源访问。
2. 声明字体:CSS @font-face
编辑packages/excalidraw/fonts/assets/fonts.css,末尾添加:
@font-face { font-family: "Yutong"; src: url(./Yutong.ttf) format("truetype"); style: normal; display: swap; }⚠️ 注意:必须写format("truetype")而非format("ttf"),这是 WOFF 规范的要求,否则浏览器可能拒绝解析。
3. Node 端注册字体(导出 PNG/SVG 必须)
编辑packages/excalidraw/index-node.ts,找到已有字体注册代码:
registerFont("./public/Virgil.woff2", { family: "Virgil" }); registerFont("./public/Cascadia.woff2", { family: "Cascadia" });在其下方加入:
registerFont("./public/Yutong.ttf", { family: "Yutong" });这一步至关重要。没有它,当你点击“Export as PNG”时,Canvas 将无法绘制该字体,导致中文回退为默认字体甚至乱码。
4. 添加预加载提升性能
为了避免首次加载时出现字体闪烁(FOIT/FOUT),需在 HTML 中预加载字体资源。
修改excalidraw-app/index.html,在<head>内添加:
<link rel="preload" href="/Yutong.ttf" as="font" type="font/ttf" crossorigin="anonymous" />如果你使用了自定义 Vite 插件处理字体注入(如scripts/woff2/woff2-vite-plugins.js),也应同步添加此 preload 标签。
5. 扩展字体枚举常量
打开packages/excalidraw/constants.ts,查找FONT_FAMILY对象:
export const FONT_FAMILY = { Virgil: 1, Helvetica: 2, Cascadia: 3, Excalifont: 5, Nunito: 6, "Lilita One": 7, };为了避免与未来官方新增字体冲突,我们使用一个较大的数值作为键值:
Yutong: 999最终结果为:
export const FONT_FAMILY = { // ... existing fonts Yutong: 999 };6. 添加字体选择器 UI
编辑packages/excalidraw/components/FontPicker/FontPicker.tsx,这是顶部工具栏中的字体下拉菜单组件。
在选项数组中新增一项:
{ value: FONT_FAMILY.Yutong, icon: FontFamilyHandDrawnIcon, // 复用手绘图标 text: "手写体 (中文)", testId: "font-family-yutong", },💡 可选优化:你可以将"手写体 (中文)"提取为国际化字段,写入lang/zh-CN.json等语言包,实现多语言支持。
7. 注册字体元数据(防止 fallback)
最后一步,确保字体被正确纳入 Excalidraw 内部的字体管理系统。
编辑packages/excalidraw/fonts/index.ts,在_register调用后添加:
import Yutong from "./assets/Yutong.ttf"; // ... _register("Yutong", FONT_METADATA[FONT_FAMILY.Excalifont], { uri: Yutong, });这里复用了Excalifont的度量信息(如行高、基线偏移),虽然不完全匹配,但在大多数场景下足以保证排版一致性。如果追求极致精准,也可以单独测量并定义新的FONT_METADATA条目。
功能验证:看看“写的字”像不像人写的
重新启动应用:
yarn run start进入页面后操作如下:
1. 点击左侧 “T” 文本工具
2. 输入一段中文,例如:“用户登录流程”
3. 在顶部样式面板中选择新添加的“手写体 (中文)”选项
4. 观察文字是否呈现自然笔触
✅ 成功标志:中文字体呈现出轻微的粗细波动和圆润转角,与英文手绘线条风格协调一致,整体画面不再有割裂感。
此时你已经拥有了一款真正意义上的“双语手绘风”画板。
构建与部署:让它被世界看见
为了让团队成员或公众也能使用这个定制版本,我们可以将其部署为静态站点。推荐使用GitHub Pages实现免费托管。
1. 安装 gh-pages 工具
yarn add -D gh-pages2. 添加 deploy 脚本
在package.json的scripts字段中增加:
"deploy": "gh-pages -d excalidraw-app/build"完整示例如下:
"scripts": { "start": "concurrently \"yarn:start:app\" \"yarn:start:svr\"", "build": "yarn build:app", "deploy": "gh-pages -d excalidraw-app/build" }3. 配置相对路径(适配二级目录)
如果你计划部署在https://yourname.github.io/excalidraw-zh/这样的子路径下,必须启用相对路径。
编辑excalidraw-app/vite.config.mts:
export default defineConfig({ base: './', // 改为相对路径 // ... });否则所有资源请求都会指向根目录/assets/...,导致 404 错误。
4. 执行构建与部署
依次运行:
yarn run build yarn run deploygh-pages会自动创建一个名为gh-pages的分支,并推送构建产物。几分钟后,前往仓库 Settings > Pages 即可看到部署成功的提示。
访问链接形如:https://yourname.github.io/excalidraw-zh/
即可全球访问你专属的中文手写风格 Excalidraw 画板!
常见问题与解决方案(实战踩坑记录)
❌ 问题1:字体未生效,显示为方块或默认字体
原因分析:
-@font-face中format('ttf')不合法
- 浏览器缓存旧 CSS 或字体文件
- 字体路径错误或未正确复制到public/
解决方法:
1. 检查fonts.css是否使用format("truetype")
2. 清除浏览器缓存(Ctrl + F5 强刷)
3. 打开 DevTools Network 面板,确认Yutong.ttf是否成功加载
❌ 问题2:导出 PNG/SVG 时中文乱码或缺失
根本原因:Node.js 环境未注册字体。
修复方式:务必确认已在index-node.ts中调用registerFont(),且路径相对于public/目录正确无误。
❌ 问题3:部署后报错Could not parse as value list
典型错误信息:
DOMException: The source provided ('url(/assets/Yutong-Bla.ttf) format('ttf')') could not be parsed...根源:Excalidraw 内部根据扩展名自动生成format(xxx),而.ttf被误判为ttf,但标准应为truetype。
修复方案:
修改packages/excalidraw/fonts/ExcalidrawFont.ts中的getFormat方法:
private static getFormat(url: URL) { try { const pathname = new URL(url).pathname; const parts = pathname.split("."); if (parts.length === 1) return ""; let ext = parts.pop()?.toLowerCase(); switch (ext) { case "ttf": return "format('truetype')"; case "woff": return "format('woff')"; case "woff2": return "format('woff2')"; case "otf": return "format('opentype')"; default: return ""; } } catch (error) { return ""; } }重新构建后,该问题即可消除。
进阶方向:结合 AI 快速生成图表
Excalidraw 不只是一个绘图工具,它还能成为AI 驱动的知识生产引擎。
想象一下这样的场景:你在会议中说了一句“帮我画个电商系统的微服务架构”,下一秒白板上就出现了带注释的组件图——这不是科幻,而是完全可以实现的工作流。
推荐集成路径:
| 方案 | 说明 |
|---|---|
| Excalidraw + Mermaid + AI Prompt | 利用 AI 解析自然语言生成 Mermaid 代码,再通过转换器导入 Excalidraw |
| Custom AI Plugin | 开发插件调用 OpenAI API,输入描述返回 JSON 元素数组 |
| Obsidian 插件联动 | 若你使用 Obsidian,可通过其 Excalidraw 插件实现笔记内嵌 AI 生图 |
示例 Prompt:
“请生成一个包含用户登录、订单管理、支付网关的电商系统架构图,使用手绘风格。”
这类能力不仅能极大提升敏捷开发中的原型效率,也让非技术人员能轻松参与系统设计。
这种高度集成的设计思路,正引领着智能协作工具向更自然、更高效的方向演进。而你刚刚完成的,不只是一个字体替换,更是一次对开源生态的创造性回应——当工具不再只是“可用”,而是真正“好用”于你的母语文化时,创造力才真正获得了自由。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考