模板方法模式可能是你最熟悉却最不自知的设计模式。它不像策略模式那样需要刻意设计,也不像观察者模式那样需要维护订阅列表。它简单到几乎透明------定义一个流程骨架,让子类填充细节------但它的思想贯穿了几乎所有现代前端框架。
然而,现在 React 开发中大家都用 Hooks 了,Vue 也全面转向 Composition API。相对于传统的继承方式,Hooks 体现的是"组合代替继承"的思想------将通用逻辑抽离到函数级别,模块粒度适中,控制起来比继承更加灵活。但这是否意味着模板方法模式过时了?答案是否定的------它只是换了一副面孔,从"继承"变成了"组合"。
本文从 Vue/React 生命周期钩子、通用请求封装、Vue 3 composable 三个真实场景出发,给出模板方法模式的完整实现,对比它与策略模式的本质区别,并讨论模板方法模式在现代前端中从继承到组合的演进。

一、从一个"流程固定"的需求说起
假设你在写一个数据请求模块,每次调用接口都需要经过相同的步骤:添加 Token → 显示 Loading → 发送请求 → 解析响应 → 隐藏 Loading → 错误处理。
最直接的写法是每次都把这几步写一遍,结果就是每个接口函数里都充满了重复的 Loading 控制和 try-catch。
模板方法模式要解决的问题正是:把固定的流程在父类中定义好,把可变的步骤留给子类实现。 子类不需要关心 Loading 什么时候显示、错误什么时候捕获------这些都由父类统一管理。
二、模板方法模式的核心思想
在父类中定义一个算法的骨架(流程),将某些步骤延迟到子类中实现。子类可以重写特定步骤,但不能改变算法的整体结构。
关键点:
- 固定流程:算法的执行顺序在父类中确定,子类无法改变。
- 钩子方法:父类提供可选的钩子方法,子类可以选择性地覆盖。
- 复用与扩展:公共逻辑在父类中复用,差异逻辑在子类中扩展。
如果面试官追问"和策略模式有什么区别",可以回答:模板方法关注"流程的骨架"------步骤顺序固定,部分步骤可定制;策略模式关注"算法的替换"------整个算法可以完全替换。 模板方法是"填空",策略是"换人"。
三、场景1:Vue/React 生命周期钩子------框架内置的模板方法
在前端框架中,模板方法模式最经典的体现就是组件的生命周期。框架定义了组件从创建到销毁的完整流程,开发者在特定的钩子函数中填写自己的逻辑。
以 Vue 3 为例,Vue 内部定义了一个固定的生命周期流程:
javascript
// Vue 内部定义的"模板"(简化示意)
class ComponentInstance {
// 模板方法:固定的生命周期流程
mount() {
this.beforeCreate(); // 钩子1
this.created(); // 钩子2
this.beforeMount(); // 钩子3
this.render(); // 核心渲染(框架实现)
this.mounted(); // 钩子4:你在 mounted 里写的逻辑
this.beforeUpdate(); // 钩子5
this.updated(); // 钩子6
}
// 默认空钩子,开发者覆盖它们
beforeCreate() {}
created() {}
beforeMount() {}
mounted() {} // 你在这里写初始化逻辑
beforeUpdate() {}
updated() {}
}
使用时,你只需要在 mounted 里填写自己的初始化代码:
vue
<script setup>
import { onMounted } from 'vue';
onMounted(() => {
// 你的初始化代码:请求数据、绑定事件等
fetchData();
});
</script>
React 类组件同理------componentDidMount、componentDidUpdate、componentWillUnmount 都是模板方法中的钩子:
jsx
class MyComponent extends React.Component {
componentDidMount() {
// 你的初始化逻辑
}
componentDidUpdate(prevProps) {
// 你的更新逻辑
}
componentWillUnmount() {
// 你的清理逻辑
}
render() {
// 必须实现的步骤
}
}
Vue 的 mounted、React 的 componentDidMount 或 useEffect,本质上都是模板方法模式中的钩子。框架定义了组件的完整生命周期流程,开发者只负责在特定的钩子里填写自己的逻辑。开发者不能改变生命周期的执行顺序------mounted 一定在 created 之后执行------但可以决定在每个钩子时做什么。
四、场景2:通用请求封装------固定的请求流程
在项目中,所有接口调用都需要经过相似的流程:添加 Token → 发送请求 → 解析响应 → 错误处理 → 隐藏 Loading。模板方法模式可以将这个固定流程封装到基类中:
javascript
// 基类:定义请求流程的骨架
class BaseAPI {
// 模板方法:固定的请求流程
async request(url, options = {}) {
this.beforeRequest(options); // 步骤1:添加请求头、Token 等
try {
const response = await fetch(url, options);
const data = await this.parseResponse(response); // 步骤2:解析响应
this.onSuccess(data); // 步骤3:成功处理
return data;
} catch (error) {
this.onError(error); // 步骤4:错误处理
throw error;
} finally {
this.onFinally(); // 步骤5:收尾工作(隐藏 loading 等)
}
}
// 钩子方法:子类可覆盖
beforeRequest(options) {
options.headers = options.headers || {};
options.headers['Content-Type'] = 'application/json';
}
async parseResponse(response) {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
onSuccess(data) {} // 子类覆盖:成功后的业务处理
onError(error) {} // 子类覆盖:统一的错误提示
onFinally() {} // 子类覆盖:清理工作
}
// 子类:实现具体的接口调用
class UserAPI extends BaseAPI {
async getUser(id) {
return this.request(`/api/users/${id}`);
}
async updateUser(id, data) {
return this.request(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
});
}
// 覆盖钩子:添加统一的成功日志
onSuccess(data) {
console.log('请求成功:', data);
}
// 覆盖钩子:统一错误提示
onError(error) {
message.error(error.message || '请求失败');
}
}
模板方法模式让所有 API 请求的流程在基类中固定下来,子类只需要关注具体的接口和个性化钩子。如果某天需要统一添加 Token 刷新逻辑,只需要改基类的 beforeRequest 方法,所有子类自动生效。
五、场景3:Vue 3 自定义 Hook 的模板流程
Vue 3 的 composable 函数虽然没有继承关系,但同样体现了模板方法模式的思想。封装一个通用的数据加载 Hook,固定的状态管理逻辑在 Hook 内部定义,具体的请求逻辑由调用方传入:
typescript
// 通用数据加载模板
export function useDataLoader<T>(fetcher: () => Promise<T>) {
const data = ref<T | null>(null);
const loading = ref(false);
const error = ref<Error | null>(null);
// 模板方法:固定的加载流程
const execute = async () => {
loading.value = true;
error.value = null;
try {
data.value = await fetcher(); // 具体的数据获取逻辑由调用方提供
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
};
return { data, loading, error, execute };
}
// 使用:调用方只需提供 fetcher 函数
const { data: users, loading, execute } = useDataLoader(
() => fetch('/api/users').then(r => r.json())
);
onMounted(() => execute());
useDataLoader 定义了加载数据的固定流程------设置 loading、请求数据、处理结果、finally 收尾。调用方只需要传入一个具体的 fetcher 函数。这和模板方法模式的思想完全一致:流程固定,具体步骤可定制。只是 Vue 3 用组合替代了继承。
六、从继承到组合------模板方法模式在现代前端的演进
为什么类继承式模板方法淡出了?
传统模板方法模式依赖继承。但在 Hooks 时代,这种写法暴露了三个问题:
-
灵活性差 :子类只能覆盖父类预留的钩子,不能改变流程的执行顺序。如果某个接口不需要
onFinally,或者想在beforeRequest之前做点别的,就很别扭。 -
组合困难:一个类只能继承一个父类。如果你想同时复用"日志记录"、"缓存处理"、"错误重试"这些能力,用继承几乎做不到,而组合可以随意拼接。
-
与现代框架理念不符 :React 的 Hooks 和 Vue 的 Composables 都强调组合优于继承------把可复用的逻辑抽成独立函数,哪里需要就组合哪里,而不是通过继承链来层层叠加。
模板方法的思想并没有消失------它变成了"组合式模板"
模板方法模式的精髓不是"继承",而是"固定流程 + 可替换的步骤"。 这个思想在 Hooks 时代以更灵活的方式延续着。
我们用一个组合式的 useRequest Hook 来替代前面的 BaseAPI 类:
typescript
// hooks/useRequest.ts ------ 组合式模板方法
interface UseRequestOptions<T> {
fetcher: () => Promise<T>;
onBefore?: () => void;
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
onFinally?: () => void;
}
export function useRequest<T>(options: UseRequestOptions<T>) {
const { fetcher, onBefore, onSuccess, onError, onFinally } = options;
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const execute = async () => {
onBefore?.(); // 钩子1:前置处理
setLoading(true);
setError(null);
try {
const result = await fetcher(); // 核心步骤:由调用方提供
setData(result);
onSuccess?.(result); // 钩子2:成功回调
} catch (err) {
const e = err as Error;
setError(e);
onError?.(e); // 钩子3:错误回调
} finally {
setLoading(false);
onFinally?.(); // 钩子4:收尾回调
}
};
return { data, loading, error, execute };
}
使用方式------调用方用组合的方式传入钩子函数:
tsx
function UserList() {
const { data: users, loading, execute } = useRequest({
fetcher: () => fetch('/api/users').then(r => r.json()),
onSuccess: (data) => message.success(`加载了 ${data.length} 条数据`),
onError: (err) => message.error(err.message),
});
useEffect(() => { execute(); }, []);
if (loading) return <Spin />;
return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
模板方法思想在这里的体现:
useRequest的execute函数定义了固定的执行流程:前置钩子 → Loading → 请求 → 成功/失败回调 → Finally 收尾。- 这个流程是固定的,调用方不能改变
try/catch/finally的执行顺序。 - 但每一步的具体行为 由调用方通过传入的函数(
fetcher、onSuccess、onError等)来定制。
这和类继承式模板方法的本质完全一致------流程固定,步骤可替换 。区别只在于:传统模板方法用 extends(继承) 实现,组合式模板用 参数传入(组合) 实现。
模板方法思想在现代框架中的其他体现
你每天都在用模板方法模式的思想,只是不需要自己写类来继承。
React 的生命周期 :React 组件的渲染流程是固定的------挂载、更新、卸载------但你可以在 useEffect、useLayoutEffect 等钩子中插入自定义逻辑。React 定义了"什么时候执行"(模板方法),你决定了"执行什么"(钩子内容)。
Webpack 的 Plugin 和 Loader:Webpack 的构建流程是固定的(读取文件 → 解析 → 转换 → 输出),但你可以在特定阶段通过 Plugin 和 Loader 插入自己的处理逻辑。
路由守卫 :Vue Router 的 beforeEach、React Router 的路由配置中的鉴权逻辑,本质都是"框架定义了路由跳转的固定流程,开发者在特定节点插入校验逻辑"。
什么时候还是用传统模板方法(类继承)?
组合式模板虽然灵活,但继承式模板方法在某些场景下仍然有价值:
-
需要强制约束流程时:继承式模板方法通过父类控制流程,子类无法绕过。组合式中,调用方理论上可以不传某些钩子,约束力稍弱。
-
框架/库的底层设计:Vue、React、Webpack 这些框架内部依然大量使用继承来定义基础类,因为框架需要强约束,不能依赖使用者"自觉"传参。
-
与现有面向对象代码集成:如果你维护的是一套旧版类组件,或者使用了 NestJS 这类重度依赖装饰器和继承的框架,传统的模板方法模式仍然是首选。
八、总结
模板方法模式是现代前端中最"透明"的设计模式。它的核心价值在于固定流程、开放扩展:
| 场景 | 模板定义者 | 步骤填充者 | 实现方式 |
|---|---|---|---|
| Vue 生命周期 | Vue 框架(mount 流程) |
开发者(mounted 钩子) |
继承 / 组合 |
| React 类组件 | React 框架(渲染流程) | 开发者(componentDidMount 等) |
继承 |
| 通用请求封装(类) | BaseAPI(request 方法) |
子类(onSuccess、onError) |
继承 |
| 数据加载 Hook | useDataLoader(execute 流程) |
调用方(fetcher 函数) |
组合 |
| useRequest Hook | execute 流程 |
调用方(钩子函数参数) | 组合 |
模板方法模式让"流程"和"细节"分离。当你发现自己在多个地方写着结构相似但内容不同的代码时,问自己一句:这个流程能固定下来,只让调用方填空吗? 如果可以,模板方法模式就是你需要的答案------至于用继承还是组合来实现,取决于你的项目风格和约束需求。在函数式编程成为主流的今天,组合式模板方法往往是更灵活、更现代的选择。