微信小程序开发深刻解读

下面结合开发文档以及我的开发经验对微信小程序关键部分进行解读(不是入门教程,具体入门读者能够看官网),但愿看完的读者对微信小程序有大概的认识或者有所启发。javascript

本文同步于我的博客 www.imhjm.com/article/597…css

官方开发文档 mp.weixin.qq.com/debug/wxado…
官方开发者社区 developers.weixin.qq.com/html

运行环境

微信小程序运行在三端:iOS、Android 和 用于调试的开发者工具。
三端的脚本执行环境聚以及用于渲染非原生组件的环境是各不相同的:vue

  • 在 iOS 上,小程序的 javascript 代码是运行在 JavaScriptCore 中,是由 WKWebView 来渲染的,环境有 iOS八、iOS九、iOS10
  • 在 Android 上,小程序的 javascript 代码是经过 X5 JSCore来解析,是由 X5 基于 Mobile Chrome 53 内核来渲染的
  • 在 开发工具上, 小程序的 javascript 代码是运行在 nwjs 中,是由 Chrome Webview 来渲染的

引用:mp.weixin.qq.com/debug/wxado…java

正因为脚本执行环境的不一样,因此真机与开发者工具备些表现仍是差别挺大的,特别表如今原生组件方面(后面会讲到部分原生组件注意点),iOS以及Android都须要多加测试才能保证程序没有问题。
同时由于是在JsCore中执行,JsCore没有窗口对象,因此没有window、document等等(因此不少外部生态插件/库没法直接使用,须要稍做修改)node

生命周期

小程序全局有App、Page内置的全局变量,用于注册小程序以及注册页面webpack

App实例生命周期

  • onLaunch
    监听小程序初始化,当小程序初始化完成时,会触发 onLaunch(全局只触发一次)
  • onShow
    监听小程序显示 当小程序启动,或从后台进入前台显示,会触发 onShow
  • onHide
    监听小程序隐藏 当小程序从前台进入后台,会触发 onHide

前台、后台定义: 当用户点击左上角关闭,或者按了设备 Home 键离开微信,小程序并无直接销毁,而是进入了后台;当再次进入微信或再次打开小程序,又会从后台进入前台。须要注意的是:只有当小程序进入后台必定时间,或者系统资源占用太高,才会被真正的销毁。git

Page实例生命周期

具体读者能够看文档中的「Page实例生命周期」,左边是视图线程,右边是逻辑层线程
能够看到View Thread分四个阶段es6

  • Start
  • Inited
  • Ready
  • End

AppSevice Thread也分四个阶段github

  • Start
  • Created
  • Active (Alive)
  • End

咱们从图中能够简单地分析出「Page实例生命周期」

  • View Thread以及AppSevice Thread进入Start
  • AppSevice Thread调用Page方法传入配置Created后,调用onLoad(监听页面加载)以及onShow(监听页面显示)方法,AppSevice Thread等待View Thread的通知
  • View Thread进入初始化阶段(Inited)后,通知(Notify)AppSevice Thread已经初始化好了,而后AppSevice Thread传入App实例中的初始化数据,AppSevice Thread等待View Thread的下一次通知
  • View Thread收到初始化的数据以后,第一次渲染页面(First Render),进入Ready阶段,渲染完毕通知AppSevice Thread,AppSevice Thread调用onReady方法,进入Active阶段
  • 在AppSevice Thread Active阶段,会调用一些setData的方法,就是传递数据给View Thread中的渲染器(Rerender),进行视图更新
  • 当小程序切到后台或者当前Page跳转(具体看后面路由部分或文档)调用onHide方法,进入Alive阶段,再切回来前台调用onShow进入Active阶段
  • 最后Page销毁,调用onUnload方法,页面卸载

从上面声明周期的分析,咱们能够获得如下几个结论:

  • onLoad只调用一次,onShow页面显示屡次调用
  • First Render是Page传入的data数据进行Render,在onLoad阶段进行setData其实也是在进入Active阶段发送视图更新的(也就是在OnReady后),因此,假如在onLoad阶段setData跟Intial Data不同的数据,是能够看到页面闪烁了一下的

Page实例生命周期
Page实例生命周期

数据驱动(响应的数据绑定)

