原文链接:2020.09.09 - 钉钉企业级前端应用的最佳实践 | ArchSummit
一、钉钉桌面端技术演进架构
1.1 演进历程
- 纯 Web:技术栈Angular 1.3,纯Web。目的是快速抢占市场;
- NW + Angular:迎来技术红利,Electron 和 NW 桌面端技术出现,此时便将 Web 应用移植到 NW 上面。为什么选择 NW 而不是 Electron,因为在中国环境下有大量的 XP 系统,而 NW 是支持XP系统的。
- Native + CEF + Web:两年前用户越来越多,耗时瓶颈、体验瓶颈达到无法忍受地步,如一些重度用户会话会特别多,登录以后需要等一两秒以后才能进行交互操作,这是无法忍受的。其次随着业务发展,人也越来越多,于是开始探索新的架构,即混合APP方案(Native+CEF+Web)。采用Hybrid混合模式(Native UI 和 Web UI ),Native 会给一个CEF(Chromium Embedded Framework),即 C++ 控件。它用于提供一个容器,在这个容器之上会加各种Web APP应用。
1.2 钉钉桌面端架构
钉钉桌面端架构是一个分代模型。15年采用React,上层有五六十个应用,希望这五六十应用依赖一套底层基础架构,为了效率采用了这个方案。随着业务不断发展,发现有些应用开始趋于稳定,好几个迭代都不更新了。有些应用,像IM、搜索,更新频率会非常高。当对这些活跃应用去升级它的技术体系的时候,就破坏了非活跃应用的底层架构,回归成本是非常高的,是不能接受的。所以在这个基础上提出了分代模型。
针对老的应用依旧采用老的架构(上图蓝框),对于它来说,升级没有太多收货,反而会带来额外的成本。对于新的活跃的应用,会给它新的架构,新的架构是基于React 16的hooks体系(注:上图中的modelHub和SAAS项目的MarkingHub原理相似)。
普通前端应用可理解为是面向接口编程,开发人员会常说这个页面需要两个接口(注:见中后台项目)。但对于钉钉桌面端来说则是面向领域模型编程,如消息Model里面是不是要增加某个字段,会话Model某个方法是不是不对。这是二者最大区别。
有了这套分代模型之后,它可以支持独立架构异构体系,为支持个性化的包提供了技术可能性。
二、富前端应用的挑战
开发富前端应用面临以下几大挑战:
- 多人协作开发的沟通成本和效率:前端内部的沟通协作成本会跟随整个业务复杂度和代码量复杂度呈指数级增长;
- 复杂异步数据流之间的竞争和协作:如果数据层的数据不对,对用户体验来说是非常致命的;
- 副作用管理:副作用管理主要是聚焦在UI层。React、Vue这些框架解决了一个核心问题,即DOM和State的一致性问题,但并没有解决副作用(如进入页面要拉取些数据)。而这些副作用往往是开发人员最容易被忽略的,这导致代码没那么容易被复用,修改成本较高。
- 稳定性保障:对于企业级应用来说,最重要的是稳定性。
2.1 挑战
多人协作开发的沟通成本和效率
理想情况下,每个人只负责自己的部分,如下图所示:
但实际上这种模式不管是对个人还是组织都是有伤害性的。对于个人来说,很容易变成井底之蛙,只会了解自己所Coding的那部分逻辑,其他的都不会care,容易限制个人发展高度,导致无法站在全局去思考问题。
对于团队而言,如果是这样的模式,隐性风险就会暴增。当某人因为个人原因离职时,组织会陷入瘫痪。
所以对于一个组织来说,健康的模型应该是长这样的。
单点依赖就不会变得那么严重,每个人维护不同模块代码,并理解别人背后的业务逻辑。但这种模型交叉点会非常多,每个交叉点都意味着一次沟通成本(注:这个思路很棒),那这个沟通成本是无法想象的。
复杂异步数据流之间的竞争和协作
比如有两个request:first request 和 second request。发起请求的先后顺序是确定的,但返回的response的先后顺序是不确定的。当发送和响应对不上时候,就会出现异步竞争。这种情况属于典型的异步竞争。
异步协作
并行、串行、过滤、直到、去重、防抖、节流、映射、组合、分裂、采样、重试
对于通用的异步协作,采用的是rxjs。
业务层协作
举例:点击选择会话到整个会话完整展示出来,不仅需要拉取会话信息,还需拉取灰度信息,甚至还需拿到用户信息,等这些信息都拿到之后才能显示完整的会话,这就是所谓的异步数据流的协作。如果不去做抽象,那遇到相似场景,代码重写一遍,对整个代码质量挑战非常大。
副作用管理
React 实际上只是帮你降低写组件难度,但并没有帮你降低设计组件的难度。写Class组件时候常会发生一个问题,即组件写得不够纯粹,导致别人无法复用。
蓝色代表纯组件,只依赖props。红色代表副作用组件,有的依赖props,有的依赖state。可以看到红色组件A1是没办法复用的,因为它耦合了你的副作用。
2.2 如何解决
多人协作 - 代码即文档
写文档不太现实,当代码量越来越多,这会导致维护文档和代码同时进行。
每个 class 都会有完整的 Props 的 Type定义,同时Service会有完整的Type定义。
一个Service会有多个APP。服务端会有一个DSL定义,会有专门的工具去自动拉取并同时生成各个客户端代码。
即将DSL转换成AST,
异步数据竞争和协作 - rxjs
对于复杂的前端应用来说,数据流是它的生命,因而对数据层的稳定性要求非常高。数据层一旦出问题,都是灭顶之灾。
传统的前端应用,在数据层不会这么复杂,页面级的数据层只是面向接口编程,只需几个Request接口就OK。但钉钉前端是面向领域模型编程,模型的变更牵扯到需要处理数据。
rxjs 有两个非常重要的概念,其中一个是 Observable。什么是 Observable?Observable 就是一个持续不断产生数据的 Streaming。Observer可以订阅它,对哪个Observable感兴趣就去订阅它,在Observable生命周期内产生的所有数据都可以拿到,都可以消费。
可以把 Observable 假想成 Promise,但Promise只能产生一个数据,要么resolve要么reject,只能一次。对于Promise来说,它的生命周期只会产生一次数据。
示例:如下图所示,随着时间之前的数据会累加1的Observable
rxjs 真正有魅力的是它的操作符(Operator),操作符可以将一个 Observable 转化成另外一个 Observable。如定义一个操作符,可以将拉取用户头像的Observable转化成先拉用户头像、再拉灰度信息、再拉消息,然后再返回一个新的Observable。这就是Operator的真正魅力。Operator可以处理异步的Observable之间的竞争跟协作,同时可以对你的业务逻辑进行沉淀,下沉到base层。沉淀到base层对你的代码质量会有非常大的提升,一般base层的代码质量要求非常高,会有专门的测试用例。
Operator的定义
可以看到,输入是一个Observable,输出是一个Observable。本质就是转化器。
当你的rxjs比较复杂,有明显的异步数据流竞争协作的时候,这是一个非常好的架构。
Base Observable可以理解为基础的request或者基础领域模型的Model。Base Operator就是定义的业务逻辑,这是完全可以达到复用的。业务开发人员只需要做一件事情,即从Biz Observable拿到适合他场景下的操作符,然后将他的操作符 pipe到操作符中即可完成。如果没有现有的Operator,可以找到Base Operator来写这样一个操作符。
副作用管理 - hooks
三、回顾
对于一个复杂的前端应用,它大致分为以下几大块:
- 数据层:
- global state
- global state层需要有同步的数据处理方案(如redux)和异步数据处理方案(如rxjs)。
- 业务逻辑单元的抽象,比如Operator。
- UI 层:
- container:从store里面把感兴趣的model取出来。
- pure function component:只做UI层的渲染,没有逻辑;
- local state hooks: 进行跨组件的共享
- effect hooks: 副作用的hooks
- 规章制度:
- 公共代码的提案机制:如针对base层代码,除了单测,还有个重要的就是提案机制。即base层代码不是随意可以进的,先JavaScript提案机制,Stage1谁审核,Stage2谁负责审核,都通过且线上验证后才可以进来。
- codereview机制: