接上文:一套代码小程序&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>
 Page({
data: {
name: 'hello world'
},
clickHandler: function () {
this.setData({
name: '叶小钗'
})
}
})

这里第一个关键便是将html模板转换为js代码,如果是之前我们直接会用这种代码:

 _.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/,稍作更改后,便可以得到以下代码:

 /*
* 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对象的工作:

 <!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做成一个方法:

 <!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>
 <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以及属性大同小异,我们这里开始处理,介于篇幅,下次继续

一套代码小程序&Web&Native运行的探索02的更多相关文章

  1. 一套代码小程序&Web&Native运行的探索03——处理模板及属性

    接上文:一套代码小程序&Web&Native运行的探索02 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/master/m ...

  2. 一套代码小程序&Web&Native运行的探索06——组件系统

    接上文:一套代码小程序&Web&Native运行的探索05——snabbdom 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tre ...

  3. 一套代码小程序&Web&Native运行的探索05——snabbdom

    接上文:一套代码小程序&Web&Native运行的探索04——数据更新 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/ma ...

  4. 一套代码小程序&Web&Native运行的探索04——数据更新

    接上文:一套代码小程序&Web&Native运行的探索03 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/master/m ...

  5. 一套代码小程序&Web&Native运行的探索07——mpvue简单调研

    前言 接上文:[一套代码小程序&Native&Web阶段总结篇]可以这样阅读Vue源码 最近工作比较忙,加之上个月生了小孩,小情人是各种折腾他爸妈,我们可以使用的独立时间片不多,虽然这 ...

  6. 一套代码小程序&Web&Native运行的探索01

    前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 并且用小程序翻写了之前一个demo:[组件化开发]前端进阶篇之如何编写可维护可升级的代码 之前一直在跟业务方打交道 ...

  7. 【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码

    前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一 ...

  8. 小程序 web 端实时运行工具

    微信小程序 web 端实时运行工具 https://chemzqm.github.io/wept/

  9. 小程序web开发框架-weweb介绍

    weweb是一个兼容小程序语法的前端框架,你可以用小程序的写法,来写web单面应用.如果你已经有小程序了,通过它你可以将你的小程序运行在浏览器中.在小程序大行其道的今天,它可以让你的小程序代码得到最大 ...

随机推荐

  1. Elasticsearch笔记三之版本控制和插件

    版本控制 1:关系型数据库使用的是悲观锁,数据被读取后就被锁定其他的线程就无法对其进行修改. 2:ex使用的是乐观锁,数据被读取后其他程序还可以对其进行修改,而执行修改时发现此数据已经被修改则修改就会 ...

  2. 【bzoj 1095】[ZJOI2007]Hide 捉迷藏

    题目链接: TP 题解: 样例好良心,调样例3h一A…… 细节好多……诸如没完没了的pop和push……搞得头都大了. 同情zzh……调了整一天了. 动态点分治裸题……果然每个“裸题”打起来都跟shi ...

  3. POJ_3342_Party at Hali-Bula_树形DP

    POJ_3342_Party at Hali-Bula_树形DP 题意:直接上司和本人不能同时参加,求最多参加人数,并回答是否唯一解. 分析:常规树形DP,需要再维护一下选/不选当前点取得最大值时是否 ...

  4. SQL Server 中如何做到连续时间段的拆分?

    今天在工作中遇到了一个很实际的问题,客户在OA接口的员工休假中间表中提供了连续时间段的休假记录,例如: 张三,2018-12-1 ~2018-12-31 ,病假,31天.这样带来的问题是,如果我需要统 ...

  5. 【爆料】-《悉尼科技大学毕业证书》UTS一模一样原件

    ☞悉尼科技大学毕业证书[微/Q:2544033233◆WeChat:CC6669834]UC毕业证书/联系人Alice[查看点击百度快照查看][留信网学历认证&博士&硕士&海归 ...

  6. 前端学习笔记之CSS选择器

    阅读目录 一 基本选择器 二 后代选择器.子元素选择器 三 兄弟选择器 四 交集选择器与并集选择器 五 序列选择器 六 属性选择器 七 伪类选择器 八 伪元素选择器 九 CSS三大特性 一 基本选择器 ...

  7. zookeeper配置管理+集群管理实战

    引言 之前就了解过kafka,看的似懂非懂,最近项目组中引入了kafka,刚好接着这个机会再次学习下. Kafka在很多公司被用作分布式高性能消息队列,kafka之前我只用过redis的list来做简 ...

  8. asp.net core 系列之用户认证(authentication)

    ASP.NET Core 的 identity 是一种需要用户登录的会员系统,用户可以创建一个登录信息存储在 Identity 的的账号, 或者也可以使用第三方登录,支持的第三方登录包括:Facebo ...

  9. centos7开放端口和防火墙设置

    centos7开放端口和防火墙设置. 查看防火墙状态: firewall-cmd --state 如果显示: not running 打开防火墙服务: systemctl start firewall ...

  10. Stackoverflow上有哪些声望高or值得关注的国人

    Stackoverflow上有哪些声望高/值得关注的国人? 以下回答并不严格按照 Reputation 排名来列,也不收录不确定是Chinese(中国人或华人)的用户,欢迎补充- 1.李杨 @Li L ...