轉自https://typescript.bootcss.com/tutorials/react.html
這篇快速上手指南會教你如何將TypeScript與React結合起來使用。 在最後,你將學到:
我們會使用create-react-app工具快速搭建工程環境。
這裏假設你已經在使用Node.js和npm。 並且已經瞭解了React的基礎知識。
安裝create-react-app
我們之所以使用create-react-app是因爲它能夠爲React工程設置一些有效的工具和權威的默認參數。 它僅僅是一個用來搭建React工程的命令行工具而已。
npm install -g create-react-app
創建新工程
讓我們首先創建一個叫做my-app
的新工程:
create-react-app my-app --scripts-version=react-scripts-ts
react-scripts-ts是一系列適配器,它利用標準的create-react-app工程管道並把TypeScript混入進來。
此時的工程結構應如下所示:
my-app/
├─ .gitignore
├─ node_modules/
├─ public/
├─ src/
│ └─ ...
├─ package.json
├─ tsconfig.json
└─ tslint.json
注意:
tsconfig.json
包含了工程裏TypeScript特定的選項。tslint.json
保存了要使用的代碼檢查器的設置,TSLint。package.json
包含了依賴,還有一些命令的快捷方式,如測試命令,預覽命令和發佈應用的命令。public
包含了靜態資源如HTML頁面或圖片。除了index.html
文件外,其它的文件都可以刪除。src
包含了TypeScript和CSS源碼。index.tsx
是強制使用的入口文件。
運行工程
通過下面的方式即可輕鬆地運行這個工程。
npm run start
它會執行package.json
裏面指定的start
命令,並且會啓動一個服務器,當我們保存文件時還會自動刷新頁面。 通常這個服務器的地址是http://localhost:3000
,頁面應用會被自動地打開。
它會保持監聽以方便我們快速地預覽改動。
測試工程
測試也僅僅是一行命令的事兒:
npm run test
這個命令會運行Jest,一個非常好用的測試工具,它會運行所有擴展名是.test.ts
或.spec.ts
的文件。 好比是npm run start
命令,當檢測到有改動的時候Jest會自動地運行。 如果喜歡的話,你還可以同時運行npm run start
和npm run test
,這樣你就可以在預覽的同時進行測試。
生成生產環境的構建版本
在使用npm run start
運行工程的時候,我們並沒有生成一個優化過的版本。 通常我們想給用戶一個運行的儘可能快並在體積上儘可能小的代碼。 像壓縮這樣的優化方法可以做到這一點,但是總是要耗費更多的時間。 我們把這樣的構建版本稱做“生產環境”版本(與開發版本相對)。
要執行生產環境的構建,可以運行如下命令:
npm run build
這會相應地創建優化過的JS和CSS文件,./build/static/js
和./build/static/css
。
大多數情況下你不需要生成生產環境的構建版本, 但它可以幫助你衡量應用最終版本的體積大小。
創建一個組件
下面我們將要創建一個Hello
組件。 這個組件接收任意一個我們想對之打招呼的名字(我們把它叫做name
),並且有一個可選數量的感嘆號做爲結尾(通過enthusiasmLevel
)。
若我們這樣寫<Hello name="Daniel" enthusiasmLevel={3} />
,這個組件大至會渲染成<div>Hello Daniel!!!</div>
。 如果沒指定enthusiasmLevel
,組件將默認顯示一個感嘆號。 若enthusiasmLevel
爲0
或負值將拋出一個錯誤。
下面來寫一下Hello.tsx
:
// src/components/Hello.tsx
import * as React from 'react';
export interface Props {
name: string;
enthusiasmLevel?: number;
}
function Hello({ name, enthusiasmLevel = 1 }: Props) {
if (enthusiasmLevel <= 0) {
throw new Error('You could be a little more enthusiastic. :D');
}
return (
<div className="hello">
<div className="greeting">
Hello {name + getExclamationMarks(enthusiasmLevel)}
</div>
</div>
);
}
export default Hello;
// helpers
function getExclamationMarks(numChars: number) {
return Array(numChars + 1).join('!');
}
注意我們定義了一個類型Props
,它指定了我們組件要用到的屬性。 name
是必需的且爲string
類型,同時enthusiasmLevel
是可選的且爲number
類型(你可以通過名字後面加?
爲指定可選參數)。
我們創建了一個無狀態的函數式組件(Stateless Functional Components,SFC)Hello
。 具體來講,Hello
是一個函數,接收一個Props
對象並拆解它。 如果Props
對象裏沒有設置enthusiasmLevel
,默認值爲1
。
使用函數是React中定義組件的兩種方式之一。 如果你喜歡的話,也可以通過類的方式定義:
class Hello extends React.Component<Props, object> {
render() {
const { name, enthusiasmLevel = 1 } = this.props;
if (enthusiasmLevel <= 0) {
throw new Error('You could be a little more enthusiastic. :D');
}
return (
<div className="hello">
<div className="greeting">
Hello {name + getExclamationMarks(enthusiasmLevel)}
</div>
</div>
);
}
}
當我們的組件具有某些狀態的時候,使用類的方式是很有用處的。 但在這個例子裏我們不需要考慮狀態 - 事實上,在React.Component<Props, object>
我們把狀態指定爲了object
,因此使用SFC更簡潔。 當在創建可重用的通用UI組件的時候,在表現層使用組件局部狀態比較適合。 針對我們應用的生命週期,我們會審視應用是如何通過Redux輕鬆地管理普通狀態的。
現在我們已經寫好了組件,讓我們仔細看看index.tsx
,把<App />
替換成<Hello ... />
。
首先我們在文件頭部導入它:
import Hello from './components/Hello.tsx';
然後修改render
調用:
ReactDOM.render(
<Hello name="TypeScript" enthusiasmLevel={10} />,
document.getElementById('root') as HTMLElement
);
類型斷言
這裏還有一點要指出,就是最後一行document.getElementById('root') as HTMLElement
。 這個語法叫做類型斷言,有時也叫做轉換。 當你比類型檢查器更清楚一個表達式的類型的時候,你可以通過這種方式通知TypeScript。
這裏,我們之所以這麼做是因爲getElementById
的返回值類型是HTMLElement | null
。 簡單地說,getElementById
返回null
是當無法找對對應id
元素的時候。 我們假設getElementById
總是成功的,因此我們要使用as
語法告訴TypeScript這點。
TypeScript還有一種感嘆號(!
)結尾的語法,它會從前面的表達式裏移除null
和undefined
。 所以我們也可以寫成document.getElementById('root')!
,但在這裏我們想寫的更清楚些。
:sunglasses:添加樣式
通過我們的設置爲一個組件添加樣式很容易。 若要設置Hello
組件的樣式,我們可以創建這樣一個CSS文件src/components/Hello.css
。
.hello {
text-align: center;
margin: 20px;
font-size: 48px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif
}
.hello button {
margin-left: 25px;
margin-right: 25px;
font-size: 40px;
min-width: 50px;
}
create-react-app
包含的工具(Webpack和一些加載器)允許我們導入樣式表文件。 當我們構建應用的時候,所有導入的.css
文件會被拼接成一個輸出文件。 因此在src/components/Hello.tsx
,我們需要添加如下導入語句。
import './Hello.css';
使用Jest編寫測試
我們對Hello
組件有一些假設。 讓我們在此重申一下:
- 當這樣寫
<Hello name="Daniel" enthusiasmLevel={3} />
時,組件應被渲染成<div>Hello Daniel!!!</div>
。- 若未指定
enthusiasmLevel
,組件應默認顯示一個感嘆號。- 若
enthusiasmLevel
爲0
或負值,它應拋出一個錯誤。
我們將針對這些需求爲組件寫一些註釋。
但首先,我們要安裝Enzyme。 Enzyme是React生態系統裏一個通用工具,它方便了針對組件的行爲編寫測試。 默認地,我們的應用包含了一個叫做jsdom的庫,它允許我們模擬DOM以及在非瀏覽器的環境下測試運行時的行爲。 Enzyme與此類似,但是是基於jsdom的,並且方便我們查詢組件。
讓我們把它安裝爲開發依賴項。
npm install -D enzyme @types/enzyme react-addons-test-utils
注意我們同時安裝了enzyme
和@types/enzyme
。 enzyme
包指的是包含了實際運行的JavaScript代碼包,而@types/enzyme
則包含了聲明文件(.d.ts
文件)的包,以便TypeScript能夠了解該如何使用Enzyme。 你可以在這裏瞭解更多關於@types
包的信息。
我們還需要安裝react-addons-test-utils
。 它是使用enzyme
所需要安裝的包。
現在我們已經設置好了Enzyme,下面開始編寫測試! 先創建一個文件src/components/Hello.test.tsx
,與先前的Hello.tsx
文件放在一起。
// src/components/Hello.test.tsx
import * as React from 'react';
import * as enzyme from 'enzyme';
import Hello from './Hello';
it('renders the correct text when no enthusiasm level is given', () => {
const hello = enzyme.shallow(<Hello name='Daniel' />);
expect(hello.find(".greeting").text()).toEqual('Hello Daniel!')
});
it('renders the correct text with an explicit enthusiasm of 1', () => {
const hello = enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={1}/>);
expect(hello.find(".greeting").text()).toEqual('Hello Daniel!')
});
it('renders the correct text with an explicit enthusiasm level of 5', () => {
const hello = enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={5} />);
expect(hello.find(".greeting").text()).toEqual('Hello Daniel!!!!!');
});
it('throws when the enthusiasm level is 0', () => {
expect(() => {
enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={0} />);
}).toThrow();
});
it('throws when the enthusiasm level is negative', () => {
expect(() => {
enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={-1} />);
}).toThrow();
});
這些測試都十分基礎,但你可以從中得到啓發。
添加state管理
到此爲止,如果你使用React的目的是隻獲取一次數據並顯示,那麼你已經完成了。 但是如果你想開發一個可以交互的應用,那麼你需要添加state管理。
state管理概述
React本身就是一個適合於創建可組合型視圖的庫。 但是,React並沒有任何在應用間同步數據的功能。 就React組件而言,數據是通過每個元素上指定的props向子元素傳遞。
因爲React本身並沒有提供內置的state管理功能,React社區選擇了Redux和MobX庫。
Redux依靠一個統一且不可變的數據存儲來同步數據,並且更新那裏的數據時會觸發應用的更新渲染。 state的更新是以一種不可變的方式進行,它會發布一條明確的action消息,這個消息必須被reducer函數處理。 由於使用了這樣明確的方式,很容易弄清楚一個action是如何影響程序的state。
MobX藉助於函數式響應型模式,state被包裝在了可觀察對象裏,並通過props傳遞。 通過將state標記爲可觀察的,即可在所有觀察者之間保持state的同步性。 另一個好處是,這個庫已經使用TypeScript實現了。
這兩者各有優缺點。 但Redux使用得更廣泛,因此在這篇教程裏,我們主要看如何使用Redux; 但是也鼓勵大家兩者都去了解一下。
後面的小節學習曲線比較陡。 因此強烈建議大家先去熟悉一下Redux。
設置actions
只有當應用裏的state會改變的時候,我們才需要去添加Redux。 我們需要一個action的來源,它將觸發改變。 它可以是一個定時器或者UI上的一個按鈕。
爲此,我們將增加兩個按鈕來控制Hello
組件的感嘆級別。
安裝Redux
安裝redux
和react-redux
以及它們的類型文件做爲依賴。
npm install -S redux react-redux @types/react-redux
這裏我們不需要安裝@types/redux
,因爲Redux已經自帶了聲明文件(.d.ts
文件)。
定義應用的狀態
我們需要定義Redux保存的state的結構。 創建src/types/index.tsx
文件,它保存了類型的定義,我們在整個程序裏都可能用到。
// src/types/index.tsx
export interface StoreState {
languageName: string;
enthusiasmLevel: number;
}
這裏我們想讓languageName
表示應用使用的編程語言(例如,TypeScript或者JavaScript),enthusiasmLevel
是可變的。 在寫我們的第一個容器的時候,就會明白爲什麼要令state與props稍有不同。
添加actions
下面我們創建這個應用將要響應的消息類型,src/constants/index.tsx
。
// src/constants/index.tsx
export const INCREMENT_ENTHUSIASM = 'INCREMENT_ENTHUSIASM';
export type INCREMENT_ENTHUSIASM = typeof INCREMENT_ENTHUSIASM;
export const DECREMENT_ENTHUSIASM = 'DECREMENT_ENTHUSIASM';
export type DECREMENT_ENTHUSIASM = typeof DECREMENT_ENTHUSIASM;
這裏的const
/type
模式允許我們以容易訪問和重構的方式使用TypeScript的字符串字面量類型。
接下來,我們創建一些actions以及創建這些actions的函數,src/actions/index.tsx
。
import * as constants from '../constants'
export interface IncrementEnthusiasm {
type: constants.INCREMENT_ENTHUSIASM;
}
export interface DecrementEnthusiasm {
type: constants.DECREMENT_ENTHUSIASM;
}
export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm;
export function incrementEnthusiasm(): IncrementEnthusiasm {
return {
type: constants.INCREMENT_ENTHUSIASM
}
}
export function decrementEnthusiasm(): DecrementEnthusiasm {
return {
type: constants.DECREMENT_ENTHUSIASM
}
}
我們創建了兩個類型,它們負責增加操作和減少操作的行爲。 我們還定義了一個類型(EnthusiasmAction
),它描述了哪些action是可以增加或減少的。 最後,我們定義了兩個函數用來創建實際的actions。
這裏有一些清晰的模版,你可以參考類似redux-actions的庫。
添加reducer
現在我們可以開始寫第一個reducer了! Reducers是函數,它們負責生成應用state的拷貝使之產生變化,但它並沒有副作用。 它們是一種純函數。
我們的reducer將放在src/reducers/index.tsx
文件裏。 它的功能是保證增加操作會讓感嘆級別加1,減少操作則要將感嘆級別減1,但是這個級別永遠不能小於1。
// src/reducers/index.tsx
import { EnthusiasmAction } from '../actions';
import { StoreState } from '../types/index';
import { INCREMENT_ENTHUSIASM, DECREMENT_ENTHUSIASM } from '../constants/index';
export function enthusiasm(state: StoreState, action: EnthusiasmAction): StoreState {
switch (action.type) {
case INCREMENT_ENTHUSIASM:
return { ...state, enthusiasmLevel: state.enthusiasmLevel + 1 };
case DECREMENT_ENTHUSIASM:
return { ...state, enthusiasmLevel: Math.max(1, state.enthusiasmLevel - 1) };
}
return state;
}
注意我們使用了對象展開(...state
),當替換enthusiasmLevel
時,它可以對狀態進行淺拷貝。 將enthusiasmLevel
屬性放在末尾是十分關鍵的,否則它將被舊的狀態覆蓋。
你可能想要對reducer寫一些測試。 因爲reducers是純函數,它們可以傳入任意的數據。 針對每個輸入,可以測試reducers生成的新的狀態。 可以考慮使用Jest的toEqual方法。
創建容器
在使用Redux時,我們常常要創建組件和容器。 組件是數據無關的,且工作在表現層。 容器通常包裹組件及其使用的數據,用以顯示和修改狀態。 你可以在這裏閱讀更多關於這個概念的細節:Dan Abramov寫的表現層的容器組件。
現在我們修改src/components/Hello.tsx
,讓它可以修改狀態。 我們將添加兩個可選的回調屬性到Props
,它們分別是onIncrement
和onDecrement
:
export interface Props {
name: string;
enthusiasmLevel?: number;
onIncrement?: () => void;
onDecrement?: () => void;
}
然後將這兩個回調綁定到兩個新按鈕上,將按鈕添加到我們的組件裏。
function Hello({ name, enthusiasmLevel = 1, onIncrement, onDecrement }: Props) {
if (enthusiasmLevel <= 0) {
throw new Error('You could be a little more enthusiastic. :D');
}
return (
<div className="hello">
<div className="greeting">
Hello {name + getExclamationMarks(enthusiasmLevel)}
</div>
<div>
<button onClick={onDecrement}>-</button>
<button onClick={onIncrement}>+</button>
</div>
</div>
);
}
通常情況下,我們應該給onIncrement
和onDecrement
寫一些測試,它們是在各自的按鈕被點擊時調用。 試一試以便掌握編寫測試的竅門。
現在我們的組件更新好了,可以把它放在一個容器裏了。 讓我們來創建一個文件src/containers/Hello.tsx
,在開始的地方使用下列導入語句。
import Hello from '../components/Hello';
import * as actions from '../actions/';
import { StoreState } from '../types/index';
import { connect, Dispatch } from 'react-redux';
兩個關鍵點是初始的Hello
組件和react-redux的connect
函數。 connect
可以將我們的Hello
組件轉換成一個容器,通過以下兩個函數:
mapStateToProps
將當前store裏的數據以我們的組件需要的形式傳遞到組件。mapDispatchToProps
利用dispatch
函數,創建回調props將actions送到store。
回想一下,我們的應用包含兩個屬性:languageName
和enthusiasmLevel
。 我們的Hello
組件,希望得到一個name
和一個enthusiasmLevel
。 mapStateToProps
會從store得到相應的數據,如果需要的話將針對組件的props調整它。 下面讓我們繼續往下寫。
export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
return {
enthusiasmLevel,
name: languageName,
}
}
注意mapStateToProps
僅創建了Hello
組件需要的四個屬性中的兩個。 我們還想要傳入onIncrement
和onDecrement
回調函數。 mapDispatchToProps
是一個函數,它需要傳入一個調度函數。 這個調度函數可以將actions傳入store來觸發更新,因此我們可以創建一對回調函數,它們會在需要的時候調用調度函數。
export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
return {
onIncrement: () => dispatch(actions.incrementEnthusiasm()),
onDecrement: () => dispatch(actions.decrementEnthusiasm()),
}
}
最後,我們可以調用connect
了。 connect
首先會接收mapStateToProps
和mapDispatchToProps
,然後返回另一個函數,我們用它來包裹我們的組件。 最終的容器是通過下面的代碼定義的:
export default connect(mapStateToProps, mapDispatchToProps)(Hello);
現在,我們的文件應該是下面這個樣子:
// src/containers/Hello.tsx
import Hello from '../components/Hello';
import * as actions from '../actions/';
import { StoreState } from '../types/index';
import { connect, Dispatch } from 'react-redux';
export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
return {
enthusiasmLevel,
name: languageName,
}
}
export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
return {
onIncrement: () => dispatch(actions.incrementEnthusiasm()),
onDecrement: () => dispatch(actions.decrementEnthusiasm()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Hello);
創建store
讓我們回到src/index.tsx
。 要把所有的東西合到一起,我們需要創建一個帶初始狀態的store,並用我們所有的reducers來設置它。
import { createStore } from 'redux';
import { enthusiasm } from './reducers/index';
import { StoreState } from './types/index';
const store = createStore<StoreState>(enthusiasm, {
enthusiasmLevel: 1,
languageName: 'TypeScript',
});
store
可能正如你想的那樣,它是我們應用全局狀態的核心store。
接下來,我們將要用./src/containers/Hello
來包裹./src/components/Hello
,然後使用react-redux的Provider
將props與容器連通起來。 我們將導入它們:
import Hello from './containers/Hello';
import { Provider } from 'react-redux';
將store
以Provider
的屬性形式傳入:
ReactDOM.render(
<Provider store={store}>
<Hello />
</Provider>,
document.getElementById('root') as HTMLElement
);
注意,Hello
不再需要props了,因爲我們使用了connect
函數爲包裹起來的Hello
組件的props適配了應用的狀態。
退出
如果你發現create-react-app使一些自定義設置變得困難,那麼你就可以選擇不使用它,使用你需要配置。 比如,你要添加一個Webpack插件,你就可以利用create-react-app提供的“eject”功能。
運行:
npm run eject
這樣就可以了!
你要注意,在運行eject前最好保存你的代碼。 你不能撤銷eject命令,因此退出操作是永久性的除非你從一個運行eject前的提交來恢復工程。
下一步
create-react-app帶有很多很棒的功能。 它們的大多數都在我們工程生成的README.md
裏面有記錄,所以可以簡單閱讀一下。
如果你想學習更多關於Redux的知識,你可以前往官方站點查看文檔。 同樣的,MobX官方站點。
如果你想要在某個時間點eject,你需要了解再多關於Webpack的知識。 你可以查看React & Webpack教程。
有時候你需要路由功能。 已經有一些解決方案了,但是對於Redux工程來講react-router是最流行的,並經常與react-router-redux聯合使用。