yanyue404 / blog

Just blog and not just blog.
https://yanyue404.github.io/blog/
Other
88 stars 13 forks source link

2022年的微信小程序开发技巧 #233

Open yanyue404 opened 2 years ago

yanyue404 commented 2 years ago

1. 使用解构赋值、mixins 的方式对小程序 page.js 可复用的代码的组合,减少重复

// 用basePage复用的Page实现
Page({
  ...getBasePage(),
  ...{
    // 这里就和正常的page一样实现就好
    data:{
      a:1
    },
    next(){
      // 子页面只需要专注实现next函数即可
    },
  })

2. require 的路径不支持绝对路径

源码: const util = require('../../../utils/fetch.js')

解决:在 App 绑定 require,Page 里获取 app,直接 app.require 引入。

// in app.js
App({
  onLaunch() {},
  require(path) {
    return require(`${path}`);
  },
});

// in page.js
const app = getApp();
const util = app.require("./utils/util.js");

3. miniprogram-api-promise、wx-promise-pro 扩展小程序 api promise 化

// 例: wx-promise-pro
wx.pro
  .showLoading({
    title: "加载中",
    mask: true,
  })
  .then(() => console.log("in promise ~"));

4. 动画

WxCountUp 数字滚动,代替 requestAnimationFrame 帧渲染。

import WxCountUp from "../../plugins/wx-countup/WxCountUp.js";

Page({
  data: {
    number: 0,
  },
  onLoad: function () {
    // 最后一个参数必填
    this.countUp = new WxCountUp("number", 5234, {}, this);
  },

  start() {
    this.countUp = new WxCountUp("number", 5234, {}, this);
    // 开始动画
    this.countUp.start();
    // this.countUp.start(() => console.log('Complete!'));
  },

  pauseResume() {
    // 暂停/重新开始
    this.countUp.pauseResume();
  },

  reset() {
    // 重置
    this.countUp.reset();
  },

  update() {
    // 更新为新值
    this.countUp.update(1000);
  },
});

5. npm 包

合理使用小程序 npm 包的组件化方式复用组件等。

js 中引入 npm 包:

const myPackage = require("packageName");
const packageOther = require("packageName/other");

使用 npm 包中的自定义组件:

{
  "usingComponents": {
    "myPackage": "packageName",
    "package-other": "packageName/other"
  }
}

6. 性能优化

评测方法与规则,结合我的优化实例:

(1)存在渲染界面的耗时过长的情况;

渲染界面的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要校验下是否同时渲染的区域太大

解决方案:页面数据过大,一次渲染耗费时长,按模块分多次渲染,加入骨架屏

(2)存在 setData 的数据过大

由于小程序运行逻辑线程与渲染线程之上,setData 的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间;变量单次赋值 598k;

解决方案:产品列表页所有数据来源一个接口,数据过大,大量冗余字段;先是接口拆分,然后数据清洗,去除无用字段,减少数据大小,一般不超过 256k;

(3)滚动区域没有开启惯性滚动

惯性滚动会使滚动比较顺畅,在安卓下默认有惯性滚动,而在 iOS 下需要额外设置 -webkit-overflow-scrolling: touch 的样式;

解决方案:使用 scroll-view 组件,添加-webkit-overflow-scrolling: touch 的样式

(4)存在将未绑定在 WXML 的变量传入 setData

setData 操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入 setData 会造成不必要的性能消耗;

解决方案:按要求处理,减少 setData 的调用 (可以使用 this.data 存储不需要在 wxml 中展示的变量)

(5) 存在短时间内发起太多的图片请求

短时间内发起太多图片请求会触发浏览器并行加载的限制,可能导致图片加载慢,用户一直处理等待。应该合理控制数量,可考虑使用雪碧图技术、拆分域名或在屏幕外的图片使用懒加载

解决方案:懒加载需要监听滚动的高度,计算当前 dom 的高度,调用 setData 改变图片的显隐状态,会增加另外性能损失,再考虑...

(6)存在 setData 的调用过于频繁

setData 接口的调用涉及逻辑层与渲染层间的线程通过,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用 pages/home/home:onPageScroll 方法 38 次/秒,touchEnd 方法 26 次/秒;

解决方案:滚动监听处理数据,使用节流处理;页面其他多次调用,减少非必要的调用,非数据绑定的使用常规赋值方法;

7. 错误监控

给小程序增加错误信息收集,包括 js 脚本错误信息收集和 http 请求错误信息收集。

脚本错误收集

对于脚本错误收集,这个相对比较简单,因为在 app.js 中提供了监听错误的 onError 函数。

只不过错误信息是包括堆栈等比较详细的错误信息,然后当上传时我们并不需要这么信息,第一浪费宽带,第二看着累又无用。我们需要的信息是:错误类型、错误信息描述、错误位置。

thirdScriptError
aa is not defined;at pages/index/index page test function
ReferenceError: aa is not defined
    at e.test (http://127.0.0.1:62641/appservice/pages/index/index.js:17:3)
    at e.<anonymous> (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:31500)
    at e.a (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:26386)
    at J (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:20800)
    at Function.<anonymous> (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:22389)
    at http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:27889
    at http://127.0.0.1:62641/appservice/__dev__/WAService.js:6:16777
    at e.(anonymous function) (http://127.0.0.1:62641/appservice/__dev__/WAService.js:4:3403)
    at e (http://127.0.0.1:62641/appservice/appservice?t=1543326089806:1080:20291)
    at r.registerCallback.t (http://127.0.0.1:62641/appservice/appservice?t=1543326089806:1080:20476)

这是错误信息字符串,接下来我们对它进行截取只需要拿我们想要的信息即可。我们发现这个字符串是有规则的。第一行是错误类型,第二行是错误详情和发生的位置,并且是";"分好分开。所以我们还是很容易就可以拿到我们想要的信息。

//格式化错误信息
function formateErroMsg(errorMsg) {
  //包一层try catch 不要让信息收集影响了业务
  try {
    let detailMsg = "";
    let detailPosition = "";
    let arr = errorMsg.split("\n");
    if (arr.length > 1) {
      //错误详情和错误位置在第二行并用分好隔开
      const detailArr = arr[1].split(";");
      detailMsg = detailArr.length > 0 ? detailArr[0] : "";
      if (detailArr.length > 1) {
        detailArr.shift();
        detailPosition = detailArr.join(";");
      }
    }

    const obj = {
      //错误类型就是第一行
      error_type: arr.length > 0 ? arr[0] : "",
      error_msg: detailMsg,
      error_position: detailPosition,
    };
    return obj;
  } catch (e) {}
}

获取到我们想要的信息,就可以发送到我们服务后台,进行数据整理和显示,这个需要服务端配合,就不深入讲了,我们拿到了数据,其他都不是事。

http 请求错误信息收集

对于 http 请求错误信息收集方式,我们尽量不要暴力埋点,每个请求发送前发送后加上我们的埋点。这样工作量太大,也不易维护。因此,我们可以从底层出发,拦截 wx.request 请求。使用 Object.definePropert 对 wx 对象的 request 进行重新定义。具体实现如下:

// 请求排除:对于发送错误信息的接口不收集,防止死循环
const reqExclude = [/reciveFrontEndResourceToS3|appendVideo|healthcare/i];

function rewriteRequest() {
  try {
    const originRequest = wx.request;
    Object.defineProperty(wx, "request", {
      configurable: true,
      enumerable: true,
      writable: true,
      value: function () {
        let options = arguments[0] || {};

        if (reqExclude.some((reg) => reg.test(options.url))) {
          //这里要执行原来的方法
          return originRequest.call(this, options);
        }
        //这里拦截请求成功或失败接口,拿到请求后的数据
        ["success", "fail"].forEach((methodName) => {
          let defineMethod = options[methodName];
          options[methodName] = function () {
            try {
              //在重新定义函数中执行原先的函数,不影响正常逻辑
              defineMethod && defineMethod.apply(this, arguments);
              //开始信息收集
              let statusCode, result, msg;
              //请求失败
              if (methodName == "fail") {
                statusCode = 0;
                result = "fail";
                msg = (arguments[0] && arguments[0].errMsg) || "";
              }
              //请求成功,
              //收集规则为:
              // 1、 statusCode非2xx,3xx
              // 2、 statusCode是2xx,3xx,但接口返回result不为ok
              if (methodName == "success") {
                let data = arguments[0] || {};
                statusCode = data.statusCode || "";
                if (
                  data.statusCode &&
                  Number(data.statusCode) >= 200 &&
                  Number(data.statusCode) < 400
                ) {
                  let resData = data.data
                    ? typeof data.data == "object"
                      ? data.data
                      : JSON.parse(data.data)
                    : {};
                  //请求成功,不收集
                  if (resData.result == "ok") {
                    return;
                  }
                  result = resData.result || "";
                  msg = resData.msg || "";
                } else {
                  result = "";
                  msg = data.data || "";
                }
              }
              //过滤掉header中的敏感信息
              if (options.header) {
                options.header.userid && delete options.header.userid;
              }
              //过滤掉data中的敏感信息
              if (options.data) {
                options.data.userid && delete options.data.userid;
              }

              let collectInfo = {
                url: options.url || "", //请求地址
                method: options.method || "GET", //请求方法
                request_header: JSON.stringify(options.header || {}), //请求头部信息
                request_data: JSON.stringify(options.data || {}), //请求参数
                resp_code: statusCode + "", //请求状态码
                resp_result: result, //请求返回结果
                resp_msg: msg, //请求返回描述信息
              };
              //提交参数与上一次不同,或者参数相同,隔了1s
              if (
                JSON.stringify(collectInfo) != lastParams.paramStr ||
                new Date().getTime() - lastParams.timestamp > 1000
              ) {
                //上传错误信息
                Post.post_error(_miniapp, "http", collectInfo);
                lastParams.paramStr = JSON.stringify(collectInfo);
                lastParams.timestamp = new Date().getTime();
              }
            } catch (e) {
              //console.log(e);
            }
          };
        });
        return originRequest.call(this, options);
      },
    });
  } catch (e) {
    // Do something when catch error
  }
}

包装拦截 wx.request 如下:

function my_request() {
  //只要执行一次拦截代码即可
  !_isInit && rewriteRequest();
  return wx.request(options);
}

接下来我们看下后台数据,持续监控,会帮我们找出很多隐藏的 bug。

参考