写点什么

携程火车票 Rematch 框架实践

2020 年 5 月 19 日

携程火车票Rematch框架实践

本文主要介绍携程火车票模块在进行新业务开发和老代码重构时,使用 rematch 状态管理框架的实践经验总结,包括在过程中暴露出来的一系列问题以及相应的解决方案。


一、 背景

携程火车票业务迭代至今,已经实现了全流程的 RN 化。与此同时,之前基于 redux 状态管理方式写的业务代码,其中的问题也逐渐显现出来,主要体现在:


1)写法复杂,且状态改变的触发逻辑和处理逻辑很分散,代码可读性较差,新人上手难;


2)组件状态严重依赖于页面,与页面有强耦合,导致页面逻辑复杂难懂,组件也无法得到有效的复用。


问题的根源在于状态管理,于是我们开始尝试寻找新的状态管理方案。rematch 作为 redux 的最佳实践,进入了我们的视线。


Rematch 基于 redux,进行了封装,简化了 redux 的使用方式,写同样的逻辑,所需的样板代码更少;且它有全局分发器 dispatch,有利于页面和组件之间的解耦。因此,我们使用 rematch 这种新的状态管理方案,来进行了流程改造。


二、 改造流程设计

为了兼顾日常开发任务,我们将项目 rematch 化的改造计划分为了三期。


1)第一期:先在新页面中来尝试使用 rematch 框架,我们找了一个与流程几乎没有什么耦合的新页面来试水。一方面来用来熟悉 rematch 框架,另一方面也为了测试该框架在项目中的兼容性和稳定性;


2)第二期:火车票详情页使用 rematch 进行重构,积累一些重构经验,为后面的全流程推广奠定基础;


3)第三期:火车票全流程使用 rematch 框架。


三、 问题与解决

在实践过程中,我们遇到了很多问题,针对这些问题,我们总结了相关的经验。


3.1 Rematch 和 Redux 的 store 如何兼容

rematch 提供了相关接口,可以在同一个 store 中,兼容 redux,这是一种渐进式的改造过程,适用于在原页面上添加一个使用 rematch 的新组件。这种方式会使页面处于 redux 和 rematch 共存的中间状态,后续还需要进行再次改造,略显麻烦。


我们的做法是,给 rematch 建立一个新的 store,以页面为纬度进行改造。在根组件中,首先获取当前页面的路由。在事先声明的路由与 store 的映射表中,指定各个页面匹配对应的 store,来达到切换 store 的目的。


3.1.1 配置需要使用新的 store 页面

const newStorePages = [    "Page1",    "Page2"]
复制代码


3.1.2 在模块初始化的时候,根据配置表加载不同的 store

在页面跳转时拿到初始化页面 initialPage。拿到初始化页面以后,根据之前的映射表来判断当前应该使用哪种 store,从而保证在一个 RN 实例中只会存在一个 store,实现了两个 store 在项目中的兼容。


render() {    ......    return (        <Provider store = newStorePages.indexOf(urlQuery.initialPage || "") > -1 ? newStore : store>            <AppWithContext {...this.props} />        </Provider>    )}
复制代码


3.2 如何使用 Rematch 实现模块间完全解耦

在结构复杂、业务多变的互联网产品中,要做到模块具有较强的独立性、易用性、可移植性以及扩展性,那么模块之间完全解耦就显得尤为重要了。完全解耦的终极目的,是在删除、修改、迁移这个模块时,只需要对应地去操作这个模块文件以及这个文件的引用。除此之外,不需要修改任何其他模块、文件,如此即达到了组件最大化解耦。


因此,我们将组件放置在单独的文件夹中,其中包含两个文件 index.js 以及 model.js, index 文件主要是描述组件视图, model.js 里封装了组件所有的逻辑。下面以一个弹窗逻辑为例,看下新老两种方式的对比:


3.2.1 传统方式

1)先在页面中声明一个 state 去控制组件的显示隐藏


this.state = {    showManualSpeed: false}
复制代码


2)作为属性传入组件


<ManualSpeedLayer    orderInfo={orderInfo}    isShow={this.state.showManualSpeed}    cancel={() => {        this.setState({showManualSpeed:false})    }}/>
复制代码


3)改变状态去控制组件的显示隐藏


this.setState({    showManualSpeed: true})
复制代码


可以看到,传统方式,主页面和组件之间耦合十分严重,组件的属性都在页面引入时传入,而这些属性页面其实都不用知道,页面只需引用组件就好了。组件的状态变化应该由实际触发其变化的地方去执行,而传统的方式将 state 都绑定在页面上,使得组件间通信必须经过页面来触发,导致主页面和组件的强耦合。


3.2.2 使用 rematch 的方式

1)先看看组件的结构


- ManualSpeedPopupView    - index.js // 组件UI    - model.js // 组件状态及逻辑
复制代码


2)在 model.js 中暴露显示或隐藏弹窗的方法


const manualSpeedLayer = {    state : false,    reducers: {        show(state, playload) {            return true;        },        hide(state, playload) {            return false;        }    },}
复制代码


3)使用时,只需要在调用 redux 的 connnect 方法引入就可直接显示或隐藏该弹窗。这样能够避免多余的状态声明和管理,而且与父组件完全解耦。


......
const mapState = state => ({ isShow: state?.manualSpeedLayer })const mapDispatch = ({manualSpeedLayer}) => { return { hide: () => {manualSpeedLayer.hide()} }}
export default memo(connect(mapState, mapDispatch)(ManualSpeedPopupView))
复制代码


4)在页面中的引入也变得十分简单


<ManualSpeedPopupView />
复制代码


Rematch 中把 state、reducers 和异步处理放在了一起,相比于 redux 的传统写法,这样的写法来的更加简洁方便。组件相关的逻辑都收到了一起,这样页面在引用时,无需再进行多余的状态声明和管理,代码可读性也大大提升。


组件和页面的强耦合,还体现在与组件操作相关的函数中。之前的处理方式,是将页面 page 传给函数。


export function clickSchoolActivityBtn(page) {    let {        orderInfo,    } = page.props;        ......        page.props.setShowDialog(false);}
复制代码


由于状态和 action 都绑定在页面上,所以需要通过 page 来获取相关的状态以及触发一些 action。但其实页面不需要关心这些状态和 action,那么如何将这部分逻辑解耦出来呢?


使用 rematch 之后的做法是,将这个函数改为一个异步 action,迁移到组件的 model 中去。


const clickSchoolActivityBtn = (rootState, dispatch) => {    let {        orderInfo,    } = rootState;        ......        dispatch.schoolActivityShareLayer.hide();}
const schoolActivityDetail = { state: null, reducers: { setSchoolActivityDetail(state, playload) { return playload; }, clear(state, playload) { return null; } }, effects: (dispatch) => ({ async clickSchoolActivityBtn(params, rootState) { clickSchoolActivityBtn(rootState, dispatch); } })}
复制代码


异步 action 中的 rootState 包含了当前域内的所有状态,而 dispatch 可以索引到所有 action 函数,因此可以使用 rootState 和 dispatch 来接管原先 page 的工作,从而完全舍弃 page,实现解耦。


3.3 如何实现组件复用

组件内容都抽到一个文件了,那么具体怎么去复用呢?开始我们想的方案是在组件绑定状态的地方更改数据源。例如,加入 A、B 两个页面,都需要用到该组件,且组件除了数据源不一样以外其余逻辑都相同,如下所示。


const mapState = state => ({    noticeDetail:state?.pageASource,})
复制代码


const mapState = state => ({    noticeDetail:state?.pageBSource,})
复制代码


但这个方案的缺点是,每次在一个新场景使用该组件,都要复制一份入口文件,且需要更改入口文件的数据源,这样一来不仅入口文件代码会重复而且操作也略显麻烦。那么有不需要改动入口文件的方案么?


这时我们想到了 rematch 的异步 action,在异步 action 中,第一个参数是自定义的,可以传入任意自己所需的数据。因此可以通过异步 action 来暴露一个函数出来,单独给页面设置数据源。这样一来,对组件来说,就屏蔽了调用方的细节,组件内只需要这个数据类型,而组件外具体是哪个页面使用,数据来源是什么,都不用关心。


const noticeDetail = {    state: null,    reducers: {        setNoticeDetail(state, playload) {            return playload        },        clear(state, playload) {            return null        }    },    effects: (dispatch) => {        async setDataSource(params, rootState) {            let res = await getRecommendForOrder({orderNumber: params.orderNumber});            if (res) {                dispatch.noticeDetail.setNoticeDetail(res);            }        }    }}
复制代码


在上图这个场景中,我们暴露出了一个 setDataSource 方法,在页面中使用该组件时,只需引入组件,并在合适的地方给组件设置数据源即可。


dispatch.noticeDetail.setDataSource({'orderNumber': pageA.orderNumber});
复制代码


dispatch.noticeDetail.setDataSource({'orderNumber': pageB.orderNumber});
复制代码


这样一来,如果需要在新页面中加入这个组件,只需要在页面方设置数据源即可,组件无需任何改动。


3.4 其它问题

3.4.1 如何及时获取最新状态

在异步 action 中,如果在通过 dispatch 改变某个状态后,通过 rootState 去拿是无法拿到最新状态的,因为其状态改变最终都是通过 setState 来触发,而这个方法不是同步执行的。如果需要立即拿到最新的状态,可以直接从 store 中去获取。


import {newStore} from "../../../Store";
let orderInfo = newStore?.getState()?.orderInfo;
复制代码


3.4.2 预加载组件缓存问题

为了加快二次启动的速度,之前在 RN 里做了预加载优化。RN 在开了预加载的情况下,由于先前的状态仍然保存着,下次再进入该页面会造成页面数据显示不准确问题,所以就需要在页面退出之前,清除掉之前的状态。由于组件之间各自独立, 需要各个组件暴露自己的 clear 方法,用以清理自身的状态。


const isRefreshing = {    state: false,    reducers: {        setIsRefreshing(state, playload) {            return playload;        },        clear(state, playload) {            return true;        }    }}
复制代码


组件自己在销毁的时候需要清除掉自己的状态,如下所示:


useEffect(() => {    return () => {        props?.clear();    }}, []);
复制代码


另外页面在销毁的时候也需要清除所有子组件的状态。如下图,通过 connect,将各个组件的状态引入,通过将各个组件的 clear 方法集中来达到清理所有状态的目的。


const mapDispatch = ({component1, component2, component3}) => {    clear: () => {        component1.clear();        component2.clear();        component3.clear();    }}
复制代码


四、 结果与回顾

目前我们的改造计划已经完成了第一期和第二期,实践下来效果达到了预期。


新页面使用 rematch 框架开发,写法简便,配合函数式和 react hooks,大大节省了代码量。详情页使用 rematch 框架重构,主页面变得清晰可读,index 文件的代码量简化到了原来的 32%,且详情页各个组件变得独立可复用。


后续我们也会开展第三期的改造,将 rematch 框架应用到全流程当中去。


作者介绍


滨峰,携程开发经理;国春,携程高级开发工程师。


本文转载自公众号携程技术(ID:ctriptech)。


原文链接


https://mp.weixin.qq.com/s?__biz=MjM5MDI3MjA5MQ==&mid=2697269670&idx=2&sn=1b6f2b8a8dc2b0377c4f84b8061ef98d&chksm=8376ee92b4016784411a1c4c375dd5648a844fd2fda7f3f1e1fe2691fa0286c2ee6deb1fa009&scene=27#wechat_redirect


2020 年 5 月 19 日 14:03913

评论

发布
暂无评论
发现更多内容

博睿数据深化信创布局,通过华为鲲鹏920和统信UOS测试认证

博睿数据

信创 国产化 博睿数据

【立刻报名】加速开发 Perforce on Tour China 2021-龙智

龙智—DevOps解决方案

算法如何促成亿级别扶持曝光视频?爱奇艺Budget Pacing智能扶植系统实践

爱奇艺技术产品团队

推荐 广告 架构· #算法# 扶植

【得物技术】JS资源分包

得物技术

CSS 前端 js 版本 页面

干货来袭!上线8天遭Github疯狂转载的阿里首发Java成长笔记(2021最新版)到底有多强?

程序员小毕

Java 程序员 架构 面试 分布式

深入理解iOS图文混排原理并自定义图文控件

Geen练

图文混排 iOS Developer iOS 知识体系

【环球网】博睿数据CEO冯云彪:做好生态链企业的适配工作

博睿数据

博睿数据 博睿数据数据链DNA 生态链

架构实战营模块5作业

En wei

架构实战营

Gartner在线研讨会:利用分布式云扩展混合云的能力

Geek_bacee5

Gartner 混合云 Gartner在线研讨会 分布式云优势

Nacos-- docker搭建

是老郭啊

Docker nacos

最新出炉,头条三面技术四面HR,看我如何一步一步攻克面试官?

不秃顶的Java程序员

Java 程序员 面试

MySQL基础之二:SQL语句介绍与使用

打工人!

MySQL sql查询 6月日更

理解【Apache Zookeeper】

awen

zookeeper

《原则》(四)

Changing Lin

极光开发者周刊【No.0604】

极光开发者

程序员 开发者 IT行业

聊聊MySQL全局锁

架构精进之路

MySQL 6 月日更

区块链—重构新商业体系和全新商业生态环境

CECBC区块链专委会

面试官最爱问的Kafka,基础+进阶+高级,44问疯狂轰炸!

不秃顶的Java程序员

Java kafka 程序员 面试 中间件

博睿数据重塑APM,引领IT运维新标杆

博睿数据

博睿数据 数据链DNA 服务可达

全栈程序员这个月写了啥代码?

程序员鱼皮

Java JavaScript 前端 后端 全栈

与黑客讨价还价,勒索攻击企业数据是关键

Machine Gun

网络安全 信息安全 渗透测试 Kail linux

实战到源码一套搞定,阿里最新推出的SpringCloud Alibaba微服务学习笔记太全了!

Java王路飞

Java 架构 面试 微服务 SpringCloud

KaliLinux 2021.2发布,增加大量新工具和功能(文末获取)

Machine Gun

网络安全 信息安全 渗透测试 Kail linux

区块链作为“信任的机器”,将改变社会价值的传递方式

CECBC区块链专委会

从天而降的AI“青云梯”,开发者们准备好了吗?

白洞计划

区块链技术为版权保护与运营提供科技支撑

CECBC区块链专委会

dubbo使用curator作为zk客户端优雅停机存在的问题

林一

zookeeper dubbo curator

Nacos--简介

是老郭啊

微服务架构 nacos 服务注册与发现

模块五- 课后作业

Presley

架构实战营 #架构实战营

膜拜!阿里内部 Java 开发成长手册(2021 版)来了,不管你工作几年都可以看

神奇小汤圆

Java 程序员 架构 面试

拍乐云运维专家受邀QECon大会,畅谈多云环境伸缩实践

拍乐云Pano

「中国技术开放日·长沙站」现场直播

「中国技术开放日·长沙站」现场直播

携程火车票Rematch框架实践-InfoQ