7月阅读周·前端架构:从入门到微前端 | 架构设计:单页面应用
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读六个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》。
当前阅读周书籍:《前端架构:从入门到微前端》。
架构设计:单页面应用
在业务不断发展的过程中,由于前端项目变得越来越复杂,所以我们要考虑拆分出前端应用部分,使之成为一个能独立开发、运行的应用,而非依赖于后端渲染出HTML的多页面应用。我们不得不采用更符合复杂客户端、UI层开发的MVC架构,以便更快速地开发前端应用。前端应用采用了MVC框架,也变成了单页面应用,前后端便开始分离了。
前端MV*原理
前端MVC架构的三个层次的作用:
- Model(模型层),用来获取、存放所有的对象数据。
- View(表现层),呈现信息给用户。
- Controller(控制层),模型和视图之间的纽带。
就前端而言,当我们更新数据模型(Model)时,就需要通过控制器(Controller)来更新UI(View)。想要更好地了解它们的关系,可以先来看一个前端MVC架构的示例。
前端MVC架构原理
了解前端MVC最好的形式,便是从代码中来看MVC架构。
一个与MVC架构相关的示例:单击页面上的一个名为hello的按钮,然后将按钮的显示形式变成world。功能很简单,但是要以MVC的形式来呈现。如果以非MVC的形式来呈现,那么代码如下:
<button id="demo-button"></button>
<script>
var text = 'hello';
var demoButton = document.getElementById('demo-button');
demoButton.innerText = text;
demoButton.addEventListener('click', {
handleEvent: function (event) {
event.target.innerText = 'world';
},
});
</script>
在上面的代码中,初始化时会更新按钮的显示文字,单击按钮时也会更新按钮上的文字。为了方便后面的代码解释,我们传入的addEventListener变量是一个带handleEvent参数的变量,而非一个函数,两者的效果是等价的。
由于我们要修改的是按钮背后的文字,所以需要将文字变成一个变量,存于Model函数中。对应的Model函数代码如下:
// 代码位于chapter06/basic-mvc目录下
function Model() {
this.text = 'hello';
}
在这个Model函数里,我们创建了一个text对象,在text对象中存储了一个“hello”字符串,所以Model函数的作用是:提供和存放数据。
随后,在我们的MVC架构中,还需要有一个Controller实现:
//代码位于chapter06/basic-mvc目录下
function Controller(model) {
var that = this;
this.model = model;
this.handleEvent = function (e) {
e.stopPropagation();
switch (e.type) {
case 'click':
that.clickHandler(e.target);
break;
default:
console.log(e.target);
}
};
this.getModelByKey = function (modelKey) {
return that.model[modelKey];
};
this.clickHandler = function (target) {
that.model.text = 'world';
target.innerText = that.getModelByKey('text');
};
}
Controller主要包含三个函数:
- handleEvent,用于响应相关的事件。
- getModelByKey,返回对应的模型数据的值。
- clickHandler,用户单击后对应的操作。
在有了Controller之后,还需要实现一层View,代码如下:
function View(controller) {
this.controller = controller;
this.demoButton = document.getElementById('demo-button');
this.demoButton.innerText = controller.getModelByKey('text');
this.demoButton.addEventListener('click', controller);
}
View层实现了相应的初始化和事件绑定。初看应该在Controller进行绑定,但是有了双向绑定之后,都是由View层来自动绑定的。
最后,我们将它们(Model、View、Controller)结合到一起,便是一个粗糙的MVC示例:
function main() {
var model = new Model();
var controller = new Controller(model);
var view = new View(controller);
}
以这种MVC的方式实现事件的响应和逻辑处理,有助于帮助我们处理复杂的前端应用。但是由于不可能在每个小的部分中都以这种方式去实现每个功能,所以相关的操作变得非常烦琐。而且,当我们拥有大量的交互时,使用原生的JavaScript来开发应用,代码将变得难以维护。这时,我们需要用前端框架来解决:
- 保持UI与模型的同步(即双向绑定)的问题。
- 原生的JavaScript难以编写复杂、高效、易维护的UI的问题。
进阶:设计双向绑定的MVC
实现一个观察者模式,即定义对象间的一种一对多的依赖关系,使得每当一个对象状态发生改变时,其相关的依赖对象皆得到通知并被自动更新。
为此,我们需要在MVC应用里添加两种类型的观察者模式:
- 基于数据劫持的观察者模式。当数据模型的值发生变化时,调用对应的函数。
- 常规的观察者模式。提供可供调用的调用方式。
当数据模型发生变化时,对Model函数进行修改,代码如下:
//代码位于chapter06/basic-mvc-twb目录下
function Model() {
var that = this;
var text = 'hello';
this.listeners = [];
Object.defineProperty(that, 'text', {
get: function () {
return text;
},
set: function (value) {
text = value;
that.notify();
},
});
}
Model.prototype.subscribe = function (listener) {
this.listeners.push(listener);
};
Model.prototype.notify = function (value) {
var that = this;
this.listeners.forEach(function (listener) {
listener.call(that, value);
});
};
数据更新(Setter)的相关函数被抽取出来并放到notify函数中。一旦某个数据的值发生变化,就会触发相应的监听者的调用逻辑。而在View层里,我们则会进行之前的双向绑定逻辑,并注册成为一个订阅者,以便在数据发生更新的时候更改对应的View。
于是,View层得到进一步的完善:
function View(controller) {
var that = this;
this.controller = controller;
var elements = document.querySelectorAll('[data-tw-bind]');
elements.forEach(function (element) {
if (element.type === 'button') {
element.innerText = controller.getModelByKey('text');
that.call = function (data) {
element.innerText = data.text;
};
element.addEventListener('click', controller);
}
});
this.controller.model.subscribe(this);
}
与之前的双向绑定类似,我们会遍历所有的data-tw-bind属性,为button类型的元素的初始化值自动绑定click事件。同时,添加了订阅的响应函数call,当Model层的数据发生变化时更新button上的显示文本。此外,将View注册为数据模型的监听者。
最后,Controller通过clickHandler方法对数据进行更新。
function Controller(model) {
...
this.clickHandler = function(target) {
that.model.text = 'world';
}
}
当这个数据发生变化时,会调用数据劫持Setter中的相关notify方法。也就会调用所有订阅者的响应函数call方法,并且还会更新按钮上的文字。为了验证双向绑定是可以工作的,我们可以在Controller中对setTimeout进行设置,让其在3秒后改变model的值,以观察对应的文本是否更新:
setTimeout(function () {
that.controller.model.text = '3s';
}, 3000);
前端MV*框架的原理就是通过观察和订阅来进行联动操作,以自动触发各种逻辑函数。
前端框架选型
选定一个前端框架时,我们还要考虑的因素有:
- 框架是否能满足大部分应用的需求?如果不能,那么需要使用哪个框架?
- 框架是否有丰富的组件库?如果没有,我们的团队和组织是否有独立开发的能力?
- 框架的社区支持怎样?在遇到问题时能否快速方便地找到人解答?
- 框架的替换成本如何?假如我们的新项目将使用B框架,那么我们还需要额外学习什么内容?
启动前端应用
选择好前端框架后就可以进行下一步开发了:
- 寻找合适的脚手架,编写出第一个“Hello, world”。
- 选择合适的UI框架,以快速开发前端页面。
- 确认浏览器的支持范围,以明确测试边界。
- 明确响应式设计的需求,以明确在编码的过程中支持哪些设备。
这部分只是那些独立于组织、公司的通用内容。对于流程规范化的组织内部,还会更复杂。
服务端渲染
单页面应用的服务端渲染与纯服务端渲染有所不同,多数情况下,单页面应用的服务端渲染依赖于服务端拥有一个JavaScript引擎,由其运行JavaScript后返回相应的模板。但是,当服务端所使用的语言拥有相同的模板引擎时,也可以不需要这种方式。依笔者的开发经验来看,单页面应用的服务端渲染可以分成三种类型:
- 非JavaScript语言的同构渲染。
- 基于JavaScript语言的同构渲染。
- 预渲染。
每种方式都各有特色,也各自适合不同的场景。
总结
本篇文章的核心内容:
- 结合MVC框架应用介绍了MVC架构的基本原理,并结合双向绑定的内容开发了一个支持双向绑定的MVC应用/框架。
- 前端应用介绍从寻找合适的脚手架开始,到选择合适的UI框架,再到确认浏览器的支持范围、明确响应式设计的需求等。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)