在 Create-React-App 中使用 TypeScript(漢化)

CyanGlory發表於2017-10-19

TypeScript React Starter

這篇快速入門指南將告訴你 如何將 TypeScript 和 React 聯絡起來.
學習結束之後, 你將獲得:

  • 一個同時使用 React 和 TypeScript 的專案
  • 使用 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-specific 對於我們當前專案, 的配置選項.
  • tslint.json 儲存檢測工具的設定, TSLint, 將會使用.
  • package.json 包含我們的依賴, 以及一些我們可能會用來測試、預覽、構建 app 用的快捷鍵的命令.
  • public 包含一些我們正計劃部署的像 HTML 這樣的靜態資源, 或者 images. 在這個資料夾裡, 除了 index.html 這個檔案, 其他都可以刪除.
  • src 包含我們的 TypeScript 和 CSS 程式碼. index.tsx 是一個 強制性 的入口檔案.

執行專案

執行這個專案就像跑步不一樣簡單.

npm run start複製程式碼

這將執行我們在 package.json 裡面指定的 start 指令碼, 當我們儲存檔案的時候, 將孵化一個服務用以過載介面(熱載入).
通常服務執行在 http://localhost:3000, 但應該為您自動開啟.

緊湊的輪詢使得我們可以快速的預覽改動.

測試專案

測試也僅僅是一個命令而已:

npm run test複製程式碼

這個命令執行 Jest, a一個非常有用的測試工具, 針對其副檔名結尾的所有檔案 .test.ts or .spec.ts.
就像執行 npm run start 命令一樣, Jest 將會在它偵測到改動時立即自動執行.
如果你願意, 你可以同時執行 npm run startnpm run test , 所以你能夠在預覽改動的同時測試它們.

建立一個生產環境的版本

當時用 npm run start 執行專案的時候, 我們沒有做打包優化.
典型的, 我們希望我們傳送給客戶的程式碼, 經可能短小精悍.
一些像是 minification 這樣的優化可以實現這一點, 但這意味著要花費更多的時間.
我們稱它為生產環境的構建 (區別於 開發環境 的構建).

執行一個生產環境的構建只需要執行以下命令:

npm run build複製程式碼

這將 ./build/static/js and ./build/static/css 目錄下, 分別建立一個優化過後的 JS 和 CSS 構建.

大多數情況下你不需要執行生產環境版本,
如果你需要知道打包好的 app 有多大, 這通常是有用的.

建立一個元件

我們將寫一個 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 型別 (你可以從 ? 中得知這一點, 我們寫在它的名字後面).

我們把 Hello 寫成了一個無狀態的函式元件 (一個 stateless function component 簡稱 SFC).
具體的, 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>
    );
  }
}複製程式碼

當你的元件包含狀態的時候 類是非常有效的.
但是我們真的不需要在這個例子裡面考慮狀態 - 事實上, 我們指定它是一個 object 型別, 在 React.Component<Props, object>裡, 所以寫一個 SFC 往往更加精煉.
當建立可以在庫之間共享的通用UI元素時, 本地元件狀態在現實層面更為有用.
在我們的應用的生命週期裡, 我們將重新審視應用程式, 如何通過 Redux 管理一般的狀態.

現在我們已經寫了我們的元件, 讓我們深入到 index.tsx 並且用 <Hello ... /> 的 render 方法, 替代 <App /> 元件的 render 方法.

首先, 我們將在檔案頂部引入它:

import Hello from './components/Hello';複製程式碼

然後更改我們的render呼叫:

ReactDOM.render(
  <Hello name="TypeScript" enthusiasmLevel={10} />,
  document.getElementById('root') as HTMLElement
);複製程式碼

型別斷言

在這一節中, 我們將要指出的最後一件事就是這一行 document.getElementById('root') as HTMLElement.
這種寫法是一個 型別斷言 的呼叫, 有時也稱為 cast.
當你比型別檢測更加清楚表示式的真實型別的時候, 這是一種告訴 TypeScript 的非常有用的方式.

