React & TypeScript项目总结

preview

关于项目

自从2018开年开始,我就开启了懒人模式,天天迷上打游戏,而新公司压力又不大所以划水到现在。前段时间想了想了这样下去不行,再这样下去就不能当上CEO,走上人生巅峰了😁。于是就又了这个项目。

这个项目是慕课网的一个react课程,项目比较小,用的版本也比较低。 所以按教程一葫芦画瓢,肯定学不到什么东西,也没挑战。所以就想着用最新的react技术栈来学习这个项目,并用上了TypeScript。

TypeScript

TypeScript 是微软推出的强类型javascript的超集,最重要的特点就是类型检查。之前在做Vue项目的时候,很想用到项目里,后面因为Vue对TS的支持和上手难度的问题不了了之了。而react对TS比较好,社区里也有很多成熟的使用例子,所有就上了TS。

技术栈

  • create-react-app
  • React
  • TypeScript
  • Less

开发过程

1、 create-react-app

create-react-app创建TypeScript项目模板

1
2
3
4
# 全局安装creat-react-app
npm install creat-react-app -g
# 创建ts项目模板
create-react-app my-app --scripts-version=react-scripts-ts

接下来配置Less,因为creat-react-app创建的项目是免配置的所以先要开启配置模式,暴露配置文件。

1
2
npm run eject # 暴露配置文件
npm install less -save

开启配置后项目目录就多了好几个文件夹

1
2
+  /config
+ /scripts

在以下文件修改配置:
/config/webpack.config.dev.js
/config/webpack.config.prod.js

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
{

- test: /\.(css)$/,
+ test: /\.(css|less)$/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebookincubator/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
"Android > 4.1",
'>1%',
'last 4 versions',
'Firefox ESR',
'not ie < 9', // React doesn't support IE8 anyway
],
flexbox: 'no-2009',
}),
],
},
},
{
+ loader: require.resolve('less-loader') // compiles Less to CSS
+ }
],
},

环境准备好了,接下来就可以编码了,首先先写app.tsx

基础结构

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
<!-- 结构 -->
<section className="stage" ref={(node: HTMLElement) => { this.stageDom = node }}>
<section className="img-sec">
{
imageArr.map((item, index) => {
if (!this.state.imgsArrangeArr[index]) {
this.state.imgsArrangeArr[index] = {
isCenter: false,
isInverse: false,
pos: {
left: 0,
top: 0
},
rotate: 0
}
}
controllerUnits.push(
<ControllerUnit
inverse={this.inverse(index)}
arrange={this.state.imgsArrangeArr[index]}
center={this.center(index)}
key={index} />
)
return (
<ImgFigure
key={index} {...item}
index={index}
inverse={this.inverse(index)}
arrange={this.state.imgsArrangeArr[index]}
imgRef={this.bindImgRefs}
center={this.center(index)}
/>
)
})
}
</section>
<nav className="controller-nav">
{
controllerUnits
}
</nav>
</section>

通过结构可以看到其中有两个子组件,stage容器通过ref获取真实DOM以便后续获取dom的一些属性,其中ref回调中的node是一个dom元素并定义了类型为HTMLElement,后续当我们用this.stageDom的时候就可以享受到TS带来的智能感应的福利,如下图。

vscode

ref

ref 不仅可以获取真实DOM也可以获取React组件的实例,需要注意的是要获取的组件必须是类组件,因为函数组件是没有生命周期的。

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
class App extends React.Component {
[x: string]: any;
public stageDom: HTMLElement;
public Constant = {
centerPos: {
left: 0,
top: 0
},
hPosRange: { // 水平方向的取值
leftSecX: [0, 0],
rightSecx: [0, 0],
y: [0, 0]
},
vPosRange: { // 垂直方向的取值
topY: [0, 0],
x: [0, 0]
}
}
public state: IstateType;
constructor(props: object) {
super(props);
this.bindImgRefs = this.bindImgRefs.bind(this);
this.inverse = this.inverse.bind(this);
this.state = {
imgsArrangeArr: [
// {
// pos: {
// left: '0',
// top: '0'
// },
// rotate: 0,
// isInverse: false,
// isCenter: false,
// }
]
}
}
/**
* 批量绑定ImgRefs方法
* @param node dom对象
* @param index 索引
*/
public bindImgRefs(node: any, index: number): void {
this[`figureImgDom${index}`] = node;
}

/**
* @param index 要翻转图片的index
*/
public inverse(index: number): any {
return () => {
const imgsArrangeArr = this.state.imgsArrangeArr;
imgsArrangeArr[index].isInverse = !imgsArrangeArr[index].isInverse;
this.setState({
imgsArrangeArr
})
};
}

public center(index: number) {
return () => {
this.rearrange(index)
}
}
/**
* 重新布局所有图片
* @param centerIndex 指定中心是哪个图片
*/
public rearrange(centerIndex: number) {
...
}
// 组件挂载以后, 为每张图片计算其位置的范围
public componentDidMount() {
...

this.rearrange(0);
}

public render() {
const controllerUnits: any = [];

return ...
}
}

