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

8,467 阅读17分钟

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
有道云笔记的分享挂了, 直接把源文搬过来, 希望能帮到大家