Rxjs 响应式编程-第六章 使用Cycle.js的响应式Web应用程序
Rxjs 响应式编程-第一章:响应式
Rxjs 响应式编程-第二章:序列的深入研究
Rxjs 响应式编程-第三章: 构建并发程序
Rxjs 响应式编程-第四章 构建完整的Web应用程序
Rxjs 响应式编程-第五章 使用Schedulers管理时间
Rxjs 响应式编程-第六章 使用Cycle.js的响应式Web应用程序
使用Cycle.js的反应式Web应用程序
随着单页应用程序的出现,网站突然被期望做更多,甚至与“原生”应用程序进行竞争。在尝试更快地开发Web应用程序时,开发人员意识到特定领域是瓶颈,使Web应用程序不像其本地应用程序那样快速和强大。
在Facebook React的带领下,有几个Web框架正在使用着新技术,以便在保持代码简单和声明式的同时制作更快的Web应用程序。
在本章中,我们将介绍一些开发Web应用程序的新技术,例如Virtual DOM。 我们将使用Cycle.js,这是一个现代,简单,漂亮的框架,在内部使用RxJS并将响应式编程概念应用于前端编程。
Cycle.js
Cycle.js是RxJS之上的一个小框架,用于创建响应式用户界面。 它提供了现代框架(如React)中的功能,例如虚拟DOM和单向数据流。
Cycle.js以反应方式设计,Cycle.js中的所有构建块都是Observables,这给我们带来了巨大的优势。 它比其他框架更容易掌握,因为理解和记忆的概念要少得多。 例如,与状态相关的所有操作都不在路径中,封装在称为驱动程序的函数中,我们很少需要创建新的操作。
什么是虚拟DOM?文档对象模型(DOM)定义HTML文档中元素的树结构。 每个HTML元素都是DOM中的一个节点,每个节点都可以使用节点上的方法进行操作。
DOM最初是为了表示静态文档而创建的,而不是我们今天拥有的超级动态网站。 因此,当DOM树中的元素经常更新时,它的设计并不具有良好的性能。 这就是为什么当我们对DOM进行更改时会出现性能损失。
虚拟DOM是用JavaScript的DOM的映射。 每次我们更改组件中的状态时,我们都会为组件重新计算一个新的虚拟DOM树,并将其与之前的树进行比较。 如果存在差异,我们只会渲染这些差异。 这种方法非常快,因为比较JavaScript对象很快,我们只对“真正的”DOM进行绝对必要的更改。
这种方法意味着我们可以编写代码,就好像我们为每个更改生成了整个应用程序UI。 我们不必跟踪DOM中的状态。 在幕后,Cycle.js将检查每次更新是否有任何不同,并负责有效地渲染我们的应用程序。
安装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 Core和Cycle DOM驱动程序。 我将在下一节中解释Cycle.js驱动程序的内容。
- 主要功能始终是我们应用程序的入口点。 它返回一组Observable,一个用于应用程序中的每个驱动程序。 到目前为止,我们只使用一个驱动程序:DOM驱动程序。
DOM驱动程序的Observable发出一个虚拟树,我们使用Cycle DOM库中的h方法创建。 在这种情况下,我们只创建一个带有“Hi there!”文本的span元素。 DOM驱动程序使用该虚拟树并从中呈现页面上的实际DOM。 - 我们创建一个DOM驱动程序,它将根据main函数构建DOM树。 DOM树将构建在我们作为参数传递的元素或选择器中。 在这里传的是#container。
- Cycle.run将main函数与drivers对象连接起来,在两者之间创建循环流。
Cycle.js驱动程序
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
我们可以使用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值:
- 节流结果最多每300毫秒接收一个。
- 提取输入框的值。
- 仅采用长度超过两个字符的文本。
- 将最终值附加到Wikipedia的API URL。
太棒了! 到目前为止,我们有生成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以我们想要的形式获取搜索结果:
- esponses.JSONP会在应用程序中发出所有JSONP响应。 我们首先在其请求中过滤包含Wikipedia的API URL的内容,以确保我们正在处理相关的响应。
- respond.JSONP是一个Observable of Observables。 对于每个响应,都有一个Observable。 在这一行中,我们将它们全部展平,因此我们从现在开始处理响应,而不是它们的Observables。
- 响应是JSON对象,我们感兴趣的信息在query.search属性中。 我们使用pluck运算符来提取它。
- 我们不知道我们是否会有任何结果,所以至少我们确保我们有一个空数组。
- 最后,我们将vtreeElements函数应用于维基百科的每个结果。 这将更新我们的UI。
- 注意变量名称末尾的$符号。 在本章中,我采用了Cycle.js代码中使用的命名约定,它将$添加到变量名称,表示它是一个Observable。 我发现它可以更容易理解基于Observable的代码!
前面代码中最重要的一点是,在最后一步中,我们似乎重新绘制了我们收到的每个结果的整个UI。 但这里是虚拟DOM闪耀的地方。 无论我们重新呈现页面多少次,虚拟DOM将始终确保仅呈现差异,从而使其非常高效。 如果虚拟DOM没有更改,则不会在页面中呈现任何更改。
这样我们就不必担心添加或删除元素了。 我们每次只渲染整个应用程序,我们让Virtual DOM找出实际更新的内容。
Model-View-Intent
我们用于构建维基百科实时搜索的架构方法不仅仅是另一个框架的编程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的搜索框的应用程序中轻松地重用该小部件。 这些是主要的变化:
- 导入我们刚刚创建的searchBox小部件。
- 创建一个SearchBox实例,传递DOM驱动程序和我们想要搜索小部件的属性。
- 我们的wpSearchBox最终将从其DOMTree Observable中发出项目。 我们在这里分配它以便在我们渲染实际DOM时使用它们。
- 我们将Wikipedia查询URL发送到JSONP驱动程序,以便检索其结果。 当这些可用时,它将在response.JSONP中发出它们,我们在searchResults中对它进行了优化。
- 为了渲染最终的DOM树,我们使用combineLatest与searchDOM和searchResults。它们中的每一个都会导致布局发生变化,因此只要这两个Observable中的一个发出一个项目,我们就会重新渲染DOM树。
有了最终的代码,我们可以看到Cycle.js的最大亮点。 框架中没有不同的类,特殊类型或“魔术”。 这是所有无副作用的函数,它们接受Observable并输出更多的Observable。 只有这样,我们才有一个简洁的Web应用程序框架,清晰,反应灵敏,使用起来很有趣。 它不惜一切代价避免副作用,使我们的Web应用程序更加健壮。
改进的想法
除了迫切需要更好的图形设计外,我们的应用程序可以使用一些功能,而不仅仅是快速重定向到维基百科的结果:
- 让用户为特定结果添加书签。 您可以在列表中的每个结果旁边添加一个小星星,这样当用户点击时,它会将该结果保存为收藏夹。 你可以将星星变成自己的小部件。 如果您使用某些持久性API(反应性!),例如本地存储或IndexedDB,则需要额外的分数。
- 如果用户单击链接,则在屏幕右侧显示结果的“预览”,其中包含概要及其相关元信息。 如果用户想要查看实际的Wikipedia结果,则可以在其中包含“阅读更多”链接。 将其实现为小部件。
总结
现在您知道如何开发使用现代技术的Web应用程序而不放弃响应性理念。 本章提供了如何使用Observables和RxJS作为其他框架或应用程序的内部引擎的想法。 通过站在Observables的肩膀和活跃的生活方式,我们可以极大地简化Web应用程序并将状态降低到最小的表达,使我们的Web应用程序不那么脆弱和易于维护。
感谢您阅读本书。 我希望它能帮助您重新思考开发JavaScript应用程序的方式,并挑战一些有关编程的现有概念。 这是快速,强大和反应性的软件!
关注我的微信公众号,更多优质文章定时推送