序
iframe 实现代码沙箱是一个非常优质的选择,不仅能隔绝代码,最主要的还能隔绝样式。
实际上我们只要实现一件事情,即 在 iframe 中加载我们想要的代码。
我想到两种实现方式:
- 把代码转为一个可以访问的地址,让 iframe 指向这个地址
- 解析代码的 dom 结构,替换 iframe 中的 dom
实现 1 - 把代码转为一个可以访问的地址,让 iframe 指向这个地址
第一个实现需要使用 Blob,在要加载的代码改变时,生成 blob 的 url 访问,然后重写 iframe 的地址:
function getCodeTextarea() {
return document.getElementsByTagName("textarea")[0];
}
function onCodeChange() {
const iframe = document.getElementsByTagName("iframe")[0];
const blob = new Blob([getCodeTextarea().value], { type: "text/html;charset=UTF-8" });
iframe.src = URL.createObjectURL(blob);
}
注意要写字符集,不然就炸了。这一刻突然了解了 Content-Type
请求头为什么可以设置字符集。
实现 2 - 解析代码的 dom 结构,替换 iframe 中的 dom
第二种实现是解析 DOM 并替换,粗暴点,我们可以直接替换根节点。
function createCodeDom() {
const parser = new DOMParser();
return parser.parseFromString(getCodeTextarea().value, "text/html");
}
function onCodeChange() {
const iframe = document.getElementsByTagName("iframe")[0];
const iframeDom = iframe.contentWindow.document;
iframeDom.replaceChild(createCodeDom().documentElement, iframeDom.documentElement);
}
小结
上面的实现方式中,【把代码转为一个可以访问的地址,让 iframe 指向这个地址】更加干净,每个生命周期不会互相污染,但需要消费额外的 iframe 生命周期计算,不过这其实很快。
第二种【解析代码的 dom 结构,替换 iframe 中的 dom】更快,因为直接替换 dom,不会在跑一个 iframe 生命周期,坏处就是上次代码的定时器会在下次代码接着跑,沙箱内部会污染。
附录 - 在 iframe 中跑了 bootstrap
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<title>index</title>
<style>
.root {
display: grid;
height: 100vh;
grid-template-columns: 1fr 1fr;
}
textarea {
height: 100%;
background-color: transparent;
}
iframe {
height: 100%;
width: 100%;
background-color: transparent;
}
</style>
</head>
<body>
<div class="root">
<textarea class="textarea">
<!DOCTYPE html>
<html>
<head>
<link
rel="stylesheet"
href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css"
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu"
crossorigin="anonymous"
/>
</head>
<body>
<!-- Standard button -->
<button type="button" class="btn btn-default">(默认样式)Default</button>
<!-- Provides extra visual weight and identifies the primary action in a set of buttons -->
<button type="button" class="btn btn-primary">(首选项)Primary</button>
<!-- Indicates a successful or positive action -->
<button type="button" class="btn btn-success">(成功)Success</button>
<!-- Contextual button for informational alert messages -->
<button type="button" class="btn btn-info">(一般信息)Info</button>
<!-- Indicates caution should be taken with this action -->
<button type="button" class="btn btn-warning">(警告)Warning</button>
<!-- Indicates a dangerous or potentially negative action -->
<button type="button" class="btn btn-danger">(危险)Danger</button>
<!-- Deemphasize a button by making it look like a link while maintaining button behavior -->
<button type="button" class="btn btn-link">(链接)Link</button>
</body>
</html>
</textarea>
<iframe></iframe>
</div>
<script>
function getCodeTextarea() {
return document.getElementsByTagName("textarea")[0];
}
// 实现 1 - 把代码转为一个可以访问的地址,让 iframe 指向这个地址
function onCodeChange() {
const iframe = document.getElementsByTagName("iframe")[0];
const blob = new Blob([getCodeTextarea().value], { type: "text/html;charset=UTF-8" });
iframe.src = URL.createObjectURL(blob);
}
// function createCodeDom() {
// const parser = new DOMParser();
// return parser.parseFromString(getCodeTextarea().value, "text/html");
// }
// 实现 2 - 解析代码的 dom 结构,替换 iframe 中的 dom
// function onCodeChange() {
// const iframe = document.getElementsByTagName("iframe")[0];
// const iframeDom = iframe.contentWindow.document;
// iframeDom.replaceChild(createCodeDom().documentElement, iframeDom.documentElement);
// }
function listenCodeChange() {
onCodeChange();
getCodeTextarea().addEventListener("input", onCodeChange);
return () => void getCodeTextarea().removeEventListener("input", onCodeChange);
}
function main() {
listenCodeChange();
}
main();
</script>
</body>
</html>