TypeScript + React + Redux 的个人总结

使用 TypeScript 编写 React 需要注意的规范

必须遵守的要求:

  • 所有用到 jsx 语法的文件都需要以 tsx 后缀命名
  • 使用组件声明时的 Component<P, S> 泛型参数声明,来代替 PropTypes进行类型校验

额外的代码规范:

  • 全局变量或者自定义的 window 对象属性,统一在项目根下的 global.d.ts 中进行声明定义
  • 对于项目中常用到的接口数据对象,最好在 types/ 目录下定义好其结构化类型声明

创建项目

npx create-react-app typescript-react-app --scripts-version=react-scripts-ts

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

项目创建成功后,此时项目结构如下所示:

typescript-react-app/
├─ node_modules/
├─ public/
├─ src/
│  └─ ...
├─ .gitignore
├─ images.d.ts
├─ package.json
├─ README.md
├─ tsconfig.json
├─ tsconfig.prod.json
├─ tsconfig.test.json
├─ tslint.json
└─ yarn.lock

注意:

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

运行项目

先运行项目,看看是否能够正常启动,如果可以,说明项目创建没有问题。 运行命令:

$ npm run start

# 或者运行 yarn run start

React 配合 TypeScript 的基本使用

在当前项目中,可以看到 index.tsx 和 App.jsx 文件中已经使用了 TypeScript,我们现在自己来用 TypeScript 编写一个 React 组件吧。

定义一个 Counter 组件

我们在 src 下创建一个 container目录,新增 Counter 组件:

Counter.tsx

import * as React from 'react';


// 创建类型接口
export interface Iprops {
    value: number
}

// 使用接口代替 PropTypes 进行类型校验:函数组件
const Counter = ({ value }: Iprops) => {
    return <p>Clicked: { value } times</p>
}

// 使用接口代替 PropTypes 进行类型校验:使用类的方式
const Counter = ({ value }: Iprops) => {
    return <p>Clicked: { value } times</p>
}

export default Counter;

我们可以使用函数组件或者类组件,注意两者的区别是类方式组件有react的生命周期,而函数组件没有,建议函数组件来做纯显示

在 App.tsx 中引用 Counter 组件并展示

import * as React from 'react';
import './App.css';

import Counter from './components/Counter.jsx';
// import logo from './logo.svg';

class App extends React.Component {
  public render() {
    return (
      <div className="App">
        <Counter value={ 0 } />
      </div>
    );
  }
}

export default App;

运行项目:npm run start,可以看到浏览器中展示出了 Clicked: 0 times,说明我们第一个 Counter 组件已经编写并使用成功了。

进阶:配合 Redux 进行使用

安装项目需要的插件

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

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

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

定义应用的状态 State

一般会将常用的结构类型存放到 /types 目录下。所以我们在 src 目录下新建 types 目录。此时项目中只有一个 state,就是 Counter 中的点击次数,所以就没有使用借口来作为约束,而是直接使用了 type。

type/index.ts

// 定义 State 结构类型
export type StoreState = number;

添加 actions-type

在 src 下创建 store目录,在const.ts 文件中添加需要响应的消息类型

src/store/const.ts

// 定义增加 state 类型常量
export const INCREMENT = "INCREMENT";
export type INCREMENT_TYPE = typeof INCREMENT;

// 定义减少 state 类型常量
export const DECREMENT = "DECREMENT";
export type DECREMENT_TYPE = typeof DECREMENT;

这里的const/type模式允许我们以容易访问和重构的方式使用TypeScript的字符串字面量类型。 接下来,我们创建一些 actions 以及创建这些 actions 的函数

添加 actions

src/store/actions/index.ts

import {DECREMENT, DECREMENT_TYPE, INCREMENT, INCREMENT_TYPE} from '../const'

export interface IINCREMENTAction {
  type: INCREMENT_TYPE;
}

export interface IDECREMENTAction {
  type: DECREMENT_TYPE;
}

// 定义 modifyAction 类型,包含 IINCREMENTAction 和 IDECREMENTAction 接口类型
export type ModifyAction = IINCREMENTAction | IDECREMENTAction;


// 增加 state 次数的方法
export const increment = (): IINCREMENTAction => ({
  type: INCREMENT,
})

// 减少 state 次数的方法
export const decrement = (): IDECREMENTAction => ({
  type: DECREMENT
})

添加 reducer

我们的reducer将放在src/reducers/index.tsx文件里。 它的功能是保证增加操作会让 times 加1,减少操作则要将 times 减1。

reducers/index.tsx

import { ModifyAction } from '../actions';
import { DECREMENT, INCREMENT } from '../const';


// 处理并返回 state 
export default (state = 0, action: ModifyAction): number => {
    switch (action.type) {
      case INCREMENT:
        return state + 1
      case DECREMENT:
        return state - 1
      default:
        return state
    }
}

