业务背景
目前团队内的开发模式多是面向组件的,UI 层和逻辑层均强耦合在一起,由于业务的差异性,往往很难完全复用。
闲鱼前端业务处在高速发展不断尝试的阶段,如何能更快更稳定地完成需求,更好的支撑业务发展绝对是一个值得探索的问题。
在接手一个复杂的老业务代码时,经过较多人的修改,往往可维护性较差,有时只想修改某个小地方却需要较大的理解成本,所以用一套统一的组件开发规范在长期维护中显得格外重要。
闲鱼技术体系经历了从 weex、rax0.x 到现在 rax1.x 的变更,中间有过一些前端资产的积累,但是由于迁移的成本后期都不再维护,如何用更小的成本让业务层平稳过渡到新的技术体系?
对于以上的问题我们希望能用框架一并解决,对于该框架的目标主要包括:
提高代码可复用性
规范代码,降低长期维护成本
降低业务层与技术体系的关联
思路
关于提效,其中比较重要的是相同的代码不要重复写,做更细的区分和提取,提高可复用的颗粒度。另一方面是解决现有开发下比较影响开发效率的问题。
组件的分层
所以我们将面向组件的开发模式分为 UI 层 View 和逻辑层 Store,以 Interface 进行隔离和耦合。
图一:组件构成
在 UI 层无需关心状态的流转,只负责展示和交互方法的调用,DOM 相关的动画交互等行为逻辑也会放到该层中。
图二:组件分工
在确认了分层的逻辑后自然就引入了 Interface,主要分为两部分:一部分是 IProps,申明该组件所需的 Props,在使用者调用该组件时进行对应的提示和约束;另一部分则负责连接 Store 和 View,其中包括状态 state 和交互方法;见下面的 Interface 示例:
总结一下:所有的 state 和交互方法都在 store 中管理,供 View 消费;View 中只负责和 dom 相关的逻辑操作,View 和 store 的职责分界线就是 View 和 store 分别单独使用时其交互和效果都能保持不变;以此实现 View 和 store 分别能有更多的复用。
状态管理
现有的业务开发中基本所有的需求都是基于 hooks 的状态管理,主要存在以下问题:
对于较复杂的组件 hooks 在多次迭代后的维护成本会非常高;
有时候,你的 useEffect 依赖某个函数的不可变性,这个函数的不可变性又依赖于另一个函数的不可变性,这样便形成了一条依赖链。一旦这条依赖链的某个节点意外地被改变了,那么 useEffect 就被意外地触发了后面的情况就会变得不可控。
异步陷阱
状态的修改是异步的 useState 返回的修改函数是异步的,并不会直接生效,所以此时读取该值获取到的是旧值。要在下次重绘才能获取新值。不要试图在更改状态之后立马获取状态。
•timeout 指向的是旧值
timeout 指向的是旧值,即使在外部已经重新设置,由于闭包所有在 setTimeout 中获取到的都是之前的值。
•何时使用 useCallback/useMemo 等对于新手来说存在一定的门槛。
关于 Hook 中的闭包:useEffect、useMemo、useCallback 都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state、props)。所以每一次这三种 Hook 的执行,反映的也都是当时的状态,无法获取最新的状态。对于这种情况,应该使用 ref 来访问。
对于状态管理 react 体系中最受欢迎的应是 redux 与 mobx
图三:Redux Flow
redux 的特点从上图可以总结得到下面的三大原则:
•单一数据源
•state 是只读的
•使用纯函数来执行修改
但是 redux 的问题也是十分明显的:开发者需要写更多附加的样板代码,并且留下更多需要我们维护的代码。
与 Redux 相似的,另一个状态管理方案是 MobX:
图四:Mobx Flow
相比 Redux 的强规则约定,MobX 更简单灵活,核心原理是通过 action 触发 state 的变化,进而触发 state 的衍生对象(Computed value & Reactions)。开发者只需要定义需要 Observe 的数据和由此衍生的数据(Computed value)或者操作 (Reactions),剩下的更新就交给 MobX 去做就可以了。一句话总结就是:
任何源自应用状态的东西都应该自动地获得。
分析闲鱼的业务特色并不存在 5 个以上同学同时维护一个项目的超大型需求,强约定的 redux 对我们来说收益有限,而 MobX 确实比 Redux 上手更容易些,并且不需要写很多样板代码,可以提供更高效的选择。
实现
我们给框架取名:Linke,来自 switch 的游戏塞尔达,希望它能像林克一样点亮一个个神庙。
基于上面的分析思路结合实际业务中的技术体系(Rax)最后我们设计了下面的研发体系:UI 部分也就是 View 还是沿用原有的 Rax,UI 用到的状态也直接在 View 中管理。业务逻辑部分也就是 Store 用 Mobx 的能力解决上面提到的现有 hooks 开发遇到的问题,两者没有强关联。
Linke 做为中间耦合层对他们进行约束和桥接。
图五:基于 Linke 的研发体系
API
为保证开发者最低的学习成本,Linke 在设计时尽可能地减少 API,最终只有一个方法和 4 个 Store 内置方法,详见:
observer(baseComponent, Store)
保证组件能响应 store 中的可观察对象(observable)变更,即 store 更新,组件视图响应式更新
Store 内置方法
•成员方法 - $$set: 所有状态变化必须通过 $$set 来完成,与微信的 setData()类似
•成员方法 - $$setProps:处理外部传入的组件 props,View 初始化或者 props 发生变化时调用
•成员方法 - $$didMount:提供 View 的生命周期,View 被插入 DOM 时调用
•成员方法 - $$unMount:提供 View 的生命周期,View 被移除 DOM 时调用 可以看出 Store 内置方法中除了 $$set 其他三个都是生命周期方法,其调用顺序为:$$setProps -> $$didMount -> $$unMount
demo
Interface.ts
index.tsx
store.ts
上面就是一个完整的组件 demo。
对比
现在的组件开发模块模式如下图六所示,以组件为单位所有的逻辑是耦合在一起的,相互之间没有分界,即便是相同的样式也很难实现复用。无论是在代码理解还是二次开发上都存在较大的成本和不稳定性风险。
图六:原组件的开发模式
基于 Linke 的组件开发模式如下图所示:
图七:基于 Linke 的组件开发模式
View 和 Store 相对独立没有强耦合性,这样的好处显而易见:
• 通过阅读 Interface 就能知道 Store/View 的基本逻辑,减少理解成本
• 数据逻辑和 View 逻辑分别在 Store 和 View 中管理,真正实现各司其职,减少维护成本。
• 最重要的一点是通过分离让 Store 和 View 分别实现了复用,组合不同的 Store/View 生成不同的组件
图八:Store 分别和不同的 View 组合
图九:不同的 Store 和同一个 View 组合
应用
目前 Linke 已经应用在了闲鱼前端各个新项目中,包括 2 个线上项目和 3 个正在开发的项目收益明显,什么功能的代码在什么位置一目了然配合 Interface 中的注释大大减少了接手项目的理解成本。
通用基础组件和业务组件都在有序的抽离中,同时随着 View/Store 库的不断丰富,可以复用的物料资源增加,不同业务和同一业务不同场景中可以复用的 View/Store 越来越多,在一定程度上大大减少开发成本提高效率。
展望
目前新财年除了现有的 H5 业务外,最大的特点是会对各个小程序做一些流量探索,比如淘系轻应用、微信小程序、支付宝轻应用等,这些应用的特点是与端内的 H5 业务及其相似,但是会有各自的细微差异。所以我们也在探索基于 Linke 对此类业务场景的提效。
本文转载自:闲鱼技术(ID:XYtech_Alibaba)
原文链接:低成本可复用前端框架——Linke
评论