前言

TypeScript 作為 JavaScript 的超集,強類型、類型推斷等優點簡直棒棒噠。對項目來說,typescript 可以讓你的 app 更穩定、易閱讀、好管理。當你在你的項目中引入 TypeScript ,你將能節省大量的時間以及精力,來開發維護你的項目。照目前趨勢來看,前端各大框架或庫都慢慢趨於倒向 TypeScript 。希望你也可以考慮一下。

typescript集合圖

這裡本文主要做了對 TypeScript + React + Redux 的集成總結,並輸出了一個 TodoList Demo 避免紙上談兵,其中遇到的問題也是蠻多的,在倉庫中都有說明,歡迎大家 code review 。有任何建議可以提出。


Create React App

首先,我們運行如下指令,創建一個新的基於 TypeScript 的 React App,當然你也可以參照官方文檔把舊項目遷移到 TypeScript 上來。

$ npx create-react-app app_name --typescript

參考:Adding TypeScript · Create React App?

facebook.github.io圖標

完成之後,你會看到一個基本的 React with TypeScript 項目結構,其中包含了以下幾點變化(對比 JavaScript ):

  1. 根目錄下出現了 tsconfig.json 文件

接觸過 TypeScript 的應該很熟悉,這是作為你項目的 TypeScript 編譯選項配置。

2. 文件擴展名變化:.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

三種JSX作模式

你可以在命令行中使用 --jsx 參數或者在 tsconfig.json 中指定其工作模式(使用 Create-React-App 構建默認配置的是 react 模式)。

typescript支持jsx配置

更多詳情可以參考:tslang.cn/docs/handbook

3. 每個組件變成了優雅的 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/jest@types/node@types/react@types/react-dom 等等。這裡每個 @types/xxx聲明文件,表示為指定模塊 xxx 提供其包含的聲明,它們會放在 node_modules/@types 文件夾下,TypeScript 會自動從這裡來獲取模塊內相關的類型定義,當我們開發時就可以獲得對應的代碼補全、介面提示等功能啦。一般需要獨立安裝這些聲明文件。當一個第三方庫沒有提供聲明文件時,我們就需要自己書寫其聲明文件了(形如 xxx.d.ts 文件),這裡不扯太多。

需要注意的有@types 支持全局和模塊類型定義

  • 全局 @types

默認情況下,TypeScript 會自動包含支持全局使用的任何定義。例如,對於 jQuery,你應該能夠在項目中開始全局使用 $

  • 模塊 @types

安裝完之後,不需要特別的配置,你就可以像使用模塊一樣使用它:

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 作為案例,分析它哪些地方需要用到類型以及如何好地定義它們。

  1. 一個獨立的實體定義一個類型

// 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. 為每個組件的 propsstate 規定類型

將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 中三個過濾條件:ALLCOMPLETEDACTIVE,我們使用其枚舉值,同時也可以把它當作類型使用。

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 時就可以直接拿來使用。這裡也再次體現了類型聲明後代碼提示的強大之處。

請教:這裡語法工具提示 event.target 類型是 EventTarget 。本應該有 value 屬性的,但是提示其上沒有 value 屬性。所以轉成了 any 。算是臨時解決,大佬看到有更好的方式歡迎評論指出,感謝~

vscode + typescript = 強大
參考:聲明文件?

ts.xcatliu.com
圖標

集成 Redux

Redux作為一個狀態管理工具,相信大家並不陌生。Redux github:github.com/reduxjs/redu

安裝 Redux

首先安裝redux、react-redux,還有它們的@types包,使用如下命令:

npm install -S redux react-redux @types/react-redux

細心的大夥應該發現,我們這裡並沒有安裝@types/redux,因為 Redux 就自己提供了自己的聲明文件( redux/index.d.ts ),並沒有單獨抽離出來

確定 store.state 類型

引用Redux後,我們創建 src/types/index.ts 的文件,用來統一定義 store.state 類型,以及我們可能在應用開發中用到的類型,它們將很好地幫助我們管理和維護應用程序的狀態。

// srcstores ypesindex.ts

export interface IStoreState {
todos: Todo[];
currentFilter: FiltersEnum;
}

定義 action

通常我們在 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)。

比如之前所說的創建實體類型時,推薦 class,就是因為 class既可作類型也可以當做 JavaScript 中可見的值來使用。更多詳見文檔 Declaration Merging。

我們將在srcstoresactionsindex.ts中引入它們,在這個文件中,我們定義了每個 actioninterface 以及 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 時可以很好地提醒開發者,避免不必要的錯誤。

定義 reducer

在上一步中,我們使用歸一大法,即union types,將每個 action 類型歸一為 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

參考:Union-Types · TypeScript?

www.typescriptlang.org

初始化 state

使用 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 對象。

// 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 作為節流優化。

參考:Persisting Redux State to Local Storage?

medium.com


Redux 到組件

首先我們要明確引入 Redux 之後數據的流向。如下圖所示,我們要把 store.state 以及改變 store 的唯一途徑 store.dispatch(action) 方法傳遞給組件。這裡以TodoItem組件為例,我們使用mapStateToPropsmapDispatchToProps來做這項工作,建立redux和組件的映射關係。

Redux數據流向

mapStateToProps (state: IStoreState, ownProps: IStateProps)

mapStateToProps是一個函數,用於建立組件跟 storestate 的映射關係。第一個參數 state 為訂閱 store.state 改變後的 state ,類型為 IStoreState 沒有任何問題;第二個參數 ownProps 為從父組件傳入的 props 。通過這種方式,容器組件可以監聽 store 以及父組件傳入 props 的變化,然後重新被計算 mapStateToProps ,從而影響組件,更新UI

mapDispatchToProps (dispatch: Dispatch, ownProps: IStateProps)

mapDispatchToProps :用於建立組件跟 store 的dispatch的映射關係。可以是一個對象,也可以是一個函數,傳入dispatchownProps定義該組件如何發出 action ,即調用 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

參考:mapStateToProps,mapDispatchToProps的使用姿勢 - 騰訊Web前端 IMWeb 團隊社區?

imweb.io
圖標

接下來,我們使用 connect() 方法將 store 中的數據作為 props 綁定到組件上:

// 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);


項目核心的文件結構

TodoList的文件結構

總結

寫這篇文章,主要是為了對之前使用 TypeScript - React - Redux 三者結合的開發總結,主要討論了和普通 JavaScript 開發的不同點,以及如何使用 TypeScript 的一些特性提高我們的開發效率、提高我們的項目質量,包括如何使用 interface ,如何為組件的 propsstate 定義類型,以及如何集成 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 進行項目開發(入門篇,附源碼)

推薦閱讀:

相關文章