# 小程序开发填坑总结

最近新公司接手一个题库小程序,也是我第一个小程序项目,属于一期升二期改版。

# 架构

首先对原项目结构进行了优化,将配置文件、常量等从util文件夹提出,对所有ajax请求放到api文件夹统一管理,引入状态管理器,引入eslint配置。由于是第一个小程序,暂未使用第三方框架,下次从零开始写新项目考虑使用mpvue或者wepy

# 目录结构

# ajax库

由于小程序没有dom的概念,axios在小程序中无法使用。找到一款fly.js,全面支持浏览器、小程序、node。RN等多种环境,用法类似axios,可以设置拦截器等。

直接把wx.umd.min.js文件放到lib目录,在api/index.js中引入。使用如下:

import { host, app, version } from '../config/index.js';
var Fly = require('../libs/flyio/wx.umd.min.js');
var fly = new Fly();

// 配置请求基地址
fly.config.baseURL = host;
fly.config.timeout = 10000;
// 添加拦截器
/**
request: {
  baseURL,  //请求的基地址
  body, //请求的参数
  headers, //自定义的请求头
  method, // 请求方法
  timeout, //本次请求的超时时间
  url, // 本次请求的地址
  withCredentials //跨域请求是否发送第三方cookie
}
**/
fly.interceptors.request.use(
  request => {
    wx.showLoading();
    // post传参用formdata
    if (request.method === 'POST') {
      request.headers['Content-Type'] = 'application/x-www-form-urlencoded ';
    }
    // 检查token
    if (token == null || token === '') {
      fly.lock();
      // do sth
      fly.unlock();
      return request;
    } else {
      return request;
    }
  }
);
/**
response: {
  data, //服务器返回的数据
  engine, //请求使用的http engine(见下面文档),浏览器中为本次请求的XMLHttpRequest对象
  headers, //响应头信息
  request  //本次响应对应的请求信息
}
**/
fly.interceptors.response.use(
  response => {
    wx.hideLoading();
    // 处理返回码
    return Promise.reject(response.data);
  },
  err => {
    wx.hideLoading();
    // 处理错误
    return err;
  }
);

// 业务接口

/** ******************* 登陆 *****************/
// 获取登录状态
export const checkLoginStatus = params => fly.get('/sso/checkLogin.html', params);
// 获取免费资料保存微信
export const getFreeCourse = data => fly.post('/user/getFreeCourse.html', data);
...

# 状态管理

网上多使用redux和mobx,mpvue可直接使用vuex。相比redux的繁琐,mobx更适合小程序。找到一款基于mobx的第三方库wechat-weapp-mobx

mobx可创建多个store,在store目录中建立各个store文件。由于小程序是多页面,页面跳转时,需要将状态存储到全局,可以使用app.js的globalData。


App({
  onLaunch: function () {
    ...
  },
  globalData: {
    stores: {
      userStore,
      homeStore,
      cartStore,
      topicStore,
      courseStore,
      openCourseStore,
    },
  },
});

store文件编写如下:

/**
 * 用户中心
 */
import * as api from '../api/index';
var mobx = require('../libs/mobx/mobx');
var extendObservable = mobx.extendObservable;

var UserStore = function () {
  extendObservable(this, {
    /** data **/
    key: '',

    /** 计算属性 **/
    get xxx () {
      return 'xxx'
    }
  });

  /** action **/
  this.showLoginModal = function () {
    this.isLoginModalShow = true;
  };
  this.login = function () {
    return api.login()      
  }
}

module.exports = new UserStore();

在页面引入:

// index.js
const observer = require('../../libs/mobx/observer').observer;
const globalData = getApp().globalData;

Page(observer({
  props: {
    homeStore: globalData.stores.homeStore,
    userStore: globalData.stores.userStore,
  },
  data: {
    ...
  },
  onLoad: function (options) {
  },
  onShow: function () {
  },
  
  ...
}));

发现对于深层属性,在赋值时设置过的就会动态监听,而直接在对象添加属性则不会触发刷新。

在action中返回promise对象,在js中调用并执行then函数,发现get计算属性先执行,then中可以拿到计算后的结果。

# Eslint

习惯使用eslint对项目进行格式校验,配合prettier自动格式对齐。

在根目录创建package.json文件,安装相关库:

npm install --save-dev eslint babel babel-eslint eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard

新建.eslintrc.js文件添加常用规则,在globals中设置小程序全局变量:

module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: [
    'standard'
  ],
  plugins: [
  ],
  rules: {
    'semi': [2, 'always'],
    'no-console': 'off',
    'no-debugger': 'off',
    "comma-dangle": [2, "always-multiline"],
    "indent": [2, 2],
    'prefer-promise-reject-errors': 0,
  },
  parserOptions: {
    parser: 'babel-eslint'
  },
  globals: {
    wx: null,
    App: null,
    Page: null,
    getApp: null,
    Component: null,
    getCurrentPages: null,
  },
}

新建.eslintignore和.gitignore文件。

# fundebug

fundebug官网创建项目,在app.js引入fundebug并初始化。这样项目运行过程中发生的接口错误都能在fundebug控制台查看详细情况,便于分析错误。

var fundebug = require('./libs/fundebug.0.8.2.min.js');
fundebug.init({
  apikey: 'xxxxxxx',
});

# 填坑

# 页面生命周期

onLoad在页面加载时执行,onShow在页面显示时执行。开始以为onShow就是对应小程序后台切前台,后来发现在页面加载阶段也会执行,在页面后退时也会执行。

小程序页面可以存在5层。通过redirectTo跳转时,页面销毁并创建新页面。而通过navigateTo跳转时,页面并未销毁,而是在下一层打开新页面。当点击后退时,下一层页面销毁并后退到上一层,此时触发onShow事件。

另外页面加载时onLoad和onShow都后执行,onShow无法拿到onLoad的参数,如果onLoad中setData,onShow从data取值不能一定取到。最好是设置一个flag,当第一次登陆时,从onLoad执行相关函数;onShow判断第一次执行修改flag,以后执行相关函数。

在开发者工具的AppData中可以清楚地看到页面结构:

# data初始化问题

小程序初始化页面时,data中直接使用globalData[key],当对应值在app.js中设置时可正常获取,当这个值是在其他页面设置的时,得到null。data中使用wx.getStorageSync取值同样得到null。所以应当在onload中设置data项的值。

# setData异步问题

文档中说setData是异步函数,实际改变数值是同步的,渲染页面是异步的。

# 分享与URL传值截断

小程序的分享默认分享当前页面,但是分享后对方进入无法后退。网上存在两种办法,一是在分享页面增加一个回到首页的按钮,二是分享页面先到首页再跳转到分享页。

这里我采取了第二种方法,分享方法如下:

onShareAppMessage: function () {
  return {
    path: `/pages/home/index?target=${url}`,
  };
},

然后发现存在一个问题,就是对于带参数的页面,参数没有带过去。然后增加参数:

url = url + '?' + 'key=value'

发现依然没有接收到参数,原来是index后面的?和url中的?,存在两个?,而拿到的options到第二个?就截断了。

最终分享如下:

onShareAppMessage: function () {
  let path = getPath();
  return {
    path: `/pages/home/index?target=${path.url}&targetOptions=${path.options}`,
  };
},

# wxs使用

wxml模板不支持引用js中的函数,但是可以通过使用wxs来达到同样的效果。

wxs文件:

module.exports = {
  fix2: function (num) {
    return num.toFixed(2);
  },
};

在wxml中引用:

<wxs src="./oldExamList.wxs" module="m1"></wxs>

<view>¥{{m1.fix2(item.price)}}</view>

# wxs报错

wxs并非完全的js环境,要注意和js的区别。wxs的报错不可见,这个比较坑。如果小程序突然编译错误,页面完全空白,注意wxs,即便不引入wxs文件也不行。

  • 在使用wxs的过程中,遇到日期解析的问题。查阅官方文档,小程序支持Date.now()和Date.parse()方法,但是一直解析失败,后来发现是日期格式的问题,在ios设备上不支持xxxx-xx-xx这样的格式,转换为/后正常。
  • 在wxs中不能使用es6,即使模拟器开启es6转换也没用。
  • 在wxs中,遇到for循环中第一个条件声明多个变量报错。

# scroll-view与overflow: auto

scroll-view除了有一些事件之外,还能够惯性滚动,而overflow: auto则不具有惯性。未测试overflow: auto加-webkit-overflow-scrolling:touch与scroll-view对比。

# textarea组件层级

小程序中textarea属于原生组件,层级高于webview。因此存在一个严重的bug,无论怎么设置z-index,textarea都会在最上方,小程序对此提供了cover-viewcover-image组件,但是cover-view中只能包含这两种组件,基本用不上。

唯一的办法就是切换textarea的显示,在弹窗都时候隐藏textarea。但是当前项目做题页面要能够左右滑动,此时textarea就无法隐藏了,我目前的解决方案是平时将textarea替换为普通的view显示,点击时修改为textarea,blur时再替换为view。这样唯一的问题就是用户需要点两次输入框才能编辑。textarea自带的focus属性并不能解决问题。

# 富文本解析及内存

小程序对富文本的支持很差,原生rich-text对象需要的数据格式为节点对象,且无法对内部图片等再设置属性和点击预览等。使用最多的库是wxParse

下载wxParse相关文件放到libs中。在要使用的页面引入js文件,并调用:

const WxParse = require('../../libs/wxParse/wxParse.js');
WxParse.wxParse(`question`, 'html', this.getTopicQuestion(curTopic), this);

会在页面的data中创建一个question的字段,值为解析后的节点对象。

