STAY HUNGRY , STAY FOOLISH.

求知若饥,虚心若愚。

       浏览:

React常见问题及解答(上)

下面总结一些关于react常见问题。

  • 1.Ant Design是UI库(antd),那Ant Design Pro呢?
  • 2.Ant Design Pro项目目录结构是怎样的呢?
  • 3.Ant Design Pro项目pages下的目录结构是怎样的呢?
  • 4.什么是Dva.js?
  • 5.什么是Umi.js?
  • 6.Bigfish的五层架构?
  • 7.Umi插件的生命周期?
  • 8.在Antd Pro中完整规范的jsx文件是什么样的?
  • 9.上面@connect()是什么呢?
  • 10.在Antd Pro中完整规范的model是什么样的?
  • 11.分析在Antd Pro中获取数据请求后渲染到页面的完整过程?
  • 12.effects内函数有什么特点?里面call, put, select是什么呢?*function、yield又是什么?

最近参与react项目的研发,总结一下自己在react上的问题。
在此之前,说下目前前端开发中后台管理系统的技术栈或UI库。
管理后台,
如果技术栈是vue的话,那么首选UI就是饿了么Element UI
如果技术栈是react的话,那么首选UI就是阿里Ant Design
尽管element ui也有react版的,ant design也有vue版的,但这两家公司起步的技术栈走向本身就不同,饿了么主要是vue,阿里主要是react,后面为了支持其他栈的框架,才出来其他的版本。


vue_site

技术栈vue:
官网:https://element.eleme.cn/#/zh-CN
UI:element-ui
脚手架集成:vue-element-admin
脚手架简易模板:vue-admin-template


react_site

技术栈react:
UI:ant-design
脚手架集成:ant-design-pro,选ant-design-pro
脚手架简易模板:ant-design-pro,选app

1.Ant Design是UI库(antd),那Ant Design Pro呢?

antd_pro

官方定义:Ant Design Pro 是一个企业级中后台前端/设计解决方案

Ant Design Pro = ES2015+ + React + UmiJS + Dva + G2 + Antd

因此,想用好这个脚手架,需要先去学习或了解,ES规范、react.js、umi.js、dva.js、g2.js、antd。

ES规范,推荐阮一峰的ES6,地址:https://es6.ruanyifeng.com/
react.js官网,地址:https://zh-hans.reactjs.org/
umi.js官网,地址:https://umijs.org/zh-CN
dva.js官网,地址:https://dvajs.com/
g2.js官网,地址:https://g2.antv.vision/zh
antd官网,地址:https://ant.design/index-cn

所以Ant Design Pro是antd + react + other js的一个集成框架。


2.Ant Design Pro项目目录结构是怎样的呢?

下面是整个项目的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
├── config                   # umi 配置,包含路由,构建等配置
├── mock # 本地模拟数据
├── public
│ └── favicon.png # Favicon
├── src
│ ├── assets # 本地静态资源
│ ├── components # 业务通用组件
│ ├── e2e # 集成测试用例
│ ├── layouts # 通用布局
│ ├── models # 全局 dva model
│ ├── pages # 业务页面入口和常用模板
│ ├── services # 后台接口服务
│ ├── utils # 工具库
│ ├── locales # 国际化资源
│ ├── global.less # 全局样式
│ └── global.ts # 全局 JS
├── tests # 测试工具
├── README.md
└── package.json

一般我们用到最多的目录是pages,pages目录下是我们经常开发业务页面和模块的地方。


3.Ant Design Pro项目pages下的目录结构是怎样的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── .umi                     # dev临时目录 umi配置
├── student # 学生管理菜单
│ ├── studentList # 学生列表模块
│ ├── components # 学生列表组件
│ ├── Comp1.jsx # 组件1
│ ├── Comp2.jsx # 组件2
│ └── style.less # 组件公共样式
│ ├── index.jsx # 学生列表页面
│ └── style.less # 学生列表样式
│ ├── studentDetail # 学生详情模块
│ ├── models # 局部 dva model,只限学生管理模块使用
├── teacher # 老师管理菜单
│ ├── teacherDetail # 老师详情模块
│ └── teacherList # 老师列表模块

4.什么是Dva.js?

dvajs

官方定义:dva 首先是一个基于 redux 和 redux-saga 的数据流方案。然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。

DvaJS = Redux + Redux-Saga + React-Router + Fetch

