1

项目背景

clipboard.png

2017年1月9日,我们赢来了微信小程序,而在今年,小程序已经在完全融入到我们的生活中,可以说无处不在,赢来了一场真正的大爆发。微信之父张小龙在形容小程序时,是这样说的

小程序是一种不需要下载安装即可使用的应用,它实现了应用“触手可及”的梦想,用户扫一扫或者搜一下即可打开应用。也体现了“用完即走”的理念,用户不用关心是否安装太多应用的问题。应用将无处不在,随时可用,但又无需安装卸载。

而网友们在形容小程序的时候,是这么说的

App就像原配,一年用不了几次;
服务号就像小三,每个月固定用几次;
小程序就像炮友,用完即走;

那么在你点击启动一个小程序的时候,这个“p友”是如何做到无需安装卸载,触手可及,用完即走的呢,这其中在交互方面又发生了哪些不可告人的事情,让她如此特殊,下面以我最近接触到的一款小程序为例,简单总结一下小程序底层框架和一些api接口方面的设计思路。

小程序框架浅析

大家都说小程序体验好,即开即用,比普通Webview的H5页面体验好很多,这个问题的我认为需要从几个方面考虑,首先呢,抛开业务层面的设计和优化,就是小程序底层框架的设计和实现方面的特点。

当我们新建或打开一个小程序项目,以唱吧比赛小程序为例,即可看到如下图的项目结构。

图片描述

