拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

模板代码模块化设计:从复制粘贴到参数化复用

模板代码模块化设计:从复制粘贴到参数化复用

接手过一个后台项目,三个管理页面加起来将近两千行代码,其中七成是同一套骨架:顶部标题栏、查询条件区、数据表格、分页条、删除确认弹窗。同事的做法很直接——复制粘贴,然后改接口名和列名。第一次改的时候挺爽,第二次也还行,等到产品经理说“把用户列表的删除改成软删除,订单列表也要改”的时候,我翻了三个页面、改了六处,最后还是漏掉了商品页。那一刻我意识到,“模板代码”这东西,如果不做模块化设计,迟早会把项目拖垮。

所谓模板代码模块化设计,就是把项目中反复出现的页面骨架、函数封装、算法范式、报表格式这些“样板代码”,按边界拆分、按层次组织、按参数复用,让它们像函数一样被调用,而不是像贴纸一样被复制。这篇文章我会结合实际的工程案例,聊聊模板代码为什么会失控、抽取前该算什么账、三层模板架构怎么落地、参数化设计有哪些规范,以及一次性实测重构的完整过程。适合正在写管理后台、维护脚手架、做代码生成器,或者经常跟模板引擎打交道的开发者参考。

1. 为什么模板代码会从“省事”变成“噩梦”

1.1 三种常见的模板代码形态

先说第一种形态:复制粘贴式代码块。这是最隐蔽也最危险的一种。它没有名字、没有边界、没有版本概念,散落在各个业务页面里。表面上看,复制一次很省时间,但每次粘贴都等于复制了一份“带病基因”——如果源页面后期修了bug,所有粘贴过的地方都得手动同步。我之前统计过一个项目,同一个“时间范围筛选器”代码块被粘贴了11次,其中有4次的时间格式化逻辑还是旧版,线上数据差了整整8小时。

第二种形态:模板引擎里的大一统模板。用Jinja2、Twig、Thymeleaf这类模板引擎的同学应该深有体会。一开始只是给列表页加一个搜索框,于是往模板里塞了一个if分支;后来又要支持批量操作,再加一个if;再后来要区分角色显示不同按钮,又加一个if。三个月后,一个模板文件七八百行,到处都是{% if %}和${condition ? ... : ...},改一个样式要小心翼翼,生怕动了一个分支影响另一条业务线。

第三种形态:脚手架和代码生成器里的整体骨架。这类模板通常以“整套项目模板”的形式存在,比如生成一个后台管理系统模板、一个Spring Boot项目模板。好处是开箱即用,坏处是业务代码一进去,骨架就和业务长在一起了。等你想升级公共组件时,发现根本没法单独替换——骨架和血肉已经粘连成一体。

1.2 失控的本质:边界错了

我一开始以为是代码写得太烂,后来拆了几轮才明白,问题不在代码质量,而在边界。

模板代码的失控,本质上是“变的”和“不变的”没有分开。一个页面里,标题、查询条件、表格列、按钮权限、接口地址都是变量,而页面布局、弹窗交互、分页逻辑、请求loading是常量。如果这些常量和变量混在同一个文件里、同一个代码块里,那么每次业务变化都得带着整个骨架一起动,明明只是想挪一张桌子,结果把整座房子拆了重盖。

打个比方:你去饭店点菜,菜单上写的不是“宫保鸡丁”,而是把鸡丁怎么切、花生米怎么炸、酱汁怎么调的完整过程全写上了。厨师每次做这道菜都要把一大段文字从头读一遍才能动手。而真正好的菜单,只会写“宫保鸡丁,微辣”——菜品拆解和烹饪过程已经标准化了,菜单只需要给出参数。模板代码模块化设计要做的,就是把“烹饪过程”抽出来变成标准模板,把“微辣”“免葱”这类变化量提炼成参数,让菜单短、让后厨稳。

2. 抽取前先算账:复用频率与变更频率决定一切

2.1 为什么不能“见重复就抽”

很多文章会让你把所有重复代码都抽成公共模块,但我的实际经验恰恰相反——盲目抽取比复制粘贴更可怕。

原因很简单:公共模块一旦被多处引用,它的改动就要做全量回归。你抽一个list-table组件出来,可能有八个页面在用,某次为了适配用户页,给表格加了一列操作按钮,结果发现消息列表页的按钮也被“顺带”改变了。这时候,你面对的不再是“三个页面手动改三遍”,而是“一个组件改了要回归八个页面”。如果这个模块本身没有被高频复用的价值,这样的抽象就是在给自己挖坑。

