# 开发 \| 技术高人如何开发小程序？他们用这套方法

- 作者：知晓程序
- 发布时间：2017-02-16 12:04
- 原文：https://www.ifanr.com/minapp/787666

![](https://s3.ifanr.com/wp-content/uploads/2017/02/Featured-9.jpg)

文 \| 接灰的电子产品

对于我这种「不用 Rx 会死星人」来说，如果一个平台没有 Rx，在上面写代码会很痛苦。

所以，自从我开始开发微信小程序以来，就在一直在研究怎么把 RxJS 引入到微信小程序中。

这几天，我终于有了阶段性成果。那「Rx」为什么加引号？嗯，原因是……经过几天的艰苦奋战，我还是没找到把 RxJS 库正确引入到微信小程序的方法。

实际上，我找了一个替代品：XStream （ <https://github.com/staltz/xstream> ）。这个类库呢，和 RxJS 差不多，但更轻量。

**相比 RxJS，XStream 去掉了好多不常用的和重复的操作符，当然写法上也略有区别。**用起来，XStream 没有 RxJS 爽，但问题不大。

### XStream 的引入

和网上的其他类库比较起来，XStream 引入的步骤不算太烦：

1. 找一个目录，`npm install xstream` 一下；
2. 在小程序工程目录下新建一个 `libs` 目录，然后再建一个 `xstream` 目录。
3. 然后在 `node_modules/xstream` 目录中把 `index.js` 拷贝到 `libs/xstream` 下。
4. 去 `node_modules/symbol-observable/lib` 中，把 `index.js` 和 `ponyfill.js` 都拷贝到 `libs/xstream` 下。
5. 把 `index.js` 改名成 `symbol-observable.js`，要不然，就会遇到重名问题。
6. 如果你需要一些其他操作符，可以去 `node_modules/xstream/extra` 中找，找到后把相应的 JS 文件（比如 `debounce.js`）拷贝到 `libs/xstream/extra` 中。

![](https://s3.ifanr.com/wp-content/uploads/2017/02/rxjs-1.jpg)

好了，XStream 的引入至此已经完毕，我们看看，如何在小程序工程中使用 XStream 吧。

![](https://s3.ifanr.com/wp-content/uploads/2017/02/rxjs-2.jpg)

先来体验一下什么是流式编程。在 `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`。

![](https://s3.ifanr.com/wp-content/uploads/2017/02/rxjs-3.jpg)

我们来手动复原一下过程，首先 `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 的随机数。如果前面数据流发射的数大于这个随机数，我们就手动抛出一个异常，反之原样返回这个数字。

定义好这个数据流后，我们按需求进行处理：

1. 遇到异常应该重试，那我们使用 `replaceError((err) => demo$)`，每次遇到异常，我们都再执行一遍前面的数据流。
2. 我们应该控制超时时间 10 秒，所以使用 `.endWhen(xs.periodic(10000))`

这样，我们就轻松地解决了这个问题。

我们来看看输出，一开始从 0 到 3 都比较正常，然后程序抛出了异常。`replaceError((err) => demo$)` 捕获到这个异常，并且用 `demo$` 替换错误，也就是说再次执行。

慢着，那不是死循环了吗？没事，我们设定了一个退出条件，就是 10 秒结束该流。

![](https://s3.ifanr.com/wp-content/uploads/2017/02/rxjs-4.jpg)

在这个过程中，我们需要注意：**在 XStream 中所有的流默认都是 Hot Observable。**

怎么理解这个概念呢？

想象一下，我们在看电视直播，我们所有的人不管你是什么时候打开的电视，我们开的内容、进度都是一样的。这就是 Hot Observable。

但 Cold Observable 并不一样，相当于是网络视频。你看到第 20 分钟后我才打开这个视频，这个时候，我的观看进度是从头开始的。

下面是用 RxJS 写的一个每隔 1 秒生成一个增长 1 的自然数流，第二个用户在前一个用户 2 秒之后开始使用。我们会看到下面的情况。

![](https://s3.ifanr.com/wp-content/uploads/2017/02/rxjs-5.jpg)

同样的逻辑，用 xstream 实现的代码，出来的是另一番景象。

```
let demo$ = xs.periodic(1000)

demo$.addListener({
  next: x => console.log(x)
})

setTimeout(()=>{
  demo$.addListener({
    next: x => console.log(x)
  })
}, 2000)
```

![](https://s3.ifanr.com/wp-content/uploads/2017/02/6-6.jpg)

**当然在很多场景中，这种差别不会带来本质的变化。**比如 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>

#### 往期精选文章

- [开发 \| 小程序开发有哪些坑？这份笔记都整理出来了](http://www.ifanr.com/minapp/786415)
- [开发 \| 一篇文章读懂微信小程序视图层](http://www.ifanr.com/minapp/784666)
- [如何在小程序中绘制图表？](http://www.ifanr.com/minapp/783579)

**本文由知晓程序授权转载，关注微信号 zxcx0101，可获得以下内容和服务：**

- 在微信后台回复「1228」，获得全网第一本《微信小程序入门指南》。
- 在微信后台回复「加群」，加入「一起发现小程序」微信交流群。
- 在微信后台回复任意关键词，还能获得相关小程序推荐，赶紧试试吧！

![zxcx&#95;0208](https://s3.ifanr.com/wp-content/uploads/2017/02/zxcx_0208.png)
