Home

Awesome

TypeScript React 入门 translate-svg

校对 ✅

<!-- doc-templite START generated --> <!-- repo = 'Microsoft/TypeScript-React-Starter' --> <!-- commit = 'b016ad12ebdff80869a9248c3357f498dcdbb06b' --> <!-- time = '2018-7-10' -->
翻译的原文与日期最新更新更多
commit⏰ 2018-7-10last中文翻译
<!-- doc-templite END generated -->

贡献

欢迎 👏 勘误/校对/更新贡献 😊 具体贡献请看

生活

If help, buy me coffee —— 营养跟不上了,给我来瓶营养快线吧! 💰

<details> <summary> 目录 </summary> <!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> <!-- END doctoc generated TOC please keep comment here to allow auto update --> </details>

TypeScript React Starter

本快速入门指南将教您如何连接TypeScript与React. 到最后,你会有

我们将使用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

值得注意的是

运行项目

运行项目一样简单

npm run start

这运行start,是位于我们package.jsonscript字段中 ,并将在我们保存文件时 重新生成 与 重新加载 页面的服务器. 通常,服务器运行于http://localhost:3000,但应该自动为您打开了.

这允许我们快速预览更改,从而 收紧迭代循环.

测试项目

测试也只是一个命令:

npm run test

此命令针对扩展名.test.ts要么.spec.ts结尾的所有文件运行 Jest,这是一个非常有用的测试实用程序. 像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的类型interface,它指定了我们的组件将采用的属性. name是必需的string,和enthusiasmLevel是可选的number (你可以从中?得知,enthusiasmLevel?: number;).

我们也写了Hello作为无状态功能组件 (SFC) . 再具体一点,Hello是一个需要Props对象接口的函数,并对其进行解构. 如果enthusiasmLevel我们没有值,它将默认为1.

编写函数是两个主要[React 建组件的方式]((https://facebook.github.io/react/docs/components-and-props.html#functional-and-class-components)之一. 如果我们想要,我们可以把它写成一个类如下:

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

类是有用的当 我们 组件实例 有一些状态时 . 但是在这个例子中我们并不需要考虑 状态 - 事实上,我们将其指定为objectReact.Component<Props, object>,因此 编写SFC 往往会更短. 在创建 可以在库之间 共享的通用UI元素 时,本地组件状态 在 表示组件级别上 更有用. 对于我们的应用程序的生命周期,我们将 重新审视应用程序 如何使用Redux管理 通用状态.

现在我们已经编写了我们的组件,让我们深入研究index.tsx并替换我们的渲染<App />与渲染<Hello ... />.

首先,我们将它导入文件的顶部:

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回报null,当它找不到给定的id元素时. 我们假设那样getElementById实际上会成功,所以我们需要使用as语法来说服TypeScript.

TypeScript还有一个尾随"bang"语法 (!) ,从前面的表达式删除nullundefined. 所以我们可以已经写了document.getElementById('root')!,但在这种情况下,我们想要更明确一点.

添加style 😎

使用我们的设置,添加组件样式很简单. 对于我们的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组件的假设. 让我们重申它们是什么:

我们可以使用这些要求为我们的组件编写一些测试.

但首先,让我们安装 Enzyme. Enzyme是React生态系统中的常用工具,可以更轻松地编写 组件行为 方式的测试. 默认情况下,我们的应用程序包含一个名为 jsdom 的库,允许我们模拟DOM 并在没有浏览器的情况下 测试其运行时 行为.

Enzyme类似,但建立在jsdom上,可以更容易地对我们的组件进行某些查询. 让我们将其 安装 为 开发依赖项.

npm install -D enzyme @types/enzyme enzyme-adapter-react-16 @types/enzyme-adapter-react-16 react-test-renderer

注意我们安装了包enzyme以及@types/enzyme. 该enzyme指的是包含实际运行的JavaScript代码的包,而@types/enzyme是一个包含声明文件(.d.ts文件)的包 以便 TypeScript可以理解如何使用Enzyme. 您可以了解更多信息@typeshere.

我们还必须安装enzyme-adapter-react-16 and react-test-renderer. 这是事情enzyme期望安装.

在编写第一个测试之前,我们必须配置 Enzyme以使用React 16 的适配器. 我们将创建一个名为 src/setupTests.ts的文件, 这会在运行测试时自动加载:

import * as enzyme from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';

enzyme.configure({ adapter: new Adapter() });

现在我们已经设置了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依赖于函数反应模式,其中 状态通过 可观察包装 和 作为 props 传递. 通过简单地将 状态标记为可观察状态 来保持 状态完全同步 以 用于任何观察者. 更好的是,该库已经用 TypeScript编写. 两者都有各种优点和权衡.

通常Redux 倾向于看到更广泛的用法,因此为了本教程的目的,我们将专注于添加Redux;但是,你应该感到鼓励去探索两者.

以下部分可能有一个陡峭的学习曲线. 我们强烈建议你通过其文档熟悉Redux

为 actions 设定阶段

除非我们的应用程序状态发生变化,否则 添加Redux 是没有意义的.

我们需要一个可以触发更改的操作源.

这可以是计时器,也可以是U I中的某个按钮. 为了我们的目的,我们将添加两个按钮来控制我们Hello组件的热情程度.

安装Redux

要添加Redux,我们将首先安装reduxreact-redux,以及它们的类型,作为依赖.

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

在这种情况下,我们不需要安装@types/redux因为Redux已经附带了自己的定义文件 (.d.ts文件) .

简单说明 Redux 流程

Redux 本身需要设置的

  1. 要有 基础状态的存储 - Store

  2. 要有 对应更改存储的 操作名/类型 - actions

  3. 而 过滤 不同操作-actions.type后, 返回 对应更改状态 - reducer

  4. 为了 闭合 状态管理的回路, 状态有了, 操作有了, 怎么更改有了, 就是缺了 触发 - dispatch 由Redux本身提供

  5. 组合Redux 所有定义的- createStore

设置好Redux后,与组件混合成为容器

  1. 为了 组件的状态 与 Redux 的状态 联系起来有 mapStateToProps

  2. 为了 组件与Redux 的 触发操作-dispatch 联系有 mapDispatchToProps

  3. 上面两种是配置定义, 而 混合的运行 交由 connect

  4. 最后, 把 整个 store 扔进 React 渲染流程 - Provider

定义我们应用程序的状态

我们需要定义 Redux 将存储的 状态形状. 为此,我们可以创建一个名为src/types/index.tsx的文件,它将包含我们 可能在整个程序中 使用的 类型定义.

// src/types/index.tsx

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

说明: languageName将是这个应用程序编写的编程语言 (即 TypeScript或JavaScript) 和enthusiasmLevel会有所不同. 当我们编写第一个容器时,我们会理解,为什么我们故意使 我们的状态与我们的 props 略有不同.

添加action

让我们从创建一组我们的应用可以响应的消息类型开始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
    }
}