从生命周期也能够看出微信小程序跟vue等框架相似,是数据驱动视图更新,在逻辑层修改数据,视图层响应数据更新

双括号绑定数据

<view> {{ message }} </view>

Page({
  data: {
    message: 'Hello MINA!'
  }
})复制代码

如上使用双括号,便实现数据与视图绑定

数据单向流动

微信小程序一样是数据单向流动,而不是双向绑定,好比你传入它基础组件的某些数据,并不能同步到你的data中,而是调用某些监听函数去获取(好比scroll-view中scroll-top,你能经过视图传入data更新滚动位置,可是你在滚动的时候,并不能双向绑定去获取scroll-top,而是须要监听bindscroll去获取)

条件渲染&&列表渲染

条件渲染以及列表渲染做为数据驱动视图的重要部分,值得一提

1.条件渲染的wx:if以及hidden

  • wx:if会产生局部渲染,销毁条件块(或者从新渲染)
  • hidden就是直接控制display block/none了

因此官网给出的结论是

通常来讲,wx:if 有更高的切换消耗而 hidden 有更高的初始渲染消耗。所以,若是须要频繁切换的情景下,用 hidden 更好,若是在运行时条件不大可能改变则 wx:if 较好。

2.列表渲染

<view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName" wx:key="*this">
  {{idx}}: {{itemName.message}}
</view>复制代码

这里其余for,index,item这些循环渲染基本的东西就不具体说了,谈谈这个wx:key

假如咱们更新array数组,预期来讲视图从新渲染,可是咱们假如只是在array中push更多的元素,咱们的想法应该是从新排序,不去重复建立视图原来已经有的元素,这里为了标识item,咱们就能够用wx:key,有助于提高渲染的效率,而且可以保持状态(如<input/> 中的输入内容,<switch/> 的选中状态)

路由管理

小程序的路由管理部分均由框架处理,开发者只需调用API便可,可是仍是有一些地方须要注意

文档: mp.weixin.qq.com/debug/wxado…

小程序的路由管理是用一个页面栈来维护,经过出栈以及入栈加载不一样页面,能够用getCurrentPages()获取一个栈数组

下面这个表格根据官网两个表格整合而成,注意区分各类触发时机以及页面栈的表现

路由方式 触发时机 页面栈表现 路由前页面 路由后页面调用方法
初始化 小程序打开的第一个页面 新页面入栈 onLoad, onSHow
打开新页面 调用 API wx.navigateTo 或使用组件 <navigator open-type="navigateTo"/> 新页面入栈 onHide onLoad, onShow
页面重定向 调用 API wx.redirectTo 或使用组件 <navigator open-type="redirectTo"/> 当前页面出栈,新页面入栈 onUnload onLoad, onShow
页面返回 调用 API wx.navigateBack 或使用组件<navigator open-type="navigateBack">或用户按左上角返回按钮 页面不断出栈,直到目标返回页,新页面入栈 onUnload onShow
Tab 切换 调用 API wx.switchTab 或使用组件 <navigator open-type="switchTab"/> 或用户切换 Tab 页面所有出栈,只留下新的 Tab 页面 具体看官网
重启动 调用 API wx.reLaunch 或使用组件 <navigator open-type="reLaunch"/> 页面所有出栈,只留下新的页面 onUnload onLoad, onShow

注意区分页面重定向(redirectTo)以及打开新页面(navigateTo),由于小程序限制了也页面栈最多只有5个元素,因此当你深度达到5个,再调用navigateTo想让新页面再入栈就会报错,因此官方建议是

避免多层级的交互方式,或者使用wx.redirectTo

模块化&&组件/模板

js模块化

小程序默认使用CommonJs规范
使用module.exports(exports)以及require来实现模块化
固然也能够ES6转ES5使用import/export,小程序开发工具带有babel es6转es5设置,勾选便可
猜想最后也是使用webpack打包文件

这里简单说下模块化须要注意的吧,首先module.exports = exports, module就是一个对象{},exports就是对它的一个key的引用,因此须要区分下module.exports = xxx, 以及export.xxx = yyy;

还得注意区分ES6和commonjs的差别,前者模块静态编译,后者运行加载,因此表现上有不少不一样,ES6能够在编译时处理依赖关系,而且输出的值为引用,对循环引用支持比较好,不一样的是commonjs模块是运行加载,输出值为拷贝

这部分就很少说了,具体能够看 es6.ruanyifeng.com/#docs/modul…

不过这里的require加载机制不一样于nodejs,加了一些限制,好比不能用绝对路径,也不支持node_modules,因此若是要使用node_modules的内容须要手动拷贝到目录里

WXML模板

wxml经过template能够实现复用
经过is属性动态决定渲染哪一个模版

<template name="odd">
  <view> odd </view>
</template>
<template name="even">
  <view> even </view>
</template>

<block wx:for="{{[1, 2, 3, 4, 5]}}">
    <template is="{{item % 2 == 0 ? 'even' : 'odd'}}"/>
</block>复制代码

而且有本身的做用域,只能使用传入的data(这点跟组件很类似)

mp.weixin.qq.com/debug/wxado…

WXSS @import

使用@import语句能够导入外联样式表,@import后跟须要导入的外联样式表的相对路径

/** common.wxss **/
.small-p {
  padding:5px;
}
/** app.wxss **/
@import "common.wxss";
.middle-p {
  padding:15px;
}复制代码

上述三个模块化的东西能够构成相似组件同样的部分,可是引入方面太不方便,wxml/wxss/js都得引入一份,而且js耦合程度太高,需在Page中引入“组件”太多的方法去调用,也没有本身的数据做用域,data都是在Page里,弊端仍是比较明显,像组件但不是组件。

组件

小程序本身提供了一系列的基础组件,这些就是真正的组件了,可是小程序没有提供自定义组件的方式
这部分也很少说了,内容也挺多的,不少细节,具体看官方文档,后面也会讲到某些我的实战时遇到的一些经验

文档:mp.weixin.qq.com/debug/wxado…

错误监控

错误监控对于应用的稳定性相当重要,这部分也特意拿出来说下

一般应用可使用ravenjs使用window.onerror捕获错误,处理error.stack,而后接入sentry上报,固然在微信小程序也能够,可是须要作一些配置改动

在微信小程序该怎么作呢?
没有了window.onerror, 微信小程序能够在App传入onerror进行捕获错误,使用小程序的wx.request上报,而且能够附加小程序的systemInfo一块儿上报,得到更多错误信息,更好地修复bug

小程序上一段时间加了一个运维中心,能够在公众平台中设置

埋点/数据上报

数据上报也是一个好应用中不可或缺的部分,去了解用户如何使用应用,了解怎么去更好地优化以及增长功能。

小程序自带数据上报接口

官方教程: mp.weixin.qq.com/debug/wxado…

有两种上报方式,一种是使用API接口wx.reportAnalytics在代码中上报,一种是在微信公众平台直接配置事件,根据id/class和page来指定事件(好比点击事件等等)

  • 前者优势是数据粒度能够很细,缺点就是须要写在代码里,上线成本比较高
  • 后者优势是直接在微信公众平台发布事件便可,上线/删除事件成本较低,缺点是可定制数据的能力比较弱,只能使用当前Page里的data,而且须要有id/class

零散经验之谈&&开发相关问题

上面微信小程序基本的也讲了挺多了,下面开始讲一些零散的开发细节和遇到的问题以及解决方案

如何设计一个微信小程序的开发结构

若是不考虑引入像wepy这种组件化框架或者引入状态管理方案,以为采用如下开发结构也是一个良好的选择

|---model---------------跟业务逻辑相关的,跟数据交互的model
     |---xxx.js
 |---utils----------------可从业务逻辑中抽离处可复用工具
     |---xxx.js
 |---pages---------------微信小程序的各个page
         |---xxx
                 |---xxx.wxml
                 |---xxx.wxss
                 |---xxx.json
                 |---xxx.js
 |---components-----------可从page中抽离出的组件,有利于复用以及维护
         |---xxx
                 |---xxx.wxml
                 |---xxx.wxss
                 |---xxx.js
 |---static----------------静态资源文件
 |---app.js
 |---app.json
 |---app.wxss

 其余eslint、git相关等等就不放上去了复制代码

这个总体目录并不复杂,可是这样分层,每部分的职责就能够很清晰了,有利于代码维护以及复用
(稍微区分下model和utils,model便是一些跟后台交互数据的操做,能够依赖utils,utils是从业务逻辑中抽离出的可复用的工具库,但不能够依赖于model)

