【前端模板之路】二、人肉非智举,让代码帮我们写代码才是王道

简介:

写在前面

在前面一篇文章【前端模板之路】一、重构的兄弟说:我才不想看你的代码!把HTML给我交出来!中,我们举了一个人肉各种createElement的例子,那繁琐程度绝对是惨绝人寰。人生本就苦短,每天加班又占据了不少时间,这么折腾下去,还让人怎么活。面对这种场景,我们该怎么做。

无需复杂的构建工具,仅几个简单的工具函数,帮我们告别重复意义的劳动:让代码帮我们写代码!

从最简单的例子说起

让代码帮我们写代码,似乎很豪迈的话,但相信部分童鞋听着还是有些丈二和尚摸不着头脑。那我们暂且抛开这句不知所云的话,来看看下面这个例子。一段简单的HTML

<h3>小卡的测试号</h3>

现在让我们来“人肉”创建下这个节点,无非就createElement、createTextNode两个操作

var nick = document.createElement('h3');  // 元素节点
var nickTxt = document.createTextNode('小卡的测试号');  // 文本节点
nick.appendChild(nickTxt);

现在让我们在节点上加多点内容

<h3 class="title">小卡的测试号</h3>

继续我们的人肉操作,与上文类似,只是多了个setAttribute的步骤

var nick = document.createElement('h3');  // 元素节点
nick.setAttribute('class', 'title');  // 设置节点属性
var nickTxt = document.createTextNode('小卡的测试号');  // 文本节点
nick.appendChild(nickTxt);

很简单的例子,到这里为止。可能你有这样的疑惑:这样的例子跟我们的“让代码帮我们写代码”有什么关系。是的,一切的谜底就在其中,请往下看。

创建节点三部曲——你究竟看到了什么

从上面的代码,我们可以看出,人肉创建一个节点——我们用节点P来表示,包含以下三个步骤:

1. 创建节点P

2. 给节点P设置属性

3. 创建节点P的子节点C

其中,步骤3 创建子节点,跟创建一个节点P的过程完全一致,也就是说,这里的关键,是dom树的遍历过程

那么,我们将要做什么

上面我们已经简单分析了一个节点创建的几个逻辑步骤,那么,现在说下,“让代码帮我们写代码”究竟是什么意思。很简单,那就是:随便给一段HTML文本,自动生成上面那堆createElement、createTextNode、setAttribute

整体目标已经明确,现在我们来分解下子任务:

1. 节点创建(createElement...)自动化

2. 属性设置(setAttribute...)自动化

3. 代码自动格式化

一点必要的准备工作

上面我们提到,代码写代码,实现的关键点在于dom树的遍历。现在我们手头上只有一段HTML文本(字符串),如何遍历?正则什么的有点高端不敢碰,来点奇淫技巧,先把文本转成dom节点,现在,我们的HTML文本就转成可遍历的节点了,即wrapper.childeNodes

 

var wrapper = document.createElement('div');
wrapper.innerHTML = html;

var childNodes = wrapper.childNodes;  // 我们真正要遍历的节点

 

目标一:节点创建自动化

废话不多说,直接上代码,逻辑很简单,关键是区分三种不同的节点类型即可。实际上节点类型不止三种,但动态创建过程中常见的也就Element、TextNode两种,如果有需要,可自行补充

复制代码
function createNode(childNode){
    var arr  = [],
        childNodeName = getName( childNode );  // 一个工具方法,返回一个变量名,实现细节先不管它

    switch(childNode.nodeType){
        case 3:  // 文本节点
            arr = arr.concat( 'var ' + childNodeName + ' = ' + 'document.createTextNode("'+ childNode.nodeValue +'")' );
            break;
        case 8:  // 注释
            arr = arr.concat( 'var ' + childNodeName + ' = ' + 'document.createComment("'+ childNode.nodeValue +'")' );
            break;
        default:  // 其他
            arr.push( 'var '+ childNodeName + ' = ' + 'document.createElement("'+ childNode.nodeName.toLowerCase() +'")' );
            break;
    }
    return arr;
}
复制代码

 

目标二:属性设置自动化

直接上代码,我们知道,节点的属性存在一个叫做attributes的特性里,attributes是个NamedNodeMap,名字很奇怪,知道下面几点即可:

1. attributes里存的是节点的属性,举例来说,上面class="title",这个class就是节点的属性

2. attributes是个类数组,可遍历,有个length属性,表示节点属性的个数

3. 每个attributes元素是个对象,该对象有两个关键的属性,即name(节点属性名)和value(节点属性值),如下面代码所示

于是我们得到如下代码

