一套代码小程序WebNative运行的探索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版本后,他可以在小程序中运行
<viewclass="c-row search-line"data-flag="start"ontap="clickHandler"><viewclass="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 varrender;3 settings =_.defaults({}, settings, _.templateSettings);4 5 //Combine delimiters into one regular expression via alternation. 6 var matcher = newRegExp([7 (settings.escape ||noMatch).source,8 (settings.interpolate ||noMatch).source,9 (settings.evaluate ||noMatch).source10 ].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 returnmatch;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 throwe;45 }46 47 if (data) returnrender(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 returntemplate;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-Parser3 */ 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]+)))?/g9 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 functionmakeMap(str) {30 var obj = {}, items = str.split(",");31 for (var i = 0; i < items.length; i++)32 obj[items[i]] = true;33 returnobj;34 }35 36 export default functionHTMLParser(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 functionparseStartTag(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 functionparseEndTag(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 };
View Code
这是一段非常牛逼的代码,要写出这种代码需要花很多功夫,绕过很多细节,自己写很难还未必写得好,所以拿来用就好,不必愧疚......,但是我们需要知道这段代码干了什么:
他会遍历我们的字符串模板,解析后会有四个回调可供使用:start、end、chars、comment,我们要做的就是填充里面的事件,完成我们将HTML转换为js对象的工作:
1 <!doctype html> 2 <html> 3 <head> 4 <title>起步</title> 5 </head> 6 <body> 7 8 <scripttype="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 functionarrToObj(arr) {20 let map={};21 for(let i= 0, l=arr.length; i<l; i++) {22 map[arr[i].name]=arr[i].value23 }24 returnmap;25 }26 27 //存储所有节点 28 let nodes=[];29 30 //记录当前节点位置,方便定位parent节点 31 let stack=[];32 33 HTMLParser(html, {34 /* 35 unary: 是不是自闭和标签比如 <br/> input36 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 <divid="app"> 9 10 </div> 11 12 <scripttype="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 functionarrToObj(arr) {26 let map={};27 for(let i= 0, l=arr.length; i<l; i++) {28 map[arr[i].name]=arr[i].value29 }30 returnmap;31 }32 33 functionhtmlParser(html) {34 35 //存储所有节点 36 let nodes=[];37 38 //记录当前节点位置,方便定位parent节点 39 let stack=[];40 41 HTMLParser(html, {42 /* 43 unary: 是不是自闭和标签比如 <br/> input44 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: text98 });99 }else{100 node.children.push({101 type:3,102 text: text103 });104 }105 //debugger; 106 }107 });108 109 returnnodes;110 111 }112 113 class MVVM {114 /* 115 暂时要求必须传入data以及el,其他事件什么的不管116 117 */ 118 constructor(opts) {119 120 //要求必须存在,这里不做参数校验了 121 this.$el= typeofopts.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 keyinnode.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 returnel;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= newMVVM({190 el:'app',191 template: html,192 data: {193 name:'叶小钗' 194 }195 })196 197 198 199 200 </script> 201 202 </body> 203 </html>
View Code
1 <divclass="c-row search-line"data-flag="start"ontap="clickHandler"><divclass="c-span9 js-start search-line-txt"><span>叶小钗</span></div><inputtype="text"></div>
这个代码非常简陋,只是对text部分做了处理,没有对属性,style等做处理,但是越是功能简单的代码理解起来越容易,后续的style以及属性大同小异,我们这里开始处理,介于篇幅,下次继续
一套代码小程序WebNative运行的探索02相关推荐
- 一套代码小程序WebNative运行的探索03——处理模板及属性
接上文: 一套代码小程序&Web&Native运行的探索02 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/master/ ...
- 一套代码小程序WebNative运行的探索01
前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 并且用小程序翻写了之前一个demo:[组件化开发]前端进阶篇之如何编写可维护可升级的代码 之前一直在跟业务方打交道 ...
- 【一套代码小程序NativeWeb阶段总结篇】可以这样阅读Vue源码
前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一 ...
- 1000套微信小程序源码源代码带后台带运行截图预览图学习资料网盘下载
1000多套微信小程序源码带后台+教程+不同行业的源码集合 o2o行业 | - 盒马鲜生 | - 轻客洗衣 互联网行业 | - 云文档 | - 仿ofo共享单车 | - 仿美团外卖 | - 仿饿了么 ...
- uni app 调用网络打印机_一套代码,七端运行-uni-app
为什么要使用uni-app uni-app 是一个使用 Vue.js 开发跨平台应用的前端框架,开发者编写一套代码,可编译到iOS.Android.H5.小程序等多个平台. 同时在别的方面它也有很大优 ...
- 免费分享20套微信小程序源码 源码免费下载【强烈推荐】
淘源码:国内知名的源码免费下载平台 微信小程序源码包括:商城系统源码.点餐外卖源码.垃圾分类识别源码.预约洗车源码.物业管理源码.校园跑腿源码.驾考学习源码.会议预约源码.图书管理源码.智能停车源码. ...
- 阿里云ARMS重磅推出小程序监控,助力小程序稳定运行
2018年是小程序蓬勃发展的一年,各大公司如腾讯.阿里.百度.头条等都陆续推出了自己的小程序,小程序已成为一个未来必然的趋势.移动互联网的新风口.据数据统计,目前已上线的微信小程序已超过100万,支付 ...
- 使用uniapp做微信小程序,在小程序编辑器运行时编译报错:appid不合法,导致启用不了。
使用uniapp做微信小程序,在小程序编辑器运行时编译报错:appid不合法,导致启用不了.这个好坑爹啊,我这么淑女的小可爱都想骂人了,我好累,写个博客发泄发泄吧! 复现我的问题:神操作--代码跟别人 ...
- uniapp写微信小程序怎么运行到微信开发工具上
1.选择运行>运行到小程序模拟器>运行到微信开发者工具 2.这样unpackage中就多一个文件mp-weixin 3.把这个文件导入到微信小程序中就行啦
最新文章
- 七十六、Python | Leetcode二分查找和分治算法系列
- vim一些挺方便的功能
- 谁还没个黑历史了。。。 | 今日最佳
- iOS重写和成员变量访问权限
- SHA-3的获胜者:keccak - 在 3GPP TS 35.231、FIPS 202 和 SP 800-185 中标准化
- 剑指 Offer II 024. 反转链表
- mkdir命令(转)
- mysql安装包5.7.17.0_mysql-5.7.17-winx64压缩版的安装包下载和安装配置
- c语言 大数开方,c语言求一个数的平方根
- jar包里面文件修改
- ZZULIOJ 1882: 蛤玮的魔法【数学】
- 2021-2022蓝桥杯寒假集训训练 - 问题 G: HTML新の手 -图片收集者
- 服务器无法定位到现有系统分区,真正解决win7 “安装程序无法定位现有系统分区,也无法创建新的系统分区”的方法...
- 车牌号测试打分最准确的软件,车牌号码吉凶测试
- 【项目实战二】基于模板匹配和形态学操作的信用卡卡号识别(OpenCV+Python)
- Redis[5] key的过期时间删除策略、实现lru算法、持久化配置
- 关于http的refer参数
- 淘宝信誉查询软件 官网免费版
- Java 接口作为方法参数
- Pyserial 实例教程详细用法