weex從.we轉.vue之BroadcastChannel

背景描述

在weex中,從.we過分到.vue的過程當中,很難規避的就是事件派發機制當中,對BroadcastChannel的替換,按照官方的推薦採用vuex去更換,可是我在瀏覽一遍vuex的文檔以後,絕的在weex使用有點麻煩,就去社區溜達了一圈,看看有沒有小夥伴們找到更合適的方法。html

在一陣交流以後,根據大夥的推薦,在.vue文件中,都是採用 weex提供的globalEvent來處理。vue

此次的踩坑記,也是這個文檔帶來。下面我就來記錄一下,此次踩坑的歷程。git

踩坑過程

前期準備

按照文檔的要求,在fireGlobalEvent的時候,須要各端實現,所以按照要求在Objective-C,添加如下方法:github

/**
 發送全局事件

 @param eventName 事件名稱
 @param params 事件參數
 */
- (void)postGlobalEvent:(NSString *)eventName params:(NSDictionary *)params {
    [weexInstance fireGlobalEvent:eventName params:params];
}

而且暴露給weex使用: WX_EXPORT_METHOD(@selector(postGlobalEvent:params:))vuex

準備好了這些,我就開始在.vue的文件中開始測試功能了。apache

Test One: 同一實例中,子組件發送,父組件監聽

我是直接在個人項目中修改原先的代碼的,下面的demo,也是我代碼的一部分,項目中廣場頁面中,navigator組件上消息的觸發按鈕,換成調用剛剛native中擴展的postGlobalEvent方法, square-header.vue代碼以下:json

<template>
  <div :style="{ width: '750', height: navHeight, backgroundColor: 'rgba(255, 255, 255, ' + (opacity) + ')' }">
    <image src="https://static.toomao.com/weex-images/square/navigator3.png" class="nav-image" :style="{ opacity: opacity>0.8?0:(0.8-opacity) }"></image>

    <div class="nav-content" :style="{ marginTop: navHeight===128?40:0 }">
      <div :class="['nav-left', 'nav-left-' + (navigatorState)]" @click="scannerButtonClicked">
        <image :src="navigatorIcons[0]" :class="['nav-left-icon', 'nav-left-icon-' + (navigatorState)]" resize="contain"></image>
        <text :class="['nav-left-text', 'nav-left-text-' + (navigatorState)]">掃一掃</text>
      </div>
      <text :class="['nav-center', 'nav-center-' + (navigatorState)]" @click="searchTextClicked">{{tip.words ? tip.words : '請輸入搜索內容'}}</text>

      <div :class="['nav-right', 'nav-right-' + (navigatorState)]" @click="infoButtonClicked">
        <image :src="navigatorIcons[1]" :class="['nav-right-icon', 'nav-right-icon-' + (navigatorState)]" resize="contain"></image>
        <text :class="['nav-right-text', 'nav-right-text-' + (navigatorState)]">消息</text>
      </div>
    </div>
  </div>
</template>

<script>
  const utils = weex.requireModule('utils');

  module.exports = {
    methods: {
      searchTextClicked() {
        console.log('~~~~~~~~globalEvent 已經發送了~~~~~~~~~~~~~~~~~');
        utils.postGlobalEvent('test1', { index: 'current index is 1'});
      },
    },
  };
</script>

下面是square.vue的監聽事件的代碼:api

<template>
  <div>
    <!-- navigator -->
    <square-header ref="square-header"></square-header>
  </div>
</template>

<script>
;
const utils = weex.requireModule('utils');

module.exports = {
  components: {
    squareHeader: require('../components/navigator/square-header.vue'),
  },
  created() {
    // 監聽事件
    const globalEvent = weex.requireModule('globalEvent');
    globalEvent.addEventListener("test1", (e) => {
      // 事件回調
      console.log('~~~~~~~~test1~~~~~~~~~~~~~~~~~', e);
    });
  },
};</script>

測試結果

這樣我就基本完成了,這個demo的全部工做,而後build,沒有報錯、最好run,打開這個頁面,渲染成功,下面是我在點擊搜索按鈕,Xcode控制檯的打印信息:微信

