Cycle.js 是一個極簡的JavaScript框架(核心部分加上註釋125行),提供了一種函數式,響應式的人機交互介面。在這個交互模型中,人機之間的信息流互為輸出輸出,構成一個迴圈,也即 Cycle這一命名所指,框架的Logo更是以莫比烏斯環貼切的描述了這個迴圈。 ...
原文地址: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訪問等。
響應式
Cycle.js 使用 rx.js 來實現關註分離,這意味著應用程式是基於事件流的,數據流是 Observable 的:
HCI
HCI 是雙向的對話,人機互為觀察者:
在這個交互模型中,人機之間的信息流互為輸出輸出,構成一個迴圈,也即 Cycle這一命名所指,框架的Logo更是以莫比烏斯環貼切的描述了這個迴圈。
唯一的疑惑會是:迴圈無頭無尾,信息流從何處發起?好問題,答案是:
However, we need a .startWith() to give a default value. Without this, nothing would be shown! Why? Because our
sinks
is reacting tosources
, butsources
is reacting tosinks
. 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應用程式不管多複雜,都遵循一套輸入輸出的基本法,因此,組件化是很容易實現,無非就是函數對函數的組合調用
實戰
準備工作
安裝全局模塊
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
- 功能:兩個button,一加一減, 從0起步,回顯計數
- demo地址: http://output.jsbin.com/lamexacaku
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
- 功能: 一個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/