前言
在开发 Vue.js 应用时,你可能会遇到这样的场景:有两个标签页,一个显示数据表格(增删改查),另一个显示单个数据项的详细信息。当我们在详情页修改数据并提交后,希望第一个标签页的表格可以自动刷新,显示最新的数据。
跨标签页状态管理的小难题
Vuex 和 Pinia 是 Vue.js 中常用的状态管理工具,它们在管理单个 Vue 应用内的状态时非常有效。但问题在于,当我们打开多个标签页时,每个标签页都有自己的独立状态。Vuex 和 Pinia 的状态保存在 JavaScript 内存中,无法跨标签页共享。这意味着你在一个标签页中修改状态时,另一个标签页无法获取到这些变化。
解决方案
BroadcastChannel API
BroadcastChannel API 是一种浏览器原生支持的API,允许在同一浏览器中的不同标签页或 iframe 之间传递消息。这是一个简单且高效的解决方案,非常适合这一个业务场景。
使用步骤:
- 在每个标签页中创建一个
BroadcastChannel
实例。 - 在详情页中修改数据后,向频道发送消息。
- 表格页监听到消息后,触发数据刷新。
代码示例:
// broadcast-channel.ts
export class BroadcastChannelService {
private channel: BroadcastChannel;
constructor(channelName: string) {
this.channel = new BroadcastChannel(channelName);
}
// 发送消息
sendMessage(message: any) {
this.channel.postMessage(message);
}
// 监听消息
onMessage(callback: (event: MessageEvent) => void) {
this.channel.onmessage = callback;
}
// 关闭频道
close() {
this.channel.close();
}
}
<!-- TablePage -->
<template>
<table>
<!-- 表格数据 -->
</table>
</template>
<script lang="ts" setup>
import { BroadcastChannelService } from "./broadcast-channel";
const data = ref<any[]>([]);
const channelService = new BroadcastChannelService("data-sync");
// 监听消息,接收到刷新指令时重新获取数据
onMounted(() => {
channelService.onMessage(event => {
if (event.data === "refresh") {
fetchData();
}
});
fetchData();
});
// 模拟获取数据的函数
const fetchData = () => {
// 假设从 API 获取数据
data.value = [
{ id: 1, name: "数据1" },
{ id: 2, name: "数据2" },
// ...其他数据
];
};
</script>
<!-- DetailPage -->
<template>
<button @click="updateData">提交</button>
</template>
<script lang="ts" setup>
import { BroadcastChannelService } from './broadcast-channel';
// 创建 BroadcastChannelService 实例
const channelService = new BroadcastChannelService('data-sync');
// 模拟数据更新的函数
const updateData = () => {
// 假设更新数据
// ...
// 发送刷新指令
channelService.sendMessage('refresh');
};
</script>
Nest.js WebSocket
使用步骤:
- 在后端搭建 WebSocket 服务器。
- 前端建立 WebSocket 连接。
- 详情页修改数据后,通过 WebSocket 向服务器发送更新通知。
- 服务器将更新消息广播给所有连接的客户端。
伪代码:
// Nest.js
// websocket.gateway.ts
import {
WebSocketGateway,
OnGatewayConnection,
OnGatewayDisconnect,
WebSocketServer,
} from "@nestjs/websockets";
import { Server, Socket } from "socket.io";
@WebSocketGateway()
export class WebSocketGateway
implements OnGatewayConnection, OnGatewayDisconnect
{
@WebSocketServer()
server: Server;
handleConnection(client: Socket) {
console.log("Client connected:", client.id);
}
handleDisconnect(client: Socket) {
console.log("Client disconnected:", client.id);
}
@SubscribeMessage("update-user")
handleRequestUpdate(client: Socket, data: any) {
console.log(
"Received requestUpdate from client:",
client.id,
"with data:",
data
);
// 处理数据并更新
// 发送更新数据到客户端
// userService.findAll()
client.emit("update", users);
}
}
<!-- TablePage -->
<template>
<table>
<!-- 表格数据 -->
</table>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import io from "socket.io-client";
const data = ref<any[]>([]);
const socket = io("http://localhost:3000");
onMounted(() => {
socket.on("update", () => {
fetchData();
});
fetchData();
});
const fetchData = () => {
// 假设从 API 获取数据
data.value = [
{ id: 1, name: "数据1" },
{ id: 2, name: "数据2" },
// ...其他数据
];
};
// 在详情页中,当数据更新后通知服务器
const updateData = () => {
const updatedData = {
/* ... */
};
socket.emit("update", updatedData);
};
</script>
<!-- DetailPage -->
<template>
<button @click="handleSubmit">提交</button>
</template>
<script lang="ts" setup>
import io from "socket.io-client";
// 数据项
const item = ref<any>({ id: 1, name: "", value: "" });
// 连接到 WebSocket 服务器
const socket = io("http://localhost:3000");
// 提交更新
const handleSubmit = async () => {
const updatedData = {
id: item.value.id,
name: item.value.name,
value: item.value.value,
};
// 发送更新数据到服务器
socket.emit("update", updatedData);
};
</script>
Service Worker
Service Worker 主要用于处理后台任务,比如离线支持和缓存管理。它可以在后台线程中运行,并且与页面线程隔离。
使用步骤:
- 注册和配置 Service Worker。
- 在 Vue 组件中添加监听并调用事件。
代码示例:
// main.ts
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/service-worker.js")
.then(registration => {
console.log("Service Worker registered with scope:", registration.scope);
})
.catch(error => {
console.error("Service Worker registration failed:", error);
});
}
// service-worker.js
self.addEventListener("install", event => {
console.log("Service Worker installing.");
// 这里可以添加缓存逻辑
});
self.addEventListener("fetch", event => {
console.log("Service Worker fetching:", event.request.url);
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
self.addEventListener("message", event => {
if (event.data.type === "CHECK_FOR_UPDATES") {
// 假设数据已更新
event.waitUntil(
//
self.clients.matchAll().then(clients => {
clients.forEach(client => client.postMessage({ type: "UPDATE_DATA" }));
})
);
}
});
<!-- DetailPage.vue -->
<template>
<button @click="handleSubmit">提交</button>
</template>
<script lang="ts" setup>
const handleSubmit = () => {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: "CHECK_FOR_UPDATES",
});
}
};
</script>
<!-- TablePage.vue -->
<template>
<table>
<!-- 表格数据 -->
</table>
</template>
<script lang="ts" setup>
const fetchData = async () => {
// 从服务器获取数据并更新表格
};
onMounted(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", event => {
if (event.data.type === "UPDATE_DATA") {
fetchData(); // 刷新数据
}
});
}
});
</script>
Shared Worker
Shared Worker 允许多个标签页共享一个后台线程。通过对 shared worker 监听事件,可以定期检查数据变化并通知标签页。
使用步骤:
- 创建一个 Shared Worker 来共享数据。
- 使用定时器定期向 Shared Worker 查询数据变化。
- 当数据变化时,通知相关的标签页刷新数据。
代码示例:
// shared-worker.js
let connections = [];
self.onconnect = event => {
const port = event.ports[0];
connections.push(port);
port.addEventListener("message", event => {
if (event.data.type === "CHECK_FOR_UPDATES") {
// 检查数据是否更新的逻辑
const dataUpdated = true; // 假设数据已更新
if (dataUpdated) {
connections.forEach(client =>
client.postMessage({ type: "UPDATE_DATA" })
);
}
}
});
port.start();
};
<!-- 在 Vue 组件中使用 Shared Worker -->
<!-- DetailPage.vue -->
<template>
<button @click="handleSubmit">提交</button>
</template>
<script lang="ts" setup>
const handleSubmit = () => {
const worker = new SharedWorker("shared-worker.js");
worker.port.postMessage({ type: "CHECK_FOR_UPDATES" });
};
</script>
<!-- TablePage -->
<script lang="ts" setup>
const fetchData = async () => {
// 从服务器获取数据并更新表格
};
onMounted(() => {
const worker = new SharedWorker("shared-worker.js");
worker.port.addEventListener("message", event => {
if (event.data.type === "UPDATE_DATA") {
fetchData(); // 刷新数据
}
});
worker.port.start();
});
</script>
使用 IndexedDB 和定时器轮询
IndexedDB
是一种浏览器内置的数据库,适用于存储大量结构化数据。结合定时器轮询来定期检查数据是否有更新,这可以确保页面展示的数据是最新的。
使用步骤:
- 配置 IndexedDB 数据库: 创建一个
IndexedDB
数据库,并定义一个存储对象来存储数据。确保在数据库的升级阶段创建数据存储对象。 - 在 Vue 组件中使用定时器轮询: 在 Vue 组件中设置定时器,每隔一定时间从数据库中读取数据并更新组件中的状态。
// db.ts
export const openDb = async () => {
return new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open("myDatabase", 1);
request.onupgradeneeded = event => {
const db = (event.target as IDBOpenDBRequest).result;
db.createObjectStore("dataStore", { keyPath: "id" });
};
request.onsuccess = event =>
resolve((event.target as IDBOpenDBRequest).result);
request.onerror = event => reject((event.target as IDBOpenDBRequest).error);
});
};
export const fetchDataFromDb = async () => {
const db = await openDb();
return new Promise<any[]>((resolve, reject) => {
const transaction = db.transaction("dataStore", "readonly");
const store = transaction.objectStore("dataStore");
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
};
<!-- TablePage.vue -->
<!-- 主页面中使用定时器轮询 -->
<template>
<table>
<!-- 数据表格 -->
</table>
</template>
<script lang="ts" setup>
import { fetchDataFromDb } from "./db";
const data = ref<any[]>([]);
const fetchData = async () => {
data.value = await fetchDataFromDb();
};
onMounted(() => {
fetchData(); // 初次加载数据
setInterval(fetchData, 5000); // 每5秒检查一次更新
});
</script>
<!-- DetailPage.vue -->
<template>
<button @click="handleSubmit">提交</button>
</template>
<script lang="ts" setup>
import { updateDataInDb } from "./db";
const item = ref<any>({ id: 1, name: "", value: "" }); // 示例数据
const handleSubmit = async () => {
await updateDataInDb(item.value);
if (window.opener) {
window.opener.postMessage({ type: "UPDATE_DATA" }, "*");
}
};
</script>
使用 window.open 和 window.postMessage
在主页面中打开一个子窗口,当子窗口中的数据更新时,需要通知主页面进行刷新。
使用步骤:
- 在子窗口中发送消息: 在子窗口更新数据时,使用
window.postMessage
发送消息到主窗口。 - 在主窗口中处理消息: 在主窗口中监听来自子窗口的消息,收到消息后更新数据。
代码示例:
<!-- 在子窗口中发送消息 -->
<!-- DetailPage.vue -->
<template>
<button @click="handleSubmit">提交</button>
</template>
<script lang="ts" setup>
import { updateDataInDb } from "./db";
const item = ref<any>({ id: 1, name: "", value: "" }); // 示例数据
const handleSubmit = async () => {
await updateDataInDb(item.value);
if (window.opener) {
window.opener.postMessage({ type: "UPDATE_DATA" }, "*");
}
};
</script>
<!-- 在主窗口中处理消息 -->
// TablePage.vue
<template>
<table>
<!-- 表格数据 -->
</table>
</template>
<script lang="ts" setup>
import { fetchDataFromDb } from "./db";
const data = ref<any[]>([]);
const fetchData = async () => {
data.value = await fetchDataFromDb();
};
onMounted(() => {
fetchData(); // 初次加载数据
window.addEventListener("message", event => {
if (event.data.type === "UPDATE_DATA") {
fetchData(); // 刷新数据
}
});
});
</script>