React完整上手攻略(轉)

轉自https://typescript.bootcss.com/tutorials/react.html

這篇快速上手指南會教你如何將TypeScript與React結合起來使用。 在最後,你將學到:

  • 使用TypeScript和React創建工程
  • 使用TSLint進行代碼檢查
  • 使用JestEnzyme進行測試,以及
  • 使用Redux管理狀態

我們會使用create-react-app工具快速搭建工程環境。

這裏假設你已經在使用Node.jsnpm。 並且已經瞭解了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 startnpm 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,組件將默認顯示一個感嘆號。 若enthusiasmLevel0或負值將拋出一個錯誤。

下面來寫一下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還有一種感嘆號(!)結尾的語法,它會從前面的表達式裏移除nullundefined。 所以我們也可以寫成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,組件應默認顯示一個感嘆號。
  • enthusiasmLevel0或負值,它應拋出一個錯誤。

我們將針對這些需求爲組件寫一些註釋。

但首先,我們要安裝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

安裝reduxreact-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,它們分別是onIncrementonDecrement

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

通常情況下,我們應該給onIncrementonDecrement寫一些測試,它們是在各自的按鈕被點擊時調用。 試一試以便掌握編寫測試的竅門。

現在我們的組件更新好了,可以把它放在一個容器裏了。 讓我們來創建一個文件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。

回想一下,我們的應用包含兩個屬性:languageNameenthusiasmLevel。 我們的Hello組件,希望得到一個name和一個enthusiasmLevel。 mapStateToProps會從store得到相應的數據,如果需要的話將針對組件的props調整它。 下面讓我們繼續往下寫。

export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
  return {
    enthusiasmLevel,
    name: languageName,
  }
}

注意mapStateToProps僅創建了Hello組件需要的四個屬性中的兩個。 我們還想要傳入onIncrementonDecrement回調函數。 mapDispatchToProps是一個函數,它需要傳入一個調度函數。 這個調度函數可以將actions傳入store來觸發更新,因此我們可以創建一對回調函數,它們會在需要的時候調用調度函數。

export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
  return {
    onIncrement: () => dispatch(actions.incrementEnthusiasm()),
    onDecrement: () => dispatch(actions.decrementEnthusiasm()),
  }
}

最後,我們可以調用connect了。 connect首先會接收mapStateToPropsmapDispatchToProps,然後返回另一個函數,我們用它來包裹我們的組件。 最終的容器是通過下面的代碼定義的:

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';

storeProvider的屬性形式傳入:

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聯合使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章