zoukankan      html  css  js  c++  java
  • Javascript乱弹设计模式系列(5) - 命令模式(Command)

    前言

    博客园谈设计模式的文章很多,我也受益匪浅,包括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"falsefalse);
        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) - 面向对象基础以及接口和继承类的实现

  • 相关阅读:
    [leetcode]791. Custom Sort String自定义排序字符串
    [leetcode]304. Range Sum Query 2D
    [leetcode]339. Nested List Weight Sum嵌套列表加权和
    [leetcode]81. Search in Rotated Sorted Array II旋转过有序数组里找目标值II(有重)
    [leetcode]170. Two Sum III
    [leetcode]341. Flatten Nested List Iterator展开嵌套列表的迭代器
    [leetcode]445. Add Two Numbers II 两数相加II
    Node 连接mysql数据库
    Restful 表述性状态传递
    node Express 框架
  • 原文地址:https://www.cnblogs.com/liping13599168/p/1380880.html
Copyright © 2011-2022 走看看