关于软件工程:我来聊聊模型驱动的前端开发

4次阅读

共计 5986 个字符,预计需要花费 15 分钟才能阅读完成。

如果把「客户端」想成是楼,把「数据」想成是水——「Model」就是这幢楼的蓄水池,提供短缺的水源;「ViewModel」是将蓄水池里的水进行污染等加工的中央,而后输送给挨家挨户;「View」局部的每个 UI 组件就是「挨家挨户」,对水进行生产的中央。

所有皆为模型

模型是人们依据事物特色将它们分类并形象后的后果,建模是人们认知世界的一种形式。

模型驱动

数字世界这种虚拟空间,外面本无一物,是个须要被人开垦的充实的世界。那么人该如何打造数字世界呢?

就像《圣经》里形容的——上帝依照本人的样子发明了亚当这个世上第一个人类,又从他身上取下一根肋骨发明了夏娃这个世界上第二个人类。在这里,上帝将本人作为参照提取特色形象出祂所认为的「人」的模型,并依据这个模型发明出「亚当」和「夏娃」。

人在打造数字世界时必然会参照本人所存在的并且是本人所认知的世界,因为人不可能想像出本人无奈认知的事物。人们所形象的事实世界的事物的模型,就成了建设数字世界的根底,而数据则为结构数字世界的根本单元,数字世界成了事实世界的映射。

模型是数字世界万物的概念,程序是将概念具像化的工具,打造数字世界需从建模开始。

畛域驱动

下面说了在打造数字世界时首先要建设模型,而后以模型为核心开始建造。那么要怎么进行建模呢?

至今为止,软件工程倒退这么多年,产生了很多方法论,其中「畛域驱动设计」在构建大型软件时是被宽泛驳回的实际办法。它的外围就是针对问题域剖析并建设畛域模型,理出模型间的关系及业务逻辑。

畛域驱动设计最罕用在商业层面的模型上,如:蕴含名称、编号、规格、出厂日期等信息的商品模型;同时也能够用在技术层面的模型上,如:蕴含名称、编码、字段、关系、束缚等用来形容模型的信息的模型。前者称之为「业务模型」,后者则是「元模型」。业务模型能够被元模型形容。

如果把模型映射为数据库表,那么元模型所对应的表中的每条记录都是元数据,业务模型所对应的表中的每条记录都是业务数据。

MVVM 架构

规范的 MVVM 架构是 Model-View-ViewModel 三局部:

而这里所说的如下图所示:

从图中能够看到,多了个「Action」,所以实际上应该是 Model-View-ViewModel-Action 四局部。它们之间彼此拆散,以组合的形式协同工作。

为了考究对称美,将这种架构简称为「MVAVM」。

模型

模型的主要职责是前、后端协定解决,以及对数据进行读写操作。

前、后端协定的解决包含元数据适配和 HTTP 申请结构。与后端对接的工作都管制在这一层,其余层的运作都基于这层适配后的后果。

在这层中进行读写的数据,既有业务数据又有元数据。元数据只加载一次,将适配后的后果进行缓存;业务数据只临时缓存尚未长久化的处于草稿状态的记录,长久化之后会将其删除。

ViewModel

VM 的职责很单纯,就是解决业务数据流转相干的逻辑,即数据的散发、汇总与联动。实践上,在这层不间接进行任何与申请服务、执行动作相干的解决。

正如文章结尾所说——在一个利用中,数据是像水一样一直流动的,在此过程中,VM 应该起到铺设输送管线与在特定节点对数据进行解决的作用。依据这一特点,能够思考采纳管道和过滤器模式:

实例与数据的关系

每个 VM 实例都来源于数据,是数据的变形,是具备能力的数据。

依据数据源的状态,VM 实例大抵分为列表、对象和值三种。如果值是布尔、数字、字符串等简略类型,那就即刻终止;若值为对象、列表等简单类型,则要递归上来,直到末端为简略类型。

须要留神的是,VM 实例与数据一一对应,其实质就是数据自身,而不是数据的容器。也就是说,VM 实例不是装水的瓶子,不能把曾经装的水倒掉换些水进来,而是一起抛弃。

生命周期

任何对象的生命周期都可粗略地分为初始化、流动中与销毁三个阶段。

在初始化时依据策略获取本身数据源,与下级 VM 实例创立的流进行对接造成数据管道,而后创立向外推送本身变动的流。

流动期间就是一直地与外界进行数据交换:

  1. 视图输出变动时,通过对应的 VM 实例提交本身的数据变更
  2. 在解决被提交的输出数据时会对其进行保留,并收回有数据提交的信号

    1. 本身的数据变动会通过数据管道流向上级 VM 实例
    2. 内部(次要是下级)接管到信号后会做些后续解决

销毁时做些清理、善后的工作,如:移除子 VM 援用,勾销订阅等。

数据流转

