如何利用 iframe 扩展我们的项目?
前言
在初涉HTML领域时,我便首次接触了<iframe>
标签,当时仅了解到它能够用于嵌入其他网页内容。然而,在参与实际项目开发过程中,我很少有机会使用这一标签。最近,一个特殊的项目需求使我意识到<iframe>
的强大潜力,它能够通过插件的形式为我们的项目带来扩展性。这一发现激发了我的兴趣,因此,我将在这篇文章中详细介绍如何利用<iframe>
标签来增强和扩展我们的项目功能。
ifrmae 通信
单纯地将一个网站嵌入到我们的项目中,这通常不足以应对大多数复杂场景的需求。在绝大多数情况下,我们需要解决的是跨域通信的问题。只有建立了有效的通信机制,我们才能实现数据的交换和共享。通过数据交换,我们能够实现自定义的业务逻辑,从而满足各种场景下的需求,确保项目的灵活性和扩展性。
为了深入理解<iframe>
的通信机制,我们首先准备两个HTML文件:iframe.html
和index.html
。在index.html
中,我们将嵌入iframe.html
。以下是我们将要实现的需求:
- 在
index.html
中输入文本后,能够将这段文本传递到iframe.html
并显示出来。 - 当在
iframe.html
中执行保存操作时,能够通知index.html
保存的文本内容。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iframe.html</title>
</head>
<body>
<textarea id="textareaRef" rows="5" cols="33">
Hello World
</textarea>
<button onclick="saveHandle()">保存</button>
<script>
function saveHandle() {
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>index.html</title>
</head>
<body>
<div class="iframe-wrapper">
<iframe id="iframeRef" height="400px" width="600px" src="http://127.0.0.1:5500/iframe-demo/iframe.html"></iframe>
</div>
<div class="insert-wrapper">
<input id="inputRef" type="text" /><button onclick="insertHandle()">插入</button>
</div>
<script>
function insertHandle() {
}
</script>
</body>
</html>
项目运行后,效果如下:
window.postMessage
实现 ifrmae
通信的一种实现方案是利用window.postMessage
,该 API 是 HTML5 引入的一个用于安全地实现跨源通信的解决方案。
其基本使用语法如下:
otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow
:指向接收消息的窗口的引用,比如 iframe 的 contentWindow 属性message
:将要发送到其他 window 的数据。targetOrigin
:通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串 “*”(表示无限制)或者一个 URI。transfer
(可选):是一串和 message 同时传递的 Transferable 对象。
在目标窗口中,可以监听 message
事件来接收消息:
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event) {
// 对于任何不是预期来源的消息,我们忽略它
if (event.origin !== "http://example.org:8080") {
return;
}
// 处理事件,可以使用 event.data 获取发送的数据
}
在学习了 window.postMessage
的基本使用后,接下来我们便利用该 API 实现我们的需求。
父传子
首先,我们通过 iframeRef
获取与之关联的 window
对象。接着,利用这个对象调用 postMessage
方法来发送消息。
// index.html
let iframeWindow = iframeRef.contentWindow;
function insertHandle() {
iframeWindow.postMessage({
event: 'insert',
data: inputRef.value,
}, '*');
}
为了能够接收发送方的消息,接收方还需要注册一个 message
事件监听器。
// iframe.html
window.addEventListener('message', (e) => {
if (e.data.event === 'insert') {
insertTextAtCursor(textareaRef, e.data.data);
}
});
function insertTextAtCursor(element, text) {
// 获取光标位置
var startPos = element.selectionStart;
var endPos = element.selectionEnd;
// 保存原始文本
var beforeText = element.value.substring(0, startPos);
var afterText = element.value.substring(endPos);
// 插入文本
element.value = beforeText + text + afterText;
// 恢复光标位置
element.selectionStart = startPos + text.length;
element.selectionEnd = startPos + text.length;
}
子传父
若要在 iframe.html
中向 index.html
传递消息,首先需要获取 index.html
的窗口引用。可以使用以下两个 API 来实现这一目的:
-
window.top: 这个属性引用的是最顶层的窗口,即包含当前窗口或框架的最外层窗口。例如,如果
index.html
嵌入了demo.html
,而demo.html
又嵌入了iframe.html
,那么在iframe.html
中要获取index.html
的窗口引用,就可以使用window.top
。 -
window.parent:这个属性引用的是当前窗口或框架的直接父窗口。在当前项目中,由于
iframe.html
是直接嵌入在index.html
中的,因此在iframe.html
中通过window.parent
API 就可以获取到index.html
的窗口引用。
成功获取父窗口的引用之后,我们可以利用 postMessage
方法来发送消息。
// iframe.html
function saveHandle() {
window.parent.postMessage({
event: 'save',
data: textareaRef.value
}, '*');
}
在另一端,父窗口则需要设置一个 message
事件监听器,以便能够接收并处理从子窗口传递过来的消息。
// index.html
window.addEventListener('message', (e) => {
if (e.data.event === 'save') {
alert(e.data.data);
}
});
至此,我们通过 window.postMessage
完成了网页与内嵌网页的通信。
dom 操作
第二种iframe通信方式是通过DOM操作实现的。然而,这种方法具有一定的局限性,它要求iframe和父页面必须位于同一主域名下。此外,为了实现通信,需要在父页面和iframe页面中分别设置 document.domain
属性为相同的主域名。
// 在父页面中
document.domain = 'example.com';
// 在 iframe 中
document.domain = 'example.com';
我们可以通过访问iframe
的 contentDocument
属性来获取其内部的DOM节点。需要注意的是,这一操作必须等待iframe
完全加载完成后才能进行,以确保能够成功获取到DOM节点。
// index.html
document.domain = '127.0.0.1';
let contentDocument = iframeRef.contentDocument;
let textareaRef;
iframeRef.onload = function() {
const contentDocument = iframeRef.contentDocument;
textareaRef = contentDocument.querySelector('.textareaRef');
const buttonRef = contentDocument.querySelector('button');
buttonRef.addEventListener('click', () => {
alert(textareaRef.value);
});
};
function insertHandle() {
insertTextAtCursor(textareaRef, inputRef.value);
}
function insertTextAtCursor(element, text) {
// 获取光标位置
var startPos = element.selectionStart;
var endPos = element.selectionEnd;
// 保存原始文本
var beforeText = element.value.substring(0, startPos);
var afterText = element.value.substring(endPos);
// 插入文本
element.value = beforeText + text + afterText;
// 恢复光标位置
element.selectionStart = startPos + text.length;
element.selectionEnd = startPos + text.length;
}
// iframe.html
document.domain = '127.0.0.1';
总结
window.postMessage
更适合用于跨源通信,尤其是需要在不同窗口之间安全地传递数据时。而iframe.contentDocument
主要用于同源策略下的DOM操作。
项目实战
在深入理解了iframe
的通信原理之后,我们将其应用于一项具体项目中。该项目背景是:我们从前端获取后端数据,并通过表格形式进行展示。尽管表格展示数据方便,但其处理能力有限。为了突破这一局限,我们引入了univer,以此增强数据处理和展示的灵活性及功能性。
univer-sheet-plugin
首先,初始化一个项目 univer-sheet-plugin
,该项目作为一个插件被其他项目集成,通过接收上层的数据来渲染数据并集成表格处理能力。
pnpm dlx degit dream-num/univer-sheet-start-kit univer-sheet-plugin
cd univer-sheet-plugin
pnpm i
pnpm run dev
接下来我们需要做的则是,接收传递给当前窗口的消息,并将数据渲染到 sheet 中。
// univer-sheet-plugin/src/main.ts
import './style.css'
import { setupUniver } from './setup-univer'
import { setupToolbar } from './setup-toolbar'
function main() {
const univerAPI = setupUniver()
setupToolbar(univerAPI)
window.addEventListener('message', (event) => {
if (event.data.event === 'insert') {
const values = event.data.data;
const activeWorkbook = univerAPI.getActiveWorkbook();
const activeSheet = activeWorkbook?.getActiveSheet()
const range = activeSheet?.getRange(0, 0, values.length, values[0].length)
if (range) {
range.setValues(values);
}
}
});
}
main()
my-project
pnpm create vue@latest
<template>
<div class="container">
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" width="180" />
<el-table-column prop="address" label="Address" />
</el-table>
<el-button type="primary" @click="visible = true">使用 univer 打开</el-button>
</div>
<UniverDialog v-model="visible" :table-data="tableData" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import UniverDialog from './components/univer-dialog.vue';
const visible = ref(false);
const tableData = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
];
</script>
<style scoped>
.el-button {
margin-top: 20px;
}
</style>
<template>
<ElDialog v-model="visible" width="80vw">
<iframe
width="100%"
height="800px"
ref="iframeRef"
src="http://localhost:5173/"
@load="onIframeLoad"
/>
</ElDialog>
</template>
<script lang='ts' setup>
import { ElDialog } from 'element-plus';
import { watch } from 'vue';
import { ref } from 'vue';
const props = defineProps<{
tableData: Array<object>,
}>();
const visible = defineModel<boolean>()
const iframeRef = ref();
const iframeLoaded = ref(false);
const onIframeLoad = () => {
console.log('onIframeLoad');
iframeLoaded.value = true;
if (visible.value) {
postMessageToIframe();
}
};
watch(() => visible.value, (val) => {
if (val && iframeLoaded.value) {
postMessageToIframe();
}
});
const postMessageToIframe = () => {
const iframeWindow = iframeRef.value.contentWindow;
if (iframeWindow) {
iframeWindow.postMessage({
event: 'insert',
data: props.tableData.map(obj => Object.values(obj)),
}, '*');
}
};
</script>
至此,我们便完成了使用 iframe 去扩展我们的项目。