如何更好地调用接口

wx.request

因为微信小程序wx.request有并发10个的限制,而且以前若是超出并发数就会报错从而中断了超出的请求,当时总体使用本身封装的request,支持超出并发数放入队列中,当有新的请求complete再检查队列,不空则取出原先的请求retry,而且加上了超时处理,代码大概以下

let RequestQ = {
  retry: [],
  emitRequest (obj) {
    if (!obj || typeof obj !== 'object') {
      return;
    }
    let oldFail = obj.fail;
    let oldComplete = obj.complete;
    let oldSuccess = obj.success;
    let timeId;
    obj.timeout = obj.timeout || 10000;

    // 假若有timeout开启定时器
    if (obj.timeout) {
      timeId = setTimeout(() => {
        obj.over = true;
        oldFail && oldFail.apply(obj, [{ isTimeout: true }]);
        oldComplete && oldComplete.apply(obj, [{}]);
      }, obj.timeout);
    }
    obj.success = (...args) => {
      obj.end = +new Date();
      // 在队列中或者因为超时结束的直接return
      if (obj.inRetry || obj.over) {
        return;
      }
      oldSuccess && oldSuccess.apply(obj, args);
    };
    obj.fail = (...args) => {
      if (obj.over) {
        return;
      }
      if (Array.from(args)[0].errMsg === 'request:fail exceed max task count' && !obj.inRetry) {
        // 并发数超出则进入队列,不触发fail与complete
        obj.inRetry = true;
        this.retry.push(obj);
      } else {
        oldFail && oldFail.apply(obj, args);
      }
    };
    obj.complete = (...args) => {
      if (obj.inRetry || obj.over) {
        return;
      }
      clearTimeout(timeId);
      if (this.retry.length) {
        // complete完成,检查队列有则拿出来执行
        let newObj = this.retry.shift();
        newObj.inRetry = false;
        this.emitRequest(newObj);
      }
      oldComplete && oldComplete.apply(obj, args);

    };
    wx.request(obj);
  },
};

function request (obj) {
  RequestQ.emitRequest(obj);
}复制代码

不太小程序也在持续地完善,基础库在1.4.0更新了request, 队列处理也帮咱们作好了

U 更新 API request 超过并发限制作队列处理
U 更新 API request 返回 requestTask 支持 abort 操做

这里还得说个wx.request的注意点,微信小程序默认状况下dataType为'json',会尝试对响应的数据作一次JSON.parse,因此假如返回一张base64图等等数据,在真机上就会出现错误(这个错误还挺难找的)

pomise化

将接口promise化能够减小回调,代码看起来也会更加清晰
记得要引入promise-polyfill,在某些机型中微信小程序对promise的支持并很差,可使用本身的promise

具体怎么编写promise化的接口就不详细说了,在success方法 resolve, 在error方法reject, 不管什么状况均返回promise
这里引一段网上的promisify

// 连接:http://www.jianshu.com/p/4433d46e6235
// 用Promise封装小程序的其余API
export const promisify = (api) => {
    return (options, ...params) => {
        return new Promise((resolve, reject) => {
            api(Object.assign({}, options, { success: resolve, fail: reject }), ...params);
        });
    }
}复制代码

小程序尺寸单位rpx产生的微小的缝隙

官网介绍小程序这个rpx有句

注意: 在较小的屏幕上不可避免的会有一些毛刺,请在开发时尽可能避免这种状况。

遇到一个这样的问题,使用了
padding-bottom: 0rpx;
却发现padding-bottom有个微小的缝隙,只要将0rpx改为0便可

注意有时还会出现多个元素并排使用rpx,毛刺偏差累积起来可能会产生比较大的影响,假如出现这种状况,可使用白分比来替代解决

刷新方案(加载方案)

  • 下拉刷新
  • 触顶加载
  • 无限加载load more
  • 刷新按钮

下拉加载实现

  • page自带的事件监听,.json中配置enablePullDownRefresh,而且监听onPullDownRefresh,使用stopPullDownRefresh
    // index.json
    {
    "enablePullDownRefresh": true
    }复制代码
  • 监听手势事件模拟实现(这个相对复杂,而且实现出来性能以及兼容清况也未知)

触顶加载

  • 使用scroll-view的bindscrolltoupper方法

