前言
最后一篇文章,我们编写了一个app来显示学生信息并将其重构为MVVM风格。
我没有说,但也许你已经注意到,我们在上一篇中已经做了一些看起来像MVVM框架的结尾。如果你忘记了上一篇文章或者你跳过了,别担心,这里是代码(并且我加了一些注释):
|
|
Hey,你在开玩笑?一个十行的框架?
框架是对如何组织代码和整个项目如何通用运作的抽象。
这并不意味着你应该有一堆代码或混乱的类,尽管企业可用的API列表经常都很可怕的长。但是如果你看一个框架仓库的核心文件夹,你将可能会发现它会出乎意料的小(相比于整个项目来说)。其核心代码包含主要工作进程,而其他部分,也许我们可以称他们为外设,帮助开发人员以更加舒适的方式构建应用程序。一个框架究竟会有多小的一个例子是cycle.js,只有124行(包含注释和空格)。我强烈的建议你去看看Andre André Staltz的描述Cycle.js的视频。视频展示了一个框架如何设计的整体过程。
抽象你的框架
通用性捕获
我们说一个框架提供了整个程序如何工作的通用过程。这种描述是不明确的,没有任何意义。如果我知道我的程序中什么是通用的,编程就会编程至少十倍的简单。框架是暗示开发人员可能是通用的,易于重用的东西。他们为某些特定类别的问题提供了不太糟糕的模板,这就是为什么我非常尊重那些建立广泛使用框架的人。他们解决了难得部分,而留给我们简单的方法。
那么我们的学生信息app怎么样呢?我们将其重构为mvvm风格,那么什么是通用的部分呢?幸运的是,我们已经知道了mvvm和它怎么工作的:
我们的app主要由四部分组成,框架应该把他们结合在一起。这定义了接口和维护数据流。它就DIY一个个人电脑。你有CPU、内存、硬盘和其他组成部分,并且你有主板和一些插槽。你的自定义代码就像这些组件,而框架就是主板。你只需要关心组件是否需要接口。至于他们如何组合在一起,谁在乎?主板会做的。从图中可以看出,我们的框架将形成一个数据流圈:
- 数据通过适配器从
Model
层开始,最后在View
层展示 - 用户交互从
View
开始,通过行为(actions
)最终改变Model
- 然后数据从修改过的
Model
开始,重复第一步。
实际上,框架因工作原理而异。它们在接口上共享一些功能,而不是如何实现他们。
细节:为View选择工具
这一节,我们将看到制作一个框架的一些细节。这里可能会有很多问题,但我们将重点关注我认为重要的几点。
我们将做出的折中主要是基于我自己的经验,可能不适合读者。我不是在说服你。这只是我的只是加上我的个人评价的一个技术展示。
首当其冲的难题就是View
的接口。这个问题影响开发人员的用户体验。如果一个UI框架无法提供UI创建的良好体验,那真是令人沮丧的。
Web开发中的创建View
使用最广泛的技术是使用模板DSL。很多著名的解决方案都采用了它,如Angular
和React
。在SPA受欢迎之前,模板已经被广泛使用了。我们知道的最好的语言:PHP,它刚开始就是为了从服务器端的模板生成html
而设计的。
模板主要由高可读性和可重用性而闻名。为了理解它,我们来回顾下之前文章中的代码。花十秒的时间来理解下面的代码片段:
|
|
现在时间到了,我敢打赌大部分人都会感到困惑。虽然它的代码风格并不差,但这并不是你或我的错。用JavaScript DOM API
描述HTML
片段有点…额…不太直接。我们首先阅读代码,然后在我们的大脑中人为的编译并运行以获取HTML
代码。之后,我们要人为的编译HTML
代码到网页中,以了解它实际将会生成什么。但是用模板,我们只要一步人为编译:HTML -> 网页
。
|
|
很明显,大部分人都可以在几秒钟内了解它。你甚至不需要知道我在使用什么语言(Razor for ASP.NET)。模板对于用户来说是非常酷的,但是对框架开发人员来说不是那么酷。使用模板DSL通常意味着你需要使用模板引擎运送你的框架。在大多数情况下,模板引擎将带来可承受但可怕的空格。例如,著名的模板DSL
,jade.js
,其最小格式多用了46kb大小。
框架专用模板引擎或编译器要小得多,但是它需要额外的精力去维护编译器。毕竟,我认为模板是最好的解决方案。但是我们不会在我们这个小型框架中使用它。我想要一些更加容易实现的,而可读性仍然可以接受的东西。
如果你听说过Elm.js,你可能已经注意到了它创建DOM视图的特殊方式:
|
|
以上展示了用Elm
如何创建一个Hello World
页面。Elm
借鉴了Haskell
的大部分语言规范。如果你不知道Haskell
,没关系,我将把它转换成JavaScript
:
|
|
它看上去有点点混乱,但是比原来版本的Document.createElement
等起来好得多,而且实现起来容易的多。span
, class
, text
都是JS
函数,你不需要知道编译器或解析器或者类似的东西。
这是个可以妥协接受的方案(可能不适合你)。
所以我将要介绍HyperScript和helper库,使用这些库,我们可以用下面的方法很轻松的创建列表视图:
|
|
虽然HyperScript
提供了很API,但我们可以简单地使用两个规则:
|
|
细节:如何重绘
使用HyperScript
的另一个原因是它提供更好的重绘支持。最早用于更新网页的解决方案是刷新。那时,重绘意味着从服务器上再获取一次数据。然后Ajax
到来了,使用它,我们可以控制页面的哪个部分应该被重绘。但是,管理DOM整个复杂的DOM树并不容易。开发商必须在运行效率和开发效率之间取得平衡。
拿我们的学生信息app作为例子。我们每当数据改变的时候就重绘整个树,很显然,这是没有必要的:无论模型如何变化,列表视图和label spans的结构都是保持不变的。对于一个hello-world级别的演示,你怎么做影响很小。但对于工业级别项目而言,性能是很重要的。数据web框架已经试图解决这个问题很多年了,通过解析模板并收集依赖关系,许多框架都能够精确的控制每个节点。在网络开发人员的先驱们所做的一切努力中。 Facebook引入的虚拟DOM是最广泛接受的解决方案。虚拟DOM背后的想法并不是新的。Java开发人员已经使用缓冲区来建立字符串数十年了。使用虚拟DOM:
- 我们在虚拟DOM中编辑,在“真实”树中不会有任何事情发生。
- 我们结束编辑后,与旧版本的虚拟DOM树做比较
- 修补差异
- 虚拟DOM在我们的补丁上做一些优化,并更新“真实”树;
所以无论我们编辑虚拟树多少次,我们只更新真实树一次。这相比$(selector).attr(name, value)
节约了很多性能。
|
|
上面的例子是hello-world级别的虚拟DOM API的例子,但是它对我们的小型框架够用了。
细节:什么时候重绘
换句话说,我怎么知道我需要重绘了呢?
在早起研究中,我们使用轮询。轮询容易实施,其性能对于我们的学生信息App来说也可以接受。每秒一个轮询可以适用于现在CPU,但是如果你想要视图更新的更频繁,或者你需要支持一些非常老的机器,在浏览器里面轮询可能不是一个好主意。幸运的是,OOP的先驱们面临同样的问题:
如何通知系统的一部分,另一部分的状态已经改变了?
时,已经发明了一些同等的技术。我们经常使用观察者模式,每个前端开发人员即使从未听过它,也都用过它。是的,当你编写node.addEventListener(myFunc)
时,你正在享受观察者模式的便利。而你每天都遭受的回调噩梦,也可以被看作是一种特殊的观察者模式。观察者的核心思想是将“WHEN”和“WHAT”分开。被观察者知道一些东西“什么时候”发生,而观察者(或者订阅者)知道“什么”会发生。谈论概念很容易,让我们来看看代码:
|
|
一旦将观察者添加到observable
的订阅列表中,当observable
通知更改时,将立即调用onNotify()
方法。正如你所看到的,除了调用函数不是硬编码,这与直接的函数调用没有区别。如果Model是可观察的,我们可以观察它并更新改变,这正是大多数框架如何处理的。Knockout.js
是第一代MVVM工具包之一,要使用Knockout
,你需要使你的数据成为一个Knockout
可观察的:
|
|
很多人可能会认为手动进行封装是可怕重复的工作,所以他们使用es5的Object.defineProperty
API来hack Model。让我们来看怎么去做:
|
|
Vue.js
使用此技术精简了观察过程,当创建一个Vue组件时,框架将自动hack data
和computed
字段。而Cycle.js
更激进,Cycle
的观察系统完全基于Rx.js
。让我们来看一些例子:
|
|
我从Cycle的主页拷贝了这段例子,你可以看到它与我们熟悉的框架有很大的不同,Cycle背后的哲学是非常有趣的,如果你访问他们的官网,你会了解到更多。对于我来说,让我们的小型框架变小,我会选择手工方式(这也是.NET WPF中使用的最早的解决方案)。它很容易实现,并为用户留下了更多的控制权。其代价是更多的样板代码。
最后的工作
让我们来看我们的框架会是什么样,它比我刚开始想的要更加简单:
|
|
加上注释,一共27行,很惊奇,是么?并且我写了一个小型的hello world的demo,它看上去很像你在Angular.js
官网主页上看到的那个。
|
|
你可以在这里下载源码和示例。尽情的玩吧。
一些话
你可能对我的实现感到一定不舒服,尤其是你可能不喜欢notify()
函数。
|
|
实际上,在这里递归是不必要的,你可以使用代理:
|
|
这一切都是关于个人喜好的。