TypeScript 作為 JavaScript 的超集,強類型、類型推斷等優點簡直棒棒噠。對項目來說,typescript 可以讓你的 app 更穩定、易閱讀、好管理。當你在你的項目中引入 TypeScript ,你將能節省大量的時間以及精力,來開發維護你的項目。照目前趨勢來看,前端各大框架或庫都慢慢趨於倒向 TypeScript 。希望你也可以考慮一下。
這裡本文主要做了對 TypeScript + React + Redux 的集成總結,並輸出了一個 TodoList Demo 避免紙上談兵,其中遇到的問題也是蠻多的,在倉庫中都有說明,歡迎大家 code review 。有任何建議可以提出。
首先,我們運行如下指令,創建一個新的基於 TypeScript 的 React App,當然你也可以參照官方文檔把舊項目遷移到 TypeScript 上來。
$ npx create-react-app app_name --typescript
參考:Adding TypeScript · Create React App?facebook.github.io
完成之後,你會看到一個基本的 React with TypeScript 項目結構,其中包含了以下幾點變化(對比 JavaScript ):
接觸過 TypeScript 的應該很熟悉,這是作為你項目的 TypeScript 編譯選項配置。
2. 文件擴展名變化:.js變成了.ts、.jsx變成了.tsx
.js
.ts
.jsx
.tsx
這擴展名變化相信大家都懂,這裡要說明的是,為了使 TypeScript 支持 JSX ,除了 tsx 作為文件擴展名外,還需要配置 JSX 工作模式:preserve 模式和 react 模式以及 react-native 模式。這三個模式隻影響編譯策略。preserve 模式會生成代碼中會保留 JSX ,以供後續的轉換操作使用(比如:Babel),輸出的文件是 .jsx 格式的;而 react模式則會直接編譯成 React.createElement,在使用前就不需要再進行 JSX 轉換了,輸出的文件是 .js 格式的;react-native模式相當於preserve,它也保留了所有的JSX,但是輸出文件的擴展名是.js。
preserve
react
react-native
React.createElement
你可以在命令行中使用 --jsx 參數或者在 tsconfig.json 中指定其工作模式(使用 Create-React-App 構建默認配置的是 react 模式)。
--jsx
更多詳情可以參考:https://www.tslang.cn/docs/handbook/jsx.html
3. 每個組件變成了優雅的 class 寫法
class
// srcApp.tsx import * as React from react;
class TodosApp extends React.Component {
public render() { return ( <main className="todo-app"> ... ... </main> ); } }
export default TodosApp;
4. 出現大量的 @types 開頭的相關依賴包
@types
包括 @types/jest 、@types/node、@types/react、@types/react-dom 等等。這裡每個 @types/xxx 叫聲明文件,表示為指定模塊 xxx 提供其包含的聲明,它們會放在 node_modules/@types 文件夾下,TypeScript 會自動從這裡來獲取模塊內相關的類型定義,當我們開發時就可以獲得對應的代碼補全、介面提示等功能啦。一般需要獨立安裝這些聲明文件。當一個第三方庫沒有提供聲明文件時,我們就需要自己書寫其聲明文件了(形如 xxx.d.ts 文件),這裡不扯太多。
@types/jest
@types/node
@types/react
@types/react-dom
@types/xxx
xxx
node_modules/@types
xxx.d.ts
需要注意的有@types 支持全局和模塊類型定義。
默認情況下,TypeScript 會自動包含支持全局使用的任何定義。例如,對於 jQuery,你應該能夠在項目中開始全局使用 $。
$
安裝完之後,不需要特別的配置,你就可以像使用模塊一樣使用它:
import * as $ from jquery;
參考:@types | 深入理解 TypeScript?jkchao.github.io
這裡推薦一個網站:TypeSearch: https://microsoft.github.io/TypeSearch/,專門搜索第三方包的聲明文件。
另外,對於 TypeScript 裡面類型這個東西,你需要關注interface和type的區別、以及什麼時候使用any比較合適。
推薦:typescript interface 與 type 聲明類型的區別
推薦:Typescript 中的 interface 和 type 到底有什麼區別
這裡我們拿 TodoList 作為案例,分析它哪些地方需要用到類型以及如何好地定義它們。
// srcstores ypesindex.ts export interface ITodo { public id: number; public title: string; public isCompleted: boolean; }
// 這裡建議class,不僅僅可以做類型,還可以做構建函數 export class Todo { public id: number; public title: string; public isCompleted = false;
constructor(id: number, title: string) { this.id = id; this.title = title; } }
推薦:Typescript : class vs interface?medium.com
2. 為每個組件的 props 和 state 規定類型
props
state
將interface應用於組件的 props ,這將迫使我們在將 props 傳遞到某組件時始終保持統一的數據結構,確保組件在開發時,這些 props 類型為開發者提供提示,讓開發者考慮每個 props 的使用,同時也能避免無意義的的 props 被傳遞下去。
// srccomponentsTodosItemindex.tsx // 可以根據需要抽離到一個單獨的文件然後引入使用 interface TodosItemProps { todo: ITodo; toggleTodo: (id :number) => void; deleteTodo: (id :number) => void; editTodo: (id: number, text: string) => void; } interface TodosItemState { // 可選 isEditing?: boolean; }
// 類組件 export class TodosItem extends React.Component<TodosItemProps, TodosItemState> { ... }
// 無狀態函數組件 function MyForm(props: FormProps) { ... }
3. 枚舉作為類型使用
FiltersEnum 枚舉了 TodoList 中三個過濾條件:ALL、COMPLETED、ACTIVE,我們使用其枚舉值,同時也可以把它當作類型使用。
FiltersEnum
ALL
COMPLETED
ACTIVE
export const enum FiltersEnum { ALL= ALL, COMPLETED = COMPLETED, ACTIVE = ACTIVE }
// 作為類型 public getFilterTodos = (currentFilter: FiltersEnum) => {
switch (currentFilter) { // 獲取值 case FiltersEnum.ACTIVE: return this.filterTodos(item => !item.isCompleted); case FiltersEnum.COMPLETED: return this.filterTodos(item => item.isCompleted); default: return this.filterTodos(); } }
4. 事件對象類型
在vscode上,我們可以將滑鼠遊標懸停在事件上,可以自動獲取觸發某事件後的事件對象類型。這個是非常棒的。如下實例,我們可以清晰地看到綁定 onKeyDown 事件傳遞迴調函數的事件對象參數 event 類型為React.KeyboardEvent<HTMLInputElement>。當然這些類型的聲明都是在@types/react依賴裏定義好的,我們寫 handleKeyDown 時就可以直接拿來使用。這裡也再次體現了類型聲明後代碼提示的強大之處。
onKeyDown
event
React.KeyboardEvent<HTMLInputElement>
handleKeyDown
請教:這裡語法工具提示 event.target 類型是 EventTarget 。本應該有 value 屬性的,但是提示其上沒有 value 屬性。所以轉成了 any 。算是臨時解決,大佬看到有更好的方式歡迎評論指出,感謝~
event.target
EventTarget
value
any
Redux作為一個狀態管理工具,相信大家並不陌生。Redux github:https://github.com/reduxjs/redux
首先安裝redux、react-redux,還有它們的@types包,使用如下命令:
npm install -S redux react-redux @types/react-redux
細心的大夥應該發現,我們這裡並沒有安裝@types/redux,因為 Redux 就自己提供了自己的聲明文件( redux/index.d.ts ),並沒有單獨抽離出來。
redux/index.d.ts
引用Redux後,我們創建 src/types/index.ts 的文件,用來統一定義 store.state 類型,以及我們可能在應用開發中用到的類型,它們將很好地幫助我們管理和維護應用程序的狀態。
src/types/index.ts
store.state
// srcstores ypesindex.ts export interface IStoreState { todos: Todo[]; currentFilter: FiltersEnum; }
通常我們在 src/constants/index.ts 文件中定義 action.type 及其類型。
src/constants/index.ts
action.type
// srcstoresconstantsindex.ts export const ADD_TODO = "ADD_TODO"; export type ADD_TODO = typeof ADD_TODO;
export const TOGGLE_TODO = "TOGGLE_TODO"; export type TOGGLE_TODO = typeof TOGGLE_TODO;
export const EDIT_TODO = "EDIT_TODO"; export type EDIT_TODO = typeof EDIT_TODO;
export const DELETE_TODO = "DELETE_TODO"; export type DELETE_TODO = typeof DELETE_TODO;
export const TOGGLE_ALL_TODOS = "TOGGLE_ALL_TODOS"; export type TOGGLE_ALL_TODOS = typeof TOGGLE_ALL_TODOS;
export const SET_CURRENT_FILTER = "SET_CURRENT_FILTER"; export type SET_CURRENT_FILTER = typeof SET_CURRENT_FILTER;
這裡可以看到,我們使用 Const&type 模式定義 action.type ,這樣我們可以很容易訪問和使用相關的 action,有點一語雙關的作用,這主要得益與 TypeScript 的聲明合併(Declaration Merging)。
Const&type
action
Declaration Merging
比如之前所說的創建實體類型時,推薦 class,就是因為 class既可作類型也可以當做 JavaScript 中可見的值來使用。更多詳見文檔 Declaration Merging。
我們將在srcstoresactionsindex.ts中引入它們,在這個文件中,我們定義了每個 action的 interface 以及 ActionCreator 函數的實現。
srcstoresactionsindex.ts
interface
ActionCreator
// srcstoresactionsindex.ts import { ADD_TODO, ......, SET_CURRENT_FILTER } from ../constants; import { FiltersEnum } from ../types;
// Actions export interface IAddTodoAction { text: string; type: ADD_TODO; } ...... export interface ISetCurrentFilterAction { filter: FiltersEnum; type: SET_CURRENT_FILTER; }
// 歸一 export type TodoAction = IAddTodoAction | IToggleTodoAction | IEditTodoAction | IDeleteTodoAction | IToggleAllTodoAction;
// Action Creators export const addTodo = (text: string): IAddTodoAction => ({ text, type: ADD_TODO }); ...... export const setCurrentFilter = (filter: FiltersEnum): ISetCurrentFilterAction => ({ filter, type: SET_CURRENT_FILTER });
這裡 setCurrentFilter(filter) 的 action 我們使用 FiltersEnum 約束了它的參數傳入,只有預先定義好的三個值( ALL, COMPLETED, ACTIVE),在開發使用該 action 時可以很好地提醒開發者,避免不必要的錯誤。
setCurrentFilter(filter)
在上一步中,我們使用歸一大法,即union types,將每個 action 類型歸一為 TodoAction ,在 todos reducer 中我們來直接使用它。
union types
TodoAction
todos reducer
// srcstores educers odos.ts import { TodoAction } from ../actions; import { ADD_TODO, TOGGLE_TODO, EDIT_TODO, DELETE_TODO, TOGGLE_ALL_TODOS } from ../constants; import { ITodo } from ../types;
const todos = (state: ITodo[] = [], action: TodoAction): ITodo[] => { switch (action.type) { case ADD_TODO: return [ ...state, { isCompleted: false, title: action.text } ]; case TOGGLE_TODO: return state.map((todo: ITodo, idx: number): ITodo => (idx === action.index) ? {...todo, isCompleted: !todo.isCompleted} : todo ); ... ... default: return state; } };
export default todos;
這裡簡單展示了 TodoList 中todos reducer 的實現,另外還有currentFilter reducer 。
currentFilter reducer
使用 combineReducer 合併我們剛剛寫的所有 reducers ,並導出 rootReducr。
combineReducer
reducers
rootReducr
// srcstores educersindex.ts import { combineReducers } from redux;
import todos from ./todos; import currentFilter from ./filters;
export default combineReducers({ // states todos, currentFilter });
參考:Using combineReducers · Redux?redux.js.org
另外在入口文件 index.tsx ,我們生成有初始狀態的 store 對象,對 rootReducer 調用 creatStore ,並使用 Provider 包裹根組件 TodoApp ,傳入 state 對象。
index.tsx
store
rootReducer
creatStore
Provider
TodoApp
// srcindex.tsx import { localStore } from ./utils/localStorage; import rootReducer from ./stores/reducers; import { throttle } from ./utils/throttle;
// 從localstorage載入state,並初始化 const persistedState = JSON.parse( localStore.get(redux-todos-state) || {} ); const store = createStore(rootReducer, persistedState);
// 監聽state改變,並持久化 store.subscribe( throttle(() => localStore.set(redux-todos-state, store.getState()), 1000) );
ReactDOM.render( <Provider store={ store }> <TodosApp /> </Provider>, document.getElementById(root) as HTMLElement ); registerServiceWorker();
這裡我們初始化 store 對象,會提前去本地取 persistedState 作為 initialState ,同時 state 每次改動也會本地存儲,做到 store 持久化。同時為了避免頻繁調用 localstorage ,加入了 throttle 作為節流優化。
persistedState
initialState
localstorage
throttle
參考:Persisting Redux State to Local Storage?medium.com
首先我們要明確引入 Redux 之後數據的流向。如下圖所示,我們要把 store.state 以及改變 store 的唯一途徑 store.dispatch(action) 方法傳遞給組件。這裡以TodoItem組件為例,我們使用mapStateToProps 和mapDispatchToProps來做這項工作,建立redux和組件的映射關係。
store.dispatch(action)
mapStateToProps
mapDispatchToProps
mapStateToProps是一個函數,用於建立組件跟 store 的 state 的映射關係。第一個參數 state 為訂閱 store.state 改變後的 state ,類型為 IStoreState 沒有任何問題;第二個參數 ownProps 為從父組件傳入的 props 。通過這種方式,容器組件可以監聽 store 以及父組件傳入 props 的變化,然後重新被計算 mapStateToProps ,從而影響組件,更新UI。
IStoreState
ownProps
mapDispatchToProps :用於建立組件跟 store 的dispatch的映射關係。可以是一個對象,也可以是一個函數,傳入dispatch 和 ownProps ,定義該組件如何發出 action ,即調用 dispatch 方法。
dispatch
// srccomponentsTodosItemindex.tsx import { Dispatch } from redux; import { IStoreState } from ../../stores/types/index; import * as actions from ../../stores/actions;
interface IStateProps { id: number; isCompleted: boolean; title: string; } interface IDispatcherProps { deleteTodo: () => void; toggleTodo: () => void; editTodo: (text: string) => void; }
// 將 reducer 中的狀態插入到組件的 props 中 const mapStateToProps = (state: IStoreState, ownProps: IStateProps): IStateProps => ({ id: ownProps.id, isCompleted: ownProps.isCompleted, title: ownProps.title });
// 將 對應action 插入到組件的 props 中 const mapDispatcherToProps = (dispatch: Dispatch, ownProps: IStateProps): IDispatcherProps => ({ deleteTodo: () => dispatch(actions.deleteTodo(ownProps.id)), toggleTodo: () => dispatch(actions.toggleTodo(ownProps.id)), editTodo: (text: string) => dispatch(actions.editTodo(ownProps.id, text)) });
在組件中,我們可以通過 this.props.xxx 獲取這些映射過來的 store 數據來渲染組件,以及 mapDispatcher 方法來改變 store 。
this.props.xxx
mapDispatcher
接下來,我們使用 connect() 方法將 store 中的數據作為 props 綁定到組件上:
connect()
// srccomponentsTodosItemindex.tsx import { connect } from react-redux;
export type ReduxType = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatcherToProps>;
export class TodosItem extends React.Component<ReduxType> { ... ... }
export default connect(mapStateToProps, mapDispatcherToProps)(TodosItem);
寫這篇文章,主要是為了對之前使用 TypeScript - React - Redux 三者結合的開發總結,主要討論了和普通 JavaScript 開發的不同點,以及如何使用 TypeScript 的一些特性提高我們的開發效率、提高我們的項目質量,包括如何使用 interface ,如何為組件的 props 和 state 定義類型,以及如何集成 Redux 等等。如果大家有任何疑問或發現任何錯誤,歡迎指出~~ 另外附上 TodoList 源碼 ,歡迎大家參考閱讀。
參考:Adding state management
參考:how-to-use-typescript-with-react-and-redux
React + Redux + TypeScript — into the better frontend (tutorial)
參考:How to use Redux in typescript
使用 TypeScript + React + Redux 進行項目開發(入門篇,附源碼)
推薦閱讀: