⒈创建React项目

  初始化一个React项目(TypeScript环境)

⒉React集成React-Router

React项目使用React-Router

⒊React集成Redux

  Redux是React中的数据状态管理库,通常来讲,它的数据流模型如图所示:

  我们先将目光放到UI层。通过UI层触发Action,Action会进入Reducer层中去更新Store中的State(应用状态),最后因为State和UI进行了绑定,UI便会自动更新。

  React Redux应用和普通React应用的区别在于,React将应用状态存储在了React组件内部,而React Redux应用则将应用状态存储在了Store中进行统一管理。

  路由状态也是应用状态的一种,所以我们可以试验,先把路由状态存入Store中,来看一下TypeScript如何使用的,先把我们的路由和Redux进行集成。

  因为Redux的库中自己带有类型定义文件,所以不需要@types/redux。

yarn add redux react-redux  react-router-redux

  接下来创建以下文件

src/store/history.js(type环境为history.ts)
import {createBrowserHistory} from 'history';

const history = createBrowserHistory();

export default history;
src/store/index.js(type环境为index.ts)
import {routerMiddleware, routerReducer} from 'react-router-redux';
import {applyMiddleware, combineReducers, createStore} from 'redux';
import history from './history'; const middleware = routerMiddleware(history); const store = createStore(
combineReducers({
router: routerReducer,
}),
applyMiddleware(middleware),
) export default store;

  最后再绑定Store到Router组件上:

src/Router.js(type环境为Router.ts)
import {routerMiddleware, routerReducer, ConnectedRouter} from 'react-router-redux';
import React from 'react';
import {Provider} from 'react-redux';
import {Route,Router} from 'react-router';
import App from './App';
import Edit from './Edit';
import store from './store';
import history from './store/history'; export default () => (
<Provider store={store}>
<ConnectedRouter history={history}>
<>
<Route exact path="/" component={App}/>
<Route path="/edit" component={Edit}/>
</>
</ConnectedRouter>
</Provider>
)

  刷新页面后,你会发现没有任何变化

  但如果我们再稍微修改一下,你可能就会看到一些不一样的地方了:

yarn add redux-devtools-extension
src/store/index.js(type环境为index.ts)
import {routerMiddleware, routerReducer} from 'react-router-redux';
import {applyMiddleware, combineReducers, createStore} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
import history from './history'; const middleware = routerMiddleware(history); const store = createStore(
combineReducers({
router: routerReducer,
}),
process.env.NODE_ENV === 'development'? composeWithDevTools(applyMiddleware(middleware)) : applyMiddleware(middleware),
) export default store;

  然后,在Chrome中安装Redux DevTools,并打开它后再刷新一次页面,你就会看到路由信息已经完全同步进入Redux Store里了。

⒋组件

  虽然我们把React项目跑起来了,但我们并没有正式的书写一个组件,我们来构思一个编辑提醒事项的组件,它应该有一个确认框和一条信息

src/Edit.js(type环境为Edit.tsx)
import React,{Component} from 'react';

class Edit extends Component{
render(){
return (
<div>
<div>
<input type="checkbox"/>
<input type="text"/>
</div>
<div>
<button>取消</button>
<button>确定</button>
</div>
</div>
)
}
} export default Edit;

  我们需要在用户点击“确定”的时候保存下当前的数据

  可能有人会说,这很简单啊,直接加上id,然后用dom操作获取值。在React的世界中,这样做是不推荐的,我们应该尽量依靠React提供的API去解决,比如用onChange函数:

src/Edit.js(type环境为Edit.tsx)
import React,{ChangeEventHandler, Component} from 'react';
import { Interface } from 'readline'; interface IState{
isChecked: Boolean,
content: string,
} class Edit extends Component{
state: IState = {
isChecked: false,
content: '',
} onCheckboxValueChange: ChangeEventHandler<HTMLInputElement> = e => {
this.setState({
isChecked: e.target.checked,
})
} onContentValueChange: ChangeEventHandler<HTMLInputElement> = e => {
this.setState({
content: e.target.value;
})
} onSave = () => {
console.log(this.state);
} render(){
return (
<div>
<div>
<input type="checkbox" checked={this.state.isChecked} onChange={this.onCheckboxValueChange}/>
<input type="text" value={this.state.content} onChange={this.onContentValueChange}/>
</div>
<div>
<button>取消</button>
<button onClick={this.onSave}>确定</button>
</div>
</div>
)
}
} export default Edit;

  这样就完成了一个可以工作的组件,初步保证了数据在内部的存储,也可以在onSave中扩展网络请求API。

  但如果我文字写到一半,没保存,只是刷新一下页面,那所有的数据就没有了。接下来,我们可以看一下Redux全局统筹的魔力。

⒌Redux组件

  一个Redux组件需要触发Action以及根据Action操作数据的Reducer,同时,我们还需要增加一些全局的类型定义。

  首先,我们需要将redux-tools里面所看到的Redux Store的类型给定义出来:

src/typings/store.d.ts
declare interface IDraftState{
isChecked: boolean,
content: string,
} declare interface IStoreState{
route:{
location: Location
}
draft: IDraftState
}

  然后是Action

src/action/index.ts
export const EDIT_DRAFT_ACTION_TYPE = 'draft/edit';

export const editDraftAction = (payload: IDraftState) => ({
type: EDIT_DRAFT_ACTION_TYPE,
payload,
})

  再然后是创建draft的Reducer:

src/reducer/draft.ts
import {editDraftAction,EDIT_DRAFT_ACTION_TYPE} from '../action';
const defaultState: IDraftState = {
isChecked: false,
content: '',
} export default (state = defaultState,action: ReturnType<typeof editDraftAction>) => {
switch(action.type){
case EDIT_DRAFT_ACTION_TYPE:{
return action.payload
}
default:{
return state
}
}
}

  这里需要把Reducer文件引入到Store中:

src/reducer/index.ts
import draft from './draft';
export default{
draft,
}
src/store/index.ts
import {routerMiddleware, routerReducer} from 'react-router-redux';
import {applyMiddleware, combineReducers, createStore} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
import reducers from '../reducer';
import history from './history'; const middleware = routerMiddleware(history); const store = createStore(
combineReducers({
...reducers,
router: routerReducer,
}),
process.env.NODE_ENV === 'development'? composeWithDevTools(applyMiddleware(middleware)) : applyMiddleware(middleware),
) export default store;

  准备工作完成后,就可以将组件与Redux进行关联了

src/Edit.ts
import React,{ChangeEventHandler, Component} from 'react';
import {connect} from 'react-redux';
import { editDraftAction } from './action/index'; const mapStateToProps = (storeState: IStoreState) => ({
draft: storeState.draft,
}) type IStateProps = ReturnType<typeof mapStateToProps> const mapDispatchToProps = {
editDraftAction,
} type IDispatchProps = typeof mapDispatchToProps; type IProps = IStateProps & IDispatchProps; class Edit extends Component<IProps>{ onCheckboxValueChange: ChangeEventHandler<HTMLInputElement> = e => {
this.props.editDraftAction({
...this.props.draft,
isChecked:e.target.checked,
})
} onContentValueChange: ChangeEventHandler<HTMLInputElement> = e => {
this.props.editDraftAction({
...this.props.draft,
content:e.target.value,
})
} onSave = () => {
console.log(this.state);
} render(){
return (
<div>
<div>
<input type="checkbox" checked={this.props.draft.isChecked} onChange={this.onCheckboxValueChange}/>
<input type="text" value={this.props.draft.content} onChange={this.onContentValueChange}/>
</div>
<div>
<button>取消</button>
<button onClick={this.onSave}>确定</button>
</div>
</div>
)
}
} export default connect<IStateProps,IDispatchProps>(mapStateToProps,mapDispatchToProps)(Edit);

  这个时候我在编辑框中输入文字或者修改CheckBox的状态,都会同步进入Store里面。

  但是一刷新页面,数据还是没有。接下来我们来解决这个问题。

⒍Redux Persist

  既然我们的全部数据已经存入了Store中,那么只需要为Store增加一个缓存层就完工了,因此介绍Redux Persist。

   Redux Persist的架构如图所示,这也是一个Store内部的微观结构图

  如果有一个Action进入的话,它会先穿过最底下的中间件,再穿过Reducer,最后改变State。

  但在加入Redux Persist后,Redux Persist会对改变后的State进行一次存操作,默认是写入LocalStorge。当然这个存储位置是可以改变的。

  另外在初始化Redux Store的时候,Redux Persist还会默认对LocalStorge进行一次读取操作,这样就能保证网页数据的持久性了。

  现在,先看一下如何集成redux-persist吧:

yarn add redux-persist
src/store/index.ts
import {routerMiddleware, routerReducer} from 'react-router-redux';
import {applyMiddleware, combineReducers, createStore} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
import {persistReducer,persistStore,PersistConfig} from 'redux-persist';
import storage from 'redux-persist/es/storage';
import reducers from '../reducer';
import history from './history'; const middleware = routerMiddleware(history); const rootReducer = combineReducers({
...reducers,
router: routerReducer,
}) const persistConfig: PersistConfig = {
key: 'root',
storage,
whitelist: ['draft'],
} const persistedReducer: typeof rootReducer = persistedReducer(PersistConfig,rootReducer); const store = createStore(
persistedReducer,
process.env.NODE_ENV === 'development'? composeWithDevTools(applyMiddleware(middleware)) : applyMiddleware(middleware),
) const persistor = persistStore(store); export{
store,
persistor,
}
src/Router.tsx
import {routerMiddleware, routerReducer, ConnectedRouter} from 'react-router-redux';
import React from 'react';
import {Provider} from 'react-redux';
import {Route,Router} from 'react-router';
import {PersistGate} from 'redux-persist/integration/react';
import App from './App';
import Edit from './Edit';
import store, { persistor } from './store';
import history from './store/history'; export default () => (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ConnectedRouter history={history}>
<>
<Route exact path="/" component={App}/>
<Route path="/edit" component={Edit}/>
</>
</ConnectedRouter>
</PersistGate>
</Provider>
)

  在输入文字,再刷新,你就会发现数据能从缓存中读出来了。这样,我们就利用了Redux实现了数据持久化,接下来我们只需要扩展它的网络层即可。

⒎处理网络请求

  接下来只需在Redux上做文章,就可以轻松兼容网络层了,由于组件只负责发出Action,所以后面的操作完全跟组件解耦。

  组件在保存的时候发出Save的Action,然后将草稿清空:

src/action/index.ts
export const EDIT_DRAFT_ACTION_TYPE = 'draft/edit';
export const editDraftAction = (payload: IDraftState) => ({
type: EDIT_DRAFT_ACTION_TYPE,
payload,
}) export const SAVE_DRAFT_ACTION_TYPE = 'draft/save';
export const saveDraftAction = () => ({
type: SAVE_DRAFT_ACTION_TYPE,
}) export const RESET_DRAFT_ACTION_TYPE = 'draft/reset';
export const resetDraftAction = () => ({
type:RESET_DRAFT_ACTION_TYPE
})
src/reducer/draft.ts
import {editDraftAction,resetDraftAction,EDIT_DRAFT_ACTION_TYPE} from '../action';
import {RESET_DRAFT_ACTION_TYPE} from '../action/index';
const defaultState: IDraftState = {
isChecked: false,
content: '',
} type actionType = ReturnType<typeof editDraftAction> | ReturnType<typeof resetDraftAction> export default (state = defaultState,action: actionType) => {
switch(action.type){
case EDIT_DRAFT_ACTION_TYPE:{
return (action as ReturnType<typeof editDraftAction>).payload
}
case RESET_DRAFT_ACTION_TYPE:{
return defaultStatus
}
default:{
return state
}
}
}
src/Edit.tsx
import React,{ChangeEventHandler, Component} from 'react';
import {connect} from 'react-redux';
import { editDraftAction,saveDraftAction } from './action/index'; const mapStateToProps = (storeState: IStoreState) => ({
draft: storeState.draft,
}) type IStateProps = ReturnType<typeof mapStateToProps> const mapDispatchToProps = {
editDraftAction,
saveDraftAction,
} type IDispatchProps = typeof mapDispatchToProps; type IProps = IStateProps & IDispatchProps; class Edit extends Component<IProps>{ onCheckboxValueChange: ChangeEventHandler<HTMLInputElement> = e => {
this.props.editDraftAction({
...this.props.draft,
isChecked:e.target.checked,
})
} onContentValueChange: ChangeEventHandler<HTMLInputElement> = e => {
this.props.editDraftAction({
...this.props.draft,
content:e.target.value,
})
} onSave = () => {
this.props.saveDraftAction()
} render(){
return (
<div>
<div>
<input type="checkbox" checked={this.props.draft.isChecked} onChange={this.onCheckboxValueChange}/>
<input type="text" value={this.props.draft.content} onChange={this.onContentValueChange}/>
</div>
<div>
<button>取消</button>
<button onClick={this.onSave}>确定</button>
</div>
</div>
)
}
} export default connect<IStateProps,IDispatchProps>(mapStateToProps,mapDispatchToProps)(Edit);

  网络请求的过程是异步的,我们需要引入一个库来处理异步Action,在这里我们选择了Redux Thunk来进行处理,如下图所示:

  在Redux Thunk中可以获取整个Store的State,同时分发一个新的Action出去:

yarn add redux-thunk
src/store/index.ts
import {routerMiddleware, routerReducer} from 'react-router-redux';
import {applyMiddleware, combineReducers, createStore} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
import {persistReducer,persistStore,PersistConfig} from 'redux-persist';
import storage from 'redux-persist/es/storage';
import thunk from 'redux-thunk';
import reducers from '../reducer';
import history from './history'; const middleware = [thunk,routerMiddleware(history)]; const rootReducer = combineReducers({
...reducers,
router: routerReducer,
}) const persistConfig: PersistConfig = {
key: 'root',
storage,
whitelist: ['draft'],
} const persistedReducer: typeof rootReducer = persistedReducer(PersistConfig,rootReducer); const store = createStore(
persistedReducer,
process.env.NODE_ENV === 'development'? composeWithDevTools(applyMiddleware(...middleware)) : applyMiddleware(...middleware),
) const persistor = persistStore(store); export{
store,
persistor,
}

  由于当前域是localhost:3000,而API服务器是运行在localhost:3001,所以我们还需要配置一下代理:

package.json(部分)
  "proxy": {
"/work-items":{
"target": "http://localhost:3001"
}
},

  准备工作都完成了,接下来就开始改造 saveDraftAction:

import {ThunkAction} from 'redux-thunk';

const headers = new Headers({
'content-type':'application/json'
}) export const saveDraftAction = (): ThunkAction<void,IStoreState,undefined> => {
(dispatch,getState) => {
const draft = getState().draft
fetch('http://localhost:3000/work-items',{
headers,
method:'post',
body:JSON.stringify(draft)
}).then(() => {
dispatch(resetDraftAction())
})
}
}

  saveDraftAction作为一个异步Action,是不用写入Reducer里去改变State,在完成自己的工作后,再去触发别的Action就行了。

  在这里,我们还希望保存成功后再回到首页,那么只需要调用react-router已经写好的push action就好了:

import {push} from 'react-router-redux';
import {ThunkAction} from 'redux-thunk'; const headers = new Headers({
'content-type':'application/json'
}) export const saveDraftAction = (): ThunkAction<void,IStoreState,undefined> => {
(dispatch,getState) => {
const draft = getState().draft
fetch('http://localhost:3000/work-items',{
headers,
method:'post',
body:JSON.stringify(draft)
}).then(() => {
dispatch(push('/'))
dispatch(resetDraftAction())
})
}
}

  这样,UI和业务就完全进行解耦了,仅仅靠Action维持联系。

⒏实现列表

  既然可以创建提醒事项,那么接下来就可以正式渲染列表了。

  8.1实现列表页

  我们先来思考一下,完成列表页有哪些工作,我们需要获取数据,数据会存放到Store里去,然后组件连接Store取值,那么就先需要在store.d.ts中添加新的list的定义,然后写Action、Reducer,然后再是组件:

src/typings/store.d.ts
declare interface IDraftState{
isChecked: boolean,
content: string,
} declare type IList = IDraftState[] declare interface IStoreState{
route:{
location: Location
}
draft: IDraftState
list:IList
}
src/action/index.ts
export const fetchList = (): ThunkAction<void,IStoreState,undefined> =>
async(dispatch) => {
const response = await fetch('http://localhost:3000/work-item',{headers})
const data = await response.json()
dispatch(fetchListSuccess(data))
}
export const FETCH_LIST_SUCCESS_TYPE = 'list/success'
export const fetchListSuccess = (payload: IList) => ({
type: FETCH_LIST_SUCCESS_TYPE,
payload,
})
src/reducer/list.ts
import {fetchListSuccess,FETCH_LIST_SUCCESS_TYPE} from '../action/index';

const defaultState:IList = []

type actionType = ReturnType<typeof fetchListSuccess>

export default (state = defaultStatus,action: actionType) => {
switch(action.type){
case FETCH_LIST_SUCCESS_TYPE:{
return action.payload
}
default:{
return state
}
}
}

  最后再修改一下App.tsx的样式

src/App.tsx
import React,{Component} from 'react';
import './App.css';
import logo from './logo.svg';
import {fetchList} from './action/index';
import {connect} from 'react-redux'; const mapStateToProps = (storeState: IStoreState) => ({
list: storeState.list,
}) type IStoreState = ReturnType<typeof mapStateToProps> const mapDispatchToProps = {
fetchList,
} type IDispatchProps = typeof mapDispatchToProps type IProps = IStateProps & IDispatchProps class App extends Component<IProps>{
componentDidMount(){
this.props.fetchList()
} render(){
return (
<div>
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to Check List</h1>
</header>
<ul>
{this.props.list.map((item) => {
<li>{item.isChecked ? '完成' : '未完成'} - {item.content}</li>
})}
</ul>
</div>
)
}
} extends default connect<IStateProps,IDispatchProps>(mapStateToProps,mapDispatchToProps)(App)

  这里有个新的问题,如果我想点击列表的某一项直接进行编辑呢?

  8.2复用编辑组件

  因为后端代码也是我们自己编写的,所以我们知道,创建一个数据的时候,它是没有主键ID的,而更新删除的时候是有主键ID的。所以我们可以通过是否有主键ID来区分路由,从两个不同的路由渲染同一个组件,然后再在内部做一些业务上的区分。

  那么,根据主键ID的设定,我们需要先更新一下store.d.ts:

src//typings/store.d.ts
declare interface IDraftState{
id?: number,
isChecked: boolean,
content: string,
} declare type IList = IDraftState[] declare interface IStoreState{
route:{
location: Location
}
draft: IDraftState
list:IList
}

  然后更新路由

