这个快速入门指南将教你如何使用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