深入理解javascript动态插入技术


Posted in Javascript onNovember 12, 2013

最近发现各大类库都能利用div.innerHTML=HTML片断来生成节点元素,再把它们插入到目标元素的各个位置上。这东西实际上就是insertAdjacentHTML,但是IE可恶的innerHTML把这优势变成劣势。首先innerHTML会把里面的某些位置的空白去掉,见下面运行框的结果:

<!doctype html>
<html dir="ltr" lang="zh-CN">
    <head>
        <meta charset="utf-8" />
        <title>
            IE的innerHTML By 司徒正美
        </title>
        <script type="text/javascript">
            window.onload = function() {
                var div = document.createElement("div");
                div.innerHTML = "   <td>    <b>司徒</b>正美         </td>        "
                alert("|" + div.innerHTML + "|");
                var c = div.childNodes;
                alert("生成的节点个数  " + c.length);
                for(var i=0,n=c.length;i<n;i++){
                      alert(c[i].nodeType);
                      if(c[i].nodeType === 1){
                          alert(":: "+c[i].childNodes.length);
                      }
                }        
            }
        </script>
    </head>
    <body>
        <p id="p">
        </p>
    </body>

</html>

另一个可恶的地方是,在IE中以下元素的innerHTML是只读的:col、 colgroup、frameset、html、 head、style、table、tbody、 tfoot、 thead、title 与 tr。为了收拾它们,Ext特意弄了个insertIntoTable。insertIntoTable就是利用DOM的insertBefore与appendChild来添加,情况基本同jQuery。不过jQuery是完全依赖这两个方法,Ext还使用了insertAdjacentHTML。为了提高效率,所有类库都不约而同地使用了文档碎片。基本流程都是通过div.innerHTML提取出节点,然后转移到文档碎片上,然后用insertBefore与appendChild插入节点。对于火狐,Ext还使用了createContextualFragment解析文本,直接插入其目标位置上。显然,Ext的比jQuery是快许多的。不过jQuery的插入的不单是HTML片断,还有各种节点与jQuery对象。下面重温一下jQuery的工作流程吧。

