一套程式碼小程式&Web&Native執行的探索02
接上文: ofollow,noindex" target="_blank">一套程式碼小程式&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({ 2data: { 3name: 'hello world' 4}, 5clickHandler: function () { 6this.setData({ 7name: '葉小釵' 8}) 9} 10 })
這裡第一個關鍵便是將html模板轉換為js程式碼,如果是之前我們直接會用這種程式碼:
1 _.template = function (text, data, settings) { 2var render; 3settings = _.defaults({}, settings, _.templateSettings); 4 5// Combine delimiters into one regular expression via alternation. 6var 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. 13var index = 0; 14var source = "__p+='"; 15text.replace(matcher, function (match, escape, interpolate, evaluate, offset) { 16source += text.slice(index, offset) 17.replace(escaper, function (match) { return '\\' + escapes[match]; }); 18 19if (escape) { 20source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 21} 22if (interpolate) { 23source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 24} 25if (evaluate) { 26source += "';\n" + evaluate + "\n__p+='"; 27} 28index = offset + match.length; 29return match; 30}); 31source += "';\n"; 32 33// If a variable is not specified, place data values in local scope. 34if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 35 36source = "var __t,__p='',__j=Array.prototype.join," + 37"print=function(){__p+=__j.call(arguments,'');};\n" + 38source + "return __p;\n"; 39 40try { 41render = new Function(settings.variable || 'obj', '_', source); 42} catch (e) { 43e.source = source; 44throw e; 45} 46 47if (data) return render(data, _); 48var template = function (data) { 49return render.call(this, data, _); 50}; 51 52// Provide the compiled function source as a convenience for precompilation. 53template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 54 55return 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*(\/?)>/, 7endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, 8attr = /([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) { 30var obj = {}, items = str.split(","); 31for (var i = 0; i < items.length; i++) 32obj[items[i]] = true; 33return obj; 34 } 35 36 export default function HTMLParser(html, handler) { 37var index, chars, match, stack = [], last = html; 38stack.last = function () { 39return this[this.length - 1]; 40}; 41 42while (html) { 43chars = true; 44 45// Make sure we're not in a script or style element 46if (!stack.last() || !special[stack.last()]) { 47 48// Comment 49if (html.indexOf("<!--") == 0) { 50index = html.indexOf("-->"); 51 52if (index >= 0) { 53if (handler.comment) 54handler.comment(html.substring(4, index)); 55html = html.substring(index + 3); 56chars = false; 57} 58 59// end tag 60} else if (html.indexOf("</") == 0) { 61match = html.match(endTag); 62 63if (match) { 64html = html.substring(match[0].length); 65match[0].replace(endTag, parseEndTag); 66chars = false; 67} 68 69// start tag 70} else if (html.indexOf("<") == 0) { 71match = html.match(startTag); 72 73if (match) { 74html = html.substring(match[0].length); 75match[0].replace(startTag, parseStartTag); 76chars = false; 77} 78} 79 80if (chars) { 81index = html.indexOf("<"); 82 83var text = index < 0 ? html : html.substring(0, index); 84html = index < 0 ? "" : html.substring(index); 85 86if (handler.chars) 87handler.chars(text); 88} 89 90} else { 91html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function (all, text) { 92text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2"); 93if (handler.chars) 94handler.chars(text); 95 96return ""; 97}); 98 99parseEndTag("", stack.last()); 100} 101 102if (html == last) 103throw "Parse Error: " + html; 104last = html; 105} 106 107// Clean up any remaining tags 108parseEndTag(); 109 110function parseStartTag(tag, tagName, rest, unary) { 111tagName = tagName.toLowerCase(); 112 113if (block[tagName]) { 114while (stack.last() && inline[stack.last()]) { 115parseEndTag("", stack.last()); 116} 117} 118 119if (closeSelf[tagName] && stack.last() == tagName) { 120parseEndTag("", tagName); 121} 122 123unary = empty[tagName] || !!unary; 124 125if (!unary) 126stack.push(tagName); 127 128if (handler.start) { 129var attrs = []; 130 131rest.replace(attr, function (match, name) { 132var value = arguments[2] ? arguments[2] : 133arguments[3] ? arguments[3] : 134arguments[4] ? arguments[4] : 135fillAttrs[name] ? name : ""; 136 137attrs.push({ 138name: name, 139value: value, 140escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //" 141}); 142}); 143 144if (handler.start) 145handler.start(tagName, attrs, unary); 146} 147} 148 149function parseEndTag(tag, tagName) { 150// If no tag name is provided, clean shop 151if (!tagName) 152var pos = 0; 153 154// Find the closest opened tag of the same type 155else 156for (var pos = stack.length - 1; pos >= 0; pos--) 157if (stack[pos] == tagName) 158break; 159 160if (pos >= 0) { 161// Close all the open elements, up the stack 162for (var i = stack.length - 1; i >= pos; i--) 163if (handler.end) 164handler.end(stack[i]); 165 166// Remove the open elements from the stack 167stack.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 10import HTMLParser from './src/core/parser/html-parser.js' 11 12let 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 19function arrToObj(arr) { 20let map = {}; 21for(let i = 0, l = arr.length; i <l; i++) { 22map[arr[i].name] = arr[i].value 23} 24return map; 25} 26 27//儲存所有節點 28let nodes = []; 29 30//記錄當前節點位置,方便定位parent節點 31let stack = []; 32 33HTMLParser(html, { 34/* 35unary: 是不是自閉和標籤比如 <br/> input 36attrs為屬性的陣列 37*/ 38start: function( tag, attrs, unary ) { //標籤開始 39/* 40stack記錄的父節點,如果節點長度大於1,一定具有父節點 41*/ 42let parent = stack.length ? stack[stack.length - 1] : null; 43 44//最終形成的node物件 45let node = { 46//1標籤, 2需要解析的表示式, 3 純文字 47type: 1, 48tag: tag, 49attrs: arrToObj(attrs), 50parent: parent, 51//關鍵屬性 52children: [], 53text: null 54}; 55 56//如果存在父節點,也標誌下這個屬於其子節點 57if(parent) { 58parent.children.push(node); 59} 60//還需要處理<br/> <input>這種非閉合標籤 61//... 62 63//進入節點堆疊,當遇到彈出標籤時候彈出 64stack.push(node) 65nodes.push(node); 66 67debugger; 68}, 69end: function( tag ) { //標籤結束 70//彈出當前子節點,根節點一定是最後彈出去的,兄弟節點之間會按順序彈出,其父節點在最後一個子節點彈出後會被彈出 71stack.pop(); 72debugger; 73}, 74chars: function( text ) { //文字 75//如果是空格之類的不予處理 76if(text.trim() === '') return; 77let node = nodes[nodes.length - 1]; 78//如果這裡是表示式{{}}需要特殊處理 79if(node) node.text = text.trim() 80debugger; 81} 82}); 83 84console.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 14import HTMLParser from './src/core/parser/html-parser.js' 15 16let 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 25function arrToObj(arr) { 26let map = {}; 27for(let i = 0, l = arr.length; i <l; i++) { 28map[arr[i].name] = arr[i].value 29} 30return map; 31} 32 33function htmlParser(html) { 34 35//儲存所有節點 36let nodes = []; 37 38//記錄當前節點位置,方便定位parent節點 39let stack = []; 40 41HTMLParser(html, { 42/* 43unary: 是不是自閉和標籤比如 <br/> input 44attrs為屬性的陣列 45*/ 46start: function( tag, attrs, unary ) { //標籤開始 47/* 48stack記錄的父節點,如果節點長度大於1,一定具有父節點 49*/ 50let parent = stack.length ? stack[stack.length - 1] : null; 51 52//最終形成的node物件 53let node = { 54//1標籤, 2需要解析的表示式, 3 純文字 55type: 1, 56tag: tag, 57attrs: arrToObj(attrs), 58parent: parent, 59//關鍵屬性 60children: [] 61}; 62 63//如果存在父節點,也標誌下這個屬於其子節點 64if(parent) { 65parent.children.push(node); 66} 67//還需要處理<br/> <input>這種非閉合標籤 68//... 69 70//進入節點堆疊,當遇到彈出標籤時候彈出 71stack.push(node) 72nodes.push(node); 73 74 //debugger; 75}, 76end: function( tag ) { //標籤結束 77//彈出當前子節點,根節點一定是最後彈出去的,兄弟節點之間會按順序彈出,其父節點在最後一個子節點彈出後會被彈出 78stack.pop(); 79 80 //debugger; 81}, 82chars: function( text ) { //文字 83//如果是空格之類的不予處理 84if(text.trim() === '') return; 85text = text.trim(); 86 87//匹配 {{}} 拿出表示式 88let reg = /\{\{(.*)\}\}/; 89let node = nodes[nodes.length - 1]; 90//如果這裡是表示式{{}}需要特殊處理 91if(!node) return; 92 93if(reg.test(text)) { 94node.children.push({ 95type: 2, 96expression: RegExp.$1, 97text: text 98}); 99} else { 100node.children.push({ 101type: 3, 102text: text 103}); 104} 105 //debugger; 106} 107}); 108 109return nodes; 110 111} 112 113class MVVM { 114/* 115暫時要求必須傳入data以及el,其他事件什麼的不管 116 117*/ 118constructor(opts) { 119 120//要求必須存在,這裡不做引數校驗了 121this.$el = typeof opts.el === 'string' ? document.getElementById(opts.el) : opts.el; 122 123//data必須存在,其他不做要求 124this.$data = opts.data; 125 126//模板必須存在 127this.$template = opts.template; 128 129//存放解析結束的虛擬dom 130this.$nodes = []; 131 132//將模板解析後,轉換為一個函式 133this.$initRender(); 134 135//渲染之 136this.$render(); 137 debugger; 138} 139 140$initRender() { 141let template = this.$template; 142let nodes = htmlParser(template); 143this.$nodes = nodes; 144} 145 146//解析模板生成的函式,將最總html結構渲染出來 147$render() { 148 149let data = this.$data; 150let root = this.$nodes[0]; 151let parent = this._createEl(root); 152//簡單遍歷即可 153 154this._render(parent, root.children); 155 156this.$el.appendChild(parent); 157} 158 159_createEl(node) { 160let data = this.$data; 161 162let el = document.createElement(node.tag || 'span'); 163 164for (let key in node.attrs) { 165el.setAttribute(key, node.attrs[key]) 166} 167 168if(node.type === 2) { 169el.innerText = data[node.expression]; 170} else if(node.type === 3) { 171el.innerText = node.text; 172} 173 174return el; 175} 176_render(parent, children) { 177let child = null; 178for(let i = 0, len = children.length; i < len; i++) { 179child = this._createEl(children[i]); 180parent.append(child); 181if(children[i].children) this._render(child, children[i].children); 182} 183} 184 185 186} 187 188 189let vm = new MVVM({ 190el: 'app', 191template: html, 192data: { 193name: '葉小釵' 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以及屬性大同小異,我們這裡開始處理,介於篇幅,下次繼續