現(xiàn)學(xué)現(xiàn)賣微信小程序開發(fā)(一)
現(xiàn)學(xué)現(xiàn)賣微信小程序開發(fā)(二)
現(xiàn)學(xué)現(xiàn)賣微信小程序開發(fā)(三):引入Rx,為小程序插上翅膀
引入“Rx”,為小程序插上翅膀
對(duì)于我這種“不用Rx會(huì)死星人“來說籍救,一個(gè)平臺(tái)如果沒有Rx,簡直痛苦死了渠抹。所以一直在研究怎么把RxJS引入到微信小程序中蝙昙,這幾天終于有了階段性成果闪萄,那“Rx”為什么加引號(hào)?嗯奇颠,這是個(gè)好問題败去,原因是。烈拒。为迈。經(jīng)過幾天的艱苦奮戰(zhàn),我終于還是沒有找到把RxJS庫正確引入到微信小程序的方法缺菌。所以呢葫辐,我找了一個(gè)替代品:xstream ( https://github.com/staltz/xstream )。這個(gè)類庫呢伴郁,和RxJS差不多耿战,更輕量級(jí),因?yàn)槿サ袅撕枚嗖怀S玫暮椭貜?fù)的操作符焊傅,當(dāng)然寫法上也略有區(qū)別剂陡,感覺其實(shí)沒有RxJS爽,但問題不大狐胎。
xstream的引入
和網(wǎng)上的其他類庫的引入來比較的話鸭栖,xstream引入的步驟不算太煩:
- 找一個(gè)目錄,使用npm install xstream握巢。
- 在小程序工程目錄下新建一個(gè)libs目錄晕鹊,然后再建一個(gè)xstream目錄。
- 然后在
node_modules/xstream
目錄中把index.js
拷貝到libs/xstream
下暴浦。 - 由于這個(gè)文件依賴
symbol-observable
溅话,而symbol-observable
又依賴ponyfill
,所以我們?nèi)?node_modules/symbol-observable/lib
中把index.js
和ponyfill.js
都拷貝到libs/xstream
下歌焦。哦飞几,對(duì)了,要把index.js
改名成symbol-observable.js
要不就和上面重名了独撇。 - 這樣的話xstream的core就可以正常工作了屑墨,但如果你需要一些其他操作符,比如debounce等纷铣,可以去
node_modules/xstream/extra
中找卵史,找到后把js文件(比如debounce.js)拷貝到libs/xstream/extra
中。
![一些額外的操作符可以去xstream的extra目錄中尋找](http://static.zybuluo.com/wpcfan/8rgyt3auxl6619x3wohl9zrg/image_1b5km0j1lk915sh1hnv1ati1a9q9.png)
好了关炼,xstream的引入至此已經(jīng)完畢程腹,我們看看怎么使用。
![引入完畢后的目錄結(jié)構(gòu)](http://static.zybuluo.com/wpcfan/qlyn9o1kra7mm8094n1eo0rt/image_1b5kn7ppn16gofb9gs31p9epm4m.png)
先來體驗(yàn)一下什么是流式編程儒拂,在 pageParams.onLoad
中加上如下代碼寸潦,當(dāng)然別忘了引入xs import xs from '../../libs/xstream/index'
// 每隔1秒計(jì)數(shù)加1,
// 過濾出偶數(shù)
// 將數(shù)字轉(zhuǎn)換為其平方
// 5秒后結(jié)束
let stream = xs.periodic(1000)
.filter(i => i % 2 === 0)
.map(i => i * i)
.endWhen(xs.periodic(5000).take(1))
// 到目前為止,stream還處于idle狀態(tài)
// 從有第一個(gè)訂閱者開始社痛,它就處于激活狀態(tài)了
stream.addListener({
next: i => console.log(i),
error: err => console.error(err),
complete: () => console.log('completed'),
})
到Console中看一下见转,輸出結(jié)果為0,4蒜哀,completed
![console中的輸出](http://static.zybuluo.com/wpcfan/eb5ntpeoz5leguhqiq7j6e10/image_1b5kns5ur1ir018rhm40uj5jdv13.png)
我們來手動(dòng)復(fù)原一下過程斩箫,首先 xs.periodic(1000)
是這樣一個(gè)流
periodic(1000)
---0---1---2---3---4---...
第一秒是發(fā)射0,0是偶數(shù)滿足filter條件撵儿,進(jìn)入轉(zhuǎn)換0的平方還是0乘客,結(jié)束條件未滿足,于是輸出0淀歇;第二秒時(shí)發(fā)射1易核,1為奇數(shù)被淘汰;第三秒時(shí)發(fā)射2浪默,2是偶數(shù)滿足filter條件牡直,進(jìn)入轉(zhuǎn)換2的平方是4,結(jié)束條件未滿足纳决,于是輸出4碰逸;第四秒時(shí)發(fā)射3,3為奇數(shù)被淘汰阔加;第五秒時(shí)輸出4饵史,4是偶數(shù)滿足filter條件,進(jìn)入轉(zhuǎn)換4的平方是16胜榔,但可惜結(jié)束條件已滿足约急,輸出completed。
這個(gè)小例子雖然簡單苗分,但是涉及到了多個(gè)流式編程的操作符厌蔽,這種串(chain)起來的感覺真是很爽。
微信小程序中的響應(yīng)式編程
由于微信小程序的基于回調(diào)函數(shù)的設(shè)計(jì)摔癣,我們需要對(duì)其API進(jìn)行封裝后使其具備響應(yīng)式編程的能力奴饮。那么我們就拿本次的 todos.onLoad
來練手,沒用xstream之前是下面的樣子择浊。
pageParams.onLoad = function () {
const that = this
wx.request({
url: URL,
data: JSON.stringify({}),
header: { 'content-type': 'application/json' },
method: 'GET',
success: res => {
console.log(res.data)
that.setData({
todos: res.data
})
},
fail: () => console.error('something is wrong'),
complete: () => console.log('get req completed')
})
}
我們來用xstream改造一下吧
import xs from '../../lib/xstream/index'
pageParams.onLoad = function () {
const that = this
const producer = {
start: listener => {
start: wx.request({
url: URL,
data: JSON.stringify({}),
header: { 'content-type': 'application/json' },
method: 'GET',
success: res => listener.next(res),
fail: () => listener.error('something is wrong'),
complete: () => listener.complete()
})
},
stop: () => {}
}
let http$ = xs.create(producer)
http$.subscribe({
next: res => that.setData({
todos: res.data
}),
error: console.log('http request failed'),
complete: console.log('http request completed')
})
}
我勒個(gè)去戴卜,這比原來代碼還多,搞什么琢岩?先別急投剥,我們仔細(xì)想想其實(shí)前面的一大部分代碼是在將傳統(tǒng)的函數(shù)改造成流式的函數(shù)。這些改造工作如果在普通的HTML+Javascript環(huán)境中是很好解決的担孔,因?yàn)椴徽撌荝xJS還是xstream都提供了諸如 from
和 fromEvent
等轉(zhuǎn)換類操作符可以方便的幫我們進(jìn)行這種傳統(tǒng)到流式的轉(zhuǎn)換江锨。但現(xiàn)在不行啊吃警,這些老外的類庫寫的時(shí)候肯定不會(huì)考慮微信的,那怎么辦啄育?只好自己寫吧酌心。
還是拿這個(gè)例子練手,我們創(chuàng)建一個(gè)叫 http.js
的文件挑豌,在這里面我們對(duì)應(yīng)4種request方法(GET安券,POST,PUT氓英,DELETE)分別構(gòu)造了專門的函數(shù)用語轉(zhuǎn)換侯勉。
import xs from '../lib/xstream/index'
const REQ_METHOD = {
GET: 'GET',
POST: 'POST',
PUT: 'PUT',
DELETE: 'DELETE'
}
let http = {}
http.get = (url, data={}, header={'content-type': 'application/json'}) => {
return http_request(url, REQ_METHOD.GET, data, header)
}
http.post = (url, data={}, header={'content-type': 'application/json'}) => {
return http_request(url, REQ_METHOD.POST, data, header)
}
http.put = (url, data={}, header={'content-type': 'application/json'}) => {
return http_request(url, REQ_METHOD.PUT, data, header)
}
http.delete = (url, data={}, header={'content-type': 'application/json'}) => {
return http_request(url, REQ_METHOD.DELETE, data, header)
}
function http_request(
url,
method=REQ_METHOD.GET,
data={},
header={'content-type': 'application/json'}) {
const producer = {
start: listener => {
wx.request({
url: url,
data: JSON.stringify(data),
header: header,
method: method,
success: res => listener.next(res),
fail: () => listener.error(`http request failed: ${url} | method: ${method} | data: ${data} | header: ${header}`),
complete: () => listener.complete()
})
},
stop: () => {}
}
return xs.create(producer)
}
module.exports = {
http: http
}
這樣一個(gè)工具類建好之后呢,我們的 onLoad
函數(shù)就變得很簡單了是不是铝阐?
pageParams.onLoad = function() {
const that = this
http.get(URL).subscribe({
next: res => that.setData({
todos: res.data
}),
error: err => console.error(err),
complete: () => console.info('Todos--get completed')
})
}
你想了一下跟我說址貌,你特么唬我是不是,我不用xstream也可以這樣封裝饰迹,代碼也會(huì)簡潔很多啊芳誓。別急別急,我們費(fèi)這么大勁把它轉(zhuǎn)換成流式函數(shù)啊鸭,不是只是為了簡潔锹淌,而是我們可用使用響應(yīng)式編程的很多特性了。比如上面的代碼我們加一個(gè)需求赠制,在出錯(cuò)后再進(jìn)行若干次重試赂摆,但一切要控制在一個(gè)超時(shí):比如10秒內(nèi)。這個(gè)需求其實(shí)還是挺常見的钟些,但是常規(guī)寫法是比較痛苦的烟号。我們看看用響應(yīng)式編程方式怎么做。
let demo$ = xs.periodic(1000)
.map(x => {
const i = Math.floor((Math.random() * 10) + 1);
if(x > i)
x.throw(new Error('something is wrong'))
return x
})
demo$
.replaceError((err) => demo$)
.endWhen(xs.periodic(10000))
.subscribe({
next: x => console.log(x),
error: err => console.warn(err),
complete: () => console.info('I am completed')
})
上面代碼中我們每隔一秒( periodic(1000)
)輸出一個(gè)從0開始每次增長1的自然數(shù)政恍,然后在轉(zhuǎn)換函數(shù)中生成一個(gè)1-10的隨機(jī)數(shù)汪拥,如果前面數(shù)據(jù)流發(fā)射的數(shù)大于這個(gè)隨機(jī)數(shù),我們就手動(dòng)拋出一個(gè)異常篙耗,反之原樣返回這個(gè)數(shù)字迫筑。定義好這個(gè)數(shù)據(jù)流后,我們按需求進(jìn)行處理:
第一個(gè)需求:遇到異常應(yīng)該重試宗弯,那我們使用 replaceError((err) => demo$)
脯燃,每次遇到異常,我們都再執(zhí)行一遍前面的數(shù)據(jù)流蒙保。
第二個(gè)需求:我們應(yīng)該控制超時(shí)時(shí)間10秒辕棚,所以使用 .endWhen(xs.periodic(10000))
這樣就輕松的解決了這個(gè)問題,我們來看看輸出,一開始從0-3是比較正常逝嚎,然后程序拋出了異常扁瓢。我們的 replaceError((err) => demo$)
捕獲到這個(gè)異常并且用 demo$
替換錯(cuò)誤,也就是說再次執(zhí)行懈糯。慢著涤妒,那不是死循環(huán)了嗎单雾?沒事赚哗,我們后面有個(gè)退出條件就是10秒結(jié)束該流。
![Console中demo$的輸出](http://static.zybuluo.com/wpcfan/oydqggws57qr8x0i9onuictp/image_1b5q3srp4t1o1hgg16mo5dr18nk9.png)
當(dāng)然需要注意一點(diǎn)硅堆,在xstream中所有的流默認(rèn)都是Hot Observable屿储。怎么理解這個(gè)概念呢,想象一下我們?cè)诳措娨曋辈ソヌ樱覀兯械娜瞬还苣闶鞘裁磿r(shí)候打開的電視够掠,我們開的內(nèi)容、進(jìn)度都是一樣的茄菊。但Cold Observable并不一樣疯潭,相當(dāng)于是網(wǎng)絡(luò)視頻,你看到第20分鐘面殖,但這個(gè)時(shí)候我打開還是從頭開始看竖哩。這個(gè)內(nèi)在含義我們舉一個(gè)小例子,下面是用RxJS寫的一個(gè)每隔1秒生成一個(gè)增長1的自然數(shù)流脊僚,第一個(gè)訂閱者立即開始相叁,另一個(gè)訂閱者2秒之后開始訂閱,我們會(huì)看到下面的景象
![RxJS實(shí)現(xiàn)的Cold Obervable的效果](http://static.zybuluo.com/wpcfan/y8tcshg5kaswczk6i5a01r1r/image_1b5q4896m1h3g99o110qbunv5pm.png)
但同樣邏輯用xstream實(shí)現(xiàn)的下面代碼辽幌,出來的是另一番景象增淹。
let demo$ = xs.periodic(1000)
demo$.addListener({
next: x => console.log(x)
})
setTimeout(()=>{
demo$.addListener({
next: x => console.log(x)
})
}, 2000)
![用xstream生成的Hot Observable的效果](http://static.zybuluo.com/wpcfan/lzemqalzbxdo9owcb8vxhj4b/image_1b5q5cksq14cu1lac1n94nkjm3f13.png)
當(dāng)然在很多場(chǎng)景中,這種差別不會(huì)帶來本質(zhì)的變化乌企,比如http請(qǐng)求虑润,本身就是一次性的請(qǐng)求,所以hot和cold的結(jié)果是一樣的加酵。當(dāng)然RxJS作為大而全的類庫是既支持Hot Observable又支持Cold Observable的拳喻。xstream的作者其實(shí)也是RxJS的contributor,但他認(rèn)為在web前端領(lǐng)域hot的應(yīng)用頻率遠(yuǎn)比cold要強(qiáng)虽画,所以做了這個(gè)精簡版的響應(yīng)式類庫舞蔽。
事件的處理
上述方法用于普通API的封裝一點(diǎn)問題也沒有,但是在做輸入事件時(shí)码撰,我遇到了一些小麻煩渗柿。當(dāng)然獲取輸入事件并不是很困難,微信小程序?qū)τ谳斎胧录慕壎ㄒ彩窃?wxml
中的 <input>
控件中用 bindinput
來指定一個(gè)eventHandler,這里我們起了個(gè)名叫 addTodo
朵栖。
<input bindinput="addTodo" placeholder="What do you want to do today?"/>
標(biāo)準(zhǔn)的微信小程序可以這樣來寫事件處理颊亮。
pageParams.addTodo = function(event) {
...
}
如果要把事件截獲并以數(shù)據(jù)流輸出的話,我們需要在onLoad中進(jìn)行事件處理函數(shù)的定義陨溅,比如下面的代碼可以讓我們實(shí)現(xiàn)對(duì)于輸入事件的定義终惑,在其定義中我們其實(shí)使用了流數(shù)據(jù)的發(fā)射作為其函數(shù)體。
pageParams.onLoad = function() {
...
const evProducer = {
start: listener => {
this.addTodo = ev => {
listener.next(ev.detail.value)
}
},
stop: () => { }
}
const input$ = xs.create(evProducer)
}
這樣封裝后门扇,我們可以使用一些操作符來實(shí)現(xiàn)雹有,比如濾波器等功能,下面的代碼片段就是濾掉快速輸入時(shí)(小于400毫秒)的事件臼寄。
input$.compose(debounce(400)).subscribe({
next: val => console.log(val)
})
但上面形式的封裝有個(gè)問題就是我們要把這個(gè)封裝提取出來作為一個(gè)單獨(dú)函數(shù)時(shí)霸奕,由于this.addTodo仍為初始化,無法作為參數(shù)傳遞吉拳,而且這個(gè) addTodo
我也不想寫死质帅。怎么辦好呢?我試了幾種方案后采用了使用 Object.defineProperty
的形式去動(dòng)態(tài)定義pageParams對(duì)象的命名屬性留攒,當(dāng)然還是有一些問題煤惩,仍然需要給這些方法一個(gè)初始值(有同學(xué)如果有更好的建議請(qǐng)指教)。下面就是目前實(shí)現(xiàn)的抽象封裝代碼炼邀,在下面的代碼中魄揉,由于我們對(duì)外發(fā)射的是事件(event),所以其實(shí)它不光可以用于輸入事件汤善,理論上任意事件都可以什猖。也就是說我們自己實(shí)現(xiàn)了類似 Rx.Observable.fromEvent
的功能。
import xs from '../lib/xstream/index'
let event = {}
event.fromEvent = (srcObj, propertyName) => {
const evProducer = {
start: (listener) => {
Object.defineProperty(
srcObj,
propertyName,
{value: ev => listener.next(ev)})
},
stop: () => {}
}
return xs.create(evProducer)
}
module.exports = {
event: event
}
Todo的完善
按著我們上面的封裝红淡,現(xiàn)在的 todos.js
看起來是這個(gè)樣子不狮。我們可以看到和Web API交互的部分都不在Page中了,雖然微信中不支持service在旱,但我們完全可以另寫一個(gè)文件存儲(chǔ)Web API的交互摇零。
import { xs, http, event, debounce } from '../../wxstream/index'
const URL = 'http://localhost:3000/todos'
// 非常遺憾的是目前仍需要初始化event handler,否則會(huì)出現(xiàn)undefined錯(cuò)誤
let pageParams = {
data: { todos: [] },
addTodo: () => {},
changeText: () => {},
removeTodo: () => {},
toggleTodo: () => {}
}
// 獲得所有todos的流
const todos$ = http.get(URL).map(res => res.data)
// 輸入框的文本變化事件流
const input$ = event
.fromEvent(pageParams, 'changeText')
.compose(debounce(500))
.map(ev => ev.detail.value)
// 添加按鈕的點(diǎn)觸事件流
const addTodo$ = event
.fromEvent(pageParams, 'addTodo')
.mapTo(null) //null because we do NOT care about the value
// 根據(jù)按鈕的點(diǎn)擊和輸入合并成一個(gè)新的流桶蝎,提交服務(wù)器產(chǎn)生新的Todo
const newTodo$ = xs.combine(input$, addTodo$)
.map(([input, click]) => {
const todoToBeAdded = {
desc: input,
completed: false
}
return http.post(URL, todoToBeAdded)
})
.flatten()
// 監(jiān)視toggleTodo事件驻仅,該事件發(fā)生后提交服務(wù)器更新該Todo
const toggleTodo$ = event
.fromEvent(pageParams, 'toggleTodo')
.map(ev => ev.target.dataset.todo)
.map(todo => {
const url = `${URL}/${todo.id}`
const updatedTodo = Object.assign({}, todo, { completed: !todo.completed })
return http.put(url, updatedTodo).mapTo(updatedTodo)
})
.flatten()
// 監(jiān)視r(shí)emoveTodo事件,該事件發(fā)生后提交服務(wù)器更新該Todo
const removeTodo$ = event
.fromEvent(pageParams, 'removeTodo')
.map(ev => ev.target.dataset.todo)
.map(todo => {
const url = `${URL}/${todo.id}`;
return http.delete(url, todo).mapTo(todo)
})
.flatten()
let sub_todos, sub_new, sub_toggle, sub_remove
// 現(xiàn)在頁面邏輯中沒有服務(wù)端API的交互了登渣,只有對(duì)成員數(shù)組的控制
pageParams.onLoad = function() {
const that = this
sub_todos = todos$.subscribe({
next: todos => that.setData({
todos: todos
}),
error: err => {console.log(err)},
complete: () => {console.log('Todos--get completed')}
})
sub_new = newTodo$.subscribe({
next: res => that.setData({
todos: [
...that.data.todos,
res.data
]
})
})
sub_toggle = toggleTodo$.subscribe({
next: value => that.setData({
todos: that.data.todos.map(todo => {
if (todo.id === value.id) {
return value
}
return todo
})
}),
error: err => console.error(err),
complete: () => console.info('Todos--toggle completed')
})
sub_remove = removeTodo$.subscribe({
next: value => that.setData({
todos: that.data.todos.filter(todo => todo.id !== value.id)
}),
error: err => console.error(err),
complete: () => console.info('Todos--toggle completed')
})
}
// 取消訂閱噪服,釋放內(nèi)存
pageParams.onUnload = () => {
if(sub_todos !== null)
sub_todos.unsubscribe()
if(sub_new !== null)
sub_new.unsubscribe()
if(sub_toggle !== null)
sub_toggle.unsubscribe()
if(sub_remove !== null)
sub_remove.unsubscribe()
}
Page(pageParams)
wxstream(微信小程序的流式封裝)
我為了這件事,建了一個(gè)Github項(xiàng)目叫 wxstream
( https://github.com/wpcfan/wxstream ) 就是“微信stream”的縮寫胜茧。只要把這個(gè)項(xiàng)目拉下來粘优,拷貝到微信小程序目錄仇味,就立即可用了,包括xstream的支持都在里面了雹顺。目前還沒什么文檔丹墨,接口也大部分都沒測(cè)過呢,實(shí)在汗顏嬉愧。后續(xù)我逐漸添加文檔和進(jìn)行測(cè)試吧贩挣,現(xiàn)在只是個(gè)骨架,大家也幫忙測(cè)一下吧;-)没酣。我看看過幾天有時(shí)間再研究一些redux怎么在微信小程序中使用王财。