zoukankan      html  css  js  c++  java
  • XMPP即时通讯协议使用(七)——利用Strophe实现WebIM及strophe.plugins插件使用

    Strophe简介与Openfire配置

    Strophe.js是为XMPP写的一个js类库。因为http协议本身不能实现持久连接,所以strophe利用BOSH模拟实现持久连接。

    官方文档:

    http://strophe.im/strophejs/doc/1.2.15/files/strophe-js.html

    https://stackoverflow.com/questions/17311901/strophe-js-giving-authfail-status-always

    Strophe操作相关插件:

    https://github.com/ggozad/strophe.plugins/

    https://github.com/gm19900510/strophejs-plugins

    连接状态常量
    发起一个链接后,会返回一个连接状态
    Status.ERROR
    错误
    Status.CONNECTING
    正在创建连接
    Status.CONNFAIL
    连接创建失败
    Status.AUTHENTICATING
    正在验证
    Status.AUTHFAIL
    验证失败
    Status.CONNECTED
    连接创建成功
    Status.DISCONNECTED
    连接已关闭
    Status.DISCONNECTING
    连接正在关闭
    XMPP服务器通常会实现BOSH扩展,下面是Openfire和Tigase的BOSH默认URL:

    Openfire:http://host:7070/http-bind
    Tigase:http://host:5280

    在使用Strophe.js的时候,需要使用对应的HTTP地址才能连接上XMPP服务器。

    如果使用Opnefire,还需要在管理后台配置一下:

    注意图中的pc-20170308pkrs是下图的服务器名称


    XMPP协议简介:

    XMPP服务器和客户端之间,是通过XML节(XML Stanza)来进行通讯。其中有三种非常重要的XML Stanza类型:<message>、<presence>、<iq>。

    <message>:

    聊天消息的发送和接收就是通过message节来实现。例如xxg1@host发送一条信息"你好"给xxg2@host,xxg1@host客户端会将下面的这段XML发送到XMPP服务器,服务器再推送给xxg2@host客户端。其中<message>的from属性是发送者,to属性是接收者,<body>子元素的内容就是聊天信息。

    <message from="a@pc-20170308pkrs" to="b@pc-20170308pkrs" type="chat">  
        <body>你好</body>  
    </message>
    <presence>:
    可用于表明用户的状态,例如用户状态改变成“Do not disturb”(“请勿打扰”),会向服务器发送:
    <presence from="xxg@host">  
        <status>Do not disturb</status>  
        <priority>0</priority>  
        <show>dnd</show>  
    </presence>
    <iq>:

    iq即Info/Query,采用“请求-响应”机制,类似于HTTP的机制。下面的例子是客户端通过<iq>请求获取联系人,XMPP服务器将结果返回:

    客户端请求获取联系人:
    <iq from='a@pc-20170308pkrs' id='bv1bs71f' type='get'>  
        <query xmlns='jabber:iq:roster'/>  
    </iq>
    服务器结果返回:
      
    <iq to='a@pc-20170308pkrs' id='bv1bs71f' type='result'>  
        <query xmlns='jabber:iq:roster'>  
            <item jid='b@pc-20170308pkrs'/>  
            <item jid='c@pc-20170308pkrs'/>  
        </query>  
    </iq> 

    构建WebIM

    新建echobot.html

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
      <title>Strophe.js Echobot Example</title>
      <script type='text/javascript'
              src='jquery-1.9.1.min.js'></script>
      <script type='text/javascript'
              src='strophe.min.js'></script>
      <script type='text/javascript'
              src='echobot.js'></script>
    </head>
    <body>
        JID:<input type="text" id="input-jid" value="gm@pc-20170308pkrs">
        <br>
        密码:<input type="password" id="input-pwd">
        <br>
        <button id="btn-login">登录</button>
        <div id="msg" style="height: 400px;  400px; overflow: scroll;"></div>
        联系人JID:
        <input type="text" id="input-contacts">
        <br>
        消息:
        <br>
        <textarea id="input-msg" cols="30" rows="4"></textarea>
        <br>
        <button id="btn-send">发送</button>
    </body>
    </html>

    新建echobot.js

    var BOSH_SERVICE = 'http://pc-20170308pkrs:7070/http-bind/';
    // XMPP连接
    var connection = null;
    
    
    // 当前状态是否连接
    var connected = false;
    
    
    // 当前登录的JID
    var jid = "";
    
    
    // 连接状态改变的事件
    function onConnect(status) {
        console.log('status: ' + status)
        if (status == Strophe.Status.CONNFAIL) {
            alert("连接失败!");
        } else if (status == Strophe.Status.AUTHFAIL) {
            alert("登录失败!");
        } else if (status == Strophe.Status.DISCONNECTED) {
            alert("连接断开!");
            connected = false;
        } else if (status == Strophe.Status.CONNECTED) {
            alert("连接成功,可以开始聊天了!");
    		console.log('pubsub',connection)
            connected = true;
    
    
            // 当接收到<message>节,调用onMessage回调函数
            connection.addHandler(onMessage, null, null, null, null, null);
    
    
            // 首先要发送一个<presence>给服务器(initial presence)
            connection.send($pres().tree());
    		
    	//获取订阅的主题信息
    	connection.pubsub.getSubscriptions(onMessage,5000);
    		
        }
    }
    
    
    
    
    
    
    // 接收到<message>
    function onMessage(msg) {
    	
        console.log('--- msg ---', msg);
    
    
        // 解析出<message>的from、type属性,以及body子元素
        var from = msg.getAttribute('from');
        var type = msg.getAttribute('type');
        var elems = msg.getElementsByTagName('body');
    
    
        if (type == "chat" && elems.length > 0) {
            var body = elems[0];
            $("#msg").append(from + ":<br>" + Strophe.getText(body) + "<br>")
        }
        return true;
    }
    
    
    $(document).ready(function() {
    
    
        // 通过BOSH连接XMPP服务器
        $('#btn-login').click(function() {
            if(!connected) {
                console.log('jid: ' + $("#input-jid").val());
                console.log('pwd: ' + $("#input-pwd").val());
                connection = new Strophe.Connection(BOSH_SERVICE);
                connection.connect($("#input-jid").val(), $("#input-pwd").val(), onConnect);
                jid = $("#input-jid").val();
            }
        });
    
    
        // 发送消息
        $("#btn-send").click(function() {
            if(connected) {
                if($("#input-contacts").val() == '') {
                    alert("请输入联系人!");
                    return;
                }
    
    
                // 创建一个<message>元素并发送
                var msg = $msg({
                    to: $("#input-contacts").val(),
                    from: jid, 
                    type: 'chat'
                }).c("body", null, $("#input-msg").val());
                connection.send(msg.tree());
    
    
                $("#msg").append(jid + ":<br>" + $("#input-msg").val() + "<br>");
                $("#input-msg").val('');
            } else {
                alert("请先登录!");
            }
        });
    });
    

    strophe.plugins插件使用(connection+“插件名称”+ “对应方法”)

    connection.pubsub.getSubscriptions(onMessage,5000);

    新建strophe.pubsub.js

    /*
        This program is distributed under the terms of the MIT license.
        Please see the LICENSE file for details.
        Copyright 2008, Stanziq  Inc.
        Overhauled in October 2009 by Liam Breck [How does this affect copyright?]
    */
    
    /** File: strophe.pubsub.js
     *  A Strophe plugin for XMPP Publish-Subscribe.
     *
     *  Provides Strophe.Connection.pubsub object,
     *  parially implementing XEP 0060.
     *
     *  Strophe.Builder.prototype methods should probably move to strophe.js
     */
    
    /** Function: Strophe.Builder.form
     *  Add an options form child element.
     *
     *  Does not change the current element.
     *
     *  Parameters:
     *    (String) ns - form namespace.
     *    (Object) options - form properties.
     *
     *  Returns:
     *    The Strophe.Builder object.
     */
    Strophe.Builder.prototype.form = function (ns, options)
    {
        var aX = this.node.appendChild(Strophe.xmlElement('x', {"xmlns": "jabber:x:data", "type": "submit"}));
        aX.appendChild(Strophe.xmlElement('field', {"var":"FORM_TYPE", "type": "hidden"}))
          .appendChild(Strophe.xmlElement('value'))
          .appendChild(Strophe.xmlTextNode(ns));
    
        for (var i in options) {
            aX.appendChild(Strophe.xmlElement('field', {"var": i}))
            .appendChild(Strophe.xmlElement('value'))
            .appendChild(Strophe.xmlTextNode(options[i]));
        }
        return this;
    };
    
    /** Function: Strophe.Builder.list
     *  Add many child elements.
     *
     *  Does not change the current element.
     *
     *  Parameters:
     *    (String) tag - tag name for children.
     *    (Array) array - list of objects with format:
     *          { attrs: { [string]:[string], ... }, // attributes of each tag element
     *             data: [string | XML_element] }    // contents of each tag element
     *
     *  Returns:
     *    The Strophe.Builder object.
     */
    Strophe.Builder.prototype.list = function (tag, array)
    {
        for (var i=0; i < array.length; ++i) {
            this.c(tag, array[i].attrs)
            this.node.appendChild(array[i].data.cloneNode
                                ? array[i].data.cloneNode(true)
                                : Strophe.xmlTextNode(array[i].data));
            this.up();
        }
        return this;
    };
    
    Strophe.Builder.prototype.children = function (object) {
        var key, value;
        for (key in object) {
            if (!object.hasOwnProperty(key)) continue;
            value = object[key];
            if (Array.isArray(value)) {
                this.list(key, value);
            } else if (typeof value === 'string') {
                this.c(key, {}, value);
            } else if (typeof value === 'number') {
                this.c(key, {}, ""+value);
            } else if (typeof value === 'object') {
                this.c(key).children(value).up();
            } else {
                this.c(key).up();
            }
        }
        return this;
    };
    
    // TODO Ideas Adding possible conf values?
    /* Extend Strophe.Connection to have member 'pubsub'.
     */
    Strophe.addConnectionPlugin('pubsub', {
    /*
    Extend connection object to have plugin name 'pubsub'.
    */
        _connection: null,
        _autoService: true,
        service: null,
        jid: null,
    
        //The plugin must have the init function.
        init: function(conn) {
    
            this._connection = conn;
    
            /*
            Function used to setup plugin.
            */
    
            /* extend name space
            *  NS.PUBSUB - XMPP Publish Subscribe namespace
            *              from XEP 60.
            *
            *  NS.PUBSUB_SUBSCRIBE_OPTIONS - XMPP pubsub
            *                                options namespace from XEP 60.
            */
            Strophe.addNamespace('PUBSUB',"http://jabber.org/protocol/pubsub");
            Strophe.addNamespace('PUBSUB_SUBSCRIBE_OPTIONS',
                                 Strophe.NS.PUBSUB+"#subscribe_options");
            Strophe.addNamespace('PUBSUB_ERRORS',Strophe.NS.PUBSUB+"#errors");
            Strophe.addNamespace('PUBSUB_EVENT',Strophe.NS.PUBSUB+"#event");
            Strophe.addNamespace('PUBSUB_OWNER',Strophe.NS.PUBSUB+"#owner");
            Strophe.addNamespace('PUBSUB_AUTO_CREATE',
                                 Strophe.NS.PUBSUB+"#auto-create");
            Strophe.addNamespace('PUBSUB_PUBLISH_OPTIONS',
                                 Strophe.NS.PUBSUB+"#publish-options");
            Strophe.addNamespace('PUBSUB_NODE_CONFIG',
                                 Strophe.NS.PUBSUB+"#node_config");
            Strophe.addNamespace('PUBSUB_CREATE_AND_CONFIGURE',
                                 Strophe.NS.PUBSUB+"#create-and-configure");
            Strophe.addNamespace('PUBSUB_SUBSCRIBE_AUTHORIZATION',
                                 Strophe.NS.PUBSUB+"#subscribe_authorization");
            Strophe.addNamespace('PUBSUB_GET_PENDING',
                                 Strophe.NS.PUBSUB+"#get-pending");
            Strophe.addNamespace('PUBSUB_MANAGE_SUBSCRIPTIONS',
                                 Strophe.NS.PUBSUB+"#manage-subscriptions");
            Strophe.addNamespace('PUBSUB_META_DATA',
                                 Strophe.NS.PUBSUB+"#meta-data");
            Strophe.addNamespace('ATOM', "http://www.w3.org/2005/Atom");
    
            if (conn.disco)
                conn.disco.addFeature(Strophe.NS.PUBSUB);
    
        },
    
        // Called by Strophe on connection event
        statusChanged: function (status, condition) {
            var that = this._connection;
            if (this._autoService && status === Strophe.Status.CONNECTED) {
                this.service =  'pubsub.'+Strophe.getDomainFromJid(that.jid);
                this.jid = that.jid;
            }
        },
    
        /***Function
        Parameters:
        (String) jid - The node owner's jid.
        (String) service - The name of the pubsub service.
        */
        connect: function (jid, service) {
            var that = this._connection;
            if (service === undefined) {
                service = jid;
                jid = undefined;
            }
            this.jid = jid || that.jid;
            this.service = service || null;
            this._autoService = false;
        },
    
        /***Function
        Create a pubsub node on the given service with the given node
        name.
        Parameters:
        (String) node -  The name of the pubsub node.
        (Dictionary) options -  The configuration options for the  node.
        (Function) call_back - Used to determine if node
        creation was sucessful.
        Returns:
        Iq id used to send subscription.
        */
        createNode: function(node,options, call_back) {
            var that = this._connection;
    
            var iqid = that.getUniqueId("pubsubcreatenode");
    
            var iq = $iq({from:this.jid, to:this.service, type:'set', id:iqid})
              .c('pubsub', {xmlns:Strophe.NS.PUBSUB})
              .c('create',{node:node});
            if(options) {
                iq.up().c('configure').form(Strophe.NS.PUBSUB_NODE_CONFIG, options);
            }
    
            that.addHandler(call_back, null, 'iq', null, iqid, null);
            that.send(iq.tree());
            return iqid;
        },
    
        /** Function: deleteNode
         *  Delete a pubsub node.
         *
         *  Parameters:
         *    (String) node -  The name of the pubsub node.
         *    (Function) call_back - Called on server response.
         *
         *  Returns:
         *    Iq id
         */
        deleteNode: function(node, call_back) {
            var that = this._connection;
            var iqid = that.getUniqueId("pubsubdeletenode");
    
            var iq = $iq({from:this.jid, to:this.service, type:'set', id:iqid})
              .c('pubsub', {xmlns:Strophe.NS.PUBSUB_OWNER})
              .c('delete', {node:node});
    
            that.addHandler(call_back, null, 'iq', null, iqid, null);
            that.send(iq.tree());
    
            return iqid;
        },
    
        /** Function
         *
         * Get all nodes that currently exist.
         *
         * Parameters:
         *   (Function) success - Used to determine if node creation was sucessful.
         *   (Function) error - Used to determine if node
         * creation had errors.
         */
        discoverNodes: function(success, error, timeout) {
    
            //ask for all nodes
            var iq = $iq({from:this.jid, to:this.service, type:'get'})
              .c('query', { xmlns:Strophe.NS.DISCO_ITEMS });
    
            return this._connection.sendIQ(iq.tree(),success, error, timeout);
        },
    
        /** Function: getConfig
         *  Get node configuration form.
         *
         *  Parameters:
         *    (String) node -  The name of the pubsub node.
         *    (Function) call_back - Receives config form.
         *
         *  Returns:
         *    Iq id
         */
        getConfig: function (node, call_back) {
            var that = this._connection;
            var iqid = that.getUniqueId("pubsubconfigurenode");
    
            var iq = $iq({from:this.jid, to:this.service, type:'get', id:iqid})
              .c('pubsub', {xmlns:Strophe.NS.PUBSUB_OWNER})
              .c('configure', {node:node});
    
            that.addHandler(call_back, null, 'iq', null, iqid, null);
            that.send(iq.tree());
    
            return iqid;
        },
    
        /**
         *  Parameters:
         *    (Function) call_back - Receives subscriptions.
         *
         *  http://xmpp.org/extensions/tmp/xep-0060-1.13.html
         *  8.3 Request Default Node Configuration Options
         *
         *  Returns:
         *    Iq id
         */
        getDefaultNodeConfig: function(call_back) {
            var that = this._connection;
            var iqid = that.getUniqueId("pubsubdefaultnodeconfig");
    
            var iq = $iq({from:this.jid, to:this.service, type:'get', id:iqid})
              .c('pubsub', {'xmlns':Strophe.NS.PUBSUB_OWNER})
              .c('default');
    
            that.addHandler(call_back, null, 'iq', null, iqid, null);
            that.send(iq.tree());
    
            return iqid;
        },
    
        /***Function
            Subscribe to a node in order to receive event items.
            Parameters:
            (String) node         - The name of the pubsub node.
            (Array) options       - The configuration options for the  node.
            (Function) event_cb   - Used to recieve subscription events.
            (Function) success    - callback function for successful node creation.
            (Function) error      - error callback function.
            (Boolean) barejid     - use barejid creation was sucessful.
            Returns:
            Iq id used to send subscription.
        */
        subscribe: function(node, options, event_cb, success, error, barejid) {
            var that = this._connection;
            var iqid = that.getUniqueId("subscribenode");
    
            var jid = this.jid;
            if(barejid)
                jid = Strophe.getBareJidFromJid(jid);
    
            var iq = $iq({from:this.jid, to:this.service, type:'set', id:iqid})
              .c('pubsub', { xmlns:Strophe.NS.PUBSUB })
              .c('subscribe', {'node':node, 'jid':jid});
            if(options) {
                iq.up().c('options').form(Strophe.NS.PUBSUB_SUBSCRIBE_OPTIONS, options);
            }
    
            //add the event handler to receive items
            that.addHandler(event_cb, null, 'message', null, null, null);
            that.sendIQ(iq.tree(), success, error);
            return iqid;
        },
    
        /***Function
            Unsubscribe from a node.
            Parameters:
            (String) node       - The name of the pubsub node.
            (Function) success  - callback function for successful node creation.
            (Function) error    - error callback function.
        */
        unsubscribe: function(node, jid, subid, success, error) {
            var that = this._connection;
            var iqid = that.getUniqueId("pubsubunsubscribenode");
    
            var iq = $iq({from:this.jid, to:this.service, type:'set', id:iqid})
              .c('pubsub', { xmlns:Strophe.NS.PUBSUB })
              .c('unsubscribe', {'node':node, 'jid':jid});
            if (subid) iq.attrs({subid:subid});
    
            that.sendIQ(iq.tree(), success, error);
            return iqid;
        },
    
        /***Function
        Publish and item to the given pubsub node.
        Parameters:
        (String) node -  The name of the pubsub node.
        (Array) items -  The list of items to be published.
        (Function) call_back - Used to determine if node
        creation was sucessful.
        */
        publish: function(node, items, call_back) {
            var that = this._connection;
            var iqid = that.getUniqueId("pubsubpublishnode");
    
            var iq = $iq({from:this.jid, to:this.service, type:'set', id:iqid})
              .c('pubsub', { xmlns:Strophe.NS.PUBSUB })
              .c('publish', { node:node, jid:this.jid })
              .list('item', items);
    
            that.addHandler(call_back, null, 'iq', null, iqid, null);
            that.send(iq.tree());
    
            return iqid;
        },
    
        /*Function: items
        Used to retrieve the persistent items from the pubsub node.
        */
        items: function(node, success, error, timeout) {
            //ask for all items
            var iq = $iq({from:this.jid, to:this.service, type:'get'})
              .c('pubsub', { xmlns:Strophe.NS.PUBSUB })
              .c('items', {node:node});
    
            return this._connection.sendIQ(iq.tree(), success, error, timeout);
        },
    
        /** Function: getSubscriptions
         *  Get subscriptions of a JID.
         *
         *  Parameters:
         *    (Function) call_back - Receives subscriptions.
         *
         *  http://xmpp.org/extensions/tmp/xep-0060-1.13.html
         *  5.6 Retrieve Subscriptions
         *
         *  Returns:
         *    Iq id
         */
        getSubscriptions: function(call_back, timeout) {
            var that = this._connection;
            var iqid = that.getUniqueId("pubsubsubscriptions");
    
            var iq = $iq({from:this.jid, to:this.service, type:'get', id:iqid})
              .c('pubsub', {'xmlns':Strophe.NS.PUBSUB})
              .c('subscriptions');
    
            that.addHandler(call_back, null, 'iq', null, iqid, null);
            that.send(iq.tree());
    		console.log('-- iq pubsub --',iq.tree())
            return iqid;
        },
    
        /** Function: getNodeSubscriptions
         *  Get node subscriptions of a JID.
         *
         *  Parameters:
         *    (Function) call_back - Receives subscriptions.
         *
         *  http://xmpp.org/extensions/tmp/xep-0060-1.13.html
         *  5.6 Retrieve Subscriptions
         *
         *  Returns:
         *    Iq id
         */
        getNodeSubscriptions: function(node, call_back) {
            var that = this._connection;
           var iqid = that.getUniqueId("pubsubsubscriptions");
    
           var iq = $iq({from:this.jid, to:this.service, type:'get', id:iqid})
             .c('pubsub', {'xmlns':Strophe.NS.PUBSUB_OWNER})
             .c('subscriptions', {'node':node});
    
           that.addHandler(call_back, null, 'iq', null, iqid, null);
           that.send(iq.tree());
    
           return iqid;
        },
    
        /** Function: getSubOptions
         *  Get subscription options form.
         *
         *  Parameters:
         *    (String) node -  The name of the pubsub node.
         *    (String) subid - The subscription id (optional).
         *    (Function) call_back - Receives options form.
         *
         *  Returns:
         *    Iq id
         */
        getSubOptions: function(node, subid, call_back) {
            var that = this._connection;
            var iqid = that.getUniqueId("pubsubsuboptions");
    
            var iq = $iq({from:this.jid, to:this.service, type:'get', id:iqid})
              .c('pubsub', {xmlns:Strophe.NS.PUBSUB})
              .c('options', {node:node, jid:this.jid});
            if (subid) iq.attrs({subid:subid});
    
            that.addHandler(call_back, null, 'iq', null, iqid, null);
            that.send(iq.tree());
    
            return iqid;
        },
    
        /**
         *  Parameters:
         *    (String) node -  The name of the pubsub node.
         *    (Function) call_back - Receives subscriptions.
         *
         *  http://xmpp.org/extensions/tmp/xep-0060-1.13.html
         *  8.9 Manage Affiliations - 8.9.1.1 Request
         *
         *  Returns:
         *    Iq id
         */
        getAffiliations: function(node, call_back) {
            var that = this._connection;
            var iqid = that.getUniqueId("pubsubaffiliations");
    
            if (typeof node === 'function') {
                call_back = node;
                node = undefined;
            }
    
            var attrs = {}, xmlns = {'xmlns':Strophe.NS.PUBSUB};
            if (node) {
                attrs.node = node;
                xmlns = {'xmlns':Strophe.NS.PUBSUB_OWNER};
            }
    
            var iq = $iq({from:this.jid, to:this.service, type:'get', id:iqid})
              .c('pubsub', xmlns).c('affiliations', attrs);
    
            that.addHandler(call_back, null, 'iq', null, iqid, null);
            that.send(iq.tree());
    
            return iqid;
        },
    
        /**
         *  Parameters:
         *    (String) node -  The name of the pubsub node.
         *    (Function) call_back - Receives subscriptions.
         *
         *  http://xmpp.org/extensions/tmp/xep-0060-1.13.html
         *  8.9.2 Modify Affiliation - 8.9.2.1 Request
         *
         *  Returns:
         *    Iq id
         */
        setAffiliation: function(node, jid, affiliation, call_back) {
            var that = this._connection;
            var iqid = thiat.getUniqueId("pubsubaffiliations");
    
            var iq = $iq({from:this.jid, to:this.service, type:'set', id:iqid})
              .c('pubsub', {'xmlns':Strophe.NS.PUBSUB_OWNER})
              .c('affiliations', {'node':node})
              .c('affiliation', {'jid':jid, 'affiliation':affiliation});
    
            that.addHandler(call_back, null, 'iq', null, iqid, null);
            that.send(iq.tree());
    
            return iqid;
        },
    
        /** Function: publishAtom
         */
        publishAtom: function(node, atoms, call_back) {
            if (!Array.isArray(atoms))
                atoms = [atoms];
    
            var i, atom, entries = [];
            for (i = 0; i < atoms.length; i++) {
                atom = atoms[i];
    
                atom.updated = atom.updated || (new Date()).toISOString();
                if (atom.published && atom.published.toISOString)
                    atom.published = atom.published.toISOString();
    
                entries.push({
                    data: $build("entry", { xmlns:Strophe.NS.ATOM })
                            .children(atom).tree(),
                    attrs:(atom.id ? { id:atom.id } : {}),
                });
            }
            return this.publish(node, entries, call_back);
        },
    
    });

    效果:


  • 相关阅读:
    用mvc实现增删查改
    hibernate Annotation版本的helloworld
    hibernate 级联操作
    Hibrenate一对一外键关联
    hibernate主键生成
    Action属性接收参数
    WildCard的使用
    Struts 路径分析以及是否一定要执行excute()方法
    学习Struts2经验总结
    基于Struts分层web框架,研究传值问题
  • 原文地址:https://www.cnblogs.com/gmhappy/p/9472406.html
Copyright © 2011-2022 走看看