React與Typescript整合


 0. Typescript

  Typescript對於前端來說可以說是越來越重要了,前端的很多項目都用Typescript進行了重構。這主要得益於Typescript有比較好的類型支持,在編碼的過程中可以很好地做一些類型推斷(主要是編輯器會有代碼提示,就很舒服)。再者Typescript的語法相較於javascript更加嚴謹,有更好的ES6的支持,這些特性使得使用ts編碼更加高效,盡量避免javascript中容易造成模糊的坑點。 我最近也正在學Typescript的一些知識,無奈本人實習所在的公司貌似暫時還不打算使用typescript,無奈只好自己琢磨,嘗試將typescript與react進行整合,花了挺長時間的,關鍵在於typescript中需要對變量進行約束。

1. react的typescript版本項目初始化

  這個沒有好說的,使用react腳手架就可以初始化react項目,默認情況下安裝的是javascript版本的項目,在腳手架命令中加上typescript的配置參數,就可以初始化typescript版本的react項目啦。

create-react-app react-todo-ts --typescript

2. react-todo-ts

  本次主要通過一個簡單的Todo應用,對於在React中整合typescript的流程有一個簡單的認識。我采用的目錄結構比較簡單(ps:按照常理,一個簡單的Todo應用其實沒有必要整的這么復雜,也沒有必要使用redux增加項目的復雜度,不過此處只是做一個演示,而在redux中也是需要使用typescript的類型聲明的,否則可能無法通過編譯)’

目錄結構如下:

做個簡單的說明:

  • components中主要存放組件

  • store中包含了一些redux相關的代碼

  • types只用存放公用的類型定義以及接口

  • index.tsx是項目默認的入口文件

package.json文件的說明:

其中有幾個聲明文件是不需要的:@types/antd,@types/redux-thunk這兩個聲明文件是不需要的,它們的類型聲明文件在antd和redux-thunk庫中已經存在。(另外,本來想使用redux-thunk模擬一下異步請求的,但在整合的過程中稍微還有點問題,因此此版本並沒有異步action的整合)。