无限加载loadmore

  • 直接使用page的onReachBottom监听
  • 使用scroll-view的bindscrolltolower方法

刷新按钮

  • 由于小程序没有当前页面的刷新方式,可使用position fixed作一个按钮,z-index设层级高一点便可

swiper-view实现频道滑动切换

为了实现跟原生应用接近的体验,采用手势左右滑动来实现频道切换

先讲讲swiper-view如何实现滑动的呢?


从上图swiper-item能够看到其实就是改变translate去实现的
swiper-item绝对定位,并加入will-change:auto提高为合成层,在实现动画translate时让页面不发生重绘,在GPU完成

注意到一个absolute,因此swiper-item内部的内容是没法把外部给撑开的,因此没法实现自适应,必须本身指定高度

咱们的需求是要实现上面预留导航栏,全屏滑动,css上就能够这样

page {
  box-sizing: border-box;
  -webkit-box-sizing: border-box;
  height: 100%;
    /* 预留顶部导航栏 */
  padding-top: 89rpx;  
}
.swiper-container {
    height: 100%;
}复制代码

假如你还想在里面放入可滚动的列表项,毫无疑问得使用scroll-view,而不是view(overflow:auto)了,否则reachBottom的触发就会出问题,由于原本就只有一屏了

加入scroll-view的话,Page下拉加载是跟scroll-view相冲突的,因此要么抛弃下拉加载,要么只能使用触顶加载

scroll-view注意点

scroll-view有一个地方很容易让人忽视,就是你在绑定scrolltoupper以及bindscrolltolower方法,你会困惑为什么并非滑到顶部和底部再触发事件,而是接近的时候才触发,其实仔细看文档你会发现

属性名 类型 默认值 说明
upper-threshold Number 50 距顶部/左边多远时(单位px),触发 scrolltoupper 事件
lower-threshold Number 50 距底部/右边多远时(单位px),触发 scrolltolower 事件

它的默认值是50,因此距离50px就会触发,因此若是要真正地触顶(底),能够先设置它们为0

video组件

开发时用到video组件,遇到一些问题也拿出来说下
首先开发者须要记住的一个很重要的点

map、canvas、video、textarea 是由客户端建立的原生组件,原生组件的层级是最高的,因此页面中的其余组件不管设置 z-index 为多少,都没法盖在原生组件上。 原生组件暂时还没法放在 scroll-view 上,也没法对原生组件设置 css 动画。

其次video组件是没办法跟着屏幕滚动的,假如你放了一个video组件fixed在顶部,它也是没法跟着屏幕滚动的,开发者工具能够实现,可是真机滚动后是会出现黑影的,视频仍是一直定位在原来的位置(这个也体现了本文开头的环境的区别),要解决这个问题就只能是不能全屏滚动,用页面的一部分scroll-view滚动便可,让视频不用滚动

还有一个就是video组件其实你用wx:if去控制渲染隐藏是有问题的,当你屡次切换,会发如今某些机型上发热严重,抓包发现以前建立的video实例并无真正地随着wx:if销毁,还在请求数据,因此,假如须要控制渲染隐藏video组件的时候,能够尝试使用hidden属性配合wx.createVideoContext控制暂停来解决问题

小程序性能调优

近期官网也出来了一个优化建议,开发者务必要看看

mp.weixin.qq.com/debug/wxado…

大致上就是

  • 不要频繁地去setData,能合成一个setData尽可能合成一个
  • 不须要视图更新的data不要使用setData
  • setData数据不要过大(当数据量过大时会增长脚本的编译执行时间,占用 WebView JS 线程)
  • 因为用户使用小程序是从CDN下载,而且目前小程序打包是会将工程下全部文件都打入代码包内(这个仍是须要小程序那边优化,按需会好点),因此目前你代码包多放东西,意味着用户得多下资源,多耗费流量,首次打开速度也会变慢

如何看文档

不得不吐槽小程序的文档搜索功能实在是太差了,基本是没法使用的,建议直接当前页面command+F去搜索,看文档必须注意看文档中的tip,这样就能够躲过不少坑

最后

谢谢阅读~
欢迎follow我哈哈github.com/BUPT-HJM
欢迎继续观光个人新博客~(老博客近期可能迁移)

欢迎关注

相关文章
相关标签/搜索