返回介绍

实现投票界面

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

应用的投票界面非常简单:一旦投票启动,它将现实 2 个按钮,分别用来表示 2 个可选项,当投票结束,它显示最终结果。

我们之前都是以测试先行的开发方式,但是在 react 组件开发中我们将先实现组件,再进行测试。这是因为
webpack 和 react-hot-loader 提供了更加优良的 反馈机制
而且,也没有比直接看到界面更加好的测试 UI 手段了。

让我们假设有一个 Voting 组件,在之前的入口文件 index.html#app div 中加载它。由于我们的代码中
包含 JSX 语法,所以需要把 index.js 重命名为 index.jsx

//src/index.jsx

import React from 'react';
import Voting from './components/Voting';

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

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

Voting 组件将使用 pair 属性来加载数据。我们目前可以先硬编码数据,稍后我们将会用真实数据来代替。
组件本身是纯粹的,并且对数据来源并不敏感。

注意,在 webpack.config.js 中的入口点文件名也要修改:

//webpack.config.js

entry: [
  'webpack-dev-server/client?http://localhost:8080',
  'webpack/hot/only-dev-server',
  './src/index.jsx'
],

如果你此时重启 webpack-dev-server,你将看到缺失 Voting 组件的报错。让我们修复它:

//src/components/Voting.jsx

import React from 'react';

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

你将会在浏览器上看到组件创建的 2 个按钮。你可以试试修改代码感受一下浏览器自动更新的魅力,没有刷新,
没有页面加载,一切都那么迅雷不及掩耳盗铃。

现在我们来添加第一个单元测试:

//test/components/Voting_spec.jsx

import Voting from '../../src/components/Voting';

describe('Voting', () => {

});

测试组件渲染的按钮,我们必须先看看它的输出是什么。要在单元测试中渲染一个组件,我们需要 react/addons 提供
的辅助函数 renderIntoDocument

//test/components/Voting_spec.jsx

import React from 'react/addons';
import Voting from '../../src/components/Voting';

const {renderIntoDocument} = React.addons.TestUtils;

describe('Voting', () => {

  it('renders a pair of buttons', () => {
    const component = renderIntoDocument(
      <Voting pair={["Trainspotting", "28 Days Later"]} />
    );
  });
});

一旦组件渲染完毕,我就可以通过 react 提供的另一个辅助函数 scryRenderedDOMComponentsWithTag
来拿到 button 元素。我们期望存在两个按钮,并且期望按钮的值是我们设置的:

//test/components/Voting_spec.jsx

import React from 'react/addons';
import Voting from '../../src/components/Voting';
import {expect} from 'chai';

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

describe('Voting', () => {

  it('renders a pair of buttons', () => {
    const component = renderIntoDocument(
      <Voting pair={["Trainspotting", "28 Days Later"]} />
    );
    const buttons = scryRenderedDOMComponentsWithTag(component, 'button');

    expect(buttons.length).to.equal(2);
    expect(buttons[0].getDOMNode().textContent).to.equal('Trainspotting');
    expect(buttons[1].getDOMNode().textContent).to.equal('28 Days Later');
  });
});

如果我们跑一下测试,将会看到测试通过的提示:

npm run test

当用户点击某个按钮后,组件将会调用回调函数,该函数也由组件的 prop 传递给组件。

让我们完成这一步,我们可以通过使用 React 提供的测试工具 Simulate
来模拟点击操作:

//test/components/Voting_spec.jsx

import React from 'react/addons';
import Voting from '../../src/components/Voting';
import {expect} from 'chai';

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

describe('Voting', () => {

  // ...

  it('invokes callback when a button is clicked', () => {
    let votedWith;
    const vote = (entry) => votedWith = entry;

    const component = renderIntoDocument(
      <Voting pair={["Trainspotting", "28 Days Later"]}
              vote={vote}/>
    );
    const buttons = scryRenderedDOMComponentsWithTag(component, 'button');
    Simulate.click(buttons[0].getDOMNode());

    expect(votedWith).to.equal('Trainspotting');
  });
});

要想使上面的测试通过很简单,我们只需要让按钮的 onClick 事件调用 vote 并传递选中条目即可:

//src/components/Voting.jsx

import React from 'react';

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

这就是我们在纯组件中常用的方式:组件不需要做太多,只是回调传入的参数即可。

注意,这里我们又是先写的测试代码,我发现业务代码的测试要比测试 UI 更容易写,所以后面我们会保持这种
方式:UI 测试后行,业务代码测试先行。

一旦用户已经针对某对选项投过票了,我们就不应该允许他们再次投票,难道我们应该在组件内部维护某种状态么?
不,我们需要保证我们的组件是纯粹的,所以我们需要分离这个逻辑,组件需要一个 hasVoted 属性,我们先硬编码
传递给它:

//src/index.jsx

import React from 'react';
import Voting from './components/Voting';

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

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

我们可以简单的修改一下组件即可:

//src/components/Voting.jsx

import React from 'react';

export default React.createClass({
  getPair: function() {
    return this.props.pair || [];
  },
  isDisabled: function() {
    return !!this.props.hasVoted;
  },
  render: function() {
    return <div className="voting">
      {this.getPair().map(entry =>
        <button key={entry}
                disabled={this.isDisabled()}
                onClick={() => this.props.vote(entry)}>
          <h1>{entry}</h1>
        </button>
      )}
    </div>;
  }
});

