模型驱动

分层而治

为什么在web前端很少有人会提到分层架构,例如经典MVC架构,这是因为浏览器诞生之初就只是作为一个后端数据的UI渲染器。也就是说整体来看,web1.0时代的整个web前端工程就是一个View层,而ModelController就是指后端,所以根本无需在web前端工程中去提什么MVC。

然而web生态发展到今天,浏览器越来越强大,赋能越来越多,甚至不亚于一个小型操作系统,这时候的Web前端早已不是当初简单的数据渲染器,状态管理、会话维持、数据持久化、文件缓存、通信协议...随着PWA、小程序、快应用的推广,WebAPP不再是瘦客户端,渐渐成长为大胖小子。如果我们Web前端的架构还停留在View的时代,那么显然是落伍的。

View是最直观的、最原始的驱动源,解决了所有View的问题也就实现了WebApp的所有功能,前端开发人员的全部工作就是一个又一个的编写Component。正是因为这种以View为核心的架构思维,导致我们只为解决表层问题而架构,不去思考顶层设计,这也是很多Web前端工程经过几轮迭代和人员转手后,很难继续维护下去的根本原因。

近年来涌现出一些优秀的UI渲染框架,比如React/Vue/Anglar,它们很强大,可以干很多分内和分外的事情。正因为如此,更造成了开发者的惰性,将所有逻辑都直接以最原始的方式写在View中,不管是Mode还是Controller,不管是业务逻辑、还是渲染逻辑、还是存储逻辑、还是通讯逻辑、还是路由逻辑...所有各类逻辑叠加各种UI生命周期,全部都耦合在一个Component中。

从视图驱动到领域驱动

是时候改变我们的架构思维,从视图驱动升华到领域驱动。虽然视图驱动是最直观也是最简单的一种架构模式,但是我们不仅要解决问题,还要思考如何优雅的解决问题,这也好比是排版设计的区别吧!

或者说,在项目野蛮生长的混沌初期你可以从下往上构建,所建即所得。但到了某个重构的时间点,你还是得站在顶部进行上层设计。

当然,这里有个默认前提,那就是需要长期维护的中大型项目,小而快的项目无需谈各种架构模式...

将UI框架拉下神坛

UI不是工程的全部,UI的指责只有2个:输入与输出,仅此而已。所以即便React/Vue再强大,你也应当仅把它们当成一个GUI工具,而非整个工程React/Vue一把梭。

UI的职能

UI只是指令的收集者、传达者、反馈者,而不应当成为指令的执行者。

应用的核心是业务逻辑

应用的核心的逻辑是什么?是游戏规则(业务逻辑),一个应用不管选择哪个UI设计师、哪款UI框架、哪个运行平台,它的业务逻辑总是通行不变的。

现在问你一个问题:

如果没有UI,你的应用可以通过命令来驱动吗?

你可能会怼我说,用户怎么可能会通过命令来使用我的应用,没有这样的业务场景,你这个假设是毫无意义的。然而,这种场景或许并不是为用户而设,而是为合作伙伴而设、为测试工具而设、为日志分析而设、为今后的扩展和生态建设而设...

再问你一个问题:

  • UI说:应用要改版,皮肤、交互、页面组织都要调整,要多久?
  • 产品说:把H5改改,做成小程序、APP吧,要多久?
  • 经理说:React人太难招了,要不我们换成Vue吧,要多久?
  • Leader说:Vue3出来了,我们升级为最新版吧,要多久?
  • 架构师说:Svelte快到飞,我们重构为Svelte吧,要多久?

业务逻辑不变,仅调整UI和其运行平台,问你要多久?这个时候,如果你的全部逻辑都是耦合在某种特定的UI框架中的,那你只能是哀默之心大于死...

为什么要分离UI逻辑与业务逻辑

从早期的前后端一体化,到后面的前后端逻辑分离,在到这里我重提UI逻辑与业务逻辑分离,都只是顺着2件事件在做:让需要内聚的更内聚,让可以解耦的更松散。

  • 业务逻辑抽象,UI逻辑具体:业务逻辑是抽象的数据模型,而UI逻辑则是抽象事物逐层泛化后的具体表现,它们两个本身就不再同一个Level上。

  • 业务逻辑通用,UI逻辑特殊:UI逻辑通常需要借助于某个具体的UI框架而表达,而UI框架通常都会与运行平台息息相关,同时也会加入自己的各种各种限制、约束、约定、魔术方法等。所以形象来说,前者是JS国度的普通话,而后者则是方言。

  • 业务逻辑直观,UI逻辑隐晦:UI渲染本身就是一件很复杂的事情,其间涉及到各种组件的各种生命周期、创建、销毁、更新、重绘等等、在加上各种优化手段造成的心智负担。所以你会发现某些业务逻辑放在Redux或是Vuex等纯状态管理框架中,比使用组件内部的Hooks去维护更简单和直观。尽管Hooks也可以模拟这些Flux框架,但是不及前者有条理,比如测试、监控、分析、回滚一个Action多容易,你测试、监控、分析、回滚一个Hooks试试...

  • 业务逻辑稳定、UI逻辑多变:UI/UE太感性、太灵活了,带有很强的个人主观色彩,经常会被优化、修改、甚至推翻,不像业务逻辑那么稳定。这也是要将UI逻辑和业务逻辑分开的重要原因,如果你将业务逻辑和UI逻辑写在一起,你本来稳定的代码将受到不稳定代码的严重骚挠。