src/Router.tsx
import {routerMiddleware, routerReducer, ConnectedRouter} from 'react-router-redux';
import React from 'react';
import {Provider} from 'react-redux';
import {Route,Router} from 'react-router';
import {PersistGate} from 'redux-persist/integration/react';
import App from './App';
import Edit from './Edit';
import store, { persistor } from './store';
import history from './store/history'; export default () => (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ConnectedRouter history={history}>
<>
<Route exact path="/" component={App}/>
<Route path="/edit/new" component={Edit}/>
<Route path="/edit/:id" component={Edit}/>
</>
</ConnectedRouter>
</PersistGate>
</Provider>
)

  为单个item添加点击事件:

src/App.tsx
import React,{Component} from 'react';
import './App.css';
import logo from './logo.svg';
import {fetchList} from './action/index';
import {connect} from 'react-redux';
import {push} from 'react-router-redux'; const mapStateToProps = (storeState: IStoreState) => ({
list: storeState.list,
}) type IStoreState = ReturnType<typeof mapStateToProps> const mapDispatchToProps = {
fetchList,
push,
} type IDispatchProps = typeof mapDispatchToProps type IProps = IStateProps & IDispatchProps class App extends Component<IProps>{ componentDidMount(){
this.props.fetchList()
} navigateToEditor = (id?: number) => () => this.props.push(`/edit/${id}`) render(){
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to Check List</h1>
</header>
<ul>
{this.props.list.map((item) => {
<li onClick={this.navigateToEditor(item.id)}>{item.isChecked ? '完成' : '未完成'} - {item.content}</li>
})}
</ul>
</div>
)
}
} extends default connect<IStateProps,IDispatchProps>(mapStateToProps,mapDispatchToProps)(App)

  但跳转过去后,发现内容都是空的

  那么我们能不能直接去读本地的存储呢?答案是可以,但不能完全只读本地存储,因为如果直接访问这个地址,就没有本地存储可读了。

  所以最稳妥的方法是发一次API拉取一次数据。我们要考虑如何最省力地去设计,以便减少修改代码的工作

  毫无疑问,凡是进入编辑页面,都是我们希望能保存的。所以这里的编辑也不例外,我们的draft需要改造成一个字典,那么,创建的内容可以放在一个特殊关键字里面。这样修改的量可以达到最小。