官方文档规定,入口文件app.js, 全局样式文件app.wxss,全局配置文件app.json,
每个页面中再分视图wxml,wxss和逻辑js、文件配置json等,从这里我们可以看出,整个小程序的上层框架,也就是大体分为视图层和逻辑层两个部分。
(官方文档https://developers.weixin.qq....

clipboard.png

小程序采用的MINA框架,View层主要用来渲染页面结构,App Service层用来逻辑处理、数据请求、接口调用,它们在两个线程里运行,整个小程序是只有一个App Service的,并且整个小程序的生命周期内它是常驻内存的。View层主要使用WebView渲染,而App Service逻辑层是使用JSCore运行。

通信方面,View和AppService是双线程通信的,主要通过系统层的JSBridage进行通信,AppService把数据变化通知到View,表现方法也就是setData方法,触发View页面更新,View把触发的事件通知到AppService进行业务处理。

这里要说的是,小程序是没有DOM结构的,那么视图层的渲染是如何做到的呢,就是运行环境中会把pages中的WXML的节点树结构,转化为JS的对象,进行渲染,这也是小程序体验优于普通分享页面的一大原因,省去了很多关于浏览器DOM的操作,由JS运行环境之间进行渲染解析。

唱吧小程序底层搭建

那么话说回来,这次在搭建唱吧小程序底层的时候,我们其实做了哪些事情呢。首先,我们并没有进行纯Native层的搭建和改造,而是对上述提到的API层的一次的封装,尤其是在关于网络请求的改造和小程序启动的登录流程方面,我们前端团队尝试去做一些分层和优化。

  • 网络请求方面

首先网络请求优化方面,微信提供的请求接口基本长这样:

wx.request({
  url: 'test.php', //仅为示例,并非真实的接口地址
  data: {
     x: '' ,
     y: ''
  },
  header: {
      'content-type': 'application/json' // 默认值
  },
  success: function(res) {
    console.log(res.data)
  }
})

是不是感觉和之前的某种请求模式很像,没错,就是古老的$.ajax,这时候我们又想起了ajax的回调地狱,如果页面的请求很多,请求的顺序还有限定,瞬间又是各种嵌套,可以说是要从请求到懵逼了。

所以为了解决回调地狱和同时优化请求代码逻辑,我们在封装wx.request的同时,我们在小程序开发中,引入了async/await语法糖,用到了来自facebook的regenerator模块(详情请戳:https://github.com/facebook/r...),async、await函数经babel编译后,再用regenerator-runtime模块用于提供功能实现,这一方面也得力于小程序支持ES6语法的编译。

实现过程中,单独用一个公共方法封装,返回wx.request的promise

//wechat.js

const request = (url,options) => {
  return new Promise((resolve, reject) => {
    wx.request({
      url: url,
      method: options.method,
      data: Object.assign({}, options.data),
      header: options.header,
      success: resolve,
      fail: reject
    })
  })
}

之后在我们的上层公共库中,编写与请求相关的处理逻辑。

// changba.js

const regeneratorRuntime = require('./regenerator-runtime.js')
const wechat = require('./wechat')
const URI = 'xxx'

const requestAPI = async (url,opt) => {
  const app = getApp()
  let options = Object.assign({data: {}},opt)
    if (/^\/api\/(.+)$/.test(url)) {
        url = URI + url;
    }
    if (!options.method) {
        options.method = 'POST';
    }
    let header = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    options.header = options.header || header ;
    //除了login方法  其余接口都要加入sessionInfo也就是后端加密过的session_key
    if (!url.includes('/checkCode')) {
      options.data['sessionInfo'] = app.globalData.sessionkey;
    }
    let isTimeout = false;
    try {
      const ree = await wechat.checkSession();
    } catch (error) {
      isTimeout = true;
    };
    try {
      if (isTimeout) {
        let aaa = await login(app);
      }
      wx.showLoading({
        title: '加载中'
      });
        const res = await wechat.request(url,options)
        if (res && res.statusCode) {
            if (res.statusCode != 200) {
                if (wx.hideLoading) {
                    wx.hideLoading()
                }
                wx.showModal({
                    content: wechat.errMsg(res.statusCode).message || '请求失败,请重新尝试',
                    title: '提示',
                    showCancel: false
                })
            } else {
                if (res.data && res.data.code === 1) {
                  if (wx.hideLoading) {
                     wx.hideLoading()
                  }
                  return res.data
                } else {
                    // xxx 其他情况业务逻辑处理
                }
            }
        }
    } catch (error) {
        console.log('请求异常信息:' + error)
        if (wx.hideLoading) {
          wx.hideLoading()
        }
        wx.showModal({
            content: '请求信息异常',
            title: '',
            showCancel: false
        })
    }
}

上述封装过程中,所以除了考虑到请求超时、检查用户身份等操作,还可以加入session过期等相关其他的业务处理逻辑,这也是自己搭建请求的好处,针对自己的业务需求,进行匹配和改造。

然而在经历这样两层封装之后,在写业务逻辑代码的过程中,就可以一目了然的发送请求了,达到逻辑清晰且书写自如,如果习惯了fetch以及axios的朋友应该都会比较喜欢这种方式。

   async getdata() {
     let self = this;
     let cb_getdata = await app.changba.requestAPI('/api/xxx', { data: { id: self.data.id } });
     if (cb_getdata && cb_getdata.code === 1) {
           // xxx
     }
   }
  • 登录流程方面

下面说下,启动小程序后的登录流程方面,在这一方面,小程序与其他不同的是,没有固定的登录启动页面,而是完全后台交互,当然根据产品定位和需求,也可以自己做一套登录系统,但是微信官方给出的文档基本长这样:

图片描述

没错,看着很头大,然而最头大的不是你按部就班的实现了这个流程,而是实现了之后,你还会遇到很多意想不到的问题。

基本的流程不用多说,按部就班即可,就是使用wx.login()可以获得开发者服务器向微信接口服务器请求获得session_key等数据时所需要的参数code,开发者服务器以code+appid+appsecret换取用户唯一标识openid和会话密钥session_key。但每一次调用wx.login()都会更新微信接口服务器上的session_key。

同样,改造微信api先,

// wechat 登录封装
const login = () => {
    return new Promise((resolve, reject) => {
      wx.login({ success: resolve, fail: reject })
    })
}

而后,在做自己的登录封装时,可以先去请求微信的code,然后用在自己的请求中,获取并存储自己的登录态。

let we_login = await wechat.login() // 微信登录
let cb_login = await requestAPI('xxxx/checkCode', { data: {code: we_login.code}})
if (cb_login && cb_login.code === 1) {
      // xxxx 业务逻辑
  } catch (error) {
    wx.showModal({
      title: '登录提示',
      content: '登录失败',
      showCancel: false
    })
  }

而在完成上面整套业务逻辑过程中,可能会遇到一些意想不到的坑,这里面我印象比较深刻的有两个,第一个是关于授权的问题,另一个就是关于小程序生命周期与页面生命周期初始化过程中异步请求回调顺序的问题。

  1. 授权问题

先说第一个问题,关于授权框唤起的问题,也就是你常见的下面这个框。

clipboard.png

只有用户授权后,才可以进一步获取用户的信息,这个框在最初是可以通过wx.getUserinfo()方法直接唤起,而在5月份以后,微信去掉了这个方法的功能,只能通过固定的button open-type去引导用户授权。

clipboard.png

所以在底层逻辑的设计过程中,就要抛弃之前login之后获取用户授权信息的设计思路,而是进行拆分,将login和授权的逻辑分开。

在必须要授权操作的地方例如我们小程序中需要“参赛”或者“关注”的地方,进行单独授权的处理,通过使用wx.getSetting获取用户的授权情况
1) 如果用户已经授权,直接调用wx.getUserInfo获取用户最新的信息
2) 用户未授权,在界面中显示一个按钮提示用户登入,当用户点击并授权后就获取到用户的最新信息。

  1. onLaunch和onLoad异步回调顺序问题

