本篇文章参考书籍《JavaScript设计模式》–张容铭
前言
不知道大家有没有这样的经历,来了个需求,希望可以实现什么什么样的功能,但是其中有一个功能,还没确定具体的效果,所以这个需求是做,还是不做?
做吧,到时候还得改,白费功夫不说,对心情影响还不小;不做吧,项目周期又保证不了。反正怎么着都难受,这可咋整,总得想个法子应对。
别着急,智慧超群的前辈们早就遇到过这些情况,已经帮我们想好策略了。
职责链模式
职责链模式可以解决请求的发送者与接收者之间的耦合,通过职责链上的多个对象对请求流程进行分解,实现请求再多个对象之间的传递 。
就像上面的需求一样,当需要做很多事情的时候,把每件事独立成一个模块对象去处理,这样一个完整的需求,就被分解成一部分一部分相互独立的模块需求,通过每个对象分工协作,只做自己分内的事,无关的事交给下一个对象去做,直到需求完成。
这样做的好处就是可以把精力都投入到那些确定的模块里,即便前一个对象功能不确定,也不会影响到下一个模块。
而且划分模块的粒度越细,单元测试时越有把握。举个例子,比如现在需要做一个表单提交的功能,但是表单校验部分的显示效果还不确定,对于这个需求,我们可以进行如下设计:
首先,有的输入框需要绑定 keyup 事件,有的输入框需要绑定 change 事件,所以绑定事件应当是第一部分,第二部分就是创建 XHR 进行异步请求获取数据,第三步就是对响应数据进行适配,最后一步就是向组件中传入数据,渲染组件。
/**
* 异部请求
* data 请求数据
* dealType 响应数据处理对象
* dom 事件源
*/
var sendData = function(data, dealType, dom) {
//XHR对象 简化版本
var xhr = new XMLHttpRequest(),
url = 'getData.php?mod=userInfo';
//请求返回事件
xhr.onload = function(event) {
//请求成功
if(xhr.status >= 200 && xhr.status <300 || xhr.status == 304) {
dealData(xhr.responseText, dealType, dom);
} else {
//请求失败
}
};
//拼接请求字符串
for(car i in data) {
url += '&' + i + '=' + data[i];
}
//发送异步请求
xhr.open('get', url, true);
xhr.send(null);
}
/**
* 处理相应数据
* data 相应数据
* dealType 响应数据处理对象
* dom 事件源
*/
var dealData = function(data, dealType, dom) {
//对象 toString 方法简化引用
var dataType = Object.prototype.toString.call(data);
//判断相应数据处理对象
switch(dealType) {
//输入框提示功能
case 'sug':
//如果数据为数组
if(dataType === '[Object Array]') {
//创建提示框组件
return createSug(data, dom);
}
//将相应的对象数据转化成数组
if(dataType === '[Object Object]') {
var newDAta = [];
for(car i in data) {
newData.push(data[i]);
}
//创建提示框组件
return createSug(newData, dom);
}
//将响应的其他数据转化为数组
return createSug([data], dom);
break;
case 'validate':
//创建校验组件
return createValidataResult(data, dom);
break;
}
}
/**
* 创建提示框组件
* data 响应适配数据
* dom 事件源
*/
var createSug = function(data, dom) {
var i = 0,
len = data.length,
html = '';
//拼接每一句提示语
for(; i < len; i++) {
html += '<li>' + data[i] + '</li>';
}
//显示提示框
dom.parentNode.getElementsByTagName('span')[0].innerHTML = data;
}
上面就是我们搭建的需求职责链,现在需求还没确定,不过我们已经可以对模块进行单元测试了。例如对于 dealData 模块进行单元测试,需要先分析这个对象方法都可能接收哪些类数据,然后要对这些类数据运行结果进行测试,如果每一次运行结果跟预期的一致,那么创建的对象方法是安全的。
dealData('用户名不正确', 'validate', input[0]);
dealData('123', 'sug', input[1]);
dealData(['爱奇艺', '腾讯视频', '优酷'], 'validate', input[0]);
dealData({
iqy: '爱奇艺',
txsp: '腾讯视频',
yk: '优酷'
}, 'sug', input[1]);
测试完成了,现在讨论结果也出来了,要求创建两个输入框,第一个要具有验证功能,第二个要具有下拉框提示功能。这个时候补充完我们的职责链
var input = document.getElementsByTagName('input');
//监听内容改变事件做内容校验
input[0].onchange = function(e) {
sendData({
value: input[0].value}, 'validate', input[0]);
}
//监听键盘事件对内容做提示处理
input[1].onkeydown = function(e) {
sendData({
value: input[1].value}, 'sug', input[1]);
}
现在只需要对事件源绑定事件,有了单元测试保证,后面的处理逻辑就不用再操心了。职责链模式给我们开启了一个新思路,对复杂模块进行分解,在一定程度上可以减少开发过城中的制约,下一节我们反过来,学习一种把细小粒度整合成复杂对象的一种设计模式。
命令模式
命令模式可以将请求与实现解耦,并封装成独立对象,从而使不同的请求对客户端的实现参数化 。
上面的概念不太好理解,解释一下就是将创建模块的逻辑封装再一个对象里,这个对象提供一个参数化的请求接口,同过调用这个接口并传递一些参数实现调用命令对象内部中的一些方法。
请求部分很简单,只需要按照给定的参数格式书写指令即可,所以实现部分的封装才是重点,应为它需要为请求部分提供所需要的方法。
举个例子,现在需要做一个活动页面,里面每个模块都有图片展示区,标题。不同的是,每个产品的图片数量和排列不同。既然动态展示不同模块,所以创建元素这一需求就是变化的,因此创建元素的方法,展示方法应该被命令化。
//模块实现模块
var viewCommand = (function() {
//方法集合
var Action = {
//创建方法
create: function() {
},
//展示方法
display: function() {
}
}
//命令接口
return function excute() {
};
}) ();
接下来开始实现命令对象的每一种方法,有一点需要注意,创建视图过程中如果单纯采用 DOM 操作拼凑页面的开销实在有点大,索性格式化字符串模板来创建页面。
//模块实现模块
var viewCommand = (function() {
var tpl = {
//展示图片接口模板
product: [
'<div>',
'<img src="{#src#}"/>',
'<p>{#text#}</p>',
'</div>'
].join(''),
//展示标题结构模板
title: [
'<div class="title">',
'<div class="main">',
'<h2>{#title#}</h2>',
'<p>{#tips#}</p>',
'</div>',
'</div>'
].join('')
},
//格式化字符串缓存字符串
html = '';
//格式化字符串如:'<div>{#content#}</div>'用{content: 'demo'}替换后可得到字符串:'<div>demo</div>'
function formateString(str, obj) {
//替换'{#'与'#}'之间的字符串
return str.replace(/\{#(\w+)#\}/g, function(match, key) {
return obj[key];
})
}
//方法集合
var Action = {
};
//命令接口
return function excute() {
}
}) ();
创建模块视图就可以通过对模块视图数据进行字符串模板格式化来获取。并封装再 create 方法里面。
create: function (data, view) {
//解析数据,如果数据是一个数组
if(data.length) {
//遍历数组
for(var i = 0, len = data.length; i < len; i++) {
//将格式化之后的字符串缓存到html中
html += formateString(tpl[view], data);
}
} else {
//直接格式化字符串缓存到html中
html += formateString(tpl[view], data);
}
}
还有一个视图展示方法,实现如下:
//展示方法
display: function(container, data, view) {
//如果传入数据
if(data) {
//根据给定数据创建视图
this.create(data, view);
}
//展示模块
document.getElementById(container).innerHTML = html;
//展示后清空缓存字符串
html = '';
}
创建视图与展示视图实现完成了,接下来就是实现命令接口了。这个接口的参数应该包含两部分,第一部分是命令对象内部的方法名称,第二部分是命令对象内部方法对应的参数。
//命令接口
return function excute(msg) {
//解析命令,如果 msg.param 不是数组,则将其转化成数组
msg.param = Object.prototype.toString.call(msg.param) === '[Object Array]' ? msg.param : [msg.param];
//Action 内部调用的方法引用 this,所以此处为保证作用域 this 执行传入 Action
Action[msg.command].apply(Action, msg.param)
}
完成了,命令模式我们搞定了,记下来测试一下
//产品展示数据
var productData = [
{
src: 'command/02.jpg',
text: '开心的棉花糖'
},
{
src: 'command/03.jpg',
text: '阳光下的泡泡'
},
{
src: 'command/04.jpg',
text: '镜子里的喵喵'
}
],
//模块标题数据
titleData = {
title: '冬天雪地放炮仗',
tips: '暖暖的温情为这个季节带来了更丰富的色彩'
};
接下来展示一个标题模块
viewCommand({
//参数说明 方法 display
command: 'display',
//参数说明 paraml 元素容器 parma2 标题数据 param3 元素模板
param: ['title', 'titleData', 'title']
});
创建一个图片
viewCommand({
commadn: 'create',
param: [{
src: 'command/01/png',
text: '迎着朝阳的野菊花'
}, 'product']
})
有了命令模式以后,想创建任何视图页面都是一件很简单的事。
命令模式应用
其实命令模式也常用于解耦,比如在使用 canvas 时会经常调用一些内置方法,再多人合作模式中,耦合度是比较高的,如果一个人不小心篡改了 canvas 元素的上下文引用,那么后果是无法估计的。
我们通常的做法是将上下文引用对象安全的封装在一个命令对象的内部,如果有人想绘图,直接通过命令对象书写一条命令,即可调用命令对象内部方法来完成需求。
//实现对象
var CanvasCommand = (function() {
//获取canvas
var canvas = document.getElementById('canvas'),
//canvas 元素的上下文引用对象缓存在命令对象内部
ctx = canvas.getContext('2d');
//内部方法对象
var Action = {
//填充色彩
fillStyle: function(c) {
ctx.fillStyle = c;
},
//填充矩形
fillRect: function(x, y, width, height) {
ctx.fillRect(x, y, width, height);
},
//描边色彩
strokeStyle: function(c) {
ctx.strokeStyle = c;
},
//描边矩形
strokeRect: function(x, y, width, height) {
ctx.strokeRect(x, y, width, height);
},
//填充字体
fillText: function(text, x, y) {
ctx.fillText(text, x, y);
},
//开启路径
beginPath: function() {
ctx.beginPath();
},
//移动画笔触电
moveTo: function(x, y) {
ctx.moveTo(x, y);
},
//画笔连线
lineTo: function(x, y) {
ctx.lineTo(x, y);
},
//绘制弧线
arc: function(x, y, r, begin, end, dir) {
ctx.arc(x, y, r, begin, end, dir);
},
//填充
fill: function() {
ctx.fill();
},
//描边
stroke: function() {
ctx.stroke();
}
}
return {
//命令接口
excute: function(msg) {
//如果没有指令返回
if(!msg) return;
//如果命令是一个数组
if(msg.length) {
//遍历执行多个命令
for(var i = 0, len = msg.length; i < len; i++) {
arguments.callee(msg[i]);
}
//执行一个命令
} else {
//如果msg.param不是一个数组,将其转化为数组,apply第二个参数要求格式
msg.param = Object.prototype.toString.call(msg.param) === '[object Array]' ? msg.param : [msg.param];
//Action内部调用的方法可能引用this,为保证作用域中this指向正确,故传入Action
Action[msg.command].apply(Action, msg.param);
}
}
}
}) ();
有了上面那个对象,任何人想要绘制图形都不用以来 canvas 了,只要是按照命令对象结构给出命令格式,写一条命令即可。比如:
//绘制一个红色的矩形
CanvasCommand.excute([
{
command: 'fillStyle', param: 'red'},
{
command: 'fillRect', param: [20, 20, 100, 100]}
]);
封装功能,并提供简单有效的 API 是提高团队效率的可行方案,命令模式的运用,便是遵循了这一方案。本节就到此结束了,文中有错误的地方,欢迎各位大佬指出。