在這種情況下我們需要這樣做的原因是這樣的 getElementById 的返回型別是 HTMLElement | null.
簡單的說, getElementById 通過給定的 id 找不到元素的時候, 返回 null.
我們假設 getElementById 總是成功的, 所以我們需要讓 TypeScript 確信這一點, 通過使用 as 語法.

TypeScript 也擁有一個尾隨 "bang" 語法 (!), 將從前面的表示式中移除 nullundefined.
所以我們 也可以 這麼寫 document.getElementById('root')!, 但在這種情況下, 我們想要更加明確.

新增樣式 ?

使用我們的設定對元件進行樣式修飾很簡單.
修飾我們的 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 所使用的工具 (namely, Webpack 和 各種 loaders) 允許我們僅僅匯入我們感興趣的樣式表.
當我們的構建執行時, 任何匯入的.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 files) 的包, 與便於 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();
});複製程式碼

這些測試是非常基礎的, 但你應該能夠得到些事情的要點.

新增狀態管理

在這一點上, 如果你正在使用 React 請求一次資料並展示它, 您可以考慮自己完成.
但是, 如果您正在開發更具互動性的應用程式, 則可能需要新增狀態管理.

通常情況下的狀態管理

React是一個用於建立可組合檢視的有用庫.
但是, React並沒有任何設施在應用程式之間同步資料.
就React元件而言, 資料流通過 props 流向每一個你所指定的子元素.

因為 React 本身不包含 對於狀態管理的內建支援, React 使用像 Redux 和 MobX 的庫.

Redux 依賴於通過集中和不可變的資料儲存同步資料, 並且對該資料的更新將觸發我們的應用程式的重新渲染.
通過傳送明確的 action 訊息, 狀態以不變的方式更新, 且它必須由稱為 reducers 的函式來處理.
由於明確的性質, 通常更容易理解行為將如何影響您的程式的狀態.

MobX 依賴於函式式的反應模式, 其中, state 通過可觀測的 props 傳遞.
通過簡單地將狀態標記為可觀察來完成任何觀察者的狀態完全同步.
很棒的是, 這個庫已經使用 TypeScript 來編寫了.

兩者都有不同的優點和權衡.
一般來說, Redux往往會看到更廣泛的使用, 所以為了本教程的目的, 我們將重點放在新增Redux;
然而, 我們仍然鼓勵你在兩個方面都進行探索.

以下部分可能有一個陡峭的學習曲線.
我們直接建議你 通過其文件熟悉Redux.

為 actions 設定舞臺

除非我們的應用程式的狀態發生變化, 否則, 新增Redux是沒有意義的.
我們需要一個可以觸發更改的動作來源.
這可能是一個 計時器, 或者 按鈕一樣的 UI 元素.

為了我們的目的, 我們將新增兩個按鈕來控制我們的 Hello 元件的受歡迎程度.

安裝 Redux

新增 Redux, 我們首先安裝 reduxreact-redux, 以及它們的 types 庫, 作為依賴項.

npm install -S redux react-redux @types/react-redux複製程式碼

在這個示例裡面我們不需要安裝 @types/redux 因為 Redux 已經有自己的定義檔案 (.d.ts files).

定義我們的 app 的 state

我們需要定義 Redux 將儲存的狀態的樣子.
為此, 我們可以建立一個名為 src/types/index.tsx 的檔案, 其中將包含整個程式中可能使用的型別的定義.

// src/types/index.tsx

export interface StoreState {
    languageName: string;
    enthusiasmLevel: number;
}複製程式碼

我們的意圖是 languageName 將是此應用程式編寫的程式語言 (i.e. TypeScript or 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 的字串字面量型別.

接下來, 我們將在 src/actions/index.tsx 中建立一組 actions, 以及 actions 建構函式.

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
    }
}複製程式碼

我們建立了兩種型別, 用以描述什麼是 增加 actions, 什麼是 減少 actions.

我們還建立了一個型別 (EnthusiasmAction) 來描述 actions 可以是增量或減量的情況.
最後, 我們做了兩個函式, 用來製造了我們可以使用的 actions, 而不是寫出龐大的物件字面量.