铁打的MV流水的C

现在提MVC是不是过时了?非也,MVC是其实一种最简单的哲学思想,Model代表抽象事物,View代表具体事物,而Controller则是代表如何在抽象事物和具体事物之间泛化。所以MVC是一种思想,而不是特指某种框架,过时的只会是框架,而非思想。

近年来各种演化如:MVPMVVMMVI等等,其实都是在围绕C做文章,最终的目的无非就是更好的隔离MV,也就是千方百计分离UI逻辑业务逻辑

  • 剥离了业务逻辑,UI层变得更纯粹,它只是负责展示、交互和传递用户事件。
  • 剥离了UI逻辑,业务层不再受到各种生命周期和糖衣语法的干挠,更纯净透明。
  • 分层而治,将稳定的业务层和灵活的UI层分离,增加了代码的可复用性和可移植性。

组件状态与应用状态

模型驱动是什么

对业务逻辑进行领域建模、抽象、提纯、精炼,我们将得到一个最有价值的、最稳定的、最通用的核心逻辑,这便是业务模型

模型是一个可以运转的有机体,状态是它的一个切片,模型包括状态、以及改变状态的各种动作和事件。

模型是应用的骨骼,UI则是皮肉,所谓的模型驱动是一种设计理念:用骨骼来驱动皮肉,而不是用皮肉来驱动骨骼

如何建立模型

业务模型包括业务状态以及改变状态的各种业务动作。所以建立Model的关键是:

  1. 分离UI组件状态业务状态,将UI组件状态定义在Component内部,将业务状态定义在Model内部:
    • ComponentState用来描述Component的内部的状态,外界根本不关心它的存在与变化,它与Component唇齿相依,跟随Component诞生和销毁。
    • BusinessState用来描述整个应用的当前业务状态,它与具体哪个Component无关,只会跟随业务动作诞生和销毁。
  2. 分离UI事件逻辑业务动作逻辑,将UI事件的处理逻辑封装在Component内部,将业务动作的处理逻辑封装在Model内部:
    • 事件Event:用户通过UI界面产生的人机交互事件。
    • 业务Action:用户通过UI事件背后想达成的业务目的。
    • 它们一个是因一个是果,一个是表一个本。

举个具体的例子吧:"用户登录",通常逻辑如下:

async function login(username:string, password:string){
    //验证username、password是否有效
    //验证当前用户是否已经登录
    //请求后端API,并等待返回
    //如果成功,保存用户信息,并跳回原页面
    //如果失败,提示错误,并留在原地
}

首先,这是一个业务动作,因为它可以不依赖哪个具体UI而运行,用户可能通过onClick事件点击登录按钮来触发,也可能通过onKeyPress按下回车键来触发,甚至你可以直接让用户通过login命令来触发。所以该方法应当写在Model中:

class Model{
    async login(username:string, password:string){
        //验证username、password是否有效
        //验证当前用户是否已经登录
        //请求后端API,并等待返回
        //如果成功,保存用户信息,并跳回原页面
        //如果失败,提示错误,并留在原地
    }
}

而UI中的逻辑只是监听Event事件,并关联到此业务动作:

export default ({dispatch}) => {
  const onLogin = () => {
    dispatch(userActions.login('admin', '123456'));
  };
  return (
     <div>
        <button onClick={onLogin} >登录</button>
     </div>
  );
}

将业务逻辑移出UI组件,这样UI层就变薄了,回归到了它的本质:只负责收集业务动作,不负责处理它

借助于模型驱动实现跨端开发

业务模型与运行平台无关、与UI框架无关,它具有天然的跨端跨平台特性。

我们跨端开发的主要魔障其实就是UI层,这是一个感性的东西、它灵活多变、不但不存在标准反而还追求个性。各端都坚守自己用户习惯、设计风格、传统文化、个性表现、默认优化、方言约定等等,这已经不是技术范畴的事了,即便你强行抹平了,也只能顾此失彼...

如果我们能够把UI层剥离出来,并削薄它,那么为什么不直面UI的个性化,最大程度复用Model、然后为每个端量身定制一套漂亮的外衣呢?

轻UI,重Model

UI层越薄,应用的通用代码就越多,跨端跨平台的工作量就越小...

elux模型驱动示意图