30分钟熟悉微信小程序

作者:叶小钗 

www.cnblogs.com/yexiaochai/p/9431816.html


首先我们来一言以蔽之,什么是微信小程序?PS:这个问题问得好像有些扯:)

小程序是一个不需要下载安装就可使用的应用,它实现了应用触手可及的梦想,用户扫一扫或者搜一下即可打开应用。也体现了用完即走的理念,用户不用关心是否安装太多应用的问题。应用将无处不在,随时可用,但又无需安装卸载。从字面上看小程序具有类似Web应用的热部署能力,在功能上又接近于原生APP。

所以说,其实微信小程序是一套超级Hybrid的解决方案,现在看来,小程序应该是应用场景最广,也最为复杂的解决方案了。

很多公司都会有自己的Hybrid平台,我这里了解到比较不错的是携程的Hybrid平台、阿里的Weex、百度的糯米,但是从应用场景来说都没有微信来得丰富,这里根本的区别是:

微信小程序是给各个公司开发者接入的,其他公司平台多是给自己业务团队使用,这一根本区别,就造就了我们看到的很多小程序不一样的特性:

小程序定义了自己的标签语言WXML

小程序定义了自己的样式语言WXSS

小程序提供了一套前端框架包括对应Native API

禁用浏览器Dom API(这个区别,会影响我们的代码方式)

只要了解到这些区别就会知道为什么小程序会这么设计:

因为小程序是给各个公司的开发做的,其他公司的Hybrid方案是给公司业务团队用的,一般拥有Hybrid平台的公司实力都不错。但是开发小程序的公司实力良莠不齐,所以小程序要做绝对的限制,最大程度的保证框架层(小程序团队)对程序的控制。因为毕竟程序运行在微信这种体量的APP中

之前我也有一个疑惑为什么微信小程序会设计自己的标签语言,也在知乎看到各种各样的回答,但是如果出于设计层面以及应用层面考虑的话:这样会有更好的控制,而且我后面发现微信小程序事实上依旧使用的是webview做渲染(这个与我之前认为微信是NativeUI是向左的),但是如果我们使用的微信限制下面的标签,这个是有限的标签,后期想要换成NativeUI会变得更加轻易:

另一方面,经过之前的学习,我这边明确可以得出一个感受:

小程序的页面核心是标签,标签是不可控制的(我暂时没用到js操作元素的方法),只能按照微信给的玩法玩,标签控制显示是我们的view

标签的展示只与data有关联,和js是隔离的,没有办法在标签中调用js的方法

而我们的js的唯一工作便是根据业务改变data,重新引发页面渲染,以后别想操作DOM,别想操作Window对象了,改变开发方式,改变开发方式,改变开发方式!


this.setData({'wxml': `

  <my-component>

  <view>动态插入的节点</view>

  </my-component>

`});


然后可以看到这个是一个MVC模型

每个页面的目录是这个样子的:

project

├── pages

|   ├── index

|   |   ├── index.json  index 页面配置

|   |   ├── index.js    index 页面逻辑

|   |   ├── index.wxml  index 页面结构

|   |   └── index.wxss  index 页面样式表

|   └── log

|       ├── log.json    log 页面配置

|       ├── log.wxml    log 页面逻辑

|       ├── log.js      log 页面结构

|       └── log.wxss    log 页面样式表

├── app.js              小程序逻辑

├── app.json            小程序公共设置

└── app.wxss            小程序公共样式表


每个组件的目录也大概是这个样子的,大同小异,但是入口是Page层。

小程序打包后的结构(这里就真的不懂了,引用:小程序底层框架实现原理解析):

所有的小程序基本都最后都被打成上面的结构

1、WAService.js 框架JS库,提供逻辑层基础的API能力

2、WAWebview.js 框架JS库,提供视图层基础的API能力

3、WAConsole.js 框架JS库,控制台

4、app-config.js 小程序完整的配置,包含我们通过app.json里的所有配置,综合了默认配置型

5、app-service.js 我们自己的JS代码,全部打包到这个文件

6、page-frame.html 小程序视图的模板文件,所有的页面都使用此加载渲染,且所有的WXML都拆解为JS实现打包到这里

7、pages 所有的页面,这个不是我们之前的wxml文件了,主要是处理WXSS转换,使用js插入到header区域

从设计的角度上说,小程序采用的组件化开发的方案,除了页面级别的标签,后面全部是组件,而组件中的标签view、data、js的关系应该是与page是一致的,这个也是我们平时建议的开发方式,将一根页面拆分成一个个小的业务组件或者UI组件:

所有的小程序基本都最后都被打成上面的结构

WAService.js 框架JS库,提供逻辑层基础的API能力

WAWebview.js 框架JS库,提供视图层基础的API能力

WAConsole.js 框架JS库,控制台

app-config.js 小程序完整的配置,包含我们通过app.json里的所有配置,综合了默认配置型

app-service.js 我们自己的JS代码,全部打包到这个文件

page-frame.html 小程序视图的模板文件,所有的页面都使用此加载渲染,且所有的WXML都拆解为JS实现打包到这里

pages 所有的页面,这个不是我们之前的wxml文件了,主要是处理WXSS转换,使用js插入到header区域

从设计的角度上说,小程序采用的组件化开发的方案,除了页面级别的标签,后面全部是组件,而组件中的标签view、data、js的关系应该是与page是一致的,这个也是我们平时建议的开发方式,将一根页面拆分成一个个小的业务组件或者UI组件:

从我写业务代码过程中,觉得整体来说还是比较顺畅的,小程序是有自己一套完整的前端框架的,并且释放给业务代码的主要就是page,而page只能使用标签和组件,所以说框架的对业务的控制力度很好。

最后我们从工程角度来看微信小程序的架构就更加完美了,小程序从三个方面考虑了业务者的感受:

开发工具+调试工具

开发基本模型(开发基本标准WXML、WXSS、JS、JSON)

完善的构建(对业务方透明)

自动化上传离线包(对业务费透明离线包逻辑)

监控统计逻辑

所以,微信小程序从架构上和使用场景来说是很令人惊艳的,至少惊艳了我……所以我们接下来在开发层面对他进行更加深入的剖析,我们这边最近一直在做基础服务,这一切都是为了完善技术体系,这里对于前端来说便是我们需要做一个Hybrid体系,如果做App,React Native也是不错的选择,但是一定要有完善的分层:

底层框架解决开发效率,将复杂的部分做成一个黑匣子,给页面开发展示的只是固定的三板斧,固定的模式下开发即可

工程部门为业务开发者封装最小化开发环境,最优为浏览器,确实不行便为其提供一个类似浏览器的调试环境

如此一来,业务便能快速迭代,因为业务开发者写的代码大同小异,所以底层框架配合工程团队(一般是同一个团队),便可以在底层做掉很多效率性能问题。

稍微大点的公司,稍微宽裕的团队,还会同步做很多后续的性能监控、错误日志工作,如此形成一套文档->开发->调试->构建->发布->监控、分析 为一套完善的技术体系

如果形成了这么一套体系,那么后续就算是内部框架更改、技术革新,也是在这个体系上改造,这块微信小程序是做的非常好的。但很可惜,很多其他公司团队只会在这个路径上做一部分,后面由于种种原因不在深入,有可能是感觉没价值,而最恐怖的行为是,自己的体系没形成就贸然的换基础框架,戒之慎之啊!好了闲话少说,我们继续接下来的学习。

微信小程序的执行流程

微信小程序为了对业务方有更强的控制,App层做的工作很有限,我后面写demo的时候根本没有用到app.js,所以我这里认为app.js只是完成了一个路由以及初始化相关的工作,这个是我们看得到的,我们看不到的是底层框架会根据app.json的配置将所有页面js都准备好。

我这里要表达的是,我们这里配置了我们所有的路由:

"pages":[

  "pages/index/index",

  "pages/list/list",

  "pages/logs/logs"

],


微信小程序一旦载入,会开3个webview,装载3个页面的逻辑,完成基本的实例化工作,只显示首页!这个是小程序为了优化页面打开速度所做的工作,也势必会浪费一些资源,所以到底是全部打开或者预加载几个,详细底层Native会根据实际情况动态变化,我们也可以看到,从业务层面来说,要了解小程序的执行流程,其实只要能了解Page的流程就好了,关于Page生命周期,除了释放出来的API:onLoad -> onShow -> onReady -> onHide等,官方还出了一张图进行说明:

Native层在载入小程序时候,起了两个线程一个的view Thread一个是AppService Thread,我这边理解下来应该就是程序逻辑执行与页面渲染分离,小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。

因为之前我认为页面是使用NativeUI做渲染跟Webview没撒关系,便觉得这个图有问题,但是后面实际代码看到了熟悉的shadow-dom以及Android可以看到哪部分是Web的,其实小程序主体还是使用的浏览器渲染的方式,还是webview装载HTML和CSS的逻辑,最后我发现这张图是没有问题的,有问题的是我的理解,哈哈,这里我们重新解析这张图:

WXML先会被编译成JS文件,引入数据后在WebView中渲染,这里可以认为微信载入小程序时同时初始化了两个线程,分别执行彼此逻辑:

WXML&CSS编译形成的JS View实例化结束,准备结束时向业务线程发送通知

业务线程中的JS Page部分同步完成实例化结束,这个时候接收到View线程部分的等待数据通知,将初始化data数据发送给View

View线程接到数据,开始渲染页面,渲染结束执行通知Page触发onReady事件

这里翻开源码,可以看到,应该是全局控制器完成的Page实例化,完成后便会执行onLoad事件,但是在执行前会往页面发通知:

__appServiceSDK__.invokeWebviewMethod({

    name: "appDataChange",

    args: o({}, e, {

        complete: n

    }),

    webviewIds: [t]

})


真实的逻辑是这样的,全局控制器会完成页面实例化,这个是根据app.json中来的,全部完成实例化存储起来然后选择第一个page实例执行一些逻辑,然后通知view线程,即将执行onLoad事件,因为view线程和业务线程是两个线程,所以不会造成阻塞,view线程根据初始数据完成渲染,而业务线程继续后续逻辑,执行onLoad,如果onLoad中有setData,那么会进入队列继续通知view线程更新。

所以我个人感觉微信官网那张图不太清晰,我这里重新画了一个图:

再引用一张其他地方的图:

模拟实现

都这个时候了,不来个简单的小程序框架实现好像有点不对,我们做小程序实现的主要原因是想做到一端代码三端运行:web、小程序、Hybrid甚至Servce端

我们这里没有可能实现太复杂的功能,这里想的是就实现一个基本的页面展示带一个最基本的标签即可,只做Page一块的简单实现,让大家能了解到小程序可能的实现,以及如何将小程序直接转为H5的可能走法

<view>

  <!-- 以下是对一个自定义组件的引用 -->

  <my-component inner-text="组件数据"></my-component>

  <view>{{pageData}}</view>

</view>

Page({

  data: {

    pageData: '页面数据'

  },

  onLoad: function () {

    console.log('onLoad')

  },

})

<!-- 这是自定义组件的内部WXML结构 -->

<view class="inner">

  {{innerText}}

</view>

<slot></slot>

Component({

  properties: {

    // 这里定义了innerText属性,属性值可以在组件使用时指定

    innerText: {

      type: String,

      value: 'default value',

    }

  },

  data: {

    // 这里是一些组件内部数据

    someData: {}

  },

  methods: {

    // 这里是一个自定义方法

    customMethod: function () { }

  }

})

我们直接将小程序这些代码拷贝一份到我们的目录:

我们需要做的就是让这段代码运行起来,而这里的目录是我们最终看见的目录,真实运行的时候可能不是这个样,运行之前项目会通过我们的工程构建,变成可以直接运行的代码,而我这里思考的可以运行的代码事实上是一个模块,所以我们这里从最终结果反推、分拆到开发结构目录,我们首先将所有代码放到index.html,可能是这样的:

<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8">

  <title>Title</title>

</head>

<body>


<script type="text/javascript" src="libs/zepto.js" ></script>

<script type="text/javascript">


  class View {

    constructor(opts) {

      this.template = '<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>';


      //由控制器page传入的初始数据或者setData产生的数据

      this.data = {

        pageShow: 'pageshow',

        pageData: 'pageData',

        pageShow1: 'pageShow1'

      };


      this.labelMap = {

        'view': 'div',

        '#text': 'span'

      };


      this.nodes = {};

      this.nodeInfo = {};

    }


    /*

      传入一个节点,解析出一个节点,并且将节点中的数据以初始化数据改变

      并且将其中包含{{}}标志的节点信息记录下来

    */

    _handlerNode (node) {


      let reg = /{{([sS]+?)}}/;

      let result, name, value, n, map = {};

      let attrs , i, len, attr;


      name = node.nodeName;

      attrs = node.attributes;

      value = node.nodeValue;

      n = document.createElement(this.labelMap[name.toLowerCase()] || name);


      //说明是文本,需要记录下来了

      if(node.nodeType === 3) {

        n.innerText =  this.data[value] || '';


        result =  reg.exec(value);

        if(result) {

          n.innerText =  this.data[result[1]] || '';


          if(!map[result[1]]) map[result[1]] = [];

          map[result[1]].push({

            type: 'text',

            node: n

          });

        }

      }


      if(attrs) {

        //这里暂时只处理属性和值两种情况,多了就复杂10倍了

        for (i = 0, len = attrs.length; i < len; i++) {

          attr = attrs[i];

          result = reg.exec(attr.value);


          n.setAttribute(attr.name, attr.value);

          //如果有node需要处理则需要存下来标志

          if (result) {

            n.setAttribute(attr.name, this.data[result[1]] || '');


            //存储所有会用到的节点,以便后面动态更新

            if (!map[result[1]]) map[result[1]] = [];

            map[result[1]].push({

              type: 'attr',

              name: attr.name,

              node: n

            });


          }

        }

      }


      return {

        node: n,

        map: map

      }


    }


    //遍历一个节点的所有子节点,如果有子节点继续遍历到没有为止

    _runAllNode(node, map, root) {


      let nodeInfo = this._handlerNode(node);

      let _map = nodeInfo.map;

      let n = nodeInfo.node;

      let k, i, len, children = node.childNodes;


      //先将该根节点插入到上一个节点中

      root.appendChild(n);


      //处理map数据,这里的map是根对象,最初的map

      for(k in _map) {

        if(map[k]) {

          map[k].push(_map[k]);

        } else {

          map[k] = _map[k];

        }

      }


      for(i = 0, len = children.length; i < len; i++) {

        this._runAllNode(children[i], map, n);

      }


    }


    //处理每个节点,翻译为页面识别的节点,并且将需要操作的节点记录

    splitTemplate () {

      let nodes = $(this.template);

      let map = {}, root = document.createElement('div');

      let i, len;


      for(i = 0, len = nodes.length; i < len; i++) {

        this._runAllNode(nodes[i], map, root);

      }


      window.map = map;

      return root

    }


      //拆分目标形成node,这个方法过长,真实项目需要拆分

    splitTemplate1 () {

      let template = this.template;

      let node = $(this.template)[0];

      let map = {}, n, name, root = document.createElement('div');

      let isEnd = false, index = 0, result;


      let attrs, i, len, attr;

      let reg = /{{([sS]+?)}}/;


      window.map = map;


      //开始遍历节点,处理

      while (!isEnd) {

        name = node.localName;

        attrs = node.attributes;

        value = node.nodeValue;

        n = document.createElement(this.labelMap[name] || name);


        //说明是文本,需要记录下来了

        if(node.nodeType === 3) {

          n.innerText =  this.data[value] || '';


          result =  reg.exec(value);

          if(result) {

            n.innerText =  this.data[value] || '';


            if(!map[value]) map[value] = [];

            map[value].push({

              type: 'text',

              node: n

            });

          }

        }


        //这里暂时只处理属性和值两种情况,多了就复杂10倍了

        for(i = 0, len = attrs.length; i < len; i++) {

          attr = attrs[i];

          result =  reg.exec(attr.value);


          n.setAttribute(attr.name, attr.value);

          //如果有node需要处理则需要存下来标志

          if(result) {

            n.setAttribute(attr.name, this.data[result[1]] || '');


            //存储所有会用到的节点,以便后面动态更新

            if(!map[result[1]]) map[result[1]] = [];

            map[result[1]].push({

              type: 'attr',

              name: attr.name,

              node: n

            });


          }

        }


debugger


        if(index === 0) root.appendChild(n);

        isEnd = true;

        index++;


      }


      return root;



      console.log(node)

    }


  }


  let view = new View();


  document.body.appendChild(window.node)


</script>

</body>

</html>


这段代码,非常简单:

① 设置了一段模板,甚至,我们这里根本不关系其格式化状态,直接写成一行方便处理

this.template = '<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>';

② 然后我们将这段模板转为node节点(这里可以不用zepto,但是模拟实现怎么简单怎么来吧),然后遍历处理所有节点,我们就可以处理我们的数据了,最终形成了这个html:

<div><div><span>ffsd</span></div><div class="ddd" is-show="pageshow"><span>pageshow</span><div class="c1"><span>pageData</span></div></div></div>


③ 与此同时,我们存储了一个对象,这个对象包含所有与之相关的节点:

这个对象是所有setData会影响到node的一个映射表,后面调用setData的时候,便可以直接操作对应的数据了,这里我们分拆我们代码,形成了几个关键部分,首先是View类,这个对应我们的模板,是核心类:

//View为模块的实现,主要用于解析目标生产node

class View {

  constructor(template) {

    this.template = template;


    //由控制器page传入的初始数据或者setData产生的数据

    this.data = {};


    this.labelMap = {

      'view': 'div',

      '#text': 'span'

    };


    this.nodes = {};

    this.root = {};

  }


  setInitData(data) {

    this.data = data;

  }


  //数据便会引起的重新渲染

  reRender(data, allData) {

    this.data = allData;

    let k, v, i, len, j, len2, v2;


    //开始重新渲染逻辑,寻找所有保存了的node

    for(k in data) {

      if(!this.nodes[k]) continue;

      for(i = 0, len = this.nodes[k].length; i < len; i++) {

        for(j = 0; j < this.nodes[k][i].length; j++) {

          v = this.nodes[k][i][j];

          if(v.type === 'text') {

            v.node.innerText = data[k];

          } else if(v.type === 'attr') {

            v.node.setAttribute(v.name, data[k]);

          }

        }

      }

    }

  }

  /*

    传入一个节点,解析出一个节点,并且将节点中的数据以初始化数据改变

    并且将其中包含{{}}标志的节点信息记录下来

  */

  _handlerNode (node) {


    let reg = /{{([sS]+?)}}/;

    let result, name, value, n, map = {};

    let attrs , i, len, attr;


    name = node.nodeName;

    attrs = node.attributes;

    value = node.nodeValue;

    n = document.createElement(this.labelMap[name.toLowerCase()] || name);


    //说明是文本,需要记录下来了

    if(node.nodeType === 3) {

      n.innerText =  this.data[value] || '';


      result =  reg.exec(value);

      if(result) {

        n.innerText =  this.data[result[1]] || '';


        if(!map[result[1]]) map[result[1]] = [];

        map[result[1]].push({

          type: 'text',

          node: n

        });

      }

    }


    if(attrs) {

      //这里暂时只处理属性和值两种情况,多了就复杂10倍了

      for (i = 0, len = attrs.length; i < len; i++) {

        attr = attrs[i];

        result = reg.exec(attr.value);


        n.setAttribute(attr.name, attr.value);

        //如果有node需要处理则需要存下来标志

        if (result) {

          n.setAttribute(attr.name, this.data[result[1]] || '');


          //存储所有会用到的节点,以便后面动态更新

          if (!map[result[1]]) map[result[1]] = [];

          map[result[1]].push({

            type: 'attr',

            name: attr.name,

            node: n

          });


        }

      }

    }


    return {

      node: n,

      map: map

    }


  }


  //遍历一个节点的所有子节点,如果有子节点继续遍历到没有为止

  _runAllNode(node, map, root) {


    let nodeInfo = this._handlerNode(node);

    let _map = nodeInfo.map;

    let n = nodeInfo.node;

    let k, i, len, children = node.childNodes;


    //先将该根节点插入到上一个节点中

    root.appendChild(n);


    //处理map数据,这里的map是根对象,最初的map

    for(k in _map) {

      if(!map[k]) map[k] = [];

      map[k].push(_map[k]);

    }


    for(i = 0, len = children.length; i < len; i++) {

      this._runAllNode(children[i], map, n);

    }


  }


  //处理每个节点,翻译为页面识别的节点,并且将需要操作的节点记录

  splitTemplate () {

    let nodes = $(this.template);

    let map = {}, root = document.createElement('div');

    let i, len;


    for(i = 0, len = nodes.length; i < len; i++) {

      this._runAllNode(nodes[i], map, root);

    }


    this.nodes = map;

    this.root = root;

  }


  render() {

    let i, len;

    this.splitTemplate();

    for(i = 0, len = this.root.childNodes.length; i< len; i++)

      document.body.appendChild(this.root.childNodes[0]);

  }


}


这个类主要完成的工作是:

接受传入的template字符串(直接由index.wxml读出)

解析template模板,生成字符串和兼职与node映射表,方便后期setData导致的改变

渲染和再次渲染工作

然后就是我们的Page类的实现了,这里反而比较简单(当然这里的实现是不完善的):

//这个为js罗杰部分实现,后续会释放工厂方法

class PageClass {

  //构造函数,传入对象

  constructor(opts) {


    //必须拥有的参数

    this.data = {};

    Object.assign(this, opts);

  }


  //核心方法,每个Page对象需要一个模板实例

  setView(view) {

    this.view = view;

  }


  //核心方法,设置数据后会引发页面刷新

  setData(data) {

    Object.assign(this.data, data);


    //只影响改变的数据

    this.view.reRender(data, this.data)

  }


  render() {

    this.view.setInitData(this.data);

    this.view.render();


    if(this.onLoad) this.onLoad();

  }


}


现在轮着我们实际调用方,Page方法出场了:

function Page (data) {

  let page = new PageClass(data);

  return page;

}


基本上什么都没有干的感觉,调用层代码这样写:

function main() {

  let view = new View('<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>');

  let page = Page({

    data: {

      pageShow: 'pageshow',

      pageData: 'pageData',

      pageShow1: 'pageShow1'

    },

    onLoad: function () {

      this.setData({

        pageShow: '我是pageShow啊'

      });

    }

  });


  page.setView(view);

  page.render();

}


main();


于是,我们可以看到页面的变化,由开始的初始化页面到执行onLoad时候的变化:


这里是最终完整的代码:


<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8">

  <title>Title</title>

</head>

<body>


<script type="text/javascript" src="libs/zepto.js" ></script>

<script type="text/javascript">


//这个为js罗杰部分实现,后续会释放工厂方法

class PageClass {

  //构造函数,传入对象

  constructor(opts) {


    //必须拥有的参数

    this.data = {};

    Object.assign(this, opts);

  }


  //核心方法,每个Page对象需要一个模板实例

  setView(view) {

    this.view = view;

  }


  //核心方法,设置数据后会引发页面刷新

  setData(data) {

    Object.assign(this.data, data);


    //只影响改变的数据

    this.view.reRender(data, this.data)

  }


  render() {

    this.view.setInitData(this.data);

    this.view.render();


    if(this.onLoad) this.onLoad();

  }


}


//View为模块的实现,主要用于解析目标生产node

class View {

  constructor(template) {

    this.template = template;


    //由控制器page传入的初始数据或者setData产生的数据

    this.data = {};


    this.labelMap = {

      'view': 'div',

      '#text': 'span'

    };


    this.nodes = {};

    this.root = {};

  }


  setInitData(data) {

    this.data = data;

  }


  //数据便会引起的重新渲染

  reRender(data, allData) {

    this.data = allData;

    let k, v, i, len, j, len2, v2;


    //开始重新渲染逻辑,寻找所有保存了的node

    for(k in data) {

      if(!this.nodes[k]) continue;

      for(i = 0, len = this.nodes[k].length; i < len; i++) {

        for(j = 0; j < this.nodes[k][i].length; j++) {

          v = this.nodes[k][i][j];

          if(v.type === 'text') {

            v.node.innerText = data[k];

          } else if(v.type === 'attr') {

            v.node.setAttribute(v.name, data[k]);

          }

        }

      }

    }

  }

