“Yeah It’s on. ”
微信小程序相关
小程序为什么性能还可以?
webview渲染小程序,为什么性能高,核心是预载。点击一个新页面时,webview是提前创建好的,不会走复杂的webkit、v8的初始化流程,连开发者的js代码,也是预载好的。所以点击新页面时,它的渲染速度和原生应用没什么差别。当然也有个坏处,就是启动慢。微信里启动小程序速度看着还行,其实是微信在启动小程序之前,就已经提前初始化了小程序运行环境。
在逻辑层仿造一套 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’;
header
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环境中运行,使的逻辑层和渲染层模型一致
- 高效轻量,性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小
自定义组件的创建流程
- 以Component构造器的内容创建组件对象
- 为组件对象添加注册时声明的data
- 结合WXML,生成组件Shadow Tree
- Shadow Tree装入Compose Tree
- 依次触发组件的created、attached等事件
小程序 native-component
https://developers.weixin.qq.com/miniprogram/dev/component/native-component.html
通过一定的技术手段把「原生组件」直接渲染到 WebView
层级上,
https://juejin.cn/post/6881502813105422349
三端的脚本执行环境以及用于渲染非原生组件的环境是各不相同的:
运行环境 | 逻辑层 | 渲染层 |
---|---|---|
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
非常重要的两个点
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 相关信息
区分是否插件环境
如何区分当前是 微信小程序环境 还是 微信小程序插件环境?
使用 api: wx.getAccountInfoSync();
返回值中如果有plugin => 标识当前环境为 微信小程序插件
Taro
Taro 是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发 微信 / 京东 / 百度 / 支付宝 / 字节跳动 / QQ / 飞书 小程序 / H5 / RN 等应用。
依赖预编译
Taro 参考 Vite 使用了 esbuild 收集用户使用到的第三方依赖,并分别进行打包。打包后的模块会作为 Webpack 的 entry,最终打包为模块联邦 Remote 应用,供主应用(Host)消费。
小程序编译模式
https://github.com/NervJS/taro-rfcs/blob/feat/compile-mode/rfcs/0000-compile-mode.md
小程序编译模式(CompileMode)通过在编译阶段对开发者的代码进行扫描,把 JSX、Vue template 代码提前编译为对应的小程序模板代码,达到减少小程序渲染层虚拟 DOM 树节点数量的效果,从而提升渲染性能。
在小程序分层架构的限制下,Taro3 的渲染方式仍然是动态渲染节点的唯一写法,只有这样才能使真正的 React、Vue 等 Web 框架在小程序中运行起来。
然而借鉴 Vue3 的思路,JSX、Vue template 中除了动态的部分外还会存在静态的部分。假设我们提前在编译阶段把静态部分识别出来,然后生成对应的小程序模板,就能使这部分对应的小程序渲染层虚拟 DOM 节点数趋近原生写法:
跨端原理
- 一套代码多端输出:开发者可以使用一套基于 React 的语法编写代码,而不需要关注不同平台的差异。Taro.js 提供了一套统一的 API 和组件,这些 API 和组件在运行时会根据目标平台进行转译,从而在不同的平台上实现相同的效果。
- 平台适配层:Taro.js 在运行时会根据当前的平台进行适配。它会根据目标平台的差异,例如小程序和 H5 在 API 使用、组件渲染等方面的差异,动态生成相应的代码来实现跨平台适配。
- 转译工具链:Taro.js 使用了一系列的转译和编译工具来实现跨平台的能力。这些工具链包括了基于 Babel 的代码转译器、基于 PostCSS 的样式转译器等。
Taro3之前(重编译时,轻运行时):
编译时是使用 babel-parser 将 Taro 代码解析成抽象语法树,然后通过 babel-types 对抽象语法树进行一系列修改、转换操作,最后再通过 babel-generate 生成对应的目标代码。
之前Taro团队是采用穷举的方式对 JSX 可能的写法进行了一一适配,这一部分工作量很大。
再看⼀下运⾏时的缺陷。对于每个⼩程序平台,都会提供对应的⼀份运⾏时框架进⾏适配。当修改⼀些 Bug 或者新增⼀些特性的时候,需要同时去修改多份运⾏时框架。
Taro3之后(重运行时):
Taro 3 则可以大致理解为解释型架构(相对于 Taro 1/2 而言),主要通过在小程序端模拟实现 DOM、BOM API 来让前端框架直接运行在小程序环境中,从而达到小程序和 H5 统一的目的,而对于生命周期、组件库、API、路由等差异,依然可以通过定义统一标准,各端负责各自实现的方式来进行抹平。
偏运行时的适配方案,一方面可以带来更好的用户开发体验,不去限制开发者的语法规范。另一方面能兼容更多的 Web 生态,支持绝大多数已有的开源工具和依赖的运行。
偏运行时的适配方案可以继承部分小程序运行时和编译时逻辑
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);
}
}
补充
双线程架构
大家对小程序的底层实现都是使用双线程模型,对外宣称都会说是为了:
- 方便多个页面之间数据共享和交互
- 为native开发者提供更好的编码体验
- 为了性能(防止用户的JS执行卡住UI线程)
但其实真正的原因其实是:“安全”和“管控”,其他原因都是附加上去的。
因为Web技术是非常开放的,JavaScript可以做任何事。但在小程序这个场景下,它不会给开发者那么高的权限:
- 不允许开发者把页面跳转到其他在线网页
- 不允许开发者直接访问DOM
- 不允许开发者随意使用window上的某些未知的可能有危险的API
当然,想解决这些问题不一定非要使用双线程模型,但双线程模型无疑是最合适的技术方案。
双线程之间的通信
逻辑层和渲染层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。通过把 WXML 转化为数据,通过 Native 进行转发,来实现逻辑层和渲染层的交互和通信。
- 在渲染层会把WNML转化成Js对象,Js对象会模拟DOM树
- 逻辑层更新数据的时候,通过setData方法将数据从逻辑层转发到Native,Native再转发到渲染层
- 这时候,比较两虚拟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));
taro和wepy区别
框架设计理念
-
Taro:Taro 是由京东·凹凸实验室开发的多端开发框架,支持同时编译到微信小程序、H5、React Native、支付宝小程序等多个平台。Taro 的核心目标是实现“一套代码,多端运行”。它通过使用 React(或 Vue、Nerv)来构建小程序页面和组件,并通过自己的编译工具链将这些代码转化为各平台对应的代码。
-
WePY:WePY 是一个专注于微信小程序的框架,旨在让小程序的开发更加高效和灵活。WePY 的核心理念是通过增强和封装微信小程序原生的 API 和功能,提供更高层次的抽象,以便开发者能够像开发 React/Vue 应用一样开发微信小程序。它并不专注于多端支持,而是聚焦于微信小程序的开发。
开发语言和框架
- Taro:Taro 默认使用 React(或者 Vue、Nerv)来进行开发,支持 JS/TS 和 JSX 语法,开发者可以灵活选择自己熟悉的框架。它将 React 的思维方式带入微信小程序开发,允许开发者使用类组件、函数组件、Hooks 等现代 React 技术。
- WePY:WePY 是对微信小程序原生开发方式的增强,使用类似 Vue 的方式进行开发。它支持用 Vue 风格的语法来组织小程序代码,但并没有直接集成完整的 Vue 框架。通过 WePY,开发者可以使用组件化开发、更灵活的事件处理、模块化等功能。
开发工具与生态支持
- Taro:Taro 提供了完善的 CLI 工具,支持创建、开发、编译、预览等功能,且可以很方便地集成到现代前端构建工具(如 Webpack、Babel 等)中。Taro 的生态也在不断壮大,支持与各种库(如 Redux、MobX、React Router)配合使用。
- WePY:WePY 也有自己的 CLI 工具,主要用于项目创建和构建。它的生态相对较小,但也有一些与 Vue 类似的工具和插件,例如状态管理工具
wepy-redux
等。