前言
博客园谈设计模式的文章很多,我也受益匪浅,包括TerryLee、吕震宇等等的.NET设计模式系列文章,强烈推荐。对于我,擅长于前台代码的开发,对于设计模式也有一定的了解,于是我想结合Javascript来设计前台方面的“设计模式”,以对后台“设计模式”做个补充。开始这个系列我也诚惶诚恐,怕自己写得不好,不过我也想做个尝试,一来希望能给一些人有些帮助吧,二来从写文章中锻炼下自己,三来通过写文章对自己增加自信;如果写得不好,欢迎拍砖,我会虚心向博客园高手牛人们学习请教;如果觉得写得还可以,谢谢大家的支持了:)
今天开始介绍命令模式,并且将利用命令模式设计一个简单的在线编辑器。
概述
在各种各样的行为实现中,行为请求者与行为实现者紧密耦合,当每增加一个行为实现的时候,行为请求者必须增加一个对行为的处理,这样就需要大量改动请求者的操作,显然这样不利于维护和扩展。为了让行为请求者和行为实现者解耦,可以将行为封装为一个命令对象,但需要处理行为时,只要请求者知道命令对象,它本身不需要知道命令对象都做些什么,命令对象负责执行 接收者 的真正实现,这样就达到二者之间松耦合的目的。
定义
命令模式是将请求封装成对象,这可以让你使用不同请求、队列,或者日志请求来参数化其他对象。命令模式也可以支持撤销操作。
类图
实例分析
现在开始利用命令模式来应用到一个在线编辑器的场景中,并且详细分析一下:
这里先给大家看下效果图(兼容多浏览器):
这里我引用了我的第4篇-组合模式的菜单例子进行改进;将命令模式和组合模式相结合的方式来阐述例子。
其中主菜单包括:
- 文件(子菜单:新建、导出、退出)
- 编辑(子菜单:剪切、复制、粘贴、删除)
- 格式(子菜单:字体、字号、加粗、斜体、下划线、位置、编号、字体颜色)
- 插入(子菜单:插入链接、插入图片),操作(撤销、重做、切换HTML)
- 自定义格式(子菜单:格式1)
- 帮助(子菜单:关于作者)
编辑器的这些功能实际上很常用。最后点击“得到HTML值”的按钮,可以得到HTML的内容,这里就可以按照您的需要来存储编辑器内容;文章的最后我将附上源代码。
1. 首先添加一个ICommand.js文件,其中定义一个Command接口:
var
ICommand
=
new
Interface(
"
ICommand
"
, [[
"
execute
"
]]);
其中execute作为Command执行的接口方法;
关于接口的定义,可以参考我的第0篇-面向对象基础以及接口和继承类的实现的实现。
2. 添加一个ConcreteCommand.js文件,作为继承ICommand接口的所有具体实现类:
因为子菜单中的每个按钮都可作为一个具体实现类,那么我以“加粗”按钮为例,就可以得到:
//
加粗
function
onBoldCommand() {
Interface.registerImplements(
this
, ICommand);
}
onBoldCommand.prototype.execute
=
function
() {
var
editor
=
window.frames[
"
HtmlEditor
"
];
editor.document.execCommand(
"
Bold
"
,
false
,
false
);
editor.focus();
};
其中Interface.registerImplements(this, ICommand);说明它继承于ICommand接口,而execute方法作为基于接口方法的具体实现,这里实现了文档加粗功能;
3. 现在要创建“主菜单”和“子菜单”,Menu类和MenuItem类,注意这里“子菜单”也可能是Menu类,比如“格式”主菜单下的“位置”下面还包括下级菜单“居左对齐”,“居中对齐”,“居右对齐”等等,所以作为“叶子”结点的菜单就以MenuItem类来实现:
Menu类的实现如下:
function
Menu(text, title, href) {
this
.menuComponents
=
new
Array();
this
.text
=
text;
this
.title
=
title;
this
.href
=
href;
Interface.registerImplements(
this
, MenuComponent);
}
Menu.prototype
=
{
getElement :
function
() {
if
(
this
.menuComponents.length
==
0
)
{
throw
new
Error(
this
.text
+
"
菜单下没有子菜单
"
);
}
var
liElement
=
document.createElement(
"
li
"
);
liElement.className
=
"
Menu-WithChildren
"
;
liElement.title
=
this
.title;
var
anchor
=
document.createElement(
"
a
"
);
anchor.className
=
"
Menu-Link
"
;
anchor.href
=
this
.href;
liElement.appendChild(anchor);
anchor.innerHTML
=
this
.text;
var
ulElement
=
document.createElement(
"
ul
"
);
liElement.appendChild(ulElement);
for
(
var
i
=
0
, len
=
this
.menuComponents.length; i
<
len; i
++
)
{
ulElement.appendChild(
this
.menuComponents[i].getElement());
}
return
liElement;
},
add :
function
(component) {
this
.menuComponents.push(component);
},
remove :
function
(component) {
for
(
var
i
=
0
, len
=
this
.menuComponents.length; i
<
len; i
++
)
{
if
(
this
.menuComponents[i]
==
component)
{
this
.menuComponents.splice(i,
1
);
break
;
}
}
},
removeAt :
function
(index) {
if
(
this
.menuComponents.length
<=
index)
{
this
.menuComponents.splice(index,
1
);
}
else
{
throw
new
Error(
"
索引操作数组超过上限
"
);
}
}
}
Menu继承于MenuComponent接口(var MenuComponent = new Interface("MenuComponent", [["getElement"]]);),并且在上一篇组合模式讲过,它作为Composite复合元素。
MenuItem的实现如下:
function
MenuItem(text, title, href, command) {
this
.text
=
text;
this
.title
=
title;
this
.href
=
href;
this
.command
=
command;
Interface.registerImplements(
this
, MenuComponent);
}
MenuItem.prototype
=
{
getElement :
function
() {
var
liElement
=
document.createElement(
"
li
"
);
liElement.className
=
"
Menu-Leaf
"
;
liElement.title
=
this
.title;
var
anchor
=
document.createElement(
"
a
"
);
anchor.href
=
this
.href;
liElement.appendChild(anchor);
anchor.innerHTML
=
this
.text;
var
command
=
this
.command;
addEvent(anchor,
"
click
"
,
function
(){
command.execute();
});
return
liElement;
}
}
其中参数command是作为传递进来的“命令对象”,比如前面的onBoldCommand的“加粗”命令对象,并且通过方法getElement()实现菜单按钮的点击触发达到命令对象的触发请求,而MenuItem不需要知道命令对象的具体实现;
addEvent(anchor, "click", function(){
command.execute();
});
通过这句代码来添加点击事件从而执行命令对象的实现;
4. 既然有“主菜单”,“子菜单”,那么就需要有个“导航条”来“挂接”它们了,这里我添加了一个MenuBar对象,它作为一个初始化菜单显示和所有命令按钮行为实现方法的“容器”,代码如下:
var
MenuBar
=
{
list :
new
Array(),
add :
function
(component) {
this
.list.push(component);
},
show :
function
(container) {
var
ulElement
=
document.createElement(
"
ul
"
);
ulElement.className
=
"
Menu
"
;
for
(
var
i
=
0
, len
=
this
.list.length; i
<
len; i
++
) {
ulElement.appendChild(
this
.list[i].getElement());
}
document.getElementById(container).appendChild(ulElement);
}
}
通过show方法进行初始化菜单显示和所有命令按钮行为的实现方法;
5. 现在利用命令模式来进行在线编辑器的实现,新建HTML页面,在window.onload方法中实现:
var
file_menu
=
new
Menu(
"
文件
"
,
"
文件
"
,
"
#
"
);
file_menu.add(
new
MenuItem(
"
新建
"
,
"
新建
"
,
"
#
"
,
new
onNewCommand()));
file_menu.add(
new
MenuItem(
"
导出
"
,
"
导出
"
,
"
#
"
,
new
onExportCommand()));
file_menu.add(
new
MenuItem(
"
退出
"
,
"
退出
"
,
"
#
"
,
new
onExitCommand()));
var
edit_menu
=
new
Menu(
"
编辑
"
,
"
编辑
"
,
"
#
"
);
edit_menu.add(
new
MenuItem(
"
剪切
"
,
"
剪切
"
,
"
#
"
,
new
onCutCommand()));
edit_menu.add(
new
MenuItem(
"
复制
"
,
"
复制
"
,
"
#
"
,
new
onCopyCommand()));
edit_menu.add(
new
MenuItem(
"
粘贴
"
,
"
粘贴
"
,
"
#
"
,
new
onPasteCommand()));
edit_menu.add(
new
MenuItem(
"
删除
"
,
"
删除
"
,
"
#
"
,
new
onDeleteCommand()));
var
format_menu
=
new
Menu(
"
格式
"
,
"
格式
"
,
"
#
"
);
format_menu.add(
new
MenuItem(
"
字体
"
,
"
字体
"
,
"
#
"
,
new
onFontFaceCommand()));
format_menu.add(
new
MenuItem(
"
字号
"
,
"
字号
"
,
"
#
"
,
new
onFontSizeCommand()));
format_menu.add(
new
MenuItem(
"
加粗
"
,
"
加粗
"
,
"
#
"
,
new
onBoldCommand()));
format_menu.add(
new
MenuItem(
"
斜体
"
,
"
斜体
"
,
"
#
"
,
new
onItalicCommand()));
format_menu.add(
new
MenuItem(
"
下划线
"
,
"
下划线
"
,
"
#
"
,
new
onUnderlineCommand()));
var
format_menu_1
=
new
Menu(
"
位置
"
,
"
位置
"
,
"
#
"
);
format_menu_1.add(
new
MenuItem(
"
居左对齐
"
,
"
居左对齐
"
,
"
#
"
,
new
onLeftCommand()));
format_menu_1.add(
new
MenuItem(
"
居中对齐
"
,
"
居中对齐
"
,
"
#
"
,
new
onCenterCommand()));
format_menu_1.add(
new
MenuItem(
"
居右对齐
"
,
"
居右对齐
"
,
"
#
"
,
new
onRightCommand()));
format_menu_1.add(
new
MenuItem(
"
减少缩进
"
,
"
减少缩进
"
,
"
#
"
,
new
onOutdentCommand()));
format_menu_1.add(
new
MenuItem(
"
增加缩进
"
,
"
增加缩进
"
,
"
#
"
,
new
onIndentCommand()));
format_menu.add(format_menu_1);
var
format_menu_2
=
new
Menu(
"
编号
"
,
"
编号
"
,
"
#
"
);
format_menu_2.add(
new
MenuItem(
"
数字编号
"
,
"
数字编号
"
,
"
#
"
,
new
onOrderedCommand()));
format_menu_2.add(
new
MenuItem(
"
项目编号
"
,
"
项目编号
"
,
"
#
"
,
new
onUnorderedCommand()));
format_menu.add(format_menu_2);
var
format_menu_3
=
new
Menu(
"
字体颜色
"
,
"
字体颜色
"
,
"
#
"
);
format_menu_3.add(
new
MenuItem(
"
前景颜色
"
,
"
前景颜色
"
,
"
#
"
,
new
onForeColorCommand()));
format_menu_3.add(
new
MenuItem(
"
背景颜色
"
,
"
背景颜色
"
,
"
#
"
,
new
onBackColorCommand()));
format_menu.add(format_menu_3);
var
insert_menu
=
new
Menu(
"
插入
"
,
"
插入
"
,
"
#
"
);
insert_menu.add(
new
MenuItem(
"
插入链接
"
,
"
插入链接
"
,
"
#
"
,
new
onLinkCommand()));
insert_menu.add(
new
MenuItem(
"
插入图片
"
,
"
插入图片
"
,
"
#
"
,
new
onImageCommand()));
var
opr_menu
=
new
Menu(
"
操作
"
,
"
操作
"
,
"
#
"
);
opr_menu.add(
new
MenuItem(
"
撤销
"
,
"
撤销
"
,
"
#
"
,
new
onUndoCommand()));
opr_menu.add(
new
MenuItem(
"
重做
"
,
"
重做
"
,
"
#
"
,
new
onRedoCommand()));
opr_menu.add(
new
MenuItem(
"
切换HTML
"
,
"
切换HTML
"
,
"
#
"
,
new
onToHtmlCommand()));
var
custom_menu
=
new
Menu(
"
自定义格式
"
,
"
自定义格式
"
,
"
#
"
);
custom_menu.add(
new
MenuItem(
"
格式1
"
,
"
加粗+斜体+下划线
"
,
"
#
"
,
new
onMacro1Command(
new
onBoldCommand(),
new
onItalicCommand(),
new
onUnderlineCommand())));
var
help_menu
=
new
Menu(
"
帮助
"
,
"
帮助
"
,
"
#
"
);
help_menu.add(
new
MenuItem(
"
关于作者
"
,
"
关于作者
"
,
"
#
"
,
new
onAuthorCommand()));
MenuBar.add(file_menu);
MenuBar.add(edit_menu);
MenuBar.add(format_menu);
MenuBar.add(insert_menu);
MenuBar.add(opr_menu);
MenuBar.add(custom_menu);
MenuBar.add(help_menu);
MenuBar.show(
"
main_container
"
);
各个命令对象作为命令参数传递给对应的子菜单项对象中,其中我们发现有个onMacro1Command的命令对象,它里面包含一系列的其他单命令对象,这个菜单按钮属于“自定义格式”。顾名思义,这里执行一个新的命令,它包括一连串的单命令,“加粗+斜体+下划线”,onMacro1Command类实现如下:
function
onMacro1Command() {
this
.commands
=
new
Array();
for
(
var
i
=
0
, len
=
arguments.length; i
<
len; i
++
)
{
this
.commands.push(arguments[i]);
}
Interface.registerImplements(
this
, ICommand);
}
onMacro1Command.prototype.execute
=
function
() {
for
(
var
i
=
0
, len
=
this
.commands.length; i
<
len; i
++
)
{
this
.commands[i].execute();
}
};
这里通过一个commands数组存储这一系列的命令对象,当onMacro1Command对象执行execute方法时,就一次性地执行数组中的所有命令对象;
6. 还有其他一些JS函数介绍下:
function
addEvent(target, event_type, handler) {
if
(target.addEventListener)
target.addEventListener(event_type, handler,
false
);
else
if
(target.attachEvent)
target.attachEvent(
"
on
"
+
event_type, handler);
else
target[
"
on
"
+
event_type]
=
handler;
}
//
弹出DIV层
function
showDiver(str, width, height) {
var
iWidth
=
width;
var
iHeight
=
height;
document.getElementById(
"
diver
"
).style.width
=
iWidth
+
"
px
"
;
document.getElementById(
"
diver
"
).style.height
=
iHeight
+
"
px
"
;
document.getElementById(
"
diver
"
).style.left
=
(document.body.clientWidth
-
iWidth)
/
2 + "px";
document.getElementById(
"
diver
"
).style.top
=
(document.body.clientHeight
-
iHeight)
/
2 + "px";
document.getElementById(
"
diver
"
).style.display
=
"
inline
"
;
document.getElementById(
"
divMore
"
).innerHTML
=
str;
}
//
关闭DIV层
function
closeDiver() {
document.getElementById(
"
diver
"
).style.display
=
"
none
"
;
document.getElementById(
"
divMore
"
).innerHTML
=
""
;
}
其中addEvent方法为了兼容各个浏览器的绑定事件的实现。
7. 至于ConcreteCommand各种命令类的实现,请下载源代码自己查看研究吧,这里不进行讲述了。
附:源代码下载
总结
该篇文章用Javascript设计命令模式的思路,实现一个简单的在线编辑器。
本篇到此为止,谢谢大家阅读!
最后祝:大家在新的一年里,工作顺利,事业进步,牛年牛运!Fighting!!!
参考文献:《Head First Design Pattern》
《Professional Javascript Design Patterns》
本系列文章转载时请注明出处,谢谢合作!
相关系列文章:
Javascript乱弹设计模式系列(6) - 单件模式(Singleton)
Javascript乱弹设计模式系列(5) - 命令模式(Command)
Javascript乱弹设计模式系列(4) - 组合模式(Composite)
Javascript乱弹设计模式系列(3) - 装饰者模式(Decorator)
Javascript乱弹设计模式系列(2) - 抽象工厂以及工厂方法模式(Factory)
Javascript乱弹设计模式系列(1) - 观察者模式(Observer)
Javascript乱弹设计模式系列(0) - 面向对象基础以及接口和继承类的实现