學習RxJS:Cycle.js


原文地址: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亦可

$ npm run build
$ npm start

交互實例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/ 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM