前端跨页面通信:从基础到工程化的全面指南

在现代Web应用开发中,跨页面通信已成为不可或缺的核心技术。随着单页应用(SPA)和微前端架构的普及,开发者需要在不同浏览器上下文(如标签页、窗口、iframe)之间传递数据和消息。本文将系统介绍前端跨页面通信的主流方法,从原理、应用场景到工程化实现,帮助开发者构建更高效、安全的通信架构

一、跨页面通信的分类与适用场景

前端跨页面通信主要分为两大类:同源场景和跨域/跨窗口场景。每种场景下有不同的通信方式,各有优缺点和适用场景。

同源场景下,通信方式主要基于浏览器的同源策略,允许同一协议、域名和端口的页面直接共享数据。主要方式包括:

  1. BroadcastChannel:基于频道的广播通信,简单高效,支持多Tab/窗口
  2. localStorage + storage事件:通过存储变化触发事件,适合简单数据同步
  3. SharedWorker:共享线程,适合复杂状态管理
  4. Service Worker:后台运行,可作为通信桥梁

跨域/跨窗口场景下,由于同源策略限制,需要使用特定技术实现通信:

  1. window.postMessage:最通用、安全的跨域通信方式
  2. MessageChannel:创建点对点双向通道,需结合postMessage转移端口
  3. WebSocket:依赖服务端,实现全双工实时通信
  4. URL/Hash传参:适用于页面跳转时的简单数据传递

二、window.postMessage

原理说明

window.postMessage是HTML5引入的一个安全的跨源通信方法,它允许不同源的窗口之间进行消息传递 。其核心原理是通过事件机制在不同窗口间传递数据,发送方调用postMessage方法发送消息,接收方监听message事件接收消息。消息传递过程需要明确指定目标窗口和源(origin),以确保安全性。

工程化实现

在React中,我们可以封装一个自定义Hook来管理跨域通信:

typescript 复制代码
// usePostMessage.ts
import { useEffect, useState } from 'react';

interface PostMessageOptions {
  targetOrigin: string;
  transfer?: any[];
}

type PostMessageResult = {
  postMessage: (data: any, options: PostMessageOptions) => void;
  onMessage: (handler: (event: MessageEvent) => void) => void;
};

export function usePostMessage(): PostMessageResult {
  const [messageHandler,setMessageHandler] = useState<((event: MessageEvent) => void) | null>(null);

  // 发送消息
  const postMessage = (data: any, options: PostMessageOptions) => {
    const { targetOrigin, transfer } = options;
    const childWindow = window.open('https://subdomain.example.com', '_blank');
    childWindow?.postMessage(data, targetOrigin, transfer);
  };

  // 监听消息
  useEffect(() => {
    if (!messageHandler) return;

    const handleWindowMessage = (event: MessageEvent) => {
      if (event.origin !== 'https://trusted-domain.com') return; // 安全验证
      messageHandler(event);
    };

    window.addEventListener('message', handleWindowMessage);
    return () => {
      window.removeEventListener('message', handleWindowMessage);
    };
  }, [messageHandler]);

  return {
    postMessage,
    onMessage: (handler) =>setMessageHandler(handler),
  };
}

在Vue3中,可以创建一个插件来管理postMessage通信:

typescript 复制代码
// message-plugin.ts
import { Plugin, onMounted, onUnmounted } from 'vue';

export const messagePlugin = {
  install(app: Plugin, options: any) {
    let messageHandler: ((event: MessageEvent) => void) | null = null;

    // 发送消息
    const postMessage = (data: any, targetOrigin: string) => {
      const childWindow = window.open('https://subdomain.example.com', '_blank');
      childWindow?.postMessage(data, targetOrigin);
    };

    // 监听消息
    const listenMessage = (handler: (event: MessageEvent) => void) => {
      messageHandler = handler;
      window.addEventListener('message', handleWindowMessage);
    };

    // 取消监听
    const unlistenMessage = () => {
      messageHandler = null;
      window.removeEventListener('message', handleWindowMessage);
    };

    const handleWindowMessage = (event: MessageEvent) => {
      if (!messageHandler) return;
      if (event.origin !== 'https://trusted-domain.com') return; // 安全验证
      messageHandler(event);
    };

    app.config.globalProperties.$message = {
      postMessage,
      listenMessage,
      unlistenMessage,
    };
  },
};

// 在main.js中使用
import { createApp } from 'vue';
import App from './App.vue';
import messagePlugin from './message-plugin';

const app = createApp(App);
app.use(messagePlugin);
app.mount('#app');

三、BroadcastChannel:同源多Tab通信的轻量级解决方案

原理说明

BroadcastChannelAPI允许同源下的不同浏览器上下文(标签页、窗口、iframe等)之间通过命名频道进行广播式通信 。它采用发布-订阅模式,发送方通过postMessage发送消息,所有订阅该频道的接收方都会收到消息。相比window.postMessage,BroadcastChannel更简洁高效,无需维护窗口引用

工程化实现

在Vue3中,我们可以封装一个自定义Hook来管理BroadcastChannel通信:

typescript 复制代码
// useBroadcastChannel.ts
import { ref, onMounted, onUnmounted } from 'vue';

interface BroadcastChannelOptions {
  name: string;
  onMessage?: (data: any) => void;
}

export function useBroadcastChannel(
  options: BroadcastChannelOptions
): {
  postMessage: (data: any) => void;
  listen: (handler: (data: any) => void) => void;
  close: () => void;
} {
  const { name, onMessage } = options;
  const channel = ref(new BroadcastChannel(name));

  // 发送消息
  const postMessage = (data: any) => {
    channel.value.postMessage(data);
  };

  // 监听消息
  const listen = (handler: (data: any) => void) => {
    channel.value.onmessage = (event) => {
      handler(event.data);
    };
  };

  // 关闭频道
  const close = () => {
    channel.value.close();
    channel.value = null;
  };

  // 初始化监听
  onMounted(() => {
    if (onMessage) {
      listen(onMessage);
    }
  });

  // 清理资源
  onUnmounted(() => {
    close();
  });

  return {
    postMessage,
    listen,
    close,
  };
}

使用示例:

html 复制代码
<template>
  <div>
    <input v-model="message" placeholder="输入消息" />
    <button @click="sendMessage">发送</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useBroadcastChannel } from './useBroadcastChannel';

const message = ref('');
const bcName = 'cart更新';

// 使用BroadcastChannel
const { postMessage, listen, close } = useBroadcastChannel({
  name: bcName,
  onMessage: (data) => {
    console.log('收到购物车更新', data);
  },
});

// 发送消息
const sendMessage = () => {
  postMessage({
    type: 'UPDATE_CART',
    data: {商品ID: 1001, 数量: message.value },
  });
};

// 监听其他消息
listen((data) => {
  if (data.type === 'LOGIN_STATUS') {
    console.log('登录状态变更', data.data);
  }
});
</script>

四、localStorage + storage事件:简单数据同步的可靠选择

原理说明

localStorage是浏览器提供的一种持久化存储机制,同一域名下的所有页面都可以访问 。当localStorage中的数据发生变化时,除了修改页面外的其他页面会触发storage事件。这种机制适合简单的数据同步场景,无需服务端参与 ,但需要注意容量限制(通常为5MB)和非实时性。

工程化实现

在React中,我们可以封装一个组件来监听localStorage变化:

typescript 复制代码
// LocalStorageListener.tsx
import { useEffect, useState } from 'react';

interface LocalStorageProps {
  key: string;
  onStorageChange: (data: any) => void;
}

export default function LocalStorageListener({
  key,
  onStorageChange,
}: LocalStorageProps) {
  const [data,setData] = useState(null);

  useEffect(() => {
    // 初始化数据
    const storedData = localStorage.getItem(key);
    if (storedData) {
     setData(JSON.parse(storedData));
    }

    // 监听storage事件
    const handleStorage = (event: StorageEvent) => {
      if (event.key === key) {
        const newData = event.newValue ? JSON.parse(event.newValue) : null;
       setData(newData);
        onStorageChange(newData);
      }
    };

    window.addEventListener('storage', handleStorage);
    return () => {
      window.removeEventListener('storage', handleStorage);
    };
  }, [key, onStorageChange]);

  return null;
}

在Vue3中,可以创建一个组合式函数:

typescript 复制代码
// useLocalStorage.ts
import { ref, onMounted, onUnmounted } from 'vue';

interface LocalStorageOptions {
  key: string;
  fallbackValue?: any;
  parse?: (value: string) => any;
}

export function useLocalStorage(
  options: LocalStorageOptions
): {
  value: Ref < any >;
  save: () => void;
  remove: () => void;
} {
  const { key, fallbackValue = null, parse = JSON.parse } = options;
  const value = ref(fallbackValue);

  // 初始化
  const initializing = ref(true);
  const storedValue = localStorage.getItem(key);
  if (storedValue) {
    value.value = parse(storedValue);
  }
  initializing.value = false;

  // 保存到localStorage
  const save = () => {
    if (!initializing.value) {
      localStorage.setItem(key, JSON.stringify(value.value));
    }
  };

  // 移除数据
  const remove = () => {
    localStorage.removeItem(key);
    value.value = fallbackValue;
  };

  // 监听storage事件
  const listenStorage = () => {
    const handleStorage = (event: StorageEvent) => {
      if (event.key === key && event源 !== window) {
        const newData = event.newValue ? JSON.parse(event.newValue) : null;
        value.value = newData;
      }
    };

    window.addEventListener('storage', handleStorage);
    return () => {
      window.removeEventListener('storage', handleStorage);
    };
  };

  onMounted(listenStorage);
  return { value, save, remove };
}

使用示例:

html 复制代码
<template>
  <div>
    <input v-model="cartData.value" placeholder="购物车数据" />
    <button @click="cartData.save">保存</button>
  </div>
</template>

<script setup>
import { useLocalStorage } from './useLocalStorage';

// 使用localStorage
const cartData = useLocalStorage({
  key: 'cart',
  fallbackValue: [],
  parse: (value: string) => JSON.parse(value),
});
</script>

五、MessageChannel API:点对点通信的高级选择

原理说明

MessageChannelAPI允许我们创建一个新的消息通道,并通过它的两个端口属性(port1port2)发送数据 。核心是创建一个双向通讯的管道,这种"点对点"的通讯模式,从根源上避免了数据被无关页面拦截的风险 。消息在发送和接收的过程需要序列化和反序列化,类似于JSON的转换过程。

工程化实现

在React中,可以封装一个管理MessageChannel的组件:

typescript 复制代码
// MessageChannelManager.tsx
import { useEffect, useState, RefObject, forwardRef } from 'react';

interface MessageChannelRef {
  send: (data: any) => void;
  listen: (handler: (data: any) => void) => void;
  close: () => void;
}

const MessageChannelManager = forwardRef(
  (props: any, ref: RefObject < MessageChannelRef > ) => {
    const [channel,setChannel] = useState(new MessageChannel());
    const [ports,setPorts] = useState({
      port1: channel.port1,
      port2: channel.port2,
    });

    // 初始化
    useEffect(() => {
      ports.port1.start();
      ports.port2.start();

      // 传递port2给子应用
      const childWindow = window.open('https://subdomain.example.com', '_blank');
      childWindow?.postMessage(
        { type: 'SETUP_CHANNEL', port: ports.port2 },
        '*',
        [ports.port2] // 转移端口所有权
      );

      // 监听消息
      ports.port1.onmessage = (event) => {
        if (event.data.type === 'CART_UPDATE') {
          console.log('收到购物车更新', event.data.data);
        }
      };

      return () => {
        ports.port1.close();
        ports.port2.close();
      };
    }, []);

    // 更新端口
    const updatePorts = () => {
      const newChannel = new MessageChannel();
     setChannel(newChannel);
     setPorts({
        port1: newChannel.port1,
        port2: newChannel.port2,
      });
    };

    // 绑定ref
    useEffect(() => {
      ref.current = {
        send: (data: any) => ports.port1.postMessage(data),
        listen: (handler: (data: any) => void) => {
          ports.port1.onmessage = handler;
        },
        close: () => {
          ports.port1.close();
          ports.port2.close();
        },
      };
    }, [ports]);

    return null;
  }
);

export default MessageChannelManager;

在Vue3中,可以创建一个共享的MessageChannel服务:

typescript 复制代码
// message-channel-service.js
import { reactive, toRefs, onMounted, onUnmounted } from 'vue';

export default {
  install(app) {
    const state = reactive({
      channels: new Map(),
    });

    // 创建通道
    const createChannel = (channelName) => {
      if (state.channels.has(channelName)) {
        return state.channels.get(channelName);
      }

      const channel = new MessageChannel();
      state.channels.set(channelName, channel);

      // 监听消息
      channel.port1.onmessage = (event) => {
        console.log('收到消息:', event.data);
      };

      return channel;
    };

    // 发送消息
    const send = (channelName, data) => {
      const channel = createChannel(channelName);
      channel.port2.postMessage(data);
    };

    // 监听消息
    const listen = (channelName, handler) => {
      const channel = createChannel(channelName);
      channel.port1.onmessage = handler;
    };

    // 关闭通道
    const close = (channelName) => {
      const channel = state.channels.get(channelName);
      if (channel) {
        channel.port1.close();
        channel.port2.close();
        state.channels.delete(channelName);
      }
    };

    app.config.globalProperties.$essageChannel = {
      createChannel,
      send,
      listen,
      close,
    };
  },
};

使用示例:

html 复制代码
<template>
  <div>
    <input v-model="message" placeholder="输入消息" />
    <button @click="sendMessage">发送</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const message = ref('');
const bcName = 'cart更新';

// 使用MessageChannel
const { send, listen, close } = useMessageChannel(bcName);

// 发送消息
const发送消息 = () => {
  send({
    type: 'UPDATECart',
    data: {商品ID: 1001, 数量: message.value },
  });
};

// 监听消息
listen((data) => {
  if (data.type === 'LOGIN_STATUS') {
    console.log('登录状态变更', data.data);
  }
});

// 组件卸载时关闭通道
onUnmounted(() => {
  close(bcName);
});
</script>

六、Service Worker + indexedDB:离线通信的终极解决方案

原理说明

Service Worker是一个运行在浏览器后台的脚本,可以拦截网络请求并管理缓存资源 。结合indexedDB,它可以在浏览器中实现复杂的数据存储和同步机制。Service Worker本质上充当Web应用程序、浏览器与网络之间的代理服务器 ,通过监听message事件,可以将消息广播到所有注册了该Service Worker的标签页。

工程化实现

在Vue3中,我们可以创建一个Service Worker管理器:

typescript 复制代码
// ServiceWorkerManager.js
import { reactive, toRefs, onMounted, onUnmounted } from 'vue';

export default {
  install(app) {
    const state = reactive({
      swController: null,
      isReady: false,
    });

    // 注册Service Worker
    const register = () => {
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker
          .register('/sw.js')
          .then((registration) => {
            state.swController = registration活性;
            state.isReady = true;

            // 监听消息
            state.swController.addEventListener('message', (event) => {
              console.log('收到Service Worker消息:', event.data);
            });

            // 发送消息
            const sendToSw = (message) => {
              state.swController.postMessage(message);
            };

            // 接收消息
            const listenFromSw = (handler) => {
              state.swController.addEventListener('message', handler);
            };

            // 关闭连接
            const close = () => {
              state.swController.postMessage({ type: 'CLOSE' });
              state.swController = null;
              state.isReady = false;
            };

            // 将方法挂载到全局
            app.config.全局属性.serviceWorker = {
              isReady: computed(() => state.isReady),
              send: sendToSw,
              listen: listenFromSw,
              close: close,
            };
          })
          .catch((error) => {
            console.error('Service Worker注册失败:', error);
          });
      }
    };

    // 检查Service Worker状态
    const checkSwStatus = () => {
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker活性
          .then((controller) => {
            if (controller) {
              state.swController = controller;
              state.isReady = true;
            }
          })
          .catch((error) => {
            console.error('Service Worker检查失败:', error);
          });
      }
    };

    // 初始化
    onMounted(() => {
      register();
      checkSwStatus();
    });

    // 清理资源
    onUnmounted(() => {
      close();
    });

    return {
      ...toRefs(state),
    };
  },
};

在Service Worker脚本中,我们可以实现消息广播和indexedDB操作:

typescript 复制代码
// sw.js
importScripts(' indexedDB - helper.js');

// indexedDB初始化
const db = indexedDB.open('my-app-db', 1);
db.onupgradeneeded = (event) => {
  const db = event.target.result;
  db.createObjectStore('cart', { keyPath: 'id' });
};

// 消息处理
self.addEventListener('message', (event) => {
  const { type, data } = event.data;

  // 存储到indexedDB
  if (type === 'SAVE_CART') {
    saveToIndexedDB('cart', data);
  }

  // 广播消息
  if (type === 'BROADCAST') {
    clients.matchAll().then((clientList) => {
      clientList.forEach((client) => {
        client.postMessage(data);
      });
    });
  }

  // 关闭连接
  if (type === 'CLOSE') {
    self.postMessage({ type: 'CLOSED' });
  }
});

// indexedDB操作
const saveToIndexedDB = (storeName, data) => {
  const request = db.result.transaction(storeName, 'readwrite')
    .objectStore(storeName)
    .put(data);

  request.onsuccess = () => {
    console.log('数据保存成功');
  };

  request.onerror = () => {
    console.error('数据保存失败');
  };
};

七、综合案例:微前端购物车系统的通信架构

案例背景

我们设计一个微前端架构的电商平台,包含主应用和多个子应用(商品展示、购物车、订单等),分布在不同域上。用户在一个标签页中添加商品到购物车,其他标签页需要同步购物车内容 ;同时,用户在一个窗口中登录,所有打开的标签页都需要更新登录状态。

通信架构设计

我们的通信架构将结合多种通信方式,实现高效、安全的跨页面通信:

  1. 主应用与子应用通信 :使用postMessage传递登录状态和商品数据
  2. 子应用间状态同步 :使用SharedWorker共享购物车数据
  3. 多标签页广播 :使用BroadcastChannel通知其他标签页更新状态
  4. 持久化存储 :使用localStorageindexedDB保存购物车数据
工程化实现

主应用(React)

ts 复制代码
// 主应用购物车同步组件
import { useEffect, useState } from 'react';
import { usePostMessage } from './usePostMessage';
import { useBroadcastChannel } from './useBroadcastChannel';

const CartSync = () => {
  const [cartData, setCartData] = useState([]);
  const { postMessage, listen, close } = usePostMessage();
  const { postMessage: bcPostMessage } = useBroadcastChannel('cart更新');

  // 监听子应用消息
  useEffect(() => {
    const handler = (event: MessageEvent) => {
      if (event.data.type === 'CART_UPDATE') {
        setCartData(event.data.data);
        bcPostMessage({ type: 'UPDATE_CART', data: event.data.data });
      }
    };

    // 监听子应用消息
    listen(handler);

    // 监听其他标签页消息
    const bcHandler = (event) => {
      if (event.data.type === 'UPDATE Cart') {
        setCartData(event.data.data);
      }
    };

    window.addEventListener('storage', bcHandler);
    return () => {
      close();
      window.removeEventListener('storage', bcHandler);
    };
  }, []);

  return (
    <div>
      <h3>购物车同步</h3>
      <ul>
        {cartData.map((item, index) => (
          <li key={index}>
            {item.name} x {item.quantity}
          </li>
        ))}
      </ul>
      <button
        onClick={() => {
          // 通知所有子应用更新购物车
          postMessage(
            { type: 'UPDATE_CART', data: cartData },
            '*'
          );
        }}
      >
        更新所有子应用
      </button>
    </div>
  );
};

export default CartSync;

子应用(Vue3)

typescript 复制代码
// 子应用购物车组件
import { ref, onMounted, onUnmounted } from 'vue';
import { useLocalStorage } from './useLocalStorage';
import { useSharedWorker } from './useSharedWorker';

const Cart = () => {
  const { value: cartData, save } = useLocalStorage({
    key: 'cart',
    fallbackValue: [],
  });

  const { worker, listen, close } = useSharedWorker('cart-worker');

  // 监听主应用消息
  onMounted(() => {
    const handler = (event: MessageEvent) => {
      if (event.data.type === 'UPDATE_CART') {
        cartData.value = event.data.data;
        save();
        worker.postMessage({ type: 'UPDATE', data: cartData.value });
      }
    };

    // 监听主应用消息
    window.addEventListener('message', handler);

    onUnmounted(() => {
      window.removeEventListener('message', handler);
    });
  });

  // 监听SharedWorker消息
  onMounted(() => {
    const workerHandler = (event) => {
      if (event.data.type === 'UPDATE') {
        cartData.value = event.data.data;
        save();
      }
    };

    worker.addEventListener('message', workerHandler);

    onUnmounted(() => {
      worker.removeEventListener('message', workerHandler);
    });
  });

  // 更新购物车
  const updateCart = (newCart) => {
    cartData.value = newCart;
    save();
    worker.postMessage({ type: 'UPDATE', data: newCart });
  };

  return (
    <div>
      <h3>购物车</h3>
      <ul>
        {cartData.value.map((item, index) => (
          <li key={index}>
            {item.name} x {item.quantity}
          </li>
        ))}
      </ul>
      <button
        onClick={() => {
          // 添加商品到购物车
          updateCart([...cartData.value, { name: '新商品', quantity: 1 }]);
        }}
      >
        添加商品
      </button>
    </div>
  );
};

export default Cart;

SharedWorker

javascript 复制代码
// cart-worker.js
const ports = [];

onconnect = (event) => {
  const port = event.ports[0];
  ports.push(port);

  // 监听消息
  port.onmessage = (e) => {
    if (e.data.type === 'UPDATE') {
      // 更新所有端口
      ports.forEach((p) => {
        if (p !== port) {
          p.postMessage({ type: 'UPDATE', data: e.data.data });
        }
      });
    }
  };
};

八、通信方式选择建议与最佳实践

根据不同的应用场景和需求,以下是跨页面通信方式的选择建议:

通信方式 适用场景 优点 缺点 安全性
window.postMessage 跨域/跨窗口通信 最通用、最安全,支持复杂数据结构 需要手动传递窗口引用,无法持久化 高(需验证origin)
BroadcastChannel 同源多Tab广播通信 简洁高效,支持多对多通信 需要同源,移动端兼容性需注意 高(同源限制)
localStorage + storage事件 同源页面间简单数据同步 兼容性好,支持持久化存储 有容量限制,非实时 中(需手动验证)
SharedWorker 同源多Tab复杂状态同步 支持复杂场景,可跨浏览器窗口 实现复杂度较高,需处理连接管理 高(同源限制)
Service Worker + indexedDB 离线场景和多Tab数据同步 支持离线存储,可广播消息 需HTTPS环境,实现复杂度高 高(HTTPS要求)
URL/Hash传参 页面跳转传参 实现简单,兼容性好 仅能传递简单数据,非实时 低(数据暴露)
MessageChannel 跨上下文点对点通信 点对点安全高效,可传递复杂数据 需要手动管理端口生命周期 高(需验证来源)

最佳实践建议

  1. 安全性第一 :无论使用哪种通信方式,都应验证消息来源。对于postMessage,必须检查event origin;对于localStorage,应验证数据变更的来源。

  2. 按需选择:根据场景需求选择合适的通信方式。简单数据同步可使用localStorage;实时高频通信可使用postMessage或BroadcastChannel;复杂状态管理可使用SharedWorker或Service Worker。

  3. 工程化封装 :将通信逻辑封装为可复用的模块或组件,避免在业务代码中直接处理底层通信细节。例如,可以创建usePostMessageuseBroadcastChannel等自定义Hook。

  4. 错误处理:为通信过程添加错误处理机制,如超时、数据解析失败等。对于持久化存储,应处理存储空间不足的情况。

  5. 性能优化:避免频繁的通信操作,尤其是基于storage事件的方式。可以考虑使用消息队列或缓冲机制,减少不必要的通信开销。

  6. 兼容性处理:对于不支持某些API的浏览器,应提供降级方案。例如,如果浏览器不支持BroadcastChannel,可以使用localStorage + storage事件作为替代 。

总之,前端跨页面通信是构建现代Web应用的核心技术之一。通过合理选择和组合使用不同的通信方式,开发者可以构建高效、安全、可靠的跨页面通信架构。随着技术的发展,这一领域也将不断演进,为开发者提供更强大的工具和更简单的使用方式。

相关推荐
梵尔纳多2 小时前
electron 安装
前端·javascript·electron
心.c2 小时前
初步了解Next.js
开发语言·前端·javascript·js
挫折常伴左右2 小时前
初见HTML
前端·html
阿蓝灬2 小时前
详述单点登录(SSO)
前端
灵感__idea2 小时前
Hello 算法:以“快”著称的哈希
前端·javascript·算法
恋猫de小郭2 小时前
Flutter 官方正式解决 WebView 在 iOS 26 上有点击问题
android·前端·flutter
阿珊和她的猫3 小时前
CSS3新特性概述
前端·css·css3
前端小端长4 小时前
qiankun 微前端应用入门教程:从搭建到部署
前端
yinuo6 小时前
前端跨页面通讯终极指南⑥:SharedWorker 用法全解析
前端