0. Typescript Typescript對於前端來說可以說是越來越重要了,前端的很多項目都用Typescript進行了重構。這主要得益於Typescript有比較好的類型支持,在編碼的過程中可以很好地做一些類型推斷(主要是編輯器會有代碼提示,就很舒服)。再者Typescript的語法相較於j ...
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組件的代碼做說明
-
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介面
-
事件交互部分代碼的變化
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的事件類型分為兩類:
-
antd組件上的事件類型
antd組件中的事件類型一般在antd的庫中都會定義,但是有些組件與原生的事件定義類型一致
-
原生組件上的事件類型
原生組件的事件類型一般定義在@types/react庫中,可以從react庫中引入事件類型,一般原生事件類型的命名方式是通過(事件名稱<元素類型>)的方式來聲明的
-
在vscode下,當你不確定事件類型的時候,hover上去會有函數簽名提示,就可以比較方便地確定事件類型了
4. typescript與redux整合
主要針對todoList的操作進行
-
對於todo的結構定義一個介面
export interface ITodo { content:String; done:boolean; }
-
確定對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; }
-
定義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; } }
-
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版本沒有太大的區別|
-
使用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') );
-
內部組件引入,主要的不同點在於引入時需要將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