iframe 进行沙箱隔离
- 首先
iframe是天然自带沙箱隔离的 - 怎么配合
react进行组件使用 - 如何让几个组件
iframe相互通信
- 第一步创建一个iframe组件
export interface ISandboxProps {
style?: React.CSSProperties //iframe 样式
cssAssets?: string[] //加载插件的外部css资源
jsAssets?: string[] //加载插件的外部js资源,包括react组建资源
scope?: any //通信的变量
}
export const Sandbox: React.FC<ISandboxProps> = (props) => {
const { cssAssets, jsAssets, scope, style, ...iframeProps } = props
return React.createElement('iframe', {
...iframeProps,
ref: useSandbox(props),
style: {
height: '100%',
width: '100%',
border: 'none',
display: 'block',
...style,
},
})
}
<Sandbox
jsAssets={[
'https://unpkg.com/moment/min/moment-with-locales.js',
'https://unpkg.com/react/umd/react.production.min.js',
'https://unpkg.com/react-dom/umd/react-dom.production.min.js',
'https://unpkg.com/antd/dist/antd-with-locales.min.js',
]}
/>
// 组件使用
- 第二步准备把组件接受的参数放入
useSandbox
export const useSandbox = (props: React.PropsWithChildren<ISandboxProps>) => {
const ref = useRef<any>()
const cssAssets = props.cssAssets || []
const jsAssets = props.jsAssets || []
useEffect(() => {
if (ref.current) {
const styles = cssAssets
?.map?.((css) => {
return `<link media="all" rel="stylesheet" href="${css}" />`
})
.join('\n')
const scripts = jsAssets
?.map?.((js) => {
if (js.includes('.js')) {
return `<script src="${js}" type="text/javascript" ></script>`
} else {
return js
}
})
.join('\n')
ref.current.contentWindow['__CSP_SANDBOX_SCOPE__'] = props.scope
// 这个其实就是把父组建变量存储到iframe window上面
ref.current.contentDocument.open()
ref.current.contentDocument.write(`
<!DOCTYPE html>
<head>
${styles}
</head>
<body>
<div id="__SANDBOX_ROOT__"></div>
${scripts}
</body>
</html>
`)
ref.current.contentDocument.close()
}
}, [])
return ref
}
把拿到的css,js 插入到 iframe 里面 这样页面主体初始化就好了
- 第三步需要把自己开发的组件挂载到
iframe
<Sandbox jsAssets={[ 'xxxx', './sandbox.bundle.js',] ]} />
//sandbox.bundle.js 就是业务项目的react组件代码 如下
enderSandboxContent((props) => {
return (
<div>
<Content {...props} />
</div>
)
})
这个地方需要注意的点就是iframe 本身是不能执行tsx代码的,所以需要借助webpack 在入口引入编译一下
entry: {
playground: path.resolve(__dirname, './src/index'),
sandbox: path.resolve(__dirname, './src/sandbox'),
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].bundle.js',
}
//这样你就可以在项目里面引人sandbox.bundle.js 这样就被加载到iframe js执行了
export const useSandboxScope = () => {
return globalThisPolyfill['__CSP_SANDBOX_SCOPE__']
//获取Sandbox 组建传输入的参数
}
//挂载节点
export const renderSandboxContent = (render: (scope?: any) => JSX.Element) => {
if (isFn(render) && document.getElementById('__SANDBOX_ROOT__')) {
ReactDOM.render(
render(useSandboxScope()),
document.getElementById('__SANDBOX_ROOT__')
)
}
}
渲染效果
- 第四部通信
一开始我写的是通过
useSandbox里面的useEffect进行监听重新执行,不过发现这样会导致整个iframe重新执行,这个就比较尴尬。 想了一下,其实方法很多,这边因为用的react,所以就使用了redux进行通信const { getState, dispatch, subscribe } = createStore( function (state: any, action: any) { console.log(state) switch (action.type) { case 'demo': return { ...state, ...action.payload, } default: return state } }, { name: 'ss', } )
然后通过Sandbox的scope进行传入,这样就可以进行上下文通信了。整体了解还是清晰的
以上代码借鉴formilyjs