• 一套代码小程序&Web&Native运行的探索02


    接上文:一套代码小程序&Web&Native运行的探索01,本文都是一些探索性为目的的研究学习,在最终版输出前,内中的内容可能会有点乱

    参考:

    https://github.com/fastCreator/MVVM

    https://www.tangshuang.net/3756.html

    https://www.cnblogs.com/kidney/p/8018226.html

    经过之前的学习,发现Vue其实与小程序框架相识度比较高,业内也有mpvue这种还比较成熟的方案了,我们这边依旧不着急去研究成熟的框架,现在看看自己能做到什么程度,最近也真正的开始接触了一些Vue的东西,里面的代码真的非常不错,研究学习了下Vue的结构,发现其实跟我们要的很类似,这里想要尝试初步的方案:提供Html模板->解析Html模板,其实这里就是Vue里面Parse部分的逻辑,一小部分代码,这样有很多Vue的代码可以借鉴,也变相的学习Vue的源码,一举两得,于是我们速度开始今天的学习

    首先,我们设置一个简单的目标:设置一段简单的小程序模板,当我们做完web版本后,他可以在小程序中运行

    <view class="c-row search-line" data-flag="start" ontap="clickHandler">
      <view class="c-span9 js-start search-line-txt">
        {{name}}</view>
    </view>
     1 Page({
     2   data: {
     3     name: 'hello world'
     4   },
     5   clickHandler: function () {
     6     this.setData({
     7       name: '叶小钗'
     8     })
     9   }
    10 })

    这里第一个关键便是将html模板转换为js代码,如果是之前我们直接会用这种代码:

     1 _.template = function (text, data, settings) {
     2   var render;
     3   settings = _.defaults({}, settings, _.templateSettings);
     4 
     5   // Combine delimiters into one regular expression via alternation.
     6   var matcher = new RegExp([
     7     (settings.escape || noMatch).source,
     8     (settings.interpolate || noMatch).source,
     9     (settings.evaluate || noMatch).source
    10   ].join('|') + '|$', 'g');
    11 
    12   // Compile the template source, escaping string literals appropriately.
    13   var index = 0;
    14   var source = "__p+='";
    15   text.replace(matcher, function (match, escape, interpolate, evaluate, offset) {
    16     source += text.slice(index, offset)
    17       .replace(escaper, function (match) { return '\' + escapes[match]; });
    18 
    19     if (escape) {
    20       source += "'+
    ((__t=(" + escape + "))==null?'':_.escape(__t))+
    '";
    21     }
    22     if (interpolate) {
    23       source += "'+
    ((__t=(" + interpolate + "))==null?'':__t)+
    '";
    24     }
    25     if (evaluate) {
    26       source += "';
    " + evaluate + "
    __p+='";
    27     }
    28     index = offset + match.length;
    29     return match;
    30   });
    31   source += "';
    ";
    32 
    33   // If a variable is not specified, place data values in local scope.
    34   if (!settings.variable) source = 'with(obj||{}){
    ' + source + '}
    ';
    35 
    36   source = "var __t,__p='',__j=Array.prototype.join," +
    37     "print=function(){__p+=__j.call(arguments,'');};
    " +
    38     source + "return __p;
    ";
    39 
    40   try {
    41     render = new Function(settings.variable || 'obj', '_', source);
    42   } catch (e) {
    43     e.source = source;
    44     throw e;
    45   }
    46 
    47   if (data) return render(data, _);
    48   var template = function (data) {
    49     return render.call(this, data, _);
    50   };
    51 
    52   // Provide the compiled function source as a convenience for precompilation.
    53   template.source = 'function(' + (settings.variable || 'obj') + '){
    ' + source + '}';
    54 
    55   return template;
    56 };
    underscore里面的代码

    将上述代码做字符串处理成字符串函数,然后将data传入,重新渲染即可。然而技术在变化,在进步。试想我们一个页面某个子节点文字发生了变化,全部重新渲染似乎不太划算,于是出现了虚拟DOM概念(React 导致其流行),他出现的意义就是之前我们使用jQuery操作10次dom的时候浏览器会操作10次,这里render过程中导致的坐标计算10次render tree的形成可能让页面变得越来越卡,而虚拟DOM能很好的解决这一切,所以这里我们就需要将我们模板中的代码首先转换为虚拟DOM,这里涉及到了复杂的解析过程

    PS:回到最初Server渲染时代,每次点击就会导致一次服务器交互,并且重新渲染页面

    Virtual DOM

    我们做的第一步就是将模板html字符串转换为js对象,这个代码都不要说去实现,光是想想就知道里面必定会有大量的正则,大量的细节要处理,但我们的目标是一套代码多端运行,完全没(能力)必要在这种地方耗费时间,所以我们直接阅读这段代码:https://johnresig.com/blog/pure-javascript-html-parser/,稍作更改后,便可以得到以下代码:

      1 /*
      2  * Modified at https://github.com/blowsie/Pure-JavaScript-HTML5-Parser
      3  */
      4 
      5 // Regular Expressions for parsing tags and attributes
      6 let startTag = /^<([-A-Za-z0-9_]+)((?:s+[a-zA-Z_:@][-a-zA-Z0-9_:.]*(?:s*=s*(?:(?:"[^"]*")|(?:'[^']*')|[^>s]+))?)*)s*(/?)>/,
      7     endTag = /^</([-A-Za-z0-9_]+)[^>]*>/,
      8     attr = /([a-zA-Z_:@][-a-zA-Z0-9_:.]*)(?:s*=s*(?:(?:"((?:\.|[^"])*)")|(?:'((?:\.|[^'])*)')|([^>s]+)))?/g
      9 
     10 // Empty Elements - HTML 5
     11 let empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr")
     12 
     13 // Block Elements - HTML 5
     14 let block = makeMap("a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video")
     15 
     16 // Inline Elements - HTML 5
     17 let inline = makeMap("abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var")
     18 
     19 // Elements that you can, intentionally, leave open
     20 // (and which close themselves)
     21 let closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr")
     22 
     23 // Attributes that have their values filled in disabled="disabled"
     24 let fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected")
     25 
     26 // Special Elements (can contain anything)
     27 let special = makeMap("script,style")
     28 
     29 function makeMap(str) {
     30     var obj = {}, items = str.split(",");
     31     for (var i = 0; i < items.length; i++)
     32         obj[items[i]] = true;
     33     return obj;
     34 }
     35 
     36 export default function HTMLParser(html, handler) {
     37     var index, chars, match, stack = [], last = html;
     38     stack.last = function () {
     39         return this[this.length - 1];
     40     };
     41 
     42     while (html) {
     43         chars = true;
     44 
     45         // Make sure we're not in a script or style element
     46         if (!stack.last() || !special[stack.last()]) {
     47 
     48             // Comment
     49             if (html.indexOf("<!--") == 0) {
     50                 index = html.indexOf("-->");
     51 
     52                 if (index >= 0) {
     53                     if (handler.comment)
     54                         handler.comment(html.substring(4, index));
     55                     html = html.substring(index + 3);
     56                     chars = false;
     57                 }
     58 
     59                 // end tag
     60             } else if (html.indexOf("</") == 0) {
     61                 match = html.match(endTag);
     62 
     63                 if (match) {
     64                     html = html.substring(match[0].length);
     65                     match[0].replace(endTag, parseEndTag);
     66                     chars = false;
     67                 }
     68 
     69                 // start tag
     70             } else if (html.indexOf("<") == 0) {
     71                 match = html.match(startTag);
     72 
     73                 if (match) {
     74                     html = html.substring(match[0].length);
     75                     match[0].replace(startTag, parseStartTag);
     76                     chars = false;
     77                 }
     78             }
     79 
     80             if (chars) {
     81                 index = html.indexOf("<");
     82 
     83                 var text = index < 0 ? html : html.substring(0, index);
     84                 html = index < 0 ? "" : html.substring(index);
     85 
     86                 if (handler.chars)
     87                     handler.chars(text);
     88             }
     89 
     90         } else {
     91             html = html.replace(new RegExp("([\s\S]*?)</" + stack.last() + "[^>]*>"), function (all, text) {
     92                 text = text.replace(/<!--([sS]*?)-->|<![CDATA[([sS]*?)]]>/g, "$1$2");
     93                 if (handler.chars)
     94                     handler.chars(text);
     95 
     96                 return "";
     97             });
     98 
     99             parseEndTag("", stack.last());
    100         }
    101 
    102         if (html == last)
    103             throw "Parse Error: " + html;
    104         last = html;
    105     }
    106 
    107     // Clean up any remaining tags
    108     parseEndTag();
    109 
    110     function parseStartTag(tag, tagName, rest, unary) {
    111         tagName = tagName.toLowerCase();
    112 
    113         if (block[tagName]) {
    114             while (stack.last() && inline[stack.last()]) {
    115                 parseEndTag("", stack.last());
    116             }
    117         }
    118 
    119         if (closeSelf[tagName] && stack.last() == tagName) {
    120             parseEndTag("", tagName);
    121         }
    122 
    123         unary = empty[tagName] || !!unary;
    124 
    125         if (!unary)
    126             stack.push(tagName);
    127 
    128         if (handler.start) {
    129             var attrs = [];
    130 
    131             rest.replace(attr, function (match, name) {
    132                 var value = arguments[2] ? arguments[2] :
    133                     arguments[3] ? arguments[3] :
    134                         arguments[4] ? arguments[4] :
    135                             fillAttrs[name] ? name : "";
    136 
    137                 attrs.push({
    138                     name: name,
    139                     value: value,
    140                     escaped: value.replace(/(^|[^\])"/g, '$1\"') //"
    141                 });
    142             });
    143 
    144             if (handler.start)
    145                 handler.start(tagName, attrs, unary);
    146         }
    147     }
    148 
    149     function parseEndTag(tag, tagName) {
    150         // If no tag name is provided, clean shop
    151         if (!tagName)
    152             var pos = 0;
    153 
    154         // Find the closest opened tag of the same type
    155         else
    156             for (var pos = stack.length - 1; pos >= 0; pos--)
    157                 if (stack[pos] == tagName)
    158                     break;
    159 
    160         if (pos >= 0) {
    161             // Close all the open elements, up the stack
    162             for (var i = stack.length - 1; i >= pos; i--)
    163                 if (handler.end)
    164                     handler.end(stack[i]);
    165 
    166             // Remove the open elements from the stack
    167             stack.length = pos;
    168         }
    169     }
    170 };
    View Code

    这是一段非常牛逼的代码,要写出这种代码需要花很多功夫,绕过很多细节,自己写很难还未必写得好,所以拿来用就好,不必愧疚......,但是我们需要知道这段代码干了什么:

    他会遍历我们的字符串模板,解析后会有四个回调可供使用:start、end、chars、comment,我们要做的就是填充里面的事件,完成我们将HTML转换为js对象的工作:

     1 <!doctype html>
     2 <html>
     3 <head>
     4   <title>起步</title>
     5 </head>
     6 <body>
     7 
     8 <script type="module">
     9 
    10   import HTMLParser from './src/core/parser/html-parser.js'
    11 
    12   let html = `
    13 <div class="c-row search-line" data-flag="start" ontap="clickHandler">
    14   <div class="c-span9 js-start search-line-txt">
    15     {{name}}</div>
    16 </div>
    17   `
    18 
    19   function arrToObj(arr) {
    20     let map = {};
    21     for(let i = 0, l = arr.length; i <  l; i++) {
    22       map[arr[i].name] = arr[i].value
    23     }
    24     return map;
    25   }
    26 
    27   //存储所有节点
    28   let nodes = [];
    29 
    30   //记录当前节点位置,方便定位parent节点
    31   let stack = [];
    32 
    33   HTMLParser(html, {
    34     /*
    35      unary: 是不是自闭和标签比如 <br/> input
    36      attrs为属性的数组
    37     */
    38     start: function( tag, attrs, unary ) { //标签开始
    39       /*
    40        stack记录的父节点,如果节点长度大于1,一定具有父节点
    41        */
    42       let parent = stack.length ? stack[stack.length - 1] : null;
    43 
    44       //最终形成的node对象
    45       let node = {
    46         //1标签, 2需要解析的表达式, 3 纯文本
    47         type: 1,
    48         tag: tag,
    49         attrs: arrToObj(attrs),
    50         parent: parent,
    51         //关键属性
    52         children: [],
    53         text: null
    54       };
    55 
    56       //如果存在父节点,也标志下这个属于其子节点
    57       if(parent) {
    58         parent.children.push(node);
    59       }
    60       //还需要处理<br/> <input>这种非闭合标签
    61       //...
    62 
    63       //进入节点堆栈,当遇到弹出标签时候弹出
    64       stack.push(node)
    65       nodes.push(node);
    66 
    67       debugger;
    68     },
    69     end: function( tag ) { //标签结束
    70       //弹出当前子节点,根节点一定是最后弹出去的,兄弟节点之间会按顺序弹出,其父节点在最后一个子节点弹出后会被弹出
    71       stack.pop();
    72       debugger;
    73     },
    74     chars: function( text ) { //文本
    75       //如果是空格之类的不予处理
    76       if(text.trim() === '') return;
    77       let node = nodes[nodes.length - 1];
    78       //如果这里是表达式{{}}需要特殊处理
    79       if(node) node.text = text.trim()
    80       debugger;
    81     }
    82   });
    83 
    84   console.log(nodes)
    85 
    86 </script>
    87 
    88 </body>
    89 </html>

    这里输出了我们想要的结构:

    第一个节点便是跟节点,我们可以根据他遍历整个节点,我们也可以根据数组(里面有对应的parent关系)生成我们想要的结构,可以看出借助强大的第三方工具库可以让我们的工作变得更加高效以及不容易出错,如果我们自己写上述HTMLParser会比较困难的,什么时候需要自己写什么时候需要借助,就要看你要做那个事情有没有现成确实可用的工具库了,第二步我们尝试下将这些模板标签,与data结合转换为真正的HTML结构

    简单的Virtual DOM TO HTML

    这里需要data加入了,我们简单实现一个MVVM的类,并且将上述Parser做成一个方法:

      1 <!doctype html>
      2 <html>
      3 <head>
      4   <title>起步</title>
      5 </head>
      6 <body>
      7 
      8 <div id="app">
      9 
     10 </div>
     11 
     12 <script type="module">
     13 
     14   import HTMLParser from './src/core/parser/html-parser.js'
     15 
     16   let html = `
     17 <div class="c-row search-line" data-flag="start" ontap="clickHandler">
     18   <div class="c-span9 js-start search-line-txt">
     19     {{name}}</div>
     20     <input type="text">
     21      <br>
     22 </div>
     23   `
     24 
     25   function arrToObj(arr) {
     26     let map = {};
     27     for(let i = 0, l = arr.length; i <  l; i++) {
     28       map[arr[i].name] = arr[i].value
     29     }
     30     return map;
     31   }
     32 
     33   function htmlParser(html) {
     34 
     35     //存储所有节点
     36     let nodes = [];
     37 
     38     //记录当前节点位置,方便定位parent节点
     39     let stack = [];
     40 
     41     HTMLParser(html, {
     42       /*
     43        unary: 是不是自闭和标签比如 <br/> input
     44        attrs为属性的数组
     45        */
     46       start: function( tag, attrs, unary ) { //标签开始
     47         /*
     48          stack记录的父节点,如果节点长度大于1,一定具有父节点
     49          */
     50         let parent = stack.length ? stack[stack.length - 1] : null;
     51 
     52         //最终形成的node对象
     53         let node = {
     54           //1标签, 2需要解析的表达式, 3 纯文本
     55           type: 1,
     56           tag: tag,
     57           attrs: arrToObj(attrs),
     58           parent: parent,
     59           //关键属性
     60           children: []
     61         };
     62 
     63         //如果存在父节点,也标志下这个属于其子节点
     64         if(parent) {
     65           parent.children.push(node);
     66         }
     67         //还需要处理<br/> <input>这种非闭合标签
     68         //...
     69 
     70         //进入节点堆栈,当遇到弹出标签时候弹出
     71         stack.push(node)
     72         nodes.push(node);
     73 
     74 //      debugger;
     75       },
     76       end: function( tag ) { //标签结束
     77         //弹出当前子节点,根节点一定是最后弹出去的,兄弟节点之间会按顺序弹出,其父节点在最后一个子节点弹出后会被弹出
     78         stack.pop();
     79 
     80 //      debugger;
     81       },
     82       chars: function( text ) { //文本
     83         //如果是空格之类的不予处理
     84         if(text.trim() === '') return;
     85         text = text.trim();
     86 
     87         //匹配 {{}} 拿出表达式
     88         let reg = /{{(.*)}}/;
     89         let node = nodes[nodes.length - 1];
     90         //如果这里是表达式{{}}需要特殊处理
     91         if(!node) return;
     92 
     93         if(reg.test(text)) {
     94           node.children.push({
     95             type: 2,
     96             expression: RegExp.$1,
     97             text: text
     98           });
     99         } else {
    100           node.children.push({
    101             type: 3,
    102             text: text
    103           });
    104         }
    105 //      debugger;
    106       }
    107     });
    108 
    109     return nodes;
    110 
    111   }
    112 
    113   class MVVM {
    114     /*
    115     暂时要求必须传入data以及el,其他事件什么的不管
    116 
    117     */
    118     constructor(opts) {
    119 
    120       //要求必须存在,这里不做参数校验了
    121       this.$el = typeof opts.el === 'string' ? document.getElementById(opts.el) : opts.el;
    122 
    123       //data必须存在,其他不做要求
    124       this.$data = opts.data;
    125 
    126       //模板必须存在
    127       this.$template = opts.template;
    128 
    129       //存放解析结束的虚拟dom
    130       this.$nodes = [];
    131 
    132       //将模板解析后,转换为一个函数
    133       this.$initRender();
    134 
    135       //渲染之
    136       this.$render();
    137 debugger;
    138     }
    139 
    140     $initRender() {
    141       let template = this.$template;
    142       let nodes = htmlParser(template);
    143       this.$nodes = nodes;
    144     }
    145 
    146     //解析模板生成的函数,将最总html结构渲染出来
    147     $render() {
    148 
    149       let data = this.$data;
    150       let root = this.$nodes[0];
    151       let parent = this._createEl(root);
    152       //简单遍历即可
    153 
    154       this._render(parent, root.children);
    155 
    156       this.$el.appendChild(parent);
    157     }
    158 
    159     _createEl(node) {
    160       let data = this.$data;
    161 
    162       let el = document.createElement(node.tag || 'span');
    163 
    164       for (let key in node.attrs) {
    165         el.setAttribute(key, node.attrs[key])
    166       }
    167 
    168       if(node.type === 2) {
    169         el.innerText = data[node.expression];
    170       } else if(node.type === 3) {
    171         el.innerText = node.text;
    172       }
    173 
    174       return el;
    175     }
    176     _render(parent, children) {
    177       let child = null;
    178       for(let i = 0, len = children.length; i < len; i++) {
    179         child = this._createEl(children[i]);
    180         parent.append(child);
    181         if(children[i].children) this._render(child, children[i].children);
    182       }
    183     }
    184 
    185 
    186   }
    187 
    188 
    189   let vm = new MVVM({
    190     el: 'app',
    191     template: html,
    192     data: {
    193       name: '叶小钗'
    194     }
    195   })
    196 
    197 
    198 
    199 
    200 </script>
    201 
    202 </body>
    203 </html>
    View Code
    1 <div class="c-row search-line" data-flag="start" ontap="clickHandler">
    <
    div class="c-span9 js-start search-line-txt"><span>叶小钗</span></div>
    <
    input type="text">
    </
    div>

    这个代码非常简陋,只是对text部分做了处理,没有对属性,style等做处理,但是越是功能简单的代码理解起来越容易,后续的style以及属性大同小异,我们这里开始处理,介于篇幅,下次继续

  • 相关阅读:
    jQuery.validationEngine前端验证
    Ztree异步树加载
    asp.net后台编写 loading效果
    [ASP.NET] 使用Loading遮罩防止使用者重複點擊
    Easy UI 遮罩(MASK)
    jQueryEasyUI Messager基本使用
    jquery easyui datagrid使用参考
    asp.net mvc 2.o 中使用JQuery.uploadify
    ie9,10 uploadify cleanUp bug
    SQL Server 2005 镜像构建手册
  • 原文地址:https://www.cnblogs.com/yexiaochai/p/9695298.html
Copyright © 2020-2023  润新知