俄罗斯贵宾会-俄罗斯贵宾会官网
做最好的网站

【微信小程序项目实践总结】30分钟从陌生到熟悉

微信小程序开发05-日历组件的实现

2018/08/07 · 基础技术 · 小程序

原文出处: 叶小钗   

接上文:微信小程序开发04-打造自己的UI库

github地址:https://github.com/yexiaochai/wxdemo

我们这里继续实现我们的日历组件,这个日历组件稍微有点特殊,算是相对复杂的组件了,然后一般的日历组件又会有很多的变化,所以我们这里实现最基本的标签即可:

图片 1

let View = require('behavior-view'); const util = require('../utils/util.js'); // const dateUtil = util.dateUtil; Component({ behaviors: [ View ], properties: { }, data: { weekDayArr: ['日', '一', '二', '三', '四', '五', '六'], displayMonthNum: 1, //当前显示的时间 displayTime: null, //可以选择的最早时间 startTime: null, //最晚时间 endTime: null, //当前时间,有时候是读取服务器端 curTime: new Date() }, attached: function () { //console.log(this) }, methods: { } })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let View = require('behavior-view');
const util = require('../utils/util.js');
 
// const dateUtil = util.dateUtil;
 
Component({
  behaviors: [
    View
  ],
  properties: {
    
  },
  data: {
    weekDayArr: ['日', '一', '二', '三', '四', '五', '六'],
    displayMonthNum: 1,
 
    //当前显示的时间
    displayTime: null,
    //可以选择的最早时间
    startTime: null,
    //最晚时间
    endTime: null,
 
    //当前时间,有时候是读取服务器端
    curTime: new Date()
    
  },
 
  attached: function () {
    //console.log(this)
  },
  methods: {
  
  }
})

