Rxjs 響應式編程-第一章:響應式
Rxjs 響應式編程-第二章:序列的深刻研究
Rxjs 響應式編程-第三章: 構建併發程序
Rxjs 響應式編程-第四章 構建完整的Web應用程序
Rxjs 響應式編程-第五章 使用Schedulers管理時間
Rxjs 響應式編程-第六章 使用Cycle.js的響應式Web應用程序php
隨着單頁應用程序的出現,網站忽然被指望作更多,甚至與「原生」應用程序進行競爭。在嘗試更快地開發Web應用程序時,開發人員意識到特定領域是瓶頸,使Web應用程序不像其本地應用程序那樣快速和強大。html
在Facebook React的帶領下,有幾個Web框架正在使用着新技術,以便在保持代碼簡單和聲明式的同時製做更快的Web應用程序。前端
在本章中,咱們將介紹一些開發Web應用程序的新技術,例如Virtual DOM。 咱們將使用Cycle.js,這是一個現代,簡單,漂亮的框架,在內部使用RxJS並將響應式編程概念應用於前端編程。npm
Cycle.js是RxJS之上的一個小框架,用於建立響應式用戶界面。 它提供了現代框架(如React)中的功能,例如虛擬DOM和單向數據流。編程
Cycle.js以反應方式設計,Cycle.js中的全部構建塊都是Observables,這給咱們帶來了巨大的優點。 它比其餘框架更容易掌握,由於理解和記憶的概念要少得多。 例如,與狀態相關的全部操做都不在路徑中,封裝在稱爲驅動程序的函數中,咱們不多須要建立新的操做。json
什麼是虛擬DOM?文檔對象模型(DOM)定義HTML文檔中元素的樹結構。 每一個HTML元素都是DOM中的一個節點,每一個節點均可以使用節點上的方法進行操做。segmentfault
DOM最初是爲了表示靜態文檔而建立的,而不是咱們今天擁有的超級動態網站。 所以,當DOM樹中的元素常常更新時,它的設計並不具備良好的性能。 這就是爲何當咱們對DOM進行更改時會出現性能損失。設計模式
虛擬DOM是用JavaScript的DOM的映射。 每次咱們更改組件中的狀態時,咱們都會爲組件從新計算一個新的虛擬DOM樹,並將其與以前的樹進行比較。 若是存在差別,咱們只會渲染這些差別。 這種方法很是快,由於比較JavaScript對象很快,咱們只對「真正的」DOM進行絕對必要的更改。api
這種方法意味着咱們能夠編寫代碼,就好像咱們爲每一個更改生成了整個應用程序UI。 咱們沒必要跟蹤DOM中的狀態。 在幕後,Cycle.js將檢查每次更新是否有任何不一樣,並負責有效地渲染咱們的應用程序。數組
咱們能夠經過使用<script> </script>
標記將它包含在HTML頁面中來使用Cycle.js,但這不是使用它的最佳方式,由於Cycle.js是以極其模塊化的方式設計的。 每一個模塊都儘量地自我依賴管理,而且包括幾個模塊。由於<script> </script>
能夠輕鬆加載大量重複代碼,從而致使沒必要要的下載和更長的啓動時間。
相反,咱們將使用Node Package Manager,npm和Browserify爲咱們的最終腳本生成代碼。 首先,咱們將建立一個項目將存在的新文件夾,並安裝咱們的項目依賴項:
mkdir wikipedia-search && cd wikipedia-search npm install browserify npm install @cycle/core npm install @cycle/dom
第一個npm命令安裝Browserify,它容許咱們爲瀏覽器編寫代碼,就像它是Node.js應用程序同樣。 使用Browserify,咱們可使用Node.js的模塊加載器,它將明智地包含哪些依賴項,使代碼下載儘量小。 接下來,咱們安裝了cycle-core和cycle-dom,它們是Cycle.js的兩個基本模塊。
有了這個,咱們能夠建立一個名爲index.js的文件,咱們將編輯咱們的應用程序,而後使用本地Browserify二進制文件將其編譯成一個名爲bundle.js的文件:
touch index.js `npm bin`/browserify index.js --outfile bundle.js
上面的命令將遍歷咱們的依賴樹並建立一個bundle.js文件,其中包含運行咱們的應用程序所需的全部內容,包括咱們在代碼中須要的任何依賴項。 咱們能夠在index.html中直接包含bundle.js:
cycle/index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Wikipedia search</title> </head> <body> <div id="container"></div> <script src="bundle.js"></script> </body> </html>
在本節中,咱們將構建一個搜索Wikipedia做爲用戶類型的應用程序。
RxJS已經使得檢索和處理遠程數據變得容易了,可是,正如第4章「構建完整的Web應用程序」中所看到的那樣,咱們仍然須要跳過一些環節來使咱們的DOM操做高效。
Cycle.js的目標之一是徹底消除代碼中的DOM操做。 讓咱們從一些基本的腳手架開始:
cycle/step1.js
var Cycle = require('@cycle/core'); ❶ var CycleDOM = require('@cycle/dom') var Rx = Cycle.Rx; ❷ function main(responses) { return { DOM: Rx.Observable.just(CycleDOM.h('span', 'Hi there!')) }; } var drivers = { ❸ DOM: CycleDOM.makeDOMDriver('#container') }; ❹ Cycle.run(main, drivers);
這段代碼在屏幕上顯示文字hi!,但已經有至關多的事情發生了。 重要的部分是主要功能和驅動對象。 咱們來看看這些步驟:
Cycle.js驅動程序是咱們用來引發反作用的函數。在咱們的程序中,咱們應該以任何方式修改狀態。驅動程序採用從咱們的應用程序發出數據的Observable,它們返回另外一個致使反作用的Observable。
咱們不會常常建立驅動程序 - 只有當咱們須要反作用時,例如修改DOM,從其餘接口讀取和寫入(例如,本地存儲)或發出請求。 在大多數應用程序中,咱們只須要DOM驅動程序(呈現網頁)和HTTP驅動程序(咱們可使用它來發出HTTP請求)。 在這個例子中,咱們將使用另外一個JSONP驅動程序。
咱們須要頁面的實際內容,而不只僅是span
。 讓咱們建立一個函數來建立表明咱們頁面的虛擬樹:
cycle/index.js
function vtreeElements(results) { var h = CycleDOM.h; return h('div', [ h('h1', 'Wikipedia Search '), h('input', {className: 'search-field', attributes: {type: 'text'}}), h('hr'), h('div', results.map(function(result) { return h('div', [ h('a', { href: WIKI_URL + result.title }, result.title) ]); })) ]); }
這個功能可能看起來有點奇怪,但不要驚慌。 它使用Virtual Hyperscript,一種用於建立虛擬DOM樹的特定於域的語言。 Virtual Hyperscript包含一個名爲h的方法。 h以相似於HTML的方式聲明節點,但使用JavaScript語言。咱們能夠經過將額外的對象或數組做爲參數傳遞給h來向元素添加屬性或將子元素附加到它們。生成的虛擬樹最終將呈現爲真正的瀏覽器DOM。
vtreeElements獲取一組對象,結果,並返回一個虛擬樹,表明咱們應用程序的簡單UI。 它呈現一個輸入字段和一個由結果中的對象組成的連接列表,最終將包含Wikipedia的搜索結果。 咱們將使用vtreeElements來呈現咱們的應用程序。
咱們可使用JSX編寫咱們的UI,而不是使用h函數,JSX是一種由Facebook發明的相似XML的語法擴展,它使得編寫虛擬DOM結構更容易,更易讀。 咱們的vtreeElements函數看起來像這樣:
cycle/index.js
function vtreeElementsJSX(results) { results = results.map(function(result) { var link = WIKI_URL + result.title; return <div><a href={link}>{result.title}</a></div> }); return <div> <h1>Wikipedia Search</h1> <input className="search-field" type="text" /> <hr/> <div>{results}</div> </div>; }
它看起來不是更好嗎?JSX看起來對開發人員來講比較熟悉,由於它相似於HTML,可是咱們能夠將它與JavaScript代碼一塊兒編寫,而且咱們能夠將其視爲JavaScript類型。 例如,注意咱們如何迭代結果數組,咱們直接返回一個<div>元素,使用數組元素自己中的link和result.title的值。(能夠經過將它們放在大括號內來內聯JavaScript值。)
因爲JSX是一種語法擴展,咱們須要一個編譯器將其轉換爲最終的JavaScript代碼(它看起來很是像咱們上一節中基於h的代碼)。 咱們將使用Babel。 Babel是一個編譯器,它將現代JavaScript轉換爲可在任何地方運行的JavaScript。它還轉換了一些JavaScript擴展,例如JSX,也就是以前的用例。
若是要使用JSX,則須要安裝Babel並在編譯項目時使用它。 幸運的是,Babel有一個名爲Babelify的Browserify適配器:
npm install babelify
在每一個使用JSX的文件中,咱們須要在文件頂部添加如下行:
/** @jsx hJSX */ var hJSX = CycleDOM.hJSX;
這告訴Babel使用Cycle.js的hJSX適配器來處理JSX,而不是使用默認的React。
如今,當咱們想要編譯項目時,咱們可使用如下命令:
browserify index.js -t babelify --outfile bundle.js
咱們須要一個函數來返回一個Observable of URL,它使用用戶輸入的搜索詞來查詢Wikipedia的API:
cycle/index.js
var MAIN_URL = 'https://en.wikipedia.org'; var WIKI_URL = MAIN_URL + '/wiki/'; var API_URL = MAIN_URL + '/w/api.php?' + 'action=query&list=search&format=json&srsearch='; function searchRequest(responses) { return responses.DOM.select('.search-field').events('input') .debounce(300) .map(function(e) { return e.target.value }) .filter(function(value) { return value.length > 2 }) .map(function(search) { return API_URL + search }); }
首先,咱們聲明一些咱們的應用程序將用於查詢Wikipedia的URL。 在函數searchRequest中,咱們獲取包含應用程序中全部驅動程序的響應對象,並在DOM驅動程序中使用get方法。select(element).event(type)
的行爲與fromEvent相似:它採用DOM元素的選擇器和要監聽的事件類型,並返回發出事件的Observable。
這時,代碼的其他部分看起來應該很是熟悉,由於它包含經過咱們經常使用的運算符轉換Observable值:
太棒了! 到目前爲止,咱們有生成UI的功能和從該UI檢索用戶輸入的功能。咱們如今須要添加將從維基百科獲取信息的功能。
你可能已經在以前的代碼中注意到main函數接受了一個咱們沒有使用的參數,responses
。這些是來自run函數中的responses
。驅動程序和main函數造成一個循環(所以框架的名稱):main的輸出是驅動程序的輸入,驅動程序的輸出是main的輸入。請記住,輸入和輸出始終是Observables。
咱們使用JSONP查詢Wikipedia,就像咱們在第2章中所作的那樣。咱們使用JSONP而不是HTTP來更容易在本地計算機上運行此示例,由於使用HTTP從不一樣的域檢索數據會致使某些瀏覽器由於安全緣由阻止這些請求。 在幾乎任何其餘狀況下,尤爲是在生產代碼中,使用HTTP來檢索遠程數據。
不管如何,使用JSONP並不影響本章的要點。 Cycle有一個JSONP的實驗模塊,咱們可使用npm安裝它:
npm install @cycle/jsonp
而後咱們在咱們的應用中使用它,以下所示:
cycle/step2.js
var Cycle = require('@cycle/core'); var CycleDOM = require('@cycle/dom'); var CycleJSONP = require('@cycle/jsonp'); var Rx = Cycle.Rx; var h = CycleDOM.h; function searchRequest(responses) { return responses.DOM.select('.search-field').events('input') .debounce(300) .map(function(e) { return e.target.value }) .filter(function(value) { return value.length > 2 }) .map(function(search) { return API_URL + search }); } function vtreeElements(results) { return h('div', [ h('h1', 'Wikipedia Search '), h('input', {className: 'search-field', attributes: {type: 'text'}}), h('hr'), h('div', results.map(function(result) { return h('div', [ h('a', { href: WIKI_URL + result.title }, result.title) ]); })) ]); } function main(responses) { return { DOM: Rx.Observable.just(CycleDOM.h('span', 'Hey there!')), JSONP: searchRequest(responses) } } var drivers = { DOM: CycleDOM.makeDOMDriver('#container'), JSONP: CycleJSONP.makeJSONPDriver() }; Cycle.run(main, drivers);
咱們但願將searchRequest的結果插入到JSONP方法中,這樣一旦用戶輸入搜索詞,咱們就會用術語查詢Wikipedia。
爲此,咱們使用CycleJSONP.makeJSONPDriver建立一個新的JSONP,它將接收咱們在main的返回對象中放置在屬性JSONP中的任何內容。在這以後,當咱們在輸入框中引入搜索詞時,咱們應該已經在查詢維基百科,但因爲咱們沒有將JSONP輸出鏈接到任何內容,咱們在頁面上看不到任何更改。 讓咱們改變一下:
cycle/step3.js
function main(responses) { var vtree$ = responses.JSONP .filter(function(res$) { return res$.request.indexOf(API_URL) === 0; }) .mergeAll() .pluck('query', 'search') .startWith([]) .map(vtreeElements); return { DOM: vtree$, JSONP: searchRequest(responses) }; }
main經過其響應參數接收全部驅動程序的輸出。咱們能夠在respond.JSONP中獲取JSON調用的結果,這是咱們應用程序中全部JSONP響應的Observable。完成後,咱們能夠轉換Observable以咱們想要的形式獲取搜索結果:
前面代碼中最重要的一點是,在最後一步中,咱們彷佛從新繪製了咱們收到的每一個結果的整個UI。 但這裏是虛擬DOM閃耀的地方。 不管咱們從新呈現頁面多少次,虛擬DOM將始終確保僅呈現差別,從而使其很是高效。 若是虛擬DOM沒有更改,則不會在頁面中呈現任何更改。
這樣咱們就沒必要擔憂添加或刪除元素了。 咱們每次只渲染整個應用程序,咱們讓Virtual DOM找出實際更新的內容。
咱們用於構建維基百科實時搜索的架構方法不只僅是另外一個框架的編程UI方法。結構化代碼背後有一個設計模式,就像咱們作的那樣:Model-View-Intent(MVI)。
Model-View-Intent是一個由Cycle.js建立者AndréStaltz建立的術語,用於受模型 - 視圖 - 控制器(MVC)架構啓發的體系結構.在MVC中,咱們將應用程序的功能分爲三個部分: 模型,視圖和控制器。 在MVI中,三個組件是模型,視圖和意圖。 MVI旨在適應像手套同樣的Reactive編程模型。
MVI是被動的,意味着每一個組件都會觀察其依賴關係並對依賴項的更改作出反應。 這與MVC不一樣,MVC中的組件知道其依賴項並直接修改它們。 組件(C)聲明哪些其餘組件影響它,而不是明確更新(C)的其餘組件。
MVI中的三個組件由Observables表示,每一個組件的輸出是另外一個組件的輸入。
該模型表示當前的應用程序狀態。 它從intent中獲取已處理的用戶輸入,並輸出有關視圖消耗的數據更改的事件。
視圖是咱們模型的直觀表示。 它採用具備模型狀態的Observable,並輸出全部潛在的DOM事件和頁面的虛擬樹。
意圖是MVI中的新組件。意圖從用戶獲取輸入並將其轉換爲咱們模型中的操做。若是咱們從新調整和重命名咱們的代碼,咱們能夠在咱們的應用程序中使這三種組件更清晰:
cycle/index-mvi.js
function intent(JSONP) { return JSONP.filter(function(res$) { return res$.request.indexOf(API_URL) === 0; }) .concatAll() .pluck('query', 'search'); } function model(actions) { return actions.startWith([]); } function view(state) { return state.map(function(linkArray) { return h('div', [ h('h1', 'Wikipedia Search '), h('input', {className: 'search-field', attributes: {type: 'text'}}), h('hr'), h('div', linkArray.map(function(link) { return h('div', [ h('a', { href: WIKI_URL + link.title }, link.title) ]); })) ]); }); } function userIntent(DOM) { return DOM.select('.search-field') .events('input') .debounce(300) .map(function(e) { return e.target.value }) .filter(function(value) { return value.length > 2 }) .map(function(search) { return API_URL + search }); } function main(responses) { return { DOM: view(model(intent(responses.JSONP))), JSONP: userIntent(responses.DOM) }; } Cycle.run(main, { DOM: CycleDOM.makeDOMDriver('#container'), JSONP: CycleJSONP.makeJSONPDriver() });
經過將模型,視圖和意圖拆分爲單獨的函數,咱們使代碼更加清晰。 (另外一個意圖,userIntent,是JSONP驅動程序的輸入。)大多數應用程序邏輯在咱們傳遞給main函數中的DOM驅動程序的屬性中表示爲這三個函數的組合:
function main(responses) { return { DOM: view(model(intent(responses.JSONP))), JSONP: userIntent(responses.DOM) }; }
它沒有那麼多功能!
隨着咱們製做更復雜的應用程序,咱們但願重用一些UI組件。 咱們的維基百科搜索應用程序很小,可是它已經有一些能夠在其餘應用程序中重用的組件。 以搜索輸入框爲例。 咱們絕對能夠將它變成本身的小部件。
目標是將咱們的小部件封裝在本身的組件中,以便咱們將其用做任何其餘DOM元素。 咱們還應該可以使用咱們想要的任何屬性來參數化組件。 而後咱們將在咱們的應用程序中使用它,以下所示:
var wpSearchBox = searchBox({ props$: Rx.Observable.just({ apiUrl: API_URL }) });
咱們將使用Cycle.js引入的概念構建咱們的小部件,它將一個Observable事件做爲輸入,並輸出一個Observable,其結果是將這些輸入應用於其內部邏輯。
讓咱們開始構建搜索框組件。 咱們首先建立一個函數,它接受一個響應參數,咱們將從主應用程序傳遞任何咱們想要的屬性:
cycle/searchbox.js
var Cycle = require('@cycle/core'); var CycleDOM = require('@cycle/dom'); var Rx = Cycle.Rx; var h = CycleDOM.h; var a; function searchBox(responses) { var props$ = responses.props$; var apiUrl$ = props$.map(function (props) { return props['apiUrl']; }).first(); }
searchBox接收的每一個參數都是一個Observable。 在這種狀況下,props $是一個Observable,它發出一個包含Wikipedia搜索框配置參數的JavaScript對象。
檢索屬性後,咱們爲窗口小部件定義虛擬樹。 在咱們的例子中,它只是一個很是簡單的輸入字段:
cycle/searchbox.js
var vtree$ = Rx.Observable.just( h('div', { className: 'search-field' }, [ h('input', { type: 'text' }) ]) );
咱們但願全部東西都是一個Observable,因此咱們將虛擬樹包裝在一個Observable中,它只返回一個Observable,它發出咱們傳遞它的值。
如今,只要用戶在輸入字段中鍵入搜索詞,咱們就須要搜索框來查詢Wikipedia API。 咱們重用上一節函數userIntent中的代碼:
cycle/searchbox.js
var searchQuery$ = apiUrl$.flatMap(function (apiUrl) { return responses.DOM.select('.search-field').events('input') .debounce(300) .map(function (e) { return e.target.value; }) .filter(function (value) { return value.length > 3; }) .map(function (searchTerm) { return apiUrl + searchTerm; }); });
咱們仍然須要將searchQuery的輸出鏈接到JSON驅動程序的輸入。 咱們就像在正常的Cycle應用程序中那樣作:
cycle/searchbox.js
return { DOMTree: vtree$, JSONPQuery: searchQuery$ };
最後,咱們不該該忘記導出搜索框小部件:
cycle/searchbox.js
module.exports = searchBox; // Export it as a module
如今咱們已準備好在您的應用程序中使用搜索框小部件。 主要方法如今看起來像這樣:
cycle/index-mvi2.js
var h = CycleDOM.h; ❶ var SearchBox = require('./searchbox'); function main(responses) { ❷ var wpSearchBox = SearchBox({ DOM: responses.DOM, props$: Rx.Observable.just({ apiUrl: API_URL }) }); ❸ var searchDOM$ = wpSearchBox.DOMTree; var searchResults$ = responses.JSONP .filter(function(res$) { return res$.request.indexOf(API_URL) === 0; }) .concatAll() .pluck('query', 'search') .startWith([]); return { ❹ JSONP: wpSearchBox.JSONPQuery, ❺ DOM: Rx.Observable.combineLatest( searchDOM$, searchResults$, function(tree, links) { return h('div', [ h('h1', 'Wikipedia Search '), tree, h('hr'), h('div', links.map(function(link) { return h('div', [ h('a', { href: WIKI_URL + link.title }, link.title) ]); })) ]); }) }; } Cycle.run(main, { DOM: CycleDOM.makeDOMDriver('#container'), JSONP: CycleJSONP.makeJSONPDriver() });
如今咱們將處理用戶輸入和呈現搜索框的責任委託給wpSearchBox小部件,咱們能夠在另外一個須要查詢URL API的搜索框的應用程序中輕鬆地重用該小部件。 這些是主要的變化:
有了最終的代碼,咱們能夠看到Cycle.js的最大亮點。 框架中沒有不一樣的類,特殊類型或「魔術」。 這是全部無反作用的函數,它們接受Observable並輸出更多的Observable。 只有這樣,咱們纔有一個簡潔的Web應用程序框架,清晰,反應靈敏,使用起來頗有趣。 它不惜一切代價避免反作用,使咱們的Web應用程序更加健壯。
除了迫切須要更好的圖形設計外,咱們的應用程序可使用一些功能,而不只僅是快速重定向到維基百科的結果:
如今您知道如何開發使用現代技術的Web應用程序而不放棄響應性理念。 本章提供瞭如何使用Observables和RxJS做爲其餘框架或應用程序的內部引擎的想法。 經過站在Observables的肩膀和活躍的生活方式,咱們能夠極大地簡化Web應用程序並將狀態下降到最小的表達,使咱們的Web應用程序不那麼脆弱和易於維護。
感謝您閱讀本書。 我但願它能幫助您從新思考開發JavaScript應用程序的方式,並挑戰一些有關編程的現有概念。 這是快速,強大和反應性的軟件!
關注個人微信公衆號,更多優質文章定時推送