富士通 ETERNUS 系列存储设备内置了基于 HTTPS 的 RESTful 管理接口,支持卷管理、性能统计、事件查询和用户会话控制。前端项目如果直接在每个组件里写 fetch 或 axios 请求,会很快出现认证令牌处理不一致、错误提示重复、数据格式不统一等问题。因此,在 Vue 3 工程中集成富士通存储时,第一件事不是写页面,而是搭建一个稳定的 HTTP 访问层和状态管理层。本文以 ETERNUS DX 系列为例,说明如何把存储管理 API 封装成前端可复用的服务模块。

一、存储 REST API 的接入准备与认证机制
富士通 ETERNUS 的管理接口通常监听在设备的 5665 端口,采用 HTTPS 协议。调用任何业务接口之前,客户端需要先向 /api/session 发送包含用户名和密码的 POST 请求,成功后响应体中会返回一个会话令牌,部分固件版本还支持基于角色的访问控制。这个令牌的有效期一般较短,项目里需要设计刷新策略,否则页面运行一段时间后所有请求都会返回 401。
在 Vue 3 中可以直接使用 axios 作为 HTTP 客户端。由于存储设备通常部署在内网,前端开发服务器需要配置代理,避免浏览器跨域限制。以 Vite 为例,可以在 vite.config.ts 中设置 server.proxy,把 /eternus-api 前缀的请求转发到存储设备地址。这样前端代码中只使用相对路径,生产环境再由 Nginx 或网关完成同样的代理功能。代理层还可以顺便屏蔽设备真实 IP,降低安全风险。
存储接口返回的数据字段比较底层,例如容量单位是字节,性能值可能是原始计数器。如果直接把原始数据交给图表组件,不仅渲染压力大,还容易因为单位换算错误导致展示异常。因此应在服务层做一层数据映射,把字节转换为 GB 或 TB,把性能采样转换为每秒钟的 IOPS 和吞吐量。这样上层组件拿到的就是可以直接显示的业务数据。
二、封装 axios 实例与请求拦截器
在 src/api/eternus/ 目录下创建独立的 axios 实例,不要复用全局默认实例。原因很简单:存储管理接口的超时时间、响应格式和错误提示与普通业务接口不同,混在一起会互相影响。新建 http.ts,设置 baseURL 为代理后的路径,默认超时 15 秒,并开启 withCredentials 以便携带设备下发的 Cookie(如果固件要求)。
import axios, { AxiosInstance, AxiosError } from 'axios';
const http: AxiosInstance = axios.create({
baseURL: '/eternus-api',
timeout: 15000,
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
});
let sessionToken: string | null = null;
http.interceptors.request.use((config) => {
if (sessionToken) {
config.headers['X-Auth-Token'] = sessionToken;
}
return config;
});
http.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as any;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const { data } = await axios.post('/eternus-api/api/session', {
username: import.meta.env.VITE_ETERNUS_USER,
password: import.meta.env.VITE_ETERNUS_PASS
});
sessionToken = data.token;
originalRequest.headers['X-Auth-Token'] = sessionToken;
return http(originalRequest);
}
return Promise.reject(error);
}
);
export { http, setSessionToken: (t: string) => { sessionToken = t; } };
代码中通过请求拦截器统一注入 X-Auth-Token 请求头,避免每个接口手动传递。响应拦截器在遇到 401 时尝试重新登录获取新令牌,并重放原始请求。这里使用了 _retry 标记防止无限循环,因为如果连续多次 401,说明账号权限或设备地址可能存在问题,应直接抛错到调用方处理。
需要注意,存储设备在会话令牌过期后可能返回 403 而不是 401,具体以 ETERNUS 固件文档为准。建议在拦截器里同时判断 401 和 403,并对非 GET 请求的重放谨慎处理,防止重复提交创建卷等操作。对于写操作,可在拦截器里加上请求序列号,由后端接口保证幂等性。
三、用 Pinia 管理容量与性能数据
Vue 3 的全局状态管理首选 Pinia。存储管理界面通常需要展示多个存储池、卷和主机的容量信息,这些数据来自不同接口,但相互关联。比如卷属于某个存储池,性能数据按卷 ID 查询。如果组件各自拉取数据,会产生重复请求和状态不一致。建立 useStorageStore 可以集中管理这些资源。
import { defineStore } from 'pinia';
import { http } from '@/api/eternus/http';
interface Volume {
id: string;
name: string;
capacityGb: number;
poolId: string;
status: string;
}
export const useStorageStore = defineStore('eternusStorage', {
state: () => ({
volumes: [] as Volume[],
pools: [] as any[],
loading: false,
error: null as string | null,
lastUpdated: null as Date | null
}),
actions: {
async fetchVolumes() {
this.loading = true;
this.error = null;
try {
const { data } = await http.get('/api/volumes');
this.volumes = data.volumes.map((v: any) => ({
id: v.volumeId,
name: v.name,
capacityGb: Math.round(v.capacityByte / 1024 / 1024 / 1024),
poolId: v.poolId,
status: v.status
}));
this.lastUpdated = new Date();
} catch (err: any) {
this.error = err.message || '获取卷列表失败';
} finally {
this.loading = false;
}
},
async fetchPools() {
try {
const { data } = await http.get('/api/pools');
this.pools = data.pools.map((p: any) => ({
id: p.poolId,
name: p.name,
totalGb: Math.round(p.totalCapacityByte / 1024 / 1024 / 1024),
freeGb: Math.round(p.freeCapacityByte / 1024 / 1024 / 1024)
}));
} catch (err: any) {
this.error = err.message || '获取存储池失败';
}
}
}
});
上述代码将接口返回的原始字节转换为 GB,并只保留前端需要的字段。这样做的好处是:当组件使用 volumes 时,不需要关心后端返回的复杂嵌套结构;后续若更换不同型号的富士通存储,只修改 store 中的映射逻辑即可,组件无需改动。
对于性能指标,轮询是常见需求。存储性能数据实时变化,但频繁请求会增加设备负担。可以在 store 中增加一个 startPerformancePolling 方法,使用 setInterval 每隔 10 秒拉取一次 IOPS 和吞吐量。在组件卸载时务必调用 stopPerformancePolling 清除定时器,否则会造成内存泄漏。轮询过程中如果遇到错误,不要立即清空已有数据,保留最后一次成功结果,并在界面上提示数据可能不是最新的。
四、组件中的展示与错误边界处理
构建组件时,推荐使用 Vue 3 的 <script setup> 语法。组件内部只做三件事:从 store 读取状态、在生命周期钩子中触发数据加载、根据 loading 和 error 渲染不同视图。不要在组件里直接写 axios 请求,否则 store 的作用就被架空了。
<template>
<div class="storage-dashboard">
<div v-if="store.loading" class="loading">正在加载存储信息...</div>
<div v-else-if="store.error" class="error">
<p>{{ store.error }}</p>
<button @click="retry">重新加载</button>
</div>
<div v-else class="volume-grid">
<div v-for="volume in store.volumes" :key="volume.id" class="volume-card">
<h3>{{ volume.name }}</h3>
<p>容量:{{ volume.capacityGb }} GB</p>
<p>状态:{{ volume.status }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { useStorageStore } from '@/stores/eternusStorage';
const store = useStorageStore();
async function retry() {
await store.fetchVolumes();
await store.fetchPools();
}
onMounted(async () => {
await retry();
});
</script>
上面的模板在加载中、错误和正常三种状态之间切换,用户操作清晰。错误边界不应只依赖组件内的 v-if,还可以通过全局错误处理器统一记录日志。在 main.ts 中为 Vue 应用注册 app.config.errorHandler,把未捕获异常发送到监控系统。对于存储管理这类运维型界面,记录用户操作前后的设备状态有助于定位问题。
另一个容易忽略的点是权限控制。富士通存储 API 支持不同角色,普通只读账号不应看到创建或删除卷的按钮。前端可以根据当前会话返回的角色信息,使用自定义指令 v-permission 控制按钮显隐。但前端权限只负责用户体验,真正的权限校验必须由后端或存储设备完成,不能依赖前端隐藏按钮来保证安全。
通过分层封装,Vue 3 工程接入富士通存储不再是一件难事。axios 实例处理协议和认证,Pinia store 负责数据映射和状态同步,组件专注于交互。这样的结构也方便后续扩展:当需要接入其他存储品牌时,只需实现相同的 store 接口,前端页面可以做到零修改复用。