求知若饥,虚心若愚。
下面总结一些关于react常见问题。
最近参与react项目的研发,总结一下自己在react上的问题。
在此之前,说下目前前端开发中后台管理系统的技术栈或UI库。
管理后台,
如果技术栈是vue
的话,那么首选UI就是饿了么
的Element UI
;
如果技术栈是react
的话,那么首选UI就是阿里
的Ant Design
。
尽管element ui也有react版的,ant design也有vue版的,但这两家公司起步的技术栈走向本身就不同,饿了么主要是vue,阿里主要是react,后面为了支持其他栈的框架,才出来其他的版本。
技术栈vue:
官网:https://element.eleme.cn/#/zh-CN
UI:element-ui
脚手架集成:vue-element-admin
脚手架简易模板:vue-admin-template
技术栈react:
UI:ant-design
脚手架集成:ant-design-pro,选ant-design-pro
脚手架简易模板:ant-design-pro,选app
官方定义: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的一个集成框架。
下面是整个项目的目录结构:
1 | ├── config |
一般我们用到最多的目录是pages
,pages目录下是我们经常开发业务页面和模块的地方。
1 | ├── .umi # dev临时目录 umi配置 |
官方定义: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的话,还比较花时间,因为学习成本还是蛮高的哈~。
官方定义:Umi.js是可扩展的企业级前端应用框架
。
在说到umi,不得不说到bigfish及蚂蚁金服框架发展历史。
框架发展时间线:
在 Umi 和 Bigfish 时代,从刀耕火种的时代跨入了工业化时代。因为在此之前,用户需要接触很多技术栈和细节,在 Umi 和 Bigfish 中,用户只要知道一个框架,剩下的全部不用了解。框架像一个魔法球,把各种技术栈吸到一起,加工后吐给用户,以此来支撑业务。
在两个框架合并之后,现状是这样:umi 对外开源,bigfish 对内服务
阿里同学。
bigfish 扔掉原有实现,改造成 umi + umi 插件集的一个架构。
bigfish不是第一个这么做的,类似的还有 eggjs 和 chair。这是一种很好的方式,开源和业务两不误。
所以umi只是bigfish的一部分,umi是蚂蚁金服的底层前端框架,能将各种技术栈吸到一起
。
框架不是凭空而来的,需求来自于业务,所以用框架写业务的同学往往能发现框架不足的点,他们可以开发适用于自己业务的框架插件,反哺框架。如果这是通用需求,那就亮了。框架的内部开发群有 100+ 人,包含大量来自业务线的同学,这就是插件体系的好处,人人都能贡献。为了让写插件变得简单,因此给Bigfish框架分了五层架构。
包含依赖层、插件层、插件集层、应用类型层和部署模式层
,大家可在任何一层都可贡献代码,
这是插件生命周期图,包含:
大部分插件体系只会考虑 node 编译时,加上运行时和编辑时的支持,赋予了插件更大的能力。具体做了什么就不展开了,每个框架都不同,但做的事情其实大体一致,往上说是 html、css、js,往下说还有各种工具的配置,比如 webpack、babel、postcss、dev 中间件 等等。
目前部分umi插件市场:
1 | import React, { PureComponent, Fragment } from 'react'; |
好习惯–tips:在碰到一个自己不熟悉的语法或者其他,我们需要的做的是查相关资料,弄清楚它的用法与作用。
所以,在分析 “connect 将数据和视图关联” 这部分之前,我就先考虑下:@connect(),是什么?有何作用呢?
首先,我们先了解下一些关于 “ES6修饰器
“ 课外知识。修饰器(Decorator)是一个函数,用来修改类的行为
。修饰器对类的行为的改变,是代码编译时发生
的,而不是在运行时
。这意味着,修饰器能在编译阶段运行代码。
想看ES6修饰器
更多知识,可参考阮一峰ES6,地址:https://es6.ruanyifeng.com/#docs/decorator
1 | function testable(target) { |
这里:@testable 就是一个修饰器,它修改了 MyTestableClass 这个类的行为,为它加上了静态属性 isLoading。
既然修饰器是一个函数,那它能传参数吗?回答是肯定的,如下:
1 | function testable(isLoading) { |
这样就能在修饰器中传参数了,如果需要多个参数,直接函数中:testable({ a, b}){ };即可。
看到这里是不是大概明白:@connect 是修饰器了吧。既然 connect 是修饰器,那么它给 Photo 这个类添加哪些额外属性呢?咱们一起继续往下看dva源码:
1 | /** |
可以看到,connet函数可以传mapStateToProps、mapDispatchToProps、mergeProps、options,四个参数,且都不是必传项。connect 函数传入的第一个参数是 mapStateToProps 函数,该函数需要返回一个对象,用于建立 State 到 Props 的映射关系。
1 | @connect(({ user, loading }) => ({ |
第一个函数会注入全部的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 | laoding: { |
请求中,loading为:
1 | loading: { |
请求后,loading为:
1 | loading: { |
因此,可以通过loading.effects['user/getUserInfo']
方式来展示loading状态。
1 | import { sendGetRequest, sendPostRequest } from '@/services/api'; |
看到这里,对于不熟悉dva或者redux的小伙伴来讲,肯定看的一头雾水。不过,不用怕,我们一起分析它。
其实我们分析观察到dva中的每个model,实际上都是普通的JavaScript对象
,包含:
namespace
:该字段就相当于model的索引,根据该命名空间就可以找到页面对应的model。注意 namespace 必须唯一。state
:state 是储存数据的地方,收到action以后,会更新数据。effects
:处理所有的异步逻辑,将返回结果以action的形式交给reducer处理。reducers
:处理所有的同步逻辑,将数据返回给页面。既然知道它们的含义,它们有何关系?
这要回到redux的单向数据流
:
流程简介:
首先我们要有一个store,然后页面从 store 里面取数据,如果页面想改变 store 里面的数据,需走一个流程,首先是派发一个 action 给 store ,store 把 action 和之前的数据一起给到 reducer,reducer 结合这个 action 和之前的数据返回一个新的数据给到 store,store 更新自己的数据之后,告诉页面,我的数据被更新了,页面就会自动跟着联动。
同步数据流程简介:
这张图表是不参与服务器传递数据的,通过页面View中的点击事件或者其他触发 dispatch 的 action 改变 state 的数据。所以,随着 state 发生改变,页面也会重新渲染。
异步数据流程简介:
这张图表是通过访问 url 触发 effect 的异步从服务器请求数据,将拿到的数据 data ,再通过 reducer 同步到 state 中,即 state 值发生变化,页面也会随之改变。
看到这里,估计没有 reudx 基础的小伙伴,看的云里雾里的。不过没关系,react的 redux单向数据绑定
与 vue双向数据绑定
相比较而言,本来就蛮难理解的。况且react还有 react hooks
,Hooks 在很多时候可以完成 redux 部分的事情。
首先看页面Photo.jsx的大致代码,如下:
1 | ... |
我们在 user.js 中发现有 effects 的一段代码,如下:
1 | import { sendGetRequest, sendPostRequest } from '@/services/api'; |
根据引入路径 “@/services/api” 找到api.js,如下:
1 | import request from '@/utils/request'; |
使用当前页面:
1 | import Photo from './components/Photo'; |
详细流程如下:
1.执行组件声明周期 componentDidMount 中的 getUserInfo 函数
1 | componentDidMount() { |
2.getUserInfo函数里,从 props 中获取通过 @connect 添加的 dispatch 和组件传入的 userId
1 | getUserInfo = () => { |
来源:
1 | () |
3.使用 dispatch 函数触发 action,触发 models 中的 namespace 值为 user 的 model,及 user 下面的 *getUserInfo 函数
1 | dispatch({ |
来源:
1 | namespace: 'user', |
4.从 action 里传来的 payload 里获取 userId 参数,然后从 dva 预设的 effect 创建器中使用 call 执行异步方法,去调用api.js的公共get请求并传参
1 | const response = yield call(sendGetRequest, url); |
来源:
1 | export async function sendGetRequest(requsetUrl) { |
5.请求成功后,将返回的数据,放进 payload 参数,然后从 dva 预设的 effect 创建器中使用 put 发出另一个 action ,该 action 把之前的数据一起给到 reducer
1 | yield put({ |
来源:
1 | reducers: { |
6.reducer 结合这个 action 和之前的数据返回一个新的数据给到 state,当 state 的 userInfo 值发生改变后,页面也会重新渲染,重走render 方法
1 | ... |
渲染页面:
1 | ... |
至此,从数据请求到页面渲染的完整过程结束。
最后简单总结一下流程:页面View 通过 dva 的 dispatch 触发 action 找到对应 model 中 effects 内的具体方法,方法内通过 call 执行请求获取完数据后,通过 put 再次触发 action 找到 reducers 内的具体方法,该方法需返回一个新的 state,state 发生改变,页面会自动重新渲染。
1 | effects: { |
从外观上看,我们发现自定义函数前都有 “ *
“ 修饰 ,函数有两个参数
。
大家可能对( { payload } , { select, call, put })函数参数不太理解,这是固定写法吗?里面还有其他关键字吗?
那好,我们就一起在控制台上打印出这两个参数:(action,effects)
1 | *fetchBasic( action, effects) { |
在控制台上可以观察到,第一个参数action:
在控制台上可以观察到,第二个参数effects:
分析:
第一个参数,我们可以拿到两个常用的关键字值:payload , type
。
这两个参数,是在哪儿传来的呢?答案是页面View中触发的 action。
1 | // 通过 @connect()注解将内置 dispatch 传入props |
第二个参数,从打印结果看,dva 预设的内置函数还是比较多的,但是我们比较常用 3 个,分别是:select,call ,put。
那它们都代表什么含义?怎么使用呢?
call: 用于调用异步逻辑,支持 promise 。
1 | // call用法: |
put:用于触发 action。
1 | // put用法: |
select:用于从 state 里获取数据。
1 | // select用法: |
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。