# React-ts实战梳理

# 1、工程搭建

# 1、安装脚本

npx create-react-app ts-react-app --template typescript
npm i antd axios react-router-dom lodash
npm i @types/react-router-dom @types/lodash -D
npm i redux redux-thunk react-redux
npm i @types/react-redux -D
npm i babel-plugin-import -D
npm i customize-cra react-app-rewired -D
npm i http-server http-proxy-middleware -D
1
2
3
4
5
6
7
8

# 2、工程结构

  • public:静态资源,最终会拷贝至根目录下,对静态文件引用需添加public目录: <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

  • webpack以及启动全部封装在:react-scripts库中,对外不暴露,需使用npm run eject扩展查看

  • 相关组件及库文件

    • UI组件库:antd
    • http:axios
    • router:react-router-dom
    • 状态管理:redux redux-thunk react-redux
    • mock:http-server、http-proxy-middleware
    • 工具类:customize-cra、react-app-rewired、babel-plugin-import、lodash

# 3、antd按需引入

相关资料:React 之 config-overrides文件配置 (opens new window)

解析:组件按需引入customize-cra、react-app-rewired、babel-plugin-import:在项目中配置webpack,可通过在根目录新建一个名称为config-overrides.js的文件进行覆盖;react-app-rewired的作用就是在不eject的情况下,覆盖create-react-app的配置;package.json中启动配置需将react-scripts修改为react-app-rewired

//config-overrides.js,引入组件时自动引入样式
const { override, fixBabelImports} = require('customize-cra');
module.exports = override(
		fixBabelImports('import', {
				libraryName: 'antd',
      	libraryDirectory: 'es',
				style: 'css'
		})
);
1
2
3
4
5
6
7
8
9

# 4、本地mock配置

使用http-server启动静态服务器、使用http-proxy-middleware进行请求代理

  1. 本地安装http-server启动本地http服务,package.json配置启动脚本:"server": "cd mock && http-server -p 4000 -a localhost"

解析:webpack-dev-server将public目录下文件也放置在静态服务器下,但一般不使用该目录放置mock文件,影响实际工程。

  1. 安装http-proxy-middleware插件,在src目录下配置setupProxy.js配置文件,脚手架启动时会自动读取
const { createProxyMiddleware } = require("http-proxy-middleware")
module.exports = function(app) {
    app.use(createProxyMiddleware('/api/**/*.action', {
        target: 'http://localhost:4000',
        pathRewrite(path) {
            return path.replace('/api', '/').replace('.action', '.json');
        }
    }));
};
1
2
3
4
5
6
7
8
9

# 2、组件封装

# 1、组件演化

组件复用方式 优势 略势 状态
类组件(class) 发展时间长,接受度广泛 只能继承父类 传统模式,长期存在
Mixin 可以复制任意对象的任意多个方法 组件相互依赖、耦合,可能产生冲突,不利用维护 被抛弃
高阶组件(HOC) 利用装饰器模式,在不改变组件的基础上,动态地为其添加新的能力 嵌套过多调试困难,需要遵循某些约定(不改变原始组件,透传props等) 能力强大,应用广泛
Hooks 代替class,多个Hooks互不影响,避免嵌套地狱,开发效率高 切换到新思维需要成本 React的未来

# 2、函数组件

函数组件:传统组件的区别为组件状态指定类型

React.FC<P>:props隐含children声明,函数属性自动提醒

import React from 'react'
import {Button} from 'antd'

//常规定义
/*
interface Greeting {
    name: string;
    firstName: string;
    lastName: string;
}
const Hello = (props: Greeting) => <Button>Hello {props.name}</Button>
Hello.defaultProps = {
    firstName: "",
    lastName: ""
}
*/

//函数式组件,官方提供React.FC接口,后续可能废弃,不推荐使用
//优点:props隐含children声明、函数属性自动提示,但默认属性设置时必须为可选属性
//type React.FC<P = {}> = React.FunctionComponent<P>,泛型约束的为组件的state
interface Greeting {
    name: string;
    firstName?: string;
    lastName?: string;
}
const Hello: React.FC<Greeting> = ({ name }) => <Button>Hello {name}</Button>
Hello.defaultProps = {
    firstName: "",
    lastName: ""
}

