深入理解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 相关文章推荐
同域jQuery(跨)iframe操作DOM(示例代码)
Dec 13 Javascript
JavaScript实现找出字符串中第一个不重复的字符
Sep 03 Javascript
js省市联动效果完整实例代码
Dec 09 Javascript
简单谈谈Vue 模板各类数据绑定
Sep 25 Javascript
动态加载css方法实现和深入解析
Jan 18 Javascript
微信小程序 request接口的封装实例代码
Apr 26 Javascript
JavaScript学习笔记之函数记忆
Sep 06 Javascript
关于Vue单页面骨架屏实践记录
Dec 13 Javascript
Angular实现的进度条功能示例
Feb 18 Javascript
用vue写一个仿简书的轮播图的示例代码
Mar 13 Javascript
基于JavaScript实现瀑布流布局
Aug 15 Javascript
小程序从手动埋点到自动埋点的实现方法
Jan 24 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
有关PHP性能优化的介绍
2013/06/20 PHP
ThinkPHP模板循环输出Volist标签用法实例详解
2016/03/23 PHP
PHP实现文件上传下载实例
2016/10/18 PHP
jQuery的三种$()
2009/12/30 Javascript
一个网马的tips实现分析
2010/11/28 Javascript
让AJAX不依赖后端接口实现方案
2012/12/03 Javascript
js之onload事件的一点使用心得
2013/08/14 Javascript
javascript动态修改Li节点值的方法
2015/01/20 Javascript
js实现同一页面可多次调用的图片幻灯切换效果
2015/02/28 Javascript
jQuery插件pagination实现分页特效
2015/04/12 Javascript
实例讲解避免javascript冲突的方法
2016/01/03 Javascript
Bootstrap modal 多弹窗之叠加关闭阴影遮罩问题的解决方法
2017/02/27 Javascript
Node.js 多进程处理CPU密集任务的实现
2019/05/26 Javascript
vue 使用element-ui中的Notification自定义按钮并实现关闭功能及如何处理多个通知
2019/08/17 Javascript
vue3修改link标签默认icon无效问题详解
2019/10/09 Javascript
JavaScript原型式继承实现方法
2019/11/06 Javascript
JS精确判断数据类型代码实例
2019/12/18 Javascript
vue 内联样式style中的background用法说明
2020/08/05 Javascript
微信小程序实现弹幕墙(祝福墙)
2020/11/18 Javascript
利用python获得时间的实例说明
2013/03/25 Python
深入理解python try异常处理机制
2016/06/01 Python
Python读取系统文件夹内所有文件并统计数量的方法
2018/10/23 Python
Python 从一个文件中调用另一个文件的类方法
2019/01/10 Python
Python判断两个文件是否相同与两个文本进行相同项筛选的方法
2019/03/01 Python
用Python批量把文件复制到另一个文件夹的实现方法
2019/08/16 Python
matlab 计算灰度图像的一阶矩,二阶矩,三阶矩实例
2020/04/22 Python
Python 保存加载mat格式文件的示例代码
2020/08/04 Python
python 使用openpyxl读取excel数据
2021/02/18 Python
css3 pointer-events 介绍详解
2017/09/18 HTML / CSS
HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题
2021/01/19 HTML / CSS
Michael Kors香港官网:美国奢侈品品牌
2019/12/26 全球购物
大学生党员自我评价范文
2014/04/09 职场文书
养成教育经验材料
2014/05/26 职场文书
六年级学生评语大全
2014/12/26 职场文书
大学迎新生欢迎词
2015/09/29 职场文书
python自动化测试通过日志3分钟定位bug
2021/11/20 Python