• React项目使用Redux


    ⒈创建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-16 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】什么是fiber?fiber解决了什么问题?从源码角度深入了解fiber运行机制与diff执行
    Linux驱动开发六.pinctl和gpio子系统2——蜂鸣器驱动
    Linux驱动开发六.gpio和pinctl子系统1——基础知识
    HTTP接口的中文乱码问题【python版】
    vs 2022(visual studio 2022下载地址)
    windows安装kafka
    python选定指定类型的文件复制到其他文件夹
    xml转txt
    数据结构算法可视化
    《架构师修炼之道》读书笔记三
  • 原文地址:https://www.cnblogs.com/fanqisoft/p/12022411.html
Copyright © 2020-2023  润新知