export default Hello
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 3、类组件

类组件:传统组件的区别为组件指定类型,包括属性和状态,Component<P,S>

import React, {Component} from 'react'
import {Button} from 'antd'

interface Greeting {
    name: string;
    firstName?: string;
    lastName?: string;
}

interface State {
    count: number
}

//react的声明文件中对所有api进行了重新定义
//Component定义为泛型类:class Component<P = {}, S = {}, SS = any>
//P属性类型、S为状态类型,默认都为空对象
class HelloClass extends Component<Greeting, State>{
    //初始化状态
    state: State = {
        count: 0
    }
    //设置默认值
    static defaultProps ={
        firstName: '',
        lastName: ''
    }
    //提取函数,放置每次生成新函数,导致子组件渲染
    handleClick = () => {
        this.setState({count: this.state.count + 1})
    }

    render(){
        return (
        <>
            <p>Your click num: {this.state.count}</p>
            <Button onClick={this.handleClick}>Hello {this.props.name}</Button>
        </>
        )
    }

}

export default HelloClass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 4、高阶组件

高阶组件,引入ts的泛型进行约束:

  • 被包装组件类型:React.ComponentType<P>)
  • 新组件类型:Component<P & NewProps>
//高级组件,引入ts的泛型进行约束
import { BlockList } from 'net';
import React, {Component} from 'react'
import HelloClass from './HelloClass'

interface Loading {
    loading: boolean
}

//type React.ComponentType<P = {}> = React.ComponentClass<P, any> | React.FunctionComponent<P>
//组件类型:既可以是函数组件也可以为类组件,泛型接口,P为属性类型
function HelloHOC<P>(WrappedComponent: React.ComponentType<P>){
    return class extends Component<P & Loading> {
        render(){
            const {loading, ...props} = this.props;
            return loading ? <div>Loading...</div> : <WrappedComponent {...props as P}/>
        }
    }
}

export default HelloHOC(HelloClass)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 5、hooks

hooks:useState<T>为泛型接口

import React, {useEffect, useState} from 'react'
import { Button } from 'antd'
interface Greeting {
    name: string;
    firstName: string;
    lastName: string;
}
const HelloHooks = (props: Greeting) => {
    const [count, setCount] = useState(0);
    const [text, setText] = useState<string | null>(null);
    const handleClick = () => {
        setCount(count + 1)
    }
    useEffect(()=> {
        if(count > 5){
            setText('Have a rest')
        }
    }, [count])
    return (
        <>
            <p>Your click num: {count} {text}</p>
            <Button onClick={handleClick}>Hello {props.name}</Button>
        </>
    )
}
HelloHooks.defaultProps = {
    firstName: "",
    lastName: ""
}
export default HelloHooks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 3、事件与类型

dom事件使用React.FormEvent<HTMLInputElement>类型约束

//1. 受控组件Input
const [name, setName] = useState('');
<Input
  placeholder="姓名"
  style={{ width: 120 }}
  allowClear
  value={name}
  onChange={handleNameChange}
/>
//泛型接口:interface React.FormEvent<T = Element>
const handleNameChange = (e: React.FormEvent<HTMLInputElement>) => {
  setName(e.currentTarget.value)
}
handleNameChange = (e: React.FormEvent<HTMLInputElement>) => {
  let name = e.currentTarget.value;
  this.setState({
    name: name === '' ? undefined : name.trim()
  });
}

//2. 受控组件select
const [departmentId, setDepartmentId] = useState<number | undefined>();
<Select
  placeholder="部门"
  style={{ width: 120 }}
  allowClear
  value={departmentId}
  onChange={handleDepartmentChange}
  >
  <Option value={1}>技术部</Option>
  <Option value={2}>产品部</Option>
  <Option value={3}>市场部</Option>
  <Option value={4}>运营部</Option>