我们创建了两种类型来描述增量减量actions-操作应该是什么样子. 我们还创建了一个类型 (EnthusiasmAction) 描述一个 action 可以是增量或减量的情况. 最后,我们制作了两个函数,来实际制作我们可以使用的 action ,而不是写出庞大的对象文字.

这里有明显的样板,所以你应该随意查看类似redux-actions,一旦你掌握了一切.

添加 减速机-reducer

我们准备好写第一台减速机了! Reducers 过滤 创建应用程序状态 来 生成对应的更改状态,但却是无副作用. 换句话说,他们就是我们所说的*纯函数*.

我们的减速机将进入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 是纯函数,因此可以传递 任意数据. 对于每个输入,可以通过检查 其新生成的状态 来测试减速器. 考虑一下Jest的toEqual方法,去实现这一目标.

制作一个容器

使用 Redux编写时,我们经常会编写 组件和容器. 组件通常与数据无关,并且主要在 表示级别 工作. 容器通常包装组件并向其提供 显示和修改状态 所需的任何数据. 您可以阅读有关此概念的更多信息Dan Abramov's 文章演示和容器组件.

首先让我们更新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组件以及connect函数来自react-redux. connect将能够真正转换 我们的原Hello组件 为 能使用两个函数的容器:

如果我们记得,我们的应用程序状态包含两个属性: languageNameenthusiasmLevel. 另一方面我们的Hello组件预期一个nameenthusiasmLevel. mapStateToProps将从存储 获取 相关数据,并在必要时对 我们组件的 props 进行调整. 让我们继续写下来吧.

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

注意mapStateToProps仅创建4个属性中的2个给了Hello组件期望. 也就是说,我们仍然希望传入onIncrementonDecrement.

onIncrementonDecrement 是 触发 Redux 存储更改的 函数, 往下看

mapDispatchToProps是一个采用 dispatcher-调度程序 功能的函数. 此调度程序功能 可以将 action 传递到我们的存储以进行更新,因此 我们把 dispatch 与 两个on*** 函数联系起来

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

创建存储

我们回去吧src/index.tsx. 要把这些放在一起,我们需要创建一个具有初始状态的存储,并使用我们所有的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/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函数,使我们的应用程序的状态,来适应我们包装的Hello组件的 props .

弹出-Ejecting

如果在任何时候,您觉得 某些自定义设置 使 创建 React应用程序 变得困难,您可以随时选择退出,并获取所需的各种配置选项. 例如,如果您想添加 Webpack插件,可能需要利用 create-react-app 提供的"Ejecting"功能.

简单地

npm run eject

你应该好好注意注意!

一开始,您可能希望在运行弹出之前,提交所有工作. 您无法撤消弹出命令,因此选择退出是永久性的,除非您可以在运行弹出之前,从提交中恢复.

下一步

create-react-app 附带了很多很棒的东西. 其中大部分都记录在项目的README.md,所以请快速阅读.

如果您仍想了解有关Redux的更多信息,您可以Redux官方网址用于文档. 同样的道理MobX官方网址.

如果你想在某些时候弹出,你可能需要更多地了解 Webpack. 你可以看看我们的React & Webpack 携手合奏.

在某些时候,您可能需要路由-router. 有几种解决方案,但是react-router可能是 Redux项目中 最受欢迎的,并且经常与react-router-redux结合使用.