在流动期间,数据在各层 VM 实例所连通的数据管道中流转时会发生变化,为了不便在不同场景下对数据进行解决,须要在初始化 VM 实例时将数据源进行备份,并生成几个拷贝:初始值(initial value)、默认值(default value)、原始值(data source)和以后值(current value)。

其中,初始值是获取到数据源那一刻的值,默认值在没有指定的状况下与初始值雷同,它们都是一经初始化就不会扭转的;以后值是本身一段时间内的数据变更,是最新的但不确定的值,能够了解为是一种草稿状态的值;原始值只有在下级以后值变动,接管到上级提交的数据或强制更新时才会更新,它是阶段性的确定值,能够看作是牢靠的数据。

「原始值」中的「原始」兴许会容易让人误会。在这里,它的含意是绝对于「以后值」来说,它是「原始」的,能够拿来作为参考的,而不是「最后的值」。表白「最后的值」的含意的是「初始值」。

原始值与以后值的区别与特点是:

  • 原始值是确定的,以后值是不确定的;
  • 原始值是纯的,以后值是脏的;
  • 通过「提交(commit)」操作对各级的原始值、以后值进行同步;
  • 以后值的「版本」始终不落后于原始值;
  • 有些场景下原始值与以后值始终雷同。

数据在流转时遵循以下几个准则:

  • 本身的原始值变动会引起本身的以后值以及子孙级的原始值和以后值变动,子孙能够定义摈弃变动的规定;
  • 本身的以后值变动在没提交时不会影响本身的原始值,会引起子孙级的原始值和以后值变动,子孙能够定义摈弃变动的规定;
  • 将本身的以后值提交到下级后,不会引起回流,兄弟 VM 实例也不会发生变化。

总的来说,只有在下级引发数据变动的状况下,才会产生上到下的数据流动。

各层级 VM 实例之间数据的传递过程大抵如下:

过滤器

在数据通过上下级 VM 实例之间所连通的数据管道,即数据的散发与汇总时,会通过一系列绝对独立的逻辑的解决,如:数据的裁剪、变形、校验等。每一段解决逻辑就是一个「过滤器」,每个过滤器都能够抛出异样终止后续的操作。

与视图的交互

每个 VM 实例都会提供一些供视图进行状态同步、数据联动等的接口:

interface IViewModel<ValueType> {
  // 获取原始值
  getDataSource(): ValueType;
  // 设置原始值
  setDataSource(value: ValueType): void;

  // 获取以后值
  getCurrentValue(): ValueType;
  // 设置以后值
  setCurrentValue(value: ValueType): void;

  // 监听以后值变动
  watch(handler: Function): Subscription;

  // 监听提交等事件
  on(handlers: {[key: string]: Function}): void;

  // 在散发数据的过滤器队列头部增加一个过滤器
  prependDispatchFilter(filter: Function): void;
  // 在散发数据的过滤器队列尾部增加一个过滤器
  appendDispatchFilter(filter: Function): void;

  // 在提交数据的过滤器队列头部增加一个过滤器
  prependCommitFilter(filter: Function): void;
  // 在提交数据的过滤器队列尾部增加一个过滤器
  appendCommitFilter(filter: Function): void;

  // 获取下级 VM 实例
  getParent(): IViewModel;
  // 获取上级 VM 实例
  getChildren(): IViewModel[];

  // 获取模型,返回值蕴含发申请的 API
  getModel(): IModel;

  // 执行动作,不指定 VM 实例的话应用以后 VM 实例
  call(action: IAction, vm?: IViewModel): Promise<void>;
}

动作

对于「动作」是什么,在之前的文章《我来聊聊配置驱动的视图开发》中曾经提及——

「动作」是一段残缺逻辑的形象,与函数相当,用来形容且只形容「做什么事」,不形容「长什么样」。一个可复用的动作应该是原子化的。

依据逻辑的定义、执行所在位置,能够分为客户端动作(狭义)与服务端动作:客户端动作(狭义)是定义并且执行在前端;服务端动作是定义并且执行在后端。

客户端动作(狭义)依据具体场景的用处及个性,又可分为以下几种动作:

  • 路由动作
  • CRUD 动作
  • 客户端动作(广义)
  • 组合动作

其中,路由动作的作用是进行页面跳转;CRUD 动作是对数据进行操作;客户端动作(广义)是单纯的一段逻辑,能够简略了解为是一个 JS 函数;组合动作用于将其余类型的动作「打包」解决,就像一个调用了其余函数的函数。

服务端动作能够简略粗犷地了解为是非常规 CRUD 的后端接口。

欧雷《我来聊聊配置驱动的视图开发》

除了客户端动作(广义)须要本人写逻辑之外,其余的都是齐全依据元数据执行。

路由动作是进行页面跳转的动作,这里的「页面」是狭义的,依据情景,能够了解为是浏览器窗口中的整个页面,也能够了解为是某个视图所在的宿主。在这个体系里,将视图跳转的动作称为「视图动作」,跳转到以后利用之外的页面的叫做「页面动作」。