复制代码
function createAttribute(childNode, childNodeName, tabNum){
    var attributes = childNode.attributes,
        arr = [],
        childNodeName = getName( childNode );

    for(var j=0; j<attributes.length; j++){
        var attribute = attributes[j];
        arr.push( childNodeName +'.setAttribute("' + attribute.name + '", "' + attribute.value + '");' );
    }
    return arr;
}
复制代码

 之前在jQuery源码分析系列里写了篇文章jQuery源码-jQuery.fn.attr与jQuery.fn.prop,看了你就会知道,上面这段代码其实是有坑的,但是先不引入额外的复杂度,有时间我再补充(程序员最大的谎言:TODO)

目标三:代码自动格式化 

代码多了,一堆createElement、appendChild神马的,一下就把人看晕了,完全看不出层级结构,这个时候加上合理的缩进是很有必要的,缩进的数目跟dom树的深度成正比,直接看个例子

复制代码
var div_1 = document.createElement("div")
div_1.setAttribute("nick", "casepr");
    var h1_1 = document.createElement("h1")
    h1_1.setAttribute("class", "title");
    div_1.appendChild( h1_1 )
        var text_1 = document.createTextNode("标题")
        h1_1.appendChild( text_1 )
复制代码

这里只贴个简单的工具方法,比如repeat('a', 3)返回 'aaa'

// 返回num个str拼成的字符串
function repeat(str, num){
    return new Array(num+1).join(str);
}

终极奥义——完整的代码实现

简单把代码封装了下,需要关注的是Util.getCode方法,举个例子Util.getCode('<h3 class="title">小卡的测试号</h3>'),看看输出是什么 :)

代码注释写得算是比较详细了,不缀述~~

