美文网首页
postMessage踩坑实践

postMessage踩坑实践

作者: 维李设论 | 来源:发表于2021-08-03 23:57 被阅读0次
    前端 | postMessage踩坑实践.png

    前言

    在低代码编辑器中进行页面预览常常不得不用到iframe进行外链的url引入,这就涉及到了预览页面与编辑器页面数据通信传值的问题,常常用到的方案就是postMessage传值,而postMessage本身在eventloop中也是一个宏任务,就会涉及到浏览器消息队列处理的问题,本文旨在针对项目中的postMessage的相关踩坑实践进行总结,也为想要使用postMessage传递数据的童鞋提供一些避坑思路。

    场景

    专网自服务项目大屏部署在另外一个url上,因而ui需要预览的方案不得不使用iframe进行嵌套,而这里需要将token等一系列信息传递给大屏,这里采用了postMessage进行传值

    案例

    [bug描述] 通过postMessage传递过程中,无法通过模拟点击事件进行数据传值

    [bug分析] postMessage是宏任务,触发机制会先放到浏览器的消息队列中,然后再进行处理,vue、react都会自己实现自己的事件机制,而不触发真正的浏览器的事件机制

    [解决方案] 使用setTimeout处理,将事件处理放在浏览器idle阶段触发回调函数的处理,需要注意传递message的大小

    复现

    引用的地址

    使用express启动了一个静态服务,iframe中的页面

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>3000</title>
    </head>
    
    <body>
        <h1>
            这是一个前端BFF应用
        </h1>
        <div id="oDiv">
        </div>
        <script>
            console.log('name', window.name)
            oDiv.innerHTML = window.name;
            
            // window.addEventListener('message', function(e){
            //     console.log('data', e.data)
            //     oDiv.innerHTML = e.data;
            // })
        </script>
    </body>
    
    </html>
    

    当消息回来后会在页面上进行显示

    vue应用

    图片

    vue-cli启动了一个简单的引入iframe页面的文件

    <template>
      <div id="container">
        <iframe
          ref="ifr"
          id="ifr"
          :name="name"
          :allowfullscreen="full"
          :width="width"
          :height="height"
          :src="src"
          frameborder="0"
        >
          <p>你的浏览器不支持iframes</p >
        </iframe>
      </div>
    </template>
    
    <script>
    export default {
      props: {
        src: {
          type: String,
          default: '',
        },
        width: {
          type: String | Number,
        },
        height: {
          type: String | Number,
        },
        id: {
          type: String,
          default: '',
        },
        content: {
          type: String,
          default: '',
        },
        full: {
          type: Boolean,
          default: false,
        },
        name: {
          type: String,
          default: '',
        },
      },
      mounted() {
        // this.postMessage()
        this.createPost()
      },
      methods: {
        createPost() {
          const btn = document.createElement('a')
          btn.setAttribute('herf', 'javascript:;')
          btn.setAttribute(
            'onclick',
            "document.getElementById('ifr').contentWindow.postMessage('123', '*')"
          )
          btn.innerHTML = 'postMessage'
          document.getElementById('container').appendChild(btn)
          btn.click()
          // document.getElementById('container').removeChild(btn)
        },
        postMessage() {
          document.getElementById('ifr').contentWindow.postMessage('123', '*') 
        }
      },
    }
    </script>
    
    <style>
    </style>
    

    react应用

    图片

    使用create-react-app启动了一个react应用,分别通过函数式组件及类组件进行了尝试

    函数式组件

    // 函数式组件
    import { useRef, useEffect } from 'react'
    
    const createBtn = () => {
      const btn = document.createElement('a')
      btn.setAttribute('herf', 'javascript:;')
      btn.setAttribute(
        'onclick',
        "document.getElementById('ifr').contentWindow.postMessage('123', '*')",
      )
      btn.innerHTML = 'postMessage'
      document.getElementById('container').appendChild(btn)
      btn.click()
      // document.getElementById('container').removeChild(btn)
    }
    
    const Frame = (props) => {
      const { name, full, width, height, src } = props
      const ifr = useRef(null)
      useEffect(() => {
        createBtn()
      }, [])
      return (
        <div id="container">
          <iframe
            id="ifr"
            width="100%"
            height="540px"
            src="http://localhost:3000"
            frameBorder="0"
          >
            <p>你的浏览器不支持iframes</p >
          </iframe>
        </div>
      )
    }
    
    export default Frame
    

    类组件

    // 类组件
    import React from 'react'
    
    const createBtn = () => {
      const btn = document.createElement('a')
      btn.setAttribute('herf', 'javascript:;')
      btn.setAttribute(
        'onclick',
        "document.getElementById('ifr').contentWindow.postMessage('123', '*')",
      )
      btn.innerHTML = 'postMessage'
      document.getElementById('container').appendChild(btn)
      btn.click()
      // document.getElementById('container').removeChild(btn)
    }
    
    
    class OtherFrame extends React.Component {
      constructor(props) {
        super(props)
      }
    
      componentDidMount() {
        createBtn()
      }
    
      render() {
        return (
          <div id="container">
            <iframe
              id="ifr"
              width="100%"
              height="540px"
              src="http://localhost:3000"
              frameBorder="0"
            >
              <p>你的浏览器不支持iframes</p >
            </iframe>
          </div>
        )
      }
    }
    
    export default OtherFrame
    

    原生应用

    图片

    使用原生js书写,既可以通过创建button绑定事件又可以通过a标签绑定事件,是没有任何影响的

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>原生js</title>
        <script>
        </script>
    </head>
    
    <body>
        <iframe id="ifr" src="http://localhost:3000" width="100%" height="540px" frameborder="0"></iframe>
        <!-- <script>
            window.onload = function() {
                const btn = document.createElement('button');
                btn.innerHTML = 'postMessge'
                btn.addEventListener('click', function() {
                    ifr.contentWindow.postMessage('123', "*")
                })
                document.body.appendChild(btn)
                btn.click()
                // document.body.removeChild(btn)
            }
        </script> -->
        <script>
            window.onload = function () {
                const btn = document.createElement('a')
                btn.setAttribute('herf', 'javascript:;')
                btn.setAttribute(
                    'onclick',
                    "document.getElementById('ifr').contentWindow.postMessage('123', '*')"
                )
                btn.innerHTML = 'postMessage'
                document.body.appendChild(btn)
                btn.click()
                // document.body.removeChild(btn)
            }
        </script>
    </body>
    
    </html>
    

    源码

    上面几个示例使用模拟点击事件为了清晰显示标签,发现通过页面点击事件后(通过页面句柄的方式)是可以进行message信息获取的,但是vue和react都对事件进行了代理,从而无法通过attachEvent来进行自生成标签添加事件

    vue

    // on实现原理
    // v-on是一个指令,vue中通过wrapListeners进行了一个包裹,而wrapListeners的本质是一个bindObjectListeners的renderHelper方法,将事件名称放在了一个listeners监听器中
    
    Vue.prototype.$on = function(event, fn) {
      if(Array.isArray(event)) {
        for(let i=0, l=event.length; i < l; i++) {
          this.$on(event[i], fn)
        }
      } else {
        (this._events[event] || this._events[event] = []).push(fn)
      }
    
      return this;
    }
    
    Vue.prototype.$off = function (event, fn) {
        // all
        if (!arguments.length) {
          this._events = Object.create(null)
          return this
        }
        // array of events
        if (Array.isArray(event)) {
          for (let i = 0, l = event.length; i < l; i++) {
            this.$off(event[i], fn)
          }
          return this
        }
        // specific event
        const cbs = this._events[event]
        if (!cbs) {
          return this
        }
        if (!fn) {
          this._events[event] = null
          return this
        }
        // specific handler
        let cb
        let i = cbs.length
        while (i--) {
          cb = cbs[I]
          if (cb === fn || cb.fn === fn) {
            cbs.splice(i, 1)
            break
          }
        }
        return this
      }
    
      Vue.prototype.$emit = function (event) {
        let cbs = this._events[event]
        if (cbs) {
          cbs = cbs.length > 1 ? toArray(cbs) : cbs
          const args = toArray(arguments, 1)
        }
        return this
      }
    

    react

    图片
    // 合成事件
    function createSyntheticEvent(Interface: EventInterfaceType) {
      function SyntheticBaseEvent(
        reactName: string | null,
        reactEventType: string,
        targetInst: Fiber,
        nativeEvent: {[propName: string]: mixed},
        nativeEventTarget: null | EventTarget,
      ) {
        this._reactName = reactName;
        this._targetInst = targetInst;
        this.type = reactEventType;
        this.nativeEvent = nativeEvent;
        this.target = nativeEventTarget;
        this.currentTarget = null;
    
        for (const propName in Interface) {
          if (!Interface.hasOwnProperty(propName)) {
            continue;
          }
          const normalize = Interface[propName];
          if (normalize) {
            this[propName] = normalize(nativeEvent);
          } else {
            this[propName] = nativeEvent[propName];
          }
        }
    
        const defaultPrevented =
          nativeEvent.defaultPrevented != null
            ? nativeEvent.defaultPrevented
            : nativeEvent.returnValue === false;
        if (defaultPrevented) {
          this.isDefaultPrevented = functionThatReturnsTrue;
        } else {
          this.isDefaultPrevented = functionThatReturnsFalse;
        }
        this.isPropagationStopped = functionThatReturnsFalse;
        return this;
      }
    
      Object.assign(SyntheticBaseEvent.prototype, {
        preventDefault: function() {
          this.defaultPrevented = true;
          const event = this.nativeEvent;
          if (!event) {
            return;
          }
    
          if (event.preventDefault) {
            event.preventDefault();
          } else if (typeof event.returnValue !== 'unknown') {
            event.returnValue = false;
          }
          this.isDefaultPrevented = functionThatReturnsTrue;
        },
    
        stopPropagation: function() {
          const event = this.nativeEvent;
          if (!event) {
            return;
          }
    
          if (event.stopPropagation) {
            event.stopPropagation();
          } else if (typeof event.cancelBubble !== 'unknown') {
            event.cancelBubble = true;
          }
    
          this.isPropagationStopped = functionThatReturnsTrue;
        },
    
        
        persist: function() {
          /
        },
        isPersistent: functionThatReturnsTrue,
      });
      return SyntheticBaseEvent;
    }
    

    chromium

    图片 图片

    chromium中关于postmessage的实现主要通过cast中的message实现了消息的监听与分发

    #include "components/cast/message_port/cast_core/message_port_core_with_task_runner.h"
    
    #include "base/bind.h"
    #include "base/logging.h"
    #include "base/sequence_checker.h"
    #include "base/threading/sequenced_task_runner_handle.h"
    
    namespace cast_api_bindings {
    
    namespace {
    static uint32_t GenerateChannelId() {
      // Should theoretically start at a random number to lower collision chance if
      // ports are created in multiple places, but in practice this does not happen
      static std::atomic<uint32_t> channel_id = {0x8000000};
      return ++channel_id;
    }
    }  // namespace
    
    std::pair<MessagePortCoreWithTaskRunner, MessagePortCoreWithTaskRunner>
    MessagePortCoreWithTaskRunner::CreatePair() {
      auto channel_id = GenerateChannelId();
      auto pair = std::make_pair(MessagePortCoreWithTaskRunner(channel_id),
                                 MessagePortCoreWithTaskRunner(channel_id));
      pair.first.SetPeer(&pair.second);
      pair.second.SetPeer(&pair.first);
      return pair;
    }
    
    MessagePortCoreWithTaskRunner::MessagePortCoreWithTaskRunner(
        uint32_t channel_id)
        : MessagePortCore(channel_id) {}
    
    MessagePortCoreWithTaskRunner::MessagePortCoreWithTaskRunner(
        MessagePortCoreWithTaskRunner&& other)
        : MessagePortCore(std::move(other)) {
      task_runner_ = std::exchange(other.task_runner_, nullptr);
    }
    
    MessagePortCoreWithTaskRunner::~MessagePortCoreWithTaskRunner() = default;
    
    MessagePortCoreWithTaskRunner& MessagePortCoreWithTaskRunner::operator=(
        MessagePortCoreWithTaskRunner&& other) {
      task_runner_ = std::exchange(other.task_runner_, nullptr);
      Assign(std::move(other));
    
      return *this;
    }
    
    void MessagePortCoreWithTaskRunner::SetTaskRunner() {
      task_runner_ = base::SequencedTaskRunnerHandle::Get();
    }
    
    void MessagePortCoreWithTaskRunner::AcceptOnSequence(Message message) {
      DCHECK(task_runner_);
      DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&MessagePortCoreWithTaskRunner::AcceptInternal,
                         weak_factory_.GetWeakPtr(), std::move(message)));
    }
    
    void MessagePortCoreWithTaskRunner::AcceptResultOnSequence(bool result) {
      DCHECK(task_runner_);
      DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&MessagePortCoreWithTaskRunner::AcceptResultInternal,
                         weak_factory_.GetWeakPtr(), result));
    }
    
    void MessagePortCoreWithTaskRunner::CheckPeerStartedOnSequence() {
      DCHECK(task_runner_);
      DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&MessagePortCoreWithTaskRunner::CheckPeerStartedInternal,
                         weak_factory_.GetWeakPtr()));
    }
    
    void MessagePortCoreWithTaskRunner::StartOnSequence() {
      DCHECK(task_runner_);
      DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
      task_runner_->PostTask(FROM_HERE,
                             base::BindOnce(&MessagePortCoreWithTaskRunner::Start,
                                            weak_factory_.GetWeakPtr()));
    }
    
    void MessagePortCoreWithTaskRunner::PostMessageOnSequence(Message message) {
      DCHECK(task_runner_);
      DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&MessagePortCoreWithTaskRunner::PostMessageInternal,
                         weak_factory_.GetWeakPtr(), std::move(message)));
    }
    
    void MessagePortCoreWithTaskRunner::OnPipeErrorOnSequence() {
      DCHECK(task_runner_);
      DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);
      task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(&MessagePortCoreWithTaskRunner::OnPipeErrorInternal,
                         weak_factory_.GetWeakPtr()));
    }
    
    bool MessagePortCoreWithTaskRunner::HasTaskRunner() const {
      return !!task_runner_;
    }
    
    } 
    

    总结

    postMessage看似简单,其实则包内含了浏览器的事件循环机制以及不同VM框架的事件处理方式的不同,事件处理对前端来说是一个值得深究的问题,从js的单线程非阻塞异步范式到VM框架的事件代理以及各种js事件库(如EventEmitter、co等),一直贯穿在前端的各个方面,在项目中的踩坑不能只是寻求解决问题就可以了,更重要的是我们通过踩坑而获得对于整个编程思想的认知提升,学习不同大佬的处理模式,灵活运用,才能提升自己的技术实力与代码优雅程度,共勉!!!

    参考

    相关文章

      网友评论

          本文标题:postMessage踩坑实践

          本文链接:https://www.haomeiwen.com/subject/mhpyvltx.html