笔记
0工具
0此页用于记录用户反馈问题后的每一次改进
关于
“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
不想公开、只想临时给人看:点“分享”生成一条带密码和有效期的链接,到期自动失效,你也能随时撤销。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
趋势
我们重写交互式地图编辑器学到的教训:Fabric.js、CORS 和两万行遗留 TypeScript 代码
去年我们团队接手了一个让我至今记忆犹新的任务——重写一个交互式办公室地图编辑器。说实话,刚听到“重写”这两个字的时候,我后背是发凉的。干过开发的都知道,这句话后面通常跟着的是几个月的加班、数不清的回归测试,以及没完没了的架构争论。但这次不一样,我们不仅活下来了,还学到了不少硬核的东西。今天我就把这些踩过的坑和收获分享出来,希望能帮到正在跟类似问题死磕的你。
故事的起点:一个 2270 行的“上帝组件”
先说说这个编辑器是怎么走到今天这一步的。我们做的是一款企业级的工位管理平台,公司用它来管理办公室空间和预订座位。核心功能之一就是这个地图编辑器——管理员上传楼层平面图,在上面摆放办公桌、会议室,然后发布给员工使用。
这个编辑器最初是在 AngularJS 时代写的。你没看错,就是那个已经被谷歌打入冷宫的 AngularJS。主编辑器组件是一个单文件,里面塞了将近 2270 行代码。它负责的事情包括但不限于:
- 加载地图
- 操作 Fabric.js 画布
- 增删改查各种对象
- 处理键盘快捷键
- 管理弹窗
- 保存数据
- 事件处理
基本上你能想到的编辑器功能,全都在这个文件里。更吓人的是,它背后还藏着一个将近两万行 TypeScript 的“地图引擎”,分布在 230 多个文件里。
最要命的架构问题是什么?是一个无限渲染循环。代码大概是这样的:
fabric.util.requestAnimFrame(() => this.tick());
你没看错,即使用户啥也没干,编辑器也在不停地重新渲染。它确实能工作,但每加一个新功能,成本就翻倍。就像往一个已经塞满的行李箱里硬塞东西——总有一天会炸。
为什么决定重写?不是因为 AngularJS
很多人可能会觉得,重写肯定是因为 AngularJS 太老了。其实不是。真正推动我们动手的是业务需求。产品需要几个全新的能力:
- 地图草稿:管理员可以在不发布的情况下保存编辑进度
- 安全发布:确保发布过程中不会影响到正在使用的员工
- 高质量打印:打印出来的地图要清晰,不能糊
- 多种工位模式:支持开放式工位、固定工位、共享工位等
- 更容易支持新对象类型:比如将来要加的会议室设备、消防设施等
每加一个新功能,就像在豆腐上雕花——不是不能做,但代价极大。我们逐渐意识到,自己不再是在修 bug,而是在跟架构本身作斗争。与其搞一次风险极高的“大爆炸式”重写,不如在旁边新建一套管理模块,跟旧的查看器并行运行。
新架构:把“大泥球”拆成三个子系统
我们保留了 Fabric.js,因为它在 Canvas 操作方面确实做得好。但其他的全部推倒重来。新的架构长这样:
Map Editor
│
▼
Map Engine
│
┌──────┼─────────┐
│ │ │
▼ ▼ ▼
Viewport Registry Changes Tracker
│
▼
Fabric.js
每个子系统只关心一件事:
- Viewport(视口):只负责相机控制,比如缩放、平移。它不关心地图上有什么对象。
- Object Registry(对象注册表):管理地图上的所有实体——办公桌、会议室、走廊标记等。它只负责增删改查,不关心渲染。
- Changes Tracker(变更追踪器):只记录哪些对象被修改了、哪些是新增的、哪些被删除了。它不关心对象长什么样。
而 Map Engine 对外暴露一个统一的 API,编辑器通过这个 API 跟底层交互。
这么做的好处很快就体现出来了。有一次产品经理过来说:“我们要加一个‘批量旋转所有工位’的功能。”换作以前,我得先搞清楚渲染循环的哪个环节会受影响,然后祈祷别碰坏别的东西。现在呢?我只需要在 Object Registry 里加一个批量操作方法,然后让 Changes Tracker 记录变更,最后通知 Viewport 重新渲染。整个改动不到 50 行代码。
移动相机,而不是移动对象
还有一个架构决策让我们省了不少事。以前的做法是:用户拖动画布时,我们遍历所有对象,挨个修改它们的坐标。听起来就笨,对吧?
新做法很简单:利用 Fabric.js 的 viewportTransform,我们只移动相机(视口),对象永远待在它们原来的坐标上。
// 旧做法:遍历所有对象,逐个移动
objects.forEach(obj => {
obj.left -= deltaX;
obj.top -= deltaY;
obj.setCoords();
});
canvas.renderAll();
// 新做法:移动视口变换矩阵
const currentTransform = canvas.viewportTransform;
currentTransform[4] += deltaX;
currentTransform[5] += deltaY;
canvas.setViewportTransform(currentTransform);
canvas.renderAll();
这个决策让很多事情变得简单到令人发指:
- 缩放:只需要修改
viewportTransform的前两个元素(缩放因子),对象坐标完全不用动。 - 平移:如上,改最后两个元素就行。
- 导出与打印:因为对象坐标是固定的,导出时不需要做坐标转换。
- 碰撞检测:比较两个对象的坐标时,不需要考虑当前缩放级别。
有时候一个架构决策就能同时解决好几个未来可能遇到的问题。这个就是典型的例子。
最难的问题居然不是两万行 TypeScript
当我们开始迁移时,团队里最担心的是那两万行遗留代码。结果呢?这部分反而相对顺利。真正让我们头疼的,是一个看起来人畜无害的需求:打印办公室地图。
第一次尝试:window.print()
最直观的做法:
window.print();
结果呢?浏览器打印时,DOM 渲染的分辨率大概只有屏幕分辨率。对于大型办公室地图来说,打印出来的东西糊得像马赛克。更糟的是,编辑器 UI 的按钮、工具栏、弹窗也出现在了打印文档里。这显然不行。
第二次尝试:导出 Canvas 为高分辨率图片
下一个想法明显更好:把 Fabric.js 画布导出为高分辨率图片。
canvas.toDataURL();
结果浏览器甩给我一个错误:
SecurityError: The canvas has been tainted by cross-origin data.
当时我差点把咖啡喷到屏幕上。为什么?因为楼层平面图存储在 Amazon S3 上。虽然它在浏览器里正常显示,但当你试图导出画布时,浏览器会检查所有图片资源是否都有正确的 CORS 头。没有的话,画布就被标记为“被污染(tainted)”,任何导出操作都会被禁止。
这里需要补充一点背景知识:Canvas 安全机制是浏览器为了保护用户隐私而设计的。如果一个画布中包含跨域图片(比如来自 S3 的图片),而这张图片没有通过 CORS 明确允许跨域访问,那么浏览器就不允许你从画布中读取像素数据(包括导出为图片)。想象一下,如果你在某个网站上传了一张自己的照片,然后另一个网站通过 Canvas 偷偷读取了这张照片的像素信息——这显然是个安全漏洞。所以浏览器直接一刀切:只要画布里有跨域资源,且 CORS 配置不对,就别想导出。
解决方案其实有两个方向:
- 给 S3 加上正确的 CORS 配置。但有时候这不是你能控制的——比如图片来自第三方 CDN,或者运维团队不愿意改配置。
- 分层导出。我们选了这条路。
第三次尝试:分层导出,终于成了
我们不再尝试导出单一的 PNG,而是把打印分成两个独立的层:
- 背景图片层:原始的楼层平面图,直接从 S3 加载,不经过 Canvas。
- 对象层:Fabric.js 画布上的所有对象(办公桌、会议室等),导出时不包含背景。
最终打印文档由这两层拼合而成:
背景图片 + 对象层 = 可打印文档
代码实现大概是这样的:
// 导出对象层,隐藏背景
const objectsDataUrl = exportCanvasRegion({
hideBackground: true,
});
// 返回两个独立的资源
return {
backgroundUrl: 'https://s3-bucket/floor-plan.png', // 直接引用原图URL
objectsDataUrl: objectsDataUrl, // 只包含对象的base64图片
};
然后在打印时,我们用 CSS 把这两个图片叠在一起:
<div class="print-container">
<!-- 背景图片 -->
<img src="https://s3-bucket/floor-plan.png" class="background-layer" />
<!-- 对象层,透明背景 -->
<img src="data:image/png;base64,..." class="objects-layer" />
</div>
.print-container {
position: relative;
}
.background-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.objects-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
这个方案看起来比直接导出复杂,但实际运行起来非常可靠。因为背景图片直接从 S3 加载,不受 Canvas 安全限制;对象层因为不包含跨域图片,也可以正常导出。
又一个意想不到的问题:坐标漂移
解决了 CORS 问题后,我们以为万事大吉了。结果打印出来的地图上,对象的位置全偏了——办公桌不在它们应该在的地方,会议室标记跑到走廊里去了。
排查过程非常痛苦。我们检查了坐标计算、打印缩放、CSS 布局,全都看不出问题。最后发现原因非常微妙:
- 背景图片的定位用的是像素值(比如
left: 100px; top: 200px)。 - 对象层的定位用的是百分比(比如
left: 12.5%; top: 25%)。
在屏幕上,这两者看起来完全一致,因为浏览器的渲染引擎会把百分比换算成像素。但是当打印时,浏览器的布局引擎会重新计算所有尺寸——因为打印页面的大小跟屏幕不一样。这时候,百分比换算出来的像素值就跟原来的像素值对不上了。
解决方案出奇地简单:把背景图片的定位也改成百分比。
.background-layer {
position: absolute;
left: 12.5%; /* 原来是 100px */
top: 25%; /* 原来是 200px */
width: 75%;
height: 50%;
}
改完之后,两层立即完美对齐。
这种问题最难调试的地方在于:代码本身没有 bug。没有空指针,没有类型错误,没有逻辑错误。问题是出在坐标模型本身——两个层用了不同的参考系。这就像你在画地图时,一个层用经纬度,另一个层用墨卡托投影坐标,看起来差不多,但一放大就全偏了。
重写之后,什么变了?
有趣的是,成功不是用代码行数减少来衡量的。新的实现实际上还增加了功能:
- 草稿支持:管理员可以保存多个版本,随时回退
- 安全发布:发布前自动检查冲突,确保不会覆盖别人的修改
- 高质量打印:分辨率可配置,支持 A3/A4 纸张自适应
- 多种工位模式:开箱支持三种工位类型,每种有不同的交互行为
- 更干净的架构:每个模块的职责一目了然
代码行数变成了一个毫无意义的指标。真正重要的是另一件事:加新功能不再让人提心吊胆了。
以前加一个功能,我得先花半天时间搞清楚渲染循环的哪个环节会受影响,然后祈祷测试能覆盖所有边界情况。现在呢?大部分时候,我只需要在对应的子系统里加代码,然后写几个单元测试就完事了。因为每个子系统只关心自己的事,改动的影响范围是可控的。
学到的教训
做这个项目让我深刻体会到一件事:最难的工程挑战很少属于某个特定的框架。
Angular 会过时,Fabric.js 会更新,TypeScript 会迭代。但真正的复杂性通常出现在这些技术之间的边界地带。CORS 不是 Angular 的问题,它是浏览器安全模型的问题。坐标漂移不是 Fabric.js 的问题,它是 CSS 布局模型的问题。无限渲染循环不是 TypeScript 的问题,它是架构设计的问题。
有时候,重写两万行代码不是项目中最难的部分。最难的部分往往是那个看起来人畜无害的“打印按钮”。就是这个按钮,逼着我们去深入理解浏览器渲染机制、CORS 策略、Canvas 安全限制、坐标系统和软件架构——这些知识比任何框架迁移都更有价值。
写在最后
回头看,这次迁移本质上跟 Angular 没什么关系。它关乎的是如何让产品更容易演进。新架构并没有大幅减少代码量,但它降低了未来变更的成本。有时候,这才是比代码行数更有价值的衡量标准。
如果你对这次迁移的技术细节感兴趣——比如我们是怎么设计 MapEngine 的、为什么决定保留 Fabric.js、以及如何在不停产的情况下完成管理模块的迁移——我可以再写一篇后续文章。这次先到这里,我得去修下一个 bug 了。
这篇文章基于我们构建和维护企业级工位管理平台的经验。代码示例已经过简化,实现细节做了泛化处理,在保留工程决策和教训的前提下,尊重产品机密性。