深入理解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 相关文章推荐
Javascript MD4
Dec 20 Javascript
JAVASCRIPT keycode总结
Feb 04 Javascript
jQuery Tab插件 用于在Tab中显示iframe,附源码和详细说明
Jun 27 Javascript
node.js中的fs.chown方法使用说明
Dec 16 Javascript
jQuery增加、删除及修改select option的方法
Aug 19 Javascript
jQuery移除或禁用html元素点击事件常用方法小结
Feb 10 Javascript
JavaScript实现的前端AES加密解密功能【基于CryptoJS】
Aug 28 Javascript
vue项目在安卓低版本机显示空白的原因分析(两种)
Sep 04 Javascript
JQuery判断radio单选框是否选中并获取值的方法
Jan 17 jQuery
layui实现checkbox的目录树tree的例子
Sep 12 Javascript
CountUp.js实现数字滚动增值效果
Oct 17 Javascript
微信小程序实现星星评分效果
Nov 01 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
CI框架开发新浪微博登录接口源码完整版
2014/05/28 PHP
php表单加入Token防止重复提交的方法分析
2016/10/10 PHP
PHP常用函数之获取汉字首字母功能示例
2019/10/21 PHP
JavaScript监测ActiveX控件是否已经安装过的代码
2008/09/02 Javascript
js chrome浏览器判断代码
2010/03/28 Javascript
js获取页面传来参数的方法
2014/09/06 Javascript
让JavaScript和其它资源并发下载的方法
2014/10/16 Javascript
基于jQuery实现仿淘宝套餐选择插件
2015/03/04 Javascript
jQuery实现仿百度首页滑动伸缩展开的添加服务效果代码
2015/09/09 Javascript
Javascript基础回顾之(二) js作用域
2017/01/31 Javascript
jQuery 防止相同的事件快速重复触发方法
2018/02/08 jQuery
使用 Vue cli 3.0 构建自定义组件库的方法
2019/04/30 Javascript
vue实现跨域的方法分析
2019/05/21 Javascript
基于Bootstrap和JQuery实现动态打开和关闭tab页的实例代码
2019/06/10 jQuery
javascript中undefined的本质解析
2019/07/31 Javascript
node.js中module模块的功能理解与用法实例分析
2020/02/14 Javascript
Vue-resource安装过程及使用方法解析
2020/07/21 Javascript
[45:10]NB vs Liquid Supermajor小组赛 A组胜者组决赛 BO3 第二场 6.2
2018/06/04 DOTA
从零学python系列之从文件读取和保存数据
2014/05/23 Python
将python代码和注释分离的方法
2018/04/21 Python
Python实现的生产者、消费者问题完整实例
2018/05/30 Python
python实现比对美团接口返回数据和本地mongo数据是否一致示例
2019/08/09 Python
解决Tensorflow占用GPU显存问题
2020/02/03 Python
python zip,lambda,map函数代码实例
2020/04/04 Python
Python3之外部文件调用Django程序操作model等文件实现方式
2020/04/07 Python
Keras 使用 Lambda层详解
2020/06/10 Python
浅谈Python协程
2020/06/17 Python
python字典的值可以修改吗
2020/06/29 Python
Python利用Faiss库实现ANN近邻搜索的方法详解
2020/08/03 Python
影视制作岗位职责
2013/12/04 职场文书
如何写一份好的自荐信
2014/01/02 职场文书
《曹刿论战》教学反思
2014/03/02 职场文书
大学生学习2014年全国两会心得体会
2014/03/12 职场文书
城管年度个人总结
2015/02/28 职场文书
感恩教育观后感
2015/06/17 职场文书
学校运动会开幕词
2016/03/03 职场文书