這裡有明顯的樣式程式碼, 所以你應該隨時檢視像 redux-actions 這樣的 庫.

新增 reducer

我們準備寫我們的第一個 reducer!

Reducers 只是個通過拷貝和修改我們應用程式的狀態的 函式, 沒有任何副作用.
換句話說, 這就是我們所說的 純函式.

我們的 reducer 將位於之下 src/reducers/index.tsx.

其功能是確保增量提高1點的積極性, 降低1點的積極性, 但水平不低於1。
其功能是確保 increments 使受歡迎程度上升 1, decrements 使受歡迎程度減少 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) 它允許我們建立一個 state 的淺拷貝, 同時替換 enthusiasmLevel.
值得注意的是 enthusiasmLevel 屬性要放在後面, 否則它會被舊的 state 裡面的屬性覆蓋.

您可能想為您的 reducer 寫幾個測試.
由於 reducer 是純函式, 它們可以被傳遞任意資料.

對於每個輸入, reducers 可以通過檢查其新生成的狀態進行測試.
考慮研究 Jest 的toEqual方法來實現這一點。

建立一個容器

當書寫 Redux 的時候, 我們經常會寫入元件以及容器.
元件通常與資料無關, 並且主要在一個表現層面上工作.
容器 通常包裝元件併為他們提供顯示和修改狀態所需的任何資料.

你可以在Dan Abramov 的文章 Presentational and Container Components 上更多地瞭解這個概念

首先讓我們更新 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 函式向我們的 store 觸發 actions, 通過建立回撥 props.

如果我們記得, 我們的應用程式狀態由兩個屬性組成: languageNameenthusiasmLevel.

另一方面, 我們的 Hello 元件預計會有一個 name and an enthusiasmLevel.
mapStateToProps 將從 store 獲取相關資料, 並根據需要對元件的 props 進行調整.
讓我們繼續往下寫.

export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
  return {
    enthusiasmLevel,
    name: languageName,
  }
}複製程式碼

請注意, mapStateToProps 只建立一個 Hello 元件所期望的4個屬性中的2個.

也就是說, 我們仍然希望通過 onIncrementonDecrement 回撥.
mapDispatchToProps 是接受一個 dispatcher 函式 作為引數.
這個 dispatcher 函式能通過傳入 actions 到我們的 store 來觸發更新, 所以我們可以建立一個可以呼叫 dispatcher 的回撥函式.

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, 並將其與所有的 reducer 進行配置.

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/components/Hello 通過 ./src/containers/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 函式來調整我們的應用程式的 state, 為我們包裝的 Hello 元件的 props.

Ejecting

如果在任何時候, 您覺得這兒某些 create-react-app 的因素導致設定變得困難, 您可以隨時選擇 彈出 並獲取所需的各種配置選項.
例如, 如果您想新增一個Webpack外掛, 可能需要利用create-react-app提供的 "eject" 功能.

簡單的執行

npm run eject複製程式碼

好好去吧!

小心, 你可能想要在執行彈出之前提交所有的工作.
您不能撤消 彈出 命令, 因此選擇 退出 是永久性的, 除非您可以在執行彈出之前, 從提交中恢復.

下一步

create-react-app 帶有很多好東西.

其中大部分記錄在為我們的專案生成的預設 README.md 中, 因此可以快速閱讀.

如果您還想了解有關 Redux 的更多資訊, 您可以 檢視官方網站 獲取文件.
for MobX 也一樣.

如果您想在某一時刻彈出, 您可能需要更多地瞭解 Webpack.
您可以在這裡檢視我們的 React & Webpack 這裡的演練.

在某些時候你可能需要路由.
這兒有幾個解決方案, 但是 react-router 對於 Redux 專案來說最受歡迎的, 並經常與 react-router-redux 配合使用.

translator

@author: Riu.Zen

@lastUpdateTime: 2017-10-01

補充說明

2017-10-19
有道雲筆記的分享掛了, 直接把源文搬過來, 希望能幫到大家

相關文章