在wxml中使用:

<import src="../../libs/wxParse/wxParse.wxml" />
<template is="wxParse" data="{{wxParseData:question}}" />

这里的做题页一套卷子有几十道题,用swiper组件左右切换。wxParse提供了一个wxParseTemArray方法,用法如下:

for (let i = 0; i < replyArr.length; i++) {
  WxParse.wxParse('reply' + i, 'html', replyArr[i], that);
  if (i === replyArr.length - 1) {
    WxParse.wxParseTemArray("replyTemArray",'reply', replyArr.length, that)
  }
}
<block wx:for="{{replyTemArray}}" wx:key="">
  回复{{index}}:<template is="wxParse" data="{{wxParseData:item}}"/>
</block>

初始代码遍历题目数组,对每一道题进行富文本解析并放到swiper中渲染,性能很差。在wxml中通过wx:if做判断,只有当前题目及相邻题目进行渲染,其他swiper-item内容为空,性能得到提升。但是在进入第一道题时,解析几十个富文本并放到内存,加载依然很慢,切换时很卡。

对富文本做动态解析,只解析当前题目及相邻题目,创建prevQuestion,curQuestion,nextQuestion,加载很快,但是切换时屏幕会闪,因为滑动时,下一题正常显示nextQuestion,但是滑动结束后,先显示curQuestion,再显示新的curQuestion。

思路回到之前的数组,对每道题目,不能使用相同的wxParse字段。在初始化时,解析前三道题目,每次滑动时,解析新的题目。

WxParse.wxParse(`question_${curIndex}`, 'html', this.getTopicQuestion(curTopic), this);

但是在wxml中,无法使用拼接的变量名,尝试用[]无效。我们需要一个数组进行包裹。创建一个数组,直接赋值并在页面使用数组对应索引。

questions[curIndex + 1] = this.data[`question_${curIndex + 1}`].nodes;

这时还有一个性能问题,就是切换题目时不断创建新的字段到data,内存越来越大。翻页时,直接获取data对象,删除对应字段,重新setData,发现出现闪动。把不用的题目字段赋值为null,依然在内存显示。赋值为undefined,从内存消失。

# 富文本图片大小

wxParse会对解析的图片设置图片预览,并分析图片大小设置图片属性。

从wxParse.js代码可以看到,对设置了width和height的图片,将其读取到了图片的attr中;对于没有设置宽高的图片,获取其实际宽高;对于没有设置宽高且实际宽度大于屏幕宽度的,设置宽度为屏幕宽度。

这里我们的试题富文本中,图片有设置宽高,发现插件按照屏幕宽度进行了渲染。打开wxParse.wxml,找到图片模板:

<template name="wxParseImg">
    <image class="{{item.classStr}} wxParse-{{item.tag}}" data-from="{{item.from}}" data-src="{{item.attr.src}}"  data-idx="{{item.imgIndex}}"  src="{{item.attr.src}}" mode="widthFix" bindload="wxParseImgLoad" bindtap="wxParseImgTap" style="width:{{item.width}}px;" />
</template>

加上宽高属性,发现大部分图片正常。但是少数图片实际宽度大于屏幕宽度,再加上max-width。最终代码如下:

<template name="wxParseImg">
    <image class="{{item.classStr}} wxParse-{{item.tag}}" data-from="{{item.from}}" data-src="{{item.attr.src}}"  data-idx="{{item.imgIndex}}"  src="{{item.attr.src}}" mode="widthFix" bindload="wxParseImgLoad" bindtap="wxParseImgTap" style="width:{{item.attr.width}}px; height: {{item.attr.height}};" />
</template>

# 组件传值

自定义组件可接收props,使用时从this.data.xxx取值。之前传递了一个叫data的prop,发现this.data和this.data.data循环引用了,导致取其他prop时取不到值。

# 组件slot渲染错误

小程序自定义组件支持slot。使用中遇到slot中元素未渲染到父节点下,而是渲染为兄弟节点。搜索发现其他人也遇到这个问题,暂时无解。

# 第三方授权客服和模板消息

小程序可使用微信自带的客服和模板消息。

在微信设置客服,页面使用button,设置open-type="contact"即可。

<button open-type="contact">联系客服</button>

当使用第三方授权时,如果授权了客服,则不会发送消息到自带客服,即使尚未开启消息推送。

在未开启消息推送时,可使用自带模板消息,在公众平台选择模板和字段,用form提交。

<form report-submit="true" bindsubmit="formSubmit">
  <button form-type="submit">推送模板</button>
</form>
formSubmit (e) {
  app.request.sendTemplateMessage({
    appId: '',
    data: {
      keyword1.DATA: '',
      keyword2.DATA: '',
    },
    formId: e.detail.formId,
    openId: '',
    templateId: '',
  })
},
Last Updated: 11/2/2019, 4:42:17 PM