如何在微信小程序里面實現跨頁面通信?
如何在微信小程序里面實現跨頁面通信?
我們在處理業務需求的時候,常常會遇到一些情況,在二級或者三級頁面進行某些操作或者變更后,需要將結果通知到上級頁面去。比如:
- 選擇了某些配置項,點擊保存后,外部頁面能夠立即變更
- 在上傳頭像頁面,上傳完畢后,外部頁面的頭像能夠立即顯示為新頭像。
所以,這個時候就涉及到如何在頁面之間通信的問題了。 跨頁面通信進一步說其實就是一個程序內部的事件通知機制問題,在其他平臺或者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