返回介绍

投票结果页面和路由实现

发布于 2025-02-17 12:51:32 字数 10400 浏览 0 评论 0 收藏 0

投票页面已经搞定了,让我们开始实现投票结果页面吧。

投票结果页面依然会显示两个条目,并且显示它们各自的票数。此外屏幕下方还会有一个按钮,供用户切换到下一轮投票。

现在我们根据什么来确定显示哪个界面呢?使用 URL 是个不错的主意:我们可以设置根路径 #/ 去显示投票页面,
使用 #/results 来显示投票结果页面。

我们使用 react-router 可以很容易实现这个需求。让我们加入项目:

npm install --save react-router

我们这里使用的 react-router 的 0.13 版本,它的 1.0 版本官方还没有发布,如果你打算使用其 1.0RC 版,那么下面的代码
你可能需要做一些修改,可以看 router 文档

我们现在可以来配置一下路由路径,Router 提供了一个 Route 组件用来让我们定义路由信息,同时也提供了 DefaultRoute
组件来让我们定义默认路由:

//src/index.jsx

import React from 'react';
import {Route, DefaultRoute} from 'react-router';
import App from './components/App';
import Voting from './components/Voting';

const pair = ['Trainspotting', '28 Days Later'];

const routes = <Route handler={App}>
  <DefaultRoute handler={Voting} />
</Route>;

React.render(
  <Voting pair={pair} />,
  document.getElementById('app')
);

我们定义了一个默认的路由指向我们的 Voting 组件。我们需要定义个 App 组件来用于 Route 使用。

根路由的作用就是为应用指定一个根组件:通常该组件充当所有子页面的模板。让我们来看看 App 的细节:

//src/components/App.jsx

import React from 'react';
import {RouteHandler} from 'react-router';
import {List} from 'immutable';

const pair = List.of('Trainspotting', '28 Days Later');

export default React.createClass({
  render: function() {
    return <RouteHandler pair={pair} />
  }
});

这个组件除了渲染了一个 RouteHandler 组件并没有做别的,这个组件同样是 react-router 提供的,它的作用就是
每当路由匹配了某个定义的页面后将对应的页面组件插入到这个位置。目前我们只定义了一个默认路由指向 Voting
所以目前我们的组件总是会显示 Voting 界面。

注意,我们将我们硬编码的投票数据从 index.jsx 移到了 App.jsx ,当你给 RouteHandler 传递了属性值时,
这些参数将会传给当前路由对应的组件。

现在我们可以更新 index.jsx

//src/index.jsx

import React from 'react';
import Router, {Route, DefaultRoute} from 'react-router';
import App from './components/App';
import Voting from './components/Voting';

const routes = <Route handler={App}>
  <DefaultRoute handler={Voting} />
</Route>;

Router.run(routes, (Root) => {
  React.render(
    <Root />,
    document.getElementById('app')
  );
});

run 方法会根据当前浏览器的路径去查找定义的 router 来决定渲染哪个组件。一旦确定了对应的组件,它将会被
当作指定的 Root 传给 run 的回调函数,在回调中我们将使用 React.render 将其插入 DOM 中。

目前为止我们已经基于 React router 实现了之前的内容,我们现在可以很容易添加更多新的路由到应用。让我们
把投票结果页面添加进去吧:

//src/index.jsx

import React from 'react';
import Router, {Route, DefaultRoute} from 'react-router';
import App from './components/App';
import Voting from './components/Voting';
import Results from './components/Results';

const routes = <Route handler={App}>
  <Route path="/results" handler={Results} />
  <DefaultRoute handler={Voting} />
</Route>;

Router.run(routes, (Root) => {
  React.render(
    <Root />,
    document.getElementById('app')
  );
});

这里我们用使用 <Route> 组件定义了一个名为 /results 的路径,并绑定 Results 组件。

让我们简单的实现一下这个 Results 组件,这样我们就可以看一下路由是如何工作的了:

//src/components/Results.jsx

import React from 'react/addons';

export default React.createClass({
  mixins: [React.addons.PureRenderMixin],
  render: function() {
    return <div>Hello from results!</div>
  }
});

如果你在浏览器中输入 http://localhost:8080/#/results ,你将会看到该结果组件。
而其它路径都对应这投票页面,你也可以使用浏览器的前后按钮来切换这两个界面。

接下来我们来实际实现一下结果组件:

//src/components/Results.jsx

import React from 'react/addons';

export default React.createClass({
  mixins: [React.addons.PureRenderMixin],
  getPair: function() {
    return this.props.pair || [];
  },
  render: function() {
    return <div className="results">
      {this.getPair().map(entry =>
        <div key={entry} className="entry">
          <h1>{entry}</h1>
        </div>
      )}
    </div>;
  }
});

结果界面除了显示投票项外,还应该显示它们对应的得票数,让我们先硬编码一下:

//src/components/App.jsx

import React from 'react/addons';
import {RouteHandler} from 'react-router';
import {List, Map} from 'immutable';

const pair = List.of('Trainspotting', '28 Days Later');
const tally = Map({'Trainspotting': 5, '28 Days Later': 4});

export default React.createClass({
  render: function() {
    return <RouteHandler pair={pair}
                         tally={tally} />
  }
});

现在,我们再来修改一下结果组件:

//src/components/Results.jsx

import React from 'react/addons';