<wxs module="dateUtil"> var isDate = function(date) { return date && date.getMonth; }; var isLeapYear = function(year) { //传入为时间格式需要处理 if (isDate(year)) year = year.getFullYear() if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; return false; }; var getDaysOfMonth = function(date) { var month = date.getMonth(); //注意此处月份要加1,所以我们要减一 var year = date.getFullYear(); return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; } var getBeginDayOfMouth = function(date) { var month = date.getMonth(); var year = date.getFullYear(); var d = getDate(year, month, 1); return d.getDay(); } var getDisplayInfo = function(date) { if (!isDate(date)) { date = getDate(date) } var year = date.getFullYear(); var month = date.getMonth(); var d = getDate(year, month); //这个月一共多少天 var days = getDaysOfMonth(d); //这个月是星期几开始的 var beginWeek = getBeginDayOfMouth(d); /* console.log('info',JSON.stringify( { year: year, month: month, days: days, beginWeek: beginWeek })); */ return { year: year, month: month, days: days, beginWeek: beginWeek } } module.exports = { getDipalyInfo: getDisplayInfo } </wxs> <view class="cm-calendar"> <view class="cm-calendar-hd "> <block wx:for="{{weekDayArr}}"> <view class="item">{{item}}</view> </block> </view> <view class="cm-calendar-bd "> <view class="cm-month "> </view> <view class="cm-day-list"> <block wx:for="{{dateUtil.getDipalyInfo(curTime).days + dateUtil.getDipalyInfo(curTime).beginWeek}}" wx:for-index="index"> <view wx:if="{{index < dateUtil.getDipalyInfo(curTime).beginWeek }}" class="item active"></view> <view wx:else class="item">{{index + 1 - dateUtil.getDipalyInfo(curTime).beginWeek}}</view> </block> <view class=" active cm-item--disabled " data-cndate="" data-date=""> </view> </view> </view> </view>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<wxs module="dateUtil">
  var isDate = function(date) {
    return date && date.getMonth;
  };
 
  var isLeapYear = function(year) {
    //传入为时间格式需要处理
    if (isDate(year)) year = year.getFullYear()
    if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true;
    return false;
  };
 
  var getDaysOfMonth = function(date) {
    var month = date.getMonth(); //注意此处月份要加1,所以我们要减一
    var year = date.getFullYear();
    return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
  }
 
  var getBeginDayOfMouth = function(date) {
    var month = date.getMonth();
    var year = date.getFullYear();
    var d = getDate(year, month, 1);
    return d.getDay();
  }
 
  var getDisplayInfo = function(date) {
    if (!isDate(date)) {
      date = getDate(date)
    }
    var year = date.getFullYear();
 
    var month = date.getMonth();
    var d = getDate(year, month);
 
    //这个月一共多少天
    var days = getDaysOfMonth(d);
 
    //这个月是星期几开始的
    var beginWeek = getBeginDayOfMouth(d);
 
    /*
        console.log('info',JSON.stringify( {
          year: year,
          month: month,
          days: days,
          beginWeek: beginWeek
        }));
    */
 
    return {
      year: year,
      month: month,
      days: days,
      beginWeek: beginWeek
    }
  }
 
  module.exports = {
    getDipalyInfo: getDisplayInfo
  }
</wxs>
 
 
<view class="cm-calendar">
  <view class="cm-calendar-hd ">
    <block wx:for="{{weekDayArr}}">
      <view class="item">{{item}}</view>
    </block>
  </view>
  <view class="cm-calendar-bd ">
    <view class="cm-month ">
    </view>
    <view class="cm-day-list">
 
      <block wx:for="{{dateUtil.getDipalyInfo(curTime).days + dateUtil.getDipalyInfo(curTime).beginWeek}}" wx:for-index="index">
 
        <view wx:if="{{index < dateUtil.getDipalyInfo(curTime).beginWeek }}" class="item active"></view>
        <view wx:else class="item">{{index + 1 - dateUtil.getDipalyInfo(curTime).beginWeek}}</view>
 
      </block>
 
      <view class=" active  cm-item--disabled " data-cndate="" data-date="">
 
      </view>
    </view>
  </view>
</view>

这个是非常简陋的日历雏形,在代码过程中有以下几点比较痛苦:

① WXML与js间应该只有数据传递,根本不能传递方法,应该是两个webview的通信,而日历组件这里在WXML层由不得不写一点逻辑

② 本来在WXML中写逻辑已经不太对了,而我们引入的WXS,使用与HTML中的js片段也有很大的不同

这些问题,一度让代码变得复杂,而可以看到一个简单的组件,还没有复杂功能,涉及到的文件都太多了,这里是调用层:

<ui-calendar is-show="" ></ui-calendar>

1
<ui-calendar  is-show="" ></ui-calendar>

事实上,我们以上数据根本不应该写到data里面,应该属性传递,我们这里先为了简单实现功能,接下来我们继续完善这个组件,具体代码请看git:

图片 2

这个日历组件应该是在小程序中写的最复杂的组件了,尤其是很多逻辑判断的代码都放在了WXML里面,根据之前的了解,小程序渲染在一个webview中,js逻辑在一个webview中,他这样做的目的可能是想让性能更好,但是我这里代码写起来事实上是有点痛苦的,我们这里开始组装组件,将数据配置放到属性上,开始组装abstract-page,事实上我认为日历这种非全局组件本来不应该放到基类中:

① 因为Component提供的是一个标签,而且涉及的文件很多,加上继承关系很不好管理

② 因为日历组件事实上是一个标签,所以我们会有一个引入的基础WXML,一个使用的js,完全独立一个文件更加复杂

③ 本来小程序或者复杂的页面都应该组件化开发,所以我们简历一个页面级别的组件,分散到对应的页面中

小程序像是给灵活的HTML&JS戴上了枷锁,只允许在其允许的范围灵活,我们这里尝试对页面进行再拆分:

图片 3

<import src="./mod.searchbox.wxml" /> <view> <template is="searchbox" /> </view> <include src="./mod/calendar.wxml"/> <include src="../../utils/abstract-page.wxml"/>

1
2
3
4
5
6
<import src="./mod.searchbox.wxml" />
<view>
  <template is="searchbox" />
</view>
<include src="./mod/calendar.wxml"/>
<include src="../../utils/abstract-page.wxml"/>

<ui-calendar displayTime="{{CalendarDisplayTime}}" selectedDate="{{CalendarSelectedDate}}" displayMonthNum="{{CalendarDisplayMonthNum}}" is-show="{{isCalendarShow}}" ></ui-calendar>

1
2
3
4
<ui-calendar displayTime="{{CalendarDisplayTime}}"
selectedDate="{{CalendarSelectedDate}}"
displayMonthNum="{{CalendarDisplayMonthNum}}"
is-show="{{isCalendarShow}}" ></ui-calendar>

/* 事实上一个mod就只是一个对象,只不过为了方便拆分,将对象分拆成一个个的mod 一个mod对应一个wxml,但是共享外部的css,暂时如此设计 所有日历模块的需求全部再此实现 */ module.exports = { q: 1, ddd: function(){}, data: { isCalendarShow: '', CalendarDisplayMonthNum: 2, CalendarDisplayTime: new Date(), CalendarSelectedDate: null } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
事实上一个mod就只是一个对象,只不过为了方便拆分,将对象分拆成一个个的mod
一个mod对应一个wxml,但是共享外部的css,暂时如此设计
所有日历模块的需求全部再此实现
*/
module.exports = {
  q: 1,
  ddd: function(){},
 
  data: {
    isCalendarShow: '',
    CalendarDisplayMonthNum: 2,
    CalendarDisplayTime: new Date(),
    CalendarSelectedDate: null
  }
}

核心代码还是在abstract-page里面:

//pageData为页面级别数据,mod为模块数据,要求一定不能重复 initPage(pageData, mod) { //debugger; let _pageData = {}; let key, value, k, v; //为页面动态添加操作组件的方法 Object.assign(_pageData, this.getPageFuncs(), pageData); //生成真实的页面数据 _pageData.data = {}; Object.assign(_pageData.data, this.getPageData(), pageData.data || {}); for( key in mod) { value = mod[key]; for(k in value) { v = value[k]; if(k === 'data') { Object.assign(_pageData.data, v); } else { _pageData[k] = v; } } } console.log(_pageData); return _pageData; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//pageData为页面级别数据,mod为模块数据,要求一定不能重复
  initPage(pageData, mod) {
    //debugger;
    let _pageData = {};
    let key, value, k, v;
 
    //为页面动态添加操作组件的方法
    Object.assign(_pageData, this.getPageFuncs(), pageData);
 
    //生成真实的页面数据
    _pageData.data = {};
    Object.assign(_pageData.data, this.getPageData(), pageData.data || {});
 
    for( key in mod) {
      value = mod[key];
      for(k in value) {
        v = value[k];
        if(k === 'data') {
          Object.assign(_pageData.data, v);
        } else {
          _pageData[k] = v;
        }
      }
    }
 
    console.log(_pageData);
    return _pageData;
  }

这里再改造一下,我们基本的日历组件便完成了80%了:

/* 事实上一个mod就只是一个对象,只不过为了方便拆分,将对象分拆成一个个的mod 一个mod对应一个wxml,但是共享外部的css,暂时如此设计 所有日历模块的需求全部再此实现 */ module.exports = { q: 1, ddd: function(){}, onCalendarDayTap: function (e) { let data = e.detail; var date = new Date(data.year, data.month, data.day); console.log(date) this.setData({ calendarSelectedDate: date }); }, data: { isCalendarShow: '', calendarDisplayMonthNum: 2, calendarDisplayTime: new Date(), calendarSelectedDate: null } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
事实上一个mod就只是一个对象,只不过为了方便拆分,将对象分拆成一个个的mod
一个mod对应一个wxml,但是共享外部的css,暂时如此设计
所有日历模块的需求全部再此实现
*/
module.exports = {
  q: 1,
  ddd: function(){},
  onCalendarDayTap: function (e) {
    let data = e.detail;
    var date = new Date(data.year, data.month, data.day);
    console.log(date)
    this.setData({
      calendarSelectedDate: date
    });
  },
  data: {
    isCalendarShow: '',
    calendarDisplayMonthNum: 2,
    calendarDisplayTime: new Date(),
    calendarSelectedDate: null
  }
}

图片 4

至此,我们组件相关课题基本结束,接下来,我们开始我们的业务代码

1 赞 收藏 评论

图片 5

微信小程序的执行流程

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

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

"pages":[ "pages/index/index", "pages/list/list", "pages/logs/logs" ],

1
2
3
4
5
"pages":[
  "pages/index/index",
  "pages/list/list",
  "pages/logs/logs"
],

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

图片 6

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] })

1
2
3
4
5
6
7
__appServiceSDK__.invokeWebviewMethod({
    name: "appDataChange",
    args: o({}, e, {
        complete: n
    }),
    webviewIds: [t]
})

图片 7

图片 8

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

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

图片 9

引用一张其他地方的图

图片 10

小程序中的数据请求与缓存

小程序使用这个接口请求数据,这里需要设置域名白名单:

wx.request(OBJECT)

1
wx.request(OBJECT)

图片 11

可以看到数据请求已经回来了,但是我们一般来说一个接口不止会用于一个地方,每次重新写好像有些费事,加之我这里想将重复的请求缓存起来,所以我们这里封装一套数据访问层出来

之前在浏览器中,我们一般使用localstorage存储一些不太更改的数据,微信里面提供了接口处理这一切:

wx.setStorage(OBJECT)

1
wx.setStorage(OBJECT)

我们这里需要对其进行简单封装,便与后面更好的使用,一般来说有缓存就一定要有过期,所以我们动态给每个缓存对象增加一个过期时间:

class Store { constructor(opts) { if(typeof opts === 'string') this.key = opts; else Object.assign(this, opts); //如果没有传过期时间,则默认30分钟 if(!this.lifeTime) this.lifeTime = 1; //本地缓存用以存放所有localstorage键值与过期日期的映射 this._keyCache = 'SYSTEM_KEY_TIMEOUT_MAP'; } //获取过期时间,单位为毫秒 _getDeadline() { return this.lifeTime * 60 * 1000; } //获取一个数据缓存对象,存可以异步,获取我同步即可 get(sign){ let key = this.key; let now = new Date().getTime(); var data = wx.getStorageSync(key); if(!data) return null; data = JSON.parse(data); //数据过期 if (data.deadLine < now) { this.removeOverdueCache(); return null; } if(data.sign) { if(sign === data.sign) return data.data; else return null; } return null; } /*产出页面组件需要的参数 sign 为格式化后的请求参数,用于同一请求不同参数时候返回新数据,比如列表为北京的城市,后切换为上海,会判断tag不同而更新缓存数据,tag相当于签名 每一键值只会缓存一条信息 */ set(data, sign) { let timeout = new Date(); let time = timeout.setTime(timeout.getTime() + this._getDeadline()); this._saveData(data, time, sign); } _saveData(data, time, sign) { let key = this.key; let entity = { deadLine: time, data: data, sign: sign }; let scope = this; wx.setStorage({ key: key, data: JSON.stringify(entity), success: function () { //每次真实存入前,需要往系统中存储一个清单 scope._saveSysList(key, entity.deadLine); } }); } _saveSysList(key, timeout) { if (!key || !timeout || timeout < new Date().getTime()) return; let keyCache = this._keyCache; wx.getStorage({ key: keyCache, complete: function (data) { let oldData = {}; if(data.data) oldData = JSON.parse(data.data); oldData[key] = timeout; wx.setStorage({ key: keyCache, data: JSON.stringify(oldData) }); } }); } //删除过期缓存 removeOverdueCache() { let now = new Date().getTime(); let keyCache = this._keyCache; wx.getStorage({ key: keyCache, success: function (data) { if(data && data.data) data = JSON.parse(data.data); for(let k in data) { if(data[k] < now) { delete data[k]; wx.removeStorage({key: k, success: function(){}}); } } wx.setStorage({ key: keyCache, data: JSON.stringify(data) }); } }); } } module.exports = Store 缓存层核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
class Store {
  constructor(opts) {
    if(typeof opts === 'string') this.key = opts;
    else Object.assign(this, opts);
 
    //如果没有传过期时间,则默认30分钟
    if(!this.lifeTime) this.lifeTime = 1;
 
    //本地缓存用以存放所有localstorage键值与过期日期的映射
    this._keyCache = 'SYSTEM_KEY_TIMEOUT_MAP';
 
  }
  //获取过期时间,单位为毫秒
  _getDeadline() {
    return this.lifeTime * 60 * 1000;
  }
 
  //获取一个数据缓存对象,存可以异步,获取我同步即可
  get(sign){
    let key = this.key;
    let now = new Date().getTime();
    var data = wx.getStorageSync(key);
    if(!data) return null;
    data = JSON.parse(data);
    //数据过期
    if (data.deadLine < now) {
      this.removeOverdueCache();
      return null;
    }
 
    if(data.sign) {
      if(sign === data.sign) return data.data;
      else return null;
    }
    return null;
  }
 
