前言
Hello~大家好。我是秋天的一阵风
在我们公司的项目中,存在这样一种场景:在列表页中通常会设置一个 "新增数据" 按钮,以便用户能够方便地录入新数据。一般来说,我们会采用弹窗的形式来完成这一操作。如下图所示:
然而,当录入的字段过多时,弹窗的高度会显得不足,从而导致滚动条的出现,这无疑会对用户体验造成较大影响。因此,在这种情况下,我们通常会选择 跳转到一个独立的创建数据页面。
对于前端路由跳转,以 Vue 为例,我们通常会使用以下 API 来实现:
JavaScript
this.$router.push('/login'); // 跳转到指定路由
this.$router.replace('/login'); // 替换当前路由
this.$router.go(-1); // 后退一步
this.$router.go(1); // 前进一步
目前,我们使用的三大主流框架------Vue、React 和 Angular------都属于单页面应用框架。在这种框架下,直接进行路由跳转会导致当前列表页面的数据丢失。而我们列表页中往往包含用户已经输入的一系列筛选条件,客户不希望因为新增数据的操作而离开当前页面,从而导致筛选条件丢失。
因此,用户提出了新的要求:必须通过新开一个标签页来打开新增数据页面进行数据创建。并且,在完成数据创建后,原标签页(即列表页面)应能够基于当前的筛选条件进行实时搜索。
一、流程与效果
1. 流程
- 在列表页点击'新增数据'按钮,通过
window.open
打开新标签创建数据页面 - 在创建好数据以后,需要通知旧标签也就是列表页面,基于当前的筛选条件重新请求数据
2. 最终效果
二、八种方案
1. Cookie 定时器轮询
我们可以在创建数据页面成功创建数据后,往cookie里存入一个标识(比如:needRefresh:true
),在数据列表页通过定时器轮询的方式获取,得到创建成功的标识后,更新数据。
在文中这个案例,为了方便处理和展示效果,我是直接在cookie
里保存数据对象,然后在列表页中直接push进数组中。
javascript
npm install js-cookie
javascript
// main.js
import Cookies from "js-cookie";
Vue.prototype.$setCookie = (key, value) => {
Cookies.set(key, value, { expires: 1 }); // 设置 Cookie,有效期为 1 天
};
Vue.prototype.$getCookie = (key) => {
return Cookies.get(key);
};
Vue.prototype.$removeCookie = (key) => {
return Cookies.remove(key);
};
javascript
// edit.vue 创建数据页
methods: {
onSubmit() {
this.$message({
message: "创建成功!",
type: "success",
});
this.$setCookie("newData", JSON.stringify(this.formData));
},
},
javascript
// index.vue 列表页
data() {
return {
filterForm: {
id: "",
name: "",
status: "",
category: "",
priority: "",
assignedTo: "",
createdBy: "",
},
tableData: [],
filteredData: [],
pollingTimer: null, // 定时器
};
},
mounted() {
this.pollCookieData();
},
beforeDestroy() {
// 在组件销毁前清除定时器
if (this.pollingTimer) {
clearInterval(this.pollingTimer);
}
},
methods: {
pollCookieData() {
this.pollingTimer = setInterval(() => {
const cookieData = this.$getCookie("newData");
if (cookieData) {
try {
const parsedData = JSON.parse(cookieData);
this.tableData.push(parsedData);
this.applyFilters(); // 基于已有筛选条件过滤数据
this.$removeCookie("newData") // 防止重复添加
} catch (error) {
console.error("解析 Cookie 数据失败:", error);
}
}
}, 2000);
},
}
2. LocalStorage 和 Window.Onstorage
javascript
//edit.vue 创建数据页
methods: {
onSubmit() {
this.$message({
message: "创建成功!",
type: "success",
});
localStorage.setItem('newData', JSON.stringify(this.formData));
},
},
javascript
// index.vue 列表页
mounted() {
// 监听 storage 事件
window.addEventListener("storage", this.handleStorageEvent);
},
beforeDestroy() {
window.removeEventListener("storage", this.handleStorageEvent);
},
methods: {
handleStorageEvent(event) {
// 监听 storage 事件
if (event.key === 'newData') {
if(event.newValue){
try {
const parsedData = JSON.parse(event.newValue);
this.tableData.push(parsedData);
this.applyFilters();
} catch (error) {
console.error("解析 Storage 数据失败:", error);
}
}
}
},
}
3. window.opener 和 window.postMessage
1. window.opener
定义
window.opener
是一个全局属性,用于引用打开当前窗口的窗口对象。如果当前窗口是通过 window.open()
方法打开的,window.opener
就会指向打开它的父窗口;如果没有父窗口,则为 null
。
用途
- 跨窗口操作 :可以通过
window.opener
访问父窗口的 DOM、变量或方法,从而实现父子窗口之间的交互。 - 关闭父窗口 :可以通过
window.opener.close()
关闭父窗口。 - 与父窗口通信 :可以通过直接操作
window.opener
来传递数据或调用方法
限制
- 同源策略 :出于安全考虑,
window.opener
只能在同源策略下工作。如果父窗口和子窗口的来源(协议、域名、端口)不同,浏览器会阻止对window.opener
的访问。 - 用户体验:直接操作父窗口可能导致用户体验问题(如弹窗或页面篡改),因此需要谨慎使用。
2. window.postMessage
定义
window.postMessage
是一个用于跨窗口(或跨域)通信的方法。它允许在不同来源(origin)的窗口或 iframe 之间安全地传递消息,而不会违反浏览器的同源策略。
用途
- 跨域通信:可以在不同来源的页面之间传递消息,例如在主页面和嵌入的 iframe 之间。
- 父子窗口通信 :可以在父窗口和通过
window.open()
打开的子窗口之间通信。 - 安全交互 :通过指定目标来源(
targetOrigin
),可以确保消息只发送到预期的窗口,避免安全风险。
语法
JavaScript
otherWindow.postMessage(message, targetOrigin);
otherWindow
:目标窗口的引用,可以是window.open()
返回的窗口对象,或者 iframe 的contentWindow
。message
:要发送的消息,可以是字符串、对象或其他可序列化的数据。targetOrigin
:目标窗口的来源(协议、域名和端口)。出于安全考虑,必须明确指定目标来源,而不是使用通配符(*
)。
javascript
//edit.vue 创建数据页
methods: {
onSubmit() {
this.$message({
message: "创建成功!",
type: "success",
});
if (window.opener) {
window.opener.postMessage(this.formData, "*"); // 注意:实际使用中应指定具体来源
}
},
},
}
javascript
// index.vue 列表页
mounted() {
window.addEventListener("message", this.handleMessage);
},
beforeDestroy() {
window.removeEventListener("message", this.handleMessage);
},
methods: {
handleMessage(event) {
if (event.origin !== window.origin) {
console.warn("消息来源不安全,已忽略");
return;
}
this.tableData.push(event.data);
this.applyFilters();
},
}
4. websocket
WebSocket 是一种网络通信协议,用于在客户端(通常是浏览器)和服务器之间建立全双工(full-duplex)的通信通道。与传统的 HTTP 协议不同,WebSocket 允许服务器主动向客户端发送消息,而客户端也可以随时向服务器发送消息,而无需重新建立连接。这种双向通信机制使得 WebSocket 非常适合实时通信场景,如在线聊天、实时数据更新、游戏等。
WebSocket 的特点
-
全双工通信:
- 客户端和服务器可以同时发送和接收消息,无需像 HTTP 那样每次都建立新的连接。
- 一旦连接建立,数据可以在客户端和服务器之间双向流动。
-
低延迟:
- WebSocket 的通信基于 TCP,数据传输效率高,延迟低。
- 适合对实时性要求较高的应用,如在线游戏、股票行情等。
-
轻量级:
- WebSocket 的消息头较小,通常只有 2-10 字节,相比 HTTP 的头部(通常几百字节)要轻量得多。
- 减少了数据传输的开销。
-
支持多种数据格式:
- WebSocket 支持二进制数据和文本数据(如 JSON),可以灵活地传输不同类型的数据。
-
基于 TCP:
- WebSocket 基于 TCP 协议,确保数据的可靠传输。
- 可以通过现有的 HTTP 端口(80 和 443)进行通信,方便穿透防火墙。
javascript
// main.js
const ws = new WebSocket('ws://yourserver.com/path');
window.ws = ws; // 挂载到全局对象
ws.onopen = () => {
console.log('WebSocket连接成功');
};
ws.onmessage = (event) => {
console.log('收到服务器消息:', event.data);
};
ws.onerror = (error) => {
console.error('WebSocket连接发生错误:', error);
};
ws.onclose = () => {
console.log('WebSocket连接关闭');
};
javascript
// A.vue
export default {
methods: {
sendMessage() {
const message = 'Hello from Page A';
window.ws.send(message); // 通过全局 WebSocket 发送消息
}
}
};
javascript
// B.vue
export default {
mounted() {
window.ws.onmessage = (event) => {
const message = event.data;
console.log('收到消息:', message);
this.receivedMessage = message; // 可以将消息存储到组件的 data 中
};
},
data() {
return {
receivedMessage: null
};
}
};
5. IndexedDB + 定时器轮询
IndexedDB 介绍
IndexedDB 是一种浏览器内置的 NoSQL 数据库,用于在客户端存储大量结构化数据。它支持复杂的数据结构,包括对象和二进制数据,并且可以通过索引快速检索。
主要特性
- 存储量大 :IndexedDB 的存储空间通常比
localStorage
更大,一般不少于 250MB,甚至没有上限。 - 异步操作:所有操作都是异步的,不会阻塞主线程。
- 支持事务:通过事务确保数据操作的原子性和一致性。
- 键值对存储:数据以键值对的形式存储,每个对象存储(object store)类似于数据库中的表。
- 支持索引:可以通过索引加速数据检索。
- 同源限制:IndexedDB 遵循同源策略,只能访问当前域名下的数据库。
基本概念
- 数据库(Database) :存储数据的容器,每个域名可以创建多个数据库。
- 对象存储(Object Store) :类似于数据库中的表,用于存储数据。
- 事务(Transaction) :用于执行数据库操作,确保数据一致性。
- 索引(Index) :用于加速数据检索。
- 游标(Cursor) :用于遍历对象存储中的记录。
bash
// 简化indexedDB操作
npm install idb
javascript
// edit.vue 创建数据页
import { openDB } from "idb";
export default {
methods: {
onSubmit() {
this.$message({
message: "创建成功!",
type: "success",
});
const dbPromise = openDB("sharedDB", 2, {
upgrade(db) {
if (!db.objectStoreNames.contains("messages")) {
db.createObjectStore("messages", {
keyPath: "id",
autoIncrement: true,
});
}
},
});
sendMessage();
const _that = this;
async function sendMessage() {
const db = await dbPromise;
const tx = db.transaction("messages", "readwrite");
const store = tx.objectStore("messages");
const message = { content: JSON.stringify(_that.formData) };
await store.add(message);
}
},
},
},
}
javascript
// index.vue 列表页
mounted() {
this.handleIndexedDB();
},
beforeDestroy() {
if (this.indexedDBTimer) {
clearInterval(this.indexedDBTimer);
}
},
methods: {
handleIndexedDB() {
const dbPromise = openDB("sharedDB", 2, {
upgrade(db) {
if (!db.objectStoreNames.contains("messages")) {
db.createObjectStore("messages", {
keyPath: "id",
autoIncrement: true,
});
}
},
});
const _that = this;
async function pollMessages() {
const db = await dbPromise;
const tx = db.transaction("messages", "readonly");
const store = tx.objectStore("messages");
const messages = await store.getAll();
if (messages.length) {
if (
!_that.tableData.some((item) => item.id === messages[0].id) &&
!messages[0].id
) {
_that.tableData.push(JSON.parse(messages[0].content));
_that.applyFilters();
}
}
}
// 每隔一段时间轮询一次
this.indexedDBTimer = setInterval(pollMessages, 1000);
},
}
6. shared worker + 定时器轮询
Shared Worker 是一种特殊的 Web Worker,允许多个浏览器上下文(如多个标签页、窗口或 iframe)共享同一个后台线程。与普通的 Dedicated Worker 不同,Shared Worker 的生命周期独立于页面,即使所有连接的页面都关闭,Shared Worker 仍可继续运行。
核心特点
- 共享性:多个页面可以共享同一个 Shared Worker,实现数据共享和通信。
- 持久性 :即使所有连接的页面关闭,Shared Worker 也不会立即终止,除非显式调用
close()
。 - 单线程:Shared Worker 内部仍然是单线程执行,通过事件循环处理多个连接的请求。
- 同源限制:Shared Worker 的脚本文件必须遵守同源策略。
javascript
// shared-worker.js
const connections = [];
onconnect = (event) => {
const port = event.ports[0];
connections.push(port);
port.onmessage = (event) => {
const message = event.data;
console.log(`Shared Worker received message: ${message}`);
// 转发消息到所有连接的端口(除了发送消息的端口)
connections.forEach((conn) => {
if (conn !== port) {
conn.postMessage(message);
}
});
};
port.onclose = () => {
console.log('Connection closed');
connections.splice(connections.indexOf(port), 1);
};
};
javascript
//edit.vue 创建数据页
mounted:{
this.worker = new SharedWorker('./shared-worker.js');
this.worker.port.start();
},
methods: {
onSubmit() {
this.$message({
message: "创建成功!",
type: "success",
});
this.worker.port.postMessage(this.formData);
},
},
}
javascript
// index.vue 列表页
mounted() {
this.worker = new SharedWorker('./shared-worker.js');
this.worker.port.start();
this.worker.port.onmessage = (event) => {
this.receivedMessage = event.data;
this.$message.info(`收到新消息: ${this.receivedMessage}`);
};
},
7. service Worker
Service Worker 是一种运行在浏览器背景中的脚本,用于实现诸如离线支持、消息推送、后台同步等功能。它为 Web 应用提供了更强大的功能和更好的用户体验,尤其是在网络不稳定或离线状态下。
1. 主要功能
- 离线支持:通过缓存资源,Service Worker 可以在离线状态下为用户提供页面内容。
- 消息推送:即使页面未打开,Service Worker 也可以接收和处理推送消息。
- 后台同步:允许在后台同步数据,例如自动上传用户数据。
- 自定义网络请求:拦截和修改网络请求,实现缓存优先、网络优先等策略。
- 性能优化:通过缓存静态资源,减少网络请求,提升页面加载速度。
2. 工作原理
Service Worker 是一个独立于主线程运行的脚本,它通过事件驱动的方式工作,主要监听以下事件:
install
:Service Worker 被安装时触发,通常用于缓存静态资源。activate
:Service Worker 被激活时触发,用于清理旧的缓存。fetch
:拦截网络请求,允许自定义响应。push
:接收推送消息。sync
:处理后台同步任务。
3. 生命周期
- 注册:通过 JavaScript 在页面中注册 Service Worker。
- 安装(Install) :Service Worker 被安装到浏览器中。
- 激活(Activate) :Service Worker 被激活并接管页面。
- 运行(Running) :Service Worker 开始处理事件。
- 更新:当 Service Worker 脚本更新时,会重新安装并激活。
javascript
// service-worker.js
self.addEventListener('message', (event) => {
const message = event.data;
console.log('Service Worker received message:', message);
// 将消息转发到所有客户端(标签页)
self.clients.matchAll().then((clients) => {
clients.forEach((client) => {
if (client.id !== event.source.id) {
client.postMessage(message);
}
});
});
});
javascript
//edit.vue 创建数据页
mounted:{
this.registerServiceWorker();
},
methods: {
async registerServiceWorker() {
if ('serviceWorker' in navigator) {
await navigator.serviceWorker.register('/service-worker.js');
console.log('Service Worker 注册成功');
} else {
console.warn('Service Worker 不支持');
}
},
onSubmit() {
this.$message({
message: "创建成功!",
type: "success",
});
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage(this.formData);
}
},
},
}
javascript
// index.vue 列表页
mounted() {
this.registerServiceWorker();
this.setupMessageListener();
},
methods: {
async registerServiceWorker() {
if ('serviceWorker' in navigator) {
await navigator.serviceWorker.register('/service-worker.js');
console.log('Service Worker 注册成功');
} else {
console.warn('Service Worker 不支持');
}
},
setupMessageListener() {
navigator.serviceWorker.addEventListener('message', (event) => {
console.log(event)
});
}
}
8. BroadCast Channel
Broadcast Channel 是一种用于在同一来源(协议、域名、端口)下不同浏览器上下文(如标签页、iframe、Worker、Service Worker)之间进行消息广播的 Web API。它提供了一种简单、高效且实时的通信机制,适用于跨窗口或标签页的实时数据同步。
工作原理
- 创建频道 :通过
new BroadcastChannel(name)
创建一个广播频道实例,name
是一个字符串,用于标识频道。 - 发送消息 :调用
channel.postMessage(message)
方法向频道发送消息,消息可以是任意类型的数据。 - 接收消息 :在其他上下文中创建相同名称的频道实例,并通过
channel.onmessage
或addEventListener('message', callback)
监听消息。 - 关闭频道 :不再需要时,调用
channel.close()
关闭频道以释放资源。
核心特性
- 同源策略:仅支持同源上下文之间的通信,确保安全性。
- 实时通信:消息传递几乎是即时的,适合需要即时同步的场景。
- 简单易用:API 简洁明了,易于实现和维护。
使用场景
- 用户状态同步:当用户在一个标签页中登录或注销时,其他标签页可以实时同步状态。
- 实时数据更新:在多个标签页中实时显示更新的数据,例如股票行情或聊天消息。
- 多标签页协同工作:用户在多个标签页中操作同一个应用时,各标签页可以协同工作。
注意事项
- 不支持跨域 :
BroadcastChannel
仅适用于同源上下文,无法用于跨域通信。 - 不持久化消息:页面刷新或关闭后,之前的消息无法再获取。
- 浏览器兼容性 :现代浏览器普遍支持
BroadcastChannel
,但在使用时需检查兼容性。
javascript
// broadcastChannel.js
const broadcastChannel = {
channel: null,
createChannel(channelName) {
this.channel = new BroadcastChannel(channelName);
},
sendMessage(message) {
if (this.channel) {
this.channel.postMessage(message);
}
},
receiveMessage(callback) {
if (this.channel) {
this.channel.onmessage = (event) => {
callback(event.data);
};
}
},
closeChannel() {
if (this.channel) {
this.channel.close();
this.channel = null;
}
}
};
export default broadcastChannel;
javascript
//edit.vue 创建数据页
import broadcastChannel from '@/broadcastChannel.js';
mounted:{
broadcastChannel.createChannel('myChannel');
},
beforeDestroy() {
broadcastChannel.closeChannel();
},
methods: {
onSubmit() {
this.$message({
message: "创建成功!",
type: "success",
});
broadcastChannel.sendMessage(this.formData);
},
},
}
javascript
// index.vue 列表页
import broadcastChannel from '@/broadcastChannel.js';
mounted() {
broadcastChannel.createChannel('myChannel');
broadcastChannel.receiveMessage((data) => {
console.log(data)
});
},
beforeDestroy() {
broadcastChannel.closeChannel();
}