小程序相关

Posted by Qz on May 26, 2018

“Yeah It’s on. ”

微信小程序相关

小程序为什么性能还可以?

webview渲染小程序,为什么性能高,核心是预载。点击一个新页面时,webview是提前创建好的,不会走复杂的webkit、v8的初始化流程,连开发者的js代码,也是预载好的。所以点击新页面时,它的渲染速度和原生应用没什么差别。当然也有个坏处,就是启动慢。微信里启动小程序速度看着还行,其实是微信在启动小程序之前,就已经提前初始化了小程序运行环境。

在逻辑层仿造一套 DOM 接口

在逻辑层仿造一套 DOM 接口,直接维护一棵 DOM 树,这当然没问题。但是没有代理 DOM 接口,逻辑层的 DOM 树没法反映到渲染层,因为渲染层具体会出现什么样的组件,是运行时才能知道的,这不就没法生成静态模板了?

可以做一个通用的自定义组件,让它根据传入的参数不同,变成不同的小程序内置组件。而且自定义组件还支持在自己的模板中引用自己,那么我只需要一个这个通用组件,然后从逻辑层用代码去控制当前组件应该渲染成什么内置组件,再根据它是否有子节点去递归引用自己进行渲染就可以了。

为什么小程序没有dom接口

小程序没有 DOM 接口

线程之间的通信是需要时间的呀。将调用发送到渲染层,再将 DOM 调用结果发送回来,这中间由于线程通信发生的时间损耗可能会比这个接口本身需要的时间要多得多。如果以此为基础使用基于 DOM 接口的前端框架,大量的 DOM 调用可能会非常缓慢,让这个设计失去意义。

在实际测试中,如果每次 DOM 调用都进行一次线程通信,耗时大约是同等节点规模直接在渲染层调用的百倍以上;如果忽略通信需要的时间,一个实现良好的基于 DOM 代理的框架可以近似地看成一个动态模板的框架,而动态模板和静态模板相比要慢至少 50%

app的onLaunch和页面onShow同时发出请求

写wepy框架的时候遇到个坑,要在app的onLaunch中发出一些请求,并把结果保存在本地,然而在页面中onShow也发出了这些请求,也就造成了多余不必要的请求

最终解决:做了一个缓存队列,在一个请求发送的过程中,如果还有同样的一些请求发送,就将这些请求缓存在队列中,共享第一个请求的回调,不必要发送请求

// 具体做法
// global.singleton 为一个全局对象
// 一个请求对应多个resolve

function getAbTestByRequest(data = {}) {
  return new Promise(async (resolve, reject) => {
    // console.log('singleton start', '2');
    global.singleton = global.singleton || {};
    if (global.singleton[AB_TEST_KEY]) {
      global.singleton[AB_TEST_KEY].push(resolve);
    } else {
      global.singleton[AB_TEST_KEY] = [resolve];
	  //发起请求
      _getAbTestForPure(data).then((output) => {
        let func = {};
        // eslint-disable-next-line
        while (func = global.singleton[AB_TEST_KEY].shift()) {
          func(output.data.data);
          if (!hasAbTest()) {
            _saveAbTest(output.data.data);
          }
          // console.log('singleton end', output);
        }
      }, rej => {
        reject(rej);
      });
    }
  });
}

app的onLaunch和页面onShow几乎是同时触发的

Content-type问题

官网里面的示例代码中content-type是设置为’application/json’的,然而……!!!

但是原来是微信开发工具升级后,请求的header的Content-type写法变了,要改成:

header: { content-type: ‘json’ }

tip: content-type 默认为 ‘application/json’;

网络请求之re.request 和那些坑

header 为 application/json,接口传回来的参数要是json 格式的,否则会报500错误,接口返回来的参数是xml则header[‘content-type’] 要设置为’application/x-www-form-urlencoded’

method

需大写

有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT

data 数据说明

  • 对于 GET 方法的数据,会将数据转换成 query string(encodeURIComponent(k)=encodeURIComponent(v)&encodeURIComponent(k)=encodeURIComponent(v)…)
  • 对于 POST 方法且 header[‘content-type’] 为 application/json 的数据,会对数据进行 JSON 序列化
  • 对于 POST 方法且 header[‘content-type’] 为 application/x-www-form-urlencoded 的数据,会将数据转换成 query string (encodeURIComponent(k)=encodeURIComponent(v)&encodeURIComponent(k)=encodeURIComponent(v)…)

scroll-view横向滚动

scroll-view横向无法滚动

要设置css样式

white-space: nowrap;

去除border边框

问题: 使用传统的用“border:none;来去除边框”,依旧有一条细细的border;

解决: 使用 button::after{ border: none; } 来去除边框

canvas的drawImage

问题:真机canvas的drawImage无法加载加载网络图片

解决:canvas上绘制的目前只支持本地图片,网页上的图片如果是网络图片,在显示时都会暂存在本地,有的直接在缓存中。所以,我们使用getimageinfo去获取到我们的图片的信息先(主要是缓存在本地的时候,会保存下一个临时文件),接着再去使用这个方式去保存到本地的相册

    //拿到可以在canvas绘制的图片url
    getImageInfo (url) {
      return new Promise((resolve, reject) => {
        /* 获得要在画布上绘制的图片 */
        const objExp = new RegExp(/^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/)
        if (objExp.test(url)) {
          wx.getImageInfo({
            src: url,
            complete (res) {
              if (res.errMsg === 'getImageInfo:ok') {
                console.log('网络图片', res.path)
                resolve(res.path)
              } else {
                reject(new Error('getImageInfo fail'))
              }
            }
          })
        } else {
          console.log('本地图片', url)
          resolve(url)
        }
      })
    }

e.target.dataset的问题

 在微信开发中我们经常会用到标签中属性的属性值,有时候我们通过 data-* 和 e.target.dateset 来获取属性值会出现一点小bug,即是调用出来的数据是undefined。

 <--HTML写法:-->
<button binTap="buy" data-textId="101"></button>

很多人可能会像我我一样卡在这里了,怎么找都找不到原因,怎么更改都是undefined。   那就是data后面的属性名写得不规范!在data后面的属性名是不能按照驼峰式的写法,只要把定义的属性名全部换成小写就没有问题了!

如何禁止页面的滚动

在组件的包裹层加上catchtouchmove=’true’ 就好了,这样手指移动的事件就不会冒泡到page层

举个例子 在mpvue中


<div class="shopping" @touchmove.stop="true">
    ...
</div>

observers中箭头函数获取不到this

数据监听器可以用于监听和响应任何属性和数据字段的变化。从小程序基础库版本 2.6.1 开始支持。

注意:从小程序基础库版本 2.6.1 开始支持。

// 下面这种写法是错误的

  observers: {
    'imgUrl': (imgUrl) => {
      console.log('this', this)
      console.log('this.data', this.data)
      console.log('this.properties', this.properties)
      console.log(imgUrl)
    }
  },
// 正确的写法

  observers: {
    imgUrl(imgUrl) {
      console.log('this', this)
      console.log('this.data', this.data)
      console.log('this.properties', this.properties)
      console.log(imgUrl)
    }
  },

还有一个非常重要的关注点:

这里this.data和this.properties指向的是同一个对象,这个对象包括data和properties中的数据。

Component constructors报错

出现了一条很奇怪的报错

VM1304:1 Component constructors should be called while initialization. A constructor call has been ignored.


console.error @ VM1304:1
_e @ VM1320 WAService.js:1
yt @ VM1320 WAService.js:1
Wt @ VM1320 WAService.js:1
(anonymous) @ index.js? [sm]:29
require @ VM1320 WAService.js:1
(anonymous) @ VM1422:6
scriptLoaded @ appservice?t=1572581149300:8790
script.onload @ appservice?t=1572581149300:8833
load (async)
loadBabelModule @ appservice?t=1572581149300:8827
window.loadBabelMod @ appservice?t=1572581149300:8841
(anonymous) @ possibleConstructorReturn.js:15

Component constructors should be called while initialization. A constructor call has been ignored.

最后发现是Component组件下json文件的问题

// 错误写法
{
  "enablePullDownRefresh": true
}

改成

// 正确写法
{
  "usingComponents": {},
  "enablePullDownRefresh": true
}

console.log中复制的对象

https://blog.csdn.net/weixin_30478757/article/details/98130152

例如:

aaa (109) [76, 1, 50, 255, 253, 255, 253, 255, 253, 74, 255, 253, 15, 255, 253, 122, 255, 253, 255, 253, 255, 253, 45, 255, 253, 255, 253, 59, 112, 59, 255, 253, 255, 253, 255, 253, 255, 253, 85, 255, 253, 255, 253, 124, 255, 253, 255, 253, 255, 253, 58, 117, 92, 84, 100, 111, 48, 71, 255, 253, 255, 253, 41, 255, 253, 255, 253, 29, 255, 253, 255, 253, 82, 76, 255, 253, 31, 16, 255, 253, 255, 253, 90, 255, 253, 125, 255, 253, 255, 253, 255, 253, 3, 47, 255, 253, 255, 253, 34, 255, …]

我们没办法完成copy它

这时候就要可以通过控制台的copy() 方法来进行打印结果的复制

增强编译后无法判断函数是否是async函数

在一般开发中 我们可以这样判断一个函数是否是async函数

asyncfunction test() {
   return "test"
 }
 
 console.log(test.constructor.name) //AsyncFunction

但是,在小程序中,打开了增强编译

asyncfunction test() {
  return "test"
}
 
console.log(test.constructor.name) //t

test.constructor.name 变成了 “t” 无法判断这个函数是否是async函数

编译后函数长这个样子:

ƒ test() {
  return _test.apply(this, arguments);
}

也就是增强编译后就无法判断原来的函数是否是async函数……

按理说es6转es5,也会去掉async语法,所以无法判断也属于正常情况。

webSocket

https://developers.weixin.qq.com/miniprogram/dev/api/network/websocket/wx.sendSocketMessage.html

并发数

  • 1.7.0 及以上版本,每个小程序端上最多可以同时存在 5 个 WebSocket 连接。
  • 1.7.0 以下版本,一个小程序同时只能有一个 WebSocket 连接,如果当前已存在一个 WebSocket 连接,会自动关闭该连接,并重新创建一个 WebSocket 连接。

Android cronet 的错误返回可以参考: https://chromium.googlesource.com/chromium/src/+/master/net/base/net_error_list.h

web-view

微信小程序的web-view组件是一个原生组件,用于在小程序中加载 web 页面。它基于微信客户端的内置浏览器内核来进行渲染,可以展示外部链接的页面内容。因此,使用 web-view 可以在小程序中嵌入网页,并利用网页技术实现一些复杂的功能。它提供了丰富的属性和事件,可以控制和监听 web 页面的加载和操作过程。

一个页面内通过 web-view 组件打开的 H5 页面也是在一个新的 WebView 中打开的,而不是在当前 WebView 中打开。这个新的 WebView 实例是独立于当前 WebView 实例的,拥有自己的 JavaScript 执行环境和渲染引擎。

web-view 组件所打开的 H5 页面是运行在微信客户端内部的,而不是在真正的浏览器环境中运行。

web-view 组件所支持的 Web API 和浏览器特性可能与真正的浏览器有所不同。

微信内部的 H5 运行环境

微信内部的H5运行环境使用的是自研的 XWeb 无头浏览器内核,它最早是基于 Chromium 进行开发和优化。

Exparser框架

