十一、使用 React 和 Flex 构建 RSS 阅读器
React 不足以构建一个完整的应用,因为它只是视图层。我们需要一个用于保存应用逻辑和数据的架构,这就是 Flux 的作用。显然,React 可以用于任何其他体系结构,但是 Flux 是 React 最常用的,因为 Flux 是基于单向数据流的,就像 React 一样。在本章中,我们将使用 React 和 Flux 构建一个单页 RSS 阅读器。
我们将涵盖以下主题:
- 深度 Flex 架构
- 使用 React 路由器库进行路由
- 使用 Flux.js 创建一个调度程序
- 使用 MicroEvent.js 发出事件
- 集成通量和路由
理解 Flex
Flux 是一个应用架构,而不是一个框架。你可以把它当成 MVC 的替代品。它主要是为了与 React 一起使用而开发的,因为两者都是基于单向数据流的。Flex 架构强制单向数据流。
下面是一个图表,显示了 Flux 体系结构的所有部分以及数据如何在其中流动:
以下是各部分的工作原理:
- 动作:一个动作是一个描述我们想要做什么以及我们需要做什么的数据的对象。在 Flux 中,来自所有来源的所有事件和数据都被转换为动作。甚至 UI 事件也会转换为动作。
- 调度员:调度员是一种特殊类型的事件系统。它用于向注册的回调广播操作。调度程序所做的与发布/订阅系统不同,因为回调不会订阅特定的事件。相反,每个操作都被分派给每个注册的回调。一个应用应该只包含一个调度程序。
- 动作创建者:动作创建者是将动作分派给分派者的方法。
- 存储:存储是存储应用数据和逻辑的对象。商店对行动做出 React。每当调度程序调度存储依赖的操作时,回调 ping 存储以采取适当的操作。
- React 视图:React 视图是 React 组件,可以从商店中检索数据并显示,还可以在商店存储的数据发生变化时收听商店发出的事件。请注意,存储发出的事件不会转换为操作。
因此,在 Flux 中,来自不同来源的所有事件和数据都作为动作被分派给调度器,然后每当调度器分派动作时商店就会更新它们自己,最后,每当商店更新时视图就会更新。
这是另一个图表,它提供了 Flux 如何工作的更高层次的抽象:
这里可以看到数据是单向流动的,即数据和事件先去 调度员,然后去商店,最后去查看。因此,我们可以说调度器、存储和视图是 Flux 架构的三个主要部分。
正如有许多 MVC 框架,如 Angular、Ember 和主干一样,也有许多 Flux 框架,如 Fluxible、Redux、Alt 和 Redux。但是为了让事情变得简单和容易学习,我们不会使用这些框架中的任何一个。相反,我们将使用 Flux.js 和 MicroEvent.js 库来实现 Flux 架构。
使用 Flux.js
Flux.js 是由 Flux 的创建者创建的库。用于建造调度员。你可以在https://github.com/facebook/flux和找到 Flux.js 源代码,在https://cdnjs.com/libraries/flux找到 CDN 版本。
使用Dispatcher
构造函数创建调度程序。它有五种方法,如下所示:
register(callback)
:这个方法让我们注册一个回调。它返回一个名为callback
的字符串来唯一标识回调。unregister(id)
:这个是一个让我们注销注册回调的方法。要注销,我们需要传递想要注销的回调的 ID。waitFor(array)
:这将等待指定的回调被调用,然后继续执行当前回调。此方法应该仅由响应调度操作的回调使用。dispatch(action)
:这个向注册的回调调度一个动作。isDispatching()
:此返回一个布尔指示,指示调度员当前是否正在调度。
我们将在构建 RSS 提要阅读器时通过示例代码浏览。
使用 MicroEvent.js
MicroEvent.js 是一个事件发射器库,为 JavaScript 对象提供观察者模式。我们需要 MicroEvent.js 来触发来自商店的事件来更新视图。
你可以从 http://notes.jetienne.com/2011/03/22/microeventjs.html 获得微事件。
为了使一个对象或构造函数能够发出事件,而其他对象能够订阅它,我们需要使用MicroEvent.mixin
方法将一个MicroEvent
接口集成到对象或构造函数中。
现在,在对象或构造函数内部,我们可以使用this.trigger()
触发事件,其他人可以使用对象的bind()
方法订阅事件。我们也可以使用unbind()
方法解除绑定。
我们将在构建 RSS 提要阅读器时查看示例代码。
React 路由器简介
我们将创建的 RSS 提要阅读器应用将是一个单页应用。在单页应用中,路由是在前端而不是后端定义的。我们需要某种类型的库,让我们定义路由并为它们分配组件,也就是说,它可以保持用户界面与网址同步。
React 路由器是 React 最受欢迎和推荐的路由库。它提供了一个简单的应用编程接口,内置了动态路线匹配和位置转换处理等强大功能。
你可以在https://github.com/reactjs/react-router找到 React Router 的源代码,在https://cdnjs.com/libraries/react-router找到 CDN 版本。
下面是如何使用 React Router 定义路由并为其分配组件的代码示例:
var Router = ReactRouter.Router;
var Route = ReactRouter.Route;
var Link = ReactRouter.Link;
var BrowserHistory = ReactRouter.browserHistory;
var Routes = (
<Router history={BrowserHistory}>
<Route path="/" component={Home}></Route>
<Route path="/profile/:username" component={Profile}></Route>
<Route path="*" component={NotFound}/>
</Router>
)
ReactDOM.render(Routes, document.body);
下面是前面代码的工作原理:
- React 路由器允许我们使用 React 组件本身定义路由及其组件。这使得编写路线变得容易。
- 一个
Route
组件用于定义单独的路线。路线的路径与快速通道的模式相同。 - 所有的
Route
组件都用Router
组件包装,Router
组件呈现在页面上。Router
组件找到当前网址的匹配路径,并渲染分配给该路径的组件。 - 我们将
Router
组件的history
属性分配给了ReactRouter.browserHistory
,这使得Router
使用了 HTML5 历史 API。 - 应该使用
Link
组件而不是<a>
标签,因为该组件防止整页重新加载,而只是更改网址并呈现匹配的组件。
创建 RSS 源阅读器
我们将创建的 RSS 提要阅读器将允许您添加提要网址,查看添加的网址列表,并查看每个提要网址的内容。我们将把网址存储在 HTML5 本地存储器中。
设置项目目录和文件
在本章的练习文件中,会找到两个目录:Initial
和Final
。Final
包含应用的最终源代码,而Initial
包含帮助您快速开始构建应用的文件。
在Initial
目录中,你会发现app.js
、package.json
,以及一个包含要提供给前端的文件的公共目录。app.js
文件将包含后端代码。目前,app.js
和package.json
不含代码。
我们将把我们的 HTML 代码放在public/html/index.html
中,在public/js/index.js
文件中,我们将放置我们的前端 JavaScript 代码,也就是 React 代码。
让我们首先构建后端,然后我们将构建前端。
构建后端
首先,让我们下载后端所需的包。将该代码放入package.json
文件:
{
"name": "rss-reader",
"dependencies": {
"express": "4.13.3",
"request": "2.69.0",
"xml2json": "0.9.0"
}
}
现在,运行Initial
目录中的npm install
下载软件包。这里,我们需要express
、request
和xml2json
npm 包。
将以下代码放入app.js
文件:
var express = require("express");
var app = express();
var request = require("request");
var parser = require("xml2json");
app.use(express.static(__dirname + "/public"));
app.get("/feed", function(httpRequest, httpResponse, next){
request(httpRequest.query.url, function (error, response, body) {
if (!error && response.statusCode == 200)
{
httpResponse.send(parser.toJson(body));
}
})
})
app.get("/*", function(httpRequest, httpResponse, next){
httpResponse.sendFile(__dirname + "/public/html/index.html");
})
app.listen(8080);
以上代码是这样工作的:
- 首先,我们导入库。
- 然后,我们添加一个中间件程序来服务静态文件。
- 然后,我们创建一个路由,将一个网址作为查询参数,获取该网址的内容,并将其作为响应发送回去。因为 CROS,我们无法从前端获取提要;因此,我们将通过这条路线获取它。它还将 XML 转换为 JSON,因为 JSON 更容易使用。
- 然后,对于所有其他路径,我们返回
index.html
文件。 - 最后,我们监听端口号
8080
。
建设前端
在 public/js
目录中,你会发现我们将在前端使用的所有库。在public/css
目录中,你会发现 Bootstrap 4,我们将使用它进行设计。
将此代码放在index.html
文件中,将 JS 和 CSS 文件入队,并为 React 组件创建一个容器进行渲染:
<!doctype html>
<html>
<head>
<title>RSS Feed Reader</title>
<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css">
</head>
<body>
<div id="appContainer"></div>
<script src="/js/react.js"></script>
<script src="/js/react-dom.js"></script>
<script src="/js/ReactRouter.js"></script>
<script src="/js/Flux.js"></script>
<script src="/js/microevent.js"></script>
<script src="/js/index.js"></script>
</body>
</html>
首先,我们将 Bootstrap 4 排队。然后,我们将 React、React 路由器、Flex 和微事件库排队。最后,我们将index.js
文件入队,我们将把我们的应用代码放入其中。
appContainer
元素是显示所有用户界面的元素。
定义路线
下面是为我们的应用定义路由的代码。使用巴别塔编译它,并将其放入index.js
文件中:
var Router = ReactRouter.Router;
var Route = ReactRouter.Route;
var Link = ReactRouter.Link;
var BrowserHistory = ReactRouter.browserHistory;
var Routes = (
<Router history={BrowserHistory}>
<Route path="/" component={FeedList}></Route>
<Route path="/feed/:id" component={Feed}></Route>
<Route path="submit" component={SubmitFeed}></Route>
<Route path="*" component={NotFound}/>
</Router>
)
ReactDOM.render(Routes, document.getElementById("appContainer"));
我们在这里定义了四条路线,如下所示:
- 第一条路线是主页。当用户访问主页时,我们将显示用户添加的提要网址列表。
- 第二条路线用于显示提要的内容。
- 第三种方法是添加新的提要网址。
- 最后,如果没有匹配,那么第四条路线显示未找到的信息。
创建调度程序、操作和存储
让我们创建调度器,一个让用户管理提要网址的商店,以及用于在主页上显示提要网址的FeedList
组件。要创建所有这些,编译并在index.js
文件中放置以下代码:
var AppDispatcher = new Flux.Dispatcher();
var FeedStore = {
addFeed: function(url){
var valid = /^(ftp|http|https):\/\/[^ "]+$/.test(url);
if(valid)
{
var urls = localStorage.getItem("feed-urls");
urls = JSON.parse(urls);
if(urls == null)
{
urls = [url];
}
else
{
urls[urls.length] = url;
}
localStorage.setItem("feed-urls", JSON.stringify(urls));
this.trigger("valid-url");
}
else
{
this.trigger("invalid-url");
}
},
getFeeds: function(){
var urls = localStorage.getItem("feed-urls");
urls = JSON.parse(urls);
if(urls == null)
{
return [];
}
else
{
return urls;
}
}
}
MicroEvent.mixin(FeedStore);
var Header = React.createClass({
render: function(){
return(
<nav className="navbar navbar-light bg-faded">
<ul className="nav navbar-nav">
<li className="nav-item">
<Link className="nav-link" to="/">Home</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="submit">Add</Link>
</li>
</ul>
</nav>
)
}
})
var FeedList = React.createClass({
getInitialState: function(){
return {
urls: FeedStore.getFeeds()
};
},
render: function(){
var count = 0;
return(
<div>
<Header />
<div className="container">
<br />
<ul>
{
this.state.urls.map(function(url)
{
count++;
return <li> <Link to={"/feed/" + count}>{url}</Link></li>;
})}
</ul>
</div>
</div>
)
}
})
这就是代码的工作原理:
- 首先,我们为我们的应用创建一个调度程序。
- 然后,我们创建一个名为
FeedStore
的商店,它为我们提供了添加或检索提要网址列表的方法。如果我们试图添加一个无效的网址,它会发出一个invalid-url
事件;否则,它会发出一个valid-url
事件,这样我们就可以向用户显示一条消息,指示 URL 是否被成功添加。该存储从 HTML5 本地存储中存储和检索提要 URL。 - 然后,我们通过传递
FeedStore
作为参数来调用MicroEvent.mixin
,这样商店就能够触发事件,其他人也可以绑定到这些事件。 - 然后,我们创建一个
Header
组件,这将是我们的应用头。Header
组件目前只显示两个链接:根路径和添加新网址的路径。 - 最后,我们创建
FeedList
组件。该组件的getInitialState
方法从FeedStore
检索提要网址列表,并将其返回显示。请注意,我们在显示列表时没有使用<a>
标签;相反,我们使用的是Link
组件。提要的标识是它在本地存储中存储的数组中的位置。
现在,让我们创建SubmitFeed
组件,它允许我们添加一个新的提要网址,然后显示该网址是否已成功添加。这是它的代码。编译并放入index.js
文件:
var SubmitFeed = React.createClass({
add: function(){
AppDispatcher.dispatch({
actionType: "add-feed-url",
feedURL: this.refs.feedURL.value
});
},
componentDidMount: function()
{
FeedStore.bind("invalid-url", this.invalid_url);
FeedStore.bind("valid-url", this.valid_url);
},
valid_url: function()
{
alert("Added successfully");
},
invalid_url: function()
{
alert("Please enter a valid URL");
},
componentWillUnmount: function()
{
FeedStore.unbind("invalid-url", this.invalid_url);
FeedStore.unbind("valid-url", this.valid_url);
},
render: function(){
return(
<div>
<Header />
<div className="container">
<br />
<form>
<fieldset className="form-group">
<label for="formGroupURLInput">Enter URL</label>
<input type="url" className="form-control" id="formGroupURLInput" ref="feedURL" placeholder="Enter RSS Feed URL" />
</fieldset>
<input type="button" value="Submit" className="btn" onClick={this.add} />
</form>
</div>
</div>
)
}
})
AppDispatcher.register(function(action){
if(action.actionType == "add-feed-url")
{
FeedStore.addFeed(action.feedURL);
}
})
下面是这段代码的工作原理:
SubmitFeed
组件显示一个带有文本字段和提交按钮的表单。- 当用户点击提交 按钮时,调用
add
处理程序。add
处理程序调度一个带有add-feed-url
动作类型和要添加为数据的网址的动作。 - 组件一安装好,我们就开始收听来自
FeedStore
的invalid-url
和valid-url
事件。如果网址添加成功,我们会显示一条成功消息;否则,我们会收到失败消息。 - 并且,一旦组件被卸载,我们就从
FeedStore
开始停止监听事件。我们应该解除绑定,否则我们会有多个侦听器。 - 最后,我们注册一个检查
add-feed-url
动作类型的动作回调,并调用FeedStore
存储的addFeed
方法。
现在,让我们创建Feed
组件,它显示单个提要 URL 的内容。这是它的代码。编译并放入index.js
文件:
var SingleFeedStore = {
get: function(id){
var urls = localStorage.getItem("feed-urls");
urls = JSON.parse(urls);
var request_url = urls[id - 1];
var request;
if(window.XMLHttpRequest)
{
request = new XMLHttpRequest();
}
else if(window.ActiveXObject)
{
try
{
request = new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{
request = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{}
}
}
request.open("GET", "/feed?url=" + encodeURIComponent(request_url));
var self = this;
request.addEventListener("load", function(){
self.trigger("feed-fetched", request.responseText);
}, false);
request.send(null);
}
}
MicroEvent.mixin(SingleFeedStore);
var Feed = React.createClass({
getInitialState: function(){
return {
data: []
};
},
componentDidMount: function(){
SingleFeedStore.get(this.props.params.id);
SingleFeedStore.bind("feed-fetched", this.update);
},
update: function(data){
var data = JSON.parse(data);
this.setState({data: data.rss.channel.item});
},
componentWillUnmount: function(){
SingleFeedStore.unbind("feed-fetched", this.update);
},
render: function(){
return(
<div>
<Header />
<div className="container">
<br />
<ul>
{this.state.data.map(function(post) {
return <li><a href={post.link}>{post.title}</a></li>;
})}
</ul>
</div>
</div>
)
}
})
以下是它的工作原理:
- 首先,我们创建
SingleFeedStore
,它有一个get
方法,返回一个提要网址的内容。它使用我们的服务器路由来获取网址的内容。一旦获取了内容,它就会用该内容触发feed-fetched
事件。 - 然后,我们通过传递
SingleFeedStore
作为参数来调用MicroEvent.mixin
,以便商店能够触发事件,其他人可以绑定到这些事件。 - 然后,在
Feed
组件的getInitialState
方法中,我们返回一个空的数据数组,在componentDidMount
方法中,我们请求SingleFeedStore
作为SingleFeedStore
的get
方法异步获取数据。 - 在
componentDidMount
中,我们为feed-fetched
事件绑定一个事件处理程序,并在事件发生时立即更新视图。 - 像往常一样,一旦组件被卸载,我们就解除事件处理程序的绑定。
最后,让我们创建NotFound
组件。这是它的代码。编译并放入index.js
文件:
var NotFound = React.createClass({
render: function(){
return(
<h1>Page Not Found</h1>
)
}
})
测试应用
我们现在已经完成了应用的构建。要运行网络服务器,在Initial
目录中,运行node app.js
。现在,在浏览器中,打开localhost:8080
。您只能看到标题,因为我们还没有添加任何内容。它应该是这样的:
现在,点击上的添加菜单项。您会看到这样一个表单:
输入一个有效的 feed URL,如http://qnimate.com/feed/,点击提交。现在,回到主页,您将看到以下输出:
现在,点击网址上的查看提要的内容。输出如下所示:
点击任何标题上的将在同一标签中打开网址。
总结
在本章中,我们学习了如何使用 React 和 Flux 构建单页应用。我们还探索了许多库,如xml2json
、Flux.js
、MicroEvent.js
和 React Router。之后,我们构建了一个完全可操作的 RSS 提要阅读器。
现在,您可以继续向应用添加新内容,例如实时订阅源更新和通知。
版权属于:月萌API www.moonapi.com,转载请注明出处