介绍如何使用iframe 进行沙箱隔离

1,809 阅读2分钟
iframe 进行沙箱隔离
  1. 首先iframe是天然自带沙箱隔离的
  2. 怎么配合react进行组件使用
  3. 如何让几个组件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
}

把拿到的cssjs 插入到 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__')
    )
  }
}

渲染效果

image.png

  • 第四部通信 一开始我写的是通过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',
     }
    )
    

然后通过Sandboxscope进行传入,这样就可以进行上下文通信了。整体了解还是清晰的

以上代码借鉴formilyjs