让我们再为按钮添加一个提示,当用户投票完毕后,在选中的项目上添加标识,这样用户就更容易理解:

//src/components/Voting.jsx

import React from 'react';

export default React.createClass({
  getPair: function() {
    return this.props.pair || [];
  },
  isDisabled: function() {
    return !!this.props.hasVoted;
  },
  hasVotedFor: function(entry) {
    return this.props.hasVoted === entry;
  },
  render: function() {
    return <div className="voting">
      {this.getPair().map(entry =>
        <button key={entry}
                disabled={this.isDisabled()}
                onClick={() => this.props.vote(entry)}>
          <h1>{entry}</h1>
          {this.hasVotedFor(entry) ?
            <div className="label">Voted</div> :
            null}
        </button>
      )}
    </div>;
  }
});

投票界面最后要添加的,就是获胜者样式。我们可能需要添加新的 props:

//src/index.jsx

import React from 'react';
import Voting from './components/Voting';

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

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

我们再次修改一下组件:

//src/components/Voting.jsx

import React from 'react';

export default React.createClass({
  getPair: function() {
    return this.props.pair || [];
  },
  isDisabled: function() {
    return !!this.props.hasVoted;
  },
  hasVotedFor: function(entry) {
    return this.props.hasVoted === entry;
  },
  render: function() {
    return <div className="voting">
      {this.props.winner ?
        <div ref="winner">Winner is {this.props.winner}!</div> :
        this.getPair().map(entry =>
          <button key={entry}
                  disabled={this.isDisabled()}
                  onClick={() => this.props.vote(entry)}>
            <h1>{entry}</h1>
            {this.hasVotedFor(entry) ?
              <div className="label">Voted</div> :
              null}
          </button>
        )}
    </div>;
  }
});

目前我们已经完成了所有要做的,但是 render 函数看着有点丑陋,如果我们可以把胜利界面独立成新的组件
可能会好一些:

//src/components/Winner.jsx

import React from 'react';

export default React.createClass({
  render: function() {
    return <div className="winner">
      Winner is {this.props.winner}!
    </div>;
  }
});

这样投票组件就会变得很简单,它只需关注投票按钮逻辑即可:

//src/components/Vote.jsx

import React from 'react';

export default React.createClass({
  getPair: function() {
    return this.props.pair || [];
  },
  isDisabled: function() {
    return !!this.props.hasVoted;
  },
  hasVotedFor: function(entry) {
    return this.props.hasVoted === entry;
  },
  render: function() {
    return <div className="voting">
      {this.getPair().map(entry =>
        <button key={entry}
                disabled={this.isDisabled()}
                onClick={() => this.props.vote(entry)}>
          <h1>{entry}</h1>
          {this.hasVotedFor(entry) ?
            <div className="label">Voted</div> :
            null}
        </button>
      )}
    </div>;
  }
});

最后我们只需要在 Voting 组件做一下判断即可:

//src/components/Voting.jsx

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

export default React.createClass({
  render: function() {
    return <div>
      {this.props.winner ?
        <Winner ref="winner" winner={this.props.winner} /> :
        <Vote {...this.props} />}
    </div>;
  }
});

注意这里我们为胜利组件添加了 ref ,这是因为我们将在单元测试中利用它获取 DOM 节点。

这就是我们的纯组件!注意目前我们还没有实现任何逻辑:我们并没有定义按钮的点击操作。组件只是用来渲染 UI,其它
什么都不需要做。后面当我们将 UI 与 Redux Store 结合时才会涉及到应用逻辑。

继续下一步之前我们要为刚才新增的特性写更多的单元测试代码。首先, hasVoted 属性将会使按钮改变状态:

//test/components/Voting_spec.jsx

it('disables buttons when user has voted', () => {
  const component = renderIntoDocument(
    <Voting pair={["Trainspotting", "28 Days Later"]}
            hasVoted="Trainspotting" />
  );
  const buttons = scryRenderedDOMComponentsWithTag(component, 'button');

  expect(buttons.length).to.equal(2);
  expect(buttons[0].getDOMNode().hasAttribute('disabled')).to.equal(true);
  expect(buttons[1].getDOMNode().hasAttribute('disabled')).to.equal(true);
});

hasVoted 匹配的按钮将显示 Voted 标签:

//test/components/Voting_spec.jsx

it('adds label to the voted entry', () => {
  const component = renderIntoDocument(
    <Voting pair={["Trainspotting", "28 Days Later"]}
            hasVoted="Trainspotting" />
  );
  const buttons = scryRenderedDOMComponentsWithTag(component, 'button');

  expect(buttons[0].getDOMNode().textContent).to.contain('Voted');
});

当获胜者产生,界面将不存在按钮,取而代替的是胜利者元素:

//test/components/Voting_spec.jsx

it('renders just the winner when there is one', () => {
  const component = renderIntoDocument(
    <Voting winner="Trainspotting" />
  );
  const buttons = scryRenderedDOMComponentsWithTag(component, 'button');
  expect(buttons.length).to.equal(0);

  const winner = React.findDOMNode(component.refs.winner);
  expect(winner).to.be.ok;
  expect(winner.textContent).to.contain('Trainspotting');
});

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

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

发布评论

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