React

    • 使用TypeScript和React创建工程
    • 使用TSLint进行代码检查
    • 使用和Enzyme进行测试,以及
    • 使用管理状态

    我们会使用create-react-app工具快速搭建工程环境。

    这里假设你已经在使用和npm。 并且已经了解了。

    我们之所以使用create-react-app是因为它能够为React工程设置一些有效的工具和权威的默认参数。 它仅仅是一个用来搭建React工程的命令行工具而已。

    创建新工程

    让我们首先创建一个叫做的新工程:

    1. create-react-app my-app --scripts-version=react-scripts-ts

    是一系列适配器,它利用标准的create-react-app工程管道并把TypeScript混入进来。

    此时的工程结构应如下所示:

    1. my-app/
    2. ├─ .gitignore
    3. ├─ node_modules/
    4. ├─ public/
    5. ├─ src/
    6. └─ ...
    7. ├─ package.json
    8. ├─ tsconfig.json
    9. └─ tslint.json

    注意:

    • tsconfig.json包含了工程里TypeScript特定的选项。
    • tslint.json保存了要使用的代码检查器的设置,TSLint
    • package.json包含了依赖,还有一些命令的快捷方式,如测试命令,预览命令和发布应用的命令。
    • public包含了静态资源如HTML页面或图片。除了index.html文件外,其它的文件都可以删除。
    • src包含了TypeScript和CSS源码。index.tsx是强制使用的入口文件。

    运行工程

    通过下面的方式即可轻松地运行这个工程。

    1. npm run start

    它会执行package.json里面指定的start命令,并且会启动一个服务器,当我们保存文件时还会自动刷新页面。 通常这个服务器的地址是http://localhost:3000,页面应用会被自动地打开。

    它会保持监听以方便我们快速地预览改动。

    测试也仅仅是一行命令的事儿:

    1. npm run test

    这个命令会运行Jest,一个非常好用的测试工具,它会运行所有扩展名是.test.ts.spec.ts的文件。 好比是npm run start命令,当检测到有改动的时候Jest会自动地运行。 如果喜欢的话,你还可以同时运行npm run startnpm run test,这样你就可以在预览的同时进行测试。

    生成生产环境的构建版本

    在使用npm run start运行工程的时候,我们并没有生成一个优化过的版本。 通常我们想给用户一个运行的尽可能快并在体积上尽可能小的代码。 像压缩这样的优化方法可以做到这一点,但是总是要耗费更多的时间。 我们把这样的构建版本称做“生产环境”版本(与开发版本相对)。

    要执行生产环境的构建,可以运行如下命令:

    1. 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

    1. // src/components/Hello.tsx
    2. import * as React from 'react';
    3. export interface Props {
    4. name: string;
    5. enthusiasmLevel?: number;
    6. }
    7. function Hello({ name, enthusiasmLevel = 1 }: Props) {
    8. if (enthusiasmLevel <= 0) {
    9. throw new Error('You could be a little more enthusiastic. :D');
    10. }
    11. return (
    12. <div className="hello">
    13. <div className="greeting">
    14. Hello {name + getExclamationMarks(enthusiasmLevel)}
    15. </div>
    16. </div>
    17. );
    18. }
    19. export default Hello;
    20. // helpers
    21. function getExclamationMarks(numChars: number) {
    22. return Array(numChars + 1).join('!');
    23. }

    注意我们定义了一个类型Props,它指定了我们组件要用到的属性。 name是必需的且为string类型,同时enthusiasmLevel是可选的且为number类型(你可以通过名字后面加?为指定可选参数)。

    我们创建了一个函数组件Hello。 具体来讲,Hello是一个函数,接收一个Props对象并拆解它。 如果Props对象里没有设置enthusiasmLevel,默认值为1

    使用函数是React中定义组件的两种方式之一。 如果你喜欢的话,也可以通过类的方式定义:

    1. class Hello extends React.Component<Props, object> {
    2. render() {
    3. const { name, enthusiasmLevel = 1 } = this.props;
    4. if (enthusiasmLevel <= 0) {
    5. throw new Error('You could be a little more enthusiastic. :D');
    6. }
    7. return (
    8. <div className="hello">
    9. Hello {name + getExclamationMarks(enthusiasmLevel)}
    10. </div>
    11. </div>
    12. );
    13. }
    14. }

    当我们的的时候,使用类的方式是很有用处的。 但在这个例子里我们不需要考虑状态 - 事实上,在React.Component<Props, object>我们把状态指定为了object,因此使用函数组件更简洁。 当在创建可重用的通用UI组件的时候,在表现层使用组件局部状态比较适合。 针对我们应用的生命周期,我们会审视应用是如何通过Redux轻松地管理普通状态的。

    现在我们已经写好了组件,让我们仔细看看index.tsx,把<App />替换成<Hello ... />

    首先我们在文件头部导入它:

    1. import Hello from './components/Hello';

    然后修改render调用:

    1. ReactDOM.render(
    2. <Hello name="TypeScript" enthusiasmLevel={10} />,
    3. document.getElementById('root') as HTMLElement
    4. );

    这里还有一点要指出,就是最后一行document.getElementById('root') as HTMLElement。 这个语法叫做类型断言,有时也叫做转换。 当你比类型检查器更清楚一个表达式的类型的时候,你可以通过这种方式通知TypeScript。

    这里,我们之所以这么做是因为getElementById的返回值类型是HTMLElement | null。 简单地说,getElementById返回null是当无法找对对应id元素的时候。 我们假设总是成功的,因此我们要使用as语法告诉TypeScript这点。

    通过我们的设置为一个组件添加样式很容易。 若要设置Hello组件的样式,我们可以创建这样一个CSS文件src/components/Hello.css

    create-react-app包含的工具(Webpack和一些加载器)允许我们导入样式表文件。 当我们构建应用的时候,所有导入的.css文件会被拼接成一个输出文件。 因此在src/components/Hello.tsx,我们需要添加如下导入语句。

    1. import './Hello.css';

    使用Jest编写测试

    如果你没使用过Jest,你可能先要把它安装为开发依赖项。

    1. npm install -D jest jest-cli jest-config

    我们对Hello组件有一些假设。 让我们在此重申一下:

    我们将针对这些需求为组件写一些注释。

    但首先,我们要安装Enzyme。 是React生态系统里一个通用工具,它方便了针对组件的行为编写测试。 默认地,我们的应用包含了一个叫做jsdom的库,它允许我们模拟DOM以及在非浏览器的环境下测试运行时的行为。 Enzyme与此类似,但是是基于jsdom的,并且方便我们查询组件。

    让我们把它安装为开发依赖项。

    1. npm install -D enzyme @types/enzyme enzyme-adapter-react-16 @types/enzyme-adapter-react-16

    如果你的react版本低于15.5.0,还需安装如下

    1. npm install -D react-addons-test-utils

    注意我们同时安装了enzyme@types/enzymeenzyme包指的是包含了实际运行的JavaScript代码包,而@types/enzyme则包含了声明文件(.d.ts文件)的包,以便TypeScript能够了解该如何使用Enzyme。 你可以在这里了解更多关于@types包的信息。

    我们还需要安装enzyme-adapterreact-addons-test-utils。 它们是使用enzyme所需要安装的包,前者作为配置适配器是必须的,而后者若采用的React版本在15.5.0之上则毋需安装。

    现在我们已经设置好了Enzyme,下面开始编写测试! 先创建一个文件src/components/Hello.test.tsx,与先前的Hello.tsx文件放在一起。

    1. // src/components/Hello.test.tsx
    2. import * as React from 'react';
    3. import * as enzyme from 'enzyme';
    4. import * as Adapter from 'enzyme-adapter-react-16';
    5. import Hello from './Hello';
    6. enzyme.configure({ adapter: new Adapter() });
    7. it('renders the correct text when no enthusiasm level is given', () => {
    8. const hello = enzyme.shallow(<Hello name='Daniel' />);
    9. expect(hello.find(".greeting").text()).toEqual('Hello Daniel!')
    10. });
    11. it('renders the correct text with an explicit enthusiasm of 1', () => {
    12. const hello = enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={1}/>);
    13. expect(hello.find(".greeting").text()).toEqual('Hello Daniel!')
    14. });
    15. it('renders the correct text with an explicit enthusiasm level of 5', () => {
    16. const hello = enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={5} />);
    17. expect(hello.find(".greeting").text()).toEqual('Hello Daniel!!!!!');
    18. });
    19. it('throws when the enthusiasm level is 0', () => {
    20. expect(() => {
    21. enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={0} />);
    22. }).toThrow();
    23. });
    24. it('throws when the enthusiasm level is negative', () => {
    25. expect(() => {
    26. enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={-1} />);
    27. }).toThrow();
    28. });

    这些测试都十分基础,但你可以从中得到启发。

    添加state管理

    到此为止,如果你使用React的目的是只获取一次数据并显示,那么你已经完成了。 但是如果你想开发一个可以交互的应用,那么你需要添加state管理。

    state管理概述

    React本身就是一个适合于创建可组合型视图的库。 但是,React并没有任何在应用间同步数据的功能。 就React组件而言,数据是通过每个元素上指定的props向子元素传递。

    因为React本身并没有提供内置的state管理功能,React社区选择了Redux和MobX库。

    依靠一个统一且不可变的数据存储来同步数据,并且更新那里的数据时会触发应用的更新渲染。 state的更新是以一种不可变的方式进行,它会发布一条明确的action消息,这个消息必须被reducer函数处理。 由于使用了这样明确的方式,很容易弄清楚一个action是如何影响程序的state。

    MobX借助于函数式响应型模式,state被包装在了可观察对象里,并通过props传递。 通过将state标记为可观察的,即可在所有观察者之间保持state的同步性。 另一个好处是,这个库已经使用TypeScript实现了。

    这两者各有优缺点。 但Redux使用得更广泛,因此在这篇教程里,我们主要看如何使用Redux; 但是也鼓励大家两者都去了解一下。

    后面的小节学习曲线比较陡。 因此强烈建议大家先去。

    设置actions

    只有当应用里的state会改变的时候,我们才需要去添加Redux。 我们需要一个action的来源,它将触发改变。 它可以是一个定时器或者UI上的一个按钮。

    为此,我们将增加两个按钮来控制Hello组件的感叹级别。

    安装reduxreact-redux以及它们的类型文件做为依赖。

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

    这里我们不需要安装@types/redux,因为Redux已经自带了声明文件(.d.ts文件)。

    定义应用的状态

    我们需要定义Redux保存的state的结构。 创建src/types/index.tsx文件,它保存了类型的定义,我们在整个程序里都可能用到。

    1. // src/types/index.tsx
    2. export interface StoreState {
    3. languageName: string;
    4. enthusiasmLevel: number;
    5. }

    这里我们想让languageName表示应用使用的编程语言(例如,TypeScript或者JavaScript),enthusiasmLevel是可变的。 在写我们的第一个容器的时候,就会明白为什么要令state与props稍有不同。

    添加actions

    下面我们创建这个应用将要响应的消息类型,src/constants/index.tsx

    1. // src/constants/index.tsx
    2. export const INCREMENT_ENTHUSIASM = 'INCREMENT_ENTHUSIASM';
    3. export type INCREMENT_ENTHUSIASM = typeof INCREMENT_ENTHUSIASM;
    4. export const DECREMENT_ENTHUSIASM = 'DECREMENT_ENTHUSIASM';
    5. export type DECREMENT_ENTHUSIASM = typeof DECREMENT_ENTHUSIASM;

    这里的const/type模式允许我们以容易访问和重构的方式使用TypeScript的字符串字面量类型。

    接下来,我们创建一些actions以及创建这些actions的函数,src/actions/index.tsx

    1. import * as constants from '../constants'
    2. export interface IncrementEnthusiasm {
    3. type: constants.INCREMENT_ENTHUSIASM;
    4. }
    5. export interface DecrementEnthusiasm {
    6. type: constants.DECREMENT_ENTHUSIASM;
    7. }
    8. export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm;
    9. export function incrementEnthusiasm(): IncrementEnthusiasm {
    10. return {
    11. }
    12. }
    13. export function decrementEnthusiasm(): DecrementEnthusiasm {
    14. return {
    15. type: constants.DECREMENT_ENTHUSIASM
    16. }
    17. }

    这里有一些清晰的模版,你可以参考类似的库。

    现在我们可以开始写第一个reducer了! Reducers是函数,它们负责生成应用state的拷贝使之产生变化,但它并没有副作用。 它们是一种纯函数

    我们的reducer将放在src/reducers/index.tsx文件里。 它的功能是保证增加操作会让感叹级别加1,减少操作则要将感叹级别减1,但是这个级别永远不能小于1。

    注意我们使用了对象展开...state),当替换enthusiasmLevel时,它可以对状态进行浅拷贝。 将enthusiasmLevel属性放在末尾是十分关键的,否则它将被旧的状态覆盖。

    你可能想要对reducer写一些测试。 因为reducers是纯函数,它们可以传入任意的数据。 针对每个输入,可以测试reducers生成的新的状态。 可以考虑使用Jest的方法。

    创建容器

    在使用Redux时,我们常常要创建组件和容器。 组件是数据无关的,且工作在表现层。 容器通常包裹组件及其使用的数据,用以显示和修改状态。 你可以在这里阅读更多关于这个概念的细节:。

    现在我们修改src/components/Hello.tsx,让它可以修改状态。 我们将添加两个可选的回调属性到Props,它们分别是onIncrementonDecrement

    1. export interface Props {
    2. name: string;
    3. enthusiasmLevel?: number;
    4. onIncrement?: () => void;
    5. onDecrement?: () => void;
    6. }

    然后将这两个回调绑定到两个新按钮上,将按钮添加到我们的组件里。

    1. function Hello({ name, enthusiasmLevel = 1, onIncrement, onDecrement }: Props) {
    2. if (enthusiasmLevel <= 0) {
    3. throw new Error('You could be a little more enthusiastic. :D');
    4. }
    5. return (
    6. <div className="hello">
    7. <div className="greeting">
    8. Hello {name + getExclamationMarks(enthusiasmLevel)}
    9. </div>
    10. <div>
    11. <button onClick={onDecrement}>-</button>
    12. <button onClick={onIncrement}>+</button>
    13. </div>
    14. </div>
    15. );
    16. }

    通常情况下,我们应该给onIncrementonDecrement写一些测试,它们是在各自的按钮被点击时调用。 试一试以便掌握编写测试的窍门。

    现在我们的组件更新好了,可以把它放在一个容器里了。 让我们来创建一个文件src/containers/Hello.tsx,在开始的地方使用下列导入语句。

    1. import Hello from '../components/Hello';
    2. import * as actions from '../actions/';
    3. import { StoreState } from '../types/index';
    4. import { connect, Dispatch } from 'react-redux';

    两个关键点是初始的Hello组件和react-redux的connect函数。 connect可以将我们的Hello组件转换成一个容器,通过以下两个函数:

    • mapStateToProps将当前store里的数据以我们的组件需要的形式传递到组件。
    • mapDispatchToProps利用dispatch函数,创建回调props将actions送到store。

    回想一下,我们的应用包含两个属性:languageNameenthusiasmLevel。 我们的Hello组件,希望得到一个name和一个enthusiasmLevelmapStateToProps会从store得到相应的数据,如果需要的话将针对组件的props调整它。 下面让我们继续往下写。

    1. export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
    2. return {
    3. enthusiasmLevel,
    4. name: languageName,
    5. }
    6. }

    注意mapStateToProps仅创建了Hello组件需要的四个属性中的两个。 我们还想要传入onIncrementonDecrement回调函数。 mapDispatchToProps是一个函数,它需要传入一个调度函数。 这个调度函数可以将actions传入store来触发更新,因此我们可以创建一对回调函数,它们会在需要的时候调用调度函数。

    1. export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
    2. return {
    3. onIncrement: () => dispatch(actions.incrementEnthusiasm()),
    4. onDecrement: () => dispatch(actions.decrementEnthusiasm()),
    5. }
    6. }

    最后,我们可以调用connect了。 connect首先会接收mapStateToPropsmapDispatchToProps,然后返回另一个函数,我们用它来包裹我们的组件。 最终的容器是通过下面的代码定义的:

    1. export default connect(mapStateToProps, mapDispatchToProps)(Hello);

    现在,我们的文件应该是下面这个样子:

    1. // src/containers/Hello.tsx
    2. import Hello from '../components/Hello';
    3. import * as actions from '../actions/';
    4. import { StoreState } from '../types/index';
    5. import { connect, Dispatch } from 'react-redux';
    6. export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
    7. return {
    8. enthusiasmLevel,
    9. name: languageName,
    10. }
    11. }
    12. export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
    13. return {
    14. onIncrement: () => dispatch(actions.incrementEnthusiasm()),
    15. onDecrement: () => dispatch(actions.decrementEnthusiasm()),
    16. }
    17. }
    18. export default connect(mapStateToProps, mapDispatchToProps)(Hello);

    创建store

    让我们回到src/index.tsx。 要把所有的东西合到一起,我们需要创建一个带初始状态的store,并用我们所有的reducers来设置它。

    1. import { createStore } from 'redux';
    2. import { enthusiasm } from './reducers/index';
    3. import { StoreState } from './types/index';
    4. const store = createStore<StoreState>(enthusiasm, {
    5. enthusiasmLevel: 1,
    6. languageName: 'TypeScript',
    7. });

    store可能正如你想的那样,它是我们应用全局状态的核心store。

    接下来,我们将要用./src/containers/Hello来包裹./src/components/Hello,然后使用react-redux的Provider将props与容器连通起来。 我们将导入它们:

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

    storeProvider的属性形式传入:

    注意,Hello不再需要props了,因为我们使用了connect函数为包裹起来的Hello组件的props适配了应用的状态。

    如果你发现create-react-app使一些自定义设置变得困难,那么你就可以选择不使用它,使用你需要配置。 比如,你要添加一个Webpack插件,你就可以利用create-react-app提供的“eject”功能。

    运行:

      这样就可以了!

      你要注意,在运行eject前最好保存你的代码。 你不能撤销eject命令,因此退出操作是永久性的除非你从一个运行eject前的提交来恢复工程。

      下一步

      create-react-app带有很多很棒的功能。 它们的大多数都在我们工程生成的里面有记录,所以可以简单阅读一下。

      如果你想学习更多关于Redux的知识,你可以前往官方站点查看文档。 同样的,官方站点。

      如果你想要在某个时间点eject,你需要了解再多关于Webpack的知识。 你可以查看React & Webpack教程

      有时候你需要路由功能。 已经有一些解决方案了,但是对于Redux工程来讲是最流行的,并经常与react-router-redux联合使用。