FreeMia:Aopemiacompatibleframeworkforruigibrowserorwebview.一个兼容微信小程序Mia框架的开源框架
从小程序的设计来看,微信正走向封闭生态。我们开发的微信小程序很难在其他地方使用。
最近一段时间,我花了大量精力来查找相关资料。包括React、ReactNative。我本来不算一个JS程序员,但也为此学习或了解了BableWebPatch,ES2015等等一系列我原本不熟悉的内容。
真正有巨大收获的是@phodal大神发的五篇文章,它还做了一个forfu的框架wiv。仔细学习了这个框架,并提交了一个补丁,改善了一点点小功能。昨天晚上,我就在考虑到底是在这个框架上修改,还是自己开一个。
反复思量,觉得如果要改进,那么基本上要重写所有的代码,和重开一个无益。一个项目初始的架构很重要,差的架构让人难以提起改进的兴趣。另外,大神有6个月没有更新项目了。
总之,再次感谢@phodal。
设计目的和计划完全兼容微信小程序的所有API。让微信小程序能移植到自己的APP上。
当然这个目标从现在看有些“宏伟”了。
要做的工作:
解析wxmldom,并生成相应的html。这一点,@phodal已经做了大量的贡献。但性能需要改进一下。另外,我学习了facebook的diff算法,准备在今后的改进中加入。
wxml中{{}}格式数据的处理。我给wiv这个项目添加了{{obj.ame}}这样的支持。但还缺少if和for这两个非常重要的环节。
事件系统。目前已经实现了一些,但还远远没有完成。但大体的设计已经有了。
打包等项目工具。微信小程序将所有的文件全部打包在一起。这个并非简单的用webpack进行打包。还对程序作了一定的预处理。对于将xml生成为js的做法,我觉得还需要考虑,到底需不需要这么做。jso的处理相对简单,require进去就好。
我实现打包工具的思路是
首先给Page打包,给添加上两个参数,把xml和文件名一起传给Page函数。
使用webpack等工具打包到一起。
App支持。wx中有很多函数,没有App的帮助是无法实现的。这一部分的做法
在web中能用web试下你的用web实现,不能实现的暂不实现。
在App中,给出原生支持。。不过我目前只会adroid。苹果的没钱买那么贵的设备。毕竟玩票性质。。。
实现方案项目工具整个项目使用odejs管理。使用gulp完成编译和监视文件自动编译的功能。使用bable进行ES6的转义。直接拿了别人项目的配置。。。(羞。。。)详见package.jso
项目入口项目的入口是src/freemia.js
/** * Created by Togfeg Yag o 2017/1/25. */import Page from './Page'import App from './App'import FExceptio from './FExceptio'cost freemia={ addPage(opt,ame,wxml){ cosole.log("add Page :"+ame); if(!widow.App ){ throw ew FExceptio("App() fuctio should be called before callig Page"); } let p = ew Page(opt,ame); p.setWXml(wxml); widow.App.addPage(ame,p); }, setApp(opt){ cosole.log("set App called"); widow.App = ew App(opt); }, start(){ widow.Page = this.addPage; widow.App = this.setApp;// let e = ew CustomEvet('oLauch',{});// widow.App.evetHadler(e) }, fiishLoad(){ var e={type:"oLauch",detail:{}}; widow.App.evetHadler(e); }}export default freemia;包含了setApp和addPage方法,这个方法在start方法中被暴露到widow中,所以,就可以使用App({})和Page({})的方法来使用它们。setApp直接创建App类。addPage方法首先创建Page对象,随后调用App的addPage方法将其加入管理之中。并使用setWxml将wxml设置进去。
App类先看代码
/** * Created by Togfeg Yag o 2017/1/25. */export default class App{ costructor(opt){ this.opt = opt; this.pageMap = []; this.addPage=this.addPage.bid(this); this.evetHadler=this.evetHadler.bid(this); this.reder=this.reder.bid(this); this.regEvet.bid(this)(); } regEvet(){ documet.addEvetListeer("oShow",(e)=>{ //oLoad fuctio of page is called succ }); } addPage(ame,p){ if(this.pageMap.legth==0){ this.curPage = p; } p.setName(ame); this.pageMap[ame] = p; } evetHadler(e){ //CustomEvet if(this.opt[e.type]){ this.opt[e.type](e.detail); } if(e.type == "oLauch"){ let eame = this.curPage.ame+"_oLoad"; e= ew CustomEvet(eame,{}); documet.dispatchEvet(e); } } reder(){ if(this.curPage){ curPage.reder(); } }}使用ES6实现的,老实说,如果不是能用class,我是不愿意入js这个坑的。但一堆堆的bid还是亮瞎了我眼。。。
App对象维护一个Page对象列表。和一个curPage指向当前对象。目前,默认认为第一个注册的Page是入口。(因为还没实现App的配置,所以暂时忍一下吧!!!)
下面是事件处理的核心evetHadler。我们在写App时这么写:App({ oLauch:fuctio(){...}})这个传进app的是一个对象或者说是hashmap。在app的构造函数中,传递给了this.optevetHadler被调用时,给出了一个e,这个e可以是CustomEvet,也可以是
{type:'oLauch',detail:{}}这样的对象。如果收到上述的这个oLauch消息,这个函数就会判断opt中(就是你传进的对象)是否有这个方法。如果有,则调用它。
调用万oLauch开始调用Page的oLoad了。怎么调用呢?在这里发出一个消息。如果页面的名字是idex,那么就发出idex_oLoad消息。idex这个页面会监听这个消息,进而收到这个oLoad事件。
下面来看Page的实现
Page的实现先贴代码。
/** * Created by Togfeg Yag o 2017/1/25. */import WXmlParser from "./WXmlParser"cost evet_list = [ 'oLoad','oDestory','reder'];export default class Page{ costructor(opt){ this.opt = opt; this.evetHadler=this.evetHadler.bid(this); this.registerEvetHadler=this.registerEvetHadler.bid(this); this.removeEvetListeer=this.removeEvetListeer.bid(this); this.setName=this.setName.bid(this); this.reder=this.reder.bid(this); this.setWXml=this.setWXml.bid(this); this.fireMyEvet = this.fireMyEvet.bid(this); this.getData =this.getData.bid(this); } setName(ame){ if(ame == this.ame)retur; this.removeEvetListeer(); this.ame = ame; this.registerEvetHadler(); } removeEvetListeer(){ for(var e i evet_list ){ let eame = evet_list[e]; cosole.log("removeEvetListeer:"+this.ame+'_'+eame); documet.removeEvetListeer(this.ame+'_'+eame); } } registerEvetHadler(){ for(var e i evet_list ){ let eame = evet_list[e]; cosole.log("addEvetListeer:"+this.ame+'_'+eame); documet.addEvetListeer(this.ame+'_'+eame,this.evetHadler); } } getData(){ retur this.opt.data; } evetHadler(e){ //CustomEvet cosole.log("page this = "+this); cosole.log(this); let type = e.type.slice(this.ame.legth+1); // eg : idex_oLoad , remove 'idex_' cosole.log("recv evet "+e.type); if(this.opt[type]){ this.opt[type]({}); }else if(this[type]){ this[type].bid(this)(); }else{ cosole.log("Page: ukow evet "+ type); } if(type == 'oLoad'){ //if oload fiish ,start to reder this.reder(); //this.fireMyEvet.bid(this)('reder'); } } setWXml(wxml){ this.wxml = wxml; } reder(){ cosole.log("reder called"); let template = this.wxml; let parser =ew WXmlParser(this.getData()); let domJso = parser.strigToDomJSON(template)[0]; let dom = parser.jsoToDom(domJso); documet.getElemetById('app').appedChild(dom); this.fireEvet('oShow');//for App object } fireMyEvet(type){ type = this.ame+"_"+type; cosole.log("fireEvet "+type); documet.dispatchEvet(ew CustomEvet(type,{})); } fireEvet(type){ cosole.log("fireEvet "+type); documet.dispatchEvet(ew CustomEvet(type,{})); }}构造函数中又是一堆晃瞎我眼的bid。另外你换进来的那个对象仍然被存到了opt中。setName函数是被App调用的。设置了这个页面的名字。在名字设定后,就会注册一堆事件监听者。注册的列表在evet_list这个变量里。以后这个列表可以逐渐完善。上面说到的那个idex_oLoad事件就是通过页面名字和事件名拼接出来的。
事件监听函数是evetHadle。把oLoad这个字眼从idex_oLoad中切除来。
let type = e.type.slice(this.ame.legth+1); // eg : idex_oLoad , remove 'idex_'然后查找this.opt就是你传进来的那个对象是否有oLoad的声明。如果有,则调用,如果没有,则尝试在this中查找,如果还是没有,就真的没有了。
下面说比较重要的渲染问题
WXmlParser渲染wxml文件这部分参考了wiv,里面也有少量我贡献的嗲吗,我只是对其做了重构,以方便调用。看代码
/** * Created by Togfeg Yag o 2017/1/25. * Some code copied from https://github.com/phodal/wiv ,which is uder MIT . */class Utils{ removeTemplateTag(str){ retur str.substr(2, str.legth - 4); } isTemplateTag(strig){ retur /{{[a-zA-Z1-9\\.]+}}/.test(strig); }}export default class WXmlParser{ costructor(data){ this.data= data; this.strigToDomJSON=this.strigToDomJSON.bid(this); this.odeToJSON=this.odeToJSON.bid(this); this.jsoToDom=this.jsoToDom.bid(this); this.domParser = this.domParser.bid(this); this.getData = this.getData.bid(this); this.utils = ew Utils(); } strigToDomJSON(strig){ strig = '<div class="page"><div class="page__hd">' + strig + '</div></div>'; var jso = this.odeToJSON(this.domParser(strig)); if (jso.odeType === 9) { jso = jso.childNodes; } retur jso; } getData(key) { if(!key)retur ull; var ka = key.split("."); var ret = this.data[ka[0]]; for(var i = 1;i<ka.legth;i++){ if(!ret)retur ull; //ca't fid ! ret= ret[ka[i]]; } retur ret; } odeToJSON(ode){ // Code base o https://gist.github.com/sstur/7379870 ode = ode || this; var obj = { odeType: ode.odeType }; if (ode.tagName) { obj.tagName = 'wiv-' + ode.tagName.toLowerCase(); } else if (ode.odeName) { obj.odeName = ode.odeName; } if (ode.odeValue) { obj.odeValue = ode.odeValue; if(this.utils.isTemplateTag(ode.odeValue)){ obj.odeValue = this.getData(this.utils.removeTemplateTag(ode.odeValue)); } } var attrs = ode.attributes; if (attrs) { var legth = attrs.legth; var arr = obj.attributes = ew Array(legth); for (var i = 0; i < legth; i++) { var attr = attrs[i]; arr[i] = [attr.odeName, attr.odeValue]; } } var childNodes = ode.childNodes; if (childNodes) { legth = childNodes.legth; arr = obj.childNodes = ew Array(legth); for (i = 0; i < legth; i++) { arr[i] = this.odeToJSON(childNodes[i]); } } retur obj; } jsoToDom(obj) { // Code base o https://gist.github.com/sstur/7379870 if (typeof obj == 'strig') { obj = JSON.parse(obj); } var ode, odeType = obj.odeType; switch (odeType) { case 1: //ELEMENT_NODE ode = documet.createElemet(obj.tagName); var attributes = obj.attributes || []; for (var i = 0, le = attributes.legth; i < le; i++) { var attr = attributes[i]; ode.setAttribute(attr[0], attr[1]); } break; case 3: //TEXT_NODE ode = documet.createTextNode(obj.odeValue); break; case 8: //COMMENT_NODE ode = documet.createCommet(obj.odeValue); break; case 9: //DOCUMENT_NODE ode = documet.implemetatio.createDocumet('https://www.w3.org/1999/xhtml', 'html', ull); break; case 10: //DOCUMENT_TYPE_NODE ode = documet.implemetatio.createDocumetType(obj.odeName); break; case 11: //DOCUMENT_FRAGMENT_NODE ode = documet.createDocumetFragmet(); break; default: retur ode; } if (odeType == 1 || odeType == 11) { var childNodes = obj.childNodes || []; for (i = 0, le = childNodes.legth; i < le; i++) { ode.appedChild(this.jsoToDom(childNodes[i])); } } retur ode; } domParser(strig){ var parser = ew DOMParser(); retur parser.parseFromStrig(strig, 'text/xml'); }}Page.js中的渲染函数
reder(){ cosole.log("reder called"); let template = this.wxml; let parser =ew WXmlParser(this.getData()); let domJso = parser.strigToDomJSON(template)[0]; let dom = parser.jsoToDom(domJso); documet.getElemetById('app').appedChild(dom); this.fireEvet('oShow');//for App object }基本原理首先通过DOMParser将wxml解析一下(domParser)。编程一个dom对象。将其变为domJSON.然后再讲domJSON转换会dom对象。这一步中包含{{}}标签的处理。用的正则表达式匹配。
最后,这个问题还是很多的。比如那个appedChild。。在后面的开发中会替换成diff和apply。渲染完了,发送事件。
TODO事件机制还不完善。
渲染的diff和apply的实现。
setData这个核心的函数实现。
参照微信文档进行界面完全兼容
完善wx的API函数(可能会用Adroid实现)IOS就算了,听说基于WebView的通不过审核!另外,我没钱买Mac。。。
最后感谢您的阅读。如果有可能请贡献些代码。。。
评论