React-Redux基础(三):Redux 在项目中的实际写法,组合状态对象的设计和疑问,Provider 和 Router 的冲突原因和解决方法

在上篇博客里面,我们有说过,之前对 redux 的用法有几个问题,这里我们一起来看看:

首先第一个问题,就是 index.js。

我们现在是把 reducer 和 store 全都扔在 index.js 里面了,这个不太好。

因为 index.js 一般来说比较专用一些,它就是一个启动的入口。

我们如果把太多业务代码直接扔到 index.js 里面是不太好的。

尤其是像 store,将来会膨胀成一个很大的东西,因为我们的数据可能会很多,这个是第一个问题,所以我们需要把它拿走。

然后第二个问题就是 action。

我们现在写的都是一堆的字符串,这个太麻烦了。

如果因为某种原因不得不改名字,那你所有用到的这个名字的地方,全得改。可能有几百上千个组件,这没人能记得住,所以这是第二个问题。

那么第三个问题:就是我们现在是单一数据对象,换句话来说,就是只有一个 reducer。

其实真正在应用当中,我们应该是一个模块化的 reducer。比如说有用户数据,购物车数据,等等一大堆东西。

当然,还有第四个问题,就是我们直接把 App 放在这里不太好,是会有问题的,在用 Router 的时候就表现出来了。

那么我们一个个来。

首先,我们先把 store 给拿出去:

第一步,新建 store.js。

当然现在创建一个 store.js 只是简单起见,做过项目应该知道,以后多了其实应该是个目录才对。

然后,createStore 不需要放在 index.js 里面了,因为我们已经不是在 index.js 里面去创建了。

当然 Provider 还放在 index.js 这,因为我们需要 Provider 帮我们连接上去。

接下来,reducer 和 createStore 也就都可以直接拿走了:

然后我们就可以把这个创建好的 store 对象给它 export 出去,那么在 index.js 里面就直接引用了:

这个时候 index.js 就变得更加的专用,简单了。

因为大家维护程序的时候,都是从 index.js 这个入口文件开始看起,如果太乱了,太杂了,就不方便。

接下来我们来看第二个问题:

就是 action 的名字都是以一个字符串的形式存在,这是一个非常巨大的风险。

所以我们可以把 actions 都单独的独立出来,创建一个 actions.js 文件:

那么怎么独立呢?很简单,我们只需要能够把字符串输出出来就行:

并且千万别忘了,我们用到 setname 的组件里面也需要更改:

所以,以后如果某天我们需要改这个东西,直接在 actions 里面更改就行了。

比如我将 setname 改成了 set_name:

可以看到,是没有问题的。因为我这一改,所有的地方全都改了,这样我们就会方便很多。

一般我们如果光是去封装 action,其实还是不够的,倒不是说完全不行,只不过还是有点麻烦。

因为有可能在很多组件里面,都会要用到 setname 这个方法。比如说,我在头像里面可以修改名字,个人资料也可以修改名字,群聊里面还是可以修改名字。所以一个操作其实它是有可能在很多个组件里面需要跨着来用的。

所以其实我们还可以更近一步,把这个东西封装的更深一点。

我们直接对外输出一个 function,名字叫啥无所谓,都是可以改的:

这个 setname 函数,其实就是我们在 Cmp2 组件里面用的 setname:

换句话说,我不再把它放到组件里面来定义了,而是直接提取出来,大家公用:

所以,用到它的地方,连 SET_NAME 都不用引了,我们直接就引入 setname,然后放进去就行了:

这样的话,所有人用到 setname 的时候,都是同一个函数,这样就更统一,更好了。

那么现在我们就解决了前两个问题:

1,我们不可能把整个 store 都放到 index.js 里面。

因为这个 store 未来会越膨胀越大,现在只是一个简单的小 demo 就已经有20多行了,将来真要是一个大程序的话,几千行都不够,所以我们会选择把它拿出来。

2,我们把 action 里面的字符串,甚至还有操作本身都给拿出来。这样就会特别的方便。

那么第三个问题:

只有一个 reducer 的时候,我们叫做单一状态对象。但是我们是不可能只有一个 reducer 的,全在这一个里面,你的 reducer 函数得写多大啊。

然后接下来,我们就看看怎么样去用所谓的组合状态对象

redux 里面除了 createStore,它还有一个方法:combineReducers。

combine 翻译过来就是组合的意思。看它名字就知道,它能够把 reducers 给组合到一起。那这个东西怎么用呢?

首先,我们先准备 2 个 reducer,叫啥名都行。

之前的 reducer 我们改名为 user,然后在随便加个 news。

当然,这两个 reducer 本身都是一样的结构。这里需要的注意的一点就是,没人规定过 state 必须是 json,其实什么都可以,比如我这里就定义为一个数组:

为了后面方便演示,我们在 news 里面先随便加一个 addnews:

那么现在,我们就已经有了 2 个 reducer,接下来就是时候把他们都合并起来了。

我们需要 2 个步骤:

首先,我们在真正去创建 store 之前,需要做一件事:就是直接用 combineReducers 把它们给合起来。

combineReducers 它里面用的是一个 json,因为每个 reducer 都是有名字的,也就是我们所说的命名空间。

所以就是 user 对应 user,news 对应 news。那这里我们就可以不用写这么麻烦,可以直接简写:

那么组合起来之后,我们就随便起个名字,叫做 reducers 吧,然后把它扔到 createStore 里面就可以了

然后,如果你嫌麻烦,也可以直接这么写:

这样就不用写很多名字,github 上面很多项目也都是这种写法,比较方便。

那么这个时候,我们就已经对外输出了一个组合后的 store。

当然,其他位置的代码是不需要改的,这个组合后的 store,照样可以扔到它里面去,是没有问题的。

然后我们添加一个组件 News.js 来试试,做个实验:

这里需要注意的:因为在 store.js 里面有了名字,所以用的时候就需要加上 news 前缀。这个 news 指代的就是 state 里面的那个空数组,所以我们才可以直接用 map 方法。

包括我们以前在 App 组件里面用的 this.props.name,现在得改成 this.props.user.name。

因为它现在有一个命名空间的概念在里面。

如果我们不加 user 前缀的话,它就找不到数据,这样是会出事的:

然后我们引入 News 组件,点击添加按钮,页面上就会多出个 aaa,bbb。

并且添加年龄,修改姓名的功能也没有受到任何的影响。

所以现在,我们就学会了一个新东西,就是如何让单一的状态对象,变成一个组合的状态对象。

这个是非常有用的,因为在稍微有点规模的项目当中,你不可能只有一个数据,那样的话是非常麻烦的。

然后这里面还有一个小事,我们需要知道一下,那就是:

如果你的组件那边发出任何一个 action,那么实际上来说, user 和 news 这两个 reducer 会共同执行。

什么意思呢?一起来看下就知道了。

我们在 user 和 news 里面都打印一下 console.log:

然后我们现在想做一个年龄 +3 的操作:

你可以看到,其实这两个 reducer 都有反应,并且它们得到的是一个相同的 action。

那么这时候,我们就需要知道 2 件事:

1,为什么?人家这么设计是有什么原因的吗?

2,这样对我写程序有没有什么影响?

首先第一个,为什么这个东西不能有一个命名空间一样的感觉?为什么非得要给它都触发了?

首先,redux 里面是故意这么设计的。那为什么要故意来设计这个事呢?

原因很简单,因为它考虑到你有可能会通过一个 action,需要让所有的这些 reducer 都发生一定的变化。

比如我举个最简单的例子,假设这个网站有一个服务:

如果你是我们的 VIP,我们的商品就给你打 8 折。这个需求很正常吧?

那么这时候,假设用户突然冲了一个 VIP,那这时候你商品里面的价格要不要有变化?肯定要有变化的,因为都要乘以 0.8,打8折。

然后你购物车里面的那些数据要不要有变化?也要。

甚至于包括你的用户信息,头像那里是不是也要有变化?比如多个 VIP 标志之类的。

所以你这一个操作,有可能引发好多个状态都去变化。

那么在这种情况下,我们确实存在着一个 action,然后要触发多个地方,这种情况。

所以 redux 是故意这么设计的,它设计成了一个 action 可以同时触发所有的 reducer。

但这时候,大家更多的疑问是:会不会对我们的程序造成影响?

答案是不太会。因为我们正常的时候,是这么写的:

在 user 里面,我们是有个 case setname 和 case addage。

但是在 news 里面,我们并没有去处理 setname 和 addage。

那么我们上面那个例子的 action,其实它找的是 user 里面的 addage。 

那么在 news 里面,它走的就是 default。也就是说,state 是怎么样,就还是怎么样,原模原样的返回出来,并且它不是一个新对象,而是一个老对象。

那么现在就带来了 2 个好处:

1,是不是就没有创建对象的开销和成本了。

2,就是经过 redux 的检查,redux 认为你这个数据对象没变,所以这时候它也不会通知用到这个数据的组件更新,也就可以防止过多的开销。

那么,唯一的一个程序开销就是 switch,但是它运行的速度是非常快的,你不用担心。一般我们在工作中,差不多10-20 个就已经顶天了。就算你有几百上千个,一个 switch 造成的开销也非常的小,所以完全不用担心。

所以:

第一,当你提出任何一个 action 的时候,所有的 reducer 函数都会被执行。

第二,对我们的性能没有影响,你不用担心。因为跟你没关系的东西,它会直接走 default 略过去。

除非我们自己犯了错误,把字符串写成了一样:

那这个时候,它的确一个 action 会触发很多个东西,但这个就是我们自己写代码的错误了。

我们本来就不应该让它们相同,这样就不会出事了。

所以我们现在就又理解了一个东西,就是关于组合状态对象的问题。

那么接下来,我们对 redux 就算是有了一些比较深的认识。但是我们还有另一个事情要说,就是 redux 和 router:

redux 是用来共享数据的,同一套数据大家用,每个组件还有一些自己的状态,但是有一些公共的状态,这是 redux。

而 router 是根据路径不同,让不同的组件蹦出来。

redux 跟 router 这两个东西,它们本身是不相关的,但是如果我们把它们放在一块用,就容易出事。

我们来看看是怎么回事,以及如何解决避免。

为了方便更好的演示,我们重开一个全新的项目:

创建完新项目之后,我们先安装:npm install redux react-redux react-router-dom -D

首先,在我们的 index.js 里面,我们不可能让 App 空着,我们肯定至少要放 2 个东西:

一个是 Router,我们要用 Router 来包住这个 App。同时,我们还要让 Provider 来包住这个 App。

那我怎么包?是先放 Router 还是 Provider?

其实无所谓,你先放哪个都可以。

我们直接来试试,首先,先创建 store.js 文件。

这里就不用 combineReducers 了,因为有没有 combineReducers 都不影响它跟 Router 配合的一个问题。

然后在 index.js 里面引入 store.js,并加上 Provider 和 Router:

然后我们在创建 2 个组件 Cmp1 和 Cmp2。

在 Cmp1 中显示姓名,这个 name 就是从 store 来的。

并且我们还有个按钮,我们让它可以设置年龄 +3。

在 Cmp2 里面,我们显示的是年龄,然后反过来改 Cmp1 的姓名。

这里我们是故意的让 Cmp1 去改 Cmp2 的东西,Cmp2 里面在反过来改 Cmp1 的东西。

然后在 App.js 里面引入,并写入路由跳转,因为我们需要在 App.js 里面有一个路由关系来方便演示:

然后我们看看能不能正常显示:

可以看到,Cmp1 和 Cmp2 正常显示是没有问题的,那么我们来稍微的试一下:

好像是挺正常的,那你前面不是说有问题吗?

别忘了,我们这个例子其实是 redux 和路由综合来应用的。

所以这时候,我还希望来测验一下路由的跳转,那么这会就出事了:

首先我们在 Cpm2 里面加个功能,可以跳到 Cpm1中:

然后我们从 Cmp2 跳转到 Cmp1:

可以看到,还是没问题。但是注意,你现在内部的 Cpm1,Cmp2 它们都没问题,但是出问题的是 App。

比如说,我们在 App 里面不用 Link 来跳转了,我们用 JS 来做跳转:

表面上看起来好像一切都没问题,而且我们刚才在 Cmp2 里面已经试过了,是可以跳转的。

但就是在这个 App 上,它就不能跳转。

当我们点击按钮跳转的时候:

这时候就跳转不过去,出事了。

这个就是我们所说的:在组件里面,想要跳转没问题,但是在 App 上是用不了的。

那么是怎么回事呢?

首先,如果想解决问题的话,千万别扑腾扑腾一顿改,先分析一下原因在哪,为什么会有问题。

我们可以先把 this.props 打印出来看看是什么:

可以看到,这时候的 this.props 是个空对象。

那么 this.props.history 自然就是一个 undefined 了,我们在一个 undefined 身上去调用 push,出不来才是正常的。

奇怪了,我们原来没用 redux 的时候,是没这个事的,用了 redux 才有这个问题,为什么呢?其实问题就出在争宠上面。

简单来说, Provider 和 Router 这两个东西都在争 props。

本身我们在用 Router 的时候,它会给 props 上面加一堆东西,比如 history,match 等等,而 Provider 它里面也要装一堆东西。

我们在上面已经看到了,App 它身上的 props 已经没了,是个空对象了。

因为我们现在需要给 App 去加上 Provider 的各种各样的数据,所以就导致 Router 给它加的东西没了。

那么这时候怎么办呢?我们有一个小小的办法来解决。

我们可以先删掉 App,然后在引入一个 Route:

那么我们就不用直接写的方式了,而是用一个 Route,并且这个 Route 它对应的组件是 App:

其实这个时候,应该跟我原来直接写个App,效果差不多才对。

然后我们跳转到 Cmp2:

可以看到,跳转是没问题的,并且数据也是好的。

那么我们点下修改姓名,在跳回 Cmp1:

可以看到,不管我们怎么操作,现在都没问题了。

说明这种情况下,我们既能够把 Provider 相关的功能发挥出来,同时又能把 Router 相关的功能发挥出来,就避免了这个问题。

那么是怎么回事呢?

很简单,因为直接放到 Provider 里面的那个组件,就是我们刚才的 App,它身上带的那个 props 就会被影响到。

而我们给它加了一层 Route,其实是把它保护起来了。

相当于 Provider 有辐射,我们给 App 套层壳,真正承受这个东西的是 Route,我们的 App 并没有直接暴漏在外,所以 App 还是经由我们的 Route,经过它的封装,给它调取出来的,它并不是直接放在那的。

所以这种时候,我们的 App 身上跟 Route 相关的那些东西消失,这个问题就可以解决了。

这个问题如果往深的说,其实还是因为 Provider 和 Router 它们两个人都在抢这个 props 造成的。

当然这也没办法,大家都觉得 props 好。

因为第一,如果 props 它里面发生变化,这个组件会自动重新渲染。第二,这个 props 还是受保护的,它是只读的,无法被修改。总之怎么看都觉得 props 好,所以大家就都在找这个 props。

而 react-redux 和 react-router-dom 这两个组件不是一个作者写的,所以在这个过程当中它们就产生了冲突。

如果我们看过它们的源码就知道:

直接被 Provider 包住的所有的这些组件,其实都会受到它的影响,它上面一部分的 props 会消失,因为它会先把它给重新创建一遍,然后在加上自己的 props,这是它内部的原理。

所以在这时候,App 它身上所带的跟路由相关的属性就消失了,所以这时候我们要做的是让 Provider 直接去影响 Route,它没事,它本身就是路由的东西,所以它不需要 history,match 之类的东西。

所以我们相当于是让 Route 来承受 Provider 的影响,让 App 躲在里面。因为现在这个 App 并非是直接渲染出来的,而是被 Route 创建出来的,所以 Route就能够把它那些相应的属性给加上去。

那么你肯定会有个疑问,为什么 App 身上 Provider 相关的东西怎么没消失呢?

这是因为 Router 作者在写这个代码的时候,它内部是把它原本的 props 剥离下来,然后不做任何改变,直接又放上去了,所以它不会破坏东西。

所以到这为止,我们就了解了如何让 redux 和 router 放在一起来用,就一个:

那就是让 Route 包裹住 App 就行了。

直接写 App 的话,App 它里面的组件是没事,但如果说我这个 App 里面就是有路由相关功能的情况下,那这时候你就必须得用 Route 包裹住 App 这种方法了。

这样做的目的是为了让 App 不被直接渲染出来,而是通过 Route 给创建出来,那这时候 Route 就会把路由相关的一些属性都放到 App 身上去,就没事了。

发布了78 篇原创文章 · 获赞 7 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43921436/article/details/105440624