回到之前对Antd Pro的定义,Ant Design Pro = ES2015+ + React + UmiJS + dva + g2 + antd,将dva等式换成上面的,得出终极等式。

Ant Design Pro = ES2015+ + React + UmiJS + Redux + Redux-Saga + React-Router + Fetch + G2 + Antd

如果Antd Pro项目使用TS(TypeScript)的话,几乎所有react主流技术栈和前端前沿技术都包括完了。
当然dva使用起来是很简单的,想深入了解Antd Pro的话,还比较花时间,因为学习成本还是蛮高的哈~。


5.什么是Umi.js?

umi

官方定义:Umi.js是可扩展的企业级前端应用框架

在说到umi,不得不说到bigfish及蚂蚁金服框架发展历史。

infoq

框架发展时间线:

  • 2015 年之前有 Sea.JS、Arale、SPM 开源技术方案,大家可以有所耳闻。
  • 2015 年接入 React,从自研的 Roof 到 Redux 再到开源的 Dva,一步步验证出来的最佳实践,并把这些实践交给开源社区检验。
  • 2017 年开始尝试新一代的企业级前端框架,Umi 和 Bigfish,前者是从无线业务中长出来的,后者是从中台业务中长出来的。
  • 一个团队出两个框架毕竟不是长久之计,后来直接把两拨人调到一个组,于是就愉快地合并在了一起。

bigfish

在 Umi 和 Bigfish 时代,从刀耕火种的时代跨入了工业化时代。因为在此之前,用户需要接触很多技术栈和细节,在 Umi 和 Bigfish 中,用户只要知道一个框架,剩下的全部不用了解。框架像一个魔法球,把各种技术栈吸到一起,加工后吐给用户,以此来支撑业务。

open_nei

在两个框架合并之后,现状是这样:
umi 对外开源,bigfish 对内服务阿里同学。
bigfish 扔掉原有实现,改造成 umi + umi 插件集的一个架构。
bigfish不是第一个这么做的,类似的还有 eggjs 和 chair。这是一种很好的方式,开源和业务两不误。
所以umi只是bigfish的一部分,umi是蚂蚁金服的底层前端框架,能将各种技术栈吸到一起


6.Bigfish的五层架构?

bigfish_five

框架不是凭空而来的,需求来自于业务,所以用框架写业务的同学往往能发现框架不足的点,他们可以开发适用于自己业务的框架插件,反哺框架。如果这是通用需求,那就亮了。框架的内部开发群有 100+ 人,包含大量来自业务线的同学,这就是插件体系的好处,人人都能贡献。为了让写插件变得简单,因此给Bigfish框架分了五层架构。

包含依赖层、插件层、插件集层、应用类型层和部署模式层,大家可在任何一层都可贡献代码,

  • 可以写一个独立的功能插件,比如和某个服务的对接,比如扩展路由的某个功能,比如实现一套特殊的补丁方案;
  • 可以做归类,把一系列插件整理到一个插件集里,适用于某一类的业务开发;
  • 可以扩展应用类型,比如 SPA、MPA、微前端等等;
  • 可以扩展部署模式,比如和不同的框架或平台做结合;

7.Umi插件的生命周期?

umi_plugins

这是插件生命周期图,包含:

  • node 环境执行的编译时
  • 浏览器上执行的运行时
  • ui 辅助层的编辑时

大部分插件体系只会考虑 node 编译时,加上运行时和编辑时的支持,赋予了插件更大的能力。具体做了什么就不展开了,每个框架都不同,但做的事情其实大体一致,往上说是 html、css、js,往下说还有各种工具的配置,比如 webpack、babel、postcss、dev 中间件 等等。

目前部分umi插件市场:

umi_plugins_mark


8.在Antd Pro中完整规范的jsx文件是什么样的?

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
import React, { PureComponent, Fragment } from 'react';
import { Avatar } from 'antd';
import { connect } from 'dva';
import styles from './style.less';

@connect(({ user, loading }) => ({
user,
loading: loading.effects['user/getUserInfo'],
}))
class Photo extends React.PureComponent {
constructor(props) {
super(props);
this.state = { };
this.defaultUrl = '';
}
componentDidMount() { }
...
avatarError = () => {
const { defaultUrl } = this
....
}
...
render() {
const { userInfo: { name, url }, loading } = this.props
return (
<Fragment>
{ loading && <Avatar src={url} onError={this.avatarError}/> }
{ loading && <p style={styles.name}>{name}</p> }
</Fragment>
);
}
}
export default Photo;