注意上面整个reducer只有一个reducer,就是一个number,如果需要多个reducer可以combineReducers组建多个reducer

import { combineReducers } from 'redux'

// 一个state
function count (state = 0, action: ModifyAction): number => {
    switch (action.type) {
      case INCREMENT:
        return state + 1
      case DECREMENT:
        return state - 1
      default:
        return state
    }
    
function test (state = 0, action: ModifyAction): number => {
    switch (action.type) {
      ...
      default:
        return state
    }
    
 这样可以吧store变成一个对象来组合reducer = state
 const rootReducer = combineReducers({
   count,
   test
})

创建容器组件

创建一个 container 目录,用来存放需要与数据交互的组件,新建 CounterCon.tsx 文件.

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

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

import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';

import { decrement, increment } from '../store/actions';
import { StoreState } from '../types';


// 创建类型接口
export interface IProps {
  value: number,
  onIncrement: () => void,
  onDecrement: () => void
}

// 使用接口代替 PropTypes 进行类型校验
 class Counter extends React.PureComponent<IProps> {
  public componentWillMount () {
    // tslint:disable-next-line
    console.log(this.props) // 这里的prop是拿不到dispatch函数,因为组合高阶函数的时候做了处理,没有传入dispatch,只有{value: 0, onDecrement: ƒ, onIncrement: ƒ}
  }
  
  public render() {
      const { value, onIncrement, onDecrement } = this.props;
      return (
          <p>
              Clicked: { value } times
              <br />
              <br />
              <button onClick={ onIncrement } style={{ marginRight: 20 }}> +  </button>
              <button onClick={ onDecrement }> - </button>
          </p>
      )
  }
}

// 将 reducer 中的状态插入到组件的 props 中
// 下面是单个reducer的时候,多个的时候需要选传入哪个reducer
// const { test, count } = state
const mapStateToProps = (state: StoreState): { value: number } => ({
  value: state
})

// 将 对应action 插入到组件的 props 中
const mapDispatchToProps = (dispatch: Dispatch) => ({
  onDecrement: () => dispatch(decrement()),
  onIncrement: () => dispatch(increment())
})

// 使用 connect 高阶组件对 Counter 进行包裹
export default connect(mapStateToProps, mapDispatchToProps)(Counter);

另外如果connect的时候不传入数据的时候connect()(Test),组件的this.prop只能拿到dispatch的函数,和外层父元素的props,并不能拿到store的数据,而且高阶函数返回一个可以注入store的prop的组件,依然可以接受外层的props,所以注意reducer必须要注入,否则没有,只有dispatch,而且必须是一个对象!!!!!!

创建 store

让我们回到src/index.tsx。 要把所有的东西合到一起,我们需要创建一个带初始状态的store,并用我们所有的reducers来设置它。 并且使用 react-redux 的 Provider 将 props 和 容器连接起来

index.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension'

import App from './App';
import './index.css';
import reducer from './reducer'; 
import registerServiceWorker from './registerServiceWorker';


// 1、创建 store
const store = createStore(reducer, composeWithDevTools());

ReactDOM.render(
    // 2、然后使用react-redux的Provider将props与容器连通起来
    <Provider store={ store }>
        <App />
    </Provider> ,
    document.getElementById('root') as HTMLElement
);
registerServiceWorker();

其中composeWithDevTools是Chrome里面的调试工具

回到我们的 App.tsx 文件中。改写如下:

App.tsx

import * as React from 'react';
import './App.css';
import Counter from './containers/Counter'
import logo from './logo.svg';

class App extends React.Component {
  public render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <Counter/>
      </div>
    );
  }
}

export default App;

注意,此时 Counter不再需要 props 了,因为我们使用了 connect 函数为包裹起来的 Hello 组件的 props 适配了应用的状态,当然也可以传进去,props也可以拿到的。

此时,运行项目,点击 + 或者 - 按钮,即可看到 times 的次数会发生变化。

总结

至此,对于使用 TypeScript 编写 React 应用应该有了一定的了解。其实写法也比较固定,刚接触的话可能有些地方容易出现问题,多写几个组件之后,应该就没什么问题了。在编写项目的过程中,create-react-app 自带的 tslint 可能要求比较严严格,比如:

  • 在标签里不允许使用 lambda 表达式,在 tslint.json 文件 rules 属性中添加:"jsx-no-lambda": false 即可
  • 在导入模块时,必须按照字母顺序导入,在 tslint.json 文件 rules 属性中添加:"ordered-imports": false 即可

还有很多别的配置,有需要的话,可以查看文档:TSLint core rules

参考文档

[使用 TypeScript + React + Redux 进行项目开发(入门篇,附源码]

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容