基于postMessage实现iframe中的抽屉交互🤔

写在开头

Hello,各位好呀!

最近,连日阴雨,两双鞋都湿得透透的😫,打工人的心酸莫过于加班后,在下班路上遭遇暴雨,那会不仅是身体上有凉意侵袭,心里也是拔凉拔凉的。😔

期待这个周末能放晴吧,出门接触一下自然,恢复恢复元气,否则人快发霉嗷❗

然后呢,半年已至,小编终于在新公司顺利转正啦,也算是喜事一件。🎉

还有,就是这两天又收到一个好友年底11月份要结婚,提前来和我预约时间了😂,看着身边的人陆续步入婚姻的殿堂,心里有一种说不出的感觉,当然,更多的是为相识的朋友能找到一生伴侣而欣喜。

最后,就是看了一段毒鸡汤,也分享给大家:

我们穷尽一生所追求的幸福,不在过去,也不在未来,而是在当下,眼中景,碗中餐,身边人。三餐四季,家人闲坐,幸福安康,灯火可亲,便是人间好光景。

好啦,回到正题,本次要分享的是关于在固定宽度的 iframe 中实现抽屉交互效果,主要是通过 postMessage 实现动态布局扩展。效果如下,请诸君按需食用哈。

🎯需求背景

起初,小编在开发有一个类似AI聊天助手 的项目,它是基于Dify的项目进行的二开。对于不了解Dify的小伙伴,可以参考官方文档: 传送门。这是为什么小编的AI聊天助手项目是通过iframe嵌入到其他业务系统的原因------因为Dify提供的二开项目就是如此集成到其他项目中的。✅

然后呢,小编在这基础之上进行后续迭代开发的,但这不重要,重要的是在产品经理的"威逼"下,这次要在 iframe 中增加抽屉的交互,以提升用户体验。

为什么需要抽屉交互❓

在小编的实际项目中,有一些图表和表格需要展示。最初小编是通过在 iframe 中弹窗展示这些内容的,但是由于 iframe 本身就比较小(项目中固定了 500px 的宽度),用弹窗就更小了,所以展示效果很不理想。用户看图表和表格时体验极差,数据密密麻麻挤在一起,根本看不清楚。

于是产品经理改了交互方案,要求采用抽屉的形式:

  • 默认状态:iframe保持500px宽度,显示聊天界面。
  • 展开状态:iframe扩展到1000px宽度(实际中采用了60%,会比较好哦😋),左侧500px显示抽屉内容,右侧500px保持聊天界面这样既保证了聊天功能的正常使用,又为数据展示提供了足够的空间。

📝三种实现方案

面对这个需求,虽然它不难,但是小编也思考了几种可能的实现方案,咱们一一来瞧瞧哈👉:

方案1️⃣:

思路:将抽屉交由业务系统来完成, AI聊天助手 的项目仅通过 postMessage 告诉业务系统什么时候打开或关闭抽屉就行。

优点:

  • 实现简单,iframe内部只需要发送消息即可。
  • 不受iframe宽度限制,抽屉可以任意大小。
  • 业务系统有完全的控制权,可以灵活定制,样式可以与业务系统保持一致。

缺点:

  • 需要业务系统配合开发抽屉功能,增加了业务系统的开发工作量。
  • 如果多个系统都需要集成,每个系统都要实现抽屉逻辑。
  • 对业务系统有一定的侵入性。

方案2️⃣:

思路: 抽屉交由 JS 来动态生成,由于我们会单独写一个 JS 来动态插入iframe,所以,也能顺便动态生成另外的抽屉交互。

优点:

  • 对业务系统侵入性最小,只需要引入一个 JS 文件。
  • 可以实现完全独立的抽屉功能,一次开发,多处复用。

缺点:

  • 开发复杂度最高,需要处理 DOM 操作、样式冲突等问题。
  • 调试和维护相对困难。

方案3️⃣:(推荐)

思路: 抽屉交由 AI聊天助手 的项目来完成,但是需要通过 postMessage 来通知让 iframe 改变宽度,抽屉需要延时一点时间,让抽屉的交互效果展现。

优点:

  • 抽屉逻辑集中在AI助手项目中,便于维护。
  • 业务系统只需要监听消息并调整 iframe 宽度,对业务系统改动最小,实现简单。

缺点:

  • 需要精确控制动画时序。
  • 抽屉宽度受限于 iframe 的最大宽度,iframe 宽度变化可能影响页面布局。

经过权衡与基于实际开发,小编最终是选择了方案三,它平衡了开发复杂度和用户体验,对业务系统侵入性最小,代码维护性好。✅

🚀实现过程

为了演示方便,咱们需要准备两个项目:

  1. Vue项目 :作为实际的AI聊天助手项目(iframe内容)。
  2. HTML文件 + JS文件 :作为宿主页面,动态生成 iframe。

