如何在微信小程序里面實現跨頁面通信?

vmvg3395 8年前發布 | 64K 次閱讀 微信小程序 移動開發 JavaScript

如何在微信小程序里面實現跨頁面通信?

我們在處理業務需求的時候,常常會遇到一些情況,在二級或者三級頁面進行某些操作或者變更后,需要將結果通知到上級頁面去。比如:

  • 選擇了某些配置項,點擊保存后,外部頁面能夠立即變更
  • 在上傳頭像頁面,上傳完畢后,外部頁面的頭像能夠立即顯示為新頭像。

所以,這個時候就涉及到如何在頁面之間通信的問題了。 跨頁面通信進一步說其實就是一個程序內部的事件通知機制問題,在其他平臺或者OS上都一些相應的實現,比如:

  • iOS SDK自帶的 NotificationCenter
  • Android 平臺著名的第三方庫 EventBus

目前微信小程序官方SDK還沒有提供 Event API 來幫助開發者實現頁面間通信,所以我們今天來看看,自己如何實現這樣一個簡單的小工具。

開始前先打個廣告

強烈推薦我司出品的好用到違反廣告法的助眠軟件——云夢

“云夢”可以使你保持良好作息規律,將生物鐘調節到最佳狀態,令生活工作增強動力,保持效率的同時心情舒暢。

說到這里就不得不說“云夢”的微信小程序版本了,在小程序開始公測后,我們也在第一時間將“云夢”的基本功能移植到了小程序平臺上。 整個過程相當順利,除了小程序的IDE還不是太穩定外,基本上沒啥大問題。 開發過程和React-Native基本相似,大概一天時間就搞定了。

Quick And Dirty

我們知道,在小程序里面一個頁面的變化,是通過調用 setData 函數來實現的。所以想做到在二級頁面里讓一級頁面產生變化,最 Quick And Dirty 的做法就是把一級頁面的 this 傳入到二級頁面去,這樣我們在二級頁面調用 page1.setData(…) 就可以立即引發外部的變化。

但是這并不是一個好的方案,不僅產生了頁面的耦合,而且也并不能處理復雜的數據邏輯,因為二級頁面不并清楚也不應該關心一級頁面想怎么處理當前數據。所以二級頁面只應該把變更后的數據通知給一級頁面即可,至于一級頁面是想刷新界面,還是想本地存儲或者發起網絡通信,別人都不需知曉了。

簡單的Callback

如果只是想把數據通知給外部頁面,那應該怎么做呢? 我們來看看第二個方案,如果想產生一個通知,這里就需要用到 callback 機制了。 即關心數據變化的頁面,注冊一個 callback 函數到一個公共的地方;而數據變更者在變更數據后,將新的數據放入同一個公共的地方;在放入數據時,同時調用這個 callback 函數,讓 callback 函數實現者接收到這個變化。

哪這個公共的地方在哪里呢? 第一反應就是 app.js 里面,因為小程序提供了一個 API 叫做 getApp(),讓 page 初始化時,可以通過以下代碼:

var app = getApp()

來獲取 app 實例,從而實現全局的數據共享,并且微信也很貼心的在 Demo 代碼里面留了一個 globalData 字段,以暗示開發者這里是可以用來存儲全局數據的。

App({
    ...
    globalData:{
        userInfo:null
    }
    ...
})

基于 app.js 方案的偽代碼如下:

//app.js
App({
    addListener: function(callback) {
        this.callback = callback;
    },

setChangedData: function(data) {
    this.data = data;
    if(this.callback != null) {
        this.callback(data);
    }
}

})</code></pre>

然后我們在一級頁面的 onLoad中 調用 addListener:

//page1.js
var app = getApp()
Page({
    onLoad: function () {
        app.addListener(function(changedData) {
            that.setData({
                data: changedData
            });
        });
    }
})

在二級頁面數據變更的地方調用:

//page2.js
var app = getApp()
Page({
    onBtnPress: function() {
        app.setChangedData('page2-data');
    }
})

