阅读 226

Redux助力美团点评前端进阶之路


内容来源:2017年4月8日,美团点评前端技术专家张强在“第二届中国前端开发者大会”进行《Redux的打开方式》演讲分享。IT大咖说作为独家视频合作方,经主办方和讲者审阅授权发布。
阅读字数:3312 | 5分钟阅读

摘要

都说Redux好,但Redux到底好在哪,它真的解决了业务中遇到的问题吗?

因为在业务中引入Redux而带来的额外成本是否让你苦恼过?

会不会是我们打开Redux的方式不对?

本次分享将主要介绍美团点评以React+Nodejs全栈开发为背景,对redux的模块化尝试与思考。

嘉宾演讲视频及PPT地址:t.cn/RNxyI2Q

简明前端史

我对于Web前端历史的划分会站在数据以及代码可维护性的角度,把前端历史划分为古典时代、中世纪和文艺复兴三个阶段。

作为前端工程师,怎样把数据传递给用户,并把用户的想法意图转化为数据,这是我们要解决的本质问题。

人脑是无法直接读懂数据的,于是我们有了UI,UI成为了人与数据之间的桥梁。UI捕获用户的输入,然后UI按照数据源的接口对数据源进行变更操作。数据源根据变更后的最新数据按照UI能理解的格式进行渲染并传输到UI,最后UI用人们能理解的格式展现数据。


这就是我们早在90年代就开始使用的最传统的BS架构。

对于当时的Web应用来说,数据源只有一个,就是远程GDB Server。所有对于数据的变更操作都是用URL来区分不同的请求。所有被试图更新的操作都靠刷新完整页面来进行。浏览器维护的history通过记录UI变化来维护不同状态的切换,最典型的使用场景就是浏览器提供的前进后退按钮。

总结

我们当时的Web框架非常可靠,不容易出错,即使出了错也很容易定位。浏览器帮我们搞定了大部分事情,开发者只需关心UI渲染html+css。这种架构的设计简单明了,学习成本低。

古典时代:AJXJ来了(2005)

它对于前端开发来说是最好的时代,但是对于用户来说这却是最坏的时代。



AJAX带来的变化

以前只有单一数据源,现在又多了很多本地的临时数据。

以前只通过URL进行数据变更,现在增加了AJAX异步请求,而且同时用户输入会使得UI对本地存放的临时数据进行修改。

曾经是后端统一直出html,现在变成前端直接通过DOM操作进行局部渲染。

曾经浏览器的前进后退功能都无效了,数据状态只能靠自己管理。

总结

应用中有多个数据源,维护多个数据源之间的一致性将变得非常困难。

因为多个数据源之间是有关联的,导致应用内会有多处代码来操作同一处数据,预测一个代码带来的数据变更愈发困难。

整个应用内的任何代码都能随便修改DOM,当出现问题的时候不知道谁修改了DOM。

状态管理更是无从谈起。

从此前端代码变得复杂又混乱。BUG越来越多,程序员需要加班修BUG。页面经过多次迭代,代码无法维护,程序员又得加班重构。

中世纪:React(2003)

因此我将2005年之后的这段时期定义为“中世纪”,无尽的BUG,无尽的黑暗。

到2013年出现了转机,Facebook开源了React。


React强势把应用拆分成组件树,每个组件内的数据由state和props构成。Props由父组件传进来,state则是内部维护的一个本地状态。state和props的任何变化都会触发组件的重新渲染。

裸用React

每个组件都有自己本地的state,而React间组件的通信非常繁琐。所以要依靠React组件之间的通信去同步多个state之间的数据将变得非常痛苦。

React没有对数据变更进行约束。

在UI渲染方面React做得很好,没有DOM操作,与真实DOM隔离。为我们省去了很多关于渲染性能优化的工作。

React+Redux(2015)

Redux诞生于2015年,诞生不久就被官方输入了React的豪华全家桶之内。

Redux要搭配React使用首先就要摈弃React组件内部的state。这时React就将回归纯渲染,意味着传给最顶层的父组件一个Props数据,整个组件树构成的view就渲染出来了。

因此我们可以把React组件树抽象为一个函数。


这是一个纯函数,意味着输入一个确定的参数Props,它就会输出一个确定的view。只要输入的Props不变,那么输出的view就一定不会改变。


React+Redux数据流


React和Redux结合使用有一点需要注意的是,Redux启用了一个中间件的机制,中间件可以拦截全局触发的action,并根据自己拦截的action按需进行修改或再次触发其它action。

这个中间件的设计非常强大,使得Redux的扩展性得到很大的提升。

Redux三大原则

单一数据源。

state只读,只能通过触发action来进行更改。

action通过reducer来修改state,reducer必须为纯函数。

时间旅行

我们只要拿到最初始的state和用户会话中触发的所有action,我们就能一一还原出本次会话的所有空间状态。

又因为reducer渲染成view本身也是一个纯函数,我们就能通过state还原当前用户会话的所有UI变化。

Redux官方称这种变化为时间旅行。

总结

React+Redux的架构只有一个数据源,就是React的全局state。所有变更都统一由action触发,页面的渲染则统一由React组件树来完成。“时间旅行”的特性使状态管理变得非常容易。

文艺复兴

我把2013年至今的这段时间定义为“文艺复兴”时代,前端代码重新变得清晰有序,化繁为简。

但是Redux看似简单,用起来却不容易。

大象关冰箱with Redux

用Redux来解决“把大象关进冰箱要几步”这个经典哲学问题。

为了区分action,首先要定义三个不同的action type:“打开冰箱门”、“把大象关进去”、“关上冰箱门”。

然后需要实现三个actionCreator,去创建并触发这三个不同的action。

最后还要实现三个reducer去处理这三个action。


当我第一次看到Redux文档的时候我好像突然顿悟了,但当我第一次写Redux应用的时候,我的内心是崩溃的。


Redux在处理异步这方面也是有问题的。

它并没有明确规定异步处理应该放在哪一层来做,这导致每个开发都有自己的理解。

因此在一个Redux项目里,AJAX请求满天飞,写出来的代码简直没法看。

Redux的模块化

任何大型应用都无法避免多人协同开发,而协同开发一定离不开模块化。然而Redux官方并没有提及模块化方面的实践思路。

每个模块都拥有自己的action/actionType/reducer。

我们必须要保证不同模块的actionType避免重名。

模块之间要具备通信功能。

解决模块动态加载破坏了reducer纯净的问题。

Redux的API

Redux一共对外暴露了10个API,其中有5个与Redux的扩展性相关。这说明Redux需要被扩展和加强。

综上所述,Redux只提供了核心的状态管理器,并为此实现了尽可能简化的API。缺乏约束的设计使得Redux社区出现了N种最佳实践,这对于社区来说是好事,但对于普通开发者来说则未必。所以我觉得Redux不适合直接用于生产环境。

因此,我觉得我们需要一款框架对Redux进行封装和约束。

duxjs

duxjs是一个可用于生产环境的、基于React+Redux的前端框架。duxjs的部分思想借鉴了ducks,它的部分API设计则借鉴了choo。

duxjs特性

声明式API,没有样板代码。

模块化/组件化,可嵌套,可动态加载。

统一的异步处理。

duxjs同时也支持同构、热替换以及插件功能。


组件是duxjs中对于业务进行封装的最小容器。组件内可以定义自己的state、actions,selectors、views、subscriptions和components。组件内可以定义子组件,说明组件之间可以嵌套,最终形成一个组件树。

duxjs把actions分为同步action和异步action。

同步action严格按照Flux的action标准进行约束,整个action实体都可被序列化。


每个异步action都有一个effect,异步操作都会写在effect里面。除了effect还有子action,子action必须是同步的action。现在每个异步action分别默认附带了三个子action。也可以扩展更多自己的子action。Effect的异步任务执行过程中可以触发任何其它action。


State

定义组件内的初始化state。

Selector

对外暴露的state数据接口。

View

集成与组件相关的view。

Subscriptions

订阅来自外部系统的消息,比如websocket、全局键盘事件以及jsbridge通知。

Module

在组件之外我们还有一个模块的概念,就是module。duxjs的组件可以形成组件树,模块就是这个组件树的容器。和组件一样,模块也能定义在组件中成为子模块。


模块和组件的区别就在于,同一个模块内,同一个module组件是耦合的。同一模块内不同组件定义的所有action与module 和selector都共享空间,而模块与模块之间是完全解耦的。

在component中定义子模块,这里我们支持模块的静态加载和动态加载两种方式。

子模块如果向父模块通信,首先父模块在定义子模块的时候,还需要定义好想监听的函数。父模块对子模块的特点action进行监听,当监听被触发时可以就可以做一些想做的事件。

父模块如果向子模块通信,父模块可以直接获取子模块的action进行subscription,父模块可以直接访问selector进行取值。父模块也能拿到子模块的view进行渲染。

Module间的解耦

不同模块在全局state中的空间完全隔离。

由父模块指定命名空间,确保模块内所有action、selector、view的全局唯一性,不用担心重名的问题。

只有父子module能通信,禁止隔代通信。


每个action都有自己唯一ID的值,以及action被触发时的源信息。


只要拿到用户单次操作所触发的所有action,就能用图表形式将这个action的调用栈画出来,这对我们理清业务逻辑和排查BUG是非常有帮助的。

Plugin

Plugin就是加强版的module。

除了模块具备的所有能力之外,还能劫持全局的同步/异步action。

监听全局的state变化。

捕获全局的异常。

有自己的view。


可以只使用duxjs的状态管理功能,所有关于view的实现都独立于duxjs之外。


混合模式可以部分使用duxjs的模块view。


全承载模式是完全使用duxjs应用内的数据和视图进行封装和管理。

duxjs现状

duxjs在美团点评中还处于内测阶段,我们会根据实际使用的情况去调整API设计。

内测完毕后将进行开源。

我今天的分享就到这里,感谢聆听!


关注下面的标签,发现更多相似文章
评论