【锁定直播】字节、华为云、阿里云等技术专家讨论如何将大模型接入 AIOps 解决实际问题,戳>>> 了解详情
写点什么

携程火车票 Rematch 框架实践

  • 2020-05-19
  • 本文字数:4614 字

    阅读完需:约 15 分钟

携程火车票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-05-19 14:031544

评论

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

给弟弟的信第12封|阅读对一个人有多重要

大菠萝

28天写作

推倒重来的觉悟

FunTester

单元测试 Mockito powermock spock FunTester

使用 JavaScript 给微信用户发送消息

Jerry Wang

微信 sdk 28天写作 Web JS SDK 12月日更

❤️使用 HTML、CSS 和 JS 创建在线音乐播放器(免费送完整源码)❤️

海拥(haiyong.site)

响应式 大前端 28天写作 签约计划第二季 12月日更

dart系列之:安全看我,dart中的安全特性null safety

程序那些事

flutter dart 程序那些事 12月日更 flutter 调试工具

阿里巴巴、埃森哲、德勤三方圆桌对话——《与数字化先锋共绘创新蓝图》

大咖说

阿里巴巴 大咖说 数字经济 数智化

设计模式【6.1】-- 初探适配器模式

秦怀杂货店

Java 设计模式

0 基础学习 Flutter~(mini 进阶版)

阿策小和尚

Flutter 小菜 0 基础学习 Flutter 内容合集 签约计划第二季

存储空间降为原来的1/7,TDengine在中移物联网轨迹数据存储中的应用

TDengine

数据库 tdengine 时序数据库

音视频学习--X264码率控制--前瞻

Fenngton

音视频 H264 视频编解码 签约计划第二季

React进阶(四):路由介绍

No Silver Bullet

React 路由 12月日更

大数据开发技术NN和2NN工作机制

编程江湖

大数据

使用 scipy.fft 进行Fourier Transform:Python 信号处理

华为云开发者联盟

Python 音频 信号处理 傅立叶变换 Fourier transform

react源码解析9.diff算法

buchila11

React

react源码解析10.commit阶段

buchila11

React

Linux学习方法《Linux一学就会》:网络管理技术

侠盗安全

Linux linux运维 运维工程师 云计算架构师

技术分享| Linux高并发踩过的坑及性能优化

anyRTC开发者

Linux 音视频 高并发 服务器 高并发优化

JavaMoney规范(JSR 354)与对应实现解读

vivo互联网技术

Java、 API 虚拟化 虚拟货币

OPPO云VPC网络实践

安第斯智能云

云计算 SDN架构 智能网卡vpc

前端开发之cypress的自动化实践

@零度

前端 Cypress

设计模式【6.2】-- 再聊聊适配器模式

秦怀杂货店

Java 设计模式 适配器模式

今年双12,《代码随想录》冲榜TOP1

博文视点Broadview

基于JSX的全新BDD工具 - Crius

RingCentral铃盛

【LeetCode】保持城市天际线Java题解

Albert

算法 LeetCode 12月日更

初识腾讯移动通讯 TPNS~

阿策小和尚

28天写作 Android 小菜鸟 12月日更

Vue之全局事件总线和消息订阅与发布

编程江湖

前端开发

龙蜥社区一周动态 | 12.06-12.10

OpenAnolis小助手

龙蜥社区

在中国如何打造一个有生命力的开源项目?

Jianmu

DevOps 持续集成 CI/CD 开源社区 持续部署

面试官:说说react的渲染过程

全栈潇晨

React

Java反射机制获取运行时类的完整结构

编程江湖

JAVA开发

云原生时代,领域驱动设计思想(DDD)如何落地?

华为云开发者联盟

软件架构 架构设计 建模 领域驱动设计思想 领域模型

携程火车票Rematch框架实践_软件工程_滨峰_InfoQ精选文章