一個基本合格的方案

以上就是跨頁面通信的最基本原理,不過這也是一個很 dirty 的方案,因為上面的代碼只能支持一種 Event 的通知,而且也不能針對這個 Event 添加多個監聽者(比如有多個頁面需要同時知道某數據變更)。 讓我們來看看一個基本合格的 Event 管理器應該具備怎樣的能力?

  • 支持多種 Event 的通知
  • 支持對某一 Event 可以添加多個監聽者
  • 支持對某一 Event 可以移除某一監聽者
  • 將 Event 的存儲和管理放在一個單獨模塊中,可以被所有文件全局引用

根據以上的描述,我們來設計一個新的 Event 模塊,對應上面的能力,它應該具有如下三個函數:

  • on 函數,用來向管理器中添加一個 Event 的 Callback,且每一個 Event 必須有全局唯一的 EventName,函數內部通過一個數組來保存同一 Event 的多個 Callback
  • remove 函數,用來向管理器移除一個 Event 的 Callback
  • emit 函數,用來觸發一個 Event

我們在小程序的 utils 目錄中,新建一個 event.js 文件,來作為一個獨立的模塊,偽代碼如下:

//event.js
var events = {};

function on(name, callback) { var callbacks = events[name]; addToCallbacks(callbacks, callback); }

function remove(name, callback) { var callbacks = events[name]; removeFromCallbacks(callbacks, callback); }

function emit(name, data) { var callbacks = events[name]; emitToEveryCallback(callbacks, data); }

exports.on = on; exports.remove = remove; exports.emit = emit;</code></pre>

我們來看看在一二級頁面應該如何來使用這個 Event 模塊

在二級頁面中觸發事件:

//page2.js
var event = require('../../utils/event.js');
Page({
    onBtnPress: function() {
        event.emit('DataChanged', 'page2-data');
    }
});

在一級頁面的 onLoad 中監聽事件,onUnload 中取消監聽:

//page1.js
var event = require('../../utils/event.js');
Page({
    onLoad: function() {
        var that = this;
        event.on('DataChanged', function(changedData) {
            that.setData({
                data: changedData
            });
        });
    },

onUnload: function() {
    event.remove('DataChanged', ...);
}

});</code></pre>

咦,似乎哪里不對?

remove 需要接受兩個參數,第一個是 EventName,第二個是 Callback,但是我們的 Callback 以匿名函數的方式寫在了 event.on(...) 的調用語句里面

好吧,那我們不得不修改一下語句的調用方式:

//page1.js
var event = require('../../utils/event.js');
Page({
    onDataChanged: function(changedData) {
        this.setData({
            data: changedData
        })
    },

onLoad: function() {
    event.on('DataChanged', this.onDataChanged);
},

onUnload: function() {
    event.remove('DataChanged', this.onDataChanged);
}

});</code></pre>

這樣就 OK 了么?NO NO NO NO

熟悉 Javascript this 這個大坑的朋友們一定會知道,在 onDataChanged 這個函數中調用的 this 并不是我們 Page 中的那個 this,所以根本不可能調用到 this.setData(....),于是我們用 bind 大法稍微調整一下:

onLoad: function() {
    event.on('DataChanged', this.onDataChanged.bind(this));
}

onUnload: function() { event.remove('DataChanged', this.onDataChanged.bind(this)); }</code></pre>

現在OK了么?NO NO NO NO!如果大伙敲代碼試試,就會發現依然還是不行!

因為

this.onDataChanged.bind(this)

會產生一個新的匿名函數,即 bind的 返回值是一個函數,那么在 onLoad 和 onUnload 里面,各自調用了 bind 大法,從而產生了各自的匿名函數,也就是說 event.remove(...) 塞進去的那個函數,并不是 event.on(...) 塞進去的那個函數,這樣就造成了 remove 時無法正確匹配。removeFromCallbacks 的偽代碼大致如下:

function removeFromCallbacks(callbacks, callback) {
    var newCallbacks = [];
    for(var item in callbacks) {
        if(item != callback) {
            newCallbacks.push(item);
        }
    }
    return newCallbacks;
}

所以我們會發現 remove 傳入的 callback 永遠無法在 callbacks 數組中被匹配到,從而也就無法正確移除了。

最終的代碼實現

當 EventName + Callback 無法唯一決定需要移除的監聽者時,那么自然想到的就是再增加一個 key 值,我們可以用Page自身的某個特性來做 key,比如 page name ,新的 remove 原型如下:

function remove(eventName, pageName, callback);

pageName 是一個字符串,如果開發者不能做到全局內 page name 唯一的話(比如開發者一不小心寫錯了),那就可能會出現后來監聽者沖掉前面監聽者的情況,從而造成無法收到通知的 bug。 所以這里看起來還是用 page 的 this 做 key 比較靠譜,修改后的函數原型如下:

function on(name, self, callback);

function remove(name, self, callback);</code></pre>

讓我們來看看內部具體怎么實現。以下是一個完整的 on 函數實現:

function on(name, self, callback) {
    var tuple = [self, callback];
    var callbacks = events[name];
    if (Array.isArray(callbacks)) {
        callbacks.push(tuple);
    }
    else {
        events[name] = [tuple];
    }
}
  • 第二行我們將 self (即 page 的 this)和 callback 合并成一個 tuple
  • 第三行從 events 容器中,取出該 EventName 下的監聽者數組 callbacks
  • 如果該數組存在,則將 tuple 加入數組;如果不存在,則新建一個數組。

remove的完整實現:

function remove(name, self) {
    var callbacks = events[name];
    if (Array.isArray(callbacks)) {
        events[name] = callbacks.filter((tuple) => {
            return tuple[0] != self;
        });
    }
}
  • 第二行從 events 容器中,取出該 EventName 下的監聽者數組 callbacks
  • 如果 callbacks 不存在,則直接返回
  • 如果存在,則調用 callbacks.filter(fn) 方法

filter 方法的含義是通過 fn 來決定是否過濾掉 callbacks 中的每一個項。fn 返回 true 則保留,fn 返回 false 則過濾掉。所以我們調用 callbacks.filter(fn) 后,callbacks 中的每一個 tuple 都會被依次判定。

fn的定義為:

(tuple) => { 
    return tuple[0] != self; 
}

tuple 中的第一個元素 self 和 remove 傳入的 self 相比較,如果不相等則返回 true 被保留,如果相等則返回 false 被過濾掉。 callbacks.filter(fn) 會返回一個新的數組,然后重新寫入 events[name],最終達到移除callbacks中某一項的邏輯。

最后再來看看emit的實現:

function emit(name, data) {
    var callbacks = events[name];
    if (Array.isArray(callbacks)) {
        callbacks.map((tuple) => {
            var self = tuple[0];
            var callback = tuple[1];
            callback.call(self, data);
        });
    }
}
  • 第二行從 events 容器中,取出該 EventName 下的監聽者數組 callbacks
  • 如果 callbacks 不存在,則直接返回
  • 如果存在,則調用 callbacks.map(fn) 方法

和 filter 的用法類似,map 函數的作用相當于 for 循環,依次取出 callbacks 中的每一個項,然后對其執行 fn(tuple),從其名字就可以看出 map 就是映射變換的意思,將 item 變換為另外一種東西,這個映射關系就是fn。

fn 的定義為:

(tuple) => {
    var self = tuple[0];
    var callback = tuple[1];
    callback.call(self, data);
}

對傳入的 tuple,分別取出 self 和 callback,然后調用 Javascript 的 call大法:

fn.call(this, args)

從而最終實現調用到監聽者的目的。

講到這里就基本上差不多了,因為 Event 模塊持有了 Page 的 this,所以一定要在 Page 的 Unload 函數中調用 event.remove(…),不然會造成內存泄露。

 

來自:https://github.com/danneyyang/weapp-event/blob/master/README.md

 

 本文由用戶 vmvg3395 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!