在现代前端应用中,与后端服务的 HTTP 通信是项目的命脉。我们频繁地发起请求、处理响应。但如果每个请求都需要手动处理通用逻辑(如添加 token、错误处理),代码将变得冗余、难以维护。这时,拦截器便应运而生,它如同我们与服务器之间的"智能关卡",赋予了我们在请求发送前和响应返回后执行自定义逻辑的强大能力。
1. 什么是拦截器?为什么需要它?
拦截器,顾名思义,是在 HTTP 请求或响应的传输过程中"拦截"它们,并在其被 then 或 catch 处理之前,执行一段特定的代码。
大多数主流 HTTP 客户端库,如 Axios,都内置了拦截器机制。它分为两种:
- 请求拦截器:在请求发送到服务器之前执行。
- 响应拦截器:在服务器返回响应,但在 Promise 的 then 或 catch 处理之前执行。
一个没有拦截器的痛点场景:
想象一下,你的应用中每个 API 请求都需要:
- 携带一个身份验证令牌。
- 显示一个全局的加载动画。
- 统一处理各种 HTTP 错误码(如 401 未授权、500 服务器错误)。
你的代码可能会是这样:
javascript
// 请求 A
function fetchUserA() {
showLoading();
const token = localStorage.getItem('token');
axios.get('/api/user/a', { headers: { Authorization: `Bearer ${token}` } })
.then(response => {
// ...处理成功逻辑
})
.catch(error => {
if (error.response?.status === 401) {
redirectToLogin();
} else if (error.response?.status === 500) {
showError('服务器错误');
} else {
showError('网络异常');
}
})
.finally(() => {
hideLoading();
});
}
// 请求 B
function fetchUserB() {
showLoading();
const token = localStorage.getItem('token');
axios.get('/api/user/b', { headers: { Authorization: `Bearer ${token}` } })
.then(response => {
// ...处理成功逻辑
})
.catch(error => {
// ...完全相同的错误处理逻辑
})
.finally(() => {
hideLoading();
});
}
这简直是维护的灾难!代码高度重复,逻辑耦合严重。而拦截器,正是解决这类问题的"银弹"。
2. 拦截器核心应用场景
让我们看看拦截器如何优雅地解决上述问题。
场景一:请求拦截器 ------ 统一处理认证与信息
请求拦截器最常见的用途是自动添加认证信息。
javascript
// src/utils/request.js
import axios from 'axios';
const service = axios.create({ baseURL: '/api' });
// 添加请求拦截器
service.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么
const token = localStorage.getItem('token');
if (token) {
// 如果 token 存在,则统一在请求头中添加 Authorization 字段
config.headers.Authorization = `Bearer ${token}`;
}
// 可以在这里添加其他通用信息,如请求ID、时间戳等
// config.headers['X-Request-ID'] = generateUUID();
return config; // 必须返回 config,否则请求将无法发送
},
(error) => {
// 对请求错误做些什么
console.error('Request Error:', error);
return Promise.reject(error);
}
);
现在,所有通过这个 service 实例发起的请求,都会自动带上 Authorization 头,业务代码无需再关心此事。
场景二:响应拦截器 ------ 统一处理响应与错误
响应拦截器是处理通用逻辑的另一个关键环节,主要用于 数据格式化 和 错误处理。
javascript
// src/utils/request.js (接上文)
// 添加响应拦截器
service.interceptors.response.use(
(response) => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么,例如:直接返回我们关心的 data 部分
const res = response.data;
// 假设后端约定,所有成功响应的 code 都是 0
if (res.code !== 0) {
// 如果 code 不是 0,说明业务逻辑上存在错误
showError(res.message || '业务错误');
return Promise.reject(new Error(res.message || 'Error'));
} else {
// 返回真正的业务数据
return res.data;
}
},
(error) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
if (error.response) {
switch (error.response.status) {
case 401:
// 未授权,token 失效或过期
showError('登录已过期,请重新登录');
// 清除本地 token 并跳转到登录页
localStorage.removeItem('token');
redirectToLogin();
break;
case 403:
showError('没有权限访问');
break;
case 404:
showError('请求的资源不存在');
break;
case 500:
showError('服务器内部错误');
break;
default:
showError(`请求失败: ${error.response.status}`);
}
} else if (error.request) {
// 请求已发出,但没有收到响应
showError('网络连接异常,请检查网络');
} else {
// 在设置请求时触发了错误
showError('请求配置错误');
}
return Promise.reject(error); // 将错误继续传递下去,方便业务层做特定处理
}
);
经过这样的配置,我们的业务代码变得异常简洁:
javascript
// 简洁的业务代码
import request from '@/utils/request';
export function fetchUserA() {
return request.get('/user/a'); // 自动携带token,自动处理错误,自动返回 data
}
export function fetchUserB() {
return request.get('/user/b');
}
3. 企业级开发中的拦截器最佳实践
在企业级项目中,拦截器的使用需要更加规范和健壮。以下是几条重要的最佳实践。
实践一:封装统一的请求模块
❗❗❗永远不要在组件中直接使用 axios!应该创建一个统一的请求模块(如 src/utils/request.js),在该模块中创建 axios 实例并配置所有拦截器。所有业务代码都应通过导入这个模块来发起请求。
这样做的好处是:
- 统一配置:所有 baseURL、timeout、拦截器逻辑都集中在一处。
- 易于替换:未来如果想从 Axios 迁移到 fetch 或其他库,只需修改这一个文件,而不用改动全业务代码。
- 职责分离:将网络通信的底层逻辑与业务逻辑解耦。
实践二:Token 刷新与无感刷新
在企业应用中,Token(尤其是 JWT)有过期时间。当 Token 过期时,用户会频繁被踢下线,体验极差。拦截器可以实现 无感刷新 Token。
核心流程:
- 响应拦截器捕获到 401 错误。
- 判断是否为 Token 过期(而非无效)。
- 如果是,则发起一个刷新 Token 的请求到后端(使用 refresh_token )。
- 如果刷新成功,将新的 access_token 存入本地存储,并重新发送刚才失败的请求。
- 如果刷新失败,则说明用户需要重新登录,引导其跳转。
关键点:
防止并发刷新:当多个请求同时返回 401 时,应确保只发送一个刷新请求。可以使用一个标志位(如 isRefreshing )和一个待处理请求队列来实现。
失败处理:刷新 Token 的请求本身也可能失败,需要有兜底逻辑(如强制登出)。
实践三:取消重复请求
在某些场景下,用户快速点击按钮可能会发起多个完全相同的请求,造成服务器压力和数据混乱。拦截器可以帮助我们取消正在进行的重复请求。
实现思路:
- 在请求拦截器中,为每个请求生成一个唯一的 key (如 method + url + params )。
- 维护一个 Map 或 Object 来存储每个 key 对应的 CancelToken 。
- 当新请求进来时,检查其 key 是否已存在于 Map 中。
- 如果存在,则取消上一次的请求,并用新的 CancelToken 覆盖它。
- 请求完成后(无论成功失败),从 Map 中移除该 key 。
Axios 的 CancelToken (在新版本中推荐使用 AbortController )是实现此功能的关键。
实践四:请求/响应日志与监控
在开发和生产环境中,对请求 和响应进行日志记录是排查问题的关键。
- 开发环境:可以在拦截器中 console.log 请求的 config 和响应的 data ,方便调试。
- 生产环境:可以将请求失败、API 耗时过长等关键信息上报到监控系统(如 Sentry、Fundebug),帮助团队快速发现和定位线上问题。
javascript
// 在响应拦截器中添加日志
service.interceptors.response.use(
response => {
// ...其他逻辑
const duration = Date.now() - response.config.metadata.startTime;
if (duration > 3000) { // 监控慢请求
monitor.logSlowRequest(response.config.url, duration);
}
return response.data;
},
error => {
// ...错误处理
monitor.logApiError(error); // 上报 API 错误
return Promise.reject(error);
}
);
// 在请求拦截器中记录开始时间
service.interceptors.request.use(config => {
config.metadata = { startTime: Date.now() };
return config;
});
4. 🪄手把手教你写响应拦截器
我将用最直白的话,手把手教你如何让根据不同情况写响应拦截器。
想象一下,响应拦截器 就像一个"快递签收检查员"。
- 快递员(后端服务器)把包裹(HTTP响应)送到你家门口。
- 检查员(响应拦截器)先帮你签收,然后打开包裹检查。
检查结果分两种:
- 包裹完好,东西是你想要的(成功响应):检查员把里面的宝贝(数据)直接递给你。
- 包裹有问题(失败响应):比如地址错了(404)、没权限(401)、或者快递公司仓库着火了(500)。检查员会告诉你具体是什么问题,并帮你处理(比如让你重新登录)。
这样,你每次收快递就不用自己费劲去检查了,检查员都帮你搞定了。我们开始吧!
第零步:准备工作
我们以最流行的 axios 库为例。首先,你的项目里需要安装它。
bash
npm install axios
# 或者
yarn add axios
然后,在你的项目里新建一个文件,专门用来管网络请求。比如 src/utils/request.js。我们所有的拦截器都写在这里。
情况一:最简单的成功响应("检查员只递给你宝贝")
**场景:**后端返回的数据格式是 { code: 0, message: 'success', data: { ... } }。我们只关心 data 里的内容,不想每次都写 response.data.data。
**目标:**让业务代码直接拿到 data。
在 src/utils/request.js 中这样写:
javascript
// 1. 导入 axios
import axios from 'axios';
// 2. 创建一个 axios 实例(就像创建一个专属的快递员)
const service = axios.create({
baseURL: 'https://api.example.com', // 你的API基地址
timeout: 5000 // 请求超时时间
});
// 3. 添加响应拦截器(我们的"检查员"上岗了!)
service.interceptors.response.use(
// ✅ 这个函数处理成功的情况 (HTTP状态码 2xx)
(response) => {
// response 是整个响应对象,里面包含了 headers, status, data 等
// 我们只需要把 response.data 里的 data 字段返回出去
return response.data.data;
},
// ❌ 这个函数处理失败的情况 (HTTP状态码非 2xx)
(error) => {
// 现在先简单地把错误抛出去,我们后面再详细说
return Promise.reject(error);
}
);
// 4. 把这个配置好的 service 导出,给其他页面用
export default service;
怎么用?
在你的业务文件里(比如 src/api/user.js):
javascript
import request from '@/utils/request'; // 引入我们配置好的"快递员"
// 获取用户信息
export function getUserInfo(userId) {
return request.get(`/users/${userId}`);
}
// 在组件里调用
// getUserInfo(123).then(data => {
// console.log(data); // 这里拿到的直接就是 { name: '张三', age: 25 },而不是 { code: 0, data: { name: '张三', age: 25 } }
// })
看,是不是清爽多了?这就是拦截器的第一个威力:数据精简 。
情况二:处理业务逻辑错误("检查员告诉你快递里的东西不对")
**场景:**后端返回的数据格式是 { code: 0, message: 'success', data: ... }。当 code 不是 0 时,表示业务逻辑有问题,比如"用户名或密码错误"(code: 1001)。
**目标:**当 code 不为 0 时,弹窗提示用户错误信息,并让请求失败。
修改 src/utils/request.js 中的成功处理函数:
javascript
// ... 前面的代码不变 ...
service.interceptors.response.use(
(response) => {
const res = response.data;
// 判断后端返回的业务状态码 code
if (res.code === 0) {
// 成功,直接返回数据
return res.data;
} else {
// 失败,弹出错误提示
// 假设你有一个全局的弹窗提示工具,比如 Element UI 的 Message
// 或者简单的浏览器原生弹窗
alert(res.message || '请求失败');
// 并让 Promise 进入失败状态,这样业务代码的 .catch 就能捕获到
return Promise.reject(new Error(res.message || 'Error'));
}
},
// ... 失败处理函数不变 ...
);
怎么用?
javascript
// 在组件里调用
getUserInfo(123).then(data => {
// 只有 code 为 0 时,这里才会执行
console.log('获取成功:', data);
}).catch(error => {
// code 不为 0 时,这里会执行
console.error('获取失败:', error.message); // 这里会打印 "用户名或密码错误"
});
现在,你的检查员不仅能递宝贝,还能帮你检查宝贝对不对,不对就马上告诉你!
情况三:处理 HTTP 错误("快递没送到,检查员告诉你原因")
**场景:**请求本身失败了,比如 401 (没权限)、404 (地址不存在)、500 (服务器崩了)。
**目标:**针对不同的 HTTP 错误,给用户不同的友好提示。
修改 src/utils/request.js 中的失败处理函数:
javascript
// ... 前面的代码不变 ...
service.interceptors.response.use(
// ... 成功处理函数不变 ...
(error) => {
// error.response 包含了服务器返回的详细信息
if (error.response) {
// 根据不同的状态码做不同的事
switch (error.response.status) {
case 401:
alert('你没权限,请先登录!');
// 这里可以加一个跳转到登录页的逻辑
// window.location.href = '/login';
break;
case 403:
alert('禁止访问!');
break;
case 404:
alert('请求的资源不存在!');
break;
case 500:
alert('服务器出错了,请稍后再试!');
break;
default:
alert(`请求失败,错误码: ${error.response.status}`);
}
} else if (error.request) {
// 请求发了,但没收到响应(比如网络断了)
alert('网络连接异常,请检查你的网络!');
} else {
// 请求配置出错了
alert('请求配置错误!');
}
// 最后,一定要把错误返回,让业务代码知道请求失败了
return Promise.reject(error);
}
);
// ... 后面的代码不变 ...
现在,你的"检查员"已经非常全能了!无论是包裹里的东西有问题,还是快递路上出了意外,他都能妥善处理并给你反馈。
情况四:终极挑战 - Token 过期自动刷新("检查员帮你偷偷换新钥匙")
**场景:**用户登录后会有一个 token(钥匙),但有时效性。过期后,所有请求都会返回 401。我们不能让用户重新登录,而是要偷偷用 refresh_token 换个新的 token,然后重新发送刚才失败的请求,用户对此无感知。
**目标:**实现无感刷新 Token。
这个稍微复杂一点,我们需要在拦截器外面加一些辅助变量。
修改 src/utils/request.js:
javascript
import axios from 'axios';
import { refreshTokenAPI } from '@/api/auth'; // 假设你有一个刷新 token 的接口
const service = axios.create({ baseURL: 'https://api.example.com' });
let isRefreshing = false; // 标记是否正在刷新 token
let failedQueue = []; // 存储因为 token 过期而失败的请求
// 处理失败队列的函数
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
// 响应拦截器
service.interceptors.response.use(
(response) => {
// ... 和之前一样,处理业务逻辑错误 ...
const res = response.data;
if (res.code === 0) {
return res.data;
} else {
return Promise.reject(new Error(res.message || 'Error'));
}
},
async (error) => {
const originalRequest = error.config;
// 如果是 401 错误,并且不是刷新 token 的请求,并且没有重试过
if (error.response.status === 401 && !originalRequest._retry && !originalRequest.url.includes('/refresh')) {
if (isRefreshing) {
// 如果正在刷新,就把当前请求加入失败队列
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
// 刷新成功后,为原请求设置新的 token
originalRequest.headers.Authorization = `Bearer ${token}`;
return service(originalRequest); // 重新发送原请求
}).catch(err => {
return Promise.reject(err);
});
}
originalRequest._retry = true; // 标记这个请求已经重试过一次了
isRefreshing = true; // 标记正在刷新 token
try {
// 调用刷新 token 的接口
const res = await refreshTokenAPI();
const newToken = res.token;
// 把新 token 存起来
localStorage.setItem('token', newToken);
// 为原请求设置新的 token
originalRequest.headers.Authorization = `Bearer ${newToken}`;
// 处理队列中的请求
processQueue(null, newToken);
// 重新发送原请求
return service(originalRequest);
} catch (refreshError) {
// 刷新 token 失败,比如 refresh_token 也过期了
processQueue(refreshError, null);
localStorage.removeItem('token');
alert('登录已过期,请重新登录');
// window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false; // 无论成功失败,都结束刷新状态
}
}
// 其他非 401 错误,照常处理
// ... (可以复制情况三的错误处理逻辑) ...
return Promise.reject(error);
}
);
export default service;
这段代码的逻辑是:
(1) 拦截到 401 错误。
(2) 检查是否已经在刷新 token( isRefreshing )。
- 如果正在刷新:就把这个请求先放进一个队列( failedQueue )里等着。
- 如果没在刷新:
- 把 isRefreshing 设为 true ,并开始调用刷新 token 的接口。
- 如果刷新成功,拿到新 token,给队列里所有等待的请求都换上新 token 并重新发送。
- 如果刷新失败,那就没办法了,清空 token,让用户去登录页。
- 最后,把 isRefreshing 改回 false 。
总结:响应拦截器的各种写法。
情况一:帮你精简数据,让代码更优雅。
情况二:帮你处理业务错误,提示更友好。
情况三:帮你处理 HTTP 错误,应用更健壮。
情况四:帮你实现无感刷新,用户体验更丝滑。
5. 🪄手把手教你写请求拦截器
我将用最直白的话,手把手教你如何让根据不同情况写请求拦截器。
想象一下,请求拦截器 就像一个"快递打包员"。
- 你(业务代码)准备要寄一个包裹(发起请求)。
- 在快递员(浏览器)来取件之前,打包员(请求拦截器)会先帮你检查包裹。
他会做一些标准操作,比如:
- 给所有包裹贴上统一的发货单(添加 baseURL)。
- 给每个包裹盖上你的专属印章(添加 Token)。
- 用统一的包装纸(转换数据格式)把东西包起来。
这样,你每次寄东西就不用自己费心去做这些重复的准备工作了,打包员都帮你搞定了。我们开始吧!
第零步:准备工作
我们依然以最流行的 axios 库为例,并在上一次创建的src/utils/request.js 文件里添加请求拦截器。
javascript
// src/utils/request.js
import axios from 'axios';
const service = axios.create({
baseURL: 'https://api.example.com', // 你的API基地址
timeout: 5000
});
// 👇 我们将要在这里添加请求拦截器 👇
// 👆 我们将要在这里添加响应拦截器 👆
export default service;
情况一:最常见的需求 - 统一添加身份认证 Token
**场景:**用户登录后,服务器会给他一个 token(类似一张临时身份证)。之后,用户每次请求数据,都必须带上这个 token,否则服务器会认为他是非法用户。
目标: 在每个请求的 HTTP Header 里自动加上 Authorization: Bearer <token>,不用每次都手动写。
在 src/utils/request.js 中这样写:
javascript
// ... 前面的代码不变 ...
// ✅ 添加请求拦截器(我们的"打包员"上岗了!)
service.interceptors.request.use(
// 这个函数在请求发送前执行
(config) => {
// config 是本次请求的配置对象,包含了 url, method, headers 等信息
// 1. 从本地存储(比如 localStorage)里获取 token
const token = localStorage.getItem('token');
// 2. 如果 token 存在,就把它添加到请求头的 Authorization 字段中
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 3. 一定要返回 config,否则请求就发不出去了!
return config;
},
// 这个函数在请求发送出错时执行(非常少见)
(error) => {
console.error('请求拦截器出错:', error);
return Promise.reject(error);
}
);
// ... 后面的响应拦截器代码不变 ...
怎么用?
在你的业务代码里,你完全不需要关心 token 的事:
javascript
import request from '@/utils/request';
// 获取用户信息
export function getUserInfo(userId) {
// 打包员会自动在请求头里加上 Authorization
return request.get(`/users/${userId}`);
}
这就是请求拦截器最核心、最常用的功能:统一注入认证信息。
情况二:处理 POST/PUT 请求的数据格式
场景: 大多数后端接口期望接收的数据格式是JSON 字符串。但有时我们可能习惯直接传入一个 JavaScript 对象。
**目标:**确保所有 POST、PUT、PATCH 请求发送的数据都是 JSON 格式。
修改 src/utils/request.js 中的请求拦截器:
javascript
// ... 前面的代码不变 ...
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// ✅ 新增逻辑:处理请求数据
// 判断请求方法,如果是 POST, PUT, PATCH
if (['post', 'put', 'patch'].includes(config.method)) {
// 并且如果 data 是一个对象,就把它转换成 JSON 字符串
// (其实 axios 大部分时候会自动做,但手动控制更保险)
if (config.data && typeof config.data === 'object') {
config.data = JSON.stringify(config.data);
}
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// ... 后面的代码不变 ...
怎么用?
现在你可以直接传对象,拦截器会帮你搞定转换:
javascript
import request from '@/utils/request';
// 创建新用户
export function createUser(userData) {
// userData 是一个对象 { name: '张三', age: 25 }
// 打包员会自动把它变成 '{"name":"张三","age":25}' 再发出去
return request.post('/users', userData);
}
情况三:为每个请求添加唯一标识
**场景:**在复杂的应用中,为了方便排查问题,我们希望每个请求都有一个独一无二的 ID,这样当后端报错时,可以根据这个 ID 快速定位到是前端的哪一次请求出了问题。
目标: 为每个请求自动生成一个唯一的 X-Request-ID 并添加到请求头中。
修改 src/utils/request.js 中的请求拦截器:
javascript
// ... 前面的代码不变 ...
// 先写一个生成唯一ID的简单函数
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0,
v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// ✅ 新增逻辑:添加唯一请求ID
config.headers['X-Request-ID'] = generateUUID();
return config;
},
(error) => {
return Promise.reject(error);
}
);
// ... 后面的代码不变 ...
现在,你发出去的每一个请求都会带有一个独特的 ID,方便你在日志系统和后端进行追踪。
情况四:在开发环境打印请求信息
**场景:**在本地开发时,我们经常需要查看请求的详细信息(URL、参数、Header等)来调试。但在生产环境,这些信息不应该打印出来,以免泄露信息或影响性能。
**目标:**只在开发环境下,在控制台打印出请求的配置。
修改 src/utils/request.js 中的请求拦截器:
javascript
// ... 前面的代码不变 ...
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// ✅ 新增逻辑:开发环境打印日志
// process.env.NODE_ENV 是 webpack/vite 等工具提供的环境变量
if (process.env.NODE_ENV === 'development') {
console.log('🚀 发送请求:', {
url: config.url,
method: config.method,
params: config.params,
data: config.data,
});
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// ... 后面的代码不变 ...
这样,你在本地开发时,控制台会清晰地打印出每个请求的细节,非常方便调试。当代码部署到线上服务器后, process.env.NODE_ENV 会变成 'production' ,这段日志代码就不会执行了。
总结
以上即可掌握了请求拦截器的各种实用技巧。
情况一:自动加 Token ,解决认证问题。
情况二:统一数据格式,避免后端报错。
情况三:添加请求 ID,方便问题追踪。
情况四:开发环境打印日志,提升调试效率。
6. 总结
拦截器是前端 HTTP 客户端库提供的一个强大而灵活的机制,它让我们能够以一种非侵入式的方式,在请求生命周期的关键节点注入通用逻辑。
通过合理运用拦截器,我们可以:
- 提升代码复用性:将认证、错误处理等通用逻辑抽离,避免重复代码。
- 增强代码健壮性:统一处理边界情况,如 Token 刷新、网络错误。
- 优化用户体验:实现无感刷新、取消重复请求,让应用更流畅。
- 简化业务逻辑:让开发者更专注于业务本身,而非底层通信细节。
掌握拦截器,是从"会用"一个框架到"用好"一个框架的重要进阶。在企业级开发中,一套设计良好的拦截器方案,是构建高质量、高可维护性前端应用的坚实基石。