export default React.createClass({
  mixins: [React.addons.PureRenderMixin],
  getPair: function() {
    return this.props.pair || [];
  },
  getVotes: function(entry) {
    if (this.props.tally && this.props.tally.has(entry)) {
      return this.props.tally.get(entry);
    }
    return 0;
  },
  render: function() {
    return <div className="results">
      {this.getPair().map(entry =>
        <div key={entry} className="entry">
          <h1>{entry}</h1>
          <div className="voteCount">
            {this.getVotes(entry)}
          </div>
        </div>
      )}
    </div>;
  }
});

现在我们来针对目前的界面功能编写测试代码,以防止未来我们破坏这些功能。

我们期望组件为每个选项都渲染一个 div,并在其中显示选项的名称和票数。如果对应的选项没有票数,则默认显示 0:

//test/components/Results_spec.jsx

import React from 'react/addons';
import {List, Map} from 'immutable';
import Results from '../../src/components/Results';
import {expect} from 'chai';

const {renderIntoDocument, scryRenderedDOMComponentsWithClass}
  = React.addons.TestUtils;

describe('Results', () => {

  it('renders entries with vote counts or zero', () => {
    const pair = List.of('Trainspotting', '28 Days Later');
    const tally = Map({'Trainspotting': 5});
    const component = renderIntoDocument(
      <Results pair={pair} tally={tally} />
    );
    const entries = scryRenderedDOMComponentsWithClass(component, 'entry');
    const [train, days] = entries.map(e => e.getDOMNode().textContent);

    expect(entries.length).to.equal(2);
    expect(train).to.contain('Trainspotting');
    expect(train).to.contain('5');
    expect(days).to.contain('28 Days Later');
    expect(days).to.contain('0');
  });
});

接下来,我们看一下”Next”按钮,它允许用户切换到下一轮投票。

我们的组件应该包含一个回调函数属性参数,当组件中的”Next”按钮被点击后,该回调函数将会被调用。我们来写一下
这个操作的测试代码:

//test/components/Results_spec.jsx

import React from 'react/addons';
import {List, Map} from 'immutable';
import Results from '../../src/components/Results';
import {expect} from 'chai';

const {renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate}
  = React.addons.TestUtils;

describe('Results', () => {

  // ...

  it('invokes the next callback when next button is clicked', () => {
    let nextInvoked = false;
    const next = () => nextInvoked = true;

    const pair = List.of('Trainspotting', '28 Days Later');
    const component = renderIntoDocument(
      <Results pair={pair}
               tally={Map()}
               next={next}/>
    );
    Simulate.click(React.findDOMNode(component.refs.next));

    expect(nextInvoked).to.equal(true);
  });
});

写法和之前的投票按钮很类似吧。接下来让我们更新一下结果组件:

//src/components/Results.jsx

import React from 'react/addons';

export default React.createClass({
  mixins: [React.addons.PureRenderMixin],
  getPair: function() {
    return this.props.pair || [];
  },
  getVotes: function(entry) {
    if (this.props.tally && this.props.tally.has(entry)) {
      return this.props.tally.get(entry);
    }
    return 0;
  },
  render: function() {
    return <div className="results">
      <div className="tally">
        {this.getPair().map(entry =>
          <div key={entry} className="entry">
            <h1>{entry}</h1>
            <div class="voteCount">
              {this.getVotes(entry)}
            </div>
          </div>
        )}
      </div>
      <div className="management">
        <button ref="next"
                className="next"
                onClick={this.props.next}>
          Next
        </button>
      </div>
    </div>;
  }
});

最终投票结束,结果页面和投票页面一样,都要显示胜利者:

//test/components/Results_spec.jsx

it('renders the winner when there is one', () => {
  const component = renderIntoDocument(
    <Results winner="Trainspotting"
             pair={["Trainspotting", "28 Days Later"]}
             tally={Map()} />
  );
  const winner = React.findDOMNode(component.refs.winner);
  expect(winner).to.be.ok;
  expect(winner.textContent).to.contain('Trainspotting');
});

我们可以想在投票界面中那样简单的实现一下上面的逻辑:

//src/components/Results.jsx

import React from 'react/addons';
import Winner from './Winner';

export default React.createClass({
  mixins: [React.addons.PureRenderMixin],
  getPair: function() {
    return this.props.pair || [];
  },
  getVotes: function(entry) {
    if (this.props.tally && this.props.tally.has(entry)) {
      return this.props.tally.get(entry);
    }
    return 0;
  },
  render: function() {
    return this.props.winner ?
      <Winner ref="winner" winner={this.props.winner} /> :
      <div className="results">
        <div className="tally">
          {this.getPair().map(entry =>
            <div key={entry} className="entry">
              <h1>{entry}</h1>
              <div className="voteCount">
                {this.getVotes(entry)}
              </div>
            </div>
          )}
        </div>
        <div className="management">
          <button ref="next"
                   className="next"
                   onClick={this.props.next}>
            Next
          </button>
        </div>
      </div>;
  }
});

到目前为止,我们已经实现了应用的 UI,虽然现在它们并没有和真实数据和操作整合起来。这很不错不是么?
我们只需要一些占位符数据就可以完成界面的开发,这让我们在这个阶段更专注于 UI。

接下来我们将会使用 Redux Store 来将真实数据整合到我们的界面中。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文