{
  "name": "react-todo-ts",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@types/antd": "^1.0.0",
    "@types/jest": "24.0.17",
    "@types/node": "12.7.2",
    "@types/react": "16.9.2",
    "@types/react-dom": "16.8.5",
    "@types/react-redux": "^7.1.2",
    "@types/redux-thunk": "^2.1.0",
    "antd": "^3.21.4",
    "babel-plugin-import": "^1.12.0",
    "react": "^16.9.0",
    "react-dom": "^16.9.0",
    "react-redux": "^7.1.0",
    "react-scripts": "3.1.1",
    "redux": "^4.0.4",
    "redux-thunk": "^2.3.0",
    "typescript": "3.5.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
​

組件拆分說明:

3.typescript與antd整合

此處選取Header組件的代碼做說明

  1. Component類的變化

    首先,變化的是Component類,我們可以通過泛型的方式約束組件的state和props的類型

interface IHeaderProps {
  todoList:ITodo[];
  addTodoAction: typeof addTodoAction
}
​
interface IHeaderState {
  todoText:string;
}
​
class Header extends Component<IHeaderProps, IHeaderState> {
  state = {
    todoText: ''
  }
    ...
    ...
  render() {
    return (
      <Row>
        <Col span={16}>
          <Input placeholder="please input todo:" value={this.state.todoText} onChange={(e) => this.handleChange(e)} onKeyDown={(e) => this.handleKeyDown(e)}></Input>
        </Col>
        <Col span={8}>
          <Button disabled={this.state.todoText.trim() === ''} type={'primary'} style={{ marginLeft: '50%', transform: 'translateX(-50%)' }} onClick={() => this.handleAdd()}>添加</Button>
        </Col>
      </Row>
    )
  }
}

此處通過Component<IHeaderProps, IHeaderState>約束Header組件中的props和state屬性,這樣做以后,Header中的props屬性必須滿足IHeaderProps接口,state必須滿足IHeaderState接口

  1. 事件交互部分代碼的變化

    handleChange = (e:ChangeEvent<HTMLInputElement>) => {
        const { value } = e.currentTarget;
        this.setState({ todoText: value });
      }
    ​
      handleAdd = () => {
        const { todoText } = this.state;
        if(todoText.trim() === '') {
          return;
        }
        this.props.addTodoAction({
          content: todoText,
          done: false
        });
        this.setState({ todoText: '' })
      }
    ​
      handleKeyDown = (e:KeyboardEvent<HTMLInputElement>) => {
        if(e.keyCode === 13) {
          console.log(e.keyCode);
          this.handleAdd();
        }
      }
      
       render() {
        return (
          <Row>
            <Col span={16}>
              <Input placeholder="please input todo:" value={this.state.todoText} onChange={(e) => this.handleChange(e)} onKeyDown={(e) => this.handleKeyDown(e)}></Input>
            </Col>
            <Col span={8}>
              <Button disabled={this.state.todoText.trim() === ''} type={'primary'} style={{ marginLeft: '50%', transform: 'translateX(-50%)' }} onClick={() => this.handleAdd()}>添加</Button>
            </Col>
          </Row>
        )
      }

    在ts中我們定義一個函數時必須要指定函數參數的類型,當我們在定義handler函數時,需要用到event對象時,我們又該如何聲明event對象的類型呢?

    最開始的時候,我一般為了避免報錯,不管三七二十一就是一個any聲明,但這樣其實就失去了類型推斷的意義。

    在本項目中react的事件類型分為兩類:

    1. antd組件上的事件類型

      antd組件中的事件類型一般在antd的庫中都會定義,但是有些組件與原生的事件定義類型一致

    2. 原生組件上的事件類型

      原生組件的事件類型一般定義在@types/react庫中,可以從react庫中引入事件類型,一般原生事件類型的命名方式是通過(事件名稱<元素類型>)的方式來聲明的

   在vscode下,當你不確定事件類型的時候,hover上去會有函數簽名提示,就可以比較方便地確定事件類型了 


4. typescript與redux整合

主要針對todoList的操作進行

  1. 對於todo的結構定義一個接口

    export interface ITodo {
      content:String;
      done:boolean;
    }
  2. 確定對todoList的操作(添加todo,刪除todo,修改完成狀態),然后定義相關的action

    import { ADD_TODO, DELETE_TODO, CHANGE_TODO_STATUS } from './action-types';
    ​
    import { ITodo } from '../types';
    ​
    export const addTodoAction = (todo:ITodo):AddTodoAction => ({ type: ADD_TODO, todo });
    export const deleteTodoAction = (index:number):DeleteTodoAction => ({ type: DELETE_TODO, index });
    export const changeTodoStatusAction = (index:number):ChangeTodoStatusAction => ({ type: CHANGE_TODO_STATUS, index });
    ​
    ​
    export type AddTodoAction = {
      type: typeof ADD_TODO,
      todo: ITodo;
    }
    ​
    export type DeleteTodoAction = {
      type: typeof DELETE_TODO,
      index:number;
    }
    ​
    export type ChangeTodoStatusAction = {
      type: typeof CHANGE_TODO_STATUS,
      index:number;
    }
  1. 定義todoReducer,傳入todoReducer的action有三種可能,從actions.ts中將action的類型導入

    import { ADD_TODO, DELETE_TODO, CHANGE_TODO_STATUS } from './action-types';
    import { ITodo  } from '../types';
    import { AddTodoAction, DeleteTodoAction, ChangeTodoStatusAction } from './actions'
    ​
    const initTodoList:ITodo[] = [];
    ​
    export const todoReducer = (todos:ITodo[] = initTodoList, action:AddTodoAction | DeleteTodoAction | ChangeTodoStatusAction) => {
      switch(action.type) {
        case ADD_TODO:
          // 由於action傳入的類型有三種可能,沒法准確判斷action類型。但經過case判斷以后,action的類型應當是確定的,因此在此處我使用了類型斷言的方式,將action斷言為AddTodoAction(下同)
          return [(action as AddTodoAction).todo, ...todos];
        case DELETE_TODO:
          return todos.filter((todo, index) => index !== (action as DeleteTodoAction).index);
        case CHANGE_TODO_STATUS:
          const nextTodo:ITodo[] = [...todos];
          let target:ITodo = nextTodo.find((todo, index) => index === (action as ChangeTodoStatusAction).index) as ITodo;
          target.done = !target.done;
          return nextTodo;
        default:
          return todos;
      }
    }
    
  2. store中暴露store工廠函數,獲取store類型的時候可以通過ReturnType獲取

    import { todoReducer } from './reducers';
    import { combineReducers, createStore, applyMiddleware} from 'redux';
    import thunk from 'redux-thunk';
    ​
    const rootReducer = combineReducers({
      todoList: todoReducer
    })
    ​
    export type RootState = ReturnType<typeof rootReducer>
    // 向外暴露store工廠
    export function configStore() {
      return createStore(
        rootReducer,
        applyMiddleware(thunk)
      );
    }

5. react-redux整合

通過react-redux分離依賴的方式與javascript版本沒有太大的區別|

  1. 使用provider高階組件包裹App組件

    import React from 'react';
    import ReactDom from 'react-dom';
    import 'antd/dist/antd.css'
    ​
    import { Provider } from 'react-redux';
    import App from './components/app';
    import { configStore } from './store';
    ​
    const store = configStore();
    ​
    const Root = () => {
      return (
        <Provider store={store}>
          <App/>
        </Provider>
      )
    }
    ​
    ReactDom.render(
      (
        <Root/>
      ),
      document.querySelector('#root')
    );
    ​
  2. 內部組件引入,主要的不同點在於引入時需要將RootState的類型一同引入,在定義mapStateToProps函數時需要定義參數的類型。

    import React, { Component } from 'react';
    import { connect } from 'react-redux';
    import { Row, Col, Checkbox, Button, Empty, message } from 'antd';
    ​
    import { RootState } from '../../store';
    import { ITodo } from '../../types';
    import { deleteTodoAction, changeTodoStatusAction } from '../../store/actions';
    ​
    interface IListProp {
      todoList:ITodo[];
      deleteTodoAction: typeof deleteTodoAction;
      changeTodoStatusAction:typeof changeTodoStatusAction;
    }
    ​
    class List extends Component<IListProp> {
    ​
      handleChange = (index:number) =>  {
        this.props.changeTodoStatusAction(index);
      }
    ​
      handleDelete = async (index:number) => {
        await this.props.deleteTodoAction(index);
        message.success("刪除成功", 0.5);
      }
    ​
      render() {
        const { todoList } = this.props;
        return (
          <div>
          {
            todoList.length ? (
              <div>
                {
                  todoList.map((todo, index) => (
                   <Row key={index}>
                     <label>
                        <Col span={1}>
                          <Checkbox checked={todo.done} onChange={() => { this.handleChange(index) }}></Checkbox>
                        </Col>
                        <Col span={20}>
                          <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
                            {
                              todo.content
                            }
                          </span>
                        </Col>
                        <Col span={3} style={{marginTop: '10px'}}>
                          <Button type={'danger'} size={'small'} onClick={() => {this.handleDelete(index)}}>刪除</Button>
                        </Col>
                     </label>
                   </Row>
                  ))
                }
              </div>
            )
            :
            (<Empty/>)
          }
          </div>
        )
      }
    }
    ​
    const mapStateToProps = (state:RootState) => ({
      todoList: state.todoList,
    })
    ​
    export default connect(
      mapStateToProps,
      {
        deleteTodoAction,
        changeTodoStatusAction
      }
    )(List);
    ​

6. 異步action

redux本身並不支持異步action,可是在使用的時候往往是需要發送異步請求的。在整合的過程中,存在一些問題,在View層通過事件發送一個異步action后,如何獲得對應的promise狀態,然后根據promise的狀態做出相應的響應,可能還需要再看一看。

---------------------------------------------------------------------------------------

項目源碼請戳--> https://github.com/zhangzhengsmiling/React-Todo-typescript.git

---------------------------------------------------------------------------------------


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM