写在开头
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 宽度变化可能影响页面布局。
经过权衡与基于实际开发,小编最终是选择了方案三,它平衡了开发复杂度和用户体验,对业务系统侵入性最小,代码维护性好。✅
🚀实现过程
为了演示方便,咱们需要准备两个项目:
- Vue项目 :作为实际的AI聊天助手项目(iframe内容)。
- 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) → 跳转到具体业务页面
至此,本篇文章就写完啦,撒花撒花。

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。