复制代码
var Util = (function(){

    var map = {};
    //console.log( arr.join('\n') );

    /**
     * 核心方法,遍历一个节点,返回创建这个节点需要的完整步骤
     * 
     * @param  {HTMLElement} parentNode           dom节点
     * @param  {Boolean} needCreateParentNode true: 需要添加parentNode本身的创建步骤;false:不需要
     * @param  {Number} tabNum               tab缩进的数目
     * @param  {String} parentNodeName       我们已经为parentNode生成的变量名,如无,则为空字符串
     * @return {Array}                      创建parentNode所需要的完整步骤
     */
    function getCodeRecursively(parentNode, needCreateParentNode, tabNum, parentNodeName){
        
        var childNodes = parentNode.childNodes,
            i =0,
            len = childNodes.length,
            arr = [];

        parentNodeName = parentNodeName || getName(parentNode);
        if( needCreateParentNode ){
            arr = arr.concat( createNode(parentNode, parentNodeName, tabNum) );    // 1、create父节点,给父节点setAttribute    
        }
        
        ++tabNum;

        for(; i<len; i++){
            
            var childNode = childNodes[i];

            if( shouldTravel(childNode) ){
                
                var childNodeName = getName(childNode);

                arr = arr.concat( createNode(childNode, childNodeName, tabNum) );
                arr.push( repeat('\t', tabNum) + parentNodeName +'.appendChild( '+ childNodeName +' )' );    // 3、塞子节点
                arr = arr.concat( getCodeRecursively( childNode, false, tabNum, childNodeName ) );
            }
        }
        return arr;
    }

    /**
     * 创建属性
     * @param  {HTMLElement} node     节点
     * @param  {String} variName 为node起的变量名
     * @param  {Number} tabNum   缩进数目
     * @return {Array}          详细步骤
     */
    function createAttribute(node, variName, tabNum){
        var attributes = node.attributes,
            arr = [];
        for(var j=0; j<attributes.length; j++){
            var attribute = attributes[j];
            arr.push( repeat('\t', tabNum) + variName +'.setAttribute("' + attribute.name + '", "' + attribute.value + '");' );
        }
        return arr;
    }

    /**
     * 创建节点
     * @param  {HTMLElement} node     节点
     * @param  {String} variName 为node起的变量名
     * @param  {Number} tabNum   缩进数目
     * @return {Array}          详细步骤
     */
    function createNode(node, variName, tabNum){
        var arr  = [];

        switch(node.nodeType){
            case 3:  // 文本节点
                arr = arr.concat( repeat('\t', tabNum) + 'var ' + variName + ' = ' + 'document.createTextNode("'+ node.nodeValue +'")' );
                break;
            case 8:  // 注释
                arr = arr.concat( repeat('\t', tabNum) + 'var ' + variName + ' = ' + 'document.createComment("'+ node.nodeValue +'")' );
                break;
            default:  // 其他
                arr.push( repeat('\t', tabNum) + 'var '+ variName + ' = ' + 'document.createElement("'+ node.nodeName.toLowerCase() +'")' );
                arr = arr.concat( createAttribute(node, variName, tabNum) );
                break;
        }
        return arr;
    }

    /**
     * 是否应该遍历节点(这个方法是否恰当??)
     * @param  {HTMLElement} node 节点
     * @return {Boolean}      true:应该遍历;false:不应该遍历
     */
    function shouldTravel( node ){
        return node.nodeType==1 || node.nodeValue.trim()!='';
    }

    /**
     * 返回一个变量名,
     * @param  {HTMLElement} node 
     * @return {String}      变量名,格式为 nodeName_XXX,其中nodeName是节点名的小写,XX为数字,例: div_1
     */
    function getName(node){
        var nodeName = node.nodeName.toLowerCase().replace('#', '');
        if(!map[nodeName]){
            map[nodeName] = 1;
        }else{
            map[nodeName]++;
        }
        return nodeName+ '_' +map[nodeName];
    }

    /**
     * 返回num个str拼成的字符串
     * @param  {String} str 一段字符
     * @param  {Number} num 重复次数
     * @return {String}     num个str拼成的字符串
     */
    function repeat(str, num){
        return new Array(num+1).join(str);
    }

    return {
        /**
         * 根据html字符串,返回这段字符串对应的dom节点的完整创建过程
         * @param  {String} html HTML字符串
         * @return {Array}      创建步骤
         */
        getCode: function(html){
            var arr = [],
                // map = {},
                i = 0,
                len = 0,
                childNodes = [];
map = {};
var wrapper = document.createElement('div'); wrapper.innerHTML = html; childNodes = wrapper.childNodes; // 这段代码也是可以提取的,TODO吧 len = childNodes.length; for(; i<len; i++){ var childNode = childNodes[i]; if(shouldTravel(childNode)){ arr = arr.concat( getCodeRecursively(childNode, true, 0, '') ); } } return arr; } }; })();
复制代码

你让我肿么相信你——测试用例

附上简短测试用例一枚:

复制代码
var html = '<div nick="casepr">\
                <h1 class="title">标题</h1>\
                纯文本节点\
                <!--注释-->\
                <div class="content">\
                    <div class="preview">预览</div>\
                    <div class="content">正文</div>\
                </div>\
                <label for="box" class="select">选择:</label>\
                <input type="checkbox" id="box" name="box" checked="checked" />\
            </div>';
console.log( Util.getCode(html).join('\n') );
复制代码

输出结果:

复制代码
var div_1 = document.createElement("div")
div_1.setAttribute("nick", "casepr");
    var h1_1 = document.createElement("h1")
    h1_1.setAttribute("class", "title");
    div_1.appendChild( h1_1 )
        var text_1 = document.createTextNode("标题")
        h1_1.appendChild( text_1 )
    var text_2 = document.createTextNode("                纯文本节点                ")
    div_1.appendChild( text_2 )
    var comment_1 = document.createComment("注释")
    div_1.appendChild( comment_1 )
    var div_2 = document.createElement("div")
    div_2.setAttribute("class", "content");
    div_1.appendChild( div_2 )
        var div_3 = document.createElement("div")
        div_3.setAttribute("class", "preview");
        div_2.appendChild( div_3 )
            var text_3 = document.createTextNode("预览")
            div_3.appendChild( text_3 )
        var div_4 = document.createElement("div")
        div_4.setAttribute("class", "content");
        div_2.appendChild( div_4 )
            var text_4 = document.createTextNode("正文")
            div_4.appendChild( text_4 )
    var label_1 = document.createElement("label")
    label_1.setAttribute("for", "box");
    label_1.setAttribute("class", "select");
    div_1.appendChild( label_1 )
        var text_5 = document.createTextNode("选择:")
        label_1.appendChild( text_5 )
    var input_1 = document.createElement("input")
    input_1.setAttribute("type", "checkbox");
    input_1.setAttribute("id", "box");
    input_1.setAttribute("name", "box");
    input_1.setAttribute("checked", "checked");
    div_1.appendChild( input_1 ) 
复制代码

写在后面

罗里八嗦地写了这么多,终于实现了本文最前面提到的“让代码帮我们写代码”这个目的,实现原理很简单,代码也不复杂,不过真正调试的时候还是花了点时间。时间精力所限,代码难免有疏漏之处(不是无聊的谦词,比如“属性设置自动化”那里的坑还没填。。。),如发现,请指出!!!!!!!

目录
相关文章
|
27天前
|
JavaScript 前端开发 Docker
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
在使用 Deno 构建项目时,生成的可执行文件体积较大,通常接近 100 MB,而 Node.js 构建的项目体积则要小得多。这是由于 Deno 包含了完整的 V8 引擎和运行时,使其能够在目标设备上独立运行,无需额外安装依赖。尽管体积较大,但 Deno 提供了更好的安全性和部署便利性。通过裁剪功能、使用压缩工具等方法,可以优化可执行文件的体积。
110 3
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
|
7天前
|
Web App开发 缓存 监控
前端性能优化实战:从代码到部署的全面策略
前端性能优化实战:从代码到部署的全面策略
13 1
|
10天前
|
前端开发 JavaScript
前端界的革命:掌握这些新技术,让你的代码简洁到让人惊叹!
前端技术的快速发展带来了许多令人惊叹的新特性。ES6及其后续版本引入了箭头函数、模板字符串等简洁语法,极大减少了代码冗余。React通过虚拟DOM和组件化思想,提高了代码的可维护性和效率。Webpack等构建工具通过模块化和代码分割,优化了应用性能和加载速度。这些新技术正引领前端开发的革命,使代码更加简洁、高效、可维护。
14 2
|
10天前
|
前端开发 JavaScript 测试技术
前端工程师的必修课:如何写出优雅、可维护的代码?
前端工程作为数字世界的门面,编写优雅、可维护的代码至关重要。本文从命名规范、模块化设计、注释与文档、遵循最佳实践四个方面,提供了提升代码质量的方法。通过清晰的命名、合理的模块划分、详细的注释和持续的学习,前端工程师可以写出高效且易于维护的代码,为项目的成功打下坚实基础。
23 2
|
15天前
|
监控 前端开发 JavaScript
前端开发的终极奥义:如何让你的代码既快又美,还不易出错?
【10月更文挑战第31天】前端开发是一个充满挑战与机遇的领域,本文从性能优化、代码美化和错误处理三个方面,探讨了如何提升代码的效率、可读性和健壮性。通过减少DOM操作、懒加载、使用Web Workers等方法提升性能;遵循命名规范、保持一致的缩进与空行、添加注释与文档,让代码更易读;通过输入验证、try-catch捕获异常、日志与监控,增强代码的健壮性。追求代码的“快、美、稳”,是每个前端开发者的目标。
31 3
|
17天前
|
前端开发 JavaScript 开发者
前端开发的终极技巧:如何让你的代码既简洁又高效,还能减少bug?
【10月更文挑战第30天】前端开发充满挑战与创新,如何编写简洁高效且少bug的代码是开发者关注的重点。本文介绍五大技巧:1. 模块化,提高代码复用性;2. 组件化,降低代码耦合度;3. 使用现代框架,提高开发效率;4. 统一代码规范,降低沟通成本;5. 利用工具,优化代码质量。掌握这些技巧,让前端开发更高效。
36 1
|
24天前
|
前端开发 JavaScript 开发者
揭秘前端高手的秘密武器:深度解析递归组件与动态组件的奥妙,让你代码效率翻倍!
【10月更文挑战第23天】在Web开发中,组件化已成为主流。本文深入探讨了递归组件与动态组件的概念、应用及实现方式。递归组件通过在组件内部调用自身,适用于处理层级结构数据,如菜单和树形控件。动态组件则根据数据变化动态切换组件显示,适用于不同业务逻辑下的组件展示。通过示例,展示了这两种组件的实现方法及其在实际开发中的应用价值。
29 1
|
1月前
|
前端开发 JavaScript 开发者
利用代码分割优化前端性能:高级技巧与实践
【10月更文挑战第2天】在现代Web开发中,代码分割是优化前端性能的关键技术,可显著减少页面加载时间。本文详细探讨了代码分割的基本原理及其实现方法,包括自动与手动分割、预加载与预取、动态导入及按需加载CSS等高级技巧,旨在帮助开发者提升Web应用性能,改善用户体验。
|
1月前
|
前端开发 小程序 JavaScript
信前端里的循环显示如何编写代码?
信前端里的循环显示如何编写代码?
71 5
|
2月前
|
前端开发
前端使用xlsx模板导出表格
【9月更文挑战第10天】在前端使用 `xlsx` 模板导出表格需完成以下步骤:首先引入 `xlsx` 库,准备数据与模板;接着读取模板文件,选择特定工作表并填充数据;最后将填充后的表格转换为二进制数据,创建 Blob 对象并触发下载。整个过程中可根据需求调整模板格式和数据填充方式。
下一篇
无影云桌面