惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Privacy & Cybersecurity Law Blog
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
B
Blog RSS Feed
罗磊的独立博客
V
V2EX
V
Visual Studio Blog
博客园 - 叶小钗
W
WeLiveSecurity
小众软件
小众软件
K
Kaspersky official blog
美团技术团队
雷峰网
雷峰网
阮一峰的网络日志
阮一峰的网络日志
Martin Fowler
Martin Fowler
Recorded Future
Recorded Future
Project Zero
Project Zero
Hugging Face - Blog
Hugging Face - Blog
Engineering at Meta
Engineering at Meta
Security Latest
Security Latest
Microsoft Azure Blog
Microsoft Azure Blog
V
Vulnerabilities – Threatpost
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
Help Net Security
Help Net Security
博客园 - Franky
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
F
Fortinet All Blogs
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Forbes - Security
Forbes - Security
M
MIT News - Artificial intelligence
H
Hacker News: Front Page
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
Spread Privacy
Spread Privacy
The Register - Security
The Register - Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
量子位
Google Online Security Blog
Google Online Security Blog
PCI Perspectives
PCI Perspectives
The Last Watchdog
The Last Watchdog
AI
AI
N
News | PayPal Newsroom
D
DataBreaches.Net
Cloudbric
Cloudbric
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 司徒正美
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog

博客园 - 莫耶

学习RxJS: 导入 [Node.js] Node.js项目的持续集成 [Node.js] 使用node-forge保障Javascript应用的传输安全 [Node.js] 对称加密、公钥加密和RSA [Node.js] DSL in action [Node.js] 使用TypeScript编写Node项目 [Node.js] Node.js中的流 [Node.js] 基于Socket.IO 的私聊 [Node.js] 闭包和高阶函数 [Node.js] Promise,Q及Async [Node.js] Express的测试覆盖率 [Node.js] BDD和Mocha框架 [Node.js] 也说this [Node.js] ECMAScript 6中的生成器及koa小析 [Node.js] 使用File API 异步上传文件 [Node.js] OAuth 2 和 passport框架 [Node.js] Node + Redis 实现分布式Session方案 [Node.js] Cluster,把多核用起来 Python开源框架Scrapy安装及使用
学习RxJS:Cycle.js
莫耶 · 2016-06-16 · via 博客园 - 莫耶

2016-06-16 23:16  莫耶  阅读(3141)  评论()    收藏  举报

原文地址:http://www.moye.me/2016/06/16/learning_rxjs_part_two_cycle-js/

是什么

Cycle.js 是一个极简的JavaScript框架(核心部分加上注释125行),提供了一种函数式,响应式的人机交互接口(以下简称HCI):

函数式

Cycle.js 把应用程序抽象成一个纯函数 main(),从外部世界读取副作用(sources),然后产生输出(sinks) 传递到外部世界,在那形成副作用。这些外部世界的副作用,做为Cycle.js的插件存在(drivers),它们负责:处理DOM、提供HTTP访问等。

circuit_flow

响应式

Cycle.js 使用 rx.js 来实现关注分离,这意味着应用程序是基于事件流的,数据流是 Observable 的:

observable_stream

HCI

HCI 是双向的对话,人机互为观察者:

HCI_anthropomorphism

在这个交互模型中,人机之间的信息流互为输出输出,构成一个循环,也即 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 监听sinksmain()的输出)做为输入,执行一些命令式的副作用,并产生出sources做为main()的输入。

DOM Driver

即 @cycle/dom,是使用最为频繁的driver。实际应用中,我们的main()会与DOM进行交互:

  • 需要传递内容给用户时,main()会返新的DOM sinks,以触发domDriver()生成virtual-dom,并渲染
  • main()订阅domDriver()的输出值(做为输入),并据此进行响应

domDriver

组件化

每个Cycle.js应用程序不管多复杂,都遵循一套输入输出的基本法,因此,组件化是很容易实现,无非就是函数对函数的组合调用

Composable

实战

准备工作

安装全局模块

npm install -g http-server

依赖模块一览

"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

cycle_example_1

HTML代码 (实例2同,略
<!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

cycle_example_2

  • 功能: 一个button一个框,输入并点button后,通过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 这些进阶项,还是留给看官们自行探索吧。

更多文章请移步我的blog新地址: http://www.moye.me/ 

Creative Commons License