使用 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-redux
的 connect
函数。 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。