  /*产出页面组件需要的参数
  sign 为格式化后的请求参数,用于同一请求不同参数时候返回新数据,比如列表为北京的城市,后切换为上海,会判断tag不同而更新缓存数据,tag相当于签名
  每一键值只会缓存一条信息
  */
  set(data, sign) {
    let timeout = new Date();
    let time = timeout.setTime(timeout.getTime() + this._getDeadline());
    this._saveData(data, time, sign);
  }
  _saveData(data, time, sign) {
    let key = this.key;
    let entity = {
      deadLine: time,
      data: data,
      sign: sign
    };
    let scope = this;
 
    wx.setStorage({
      key: key,
      data: JSON.stringify(entity),
      success: function () {
        //每次真实存入前,需要往系统中存储一个清单
        scope._saveSysList(key, entity.deadLine);
      }
    });
  }
  _saveSysList(key, timeout) {
    if (!key || !timeout || timeout < new Date().getTime()) return;
    let keyCache = this._keyCache;
    wx.getStorage({
      key: keyCache,
      complete: function (data) {
        let oldData = {};
        if(data.data) oldData = JSON.parse(data.data);
        oldData[key] = timeout;
        wx.setStorage({
          key: keyCache,
          data: JSON.stringify(oldData)
        });
      }
    });
  }
  //删除过期缓存
  removeOverdueCache() {
    let now = new Date().getTime();
    let keyCache = this._keyCache;
    wx.getStorage({
      key: keyCache,
      success: function (data) {
        if(data && data.data) data = JSON.parse(data.data);
        for(let k in data) {
          if(data[k] < now) {
            delete data[k];
            wx.removeStorage({key: k, success: function(){}});
          }
        }
        wx.setStorage({
          key: keyCache,
          data: JSON.stringify(data)
        });
      }
    });
  }
 
}
 
module.exports = Store
 
缓存层核心代码

这个类的使用也非常简单,这里举个例子:

sss = new global.Store({key: 'qqq', lifeTime: 1}) sss.set({a: 1}, 2) sss.get()//因为没有秘钥会是null sss.get(2)//sss.get(2)

1
2
3
4
sss = new global.Store({key: 'qqq', lifeTime: 1})
sss.set({a: 1}, 2)
sss.get()//因为没有秘钥会是null
sss.get(2)//sss.get(2)

这个时候我们开始写我们数据请求的类:

首先还是实现了一个抽象类和一个业务基类,然后开始在业务层请求数据:

class Model { constructor() { this.url = ''; this.param = {}; this.validates = []; } pushValidates(handler) { if (typeof handler === 'function') { this.validates.push(handler); } } setParam(key, val) { if (typeof key === 'object') { Object.assign(this.param, key); } else { this.param[key] = val; } } //<a href='http://www.jobbole.com/members/wx610506454'&gt;@override&lt;/a&gt; buildurl() { return this.url; } onDataSuccess() { } //执行数据请求逻辑 execute(onComplete) { let scope = this; let _success = function(data) { let _data = data; if (typeof data == 'string') _data = JSON.parse(data); // @description 开发者可以传入一组验证方法进行验证 for (let i = 0, len = scope.validates.length; i < len; i++) { if (!scope.validates[i](data)) { // @description 如果一个验证不通过就返回 if (typeof onError === 'function') { return onError.call(scope || this, _data, data); } else { return false; } } } // @description 对获取的数据做字段映射 let datamodel = typeof scope.dataformat === 'function' ? scope.dataformat(_data) : _data; if (scope.onDataSuccess) scope.onDataSuccess.call(scope, datamodel, data); if (typeof onComplete === 'function') { onComplete.call(scope, datamodel, data); } }; this._sendRequest(_success); } //删除过期缓存 _sendRequest(callback) { let url = this.buildurl(); wx.request({ url: this.buildurl(), data: this.param, success: function success(data) { callback && callback(data); } }); } } module.exports = Model 数据请求核心类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class Model {
  constructor() {
    this.url = '';
    this.param = {};
    this.validates = [];
  }
  pushValidates(handler) {
    if (typeof handler === 'function') {
      this.validates.push(handler);
    }
  }
  setParam(key, val) {
    if (typeof key === 'object') {
      Object.assign(this.param, key);
    } else {
      this.param[key] = val;
    }
  }
  //<a href='http://www.jobbole.com/members/wx610506454'>@override</a>
  buildurl() {
    return this.url;
  }
  onDataSuccess() {
  }
  //执行数据请求逻辑
  execute(onComplete) {
    let scope = this;
    let _success = function(data) {
      let _data = data;
      if (typeof data == 'string') _data = JSON.parse(data);
 
      // @description 开发者可以传入一组验证方法进行验证
      for (let i = 0, len = scope.validates.length; i < len; i++) {
        if (!scope.validates[i](data)) {
          // @description 如果一个验证不通过就返回
          if (typeof onError === 'function') {
            return onError.call(scope || this, _data, data);
          } else {
            return false;
          }
        }
      }
 
      // @description 对获取的数据做字段映射
      let datamodel = typeof scope.dataformat === 'function' ? scope.dataformat(_data) : _data;
 
      if (scope.onDataSuccess) scope.onDataSuccess.call(scope, datamodel, data);
      if (typeof onComplete === 'function') {
        onComplete.call(scope, datamodel, data);
      }
    };
    this._sendRequest(_success);
  }
 
  //删除过期缓存
  _sendRequest(callback) {
    let url = this.buildurl();
    wx.request({
      url: this.buildurl(),
      data: this.param,
      success: function success(data) {
        callback && callback(data);
      }
    });
  }
}
module.exports = Model
 
数据请求核心类

这里是业务基类的使用办法:

let Model = require('./abstract-model.js'); class DemoModel extends Model { constructor() { super(); let scope = this; this.domain = 'https://apikuai.baidu.com'; this.param = { head: { version: '1.0.1', ct: 'ios' } }; //如果需要缓存,可以在此设置缓存对象 this.cacheData = null; this.pushValidates(function(data) { return scope._baseDataValidate(data); }); } //首轮处理返回数据,检查错误码做统一验证处理 _baseDataValidate(data) { if (typeof data === 'string') data = JSON.parse(data); if (data.data) data = data.data; if (data.errno === 0) return true; return false; } dataformat(data) { if (typeof data === 'string') data = JSON.parse(data); if (data.data) data = data.data; if (data.data) data = data.data; return data; } buildurl() { return this.domain + this.url; } getSign() { let param = this.getParam() || {}; return JSON.stringify(param); } onDataSuccess(fdata, data) { if (this.cacheData && this.cacheData.set) this.cacheData.set(fdata, this.getSign()); } //如果有缓存直接读取缓存,没有才请求 execute(onComplete, ajaxOnly) { let data = null; if (!ajaxOnly && this.cacheData && this.cacheData.get) { data = this.cacheData.get(this.getSign()); if (data) { onComplete(data); return; } } super.execute(onComplete); } } class CityModel extends DemoModel { constructor() { super(); this.url = '/city/getstartcitys'; } } module.exports = { cityModel: new CityModel } 业务请求基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
let Model = require('./abstract-model.js');
 
class DemoModel extends Model {
  constructor() {
    super();
    let scope = this;
    this.domain = 'https://apikuai.baidu.com';
    this.param = {
      head: {
        version: '1.0.1',
        ct: 'ios'
      }
    };
 
    //如果需要缓存,可以在此设置缓存对象
    this.cacheData = null;
 
    this.pushValidates(function(data) {
      return scope._baseDataValidate(data);
    });
  }
 
  //首轮处理返回数据,检查错误码做统一验证处理
  _baseDataValidate(data) {
    if (typeof data === 'string') data = JSON.parse(data);
    if (data.data) data = data.data;
    if (data.errno === 0) return true;
    return false;
  }
 
  dataformat(data) {
    if (typeof data === 'string') data = JSON.parse(data);
    if (data.data) data = data.data;
    if (data.data) data = data.data;
    return data;
  }
 
  buildurl() {
    return this.domain + this.url;
  }
 
  getSign() {
    let param = this.getParam() || {};
    return JSON.stringify(param);
  }
  onDataSuccess(fdata, data) {
    if (this.cacheData && this.cacheData.set)
      this.cacheData.set(fdata, this.getSign());
  }
 
  //如果有缓存直接读取缓存,没有才请求
  execute(onComplete, ajaxOnly) {
    let data = null;
    if (!ajaxOnly && this.cacheData && this.cacheData.get) {
      data = this.cacheData.get(this.getSign());
      if (data) {
        onComplete(data);
        return;
      }
    }
    super.execute(onComplete);
  }
 
}
 
class CityModel extends DemoModel {
  constructor() {
    super();
    this.url = '/city/getstartcitys';
  }
}
 
module.exports = {
  cityModel: new CityModel
 
}
 
业务请求基类

接下来是实际调用代码:

let model = models.cityModel; model.setParam({ type: 1 }); model.execute(function(data) { console.log(data); debugger; });

1
2
3
4
5
6
7
8
let model = models.cityModel;
model.setParam({
  type: 1
});
model.execute(function(data) {
  console.log(data);
  debugger;
});

数据便请求结束了,有了这个类我们可以做非常多的工作,比如:

① 前端设置统一的错误码处理逻辑

② 前端打点,统计所有的接口响应状态

③ 每次请求相同参数做数据缓存

④ 这个对于错误处理很关键,一般来说前端出错很大可能都是后端数据接口字段有变化,而这种错误是比较难寻找的,如果我这里做一个统一的收口,每次数据返回记录所有的返回字段的标志上报呢,就以这个城市数据为例,我们可以这样做:

class CityModel extends DemoModel { constructor() { super(); this.url = '/city/getstartcitys'; } //每次数据访问成功,错误码为0时皆会执行这个回调 onDataSuccess(fdata, data) { super.onDataSuccess(fdata, data); //开始执行自我逻辑 let o = { _indate: new Date().getTime() }; for(let k in fdata) { o[k] = typeof fdata[k]; } //执行数据上报逻辑 console.log(JSON.stringify(o)); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CityModel extends DemoModel {
  constructor() {
    super();
    this.url = '/city/getstartcitys';
  }
  //每次数据访问成功,错误码为0时皆会执行这个回调
  onDataSuccess(fdata, data) {
    super.onDataSuccess(fdata, data);
    //开始执行自我逻辑
    let o = {
      _indate: new Date().getTime()
    };
    for(let k in fdata) {
      o[k] = typeof fdata[k];
    }
    //执行数据上报逻辑
    console.log(JSON.stringify(o));
  }
}

这里就会输出以下信息:

{"_indate":1533436847778,"cities":"object","hots":"object","total":"number","page":"string"}

1
{"_indate":1533436847778,"cities":"object","hots":"object","total":"number","page":"string"}

如果对数据要求非常严苛,对某些接口做到字段层面的验证,那么加一个Validates验证即可,这样对接口的控制会最大化,就算哪次出问题,也能很好从数据分析系统之中可以查看到问题所在,如果我现在想要一个更为具体的功能呢?我想要首次请求一个接口时便将其数据记录下来,第二次便不再请求呢,这个时候我们之前设计的数据持久层便派上了用处:

let Store = require('./abstract-store.js'); class CityStore extends Store { constructor() { super(); this.key = 'DEMO_CITYLIST'; //30分钟过期时间 this.lifeTime = 30; } } module.exports = { cityStore: new CityStore }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let Store = require('./abstract-store.js');
 
class CityStore extends Store {
  constructor() {
    super();
    this.key = 'DEMO_CITYLIST';
    //30分钟过期时间
    this.lifeTime = 30;
  }
}
 
module.exports = {
  cityStore: new CityStore
}

class CityModel extends DemoModel { constructor() { super(); this.url = '/city/getstartcitys'; this.cacheData = Stores.cityStore; } //每次数据访问成功,错误码为0时皆会执行这个回调 onDataSuccess(fdata, data) { super.onDataSuccess(fdata, data); //开始执行自我逻辑 let o = { _indate: new Date().getTime() }; for(let k in fdata) { o[k] = typeof fdata[k]; } //执行数据上报逻辑 console.log(JSON.stringify(o)); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CityModel extends DemoModel {
  constructor() {
    super();
    this.url = '/city/getstartcitys';
    this.cacheData = Stores.cityStore;
  }
  //每次数据访问成功,错误码为0时皆会执行这个回调
  onDataSuccess(fdata, data) {
    super.onDataSuccess(fdata, data);
    //开始执行自我逻辑
    let o = {
      _indate: new Date().getTime()
    };
    for(let k in fdata) {
      o[k] = typeof fdata[k];
    }
    //执行数据上报逻辑
    console.log(JSON.stringify(o));
  }
}

这个时候第二次请求时候便会直接读取缓存了

图片 12

前言

我们之前对小程序做了基本学习:

1. 微信小程序开发07-列表页面怎么做

2. 微信小程序开发06-一个业务页面的完成

3. 微信小程序开发05-日历组件的实现

4. 微信小程序开发04-打造自己的UI库

5. 微信小程序开发03-这是一个组件

6. 微信小程序开发02-小程序基本介绍

7. 微信小程序开发01-小程序的执行流程是怎么样的?

阅读本文之前,如果大家想对小程序有更深入的了解,或者一些细节的了解可以先阅读上述文章,本文后面点需要对着代码调试阅读

对应的github地址是:https://github.com/yexiaochai/wxdemo

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

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

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

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

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

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

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

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

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

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

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

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

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

图片 13

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

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

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

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

this.setData({'wxml': ` <my-component> <view>动态插入的节点</view> </my-component> `});

1
2
3
4
5
this.setData({'wxml': `
  <my-component>
  <view>动态插入的节点</view>
  </my-component>
`});

图片 14

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

图片 15

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

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 小程序公共样式表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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层。

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

图片 16

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

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组件:

图片 17

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

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

① 开发工具+调试工具

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

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

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

⑤ 监控统计逻辑

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

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

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

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

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

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

我对小程序的理解有限,因为没有源码只能靠惊艳猜测,如果文中有误,请各位多多提点

文章更多面对初中级选手,如果对各位有用,麻烦点赞哟

小程序中的Page的封装

小程序的Page类是这样写的:

Page({ data: { pageData: '页面数据' }, onLoad: function () { console.log('onLoad') }, })

1
2
3
4
5
6
7
8
Page({
  data: {
    pageData: '页面数据'
  },
  onLoad: function () {
    console.log('onLoad')
  },
})

传入的是一个对象,显然,我们为了更好的拆分页面逻辑,前面我们介绍了小程序是采用组件化开发的方式,这里的说法可以更进一步,小程序是采用标签化的方式开发,而标签对应的控制器js只会改变数据影响标签显示,所以某种程度小程序开发的特点是:先标签后js,我们构建一个页面,首先就应该思考这个页面有哪些标签,哪些标签是公共的标签,然后设计好标签再做实现。

比如我们一个页面中有比较复杂的日历相关模块,事实上这个日历模块也就是在操作日历标签的数据以及设置点击回调,那么我们就需要将页面分开

图片 18

比如这里的业务日历模块仅仅是index的一部分(其他页面也可能用得到),所以我们实现了一个页面共用的记录,便与我们更好的分拆页面:

class Page { constructor(opts) { //用于基础page存储各种默认ui属性 this.isLoadingShow = 'none'; this.isToastShow = 'none'; this.isMessageShow = 'none'; this.toastMessage = 'toast提示'; this.alertTitle = ''; this.alertMessage = 'alertMessage'; this.alertBtn = []; //通用方法列表配置,暂时约定用于点击 this.methodSet = [ 'onToastHide', 'showToast', 'hideToast', 'showLoading', 'hideLoading', 'onAlertBtnTap', 'showMessage', 'hideMessage' ]; //当前page对象 this.page = null; } //产出页面组件需要的参数 getPageData() { return { isMessageShow: this.isMessageShow, alertTitle: this.alertTitle, alertMessage: this.alertMessage, alertBtn: this.alertBtn, isLoadingShow: this.isLoadingShow, isToastShow: this.isToastShow, toastMessage: this.toastMessage } } //pageData为页面级别数据,mod为模块数据,要求一定不能重复 initPage(pageData, mod) { //debugger; let _pageData = {}; let key, value, k, v; //为页面动态添加操作组件的方法 Object.assign(_pageData, this.getPageFuncs(), pageData); //生成真实的页面数据 _pageData.data = {}; Object.assign(_pageData.data, this.getPageData(), pageData.data || {}); for( key in mod) { value = mod[key]; for(k in value) { v = value[k]; if(k === 'data') { Object.assign(_pageData.data, v); } else { _pageData[k] = v; } } } console.log(_pageData); return _pageData; } onAlertBtnTap(e) { let type = e.detail.target.dataset.type; if (type === 'default') { this.hideMessage(); } else if (type === 'ok') { if (this.alertOkCallback) this.alertOkCallback.call(this); } else if (type == 'cancel') { if (this.alertCancelCallback) this.alertCancelCallback.call(this); } } showMessage(msg) { let alertBtn = [{ type: 'default', name: '知道了' }]; let message = msg; this.alertOkCallback = null; this.alertCancelCallback = null; if (typeof msg === 'object') { message = msg.message; alertBtn = []; msg.cancel.type = 'cancel'; msg.ok.type = 'ok'; alertBtn.push(msg.cancel); alertBtn.push(msg.ok); this.alertOkCallback = msg.ok.callback; this.alertCancelCallback = msg.cancel.callback; } this.setData({ alertBtn: alertBtn, isMessageShow: '', alertMessage: message }); } hideMessage() { this.setData({ isMessageShow: 'none', }); } //当关闭toast时触发的事件 onToastHide(e) { this.hideToast(); } //设置页面可能使用的方法 getPageFuncs() { let funcs = {}; for (let i = 0, len = this.methodSet.length; i < len; i++) { funcs[this.methodSet[i]] = this[this.methodSet[i]]; } return funcs; } showToast(message, callback) { this.toastHideCallback = null; if (callback) this.toastHideCallback = callback; let scope = this; this.setData({ isToastShow: '', toastMessage: message }); // 3秒后关闭loading setTimeout(function() { scope.hideToast(); }, 3000); } hideToast() { this.setData({ isToastShow: 'none' }); if (this.toastHideCallback) this.toastHideCallback.call(this); } //需要传入page实例 showLoading() { this.setData({ isLoadingShow: '' }); } //关闭loading hideLoading() { this.setData({ isLoadingShow: 'none' }); } } //直接返回一个UI工具了类的实例 module.exports = new Page 所有page页面基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class Page {
  constructor(opts) {
    //用于基础page存储各种默认ui属性
    this.isLoadingShow = 'none';
    this.isToastShow = 'none';
    this.isMessageShow = 'none';
 
    this.toastMessage = 'toast提示';
 
    this.alertTitle = '';
    this.alertMessage = 'alertMessage';
    this.alertBtn = [];
 
    //通用方法列表配置,暂时约定用于点击
    this.methodSet = [
      'onToastHide',
      'showToast',
      'hideToast',
      'showLoading',
      'hideLoading',
      'onAlertBtnTap',
      'showMessage',
      'hideMessage'
    ];
 
    //当前page对象
    this.page = null;
  }
  //产出页面组件需要的参数
  getPageData() {
    return {
      isMessageShow: this.isMessageShow,
      alertTitle: this.alertTitle,
      alertMessage: this.alertMessage,
      alertBtn: this.alertBtn,
 
      isLoadingShow: this.isLoadingShow,
      isToastShow: this.isToastShow,
      toastMessage: this.toastMessage
 
    }
  }
 
  //pageData为页面级别数据,mod为模块数据,要求一定不能重复
  initPage(pageData, mod) {
    //debugger;
    let _pageData = {};
    let key, value, k, v;
 
    //为页面动态添加操作组件的方法
    Object.assign(_pageData, this.getPageFuncs(), pageData);
 
    //生成真实的页面数据
    _pageData.data = {};
    Object.assign(_pageData.data, this.getPageData(), pageData.data || {});
 
    for( key in mod) {
      value = mod[key];
      for(k in value) {
        v = value[k];
        if(k === 'data') {
          Object.assign(_pageData.data, v);
        } else {
          _pageData[k] = v;
        }
      }
    }
 
    console.log(_pageData);
    return _pageData;
  }
  onAlertBtnTap(e) {
    let type = e.detail.target.dataset.type;
    if (type === 'default') {
      this.hideMessage();
    } else if (type === 'ok') {
      if (this.alertOkCallback) this.alertOkCallback.call(this);
    } else if (type == 'cancel') {
      if (this.alertCancelCallback) this.alertCancelCallback.call(this);
    }
  }
  showMessage(msg) {
    let alertBtn = [{
      type: 'default',
      name: '知道了'
    }];
    let message = msg;
    this.alertOkCallback = null;
    this.alertCancelCallback = null;
 
    if (typeof msg === 'object') {
      message = msg.message;
      alertBtn = [];
      msg.cancel.type = 'cancel';
      msg.ok.type = 'ok';
 
      alertBtn.push(msg.cancel);
      alertBtn.push(msg.ok);
      this.alertOkCallback = msg.ok.callback;
      this.alertCancelCallback = msg.cancel.callback;
    }
 
    this.setData({
      alertBtn: alertBtn,
      isMessageShow: '',
      alertMessage: message
    });
  }
  hideMessage() {
    this.setData({
      isMessageShow: 'none',
    });
  }
  //当关闭toast时触发的事件
  onToastHide(e) {
    this.hideToast();
  }
  //设置页面可能使用的方法
  getPageFuncs() {
    let funcs = {};
    for (let i = 0, len = this.methodSet.length; i < len; i++) {
      funcs[this.methodSet[i]] = this[this.methodSet[i]];
    }
    return funcs;
  }
 
  showToast(message, callback) {
    this.toastHideCallback = null;
    if (callback) this.toastHideCallback = callback;
    let scope = this;
    this.setData({
      isToastShow: '',
      toastMessage: message
    });
 
    // 3秒后关闭loading
    setTimeout(function() {
      scope.hideToast();
    }, 3000);
  }
  hideToast() {
    this.setData({
      isToastShow: 'none'
    });
    if (this.toastHideCallback) this.toastHideCallback.call(this);
  }
  //需要传入page实例
  showLoading() {
    this.setData({
      isLoadingShow: ''
    });
  }
  //关闭loading
  hideLoading() {
    this.setData({
      isLoadingShow: 'none'
    });
  }
}
//直接返回一个UI工具了类的实例
module.exports = new Page
 
所有page页面基类

其中页面会用到的一块核心就是:

//pageData为页面级别数据,mod为模块数据,要求一定不能重复 initPage(pageData, mod) { //debugger; let _pageData = {}; let key, value, k, v; //为页面动态添加操作组件的方法 Object.assign(_pageData, this.getPageFuncs(), pageData); //生成真实的页面数据 _pageData.data = {}; Object.assign(_pageData.data, this.getPageData(), pageData.data || {}); for( key in mod) { value = mod[key]; for(k in value) { v = value[k]; if(k === 'data') { Object.assign(_pageData.data, v); } else { _pageData[k] = v; } } } console.log(_pageData); return _pageData; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//pageData为页面级别数据,mod为模块数据,要求一定不能重复
initPage(pageData, mod) {
  //debugger;
  let _pageData = {};
  let key, value, k, v;
 
  //为页面动态添加操作组件的方法
  Object.assign(_pageData, this.getPageFuncs(), pageData);
 
  //生成真实的页面数据
  _pageData.data = {};
  Object.assign(_pageData.data, this.getPageData(), pageData.data || {});
 
  for( key in mod) {
    value = mod[key];
    for(k in value) {
      v = value[k];
      if(k === 'data') {
        Object.assign(_pageData.data, v);
      } else {
        _pageData[k] = v;
      }
    }
  }
 
  console.log(_pageData);
  return _pageData;
}

调用方式是:

Page(_page.initPage({ data: { sss: 'sss' }, // methods: uiUtil.getPageMethods(), methods: { }, goList: function () { if(!this.data.cityStartId) { this.showToast('请选择出发城市'); return; } if(!this.data.cityArriveId) { this.showToast('请选择到达城市'); return; } wx.navigateTo({ }) } }, { modCalendar: modCalendar, modCity: modCity }))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Page(_page.initPage({
  data: {
    sss: 'sss'
  },
  // methods: uiUtil.getPageMethods(),
  methods: {
  },
  goList: function () {
    if(!this.data.cityStartId) {
      this.showToast('请选择出发城市');
      return;
    }
    if(!this.data.cityArriveId) {
      this.showToast('请选择到达城市');
      return;
    }
 
    wx.navigateTo({
    })
 
  }
}, {
  modCalendar: modCalendar,
  modCity: modCity
}))

可以看到,其他组件,如这里的日历模块只是一个对象而已:

module.exports = { showCalendar: function () { this.setData({ isCalendarShow: '' }); }, hideCalendar: function () { this.setData({ isCalendarShow: 'none' }); }, preMonth: function () { this.setData({ calendarDisplayTime: util.dateUtil.preMonth(this.data.calendarDisplayTime).toString() }); }, nextMonth: function () { this.setData({ calendarDisplayTime: util.dateUtil.nextMonth(this.data.calendarDisplayTime).toString() }); }, onCalendarDayTap: function (e) { let data = e.detail; var date = new Date(data.year, data.month, data.day); console.log(date) //留下一个钩子函数 if(this.calendarHook) this.calendarHook(date); this.setData({ isCalendarShow: 'none', calendarSelectedDate: date.toString(), calendarSelectedDateStr: util.dateUtil.format(date, 'Y年M月D日') }); }, onContainerHide: function () { this.hideCalendar(); }, data: { isCalendarShow: 'none', calendarDisplayMonthNum: 1, calendarDisplayTime: selectedDate, calendarSelectedDate: selectedDate, calendarSelectedDateStr: util.dateUtil.format(new Date(selectedDate), 'Y年M月D日') } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
module.exports = {
  showCalendar: function () {
    this.setData({
      isCalendarShow: ''
    });
  },
  hideCalendar: function () {
    this.setData({
      isCalendarShow: 'none'
    });
  },
  preMonth: function () {
 
    this.setData({
      calendarDisplayTime: util.dateUtil.preMonth(this.data.calendarDisplayTime).toString()
    });
  },
  nextMonth: function () {
    this.setData({
      calendarDisplayTime: util.dateUtil.nextMonth(this.data.calendarDisplayTime).toString()
    });
  },
  onCalendarDayTap: function (e) {
    let data = e.detail;
    var date = new Date(data.year, data.month, data.day);
    console.log(date)
 
    //留下一个钩子函数
    if(this.calendarHook) this.calendarHook(date);
    this.setData({
      isCalendarShow: 'none',
      calendarSelectedDate: date.toString(),
      calendarSelectedDateStr: util.dateUtil.format(date, 'Y年M月D日')
    });
  },
  onContainerHide: function () {
    this.hideCalendar();
  },
 
  data: {
    isCalendarShow: 'none',
    calendarDisplayMonthNum: 1,
    calendarDisplayTime: selectedDate,
    calendarSelectedDate: selectedDate,
    calendarSelectedDateStr: util.dateUtil.format(new Date(selectedDate), 'Y年M月D日')
  }
}

但是在代码层面却帮我们做到了更好的封装,这个基类里面还包括我们自定义的常用组件,loading、toast等等:

图片 19

page是最值得封装的部分,这里是基本page的封装,事实上,列表页是常用的一种业务页面,虽然各种列表页的筛选条件不一样,但是主体功能无非都是:

① 列表渲染

② 滚动加载

③ 条件筛选、重新渲染

所以说我们其实可以将其做成一个页面基类,跟abstract-page一个意思,这里留待我们下次来处理吧

本文由俄罗斯贵宾会发布于Web前端,转载请注明出处:【微信小程序项目实践总结】30分钟从陌生到熟悉

您可能还会对下面的文章感兴趣: