FreeMina 兼容微信小程序Mina框架开源项目

我要开发同款
匿名用户2017年01月25日
56阅读

技术信息

开源地址
https://github.com/tencentyun/wecos
授权协议
Apache

作品详情

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。。。

最后

感谢您的阅读。如果有可能请贡献些代码。。。

功能介绍

FreeMina: An open mina compatible framework for running in browser or webview. 一个兼容微信小程序Mina框架的开源...

声明:本文仅代表作者观点,不代表本站立场。如果侵犯到您的合法权益,请联系我们删除侵权资源!如果遇到资源链接失效,请您通过评论或工单的方式通知管理员。未经允许,不得转载,本站所有资源文章禁止商业使用运营!
下载安装【程序员客栈】APP
实时对接需求、及时收发消息、丰富的开放项目需求、随时随地查看项目状态

评论