我为什么从 React 转向 Cycle.js

独家号 不错的好文 原文链接

编者按:本文由is_january在众成翻译平台上翻译。

(图片来自网络)

我猜现在大部分开发者都在使用某种框架来开发应用,框架可以帮助我们架构复杂的应用,同时为我们节约时间。每天我们都会看到许多关于框架的讨论,关注哪种框架最好、哪种框架你应该最先学习等等。今天,我将要分享我的经验,以及为什么我会从 React 转向 Cycle.js。

React 也许是当下最流行的前端框架,而且它也有一个很棒的社区。我是 React 的一个狂热粉丝,它让我重新认识了 Web 应用以及它的开发方式。有些开发者喜欢它,而有些则认为它并不像大家说的那么好。

不知道大家在开始使用 React 之前,会不会去想是否还会有一种更好的方式来搭建 Web 应用呢?抱着这样的思考,我去尝试了 Cycle.js,一种日渐流行的新的响应式框架。在本文中,我要阐述响应式编程是什么,Cycle.js 如何工作,以及为什么我认为它比 React 更好。赶紧上车吧!

什么是响应式编程?

响应式编程(Reactive programming,简称 RP) 是一种基于异步数据流的编程。如果你开发过 web 应用,你也许做了很多响应式编程。举例来说,点击事件就是异步数据流,我们可以监听它们,来做一些效果。响应式编程的思想是提供一种能从任何地方创建数据流并操纵它们的能力,那么我们就能对所有的功能做出同样的抽象层,这些实际功能将会更容易使用、维护以及测试。

你也许正在想:为什么我需要响应式编程呢?答案很简单:响应式编程将能帮助你统一代码,增强代码一致性。你不用考虑这些东西应该如何工作,以及如何正确地实现它们。你只需要以同样的方式编码,不用管你针对什么数据对象(点击事件,HTTP 请求,web socket,等等)。所有东西都只是一个数据流,并且每一个流都有很多函数,你可以使用这些函数来和流一起工作,像是mapfilter 函数。这些函数会返回新的你能使用的流,诸如此类。

响应式编程将会最大程度上抽象你的代码,它将会给你提供一种创建交互式用户体验和专注在业务逻辑上的能力。

图片来自 https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

Javascript 中的响应式编程

在 Javascript 中,有很多用来处理数据流的优秀的库,最著名的一个是 RxJS 。它是一个 ReactiveX 的扩展,ReactiveX 是一个使用可观察流的异步编程 API。你可以创建一个 Observable (一个数据流),然后用各种函数来操作它。

第二个是 Most.js。它拥有绝佳的性能,并且提供了数据来证明: 性能对比

还有一个小而快的库,由 Cycle.js 的作者专门为 Cycle.js 制作。它叫做 xstream,它只有 26 个方法,大约 30Kb,而且也是 JS 里响应式编程方面最快的库之一。

在下面的例子里,我将使用 xstream 库。Cycle.js 是以一个轻量级框架的目标编写的,所以我想要给它最轻量的响应式库。

什么是 Cycle.js?

Cycle.js 是一个函数式的、响应式的 Javascript 框架。它把你的应用抽象成为一个纯函数:main()。在函数式编程里,函数应该只有输入和输出,而没有其它作用。在 Cycle.js 的main() 函数里,输入是来函数外部,输出也只会影响函数外部。管理不同的效果是通过 drivers 来完成的,drivers是用来处理 DOM 结果、HTTP 结果和 web socket 等等的插件。

图片来自 Cycle.js 的网站

Cycle.js 是来帮助我们构建我们的 UI,测试 UI,以及编写可复用的代码的,每个组件都只不过是个能独立运行的纯函数。

核心 API 只有一个函数,run 函数。

`run(app, drivers);`

run 函数有两个入参,appdriversapp 是主纯函数,drivers 是处理数据的插件。

Cycle.js 把额外的功能分成了更小的模块,他们是:

  • @cycle/dom – 一个和 DOM 一起工作的 drivers 集合;它有一个 DOM driver 和一个 HTML driver,它们是基于snabdom 的虚拟 DOM 库。

  • @cycle/history – 一个有关 history API 的 driver

  • @cycle/http –  一个给 HTTP 请求用的 driver,基于 superagent

  • @cycle/isolate – 一个用来制作带作用域数据流组件的功能

  • @cycle/jsonp – 通过 JSONP 来发出 HTTP 请求

  • @cycle/most-run – 用 most 写的给应用使用的 run 函数

  • @cycle/run – 用 xstream 写的给应用使用的 run 函数

  • @cycle/rxjs-run – 用 rxjs 写的给应用使用的 run 函数

Cycle.js 代码

让我们来看一些 Cycle.js 实践吧?下面我们会创建一个应用,展示 Cycle.js 是如何工作的。我认为计数器应用对于这个例子来说应该比较理想,我们将会看到在 Cycle.js 里是如何处理 DOM 事件和重新渲染 DOM 的。

让我们创建两个文件,index.htmlmain.jsindex.html 只是为了引入 main.jsmain.js 是会处理所有逻辑。我们同样会创建一个 package.json 文件,所以运行:

npm init -y

下一步,安装主要的依赖:

npm install @cycle/dom @cycle/run xstream --save

这会安装 @cycle/dom, @cycle/xstream-runxstream。我们同样也需要babel, browserifymkdirp,所以把它们一起安装: @cycle/dom, @cycle/xstream-run, 以及xstream.

npm install babel-cli babel-preset-es2015 babel-register babelify browserify mkdirp --save-dev

因为要用 Babel 工作,创建一个包含以下内容的 .babelrc 文件:

{

  "presets": ["es2015"]

}

为了更方便的运行我们的应用,在 package.json 里面添加脚本:

"scripts": {

  "prebrowserify": "mkdirp dist",

  "browserify": "browserify main.js -t babelify --outfile dist/main.js",

  "start": "npm install && npm run browserify && echo 'OPEN index.html IN YOUR BROWSER'"

}

我们会使用 npm run start来运行我们的 Cycle.js 应用。

就这样,我们的安装已经完成了,现在我们可以开始写代码了。先往 index.html 里面添加一点 HTML 代码:

< !DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8"/>

    <title>Cycle.js counter</title>

</head>

<body>

    <div id="main"></div>

    ``<script src="./dist/main.js">``</script>

</body>

</html>

我们创建了一个 id 为 main 的 div,Cycle.js 会关联这个 div 并且在里面渲染整个应用。我们同样已经引入了 dist/main.js 文件,它是 main.js 编译和打包后的 JS 文件。

现在我们来写点 Cycle.js 代码。打开 `main.js` 文件,然后 import 所有我们需要的依赖:

import xs from 'xstream';

import { run } from '@cycle/run';

import { div, button, p, makeDOMDriver } from '@cycle/dom';

我们引入了 xstreamrunmakeDOMDriver 能帮助我们和虚拟 DOM 一起工作的函数(这个例子里是divbuttonp).

完成了 main 函数,它看起来应该像这样:

function main(sources) {

  const action$ = xs.merge(

    sources.DOM.select('.decrement').events('click').map(ev => -1),

    sources.DOM.select('.increment').events('click').map(ev => +1)

  );


  const count$ = action$.fold((acc, x) => acc + x, 0);


  const vdom$ = count$.map(count =>

    div([

      button('.decrement', 'Decrement'),

      button('.increment', 'Increment'),

      p('Counter: ' + count)

    ])

  );


  return {

    DOM: vdom$,

  };

}


run(main, {

  DOM: makeDOMDriver('#main')

});

这就是 main 函数,它获取了我们的 sources(源)并且返回了sinks。输入是 DOM 流,输出是虚拟 DOM,接下来我们一部分一部分来解释。

const action$ = xs.merge(

  sources.DOM.select('.decrement').events('click').map(ev => -1),

  sources.DOM.select('.increment').events('click').map(ev => +1)

);

这里,我们把两个事件流合并成一个叫做 action$ 的流(通常在包含流的变量名后面添加一个带 $ 的后缀)。一个在 decrement 的点击流上,另一个在 increment 按钮上。我们把这两个事件分别映射到数字 -1+1 上。合并结束以后,这个 action$ 流应该像这样:

----(-1)-----(+1)------(-1)------(-1)------

下一个流是 count$,它创建出来后长这样:

const count$ = action$.fold((acc, x) => acc + x, 0);

这里的 fold 函数很具有参考性,它接受两个参数,accumulateseedseed 在事件一触发的时候就被触发,下一个事件基于 accumulate 函数和 seed 结合在一起,基本上来说,它就是一个流的 reduce()

我们的 count$ 流接收 0 作为起始值,然后对于每一个从 action$ 流中获得的新值,我们把它和 count$ 流中的当前值加起来。

最后,为了能把整个流程跑起来,我们需要在 main 下面调用 run 函数。

最后一件事就是创建一个虚拟 DOM,这是完成虚拟 DOM 的代码:

const vdom$ = count$.map(count =>

  div([

    button('.decrement', 'Decrement'),

    button('.increment', 'Increment'),

    p('Counter: ' + count)

  ])

);

我们对 count$ 流里的数据做了一个遍历,并且给流中的每项都返回了虚拟 DOM,此虚拟 DOM 包含了一个 div 包装(wrapper),两个 button,一个 p。如你所见, Cycle.js 使用 Javascript 函数来使用 DOM,但是 JSX 也能做到

在 `main` 函数的最后,我们返回了虚拟 DOM:

return {

  DOM: vdom$,

};

我们传递了 main 函,以及一个 ID 为 main 的 div 的 DOM 驱动,同时我们也从这个 div 获取了事件流。我们结束了整个流程,并且写了一个完美的 Cycle.js 应用。

实际的效果是这样的:

收工!这就是 DOM 流工作的方式。如果你想要看看 Cycle.js 里的 HTTP 流如何工作,我在博客里写了一篇相关文章 [http://ivanjov.com/working-with-http-streams-with-cycle-js/]。

所有代码都在 Github repo,可以拉下来看看,在你的本地机器上跑跑看。

为什么我从 React 转向 Cycle.js ?

现在你已经响应式编程的基本概念,也看过了 Cycle.js 的一个简单例子,让我们谈谈为什么我会在我的下个项目里使用 Cycle.js.

在我设计网页应用的时候,我遇到的最大问题是怎样处理庞大的代码库,以及如何处理大量来自不同源的数据。我是 React 的粉丝,而且我也在很多项目里使用它,但是 React 没有解决我的问题。

如果讨论渲染数据和改变应用状态,那么 React 还不错。实际上,它的整个组件方法体系的确令人惊艳,而且它真的帮我写出了更好的、可测试的、可维护的代码,但其中总有些疏漏。

我们来看看相较 React 而言,Cycle.js 好在哪里,又差在哪里.

优势

1. 大型代码仓库

当你的应用变得更加庞大的时候,React 会出现一些问题。想你你在 100 个容器里有 100 个组件,每一个组件都有自己的样式、功能和测试,那将会有巨多目录里的巨多文件,在这么多的文件里还有超多行的代码.

Cycle.js 在这些方面能帮到我们。它的设计初衷就是通过把项目分解成可独立的、可测试的组件,来解决大型代码仓库的问题,而没有任何副作用。

2. 数据流

数据流是我在 React 里遇到的最大问题。React 一开始并不是为数据流设计的,数据流不是 React 的核心。开发者已经在尝试解决这个问题,我们有很多库和方法,努力去解决这个问题,其中最受欢迎的是 Redux。但 Redux 并不完美,你需要花一些时间去配置,并且需要专门写一些代码而只是为了用数据流工作。

Cycle.js 的作者希望用 Cycle.js 来创建一种能够处理数据流的框架,因为你不应该考虑数据流。你只需要写一些用数据做某种操作的函数,然后 Cycle.js 会来解决其他的一切。

3. 副作用(side effects)

React 在处理副作用上存在一些问题。在 React 应用里,没有标准化的方法来处理副作用。有许多工具来帮你解决这个问题,但那同样需要花些时间去安装和学习如何使用。最流行的有 redux-saga, redux-effects, redux-side-effects, and redux-loop。你知道我什么意思吧?有太多这样的工具,你需要选择合适的库,并且在你的代码里使用它。

Cycle.js 不需要那么做,你只要简单地引入你想要的驱动(DOM,HTTP 或其他) 然后使用它,驱动会把数据发送给你的纯函数,你可以修改它,并且发回给要渲染这些数据或做其他事情的驱动。最重要的是,它是标准化的!这就是 Cycle.js 带来的好处,而且你不需要依赖一个第三方库。就是这么简单!

4. 函数式编程

最后同样重要的是,函数式编程。React 作者宣称 React 使用了函数式编程,但事实并非真正如此。React 中有很多面向对象编程、类、this 关键字的使用,这些都会在没有被正确使用的时候让你头疼。Cycle.js 是用函数式编程范例构建的,所有东西都是不依赖于任何外部状态的函数。另一方面,也没有任何类或者类似类的概念,更易于测试和维护。

缺陷

1. 社区

现在,React 是最受欢迎的框架并且被广泛使用,Cycle.js 则不然。它目前仍没有那么流行,因此当你遇到一些预期外的问题,并且不能在你的代码里找到这个问题的解决方案的时候,这会是一个大问题。有时候你无法在网上找到答案,只能自己吭哧吭哧硬着头皮上。如果你在一些边缘项目又有充足时间的话,这不是问题,但如果你在一家工期很紧的公司里上班会怎么样呢?你会花费一些时间 debug 你的代码。

但情况正在好转,很多开发者开始使用 Cycle.js 并且在讨论它,讨论问题,然后一起解决问题。Cycle.js 同样也有很漂亮的文档,上面有很多例子,而且至今我都没有遇到任何复杂到难以 debug 的问题。

2. 学习一个新的范式

响应式编程是一种不同的范式,你会花点时间来习惯这些事该怎么做。如果你掌握了,那就没什么难的了,但如果你的工期很紧,那么花时间学新东西会是个麻烦。

3. 某些应用不需要用响应式来做

是的,有些应用真的不需要做成响应式。博客、市场网站、登录页面、以及其它只有有限交互的静态站点,都不需要做成响应式。没有数据会在应用里实时使用,没有那么多的表格和按钮。使用响应式框架也许会让这些站点有点慢,你应该在一个 web 应用需要使用 Cycle.js 的时候再来使用它。

结语

一个理想的框架应该帮助你专注在构建和发布新功能上,而且不能强迫你写一些模板代码。我认为 Cycle.js 已经向我们展示了这是完全有可能的,也让我们去寻找更好的方式以完成我们的代码和发布特性。但记住,没有什么是完美的,总是会有提高的空间。

你试过响应式编程或 Cycle.js 了吗?我有没有说服你去尝试一下?在评论里让我了解你的想法!


奇舞周刊

——————————————————

领略前端技术 阅读奇舞周刊


长按二维码,关注奇舞周刊



开发者头条

程序员分享平台