2017-06-22 10:47:53.338723 [fg128,128,128; <Weex>[log]WXJSCoreBridge.m:110, jsLog: ~~~~~~~~globalEvent 已經發送了~~~~~~~~~~~~~~~~~  [;
2017-06-22 10:48:15.584984  [fg128,128,128; <Weex>[log]WXJSCoreBridge.m:110, jsLog: ~~~~~~~~test1~~~~~~~~~~~~~~~~~ {"index":"current index is 1"}  [;

看到這結果,我表示心情還挺不錯,由於還挺好用的,感受一會兒找到了好的方式去替換這些方法了。weex

就在我開心的勁頭上,我繼續開始了我實際方法的更換,首先第一個就是在登陸成功後,要給多個頁面(我的信息、權限相關的頁面)發送通知,所以我開始了第二個測試,即在不一樣實例之間進行發送通知與監聽。本覺得信息十足的,結果出現的問題:

Test Two: 不一樣實例之間發送與接收

發送事件的實例:login.vue:

<template>
  <scroller style="width: 750px; height: 1334px;">
    <!-- bgImg、 back button -->
    <image :src="getterNativeImgSrc('navigator/login_bg')" style="width: 750px; height: 1334px; position: absolute; top: 0px; left: 0px;"></image>
    <image :src="getterNativeImgSrc('login/login_back')" style="width: 35px; height: 35px; position: absolute; top: 60px; left: 24px;" @click="backButtonClicked"></image>
    <!-- input -->
    <div class="userInfo">
      <div class="inputWrapper">
        <input type="text" name="username" class="input" ref="username" placeholder="請輸入您的手機號碼" maxlength="11" @input="oninput">
      </div>
      <div class="inputWrapper">
        <input class="input" :type="passwordType" name="password" ref="password" maxlength="20" placeholder="請輸入密碼" @input="oninput">
        <image style="width: 30px; height: 30px; background-color: #0ff;" :src="openEyes ? eyeSelected : eye" resize="contain" @click="eyeButtonClicked"></image>
      </div>
    </div>

    <!-- 登陸按鈕 -->
    <wxc-form :action="(apiBase) + '/1.1/login?username=' + (userName) + '&password=' + (password)" method="GET" ref="login" style="margin-top: 40px;">
      <text class="loginButton" @click="loginButtonClicked">登  錄</text>
    </wxc-form>

    <!-- 註冊  忘記密碼 -->
    <div style="width: 750px; flex-direction: row; justify-content: space-between; padding: 24; margin-top: 20px;">
      <text style="font-size: 28px; color: #707070;" @click="signUpButtonClick">註冊帳號</text>
      <text style="font-size: 28px; color: #707070;" @click="forgotButtonClick">忘記密碼</text>
    </div>

    <div v-if="isInstallWX" style="margin-top: 350px; flex-direction: column; justify-content: center; width: 750px; align-items: center;" @click="thirdLoginButtonClicked">
      <text style="font-size: 26px; color: #707070;">使用第三方登陸</text>
      <image :src="getterNativeImgSrc('login/weixin')" style="margin-top: 20px; width: 60px; height: 60px;" resize="contain"></image>
    </div>
    <wxc-form :action="(apiBase) + '/1.1/loginByWechat?unionid=' + (thirdUserInfo.unionid)" method="GET" ref="third-login" style="margin-top: 60px;">
    </wxc-form>
    <tm-loading ref="tm-loading" inithide="true"></tm-loading>
  </scroller>
</template>

<script>
;


const navigator = weex.requireModule('navigator');
const utils = weex.requireModule('utils');
const storage = weex.requireModule('storage');

const { serverPath, getNativeResourcePath, navigatorPushWithPath, toast, errorDeals } = require('../util.js');

module.exports = {
  components: {
    wxcForm: require('components/wxc-form/wxc-form.vue'),
    tmLoading: require('../components/tm-loading.vue')
  },

  props: {
    apiBase: {
      default: serverPath()
    },
    userName: {
      default: ''
    },
    password: {
      default: ''
    },
    userInfo: {
      default: function () {
        return {};
      }
    },
    openEyes: {
      default: false
    },
    eye: {
      default: ''
    }, // 閉眼
    eyeSelected: {
      default: ''
    }, // 睜眼
    isInstallWX: {
      default: true
    },
    thirdUserInfo: {
      default: function () {
        return {};
      }
    }, // 微信用戶信息
    loginButtonEnable: {
      default: true
    },
    passwordType: {
      default: 'password'
    }
  },
  created() {
    this.eye = getNativeResourcePath(this, 'login/login_eye');
    this.eyeSelected = getNativeResourcePath(this, 'login/login_eye_selected');
    if (weex.config.env.platform === 'iOS') {
      navigator.setNavBarHidden({ hidden: true }, () => {});
    }
    try {
      utils.weexInstalledWeChatClient(e => {
        this.isInstallWX = e.result;
      });
    } catch (e) {}
  },
  mounted() {
    this.$refs['tm-loading'].hide();
  },
  methods: {
    backButtonClicked() {
      navigator.pop({ animation: 'ture' }, () => {});
    },
    oninput(e) {
      const id = e.target.attr.name;
      if (id === 'username') {
        this.userName = e.target.attr.value;
      } else {
        this.password = e.target.attr.value;
      }
    },

    onchange(e) {},

    loginButtonClicked() {
      if (this.userName.length != 11) {
        toast('請輸入正確的手機號碼', 1);
        return;
      }
      if (this.password.length < 6 || this.password.length > 20) {
        toast('請輸入6-20位密碼', 1);
        return;
      }
      const form = this.$refs.login;
      form.headers = {
        'content-type': 'application/json'
      };
      if (!this.loginButtonEnable) return;
      this.loginButtonEnable = false;
      const that = this;
      this.$refs['tm-loading'].show();
      form.submit(res => {
        that.$refs['tm-loading'].hide();
        that.loginButtonEnable = true;
        if (res.ok) {
          const data = JSON.stringify(res.data);
          storage.setItem('userInfo', data, event => {
            console.log('~~~~~~~~登陸成功  發送通知 ~~~~~~~~~~~~~~~~~');
            utils.postGlobalEvent('login-success', 'login succeed');
            // const Hulk = new BroadcastChannel('login-success');
            // Hulk.postMessage('login succeed');
            that.backButtonClicked();
          });
        } else {
          errorDeals(res);
        }
      });
    },
    // 第三方登陸
    thirdLoginButtonClicked() {
      this.$refs['tm-loading'].show();
      try {
        utils.getWeChatUserInfo(e => {
          this.$refs['tm-loading'].hide();
          if (e.result === 'success') {
            this.thirdUserInfo = e.data;
            this.$renderThen(() => {
              this.requestThirdLoginUserInfo();
            });
          } else {
            toast('受權失敗', 1);
          }
        });
      } catch (e) {}
    },
    // 第三方登陸請求
    requestThirdLoginUserInfo() {
      const form = this.$refs['third-login'];
      const that = this;
      form.submit(res => {
        if (res.ok) {
          // 存儲以前,先將對象序列化成存儲字符串
          const data = JSON.stringify(res.data);
          storage.setItem('userInfo', data, event => {
            utils.postGlobalEvent('login-success', 'login succeed');
            // const Hulk = new BroadcastChannel('login-success');
            // Hulk.postMessage('login succeed');
            that.backButtonClicked();
          });
        } else {
          const data = res.data;
          if (res.status === 400 && data.code === 4105) {
            // 第一次登陸  去綁定帳號
            const userStr = JSON.stringify(this.thirdUserInfo);
            navigatorPushWithPath(`login/association-account.js?config=${encodeURIComponent(userStr)}`);
          }
        }
      });
    },
    // 註冊
    signUpButtonClick() {
      navigatorPushWithPath('login/sign-up.js');
    },
    // 忘記密碼
    forgotButtonClick() {
      navigatorPushWithPath('login/forgot-password.js');
    },
    eyeButtonClicked() {
      this.openEyes = !this.openEyes;
      this.passwordType = this.openEyes ? 'text' : 'password';
    },
    // 獲取圖片路徑
    getterNativeImgSrc(src) {
      return getNativeResourcePath(this, src);
    }
  }
};</script>

在上面代碼中,能夠定位到loginButtonClicked()方法,這是登陸按鈕執行的方面,在這個方法請求成功後,我會調用 utils.postGlobalEvent('login-success', 'login succeed');方法,即發送一個全局事件的通知,名字叫作login-success;並在發送後返回到上一頁面。

監聽事件的實例: mine.vue

<template>
  <div style="background-color: #f4f4f4;" @viewappear="viewappear">
    <wxc-form :action="(baseAPI) + '/1.1/my/pageinfo'" method="GET" ref="loaderPage"></wxc-form>
    <list style="width: 750px; height: 1244">
      <cell>
        <mine-header ref="header"></mine-header>
      </cell>
      <cell>
        <mine-orders-toolbar ref="orders"></mine-orders-toolbar>
      </cell>
      <cell>
        <mine-more-tools></mine-more-tools>
      </cell>
    </list>

    <div class="navigator">
      <div class="content">
        <image src="https://pic.toomao.com/becb9c4ffda30defcda9b760b9478633bbdb7d22" style="width: 50px; height: 50px;" @click="settingButtonClicked"></image>
      </div>
    </div>
  </div>
</template>

<script>
;

const { getBaseAPI, asyncReady, navigatorPushWithPath } = require('../util.js');

module.exports = {
  components: {
    wxcForm: require('components/wxc-form/wxc-form.vue'),
    mineHeader: require('../components/mine/mine-header.vue'),
    mineOrdersToolbar: require('../components/mine/mine-orders-toolbar.vue'),
    mineMoreTools: require('../components/mine/mine-more-tools.vue'),
    tmNavpage: require('../components/navigator/tm-navpage.vue')
  },

  props: {
    baseAPI: {
      default: getBaseAPI()
    },
    userInfo: {
      default: function () {
        return {};
      }
    },
    data: {
      default: function () {
        return {};
      }
    }
  },
  created() {
    const globalEvent = weex.requireModule('globalEvent');
    console.log('~~~~~~~~addEventListener ~~~~~~~~~~~~~~~~~');
    globalEvent.addEventListener("login-success", (e) => {
      console.log('~~~~~~~~addEventListener CallBack~~~~~~~~~~~~~~~~~', e);
      this.receiveLoginSuccessedNotify();
    });
  },
  mounted: asyncReady(function () {
    if (this.userInfo.sessionToken) {
      this.requestPageInfo();
    }
  }),
  methods: {
    viewappear: asyncReady(function () {}),

    receiveLoginSuccessedNotify() {
      asyncReady(function () {
        if (this.userInfo.sessionToken) {
          this.requestPageInfo();
        }
      }).call(this);
    },
    settingButtonClicked() {
      navigatorPushWithPath('mine/setting/setting.js');
    },
    requestPageInfo() {
      const header = this.$refs.header;
      const oreders = this.$refs.orders;
      const pageLoader = this.$refs.loaderPage;
      pageLoader.headers = {
        'X-AVOSCloud-Session-Token': this.userInfo.sessionToken
      };
      pageLoader.submit(res => {
        if (res.ok) {
          this.data = res.data;
          header.setUpCardData(res.data);
          oreders.setUpOrderNumber(res.data.ordercnt);
        }
      });
    }
  }
};</script>

<style scoped>
  .wrapper {
    background-color: #eee;
  }
  .navigator {
    position: absolute;
    top: 0px;
    left: 0px;
    width: 750px;
    height: 128px;
    padding-top: 40px;
    /*background-color: #0f0;*/
  }
  .content {
    width: 750px;
    height: 88px;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    /*background-color: #999;*/
    padding: 24;
  }

測試結果

測試過程:先在未登陸的狀況下,訪問mine頁面,而後點擊我的信息進入到登陸頁面,登陸成功後,發送通知,並返回到個人頁面,正常狀況下,個人頁面會接收通知,並從本地獲取新數據刷新UI的。但實際過程以下,能夠注意我代碼中的幾個log:

2017-06-22 11:11:25.084999 [fg128,128,128; <Weex>[log]WXJSCoreBridge.m:110, jsLog: ~~~~~~~~addEventListener ~~~~~~~~~~~~~~~~~  [;
2017-06-22 11:12:42.884790 [fg128,128,128; <Weex>[log]WXJSCoreBridge.m:110, jsLog: WXC-FORM?: [object Object]  [;
2017-06-22 11:12:43.227708 [fg128,128,128; <Weex>[log]WXJSCoreBridge.m:110, jsLog: ~~~~~~~~登陸成功  發送通知 ~~~~~~~~~~~~~~~~~  [;

上面結果能夠看到,我監聽了事件,而且也發送了事件,可是我沒有收到事件的callBack。

爲了探究一下,這個事件爲啥沒有接收到,我跟着native的代碼,進入到 weex SDK去看看了具體實現。找到globalEventiOS的實現類WXGlobalEventModule,(在尋找這個module的時候,能夠直接根據globalEventSDK裏面搜索,這樣比較快) 並獲取addEventListener方法:

- (void)addEventListener:(NSString *)event callback:(WXModuleKeepAliveCallback)callback
{
    WXThreadSafeMutableArray * array = nil;
    if (_eventCallback[event]) {
        if (callback) {
            [_eventCallback[event] addObject:callback];
        }
    } else {
        array = [[WXThreadSafeMutableArray alloc] init];
        if (callback) {
            [array addObject:callback];
        }
        _eventCallback[event] = array;
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fireGlobalEvent:) name:event object:nil];
    }
}

能夠看出在監聽方法中,主要是使用了NSNotification添加了一個觀察者,而且將每一個evet對應的callBack都保存起來;所以找到接受通知的實現方法:fireGlobalEvent的實現以下:

- (void)fireGlobalEvent:(NSNotification *)notification
{
    NSDictionary * userInfo = notification.userInfo;
    NSString * userWeexInstanceId = userInfo[@"weexInstance"];
    /* 
     1. The userWeexInstanceId param will be passed by globalEvent module notification.
     2. The notification is posted by native user using NotificationCenter, native user don't need care about what the userWeexInstanceId is. What you do is to addEventListener in weex file using globalEvent module, and then post notification anywhere.
     */
    WXSDKInstance * userWeexInstance = [WXSDKManager instanceForID:userWeexInstanceId];
     // In case that userInstanceId exists but instance has been dealloced
    if (!userWeexInstanceId || userWeexInstance == weexInstance) {
        
        for (WXModuleKeepAliveCallback callback in _eventCallback[notification.name]) {
            callback(userInfo[@"param"], true);
        }
    }
}

在處理通知的方法中,能夠發如今調用callback以前有兩個判斷!userWeexInstanceId || userWeexInstance == weexInstance, 要麼這個實例id不存在,要麼兩個實例相同,看到這裏彷佛能明白剛剛爲啥在login.vue頁面中發送的事件在mine.vue的監聽這沒有收到回調了。

那麼根據NSString * userWeexInstanceId = userInfo[@"weexInstance"];代碼分析: 這個userWeexInstanceId是通知的userInfo裏面設置的。爲此我須要找到post這個通知在什麼位置。這時候確定就是native暴露給weex用來發送通知的那個方法了:

/**
 發送全局事件

 @param eventName 事件名稱
 @param params 事件參數
 */
- (void)postGlobalEvent:(NSString *)eventName params:(NSDictionary *)params {
    [weexInstance fireGlobalEvent:eventName params:params];
}

進入這個這個方法裏面獲得的代碼以下:

- (void)fireGlobalEvent:(NSString *)eventName params:(NSDictionary *)params
{
    if (!params){
        params = [NSDictionary dictionary];
    }
    NSDictionary * userInfo = @{
            @"weexInstance":self.instanceId,
            @"param":params
    };
    [[NSNotificationCenter defaultCenter] postNotificationName:eventName object:self userInfo:userInfo];
}

哈哈哈,看到這裏就基本清楚全部的內容所在了,userInfo這個參數也是在這裏設置的。其實走到這步我仍是不明白個人問題該怎麼解決,由於在通知callBack的兩個條件,該怎麼避免,我感受官方把本身的路給堵死了,所以帶這個問題去請求老司機, 獲得如下回應:

F168788E-52C7-43ED-84F0-1FA712E92862.png

A0E55571-6608-4A57-9B15-60F16BDC5A41.png

看到weex見解這這樣的迴應,個人心裏微微一笑-_-。看來目前也只能這樣了,所以調整代碼:

/**
 發送全局事件

 @param eventName 事件名稱
 @param params 事件參數
 */
- (void)postGlobalEvent:(NSString *)eventName params:(NSDictionary *)params {
    if (!params){
        params = [NSDictionary dictionary];
    }
    NSDictionary * userInfo = @{
                                @"param":params
                                };
    [[NSNotificationCenter defaultCenter] postNotificationName:eventName object:self userInfo:userInfo];
}

再次運行獲得如下結果:

2017-06-22 11:40:02.134405 [fg128,128,128; <Weex>[log]WXJSCoreBridge.m:110, jsLog: ~~~~~~~~登陸成功  發送通知 ~~~~~~~~~~~~~~~~~  [;
2017-06-22 11:40:02.138522 [fg128,128,128; <Weex>[log]WXJSCoreBridge.m:110, jsLog: ~~~~~~~~addEventListener CallBack~~~~~~~~~~~~~~~~~ login succeed  [;
2017-06-22 11:40:02.184861

哈;此次終於看到了~~~~~~~~addEventListener CallBack~~~~~~~~~~~~~~~~~的打印信了,而且也將傳遞的參數login succeed獲取了,至此,這個坑算是踩完了。

相關文章
相關標籤/搜索