TypeScript React Starter

這個快速入門指南將教你如何使用React連接TypeScript。 最後,將會獲得:

  • 一個使用React和TypeScript的項目

  • TSLint項目檢查

  • JestEnzyme進行測試,

  • Redux流程管理

我們將使用 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存儲我們的linter,TSLint將使用的設置。

  • package.json包含我們的依賴關係,以及我們想要運行的用於測試,預覽和部署應用程序命令的一些快捷方式。

  • public包含靜態資源,比如我們計劃部署到的HTML頁面或圖像。 您可以刪除該文件夾中除index.html之外的任何文件。

  • src包含我們的TypeScript和CSS代碼。 index.tsx是我們文件的入口點,是強制性的。


  • 運行項目

運行項目只需簡單的一個命令:

npm run start

這將運行我們的package.json指定的啓動腳本,並將生成一個服務器,當我們保存文件時重新加載頁面。通常,服務器運行在http://localhost:3000,自動爲你打開。

這可以通過允許我們快速預覽更改來收緊迭代循環。

  • 測試項目

測試也是通過一個簡單的命令:

npm run test

此命令運行Jest,這是一個非常有用的測試實用程序,針對擴展名以.test或.spec.ts結尾的所有文件。 像npm run start命令一樣,Jest會在檢測到更改後立即自動運行。 如果你願意,你可以並行運行npm run start和npm run test,以便您可以預覽更改並同時測試。

  • 創建生產構建

當以npm運行啓動運行項目時,我們沒有最終構建優化版本。 通常,我們希望我們傳送給用戶的代碼儘可能快和小。 某些優化如縮小可以實現這一點,但往往需要更多的時間。 我們稱這樣的“生產”構建(而不是開發版本)。

運行生產構建,只需運行

npm run build

這將分別在./build/static/js和./build/static/css中創建優化的JS和CSS構建。

大多數時間您不需要運行生產版本,但如果您需要測量類似於應用程序最終大小,這將非常有用。

  • 創建組件

我們將寫一個Hello組件。該組件將以我們要打招呼的內容命名(我們稱之爲name),以及任意感嘆號的數值(enthusiasmLevel)跟蹤。

當我們寫一些代碼比如<Hello name =“Daniel”enthusiasmLevel = {3} />時,組件將會渲染爲<div> Hello Daniel !!! </ div>。如果沒有指定enthusiasmLevel,組件就會給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是一個任意數字(你可以從name 後面的?看出來)。

我們還寫了Hello作爲無狀態函數組件(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>
    );
  }
}

當我們的組件實例有一些狀態時,類是很有用的。但是在這個例子中我們並不需要考慮狀態 - 實際上我們將它指定爲React.Component <Props,object>中的對象,所以編寫SFC往往會更短。當創建可以在庫之間共享的通用UI元素時,本地組件狀態在演示級別更有用。對於我們的應用程序的生命週期,我們將重新審視應用程序如何使用Redux管理一般狀態。

現在我們已經編寫了我們的組件,讓我們來看看index.tsx,並用<Hello ... />的渲染替換我們的<App />渲染。

首先我們在文件的頂部引入它:

import Hello from './components/Hello';

然後更改我們的渲染調用:

ReactDOM.render(
  <Hello name="TypeScript" enthusiasmLevel={10} />,
  document.getElementById('root') as HTMLElement
);
  • 鍵入斷言

我們在本節中將要指出的最後一件事就是將document.getElementById('root')作爲HTMLElement。這種語法稱爲類型斷言,有時也稱爲轉換。當您比類型檢查器更瞭解時,這是一種很有用的方式,告訴TypeScript表達式的真實類型是什麼。

在這種情況下我們需要這樣做的原因是getElementById的返回類型是HTMLElement |null。簡單來說,當getElementById找不到具有給定ID的元素時,返回null。我們假設getElementById實際上會成功,所以我們需要使用as語法來說服它的TypeScript。

TypeScript還有一個尾隨的“bang”語法(!),它從先前的表達式中去除了null和undefined。所以我們可以編寫document.getElementById('root')!但是在這種情況下,我們想要更加明確。

  • 添加樣式�

使用我們的設置對組件進行樣式編寫很簡單。爲了調整我們的Hello組件,我們可以在src/components/Hello.css創建一個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();
});

這些測試是非常基本的,但你應該能領會到其中的要點。

  • 添加狀態管理

在這一點上,如果您正在使用React獲取數據並顯示它,您可以考慮自己完成。但是,如果您正在開發更具互動性的應用程序,那麼您可能需要添加狀態管理。

  • 常用狀態管理

React是一個用於創建可組合視圖的有用庫。但是,React並沒有任何在應用程序之間同步數據的功能。就React組件而言,數據通過您在每個元素上指定的props向下流過其子項。

由於React本身不提供內部的狀態管理支持,所以React社區使用像Redux和MobX這樣的庫。

Redux依賴於通過集中和不可變的數據存儲來同步數據,並且該數據的更新將觸發我們的應用程序的重新渲染。狀態通過發送必須由稱爲reducers的函數處理的顯式操作消息以不變的方式更新。由於明確的性質,通常更容易理解一個行爲將如何影響你的程序的狀態。

MobX依賴於功能反應模式,其中狀態通過可觀測量包裹並作爲道具傳遞。通過簡單地將狀態標記爲可觀察來完成任何觀察者的狀態完全同步。作爲一個很好的獎勵,該庫已經在TypeScript中編寫。

兩者都有不同的優點和權衡。一般來說,Redux往往會看到更廣泛的使用,所以爲了本教程的目的,我們將專注於添加Redux;但是,你應該感到鼓舞去開發這兩者。