  /*

    传入一个节点,解析出一个节点,并且将节点中的数据以初始化数据改变

    并且将其中包含{{}}标志的节点信息记录下来

  */

  _handlerNode (node) {


    let reg = /{{([sS]+?)}}/;

    let result, name, value, n, map = {};

    let attrs , i, len, attr;


    name = node.nodeName;

    attrs = node.attributes;

    value = node.nodeValue;

    n = document.createElement(this.labelMap[name.toLowerCase()] || name);


    //说明是文本,需要记录下来了

    if(node.nodeType === 3) {

      n.innerText =  this.data[value] || '';


      result =  reg.exec(value);

      if(result) {

        n.innerText =  this.data[result[1]] || '';


        if(!map[result[1]]) map[result[1]] = [];

        map[result[1]].push({

          type: 'text',

          node: n

        });

      }

    }


    if(attrs) {

      //这里暂时只处理属性和值两种情况,多了就复杂10倍了

      for (i = 0, len = attrs.length; i < len; i++) {

        attr = attrs[i];

        result = reg.exec(attr.value);


        n.setAttribute(attr.name, attr.value);

        //如果有node需要处理则需要存下来标志

        if (result) {

          n.setAttribute(attr.name, this.data[result[1]] || '');


          //存储所有会用到的节点,以便后面动态更新

          if (!map[result[1]]) map[result[1]] = [];

          map[result[1]].push({

            type: 'attr',

            name: attr.name,

            node: n

          });


        }

      }

    }


    return {

      node: n,

      map: map

    }


  }


  //遍历一个节点的所有子节点,如果有子节点继续遍历到没有为止

  _runAllNode(node, map, root) {


    let nodeInfo = this._handlerNode(node);

    let _map = nodeInfo.map;

    let n = nodeInfo.node;

    let k, i, len, children = node.childNodes;


    //先将该根节点插入到上一个节点中

    root.appendChild(n);


    //处理map数据,这里的map是根对象,最初的map

    for(k in _map) {

      if(!map[k]) map[k] = [];

      map[k].push(_map[k]);

    }


    for(i = 0, len = children.length; i < len; i++) {

      this._runAllNode(children[i], map, n);

    }


  }


  //处理每个节点,翻译为页面识别的节点,并且将需要操作的节点记录

  splitTemplate () {

    let nodes = $(this.template);

    let map = {}, root = document.createElement('div');

    let i, len;


    for(i = 0, len = nodes.length; i < len; i++) {

      this._runAllNode(nodes[i], map, root);

    }


    this.nodes = map;

    this.root = root;

  }


  render() {

    let i, len;

    this.splitTemplate();

    for(i = 0, len = this.root.childNodes.length; i< len; i++)

      document.body.appendChild(this.root.childNodes[0]);

  }


}


function Page (data) {

  let page = new PageClass(data);

  return page;

}


function main() {

  let view = new View('<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>');

  let page = Page({

    data: {

      pageShow: 'pageshow',

      pageData: 'pageData',

      pageShow1: 'pageShow1'

    },

    onLoad: function () {

      this.setData({

        pageShow: '我是pageShow啊'

      });

    }

  });


  page.setView(view);

  page.render();

}


main();


</script>

</body>

</html>


我们简单的模拟便先到此结束,这里结束的比较仓促有一些原因:

这段代码可以是最终打包构建形成的代码,但是我这里的完成度只有百分之一,后续需要大量的构建相关介入

这篇文章目的还是接受开发基础,而本章模拟实现太过复杂,如果篇幅大了会主旨不清

这个是最重要的点,我一时也写不出来啊!!!,所以各位等下个长篇,小程序前端框架模拟实现吧

如果继续实现,这里马上要遇到组件处理、事件模型、分文件构建等高端知识,时间会拉得很长

感兴趣的小伙伴,可以关注公众号【grain先森】,回复关键词 “小程序”,获取更多资料,更多关键词玩法期待你的探索~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容