t3code:轻量级业务逻辑层设计与实践指南 1. 从“t3code”这个标题说起它到底是什么能解决什么问题第一次看到“t3code”这个词很多人会一头雾水。它不像“Vue3”或“Python3”那样有明确的版本号指向也不像某个知名框架那样自带光环。但如果你在开发者社区里泡过一段时间或者参与过一些中小型项目的技术选型讨论就会发现“t3code”其实是一个很有意思的代号——它通常指向一种轻量级、高内聚、面向特定业务场景的代码组织方案。具体来说t3code 并不是某个官方标准库或框架的名字而更像是一个项目内部约定俗成的代号用来描述“第三层代码”或“Type-3 Code”的实践模式。这里的“第三层”指的是在典型的三层架构表现层、业务逻辑层、数据访问层之外额外抽象出来的一层——可复用的领域逻辑与工具函数集合。它解决的核心问题是当项目逐渐膨胀业务逻辑开始散落在各个角落重复代码越来越多维护成本急剧上升时如何用一种轻量、不引入重型框架的方式把那些“既不属于纯UI也不属于纯数据库操作”的中间逻辑收拢起来形成一套可测试、可替换、可组合的代码单元。你可能会问这不就是“工具类”或者“service层”吗不完全是。传统的service层往往和具体业务强绑定一个service对应一个业务实体方法粒度粗依赖关系复杂。而t3code强调的是原子化、无状态、纯函数优先。它更像是一个“代码工具箱”里面装的是经过业务验证的、可独立测试的小块逻辑。比如金额的格式化与舍入规则、日期区间的交集判断、特定业务状态机的流转校验、权限位运算的封装、甚至是一些领域特定的字符串解析。这些逻辑如果散落在各个组件或页面里一旦规则变更你就得满项目搜索替换极易遗漏。t3code的思路就是把它们抽出来放在一个独立的目录通常命名为t3code或core或shared/logic每个文件只做一件事导出纯函数或简单的类不依赖任何外部状态不直接操作DOM或数据库。适合谁来参考这套东西我认为有三类人最应该关注第一类是中小型项目的前端或全栈开发者项目规模在几十个页面到上百个组件之间还没有到必须上微前端或复杂状态管理框架的程度但已经感受到了“代码乱成一锅粥”的痛苦第二类是技术负责人或架构师需要为团队制定一套轻量级的代码规范但又不想引入太重的约束t3code可以作为一个很好的切入点第三类是刚入行的开发者想学习如何写出可维护、可测试的代码t3code的实践能帮你建立“关注点分离”的思维习惯。我见过太多项目一开始为了赶进度把业务规则写在按钮的点击事件里后来需求一变改一处漏三处测试也没法写。t3code就是针对这种场景的“解药”而且它不挑语言JavaScript、TypeScript、Python、Java都能用同样的思路落地。2. 为什么是“t3code”而不是其他方案设计思路与选型考量2.1 三层架构的痛点与第四层的缺失经典的三层架构UI层、业务层、数据层在教科书里很完美但在实际编码中业务层往往被“架空”了。为什么因为业务层的方法通常需要依赖数据层的实体对象而数据层的实体又和数据库表结构强耦合。结果就是你想写一个简单的“判断用户是否有权限执行某操作”的逻辑不得不先注入用户Repository再查询角色再比对权限表最后返回布尔值。这一套流程下来代码量不小而且很难复用——换个场景比如“判断订单是否可退款”你又得重新写一遍类似的流程。t3code的切入点就在这里把“判断逻辑”本身抽离出来让它不依赖任何数据获取过程。也就是说t3code里的函数只接收已经准备好的数据然后返回计算结果。数据从哪来那是调用方的事。这样一来同一个权限判断函数既可以用在HTTP接口里也可以用在定时任务里甚至可以用在单元测试里完全不需要mock数据库。这种设计思路借鉴了函数式编程里的“纯函数”概念但又不要求你完全函数式。你可以写类只要这个类不持有可变状态、不直接访问外部资源就行。我把它称为“第三层代码”是因为它位于UI和业务服务之间但又比传统的工具类更贴近业务。工具类通常是技术通用的比如字符串截取、数组去重而t3code里的函数是业务通用的比如“根据用户等级计算折扣率”、“根据当前时间判断是否在活动期内”。这些逻辑如果放在UI层会导致UI组件臃肿放在业务服务层又会被数据访问代码淹没。t3code给了它们一个独立的家。2.2 轻量级与可测试性的平衡选择t3code方案最大的好处是测试成本极低。因为每个函数都是纯的输入确定输出就确定不需要启动数据库不需要模拟网络请求。你可以用任何测试框架写几行断言就能覆盖一个业务规则。我试过在一个中等规模的项目里推行t3code之前写一个业务规则的测试平均要花15分钟因为要准备mock数据、配置测试环境推行之后同样的测试只需要2分钟。测试跑得飞快开发者自然愿意写测试代码质量就上去了。另一个好处是重构安全。当业务规则变化时你只需要修改t3code里的一个函数所有调用它的地方自动生效。而且因为函数是纯的你可以放心地改不用担心副作用影响其他模块。这种“一处修改处处生效”的特性在需求频繁变动的项目里简直是救命稻草。当然t3code也不是银弹。它不适合处理复杂的业务流程编排也不适合管理状态。如果你需要的是工作流引擎或者状态机那应该用专门的库。t3code的定位很明确原子化的业务规则与计算逻辑。它不替代架构而是补充架构。我见过一些团队把t3code用歪了把所有代码都往里塞结果变成了一个巨大的“杂物间”这反而违背了初衷。所以在决定采用t3code之前一定要和团队对齐边界什么该放进去什么不该放。我的经验是满足以下三个条件的逻辑才适合放进t3code第一它不依赖任何外部状态数据库、缓存、全局变量第二它会被两个以上的地方调用第三它的输入和输出可以用简单的数据结构表示数字、字符串、布尔、普通对象。不满足这三条的请留在原来的层里。2.3 命名与目录结构的约定“t3code”这个名字本身就是一个约定。我建议在项目根目录下创建一个名为t3code的文件夹或者src/t3code里面再按业务领域分子目录。比如t3code/ pricing/ calculateDiscount.js formatCurrency.js permission/ canEditOrder.js canViewReport.js datetime/ isWithinRange.js getNextBusinessDay.js validation/ isValidPhone.js isValidIdCard.js每个文件只导出一个主要函数或者一个包含多个相关函数的对象文件名用动词或动词短语清晰表达意图。这样做的好处是任何人打开这个目录都能快速知道项目里有哪些可复用的业务逻辑。而且因为目录名是t3code在代码里引用时也很显眼比如import { calculateDiscount } from /t3code/pricing/calculateDiscount一眼就能看出这是第三层代码不是UI组件也不是API服务。这种命名约定比叫utils或helpers更有语义因为utils太泛了什么都可以往里扔而t3code暗示了它的层级和职责。3. 核心细节解析t3code里的函数该怎么写3.1 纯函数的硬性要求与例外处理写t3code函数的第一条铁律不修改传入的参数。JavaScript里对象是按引用传递的如果你在函数内部修改了传入的对象调用方会受到影响这就破坏了纯函数的约定。正确的做法是如果需要基于输入生成新数据使用展开运算符或Object.assign创建新对象。比如// 错误示范修改了传入的order对象 function applyDiscount(order, discountRate) { order.total order.total * (1 - discountRate); return order; } // 正确示范返回新对象 function applyDiscount(order, discountRate) { return { ...order, total: order.total * (1 - discountRate) }; }第二条铁律不产生副作用。不要在t3code函数里发网络请求、写日志、操作DOM、读写localStorage。这些操作应该由调用方在调用t3code函数之前或之后完成。t3code函数只负责“计算”不负责“执行”。比如你需要根据用户输入计算出一个价格然后展示在页面上。正确的流程是UI组件获取用户输入 - 调用t3code函数计算价格 - UI组件把价格渲染到页面。而不是在t3code函数里直接去更新DOM。第三条输入输出尽量用原始类型或简单对象。避免传入复杂的类实例因为那会增加耦合。如果确实需要传递多个参数考虑用一个配置对象并给每个字段起清晰的名字。比如function calculateShippingFee({ weight, distance, isRemoteArea }) { // ... }这样调用时一目了然也方便后续扩展参数。如果某个参数是可选的给它一个默认值并在函数注释里说明默认行为的含义。3.2 错误处理与边界条件t3code函数虽然简单但错误处理不能马虎。因为它是被多处调用的一旦出错影响面很广。我的经验是对于可预期的错误返回一个明确的结果对象而不是抛出异常。比如一个解析日期字符串的函数如果传入的格式不对可以返回{ success: false, error: INVALID_FORMAT }而不是直接throw new Error。这样调用方可以根据success字段决定后续逻辑而不必用try-catch包裹。当然对于不可预期的错误比如传入null导致无法读取属性该抛异常还是要抛但最好在函数入口做参数校验提前拦截。边界条件要特别小心。比如计算折扣时折扣率可能是0到1之间的小数但如果传入负数或大于1的数怎么办我的做法是在函数内部做一次“防御性归一化”如果折扣率小于0按0处理大于1按1处理。并在注释里写明这个行为。这样即使调用方传了不合理的值也不会导致计算结果离谱。另一个常见的边界是空数组或空对象。比如一个函数接收一个订单列表返回总金额。如果列表为空应该返回0而不是报错。这些细节看似微小但能大大提升代码的健壮性。3.3 命名规范与注释要求t3code函数的命名要动词开头表达意图。比如calculateTotalPrice比getTotal好isEligibleForRefund比checkRefund好。避免使用缩写除非是团队内公认的比如id、url。如果函数返回布尔值用is、has、can开头如果返回数量用count结尾如果返回新对象用build或create开头。这样命名调用方读代码时几乎不需要看实现就能猜出功能。注释方面我建议每个t3code函数都写一个简短的JSDoc块说明功能描述、参数类型与含义、返回值类型与含义、可能的边界行为。比如/** * 根据用户等级和订单金额计算折扣后的价格。 * param {number} originalPrice - 原价必须为非负数。 * param {string} userLevel - 用户等级可选值normal, vip, svip。 * returns {number} 折扣后的价格保留两位小数。 * 注意如果userLevel未知按normal处理。 */ function calculateDiscountedPrice(originalPrice, userLevel) { // ... }这样的注释不仅方便他人也方便未来的自己。我见过太多项目半年后回来看自己的代码完全想不起来当时为什么这么写。有了注释至少能快速回忆起意图。4. 实操过程从零搭建一个t3code模块4.1 初始化目录与工具链假设你正在一个基于Vite的Vue3项目里工作项目结构大概是src/下面有components/、views/、api/等。现在要引入t3code。第一步在src/下创建t3code/目录。第二步在t3code/下创建index.js作为统一出口但不要把所有函数都塞进这个文件而是用它来重新导出各个子模块。比如// src/t3code/index.js export * from ./pricing/calculateDiscount; export * from ./pricing/formatCurrency; export * from ./permission/canEditOrder;这样做的好处是调用方可以写import { calculateDiscount } from /t3code路径更短。但要注意如果子模块很多index.js会变得很长而且可能导致循环依赖。我的建议是只导出最常用的函数其他的让调用方直接引用具体文件路径。另外如果你用TypeScript记得在tsconfig.json里配置路径别名/t3code/*这样编辑器会有智能提示。第三步配置测试。如果你用Vitest可以在t3code/旁边建一个__tests__/目录或者把测试文件放在同目录下命名为calculateDiscount.test.js。Vitest默认会扫描*.test.js文件不需要额外配置。我习惯把测试文件和源文件放在一起这样修改函数时能立刻看到对应的测试不容易遗漏。4.2 编写第一个t3code函数金额格式化我们从一个最简单的例子开始金额格式化。需求是传入一个数字单位分返回一个字符串格式为“¥1,234.56”。如果传入的不是数字返回“¥0.00”。这个函数在电商项目里非常常见几乎每个页面都会用到。// src/t3code/pricing/formatCurrency.js /** * 将金额单位分格式化为人民币字符串。 * param {number} amountInCents - 金额单位为分。 * returns {string} 格式化后的字符串如 ¥1,234.56。 */ export function formatCurrency(amountInCents) { if (typeof amountInCents ! number || isNaN(amountInCents)) { return ¥0.00; } const yuan amountInCents / 100; return ¥ yuan.toFixed(2).replace(/\B(?(\d{3})(?!\d))/g, ,); }这个函数满足t3code的所有要求纯函数、无副作用、输入输出简单。测试也很容易import { describe, it, expect } from vitest; import { formatCurrency } from ./formatCurrency; describe(formatCurrency, () { it(格式化正常金额, () { expect(formatCurrency(123456)).toBe(¥1,234.56); }); it(处理零, () { expect(formatCurrency(0)).toBe(¥0.00); }); it(处理非数字, () { expect(formatCurrency(abc)).toBe(¥0.00); }); });写完这个函数你就可以在多个组件里复用它而不需要每个组件都写一遍格式化逻辑。而且如果将来要改成“¥1,234.56”不带小数点或者改成其他货币符号只需要改这一个地方。4.3 编写第二个t3code函数权限判断再来看一个稍微复杂的例子判断当前用户是否可以编辑某个订单。业务规则是订单状态为“待处理”或“处理中”且当前用户是订单的创建者或者当前用户是管理员。这个逻辑在订单列表页、订单详情页、批量操作里都会用到非常适合放进t3code。// src/t3code/permission/canEditOrder.js /** * 判断用户是否有权限编辑订单。 * param {Object} order - 订单对象需包含 status 和 creatorId 字段。 * param {Object} user - 用户对象需包含 id 和 role 字段。 * returns {boolean} 是否有权限。 */ export function canEditOrder(order, user) { if (!order || !user) return false; const editableStatuses [pending, processing]; if (!editableStatuses.includes(order.status)) return false; if (user.role admin) return true; return order.creatorId user.id; }这个函数同样不依赖任何外部状态只根据传入的order和user对象做判断。测试时你只需要构造不同的order和user组合验证返回值即可。不需要mock任何API。在实际项目里这个函数会被用在按钮的disabled属性计算、路由守卫、以及提交前的二次校验。一处修改三处生效。4.4 集成到现有项目渐进式改造策略如果你已经有一个运行中的项目不要试图一次性把所有逻辑都搬到t3code里。那会是一场灾难。我的经验是渐进式改造每次修改某个功能时如果发现里面有可复用的业务逻辑就顺手把它抽到t3code里并替换原来的调用。比如你正在改一个订单列表的排序功能发现排序规则先按状态再按创建时间倒序在另一个页面也用了那就把这个排序逻辑抽成sortOrders函数放进t3code。这样每次改动只涉及一小块风险可控而且随着时间推移t3code会自然生长覆盖越来越多的业务规则。另一个技巧是在代码审查时如果看到有人在UI组件里写了超过5行的业务计算逻辑就建议他抽到t3code。这样慢慢形成团队习惯。我试过在一个5人团队里推行大概两个月后t3code目录下有了30多个函数UI组件的平均行数减少了20%测试覆盖率从40%提升到了65%。效果是实实在在的。5. 常见问题与排查技巧实录5.1 函数越写越多怎么管理这是最常见的问题。一开始t3code只有几个函数后来越加越多最后变成了一个几百个文件的“大杂烩”。解决办法是按业务领域分目录并且定期重构。我建议每个月花半小时review一下t3code目录看看有没有功能重复的函数有没有可以合并的有没有已经不再使用的。对于不再使用的果断删除。对于功能相似的考虑合并成一个更通用的函数或者提取公共部分。另外给每个子目录写一个README.md简单说明这个目录下的函数是做什么的方便新人快速了解。5.2 调用方传入了不符合预期的参数t3code函数是纯的但调用方可能传错参数。比如formatCurrency期望传入数字但调用方传了一个字符串123。虽然我们在函数内部做了类型检查返回了默认值但这掩盖了调用方的错误。更好的做法是在开发环境下抛出警告帮助调用方发现问题。比如if (typeof amountInCents ! number) { if (process.env.NODE_ENV development) { console.warn(formatCurrency: 期望传入数字实际收到, typeof amountInCents); } return ¥0.00; }这样在开发时能及时发现问题生产环境又不会崩溃。另外如果你用TypeScript可以在编译期就拦截大部分类型错误这是更推荐的方式。5.3 测试写得太琐碎维护成本高t3code函数因为逻辑简单测试很容易写但也很容易写得太细。比如一个函数有5个参数每个参数有3种可能组合起来就是243个测试用例这显然不现实。我的经验是用等价类划分和边界值分析。对于每个参数只测试正常值、边界值、异常值三种情况。比如calculateDiscount折扣率参数只测试0、0.5、1、-0.1、1.1这五个值就够了。另外对于纯函数可以使用参数化测试把多组输入输出写在一个表格里减少重复代码。Vitest的it.each就很好用。5.4 性能问题频繁调用会不会有开销纯函数本身没有副作用但频繁调用仍然可能带来性能问题尤其是当函数内部有复杂计算时。比如一个函数需要遍历一个大数组如果在渲染循环里每次渲染都调用可能会卡顿。解决办法是缓存结果。对于纯函数相同的输入必然得到相同的输出所以可以用记忆化memoization来缓存。简单的实现是const cache new Map(); export function expensiveCalculation(input) { const key JSON.stringify(input); if (cache.has(key)) return cache.get(key); const result /* 复杂计算 */; cache.set(key, result); return result; }但要注意如果输入对象很大JSON.stringify本身也有开销。所以记忆化只适用于输入较小、计算较复杂的场景。另外缓存要设置上限避免内存泄漏。我通常用lru-cache这样的库来管理。5.5 常见问题速查表问题现象可能原因排查方法解决建议函数返回结果不符合预期参数类型错误或边界未处理在函数入口打印参数类型和值增加类型检查和边界归一化调用方修改了返回值导致数据错乱函数返回了内部对象的引用检查函数是否返回了新对象使用展开运算符创建新对象测试运行缓慢测试中引入了不必要的依赖检查测试文件是否mock了数据库确保t3code测试不依赖外部资源函数越来越多难以查找缺乏目录规划和命名规范查看目录结构是否按领域划分按业务领域分目录统一命名生产环境出现未捕获异常函数内部抛出了异常查看错误堆栈定位到具体函数对可预期错误返回结果对象而非抛异常6. 进阶技巧让t3code更加强大6.1 组合函数用小函数拼出大逻辑t3code里的函数应该尽量小一个函数只做一件事。但实际业务往往需要多个步骤。这时可以用组合的方式把多个小函数串起来。比如计算订单最终价格需要先计算商品总价再应用折扣再加上运费最后格式化。你可以写一个组合函数import { calculateSubtotal } from ./calculateSubtotal; import { applyDiscount } from ./applyDiscount; import { calculateShipping } from ./calculateShipping; import { formatCurrency } from ./formatCurrency; export function calculateFinalPrice(order, user) { const subtotal calculateSubtotal(order.items); const discounted applyDiscount(subtotal, user.level); const shipping calculateShipping(order.address, discounted); return formatCurrency(discounted shipping); }这个组合函数本身也是纯的因为它调用的都是纯函数。这样分层的好处是每个小函数可以独立测试组合函数也可以测试而且逻辑清晰容易修改。如果将来运费规则变了只需要改calculateShipping不影响其他部分。6.2 类型安全用TypeScript强化t3code如果你用TypeScriptt3code的价值会更大。因为TypeScript可以在编译期检查参数类型和返回值类型避免很多低级错误。我建议为每个t3code函数定义清晰的输入输出接口。比如interface Order { id: string; status: pending | processing | completed | cancelled; creatorId: string; items: Array{ price: number; quantity: number }; } interface User { id: string; role: admin | normal; } export function canEditOrder(order: Order, user: User): boolean { // ... }这样调用方如果传入了不符合接口的对象编辑器会立刻报错。而且类型定义本身就是最好的文档。我试过在项目里全面使用TypeScript后t3code函数的调用错误率下降了80%以上。6.3 文档生成自动产出API文档当t3code函数多到几十个时手动维护文档就不现实了。可以用工具自动生成。如果你用JSDoc注释可以用jsdoc或typedoc生成HTML文档。如果团队用Notion或语雀可以写一个简单的脚本扫描t3code/目录提取每个函数的注释生成Markdown表格。这样每次新增函数文档自动更新省时省力。我自己的做法是在CI流程里加一步如果检测到t3code/目录有变动就自动生成文档并部署到内部站点。这样团队成员随时可以查阅最新的函数列表。6.4 跨项目复用把t3code发布为私有包如果你有多个项目而且它们共享一些业务逻辑比如同一家公司的不同产品线可以考虑把t3code发布为私有npm包。这样每个项目通过npm install company/t3code来引入版本管理也方便。但要注意私有包里的函数应该尽量通用不要包含特定项目的业务规则。我的经验是先在一个项目里沉淀等某个函数被两个以上项目需要时再考虑提取到私有包。否则过早抽象会导致包变得臃肿反而不好维护。7. 我在实际项目里踩过的坑第一个坑把有状态的逻辑放进了t3code。有一次我写了一个函数用来管理购物车的选中状态里面直接修改了一个模块级的变量。结果在多个组件同时调用时状态互相干扰出现了诡异的bug。排查了半天才发现是t3code函数不纯导致的。从那以后我定了一条规矩t3code函数里绝对不能出现let或var声明的模块级变量所有数据必须通过参数传入通过返回值传出。第二个坑过度抽象。有一段时间我痴迷于把任何重复的代码都抽成函数结果t3code里出现了一堆只有一行代码的函数比如getOrderId(order) { return order.id; }。这种函数不仅没有减少重复反而增加了调用层级让代码更难读。后来我给自己定了一个标准只有当一段逻辑被重复三次以上或者逻辑本身超过5行才值得抽成t3code函数。否则宁可让它重复。第三个坑忽略了函数的副作用。虽然t3code函数本身是纯的但调用方可能在调用前后做了有副作用的操作。比如一个函数计算出了新价格调用方直接把这个价格赋值给了Vue的响应式对象导致视图更新。这本身没问题但如果多个地方同时调用可能会触发多次不必要的渲染。我的解决办法是在调用t3code函数后用computed或watch来管理派生状态而不是手动赋值。这样Vue会自动处理依赖追踪和缓存。第四个坑测试覆盖了函数但没覆盖组合场景。单个t3code函数测试通过但组合起来用的时候因为参数传递顺序错误导致结果不对。后来我在测试里增加了组合函数的测试确保端到端的逻辑正确。另外对于组合函数我会用it.each写多组输入输出覆盖不同的业务场景。8. 最后再分享几个实用小技巧如果你刚开始尝试t3code可以从一个函数开始。不要想着一次性建好整个目录结构先找一个你最近写过的、重复了两次以上的业务逻辑把它抽出来放到t3code/下然后在原来的地方引用它。感受一下“改一处处处生效”的爽快。然后每次遇到类似的情况就重复这个过程。三个月后你会发现t3code已经成了项目里不可或缺的一部分。另一个技巧是给t3code函数起名时加上业务前缀。比如order_calculateTotal、user_canEdit。这样在编辑器里输入order_时所有订单相关的函数都会列出来方便查找。虽然下划线命名在JavaScript社区不常见但在t3code这种“工具箱”场景下它比驼峰命名更清晰。当然如果你用TypeScript可以用命名空间或模块来组织效果类似。还有定期清理。每季度花一个小时看看t3code里有没有不再使用的函数有没有可以合并的有没有注释过时的。保持t3code的精简它才能持续发挥价值。我见过太多项目工具库越积越大最后没人敢动因为不知道删了会不会影响其他地方。定期清理能避免这个问题。最后把t3code的实践写进团队的编码规范。比如规定“任何被两个以上组件使用的业务计算逻辑必须放在t3code里”。这样新加入的成员也能快速理解项目的代码组织方式。规范不需要很长几条核心原则就够了。关键是执行可以在代码审查时互相提醒。坚持一段时间就会形成习惯。