以下部分可能具有陡峭的學習曲線。我們強烈建議您通過其文檔熟悉Redux

  • 設置動作來源

添加Redux是沒有意義的,除非我們的應用程序的狀態發生變化。我們需要一個可以觸發更改的動作來源。這可以是一個定時器,或者像UI中的某個按鈕。

爲了我們的目的,我們將添加兩個按鈕來控制我們的Hello組件的enthusiasm level。

  • 安裝 Redux

要添加Redux,我們將首先安裝redux和react-redux以及它們的類型作爲依賴。

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

在這種情況下,我們不需要安裝@types/redux,因爲Redux已經有自己的定義文件(.d.ts文件)。

  • 定義app狀態

我們需要定義Redux將存儲的狀態的形狀。爲此,我們可以創建一個名爲src/types/index.tsx的文件,該文件將包含整個程序中可能使用的類型的定義。

// src/types/index.tsx

export interface StoreState {
    languageName: string;
    enthusiasmLevel: number;
}

我們的意圖是languageName將是此應用程序編寫的編程語言(即TypeScript或JavaScript),而enthusiasmLevel值也會改變。當我們寫第一個容器時,我們會明白爲什麼我們故意使我們的狀態與我們的props略有不同。

  • 添加行爲

我們首先創建一組我們的應用程序可以在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;

這種常量/類型模式允許我們以易於訪問和可重構的方式使用TypeScript的字符串文字類型。

接下來,我們將創建一組可以在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)來描述動作可以是增量或減量的情況。最後,我們做了兩個功能,實際上製造了我們可以使用的動作,而不是寫出龐大的對象文字。

這裏有明顯的樣板代碼,所以你可以隨時查看像redux-actions這樣的庫當你有相關需要的話。

  • 添加reducer

我們準備寫下我們的第一個減速機!減少器只是通過創建我們應用程序狀態的修改副本而產生更改的功能,但沒有任何副作用。換句話說,它們就是我們所說的純功能

我們的reducer將在src/reducers/index.tsx下。其功能是確保增量提高1點,減量降低1點,但enthusiasm level 不低於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;
}

注意,我們正在使用對象spread(... state),它允許我們創建一個淺狀態的副本,同時替換enthusiasmLevel。重要的是,enthusiasmLevel在最後,否則將被舊狀態所覆蓋。

您可能想爲您的reducer編寫一些測試。由於reducer是純函數,它們可以被傳遞任意數據。對於每個輸入,減速器可以通過檢查其新產生的狀態進行測試。考慮研究Jest的toEqual方法來實現這一點。

  • 製作container

在使用Redux進行寫入時,我們經常會寫入組件以及容器。組件通常與數據無關,並且主要在演示層面上工作。容器通常包裝組件併爲他們提供顯示和修改狀態所需的任何數據。您可以在丹·阿布拉莫夫的文章“展示和集裝箱組件”上更多地瞭解這一概念。

首先讓我們更新src/components/Hello.tsx,以便它可以修改狀態。我們將向名爲onIncrement和onDecrement的Props添加兩個可選回調屬性:

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功能。連接將能夠實際使用我們原來的Hello組件,並使用兩個功能將其變成容器:

  • mapStateToProps將數據從當前存儲區按部件形狀組織所需。

  • mapDispatchToProps創建回調道具,以使用給定的調度功能將操作泵送到我們的商店。

如果我們記得,我們的應用狀態由兩個屬性組成:languageName和enthusiasmLevel。另一方面,我們的Hello組件預計會有一個名字和一個熱情。 mapStateToProps將從商店獲取相關數據,並在必要時對我們組件的props進行調整。讓我們繼續。

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

請注意,mapStateToProps僅創建Hello組件期望的屬性四個中的2個。也就是說,我們仍然希望通過onIncrement和onDecrement回調。 mapDispatchToProps是一個採用調度程序功能的函數。此調度程序功能可以將操作傳遞到我們的存儲中進行更新,因此我們可以創建一對可以根據需要調用調度程序的回調函數。

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,並將其與所有的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包含了我們應用程序的全局狀態。

接下來,我們將使用./src/containers/Hello交換我們對./src/components/Hello的使用,並使用react-redux的Provider將我們的props與我們的容器連接起來。我們將導入每個:

import Hello from './containers/Hello';
import { Provider } from 'react-redux';

並將我們的store通過Provider's的屬性:
ReactDOM.render(
  <Provider store={store}>
    <Hello />
  </Provider>,
  document.getElementById('root') as HTMLElement
);

注意,Hello不再需要props,因爲我們使用我們的連接功能來適應我們包裝Hello組件pops應用程序的狀態。

  • Ejecting

如果在任何時候,您覺得create-react-app設置特定的自定義有些困難,您可以隨時選擇退出並獲取所需的各種配置選項。例如,如果您想添加一個Webpack插件,可能需要利用create-react-app 提供的"eject"功能。

npm run eject

這樣你就可以更好地進行工作了。

  • 推薦

create-react-app帶有很多好東西。其中大部分記錄在爲我們的項目生成的默認README.md中,因此可以快速閱讀。

如果您還想了解有關Redux的更多信息,您可以查看官方網站的文檔。 MobX也一樣。

如果你想在某個時候eject,你可能需要更多地瞭解Webpack。您可以在這裏查看我們的React&Webpack

在某些時候你可能需要路由。有幾個解決方案,但是react-router可能是Redux項目中最受歡迎的,並且通常與react-router-redux一起使用。


原地址:https://zhuanlan.zhihu.com/p/27847933

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