低成本可复用前端框架——Linke

简介: 前端在业务层框架的尝试

业务背景

目前团队内的开发模式多是面向组件的,UI层和逻辑层均强耦合在一起,由于业务的差异性,往往很难完全复用。

  • 闲鱼前端业务处在高速发展不断尝试的阶段,如何能更快更稳定地完成需求,更好的支撑业务发展绝对是一个值得探索的问题。
  • 在接手一个复杂的老业务代码时,经过较多人的修改,往往可维护性较差,有时只想修改某个小地方却需要较大的理解成本,所以用一套统一的组件开发规范在长期维护中显得格外重要。
  • 闲鱼技术体系经历了从weex、rax0.x到现在rax1.x的变更,中间有过一些前端资产的积累,但是由于迁移的成本后期都不再维护,如何用更小的成本让业务层平稳过渡到新的技术体系?

对于以上的问题我们希望能用框架一并解决,对于该框架的目标主要包括:

  • 提高代码可复用性
  • 规范代码,降低长期维护成本
  • 降低业务层与技术体系的关联

思路

关于提效,其中比较重要的是相同的代码不要重复写,做更细的区分和提取,提高可复用的颗粒度。另一方面是解决现有开发下比较影响开发效率的问题。

组件的分层

所以我们将面向组件的开发模式分为UI层View和逻辑层Store,以Interface进行隔离和耦合。
O1CN012gVJDO1ilWLsqz0G7_!!6000000004453-2-tps-668-566.png

图一:组件构成
在UI层无需关心状态的流转,只负责展示和交互方法的调用,DOM相关的动画交互等行为逻辑也会放到该层中。
O1CN015V035g1jCXiiVf8XT_!!6000000004512-2-tps-815-340.png
图二:组件分工

在确认了分层的逻辑后自然就引入了Interface,主要分为两部分:一部分是IProps,申明该组件所需的Props,在使用者调用该组件时进行对应的提示和约束;另一部分则负责连接Store和View,其中包括状态state和交互方法;见下面的Interface示例:

export interface IMultiScrollerProps {
  tabs: string[];
  onTabChange?(i: number): void;
}

export interface IMultiScroller extends IBase {
  readonly tabIndex: number;
  readonly tabSource: ITabItem[];
  readonly children: any[];

  onSwiperChange(i: number): void;
}

总结一下:所有的state和交互方法都在store中管理,供View消费;View中只负责和dom相关的逻辑操作,View和store的职责分界线就是View和store分别单独使用时其交互和效果都能保持不变;以此实现View和store分别能有更多的复用。

状态管理

现有的业务开发中基本所有的需求都是基于hooks的状态管理,主要存在以下问题:

  • 对于较复杂的组件hooks在多次迭代后的维护成本会非常高;

    • 有时候,你的useEffect依赖某个函数的不可变性,这个函数的不可变性又依赖于另一个函数的不可变性,这样便形成了一条依赖链。一旦这条依赖链的某个节点意外地被改变了,那么useEffect就被意外地触发了后面的情况就会变得不可控。
  • 异步陷阱

    • 状态的修改是异步的

useState返回的修改函数是异步的,并不会直接生效,所以此时读取该值获取到的是旧值。要在下次重绘才能获取新值。不要试图在更改状态之后立马获取状态。

const [value, setValue] = useState(0);
setValue(100);
console.log(value); // <- 0
  • timeout指向的是旧值

timeout指向的是旧值,即使在外部已经重新设置,由于闭包所有在setTimeout中获取到的都是之前的值。

const [value, setValue] = useState(0);
window.setTimeout(() => {
  console.log('setAnotherValue', value) // <- 0
}, 1000);
setValue(100);
  • 何时使用useCallback/useMemo等对于新手来说存在一定的门槛。
关于 Hook 中的闭包:
useEffect、useMemo、useCallback都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state、props)。所以每一次这三种 Hook 的执行,反映的也都是当时的状态,无法获取最新的状态。对于这种情况,应该使用 ref 来访问。

对于状态管理react体系中最受欢迎的应是redux与mobx
O1CN017iUcSC1jlLoxLkcqA_!!6000000004588-2-tps-960-1013.png
图三:Redux Flow

redux的特点从上图可以总结得到下面的三大原则:

  • 单一数据源
  • state 是只读的
  • 使用纯函数来执行修改

但是redux的问题也是十分明显的:开发者需要写更多附加的样板代码,并且留下更多需要我们维护的代码。

与 Redux 相似的,另一个状态管理方案是 MobX:
O1CN01RXiBaY1uRA8bF83xA_!!6000000006033-2-tps-1280-439.png
图四: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做为中间耦合层对他们进行约束和桥接。
O1CN01QZKwIz1ZEhQbUQCW0_!!6000000003163-2-tps-1524-886.png
图五:基于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

O1CN01IWWQRb1XnOC7CctNM_!!6000000002968-2-tps-659-289.png

index.tsx

O1CN01H4t5tp1I20zIxSWLj_!!6000000000834-2-tps-545-386.png

store.ts
O1CN01oByjRk1Q9MGcBvHLP_!!6000000001933-2-tps-664-876.png

上面就是一个完整的组件demo。

对比

现在的组件开发模块模式如下图六所示,以组件为单位所有的逻辑是耦合在一起的,相互之间没有分界,即便是相同的样式也很难实现复用。
无论是在代码理解还是二次开发上都存在较大的成本和不稳定性风险。
O1CN01XH7HE61cZG3LIc8kd_!!6000000003614-2-tps-802-644.png
图六:原组件的开发模式

基于Linke的组件开发模式如下图所示:
O1CN01Qk0pnK1lfW0Su8Vnd_!!6000000004846-2-tps-908-674.png
图七:基于Linke的组件开发模式

View和Store相对独立没有强耦合性,这样的好处显而易见:
• 通过阅读Interface就能知道Store/View的基本逻辑,减少理解成本
• 数据逻辑和View逻辑分别在Store和View中管理,真正实现各司其职,减少维护成本。
• 最重要的一点是通过分离让Store和View分别实现了复用,组合不同的Store/View生成不同的组件
O1CN01IaT1c727ieGHkhovg_!!6000000007831-2-tps-1500-596.png
图八:Store分别和不同的View组合

O1CN01GIgZsq1pUIlzgiP6U_!!6000000005363-2-tps-1500-587.png
图九:不同的Store和同一个View组合

应用

目前Linke已经应用在了闲鱼前端各个新项目中,包括2个线上项目和3个正在开发的项目收益明显,什么功能的代码在什么位置一目了然配合Interface中的注释大大减少了接手项目的理解成本。
通用基础组件和业务组件都在有序的抽离中,同时随着View/Store库的不断丰富,可以复用的物料资源增加,不同业务和同一业务不同场景中可以复用的View/Store越来越多,在一定程度上大大减少开发成本提高效率。

下一步

目前新财年除了现有的H5业务外,最大的特点是会对各个小程序做一些流量探索,比如淘系轻应用、微信小程序、支付宝轻应用等,这些应用的特点是与端内的H5业务及其相似,但是会有各自的细微差异。所以我们也在探索基于Linke对此类业务场景的提效。

相关文章
|
3月前
|
JavaScript 前端开发 开发者
哇塞!Vue.js 与 Web Components 携手,掀起前端组件复用风暴,震撼你的开发世界!
【8月更文挑战第30天】这段内容介绍了Vue.js和Web Components在前端开发中的优势及二者结合的可能性。Vue.js提供高效简洁的组件化开发,单个组件包含模板、脚本和样式,方便构建复杂用户界面。Web Components作为新兴技术标准,利用自定义元素、Shadow DOM等技术创建封装性强的自定义HTML元素,实现跨框架复用。结合二者,不仅增强了Web Components的逻辑和交互功能,还实现了Vue.js组件在不同框架中的复用,提高了开发效率和可维护性。未来前端开发中,这种结合将大有可为。
148 0
|
2月前
|
开发框架 前端开发 JavaScript
【博客开发】前端应用开发环境搭建(可复用)
【博客开发】前端应用开发环境搭建(可复用)
48 5
|
3月前
|
设计模式 JavaScript 前端开发
Vue.js 组件设计模式:在前端热潮中找到归属感,打造可复用组件库,开启高效开发之旅!
【8月更文挑战第22天】Vue.js 以其高效构建单页应用著称,更可通过精良的组件设计打造可复用组件库。组件应职责单一、边界清晰,如一个显示文本并触发事件的按钮组件,通过 props 传递标签文本,利用插槽增强灵活性,允许父组件注入动态内容。结合 CSS 预处理器管理和封装独立模块,配以详尽文档,有效提升开发效率及代码可维护性。合理设计模式下,组件库既灵活又强大,持续实践可优化项目工作流。
49 1
|
设计模式 前端开发 JavaScript
采用「复合模式」构建可复用的 Web 前端组件
在现代 Web 前端开发中,构建可复用、可维护的组件是提高开发效率和代码质量的关键。为了实现这一目标,开发者们一直在寻找合适的设计模式和架构原则。其中,Compound Pattern(复合模式)被广泛应用于构建具有高度复用性和可扩展性的 Web 前端组件。本文将深入探讨 Compound Pattern 的概念、优点和缺点,适用场景,开源实现方案,以及其在知名项目中的应用
4862 1
采用「复合模式」构建可复用的 Web 前端组件
|
前端开发
前端学习笔记202305学习笔记第二十二天-新增修改弹框复用5
前端学习笔记202305学习笔记第二十二天-新增修改弹框复用5
57 0
|
前端开发
前端学习笔记202305学习笔记第二十二天-新增修改弹框复用1
前端学习笔记202305学习笔记第二十二天-新增修改弹框复用1
40 0
|
前端开发
前端学习笔记202305学习笔记第二十二天-分页组件封装和复用2
前端学习笔记202305学习笔记第二十二天-分页组件封装和复用2
38 0
|
前端开发
前端学习笔记202305学习笔记第二十二天-分页组件封装和复用1
前端学习笔记202305学习笔记第二十二天-分页组件封装和复用1
41 0
|
前端开发
前端学习笔记202305学习笔记第二十二天-新增修改弹框复用3
前端学习笔记202305学习笔记第二十二天-新增修改弹框复用3
61 0
|
前端开发 JavaScript API
web前端面试高频考点——Vue3.x响应式(Composition API的逻辑复用、Proxy实现响应式)
web前端面试高频考点——Vue3.x响应式(Composition API的逻辑复用、Proxy实现响应式)
150 0
下一篇
无影云桌面