十一、使用 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 体系结构的所有部分以及数据如何在其中流动:

Understanding Flux

以下是各部分的工作原理:

  • 动作:一个动作是一个描述我们想要做什么以及我们需要做什么的数据的对象。在 Flux 中,来自所有来源的所有事件和数据都被转换为动作。甚至 UI 事件也会转换为动作。
  • 调度员:调度员是一种特殊类型的事件系统。它用于向注册的回调广播操作。调度程序所做的与发布/订阅系统不同,因为回调不会订阅特定的事件。相反,每个操作都被分派给每个注册的回调。一个应用应该只包含一个调度程序。
  • 动作创建者:动作创建者是将动作分派给分派者的方法。
  • 存储:存储是存储应用数据和逻辑的对象。商店对行动做出 React。每当调度程序调度存储依赖的操作时,回调 ping 存储以采取适当的操作。
  • React 视图:React 视图是 React 组件,可以从商店中检索数据并显示,还可以在商店存储的数据发生变化时收听商店发出的事件。请注意,存储发出的事件不会转换为操作。

因此,在 Flux 中,来自不同来源的所有事件和数据都作为动作被分派给调度器,然后每当调度器分派动作时商店就会更新它们自己,最后,每当商店更新时视图就会更新。

这是另一个图表,它提供了 Flux 如何工作的更高层次的抽象:

Understanding 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);

下面是前面代码的工作原理:

  1. React 路由器允许我们使用 React 组件本身定义路由及其组件。这使得编写路线变得容易。
  2. 一个 Route组件用于定义单独的路线。路线的路径与快速通道的模式相同。
  3. 所有的Route组件都用Router组件包装,Router组件呈现在页面上。Router组件找到当前网址的匹配路径,并渲染分配给该路径的组件。
  4. 我们将Router组件的history属性分配给了ReactRouter.browserHistory,这使得Router使用了 HTML5 历史 API。
  5. 应该使用Link组件而不是<a>标签,因为该组件防止整页重新加载,而只是更改网址并呈现匹配的组件。

创建 RSS 源阅读器

我们将创建的 RSS 提要阅读器将允许您添加提要网址,查看添加的网址列表,并查看每个提要网址的内容。我们将把网址存储在 HTML5 本地存储器中。

设置项目目录和文件

在本章的练习文件中,会找到两个目录:InitialFinalFinal包含应用的最终源代码,而Initial包含帮助您快速开始构建应用的文件。

Initial目录中,你会发现app.jspackage.json,以及一个包含要提供给前端的文件的公共目录。app.js文件将包含后端代码。目前,app.jspackage.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下载软件包。这里,我们需要expressrequestxml2json 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);

以上代码是这样工作的:

  1. 首先,我们导入库。
  2. 然后,我们添加一个中间件程序来服务静态文件。
  3. 然后,我们创建一个路由,将一个网址作为查询参数,获取该网址的内容,并将其作为响应发送回去。因为 CROS,我们无法从前端获取提要;因此,我们将通过这条路线获取它。它还将 XML 转换为 JSON,因为 JSON 更容易使用。
  4. 然后,对于所有其他路径,我们返回index.html文件。
  5. 最后,我们监听端口号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"));

我们在这里定义了四条路线,如下所示:

  1. 第一条路线是主页。当用户访问主页时,我们将显示用户添加的提要网址列表。
  2. 第二条路线用于显示提要的内容。
  3. 第三种方法是添加新的提要网址。
  4. 最后,如果没有匹配,那么第四条路线显示未找到的信息。

创建调度程序、操作和存储

让我们创建调度器,一个让用户管理提要网址的商店,以及用于在主页上显示提要网址的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>
    )
  }
})

这就是代码的工作原理:

  1. 首先,我们为我们的应用创建一个调度程序。
  2. 然后,我们创建一个名为FeedStore的商店,它为我们提供了添加或检索提要网址列表的方法。如果我们试图添加一个无效的网址,它会发出一个invalid-url事件;否则,它会发出一个valid-url事件,这样我们就可以向用户显示一条消息,指示 URL 是否被成功添加。该存储从 HTML5 本地存储中存储和检索提要 URL。
  3. 然后,我们通过传递FeedStore作为参数来调用MicroEvent.mixin,这样商店就能够触发事件,其他人也可以绑定到这些事件。
  4. 然后,我们创建一个Header组件,这将是我们的应用头。Header组件目前只显示两个链接:根路径和添加新网址的路径。
  5. 最后,我们创建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);
  }
})

下面是这段代码的工作原理:

  1. SubmitFeed组件显示一个带有文本字段和提交按钮的表单。
  2. 当用户点击提交 按钮时,调用add处理程序。add处理程序调度一个带有add-feed-url动作类型和要添加为数据的网址的动作。
  3. 组件一安装好,我们就开始收听来自FeedStoreinvalid-urlvalid-url事件。如果网址添加成功,我们会显示一条成功消息;否则,我们会收到失败消息。
  4. 并且,一旦组件被卸载,我们就从FeedStore开始停止监听事件。我们应该解除绑定,否则我们会有多个侦听器。
  5. 最后,我们注册一个检查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> 
    )
  }
})

以下是它的工作原理:

  1. 首先,我们创建SingleFeedStore,它有一个get方法,返回一个提要网址的内容。它使用我们的服务器路由来获取网址的内容。一旦获取了内容,它就会用该内容触发feed-fetched事件。
  2. 然后,我们通过传递SingleFeedStore作为参数来调用MicroEvent.mixin,以便商店能够触发事件,其他人可以绑定到这些事件。
  3. 然后,在Feed组件的getInitialState方法中,我们返回一个空的数据数组,在componentDidMount方法中,我们请求SingleFeedStore作为SingleFeedStoreget方法异步获取数据。
  4. componentDidMount中,我们为feed-fetched事件绑定一个事件处理程序,并在事件发生时立即更新视图。
  5. 像往常一样,一旦组件被卸载,我们就解除事件处理程序的绑定。

最后,让我们创建NotFound组件。这是它的代码。编译并放入index.js文件:

var NotFound = React.createClass({
  render: function(){
    return(
      <h1>Page Not Found</h1>
    )
  }
})

测试应用

我们现在已经完成了应用的构建。要运行网络服务器,在Initial目录中,运行node app.js。现在,在浏览器中,打开localhost:8080。您只能看到标题,因为我们还没有添加任何内容。它应该是这样的:

Testing the application

现在,点击上的添加菜单项。您会看到这样一个表单:

Testing the application

输入一个有效的 feed URL,如http://qnimate.com/feed/,点击提交。现在,回到主页,您将看到以下输出:

Testing the application

现在,点击网址上的查看提要的内容。输出如下所示:

Testing the application

点击任何标题上的将在同一标签中打开网址。

总结

在本章中,我们学习了如何使用 React 和 Flux 构建单页应用。我们还探索了许多库,如xml2jsonFlux.jsMicroEvent.js和 React Router。之后,我们构建了一个完全可操作的 RSS 提要阅读器。

现在,您可以继续向应用添加新内容,例如实时订阅源更新和通知。