一套程式碼小程式&Web&Native執行的探索03
我們在研究如果小程式在多端執行的時候,基本在前端框架這塊陷入了困境,因為市面上沒有框架可以直接拿來用,而Vue的相識度比較高,而且口碑很好,我們便接著這個機會同步學習Vue也解決我們的問題,我們看看這個系列結束後,會不會離目標進一點,後續如果實現後會重新整理系列文章......
參考:
https://github.com/fastCreator/MVVM(極度參考,十分感謝該作者,直接看Vue會比較吃力的,但是看完這個作者的程式碼便會輕易很多,可惜這個作者沒有對應部落格說明,不然就爽了)
https://www.tangshuang.net/3756.html
https://www.cnblogs.com/kidney/p/8018226.html
https://github.com/livoras/blog/issues/13
上文中我們藉助HTMLParser這種高階神器,終於將文字中的表示式替換了出來,這裡單純說文字這裡也有以下問題:這段是不支援js程式碼的,+-、三元程式碼都不支援,所以以上都只是幫助我們理解,還是之前那句話,越是單純的程式碼,越是考慮少的程式碼,可能越是能理解實現,但是後續仍然需要補足,我們這裡還是要跟Vue對齊,這樣做有個好處,當你不知道怎麼做的時候,可以看看Vue的實現,當你思考這麼做合不合適的時候,也可以參考Vue,那可是經過烈火淬鍊的,值得深度學習,我們今天的任務比較簡單便是完整的處理完style、屬性以及表示式處理,這裡我們直接在fastCreator這個作者下的原始碼開始學習,還有種學習原始碼的方法就是抄三次......
我們學習的過程,先將程式碼寫到一起方便理解,後續再慢慢拆分,首先是MVVM類,我們新建libs資料夾,先新建兩個js檔案,一個html-parser一個index(框架入口檔案)
libs --index.js --html-parser.jsindex.html
1 import HTMLParser from './html-parser.js' 2 3 function arrToObj(arr) { 4 let map = {}; 5 for(let i = 0, l = arr.length; i < l; i++) {index6 map[arr[i].name] = arr[i].value 7 } 8 return map; 9 } 10 11 function htmlParser(html) { 12 13 //儲存所有節點 14 let nodes = []; 15 16 //記錄當前節點位置,方便定位parent節點 17 let stack = []; 18 19 HTMLParser(html, { 20 /* 21 unary: 是不是自閉和標籤比如 <br/> input 22 attrs為屬性的陣列 23 */ 24 start: function( tag, attrs, unary ) { //標籤開始 25 /* 26 stack記錄的父節點,如果節點長度大於1,一定具有父節點 27 */ 28 let parent = stack.length ? stack[stack.length - 1] : null; 29 30 //最終形成的node物件 31 let node = { 32 //1標籤, 2需要解析的表示式, 3 純文字 33 type: 1, 34 tag: tag, 35 attrs: arrToObj(attrs), 36 parent: parent, 37 //關鍵屬性 38 children: [] 39 }; 40 41 //如果存在父節點,也標誌下這個屬於其子節點 42 if(parent) { 43 parent.children.push(node); 44 } 45 //還需要處理<br/> <input>這種非閉合標籤 46 //... 47 48 //進入節點堆疊,當遇到彈出標籤時候彈出 49 stack.push(node) 50 nodes.push(node); 51 52 // debugger; 53 }, 54 end: function( tag ) { //標籤結束 55 //彈出當前子節點,根節點一定是最後彈出去的,兄弟節點之間會按順序彈出,其父節點在最後一個子節點彈出後會被彈出 56 stack.pop(); 57 58 // debugger; 59 }, 60 chars: function( text ) { //文字 61 //如果是空格之類的不予處理 62 if(text.trim() === '') return; 63 text = text.trim(); 64 65 //匹配 {{}} 拿出表示式 66 let reg = /\{\{(.*)\}\}/; 67 let node = nodes[nodes.length - 1]; 68 //如果這裡是表示式{{}}需要特殊處理 69 if(!node) return; 70 71 if(reg.test(text)) { 72 node.children.push({ 73 type: 2, 74 expression: RegExp.$1, 75 text: text 76 }); 77 } else { 78 node.children.push({ 79 type: 3, 80 text: text 81 }); 82 } 83 // debugger; 84 } 85 }); 86 87 return nodes; 88 89 } 90 91 export default class MVVM { 92 /* 93 暫時要求必須傳入data以及el,其他事件什麼的不管 94 95 */ 96 constructor(opts) { 97 98 //要求必須存在,這裡不做引數校驗了 99 this.$el = typeof opts.el === 'string' ? document.getElementById(opts.el) : opts.el; 100 101 //data必須存在,其他不做要求 102 this.$data = opts.data; 103 104 //模板必須存在 105 this.$template = opts.template; 106 107 //存放解析結束的虛擬dom 108 this.$nodes = []; 109 110 //將模板解析後,轉換為一個函式 111 this.$initRender(); 112 113 //渲染之 114 this.$render(); 115 debugger; 116 } 117 118 $initRender() { 119 let template = this.$template; 120 let nodes = htmlParser(template); 121 this.$nodes = nodes; 122 } 123 124 //解析模板生成的函式,將最總html結構渲染出來 125 $render() { 126 127 let data = this.$data; 128 let root = this.$nodes[0]; 129 let parent = this._createEl(root); 130 //簡單遍歷即可 131 132 this._render(parent, root.children); 133 134 this.$el.appendChild(parent); 135 } 136 137 _createEl(node) { 138 let data = this.$data; 139 140 let el = document.createElement(node.tag || 'span'); 141 142 for (let key in node.attrs) { 143 el.setAttribute(key, node.attrs[key]) 144 } 145 146 if(node.type === 2) { 147 el.innerText = data[node.expression]; 148 } else if(node.type === 3) { 149 el.innerText = node.text; 150 } 151 152 return el; 153 } 154 _render(parent, children) { 155 let child = null; 156 for(let i = 0, len = children.length; i < len; i++) { 157 child = this._createEl(children[i]); 158 parent.append(child); 159 if(children[i].children) this._render(child, children[i].children); 160 } 161 } 162 163 164 }
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 };html-parser
這個時候我們的index程式碼量便下來了:
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 MVVM from './libs/index.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 let vm = new MVVM({ 26 el: 'app', 27 template: html, 28 data: { 29 name: '葉小釵' 30 } 31 }) 32 33 </script> 34 </body> 35 </html>
我們現在來更改index.js入口檔案的程式碼,這裡特別說一下其中的$mount方法,他試試是要做一個這樣的事情:
//模板字串 <div id = "app"> {{message}} </div>
//render函式 function anonymous() { with(this){return _h('div',{attrs:{"id":"app"}},["\n "+_s(message)+"\n"])} }
將模板轉換為一個函式render放到引數上,這裡我們先簡單實現,後續深入後我們重新翻下這個函式,修改後我們的index.js變成了這個樣子:
1 import HTMLParser from './html-parser.js' 2 3 4 //工具函式 begin 5 6 function isFunction(obj) { 7 return typeof obj === 'function' 8 } 9 10 11 function makeAttrsMap(attrs, delimiters) { 12 const map = {} 13 for (let i = 0, l = attrs.length; i < l; i++) { 14 map[attrs[i].name] = attrs[i].value; 15 } 16 return map; 17 } 18 19 20 21 //dom操作 22 function query(el) { 23 if (typeof el === 'string') { 24 const selector = el 25 el = document.querySelector(el) 26 if (!el) { 27 return document.createElement('div') 28 } 29 } 30 return el 31 } 32 33 function cached(fn) { 34 const cache = Object.create(null) 35 return function cachedFn(str) { 36 const hit = cache[str] 37 return hit || (cache[str] = fn(str)) 38 } 39 } 40 41 let idToTemplate = cached(function (id) { 42 var el = query(id) 43 return el && el.innerHTML; 44 }) 45 46 47 48 //工具函式 end 49 50 //模板解析函式 begin 51 52 const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g 53 const regexEscapeRE = /[-.*+?^${}()|[\]/\\]/g 54 55 const buildRegex = cached(delimiters => { 56 const open = delimiters[0].replace(regexEscapeRE, '\\$&') 57 const close = delimiters[1].replace(regexEscapeRE, '\\$&') 58 return new RegExp(open + '((?:.|\\n)+?)' + close, 'g') 59 }) 60 61 62 function TextParser(text, delimiters) { 63 const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE 64 if (!tagRE.test(text)) { 65 return 66 } 67 const tokens = [] 68 let lastIndex = tagRE.lastIndex = 0 69 let match, index 70 while ((match = tagRE.exec(text))) { 71 index = match.index 72 // push text token 73 if (index > lastIndex) { 74 tokens.push(JSON.stringify(text.slice(lastIndex, index))) 75 } 76 // tag token 77 const exp = match[1].trim() 78 tokens.push(`_s(${exp})`) 79 lastIndex = index + match[0].length 80 } 81 if (lastIndex < text.length) { 82 tokens.push(JSON.stringify(text.slice(lastIndex))) 83 } 84 return tokens.join('+') 85 } 86 87 //******核心中的核心 88 function compileToFunctions(template, vm) { 89 let root; 90 let currentParent; 91 let options = vm.$options; 92 let stack = []; 93 94 //這段程式碼昨天做過解釋,這裡屬性引數比昨天多一些 95 HTMLParser(template, { 96 start: function(tag, attrs, unary) { 97 98 let element = { 99 vm: vm, 100 //1 標籤 2 文字表示式 3 文字 101 type: 1, 102 tag, 103 //陣列 104 attrsList: attrs, 105 attrsMap: makeAttrsMap(attrs), //將屬性陣列轉換為物件 106 parent: currentParent, 107 children: [] 108 }; 109 110 if(!root) { 111 vm.$vnode = root = element; 112 } 113 114 if(currentParent && !element.forbidden) { 115 currentParent.children.push(element); 116 element.parent = currentParent; 117 } 118 119 if(!unary) { 120 currentParent = element; 121 stack.push(element);