接上文:一套程式碼小程式&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版本後,他可以在小程式中執行
1 2 3 4 |
<view class="c-row search-line" data-flag="start" ontap="clickHandler"> <view class="c-span9 js-start search-line-txt"> {{name}}</view> </view> |
1 2 3 4 5 6 7 8 9 10 |
Page({ data: { name: 'hello world' }, clickHandler: function () { this.setData({ name: '葉小釵' }) } }) |
這裡第一個關鍵便是將html模板轉換為js程式碼,如果是之前我們直接會用這種程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
_.template = function (text, data, settings) { var render; settings = _.defaults({}, settings, _.templateSettings); // Combine delimiters into one regular expression via alternation. var matcher = new RegExp([ (settings.escape || noMatch).source, (settings.interpolate || noMatch).source, (settings.evaluate || noMatch).source ].join('|') + '|$', 'g'); // Compile the template source, escaping string literals appropriately. var index = 0; var source = "__p+='"; text.replace(matcher, function (match, escape, interpolate, evaluate, offset) { source += text.slice(index, offset) .replace(escaper, function (match) { return '\\' + escapes[match]; }); if (escape) { source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; } if (interpolate) { source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; } if (evaluate) { source += "';\n" + evaluate + "\n__p+='"; } index = offset + match.length; return match; }); source += "';\n"; // If a variable is not specified, place data values in local scope. if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; source = "var __t,__p='',__j=Array.prototype.join," + "print=function(){__p+=__j.call(arguments,'');};\n" + source + "return __p;\n"; try { render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; } if (data) return render(data, _); var template = function (data) { return render.call(this, data, _); }; // Provide the compiled function source as a convenience for precompilation. template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; return template; }; // 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
/* * Modified at https://github.com/blowsie/Pure-JavaScript-HTML5-Parser */ // Regular Expressions for parsing tags and attributes let startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:@][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:@][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g // Empty Elements - HTML 5 let empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr") // Block Elements - HTML 5 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") // Inline Elements - HTML 5 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") // Elements that you can, intentionally, leave open // (and which close themselves) let closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr") // Attributes that have their values filled in disabled="disabled" let fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected") // Special Elements (can contain anything) let special = makeMap("script,style") function makeMap(str) { var obj = {}, items = str.split(","); for (var i = 0; i < items.length; i++) obj[items[i]] = true; return obj; } export default function HTMLParser(html, handler) { var index, chars, match, stack = [], last = html; stack.last = function () { return this[this.length - 1]; }; while (html) { chars = true; // Make sure we're not in a script or style element if (!stack.last() || !special[stack.last()]) { // Comment if (html.indexOf("<!--") == 0) { index = html.indexOf("-->"); if (index >= 0) { if (handler.comment) handler.comment(html.substring(4, index)); html = html.substring(index + 3); chars = false; } // end tag } else if (html.indexOf("</") == 0) { match = html.match(endTag); if (match) { html = html.substring(match[0].length); match[0].replace(endTag, parseEndTag); chars = false; } // start tag } else if (html.indexOf("<") == 0) { match = html.match(startTag); if (match) { html = html.substring(match[0].length); match[0].replace(startTag, parseStartTag); chars = false; } } if (chars) { index = html.indexOf("<"); var text = index < 0 ? html : html.substring(0, index); html = index < 0 ? "" : html.substring(index); if (handler.chars) handler.chars(text); } } else { html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function (all, text) { text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2"); if (handler.chars) handler.chars(text); return ""; }); parseEndTag("", stack.last()); } if (html == last) throw "Parse Error: " + html; last = html; } // Clean up any remaining tags parseEndTag(); function parseStartTag(tag, tagName, rest, unary) { tagName = tagName.toLowerCase(); if (block[tagName]) { while (stack.last() && inline[stack.last()]) { parseEndTag("", stack.last()); } } if (closeSelf[tagName] && stack.last() == tagName) { parseEndTag("", tagName); } unary = empty[tagName] || !!unary; if (!unary) stack.push(tagName); if (handler.start) { var attrs = []; rest.replace(attr, function (match, name) { var value = arguments[2] ? arguments[2] : arguments[3] ? arguments[3] : arguments[4] ? arguments[4] : fillAttrs[name] ? name : ""; attrs.push({ name: name, value: value, escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //" }); }); if (handler.start) handler.start(tagName, attrs, unary); } } function parseEndTag(tag, tagName) { // If no tag name is provided, clean shop if (!tagName) var pos = 0; // Find the closest opened tag of the same type else for (var pos = stack.length - 1; pos >= 0; pos--) if (stack[pos] == tagName) break; if (pos >= 0) { // Close all the open elements, up the stack for (var i = stack.length - 1; i >= pos; i--) if (handler.end) handler.end(stack[i]); // Remove the open elements from the stack stack.length = pos; } } }; |
這是一段非常牛逼的程式碼,要寫出這種程式碼需要花很多功夫,繞過很多細節,自己寫很難還未必寫得好,所以拿來用就好,不必愧疚……,但是我們需要知道這段程式碼幹了什麼:
他會遍歷我們的字串模板,解析後會有四個回撥可供使用:start、end、chars、comment,我們要做的就是填充裡面的事件,完成我們將HTML轉換為js物件的工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
<!doctype html> <html> <head> <title>起步</title> </head> <body> <script type="module"> import HTMLParser from './src/core/parser/html-parser.js' let html = ` <div class="c-row search-line" data-flag="start" ontap="clickHandler"> <div class="c-span9 js-start search-line-txt"> {{name}}</div> </div> ` function arrToObj(arr) { let map = {}; for(let i = 0, l = arr.length; i < l; i++) { map[arr[i].name] = arr[i].value } return map; } //儲存所有節點 let nodes = []; //記錄當前節點位置,方便定位parent節點 let stack = []; HTMLParser(html, { /* unary: 是不是自閉和標籤比如 <br/> input attrs為屬性的陣列 */ start: function( tag, attrs, unary ) { //標籤開始 /* stack記錄的父節點,如果節點長度大於1,一定具有父節點 */ let parent = stack.length ? stack[stack.length - 1] : null; //最終形成的node物件 let node = { //1標籤, 2需要解析的表示式, 3 純文字 type: 1, tag: tag, attrs: arrToObj(attrs), parent: parent, //關鍵屬性 children: [], text: null }; //如果存在父節點,也標誌下這個屬於其子節點 if(parent) { parent.children.push(node); } //還需要處理<br/> <input>這種非閉合標籤 //... //進入節點堆疊,當遇到彈出標籤時候彈出 stack.push(node) nodes.push(node); debugger; }, end: function( tag ) { //標籤結束 //彈出當前子節點,根節點一定是最後彈出去的,兄弟節點之間會按順序彈出,其父節點在最後一個子節點彈出後會被彈出 stack.pop(); debugger; }, chars: function( text ) { //文字 //如果是空格之類的不予處理 if(text.trim() === '') return; let node = nodes[nodes.length - 1]; //如果這裡是表示式{{}}需要特殊處理 if(node) node.text = text.trim() debugger; } }); console.log(nodes) </script> </body> </html> |
這裡輸出了我們想要的結構:
第一個節點便是跟節點,我們可以根據他遍歷整個節點,我們也可以根據陣列(裡面有對應的parent關係)生成我們想要的結構,可以看出藉助強大的第三方工具庫可以讓我們的工作變得更加高效以及不容易出錯,如果我們自己寫上述HTMLParser會比較困難的,什麼時候需要自己寫什麼時候需要藉助,就要看你要做那個事情有沒有現成確實可用的工具庫了,第二步我們嘗試下將這些模板標籤,與data結合轉換為真正的HTML結構
簡單的Virtual DOM TO HTML
這裡需要data加入了,我們簡單實現一個MVVM的類,並且將上述Parser做成一個方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
<!doctype html> <html> <head> <title>起步</title> </head> <body> <div id="app"> </div> <script type="module"> import HTMLParser from './src/core/parser/html-parser.js' let html = ` <div class="c-row search-line" data-flag="start" ontap="clickHandler"> <div class="c-span9 js-start search-line-txt"> {{name}}</div> <input type="text"> <br> </div> ` function arrToObj(arr) { let map = {}; for(let i = 0, l = arr.length; i < l; i++) { map[arr[i].name] = arr[i].value } return map; } function htmlParser(html) { //儲存所有節點 let nodes = []; //記錄當前節點位置,方便定位parent節點 let stack = []; HTMLParser(html, { /* unary: 是不是自閉和標籤比如 <br/> input attrs為屬性的陣列 */ start: function( tag, attrs, unary ) { //標籤開始 /* stack記錄的父節點,如果節點長度大於1,一定具有父節點 */ let parent = stack.length ? stack[stack.length - 1] : null; //最終形成的node物件 let node = { //1標籤, 2需要解析的表示式, 3 純文字 type: 1, tag: tag, attrs: arrToObj(attrs), parent: parent, //關鍵屬性 children: [] }; //如果存在父節點,也標誌下這個屬於其子節點 if(parent) { parent.children.push(node); } //還需要處理<br/> <input>這種非閉合標籤 //... //進入節點堆疊,當遇到彈出標籤時候彈出 stack.push(node) nodes.push(node); // debugger; }, end: function( tag ) { //標籤結束 //彈出當前子節點,根節點一定是最後彈出去的,兄弟節點之間會按順序彈出,其父節點在最後一個子節點彈出後會被彈出 stack.pop(); // debugger; }, chars: function( text ) { //文字 //如果是空格之類的不予處理 if(text.trim() === '') return; text = text.trim(); //匹配 {{}} 拿出表示式 let reg = /\{\{(.*)\}\}/; let node = nodes[nodes.length - 1]; //如果這裡是表示式{{}}需要特殊處理 if(!node) return; if(reg.test(text)) { node.children.push({ type: 2, expression: RegExp.$1, text: text }); } else { node.children.push({ type: 3, text: text }); } // debugger; } }); return nodes; } class MVVM { /* 暫時要求必須傳入data以及el,其他事件什麼的不管 */ constructor(opts) { //要求必須存在,這裡不做引數校驗了 this.$el = typeof opts.el === 'string' ? document.getElementById(opts.el) : opts.el; //data必須存在,其他不做要求 this.$data = opts.data; //模板必須存在 this.$template = opts.template; //存放解析結束的虛擬dom this.$nodes = []; //將模板解析後,轉換為一個函式 this.$initRender(); //渲染之 this.$render(); debugger; } $initRender() { let template = this.$template; let nodes = htmlParser(template); this.$nodes = nodes; } //解析模板生成的函式,將最總html結構渲染出來 $render() { let data = this.$data; let root = this.$nodes[0]; let parent = this._createEl(root); //簡單遍歷即可 this._render(parent, root.children); this.$el.appendChild(parent); } _createEl(node) { let data = this.$data; let el = document.createElement(node.tag || 'span'); for (let key in node.attrs) { el.setAttribute(key, node.attrs[key]) } if(node.type === 2) { el.innerText = data[node.expression]; } else if(node.type === 3) { el.innerText = node.text; } return el; } _render(parent, children) { let child = null; for(let i = 0, len = children.length; i < len; i++) { child = this._createEl(children[i]); parent.append(child); if(children[i].children) this._render(child, children[i].children); } } } let vm = new MVVM({ el: 'app', template: html, data: { name: '葉小釵' } }) </script> </body> </html> |
1 2 3 4 5 |
<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以及屬性大同小異,我們這裡開始處理,介於篇幅,下次繼續。