无界的主要逻辑就是把 js 代码脚本放到 iframe 标签中来进行运行,因为 iframe 标签自带沙箱属性,不会影响其他的应用,把 html 和 css 放到 ShadowDOM 中来进行渲染,这样可以实现样式隔离,但是 js 脚本在 iframe 中执行,js 的 window 操作不了 ShadowDOM 的标签,所以通过代理和重写的方式来进行操作 ShadowDOM 的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>基座的代码,不被子应用的样式锁污染</div>
<!-- 加载子应用的div -->
<div id="container"></div>
<script>
// 子应用的 html 和 css
const strTemWithCSS = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="inner">子应用1 hello</div>
<style>
div{ color: red;background: yellow;}
</style>
</body>
</html>
`
// 子应用的 js 脚本
const strScript = `
window.a = 100 ; // 子应用里面的属性,不会影响到父应用
console.log(window.a,'子应用属性');
const ele = document.querySelector('#inner');
console.log(ele,'子应用元素');
`
// 创建 iframe 标签
function createIframe() {
const iframe = document.createElement('iframe');
iframe.src = 'about:blank'
document.body.appendChild(iframe)
return iframe
}
// 创建沙箱,包括 iframe 和 shadowRoot
function createSandbox() {
const sandbox = {
iframe: createIframe(), // 创建一个 iframe 沙箱,用于运行 js
shadowRoot: null
}
return sandbox
}
// 将 html、css 放入到 shadowDOM
function injectTemplate(sandbox, strTemWithCSS) {
const wrapper = document.createElement('div')
wrapper.innerHTML = strTemWithCSS
sandbox.shadowRoot.appendChild(wrapper)
}
function runScriptInSandbox(sandbox, strScript) {
const iframeWindow = sandbox.iframe.contentWindow
// 创建 script 标签,用于执行脚本
const scriptElement = document.createElement('script')
// 将 script 标签插入到 iframe 的 head 标签 中
const headElement = iframeWindow.document.querySelector('head')
// 希望在 iframe 中执行脚本内容 是操作 shadowDom 的
// 因为脚本要获取或者操作dom,而且我们是通过 shadowDom 进行渲染的,此时的 iframe 的 window 是无法操作 shadowDom 的
Object.defineProperty(iframeWindow.Document.prototype, 'querySelector', {
get() {
console.log('get');
return new Proxy(sandbox.shadowRoot['querySelector'], {
apply(target, thisArg, args) {
return thisArg.querySelector.apply(sandbox.shadowRoot, args)
}
})
}
})
// 将脚本内容放入 script 标签中
scriptElement.textContent = strScript
headElement.appendChild(scriptElement)
}
function createCustomElement() {
class WujieApp extends HTMLElement {
// 当自定义元素插入到 DOM 中时,会调用这个回调函数
connectedCallback() {
console.log("标签渲染完毕");
// 1,创建沙箱
const sandbox = createSandbox()
// 2,创建 shadowDOM
sandbox.shadowRoot = this.attachShadow({ mode: 'open' })
// 3,将 html、css 放入到 shadowDOM
injectTemplate(sandbox, strTemWithCSS)
// 4,将 js 放入到沙箱中执行
runScriptInSandbox(sandbox, strScript)
}
}
window.customElements.define('wujie-app', WujieApp);
container.appendChild(document.createElement('wujie-app'))
}
// 定义一个组件来使用
createCustomElement()
</script>
</body>
</html>