以上js逻辑部分,其中rearrange是最重要的一个函数,用来计算各个图片的位置信息的,详细了解可以通过慕课视频。

public & private

在class声明内部变量和函数需要显式声明并且要用public 和 private区分是公私属性,并且声明类型。声明函数后尽量用jsdoc注释来解释函数的,后续在你调用的时候也有很友好的提示。

vscode2

接口

在TypeScript中定义复杂形式的数据的时候可以用接口(interface)来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义一个接口
export interface IimageArrType {
disc: string,
fileName: string,
title: string,
imageUrl?: any,
imgRef?: any,
index?: number,
arrange?: any,
inverse?: any,
center?: any
}

// 使用接口
function getPropsName(prop:IimageArrType):IimageArrType {
return prop;
}

定义接口的时候命名尽量用大写的I开始命名,接口里面非必传参数可以加问号

其他组件

ControllerUnit.tsx

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
import * as React from 'react';

export interface IController {
arrange: any,
inverse: any,
center: any
}
class ControllerUnit extends React.Component<IController,{}> {

constructor(props: IController) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
public handleClick(e: any): void {
if(this.props.arrange.isCenter) {
this.props.inverse();
} else {
this.props.center();
}
e.preventDefault();
e.stopPropagation();
}
public render() {
let controllerUnitClassName = "controller-unit";
// 如果对应的是居中的图片, 显示控制按钮的居中态
if (this.props.arrange.isCenter) {
controllerUnitClassName += " is-center";
// 如果同时对于的是翻转图片,显示控制按钮的翻转态
if (this.props.arrange.isInverse) {
controllerUnitClassName += " is-inverse";
}
}
return (
<span className={controllerUnitClassName} onClick={this.handleClick}></span>
);
}
}

export default ControllerUnit;

ImgFigure.tsx

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
import * as React from "react";
import { IimageArrType } from '../App/App';

class ImgFigure extends React.Component<IimageArrType, {}> {
constructor(props: IimageArrType) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
public componentWillMount() {
// console.log(this.props);
}
public handleClick(e: any) {
if (this.props.arrange.isCenter) {
this.props.inverse();
} else {
this.props.center();
}
e.preventDefault();
e.stopPropagation();
}
public render() {
let styles: any = {};
if (this.props.arrange.pos) {
// 防止内存引用
styles = Object.assign({}, this.props.arrange.pos);
}

if (this.props.arrange.rotate) {
styles.transform = `rotate(${this.props.arrange.rotate})`;
}
if (this.props.arrange.isCenter) {
styles.zIndex = 11;
}

let imgFigureClassName = 'img-figure';
imgFigureClassName += this.props.arrange.isInverse ? ' is-inverse' : '';

return (
<figure
className={imgFigureClassName}
style={styles}
onClick={this.handleClick}
ref={(node) => { this.props.imgRef(node, this.props.index) }}
>
<img src={this.props.imageUrl} alt={this.props.title} />
<figcaption>
<h2 className="img-title">{this.props.title}</h2>
<div className="img-back" onClick={this.handleClick}>
<p>{this.props.disc}</p>
</div>
</figcaption>
</figure>
)
}
}

export default ImgFigure;

pros使用的注意

在ImgFigure组件中,props传入了一个对象arrange,一开始我直接把this.props.arrange.pos赋值给styles变量了,因为styles在render里面每次都会改变,导致后面操作图片的时候报错了.这是因为pos是个对象,直接赋值给styles相当于内存引用,而styles改变的时候,也会去尝试改变this.props.arrange. 因为props是只读的,所以会发生报错.这里的解决方法是对this.props.arrange.pos用Object.assign浅拷贝就好..

不足的地方

1、因为刚开始用ts,对一些简单的数据类型定义起来比较简单,但复杂的类型或涉及到react的类型就没有经验所以好多地方写了any类型。下次要对ts文档通读一遍。

2、在许多地方都按照视频内容写的,没有深刻的思考原理。

要做的计划

后续还会优化这个项目:

  • 引入网络请求获取图片

  • 引入redux管理状态

  • 编写单元测试

Github

https://github.com/lemontree2000/react-gallery