翻譯|開(kāi)啟React,Redux和Immutable之旅:測(cè)試驅(qū)動(dòng)教程(part1)

翻譯版本,原文請(qǐng)見(jiàn),第一部分,第二部分

Image 提供:[egghead.io](http://egghead.io/)
Image 提供:[egghead.io](http://egghead.io/)

幾周以前,我正在漫無(wú)目的的瀏覽Hacker News,讀到一篇關(guān)于Redux的頭條新聞,Redux的內(nèi)容我是了解,但是另一個(gè)談到的問(wèn)題javascript fatigue(JavaScript 疲勞)已經(jīng)困擾我了,所以我沒(méi)有太關(guān)心,知道讀到Redux的幾個(gè)特征.

  • 強(qiáng)化了函數(shù)式編程,確保app行為的可預(yù)測(cè)性
  • 允許app的同構(gòu),客戶端和服務(wù)端的大多數(shù)邏輯都可以共享
  • 時(shí)間旅行的debugger?有可能嗎?

Redux似乎是React程序state管理的優(yōu)雅方法,再者誰(shuí)說(shuō)的時(shí)間旅行不可能?所以我讀了文檔和一篇非常精彩的教程@teropa:A Comprehensive Guide to Test-First Development with Redux,React,and Immutable(這一篇也是我寫作的主要靈感來(lái)源).

我喜歡Redux,代碼非常優(yōu)雅,debugger令人瘋狂的偉大.我的意思是-看看這個(gè)
todo-action

接下來(lái)的教程第一部分希望引導(dǎo)你理解Redux運(yùn)行的原則.教程的目的僅限于(客戶端,沒(méi)有同構(gòu),是比較簡(jiǎn)單的app)保持教程的簡(jiǎn)明扼要.如果你想發(fā)掘的更深一點(diǎn),我僅建議你閱讀上面提高的那個(gè)教程.對(duì)比版的Github repo在這里,共享代碼貼合教程的步驟.如果你對(duì)代碼或者教程有任何問(wèn)題和建議,最好能留下留言.

編輯按:文章已經(jīng)更新為ES2015版的句法.

APP

為了符合教程的目的,我們將建一個(gè)經(jīng)典的TodoMVC,為了記錄需要,需求如下:

  • 每一個(gè)todo可以激活或者完成
  • 可以添加,編輯,刪除一個(gè)todo
  • 可以根據(jù)它的status來(lái)過(guò)濾篩選todos
  • 激活的todos的數(shù)目顯示在底部
  • 完成的Todo理解可以刪除

Reudux和Immutable:使用函數(shù)編程去營(yíng)救

回到幾個(gè)月前,我正在開(kāi)發(fā)一個(gè)webapp包含儀表板. 隨著app的成長(zhǎng),我們注意到越來(lái)越多的有害的bugs,藏在代碼角落里,很難發(fā)現(xiàn).類似:“如果你要導(dǎo)航到這一頁(yè),點(diǎn)擊按鈕,然后回到主頁(yè),喝一杯咖啡,回到這一頁(yè)然后點(diǎn)擊兩次,奇怪的事情發(fā)生了.”這些bug的來(lái)源要么是異步操作(side effects)或者邏輯:一個(gè)action可能在app中有意想不到的影響,這個(gè)有時(shí)候我們還發(fā)現(xiàn)不了.

這就是Redux之所以存在的威力:整個(gè)app的state是一個(gè)單一的數(shù)據(jù)結(jié)構(gòu),state tree.這一點(diǎn)意思是說(shuō):在任何時(shí)刻,展示給用戶的內(nèi)容僅僅是state tree結(jié)果,這就是單一來(lái)源的真相(用戶界面的顯示內(nèi)容是由state tree來(lái)決定的).每一個(gè)action接收state,應(yīng)用相應(yīng)的修改(例如,添加一個(gè)todo),輸出更新的state tree.更新的結(jié)果渲染展示給用戶.里面沒(méi)有模糊的異步操作,沒(méi)有變量的引用引起的不經(jīng)意的修改.這個(gè)步驟使得app有了更好的結(jié)構(gòu),分離關(guān)注點(diǎn),dubugging也更好用了.

Immutable是有Facebook開(kāi)發(fā)的助手函數(shù)庫(kù),提供一些工具去創(chuàng)建和操作immutable數(shù)據(jù)結(jié)構(gòu).盡管在Redux也不是一定要使用它,但是它通過(guò)禁止對(duì)象的修改,強(qiáng)化了函數(shù)式編程方法.有了immutable,當(dāng)我們想更新一個(gè)對(duì)象,實(shí)際上我們修改的是一個(gè)新創(chuàng)建的的對(duì)象,原先的對(duì)象保持不變.

這里是“Immutable文檔”里面的例子:

 var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 2);
assert(map1 === map2); // no change
var map3 = map1.set('b', 50);
assert(map1 !== map3); // change

我們更新了map1的一個(gè)值,map1對(duì)象保持不變,一個(gè)新的對(duì)象map3被創(chuàng)建了.

Immutable在store中被用來(lái)儲(chǔ)存我們的app的state tree.很快我們會(huì)看到Immutable提供了一下操作對(duì)象的簡(jiǎn)單和有效的方法.

配置項(xiàng)目

聲明:一些配置有@terops的教程啟發(fā).

注意事項(xiàng):推薦使用Node.js>=4.0.0.你可以使用nvm(node version manager)來(lái)切換不同的node.js的版本.

這里是比較版本的提交

開(kāi)始配置項(xiàng)目:

mkdir redux-todomvc
cd redux-todomvc
npm init -y

項(xiàng)目的目錄結(jié)構(gòu)如下:

├── dist
│   ├── bundle.js
│   └── index.html
├── node_modules
├── package.json
├── src
├── test
└── webpack.config.js

首先創(chuàng)建一個(gè)簡(jiǎn)單的HTML頁(yè)面,用來(lái)運(yùn)行我們的app
dist/index.html

 <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>React TodoMVC</title>
</head>
<body>
  <div id="app"></div>
  <script src="bundle.js"></script>
</body>
</html>

有了這個(gè)文件,我們寫一個(gè)簡(jiǎn)單的腳本文件看看包安裝的情況
src/index.js

console.log('Hello world !');

我們將會(huì)使用[Webpack]來(lái)打包成為bundle.js文件.Webpack的特性是速度,容易配置,大部分是熱更新的.代碼的更新不需要重新加載,意味著app的state保持熱加載.

讓我們安裝webpack:

npm install —save-dev webpack@1.12.14 webpack-dev-server@1.14.1

app使用ES2015的語(yǔ)法,帶來(lái)一些優(yōu)異的特性和一些語(yǔ)法糖.如果你想了解更多的ES2015內(nèi)容,這個(gè)recap是一個(gè)不錯(cuò)的資源.

Babel用來(lái)把ES2015的語(yǔ)法改變?yōu)槠胀ǖ腏S語(yǔ)法:
npm install —save-dev babel-core@6.5.2 babel-loader@6.2.4 babel-preset-es2015@6.5.0

我們將使用JSX語(yǔ)法編寫React組件,所以讓我們安裝Babel React package:
npm install —save-dev babel-preset-react@6.5.0

配置webpack輸出源文件:
package.json

 "babel": {
  "presets": ["es2015", "react"]
}

webpack.config.js

 module.exports = {
  entry: [
    './src/index.js'
  ],
  module: {
    loaders: [{
      test: /\.jsx?$/,
      exclude: /node_modules/,
      loader: 'babel'
    }]
  },
  resolve: {
    extensions: ['', '.js', '.jsx']
  },
  output: {
    path: __dirname + '/dist',
    publicPath: '/',
    filename: 'bundle.js'
  },
  devServer: {
    contentBase: './dist'
  }
};

現(xiàn)在添加React和React熱加載組件到項(xiàng)目中:

 npm install --save react@0.14.7 react-dom@0.14.7
 npm install --save-dev react-hot-loader@1.3.0

為了讓熱加載能運(yùn)行,webpack.config.js文件中要做一些修改.

webpack.config.js

 var webpack = require('webpack'); // Requiring the webpack lib

module.exports = {
  entry: [
    'webpack-dev-server/client?http://localhost:8080', // Setting the URL for the hot reload
    'webpack/hot/only-dev-server', // Reload only the dev server
    './src/index.js'
  ],
  module: {
    loaders: [{
      test: /\.jsx?$/,
      exclude: /node_modules/,
      loader: 'react-hot!babel' // Include the react-hot loader
    }]
  },
  resolve: {
    extensions: ['', '.js', '.jsx']
  },
  output: {
    path: __dirname + '/dist',
    publicPath: '/',
    filename: 'bundle.js'
  },
  devServer: {
    contentBase: './dist',
    hot: true // Activate hot loading
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin() // Wire in the hot loading plugin
  ]
};

配置單元測(cè)試框架

我們將使用Mocha和Chai來(lái)進(jìn)行測(cè)試工作.這兩個(gè)工具廣泛的被使用,他們的輸出內(nèi)容對(duì)于測(cè)試驅(qū)動(dòng)開(kāi)發(fā)非常的好.Chai-immutable是一個(gè)chai插件,用來(lái)處理immutable數(shù)據(jù)結(jié)構(gòu).

npm install --save immutable@3.7.6
npm install --save-dev mocha@2.4.5 chai@3.5.0 chai-immutable@1.5.3

在我們的例子中,我們不會(huì)依賴瀏覽器為基礎(chǔ)的測(cè)試運(yùn)行器例如Karma-替代方案是我們使用jsdom庫(kù),它將會(huì)使用純javascirpt創(chuàng)建一個(gè)假DOM,這樣做讓我們的測(cè)試更加快速.

npm install —save-dev jsdom@8.0.4

我們也需要為測(cè)試寫一個(gè)啟動(dòng)腳本,要考慮到下面的內(nèi)容.

  • 模擬documentwindow對(duì)象,通常是由瀏覽器提供
  • 通過(guò)chia-immutable告訴chai組件我們要使用immutable數(shù)據(jù)結(jié)構(gòu)

test/setup.js

 import jsdom from 'jsdom';
import chai from 'chai';
import chaiImmutable from 'chai-immutable';

const doc = jsdom.jsdom('<!doctype html><html><body></body></html>');
const win = doc.defaultView;

global.document = doc;
global.window = win;

Object.keys(window).forEach((key) => {
  if (!(key in global)) {
    global[key] = window[key];
  }
});

chai.use(chaiImmutable);

更新一下npm test腳本
package.json

 "scripts": {
  "test": "mocha --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'",
  "test:watch": "npm run test -- --watch --watch-extensions jsx"
},

npm run test:watch命令在windows操作系統(tǒng)下似乎不能工作.

現(xiàn)在,如果我們運(yùn)行npm run test:watch,所有test目錄里的.js,.jsx文件在更新自身或者源文件的時(shí)候,將會(huì)運(yùn)行mocha測(cè)試.

配置完成了:我們可以在終端中運(yùn)行webpack-dev-server,打開(kāi)另一個(gè)終端,npm run test:watch.在瀏覽器中打開(kāi)localhost:8080.檢查hello world!是否出現(xiàn)在終端中.

構(gòu)建state tree

之前提到過(guò),state tree是能提供app所有信息的數(shù)據(jù)結(jié)構(gòu).這個(gè)結(jié)構(gòu)需要在實(shí)際開(kāi)發(fā)之前經(jīng)過(guò)深思熟慮,因?yàn)樗鼘⒂绊懸恍┐a的結(jié)構(gòu)和交互作用.

作為示例,我們app是一個(gè)TODO list由幾個(gè)條目組合而成

state tree 1
state tree 1

每一個(gè)條目有一個(gè)文本,為了便于操作,設(shè)一個(gè)id,此外每個(gè)item有兩個(gè)狀態(tài)之一:活動(dòng)或者完成:最后一個(gè)條目需要一個(gè)可編輯的狀態(tài)(當(dāng)用戶想編輯的文本的時(shí)候),
所以我們需要保持下面的數(shù)據(jù)結(jié)構(gòu):


state tree 2
state tree 2

也有可能基于他們的狀態(tài)進(jìn)行篩選,所以我們天劍filter條目來(lái)獲取最終的state tree:

sate tree 3
sate tree 3

創(chuàng)建UI

首先我們把a(bǔ)pp分解為下面的組件:

  • TodoHeader組件是創(chuàng)建新todo的輸入組件
  • TodoList組件是todo的列表
  • todoItem是一個(gè)todo
  • todoInput是編輯todo的輸入框
  • TodoTools是顯示未完成的條目數(shù)量,過(guò)濾器和清除完成的按鈕
  • footer是顯示信息的,沒(méi)有具體的邏輯

我們也創(chuàng)建TodoApp組件組織所有的其他組件.

首次運(yùn)行我們的組件

提示:運(yùn)行這個(gè)版本

正如我們所見(jiàn)的,我們將會(huì)把所有組件放到合并成一個(gè)TodoApp組件.所以讓我們添加組件到index.html文件的#appDIV中:
src/index.jsx

import React from 'react';
import ReactDOM from 'react-dom';
import {List, Map} from 'immutable';

import TodoApp from './components/TodoApp';

const todos = List.of(
  Map({id: 1, text: 'React', status: 'active', editing: false}),
  Map({id: 2, text: 'Redux', status: 'active', editing: false}),
  Map({id: 3, text: 'Immutable', status: 'completed', editing: false})
);

ReactDOM.render(
  <TodoApp todos={todos} />,
  document.getElementById('app')
);

因?yàn)槲覀冊(cè)?code>index.jsx文件中使用JSX語(yǔ)法,需要在wabpack中擴(kuò)展.jsx.修改如下:
webpack.config.js

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

編寫todo list UI

現(xiàn)在我們編寫第一版本的TodoApp組件,用來(lái)顯示todo項(xiàng)目列表:
src/components/TodoApp.jsx

 import React from 'react';

export default class TodoApp extends React.Component {
  getItems() {
    return this.props.todos || [];
  }
  render() {
    return <div>
      <section className="todoapp">
        <section className="main">
          <ul className="todo-list">
            {this.getItems().map(item =>
              <li className="active" key={item.get('text')}>
                <div className="view">
                  <input type="checkbox"
                         className="toggle" />
                  <label htmlFor="todo">
                    {item.get('text')}
                  </label>
                  <button className="destroy"></button>
                </div>
              </li>
            )}
          </ul>
        </section>
      </section>
    </div>
  }
};

要記住兩件事情:
第一個(gè),如果你看到的結(jié)果不太好,修復(fù)它,我們將會(huì)使用todomvc-app-css包來(lái)補(bǔ)充一些需要的樣式

npm install --save todomvc-app-css@2.0.4
npm install style-loader@0.13.0 css-loader@0.23.1 --save-dev

我們需要告訴webpack去加載css 樣式文件:
webpack.config.js

// ...
module: {
  loaders: [{
    test: /\.jsx?$/,
    exclude: /node_modules/,
    loader: 'react-hot!babel'
  }, {
    test: /\.css$/,
    loader: 'style!css' // We add the css loader
  }]
},
//...

然后在inde.jsx文件中添加樣式:
src/index.jsx

 // ...
require('../node_modules/todomvc-app-css/index.css');

ReactDOM.render(
  <TodoApp todos={todos} />,
  document.getElementById('app')
);

第二件事是:代碼似乎很復(fù)雜,這就是我們?yōu)槭裁匆獎(jiǎng)?chuàng)建兩個(gè)或者多個(gè)組件的原因:TodoListTodoItem將會(huì)分別關(guān)注條目列表和單個(gè)的條目.

這一部分的提交代碼

src/components/TodoApp.jsx

 import React from 'react';
import TodoList from './TodoList'

export default class TodoApp extends React.Component {
  render() {
    return <div>
      <section className="todoapp">
        <TodoList todos={this.props.todos} />
      </section>
    </div>
  }
};

TodoList組件中根據(jù)獲取的props為每一個(gè)條目顯示一個(gè)TodoItem組件.

src/components/TodoList.jsx

 import React from 'react';
import TodoItem from './TodoItem';

export default class TodoList extends React.Component {
  render() {
    return <section className="main">
      <ul className="todo-list">
        {this.props.todos.map(item =>
          <TodoItem key={item.get('text')}
                    text={item.get('text')} />
        )}
      </ul>
    </section>
  }
};

src/components/TodoItem.jsx

 import React from 'react';

export default class TodoItem extends React.Component {
  render() {
    return <li className="todo">
      <div className="view">
        <input type="checkbox"
               className="toggle" />
        <label htmlFor="todo">
          {this.props.text}
        </label>
        <button className="destroy"></button>
      </div>
    </li>
  }
};

在我們深入用戶的交互操作之前,我們先在組件TodoItem中添加一個(gè)input用于編輯
src/componensts/TodoItem.jsx

 import React from 'react';

import TextInput from './TextInput';

export default class TodoItem extends React.Component {

  render() {
    return <li className="todo">
      <div className="view">
        <input type="checkbox"
               className="toggle" />
        <label htmlFor="todo">
          {this.props.text}
        </label>
        <button className="destroy"></button>
      </div>
      <TextInput /> // We add the TextInput component
    </li>
  }
};

TextInput組件如下
src/compoents/TextInput.jsx

import React from 'react';

export default class TextInput extends React.Component {
  render() {
    return <input className="edit"
                  autoFocus={true}
                  type="text" />
  }
};

”純“組件的好處:PureRenderMixin

這部分的提交代碼

除了允許函數(shù)式編程的樣式,我們的UI是單純的,可以使用PureRenderMixin來(lái)提升速度,正如React 文檔:
如果你的React的組件渲染函數(shù)是”純“(換句話就是,如果使用相同的porps和state,總是會(huì)渲染出同樣的結(jié)果),你可以使用mixin在同一個(gè)案例轉(zhuǎn)給你來(lái)提升性能.

正如React文檔(我們也會(huì)在第二部分看到TodoApp組件有額外的角色會(huì)阻止PureRenderMixin的使用)展示的mixin也非常容易的添加到我們的子組件中:
npm install --save react-addons-pure-render-mixin@0.14.7
src/components/TodoList.jsc

 import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin'
import TodoItem from './TodoItem';

export default class TodoList extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  render() {
    // ...
  }
};

src/components/TodoItem/jsx

import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin'
import TextInput from './TextInput';

export default class TodoItem extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  render() {
    // ...
  }
};

src/components/TextInput.jsx

import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin'

export default class TextInput extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  render() {
    // ...
  }
};

在list組件中處理用戶的actions

好了,現(xiàn)在我們配置好了list組件.然而我們沒(méi)有考慮添加用戶的actions和怎么添加進(jìn)去組件.

props的力量

在React中,props對(duì)象是當(dāng)我們實(shí)例化一個(gè)容器(container)的時(shí)候,通過(guò)設(shè)定的attributes來(lái)設(shè)定.例如,如果我們實(shí)例化一個(gè)TodoItem元素:

<TodoItem text={'Text of the item'} />

然后我們?cè)?code>TodoItem組件中獲取this.props.text變量:

 // in TodoItem.jsx
console.log(this.props.text);
// outputs 'Text of the item'

Redux構(gòu)架中強(qiáng)化使用props.基礎(chǔ)的原理是state幾乎都存在于他的props里面.換一種說(shuō)法:對(duì)于同樣一組props,兩個(gè)元素的實(shí)例應(yīng)該輸出完全一樣的結(jié)果.正如之前我們看到的,整個(gè)app的state都包含在一個(gè)state tree中:意思是說(shuō),state tree 如果通過(guò)props的方式傳遞到組件,將會(huì)完整和可預(yù)期的決定app的視覺(jué)輸出.

TodoList組件

這一部分的代碼修改

在這一部分和接下來(lái)的一部分,我們將會(huì)了解一個(gè)測(cè)試優(yōu)先的方法.

為了幫助我們測(cè)試組件,React庫(kù)提供了TestUtils工具插件,有一下方法:

  • renderIntoDocument,渲染組件到附加的DOM節(jié)點(diǎn)
  • scryRenderDOMComponentsWIthTag,使用提供的標(biāo)簽(例如li,input)在DOM中找到所有的組件實(shí)例.
  • scryRederDOMComponentsWithClass,同上使用的是類
  • Simulate,模擬用戶的actions(例如 點(diǎn)擊,按鍵,文本輸入…)

TestUtils插件沒(méi)有包含在react包中,所以需要單獨(dú)安裝
npm install --save-dev react-addons-test-utils@0.14.7

我們的第一個(gè)測(cè)試將確保Todolist組件中,如果filterprops被設(shè)置為active,將會(huì)展示所有的活動(dòng)條目:
test/components/TodoList_spec.jsx

 import React from 'react';
import TestUtils from 'react-addons-test-utils';
import TodoList from '../../src/components/TodoList';
import {expect} from 'chai';
import {List, Map} from 'immutable';

const {renderIntoDocument,
     scryRenderedDOMComponentsWithTag} = TestUtils;

describe('TodoList', () => {
it('renders a list with only the active items if the filter is active', () => {
  const todos = List.of(
    Map({id: 1, text: 'React', status: 'active'}),
    Map({id: 2, text: 'Redux', status: 'active'}),
    Map({id: 3, text: 'Immutable', status: 'completed'})
  );
  const filter = 'active';
  const component = renderIntoDocument(
    <TodoList filter={filter} todos={todos} />
  );
  const items = scryRenderedDOMComponentsWithTag(component, 'li');

  expect(items.length).to.equal(2);
  expect(items[0].textContent).to.contain('React');
  expect(items[1].textContent).to.contain('Redux');
});
});

我們可以看到測(cè)試失敗了,期待的是兩個(gè)活動(dòng)條目,但是實(shí)際上是三個(gè).這是再正常不過(guò)的了,因?yàn)槲覀儧](méi)有編寫實(shí)際篩選的邏輯:
src/components/TodoList.jsx

// ...
export default class TodoList extends React.Component {
  // Filters the items according to their status
  getItems() {
    if (this.props.todos) {
      return this.props.todos.filter(
        (item) => item.get('status') === this.props.filter
      );
    }
    return [];
  }
  render() {
    return <section className="main">
      <ul className="todo-list">
        // Only the filtered items are displayed
        {this.getItems().map(item =>
          <TodoItem key={item.get('text')}
                    text={item.get('text')} />
        )}
      </ul>
    </section>
  }
};

第一個(gè)測(cè)試通過(guò)了.別停下來(lái),讓我們添加篩選器:allcompleted:
test/components/TodoList_spec.js

// ...
describe('TodoList', () => {
// ...
it('renders a list with only completed items if the filter is completed', () => {
  const todos = List.of(
    Map({id: 1, text: 'React', status: 'active'}),
    Map({id: 2, text: 'Redux', status: 'active'}),
    Map({id: 3, text: 'Immutable', status: 'completed'})
  );
  const filter = 'completed';
  const component = renderIntoDocument(
    <TodoList filter={filter} todos={todos} />
  );
  const items = scryRenderedDOMComponentsWithTag(component, 'li');

  expect(items.length).to.equal(1);
  expect(items[0].textContent).to.contain('Immutable');
});

it('renders a list with all the items', () => {
  const todos = List.of(
    Map({id: 1, text: 'React', status: 'active'}),
    Map({id: 2, text: 'Redux', status: 'active'}),
    Map({id: 3, text: 'Immutable', status: 'completed'})
  );
  const filter = 'all';
  const component = renderIntoDocument(
    <TodoList filter={filter} todos={todos} />
  );
  const items = scryRenderedDOMComponentsWithTag(component, 'li');

  expect(items.length).to.equal(3);
  expect(items[0].textContent).to.contain('React');
  expect(items[1].textContent).to.contain('Redux');
  expect(items[2].textContent).to.contain('Immutable');
});
});

第三個(gè)測(cè)試失敗了,因?yàn)?code>all篩選器更新組件的邏輯稍稍有點(diǎn)不同
src/components/TodoList.jsx

 // ...
export default React.Component {
// Filters the items according to their status
getItems() {
  if (this.props.todos) {
    return this.props.todos.filter(
      (item) => this.props.filter === 'all' || item.get('status') === this.props.filter
    );
  }
  return [];
}
// ...
});

在這一點(diǎn)上,我們知道顯示在app中的條目都是經(jīng)過(guò)filter屬性過(guò)濾的.如果在瀏覽器中看看結(jié)果,沒(méi)有顯示任何條目,因?yàn)槲覀冞€沒(méi)有設(shè)置:
src/index.jsx

 // ...
const todos = List.of(
  Map({id: 1, text: 'React', status: 'active', editing: false}),
  Map({id: 2, text: 'Redux', status: 'active', editing: false}),
  Map({id: 3, text: 'Immutable', status: 'completed', editing: false})
);

const filter = 'all';

require('../node_modules/todomvc-app-css/index.css')

ReactDOM.render(
  <TodoApp todos={todos} filter = {filter}/>,
  document.getElementById('app')
);

src/components/TodoApp.jsx

// ...
export default class TodoApp extends React.Component {
  render() {
    return <div>
      <section className="todoapp">
        // We pass the filter props down to the TodoList component
        <TodoList todos={this.props.todos} filter={this.props.filter}/>
      </section>
    </div>
  }
};

使用在index.jsc文件中聲明的filter常量過(guò)濾以后,我們的條目重新出現(xiàn)了.

TodoItem component

這部分的代碼修改

現(xiàn)在,讓我們關(guān)注一下TodoItem組件.首先,我們想確信TodoItem組件真正渲染一個(gè)條目.我們也想測(cè)試一下還沒(méi)有測(cè)試的特性,就是當(dāng)一個(gè)條目完成的時(shí)候,他的文本中間有一條線
test/components/TodoItem_spec.js

 import React from 'react';
import TestUtils from 'react-addons-test-utils';
import TodoItem from '../../src/components/TodoItem';
import {expect} from 'chai';

const {renderIntoDocument,
      scryRenderedDOMComponentsWithTag} = TestUtils;

describe('TodoItem', () => {
 it('renders an item', () => {
   const text = 'React';
   const component = renderIntoDocument(
     <TodoItem text={text} />
   );
   const todo = scryRenderedDOMComponentsWithTag(component, 'li');

   expect(todo.length).to.equal(1);
   expect(todo[0].textContent).to.contain('React');
 });

 it('strikes through the item if it is completed', () => {
   const text = 'React';
   const component = renderIntoDocument(
     <TodoItem text={text} isCompleted={true}/>
   );
   const todo = scryRenderedDOMComponentsWithTag(component, 'li');

   expect(todo[0].classList.contains('completed')).to.equal(true);
 });
});

為了使第二個(gè)測(cè)試通過(guò),如果條目的狀態(tài)是complete我們使用了類complete,它將會(huì)通過(guò)props傳遞向下傳遞.我們將會(huì)使用classnames包來(lái)操作我們的DOM類.
npm install —save classnames

src/components/TodoItem.jsx

 import React from 'react';
// We need to import the classNames object
import classNames from 'classnames';

import TextInput from './TextInput';

export default class TodoItem extends React.Component {
render() {
  var itemClass = classNames({
    'todo': true,
    'completed': this.props.isCompleted
  });
  return <li className={itemClass}>
    // ...
  </li>
}
};

一個(gè)item在編輯的時(shí)候外觀應(yīng)該看起來(lái)不一樣,由isEditingprops來(lái)包裹.
test/components/TodoItem_spec.js

 // ...
describe('TodoItem', () => {
//...

it('should look different when editing', () => {
  const text = 'React';
  const component = renderIntoDocument(
    <TodoItem text={text} isEditing={true}/>
  );
  const todo = scryRenderedDOMComponentsWithTag(component, 'li');

  expect(todo[0].classList.contains('editing')).to.equal(true);
});
});

為了使測(cè)試通過(guò),我們僅僅需要更新itemClass對(duì)象:
src/components/TodoItem.jsx

 // ...
export default class TodoItem extends React.Component {
 render() {
   var itemClass = classNames({
     'todo': true,
     'completed': this.props.isCompleted,
     'editing': this.props.isEditing
   });
   return <li className={itemClass}>
     // ...
   </li>
 }
};

條目左側(cè)的checkbox如果條目完成,應(yīng)該標(biāo)記位checked:
test/components/TodoItem_spec.js

 // ...
describe('TodoItem', () => {
 //...

 it('should be checked if the item is completed', () => {
   const text = 'React';
   const text2 = 'Redux';
   const component = renderIntoDocument(
     <TodoItem text={text} isCompleted={true}/>,
     <TodoItem text={text2} isCompleted={false}/>
   );
   const input = scryRenderedDOMComponentsWithTag(component, 'input');
   expect(input[0].checked).to.equal(true);
   expect(input[1].checked).to.equal(false);
 });
});

React有個(gè)設(shè)定checkbox輸入state的方法:defaultChecked.
src/components/TodoItem.jsx

 // ...
export default class TodoItem extends React.Component {
render() {
  // ...
  return <li className={itemClass}>
    <div className="view">
      <input type="checkbox"
             className="toggle"
             defaultChecked={this.props.isCompleted}/>
      // ...
    </div>
  </li>
}
};

我們也從TodoList組件向下傳遞isCompletedisEditingprops.
src/components/TodoList.jsx

 // ...
export default class TodoList extends React.Component {
// ...
// This function checks whether an item is completed
isCompleted(item) {
  return item.get('status') === 'completed';
}
render() {
  return <section className="main">
    <ul className="todo-list">
      {this.getItems().map(item =>
        <TodoItem key={item.get('text')}
                  text={item.get('text')}
                  // We pass down the info on completion and editing
                  isCompleted={this.isCompleted(item)}
                  isEditing={item.get('editing')} />
      )}
    </ul>
  </section>
}
};

截止目前,我們已經(jīng)能夠在組件中反映出state:例如,完成的條目將會(huì)被劃線.然而一個(gè)webapp將會(huì)處理諸如點(diǎn)擊按鈕的操作.在Redux的模式中,這個(gè)操作也通過(guò)porps來(lái)執(zhí)行,稍稍特殊的是通過(guò)在props中傳遞回調(diào)函數(shù)來(lái)完成.通過(guò)這種方式,我們?cè)俅伟裊I和App的邏輯處理分離開(kāi):組件根本不需要知道按鈕點(diǎn)擊的操作具體是什么,僅僅是點(diǎn)擊觸發(fā)了一些事情.

為了描述這個(gè)原理,我們將會(huì)測(cè)試如果用戶點(diǎn)擊了delete按鈕(紅色X),delteItem函數(shù)將會(huì)被調(diào)用.

這部分的代碼修改

test/components/TodoItem_spec.jsx

 / ...
// The Simulate helper allows us to simulate a user clicking
const {renderIntoDocument,
      scryRenderedDOMComponentsWithTag,
      Simulate} = TestUtils;

describe('TodoItem', () => {
 // ...
 it('invokes callback when the delete button is clicked', () => {
   const text = 'React';
   var deleted = false;
   // We define a mock deleteItem function
   const deleteItem = () => deleted = true;
   const component = renderIntoDocument(
     <TodoItem text={text} deleteItem={deleteItem}/>
   );
   const buttons = scryRenderedDOMComponentsWithTag(component, 'button');
   Simulate.click(buttons[0]);

   // We verify that the deleteItem function has been called
   expect(deleted).to.equal(true);
 });
});