</Select>
const handleDepartmentChange = (value: number) => {
    setDepartmentId(value)
}
handleDepartmentChange = (value: number) => {
  this.setState({
    departmentId: value
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 4、数据请求

使用interface维护前后端接口:增加可维护性、自动补全、类型检查

//1. 接口定义:interface/employee.ts
//请求接口
export interface EmployeeRequest {
    name: string;
    departmentId: number | undefined;
}
//返回接口
interface EmployeeInfo {
    id: number;
    key: number;
    name: string;
    department: string;
    hiredate: string;
    level: string;
}
export type EmployeeResponse = EmployeeInfo[] | undefined

//2. 接口使用:component/employee/QueryForm.tsx
import { EmployeeRequest, EmployeeResponse } from '../../interface/employee';

interface Props{
    onDataChange(data: EmployeeResponse): void
}
const QueryFormHooks = (props: Props) => {
		//不使用redux,将数据放在父组件中,调用父组件的回调方法
    const queryEmployee = (param: EmployeeRequest) => {
        get(GET_EMPLOYEE_URL, param).then(res => {
            props.onDataChange(res.data);
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 5、Redux数据处理

# 1、安装

npm i redux redux-thunk react-redux npm i @types/react-redux -D

# 2、文件结构

-redux
		-employee
    	-index.ts    		//reducer\actionCreator
 		-rootReducer.ts   //root:combineReducers(reducers);
 		-store.ts         //store
-index.ts            //Provider注入
-components/employee/index.tsx  //store使用,将组件中的action和state剥离
1
2
3
4
5
6
7

ts状态类型:

  • state:type ReadOnly<{...}>,只对一级属性起作用
  • action:type Action = {type: string; payload: any;}
  • dispatch: Dispatch,redux提供

# 3、具体实例

//一、创建store
//1. store.ts:store创建,添加异步中间件
import { createStore, compose, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './rootReducer'
const store = createStore(rootReducer, 
    compose(
        applyMiddleware(thunk)
    )
)
export default store;

//2. rootReducer:模块解耦,reducer整合
import { combineReducers } from 'redux';
import employee from './employee';
const reducers = {
    employee
}
export default combineReducers(reducers);

//3. employee/index.ts:模块使用store,reducer及actionCreator
import { Dispatch } from 'redux';
import { get } from '../../utils/request';
import { GET_EMPLOYEE_URL } from '../../constants/urls';
import { EmployeeRequest, EmployeeResponse } from '../../interface/employee';
//state设置为readonly,后面修改state会报错
//目前readonly只对一级属性有作用,若为数组对数组内元素不起作用
type State = Readonly<{
    employeeList: EmployeeResponse
}>
type Action = {
    type: string;
    payload: EmployeeResponse;
}
//reducer创建
const initialState: State = {
    employeeList: undefined
}
export default function(state = initialState, action: Action){
    switch (action.type){
        case GET_EMPLOYEE_URL: 
            return {
                ...state,
                employeeList: action.payload
            }
        default:
            return state
    }
}
//actionCreators
export function getEmployee(param: EmployeeRequest) {
    return (dispatch: Dispatch) => {
        get (GET_EMPLOYEE_URL, param).then(res => {
            dispatch({
                type: GET_EMPLOYEE_URL,
                payload: res.data
            })
        })
    }
}

//二、注入系统
import Root from './routers';
import store from './redux/store';
import { Provider } from 'react-redux';
ReactDOM.render(
 <Provider store={store}>
    <Root />
  </Provider>,
  document.getElementById('root')
);

//三、模块使用:将state和action全部通过redux外部传入
import { bindActionCreators, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { getEmployee } from '../../redux/employee';

const mapStateToProps = (state: any) => ({
    employeeList: state.employee.employeeList
})
const mapDispatchToprops = (dispatch: Dispatch) => 
    bindActionCreators({
        onGetEmployee: getEmployee
    }, dispatch)

interface Props {
    onGetEmployee(param: EmployeeRequest): void;
    employeeList: EmployeeResponse;
}
class Employee extends Component<Props> {
    render() {
        const {employeeList, onGetEmployee} = this.props;
        return (
            <>
                {<QueryForm onDataChange={onGetEmployee} />}
                {<Table columns={employeeColumns} dataSource={employeeList} className="table" />}
            </>
        )
    }
}
export default connect(
    mapStateToProps,
    mapDispatchToprops
)(Employee)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
Last Updated: 1/28/2022, 3:36:25 PM