DIP 的核心定义只有两句话,记住这两句,就能抓住精髓:
高层模块(业务逻辑)不依赖低层模块(工具/库/接口实现),两者都依赖抽象;
抽象不依赖细节,细节(具体实现)依赖抽象。
用前端开发者能秒懂的大白话翻译:
业务代码(比如“获取用户信息”“提交订单”)不要直接依赖某个具体的请求工具(比如 axios),而是依赖一个“请求规范”(抽象);axios、fetch 这些具体的请求工具,都去实现这个“请求规范”。这样一来,换请求工具、改请求逻辑,业务代码完全不用动。
举个最直观的类比:你去餐厅吃饭(业务),不需要直接依赖“厨师A”“厨师B”(具体实现),只需要依赖“菜单”(抽象)——不管换哪个厨师,只要按照菜单做,你就能吃到想吃的菜。这里的“菜单”,就是 DIP 里的“抽象”。
先看我们平时最常写的请求代码,这种写法看似简单,实则隐藏着巨大的维护隐患。
假设我们用 axios 做请求工具,直接在业务代码里调用 axios,实现“用户相关接口”和“商品相关接口”:
npm install axios --save
# 或 yarn add axios
// api/userApi.js (用户相关接口,直接依赖 axios)
import axios from 'axios';
// 基础配置(写死在业务文件里)
axios.defaults.baseURL = 'https://api.dev.com';
axios.defaults.timeout = 5000;
// 获取用户信息
export const getUserInfo = async (userId) => {
try {
const res = await axios.get(`/user/${userId}`);
return res.data;
} catch (err) {
console.error('获取用户信息失败:', err);
throw err;
}
};
// 提交用户信息
export const submitUserInfo = async (userData) => {
try {
const res = await axios.post('/user', userData);
return res.data;
} catch (err) {
console.error('提交用户信息失败:', err);
throw err;
}
};
// api/goodsApi.js (商品相关接口,同样直接依赖 axios)
import axios from 'axios';
// 这里又要重复配置 baseURL,或者依赖上面的配置,耦合度极高
axios.defaults.baseURL = 'https://api.dev.com';
// 获取商品列表
export const getGoodsList = async (params) => {
try {
const res = await axios.get('/goods', { params });
return res.data;
} catch (err) {
console.error('获取商品列表失败:', err);
throw err;
}
};
耦合度极高:所有业务代码都直接依赖 axios,一旦需要替换请求工具(比如换成 fetch、umi-request),所有 api 文件都要逐一修改,工作量巨大;
配置混乱:baseURL、超时时间等配置分散在各个文件,修改环境(比如从开发环境切换到测试环境)时,需要到处找配置;
测试困难:单元测试时,无法模拟 axios 请求,只能真实调用接口,测试效率低、易出错;
扩展性差:如果需要添加请求拦截器(比如统一加 token)、响应拦截器(比如统一处理错误),需要在所有 api 文件中修改,重复劳动。
这就是违反依赖倒置原则的后果——高层业务模块(userApi、goodsApi)直接依赖低层模块(axios),导致代码僵硬、难以维护。
按照 DIP 的核心思想,我们分 4 步改造代码,实现“业务与请求工具解耦”,让代码更灵活、更可维护。
核心思路:先定义“请求抽象接口”(规范),再实现具体的请求工具(axios/fetch),最后让业务代码依赖抽象接口,而非具体工具。
抽象接口是 DIP 的核心,它定义了请求的“规范”——不管用什么工具,都必须实现这些方法,保证业务代码能“无缝调用”。
我们创建一个抽象类,定义 HTTP 请求的核心方法(get、post、put、delete),以及基础配置的规范:
// http/HttpClientInterface.js (抽象接口,定义规范)
class HttpClientInterface {
// 构造函数:接收基础配置(baseURL、timeout 等)
constructor(config = {}) {
this.baseURL = config.baseURL || '';
this.timeout = config.timeout || 5000;
}
// 抽象方法:get 请求(必须实现)
async get(url, params = {}, headers = {}) {
throw new Error('必须实现 get 方法');
}
// 抽象方法:post 请求(必须实现)
async post(url, data = {}, headers = {}) {
throw new Error('必须实现 post 方法');
}
// 抽象方法:put 请求(必须实现)
async put(url, data = {}, headers = {}) {
throw new Error('必须实现 put 方法');
}
// 抽象方法:delete 请求(必须实现)
async delete(url, params = {}, headers = {}) {
throw new Error('必须实现 delete 方法');
}
}
export default HttpClientInterface;
说明:这个抽象类不包含任何具体实现,只定义“必须有什么方法”“方法的参数和返回值规范”。任何具体的请求工具(axios、fetch),都必须继承这个类,实现所有抽象方法——这就是“细节依赖抽象”。
我们基于抽象接口,实现一个 axios 版本的请求工具,让它符合“请求规范”。这里可以统一处理配置、拦截器、错误等,避免重复代码。
// http/AxiosClient.js (具体实现,依赖抽象接口)
import axios from 'axios';
import HttpClientInterface from './HttpClientInterface';
class AxiosClient extends HttpClientInterface {
// 重写构造函数,初始化 axios 实例
constructor(config = {}) {
// 调用父类(抽象接口)的构造函数,继承基础配置
super(config);
// 初始化 axios 实例,应用基础配置
this.instance = axios.create({
baseURL: this.baseURL,
timeout: this.timeout,
headers: {
'Content-Type': 'application/json',
...config.headers,
},
});
// 统一请求拦截器(比如添加 token)
this.instance.interceptors.request.use(
(config) => {
// 示例:从本地存储获取 token,添加到请求头
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(err) => Promise.reject(err)
);
// 统一响应拦截器(比如统一处理错误)
this.instance.interceptors.response.use(
(response) => response.data, // 直接返回响应体,简化业务代码
(err) => {
// 统一错误处理
let errorMsg = '请求失败';
if (err.response) {
// 服务器返回错误
switch (err.response.status) {
case 401:
errorMsg = '未授权,请登录';
// 示例:跳转登录页
window.location.href = '/login';
break;
case 403:
errorMsg = '权限不足';
break;
case 404:
errorMsg = '接口不存在';
break;
case 500:
errorMsg = '服务器内部错误';
break;
default:
errorMsg = err.response.data?.message || errorMsg;
}
} else if (err.request) {
// 请求发送失败(比如网络错误)
errorMsg = '网络异常,请检查网络连接';
}
console.error(errorMsg, err);
return Promise.reject(new Error(errorMsg));
}
);
}
// 实现抽象方法:get 请求
async get(url, params = {}, headers = {}) {
return this.instance.get(url, { params, headers });
}
// 实现抽象方法:post 请求
async post(url, data = {}, headers = {}) {
return this.instance.post(url, data, { headers });
}
// 实现抽象方法:put 请求
async put(url, data = {}, headers = {}) {
return this.instance.put(url, data, { headers });
}
// 实现抽象方法:delete 请求
async delete(url, params = {}, headers = {}) {
return this.instance.delete(url, { params, headers });
}
}
export default AxiosClient;
为了体现“换工具不影响业务”,我们再基于抽象接口,实现一个 fetch 版本的请求工具。这样一来,不管用 axios 还是 fetch,业务代码都不用改。
// http/FetchClient.js (另一个具体实现,同样依赖抽象接口)
import HttpClientInterface from './HttpClientInterface';
class FetchClient extends HttpClientInterface {
constructor(config = {}) {
super(config);
// 继承父类的 baseURL、timeout 等配置
}
// 工具方法:处理请求参数(get 请求拼接 params)
_handleParams(url, params = {}) {
const paramStr = Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
return paramStr ? `${url}?${paramStr}` : url;
}
// 工具方法:处理 fetch 错误
_handleError(response) {
if (!response.ok) {
let errorMsg = '请求失败';
switch (response.status) {
case 401:
errorMsg = '未授权,请登录';
window.location.href = '/login';
break;
case 403:
errorMsg = '权限不足';
break;
case 404:
errorMsg = '接口不存在';
break;
case 500:
errorMsg = '服务器内部错误';
break;
default:
errorMsg = `请求错误:${response.status}`;
}
throw new Error(errorMsg);
}
return response.json();
}
// 实现抽象方法:get 请求
async get(url, params = {}, headers = {}) {
const fullUrl = this._handleParams(`${this.baseURL}${url}`, params);
return fetch(fullUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...headers,
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
timeout: this.timeout,
}).then(this._handleError);
}
// 实现抽象方法:post 请求
async post(url, data = {}, headers = {}) {
return fetch(`${this.baseURL}${url}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers,
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify(data),
timeout: this.timeout,
}).then(this._handleError);
}
// 实现抽象方法:put 请求
async put(url, data = {}, headers = {}) {
return fetch(`${this.baseURL}${url}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...headers,
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify(data),
timeout: this.timeout,
}).then(this._handleError);
}
// 实现抽象方法:delete 请求
async delete(url, params = {}, headers = {}) {
const fullUrl = this._handleParams(`${this.baseURL}${url}`, params);
return fetch(fullUrl, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
...headers,
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
timeout: this.timeout,
}).then(this._handleError);
}
}
export default FetchClient;
这是最关键的一步:业务代码(userApi、goodsApi)不再依赖 axios 或 fetch,而是依赖我们定义的抽象接口(HttpClientInterface)。通过“注入”具体的请求工具,实现业务与工具的解耦。
首先,创建一个请求工具的“工厂”,统一管理请求实例,方便切换工具和环境:
// http/httpClientFactory.js (请求工具工厂,统一创建实例)
import AxiosClient from './AxiosClient';
import FetchClient from './FetchClient';
// 环境配置(可根据 process.env.NODE_ENV 自动切换)
const envConfig = {
development: {
baseURL: 'https://api.dev.com',
timeout: 5000,
},
test: {
baseURL: 'https://api.test.com',
timeout: 5000,
},
production: {
baseURL: 'https://api.prod.com',
timeout: 8000,
},
};
// 获取当前环境配置
const currentConfig = envConfig[process.env.NODE_ENV || 'development'];
// 工厂函数:创建请求实例(可切换 axios/fetch)
export const createHttpClient = (type = 'axios') => {
switch (type) {
case 'axios':
return new AxiosClient(currentConfig);
case 'fetch':
return new FetchClient(currentConfig);
default:
throw new Error('不支持的请求工具类型');
}
};
// 导出默认请求实例(默认用 axios)
export default createHttpClient('axios');
然后,改造业务代码,让它依赖抽象接口,而非具体工具:
// api/userApi.js (业务代码,依赖抽象接口)
import httpClient from '../http/httpClientFactory';
// 获取用户信息(只依赖抽象接口的 get 方法,不管底层是 axios 还是 fetch)
export const getUserInfo = async (userId) => {
return httpClient.get(`/user/${userId}`);
};
// 提交用户信息(只依赖抽象接口的 post 方法)
export const submitUserInfo = async (userData) => {
return httpClient.post('/user', userData);
};
// api/goodsApi.js (业务代码,同样依赖抽象接口)
import httpClient from '../http/httpClientFactory';
// 获取商品列表
export const getGoodsList = async (params) => {
return httpClient.get('/goods', params);
};
// 更新商品信息
export const updateGoods = async (goodsId, goodsData) => {
return httpClient.put(`/goods/${goodsId}`, goodsData);
};
// 删除商品
export const deleteGoods = async (goodsId) => {
return httpClient.delete(`/goods/${goodsId}`);
};
对比之前的坏写法,遵循 DIP 改造后的代码,有以下 4 个核心优势,也是我们学习 DIP 的意义所在:
想把 axios 换成 fetch?只需要修改 `httpClientFactory.js` 里的工厂函数,把 `createHttpClient('axios')` 改成 `createHttpClient('fetch')`,所有业务代码(userApi、goodsApi)完全不用动——因为业务代码只依赖抽象接口,不关心底层用什么工具。
// 切换请求工具,只需改这一行
export default createHttpClient('fetch');
baseURL、超时时间、请求/响应拦截器、错误处理,都集中在具体的请求工具(AxiosClient、FetchClient)里,不用分散在各个业务文件。修改环境配置、添加拦截器,只需要改一个地方,避免重复劳动。
单元测试时,我们可以创建一个“模拟请求工具”,继承抽象接口,模拟请求的返回结果,不用真实调用接口。比如:
// 测试用的模拟请求工具
class MockHttpClient extends HttpClientInterface {
async get(url, params = {}) {
// 模拟返回用户信息
if (url.includes('/user/')) {
return { code: 200, data: { id: 1, name: '测试用户' }, message: 'success' };
}
// 模拟返回商品列表
if (url.includes('/goods')) {
return { code: 200, data: [{ id: 1, name: '测试商品' }], message: 'success' };
}
}
// 其他方法同理,模拟返回
async post() { /* 模拟返回 */ }
}
// 测试时,注入模拟工具
const mockHttpClient = new MockHttpClient();
// 替换业务代码里的 httpClient,即可进行单元测试
以后如果需要用新的请求工具(比如 umi-request、taro-request),只需要新建一个类,继承 `HttpClientInterface`,实现所有抽象方法,再在工厂函数里添加对应的类型,就能无缝接入业务,不用修改任何业务代码。
除了 HTTP 请求封装,DIP 在前端还有很多实用场景,比如:
日志工具封装(比如切换 console 日志、服务器日志、第三方日志库);
存储工具封装(切换 localStorage、sessionStorage、IndexedDB);
UI 组件封装(比如统一按钮、输入框的接口,切换不同 UI 库的组件);
状态管理封装(比如切换 Vuex、Pinia、Zustand)。
核心思路都是一样的:先定义抽象接口,再实现具体细节,最后让高层模块依赖抽象。
很多前端开发者觉得“设计模式是后端的东西,前端用不上”,但实际上,DIP 这样的设计原则,能帮我们解决日常开发中“代码僵硬、难以维护”的痛点。
回到 HTTP 请求的案例,我们再梳理一遍 DIP 的落地流程:
定义抽象接口(HttpClientInterface):规范请求方法和配置;
实现具体细节(AxiosClient、FetchClient):遵循抽象接口,处理具体逻辑;
高层模块(业务代码)依赖抽象:通过工厂注入具体实现,业务与细节解耦。
这样写出来的代码,不仅灵活、可维护,还能应对需求的频繁变化——这就是设计模式的价值。
最后提醒一句:DIP 不是“过度设计”,而是“提前规避风险”。对于小型项目,可能直接写 axios 也能应付,但对于中大型项目、长期维护的项目,遵循 DIP 能让你少走很多弯路,让代码更有“生命力”。
// httpClientInterface.js
class HttpClient {
get() {}
}
// AxiosClient
class AxiosClient extends HttpClient {
get(url) {
return axios.get(url);
}
}
// FetchClient
class FetchClient extends HttpClient {
get(url) {
return fetch(url).then(res => res.json());
}
}
// 业务
class UserApi {
constructor(httpClient) {
this.http = httpClient;
}
getUser() {
return this.http.get('/user');
}
}