WeKan 的 Less Code 计划:用可测量方式削减 1,880 行维护代码的六阶段实践
【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan
Wekan 团队在 docs/Design/LessCode.md 中记录了一套完整的“减码”工程实践:在不删除受支持行为、翻译、无障碍特性与测试的前提下,通过六个阶段将维护源码从 192,952 行削减到 191,220 行,并额外清除了 148 行已废弃的跟踪代码。本文将完整继承该文档的基线数据、工作规则与各阶段验收标准,并结合仓库中的实际源码(语言注册表、分页原语、板级读取策略、导入管线),讲解每一阶段“减少独立实现”而非“压缩代码”的落地方式。
基线:先量清楚,再谈减少
Less Code 计划的第一步是建立可重复的度量基线。文档于 2026-09-03 对client/、server/、imports/、models/下的 JavaScript、Jade、CSS 与 MJS 文件进行了统计:
| 来源 | 文件数 | 行数 |
|---|---|---|
| JavaScript 与 MJS | 798 | 150,724 |
| CSS | 73 | 28,941 |
| Jade | 111 | 13,287 |
| 合计 | 982 | 192,952 |
生成的 bundle、依赖、翻译 JSON 与测试均不计入。文档特别强调,基线是“导航工具”而非“刷分目标”:把代码挪进生成文件、压缩格式,都不算减少。这一约束贯穿全部六个阶段的验收标准。
初始调研找出的最大机会点如下:
| 区域 | 现有证据 | 机会 |
|---|---|---|
| 看板主题 | boardColors.css 达 6,339 行 | 主题值只声明一次,结构规则共享 |
| 语言注册表 | languages.js 达 1,724 行 | 用紧凑元数据生成重复的注册表条目 |
| 大型 UI 控制器 | 多个文件 1,400–2,900 行 | 抽取真正重复的分页、表单与动作逻辑 |
| 授权 | 同一规则散布在 models、methods、publications 与 REST 路由 | 每个操作用一个策略函数 |
| 导入器 | 各 Creator 模块重复实体创建管线 | 归一化输入后走同一条持久化管线 |
| 废弃路径 | 动态 Blaze 引用掩盖部分无用代码 | 仅在运行时与测试证据下删除 |
同时文档明确排除了几类“伪减码”:拆分大文件、把 Jade 换成 Svelte、压缩源码、把逻辑搬进依赖包,这些本身都不满足计划目标。
工作规则:小步迁移,证据先行
计划的工作规则是每阶段完成并验证后才开始下一阶段,并在文档中记录前后测量值:
- 行为变化需要正向与负向测试;可见变化还需要相应的 UI 或截图测试;
- 保留特例,而不是把它们硬塞进让代码更难理解的抽象;
- 优先小迁移——当测量显示某个抽象带来的复杂度超过它消除的复杂度时,该阶段可以中止。
这套规则直接决定了后续每阶段的“验收标准”写法:不是“删了多少行”,而是“测试是否通过、行为是否保持、维护面是否变小”。
阶段一:看板主题声明——用 CSS 变量收敛重复规则
问题出在 client/components/boards/boardColors.css:该文件为每个命名主题重复写选择器与声明,膨胀到 6,339 行。计划的做法是引入由 CSS 自定义属性支撑的共享结构规则,同时保留渐变、图片主题、Apple Glass Pastel 等异常设计的显式覆盖。
执行步骤:
- 清点普通颜色主题用到的属性角色,并归类异常主题;
- 为普通主题添加一套共享规则与一小段变量声明;
- 改造现有主题测试,让它验证计算结果而不是旧的重复选择器布局;
- 分批迁移普通主题,每批之后运行主题、页头、公共板、All Boards 与复选框测试;
- 对比 CSS 体积与桌面/移动宽度下代表性渲染的看板;特例主题若显式 CSS 更清晰则保持显式。
验收标准:所有既有视觉行为与主题选择保持可用;主题测试与相关 Playwright 测试通过;boardColors.css的声明数与字节数实质性下降;新增一个普通纯色主题主要靠声明变量即可完成。
实际结果:
- 十个普通纯色主题现在只声明十二个色板值,并共用一套结构规则;
- 渐变、图片、Relax、Dark、Apple Glass Pastel、Modern 与 Clean 主题因结构或行为不同而保持显式;
boardColors.css从 6,339 行 / 196,012 字节 / 1,266 条规则 / 2,216 条声明,降到 5,895 行 / 178,134 字节 / 1,058 条规则 / 2,064 条声明;- PostCSS 成功解析结果样式表;
themeAccents、boardTileTheme、allBoardsPage、publicBoardsPage、headerBars、checkboxesAreSquare与appleGlassPastelTheme全部通过,共 110 个断言;- 文档还诚实记录了一个环境边界:在 1,280 px 与 390 px 下的 Chromium 计算样式检查未能完成(ARM64 沙箱只有 x86-64 的 Chromium 二进制,模拟启动未结束),因此运行时视觉验证被标注为环境限制而非已声称结果。
该阶段完成于提交cd1039230。值得注意的是,仓库中该文件当前约 5,885 行,与记录值同量级,可用作交叉印证。
阶段二:语言元数据——245 个语言记录压成一行一个
imports/i18n/languages.js 原本为每种语言手写一个重复结构的注册表对象,共 1,724 行。计划要求:用紧凑元数据与生成的懒加载导入替换手写对象,但公共注册表形状与语言顺序必须保持不变,新增语言仍须满足翻译策略规定的三个集成点。
验收标准:
- 每个现有 locale 以相同的 code、tag、原生名称与 RTL 值注册;
- 所有翻译保持懒加载;
- 翻译、键顺序、占位符与语言选择器测试通过;
- 权威元数据更短,且带重复 tag 检查。
当前仓库中的实现正是这一阶段的产物。imports/i18n/languages.js 现在是一组紧凑元数据行:
const languageMetadata = [ ["ace", "ace", "ace", "Bahsa Acèh", false], ["af", "af", "af", "Afrikaans", false], ["ar", "ar", "ar", "العربية", true], ["fa-IR", "fa", "fa-IR", "فارسی/پارسی (ایران)", true], // ... 每行: [语言键, 语言码, BCP-47 tag, 原生名称, RTL 标志] ];文件头部注释点明了设计意图:“紧凑语言元数据与字面量动态导入分开存放。每个 import 都是一个静态分割点,浏览器只下载打开的那个语言”——这一点由 tests/i18nLazyLoading.test.cjs 钉住。
实际结果:
- 245 条语言记录现在每条只占一行紧凑元数据;
- 独立的 loader map 为每种语言保留一个字面量动态
import(),维持 Meteor 按语言拆包的特性; - 运行时对等比较确认新旧模块的全部 245 个条目在键、code、tag、原生名称、RTL 标志与 loader 路径上完全一致;
- 文件从 1,724 行 / 34,873 字节降到 510 行 / 24,362 字节(仓库当前 languages.js 正是 510 行);
- 防护逻辑现在拒绝重复元数据键、重复语言 tag、重复 loader 键、缺失 loader、急切导入,以及未被注册表认领的文件;
i18nLazyLoading、i18nLazyLoaded、newLanguageWiring、rtl与changeLanguageColumns测试通过,共 31 个断言。
该阶段完成于提交a3bc8155d。
阶段三:重复 UI 机制——一个 adjacentPage 原语收敛五处分页
这一阶段的铁律是“先测量重复,再抽取任何东西”,聚焦重复的分页、搜索、菜单数据、模态生命周期、表单取值与 Meteor method 结果处理,并且明确禁止为“看起来像但行为不同”的代码创建通用 helper。
验收标准:
- 每个被抽取的抽象至少有两个真实使用方;
- 用户可见的错误、键盘与无障碍行为保持;
- 焦点单元测试覆盖共享原语,UI 测试覆盖使用方;
- 计入新抽象后总维护代码仍然更低。
实际落地是一个adjacentPage原语,位于 models/lib/tablePage.js:
// One bounded step for pagers that do not use pageInfo directly. export function adjacentPage(total, page, direction, perPage = TABLE_PAGE_ROWS_PER_PAGE) { const info = pageInfo(total, page, perPage); const step = Math.sign(Number(direction)); if (!Number.isFinite(step) || step === 0) return info.page; return Math.min(info.totalPages, Math.max(1, info.page + step)); }它复用了同文件内pageInfo的钳制逻辑(页码超出范围时解析到真实存在的页,而不是空视图),现在被 All Boards、Admin Panel 通用报表、事件流、office 报表,以及 People 面板中 organizations、teams、people 与登录地钻取共四个分页上下文共用;People 把活动面板一次性映射到页码/总数/页大小状态,替换了原来分散的上/下页分支。
测试覆盖普通翻页、两个边界、归一化方向与非法方向;现有使用方套件通过 96 个断言(tablePage55、allBoardsPage27、订阅生命周期 3、分组 offices 11)。全量维护源码从 191,284 行降到 191,279 行——净减 5 行已包含新共享原语本身,而测试(不计入该度量)则增长以钉住其行为。
该阶段完成于提交eb0e34786。
阶段四:授权策略——canReadBoard 让 DDP 与 REST 同判
计划要求清点集合方法、Meteor method、publication 与 REST 端点之间的等价权限判断,把每个等价判断移入纯共享策略,传输层特有的错误格式化留在边缘。验收标准包括:同一 actor 与资源在 DDP 与 REST 下得到相同决定;策略有允许与拒绝(含缺失资源)测试;publication 不暴露被对应变更策略拒绝的记录;重复的权限条件是删除而不是包裹。
这一阶段的结论分两部分:
其一,审计确认现有的写能力策略已经同时服务集合权限、Meteor method、REST 变更与 publication 中的目标检查——于是保留它,没有再建竞争抽象。这是该计划“抽象只增不减就停手”原则的正面案例。
其二,新增了一个传输中立的canReadBoard策略,现在位于 models/lib/boardVisibility.js:
// Transport-neutral read policy for a board. Public boards accept anonymous // readers; every other board requires an active member. function canReadBoard(userId, board) { return !!(board && typeof board.isVisibleBy === 'function' && board.isVisibleBy(userId ? { _id: userId } : null)); }它现在服务两个 DDP publication、两个 HTTP 附件路由与十四处 position-history 方法检查:公共板允许匿名与已认证读取,私有板要求活跃成员,缺失或畸形板 fail closed。HTTP 与 DDP 各自在边缘保留自己的响应/错误格式化;遗留附件 publication 现在与 HTTP 下载路由做出相同的公共板判断,而不是拒绝所有匿名订阅者。仓库中的 server/publications/legacyAttachments.js、server/publications/cardsWindow.js、server/routes/legacyAttachments.js、server/routes/universalFileServer.js 与 server/methods/positionHistory.js 都是这一策略的消费方,行为由 tests/boardVisibility.test.cjs 等六个焦点策略/接线测试覆盖。
维护源码从 191,279 行降到 191,248 行(含新策略模块)。该阶段完成于提交e8c867b33。
阶段五:导入器管线——适配器归一化,写入器只有一份
计划为导入器定义了一个小型内部板表示:源特定模块负责解析与归一化数据,一个有测试的写入器统一创建用户、板、泳道、列表、卡片、评论、清单与附件。验收标准要求:现有受支持格式行为不变;源特定怪癖留在适配器中;畸形与部分导入有负向测试;实体创建与 ID 映射逻辑在可能处只有一份实现。
仓库中的落地是两个文件级别的共享实现。models/lib/importPipeline.js 中的writeImportedEntity独占“直接插入 + 可选时间戳刷新 + 旧 ID 到新 ID 记录”:
export async function writeImportedEntity(collection, document, options = {}) { const id = await collection.direct.insertAsync(document); if (options.touch) { await collection.direct.updateAsync(id, { $set: options.touch }); } if (options.ids && options.sourceId != null) { options.ids[options.sourceId] = id; } return id; }runImportPipeline则按声明的顺序执行各阶段,把已创建的板 ID 带入后续写入器,并把缺失的可选集合归一化为空数组;畸形顶层输入直接抛TypeError,任何未创建板的管线以Import pipeline did not create a boardfail closed:
export async function runImportPipeline(creator, board, stages) { if (!board || typeof board !== 'object' || Array.isArray(board)) { throw new TypeError('Imported board must be an object'); } let boardId; for (const stage of stages) { const input = stage.source ? board[stage.source] || [] : board; const result = await creatorstage.method; if (stage.createsBoard) boardId = result; } if (!boardId) throw new Error('Import pipeline did not create a board'); return boardId; }源特定归一化仍留在适配器里:models/wekanCreator.js 保留 WeKan 真实泳道与额外的依赖/规则阶段,models/trelloCreator.js 保留 Trello 的合成默认泳道与内联清单项。六个管线测试与 95 个既有导入器断言全部通过(tests/importPipeline.test.cjs),三个被改模块还通过了 Node 24 语法检查。维护源码从 191,248 行降到 191,220 行;同批清点中还删除了过时的注释掉的导入器实现与未使用的空 WeKan checker。
该阶段完成于提交8b7115335。
阶段六:基于证据的删除——先静态分析,后运行时验证
计划把静态分析当“候选生成器”:候选必须对照 Blaze 模板名、动态 import、全局注册、服务器启动副作用与打包入口点逐一核实后才允许删除。验收标准:每次删除都有搜索证据与相关回归覆盖;应用启动、生产构建与已注册测试套件成功;任何受支持的部署模式或文档化功能不得被静默移除。
实际结果:
- 静态搜索发现两个带
.disabled后缀、因此不会被 Meteor 当作 JavaScript 处理的跟踪模型实现;没有任何 import、模板、启动注册、包入口或应用源码引用它们的文件名或AttachmentsOld/AvatarsOld符号; - 删除了
models/attachments_old.js.disabled与models/avatars_old.js.disabled,共 148 行跟踪代码,可从 git 历史恢复; - 受支持的遗留 CollectionFS 读取仍保留在 models/attachmentBackwardCompatibility.js(对应仓库文件 models/attachments.js 体系内的回读模块)、遗留附件 publication 与其 HTTP 路由中——迁移抽取与文件安全套件通过;
- 一个回归测试验证被退役的文件与符号保持缺席,同时活跃的回读模块保持存在;
- 全部 709 个已注册 Node 测试套件零失败通过(48 秒);
meteor build .build-lesscode --directory成功,仅有的诊断是既有的资源大小与可选 MongoDB 依赖警告; - 带临时
WRITABLE_PATH的开发启动达到Started your app并在http://127.0.0.1:3999提供服务后干净停止; - 维护源度量保持在 191,220 不变——因为
.disabled文件本就被排除在该基线之外。删除的价值在于它删掉了 148 行可审查的跟踪代码,而不是移动或压缩它们。
该阶段完成于提交e37717d07。
汇总结果与最终结论
各阶段的测量汇总(继承自原文档):
| 阶段 | 之前 | 之后 | 测试 | 结果 |
|---|---|---|---|---|
| 基线 | 192,952 行 | 191,220 行 | 不适用 | 维护行少 1,732 |
| 1. 看板主题 | 6,339 行 / 196,012 字节 | 5,895 行 / 178,134 字节 | 110 断言通过;浏览器不可用 | 完成(cd1039230) |
| 2. 语言元数据 | 1,724 行 / 34,873 字节 | 510 行 / 24,362 字节 | 注册表对等与 31 断言通过 | 完成(a3bc8155d) |
| 3. UI 机制 | 191,284 维护行 | 191,279 维护行 | 原语与 96 个使用方断言通过 | 完成(eb0e34786) |
| 4. 授权 | 191,279 维护行 | 191,248 维护行 | 6 个策略测试与传输回归通过 | 完成(e8c867b33) |
| 5. 导入器 | 191,248 维护行 | 191,220 维护行 | 6 个管线与 95 个导入断言通过 | 完成(8b7115335) |
| 6. 删除 | 148 跟踪禁用行 | 0 跟踪禁用行 | 709 个 Node 套件、构建与启动通过 | 完成(e37717d07) |
最终结论:六个阶段全部完成,维护的 JS/MJS、Jade 与 CSS 度量从 192,952 行降到 191,220 行,即净减 1,732 行;加上阶段六删除的 148 行基线外跟踪禁用行,本次实测工作的总量为1,880 行删除或规避。
计划同样给出了明确的边界判断:这些减少来自“更少的独立实现”,而不是压缩或更换模板技术。把全部 Jade/Blaze 视图换成 Svelte 反而会临时增加代码,因为两套 UI 系统、适配器与迁移测试会共存,它本身不构成减码步骤。后续的进一步工作应当延续这种测量式方法:一次只处理一个重复行为,只有当测试与维护面都改善时才保留该变更。
对阅读此文的工程师而言,这份文档的可复用价值在于其方法论:先度量、排除刷分手段;每个阶段用“行为保持 + 正负向测试 + 维护面变窄”做验收;保留已存在且正确的抽象而非重复造轮子;删除必须留下可检索的证据链。这些约束比具体的 1,732 行数字本身更值得迁移到其他大型代码库的重构工作中。
【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考