動態

詳情 返回 返回

postMessage踩坑實踐 - 動態 詳情

前言

在低代碼編輯器中進行頁面預覽常常不得不用到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等),一直貫穿在前端的各個方面,在項目中的踩坑不能只是尋求解決問題就可以了,更重要的是我們通過踩坑而獲得對於整個編程思想的認知提升,學習不同大佬的處理模式,靈活運用,才能提升自己的技術實力與代碼優雅程度,共勉!!!

參考

  • mdn官方文檔
  • chromium源碼
  • iframe,我們來談一談
  • vue父組件異步獲取數據傳給子組件
user avatar dirackeeko 頭像 huajianketang 頭像 banana_god 頭像 imba97 頭像 zhulongxu 頭像 ccVue 頭像 wmbuke 頭像 romanticcrystal 頭像 Asp1rant 頭像 gaozhipeng 頭像 best-doraemon 頭像 nzbin 頭像
點贊 40 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.