](http://www.theodo.fr/uploads/blog//2016/03/course_banner.png)
幾周以前,我正在漫無(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è)
接下來(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)容.
- 模擬
document和window對(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è)條目組合而成

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

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

創(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)行我們的組件
正如我們所見(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è)組件的原因:TodoList和TodoItem將會(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),讓我們添加篩選器:all和completed:
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組件向下傳遞isCompleted和isEditingprops.
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,deleteItem和toggleComplete函數(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)提交),Todotools和Footer(相關(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?