AICon 上海站|日程100%上线,解锁Al未来! 了解详情
写点什么

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

  • 2021-04-19
  • 本文字数:3831 字

    阅读完需:约 13 分钟

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

业务背景

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


  • 闲鱼前端业务处在高速发展不断尝试的阶段,如何能更快更稳定地完成需求,更好的支撑业务发展绝对是一个值得探索的问题。

  • 在接手一个复杂的老业务代码时,经过较多人的修改,往往可维护性较差,有时只想修改某个小地方却需要较大的理解成本,所以用一套统一的组件开发规范在长期维护中显得格外重要。

  • 闲鱼技术体系经历了从 weex、rax0.x 到现在 rax1.x 的变更,中间有过一些前端资产的积累,但是由于迁移的成本后期都不再维护,如何用更小的成本让业务层平稳过渡到新的技术体系?


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

  • 提高代码可复用性

  • 规范代码,降低长期维护成本

  • 降低业务层与技术体系的关联

思路

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

组件的分层

所以我们将面向组件的开发模式分为 UI 层 View 和逻辑层 Store,以 Interface 进行隔离和耦合。



图一:组件构成 


在 UI 层无需关心状态的流转,只负责展示和交互方法的调用,DOM 相关的动画交互等行为逻辑也会放到该层中。 


图二:组件分工


在确认了分层的逻辑后自然就引入了 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 

图三: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

import { IBase } from '@ali/idlefish-linke';export interface IComponentProps { // 组件所需props  tabs: string[];  onTabChange?(i: number): void;}export interface IComponent extends IBase { // 连接view和store的state&交互方法  readonly items: any[];
handleLoadmore(): void;}
复制代码

index.tsx

import { observer } from "@ali/idlefish-linke";import Store from './store';import { IComponent, IComponentProps } from './interface';function Component({items, handleLoadmore}: IComponent) {  return (    <View>      {        items.map(item => {          return <Text>{item.title}</Text>        })      }      <View onClick={handleLoadmore}>load more</View>    </View>  )}export default observer<IComponentProps>(Component, Store);
复制代码

store.ts

import { makeAutoObservable } from "@ali/idlefish-linke";import { IComponent } from './interface';export default class ComponentStore implements IComponent {  /**   * 所有状态变化必须通过$$set来触发Effect   * $$set赋值来自于makeAutoObservable(this);   * this.$$set('items', [])   */  $$set;  /**   * 带初始值的属性会自动被观测   */  items: any[] = [];  page: 1;
constructor() { // 自动observable该类 makeAutoObservable(this); } $$setProps(props) { ... // 对props的处理可以放到这里 }
$$didMount() { // 通过 $$didMount / $$unMount 来感知view的生命周期 this.fetch(); }
fetch () { mtop.request('mtop.xxx', {page}) .then(d => { this.$$set('items', d.list); }) }
handleLoadmore = () => { this.$$set('page', this.page++); this.fetch(); }}
复制代码

上面就是一个完整的组件 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

2021-04-19 07:003789

评论

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

社区活动 | Apache Flink Meetup·深圳站,锁定 Flink 最佳实践

Apache Flink

flink

开篇:开机界面设置欢迎语显示IP地址

程序饲养员

Linux __init__

小白读了这篇JVM,直呼真香!(长篇干货预警)

Java架构师迁哥

(1)skyent VMware Workstation Pro下载与安装

休比

2020年“有史以来”全网最全1309道BAT大厂java面试题(附答案分享)

数据库 程序员 面试 Java 分布式

拥抱K8S系列-08-命令行工具管理K8S集群1

张无忌

Kubernetes 运维 kubectl

闪送,为何能从顺丰中杀出一条血路?

学习 编程 架构师

掌握这21个Java的核心技术点,涨薪5K起步,告别上班复制粘贴!!

Java架构师迁哥

第十一周.总结

刘璐

flutter app 打开微信小程序探究

Daniel

亚马逊宣布20亿美元“气候宣言基金”首批投资企业名单

爱极客侠

英特尔、阿里巴巴全方位深化技术创新 共同引领数智未来

E科讯

The Way To Go --- 切片

书旅

slice Go 语言

第十二周.命题作业

刘璐

加强顶层设计 推动数字出版高质量发展

CECBC

区块链 数字经济 数字出版

苹果A系列芯片的三年AI进化:为何要大规模升级AI算力?

脑极体

穷人也能建个人博客了——阿里云函数计算体验

KAMI

阿里云 云服务 Faas WordPress

LeetCode题解:144. 二叉树的前序遍历,递归,JavaScript,详细注释

Lee Chen

大前端 LeetCode

第十三周.命题作业

刘璐

2020 恒生 LIGHT 开发者大会,早鸟票限时开售

DT极客

架构师第1课作业及学习总结

小诗

谈谈力软快速开发平台B/S专业报表工具

Philips

敏捷开发 开发工具

虚拟币交易所搭建,数字货币永续合约平台搭建

360视觉入局人脸识别:终结行业“裸奔”,“安全”是终极法则

脑极体

甲方日常 19

句子

工作 随笔杂谈 日常

阿里面试,让我说说ThreadLocal,我一口气说了四种

root

Java 多线程 ThreadLocal

HashMap的7种循环姿势你都掌握了吗?

root

hashmap 循环效率 JDK1.8

深耕边缘计算 揭秘阿里云边缘云网一体化的技术实践

阿里云Edge Plus

5G 边缘计算 视频

高并发优雅的做限流

架构师修行之路

限流算法 高并发优化

坚持新媒体写作第21天了,聊聊我为什么喜欢写作

老胡爱分享

学习 写作 习惯养成 坚持 随笔杂谈 讨论写作

自己实现一个简单的 DevOps 工具系列(后端篇)- 1

sinsy

Shell CI/CD

低成本可复用前端框架——Linke_大前端_闲鱼技术_InfoQ精选文章