微信小程序中负责组件的组织框架,包括内置组件和自定义组件的管理

  • 基于shadow DOM模型,但是又不依赖浏览器和其他工具库
  • 可在JS环境中运行,使的逻辑层和渲染层模型一致
  • 高效轻量,性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小

自定义组件的创建流程

  1. 以Component构造器的内容创建组件对象
  2. 为组件对象添加注册时声明的data
  3. 结合WXML,生成组件Shadow Tree
  4. Shadow Tree装入Compose Tree
  5. 依次触发组件的created、attached等事件

小程序 native-component

https://developers.weixin.qq.com/miniprogram/dev/component/native-component.html

通过一定的技术手段把「原生组件」直接渲染到 WebView 层级上

https://juejin.cn/post/6881502813105422349

web 体系

三端的脚本执行环境以及用于渲染非原生组件的环境是各不相同的:

运行环境 逻辑层 渲染层
Android V8 Chromium 定制内核
IOS JavaScriptCore WKWebView
小程序开发者工具 NWJS Chrome WebView

ios端

WKWebView: 是 iOS 8 之后提供的一款浏览器组件,iOS 端使用 WKWebView 进行渲染,WKWebView 在内部采用的是分层的方式进行渲染。WKWebView 会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView(「原生组件」的一种)。

Compositing Layer: NA 合成层,内核一般会将多个webview内的 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系

WKChildScrollView: 「原生组件」的一种。当把一个 DOM 节点的 CSS 属性设置为 overflow: scroll (低版本需同时设置 -webkit-overflow-scrolling: touch)之后,WKWebView 会为其生成一个 WKChildScrollView,与 DOM 节点存在映射关系,这是一个原生的 UIScrollView 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 WKChildScrollView 也是「原生组件」,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题

小程序 iOS 端的「同层渲染」也正是基于 WKChildScrollView 实现的,「原生组件」在 attached 之后会直接挂载到预先创建好的 WKChildScrollView 容器下,大致的流程如下:

android端

chromium:小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图。

WebPlugin:chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述<embed /> 标签。比如 Chrome 浏览器上的 pdf 预览,它就是基于 <embed /> 标签实现的。

Android 端的「同层渲染」就是基于 <embed /> 标签结合 chromium 内核扩展来实现的, 大致流程如下:

以二进制流的形式发送request

request文档

非常重要的两个点

options.header = {‘content-type’: ‘application/octet-stream’}

options.responseType = “arraybuffer”

这样设置header 以二进制流发送

这样设置responseType 以二进制流接受响应

在小程序请求中使用qqtea加密解密

核心代码

 if (needEncrypt) {
    // 当前环境是需要的环境 且 此请求需要加密
    console.log(`${name} 请求的参数: \n`, data)
    data = qqtea.encrypt(JSON.stringify(data), TEA_KEY)
    data = str2ab(data)
    options.header = {'content-type': 'application/octet-stream'}
    options.responseType = "arraybuffer"
  }
  wx.request({
    responseType: options.responseType || "text",
    url: options.url,
    data: data,
    method: options.method || 'POST',
    header: options.header || {'content-type': 'application/x-www-form-urlencoded'},
    success: (res) => {
      if (res.statusCode >= 200 && res.statusCode < 300) {
        if (needEncrypt) {
          let byteArr = new Uint8Array(res.data)
          res.data = qqtea.decrypt(uint8ArrayToString(byteArr), TEA_KEY);
          res.data = JSON.parse(res.data)
          debugger
        }
        resolve({...res, code: 0});
      } else {
        reject({...res, code: 0});
      }
    },
    fail: (res) => {
      reject({...res, code: 1});
    },
  });
/**
 * string => arrayBuffer
 * @param str
 * @returns {ArrayBuffer}
 */
function str2ab(str) {
  let strLen = str.length
  let buf = new ArrayBuffer(strLen);
  let bufView = new Uint8Array(buf);
  for (let i = 0; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}
/**
 * @return {string}
 */
function uint8ArrayToString(arr) {
  let dataString = "";
  for (let i = 0; i < arr.length; i++) {
    dataString += String.fromCharCode(arr[i]);
  }
  return dataString
}

插件

获取插件信息

https://developers.weixin.qq.com/community/develop/doc/0006445710c678ad974e46ce359c00

console.log(__wxConfig)  // 里面有 plugins 相关信息

区分是否插件环境

如何区分当前是 微信小程序环境 还是 微信小程序插件环境?

https://developers.weixin.qq.com/miniprogram/dev/api/open-api/account-info/wx.getAccountInfoSync.html

使用 api: wx.getAccountInfoSync();

返回值中如果有plugin => 标识当前环境为 微信小程序插件

mpvue相关

http://mpvue.com/mpvue/

不支持函数

不支持在 template 内使用 methods 中的函数。

解决方法:写在computed里

滑动组件相关

http://kuangpf.com/mpvue-weui/#/slider

bindchange和bindchanging 在mpvue框架中的写法为: @change和@changing

类型为事件的属性在完成触发事件后,取值的方式为:event.mp.detail = {value: value}

支付宝小程序

Function(…) is not a function

https://blog.csdn.net/yehuozhili/article/details/125925470

在小程序中,只要写了aync await(或者是你引入的库中写了),如果你使用了babel编译且babel的版本大于7,则会出现这个问题

原因:小程序中会禁用一些动态写法,在babel/runtime中引入的index.js中写了这么一段:

try {
  regeneratorRuntime = runtime;
} catch (accidentalStrictMode) {
  if (typeof globalThis === "object") {
    globalThis.regeneratorRuntime = runtime;
  } else {
  Function("r", "regeneratorRuntime = r")(runtime);
  }
}

补充

双线程架构

微信小程序技术原理分析

大家对小程序的底层实现都是使用双线程模型,对外宣称都会说是为了:

  1. 方便多个页面之间数据共享和交互
  2. 为native开发者提供更好的编码体验
  3. 为了性能(防止用户的JS执行卡住UI线程)

但其实真正的原因其实是:“安全”和“管控”,其他原因都是附加上去的。

因为Web技术是非常开放的,JavaScript可以做任何事。但在小程序这个场景下,它不会给开发者那么高的权限:

  • 不允许开发者把页面跳转到其他在线网页
  • 不允许开发者直接访问DOM
  • 不允许开发者随意使用window上的某些未知的可能有危险的API

当然,想解决这些问题不一定非要使用双线程模型,但双线程模型无疑是最合适的技术方案。

双线程之间的通信

逻辑层和渲染层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。通过把 WXML 转化为数据,通过 Native 进行转发,来实现逻辑层和渲染层的交互和通信。

  1. 在渲染层会把WNML转化成Js对象,Js对象会模拟DOM树
  2. 逻辑层更新数据的时候,通过setData方法将数据从逻辑层转发到Native,Native再转发到渲染层
  3. 这时候,比较两虚拟DOM树的差异,最后将差异应用到真实DOM树上,更新页面。

Virtual DOM 相信大家都已有了解,大概是这么个过程:用JS对象模拟DOM树 -> 比较两棵虚拟DOM树的差异 -> 把差异应用到真正的DOM树上。

网络调优

https://developers.weixin.qq.com/miniprogram/dev/framework/performance/network.html

原有的 wx.connectSocket 接口在新版本设计中承载了创建实例 new SocketTask 的用途, 所以除了 wx.connectSocket 以外, 不应该使用其它任何挂在 wx 上的 WebSocket 接口; 在 wx.connectSocket 调用后, 请立即同步监听 SocketTask.onOpen, 否则可能会漏掉 onOpen 通知

    this.ws = wx.connectSocket({
      url: url,
      fail: res => {

      },
      success: res => {
      }
    });
		// 同步形式
    this.ws.onOpen(this._onopen.bind(this));
    this.ws.onMessage(this._onmessage.bind(this));
    this.ws.onError(this._onError.bind(this));