Vue3嵌套页面与 iframe 页面通信的 Hooks 使用说明

464 阅读3分钟

概述

本文介绍了如何使用 useNestedIframeuseIframePage 这两个 Vue 3 Hooks,来实现嵌套页面与 iframe 页面之间的通信和缓存管理。useNestedIframe 用于嵌套页面,管理与 iframe 的通信,useIframePage 则用于 iframe 页面,接收父页面发送的消息并管理缓存。

使用场景

  1. 父页面与 iframe 通信:父页面需要发送消息给 iframe,并接收其反馈。
  2. 缓存数据的管理:在 iframe 内部需要缓存数据并在页面之间切换时恢复这些数据。

useNestedIframe 使用说明

useNestedIframe Hook 主要用于嵌套页面,负责管理与 iframe 的通信、监听 iframe 消息、发送初始化信息以及清除缓存数据。

参数说明

  • iframeRef:iframe 的引用,用于定位 iframe 实例。
  • iframeSrc:iframe 的源地址,默认为空字符串。
  • config:配置对象,包含以下属性:
    • parentId:父页面标识符。
    • enableCache:是否启用缓存。
  • onReceiveMessage:回调函数,处理从 iframe 收到的消息。

返回值

  • sendMessageToIframe:向 iframe 发送消息的函数。
  • isIframeLoaded:指示 iframe 是否加载完成的布尔值。
  • clearCacheData:清除 iframe 缓存数据的函数。

示例代码

import { useNestedIframe } from '@/hooks/useIframe';
import { ref } from 'vue';

const iframeRef = ref(null);
const iframeSrc = ref('https://example.com/iframe');

const { sendMessageToIframe, clearCacheData, isIframeLoaded } = useNestedIframe({
    iframeRef,
    iframeSrc,
    config: { parentId: 'tianyi', enableCache: true },
    onReceiveMessage: (event) => {
        console.log('父页面收到来自 iframe 的消息:', event.data);
        // 处理收到的消息
    },
});

// 示例:向 iframe 发送消息
sendMessageToIframe({ type: 'custom', content: 'Hello, iframe!' });

// 示例:清除缓存数据
clearCacheData();

useIframePage 使用说明

useIframePage Hook 主要用于 iframe 页面,负责接收父页面发送的消息、管理缓存数据并与父页面通信。

参数说明

  • onReceiveMessage:收到父页面消息时的回调函数。
  • cacheData:需要缓存的数据,默认为空对象。

返回值

  • sendMessageToParent:向父页面发送消息的函数。

示例代码

import { useIframePage } from '@/hooks/useIframe';
import { ref } from 'vue';

const cacheData = ref({ key: 'initialValue' });

const { sendMessageToParent } = useIframePage({
    onReceiveMessage: (data) => {
        console.log(`父级页面传入的数据:${data}`);
    },
    cacheData,
});

// 示例:向父页面发送消息
sendMessageToParent({ type: 'ready' });

功能描述

1. 消息通信

useNestedIframeuseIframePage 通过 window.postMessage 实现消息通信。父页面可以使用 sendMessageToIframe 发送消息给 iframe,iframe 可以使用 sendMessageToParent 向父页面发送消息。

2. iframe 加载管理

useNestedIframe 提供了 isIframeLoaded 属性,可以用于监听 iframe 加载的状态。

3. 缓存数据管理

useIframePage 提供了缓存管理功能,当 config.enableCachetrue 时,cacheData 会被保存到 localStorage,并在重新加载时恢复。

4. 安全性

两个 Hook 都通过 allowedOrigins 对消息来源进行校验,以保证只接收来自可信来源的消息。

注意事项

  1. 允许的来源校验allowedOrigins 用于限定消息的来源,确保通信安全。
  2. 缓存启用:确保在需要持久化数据时启用缓存功能,以减少数据丢失风险。
  3. iframe 加载状态:在向 iframe 发送消息之前,应确认其加载完成。

适用场景

  • 父页面需要控制嵌套 iframe 的行为,例如设置参数或清空缓存。
  • iframe 页面需要与父页面同步状态,或在重新加载时恢复数据。

完整代码

import { ref, onMounted, onUnmounted, computed, watch } from 'vue';

/**
 * 嵌套页面使用的 hook,负责管理与 iframe 的通信和缓存
 *
 * @param {Object} options 配置项
 * @param {Object} options.iframeRef iframe 的引用,默认为 null
 * @param {String} options.iframeSrc iframe 的源地址,默认为空字符串
 * @param {Object} options.config 缓存配置,包括父页面标识符和缓存启用标志
 * @returns {Object} 返回一个对象,包含以下属性:
 *  - sendMessageToIframe: 向 iframe 发送消息的函数
 */
export function useNestedIframe({ iframeRef = null, iframeSrc = ref(''), config = { parentId: '', enableCache: false }, } = {}, onReceiveMessage) {
    const targetOrigin = ref('');
    // 设置允许的来源
    const allowedOrigins = computed(() => [targetOrigin.value]);
    const isIframeLoaded = ref(false);

    // 计算 targetOrigin,当 iframeSrc 变化时更新
    watch(iframeSrc, (newSrc) => {
        try {
            const url = new URL(newSrc);
            targetOrigin.value = url.origin;
        } catch (error) {
            console.warn('Invalid iframeSrc:', newSrc);
            targetOrigin.value = '';
        }
    }, { immediate: true });

    // 向 iframe 发送消息
    const sendMessageToIframe = (message) => {
        if (iframeRef?.value?.contentWindow) {
            const messageWithConfig = {
                ...message,
                config
            };
            iframeRef.value.contentWindow.postMessage(messageWithConfig, targetOrigin.value);
        } else {
            console.warn('iframeRef 未定义或 iframe 尚未加载');
        }
    };

    // 处理收到的消息
    const handleMessage = (event) => {
        const { origin, data } = event;
        const isAllowedOrigin = !allowedOrigins.value.length || allowedOrigins.value.includes(origin);
        console.log(allowedOrigins.value, origin, 'isAllowedOriginisAllowedOrigin')
        if (!isAllowedOrigin) {
            console.warn(`收到来自未授权源 ${origin} 的消息,已忽略`);
            return;
        }
        console.log(`收到ifram消息:${data}`)

        if (data.type === 'ready') {
            console.log('收到 iframe ready 消息,发送初始化配置');
            sendMessageToIframe({ type: 'init' });
        }

        onReceiveMessage?.(event);
    };

    // 清除缓存数据
    const clearCacheData = () => {
        sendMessageToIframe({ type: 'clear-cache' });
    };

    // 处理 iframe 加载完成事件
    const handleIframeLoad = () => {
        isIframeLoaded.value = true;
        console.log('iframe 页面已加载');
    };

    // 监听消息事件
    onMounted(() => {
        window.addEventListener('message', handleMessage);
        if (iframeRef?.value) {
            iframeRef.value.addEventListener('load', handleIframeLoad);
        }
    });

    // 组件卸载时移除事件监听
    onUnmounted(() => {
        window.removeEventListener('message', handleMessage);
        if (iframeRef?.value) {
            iframeRef.value.removeEventListener('load', handleIframeLoad);
        }
    });

    return {
        sendMessageToIframe,
        isIframeLoaded,
        clearCacheData
    };
}

/**
 * iframe 页面使用的 hook,负责接收父页面发送的消息和管理缓存
 *
 * @param {Object} options 配置项
 * @param {Function} options.onReceiveMessage 收到消息时的回调函数,默认为 null
 * @returns {Object} 返回一个对象,包含以下属性:
 *  - sendMessageToParent: 向父页面发送消息的函数
 */
export function useIframePage({ onReceiveMessage = null, cacheData = ref({}) } = {}) {
    const config = ref({ parentId: '', enableCache: false });
    const cacheKey = computed(() => `${config.value.parentId}-iframeData`);
    const targetOrigin = ref('');
    // 设置允许的来源
    const allowedOrigins = computed(() => [targetOrigin.value]);
    // 向父窗口发送消息
    const sendMessageToParent = (message) => {
        console.log(allowedOrigins.value)
        if (window.parent) {
            window.parent.postMessage(message, targetOrigin.value); // 发送到父页面
        } else {
            console.warn('没有父窗口可发送消息');
        }
    };

    // 监听 cacheData 的变化并保存到本地
    watch(cacheData, (newData) => {
        if (config.value.enableCache) {
            try {
                localStorage.setItem(cacheKey.value, JSON.stringify(newData));
                console.log('缓存数据已更新');
            } catch (error) {
                console.warn('无法保存缓存数据:', error);
            }
        }
    }, { deep: true });

    // 从缓存中恢复数据
    const restoreCachedData = () => {
        if (config.value.enableCache) {
            const cachedData = localStorage.getItem(cacheKey.value);
            if (cachedData) {
                try {
                    cacheData.value = JSON.parse(cachedData);
                    console.log('缓存数据已恢复');
                } catch (error) {
                    console.warn('缓存数据解析失败:', error);
                }
            }
        }
    };

    // 清除缓存数据
    const clearCacheData = () => {
        localStorage.removeItem(cacheKey.value);
        console.log('缓存数据已清除');
    };

    // 处理收到的消息
    const handleMessage = (event) => {
        const { origin, data } = event;
        const isAllowedOrigin = !allowedOrigins.value.length || allowedOrigins.value.includes(origin);
        console.log(allowedOrigins.value, origin, 'isAllowedOriginisAllowedOrigin')
        if (!isAllowedOrigin) {
            console.warn(`收到来自未授权源 ${origin} 的消息,已忽略`);
            return;
        }
        console.log(`收到父级${config.value.parentId}消息:${data}`)
        if (data.config) {
            config.value = data.config;
        }

        if (data.type === 'init') {
            console.log('收到初始化消息,恢复缓存数据');
            restoreCachedData();
        }

        if (data.type === 'clear-cache') {
            console.log('收到清除缓存消息');
            clearCacheData();
        }

        onReceiveMessage?.(event);
    };
    const setTargetOrigin = () => {
        const urlParams = new URLSearchParams(window.location.search);
        const originFromParams = urlParams.get('parentOrigin');
        if (originFromParams) {
            targetOrigin.value = decodeURIComponent(originFromParams);
            return
        }
        // 尝试从 document.referrer 获取
        try {
            const referrerUrl = new URL(document.referrer);
            targetOrigin.value = referrerUrl.origin;

        } catch (error) {
            console.warn('无法获取父页面的 origin');
            return '*'; // 或者根据需求设置默认值
        }
    };

    // 监听消息事件
    onMounted(() => {
        window.addEventListener('message', handleMessage);
        setTargetOrigin()
        sendMessageToParent({ type: 'ready' });
    });

    // 组件卸载时移除事件监听
    onUnmounted(() => {
        window.removeEventListener('message', handleMessage);
    });

    return {
        sendMessageToParent,
    };
}