既然组合动作是将其余类型的动作「打包」解决的动作,那么它就得具备调整被「打包」的动作的执行程序及如果某个动作执行失败要终止后续解决等的控制能力。实现形式能够参考 continuation 在 JS 中的实际利用。

视图

解析视图形容信息,并依据注入的 VM 实例所携带的数据进行渲染。

视图中能够本人发申请,但实践上只能发获取数据的申请,不能发批改数据的,批改数据须要通过 VM 实例或动作去解决。

视图这部分又细分为形容层、包装层和渲染层:

「形容层」即「DSL 层」,通过外部定义的 XML 标签集去形容一个界面中的 UI 元素、数据等信息,是一种相较于 JSON 来说更合乎直觉,更容易了解的界面配置。

包装层的作用是将形容层的标签转换为理论渲染的部件,渲染层则是具体的运行时环境。不像形容层那样绝对独立,包装层和形容层能够说是不能拆散的,包装层在将形容层的标签转换为理论渲染的部件时须要渲染层的撑持。

包装层的包装器与形容层的标签集里的标签能够说是一一对应的,标签通过包装器转换为部件集里的部件,但部件却不肯定与包装器一一对应,很可能一个包装器对应多个同类别的部件。

形容层

在 web 前端开发中,HTML 是一种 DSL,CSS 也是一种 DSL。在这个模型驱动的体系里,外部定义的用来形容一个界面中的 UI 元素、数据等信息的 XML 标签集就是 DSL。

形容层是运行时无关的,可能在任何平台及运行时库中运行。

日常工作交换中常会说到「模板」,这个词在不同语境中代表着不同的货色。在这个体系中,当在开发的语境里时,如果没带任何修饰词,应该就是指「一段形容界面配置的标签」,如:

<view widget="form">
  <group title="根本信息" widget="fieldset">
    <field name="name" label="姓名" widget="input" />
    <field name="gender" label="性别" widget="radio" />
    <field name="age" label="年龄" widget="number" />
    <field name="birthday" label="生日" widget="date-picker" />
  </group>
  <group title="宠物" widget="fieldset">
    <field name="dogs" label="🐶" widget="select" />
    <field name="cats" label="🐱" widget="select" />
  </group>
  <action ref="submit" text="提交" widget="button" />
  <action ref="reset" text="重置" widget="button" />
  <action ref="cancel" text="勾销" widget="button" />
</view>

模板如果不去解析,它就只是一段一般的文本,没有任何作用。

要对模板进行解析,得有一套对应模板上标签的标签集,还需有能将纯文本的模板借助标签集转换成 JS 对象的解析器。

标签集中的每个标签,也能够称为「元素」。思考到扩展性,须要有元素注册的机制,这有助于元素属性等的标准和治理。

在注册元素时,须要指定一些要害信息,如:元素名、标签名、属性描述符、行为。「属性描述符」次要是用来申明该元素所反对的属性及其值的类型;「行为」则用来告知该元素在解析后是作为父节点的子节点还是属性存在。

所有作为子节点存在的元素,根本都对应一个具体的部件。从表意上来说,这些元素分为两类:一类是较为形象的,另一类是较为具象的。较为形象的元素只有一个,它仅单纯地表白是「部件」这个含意,并没有更具体地体现出是干嘛的;其余的元素都是具象的,像 <view><field> 等,从命名就晓得是用于哪方面的。

所谓「节点」,就是将模板中的元素编译解析后所转换成的 JS 对象。整个模板会解析成一个树状构造的 JS 对象,也就是「节点树」。每个节点能够有一些办法,用来新增子节点、删除本身、获取或批改本身信息等。

包装层

包装层的作用是将形容层的产物,即节点,转换为部件。在 DSL 节点与部件之间起到桥梁作用的,就是「包装器」。

包装器外面会集了形容层所产出的一些信息,如:要生成到界面中的节点的属性及其对应部件的配置等。会依据节点所对应的元素所援用的部件的标识符去查找相应的部件,如果没指定援用则应用默认的,并将其余属性及相关联部件的配置作为部件的属性进行传递。

渲染层

简略来说,渲染层就是像 Vue、React、iOS、Android、微信小程序之类的库 / 框架、平台的运行时环境。进行理论渲染的组件、部件及作为桥梁的包装器都对其依赖,这就须要在每个运行环境下都得有一套包装器、组件和部件的封装。

思维总结

模型驱动架构正合乎我在《前端有架构吗?》所提到的架构设计的首要外围准则——以不变为核心。

在这个体系中,依据不同层、不同角色的设计指标,须要采纳适宜的编程范式,而不局限于一种。如:模型次要用 OOP,VM 应用 OOP 和 FRP,动作用到 FP。

正当且欠缺的模型驱动架构的设计与实现,可能很好地撑持企业业务的变动,疾速搭建新的利用。

数据处理相干的架构设计就到这里。

以上。


本文其余浏览地址:集体网站|微信公众号

正文完
 0