](http://www.theodo.fr/uploads/blog//2016/03/course_banner.png)
幾周以前,我正在漫無目的的瀏覽Hacker News,讀到一篇關于Redux的頭條新聞,Redux的內容我是了解,但是另一個談到的問題javascript fatigue
(JavaScript 疲勞)已經困擾我了,所以我沒有太關心,知道讀到Redux的幾個特征.
- 強化了函數式編程,確保app行為的可預測性
- 允許app的同構,客戶端和服務端的大多數邏輯都可以共享
- 時間旅行的debugger?有可能嗎?
Redux似乎是React程序state管理的優雅方法,再者誰說的時間旅行不可能?所以我讀了文檔和一篇非常精彩的教程@teropa:A Comprehensive Guide to Test-First Development with Redux,React,and Immutable(這一篇也是我寫作的主要靈感來源).
我喜歡Redux,代碼非常優雅,debugger令人瘋狂的偉大.我的意思是-看看這個接下來的教程第一部分希望引導你理解Redux運行的原則.教程的目的僅限于(客戶端,沒有同構,是比較簡單的app)保持教程的簡明扼要.如果你想發掘的更深一點,我僅建議你閱讀上面提高的那個教程.對比版的Github repo在這里,共享代碼貼合教程的步驟.如果你對代碼或者教程有任何問題和建議,最好能留下留言.
編輯按:文章已經更新為ES2015版的句法.
APP
為了符合教程的目的,我們將建一個經典的TodoMVC,為了記錄需要,需求如下:
- 每一個todo可以激活或者完成
- 可以添加,編輯,刪除一個todo
- 可以根據它的status來過濾篩選todos
- 激活的todos的數目顯示在底部
- 完成的Todo理解可以刪除
Reudux和Immutable:使用函數編程去營救
回到幾個月前,我正在開發一個webapp包含儀表板. 隨著app的成長,我們注意到越來越多的有害的bugs,藏在代碼角落里,很難發現.類似:“如果你要導航到這一頁,點擊按鈕,然后回到主頁,喝一杯咖啡,回到這一頁然后點擊兩次,奇怪的事情發生了.”這些bug的來源要么是異步操作(side effects)或者邏輯:一個action可能在app中有意想不到的影響,這個有時候我們還發現不了.
這就是Redux之所以存在的威力:整個app的state是一個單一的數據結構,state tree.這一點意思是說:在任何時刻,展示給用戶的內容僅僅是state tree結果,這就是單一來源的真相(用戶界面的顯示內容是由state tree來決定的).每一個action接收state,應用相應的修改(例如,添加一個todo),輸出更新的state tree.更新的結果渲染展示給用戶.里面沒有模糊的異步操作,沒有變量的引用引起的不經意的修改.這個步驟使得app有了更好的結構,分離關注點,dubugging也更好用了.
Immutable是有Facebook開發的助手函數庫,提供一些工具去創建和操作immutable數據結構.盡管在Redux也不是一定要使用它,但是它通過禁止對象的修改,強化了函數式編程方法.有了immutable,當我們想更新一個對象,實際上我們修改的是一個新創建的的對象,原先的對象保持不變.
這里是“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
的一個值,map1
對象保持不變,一個新的對象map3
被創建了.
Immutable在store中被用來儲存我們的app的state tree.很快我們會看到Immutable提供了一下操作對象的簡單和有效的方法.
配置項目
聲明:一些配置有@terops的教程啟發.
注意事項:推薦使用Node.js>=4.0.0.你可以使用nvm(node version manager)來切換不同的node.js的版本.
開始配置項目:
mkdir redux-todomvc
cd redux-todomvc
npm init -y
項目的目錄結構如下:
├── dist
│ ├── bundle.js
│ └── index.html
├── node_modules
├── package.json
├── src
├── test
└── webpack.config.js
首先創建一個簡單的HTML頁面,用來運行我們的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>
有了這個文件,我們寫一個簡單的腳本文件看看包安裝的情況
src/index.js
console.log('Hello world !');
我們將會使用[Webpack]來打包成為bundle.js
文件.Webpack的特性是速度,容易配置,大部分是熱更新的.代碼的更新不需要重新加載,意味著app的state保持熱加載.
讓我們安裝webpack:
npm install —save-dev webpack@1.12.14 webpack-dev-server@1.14.1
app使用ES2015的語法,帶來一些優異的特性和一些語法糖.如果你想了解更多的ES2015內容,這個recap是一個不錯的資源.
Babel用來把ES2015的語法改變為普通的JS語法:
npm install —save-dev babel-core@6.5.2 babel-loader@6.2.4 babel-preset-es2015@6.5.0
我們將使用JSX語法編寫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'
}
};
現在添加React和React熱加載組件到項目中:
npm install --save react@0.14.7 react-dom@0.14.7
npm install --save-dev react-hot-loader@1.3.0
為了讓熱加載能運行,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
]
};
配置單元測試框架
我們將使用Mocha和Chai來進行測試工作.這兩個工具廣泛的被使用,他們的輸出內容對于測試驅動開發非常的好.Chai-immutable是一個chai插件,用來處理immutable數據結構.
npm install --save immutable@3.7.6
npm install --save-dev mocha@2.4.5 chai@3.5.0 chai-immutable@1.5.3
在我們的例子中,我們不會依賴瀏覽器為基礎的測試運行器例如Karma-替代方案是我們使用jsdom庫,它將會使用純javascirpt創建一個假DOM,這樣做讓我們的測試更加快速.
npm install —save-dev jsdom@8.0.4
我們也需要為測試寫一個啟動腳本,要考慮到下面的內容.
- 模擬
document
和window
對象,通常是由瀏覽器提供 - 通過
chia-immutable
告訴chai組件我們要使用immutable數據結構
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操作系統下似乎不能工作.
現在,如果我們運行npm run test:watch
,所有test目錄里的.js
,.jsx
文件在更新自身或者源文件的時候,將會運行mocha測試.
配置完成了:我們可以在終端中運行webpack-dev-server
,打開另一個終端,npm run test:watch
.在瀏覽器中打開localhost:8080.檢查hello world!
是否出現在終端中.
構建state tree
之前提到過,state tree是能提供app所有信息的數據結構.這個結構需要在實際開發之前經過深思熟慮,因為它將影響一些代碼的結構和交互作用.
作為示例,我們app是一個TODO list由幾個條目組合而成

每一個條目有一個文本,為了便于操作,設一個id,此外每個item有兩個狀態之一:活動或者完成:最后一個條目需要一個可編輯的狀態(當用戶想編輯的文本的時候),
所以我們需要保持下面的數據結構:

也有可能基于他們的狀態進行篩選,所以我們天劍filter
條目來獲取最終的state tree:

創建UI
首先我們把app分解為下面的組件:
-
TodoHeader
組件是創建新todo的輸入組件 -
TodoList
組件是todo的列表 -
todoItem
是一個todo -
todoInput
是編輯todo的輸入框 -
TodoTools
是顯示未完成的條目數量,過濾器和清除完成的按鈕 -
footer
是顯示信息的,沒有具體的邏輯
我們也創建TodoApp
組件組織所有的其他組件.
首次運行我們的組件
提示:運行這個版本
正如我們所見的,我們將會把所有組件放到合并成一個TodoApp
組件.所以讓我們添加組件到index.html
文件的#app
DIV中:
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')
);
因為我們在index.jsx
文件中使用JSX語法,需要在wabpack中擴展.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
現在我們編寫第一版本的TodoApp
組件,用來顯示todo項目列表:
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>
}
};
要記住兩件事情:
第一個,如果你看到的結果不太好,修復它,我們將會使用todomvc-app-css包來補充一些需要的樣式
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')
);
第二件事是:代碼似乎很復雜,這就是我們為什么要創建兩個或者多個組件的原因:TodoList
和TodoItem
將會分別關注條目列表和單個的條目.
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
組件中根據獲取的props為每一個條目顯示一個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
中添加一個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
除了允許函數式編程的樣式,我們的UI是單純的,可以使用PureRenderMixin
來提升速度,正如React 文檔:
如果你的React的組件渲染函數是”純“(換句話就是,如果使用相同的porps和state,總是會渲染出同樣的結果),你可以使用mixin在同一個案例轉給你來提升性能.
正如React文檔(我們也會在第二部分看到TodoApp
組件有額外的角色會阻止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
好了,現在我們配置好了list組件.然而我們沒有考慮添加用戶的actions和怎么添加進去組件.
props的力量
在React中,props
對象是當我們實例化一個容器(container)的時候,通過設定的attributes來設定.例如,如果我們實例化一個TodoItem
元素:
<TodoItem text={'Text of the item'} />
然后我們在TodoItem
組件中獲取this.props.text
變量:
// in TodoItem.jsx
console.log(this.props.text);
// outputs 'Text of the item'
Redux構架中強化使用props
.基礎的原理是state幾乎都存在于他的props里面.換一種說法:對于同樣一組props,兩個元素的實例應該輸出完全一樣的結果.正如之前我們看到的,整個app的state都包含在一個state tree中:意思是說,state tree 如果通過props
的方式傳遞到組件,將會完整和可預期的決定app的視覺輸出.
TodoList組件
在這一部分和接下來的一部分,我們將會了解一個測試優先的方法.
為了幫助我們測試組件,React庫提供了TestUtils
工具插件,有一下方法:
-
renderIntoDocument
,渲染組件到附加的DOM節點 -
scryRenderDOMComponentsWIthTag
,使用提供的標簽(例如li
,input
)在DOM中找到所有的組件實例. -
scryRederDOMComponentsWithClass
,同上使用的是類 -
Simulate
,模擬用戶的actions(例如 點擊,按鍵,文本輸入…)
TestUtils
插件沒有包含在react
包中,所以需要單獨安裝
npm install --save-dev react-addons-test-utils@0.14.7
我們的第一個測試將確保Todolist
組件中,如果filter
props被設置為active
,將會展示所有的活動條目:
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');
});
});
我們可以看到測試失敗了,期待的是兩個活動條目,但是實際上是三個.這是再正常不過的了,因為我們沒有編寫實際篩選的邏輯:
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>
}
};
第一個測試通過了.別停下來,讓我們添加篩選器: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');
});
});
第三個測試失敗了,因為all
篩選器更新組件的邏輯稍稍有點不同
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 [];
}
// ...
});
在這一點上,我們知道顯示在app中的條目都是經過filter
屬性過濾的.如果在瀏覽器中看看結果,沒有顯示任何條目,因為我們還沒有設置:
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
常量過濾以后,我們的條目重新出現了.
TodoItem component
現在,讓我們關注一下TodoItem
組件.首先,我們想確信TodoItem
組件真正渲染一個條目.我們也想測試一下還沒有測試的特性,就是當一個條目完成的時候,他的文本中間有一條線
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);
});
});
為了使第二個測試通過,如果條目的狀態是complete
我們使用了類complete
,它將會通過props傳遞向下傳遞.我們將會使用classnames
包來操作我們的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>
}
};
一個item在編輯的時候外觀應該看起來不一樣,由isEditing
props來包裹.
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);
});
});
為了使測試通過,我們僅僅需要更新itemClass
對象:
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>
}
};
條目左側的checkbox如果條目完成,應該標記位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有個設定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
和isEditing
props.
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>
}
};
截止目前,我們已經能夠在組件中反映出state:例如,完成的條目將會被劃線.然而一個webapp將會處理諸如點擊按鈕的操作.在Redux的模式中,這個操作也通過porps
來執行,稍稍特殊的是通過在props中傳遞回調函數來完成.通過這種方式,我們再次把UI和App的邏輯處理分離開:組件根本不需要知道按鈕點擊的操作具體是什么,僅僅是點擊觸發了一些事情.
為了描述這個原理,我們將會測試如果用戶點擊了delete按鈕(紅色X),delteItem
函數將會被調用.
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);
});
});
為了是這個測試通過,我們必須在delete按鈕聲明一個onClick
句柄,他將會調用經過props傳遞的deleteItem
函數.
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>
}
};
重要的一點:實際刪除的邏輯還沒有實施,這個將是Redux的主要作用.
在同一個model,我們可以測試和實施下面的特性:
- 點擊checkbox將會調用
toggleComplete
函數 - 雙擊條目標簽,將會調用
editItem
函數
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
函數.
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>
}
};
配置其他組件
現在,你可能對流程有些熟悉了.為了讓本文不要太長,我邀請你看看組件的代碼,
TextInput
(相關提交),TodoHeader
(相關提交),Todotools
和Footer
(相關提交)組件.如果你有任何問題,請留下評論,或著在repo的issue中留下評論.
你可能主要到一些函數,例如editItem
,toggleComplete
諸如此類的,還沒有被定義.這些內容將會在教程的下一部分作為Redux actions的組成來定義,所以如果遇到錯誤,不要擔心.
包裝起來
在這篇文章中,我已經演示了我的第一個React,Redux和Immutable webapp.我們的UI是模塊化的.完全通過測試,準備和實際的app邏輯聯系起來.怎么來連接?這些傻瓜組件什么都不知道,怎么讓我們可以寫出時間旅行的app?