Skip to content

跨标签页数据同步

Published:

前言

在开发 Vue.js 应用时,你可能会遇到这样的场景:有两个标签页,一个显示数据表格(增删改查),另一个显示单个数据项的详细信息。当我们在详情页修改数据并提交后,希望第一个标签页的表格可以自动刷新,显示最新的数据。

跨标签页状态管理的小难题

Vuex 和 Pinia 是 Vue.js 中常用的状态管理工具,它们在管理单个 Vue 应用内的状态时非常有效。但问题在于,当我们打开多个标签页时,每个标签页都有自己的独立状态。Vuex 和 Pinia 的状态保存在 JavaScript 内存中,无法跨标签页共享。这意味着你在一个标签页中修改状态时,另一个标签页无法获取到这些变化。

解决方案

BroadcastChannel API

BroadcastChannel API 是一种浏览器原生支持的API,允许在同一浏览器中的不同标签页或 iframe 之间传递消息。这是一个简单且高效的解决方案,非常适合这一个业务场景。

使用步骤:

  1. 在每个标签页中创建一个 BroadcastChannel 实例。
  2. 在详情页中修改数据后,向频道发送消息。
  3. 表格页监听到消息后,触发数据刷新。

代码示例:

// 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

使用步骤:

  1. 在后端搭建 WebSocket 服务器。
  2. 前端建立 WebSocket 连接。
  3. 详情页修改数据后,通过 WebSocket 向服务器发送更新通知。
  4. 服务器将更新消息广播给所有连接的客户端。

伪代码:

// 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 主要用于处理后台任务,比如离线支持和缓存管理。它可以在后台线程中运行,并且与页面线程隔离。

使用步骤:

  1. 注册和配置 Service Worker。
  2. 在 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 监听事件,可以定期检查数据变化并通知标签页。

使用步骤:

  1. 创建一个 Shared Worker 来共享数据。
  2. 使用定时器定期向 Shared Worker 查询数据变化。
  3. 当数据变化时,通知相关的标签页刷新数据。

代码示例:

// 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 是一种浏览器内置的数据库,适用于存储大量结构化数据。结合定时器轮询来定期检查数据是否有更新,这可以确保页面展示的数据是最新的。

使用步骤:

  1. 配置 IndexedDB 数据库: 创建一个 IndexedDB 数据库,并定义一个存储对象来存储数据。确保在数据库的升级阶段创建数据存储对象。
  2. 在 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

在主页面中打开一个子窗口,当子窗口中的数据更新时,需要通知主页面进行刷新。

使用步骤

  1. 在子窗口中发送消息: 在子窗口更新数据时,使用 window.postMessage 发送消息到主窗口。
  2. 在主窗口中处理消息: 在主窗口中监听来自子窗口的消息,收到消息后更新数据。

代码示例

<!-- 在子窗口中发送消息 -->
<!-- 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>