一套代码小程序&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 += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 21 } 22 if (interpolate) { 23 source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 24 } 25 if (evaluate) { 26 source += "';\n" + evaluate + "\n__p+='"; 27 } 28 index = offset + match.length; 29 return match; 30 }); 31 source += "';\n"; 32 33 // If a variable is not specified, place data values in local scope. 34 if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 35 36 source = "var __t,__p='',__j=Array.prototype.join," + 37 "print=function(){__p+=__j.call(arguments,'');};\n" + 38 source + "return __p;\n"; 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') + '){\n' + source + '}'; 54 55 return template; 56 };
将上述代码做字符串处理成字符串函数,然后将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(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/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 };
这是一段非常牛逼的代码,要写出这种代码需要花很多功夫,绕过很多细节,自己写很难还未必写得好,所以拿来用就好,不必愧疚......,但是我们需要知道这段代码干了什么:
他会遍历我们的字符串模板,解析后会有四个回调可供使用: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>
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以及属性大同小异,我们这里开始处理,介于篇幅,下次继续