开发 | 技术高人如何开发小程序?他们用这套方法
文 | 接灰的电子产品
对于我这种「不用 Rx 会死星人」来说,如果一个平台没有 Rx,在上面写代码会很痛苦。
所以,自从我开始开发微信小程序以来,就在一直在研究怎么把 RxJS 引入到微信小程序中。
这几天,我终于有了阶段性成果。那「Rx」为什么加引号?嗯,原因是……经过几天的艰苦奋战,我还是没找到把 RxJS 库正确引入到微信小程序的方法。
实际上,我找了一个替代品:XStream(https://github.com/staltz/xstream)。这个类库呢,和 RxJS 差不多,但更轻量。
相比 RxJS,XStream 去掉了好多不常用的和重复的操作符,当然写法上也略有区别。用起来,XStream 没有 RxJS 爽,但问题不大。
XStream 的引入
和网上的其他类库比较起来,XStream 引入的步骤不算太烦:
- 找一个目录,
npm install xstream
一下; - 在小程序工程目录下新建一个
libs
目录,然后再建一个xstream
目录。 - 然后在
node_modules/xstream
目录中把index.js
拷贝到libs/xstream
下。 - 去
node_modules/symbol-observable/lib
中,把index.js
和ponyfill.js
都拷贝到libs/xstream
下。 - 把
index.js
改名成symbol-observable.js
,要不然,就会遇到重名问题。 - 如果你需要一些其他操作符,可以去
node_modules/xstream/extra
中找,找到后把相应的 JS 文件(比如debounce.js
)拷贝到libs/xstream/extra
中。
好了,XStream 的引入至此已经完毕,我们看看,如何在小程序工程中使用 XStream 吧。
先来体验一下什么是流式编程。在 pageParams.onLoad
中加上如下代码——当然,别忘了引入 XStream。
import xs from '../../libs/xstream/index'
// 每隔 1 秒计数加 1,
// 过滤出偶数
// 将数字转换为其平方
// 5 秒后结束
let stream = xs.periodic(1000)
.filter(i => i % 2 === 0)
.map(i => i * i)
.endWhen(xs.periodic(5000).take(1))
// 到目前为止,stream 还处于 idle 状态
// 从第一个用户开始,它就会被激活
stream.addListener({
next: i => console.log(i),
error: err => console.error(err),
complete: () => console.log('completed'),
})
到 Console 中看一下,输出结果为 0
、4
、completed
。
我们来手动复原一下过程,首先 xs.periodic(1000)
,是这样一个流:
periodic(1000)
---0---1---2---3---4---...
- 第一秒时,发射
0
,0
是偶数满足filter
条件,进入转换。0
的平方还是0
,结束条件未满足,于是输出0
; - 第二秒时,发射
1
,1
为奇数,被淘汰; - 第三秒时,发射
2
,2
是偶数,满足filter
条件,进入转换。2
的平方是4
,结束条件未满足,于是输出4
; - 第四秒时,发射
3
,3
为奇数,被淘汰; - 第五秒时,输出
4
,4
是偶数,满足filter
条件,进入转换。4
的平方是16
,但结束条件已满足,输出completed
。
这个小例子虽然简单,但是涉及到了多个流式编程的操作符。这种串(chain)起来的感觉真是很爽。
微信小程序中的响应式编程
由于微信小程序的基于回调函数的设计,我们需要对其 API 进行封装后使其具备响应式编程的能力。
首先,没用 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')
})
}
天啊,这比原来代码还多,怎么回事?
先别急,前面的一大部分代码,是在将传统的函数改造成流式的函数。
这些改造工作如果在普通的 HTML+Javascript 环境中是很好解决的,因为不论是 RxJS 还是 XStream,都提供了转换类操作符,可以方便的帮我们进行转换。
但现在不行啊,这些老外的类库写的时候肯定不会考虑微信的。那怎么办?只好自己写吧。
还是这个例子,我们创建一个叫 http.js
的文件。在这里,我们对应 4 种网络请求方法(GET
,POST
,PUT
,DELETE
),分别构造了专门的函数用语转换。
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
}
工具类建好之后,我们的 onLoad
函数就变得很简单了,是吧?
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 也可以这样封装,代码也会简洁很多啊。
别急,我们费这么大劲把它转换成流式函数,不是只是为了简洁,而是能够使用响应式编程更多特性。
比如,上面的代码我们加一个需求:在出错后再进行若干次重试,但需要控制总用时。这个需求很常见,但是常规写法很复杂。
我们看看用响应式编程方式怎么做。
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)
),输出一个从 0 开始、每次增长 1 的自然数。
接着,在转换函数中生成一个 1-10 的随机数。如果前面数据流发射的数大于这个随机数,我们就手动抛出一个异常,反之原样返回这个数字。
定义好这个数据流后,我们按需求进行处理:
- 遇到异常应该重试,那我们使用
replaceError((err) => demo$)
,每次遇到异常,我们都再执行一遍前面的数据流。 - 我们应该控制超时时间 10 秒,所以使用
.endWhen(xs.periodic(10000))
这样,我们就轻松地解决了这个问题。
我们来看看输出,一开始从 0 到 3 都比较正常,然后程序抛出了异常。replaceError((err) => demo$)
捕获到这个异常,并且用 demo$
替换错误,也就是说再次执行。
慢着,那不是死循环了吗?没事,我们设定了一个退出条件,就是 10 秒结束该流。
在这个过程中,我们需要注意:在 XStream 中所有的流默认都是 Hot Observable。
怎么理解这个概念呢?
想象一下,我们在看电视直播,我们所有的人不管你是什么时候打开的电视,我们开的内容、进度都是一样的。这就是 Hot Observable。
但 Cold Observable 并不一样,相当于是网络视频。你看到第 20 分钟后我才打开这个视频,这个时候,我的观看进度是从头开始的。
下面是用 RxJS 写的一个每隔 1 秒生成一个增长 1 的自然数流,第二个用户在前一个用户 2 秒之后开始使用。我们会看到下面的情况。
同样的逻辑,用 xstream 实现的代码,出来的是另一番景象。
let demo$ = xs.periodic(1000)
demo$.addListener({
next: x => console.log(x)
})
setTimeout(()=>{
demo$.addListener({
next: x => console.log(x)
})
}, 2000)
当然在很多场景中,这种差别不会带来本质的变化。比如 HTTP 请求,本身就是一次性的请求,所以 hot 和 cold 的结果是一样的。
RxJS 作为大而全的类库,当然会同时支持 Hot Observable 和 Cold Observable 的。
XStream 的作者其实也是 RxJS 的 contributor(贡献者)。但他认为,在 web 前端领域,hot 的应用频率远比 cold 要多,所以做了这个精简版的响应式类库。
事件的处理
上述方法用于普通 API 的封装一点问题也没有,但是在做输入事件时,我遇到了一些小麻烦。
获取输入事件不困难。小程序输入事件,也是绑定在 WXML 中的 <input>
控件中,用 bindinput
来指定一个 eventHandler。我将它定名为 addTodo
。
<input bindinput="addTodo" placeholder="What do you want to do today?"/>
标准的微信小程序,可以这样来写事件处理。
pageParams.addTodo = function(event) {
//...
}
如果要把事件截获并以数据流输出的话,我们需要在 onLoad
中进行事件处理函数的定义。
比如下面的代码可以让我们实现对于输入事件的定义,在其定义中我们其实使用了流数据的发射作为其函数体。
pageParams.onLoad = function() {
...
const evProducer = {
start: listener => {
this.addTodo = ev => {
listener.next(ev.detail.value)
}
},
stop: () => { }
}
const input$ = xs.create(evProducer)
}
这样封装后,我们可以使用一些操作符来实现诸如滤波器等功能。
下面的代码片段,就是用于过滤快速输入(小于 400 毫秒)事件的。
input$.compose(debounce(400)).subscribe({
next: val => console.log(val)
})
但这种的封装有个问题:我们要把这个封装提取为一个单独函数时,由于 this.addTodo
仍未初始化,他就无法作为参数传递,而且 addTodo
也不能写死。
怎么办?我试了几种方案后,选取了使用 Object.defineProperty
的形式,动态定义 pageParams
对象的命名属性的方法。
当然,这个方法还是有一些问题,比如,你仍然需要给这些方法一个初始值(有同学如果有更好的建议请指教)。
下面就是目前实现的抽象封装代码。在下面的代码中,由于我们对外发射的是事件(event
),所以其实它不光可以用于输入事件,理论上任意事件都可以。
也就是说,我们自己实现了类似 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
}
最后的话
我为了能在微信顺利使用 XStream,建立了一个 Github 项目,名叫 wxstream(https://github.com/wpcfan/wxstream)。
这名字的意思,其实就是「微信+XStream」。只要把这个项目拉下来,拷贝到微信小程序目录,就立即可用了,包括 xstream 的支持都在里面了。
目前还没什么文档,接口也大部分都没测过。后续我逐渐添加文档和进行测试,现在只是个骨架,大家也帮忙测一下吧 ;-)。
原文地址:https://gold.xitu.io/post/5870bd4b61ff4b005c3c4f6e
往期精选文章
本文由知晓程序授权转载,关注微信号 zxcx0101,可获得以下内容和服务:
- 在微信后台回复「1228」,获得全网第一本《微信小程序入门指南》。
- 在微信后台回复「加群」,加入「一起发现小程序」微信交流群。
- 在微信后台回复任意关键词,还能获得相关小程序推荐,赶紧试试吧!