前端如何优雅建模?

这篇文章放在“杂”栏目下面,说明会是一篇不成体系的文章。我想谈一下如何在前端优雅的建模。直入正题!

前端建模包括两个层面的建模:业务领域建模和交互领域建模。这两者基本上没有本质联系,但是在前端这个场景下,有的时候又有一些特殊的情况。我们来看看如何在这两个层面建模。

业务领域建模

简单讲,业务领域建模,就是把业务实体与其逻辑进行建模。在知乎有小伙伴留言说,前端不怎么适合DDD,因为前端是贫血模型。但在我实际实践中,我更多是充血模型。业务模型需要包含字段本身,以及复杂的业务逻辑。你可能会讲,业务逻辑会被放在后端,但是实际上前端也要这个业务逻辑,比如当一个订单的负责人是组织中的某个职位的角色时,需要在订单推进过程中填写审核时间这个信息。那么,在前端,必须去判断当前用户是否是该角色,或者获得当前用户的某个权限。这个逻辑是跑不掉的。

最近,我升级了tyshemo,支持了装饰器的方式进行meta的定义。现在,你可以这样定义自己的模型:

import { Model, meta, state } from 'tyshemo'

class OrderModel extends Model {
  @meta({
    type: { 
      user_id: String,
    },
  })
  master = null

  @meta({
    type: Number,
  })
  total_price = 0

  @state()
  role = 'member'

  canFillDate() {
    return this.role === 'admin'
  }
}

通过@meta来装饰字段,通过@state来装饰状态属性。这样撰写模型,会有更舒服的感觉,而且可以更好的兼容typescript,避免以前使用static属性定义时,无法与typescript很好结合的问题。

简单讲,通过业务建模,我们得到了一些模型,这些模型是独立的,自治的,在不被使用的时候,它独立描述了该业务对象的各种字段及其逻辑,但由于不在具体的业务场景中被使用,因此也只能表现有限的业务信息,它只能告诉读代码的人“我有什么,能做什么”,而不能告诉“我做了什么”。只有使用这些模型的实例,放到具体的业务模块中,才能完成真正的模块编程。所以,单纯讲,业务建模虽然重要且有用,但是如果不被适当的人使用,就会非常混乱,毫无头绪。

建立业务模型,可以把有关需求文档中,有关核心业务的东西分出一层。

交互领域建模

这是后端没有的东西。直白讲,交互领域建模就是写类似Vue一样的View Model,但是没有template那块。简单说,就是建立一个模型,考虑到将来在view层使用它,所以该模型的所有api,都是为view设计的,主要目标,是和需求文档中有关交互相关的描述一一对应。在交互模型中,实例化业务模型,把业务模型变成交互模型内部的状态。当在view中实例化交互模型,就看不到业务模型了,view拿着交互模型的接口进行渲染和事件回调。

我在nautil中提供了可用于建立交互模型的一个体系。举个例子:

import { Component } from 'nautil'
import { SomeController } from './some.controller' // 写好的交互模型

class MyPage extends Component {
  controller = new SomeController() // 实例化交互模型

  SubmitButton = this.controller.turn((props) => {
    const { someModel } = this.controller // 读取交互模型内的某个业务模型
    const { total_price } = someModel // 读取业务字段值
    return (
      <button className={total_price > 100 ? 'sale-count' : undefined}>Submit</button>
    )
  })

  render() {
    const { SubmitButton } = this // 读取定义好的组件
  }
}

上面这段代码中,SomeController是一个交互模型,里面使用了另外一个SomeModel业务模型。但是,对于view层而言,你不需要知道它是一个业务模型,你只需要调用它即可。

Nautil在controller中提供了turn方法,用于把一个用到controller的普通的组件转化为一个被controller控制的组件,当controller中某些特定信息发生变化时,这些组件就会自动更新。

分层

前端代码分层管理,从代码量上,并不比铁板一块的管理多多少,毕竟所有的代码,都来自产品的需求描述。但是,分层管理所带来的灵活性、可维护性是不可估量的。

如果你写一个有很多块的页面的vue组件,你就会发现,你的这个组件会越来越多交织在一起的代码,从一开始很容易理解,这几个状态和这几个方法是关于最顶上这一块的,但是,随着页面其他块的交互代码的增多,你就会发现,这个状态会在哪块用?这个方法会在什么情况下调?能不能删?可不可以改?都需要上下反复读代码来确认。

而如果你采取代码分层,你会先针对业务本身的实体进行建模,然后对业务中的交互进行建模,最后才是view层的编写。此时,view层的代码会清晰很多,因为它不再去管理属于业务的逻辑,而更多的是用和回调。

依赖

在view层,我们用vue或react来写,我更多使用react,因为react没有使用Proxy或defineProperty,可以使得我们建模时,使用更多魔法。但是,有些时候,由于view层的特殊机制,导致我们如果完全脱离框架进行建模时,不得不提供一些多余的接口,来帮助和view进行依赖绑定。

以我工作的项目为例,我们使用angularjs作为主体框架,如果我单纯使用ESModule的模块,就没有办法直接使用$rootScope等这种angularjs内置的服务,但是如果我提供一个angular factory,就会牺牲模型的可移植性,为之后跨平台复用带来问题。所以,nautil中提供了controller.turn这个方法来实现模型层和视图层的连接,简单说就是在框架层面调用forceUpdate来实现重新渲染。