- 前端
- 图形学
【免费下载链接】svgedit
Powerful SVG-Editor for your browser
SVG-edit 曾经把整个编辑器塞进svg-editor.js(界面)与svgcanvas.js(其余一切)两个文件,到 2010 年 10 月后者已膨胀至约 11600 行 JavaScript。这篇技术文章以项目早期官方 Wiki 文档 CodeRefactoring.md 为骨架,还原这场"拆巨文件"重构的动机、方法、里程碑与遗留 TODO,并结合当前仓库源码展示其终局形态:@svgedit/svgcanvas已按职责拆分为三十余个核心模块。读完你将理解为什么"巨型单文件"会扼杀开源项目的协作活力,掌握渐进式模块拆分 + 单元测试 + 构建期合并(Closure Compiler / Vite)这套可复制的重构方法论,并能在当前代码库中按图索骥找到当年重构设想的落地证据。
重构的动机:为什么 11600 行的单文件必须被拆开
SVG-edit 起步时极小,整个项目只包含两个 JS 文件:
svg-editor.js——本质上承载编辑器 UI;svgcanvas.js——承载其余一切(绘图、选择、路径、图层、撤销等全部逻辑)。
随着功能持续叠加,扩展机制虽然把部分特性从主代码库中分离出去,但到 2010 年 10 月,svgcanvas.js依然达到了约 11600 行 JavaScript。作者 Jeff Schiller 在文中给出了两个核心担忧:
- 对新人开发者极不友好:一个巨型文件无论注释多详尽、组织多规整,都难以吸引新贡献者——"想搞懂项目如何工作"本身就构成巨大门槛。对依赖零散业余贡献者的开源项目而言,这是致命问题。作者本人即是亲历者:2010 年初暂停重度贡献、年末回归时发现自己已迷失在这个大文件里。
- 测试手段只能靠手点:当时验证任何新改动的唯一现实途径是打开编辑器手动试功能。这种方式只能证明"新功能能用",却完全无法捕捉"是否破坏了既有功能"。只依赖人工测试的代码库,长期必然进展缓慢。
历史背景:2009 年的分文件提议与 2010 年的正式启动
其实早在 2009 年,Narendra(Narendra Sisodiya)就曾提议把 JS 功能拆分到独立文件——他自己离开项目又回归时也遇到了同样的"迷路"问题,并已贡献了独立的本地化脚本locale.js作为示范。但当时作者对 Web 应用如何做模块化尚无头绪:他不愿为每个<script>标签发起独立 GET 请求,也觉得locale.js这种做法"略让人在意",加上当时正处在功能高速迭代期,拆分计划被搁置。
等到作者自己站到 Narendra 当年的位置、代码量又增长了一个数量级之后,才真正意识到这件事的重要性。于是从版本 r1817 开始,他着手逐步拆解代码。重构的核心原则是:做增量、谨慎的拆分,绝不引入新 bug。作者坦言并未完全做到零回归,但至少没有酿成重大事故。
重构方法:命名空间收编 + 按功能拆模块 + 脚本按序加载
重构后,被拆出的代码统一收编到svgedit全局命名空间之下。当时的最终目标是一切代码都归入该命名空间,但仍有svgEditor、svgCanvas等部分暂未迁入。以browser.js为例,它导出了一批浏览器探测函数:
svgedit.browser.isOpera() svgedit.browser.isWebkit() svgedit.browser.isGecko() svgedit.browser.supportsSelectors() svgedit.browser.supportsPathReplaceItem() ...当时重构后的整体格局可以概括为四点:
- 开发版 SVG-edit 使用多个
<script>标签按序加载全部 JS 模块; - 模块间依赖关系当时仍靠人工维护,JS 文件必须按正确顺序加载;
- 每个新 JS 模块在
test/下都有对应的测试文件; Makefile已更新,使用Closure Compiler将所有 JS 模块编译合并为单个文件,把发布版的 GET 请求数重新降回合理水平。
文中特别注明:这是一项持续中的工程,当时svgcanvas.js仍约有 8800 行并偶有增长;作者的"搞怪基准"是希望每个文件都能在 Google Code 上顺畅浏览——当时直接打开svgcanvas.js会让浏览器卡死。
单元测试:拆一个模块,补一组测试
作者在重构的每一个模块/函数/类上都补写了单元测试。截至文中记录(文中写 Jan 2010,按其上下文应为 2011 年前后),项目已有接近 500 个单元测试,全部通过test/all_tests.html统一运行。
这一点在当前仓库中得到了完整继承与大幅强化:tests/unit/下按模块一一对应测试文件,例如 draw.test.js、history.test.js、path.test.js、select.test.js、sanitize.test.js 等;测试统一由 Vitest 驱动(见 package.json 中"test": "vitest run --coverage && node scripts/run-e2e.mjs"),并配合 Playwright 跑端到端用例。当年"每个模块配一个测试文件"的设想,如今已固化为tests/unit目录的真实组织结构。
当时的进行中任务与 TODO 清单
文中记录的进行中任务:
- Drawing 抽象:作者引入
Drawing概念来封装单个打开 SVG 文档的状态。编辑器持有一个当前 drawing 的句柄,用它代替直接访问 SVG DOM 元素;图层、当前编辑上下文、文档历史等代码最终都应迁入draw.js,但当时大部分仍在svgcanvas.js。 - pathActions 迁移:正把
svgcanvas.js中一大块名为pathActions的代码迁移到独立模块path.js。这块代码依赖很多,只能逐块搬移,当时约迁移了一半,且大部分"公共 API"仍留在svgcanvas.js。
TODO 清单:
- 完成将图层功能移入 Drawing 类;
- 将
current_group与上下文功能移入 Drawing; - 将
setSvgString()与importSvgString()移入独立模块; - 将
pathActions移入独立模块; - 重新生成 API 文档;
- 想办法让 Closure Compiler 把
svgedit.browser.isOpera()编译成svgedit$browser$isOpera()以便进一步优化(缩小编译产物并降低运行时开销)。
文末还列了两个拟调研方向:改用 jscode 风格的文档;适度引入 Closure 的goog.require/provide,但顾虑是 Closure 与 jQuery 功能重叠显著,需避免拉入不需要的代码。
从当年蓝图看当前仓库:重构如何真正落地
距离该文档写下十余年后的今天,这份"蓝图"的几乎每一项都已在代码库中兑现。当前项目已演化为 npm workspace 结构(见根目录 package.json 的workspaces字段),核心绘图引擎独立成包 packages/svgcanvas/package.json(@svgedit/svgcanvas,当前版本 7.4.2),彻底摆脱了"两个巨型 JS 文件"的旧格局。
svgedit命名空间与 browser 模块的现代化
当年的svgedit.browser.isWebkit()等函数式 API,如今演化为 packages/svgcanvas/common/browser.js 中的BrowserDetector类与单例导出:isWebkit()、isGecko()、isChrome()、isMac()、supportsGoodTextCharPos()仍以函数形式导出以保持兼容,同时内部采用现代特性检测 + 惰性求值 + Map 结果缓存(#cachedResults),并暴露browser.isWebkit等 getter。该模块被多处真实调用:如 select.js 用isWebkit,selected-elem.js 用isGecko,svg-exec.js 同时用到三者,text-actions.js 用supportsGoodTextCharPos,Editor.js 用isMac,MainMenu.js 用isChrome。命名空间化的思路在 ES Modules 时代以更彻底的形态延续了下来。
Drawing 类:当年设想的完全体
当年"引入 Drawing 封装单文档状态"的设想,如今完整实现在 packages/svgcanvas/core/draw.js 的Drawing类中:它封装svgElem_(被编辑的 SVG DOM 元素)、obj_num计数、idPrefix、可复用的releasedNums栈、按 z 序排列的all_layers图层数组、layer_map名称索引与current_layer当前图层,并通过se:nonce机制支持跨文档随机化元素 ID。getNextId()/releaseId()实现了 ID 的生成与回收复用。在 svgcanvas.js 的构造器中可以看到this.current_drawing_ = new draw.Drawing(this.svgContent, this.idprefix)以及clear()时重建Drawing、创建空首层、重置撤销栈的完整闭环——当年 TODO 里"完成图层功能移入 Drawing 类"已由 layer.js 与Drawing共同完成。
pathActions:从"半途迁移"到独立path模块
当年"pathActions 约迁移一半"的工程,如今由 packages/svgcanvas/core/path.js 完整承接:它向下聚合了路径段数据表segData(覆盖 MOVETO/LINETO/CURVETO/ARC 等各类绝对路径段)、控制点链接开关linkControlPts,再引入 path-method.js 的Path类与 path-actions.js 的pathActions方法集。而 svgcanvas.js 顶部通过import * as pathModule from './core/path.js'取用pathActions,构造器中this.pathActions = pathActions; pathModule.init(this)——"公共 API 留在 svgcanvas.js"的过渡态已被清晰的门面导入取代。
setSvgString / importSvgString 与 API 文档的归宿
当年 TODO 要求把setSvgString()、importSvgString()移入独立模块。在 svgcanvas.js 第 1232 行的注释("this BEFORE callingsvgCanvas.setSvgString")中仍能看到该方法作为SvgCanvas公共 API 的痕迹,而底层序列化/反序列化逻辑已分别沉淀到 json.js(JSON ↔ SVG 元素转换)与 sanitize.js(SVG 清洗)等模块中。至于"重新生成 API 文档",当前仓库的 package.json 提供了build-docs(基于 JSDoc 与 jsdoc-config.js)与open-docs等脚本,文档工作已完全脚本化。
构建与发布:从 Closure Compiler 到 Vite 打包
文档时代通过 Makefile + Closure Compiler 把所有模块合并回一个文件以减少 GET 请求;当前仓库则升级为 Vite 构建链:"build": "vite build packages/svgcanvas && vite build packages/react-test && vite build",svgcanvas 包自身的构建脚本为vite build(见 packages/svgcanvas/package.json)。模块化 + 构建期合并的核心理念一脉相承,只是工具链从 Closure 换成了 Vite,且每个子包都产出可独立发布的产物与类型声明svgcanvas.d.ts。
当前模块地图:重构终局的直观证据
packages/svgcanvas/core/目录完整呈现了当年拆分构想的终局——按职责划分的三十余个核心模块,每个都对应文档中"拆模块 + 配测试"的规划:
- 文档状态:draw.js、layer.js、dataStorage.js;
- 历史/撤销:history.js、historyrecording.js、undo.js;
- 路径与绘制:path.js、path-actions.js、path-method.js、svg-exec.js;
- 选择与文本:select.js、selection.js、selected-elem.js、text-actions.js;
- 坐标与几何:coords.js、math.js、recalculate.js、units.js;
- 基础能力:namespaces.js、sanitize.js、paint.js、clipboard.js、paste-elem.js、copy-elem.js、clear.js、event.js、blur-event.js、touch.js、svgroot.js、elem-get-set.js、json.js、utilities.js。
与此同时,src/editor/侧承载 UI 层(Editor.js、MainMenu.js、panels、components 等),src/editor/extensions 继续扮演文档中提到的"扩展机制"角色,把特色功能从主代码库中分离出去——这正是当初促使svgcanvas.js不至于无限膨胀的那道防线。
结语:一次重构留下的可迁移方法论
回看这篇写于项目早期的重构文档,它最宝贵的遗产并非某个具体拆分,而是一套任何中型前端项目都可复用的方法论:
- 模块化不只关乎代码组织,更关乎社区活力——巨型单文件会把潜在贡献者挡在门外,这在依赖志愿协作的开源项目中尤为致命;
- 以"可浏览、可测试"为基准——每拆出一个模块就配套单元测试(当时近 500 个,如今
tests/unit数十个测试文件),用测试网兜住人工验证的盲区; - 开发期多文件、发布期合并——通过构建工具(当年 Closure Compiler,如今 Vite)在发布时把模块重新折叠,平衡"开发可读性"与"运行时请求数"这对矛盾;
- 渐进式迁移而非大爆炸重写——对依赖复杂的代码块(如
pathActions)逐块搬移、保持公共 API 兼容,是降低回归风险的关键。
如今你可以在 packages/svgcanvas/core 目录中逐文件验证这份十余年前的蓝图——每一个当年 TODO 的落点,都能在当前源码里找到对应模块、对应测试与对应构建脚本。
- 前端
- 图形学
【免费下载链接】svgedit
Powerful SVG-Editor for your browser
相关推荐
78 个免费公共 Tracker 清单:两步接入 qBittorrent,让 BT 下载提速
78 个免费公共 Tracker 清单:两步接入 qBittorrent,让 BT 下载提速 trackerslist 帮你维护一份免费公共 Tracker 服
Hound白名单策略实战:如何用 --files 让AI代码审计聚焦关键文件、告别Token洪流?
Hound白名单策略实战:如何用 files 让AI代码审计聚焦关键文件、告别Token洪流? 用Hound做AI代码审计时,你是不是也遇到过这样的场景:一个中
KICS安全扫描引擎核心算法深度解析
KICS安全扫描引擎核心算法深度解析 KICS(Keeping Infrastructure as Code Secure)是Checkmarx推出的一款强大的
数据库低代码后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考