参考文档:https://react.docschina.org/docs/getting-started.html
这里不讲述概念,直接上语法,至于jsx是什么,到上面那个网站上面看
https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
[TOC]
这是react最主要的两个内置属性与内置方法,state表示当前组件的状态/属性,而要改变state的值则通过setState方法,切不可直接修改state,因为setState内部是异步修改的
例子:
export default class CartSample extends Component {
this.state = {
text: ''
}
setGoodsText = e => {
this.setState({
text: e.target.value
})
}
render() {
return (
<input type="text" onChange={this.setGoodsText}/>
)
}
}
上面CartSample就是一个组件,里面有state和setState,监听input框的change事件,一旦触发则让setState改变state中的值,这是最简单也是最常用的用法。
在jsx中,我们在大括号{}内只能写表达式,不能编写逻辑代码,如if判断、for循环,此时我们可以通过短路逻辑判断,如:
下面例子写了当state的goods长度不为0时则展示对应数据
export default class CartSample extends Component {
this.state = {
goods: [
{ id: 1, text: "web全栈架构师", count: 2 },
]
}
render() {
return (
<ul>
{this.state.goods.length && this.state.goods}
</ul>
)
}
}
引用2的例子,循环展示商品列表,因为{}里面只能是表达式,因此我们的思路是遍历数组的每个元素,并返回不同的值,因此数组内置的map函数很适合做这种工作。
export default class CartSample extends Component {
this.state = {
goods: [
{ id: 1, text: "web全栈架构师", count: 2 },
{ id: 2, text: "python全栈架构师", count: 0 },
]
}
render() {
return (
<ul>
{this.state.goods.length && this.state.goods.map( item => {
<li>{item.name}</li>
})}
</ul>
)
}
}
// 方式1: 类
import React, { Component } from 'react'
export default class index extends Component {
render() {
return (
<div>
</div>
)
}
}
// 方式2: 函数
import React from 'react'
export default function index() {
return (
<div>
</div>
)
}
本来两者是有些差异的,hook出现之前,用函数式写组件一般都是只写傻瓜式组件,也就是没有处理业务逻辑的。但是自从react 16.80版本出现后,新增加了hook,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性,因此可以试着去学学hook,这样子也可以在function里编写逻辑代码,也更简练。
文档:https://react.docschina.org/docs/hooks-intro.html
事件监听采用Javascript的驼峰式写法,如onClick,onChange,onBlur等
例子:
export default class CartSample extends Component {
this.state = {
text: ''
}
setGoodsText = e => {
this.setState({
text: e.target.value
})
}
render() {
return (
<input type="text" onChange={this.setGoodsText}/>
)
}
}
上面是最简单的例子
那么传参事件该怎么绑定呢?
学之前先注意一点: {} 是个表达式
因为是表达式,如果我们要改上面代码为带参事件,我们不能用
onChange={this.setGoodsText(args)}
这样子的话会被立刻执行,因此我们需要让他监听一个匿名函数,写法如下
onChange={() => this.setGoodsText(args)}
随着你的应用程序不断增长,你可以通过类型检查捕获大量错误。对于某些应用程序来说,你可以使用 Flow 或 TypeScript 等 JavaScript 扩展来对整个应用程序做类型检查。但即使你不使用这些扩展,React 也内置了一些类型检查的功能。要在组件的 props 上进行类型检查,你只需配置特定的 propTypes
属性
import PropTypes from 'prop-types';
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
Greeting.propTypes = {
name: PropTypes.string
};
同样的,由于在类原型上的属性属于静态属性,所以我们可以写在类里面,只要加个static关键字即可。
export class MyPropType extends React.Component{
static propTypes = {
name: PropType.string,
age: PropType.number.isRequired,
}
constructor(props) {
super(props);
console.log(props)
}
render() {
return (
<div>prop测试</div>
)
}
}
export class MyPropType extends React.Component{
static propTypes = {
name: PropType.string,
age: PropType.number.isRequired,
}
static defaultProps = {
name: 'chen',
age: 18
}
constructor(props) {
...
}
render() {
...
}
}
export function FuncPropType({name, age}) {
console.log(name, age)
return (
<>
<div>{name}</div>
<div>{age}</div>
</>
)
}
FuncPropType.propTypes = {
name: PropTypes.string,
age: PropTypes.number
}
FuncPropType.defaultProps = {
name: 'tan',
age: 17
}
给JSX绑定事件时由于没有指定上下文,this指向会指向顶层
。
而指向顶层有两种情况:严格模式与非严格模式,而react是在严格模式下
的,在<React.StrictMode>定义,所以this为undefined。
为了让我们方法中的this可以指向类本身,我们可以选择四种方式:
// 方式一 构造器中给对应函数指定上下文, 如
this.addGood = this.addGood.bind(this) ------> 推荐
// 方式二 在JSX给种绑定this
<div @click="{ this.addGood.bind(this) }"></div>
// 方式三 类里面方法用箭头函数定义,箭头函数是没有上下文的,上下文会指向类
addGood = () => {} ------> 推荐
// 方式四 JSX里用箭头函数
addGood = () => { () => {this.addGood()} }
使用setState修改值有两个方法:
一个是传入对象修改,另一个方式是传入函数修改
// 方式一
state = {
text: ''
}
this.setState({
text: '新值'
})
// 方式二
state = {
text: ''
}
this.setState(prevState => {
let text = '123'
prevState.text = text
})
在使用方式一时要注意,若多次使用方式一进行setState,react最终会将多个setState操作给合并,这时候,若是多次setState了通过值,则只取最后一次setState的时候的值,如:
state = {
text: ''
}
this.setState({
text: '新值'
})
this.setState({
text: '新新值'
})
最后的值为 : 新新值
使用方式二时,我们最好按照react给我们定的规范,每次都给state需要修改的对象重新设置一个新的的值。
如:
this.state = {
goods: [
{ id: 1, text: "web全栈架构师" },
{ id: 2, text: "python全栈架构师" }
],
text: ''
}
textChange = e => {
this.setState({ text: e.target.value });
};
<input type="text" value={this.state.text} onChange={this.textChange} />
<button onClick={this.addGood}>添加商品</button>
现在当点击某个按钮时会触发addGood事件,addGood用来添加新商品数据到goods中,此时addGood的写法是这样的
addGood = () => {
this.setState(prevState => {
return {
goods: [
...prevState.goods,
{
id: prevState.goods.length + 1,
text: prevState.text
}
]
};
});
}
会将以前state.goods里的数据展开,拷贝进一个新的数组,再将新数据加进去,这就是react的规范。
安装:
npm install antd --save
试用按钮组件:
import Button from 'antd/lib/button';
import 'antd/dist/antd.css'
export default class App extends Component {
render() {
return (
<div>
<Button type='primary'>Button</Button>
</div>
)
}
}
上面那种方式引入很麻烦,而且是样式是引用全部的样式表,如何按需引入呢?
安装react-app-rewired取代react-scripts,可以扩展webpack的配置,类似vue.config.js
npm install react-app-rewired@2.0.2-next.0 babel-plugin-import --save
修改package.json的scripts字段:
// 所有react-scripts 都替换成 react-app-rewired 如
"scripts": {
"start": "react-app-rewired start",
...
},
在根目录下新建config-overrides.js,内容如下
const { injectBabelPlugin } = require("react-app-rewired");
module.exports = function override(config, env) {
config = injectBabelPlugin(
// 在默认配置基础上注入
// 插件名,插件配置
["import", { libraryName: "antd", libraryDirectory: "es", style: "css" }],
config
);
return config;
};
这样子配置就完成了,接下来我们可以实现按需导入了,在需要引入组件的地方:
import {Button} from 'antd'
<Button type='primary'>Button</Button>
基本原则:容器组件负责数据获取,展示组件负责根据props显示信息,例子如下
import React, { Component } from 'react';
// 容器组件
export default class CommentList extends Component {
constructor(props) {
super(props);
this.state = {
comments: [],
}
}
componentDidMount() {
setTimeout(() => {
this.setState({
comments: [
{ name: 'cai', age: 20, education: 'university' },
{ name: 'chen', age: 22, education: 'university' },
]
})
}, 3000)
}
render() {
return (
<div>
{this.state.comments.map((c, i) => {
return <Comment key={i} data={c}></Comment>
})}
</div>
)
}
}
// 展示组件
function Comment({ data }) {
return (
<div>
<p>{data.name}</p>
<p>--- {data.age}</p>
<p>--- {data.education}</p>
</div>
)
}
参考文档:https://react.docschina.org/docs/optimizing-performance.html
UI 更新需要昂贵的 DOM 操作,而 React 内部使用几种巧妙的技术以便最小化 DOM 操作次数。对于大部分应用而言,使用 React 时无需专门优化就已拥有高性能的用户界面。尽管如此,你仍然有办法来加速你的 React 应用。
还是引用9的例子,只不过我们会给CommentList加一个定时器,每隔1.5s就对comments列表进行setState操作,但是我们不修改它的值:
componentDidMount() {
this.timmer = setInterval(() => {
this.setState({
comments: [
{ name: 'cai', age: 20, education: 'university' },
{ name: 'chen', age: 22, education: 'university' },
]
})
}, 1500)
}
然后再在Comment组件打印日志。我们会发现打印日志会每隔1.5s打印,这时候问题就来了,明明我们的数据没发生实质上的变化,但页面却一直在渲染,这是一个非常值得优化的点。
解决方案:
因为使用到生命周期函数,所以我们的Comment组件应该转换为类形式,shouldComponentUpdate用来检查旧值与新值是否相等,返回一个bool值,如为true则重新渲染,否则不渲染
class Comment extends React.Component{
constructor(props) {
super(props)
}
shouldComponentUpdate(nextProps){
if (nextProps.data.name === this.props.data.name &&
nextProps.data.age === this.props.data.age &&
nextProps.data.education === this.props.data.education
) {
return false;
}
return true;
}
render() {
console.log("render comment");
return (
<div>
<p>{this.props.data.name}</p>
<p>--- {this.props.data.age}</p>
<p>--- {this.props.data.education}</p>
</div>
);
}
}
首先我们先研究一下PureComponent的实现原理:
PureComponent首先会判断新值与旧值是否在同一片内存地址,如果是,则返回true,也就是相同。
否则会再对旧值的内部做一层循环比较(注意:浅比较只会进行一层for循环,有深层次的结构时浅比较是没有效果的)
由此可以看出PureComponent的两个缺点:
一、如果是一个对象,或者数组,我们对其内部进行修改,而没有改变内存地址时,页面不会重新渲染
二、如果包含二层及以上的层次结构时,浅比较无法生效
如果我们有二层的层次结构时还想进行钱比较该怎么办呢?
方法: 传给展示组件的值用第二层的值传递,而非将整个对象或数组传递给展示组件。
继续引用9的例子
此时我们传给Comment组件的值不传入每个comments列表项,而是用展开运算符将每个对象展开,传入最里边的值,意思就是,我有下面这个结构的数组:
// 需要传给展示组件的数据
comments: [
{ name: 'cai', age: 20, education: 'university' },
{ name: 'chen', age: 22, education: 'university' },
]
// 传入的值:{...c}
{this.state.comments.map((c, i) => {
return <Comment key={i} {...c}></Comment>
})}
// 此时...c == 每个对象的name、age、education值
// 展示组件继承PureComponent
class Comment extends React.PureComponent{
constructor(props) {
super(props)
}
render() {
console.log("render comment");
return (
<div>
<p>{this.props.name}</p>
<p>--- {this.props.age}</p>
<p>--- {this.props.education}</p>
</div>
);
}
类形式的组件可以进行浅比较,现在在React v16.6.0之后,函数式也添加了浅比较,也就是memo, 使用方式如下:
constJoke=React.memo(() => (
<div>
{this.props.value||'loading...'}
</div>
));
只需将组件用React.memo包裹即可。
高阶组件是参数为组件,返回值为新组件的函数
高阶组件其实就是装饰器的原型,装饰器内部就是对高阶组件封装了而已,也就是语法糖。
高阶组件的一个最明显的好处就是你可以自行扩展组件行为。想到行为扩展想必你已经联想到mixins了,那为什么不用mixins而非要弄个高阶组件出来呢? 参考文档:https://react.docschina.org/blog/2016/07/13/mixins-considered-harmful.html
接下来我们就讲讲高阶组件的使用:
假设有一个 CommentList
组件,它订阅外部数据源,用以渲染评论列表:
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// 假设 "DataSource" 是个全局范围内的数据源变量
comments: DataSource.getComments()
};
}
componentDidMount() {
// 订阅更改
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// 清除订阅
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// 当数据源更新时,更新组件状态
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
稍后,编写了一个用于订阅单个博客帖子的组件,该帖子遵循类似的模式:
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
CommentList
和 BlogPost
不同 - 它们在 DataSource
上调用不同的方法,且渲染不同的结果。但它们的大部分实现都是一样的:
DataSource
添加一个更改侦听器。setState
。你可以想象,在一个大型应用程序中,这种订阅 DataSource
和调用 setState
的模式将一次又一次地发生。我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。这正是高阶组件擅长的地方。
对于订阅了 DataSource
的组件,比如 CommentList
和 BlogPost
,我们可以编写一个创建组件函数。该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop。让我们调用函数 withSubscription
:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
第一个参数是被包装组件。第二个参数通过 DataSource
和当前的 props 返回我们需要的数据。
当渲染 CommentListWithSubscription
和 BlogPostWithSubscription
时, CommentList
和 BlogPost
将传递一个 data
prop,其中包含从 DataSource
检索到的最新数据:
// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
// ...并返回另一个组件...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ...负责订阅相关的操作...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... 并使用新数据渲染被包装的组件!
// 请注意,我们可能还会传递其他属性
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。
被包装组件接收来自容器组件的所有 prop,同时也接收一个新的用于 render 的 data
prop。HOC 不需要关心数据的使用方式或原因,而被包装组件也不需要关心数据是怎么来的。
因为 withSubscription
是一个普通函数,你可以根据需要对参数进行增添或者删除。例如,您可能希望使 data
prop 的名称可配置,以进一步将 HOC 与包装组件隔离开来。或者你可以接受一个配置 shouldComponentUpdate
的参数,或者一个配置数据源的参数。因为 HOC 可以控制组件的定义方式,这一切都变得有可能。
与组件一样,withSubscription
和包装组件之间的契约完全基于之间传递的 props。这种依赖方式使得替换 HOC 变得容易,只要它们为包装的组件提供相同的 prop 即可。例如你需要改用其他库来获取数据的时候,这一点就很有用。
比如我们有两个高阶组件,分别定义不同的行为:
// 高阶组件1
const withLog = Comp => {
console.log("日志打印...");
return props => <Comp {...props} />;
};
// 高阶组件2
const withSubscribtion = Comp => {
// 获取name,name可能来自于接口或其他手段
const name = "其他属性";
console.log("do something");
return class extends React.Component {
render() {
return <Comp {...this.props} name={name} />;
}
};
};
// 被扩展组件
class NeedToExtend extends Component {
render() {
return (
<div></div>
);
}
}
// 链式调用过程
const newComp = withLog(withSubscribtion(NeedToExtend))
ES7装饰器可用于简化高阶组件写法
npm i -D babel-plugin-transform-decorators-legacy
在config-overrides.js添加配置
config = injectBabelPlugin(
["@babel/plugin-proposal-decorators", { legacy: true }],
config
);
使用装饰器,以11.2为例子
// 链式调用过程
const newComp = withLog(withSubscribtion(NeedToExtend))
// 装饰器使用
@withSubscribtion
@withLog
class NeedToExtend extends Component {
render() {
return (
<div></div>
);
}
}
复合组件其实就是展示组件预留个插口位置待容器组件插入相应的值,等同于Vue中的插槽。
对应Vue中的匿名插槽就是props.children
,如:
import React, { Component } from 'react';
// 展示组件
function Dialog(props) {
return (
<div>
{props.children}
</div>
);
}
// WelcomeDialog通过复合提供内容
function WelcomeDialog(props) {
return (
<Dialog {...props}>
<h1>欢迎光临</h1>
<p>感谢使用react</p>
</Dialog>
);
}
export default function() {
return (
<div>
<WelcomeDialog></WelcomeDialog>
</div>
)
}
此时{props.children}
就是插进来的内容
<h1>欢迎光临</h1>
<p>感谢使用react</p>
那么对应Vue的有名插槽是怎么实现的呢?
其实很简单,就是通过props传值,如我在展示组件里在定义一个页脚,页脚内容通过容器组件传入:
function Dialog(props) {
return (
<div>
{props.children}
<div className="footer">{props.footer}</div>
</div>
);
}
const footer = <button onClick={() => alert("确定!")}>确定</button>;
<WelcomeDialog color="green" footer={footer} />
以上例子复合内容形式均为jsx。
复合内容为函数时就相当于Vue中的作用于插槽,例子如下
假设我现在定义一个Fetcher组件,组件内的复合内容是一个带参函数,我想通过这个函数的参数渲染对应的内容:
<Fetcher name="getUser">
{user => (
<p>
{user.name}-{user.age}
</p>
)}
</Fetcher>
定义组件:
// api接口
const Api = {
getUser() {
return { name: "jerry", age: 20 };
}
};
// 组件
function Fetcher(props) {
const user = Api[props.name](); //获取name并执行函数获取数据
return props.children(user); //通过props.children返回数据
}
复合内容为数组时其实实质上也是jsx
假如我们想要对复合内容(类型为数组的jsx
)进行过滤,值过滤出类型为type的元素进行展示:
我们可以用到React.Children.map
方法,遍历复合内容,并过滤出我们想要的
function Filter({ children, type }) {
return (
<div>
{React.Children.map(children, child => {
if (child.type !== type) {
return;
}
return child;
})}
</div>
);
}
<Filter type="p">
<h1>react</h1>
<p>react很不错</p>
<h1>vue</h1>
<p>vue很不错</p>
</Filter>
若我们想对传进来的复合内容进行修改,则必须谨记:
复合内容为jsx,是虚拟节点,内容不能修改,因此如果我们要修改它的内容,那我们只能克隆对应的元素在做自己想要的修改即可,如:
现在我有一个按钮组,结构如下
<RadioGroup name="mvvm">
<Radio value="vue">vue</Radio>
<Radio value="react">react</Radio>
<Radio value="react">angular</Radio>
</RadioGroup>
我不想给每个按钮都加一个name
,而是给Radio
的父组件RadioGroup
加一个name
,然后让RadioGroup
循环遍历每个Radio
,对每个Radio
加上同一个name
该怎么操作呢?
如下
function RadioGroup(props) {
return (
<div>
{React.Children.map(props.children, child => {
// vdom不可更改,克隆一个新的去改才行
return React.cloneElement(child, { name: props.name });
})}
</div>
);
}
function Radio({children, ...rest}) {
return (
<label>
<input type="radio" {...rest} />
{children}
</label>
);
}
这里需要注意的一点就是,再给Radio
赋值的时候要注意props
包含children
和父级组件传进来的值
。
因此Radio接收参数时需要做分割,分为children复合内容部分
和剩余部分
,剩余部分
传给input作为属性值。
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了解决办法。
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
你之前可能把它们叫做“无状态组件”。但现在我们为它们引入了使用 React state 的能力,所以我们更喜欢叫它”函数组件”。
自定义hook可以让我们将组件逻辑提取到一个可重复利用的组件内。
这里会有两个案例讲解自定义hook的使用
第一个简单的例子:
import {useState, useEffect} from 'react'
function Comp() {
let [num, setNum] = useState(0)
useEffect(() => {
setTimeout(() => {
setNum(++num)
}, 2000)
}, [])
return (
<h2>Comp -- {num}</h2>
)
}
function App() {
return (
<div className="App">
<Comp1></Comp1>
</div>
);
}
export default App;
如上所示,我们可以将Comp
组件的
let [num, setNum] = useState(0)
useEffect(() => {
setTimeout(() => {
setNum(++num)
}, 2000)
}, [])
给提取到一个单独的组件中,这时候便是一个自定义hook,使得我们调用的代码更简洁,且逻辑抽离到了一起。
我们将上述代码抽离到./hooks/numHook.js
中。
import {useState, useEffect} from 'react'
export default function useNum() {
let [num, setNum] = useState(0)
useEffect(() => {
setTimeout(() => {
setNum(++num)
}, 2000)
}, [])
return [num, setNum]
}
这时候我们在需要引用该hook的地方进行导入引用即可,例子将可以改写为:
import useNum from './hooks/numHook'
function Comp() {
let [num] = useNum()
return (
<h2>Comp -- {num}</h2>
)
}
function App() {
return (
<div className="App">
<Comp></Comp>
</div>
);
}
export default App;
这就是最简单的自定义hook。
这里我们要做的自定义Hook是发起数据请求的hook
实现效果如下:
安装bootstrap:
npm i -S bootstrap
server端:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors())
app.get('/list', (req, res) => {
// 当前页和每页多少条
let {currentPage, perSize} = req.query
perSize = parseInt(perSize, 10)
// 数据 页数和条数对应得数据
let list = []
// 总数据条数
let total = 66
// 总页数
let pageCount = Math.ceil(total / perSize)
// 起始索引
let offset = (currentPage - 1) * perSize
if(currentPage >= pageCount) {
perSize = total % perSize
}
for(let i = offset; i < offset + perSize; i++) {
list.push({id: i+1, name: 'cai-'+(i+1)})
}
res.json({
currentPage,
perSize,
total,
pageCount,
list
})
})
app.listen(8080, () => {
console.log('listening on 8080...')
})
记得先启动服务端:
node server
在./hooks/useRequest.js
中:
import {useState, useEffect} from 'react'
export default function useRequest() {
/* 用户可更改的请求数据 */
let [options, setOptions] = useState({currentPage: 1, pageSize: 10})
let [data, setData] = useState({
total: 0,
pageCount: 0,
list: []
})
// 请求数据
function reqeust() {
let {currentPage, pageSize} = options
fetch(`http://localhost:8080/list?currentPage=${currentPage}&perSize=${pageSize}`)
.then(res => res.json())
.then(res => setData({...res}))
}
useEffect(reqeust, [options])
return [data, options, setOptions]
}
这里的options state是单独抽离出来的,因为我们要返回给组件setOptions让他自定义请求参数(如:用户点击下一页时我们要重新改变请求参数的currentPage
为当前页+1)。
onClick={() => {
setOptions({ ...options, currentPage: index + 1 })
}}
组件一旦挂载我们就发起数据请求,然后将通过setData
设置数据,并将data
返回。可以看到最后返回的格式是[data, options, setOptions],意味着我们在组件内可以通过类似useState
的方式获取自定义hook的值。
下面组件的取法:
let [data, options, setOptions] = useRequest()
在./pages/Tabel.js
中:
import {useState} from 'react'
import useRequest from '../hooks/useRequest'
export default function Table() {
let [data, options, setOptions] = useRequest()
let { list, pageCount } = data
let [size, setSize] = useState(10)
return (
<>
<table className="table table-striped">
<thead>
<tr>
<th>id</th>
<th>name</th>
</tr>
</thead>
<tbody>
{list.map((item) => {
return (
<tr key={item.id}>
<td>{item.id}</td>
<td>{item.name}</td>
</tr>
)
})}
</tbody>
</table>
<div>
<nav>
{/* 分页组件 */}
<ul className="pagination">
{new Array(pageCount).fill(0).map((item, index) => {
return (
<li
className="page-item"
key={index}
onClick={() => {
setOptions({ ...options, currentPage: index + 1 })
}}
>
<a className="page-link" href="#">
{index + 1}
</a>
</li>
)
})}
</ul>
</nav>
{/* 下拉框 */}
<select
className="form-select"
value={size}
onChange={(e) => {setSize(e.target.value); setOptions({currentPage: 1, pageSize: e.target.value})}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={20}>20</option>
</select>
</div>
</>
)
}
useState的用法很简单也很方便,比起class的方式简直要简洁得不少
import React, { useState } from 'react';
export default function Example() {
const [count, setCount] = useState(0)
const [fruit, setFruit] = useState('banana')
return (
<div>
你点击了{count}次
<button onClick={() => setCount(count+1)}>点击</button>
<p>{fruit}</p>
</div>
)
}
如上例子所示,useState(0),0表示初始值,而将useState解构成两个成员,一个是状态属性,一个是更改状态的函数,其中useState有多个,这样子就比class的形式要简洁多了。
你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。
useEffect
就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 API。
例如,下面这个组件在 React 更新 DOM 后会设置一个页面标题:
import React, { useState, useEffect } from 'react';
export default function Example() {
const [count, setCount] = useState(0)
const [fruit, setFruit] = useState('banana')
//相当于componentDidMount 和 componentDidUpdate
useEffect(() => {
// 使用浏览器的API更新页面标题
document.title = `您点击了 ${count} 次`
})
return (
<div>
你点击了{count}次
<button onClick={() => setCount(count+1)}>点击</button>
</div>
)
}
当你调用 useEffect
时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候
副作用函数还可以通过返回一个函数来指定如何“清除”副作用。例如,在下面的组件中使用副作用函数来订阅好友的在线状态,并通过取消订阅来进行清除操作
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
因为useEffect会在组件更新时调用,所以我们可以维护一个状态,当这个状态改变,我们调用函数重新发送请求。
import React, { useState, useEffect } from 'react';
export default function Example() {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
function request() {
setCount(++count)
}
useEffect(() => {
request()
},[flag])
return (
<div>
你点击了{count}次
<button onClick={() => setFlag(!flag)}>点击</button>
</div>
)
}
当然,useEffect
和useState
一样可以同时存在多个,还可以指定每次副作用函数只对那些起作用,和指定那些副作用函数只执行一次,因为比如我们要在useEffect
中异步获取数据,我们总不能每次渲染操作都获取一次,因此有下面的解决方案:
import React, { useState, useEffect } from 'react';
export default function Example() {
const [count, setCount] = useState(0)
const [fruit, setFruit] = useState('banana')
//相当于componentDidMount 和 componentDidUpdate
useEffect(() => {
// 使用浏览器的API更新页面标题
// 该副作用函数只对count和fruit起作用
document.title = `您点击了 ${count} 次`
console.log(count)
}, [fruit, count])
// 只会调用一次
useEffect(() => {
console.log('异步获取数据操作')
},[])
return (
<div>
你点击了{count}次
<button onClick={() => setCount(count+1)}>点击</button>
</div>
)
}
如上所示,我们可以在useEffect传入一个数组参数,里面为空则表示只执行一次,如果里面有状态则表示只对里面的状态起作用。
为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
React 何时清除 effect? React 会在组件卸载的时候执行清除操作,相当于类组件的componentWillUnmount
,也会在每次组件更新之前调用该回调,相当于componentWillUpdate
。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。
了解了 useEffect
可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
其他的 effect 可能不必清除,所以不需要返回。
useEffect(() => {
document.title = `You clicked ${count} times`;
});
我们通过createContext
创建一个Context之后,在子组件可以通过useContext
拿到我们在祖先组件创建的Context
,但是前提是我们需要对子组件用Context.Provider
标签进行包裹,然后通过value
属性传值。
这里需要注意 const states = useContext(Context)
只能在子组件内部调用,不可在顶级元素使用。
例子如下:
import {createContext, useContext} from 'react'
const Context = createContext()
function Child() {
const states = useContext(Context)
return (
<div>
{states.map(state => {
return (
<h2>{state}</h2>
)
})}
</div>
)
}
function App() {
return (
<div className="App">
<Context.Provider value={[1,2,3]}>
<Child></Child>
</Context.Provider>
</div>
);
}
export default App;
在这里我们通过Context.Provider
传输了一个数字数组到子代组件,子代组件通过const states = useContext(Context)
获取到父组件传过来的数据并进行展示。
假如我们要给多个子组件提供同一份数据,那么我可以将提供数据的父组件给抽离成一个单独的负责提供数据的组件:
import {createContext, useState} from 'react'
export const Context = createContext()
export function CountContextProvider({children}) {
let [count, setCount] = useState(10)
const countObj = {
count,
add() {
setCount(++count)
},
minus() {
setCount(--count)
}
}
return (
<Context.Provider value={countObj}>
{children}
</Context.Provider>
)
}
子组件:
import {useContext} from 'react'
import {Context, CountContextProvider} from './CountContextProvider'
function CountChild() {
const {count, add, minus} = useContext(Context)
return (
<div>
<h2>{count}</h2>
<button onClick={() => {add()}}>增加</button>
<button onClick={() => {minus()}}>减少</button>
</div>
)
}
let r = () => {
return (
<CountContextProvider>
<CountChild/>
</CountContextProvider>
)
}
export default r
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
因为典型的React应用数据是通过Props属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
总体步骤
1.creareContext
2.MyContext.Provider包裹通过value传值
3.在需要该值的组件static contextType = ThemeContext;接收context
何时使用 Context
Context的目的是共享那些应该在全局共享的数据,例如当前用户的Token数据,或者首选语言。举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式:
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
// Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
// 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
// 因为必须将这个值层层传递所有组件。
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}
使用 context, 我们可以避免通过中间元素传递 props:
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
使用Context之前应该有什么考虑?
使用Context之前我们必须清楚,Context会使得组件的复用性变差,它主要应用场景在于很多不同层级的组件需要访问同样一些的数据,请谨慎使用。
如果只是为了避免层层传递一些属性,组件组合
有时候是比Context更好的解决方案:
如:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
当我们发现user只在Avatar使用到但其他上层组件都要层层传递的时候,代码会变得非常冗余。解决方案除了用Context之外,我们还可以将该Avatar组件自身传递下去,这样中间组件无需知道 user
或者 avatarSize
等 props:
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
// 现在,我们有这样的组件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}
这种对组件的控制反转减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。
而且你的组件并不限制于接收单个子组件。你可能会传递多个子组件,甚至会为这些子组件(children)封装多个单独的“接口(slots)
Context可以有三种用法
就是通过Consumer包裹需要拿到值的元素,这样子那个元素就能拿到上下文的值了
import React from 'react'
function Child(props) {
return (
<div>Child: {props.foo}</div>
)
}
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;
export default function ContextTest() {
return (
<div>
<Provider value={{foo: 'bar'}}>
<Consumer>
{ value => <Child {...value}></Child> }
</Consumer>
</Provider>
</div>
)
}
Provider设置值value,然后Consumer就可以拿到值,Consumer内部组件可以通过Props.children格式为带参函数的形式拿到value,然后让对应组件渲染结果。
Hook是一种很方便的存在,它让我们不用通过Consumer就可以很简便的取到上级传给我们的值,但是有版本限制,必须
import React, {useContext} from 'react'
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;
function Child2() {
const value = useContext(MyContext)
return (
<div>Child2: {value.foo}</div>
)
}
export default function ContextTest() {
return (
<div>
<Provider value={{foo: 'bar'}}>
<Child2></Child2>
</Provider>
</div>
)
}
import React from 'react'
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;
class Child3 extends React.Component {
static contextType = MyContext
render() {
return (
<div>Child3: {this.context.foo}</div>
)
}
}
export default function ContextTest() {
return (
<div>
<Provider value={{foo: 'bar'}}>
<Child3></Child3>
</Provider>
</div>
)
}
总共有三种方法取到Context的值,如下所示:
import React, {useContext} from 'react'
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;
// Consumer取值
function Child(props) {
return (
<div>Child: {props.foo}</div>
)
}
// Hook取值
function Child2() {
const value = useContext(MyContext)
return (
<div>Child2: {value.foo}</div>
)
}
// 类的方式指定静态属性contextType,这样子上下文就会自动有个context
class Child3 extends React.Component {
static contextType = MyContext
render() {
return (
<div>Child3: {this.context.foo}</div>
)
}
}
export default function ContextTest() {
return (
<div>
<Provider value={{foo: 'bar'}}>
{/* Consumer包裹元素 */}
<Consumer>
{ value => <Child {...value}></Child> }
</Consumer>
<Child2></Child2>
<Child3></Child3>
</Provider>
</div>
)
}
高阶组件设计功能目的:
1、给input框进行包装,从而对input事件进行管理
2、把触发的事件收集的数据放在一起,按照参数传进来的规则进行校验
3、可以提供给当前表单一个校验方法,用来判断当前的校验是否全都通过,从而决定是否提交表单信息,或者不提交
1的思路:
对input框进行包装,也就是用高阶组件进行包装,在此我们需要谨记不能对vdom进行扩展,而是通过React.cloneElement克隆组件,再进行扩展,扩展内容包括:
设置name属性
设置value值(默认为空串)
绑定onChange事件并让包装器进行管理,也就是状态提升(当子组件发生某件事的时候,让父组件处理),这样子我们就可以再父组件中管理所有input的值
在父组件的handleChange中,我们通过解构方式获取name和value,然后进行setState改变值,这样子render函数就会重新执行
当然,在setState之后我们需要进行验证,这里我们需要注意setState可能是异步的,我们的校验函数不能直接放在setState之后,而是放置在setState执行后的回调函数内。
2的思路
数据收集我们可以放在input包装器内,因为包装器传入了两个参数,第一个是校验字段,第二个是参数选项,在包装器进行克隆元素的时候我们可以缓存这些数据。
缓存步骤:
3的思路
在最外面的包装组件内,定义一个校验字段函数,函数首先获取2所缓存的校验规则,然后用数组的some方法遍历每一项rule,如果有一项不符合就设置错误信息并返回false,否则将错误信息置空。
接着再定义一个校验函数,作用就是对每个input项进行校验,可以通过Object.keys拿出缓存的options再进行map对每个项都进行校验字段。
最后拿到的是一个bool数组,通过数组的every方法判断是否校验成功,如果成功则调用回调函数,并传入成功标识符和相应数据。
最后要展示错误信息,通过判断当前字段的错误信息是否存在,如果存在则显示错误信息,否则不展示。
import React from 'react'
import {Button, Input} from 'antd'
// 创建一个高阶组件:扩展现有a表单,事件处理、数据收集、校验功能
function kFormCreate(Comp) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {}
this.options = {}
}
// 当值变化,改变state值
handleChange = e => {
const {name, value} = e.target
console.log(name, value)
this.setState({
[name]: value
},// 设置完值在做验证
() => this.validateField(name))
}
validate = cb => {
// 对所有Input框都进行验证
const rets = Object.keys(this.options).map(field => this.validateField(field))
const ret = rets.every(v => v === true)
cb(ret, this.state)
}
// 对单项input进行规则验证
validateField = field => {
const rules = this.options[field].rules
// some找到一个验证不通过的规则就不再继续遍历
const ret = rules && !rules.some(rule => {
if(rule.required && !this.state[field]) {
// 验证失败,我们要设置错误信息
this.setState({
[field+'Message']:'该字段不能为空'
})
return true
}
return false
})
// 如果验证成功,错误信息为空串,
if(ret){
this.setState({
[field+'Message']:''
})
}
return ret
}
getFieldDec = (field, options) => {
// 这里做了数据收集与状态提升到包装器内部处理
// 保存当前输入配置项
this.options[field] = options
return InputComp => (
<div>
{React.cloneElement(InputComp, {
name: field,
value: this.state[field] || '',
// 状态提升
onChange: this.handleChange
})}
{this.state[field+'Message'] && <p style={{color: 'red'}}>{this.state[field+'Message']}</p>}
</div>
)
}
render() {
return <Comp getFieldDec={this.getFieldDec} validate={this.validate}></Comp>
}
}
}
@kFormCreate
class KForm extends React.Component {
onSubmit = e => {
this.props.validate((isValid, data) => {
if(isValid) {
console.log("验证通过!")
// 做登录相关的逻辑操作
}
})
}
render() {
const { getFieldDec } = this.props
return (
<div>
{getFieldDec("userName", {
rules: [{ required: true, message: "Please input your username!" }]
})(
<Input/>
)}
<Button onClick={this.onSubmit}>提交</Button>
</div>
)
}
}
export default KForm
cnpm install redux --save
在文件store.js内:
import {createStore} from 'redux'
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'add':
return state + 1
case 'minus':
return state - 1
default:
return state
}
}
const store = createStore(counterReducer)
export default store
我们通过createStore
创建Store
对象,参数为Reducer
对象,state
是当前store
的属性值,可以是一个对象,在通过对action
参数的type
属性进行判断,修改state
的值。
接下来我们在需要引用store
里面的全局状态的组件引入store
,在此之前我们先通过在控制台打印一下store到底是什么:
如上所示,我们创建的Store
对象包括了这么些个属性,我们可以通过getState
方法获取state的值,也可以通过dispatch
方法改变state
的值,还有最后我们还要在页面渲染那里用subscribe
进行订阅。
import React from 'react'
import store from '../store'
// 原始Store组件
function StoreTest() {
return (
<>
<div>{store.getState()}</div>
<div>
<button onClick={() => store.dispatch({type: 'add'})}>+</button>
<button onClick={() => store.dispatch({type: 'minus'})}>-</button>
</div>
</>
)
}
export default StoreTest
如上所示,我们可以通过getState
获取到前面store.js
的state
值,再给绑定点击事件,执行dispatch改变state的值。
订阅
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import store from './store';
const render = () => {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
}
render()
store.subscribe(render)
只有订阅了,store
状态才能在随着页面渲染时发生改变,不然你将看不到效果。
上面的原始方法存在一个问题:在需要共享全局状态的组件,我们都需要引入store.js,这样子显得很繁杂,那么要如何改变这个问题呢?
还记得我们之前可以通过上下文进行隔代传值吗?是的,只要我们把store
通过Provider
传给根组件,其他组件自然也就可以不用引入store.js
了。为此,react已经帮我们做好了,我们可以通过react-redux
依赖来进行相应的操作。
cnpm install react-redux --save
在Provider
传入store
// index.js
import store from './store';
import {Provider} from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<App/>
</React.StrictMode>
</Provider>,
document.getElementById('root')
);
在需要用到store
里面状态的组件里
import React from 'react'
import store from '../store'
import {connect} from 'react-redux'
const mapStateToProps = state => ({num: state})
const mapDispatchToProps = {
add: () => ({type: 'add'}),
minus: () => ({type: 'minus'})
}
// 经过react-redux上下文处理的store组件
class StoreTest2 extends React.Component {
render() {
return (
<>
<div>{this.props.num}</div>
<button onClick={() => this.props.add()}>+</button>
<button onClick={() => this.props.minus()}>-</button>
</>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(StoreTest2)
在这里,我们使用了connect
高阶组件,传入两个配置项,再对需要扩展的组件进行包装,注意这里的写法已经变了,逻辑已经被我们提到了外面,此时ShowTest2
只是一个傻瓜式组件,不负责dispatch
也不用调用getState
方法,connect
内部的两个配置项分别帮我们把state
的值映射到被包装组件StoreTest2
的内部,并放在props
里面,这样子我们在组件内就可以直接通过props
拿到state
和dispatch
了。
只是个语法糖
@connect(mapStateToProps, mapDispatchToProps)
class StoreTest3 extends React.Component {
render() {
return (
<>
<div>{this.props.num}</div>
<button onClick={() => this.props.add()}>+</button>
<button onClick={() => this.props.minus()}>-</button>
</>
)
}
}
export default StoreTest3
如图所示,在处理action
的时候会先经过中间件层的处理,中间件会识别传进来的action
到底是个对象还是函数,如果是函数则表明是异步请求,要先执行完才能继续往下一步进行。
安装两个中间件
npm install redux-thunk redux-logger -S
redux-thunk
用来处理异步请求,redux-logger
是一个日志打印中间件。
使用
// store.js
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import logger from 'redux-logger'
// reducer定义
...
// 中间件执行步骤:先logger再thunk
const store = createStore(counterReducer, applyMiddleware(logger, thunk))
export default store
需要进行异步操作的地方
import React from 'react'
import store from '../store'
import {connect} from 'react-redux'
const mapStateToProps2 = state => ({num: state})
const mapDispatchToProps2 = {
add: () => ({type: 'add'}),
minus: () => ({type: 'minus'}),
asyncAdd: () => dispatch => {
setTimeout(() => dispatch({type: 'add'}), 1000)
}
}
@connect(mapStateToProps2, mapDispatchToProps2)
class StoreTest4 extends React.Component {
render() {
return (
<>
<div>{this.props.num}</div>
<button onClick={() => this.props.add()}>+</button>
<button onClick={() => this.props.minus()}>-</button>
<button onClick={() => this.props.asyncAdd()}>+</button>
</>
)
}
}
export default StoreTest4
这里新增了一个asyncAdd
方法,注意这里的写法是返回一个带dispatch
参数的函数,而不像其他同步操作直接返回对象。
由于我们全局共享状态是有多种类型的,如用户的登录态、全局的主题、用户信息等,因此我们将所有全局共享状态都放置再store.js将会导致后期变得可维护差,为了使代码更具有可用性、易用性、可维护性。我们应该将每一部分的状态都放置在一个文件。
步骤
创建store
文件夹,创建index.js
为入口,将来通过combineReducers
引入所有的状态,我们将上面的counterReducer
和其相对于的action creator
给分离到一个文件。
// store/counter.redux.js
export const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'add':
return state + 1
case 'minus':
return state - 1
default:
return state
}
}
// action creator
export const add = () => ({type: 'add'})
export const minus = () => ({type: 'minus'})
export const asyncAdd = () => dispatch => {
setTimeout(() => dispatch({type: 'add'}), 1000)
}
在入口文件引入
// store/index.js
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import {counterReducer} from './counter.redux'
const store = createStore(
counterReducer,
applyMiddleware(logger, thunk)
)
export default store
这样就完成了简单的分离,接下来我们还要修改一下程序入口导入store
的路径
// index.js
import store from './store/index'
在需要用到counter
状态的地方
...
import { add, minus, asyncAdd } from '../store/counter.redux'
const mapStateToProps3 = state => ({num: state})
const mapDispatchToProps3 = { add, minus, asyncAdd }
@connect(mapStateToProps3, mapDispatchToProps3)
class StoreTest5 extends React.Component {
render() {
return (
<>
<div>{this.props.num}</div>
<button onClick={() => this.props.add()}>+</button>
<button onClick={() => this.props.minus()}>-</button>
<button onClick={() => this.props.asyncAdd()}>+</button>
</>
)
}
}
export default StoreTest5
这里我们只需将对应的action creator
导入,使得管理起来更加方便。实现了统一在store文件夹下的文件对所有共享状态进行管理,可维护性就瞬间提升了。
npm install --save react-router-dom
index.js
// ... 其他包导入
import {BrowserRouter} from 'react-router-dom'
ReactDOM.render(
<BrowserRouter>
<Provider store={store}>
<React.StrictMode>
<App/>
</React.StrictMode>
</Provider>
</BrowserRouter>,
document.getElementById('root')
);
如上所示,我们需要给根组件包裹一个BrowserRouter
标签,这样子我们就可以在浏览器使用路由了。
在react
中,路由即组件,简单用法如下:
import { Link, Route } from 'react-router-dom'
function About() {
return (<div>关于页</div>)
}
function Home() {
return (<div>首页</div>)
}
export default function RouteTest() {
return (
<div>
<ul>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
</ul>
<div>
<Route exact path='/' component={Home}></Route>
<Route path='/about' component={About}></Route>
</div>
</div>
)
}
Link
作用类似于a
标签,会在页面展示让用户点击,而Route
就是一个真正的路由映射组件,如上所示,path
指定访问路径,component
指定要展示的组件。其中因为react-router
的路由包容性(只要path匹配到,就会渲染),我们还需对path
为/
的路由添加一个exact
字段,这样子就只会精准匹配了。
被Route
标签包裹的组件,传入的值就不是props
了,而是一个路由器对象,我们可以打印看看路由器对象到底长啥样
如上所示,有history
、location
、match
三个主要属性
// 1.history: 导航指令
// 2.match: 获取参数信息
// 3.location: 当前url信息
react
路由是包容性的,也就是匹配到的路由都会被展示出来。但是我们可以通过Switch
标签让react
路由不再具有包容性,只展示匹配到的第一个路由:
import { Link, Route, Switch, NavLink } from 'react-router-dom'
import { BrowserRouter } from 'react-router-dom'
function About(router) {
return <div>about</div>
}
function Home() {
return <div>home</div>
}
function NotFound() {
return (
<div>
<h2>not found</h2>
</div>
)
}
function App() {
return (
<div className="App">
<BrowserRouter>
{/* 内联样式 */}
<div>
<NavLink activeStyle={{color: 'blue'}} to="/home">Home</NavLink>
<NavLink activeStyle={{color: 'red'}} to="/about">about</NavLink>
</div>
{/* 一个switch为一组,只会展示第一个被匹配的组件 */}
<Switch>
<Route component={Home} path="/home"></Route>
<Route component={About} path="/about"></Route>
<Route component={NotFound}></Route>
</Switch>
</BrowserRouter>
</div>
)
}
如上所示,我们让Switch
包裹的路由为一组,在这一组中只匹配第一个匹配到的路由则停止继续匹配,可以注意到我们在最后放了一个NotFound路由,这也是路由找不到时可以写的写法,因为没有path
,前面的匹配不到则NotFound
一定会被匹配到。
redirect就是重定向组件,有一个from和to属性
常用基础写法如下
{/* 将from 定向到 to */}
<Redirect from="/abc" to='/about'></Redirect>
<Redirect to='/home'></Redirect>
<Route path="/info" render={() => {
return login ? <Info/> : <Redirect to="/home"></Redirect>
}}></Route>
子路由部分包括动态路由,那么在react
中如何编写子路由呢?
举个例子,比如我们想要在上面的About
页嵌套子路由,写法可以参考下面
function About(params) {
return (
<div>
<h3>个人中心</h3>
<div>
<Link to="/about/me">个人信息</Link>
<Link to="/about/order">订单查询</Link>
</div>
<Switch>
<Route path="/about/me" component={() => <div>Me</div>} />
<Route path="/about/order" component={() => <div>order</div>} />
<Redirect to="/about/me" />
</Switch>
</div>
);
}
很简单,只要在About
组件嵌套路由即可。
但是需要注意一点:
写子路由时父亲路由不能开启路由严格模式,即不能在父组件加个exact属性,否则一旦开启严格模式,子路由将不会被路由系统找到,从而匹配不到,如:
import { Link, Route, Switch, NavLink, Redirect } from 'react-router-dom'
import { BrowserRouter } from 'react-router-dom'
function AAA() {
return (<h2>aaa</h2>)
}
function BBB() {
return (<h2>bbb</h2>)
}
function About(router) {
return (<div>
<Link to="/about/aaa">a</Link>
<Link to="/about/bbb">b</Link>
<Route path="/about/aaa" component={AAA}></Route>
<Route path="/about/bbb" component={BBB}></Route>
</div>)
}
function Home() {
return <div>home</div>
}
function App() {
return (
<div className="App">
<BrowserRouter>
{/* 内联样式 */}
<div>
<NavLink activeStyle={{color: 'blue'}} to="/home">Home</NavLink>
<NavLink activeStyle={{color: 'red'}} to="/about">about</NavLink>
</div>
{/* 一个switch为一组,只会展示第一个被匹配的组件 */}
<Switch>
<Route exact component={Home} path="/home"></Route>
<Route exact component={About} path="/about"></Route>
</Switch>
</BrowserRouter>
</div>
)
}
export default App
在这里我们对/about
路由开启了严格模式,而About
组件下有子组件,但是我们会发现访问子组件的时候,子组件并没有展示出来,这是因为/about
路由开启了严格模式从而导致子路由匹配不到。
动态路由对于在url
传值的很简单,使用:参数
即可,如果要通过query
传值可以在Route
加上相应的属性值,然后在对应组件的参数路由对象
中的match
字段便可拿到
import {Link, Route, Switch, Redirect} from 'react-router-dom'
function Detail(props) {
return (<div>
<ul>
<li></li>
</ul>
详情页:{props.match.params.id}
</div>)
}
<Link to='/detail'>Detail</Link>
<Route path='/detail/:id' component={Detail}></Route>
react传参可以通过三种方式,可以通过params、query或者state对象进行传参
import { Link, Route, Switch, NavLink, Redirect } from 'react-router-dom'
import { BrowserRouter } from 'react-router-dom'
import { useState } from 'react'
import qs from 'querystring'
/**
* 路由传参
*/
function Home(props) {
let [data] = useState([
{id: 1, title: '新闻'},
{id: 2, title: '财经'},
{id: 3, title: '体育'},
{id: 4, title: '文化'},
])
return (<div>
<ul>
{/* params传参 /home/:id/:title */}
{/* {
data.map(item => <Link key={item.id} to={`${props.match.path}/total/${item.id}/${item.title}`} >{item.title}</Link>)
} */}
{/* query传参 /home?id=1&title=weibo */}
{/* {
data.map(item => <Link key={item.id} to={`${props.match.path}/total?id=${item.id}&title=${item.title}`} >{item.title}</Link>)
} */}
{/* state参数 */}
{
data.map(item => <Link key={item.id} to={{pathname: `${props.match.path}/total`, state: {id: item.id, title: item.title} }} >{item.title}</Link>)
}
<Route path={`${props.match.path}/total`} component={Total}></Route>
</ul>
</div>)
}
// 子组件
function Total(props) {
console.log(props)
// params取值
// let {id, title} = props.match.params
// query取值
// let queryStr = props.location.search
// let queryObj = qs.parse(queryStr.slice(1))
// console.log(queryObj)
// state参数取值
let stateObj = props.location.state
console.log(stateObj)
return (
<div>total</div>
)
}
function App() {
return (
<div className="App">
<BrowserRouter>
{/* 内联样式 */}
<div>
<NavLink activeStyle={{color: 'blue'}} to="/home">Home</NavLink>
</div>
{/* 一个switch为一组,只会展示第一个被匹配的组件 */}
<Switch>
<Route component={Home} path="/home"></Route>
</Switch>
</BrowserRouter>
</div>
)
}
export default App
reducers:数据控制器,数据修改者
store: 数据仓库
action: 描述发生了什么的一个对象
单一数据源(只创建一个store)
state是只读的(不允许被直接修改,具体修改在reducer集中化处理)
reducer使用纯函数执行修改(有个固定的值,而不是在reducer里有而外的操作,如在reducer执行一些不确定的操作,例如发起ajax异步请求,这就违犯了redux的原则)
首先引入redux的createStore
import {createStore} from 'redux'
然后我们需要创建一个store,createStore的第一个参数是reducers
,reducers
结构如下
// 默认状态
let defaultState = {
count: 0,
name: 'cai'
}
// reducers
function reducers(state = defaultState, action) {
console.log(action)
switch(action.type) {
case 'add':
return {
...state,
count: state.count+action.val,
}
default:
return state
}
}
需要注意的是,我们还需要给reducers
的第一个参数state
指定一个默认值。当我们通过判断action
的type进行改变state
的值时,我们需要通过对象展开运算符展开state
的其他属性值,因为改变state
的值是覆盖操作,所以我们必须要保持其他不需要改的属性不变,而...state
不能写在后面,不然被修改的值会被覆盖。
接着传入createStore的第一个参数
//创建store
let store = createStore(reducers);
在视图里我们可以通过以下三个API来操作store
简单用法如下:
在这里需要注意的是,在react中,component要重新渲染的话,需要通过setState改变state的值或者改变props的值导致render函数重新执行,所以我们可以利用这点,结合store.subscribe订阅,更新视图。
export default class MyComp extends Component {
componentDidMount() {
store.subscribe(() => {
this.setState({})
})
}
// 同步请求
addHandler = () => {
store.dispatch({
type: 'add',
val: 2
})
}
// 异步请求
requsetHandler = () => {
fetch("http://localhost:4000/api/citylist")
.then(res => res.json())
.then(res => {
store.dispatch({type: 'request', data: res})
})
}
render() {
let {count} = store.getState()
return (
<div>
<h2>{count}</h2>
<button onClick={this.addHandler}>add</button>
</div>
)
}
}
随着项目的逐渐扩大,为防止逻辑之间冗余,我们需要对actions、reducers
和store
进行单独的抽离。
以上面的例子为例:
新建一个store文件夹,里面包含index.js、reducers.js、actions.js
// index.js
import {createStore, applyMiddleware} from 'redux'
import reducers from './reducers'
// 中间件 redux-thunk
import thunk from 'redux-thunk'
//创建store
let store = createStore(reducers, applyMiddleware(thunk));
export default store
由于需要操作异步数据,我们需要引入redux-thunk
中间件,放在createStore的第二个参数内。
// reducers.js
// 默认状态
let defaultState = {
count: 0
}
// reducers
export default function reducers(state = defaultState, action) {
console.log(action)
switch(action.type) {
case 'add':
return {
...state,
count: state.count+action.val,
}
default:
return state
}
}
在这里我们还把fetch整个操作给抽到了actions
里面,意味着我们我们在调用的时候要用到store.dispatch()
,在redux中,当我们dispatch
的是一个函数的时候,则表明为异步操作,当dispatch
的是一个对象则表明为同步操作,所以我们在fetch
外面包裹了一层函数,且redux
知道我们需要用到store.dispatch
,所以他给我们封装了一个带dispatch
的函数,我们可以直接使用,而不需要引入store
。
// actions.js
export const add = () => {
return {
type: 'add',
val: 10
}
}
// 异步的action需要返回一个函数
// 不需要用到store,redux知道我们要用到dispatch,作为参数传进来了
export const request = () => {
return dispatch => {
fetch("http://localhost:4000/api/citylist")
.then(res => res.json())
.then(res => {
dispatch({type: 'request', data: res})
})
}
}
组件引用
import React, { Component } from 'react'
import store from '../store'
import {add, request} from '../store/actions'
export default class MyComp extends Component {
componentDidMount() {
store.subscribe(() => {
this.setState({})
})
}
addHandler = () => {
store.dispatch(add())
}
requsetHandler = () => {
store.dispatch(request())
}
render() {
let {count} = store.getState()
return (
<div>
<h2>{count}</h2>
<button onClick={this.addHandler}>add</button>
<h2>{1}</h2>
<button onClick={this.requsetHandler}>request</button>
</div>
)
}
}
在编写actions
的type时,reducers
也会用到type,如果actions
的type与reducers
的type不一致,或者说拼写错了,那么reducers
将不会触发,而且不会报错提醒你,这时候我们就需要用到常量提取,将type提取为常量,再引入使用。
如:
constants/index.js
export const ADD = 'add'
export const REQUEST = 'request'
actions.js
import {ADD, REQUEST} from './constants'
export const add = () => {
return {type: ADD, val: 2}
}
export const request = () => {
return dispatch => {
fetch("http://localhost:4000/api/foodtype")
.then(res => res.json())
.then(res => dispatch({type: REQUEST, data: res}))
}
}
reducers.js
import {REQUEST, ADD} from './constants'
let defaultState = {
count: 0,
name: 'cai'
}
export default function reducers(state = defaultState, action) {
switch (action.type) {
case ADD:
...
case REQUEST:
...
default:
return state
}
}
在上面直接使用redux的时候我们会发现在组件内我们用到很多对store的逻辑操作,而React的组件应该尽量是Pure Component,我们应该把处理逻辑都给抽取出去,这时候react-redux的作用就来了。
思想:把组件变成Pure Component,我们再通过connect(mapStateToProps, mapDispatchToProps)(Comp)
去连接store
,让这个组件可以拿到store
的数据进行展示。
这个时候就产生了两种组件区别:UI组件
与容器组件
容器组件在这里就是connect
之后返回的组件,而UI组件就是只负责展示数据的组件。
import React, { Component } from 'react'
import {add, request} from '../store/actions'
import {connect} from 'react-redux'
/**
* UI组件
*/
class Comp2 extends Component {
render() {
console.log('this.props =========>', this.props);
let {count, name, add, request} = this.props
return (
<div>
<h2>{count}</h2>
<button onClick={add}>add</button>
<h2>{name}</h2>
<button onClick={request}>request</button>
</div>
)
}
}
/**
* react-redux 容器组件
*/
function mapStateToProps(state) {
return {
count: state.count,
name: state.name
}
}
let mapDispatchToProps = {
add,
request
}
export default connect(mapStateToProps, mapDispatchToProps)(Comp2)
由于我们没有再在UI组件内使用store.subscribe
订阅,此时数据不会更新,所以我们可以通过改变props
让组件重新渲染,connect
第一个参数是mapStateToProps
,是一个函数,负责将state
映射到props
;connect
第二个参数是mapDispatchToProps
,是一个函数,负责将dispatch
映射到props
,这样子我们就能在UI组件内通过props
拿到store
的数据了,而且connect
内部还帮我们订阅了store
数据的变化,一但数据更新,props
也会跟着更新,所以就会重新渲染。
简单总结一下:React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store,目的就是让组件尽量变成PureComponent,然后用connect让这个组件跟store联系,connect还会帮我们订阅store的变化。
由于我们的actions
和reducers
是多个的,且不同组件会用到F:\学习\前端作品\JS学习\react\redux-pro\01-review\src\store\actions\request.js不同的actions
和reducers
,所以我们可以按照一个组件一个action | reducer
或者一个功能一个action | reducer
,这里采用的是后者。
将action
的抽取到不同文件很简单,只需将对应部分给抽取到对应文件,改变一下引入路径即可;而reducer
的抽取,我们要用的时候还需要通过combineReducers
将各个reducer
给合并为一个reducers
。
新建store/reducers文件夹:
// count.js
import {ADD} from '../constants'
let defaultState = {
count: 0,
}
export default function reducers(state = defaultState, action) {
console.log("state ===> ", state)
console.log("action ===> ", action)
switch (action.type) {
case ADD:
return {
...state,
count: state.count + action.val
}
default:
return state
}
}
// request.js
import {REQUEST_BEGIN, REQUEST_SUCCESS, REQUEST_FAIL} from '../constants'
let defaultState = {
name: 'cai',
isLoading: false,
err: {errCode: 0, errMsg: undefined }
}
export default function reducers(state = defaultState, action) {
console.log("state ===> ", state)
console.log("action ===> ", action)
switch (action.type) {
case REQUEST_BEGIN:
return {
...state,
isLoading: true
}
case REQUEST_SUCCESS:
return {
...state,
data: action.data,
isLoading: false,
err: {errCode: 0, errMsg: ''}
}
case REQUEST_FAIL:
return {
...state,
err: {errCode: action.err[0], errMsg: action.err[1]},
isLoading: false
}
default:
return state
}
}
// index.js
import countReducer from './count'
import requestReducer from './request'
import {combineReducers} from 'redux'
export default combineReducers({
count: countReducer,
request: requestReducer
})
这里需要注意:由于我们将reducers
给分离了,我们combineReducers
的时候分别给counrReducer
和requestReducer
给取了个别名count
和request
,我们在取值的时候需要加上命名空间:
之前的mapStateToProps
写法:
function mapStateToProps(state) {
return {
count: state.count,
name: state.name
}
}
现在:
function mapStateToProps(state) {
return {
count: state.count.count,
name: state.request.name,
}
}
新建store/actions文件夹:
// count.js
import {ADD} from '../constants'
export const add = () => {
return {type: ADD, val: 2}
}
// request.js
import { REQUEST_BEGIN, REQUEST_SUCCESS, REQUEST_FAIL } from '../constants'
export const request = () => {
return dispatch => {
// 请求前, isLoading --> true
dispatch({type: REQUEST_BEGIN, isLoading: true})
fetch("http://localhost:4000/api/foodtype")
.then(res => res.json())
.then(res => {
// 请求成功
return dispatch({type: REQUEST_SUCCESS, data: res})
})
.catch(err => {
// 请求失败
return dispatch({type: REQUEST_FAIL, err: [1, err.message]})
})
}
}
另外的:
在 根组件APP中我们需要提供store
作为props
传入需要用到react-redux
的组件。
方式一:直接传入
<div className="App">
<Comp1 store={store}></Comp1>
<Count store={store}></Count>
</div>
方式二:通过Provider统一传入(推荐方式)
import {Provider} from 'react-redux'
<div className="App">
<Provider store={store}>
<Comp1></Comp1>
<Count></Count>
</Provider>
</div>
gitee链接:https://gitee.com/cai-lunduo/react-redux
路由导航守卫是我们编写路由不可避免的一个部分,那么我们在react
如何编写路由导航呢?
答案:通过高阶组件扩展实现
现在我想实现一个常见的功能:访问About
页的时候判断用户是否处于登录态,如果是登录态则让他访问About
页,如果没登陆则重定向到Login
页。
我们一步步来,既然我们要判断用户是否处于登录态,由于登录态需要全局共享,我们需要用到redux
,则我们可以在store
下创建一个user.redux.js
,管理用户的登录状态,注意,我们login
动作也放在了这里,将来使用需要导入。
const initial = {
isLogin: false,
loading: false,
};
export default (state = initial, action) => {
switch (action.type) {
case 'requestLogin':
return {
isLogin: false,
loading: true,
}
case 'login':
return {
isLogin: true,
loading: false,
}
default:
return state
}
}
// action creator
export const login = () => dispatch => {
dispatch({type: 'requestLogin'})
setTimeout(() => {
dispatch({type: 'login'})
}, 3000)
}
在store/index.js
下引入这个Reducer
,并通过combineReducers
来合并所有的reducer
// store/index.js
import {createStore, applyMiddleware, combineReducers} from 'redux'
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import {counterReducer} from './counter.redux'
import user from './user.redux'
const store = createStore(
combineReducers({counterReducer, user}),
applyMiddleware(logger, thunk)
)
export default store
第一步完成,我们已经将用户的登录态放在全局进行管理。
接下来,我们先要创建一个自定义的组件,内部其实也是返回,但是我们将要对其功能进行增强。
function PrivateRoute({ component:Comp, isLogin, ...rest }) {
return (
<Route
{...rest}
render={props =>
isLogin ? (<Comp/>) : <Redirect to={{
pathname: '/login',
state: { redirect: props.location.pathname}
}}/>
}
/>
)
}
在这里我们做了这些工作:
1. 解构props为component, isLogin, ...rest
单独解构出component的目的:
1. 避免component属性传入Route,不然render将不生效!因为component优先级比render高
2. 给component重命名,因为react里面的组件是大写字母开头的
2. isLogin怎么来的?
我们等下会通过connect工厂函数链接react和redux,映射用户的登录状态到PrivateRoute的props中
3. 为什么要...rest?
因为我们原本的<Route/>要接收path等相关参数,我们在这里直接把这些参数搬过来放在我们将要返回的组件中
4. 返回全新组件<Route/>,在这个组件中,render是一个接收props的函数,我们在这判断用户的登录态,并返回不同的组件
1.如果用户已登录,则返回用户访问的About页面对应的组件
2.否则redirect到login页面,并给to对象里面设置一个state.redirect属性,将来登陆成功则自动定向到这个redirect去
Login
组件
function Login({location, isLogin, login}) {
const redirect = location.state && location.state.redirect || "/";
if(isLogin) {
return <Redirect path={redirect}/>
}
return (<div><button onClick={() => login()}>登录</button></div>)
}
这里我们从location
拿到重定向的地址,为将来登陆成功时指定回跳地址。
至此我们第二步完成了。
现在最后一步就是将全局的用户登录态给注入Login
和PrivateRoute
中,通过
connect(配置项)(被加工组件)
的方式注入
// ...其他包导入
import {login} from '../store/user.redux'
import {connect} from 'react-redux'
const Login = connect(
state => ({isLogin:state.user.isLogin}),
{login}
)(function({location, isLogin, login}) {
const redirect = location.state && location.state.redirect || "/";
if(isLogin) {
return <Redirect path={redirect}/>
}
return (<div><button onClick={() => login()}>登录</button></div>)
})
// 路由导航守卫
const PrivateRoute = connect(state => ({isLogin: state.user.isLogin}))(({ component:Comp, isLogin, ...rest }) => {
return (
<Route
{...rest}
render={props =>
isLogin ? (<Comp/>) : <Redirect to={{
pathname: '/login',
state: { redirect: props.location.pathname}
}}/>
}
/>
)
})
用法:
<PrivateRoute path='/about' component={About}></PrivateRoute>
redux-saga
是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。
redux-saga
内部用到了ES6的Generator功能,使得我们异步流程可以像简单的操作同步代码一样(步骤看起来像async/await),但是Generator的功能比async/await更加强大,同时可测试性还增强了。
初级教程:https://redux-saga-in-chinese.js.org/docs/introduction/BeginnerTutorial.html
如果对redux-saga
没有过了解,请点击上述链接到官方文档去看入门教程,写的非常详细也很容易明白。
你可能已经用了 redux-thunk
来处理数据的读取。不同于 redux thunk,你不会再遇到回调地狱了,你可以很容易地测试异步流程并保持你的 action 是干净的。
接下来我会以一个用户登录验证的功能来讲述redux-saga
。
store/user.redux.js
,redux中用户的登录态,我们给state设置三个状态,以此来判断用户的登录状态和展示错误信息。
const initial = {
isLogin: false,
loading: false,
error: ''
};
export default (state = initial, action) => {
switch (action.type) {
case 'requestLogin':
return {
isLogin: false,
loading: true,
error: ''
}
case 'login':
return {
isLogin: true,
loading: false,
error: ''
}
case 'requestError':
return {
isLogin: false,
loading: false,
error: action.message
}
default:
return state
}
}
// action creator for redux-thunk
// export const login = () => dispatch => {
// dispatch({type: 'requestLogin'})
// setTimeout(() => {
// dispatch({type: 'login'})
// }, 3000)
// }
export function login(uname) {
return { type: 'login', uname}
}
上面代码中, 被注释的代码是以前写
redux-thunk
的时候所遗留的,可以观察到redux-thunk
他在执行异步函数时需要返回一个函数,而在redux-saga
中,只需返回一个plain Object对象即可,(plain Object:纯粹的对象(通过 "{}" 或者 "new Object" 创建的))
接下来我们编写sagas.js
import { put, call, takeEvery } from 'redux-saga/effects'
const UserService = {
login(uname) {
return new Promise((resolve, reject) => {
// 模拟异步
setTimeout(() => {
if(uname === 'cai') {
resolve({id:1, name:'cai', mobile:'13226752873'})
}else {
reject('用户名或密码错误')
}
},2000)
})
}
}
function* login(action) {
try {
// 标识开始请求
yield put({type: 'requestLogin'})
const result = yield call(UserService.login, action.uname)
// 登录成功
yield put({type: 'login', result})
} catch(message) {
// 登录失败,将错误信息传入action,让user的reducer能够接收,为后续map到页面的props做准备
yield put({type: 'requestError', message})
}
}
function* mySaga() {
yield takeEvery('login', login)
}
export default mySaga
如上所示,我们使用了put
、call
、takeEvery
工具函数,按我个人理解:
call : 用来通知saga中间件执行异步函数的工具方法,最后会拿到异步函数执行后返回的数据
put : 用来通知saga中间件向redux进行dispatch操作,是同步的
takeEvery : 用来监听action,相当于进行了一层拦截, 然后再进行自己定义的操作,如上例所示,我们监听了login的调用,一旦login函数被触发,则会被takeEvery拦截到,然后进行我们自己定义的Generator,也就是function* login(action) 。
需要注意一点的是,dispatch并不由call和put派发,而是通知saga中间件让他进行dispatch
接下来在store
的入口文件对中间件进行注册和run一下我们自定义的mySaga,因为这样子takeEvery
才可以在全局对login
事件进行监听并派发相应动作。
// 其他包导入
import user from './user.redux'
import createSagaMiddleware from 'redux-saga'
import MySaga from './sagas'
// 1. 创建saga中间件
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
combineReducers({..., user}),
// 2. 注册saga中间件
applyMiddleware(sagaMiddleware)
)
// 3. 让我们的saga在全局监听
sagaMiddleware.run(MySaga)
export default store
在登录组件
const LoginForSaga = connect(
state => ({
// 将user的redux给map到当前组件的props
isLogin: state.user.isLogin,
error: state.user.error
}),
{login}
)(function({location, isLogin, login, error}) {
const redirect = location.state && location.state.redirect || "/";
const [uname, setUname] = useState('')
if(isLogin) {
return <Redirect path={redirect}/>
}
return (
<div>
{/* 错误信息展示 */}
{error && <p>{error}</p>}
<input type="text" onChange={e => setUname(e.target.value)}/>
{/* redux-saga */}
<button onClick={() => login(uname)}>登录</button>
</div>)
})
Umi,中文可发音为乌米,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。
Umi 是蚂蚁金服的底层前端框架,已直接或间接地服务了 3000+ 应用,包括 java、node、H5 无线、离线(Hybrid)应用、纯前端 assets 应用、CMS 应用等。他已经很好地服务了我们的内部用户,同时希望他也能服务好外部用户。
开箱即用:内置 SSR,一键开启,umi dev
即 SSR 预览,开发调试方便。
服务端框架无关:Umi 不耦合服务端框架(例如 Egg.js、Express、Koa),无论是哪种框架或者 Serverless 模式,都可以非常简单进行集成。
支持应用和页面级数据预获取:Umi 3 中延续了 Umi 2 中的页面数据预获取(getInitialProps),来解决之前全局数据的获取问题。
支持按需加载:按需加载 dynamicImport
开启后,Umi 3 中会根据不同路由加载对应的资源文件(css/js)。
内置预渲染功能:Umi 3 中内置了预渲染功能,不再通过安装额外插件使用,同时开启 ssr
和 exportStatic
,在 umi build
构建时会编译出渲染后的 HTML。
支持渲染降级:优先使用 SSR,如果服务端渲染失败,自动降级为客户端渲染(CSR),不影响正常业务流程。
支持流式渲染:ssr: { mode: 'stream' }
即可开启流式渲染,流式 SSR 较正常 SSR 有更少的 TTFB(发出页面请求到接收到应答数据第一个字节所花费的毫秒数) 时间。
兼容客户端动态加载:在 Umi 2 中同时使用 SSR 和 dynamicImport(动态加载)会有一些问题,在 Umi 3 中可同时开启使用。
SSR 功能插件化:Umi 3 内置的 SSR 功能基本够用,若不满足需求或者想自定义渲染方法,可通过提供的 API 来自定义。
Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:
把 store 及 saga 统一为一个 model
的概念, 写在一个 js 文件里面
增加了一个 Subscriptions, 用于收集其他来源的 action, eg: 键盘操作
model 写法很简约, 类似于 DSL 或者 RoR, coding 快得飞起✈️
create-react-app 是基于 webpack 的打包层方案,包含 build、dev、lint 等,他在打包层把体验做到了极致,但是不包含路由,不是框架,也不支持配置。所以,如果大家想基于他修改部分配置,或者希望在打包层之外也做技术收敛时,就会遇到困难。
next.js 是个很好的选择,Umi 很多功能是参考 next.js 做的。要说有哪些地方不如 Umi,我觉得可能是不够贴近业务,不够接地气。比如 antd、dva 的深度整合,比如国际化、权限、数据流、配置式路由、补丁方案、自动化 external 方面等等一线开发者才会遇到的问题。
.
├── dist/ // 默认的 build 输出目录
├── mock/ // mock 文件所在目录,基于 express
├── config/
├── config.js // umi 配置,同 .umirc.js,二选一
└── src/ // 源码目录,可选
├── layouts/index.js // 全局布局
├── pages/ // 页面目录,里面的文件即路由
├── .umi/ // dev 临时目录,需添加到 .gitignore
├── .umi-production/ // build 临时目录,会自动删除
├── document.ejs // HTML 模板
├── 404.js // 404 页面
├── page1.js // 页面 1,任意命名,导出 react 组件
├── page1.test.js // 用例文件,umi test 会匹配所有 .test.js 和 .e2e.js 结尾的文件
└── page2.js // 页面 2,任意命名
├── global.css // 约定的全局样式文件,自动引入,也可以用 global.less
├── global.js // 可以在这里加入 polyfill
├── app.js // 运行时配置文件
├── .umirc.js // umi 配置,同 config/config.js,二选一
├── .env // 环境变量
└── package.json
npm init
npm install umi -D
新建index页
umi g page index
启动服务器
umi dev
umi
是支持约定式路由与配置式路由的,默认为约定式路由,约定式就是会根据pages
目录自动生成路由配置。
假设pages
目录结构如下:
+ pages/
+ users/
- index.js
- list.js
- index.js
则umi会自动生成路由配置如下:
[
{ path: '/', component: './pages/index.js' },
{ path: '/users/', component: './pages/users/index.js' },
{ path: '/users/list', component: './pages/users/list.js' },
]
若.umirc.(ts|js)或者config/config.(ts|js)文件对router进行了配置,约定式路由将改为配置式路由,新的页面将不会自动被umi编译
umi里规定以$
开头的文件或目录为动态路由
如以下目录结构:
+ pages/
+ $post/
- index.js
- comments.js
+ users/
$id.js
- index.js
会生成的路由配置如下:
[
{ path: '/', component: './pages/index.js' },
{ path: '/users/:id', component: './pages/users/$id.js' },
{ path: '/:post/', component: './pages/$post/index.js' },
{ path: '/:post/comments', component: './pages/$post/comments.js' },
]
umi 里约定动态路由如果带 $
后缀,则为可选动态路由。
比如以下结构:
+ pages/
+ users/
- $id$.js
- index.js
生成的路由配置如下:
[
{ path: '/': component: './pages/index.js' },
{ path: '/users/:id?': component: './pages/users/$id$.js' },
]
umi 里约定目录下有 _layout.js
时会生成嵌套路由,以 _layout.js
为该目录的 layout 。
比如以下目录结构:
+ pages/
+ users/
- _layout.js
- $id.js
- index.js
生成的路由配置如下:
[
{ path: '/users', component: './pages/users/_layout.js',
routes: [
{ path: '/users/', component: './pages/users/index.js' },
{ path: '/users/:id', component: './pages/users/$id.js' },
],
},
]
约定 pages/404.js
为 404 页面,需返回 React 组件。
比如
export default () => {
return (
<div>I am a customized 404 page</div>
);
};
注意:开发模式下,umi 会添加一个默认的 404 页面来辅助开发,但你仍然可通过精确地访问
/404
来验证 404 页面。
约定路由文件的首个注释如果包含 yaml 格式的配置,则会被用于扩展路由。
比如:
+ pages/
- index.js
如果 pages/index.js
里包含:
/**
* title: Index Page
* Routes:
* - ./src/routes/a.js
* - ./src/routes/b.js
*/
则会生成路由配置
[
{ path: '/', component: './index.js',
title: 'Index Page',
Routes: [ './src/routes/a.js', './src/routes/b.js' ],
},
]
如果你倾向于使用配置式的路由,可以配置 .umirc.(ts|js)
或者 config/config.(ts|js)
配置文件中的 routes
属性,此配置项存在时则不会对 src/pages
目录做约定式的解析。
比如:
export default {
routes: [
{ path: '/', component: './a' },
{ path: '/list', component: './b', Routes: ['./routes/PrivateRoute.js'] },
{ path: '/users', component: './users/_layout',
routes: [
{ path: '/users/detail', component: './users/detail' },
{ path: '/users/:id', component: './users/id' }
]
},
],
};
注意:
src/pages
目录的umi 的权限路由是通过配置路由的 Routes
属性来实现。约定式的通过 yaml 注释添加,配置式的直接配上即可。
比如有以下配置:
[
{ path: '/', component: './pages/index.js' },
{ path: '/list', component: './pages/list.js', Routes: ['./routes/PrivateRoute.js'] },
]
然后 umi 会用 ./routes/PrivateRoute.js
来渲染 /list
。
./routes/PrivateRoute.js
文件示例:
export default (props) => {
return (
<div>
<div>PrivateRoute (routes/PrivateRoute.js)</div>
{ props.children }
</div>
);
}
cnpm i antd -S
cnpm install umi-plugin-react -D
修改 config/config.js
// umi2的配置方式
export default {
plugins: [
// 有参数
[
'umi-plugin-react',
{
dva: {},
antd: {},
},
],
'./plugin',
],
};
// umi3的配置方式
export default {
dva: {},
antd: {}
};
// package.json
{
"devDependencies": {
- "umi": "^2" // for umi2
+ "umi": "^3" // for umi3
- "umi-plugin-react": "^1" // for umi2
+ "@umijs/preset-react": "^1" // for umi3
}
}
import React from 'react';
import styles from './index.css';
import {Button} from 'antd'
export default function Page() {
return (
<div>
<h1 className={styles.title}>Page index</h1>
<Button>登录</Button>
</div>
);
}
https://blog.csdn.net/weixin_43787651/article/details/110224586
dva教程:
https://blog.csdn.net/qq_39523111/article/details/88050125?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242
https://zhuanlan.zhihu.com/p/29164782
一、打包报 Application entry file “build\electron.js” in the “D:\workspace\electronDemo\demo-test\demo\dist\win-unpacked\resources\app.asar” does not exist
https://blog.csdn.net/qq_40593656/article/details/100101911
二、在react打包后静态资源在electron展示不了
https://segmentfault.com/a/1190000020229885?utm_source=tag-newest
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。