这个问题简单来说,就是小程序启动有自己的生命周期onLaunch->onShow->onHide,而每个page的实例化也有自己的生命周期,onLoad->onShow->onReady->onHide->onUnload

然而在开发过程中,会遇到在App启动onLaunch的时候,发起登录请求,并注册到我们自己的服务器上以便使用,然而,这个过程中,

app on launch -> request -> success -> page onload

是无法判断success和page onload哪个先来的,会导致页面初始化数据失败的情况。

解决方案一
就是在request success中处理,使用getCurrentPages方法获取是否页面先于success生成,如果生成我们就强制让页面再次渲染。

这显然是一种hack的方式, 在实际使用过程当中,如果登录逻辑比较复杂,这个方法不是十分便利,page onload在一些特殊情况也会被调用,这显然不是我们想看到的

if (getCurrentPages().length != 0) { 
    getCurrentPages()[getCurrentPages().length - 1].onLoad() 
}

解决方案二

目前我在开发中使用的是这种方案,

在login的逻辑里,增加一个回调函数cbLoginCallBack。

Page页面判断一下当前app.globalData.sessionKey是否存在,如果没有(第一次)则定义定义一个app方法(回调函数)

// Login Request 
    if (app.cbLoginCallBack) { 
        typeof app.cbLoginCallBack == 'function' && app.cbLoginCallBack(cb_login.data) 
    } 
// 逻辑页面 
if (app.globalData.sessionkey) { 
    // init data 
    } else { 
    app.cbLoginCallBack = res => { 
        if (res) { 
        // init data 
        } 
} }

App页面在请求success后判断时候有Page页面定义的回调方法,如果有就执行该方法。因为回调函数是在Page里面定义的所以方法作用域this是指向Page页面。

总结

杂七杂八写了很多,基本都是近期开发和学习过程中自己对小程序体验和交互方面的一点总结,在这里跟大家分享,以便遇到同样的问题,可以一起探讨,找出最优的解决方案,今年小程序的热度还将持续一阵,各大小公司持续发力,未来在这个领域还会有哪些事情发生,让我们拭目以待。

原文地址: https://mp.weixin.qq.com/s/VL...


朝阳银枪小霸王
36 声望0 粉丝

你听见笑声,肯定不是我