src/typings/store.d.ts
declare interface IDraftState{
id?: number,
isChecked: boolean,
content: string,
} declare type IList = IDraftState[] declare interface IStoreState{
route:{
location: Location
}
draft: {
[id:number] :IDraftState
}
list:IList
}
src/action/index.ts
import {push} from 'react-router-redux';
import { ThunkAction } from "redux-thunk";
import {NEW_DRAFT_SYMBOL} from '../reducer/draft'; export const EDIT_DRAFT_ACTION_TYPE = 'draft/edit';
export const editDraftAction = (payload: IDraftState) => ({
type: EDIT_DRAFT_ACTION_TYPE,
payload,
}) const headers = new Headers({
'content-type':'application/json'
}) export const saveDraftAction = (id:number): ThunkAction<void,IStoreState,undefined> => {
(dispatch,getState) => {
const draft = getState().draft[id]
if(id === NEW_DRAFT_SYMBOL){
fetch('http://localhost:3000/work-items',{
headers,
method:'post',
body:JSON.stringify(draft)
}).then(() => {
dispatch(push('/'))
dispatch(resetDraftAction(id))
})
}else{
fetch(`http://localhost:3000/work-items/${id}`,{
headers,
method:'put',
body:JSON.stringify(draft)
}).then(() => {
dispatch(push('/'))
dispatch(resetDraftAction(id))
})
}
}
} export const SAVE_DRAFT_ACTION_TYPE = 'draft/save';
export const saveDraftAction = () => ({
type: SAVE_DRAFT_ACTION_TYPE,
}) export const RESET_DRAFT_ACTION_TYPE = 'draft/reset';
export const resetDraftAction = (id:number) => ({
type:RESET_DRAFT_ACTION_TYPE,
payload:{
id,
}
}) export const fetchList = (): ThunkAction<void,IStoreState,undefined> =>
async(dispatch) => {
const response = await fetch('http://localhost:3000/work-item',{headers})
const data = await response.json()
dispatch(fetchListSuccess(data))
}
export const FETCH_LIST_SUCCESS_TYPE = 'list/success'
export const fetchListSuccess = (payload: IList) => ({
type: FETCH_LIST_SUCCESS_TYPE,
payload,
}) export const fetchItemById = (id:number): ThunkAction<void,IStoreState,undefined> =>
async(dispatch) => {
const response = await fetch(`http://localhost:3000/work-item/${id}`,{headers})
const data =await response.json();
dispatch(editDraftAction(data))
}
src/reducer/draft.ts
import {editDraftAction,resetDraftAction,EDIT_DRAFT_ACTION_TYPE} from '../action';
import {RESET_DRAFT_ACTION_TYPE} from '../action/index';
const defaultState: IDraftState = {
isChecked: false,
content: '',
} import {editDraftAction,resetDraftAction,EDIT_DRAFT_ACTION_TYPE} from '../action';
import {RESET_DRAFT_ACTION_TYPE} from '../action/index'; export const NEW_DRAFT_SYMBOL = -1
const defaultState: IDraftState = {
id: NEW_DRAFT_SYMBOL,
isChecked: false,
content: '',
}
type actionType = ReturnType<typeof editDraftAction> | ReturnType<typeof resetDraftAction> export default (state = defaultState,action: actionType) => {
switch(action.type){
case EDIT_DRAFT_ACTION_TYPE:{
return{
...state,
[action.payload.id]: action.payload
}
}
case RESET_DRAFT_ACTION_TYPE:{
return {
...state,
[action.payload.id]: defaultState,
}
}
default:{
return state
}
}
}
src/Edit.ts
import React,{ChangeEventHandler, Component} from 'react';
import {connect} from 'react-redux';
import {RouteComponentProps} from 'react-router';
import { editDraftAction, fetchItemById,saveDraftAction } from './action/index';
import {NEW_DRAFT_SYMBOL} from './reducer/draft'; const mapStateToProps = (storeState: IStoreState) => ({
draft: storeState.draft,
}) type IStateProps = ReturnType<typeof mapStateToProps> const mapDispatchToProps = {
editDraftAction,
saveDraftAction,
fetchItemById,
} type IDispatchProps = typeof mapDispatchToProps; type IProps = IStateProps & IDispatchProps & RouteComponentProps<{id?:number}>; class Edit extends Component<IProps>{ get draft(){
return this.props.draft[this.props.match.params.id || NEW_DRAFT_SYMBOL]
} componentDidMount(){
if(this.props.match.params.id){
this.props.fetchItemById(this.props.match.params.id)
}
} onCheckboxValueChange: ChangeEventHandler<HTMLInputElement> = e => {
this.props.editDraftAction({
...this.props.draft,
isChecked:e.target.checked,
})
} onContentValueChange: ChangeEventHandler<HTMLInputElement> = e => {
this.props.editDraftAction({
...this.props.draft,
content:e.target.value,
})
} onSave = () => {
this.props.saveDraftAction(this.draft.id)
} render(){
const draft = this.draft
if(!draft){
return null
}
return (
<div>
<div>
<input type="checkbox" checked={draft.isChecked} onChange={this.onCheckboxValueChange}/>
<input type="text" value={draft.content} onChange={this.onContentValueChange}/>
</div>
<div>
<button>取消</button>
<button onClick={this.onSave}>确定</button>
</div>
</div>
)
}
} export default connect<IStateProps,IDispatchProps>(mapStateToProps,mapDispatchToProps)(Edit);

  这样,我们就能最大限度的复用了组件

⒐测试

  在React的开发中,测试是必不可少的一环。

  9.1配置Jest

  先安装依赖