append: function() { 
  //传入arguments对象,true为要对表格进行特殊处理,回调函数 
  return this.domManip(arguments, true, function(elem){ 
    if (this.nodeType == 1) 
      this.appendChild( elem ); 
  }); 
}, 
domManip: function( args, table, callback ) { 
  if ( this[0] ) {//如果存在元素节点 
    var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), 
    //注意这里是传入三个参数 
    scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ), 
    first = fragment.firstChild;     if ( first ) 
      for ( var i = 0, l = this.length; i < l; i++ ) 
        callback.call( root(this[i], first), this.length > 1 || i > 0 ? 
      fragment.cloneNode(true) : fragment ); 
    if ( scripts ) 
      jQuery.each( scripts, evalScript ); 
  } 
  return this; 
  function root( elem, cur ) { 
    return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ? 
      (elem.getElementsByTagName("tbody")[0] || 
      elem.appendChild(elem.ownerDocument.createElement("tbody"))) : 
      elem; 
  } 
} 
//elems为arguments对象,context为document对象,fragment为空的文档碎片 
clean: function( elems, context, fragment ) { 
  context = context || document; 
  // !context.createElement fails in IE with an error but returns typeof 'object' 
  if ( typeof context.createElement === "undefined" ) 
  //确保context为文档对象 
    context = context.ownerDocument || context[0] && context[0].ownerDocument || document; 
  // If a single string is passed in and it's a single tag 
  // just do a createElement and skip the rest 
  //如果文档对象里面只有一个标签,如<div> 
  //我们大概可能是在外面这样调用它$(this).append("<div>") 
  //这时就直接把它里面的元素名取出来,用document.createElement("div")创建后放进数组返回 
  if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) { 
    var match = /^<(\w+)\s*\/?>$/.exec(elems[0]); 
    if ( match ) 
      return [ context.createElement( match[1] ) ]; 
  } 
  //利用一个div的innerHTML创建众节点 
  var ret = [], scripts = [], div = context.createElement("div"); 
  //如果我们是在外面这样添加$(this).append("<td>表格1</td>","<td>表格1</td>","<td>表格1</td>") 
  //jQuery.each按它的第四种支分方式(没有参数,有length)遍历aguments对象,callback.call( value, i, value ) 
  jQuery.each(elems, function(i, elem){//i为索引,elem为arguments对象里的元素 
    if ( typeof elem === "number" ) 
      elem += ''; 
    if ( !elem ) 
      return; 
    // Convert html string into DOM nodes 
    if ( typeof elem === "string" ) { 
      // Fix "XHTML"-style tags in all browsers 
      elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ 
        return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? 
          all : 
          front + "></" + tag + ">"; 
      }); 
      // Trim whitespace, otherwise indexOf won't work as expected 
      var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase(); 
      var wrap = 
        // option or optgroup 
        !tags.indexOf("<opt") && 
        [ 1, "<select multiple='multiple'>", "</select>" ] || 
        !tags.indexOf("<leg") && 
        [ 1, "<fieldset>", "</fieldset>" ] || 
        tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && 
        [ 1, "<table>", "</table>" ] || 
        !tags.indexOf("<tr") && 
        [ 2, "<table><tbody>", "</tbody></table>" ] || 
        // <thead> matched above 
      (!tags.indexOf("<td") || !tags.indexOf("<th")) && 
        [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] || 
        !tags.indexOf("<col") && 
        [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] || 
        // IE can't serialize <link> and <script> tags normally 
        !jQuery.support.htmlSerialize &&//用于创建link元素 
      [ 1, "div<div>", "</div>" ] || 
        [ 0, "", "" ]; 
      // Go to html and back, then peel off extra wrappers 
      div.innerHTML = wrap[1] + elem + wrap[2];//比如"<table><tbody><tr>" +<td>表格1</td>+"</tr></tbody></table>" 
      // Move to the right depth 
      while ( wrap[0]-- ) 
        div = div.lastChild; 
      //处理IE自动插入tbody,如我们使用$('<thead></thead>')创建HTML片断,它应该返回 
      //'<thead></thead>',而IE会返回'<thead></thead><tbody></tbody>' 
      if ( !jQuery.support.tbody ) { 
        // String was a <table>, *may* have spurious <tbody> 
        var hasBody = /<tbody/i.test(elem), 
        tbody = !tags.indexOf("<table") && !hasBody ? 
          div.firstChild && div.firstChild.childNodes : 
          // String was a bare <thead> or <tfoot> 
        wrap[1] == "<table>" && !hasBody ? 
          div.childNodes : 
          []; 
        for ( var j = tbody.length - 1; j >= 0 ; --j ) 
        //如果是自动插入的里面肯定没有内容 
          if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) 
            tbody[ j ].parentNode.removeChild( tbody[ j ] ); 
      } 
      // IE completely kills leading whitespace when innerHTML is used 
      if ( !jQuery.support.leadingWhitespace && /^\s/.test( elem ) ) 
        div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild ); 
     //把所有节点做成纯数组 
      elem = jQuery.makeArray( div.childNodes ); 
    } 
    if ( elem.nodeType ) 
      ret.push( elem ); 
    else
    //全并两个数组,merge方法会处理IE下object元素下消失了的param元素 
      ret = jQuery.merge( ret, elem ); 
  }); 
  if ( fragment ) { 
    for ( var i = 0; ret[i]; i++ ) { 
      //如果第一层的childNodes就有script元素节点,就用scripts把它们收集起来,供后面用globalEval动态执行 
      if ( jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) { 
        scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] ); 
      } else { 
        //遍历各层节点,收集script元素节点 
        if ( ret[i].nodeType === 1 ) 
          ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) ); 
        fragment.appendChild( ret[i] ); 
      } 
    } 
    return scripts;//由于动态插入是传入三个参数,因此这里就返回了 
  } 
  return ret; 
},

深入理解javascript动态插入技术

真是复杂的让人掉眼泪!不过jQuery的实现并不太高明,它把插入的东西统统用clean转换为节点集合,再把它们放到一个文档碎片中,然后用appendChild与insertBefore插入它们。在除了火狐外,其他浏览器都支持insertAdjactentXXX家族的今日,应该好好利用这些原生API。下面是Ext利用insertAdjactentHTML等方法实现的DomHelper方法,官网给出的数据:

深入理解javascript动态插入技术

这数据有点老了,而且最新3.03早就解决了在IE table插入内容的诟病(table,tbody,tr等的innerHTML都是只读,insertAdjactentHTML,pasteHTML等方法都无法修改其内容,要用又慢又标准的DOM方法才行,Ext的早期版本就在这里遭遇滑铁卢了)。可以看出,结合insertAdjactentHTML与文档碎片后,IE6插入节点的速度也得到难以置信的提升,直逼火狐。基于它,Ext开发了四个分支方法insertBefore、insertAfter、insertFirst、append,分别对应jQuery的before、after、prepend与append。不过,jQuery还把这几个方法巧妙地调换了调用者与传入参数,衍生出insertBefore、insertAfter、prependTo与appendTo这几个方法。但不管怎么说,jQuery这样一刀切的做法实现令人不敢苛同。下面是在火狐中实现insertAdjactentXXX家族的一个版本:

(function() { 
    if ('HTMLElement' in this) { 
        if('insertAdjacentHTML' in HTMLElement.prototype) { 
            return
        } 
    } else { 
        return
    }     function insert(w, n) { 
        switch(w.toUpperCase()) { 
        case 'BEFOREEND' : 
            this.appendChild(n) 
            break
        case 'BEFOREBEGIN' : 
            this.parentNode.insertBefore(n, this) 
            break
        case 'AFTERBEGIN' : 
            this.insertBefore(n, this.childNodes[0]) 
            break
        case 'AFTEREND' : 
            this.parentNode.insertBefore(n, this.nextSibling) 
            break
        } 
    } 
    function insertAdjacentText(w, t) { 
        insert.call(this, w, document.createTextNode(t || '')) 
    } 
    function insertAdjacentHTML(w, h) { 
        var r = document.createRange() 
        r.selectNode(this) 
        insert.call(this, w, r.createContextualFragment(h)) 
    } 
    function insertAdjacentElement(w, n) { 
        insert.call(this, w, n) 
        return n 
    } 
    HTMLElement.prototype.insertAdjacentText = insertAdjacentText 
    HTMLElement.prototype.insertAdjacentHTML = insertAdjacentHTML 
    HTMLElement.prototype.insertAdjacentElement = insertAdjacentElement 
})()

我们可以利用它设计出更快更合理的动态插入方法。下面是我的一些实现:

//四个插入方法,对应insertAdjactentHTML的四个插入位置,名字就套用jQuery的 
//stuff可以为字符串,各种节点或dom对象(一个类数组对象,便于链式操作!) 
//代码比jQuery的实现简洁漂亮吧! 
    append:function(stuff){ 
        return  dom.batch(this,function(el){ 
            dom.insert(el,stuff,"beforeEnd"); 
        }); 
    }, 
    prepend:function(stuff){ 
        return  dom.batch(this,function(el){ 
            dom.insert(el,stuff,"afterBegin"); 
        }); 
    }, 
    before:function(stuff){ 
        return  dom.batch(this,function(el){ 
            dom.insert(el,stuff,"beforeBegin"); 
        }); 
    }, 
    after:function(stuff){ 
        return  dom.batch(this,function(el){ 
            dom.insert(el,stuff,"afterEnd"); 
        }); 
    }

它们里面都是调用了两个静态方法,batch与insert。由于dom对象是类数组对象,我仿效jQuery那样为它实现了几个重要迭代器,forEach、map与filter等。一个dom对象包含复数个DOM元素,我们就可以用forEach遍历它们,执行其中的回调方法。

batch:function(els,callback){ 
    els.forEach(callback); 
    return els;//链式操作 
},

insert方法执行jQuery的domManip方法相应的机能(dojo则为place方法),但insert方法每次处理一个元素节点,不像jQuery那样处理一组元素节点。群集处理已经由上面batch方法分离出去了。

insert : function(el,stuff,where){ 
     //定义两个全局的东西,提供内部方法调用 
     var doc = el.ownerDocument || dom.doc, 
     fragment = doc.createDocumentFragment(); 
     if(stuff.version){//如果是dom对象,则把它里面的元素节点移到文档碎片中 
         stuff.forEach(function(el){ 
             fragment.appendChild(el); 
         }) 
         stuff = fragment; 
     } 
     //供火狐与IE部分元素调用 
     dom._insertAdjacentElement = function(el,node,where){ 
         switch (where){ 
             case 'beforeBegin': 
                 el.parentNode.insertBefore(node,el) 
                 break; 
             case 'afterBegin': 
                 el.insertBefore(node,el.firstChild); 
                 break; 
             case 'beforeEnd': 
                 el.appendChild(node); 
                 break; 
             case 'afterEnd': 
                 if (el.nextSibling) el.parentNode.insertBefore(node,el.nextSibling); 
                 else el.parentNode.appendChild(node); 
                 break; 
         } 
     }; 
      //供火狐调用 
     dom._insertAdjacentHTML = function(el,htmlStr,where){ 
         var range = doc.createRange(); 
         switch (where) { 
             case "beforeBegin"://before 
                 range.setStartBefore(el); 
                 break; 
             case "afterBegin"://after 
                 range.selectNodeContents(el); 
                 range.collapse(true); 
                 break; 
             case "beforeEnd"://append 
                 range.selectNodeContents(el); 
                 range.collapse(false); 
                 break; 
             case "afterEnd"://prepend 
                 range.setStartAfter(el); 
                 break; 
         } 
         var parsedHTML = range.createContextualFragment(htmlStr); 
         dom._insertAdjacentElement(el,parsedHTML,where); 
     }; 
     //以下元素的innerHTML在IE中是只读的,调用insertAdjacentElement进行插入就会出错 
     // col, colgroup, frameset, html, head, style, title,table, tbody, tfoot, thead, 与tr; 
     dom._insertAdjacentIEFix = function(el,htmlStr,where){ 
         var parsedHTML = dom.parseHTML(htmlStr,fragment); 
         dom._insertAdjacentElement(el,parsedHTML,where) 
     }; 
     //如果是节点则复制一份 
     stuff = stuff.nodeType ?  stuff.cloneNode(true) : stuff; 
     if (el.insertAdjacentHTML) {//ie,chrome,opera,safari都已实现insertAdjactentXXX家族 
         try{//适合用于opera,safari,chrome与IE 
             el['insertAdjacent'+ (stuff.nodeType ? 'Element':'HTML')](where,stuff); 
         }catch(e){ 
             //IE的某些元素调用insertAdjacentXXX可能出错,因此使用此补丁 
             dom._insertAdjacentIEFix(el,stuff,where); 
         }      
     }else{ 
         //火狐专用 
         dom['_insertAdjacent'+ (stuff.nodeType ? 'Element':'HTML')](el,stuff,where); 
     } 
 }

insert方法在实现火狐插入操作中,使用了W3C DOM Range对象的一些罕见方法,具体可到火狐官网查看。下面实现把字符串转换为节点,利用innerHTML这个伟大的方法。Prototype.js称之为_getContentFromAnonymousElement,但有许多问题,dojo称之为_toDom,mootools的Element.Properties.html,jQuery的clean。Ext没有这东西,它只支持传入HTML片断的insertAdjacentHTML方法,不支持传入元素节点的insertAdjacentElement。但有时,我们需要插入文本节点(并不包裹于元素节点之中),这时我们就需要用文档碎片做容器了,insert方法出场了。

