MVC 的辉煌过去与现存问题
创新互联是一家集网站建设,静海企业网站建设,静海品牌网站建设,网站定制,静海网站建设报价,网络营销,网络优化,静海网站推广为一体的创新建站企业,帮助传统企业提升企业形象加强企业竞争力。可充分满足这一群体相比中小企业更为丰富、高端、多元的互联网需求。同时我们时刻保持专业、时尚、前沿,时刻以成就客户成长自我,坚持不断学习、思考、沉淀、净化自己,让我们为更多的企业打造出实用型网站。
在每个用户界面背后,我们都在使用 MVC 模式,也就是模型-视图-控制器(Model-View-Controller)。MVC
发明的时候,Web 尚不存在,当时的软件架构充其量是胖客户端在原始网络中直接与单一数据库会话。但是,几十年之后,MVC 依然在使用,持续地用于
OmniChannel 应用的构建。
Angular 2 正式版即将发布,在这个时间节点重估 MVC 模式及各种 MVC 框架为应用架构带来的贡献意义重大。
我第一次接触到 MVC 是在 1990 年,当时 NeXT 刚刚发布 Interface
Builder(让人惊讶的是,如今这款软件依然发挥着重大的作用)。当时,我们感觉 Interface Builder 和 MVC
是一个很大的进步。在 90 年代末期,MVC 模式用到了 HTTP 上的任务中(还记得 Struts 吗?),如今,就各个方面来讲,MVC
是所有应用架构的基本原则。
MVC 的影响十分深远,以致于 React.js 在介绍他们的框架时都委婉地与其划清界限:“React 实现的只是 MVC 中视图(View)的部分”。
当我去年开始使用 React 的时候,我感觉它在某些地方有着明显的不同:你在某个地方修改一部分数据,不需要显式地与 View 和
Model 进行交互,整个 UI 就能瞬间发生变化(不仅仅是域和表格中的值)。这也就是说,我很快就对 React
的编程模型感到了失望,在这方面,我显然并不孤独。我分享一下 Andre Medeiros 的观点:
React 在很多方面都让我感到失望,它主要是通过设计不佳的 API 来引导程序员[…]将多项关注点混合到一个组件之中。
作为服务端的 API 设计者,我的结论是没有特别好的方式将 API 调用组织到 React 前端中,这恰恰是因为 React 只关注 View,在它的编程模型中根本不存在控制器。
到目前为止,Facebook 一直致力于在框架层面弥合这一空白。React 团队起初引入了 Flux 模式,不过它依然令人失望,最近 Dan Abramov 又提倡另外一种模式,名为 Redux,在一定程度上来讲,它的方向是正确的,但是在将 API 关联到前端方面,依然比不上我下面所介绍的方案。
Google 发布过 GWT、Android SDK 还有 Angular,你可能认为他们的工程师熟知何为最好的前端架构,但是当你阅读
Angular 2 设计考量的文章时,便会不以为然,即便在 Google 大家也达成这样的共识,他们是这样评价之前的工作成果的:
Angular 1
并不是基于组件的理念构建的。相反,我们需要将控制器与页面上各种[元素]进行关联(attach),其中包含了我们的自定义逻辑。根据我们自定义的指令
如何对其进行封装(是否包含 isolate scope?),scope 会进行关联或继续往下传递。
基于组件的 Angular 2 看起来能简单一点吗?其实并没有好多少。Angular 2 的核心包本身就包含了 180 个语义(Semantics),整个框架的语义已经接近 500 个,这是基于 HTML5 和 CSS3 的。谁有那么多时间学习和掌握这样的框架来构建 Web 应用呢?当 Angular 3 出现的时候,情况又该是什么样子呢?
在使用过 React 并了解了 Angular 2 将会是什么样子之后,我感到有些沮丧:这些框架都系统性地强制我使用
BFF“页面可替换模式(Screen Scraping)”模式,按照这种模式,每个服务端的 API
要匹配页面上的数据集,不管是输入的还是输出的。
弃用 MVC 之后怎么走?
此时,我决定“让这一切见鬼去吧”。我构建了一个 Web 应用,没有使用 React、没有使用 Angular 也没有使用任何其他的 MVC 框架,通过这种方式,我看一下是否能够找到一种在 View 和底层 API 之间进行更好协作的方式。
就 React 来讲,我最喜欢的一点在于 Model 和 View 之间的关联关系。React 不是基于模板的,View 本身没有办法请求数据(我们只能将数据传递给 View),看起来,针对这一点进行探索是一个很好的方向。
如果看得足够长远的话,你会发现 React 唯一的目的就是将 View 分解为一系列(纯粹的)函数和 JSX 语法:
V params={M}/
它实际上与下面的格式并没有什么差别:
V = f ( M )
例如,我当前正在从事项目的 Web 站点, Gliiph,就是使用这种函数构建的:
图1:用于生成站点 Slider 组件 HTML 的函数
这个函数需要使用 Model 来填充数据:
图2:支撑 slider 的 Model
如果用简单的 JavaScript 函数就能完成任务,我们为什么还要用 React 呢?
虚拟 DOM(virtual-dom)?如果你觉得需要这样一种方案的话(我并不确定有很多的人需要这样),其实有这样的可选方案,我也期望开发出更多的方案。
GraphQL?并不完全如此。不要因为 Facebook
大量使用它就对其产生误解,认为它一定是对你有好处的。GraphQL 仅仅是以声明的方式来创建视图模型。强制要求 Model 匹配 View
会给你带来麻烦,而不是解决方案。React 团队可能会觉得使用“客户端指定查询(Client-specified
queries)”是没有问题的(就像反应型团队中那样):
GraphQL 完全是由 View 以及编写它们的前端工程师的需求所驱动的。[…]另一方面,GraphQL 查询会精确返回客户端请求的内容,除此之外,也就没什么了。
GraphQL 团队没有关注到 JSX 语法背后的核心思想:用函数将 Model 与 View 分离。与模板和“前端工程师所编写的查询”不同,函数不需要 Model 来适配 View。
当 View 是由函数创建的时候(而不是由模板或查询所创建),我们就可以按需转换 Model,使其按照最合适的形式来展现 View,不必在 Model 的形式上添加人为的限制。
例如,如果 View 要展现一个值v,有一个图形化的指示器会标明这个值是优秀、良好还是很差,我们没有理由将指示器的值放到 Model 中:函数应该根据 Model 所提供的v值,来进行简单的计算,从而确定指示器的值。
现在,把这些计算直接嵌入到 View 中并不是什么好主意,使 View-Model 成为一个纯函数也并非难事,因此当我们需要明确的 View-Model 时,就没有特殊的理由再使用 GraphQL 了:
V = f ( vm (M) )
作为深谙 MDE 之道的人,我相信你更善于编写代码,而不是元数据,不管它是模板还是像 GraphQL 这样的复杂查询语言。
这个函数式的方式能够带来多项好处。首先,与 React 类似,它允许我们将 View 分解为组件。它们创建的较为自然的界面允许我们为 Web 应用或 Web 站点设置“主题”,或者使用不同的技术来渲染 View(如原生的方式)。函数实现还有可能增强我们实现反应型设计的方式。
在接下来的几个月中,可能会出现开发者交付用 JavaScript 函数包装的基于组件的 HTML5 主题的情况。这也是最近这段时间,在我的
Web 站点项目中,我所采用的方式,我会得到一个模板,然后迅速地将其封装为 JavaScript 函数。我不再使用
WordPress。基本上花同等的工夫(甚至更少),我就能实现 HTML5 和 CSS 的最佳效果。
这种方式也需要在设计师和开发人员之间建立一种新型的关系。任何人都可以编写这些 JavaScript 函数,尤其是模板的设计人员。人们不需要学习绑定方法、JSX 和 Angular 模板的语法,只掌握简单的 JavaScript 核心函数就足以让这一切运转起来。
有意思的是,从反应型流程的角度来说,这些函数可以部署在最合适的地方:在服务端或在客户端均可。
但最为重要的是,这种方式允许在 View 与 Model 之间建立最小的契约关系,让 Model 来决定如何以最好的方式将其数据传递给 View。让 Model 去处理诸如缓存、懒加载、编配以及一致性的问题。与模板和 GraphQL 不同,这种方式不需要从 View 的角度来直接发送请求。
既然我们有了一种方式将 Model 与 View 进行解耦,那么下一个问题就是:在这里该如何创建完整的应用模型呢?“控制器”该是什么样子的?为了回答这个问题,让我们重新回到 MVC 上来。
苹果公司了解 MVC 的基本情况,因为他们在上世纪 80 年代初,从 Xerox PARC“偷来了”这一模式,从那时起,他们就坚定地实现这一模式:
图3:MVC 模式
Andre Medeiros 曾经清晰地指出,这里核心的缺点在于, MVC
模式是“交互式的(interactive)”(这与反应型截然不同)。在传统的 MVC 之中,Action(Controller)将会调用
Model 上的更新方法,在成功(或出错)之时会确定如何更新
View。他指出,其实并非必须如此,这里还有另外一种有效的、反应型的处理方式,我们只需这样考虑,Action 只应该将值传递给
Model,不管输出是什么,也不必确定 Model 该如何进行更新。
那核心问题就变成了:该如何将 Action 集成到反应型流程中呢?如果你想理解 Action 的基础知识的话,那么你应该看一下
TLA+。TLA 代表的是“Action 中的逻辑时序(Temporal Logic of Actions)”,这是由 Dr. Lamport
所提出的学说,他也因此获得了图灵奖。在 TLA+ 中,Action 是纯函数:
data’ = A (data)
我真的非常喜欢 TLA+ 这个很棒的理念,因为它强制函数只转换给定的数据集。
按照这种形式,反应型 MVC 看起来可能就会如下所示:
V = f ( M.present ( A (data) ) )
这个表达式规定当 Action 触发的时候,它会根据一组输入(例如用户输入)计算一个数据集,这个数据是提交到 Model
中的,然后会确定是否需要以及如何对其自身进行更新。当更新完成后,View 会根据新的 Model 状态进行更新。反应型的环就闭合了。Model
持久化和获取其数据的方式是与反应型流程无关的,所以,它理所应当地“不应该由前端工程师来编写”。不必因此而感到歉意。
再次强调,Action 是纯函数,没有状态和其他的副作用(例如,对于 Model,不会包含计数的日志)。
反应型 MVC 模式很有意思,因为除了 Model 以外,所有的事情都是纯函数。公平来讲,Redux 实现了这种特殊的模式,但是带有
React 不必要的形式,并且在 reducer 中,Model 和 Action 之间存在一点不必要的耦合。Action
和接口之间是纯粹的消息传递。
这也就是说,反应型 MVC 并不完整,按照 Dan 喜欢的说法,它并没有扩展到现实的应用之中。让我们通过一个简单的样例来阐述这是为什么。
假设我们需要实现一个应用来控制火箭的发射:一旦我们开始倒计时,系统将会递减计数器(counter),当它到达零的时候,会将 Model 中所有未定的状态设置为规定值,火箭的发射将会进行初始化。
这个应用有一个简单的状态机:
图4:火箭发射的状态机
其中 decrement 和 launch 都是“自动”的 Action,这意味着我们每次进入(或重新进入)counting
状态时,将会保证进行转换的评估,如果计数器的值大于零的话,decrement Action 将会继续调用,如果值为零的话,将会调用
launchAction。在任何的时间点都可以触发 abort Action,这样的话,控制系统将会转换到 aborted 状态。
在 MVC 中,这种类型的逻辑将会在控制器中实现,并且可能会由 View 中的一个计时器来触发。
这一段至关重要,所以请仔细阅读。我们已经看到,在 TLA+ 中,Action 没有副作用,只是计算结果的状态,Model 处理
Action 的输出并对其自身进行更新。这是与传统状态机语义的基本区别,在传统的状态机中,Action
会指定结果状态,也就是说,结果状态是独立于 Model 的。
在 TLA+ 中,所启用的 Action 能够在状态表述(也就是 View)中进行触发,这些 Action 不会直接与触发状态转换的行为进行关联。换
句话说,状态机不应该由连接两个状态的元组(S1, A, S2)来进行指定,传统的状态机是这样做的,它们元组的形式应该是(Sk, Ak1,
Ak2,…),这指定了所有启用的 Action,并给定了一个状态 Sk,Action 应用于系统之后,将会计算出结果状态,Model
将会处理更新。
当我们引入“state”对象时,TLA+ 提供了一种更优秀的方式来对系统进行概念化,它将 Action 和 view(仅仅是一种状态的表述)进行了分离。
我们样例中的 Model 如下所示:
model = {
counter: ,
started: ,
aborted: ,
launched:
}
系统中四个(控制)状态分别对应于 Model 中如下的值:
ready = {counter: 10, started: false, aborted: false, launched: false }
counting = {counter: [0..10], started: true, aborted: false, launched: false }
launched = {counter: 0, started: true, aborted: false, launched: true}
aborted = {counter: [0..10], started: true, aborted: true, launched: false}
这个 Model 是由系统的所有属性及其可能的值所指定的,状态则指定了所启用的 Action,它会给定一组值。这种类型的业务逻辑必须要在某个地方进行实现。我们不能指望用户能够知道哪个 Action 是否可行。在这方面,没有其他的方式。不过,这种类型的业务逻辑很难编写、调试和维护,在没有语义对其进行描述时,更是如此,比如在 MVC 中就是这样。
让我们为火箭发射的样例编写一些代码。从 TLA+ 角度来讲,next-action
断言在逻辑上会跟在状态渲染之后。当前状态呈现之后,下一步就是执行 next-action 断言,如果存在的话,将会计算并执行下一个
Action,这个 Action 会将其数据交给 Model,Model 将会初始化新状态的表述,以此类推。
图5:火箭发射器的实现
需要注意的是,在客户端/服务器架构下,当自动 Action 触发之后,我们可能需要使用像 WebSocket 这样的协议(或者在 WebSocket 不可用的时候,使用轮询机制)来正确地渲染状态表述。
我曾经使用 Java 和 JavaScript 编写过一个很轻量级的开源库,它使用 TLA+
特有的语义来构造状态对象,并提供了样例,这些样例使用
WebSocket、轮询和队列实现浏览器/服务器交互。在火箭发射器的样例中可以看到,我们并非必须要使用那个库。一旦理解了如何编写,状态实现的编码
相对来讲是很容易的。
新模式——SAM 模式
对于要引入的新模式来说,我相信我们已经具备了所有的元素,这个新模式作为 MVC 的替代者,名为 SAM 模式(状态-行为-模型,State-Action-Model),它具有反应型和函数式的特性,灵感来源于 React.js 和 TLA+。
SAM 模式可以通过如下的表达式来进行描述:
V = S ( vm ( M.present ( A (data) ) ), nap (M))
它表明在应用一个 Action A 之后,View V 可以计算得出,Action 会作为 Model 的纯函数。
在 SAM 中,A(Action)、vm(视图-模型,view-model)、nap(next-action
断言)以及S(状态表述)必须都是纯函数。在 SAM 中,我们通常所说的“状态”(系统中属性的值)要完全局限于 Model
之中,改变这些值的逻辑在 Model 本身之外是不可见的。
随便提一下,next-action 断言,即 nap ()是一个回调,它会在状态表述创建完成,并渲染给用户时调用。
图7:“修改地址”的实现
模式中的元素,包括 Action 和 Model,可以进行自由地组合:
函数组合
data’ = A (B(data))
端组合(Peer)(相同的数据集可以提交给两个 Model)
M1.present (data’)
M2.present (data’)
父子组合(父 Model 控制的数据集提交给子 Model)
M1.present (data’,M2)
function present (data, child) {
// 执行更新
…
// 同步 Model
child.present (c(data))
}
发布/订阅组合
M1.on (“topic”, present )
M2.on (“topic”, present )
或
M1.on (“data”, present )
M2.on (“data”, present )
有些架构师可能会考虑到 System of Record 和 Systems of Engagement,这种模式有助于明确这两层的接口(图8),Model 会负责与 systems of record 的交互。
图8:SAM 组合模型
整个模式本身也是可以进行组合的,我们可以实现运行在浏览器中的 SAM 实例,使其支持类似于向导(wizard)的行为(如 ToDo 应用),它会与服务器端的 SAM 进行交互:
图9:SAM 实例组合
请注意,里层的 SAM 实例是作为状态表述的一部分进行传送的,这个状态表述是由外层的实例所生成的。
会话检查应该在 Action 触发之前进行(图 10)。SAM 能够启用一项很有意思的组合,在将数据提交给 Model 之前,View
可以调用一个第三方的 Action,并且要为其提供一个 token 和指向系统 Action 的回调,这个第三方 Action
会进行授权并校验该调用的合法性。
图 10:借助 SAM 实现会话管理
从 CQRS
的角度来讲,这个模式没有对查询(Query)和命令(Command)做特殊的区分,但是底层的实现需要进行这种区分。搜索或查询“Action”只是
简单地传递一组参数到 Model 中。我们可以采用某种约定(如下划线前缀)来区分查询和命令,或者我们可以在 Model 上使用两个不同的
present 方法:
{ _name : ‘/^[a]$/i’ } // 名字以A或a开头
{ _customerId: ‘123’ } // id=123 的 customer
Model 将会执行必要的操作以匹配查询,更新其内容并触发 View 的渲染。类似的约定可以用于创建、更新或删除 Model
中的元素。在将 Action 的输出传递给 Model
方面,我们可以实现多种方式(数据集、事件、Action……)。每种方式都会有其优势和不足,最终这取决于个人偏好。我更喜欢数据集的方式。
在异常方面,与 React 类似,我们预期 Model 会以属性值的形式保存异常信息(这些属性值可能是由 Action 提交的,也可能是 CRUD 操作返回的)。在渲染状态表述的时候,会用到属性值,以展现异常信息。
在缓存方面,SAM 在状态表述层提供了缓存的选项。直观上来看,缓存这些状态表述函数的结果能够实现更高的命中率,因为我们现在是在组件/状态层触发缓存,而不是在 Action/响应层。
该模式的反应型和函数式结构使得功能重放(replay)和单元测试变得非常容易。
SAM 模式完全改变了前端架构的范式,因为根据 TLA+ 的基础理念,业务逻辑可以清晰地描述为:
Action 是纯函数
CRUD 操作放在 Model 中
状态控制自动化的 Action
作为 API 的设计者,从我的角度来讲,这种模式将 API 设计的责任推到了服务器端,在 View 和 Model 之间保持了最小的契约。
Action 作为纯函数,能够跨 Model 重用,只要某个 Model 能够接受 Action 所对应的输出即可。我们可以期望 Action 库、主题(状态表述)甚至 Model 能够繁荣发展起来,因为它们现在能够独立地进行组合。
借助 SAM 模式,微服务能够非常自然地支撑 Model。像 Hivepod.io 这样的框架能够插入进来,就像它本来就在这层似得。
最为重要的是,这种模式像 React 一样,不需要任何的数据绑定或模板。
随着时间的推移,我希望能够推动浏览器永久添加虚拟 DOM 的特性,新的状态表述能够通过专有 API 直接进行处理。
我发现这个旅程将会带来一定的革新性:在过去的几十年中,面向对象似乎无处不在,但它已经一去不返了。我现在只能按照反应型和函数式来进行思考。我
借助 SAM 所构建的东西及其构建速度都是前所未有的。另外,我能够关注于 API 和服务的设计,它们不再遵循由前端决定的模式。
检查页面是否有相关的js控制了代码,或者使用了图片的响应式或者懒加载,通过审查元素和查看源代码进行辨别。
MVC 的辉煌过去与现存问题
在每个用户界面背后,我们都在使用 MVC 模式,也就是模型-视图-控制器(Model-View-Controller)。MVC
发明的时候,Web 尚不存在,当时的软件架构充其量是胖客户端在原始网络中直接与单一数据库会话。但是,几十年之后,MVC 依然在使用,持续地用于
OmniChannel 应用的构建。
Angular 2 正式版即将发布,在这个时间节点重估 MVC 模式及各种 MVC 框架为应用架构带来的贡献意义重大。
我第一次接触到 MVC 是在 1990 年,当时 NeXT 刚刚发布 Interface
Builder(让人惊讶的是,如今这款软件依然发挥着重大的作用)。当时,我们感觉 Interface Builder 和 MVC
是一个很大的进步。在 90 年代末期,MVC 模式用到了 HTTP 上的任务中(还记得 Struts 吗?),如今,就各个方面来讲,MVC
是所有应用架构的基本原则。
MVC 的影响十分深远,以致于 React.js 在介绍他们的框架时都委婉地与其划清界限:“React 实现的只是 MVC 中视图(View)的部分”。
当我去年开始使用 React 的时候,我感觉它在某些地方有着明显的不同:你在某个地方修改一部分数据,不需要显式地与 View 和
Model 进行交互,整个 UI 就能瞬间发生变化(不仅仅是域和表格中的值)。这也就是说,我很快就对 React
的编程模型感到了失望,在这方面,我显然并不孤独。我分享一下 Andre Medeiros 的观点:
React 在很多方面都让我感到失望,它主要是通过设计不佳的 API 来引导程序员[…]将多项关注点混合到一个组件之中。
作为服务端的 API 设计者,我的结论是没有特别好的方式将 API 调用组织到 React 前端中,这恰恰是因为 React 只关注 View,在它的编程模型中根本不存在控制器。
到目前为止,Facebook 一直致力于在框架层面弥合这一空白。React 团队起初引入了 Flux 模式,不过它依然令人失望,最近 Dan Abramov 又提倡另外一种模式,名为 Redux,在一定程度上来讲,它的方向是正确的,但是在将 API 关联到前端方面,依然比不上我下面所介绍的方案。
Google 发布过 GWT、Android SDK 还有 Angular,你可能认为他们的工程师熟知何为最好的前端架构,但是当你阅读
Angular 2 设计考量的文章时,便会不以为然,即便在 Google 大家也达成这样的共识,他们是这样评价之前的工作成果的:
Angular 1
并不是基于组件的理念构建的。相反,我们需要将控制器与页面上各种[元素]进行关联(attach),其中包含了我们的自定义逻辑。根据我们自定义的指令
如何对其进行封装(是否包含 isolate scope?),scope 会进行关联或继续往下传递。
基于组件的 Angular 2 看起来能简单一点吗?其实并没有好多少。Angular 2 的核心包本身就包含了 180 个语义(Semantics),整个框架的语义已经接近 500 个,这是基于 HTML5 和 CSS3 的。谁有那么多时间学习和掌握这样的框架来构建 Web 应用呢?当 Angular 3 出现的时候,情况又该是什么样子呢?
在使用过 React 并了解了 Angular 2 将会是什么样子之后,我感到有些沮丧:这些框架都系统性地强制我使用
BFF“页面可替换模式(Screen Scraping)”模式,按照这种模式,每个服务端的 API
要匹配页面上的数据集,不管是输入的还是输出的。
弃用 MVC 之后怎么走?
此时,我决定“让这一切见鬼去吧”。我构建了一个 Web 应用,没有使用 React、没有使用 Angular 也没有使用任何其他的 MVC 框架,通过这种方式,我看一下是否能够找到一种在 View 和底层 API 之间进行更好协作的方式。
就 React 来讲,我最喜欢的一点在于 Model 和 View 之间的关联关系。React 不是基于模板的,View 本身没有办法请求数据(我们只能将数据传递给 View),看起来,针对这一点进行探索是一个很好的方向。
如果看得足够长远的话,你会发现 React 唯一的目的就是将 View 分解为一系列(纯粹的)函数和 JSX 语法:
V params={M}/
它实际上与下面的格式并没有什么差别:
V = f ( M )
例如,我当前正在从事项目的 Web 站点, Gliiph,就是使用这种函数构建的:
图1:用于生成站点 Slider 组件 HTML 的函数
这个函数需要使用 Model 来填充数据:
图2:支撑 slider 的 Model
如果用简单的 JavaScript 函数就能完成任务,我们为什么还要用 React 呢?
虚拟 DOM(virtual-dom)?如果你觉得需要这样一种方案的话(我并不确定有很多的人需要这样),其实有这样的可选方案,我也期望开发出更多的方案。
GraphQL?并不完全如此。不要因为 Facebook
大量使用它就对其产生误解,认为它一定是对你有好处的。GraphQL 仅仅是以声明的方式来创建视图模型。强制要求 Model 匹配 View
会给你带来麻烦,而不是解决方案。React 团队可能会觉得使用“客户端指定查询(Client-specified
queries)”是没有问题的(就像反应型团队中那样):
GraphQL 完全是由 View 以及编写它们的前端工程师的需求所驱动的。[…]另一方面,GraphQL 查询会精确返回客户端请求的内容,除此之外,也就没什么了。
GraphQL 团队没有关注到 JSX 语法背后的核心思想:用函数将 Model 与 View 分离。与模板和“前端工程师所编写的查询”不同,函数不需要 Model 来适配 View。
当 View 是由函数创建的时候(而不是由模板或查询所创建),我们就可以按需转换 Model,使其按照最合适的形式来展现 View,不必在 Model 的形式上添加人为的限制。
例如,如果 View 要展现一个值v,有一个图形化的指示器会标明这个值是优秀、良好还是很差,我们没有理由将指示器的值放到 Model 中:函数应该根据 Model 所提供的v值,来进行简单的计算,从而确定指示器的值。
现在,把这些计算直接嵌入到 View 中并不是什么好主意,使 View-Model 成为一个纯函数也并非难事,因此当我们需要明确的 View-Model 时,就没有特殊的理由再使用 GraphQL 了:
V = f ( vm (M) )
作为深谙 MDE 之道的人,我相信你更善于编写代码,而不是元数据,不管它是模板还是像 GraphQL 这样的复杂查询语言。
这个函数式的方式能够带来多项好处。首先,与 React 类似,它允许我们将 View 分解为组件。它们创建的较为自然的界面允许我们为 Web 应用或 Web 站点设置“主题”,或者使用不同的技术来渲染 View(如原生的方式)。函数实现还有可能增强我们实现反应型设计的方式。
在接下来的几个月中,可能会出现开发者交付用 JavaScript 函数包装的基于组件的 HTML5 主题的情况。这也是最近这段时间,在我的
Web 站点项目中,我所采用的方式,我会得到一个模板,然后迅速地将其封装为 JavaScript 函数。我不再使用
WordPress。基本上花同等的工夫(甚至更少),我就能实现 HTML5 和 CSS 的最佳效果。
这种方式也需要在设计师和开发人员之间建立一种新型的关系。任何人都可以编写这些 JavaScript 函数,尤其是模板的设计人员。人们不需要学习绑定方法、JSX 和 Angular 模板的语法,只掌握简单的 JavaScript 核心函数就足以让这一切运转起来。
有意思的是,从反应型流程的角度来说,这些函数可以部署在最合适的地方:在服务端或在客户端均可。
但最为重要的是,这种方式允许在 View 与 Model 之间建立最小的契约关系,让 Model 来决定如何以最好的方式将其数据传递给 View。让 Model 去处理诸如缓存、懒加载、编配以及一致性的问题。与模板和 GraphQL 不同,这种方式不需要从 View 的角度来直接发送请求。
既然我们有了一种方式将 Model 与 View 进行解耦,那么下一个问题就是:在这里该如何创建完整的应用模型呢?“控制器”该是什么样子的?为了回答这个问题,让我们重新回到 MVC 上来。
苹果公司了解 MVC 的基本情况,因为他们在上世纪 80 年代初,从 Xerox PARC“偷来了”这一模式,从那时起,他们就坚定地实现这一模式:
图3:MVC 模式
Andre Medeiros 曾经清晰地指出,这里核心的缺点在于, MVC
模式是“交互式的(interactive)”(这与反应型截然不同)。在传统的 MVC 之中,Action(Controller)将会调用
Model 上的更新方法,在成功(或出错)之时会确定如何更新
View。他指出,其实并非必须如此,这里还有另外一种有效的、反应型的处理方式,我们只需这样考虑,Action 只应该将值传递给
Model,不管输出是什么,也不必确定 Model 该如何进行更新。
那核心问题就变成了:该如何将 Action 集成到反应型流程中呢?如果你想理解 Action 的基础知识的话,那么你应该看一下
TLA+。TLA 代表的是“Action 中的逻辑时序(Temporal Logic of Actions)”,这是由 Dr. Lamport
所提出的学说,他也因此获得了图灵奖。在 TLA+ 中,Action 是纯函数:
data’ = A (data)
我真的非常喜欢 TLA+ 这个很棒的理念,因为它强制函数只转换给定的数据集。
按照这种形式,反应型 MVC 看起来可能就会如下所示:
V = f ( M.present ( A (data) ) )
这个表达式规定当 Action 触发的时候,它会根据一组输入(例如用户输入)计算一个数据集,这个数据是提交到 Model
中的,然后会确定是否需要以及如何对其自身进行更新。当更新完成后,View 会根据新的 Model 状态进行更新。反应型的环就闭合了。Model
持久化和获取其数据的方式是与反应型流程无关的,所以,它理所应当地“不应该由前端工程师来编写”。不必因此而感到歉意。
再次强调,Action 是纯函数,没有状态和其他的副作用(例如,对于 Model,不会包含计数的日志)。
反应型 MVC 模式很有意思,因为除了 Model 以外,所有的事情都是纯函数。公平来讲,Redux 实现了这种特殊的模式,但是带有
React 不必要的形式,并且在 reducer 中,Model 和 Action 之间存在一点不必要的耦合。Action
和接口之间是纯粹的消息传递。
这也就是说,反应型 MVC 并不完整,按照 Dan 喜欢的说法,它并没有扩展到现实的应用之中。让我们通过一个简单的样例来阐述这是为什么。
假设我们需要实现一个应用来控制火箭的发射:一旦我们开始倒计时,系统将会递减计数器(counter),当它到达零的时候,会将 Model 中所有未定的状态设置为规定值,火箭的发射将会进行初始化。
这个应用有一个简单的状态机:
图4:火箭发射的状态机
其中 decrement 和 launch 都是“自动”的 Action,这意味着我们每次进入(或重新进入)counting
状态时,将会保证进行转换的评估,如果计数器的值大于零的话,decrement Action 将会继续调用,如果值为零的话,将会调用
launchAction。在任何的时间点都可以触发 abort Action,这样的话,控制系统将会转换到 aborted 状态。
在 MVC 中,这种类型的逻辑将会在控制器中实现,并且可能会由 View 中的一个计时器来触发。
这一段至关重要,所以请仔细阅读。我们已经看到,在 TLA+ 中,Action 没有副作用,只是计算结果的状态,Model 处理
Action 的输出并对其自身进行更新。这是与传统状态机语义的基本区别,在传统的状态机中,Action
会指定结果状态,也就是说,结果状态是独立于 Model 的。
在 TLA+ 中,所启用的 Action 能够在状态表述(也就是 View)中进行触发,这些 Action 不会直接与触发状态转换的行为进行关联。换
句话说,状态机不应该由连接两个状态的元组(S1, A, S2)来进行指定,传统的状态机是这样做的,它们元组的形式应该是(Sk, Ak1,
Ak2,…),这指定了所有启用的 Action,并给定了一个状态 Sk,Action 应用于系统之后,将会计算出结果状态,Model
将会处理更新。
当我们引入“state”对象时,TLA+ 提供了一种更优秀的方式来对系统进行概念化,它将 Action 和 view(仅仅是一种状态的表述)进行了分离。
我们样例中的 Model 如下所示:
model = {
counter: ,
started: ,
aborted: ,
launched:
}
系统中四个(控制)状态分别对应于 Model 中如下的值:
ready = {counter: 10, started: false, aborted: false, launched: false }
counting = {counter: [0..10], started: true, aborted: false, launched: false }
launched = {counter: 0, started: true, aborted: false, launched: true}
aborted = {counter: [0..10], started: true, aborted: true, launched: false}
这个 Model 是由系统的所有属性及其可能的值所指定的,状态则指定了所启用的 Action,它会给定一组值。这种类型的业务逻辑必须要在某个地方进行实现。我们不能指望用户能够知道哪个 Action 是否可行。在这方面,没有其他的方式。不过,这种类型的业务逻辑很难编写、调试和维护,在没有语义对其进行描述时,更是如此,比如在 MVC 中就是这样。
让我们为火箭发射的样例编写一些代码。从 TLA+ 角度来讲,next-action
断言在逻辑上会跟在状态渲染之后。当前状态呈现之后,下一步就是执行 next-action 断言,如果存在的话,将会计算并执行下一个
Action,这个 Action 会将其数据交给 Model,Model 将会初始化新状态的表述,以此类推。
图5:火箭发射器的实现
需要注意的是,在客户端/服务器架构下,当自动 Action 触发之后,我们可能需要使用像 WebSocket 这样的协议(或者在 WebSocket 不可用的时候,使用轮询机制)来正确地渲染状态表述。
我曾经使用 Java 和 JavaScript 编写过一个很轻量级的开源库,它使用 TLA+
特有的语义来构造状态对象,并提供了样例,这些样例使用
WebSocket、轮询和队列实现浏览器/服务器交互。在火箭发射器的样例中可以看到,我们并非必须要使用那个库。一旦理解了如何编写,状态实现的编码
相对来讲是很容易的。
新模式——SAM 模式
对于要引入的新模式来说,我相信我们已经具备了所有的元素,这个新模式作为 MVC 的替代者,名为 SAM 模式(状态-行为-模型,State-Action-Model),它具有反应型和函数式的特性,灵感来源于 React.js 和 TLA+。
SAM 模式可以通过如下的表达式来进行描述:
V = S ( vm ( M.present ( A (data) ) ), nap (M))
它表明在应用一个 Action A 之后,View V 可以计算得出,Action 会作为 Model 的纯函数。
在 SAM 中,A(Action)、vm(视图-模型,view-model)、nap(next-action
断言)以及S(状态表述)必须都是纯函数。在 SAM 中,我们通常所说的“状态”(系统中属性的值)要完全局限于 Model
之中,改变这些值的逻辑在 Model 本身之外是不可见的。
随便提一下,next-action 断言,即 nap ()是一个回调,它会在状态表述创建完成,并渲染给用户时调用。
图7:“修改地址”的实现
模式中的元素,包括 Action 和 Model,可以进行自由地组合:
函数组合
data’ = A (B(data))
端组合(Peer)(相同的数据集可以提交给两个 Model)
M1.present (data’)
M2.present (data’)
父子组合(父 Model 控制的数据集提交给子 Model)
M1.present (data’,M2)
function present (data, child) {
// 执行更新
…
// 同步 Model
child.present (c(data))
}
发布/订阅组合
M1.on (“topic”, present )
M2.on (“topic”, present )
或
M1.on (“data”, present )
M2.on (“data”, present )
有些架构师可能会考虑到 System of Record 和 Systems of Engagement,这种模式有助于明确这两层的接口(图8),Model 会负责与 systems of record 的交互。
图8:SAM 组合模型
整个模式本身也是可以进行组合的,我们可以实现运行在浏览器中的 SAM 实例,使其支持类似于向导(wizard)的行为(如 ToDo 应用),它会与服务器端的 SAM 进行交互:
图9:SAM 实例组合
请注意,里层的 SAM 实例是作为状态表述的一部分进行传送的,这个状态表述是由外层的实例所生成的。
会话检查应该在 Action 触发之前进行(图 10)。SAM 能够启用一项很有意思的组合,在将数据提交给 Model 之前,View
可以调用一个第三方的 Action,并且要为其提供一个 token 和指向系统 Action 的回调,这个第三方 Action
会进行授权并校验该调用的合法性。
图 10:借助 SAM 实现会话管理
从 CQRS
的角度来讲,这个模式没有对查询(Query)和命令(Command)做特殊的区分,但是底层的实现需要进行这种区分。搜索或查询“Action”只是
简单地传递一组参数到 Model 中。我们可以采用某种约定(如下划线前缀)来区分查询和命令,或者我们可以在 Model 上使用两个不同的
present 方法:
{ _name : ‘/^[a]$/i’ } // 名字以A或a开头
{ _customerId: ‘123’ } // id=123 的 customer
Model 将会执行必要的操作以匹配查询,更新其内容并触发 View 的渲染。类似的约定可以用于创建、更新或删除 Model
中的元素。在将 Action 的输出传递给 Model
方面,我们可以实现多种方式(数据集、事件、Action……)。每种方式都会有其优势和不足,最终这取决于个人偏好。我更喜欢数据集的方式。
在异常方面,与 React 类似,我们预期 Model 会以属性值的形式保存异常信息(这些属性值可能是由 Action 提交的,也可能是 CRUD 操作返回的)。在渲染状态表述的时候,会用到属性值,以展现异常信息。
在缓存方面,SAM 在状态表述层提供了缓存的选项。直观上来看,缓存这些状态表述函数的结果能够实现更高的命中率,因为我们现在是在组件/状态层触发缓存,而不是在 Action/响应层。
该模式的反应型和函数式结构使得功能重放(replay)和单元测试变得非常容易。
SAM 模式完全改变了前端架构的范式,因为根据 TLA+ 的基础理念,业务逻辑可以清晰地描述为:
Action 是纯函数
CRUD 操作放在 Model 中
状态控制自动化的 Action
作为 API 的设计者,从我的角度来讲,这种模式将 API 设计的责任推到了服务器端,在 View 和 Model 之间保持了最小的契约。
Action 作为纯函数,能够跨 Model 重用,只要某个 Model 能够接受 Action 所对应的输出即可。我们可以期望 Action 库、主题(状态表述)甚至 Model 能够繁荣发展起来,因为它们现在能够独立地进行组合。
借助 SAM 模式,微服务能够非常自然地支撑 Model。像 Hivepod.io 这样的框架能够插入进来,就像它本来就在这层似得。
最为重要的是,这种模式像 React 一样,不需要任何的数据绑定或模板。
随着时间的推移,我希望能够推动浏览器永久添加虚拟 DOM 的特性,新的状态表述能够通过专有 API 直接进行处理。
我发现这个旅程将会带来一定的革新性:在过去的几十年中,面向对象似乎无处不在,但它已经一去不返了。我现在只能按照反应型和函数式来进行思考。我
借助 SAM 所构建的东西及其构建速度都是前所未有的。另外,我能够关注于 API 和服务的设计,它们不再遵循由前端决定的模式。
1、选择一个好的主机
WordPress主机服务在网站性能中起着重要作用。一个很好的共享托管服务提供商,如BlueHost或Siteground,都会针对WordPress采取额外的措施来优化您的网站的性能。
一般的网站刚起步使用共享主机就可以,但是如果网站内容比较多,使用比较大的主题比较吃内存或者访问量比较大,那么共享主机就比较吃力了。在高流量时段,站点速度非常慢,甚至频繁的停机。
如果你的网站现在访问速度比较慢,看看你用的是什么主机,现有的配置是否够用。
如果不够的话需要升级一下主机,推荐WP Engine管理WordPress主机或者VPS 。
2、使用轻量级主题/框架
网站访问速度过慢很有可能是你的WordPress主题没有经过优化或者包含的功能太多太臃肿导致的。
很多高级主题虽然功能强大,自定义程度比较高,但是不可避免导致主题包过大,耗费资源。
像The7一类的主题,十几M的安装包,一般的虚拟主机跑起来很吃力。
如果非要使用一些高级主题推荐使用Divi 一类轻量级优化过的WordPress主题。使用一些比较有名大公司开发的主题,尽量少用一些不知名的主题。
3、调整优化图像
网站优化中最常被忽视的方面之一是网站图片优化,我见过很多人图片不经过处理十几M的图片直接上传。WordPress图像优化可以对页面加载时间产生最大的影响。
虽然使用高清的图片可以使我们的网站看起来很好,博客上的一些好的照片真的可以带来很多的分享和评论。
但缺点是未优化的图像通常会很大,几M甚至十几M,严重拖慢网站的访问速度。图像很大也会占用大量HTTP请求。一般虚拟主机是有流量限制的。
如果您的网站上有很多图片,这可能意味着更慢的页面加载时间。因为们不是摄影网站,所以网站的内容和加载速度比图片美观更为重要。
图片的大小对网站的加速速度影响非常大,很多网站加载不完整,都是因为图片太大最后才加载出来。
我们可以在图片上传前优化调整图片尺寸,尽量不要超过1M,100-500kb最合适。
我们可以通过两种方式对图片进行优化。
第一种手动缩放和裁剪图像或者压缩
我们还可以使用无损或有损图像压缩使图片变得更小,大多数人寻找无损压缩,以确保他们的图像在他们的网站上仍然看起来效果仍然很好,即使是被压缩。
有一些工具像Photoshop一样将图片尺寸裁剪将,仍可以使用更简单的方法来完成此操作。
首先选择正确的主题 ,大部分高级主题都是“支持移动响应式设计”,它们内置的图像缩放器或图像缩放器,可以为不同的设备和屏幕尺寸创建不同的图像上传的缩略图。
第二种也是推荐使用的一种方法,使用插件自动缩放和压缩图像
我测试了5种不同的图像优化插件和服务。让我的页面加载时间缩短了一些。特别是与移动端。
以下是我测试过的一些但是效果都不怎么好
●EWWW Image Optimzation Plugin(免费工具,但是每次发布新的博客文章时,会拖慢网站的速度)。
●WP Smushit(现在有免费和高级插件选项,但是免费效果不怎么明显,付费的感觉又不值这个价)。
●Kraken.io(发现没有什么效果)。
●ShortPixel(还是没什么效果)。
所有上述产品都有其优点,但是我需要一种可以压缩图像,还可以创建的缩放,裁剪和调整大小的图像的工具,这是一个非常关键的元素。
最后,经过广泛的研究,终于找到一个插件,可以满足我所有的需求。
这是一个非常强大的WordPress插件名叫Imagify,由WP Rocket研发,他们应该是最懂网站速度优化的。
用WordPress的Imagify插件连接到Imagify API服务,用于图像缩放和优化。
对于大多数博客,您可以使用完全免费Imagify,它们允许您每个月免费使用该服务多达25 MB的图像。
更重要的是,它可以非常好地优化图像。Imagify允许您在整个网站进行批量优化,然后在加载新图像时进行单独优化。 它优化每个图像,甚至是您的主题为您创建的图像。
4.使用缓存插件
WordPress缓存插件显然是非常有用的,可以减少服务器的压力,也可以让网站的访问速度更快,所以,WordPress网站安装缓存插件是必须的
WP Super Cache 是 WordPress 官方开发人员 Donncha 开发的,是目前最高效也是最灵活的 WordPress 静态缓存插件。它把整个网页直接生成 HTML 文件,这样 Apache 就不用解析 PHP 脚本,通过使用这个插件,能使得你的网站速度将显著的提速。
还有一个类似的插件W3 Total Cache,WP Fastest Cache 也有同样的功能,选择一个使用就可以。
还有一款付费的最强大的缓存插件Wp-Rocket ,如果对速度比较在意使用这款插件吧,绝对不会让你失望,价格也不贵每年39美金。
5、使用内容传送网络(CDN)
你过你网站受众是国外各个国家客户,那么建议使用CDN全球加速服务,保证各个国家都能顺利访问。
CDN通过复制您的网站并将其托管在世界各地的服务器上来提高您的网站速度。 比如你的网站是托管在美国,但是来自日本的人想要访问它。 如果您不使用CDN,那么来自日本的访客将不得不等待美国的服务器加载您的网站。 但是,如果您使用CDN,日本的服务器将加载您的网站,使日本访客更快地加载。 这样,无论访问者在哪里,都可以确保您的站点从附近的服务器加载。
推荐的CDN服务是Max CDN,价格比较便宜,效果显著,而且操作界面使用起来非常简单只需要几分钟就可以设置完成。
6、优化主页以快速加载
虽然听起来好像是一件事,但是需要做很多事情以确保您的主页快速加载,这可能是您的网站最重要的部分,因为人们将最常访问网站的主页。
包括一下几点事情:
显示摘录而不是完整的帖子
减少页面上的帖子数(控制在5-7个之间显示)
从主页删除不必要的社交分享小部件(仅在帖子中包含它们)
删除不需要不经常使用的插件和小工具
总体而言,网站的主页不要太复杂,中国人就喜欢在网站上放大量 的内容和杂七杂八的东西。
7.优化WordPress数据库
因为 WordPress 网站的页面都是动态网页,所以对数据库的读取很频繁,数据库优化与否对于网站的访问速度影响很大,因此我们需要经常清理和优化 WordPress 数据库,手动太麻烦,大多数人也不会。
WP-Optimize 可以自动清理优化数据库,这个插件可以让您做一个简单的任务:优化数据库(垃圾邮件,修订版本,草稿,表格等),以减少开销。
我还会推荐WP-DB Manager插件,它可以安排数据库优化的日期。
8、合并压缩代码
Autoptimize 这个插件能够自动的合并 CSS 和 JS,并对其压缩,非常的方便,还可以压缩HTML而且一些主题会有大量的 inline CSS,当开启了合并 CSS 后,这些 inline CSS 会自动添加到文件中。支持CDN,可以选择去除Google字体,使用这个插件需要测试下主题是否兼容,有的主题压缩代码可能会出现问题。
9.使用GZip页面压缩
协议上的GZIP编码是一种用来改进应用程序性能的技术。大流量的站点常常使用GZIP压缩技术来让用户感受更快的速度。这一般是指WWW服务器中安装的一个功能,当有人来访问这个服务器中的网站时,服务器中的这个功能就将网页内容压缩后传输到来访的电脑浏览器中显示出来.一般对纯文本内容可压缩到原大小的40%.这样传输就快了,效果就是你点击网址后会很快的显示出来.当然这也会增加服务器的负载. 一般服务器中都安装有这个功能模块的。
减少文件大小有两个明显的好处,一是可以减少存储空间,二是通过网络传输文件时,可以减少传输的时间。gzip 是在 Linux系统中经常使用的一个对文件进行压缩和解压缩的命令,既方便又好用。
我们可以使用这个工具来检查我们的 网站是否启用了GZIP Check GZIP Compression
很多插件都支持GZIP压缩 Check and Enable GZIP compression ,WP Fastest Cache 。
10.调整Gravatar图像
Gravatar头像需要调取第三方服务,这样也会拖慢我们网站速度,如果你不建议没有头像或者不需要使用
可以在WordPress仪表板的讨论选项卡中在中找到,设置为空白即可,而不是默认图像,这样会有助于您的网站速度。
如果需要使用的话可以使用wp-user-avatars插件把头像设置为本地。
11.将LazyLoad添加到您的图像
图片懒加载也就是图片延时加载,简单点说就是只加载页面上能看到的图片,从而减短页面的等待时间,当访客需要看其他的图片的时候再加载相应的图片。
这不仅可以加快页面加载速度,还可以通过为不在页面上滚动的用户加载较少的数据来节省带宽。
推荐的插件是Lazy Load安装好以后默认就已经开启,如果你需要特别的设置可以仔细研究下各个设置项。
12.控制存储的修订版本数量
所谓的文章修订版就是你每次修改一次文章,它都会自动帮你保存修改之前的文章版本,专业术语叫做版本控制,这样保证了在误修改的情况下可以还原之前的内容,这种功能对我们这种小博客来说并没有太大的意义,而且会在数据库产生很多无用的数据,使你的数据库越来越臃肿。
第一种方法,定期清理修订版
WP Cleaner插件的功能很简单,就是为了快速删除自动保存的文章冗余修订版和草稿的,节省空间,提高速度。界面也很简单,直接安装后可以使用,WP Cleaner插件有保护机制,无论怎么操作都不会影响已发布的文章。
第二种方法,修改WP安装根目录下的 wp-config.php 文件
// 不保存任何版本(除了自动保存的版本)
define(‘WP_POST_REVISIONS’, false);
//保存所有修订版本
define(‘WP_POST_REVISIONS’, true);
// 保存 n 个修订版本
define(‘WP_POST_REVISIONS’, 3);
保存修订版本数设置为2或3,所以有些保存误删可以撤回,但不要太多。
13.关闭pingbacks和trackbacks
默认情况下,WordPress与配备pingback和trackback服务。
每当另一个博客提到您时,它会通知您的网站,从而更新该帖子上的数据。关闭此功能不会破坏您的网站的反向链接,可以禁用掉。
14.WordPress主题和插件更新到最新版本
WordPress,WordPress主题和插件更新的非常频繁,每次更新都会添加一些功能,优化性能,修复一些Bug,所以把这些都更新到最新版本,还可以保证网站更安全。
15、使用CloudFlare
这类似于上面关于使用CDN的部分,但是我很喜欢CloudFlare,因为我在我最好的网络分析帖子中讨论过,我已经决定在这里分开列出。
坦白说,CloudFlare以及上面讨论的W3 Total Cache插件是一个非常有效的组合(他们相互整合),这将大大提高您的网站的速度,但是不仅可以提高安全性。
两个都是免费的!