注意:在实际项目中,这个JS文件最好是放在 AI 聊天助手项目那边统一管理,其他业务系统想集成 AI 聊天助手项目就去引用它提供的 JS 文件即可,但这里为了演示方便就和HTML文件放一起啦。😋

第一步:创建Vue项目(iframe内容)

首先,创建细节咱们就不多说啦,直接来瞧瞧 iframe 内部的实现,这是一个Vue3项目,核心在于布局设计和postMessage通信:

js 复制代码
<template>
  <div class="ai-assistant">
    <!-- 抽屉部分 -->
    <div class="drawer-section" :class="{ 'drawer-expanded': isDrawerExpanded }">
      <div v-if="isDrawerExpanded" class="drawer-content">
        <div class="drawer-header">
          <h3>📚 抽屉内容区域</h3>
          <div class="drawer-close-icon" @click="toggleDrawer">→</div>
        </div>
        <div class="drawer-body">
          <p>这里可以放置图表、表格等需要更大空间的内容...</p>
        </div>
      </div>
    </div>
    <!-- 聊天区域部分 -->
    <div class="chat-section">
      <div class="header">
        <h2>🤖 AI智能助手</h2>
      </div>
      <div class="chat-area">
        <div class="messages">
          <div class="message bot-message">
            👋 你好!我是AI助手,有什么可以帮助你的吗?
          </div>
          <div class="message bot-message">
            点击下面的按钮可以展开抽屉查看更多内容哦!
            <button @click="toggleDrawer" class="drawer-btn">
              {{ isDrawerExpanded ? '📦 收起抽屉' : '📖 展开抽屉' }}
            </button>
          </div>
        </div>
        <div class="input-area">
          <input type="text" placeholder="输入你的问题..." class="chat-input">
          <button class="send-btn">发送</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const isDrawerExpanded = ref(false)

/**
 * @name 切换抽屉状态
 */
function toggleDrawer() {
  if (isDrawerExpanded.value) {
    // 收起抽屉 - 先设置状态,再延时通知父窗口
    isDrawerExpanded.value = false
    setTimeout(() => {
      const message = {
        type: 'toggle-drawer',
        action: 'collapse',
        width: '500px'
      }
      window.parent.postMessage(message, '*')
      console.log('向父窗口发送消息:', message)
    }, 300) // 300毫秒延时,让抽屉动画完成
  } else {
    // 展开抽屉 - 立即通知父窗口和设置状态
    isDrawerExpanded.value = true
    const message = {
      type: 'toggle-drawer',
      action: 'expand',
      width: '1000px'
    }
    window.parent.postMessage(message, '*')
    console.log('向父窗口发送消息:', message)
  }
}

/**
 * @name 处理父窗口消息
 * @param {Event} event 
 */
function handleMessage(event) {
  try {
    const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data
    if (data && data.type === 'toggle-drawer') {
      console.log('收到父窗口消息:', data)
      if (data.action === 'expand') {
        isDrawerExpanded.value = true
      } else if (data.action === 'collapse') {
        isDrawerExpanded.value = false
      }
    }
  } catch (error) {
    console.error('处理父窗口消息时出错:', error)
  }
}

onMounted(() => {
  // 监听来自父窗口的postMessage
  window.addEventListener('message', handleMessage)
})
</script>

<style scoped>
.ai-assistant {
  height: 100vh;
  position: relative;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  overflow: hidden;
  display: flex;
  justify-content: flex-end;
}
.drawer-section {
  width: 0;
  height: 100%;
  background-color: #fff;
  position: absolute;
  right: 500px;
  top: 0;
  transition: width 0.3s ease;
  overflow: hidden;
  box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
  border-right: 1px solid #e0e0e0;
}
.drawer-section.drawer-expanded {
  width: 500px;
}
.drawer-content {
  min-width: 500px;
  overflow: hidden;
  box-sizing: border-box;
  height: 100%;
  display: flex;
  flex-direction: column;
}
.chat-section {
  width: 500px;
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: #eaeaea;
}
/* ... */
</style>

这里关键点在于布局设计与延时处理,短暂的延时用户是无感知的,这能让抽屉有一个平滑的过渡。🚤

第二步:创建宿主页面的嵌入脚本

接下来是关键的嵌入脚本,这个脚本负责创建 iframe 并处理 postMessage 通信:

js 复制代码
(function () {
  // 配置常量
  const configKey = "aiAssistantConfig";
  const buttonId = "ai-assistant-bubble-button";
  const iframeId = "ai-assistant-bubble-window";
  const config = window[configKey];

  // 主要嵌入函数
  async function embedAssistant() {
    if (!config) {
      console.error(`${configKey} 配置未找到`);
      return;
    }
    const iframeUrl = config.baseUrl;

    // 创建iframe
    function createIframe() {
      const iframe = document.createElement("iframe");
      iframe.allow = "fullscreen;microphone";
      iframe.title = "AI助手窗口";
      iframe.id = iframeId;
      iframe.src = iframeUrl;
      iframe.style.cssText = `
        border: none; position: fixed; flex-direction: column;
        top: 0; right: 0; width: 500px !important; height: 100vh !important;
        border-radius: 8px 0 0 8px; display: flex; z-index: 2147483647;
        overflow: hidden; background-color: #ffffff;
      `;
      
      // 创建关闭按钮等其他UI元素...
      return iframe;
    }

    // 创建聊天按钮
    function createButton() {
      const containerDiv = document.createElement("div");
      containerDiv.id = buttonId;
      // 按钮样式和事件处理...
      document.body.appendChild(containerDiv);
    }

    // 🎯 核心功能:监听postMessage,实现动态抽屉
    window.addEventListener("message", function (event) {
      try {
        const data = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
        // 检查是否是抽屉控制消息
        if (data && data.type === "toggle-drawer") {
          const targetIframe = document.getElementById(iframeId);
          if (!targetIframe) return;
          if (data.action === "expand") {
            targetIframe.style.width = data.width || "1000px";
            targetIframe.style.backgroundColor = "transparent";
          } else if (data.action === "collapse") {
            targetIframe.style.width = "500px";
          }
        }
      } catch (error) {
        console.error("处理postMessage时出错:", error);
      }
    });
  }

  // 初始化
  if (config?.dynamicScript) {
    embedAssistant();
  } else {
    document.body.onload = embedAssistant;
  }
})();

第三步:配置和使用

最后,在宿主(业务系统)页面中进行配置:

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>iframe中的抽屉演示</title>
</head>
<body>
  <div class="container">
    <h1>🚀 iframe中的抽屉演示</h1>
    <p>点击右下角的聊天按钮,体验抽屉交互效果!</p>
  </div>

  <script>
    // 配置AI助手
    window.aiAssistantConfig = {
      baseUrl: "http://localhost:5173", // Vue项目的地址
      isDev: true,
      draggable: true,
      dragAxis: "both"
      // ...
    };
  </script>
  
  <!-- 引入AI助手脚本 -->
  <script src="./assistant.js"></script>
</body>
</html>

这种配置方式继承了小编最开始提到的 Dify 项目的设计思路,简单方便,易于集成到各种项目中。

🎉总结

以上分享的代码经过小编的精简优化,相信大家理解起来应该不会太困难哈😋。整个方案的核心关键点就是通过 postMessage 机制来动态改变iframe的固定宽度,从而让iframe的操作范围变得更加灵活和宽广。

其实,在实际的项目开发中,咱们往往还需要将AI聊天助手项目中的数据传递给业务系统。这时候,动态创建的JS文件就还需要承担数据转发的职责,将接收到的数据进一步转发到业务系统中,形成完整的数据流转链路,这是一个点哈。

还有就是关于抽屉中呈现的内容,小编建议采用独立统一的设计理念,啥意思呢?大概就是抽屉专门用来渲染表格、图表等数据可视化内容,抽屉内容应该相对独立,不过度依赖具体业务逻辑。

如果你的系统中抽屉呈现的内容与业务系统关联性比较强,小编建议采用跳转行为的方式来处理:

txt 复制代码
AI聊天助手(postMessage) → 动态JS转发(postMessage) → 业务系统(postMessage) → 跳转到具体业务页面

至此,本篇文章就写完啦,撒花撒花。

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。

相关推荐
白晓明1 分钟前
HarmonyOS NEXT端云一体化云侧云函数介绍和开发
前端·harmonyos
白晓明13 分钟前
HarmonyOS NEXT端侧工程调用云函数能力实现业务功能
前端·harmonyos
锋利的绵羊15 分钟前
【小程序】迁移非主包组件以减少主包体积
前端·微信小程序·uni-app
jqq66615 分钟前
解析ElementPlus打包源码
前端·javascript·vue.js
乐予吕19 分钟前
Promise 深度解析:从原理到实战
前端·javascript·promise
P7Dreamer20 分钟前
优雅封装:Vue3 + Element Plus 智能紧凑型搜索组件开发实践
前端·javascript
Turing_01020 分钟前
HarmonyOS隐私保护全攻略:从入门到精通
前端
Turing_01021 分钟前
HarmonyOS应用安全全攻略:从系统到代码的全面防护
前端
Beginner x_u26 分钟前
[AJAX 实战] 图书管理系统下 编辑图书
前端·javascript·ajax·bootstrap
Ace_317508877628 分钟前
# 唯品会商品详情接口开发指南
前端