news 2026/9/27 6:59:21

SVG-edit 代码重构演进史:从巨型单体 JS 到模块化 SVG Canvas 架构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SVG-edit 代码重构演进史:从巨型单体 JS 到模块化 SVG Canvas 架构
  • 前端
  • 图形学

【免费下载链接】svgedit

Powerful SVG-Editor for your browser

项目地址:https://gitcode.com/gh_mirrors/sv/svgedit
点击查看免费下载

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 在文中给出了两个核心担忧:

  1. 对新人开发者极不友好:一个巨型文件无论注释多详尽、组织多规整,都难以吸引新贡献者——"想搞懂项目如何工作"本身就构成巨大门槛。对依赖零散业余贡献者的开源项目而言,这是致命问题。作者本人即是亲历者:2010 年初暂停重度贡献、年末回归时发现自己已迷失在这个大文件里。
  2. 测试手段只能靠手点:当时验证任何新改动的唯一现实途径是打开编辑器手动试功能。这种方式只能证明"新功能能用",却完全无法捕捉"是否破坏了既有功能"。只依赖人工测试的代码库,长期必然进展缓慢。

历史背景: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 清单

文中记录的进行中任务:

  1. Drawing 抽象:作者引入Drawing概念来封装单个打开 SVG 文档的状态。编辑器持有一个当前 drawing 的句柄,用它代替直接访问 SVG DOM 元素;图层、当前编辑上下文、文档历史等代码最终都应迁入draw.js,但当时大部分仍在svgcanvas.js。
  2. 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不至于无限膨胀的那道防线。

结语:一次重构留下的可迁移方法论

回看这篇写于项目早期的重构文档,它最宝贵的遗产并非某个具体拆分,而是一套任何中型前端项目都可复用的方法论:

  1. 模块化不只关乎代码组织,更关乎社区活力——巨型单文件会把潜在贡献者挡在门外,这在依赖志愿协作的开源项目中尤为致命;
  2. 以"可浏览、可测试"为基准——每拆出一个模块就配套单元测试(当时近 500 个,如今tests/unit数十个测试文件),用测试网兜住人工验证的盲区;
  3. 开发期多文件、发布期合并——通过构建工具(当年 Closure Compiler,如今 Vite)在发布时把模块重新折叠,平衡"开发可读性"与"运行时请求数"这对矛盾;
  4. 渐进式迁移而非大爆炸重写——对依赖复杂的代码块(如pathActions)逐块搬移、保持公共 API 兼容,是降低回归风险的关键。

如今你可以在 packages/svgcanvas/core 目录中逐文件验证这份十余年前的蓝图——每一个当年 TODO 的落点,都能在当前源码里找到对应模块、对应测试与对应构建脚本。

  • 前端
  • 图形学

【免费下载链接】svgedit

Powerful SVG-Editor for your browser

项目地址:https://gitcode.com/gh_mirrors/sv/svgedit
点击查看免费下载
上一篇:跨浏览器兼容性挑战:easytimer.js 如何优雅支持IE9+到现代浏览器
下一篇:DECAF:终极动态二进制分析框架 - 让恶意软件无所遁形

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 6:59:19

新手建站避坑:一文搞懂织梦网站广告代码教程实操

新手建站避坑:一文搞懂织梦网站广告代码教程实操 域名服务器搞不懂,代码插进去报错,这是多少转行做网站新手的噩梦?别急,今天咱们不整虚的,直接上手, 一文搞懂 织梦(DedeCMS)里最让人头疼的广告代码植入。…

作者头像 李华
网站建设 2026/9/27 6:59:15

宁波高端网站建设避坑指南:3个设计细节让转化率翻倍

宁波高端网站建设避坑指南:3个设计细节让转化率翻倍 网站上线三个月,后台数据一片惨淡,流量个位数,询盘为零。这不仅是技术故障,更是设计逻辑的崩塌。很多老板以为“高端”就是堆砌动效、追求视觉华丽,结果做出一堆“花瓶”网站,用户进来看一眼就走了,根本不知道点哪里。 今天这份 宁波高端网站建设避坑指南…

作者头像 李华
网站建设 2026/9/27 6:58:31

网站的通栏怎么做性能优化

网站通栏怎么做兼顾安全与SEO的最佳实践 上周刚帮一个做B2B机械的客户排查故障,他的网站首页突然弹出了博彩广告,后台代码里赫然多了一段混淆过的JavaScript。客户急得打电话问我:“网站被黑挂马不知道怎么办?”这种时候,90%的人第一反应是重装系统或者删文件,但这往往是治标不治本。真正能救命且…

作者头像 李华
网站建设 2026/9/27 6:58:19

网站根目录表示错导致被黑?3招排查源码下载路径防挂马

网站根目录表示错导致被黑?3招排查源码下载路径防挂马 网站被黑挂马不知道怎么办?别慌,八成是网站根目录表示混乱,导致攻击者利用路径遍历漏洞植入木马。这时候急着找源码下载链接没用,得先搞清楚服务器到底把哪个文件夹当首页。很多北京做外包的朋友,项目交付时图省事,直接把 www…

作者头像 李华
网站建设 2026/9/27 6:58:04

给网站建设提意见避坑速查手册:报价与成本全解析

给网站建设提意见避坑速查手册:报价与成本全解析 网站做好了却没人访问,这大概是甲方最痛心的经历。很多老板以为付了钱,网站上线就是万事大吉,结果流量为零,转化率为零,钱白花。这往往不是设计不够好看,而是你在验收阶段没给建设方提对意见,或者压根不知道哪些环节能要价、哪些环节是智商税。这份给网站建设提意见…

作者头像 李华
网站建设 2026/9/27 6:57:57

重庆建站模板厂家避坑指南:从0到1完整流程解析

重庆建站模板厂家避坑指南:从0到1完整流程解析 自己不会代码想做网站,是不是觉得头大?别慌,找对 重庆建站模板厂家 能省一半力气。很多老板找服务商,光看价格不看 完整流程…

作者头像 李华