yarn add ts-jest @types/ts-jest sinon @types/sinon enzyme @types/enzyme enzyme-adapter-react- jest-enzyme jest-fetch-mock raf

  在package.json中添加配置

  "jest":{
"setupFiles":[
"<rootDir>/_mocks_/setupJest.js"
],
"setupTestFrameworkScriptFile":"./node_modules/jest-enzyme/lib/index.js",
"moduleNameMapper":{
"\\.(css|less)$":"<rootDir>/_mocks_/styleMock.js",
"\\.(gif|ttf|eot|svg)$":"<rootDir>/_mocks_/fileMock.js"
},
"unmockedModulePathPatterns":[
"react",
"enzyme",
"jest-enzyme"
],
"transform":{
"^.+\\.tsx?$":"ts-jest"
},
"testRegex":"(/_tests_/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions":[
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
},

  这样,配置就完成了

  在根目录下新建这三个文件

fileMock.js
module.exports = 'test-file-stub';
styleMock.js
module.exports = {};
setupJest.js
//React also depends on requestAnimationFrame(even in test environments)
//You can use the raf package to shim requestAnimationFrame require('raf/polyfill') //mock fetct
global.fetch = require('jest-fetch-mock') const Adapter = require('enzyme-adapter-react-16')
require('enzyme').configure({adapter:new Adapter()});

  一切准备就绪后,就可以开始了

  9.2组件的测试

  以App.tsx为例进行测试,一般进行组件测试的话,不需要去测试已经连接了Store的组件,那没有意义,只需要测试组件本身即可,所以先将App组件进行export操作:

export class App extends Component<IProps>{

  然后新建一个文件名为App.test.tsx,模拟组件渲染

import {shallow} from 'enzyme';
import React from 'react';
import {App} from './App'; describe('App Component Test Suits',() => {
it('renders<App /> components with empty array' () => {
const fetchList = jest.fn()
const push = jest.fn()
const wrapper = shallow(<App list={[]} fetchList={fetchList} push={push}/>)
wrapper.rander()
})
})

  然后运行

yarn jest --watch

  会根据文件的变化实时重跑测试

  我们再给一个有数据的数据试一下

import {shallow} from 'enzyme';
import React from 'react';
import {App} from './App'; const isChecked = () => Math.random() >= 0.5 describe('App Component Test Suits',() => {
it('renders<App /> components with empty array' () => {
const fetchList = jest.fn()
const push = jest.fn()
const wrapper = shallow(<App list={[]} fetchList={fetchList} push={push}/>)
wrapper.rander()
}) it('renders <App /> components with array',() => {
const fetchList = jest.fn()
const push = jest.fn()
const list = [
{id:Math.random(),content:Math.random.toString(),isChecked:isChecked()},
{id:Math.random(),content:Math.random.toString(),isChecked:isChecked()},
]
const wrapper = shallow(<App list={[]} fetchList={fetchList} push={push}/>)
wrapper.rander()
})
})

  但仅仅渲染成功还不能满足我们的要求,我们希望列表渲染的文字也能符合要求,所以可以稍微再扩展一下:

    wrapper.render()
wrapper.find('li').forEach((element,index) => {
const item = list[index]
expect(element.text()).toBe(`${item.isChecked?'完成':'未完成'} - ${item.content}`)
})

  接下来需要测试一下点击事件:

  it('li should be call by clicked', () => {
const fetchList = jest.fn()
const push = jest.fn()
const list = [
{id:Math.random(),content:Math.random.toString(),isChecked:isChecked()},
{id:Math.random(),content:Math.random.toString(),isChecked:isChecked()},
]
const wrapper = shallow(<App list={[]} fetchList={fetchList} push={push}/>)
wrapper.render()
wrapper.find('li').first().simulate('click')
expect(push.mock.calls.length).toBe(1)
})

  最后再测试一下生命周期

  it('fetchList should be call on did mount', () => {
const fetchList = jest.fn()
const push = jest.fn()
mount(<App list={[]} fetchList={fetchList} push={push}/>)
expect(fetchList.mock.calls.length).toBe(1)
})

  由于异步请求都由redux-thunk接管了,所以组件的测试就显得非常容易了。

  9.3Action的测试

  同样,我们到Action的目录下新建文件action.test.ts

  先测试一个普通的Action:

import {editDraftAction} from '.';

const isChecked = () => Math.random() >= 0.5

describe('Action Test Suits',() => {
it('test editDraftAction',() => {
const payload = {id:Math.random(),content:Math.random().toString(),isChecked:isChecked()}
expect(editDraftAction(payload)).toEqual({payload,type:'draft/edit'})
})
})

  一个普通的Action就是一个普通的函数,非常容易测试。

  但是redux-thunk的异步Action就不容易测试了,需要我们引入一个假的Store来模拟Action在Atore里的情况。

yarn add redux-mock-store @types/redux-mock-store
import fetch from 'jest-fetch-mock';
import createMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {editDraftAction} from '.';
import {fetchList,fetchListSuccess} from './index'; const isChecked = () => Math.random() >= 0.5 const middlewares = [thunk] const mockStore = createMockStore(middlewares) describe('Action Test Suits',() => { beforeEach(() => {
fetch.resetMocks()
}) it('test editDraftAction',() => {
const payload = {id:Math.random(),content:Math.random().toString(),isChecked:isChecked()}
expect(editDraftAction(payload)).toEqual({payload,type:'draft/edit'})
}) it('test fetchLisst',async () => {
const response = [{id:Math.random(),content:Math.random().toString(),isChecked:isChecked()}]
fetch.mockResponseOnce(JSON.stringify(response))
const store = mockStore({})
//tslint:disable-next-line:no-any
await store.dispatch(fetchList() as any)
expect(store.getActions()).toEqual([fetchListSuccess(response)])
})
})

  整个测试相对复杂一些,需要考虑异步的次数,还有从mock的Store中进行Action操作。

  9.4Reducer的操作

  在list.ts旁新建list.test.ts文件

  Reducer本身也是一个函数,所以测试方法与Action类似:

import {fetchListSuccess} from '../action';
import listReducer from './list'; type ActionType = ReturnType<typeof fetchListSuccess> describe('List Reducer Test Suits',() => {
it('test reducer without any action', () => {
expect(listReducer(undefined,{} as ActionType)).toEqual([])
})
})

  这里演示了一个传递空Action进去之后的输出,可以仿照上面的方法测试其他的Action情况。由于把架构进行了合理拆分,才使得React的测试非常容易编写。

  本文中,我们集成了路由,嵌入了Redux,为Redux的Store编写了声明文件,同时编写了从页面组件到Action,再到Reducer的全面测试。

  

React项目使用Redux的更多相关文章

  1. 如何在非 React 项目中使用 Redux

    本文作者:胡子大哈 原文链接:https://scriptoj.com/topic/178/如何在非-react-项目中使用-redux 转载请注明出处,保留原文链接和作者信息. 目录 1.前言 2. ...

  2. 如何优雅地在React项目中使用Redux

    前言 或许你当前的项目还没有到应用Redux的程度,但提前了解一下也没有坏处,本文不会安利大家使用Redux 概念 首先我们会用到哪些框架和工具呢? React UI框架 Redux 状态管理工具,与 ...

  3. 在react项目当中使用redux

    如果需要在你的react项目当中使用状态管理模式的话,需要引入redux和react-redux两个插件,redux提供基本的功能,react-redux提供将redux注入react的方法. imp ...

  4. 优雅的在React项目中使用Redux

    概念 首先我们会用到哪些框架和工具呢? React UI框架 Redux 状态管理工具,与React没有任何关系,其他UI框架也可以使用Redux react-redux React插件,作用:方便在 ...

  5. redux在react项目中的应用

    今天想跟大家分享一下redux在react项目中的简单使用 1 1.redux使用相关的安装 yarn add redux yarn add react-redux(连接react和redux) 2. ...

  6. react项目中引入了redux后js控制路由跳转方案

    如果你的项目中并没有用到redux,那本文你可以忽略 问题引入 纯粹的单页面react应用中,通过this.props.history.push('/list')就可以进行路由跳转,但是加上了redu ...

  7. react全家桶从0搭建一个完整的react项目(react-router4、redux、redux-saga)

    react全家桶从0到1(最新) 本文从零开始,逐步讲解如何用react全家桶搭建一个完整的react项目.文中针对react.webpack.babel.react-route.redux.redu ...

  8. 在react项目中使用redux-thunk,react-redux,redux;使用总结

    先看是什么,再看怎么用: redux-thunk是一个redux的中间件,用来处理redux中的复杂逻辑,比如异步请求: redux-thunk中间件可以让action创建函数先不返回一个action ...

  9. React,关于redux的一点小见解

    最近项目做多页面应用使用到了,react + webpack + redux + antd去构建多页面的应用,本地开发用express去模拟服务端程序(个人觉得可以换成dva).所以在这里吐槽一下我自 ...

随机推荐

  1. Class T泛型和通配符泛型的区别

    平时看java源代码的时候,如果碰到泛型的话,我想? T K V E这些是经常出现的,但是有时想不起来代表什么意思,今天整理下: ? 表示不确定的java类型. T 表示java类型. K V 分别代 ...

  2. Java 基础:Queue

    下面几个关于Java里queue的说法哪些是正确的()? 正确答案: A C A.LinkedBlockingQueue是一个可选有界队列,不允许null值 B.PriorityQueue,Linke ...

  3. [提权]sudo提权复现(CVE-2019-14287)

    2019年10月14日, sudo 官方在发布了 CVE-2019-14287 的漏洞预警. 0x00 简介 sudo 是所有 unix操作系统(BSD, MacOS, GNU/Linux) 基本集成 ...

  4. load、loads和 dump、dumps的区别

    相同点 load 和loads 都是实现“反序列化” 区别 1.loadsloads针对内存对象loads: 将 字符串 转换为 字典 # 这是一个字符串'{"b": 2, &qu ...

  5. JVM Java字节码方法表与属性

    方法表 1.methods_count  method_info,前三个字段和field_info一样 2.方法的属性结构 方法中的每个属性都是一个attribut_info结构 JVM定义了部分at ...

  6. 即时通信系统IM--XMPP

    即时通讯(Instant Messaging)是目前Internet上最为流行的通讯方式,各种各样的即时通讯软件也层出不穷:服务提供商也提供了越来越丰富的通讯服务功能. 不容置疑,Internet已经 ...

  7. gitlab 默认端口修改文件

    vim /var/opt/gitlab/nginx/conf/gitlab-http.conf listen *:80;

  8. Unix下可用的五种 I/O 模型

    介绍 当TCP客户端同时处理两个输入时:标准输入和TCP套接字,当客户端fgets(在标准输入上)被阻塞并且服务器进程被终止时,我们遇到了问题.服务器TCP正确地将FIN发送到客户端TCP,但由于客户 ...

  9. 001 安装mysql

    在安装docker之后,安装一个mysql的容器试试手.可以参考违章: URL: https://www.cnblogs.com/limingxie/p/8655457.html

  10. scipy.fftpack fft

    from scipy.fftpack import fft SciPy提供fftpack模块,可让用户计算快速傅立叶变换 例子: >>> a = np.arange(1,5) > ...