parseHTML : function(htmlStr, fragment){ 
    var div = dom.doc.createElement("div"), 
    reSingleTag =  /^<(\w+)\s*\/?>$/;//匹配单个标签,如<li> 
    htmlStr += ''; 
    if(reSingleTag.test(htmlStr)){//如果str为单个标签 
        return  [dom.doc.createElement(RegExp.$1)] 
    } 
    var tagWrap = { 
        option: ["select"], 
        optgroup: ["select"], 
        tbody: ["table"], 
        thead: ["table"], 
        tfoot: ["table"], 
        tr: ["table", "tbody"], 
        td: ["table", "tbody", "tr"], 
        th: ["table", "thead", "tr"], 
        legend: ["fieldset"], 
        caption: ["table"], 
        colgroup: ["table"], 
        col: ["table", "colgroup"], 
        li: ["ul"], 
        link:["div"] 
    }; 
    for(var param in tagWrap){ 
        var tw = tagWrap[param]; 
        switch (param) { 
            case "option":tw.pre  = '<select multiple="multiple">'; break; 
            case "link": tw.pre  = 'fixbug<div>';  break; 
            default : tw.pre  =   "<" + tw.join("><") + ">"; 
        } 
        tw.post = "</" + tw.reverse().join("></") + ">"; 
    } 
    var reMultiTag = /<\s*([\w\:]+)/,//匹配一对标签或多个标签,如<li></li>,li 
    match = htmlStr.match(reMultiTag), 
    tag = match ? match[1].toLowerCase() : "";//解析为<li,li 
    if(match && tagWrap[tag]){ 
        var wrap = tagWrap[tag]; 
        div.innerHTML = wrap.pre + htmlStr + wrap.post; 
        n = wrap.length; 
        while(--n >= 0)//返回我们已经添加的内容 
            div = div.lastChild; 
    }else{ 
        div.innerHTML = htmlStr; 
    } 
    //处理IE自动插入tbody,如我们使用dom.parseHTML('<thead></thead>')转换HTML片断,它应该返回 
    //'<thead></thead>',而IE会返回'<thead></thead><tbody></tbody>' 
    //亦即,在标准浏览器中return div.children.length会返回1,IE会返回2 
    if(dom.feature.autoInsertTbody && !!tagWrap[tag]){ 
        var ownInsert = tagWrap[tag].join('').indexOf("tbody") !== -1,//我们插入的 
        tbody = div.getElementsByTagName("tbody"), 
        autoInsert = tbody.length > 0;//IE插入的 
        if(!ownInsert && autoInsert){ 
            for(var i=0,n=tbody.length;i<n;i++){ 
                if(!tbody[i].childNodes.length )//如果是自动插入的里面肯定没有内容 
                    tbody[i].parentNode.removeChild( tbody[i] ); 
            } 
        } 
    } 
    if (dom.feature.autoRemoveBlank && /^\s/.test(htmlStr) ) 
        div.insertBefore( dom.doc.createTextNode(htmlStr.match(/^\s*/)[0] ), div.firstChild ); 
    if (fragment) { 
        var firstChild; 
        while((firstChild = div.firstChild)){ // 将div上的节点转移到文档碎片上! 
            fragment.appendChild(firstChild); 
        } 
        return fragment; 
    } 
    return div.children; 
}

嘛,基本上就是这样,运行起来比jQuery快许多,代码实现也算优美,至少没有像jQuery那样乱成一团。jQuery还有四个反转方法。下面是jQuery的实现:

jQuery.each({ 
    appendTo: "append", 
    prependTo: "prepend", 
    insertBefore: "before", 
    insertAfter: "after", 
    replaceAll: "replaceWith"
}, function(name, original){ 
    jQuery.fn[ name ] = function( selector ) {//插入物(html,元素节点,jQuery对象) 
        var ret = [], insert = jQuery( selector );//将插入转变为jQuery对象 
        for ( var i = 0, l = insert.length; i < l; i++ ) { 
            var elems = (i > 0 ? this.clone(true) : this).get(); 
            jQuery.fn[ original ].apply( jQuery(insert[i]), elems );//调用四个已实现的插入方法 
            ret = ret.concat( elems ); 
        } 
        return this.pushStack( ret, name, selector );//由于没有把链式操作的代码分离出去,需要自行实现 
    }; 
});

我的实现:

dom.each({ 
    appendTo: 'append', 
    prependTo: 'prepend', 
    insertBefore: 'before', 
    insertAfter: 'after'
},function(method,name){ 
    dom.prototype[name] = function(stuff){ 
        return dom(stuff)[method](this); 
    }; 
});

大致的代码都给出,大家可以各取所需。

