响应式页面
响应式设计师指根据不同设备浏览器尺寸或分辨率来展示不同页面结构层、行为层、表现层内容的设计方式。响应式设计网站目前比较主流的实现方式有两种:一是通过前端或者后端判断useAgent来跳转不同的页面完成不同设备浏览器的适配,也就是维护两个不同的站点进行相应的跳转。二是使用media query媒体查询手段,让页面根据不同设备浏览器自动改变页面的布局和显示,但不做跳转。
先看第一种方案。要注意的是,在浏览器端判断ua执行跳转也是可以的,但是需要页面脚本下载完成后再执行判断逻辑,比服务器端执行跳转的方式要慢。用户使用桌面浏览器和移动端浏览器分别可以访问到对应的站点,如果使用桌面浏览器直接访问移动站点域名下Web站点页面,会进行302跳转到对应的桌面Web页面路径下。
再来看下第二种方案,桌面端和移动端浏览器访问站点需要的内容可能不完全相同,这种响应式的方式只实现了内容布局显示的适应,但是要做更多差异性的功能比较难。
屏幕适配布局
屏幕适配布局是在移动端解决内容按照不同屏幕大小自动等比例缩放的一种布局计算方式。以宽度为320px的移动设备屏幕和414px的移动设备屏幕来看,页面中宽度为160px的容器在320px屏幕下占到50%,而在414宽度的屏幕下没有占到50%。对于这类屏幕适配布局问题,我们通常有两种解决方案,即依据HTML中< html>标签元素的zoom属性缩放和根据REM自适配方案实现等比例缩放。
- zoom属性控制方案。如果希望在320px宽度屏幕上显示的内容在414px的宽度屏幕上等比例自动放大,我们可以使用元素CSS的zoom属性来解决。例如我们可以设置< html>或< body>标签的CSS属性为zoom: 1.29375,其中1.29375 = 414 / 320,那么对于不同屏幕宽度我们只需要用js来设置body的zoom属性就可以。document.body.style.zoom = screen.width / 320
zoom值需要js计算得出,如果js在页面渲染后完成,会出现页面内容全部重排的情况,所以需要在页面文档开始的地方加载这部分js。 - REM屏幕适配方案。我们如果给HTML根元素一个根据屏幕自动调整的font-size,页面上所有元素的尺寸全部以rem为单位,无论屏幕宽度如何变化,页面的内容和屏幕的比例始终是不变的。
前端交互框架
在直接操作DOM时代,jQuery帮助我们简化了选择器,封装了DOM方法和AJAX(做了兼容性处理),事件绑定的封装。在使用jQuery的时候有几个注意点
- 尽可能使用id选择器进行DOM操作,减少使用组合选择器(底层都是DOM获取方法的多次调用)
- 缓存需要复用jQuery DOM对象,使用find()子查询
- 在没有特别麻烦的兼容问题时,使用原生js代替
- 使用事件代理,减少使用元素的事件绑定
- 使用链式写法来提高编程效率和代码运行效率
- 尽可能使用jQuery的静态方法
随着AJAX技术的流行,SPA(Single Page Application)单页面应用被广泛认可,SPA的思路是将整个应用的内容都在一个页面中实现并完全通过异步交互来根据用户操作加载不同的内容。这样,使用jQuery直接进行DOM交互的开发方式就显得不易管理。例如当SPA页面上的交互和异步加载的内容很多时,按照之前思路我们需要每一次数据请求后进行数据渲染和事件绑定,用户操作后进行另一部分内容的请求和事件绑定。并且需要声明不同变量保存每次异步加载时返回的数据对象。
前端MVC模式
Model用来存放请求的数据结果和数据对象,View用于页面DOM的更新和修改,Controller则用于根据前端路由条件(例如不同的HASH路由)来调用不同Model给VIEW渲染不同的数据内容。常用页面路由的实现也很简单
const router = {
get(match, fn){
let url = location.href,
routerReg = new RegExp(match, 'g');
if(routerReg.test(url)){
fn();
}
return this;
}
}
router.get('#index', function(){
_loadIndex();
})
router.get('#detail', function(){
_loadDetail();
})
另外我们可以使用H5的pushState来实现路由。history.pushState(state, url)方法可以改变当前页面的url而不发生跳转,并将不同的state数据和对应的url对应起来。
history.pushState({page: 'A'}, 'page A', 'a.html');
console.log(history.state) // {page: 'A'}
使用路由后,如果将SPA的每个路由或用户操作加载的页面内容看成是一个组件,那么之前的做法是每个组件自行独立完成各自的数据请求、渲染和数据绑定,为了规范这中间的逻辑,组件数据的请求、渲染、页面逻辑分别使用Model、View、Controller来注册调用。假如用户操作URL,那么就会调用对应的Controller A方法,请求对应的数据Model A,然后将Model A传递给视图模板View A并将最后内容渲染到浏览器中。我们举一个最简单的栗子
A组件的外层是一个div
<div id="A" onclick="A.event.change"></div>
我们根据数据对A组件进行渲染,通过监听事件对A组件的数据进行修改,接着反映到视图上
let Controller = {}, Model = {}, View = {};
View['A'] = function(data){
let tpl = '<input id="input" type="text" value="{{text}}"><span id="showText">{{text}}';
//调用模板和数据渲染成HTML
let html = render(tpl, data);
document.getElementById('A').innerHTML = html;
};
Model['A'] = {
text: 'ViewA渲染内容'
}
Controller['A'] = function(){
View['A'](Model['A']);
//用户跳转操作改变了Hash,触发Controller来改变Model和View
$('window').on('hashchange', function(){
model['A'].text = location.hash;
View['A'](model['A']);
});
//用户其他操作改变Model
self.event['change'] = function(){
model['A'].text = '新数据';
View['A'](model['A']);
}
}
成熟的MVC框架一般是通过事件监听或观察者模式来实现的,这里只是为了方便理解的简化版本。通过改进页面分成不同的小模块处理,可以使用面向对象进行封装。
//可能有一个公用的Component基类
let component = new Component();
let A = component.extend({
$el: document.getElementById('A'),
model: {
text: 'ViewA渲染内容'
},
view(data){
let tpl = '<input id="input" type="text" value="{{text}}"><span id="showText">{{text}}';
let html = render(tpl, data);
this.$el.innerHTML = html;
},
controller(){
let self = this;
self.view(self.model);
$('window').on('hashchange', function(){
self.model.text = location.hash;
self.view(self.model);
});
//用户其他操作改变Model
self.event['change'] = function(){
self.model.text = '新数据';
self.view(self.model);
}
}
})
前端MVP模式
MVP(Model-View-Presenter)可以和MVC对照起来看,和MVC一样,M就是Model,V就是View,而P代表Presenter(呈现者),它与Controller有点相似,但不同的是,用户在进行DOM修改操作时将通过View上的行为触发,然后将修改通知给Presenter来完成后面的Model修改和其他View的更新,而MVC模式下,用户的操作是直接通过Controller来控制的。Presenter和View的绑定通常是双向的。还是刚才的栗子
let component = new Component();
let A = component.extend({
$el: document.getElementById('A'),
model: {
text: 'ViewA渲染内容'
},
view: '<input id="input" type="text" value="{{text}}"><span id="showText">{{text}}',
presenter(){
let self = this;
//调用模板渲染数据获取HTML
let html = render(self.view, self.model);
self.$el.innerHTML = html;
//View上的改变将通知Presenter改变Model和其他View
$('#input').on('change', function(){
self.model.text = this.value;
html = render('{{text}}', self.model);
$('#showText').html(html);
});
//Controller上的操作处理和MVC的方式类似
self.event['change'] = function(){
self.model.text = '新数据';
html = render('{{text}}', self.model);
}
}
})
这里View的渲染方法被分到了presenter中,View和Model主要用于提供视图模板和数据而不做任何逻辑处理。但这样Presenter层的内容就变得很重了。
前端MVVM模式
MVVM则可以认为是一个自动化的MVP框架,并且使用ViewModel代替了Presenter,即数据Model的调用和模板内容的渲染不需要我们主动操作,而是ViewModel自动来触发完成,任何用户的操作也是通过ViewModel的改变来驱动的。ViewModel的数据操作最终在页面上以Directive的形式体现,通过对Directive的识别来渲染数据或绑定事件,管理起来更清晰。那么什么是Directive呢?
MVVM设计的一个很大的好处是将MVP中Presenter的工作拆分成多个小的指令步骤,然后绑定到相对应的元素中,根据相对应的数据变化来驱动触发,自动管理交互操作,并将页面中所有的同类操作复用,大大节省我们自己进行内容渲染和事件绑定的代码量。举一个栗子
<div id="A" q-on="click: change">
<input type="text" q-value="text"><span q-html="text"></span>
</div>
let viewModel = new Vm({
$el: document.getElementById('A'),
data: {
text: 'ViewA渲染内容'
},
method: {
change(){
this.text = '新数据'
}
}
})
整体上简洁了很多,模板数据的渲染和数据绑定可以通过q-html或q-click等特殊的属性来控制完成,这些特殊的元素标签属性就是我们所说的Directive(直接绑定),当然不同的框架使用的Directive前缀不一样,但作用是类似的。我们来看一个更复杂的栗子
<form action="#" id="form">
<label for="text" q-html="label"></label>
<input type="text" q-value="value" q-model="value" q-mydo="number | getValue">
<button q-on="click: submit"></button>
</form>
let viewModel = new Vm({
$el: document.getElementById('form'),
data: {
label: '用户名',
value: '输入初始值',
number: 0
},
method: {
submit(){
//do submit
}
},
//指令,自定义的执行函数
directive: {
mydo(value){
console.log(value);
}
},
//过滤器,用户希望对传入的初始化数据进行处理,然后结果交给directive
filter: {
getValue(){
return ++value;
}
}
})
在这段代码中,ViewModel初始化时自动进行了数据填充、数据双向绑定和事件绑定。我们再来看一下new Vm() 时ViewModel初始化所完成的事情。首先js会找到document.getElementById(‘form’) 的DOM节点,对这个元素的属性节点attributes和所有子节点中的attributes进行遍历,一旦遍历到q-开头的属性时,就认为是MVVM框架自定义的Directive,此时会执行相对应的操作。例如遍历到q-html=”label”时,就将ViewModel初始化时默认数据对象data中的label值付给这个元素的innerHTML。遍历到q-on=”click: submit” 的时候,就在这个元素上绑定click事件,事件函数名为submit。也可以自定q-mydo的指令,遍历该节点属性时,调用Directive中的mydo方法,输入参数为data中getValue方法的返回值,这里getValue中输入的number值自动加1并返回。用户在View层操作时自动改变ViewModel的数据,然后ViewModel会检测数据变化,重新遍历扫描节点属性,执行对应的Directive,渲染HTML视图或做事件绑定。
数据变更检测是其中重要的一环。MVVM是通过数据改变来驱动的,这样就需要进行数据的双向绑定,一般若要根据View层的变化来改变Model,是通过一些特殊元素(例如< input>< select>)的onchange事件来触发修改js中ViewModel对象数据的内容来实现的。另一方面是ViewModel修改,如何触发View的自动更新和渲染呢,这种根据数据的变化来自动触发其他操作的机制就是我们说的数据变更检测。
关于数据变更检测请关注下一篇博文现代前端技术解析(3)
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/6919.html