前言
本文将会介绍 MVVM 和 Vue.js 的相关话题. 由于本人写了一些关于 Vue.js 封装的文章, 并且在工作中帮助同事封装了一些 KendoUI 的控件, 所以本人对 Vue.js 封装 JQuery 控件有一些自己的理解. 最近因公司的需要, 也在做WPF相关的东西. 虽然不是很精通WPF, 但是接触了两个领域的优秀 MVVM 框架, 感觉二者是有共通之处的, 遂分享之, Hope it helps!
本文没有代码, 只是单纯地讨论框架使用的问题.
本文将会讨论:
- 笔者眼中的 MVVM
- 命令驱动与状态驱动
- ERP系统中的现状
- 笔者眼中的组件设计哲学
- 如何使用 Vue.js 封装 JQuery 控件
- vue-table2 简介
笔者眼中的MVVM(部分摘自MVVM 维基百科)
MVVM可以让我们写代码时将 Model(Domain Model)
层的业务逻辑代码和界面代码分离. 界面可以用标记语言(MarkupLanguage)
来实现, 也可以用单纯的界面代码(GUI Code)
来实现.
ViewModel 用于转换来自Model层的数据, 目的是让数据更容易地转化.
MVVM 是 Martin Fowler 提出的 Presentation Model 模式的一种. MVVM 抽象了界面的状态和行为, 而Presentation Model 是视图View
的抽象, 从而降低 View 与具体的UI框架的耦合性.
MVVM 和 Presentation Model 均派生自 MVC 模式.
Model-View-ViewModelパターン概念図 来源: 维基百科那么在 MVVM 中, 每一部分都是做什么的呢?
模型Model
Model, 在这里指的是领域模型domain model或者数据访问层data access layer. 这两个东西里面存放着系统中的业务逻辑和具体的数据.
视图View
和 MVC 中的 视图是一样的, 该层用于描述用户在屏幕上看到的东西(用户界面)
ViewModel
ViewModel 是 View 层的抽象. ViewModel 需要暴露自己的属性和命令Command
给 View 层.
Binder
数据绑定和命令绑定在 MVVM 模式里面比较含蓄.在微软的解决方案中, Binder 指的是 XAML(WPF). 数据绑定可以让开发者很容易地将 ViewModel 中的数据同步到 View 中. When implemented outside of the Microsoft stack the presence of a declarative databinding technology is a key enabler of the pattern.寻求翻译帮助:key enabler of the pattern 是个啥?
想必这篇文章的读者也对 MVVM 有一定的了解. 但是笔者认为, 每个人对 MVVM 的理解不尽相同, 然而, 根据 SOLID原则 来看, 如果项目中使用了 MVVM 模式, 笔者认为应该有以下约束. 这些约束是已经经过现实中妥协的.
- ViewModel 不应该了解 View 中的任何细节
- ViewModel 需要通过其他的 ViewModel 才能与其他的 View 进行通信
- ViewModel 可以了解其他ViewModel 的细节. (根据需要)
- View 可以了解 ViewModel 中的细节(要不然怎么进行Data和CommandBinding呢).
- Model 不应该了解 ViewModel 中的细节
- 各个 MVVM 框架都提供组件功能. 请使用该功能, 以降低系统的复杂度.
- 在使用组件功能的时候, 请保证, 子组件不能了解父组件的任何内容, 包括 ViewModel. 如果你真的要这么做, 说明你的软件架构出现了问题.
- 如果标记语言(XAML 或 HTML + CSS)无法表现当前界面的显示需求(动画等), 那么可以使用CodeBehind(WPF) 或者 Transition Effects(Vue.js) 来处理. 但是注意, 如果要写代码, 请只专注于界面代码
(按钮从左上角飞到右下角)
, 请不要在 View 的代码中写业务逻辑(按钮在**什么时候**从左上角飞到右下角)
.
命令驱动与状态驱动
笔者并不推荐总是践行状态驱动.
上文提到, 如果有些情况UI使用单纯的标记语言和样式表搞不定的话, 可以使用一些代码进行辅助. 包括按钮飞入, 图片下滑, 控制窗口开关等等. 这并不是在破坏MVVM中的状态驱动.
状态驱动并不是用很笨拙的方式在 ViewModel 中使用 data 或者 属性 对View进行操作. 请适当妥协
以控制窗口开关为例, 在一些公司中, 我们完全可以使用状态驱动思维写一个 Modal 窗口, 并且在外层使用状态(true 或者 false)
来控制它. 这样做是合理的, 我也推荐这样做.
但是, 有时候 Modal 窗口的实现本身并不是状态驱动的 (比如 KendoUI 中的 KendoWindow)
, 这个时候如果使用状态模式封装 Modal 窗口, 由于在封装的过程中需要使用很多的Watch, 然后需要在 Watch 中进行判断, 写起来很不友好.
那么这个时候我们倒还不如拿到窗口 ViewModel 的一个实例, 使用命令模式调用窗口组件中的方法, 控制其开关. 这样也很简单, 与状态模式并不冲突.
不合理地使用这一方式会导致程序对 MVVM 的支持不好. 因为在这种情况下, 相当于将基础组件的 "命令式思维" 传染到了其他组件之上, 所以要三思而后行.
另外, 在封装 JQuery 等老旧组件之后, 请立即切换到状态驱动思维.
ERP(类)系统中的现状
一把梭!目前中国国内有一个很奇怪的现象: 开源世界有很多优秀的前端框架, MVVM 框架也好, 工具性的框架也好, 然而, 目前国内普通的 ERP 公司似乎对这些东西并不感兴趣. 更有意思的是, 这些公司的业务逻辑往往比较复杂 (复杂表单, 复杂报表, 动态计算, 复杂数据的构造), 这个时候使用 前端的 MVVM 框架可以很好地解决一些问题.
你可能会有疑问: JSP 或者 Razor 已经足够我使用了, 你看, 复杂表单, 复杂报表, 动态计算, 复杂的数据构造, 我们直接把数据放到 ViewData (Razor) 或 Attribute (JSP) 中, 然后直接渲染到页面上不就完了吗?
这显然是有道理的, 并且我也建议, 如果后端的模板引擎对于你的业务来说足够用了, 并不推荐你强行使用前端 MVVM, 因为这样会增加系统的复杂度, 也会引入额外的学习成本. 但是如果你觉得使用 JQuery和模板渲染已经无法处理你目前的需求的时候, 或者处理起来比较不舒服的时候, 你可能就需要在前端引入MVVM了.
我这里有一个例子:
假如你现在有一个给人分配岗位的表单, 一个人可以有若干个岗位
- 你需要点击添加按钮, 给这个人添加一个岗位
- 表单中将会新增加一行, 用于填写岗位信息
- 填写岗位信息
- 如果还需要为这个人添加岗位, 再次点击添加按钮
- 点击 "提交" 按钮
这是一个简单的需求. 如果你使用 JQuery 实现, 你可能会这么做
点击新增按钮, 在表单的最后面追加一段HTML.
点击提交的时候, 将所有的这些 HTML 用选择器选出来, 然后把所有的值拿出来, 拼到数组中.
提交数组
实现以上逻辑时, 你需要兼顾数据与 DOM 元素. 在你的JavaScript 代码中也充满了对 DOM 的操作.
假设DOM结构变了, 你要选择的东西也会随之改变. 这个时候, 就需要修改你的 JavaScript 代码了.
我承认, 这并不是一个好的解决方案. 如果你有比较好的解决方案, 你可以贴在下面.
但是如果使用 MVVM 来做这个需求呢?
点击按钮, 在数组中添加一个职位
于是, 页面自动生成了给你编辑职位的地方(使用ListView (WPF) 或者 v-for (vue.js) 来渲染前端页面)
编辑职位, 编辑的同时, 数组里面的东西也在跟着界面变化你不用操心任何关于数据的事情.
点击提交时, 你可以直接把前端的数据提交到后端, 由后端处理. 或者提交的时候, 在前端把数据处理一下, 送到后端去.
如果 DOM 变了, 没关系, 把 Model 重新绑定一下就行, 就在 HTML 里面写. 不用碰 JavaScript 代码
这是两个不同的编程思维. 并没有谁好谁坏.
但是你可以明显地看出来, 在修改逻辑的时候, MVVM 可以让你更专注于你的 JavaScript 代码
当修改界面的时候, MVVM 可以让你更专注于你的界面.
很酷吧.
正如你所看到的, MVVM 模式很适合处理复杂的表单. 但是对 DOM 操作不是很友好. 然而, 在企业应用中, 多数情况下, DOM 在生成之后样式都不会再改了.
笔者眼中的组件设计哲学 (Vue.js为例)
上节提到, 如果你要使用MVVM, 那么一定要使用组件化的东西. 比如 UserControl(WPF), 和 Components (Vue.js), 以降低系统的复杂度.
如果你要在你的系统中使用组件, 你需要有一双 "组件化" 的眼睛. 你可以参考 BEM 来学习如何进行组件化思考.
这里有一个图, 来自 BEM 的 key-concepts.
要有一双组件化的眼睛笔者认为, 组件化有时候是需要权衡的, 尤其是复杂表单的组件化. 和面向对象设计一样, 一定要等必要的时候再抽取组件. 当然, 是否抽取组件时取决于开发者的. 只要你觉得这样做有助于提升代码的可维护性, 并且确实是这样的, 那无所谓啦.
所以笔者认为, 系统中好的组件结构应该符合以下特点:
- 根据需要, 父组件可以访问子组件的方法和数据
- 子组件不可以主动访问父组件的任何成员. 只能让父组件调用子组件的方法或者把数据Bind 到子组件上.
- 子组件不能依赖任何全局变量和组件之外的信息, 比如用户登录信息, 当前计算机的信息等等. 否则会增加组件的耦合度.
- 表单的字表单通常不应该包含提交操作
- 永远坚持单一责任原则
上面这几条观点都是我硬生生地从脑海中抽取出来的。可能不太对,但是希望上面这几条能让读者明白,组件化并不是随随便便就组件化的。它也得有一定的规律,也有合理和不合理之分,就像面向对象程序设计那样。
当然,以上所有的条目都是相对的。如果你封装的是一个比较通用的组件,那么你最好严格遵循以上条目。随着组件所包含的业务逻辑越来越多,它本身所实现的功能也就越多,那么以上的条目可能就不能生搬硬套了
如何使用 Vue.js 封装 JQuery 组件
我有一个同事,给我抱怨说:
Vue.js 好难用啊,完全无法和 JQuery 搭配使用
Vue.js 是一个渐进式的框架,然而它代表着一个截然不同的编程思维。我们通常的思维是: "操作 DOM 元素做这个再做那个", 但是对于 Vue.js 来说(或者MVVM来说), 编程思维就应该变为 "这个数据应该绑定到哪里哪里, 我改变这个数据之后, 界面就会变成 XXX...".
有时候我们也可以在一些复杂的需求上单独使用 Vue.js. 如果这对你来说样刚好合适, 那无所谓, 你直接这么用就可以了.
目前多数企业使用的是 JQuery. 要么是 EasyUI, 要么是 KendoUI 等 JQuery 或者说命令式相关的框架. 如果这时候需要转型到 Vue.js 的话, 势必会面临着二者的兼容性问题. 如果要避免兼容性问题, 笔者认为最好的解决方案就是:
将 JQuery 控件包装成 Vue.js 控件, 或者替换成 本身就是使用 Vue.js 开发的控件(请在这里找, Awesome vue). 基础控件开发或选型完毕后, 之后的编码全部用 Vue.js.
在讲包装组件之前, 先列举一下 Vue.js 的组件系统都为我们提供了什么样的操作.
Vue.js 中的组件操作
- 子组件向父组件中传值: 使用
event
(输出过程)
- 父组件向子组件中传值: 使用
props binding
(输入过程) - 父组件调用子组件的方法: 用
ref
拿到子组件, 然后调用里面的方法 (方法调用) - 子组件调用父组件的方法: 使用
event
(方法调用) - 使用
v-model
简化
JQuery 控件都会给我们什么样的操作呢?
对于输入型的 JQuery 控件通常会给我们的操作
1.控件的初始化
- 初始化的过程中要传入对控件的配置
- 更新控件的设置
- 设置控件的值
- 获取控件的值
那么当我们包装一个 JQuery 控件的时候, 可以将 Vue.js 中组件的操作, 映射到 JQuery 控件的各种操作. 由于我们需要在 Vue.js 加载完毕之后初始化我们的 JQuery 控件, 所以我先在下面把 Vue 的生命周期图放了出来, 以供参考.
[vue.js lifecycle diagram](https://vuejs.org/v2/guide/instance.html#Lifecycle-Diagram)组件封装的具体方法
初始化
使用props
传递控件初始化相关的信息, 然后在mounted
中将this.$el
初始化成 JQuery 控件.
注意
控件初始化相关的信息不能包含 JQuery 控件的事件等信息. 比如:
<select id="multiselect" multiple="multiple">
<option>Item1</option>
<option>Item2</option>
</select>
<script>
$("#multiselect").kendoMultiSelect({
close: function(e) {
// handle the event
}
});
</script>
如果你要封装这个控件, 可能就需要在close
事件绑定的方法, 不应该是从外界传过来的, 而应该是在里面使用 vue.js 的emit
方法, 将事件发射出来的.
使用v-model
绑定数据
vue 的官方文档 提到:
<input v-model="something">```
只是下面这个东西的语法糖
<input
v-bind:value="something"
v-on:input="something = $event.target.value">```
那么我们就可以这样做:
- 接受一个名为
value
的prop
- 当用户输入发生变化(选择变化)的时候, 在组件内部发出
input
事件 - 记得在事件中带上新的值.
临时修改组件的属性
- 你可以使用ref功能拿到子组件. 在子组件中, 你可以新建一些方法, 然后在外面使用 refs 调用里面的方法. 这种方式只适用于属性修改不频繁的时候
- 你可以使用watch监控可能会变化的值. 一旦检测到变化, 立即将变化后的值设置到控件中.
比如:
需求:
现在需要一组日期控件 startTimePicker 和 endTimePicker. 要求endTimePicker的日期要比startTimePicker的日期晚或者相等.
现在我们有一个JQuery控件, 叫DateTimePicker, 这个组件有一个属性叫 startTime, 用于限制当前可选择的最小时间.
解决方案:
- 使用
v-model
封装 dateTimePicker 组件- 新建一个prop, 用于接收startTime, 将 startTimePicker 的值通过 startTime 传入 endTimePicker
- 使用
watch
监控 startTime , 只要一有更新就马上将 endTimePicker 的 startTime 更新为新的 startTime.
vue-table2简述
vue-table2 是一个非常适用于企业应用的表格类组件. 它的前身是 vue-table.
vue-table这是一个优秀的表格组件, 其内部完全使用组件来搭建, 支持子列表(展开行), 可以考虑代替 kendoUI 的 kendoGrid.
网友评论