為了是這個(gè)測(cè)試通過(guò),我們必須在delete按鈕聲明一個(gè)onClick句柄,他將會(huì)調(diào)用經(jīng)過(guò)props傳遞的deleteItem函數(shù).

src/components/TodoItem.jsx

 // ...
export default class TodoItem extends React.Component {
 render() {
   // ...
   return <li className={itemClass}>
     <div className="view">
       // ...
       // The onClick handler will call the deleteItem function given in the props
       <button className="destroy"
               onClick={() => this.props.deleteItem(this.props.id)}></button>
     </div>
     <TextInput />
   </li>
 }
};

重要的一點(diǎn):實(shí)際刪除的邏輯還沒(méi)有實(shí)施,這個(gè)將是Redux的主要作用.
在同一個(gè)model,我們可以測(cè)試和實(shí)施下面的特性:

  • 點(diǎn)擊checkbox將會(huì)調(diào)用toggleComplete函數(shù)
  • 雙擊條目標(biāo)簽,將會(huì)調(diào)用editItem函數(shù)

test/components/TodoItem_spec.js

 // ...
describe('TodoItem', () => {
 // ...
 it('invokes callback when checkbox is clicked', () => {
   const text = 'React';
   var isChecked = false;
   const toggleComplete = () => isChecked = true;
   const component = renderIntoDocument(
     <TodoItem text={text} toggleComplete={toggleComplete}/>
   );
   const checkboxes = scryRenderedDOMComponentsWithTag(component, 'input');
   Simulate.click(checkboxes[0]);

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

 it('calls a callback when text is double clicked', () => {
   var text = 'React';
   const editItem = () => text = 'Redux';
   const component = renderIntoDocument(
     <TodoItem text={text} editItem={editItem}/>
   );
   const label = component.refs.text
   Simulate.doubleClick(label);

   expect(text).to.equal('Redux');
 });
});

src/compoents/TodoItem.jsx

 // ...
render() {
 // ...
 return <li className={itemClass}>
   <div className="view">
     // We add an onClick handler on the checkbox
     <input type="checkbox"
            className="toggle"
            defaultChecked={this.props.isCompleted}
            onClick={() => this.props.toggleComplete(this.props.id)}/>
     // We add a ref attribute to the label to facilitate the testing
     // The onDoubleClick handler is unsurprisingly called on double clicks
     <label htmlFor="todo"
            ref="text"
            onDoubleClick={() => this.props.editItem(this.props.id)}>
       {this.props.text}
     </label>
     <button className="destroy"
             onClick={() => this.props.deleteItem(this.props.id)}></button>
   </div>
   <TextInput />
 </li>

我們也從TodoList組件借助props向下傳遞editItem,deleteItemtoggleComplete函數(shù).
src/components/TodoList.jsx

 // ...
export default class TodoList extends React.Component {
 // ...
 render() {
     return <section className="main">
       <ul className="todo-list">
         {this.getItems().map(item =>
           <TodoItem key={item.get('text')}
                     text={item.get('text')}
                     isCompleted={this.isCompleted(item)}
                     isEditing={item.get('editing')}
                     // We pass down the callback functions
                     toggleComplete={this.props.toggleComplete}
                     deleteItem={this.props.deleteItem}
                     editItem={this.props.editItem}/>
         )}
       </ul>
     </section>
   }
};

配置其他組件

現(xiàn)在,你可能對(duì)流程有些熟悉了.為了讓本文不要太長(zhǎng),我邀請(qǐng)你看看組件的代碼,
TextInput(相關(guān)提交),TodoHeader(相關(guān)提交),TodotoolsFooter(相關(guān)提交)組件.如果你有任何問(wèn)題,請(qǐng)留下評(píng)論,或著在repo的issue中留下評(píng)論.

你可能主要到一些函數(shù),例如editItem,toggleComplete諸如此類的,還沒(méi)有被定義.這些內(nèi)容將會(huì)在教程的下一部分作為Redux actions的組成來(lái)定義,所以如果遇到錯(cuò)誤,不要擔(dān)心.

包裝起來(lái)

在這篇文章中,我已經(jīng)演示了我的第一個(gè)React,Redux和Immutable webapp.我們的UI是模塊化的.完全通過(guò)測(cè)試,準(zhǔn)備和實(shí)際的app邏輯聯(lián)系起來(lái).怎么來(lái)連接?這些傻瓜組件什么都不知道,怎么讓我們可以寫出時(shí)間旅行的app?

教程的第二部分在這里.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容