本文深入解析 Cycle.js 框架,阐述其基于 RxJS 的函数式响应式架构。通过纯函数 main()、Drivers 驱动机制及 Observable 数据流,实现人机交互闭环。文章包含两个完整实战案例,演示计数器与 GitHub API 搜索功能的实现,涵盖环境配置、代码结构及核心 API 使用。
Cycle.js 核心原理
简介
Cycle.js 是一个极简的 JavaScript 框架,其核心代码(含注释)仅 125 行。它提供了一种函数式、响应式的人机交互接口(以下简称 HCI):
函数式
Cycle.js 将应用程序抽象为一个纯函数 main()。该函数从外部世界读取副作用(sources),生成输出(sinks)并传递至外部世界以形成副作用。这些外部世界的副作用作为 Cycle.js 的插件存在(drivers),负责处理 DOM、提供 HTTP 访问等具体任务。
响应式 Cycle.js 利用 rx.js 实现关注点分离,这意味着应用程序基于事件流构建,数据流表现为 Observable:
HCI
HCI 是一种双向对话,人与机器互为观察者。
在这个交互模型中,人机之间的信息流互为输出输出,构成一个循环,也即 Cycle这一命名所指,框架的Logo更是以莫比乌斯环贴切的描述了这个循环。cycle_log
唯一的疑问可能是:循环无头无尾,信息流从何发起?这是一个好问题,答案是:
However, we need a .startWith() to give a default value. Without this, nothing would be shown! Why? Because our sinks is reacting to sources, but sources is reacting to sinks. If no one triggers the first event, nothing will happen. —— via examples
通过 .startWith() 提供的初始值,整个流程得以启动,从而形成一个闭环,一个事件驱动的永动机。
Drivers
Driver 是 Cycle.js 主函数 main() 与外部世界交互的接口,例如 HTTP 请求和 DOM 操作,这些由具体的 driver 负责。它的存在确保了 main() 的纯函数特性,所有副作用和繁琐细节均由 driver 实施——因此 @cycle/core 仅 125 行,而 @cycle/dom 却高达 4052 行。
Driver 也是一个函数。从流程上看,driver 监听 sinks(main() 的输出)作为输入,执行命令式副作用,并生成 sources 作为 main() 的输入。
DOM Driver
即 @cycle/dom,是使用最频繁的 driver。实际应用中,我们的 main() 会与 DOM 进行交互:
- 当需要向用户传递内容时,main() 返回新的 DOM sinks,以触发 domDriver() 生成 virtual-dom 并进行渲染。
- main() 订阅 domDriver() 的输出值(作为输入),并据此进行响应。
组件化 无论 Cycle.js 应用程序多么复杂,都遵循一套基本的输入输出规则,因此实现组件化非常容易,无非是函数对函数的组合调用。
Cycle.js 实战指南
实战
准备工作
安装全局模块
依赖模块一览
"devDependencies": {
"babel-plugin-transform-react-jsx": "^6.8.0",
"babel-preset-es2015": "^6.9.0",
"babelify": "^7.3.0",
"browserify": "^13.0.1",
"uglifyify": "^3.0.1",
"watchify": "^3.7.0"
},
"dependencies": {
"@cycle/core": "^6.0.3",
"@cycle/dom": "^9.4.0",
"@cycle/http": "^8.2.2"
}
.babelrc (插件支持 JSX 语法)
{
"plugins": [
["transform-react-jsx", { "pragma": "hJSX" }]
],
"presets": ["es2015"]
}
Scripts(热生成和运行服务器)
"scripts": {
"start": "http-server",
"build": "../node_modules/.bin/watchify index.js -v -g uglifyify -t babelify -o bundle.js"
}
以下实例运行时,需开启两个 shell,一个运行热编译,另一个启动 http-server(也可使用 currently)。
交互实例 1
功能:两个按钮,一加一减,从 0 起步,回显计数。 demo 地址: http://output.jsbin.com/lamexacaku
HTML 代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>components</title>
</head>
<body>
<div id="container"></div>
<script src="bundle.js"></script>
</body>
</html>
index.js
import Cycle from '@cycle/core'
import { makeDOMDriver, hJSX } from '@cycle/dom'
function main({ DOM }) {
const decrement$ = DOM.select('.decrement').events('click').map(_ => -1)
const increment$ = DOM.select('.increment').events('click').map(_ => +1)
const count$ = increment$.merge(decrement$)
.scan((x, y) => x + y)
.startWith(0)
return {
DOM: count$.map(count =>
<div>
<input type="button" className="decrement" value=" - "/>
<input type="button" className="increment" value=" + "/>
<div>
Clicked {count} times~
</div>
</div>
)
}
}
Cycle.run(main, {
DOM: makeDOMDriver('#container'),
})
不难看出:
- main() 是一个纯函数,始终不依赖外部状态,其所有动力来源于 DOM 事件源 click。该状态机依靠 Observable.prototype.scan() 进行计算和传递,最后生成 sinks 传递给 DOM driver 以进行渲染;
- 启动这个循环的是 .startWith();
- Cycle.run 是应用程序的入口,加载 main() 和 DOM driver,后者对一个 HTML 容器进行渲染输出。
交互实例 2
功能:一个按钮和一个输入框,输入内容并点击按钮后,通过 Github API 搜索相关的 Repo,回显总数并展示第一页 Repo 列表。
index.js
import Cycle from '@cycle/core'
import { makeDOMDriver, hJSX } from '@cycle/dom'
import { makeHTTPDriver } from '@cycle/http'
const GITHUB_SEARCH_URL = 'https://api.github.com/search/repositories?q='
function main(responses$) {
const search$ = responses$.DOM.select('input[type="button"]')
.events('click')
.map(_ => { return { url: GITHUB_SEARCH_URL } })
const text$ = responses$.DOM.select('input[type="text"]')
.events('input')
.map(e => { return { keyword: e.target.value } })
const http$ = search$.withLatestFrom(text$, (search, text)=> search.url + text.keyword)
.map(state => { return { url: state, method: 'GET' } })
const dom$ = responses$.HTTP
.filter(res$ => res$.request.url && res$.request.url.startsWith(GITHUB_SEARCH_URL))
.mergeAll()
.map(res => JSON.parse(res.text))
.startWith({ loading: true })
.map(JSON => {
return <div>
<input type="text"/>
<input type="button" value="search"/>
<br/>
<span>
{JSON.loading ? 'Loading...' : `total: ${JSON.total_count}`}
</span>
<ol>
{
JSON.items && JSON.items.map(repo =>
<div>
<span>repo.full_name</span>
<a href={ repo.html_url }>{ repo.html_url }</a>
</div>
)
}
</ol>
</div>
}
)
return {
DOM: dom$,
HTTP: http$,
}
}
const driver = {
DOM: makeDOMDriver('#container'),
HTTP: makeHTTPDriver(),
}
Cycle.run(main, driver)
有了实例 1 做铺垫,这段代码也就通俗易懂了,需要提示的是:
- Rx 的 Observable 对象,命名上约定以 $ 符为结束,以示区分。
- Observable.prototype.withLatestFrom() 的作用是:在当前 Observable 对象的事件触发时(不同于 combineLatest),去合并参数的目标 Observable 对象的最新状态,并传递给下一级 Observer。
- 以上项目完整实例,可在 /rockdragon/rx_practise/tree/master/src/web 找到。
小结
寥寥数语,并不足以概括 Cycle.js,例如 MVI 设计模式、Driver 的编写、awesome-cycle 等进阶内容,还是留给读者自行探索吧。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。