接上文:一套代碼小程序&Web&Native運行的探索01,本文都是一些探索性爲目的的研究學習,在最終版輸出前,內中的內容可能會有點亂javascript
參考:html
https://github.com/fastCreator/MVVMvue
https://www.tangshuang.net/3756.htmljava
https://www.cnblogs.com/kidney/p/8018226.htmlnode
通過以前的學習,發現Vue其實與小程序框架相識度比較高,業內也有mpvue這種還比較成熟的方案了,咱們這邊依舊不着急去研究成熟的框架,如今看看本身能作到什麼程度,最近也真正的開始接觸了一些Vue的東西,裏面的代碼真的很是不錯,研究學習了下Vue的結構,發現其實跟咱們要的很相似,這裏想要嘗試初步的方案:提供Html模板->解析Html模板,其實這裏就是Vue裏面Parse部分的邏輯,一小部分代碼,這樣有不少Vue的代碼能夠借鑑,也變相的學習Vue的源碼,一箭雙鵰,因而咱們速度開始今天的學習git
首先,咱們設置一個簡單的目標:設置一段簡單的小程序模板,當咱們作完web版本後,他能夠在小程序中運行github
<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代碼,若是是以前咱們直接會用這種代碼:web
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,這裏涉及到了複雜的解析過程express
PS:回到最初Server渲染時代,每次點擊就會致使一次服務器交互,而且從新渲染頁面canvas
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以及屬性大同小異,咱們這裏開始處理,介於篇幅,下次繼續