所以我一直有个很朴素的判断标准:同一个模板代码块,出现两次,忍住不抽;出现三次,开始认真考虑;到第四次,必须抽。

2.2 四象限判定法:一张表说清楚抽不抽

光看次数还不够,得结合两个关键维度:复用频率和变更频率。我用一张四象限表厘清思路:

维度变更频率低(半年≤2次)变更频率高(半年≥5次)
复用地(≥3处)抽成公共模块,尽量稳定,少放业务逻辑抽成可配置模块,参数化设计,把变化留给调用方
复用少(≤2处)保留原样,不抽,复制粘贴反而清晰千万别抽,抽了就成了唯一一个用公共模块的特殊页面

重点解释一下最容易被误判的两个格子。

第一格“高复用+低频变”是最理想的抽象对象,比如算法模板。像C++广搜模板、树状数组模板、快速排序代码,这类代码结构稳定、逻辑成熟、几乎不随业务变化,抽出来不仅一劳永逸,还能形成团队的“公共知识库”。我在刷题项目里就是把这些算法范式统一整理成了一个模板库,每道题引用对应的模板,再传入不同的状态定义和转移逻辑,写题速度提升了一截。

第二格“高复用+高频变”是工程里最常见的困境,比如后台列表页。几乎所有后台页面都用同一套表格交互,但每个页面的查询字段、表格列、按钮操作都在变。这种场景不能只抽一个死模板,必须把“变化的点”全部设计成参数或配置,让模板本身尽量不动。如果你把列表页抽出来之后,每次加查询条件都要改模板内部,那这个抽象是失败的——它只是把复制粘贴从“页面层”挪到了“模板层”,该痛苦的还是会痛苦。

2.3 怎么给复用和变更“算账”

算账不需要复杂的工具,靠现成的IDE和版本管理软件就能完成。

第一步是统计复用频率。我用IDE的全局搜索功能直接搜代码块的函数名或关键DOM结构,看在哪些文件出现过。更准确的办法是看版本控制系统的引用追踪,不过很多项目没有智能引用索引,直接用正则搜关键片段也够用。统计一下出现次数,如果≥3处,进入下一轮评估。

第二步是查变更记录。打开版本控制的文件历史,看这个代码块所在文件最近半年的提交次数。如果超过5次,说明它正处于高频变化期,抽的时候务必把变化点变成参数;如果只有一两次,说明它已经很稳定了,可以放心抽。

第三步是估算“改动传播半径”。每次改动这个模块,需要回归多少个调用方?如果调用方超过5个,而模块本身又不稳定,我建议先找个经验丰富的人一起review一下参数设计,避免抽出来就后悔。

想强调一点:算法模板和工程页面模板的算法完全不一样。算法模板追求的是“通用+无状态”,谁调用谁传参;工程页面模板追求的是“骨架稳定+配置驱动”。前者不会有产品经理来改需求,后者每周都可能被要求“加个导出按钮”。搞清楚你的模板属于哪种类型,再决定抽象策略,这是模块化设计的第一步。

3. 三层模板架构:原子、组合、装配

3.1 为什么是三层

模板模块化的核心是分层。最常见的层数是三层:原子模板层、业务组合层、场景装配层。

有人问为什么不是两层,我把两个管理页面的公共骨架抽成一个整页模板行不行?技术上可行,但你会遇到两个问题。第一是参数爆炸,一个页面模板要同时兼容列表、详情、表单、报表四种布局,参数得有四五十个,调用方传参的时候自己都看不明白。第二是复用粒度太大,订单页只想复用你的查询表单,结果必须把整个列表页模板拖进来,牵一发动全身。

那为什么又不是四层五层?层数越多,定位越细,但查找越难。三层是工程上比较舒服的折中:原子层管“最小零件”,组合层管“业务场景”,装配层管“最终页面长什么样”。每一层都有明确的职责,跨层引用会破坏边界,应该用规范去禁止。

3.2 一个可以直接参考的目录结构

三层架构落到项目里,我习惯这样组织模板目录:

templates/ ├── atoms/ # 原子模板层:不可再拆的最小界面元件 │ ├── search-input/ │ │ ├── index.html │ │ └── style.css │ ├── date-range/ │ ├── action-button/ │ └── confirm-modal/ ├── blocks/ # 业务组合层:按业务场景拼装原子模板 │ ├── list-filter/ # 查询条件组合 │ ├──>{% macro query_form(fields, action_url) %} <form method="get" action="{{ action_url }}"> {% for field in fields %} {% if field.type == 'input' %} <input type="text" name="{{ field.name }}" placeholder="{{ field.placeholder }}" value="{{ field.value or '' }}"> {% elif field.type == 'date_range' %} {% include 'atoms/date-range.html' with context %} {% elif field.type == 'select' %} <select name="{{ field.name }}"> {% for opt in field.options %} <option value="{{ opt.value }}">{{ opt.label }}</option> {% endfor %} </select> {% endif %} {% endfor %} <button type="submit">查询</button> </form> {% endmacro %}

调用方只需要传入一个fields配置列表:

[ { "type": "input", "name": "keyword", "placeholder": "请输入商品名称" }, { "type": "date_range", "name": "created_at" }, { "type": "select", "name": "status", "options": [ { "label": "全部", "value": "" }, { "label": "上架", "value": "1" }, { "label": "下架", "value": "0" } ] } ]

以后要新增一个查询条件,不需要动模板,只需要在配置数组里加一项。模板代码变成了一种“解释器”,业务的每次变化都体现在数据里,而不是体现在代码里。这一点很关键——模块化设计的最高境界,是让业务变化发生在数据层,而不是代码层。

4. 模板模块的参数化与命名规范

4.1 把模板模块当成一个函数来定义

我见过很多抽的所谓“公共模板”,其实就是把之前的HTML原样挪到一个新文件,里面该有的业务判断一个不少。这种模板抽了等于没抽,换汤不换药。

真正的参数化设计,是让模板模块像函数一样有清晰的“签名”—— 输入什么、输出什么、哪些点允许变化、哪些点禁止变化,在一开始就要定清楚。我把一套页面模板的“函数签名”写在文件头部,任何调用者一眼就能看懂能传什么参数:

{# 模板名: admin-list 后台列表页 参数: - title: 页面标题,必填 - columns: 表格列配置数组,必填 - filters: 查询条件配置数组,选填,默认 [] - api_url: 列表数据接口,必填 - row_actions: 行内操作按钮配置,选填 - batch_actions: 批量操作按钮配置,选填 - show_pagination: 是否显示分页,选填,默认 true 约定: - 不得在该模板内直接写死业务数据 - 所有可变量必须通过参数传入 #}

这样一来,模板模块的“接口”是显式的,不是靠人肉去阅读模板内部代码才猜得出来。我在团队里还定了一条规矩:凡是公共模板,头部必须有这样一份参数说明注释,否则视为不合格提交。

4.2 默认值优先,少写一堆if

参数化最容易犯的错,是模板内部疯狂堆if分支。看这个反面例子:

{% if show_add_button %} <button class="btn-add">新增</button> {% endif %} {% if show_export_button %} <button class="btn-export">导出</button> {% endif %}

表面上看灵活,实际上每个新需求都会在这里加一个分支,模板从30行膨胀到300行的路径就是这样走出来的。更好的做法是用“空值约定”替代“条件判断”,把按钮列表设计成数据:

{% for btn in buttons %} <button class="btn-{{ btn.style }}" >title: 用户管理 api_url: /api/users filters: - type: input name: keyword placeholder: 搜索用户名/手机号 - type: date_range name: created_at columns: - field: id label: 用户ID - field: username label: 用户名 - field: phone label: 手机号 - field: status label: 状态 render: status_tag - field: created_at label: 注册时间 row_actions: - label: 编辑 action: edit - label: 详情 action: detail batch_actions: - label: 批量删除 action: delete

第四步,删掉原先三个页面里的公共部分,只保留差异化代码和配置引用。页面主体变成一个很薄的壳:

{% include 'layouts/admin-list.html' with { "title": "用户管理", "api_url": "/api/users", "filters": [...], "columns": [...] } %}

重构后,我做了一次关键验证:把订单页的“退款按钮”和商品页的“上下架开关”通过row_actions配置保留下来,渲染结果与重构前完全一致,没有出现功能缺失。更惊喜的是,两周后产品提了个新需求“新增会员管理页”,我直接复制了一份配置、改了接口和字段,25行配置搞定,前后不到半小时。

5.3 重构过程中踩过的三个坑

这个重构能顺利落地,是因为我提前踩了几个坑,分享给你。

第一个坑是配置覆盖链过深。一开始我想设计一套配置继承机制,让页面B继承页面A的配置、再局部覆盖。看起来省事,但实际用起来极其痛苦——你看一个页面的最终配置,得先找到父配置、再找祖父配置、再叠加覆盖项,才能推算出页面真实长什么样。后来我改成“配置全部平铺”,每个页面写完整的配置,哪怕重复几行也无所谓。模板模块化要追求的是“页面文件变薄”,不是“配置继承链变深”,这个度要把握好。

第二个坑是手改生成后的代码。有次为了让一个页面显示一个特殊提示,同事直接改了渲染后的HTML,而不是改配置。结果模板升级后,那个页面被重新渲染,手动改的内容全部消失。这违背了单一数据源原则。模板模块化设计一旦落地,要有明确规矩:渲染产物是只读的,要改东西只能改配置或改模板,禁止直接动产物。

第三个坑是为了“灵活”把一切变成配置。我开始时恨不得把标题字号、边框颜色全做成配置项,后来发现模板参数越加越多,配置文件的阅读难度直线上升,甚至超过了直接写HTML的清晰度。及时止损后,我给自己定了个标准:如果一个配置项只有一两个页面在用,就不要做配置项,让它留在调用方页面里自己解决。配置项的数量要控制,只有超过一半的调用方需要的参数才有资格进入公共模板。

6. 模板模块化设计的隐性成本与防御手段

6.1 隐性成本往往被低估

模板模块化不是免费的,它的成本容易被第一次重构成功的喜悦掩盖。

第一个成本是调试定位变慢。以前一个页面逻辑写在一个文件里,报错行号一翻就找到了。抽象后,报错可能来自原子层、组合层或者装配层的上下文,排查链路拉长。我在重构后的头一个月里,几乎每天都得靠“渲染后注释提示”来定位——后面我会说这个防御手段。

第二个成本是学习门槛变高。新人接手项目,面对的是一堆模板文件和配置项,得先理解“三层架构”才能改动页面,初期产出效率会下降。我做过统计,新人在纯复制粘贴型项目里第一天就能干活,在模块化项目里通常需要3到5天的适应期。

第三个成本是公共模块的回归范围。这是最容易被忽略的。你改一行原子层代码,所有引用它的页面都在影响范围内,也许你只打算改用户页的按钮样式,结果营销页的落地按钮也被变了。所以公共模块的改动必须更加慎重,要走严格的评审和验证流程。

第四个成本是隐式依赖。比如组合层内部悄悄引用了一个不在参数说明里的全局变量,调用方完全不知情,运行时才发现某个页面数据为空。这类问题一旦出现,排查起来极耗时间。

6.2 几个有效的防御手段

针对这些隐性成本,我逐步沉淀了几条防御手段,现在都固化在团队的开发规范里了。

第一,渲染产物可追溯。我要求模板在渲染时,在关键区块输出一段HTML注释标记来源,比如:

<!-- [block:list-filter] 来自 templates/blocks/list-filter.html --> <div class="list-filter">...</div>

这样页面一旦出问题,按注释定位就能快速找到对应模块,调试时间至少缩短一半。

第二,渲染测试守住模块边界。给每个公共模板配一个固定输入的桩数据测试:给定一组参数配置,检查渲染结果是否包含预期的关键DOM节点,以及是否不包含被过滤掉的内容。不需要复杂的测试框架,写一组脚本遍历配置集合并比对输出就行。模板改了配置,测试不绿就说明有调用方受影响。

第三,公共模板变更必须过评审。不光是代码评审,还要评审“影响范围”。每次改动公共模板,提交信息里必须写清楚“本次修改会影响哪些页面”,哪怕只是改一个class名。

第四,定期清理无用参数。配置项和模板参数会像藤蔓一样越爬越多。我每个季度会翻一次公共模板的参数说明,凡是半年内没有调用方使用的参数,一律删除。参数也是负债,能还就还。

6.3 什么时候不要用这套方案

模板模块化设计不是银弹。如果是探索性原型、一次性活动页、或者一个总共只有三五个页面的小型站点,我建议你直接写业务代码,复制粘贴反而更快、更好懂。模块化是有启动成本的:设计模板边界、写参数说明、做渲染测试,这些投入需要足够的页面数量去摊薄。

我个人判断的标准很简单:当你发现同一个骨架已经重复粘贴到第三个及以上的页面,并且你预感到未来半年内它至少还要再变两三次的时候,才开始动工。在那之前,请忍住。模块化就像装修,提前动手叫设计,事后返工叫拆迁,时机比技术更重要。

最后分享一点个人体会:这套模板代码模块化方案,帮我重构过两个后台项目和一套代码生成器。最爽的不是重构完成的那一刻,而是接下来半年里,每来一个新页面需求,我只用写二十来行配置,把模板当积木拼一下就能交付。当然,我也吃过亏——在一套只有两个页面的内部工具上强行模块化,结果搭了三天的架子,后来产品取消了,白忙一场。所以那句话还是得再强调一次:模块化是手段,不是目的。你的目标永远是“让业务变化发生在数据层,让代码层稳稳当当”,记住这个目标,随时调整抽象边界,才不会被设计本身绊住。

返回列表