9.上面@connect()是什么呢?

好习惯–tips:在碰到一个自己不熟悉的语法或者其他,我们需要的做的是查相关资料,弄清楚它的用法与作用。
所以,在分析 “connect 将数据和视图关联” 这部分之前,我就先考虑下:@connect(),是什么?有何作用呢?
首先,我们先了解下一些关于 “ES6修饰器“ 课外知识。
修饰器(Decorator)是一个函数,用来修改类的行为。修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。
想看ES6修饰器更多知识,可参考阮一峰ES6,地址:https://es6.ruanyifeng.com/#docs/decorator

1
2
3
4
5
6
7
8
9
10
function testable(target) {
target.isTestable = true;
}

@testable
class MyTestableClass {
// ...
}

MyTestableClass.isTestable // true

这里:@testable 就是一个修饰器,它修改了 MyTestableClass 这个类的行为,为它加上了静态属性 isLoading。

既然修饰器是一个函数,那它能传参数吗?回答是肯定的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function testable(isLoading) {
return function(target) {
target.isLoading = isLoading;
}
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isLoading // true

@testable(false)
class MyClass {}
MyClass.isLoading // false

这样就能在修饰器中传参数了,如果需要多个参数,直接函数中:testable({ a, b}){ };即可。


看到这里是不是大概明白:@connect 是修饰器了吧。既然 connect 是修饰器,那么它给 Photo 这个类添加哪些额外属性呢?咱们一起继续往下看dva源码:

1
2
3
4
5
6
7
8
9
/**
* Connects a React component to Dva.
*/
export function connect(
mapStateToProps?: Function,
mapDispatchToProps?: Function,
mergeProps?: Function,
options?: Object
): Function;

可以看到,connet函数可以传mapStateToProps、mapDispatchToProps、mergeProps、options,四个参数,且都不是必传项。connect 函数传入的第一个参数是 mapStateToProps 函数,该函数需要返回一个对象,用于建立 State 到 Props 的映射关系。

1
2
3
4
@connect(({ user, loading }) => ({
user,
loading: loading.effects['user/getUserInfo'],
}))

第一个函数会注入全部的models,你需要返回一个新的对象,挑选该组件所需要的models。
注意事项:
1.全部的models是指在当前模块目录下models文件夹或项目最上层的models文件夹声明的models,跨模块的models是无法获取到的。
2.写上@connect()注解,无论是否传参,在props里面都默认添加了dispatch函数
3.最后的loading,是内置dav-loading插件的功劳,能监听异步请求是否完成。

说明:
当引入dva-loading插件之后,models新增了loading对象,loading对象中有三个变量,effects、global、models。
当发送一个异步请求时,loading值的变化,
请求前,loading为:

1
2
3
4
5
6
7
laoding: {
effects: {}
global: false
models: {}
}

请求前,global为false,effects和models为空对象

请求中,loading为:

1
2
3
4
5
6
7
8
9
loading: {
effects: {user/getUserInfo: true}
global: true
models: {user: true}
}

globaltrue
effects的key为dispatch的type值,valuetrue
models的key为namespace值,valuetrue

请求后,loading为:

1
2
3
4
5
6
7
8
9
loading: {
effects: {user/getUserInfo: false}
global: false
models: {user: false}
}

globalfalse
effects的key为dispatch的type值,valuefalse
models的key为namespace值,valuefalse

因此,可以通过loading.effects['user/getUserInfo']方式来展示loading状态。


10.在Antd Pro中完整规范的model是什么样的?

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
import { sendGetRequest, sendPostRequest } from '@/services/api';

export default {
namespace: 'user',

state: {
userInfo: {},
},

effects: {
*getUserInfo({ payload }, { call, put, select }) {
const url = `/api/xxx/user?$id={payload.userId}`;
const response = yield call(sendGetRequest, url);
yield put({
type: 'saveUserInfo',
payload: {
user: response.data,
},
});
},
},

reducers: {
saveUserInfo(state, { payload }) {
return {
...state,
userInfo: payload.user,
};
},
},
};

看到这里,对于不熟悉dva或者redux的小伙伴来讲,肯定看的一头雾水。不过,不用怕,我们一起分析它。
其实我们分析观察到dva中的每个model,实际上都是普通的JavaScript对象,包含:

  • namespace:该字段就相当于model的索引,根据该命名空间就可以找到页面对应的model。注意 namespace 必须唯一。
  • state:state 是储存数据的地方,收到action以后,会更新数据。
  • effects:处理所有的异步逻辑,将返回结果以action的形式交给reducer处理。
  • reducers:处理所有的同步逻辑,将数据返回给页面。

既然知道它们的含义,它们有何关系?
这要回到redux的单向数据流

redux-flow

流程简介:
首先我们要有一个store,然后页面从 store 里面取数据,如果页面想改变 store 里面的数据,需走一个流程,首先是派发一个 action 给 store ,store 把 action 和之前的数据一起给到 reducer,reducer 结合这个 action 和之前的数据返回一个新的数据给到 store,store 更新自己的数据之后,告诉页面,我的数据被更新了,页面就会自动跟着联动。


同步数据流程简介:

action

这张图表是不参与服务器传递数据的,通过页面View中的点击事件或者其他触发 dispatch 的 action 改变 state 的数据。所以,随着 state 发生改变,页面也会重新渲染。


异步数据流程简介:

action_async

这张图表是通过访问 url 触发 effect 的异步从服务器请求数据,将拿到的数据 data ,再通过 reducer 同步到 state 中,即 state 值发生变化,页面也会随之改变。

看到这里,估计没有 reudx 基础的小伙伴,看的云里雾里的。不过没关系,react的 redux单向数据绑定vue双向数据绑定 相比较而言,本来就蛮难理解的。况且react还有 react hooks,Hooks 在很多时候可以完成 redux 部分的事情。


11.分析在Antd Pro中获取数据请求后渲染到页面的完整过程?

首先看页面Photo.jsx的大致代码,如下:

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
...
@connect(({ user, loading }) => ({
user,
loading: loading.effects['user/getUserInfo'],
}))
class Photo extends React.PureComponent {
componentDidMount() {
this.getUserInfo()
}
getUserInfo = () => {
const { userId, dispatch } = this.props
dispatch({
type: 'user/getUserInfo',
paylod: { userId },
})
}
...
render() {
const { userInfo: { name, url }, loading } = this.props
return (
<Fragment>
{ loading && <Avatar src={url} onError={this.avatarError}/> }
{ loading && <p style={styles.name}>{name}</p> }
</Fragment>
);
}
}
export default Photo;

我们在 user.js 中发现有 effects 的一段代码,如下:

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
import { sendGetRequest, sendPostRequest } from '@/services/api';

export default {
namespace: 'user',

state: {
userInfo: {},
},

effects: {
*getUserInfo({ payload }, { call, put, select }) {
const url = `/api/xxx/user?$id={payload.userId}`;
const response = yield call(sendGetRequest, url);
yield put({
type: 'saveUserInfo',
payload: {
user: response.data,
},
});
},
},

reducers: {
saveUserInfo(state, { payload }) {
return {
...state,
userInfo: payload.user,
};
},
},
};

根据引入路径 “@/services/api” 找到api.js,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import request from '@/utils/request';

export async function sendGetRequest(requsetUrl) {
return request(requsetUrl)
}

export async function sendPostRequest(requsetUrl, params = {}) {
return request(requsetUrl, {
method: 'POST',
data: {
...params,
method: 'post',
},
})
}

使用当前页面:

1
2
3
4
5
6
7
8
import Photo from './components/Photo';
...
return (
<Fragment>
<Photo userId={110} />
</Fragment>
)
...

详细流程如下:
1.执行组件声明周期 componentDidMount 中的 getUserInfo 函数

1
2
3
componentDidMount() { 
this.getUserInfo()
}

2.getUserInfo函数里,从 props 中获取通过 @connect 添加的 dispatch 和组件传入的 userId

1
2
3
getUserInfo = () => {
const { userId, dispatch } = this.props
}

来源:

1
2
3
4
5
@connect()
class Photo extends React.PureComponent
...
<Photo userId={110} />
...

3.使用 dispatch 函数触发 action,触发 models 中的 namespace 值为 user 的 model,及 user 下面的 *getUserInfo 函数

1
2
3
4
dispatch({
type: 'user/getUserInfo',
paylod: { userId },
})

来源:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace: 'user',
...
*getUserInfo({ payload }, { call, put, select }) {
const url = `/api/xxx/user?$id={payload.userId}`;
const response = yield call(sendGetRequest, url);
yield put({
type: 'saveUserInfo',
payload: {
user: response.data,
},
});
},
...

4.从 action 里传来的 payload 里获取 userId 参数,然后从 dva 预设的 effect 创建器中使用 call 执行异步方法,去调用api.js的公共get请求并传参

1
const response = yield call(sendGetRequest, url);

来源:

1
2
3
export async function sendGetRequest(requsetUrl) {
return request(requsetUrl)
}

5.请求成功后,将返回的数据,放进 payload 参数,然后从 dva 预设的 effect 创建器中使用 put 发出另一个 action ,该 action 把之前的数据一起给到 reducer

1
2
3
4
5
6
yield put({
type: 'saveUserInfo',
payload: {
user: response.data,
},
});

来源:

1
2
3
reducers: { 
saveUserInfo(state, { payload }) { ... },
}

6.reducer 结合这个 action 和之前的数据返回一个新的数据给到 state,当 state 的 userInfo 值发生改变后,页面也会重新渲染,重走render 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
...
state: {
userInfo: {},
}
reducers: {
saveUserInfo(state, { payload }) {
return {
...state,
userInfo: payload.user,
};
},
}
...

渲染页面:

1
2
3
4
5
6
7
8
9
10
11
...
render() {
const { userInfo: { name, url }, loading } = this.props
return (
<Fragment>
{ loading && <Avatar src={url} onError={this.avatarError}/> }
{ loading && <p style={styles.name}>{name}</p> }
</Fragment>
);
}
...

至此,从数据请求到页面渲染的完整过程结束。
最后简单总结一下流程:
页面View 通过 dva 的 dispatch 触发 action 找到对应 model 中 effects 内的具体方法,方法内通过 call 执行请求获取完数据后,通过 put 再次触发 action 找到 reducers 内的具体方法,该方法需返回一个新的 state,state 发生改变,页面会自动重新渲染。


12.effects内函数有什么特点?里面call, put, select是什么呢?*function、yield又是什么?

1
2
3
4
5
6
7
8
9
10
11
12
effects: {
*fetchBasic({ payload }, { call, put, select }) {
const url = `/api/xxx/user?$id={payload}`;
const response = yield call(sendGetRequest, url);
yield put({
type: 'show',
payload: {
user: response.data,
},
});
},
}

从外观上看,我们发现自定义函数前都有 “ * “ 修饰 ,函数有两个参数
大家可能对( { payload } , { select, call, put })函数参数不太理解,这是固定写法吗?里面还有其他关键字吗?
那好,我们就一起在控制台上打印出这两个参数:(action,effects)

1
2
3
4
5
6
*fetchBasic( action, effects) {
...
console.log('action', action)
console.log('effects', effects)
...
},

在控制台上可以观察到,第一个参数action:

effect_action

在控制台上可以观察到,第二个参数effects:

effect_effects

分析:
第一个参数,我们可以拿到两个常用的关键字值:payload , type
这两个参数,是在哪儿传来的呢?答案是页面View中触发的 action。

1
2
3
4
5
6
// 通过 @connect()注解将内置 dispatch 传入props
const { dispatch } = this.props
dispatch({
type: 'profile/fetchBasic',
paylod: 100000000,
})

第二个参数,从打印结果看,dva 预设的内置函数还是比较多的,但是我们比较常用 3 个,分别是:select,call ,put。


那它们都代表什么含义?怎么使用呢?
call: 用于调用异步逻辑,支持 promise 。

1
2
3
4
5
// call用法:
// request :代表发送ajax请求
// payload :代表发送ajax请求时,所需要的参数

const res = yield call(request,payload);

put:用于触发 action。

1
2
3
4
5
6
7
8
9
// put用法:
// xx代表:models名
// jj代表:函数名
// res代表:所需的数据

yield put ({
type:'xx/jj',
payload:res
});

select:用于从 state 里获取数据。

1
2
3
4
5
// select用法:
// data代表:所需要的数据
// 其中state:代表所有models数据

const data = yield select(state=>state.data);

dva 不是基于redux、redux-saga的数据流方案吗?因此想使用其他方法的话,我们可以直接参考 redux-saga 的API,大同小异,地址:
https://redux-saga-in-chinese.js.org/docs/api/


那 *function、yield的作用是什么呢?
首先明白,effects里面的函数都是Generator 函数,简单理解 *function 就是语法糖,声明一个Generator函数,按照这样写就行。然后内部使用 yield 关键字,标识每一步的操作(不管是异步或同步)。
想看ES6 Generator 函数更多知识,可参考阮一峰ES6,地址:https://es6.ruanyifeng.com/#docs/generator