写点什么

低成本可复用前端框架——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:003922

评论

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

2 分钟,教你用 Serverless 每天给女朋友自动发土味情话

阿里巴巴云原生

阿里云 Serverless 云原生

鼎茂科技获得阿里云首批产品生态集成认证,携手阿里云共建新合作

阿里巴巴云原生

阿里云 云原生 加速器 企业招募

【倒计时4天】杭州专场-单元测试产品体验样板间,等你加入!

TRaaS

用javascript分类刷leetcode3.动态规划(图文视频讲解)

js2030code

JavaScript LeetCode

【Redis】Redis AOF持久化

石臻臻的杂货铺

redis 11月月更

JavaScript刷LeetCode拿offer-树的遍历

js2030code

JavaScript LeetCode

Redis6.0新特性、剖析线程模型(单线程和多线程)

C++后台开发

redis 多线程 后端开发 单线程 C++开发

vue和react的区别有哪些,哪个好

千锋IT教育

扒去Spring事件监听机制的外衣,竟然是观察者模式

程序知音

Java spring ssm Java后端 后端技术

一路同行:开发者与华为云的2022

脑极体

云原生网关的可观测性体系实践

阿里巴巴云原生

阿里云 云原生 云原生微服务

以用户为导向,vivo打造安全新范式

极客天地

8分钟带你学会SpringBoot整合Redis来实现缓存技术

好程序员IT教育

redis Spring Boot

10 分钟部署一个 API 工具

Scar 

开源 Serverless 接口测试 Vercel

Docgeni 2.0 发布,开启自动化

PingCode研发中心

大数据 Hadoop 的五大优势

千锋IT教育

10月月更获奖名单出炉啦!快来看看你上榜没有!

InfoQ写作社区官方

热门活动

数字化时代,企业需要怎样的数据中台?报名元年微课堂,一起来探索

元年技术洞察

数据中台

RocketMQ 5.0 API 与 SDK 的演进

阿里巴巴云原生

阿里云 RocketMQ 云原生

java企业级开发中常见的注入方式

千锋IT教育

首批!腾讯云通过金融开源技术服务能力评估

腾源会

开源

居民消费价格指数变化新鲜出炉,这类商品同比涨幅最大

前嗅大数据

数据分析 数据采集 居民消费 居民消费指数

实践分享:30分钟在电脑端运行小程序

FN0

桌面应用 桌面应用开发

技术解读倚天 ECS 实例——Arm 芯片的 Python-AI 算力优化 | 龙蜥技术

OpenAnolis小助手

Python AI arm ECS sig

数据分析er看过来,五款工具有你需要的

葡萄城技术团队

数据分析 前端 数据可视化

Wallys/Introduction of DR9074 series network card/qcn9074/qcn9072/qcn9024/industrial M.2 card

wallysSK

QCN9074 QCN9024 QCN9072

聚焦年轻消费人群 百草味入围最受“Z世代”喜爱品牌TOP10

E科讯

线性稳压电源和开关电源,到底有什么区别?

元器件秋姐

电源电路 元器件采购 元器件电商 元器件知识 华秋商城

Kotlin变量的空安全(Null Safety)

子不语Any

android kotlin 11月月更

Spring Boot框架下实现Excel服务端导入导出

葡萄城技术团队

前端 Excel 控件

关于Web3.0,你了解多少?个推与您畅聊下一代互联网

个推

互联网 元宇宙 Web3.0

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