Javascript 相关文章推荐
checkbox 多选框 联动实现代码
Oct 22 Javascript
使用JS 清空File控件的路径值
Jul 08 Javascript
jquery实现带复选框的表格行选中删除时高亮显示
Aug 01 Javascript
js判断iframe内的网页是否滚动到底部触发事件
Mar 18 Javascript
使用typeof判断function是否存在于上下文
Aug 14 Javascript
推荐10个2014年最佳的jQuery视频插件
Nov 12 Javascript
Node.js 中exports 和 module.exports 的区别
Mar 14 Javascript
JavaScript for循环 if判断语句(学习笔记)
Oct 11 Javascript
vue单页面在微信下只能分享落地页的解决方案
Apr 15 Javascript
聊聊鉴权那些事(推荐)
Aug 22 Javascript
在JavaScript中实现链式调用的实现
Dec 24 Javascript
浅谈JavaScript中等号、双等号、 三等号的区别
Aug 06 Javascript
在ASP.NET中使用JavaScript脚本的方法
Nov 12 #Javascript
JS常用正则表达式总结
Nov 12 #Javascript
jquery 删除cookie失效的解决方法
Nov 12 #Javascript
IE下window.onresize 多次调用与死循环bug处理方法介绍
Nov 12 #Javascript
JS获取键盘上任意按键的值(实例代码)
Nov 12 #Javascript
只需一行代码,轻松实现一个在线编辑器
Nov 12 #Javascript
JS中实现replaceAll的方法(实例代码)
Nov 12 #Javascript
You might like
[EPIC] Larva vs Flash ZvT @ Crossing Field [2017-10-09]
2020/03/17 星际争霸
PHP中is_file不能替代file_exists的理由
2014/03/04 PHP
smarty缓存用法分析
2014/12/16 PHP
php使用iconv中文截断问题的解决方法
2015/02/11 PHP
PHP生成静态HTML页面最简单方法示例
2015/04/09 PHP
php中使用GD库做验证码
2016/03/31 PHP
浅谈PHP拦截器之__set()与__get()的理解与使用方法
2016/10/18 PHP
Zend Framework动作控制器用法示例
2016/12/09 PHP
利用PHPExcel读取Excel的数据和导出数据到Excel
2017/05/12 PHP
laravel实现图片上传预览,及编辑时可更换图片,并实时变化的例子
2019/11/14 PHP
突发奇想的一个jquery插件
2010/11/19 Javascript
jquery获取table中的某行全部td的内容方法
2013/03/08 Javascript
JQuery操作三大控件(下拉,单选,复选)的方法
2013/08/06 Javascript
JavaScript1.6数组新特性介绍以及JQuery的几个工具方法
2013/12/06 Javascript
JS函数重载的解决方案
2014/05/13 Javascript
javascript实现的一个随机点名功能
2014/08/26 Javascript
javascript使用prototype完成单继承
2014/12/24 Javascript
AngularJS语法详解(续)
2015/01/23 Javascript
JS实现IE状态栏文字缩放效果代码
2015/10/24 Javascript
zTree插件下拉树使用入门教程
2016/04/11 Javascript
Node.js中常规的文件操作总结
2016/10/13 Javascript
Bootstrap中datetimepicker使用小结
2016/12/28 Javascript
JS简单判断字符在另一个字符串中出现次数的2种常用方法
2017/04/20 Javascript
node使用Koa2搭建web项目的方法
2017/10/17 Javascript
vue.js实现的经典计算器/科学计算器功能示例
2018/07/11 Javascript
js如何实现元素曝光上报
2019/08/07 Javascript
OpenLayers3实现图层控件功能
2020/09/25 Javascript
[02:40]DOTA2英雄基础教程 炼金术士
2013/12/23 DOTA
Python的lambda匿名函数的简单介绍
2013/04/25 Python
python获取糗百图片代码实例
2013/12/18 Python
python去掉行尾的换行符方法
2017/01/04 Python
初一生物教学反思
2014/01/18 职场文书
yy司仪主持词
2014/03/22 职场文书
党支部特色活动方案
2014/08/20 职场文书
巾帼文明岗汇报材料
2014/12/24 职场文书
JavaGUI模仿QQ聊天功能完整版
2021/07/04 Java/Android