小程序双线程模型架构的理解?
小程序分为视图层和逻辑层,视图层的相关任务全都在WebView
里执行。一个小程序存在多个界面,所以视图层存在多个WebView
线程。而逻辑层采用JsCore
线程运行JS脚本
。他们之间通过系统层的WeixinJsBridge
进行通信,也就是逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
所以小程序双线程模式主要解决体验和管控的问题
- 体验:web页面开发渲染线程和脚本线程是互斥的,长时间的脚本运行可能会导致页面失去响应或者白屏。而双线程模式是不会有这个问题的。而且这个模式下,强制使用了MVVM框架的数据驱动,即让视图状态和视图绑定在一起,同时也使用了虚拟dom优化体验
- 管控:阻止开发者使用浏览器的开发性接口,通过提供一个沙盒环境来运行开发者的js代码,只能使用微信提供开放的方法来获取元素的一些信息。这样就避免开发者的操作不在管控范围。除了JS用沙盒环境管控,html也改用了封装过的wxml(WeiXin Markup language) ,css改为wxss(WeiXin Style Sheet),为了管控,同时也是为了提供更多功能,例如封装了播放直播的live-player、滚动选择器picker-view。另外,也提供了wxs(WeiXin Script)让wxml在渲染的时候也可以做一些逻辑处理。
小程序更新视图数据的通信流程
每当小程序视图数据需要更新时,逻辑层会调用小程序宿主环境提供的 setData 方法将数据从逻辑层传递到视图层,经过一系列渲染步骤之后完成UI视图更新。完整的通信流程如下:
- 小程序逻辑层调用宿主环境的 setData 方法。
- 逻辑层执行 JSON.stringify 将待传输数据转换成字符串并拼接到特定的JS脚本,并通过evaluateJavascript 执行脚本将数据传输到渲染层。
- 渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染。
- WebView 线程开始执行渲染时,待更新数据会合并到视图层保留的原始 data 数据,并将新数据套用在WXML片段中得到新的虚拟节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。同时,将新的节点树替换旧节点树,用于下一次重渲染。
主要目录和文件的作用?
project.config.json
项目配置文件,做一些个性化配置,例如界面颜色、编译配置等等app.json
当前小程序的全局配置,包括了小程序的所有页面路径、界面表现、网络超时时间、底部 tab 等sitemap
配置小程序及其页面是否允许被微信索引pages
里面包含一个个具体的页面wxss
页面样式,app.wxss
作为全局样式,会作用于当前小程序的所有页面,局部页面样式page.wxss
仅对当前页面生效。app.js
小程序的逻辑js
页面逻辑json
页面配置wxml
页面结构
配置文件
- sitemap.json
- 微信会爬取你的页面内容, 当用户在自己的微信中搜索时可以搜索到你开发的小程序
- project.private.config.json:一些配置信息
- 比如:项目名字,是否开启热重载, 是否开启地址检查,当前版本库的版本号
- 这个文件中设置的内容会覆盖掉project.config.json文件中的相同设置
- 与project.config.json配置不同时会改变这个文件中的配置
- project.config.json:一些基础配置
- 比如项目名称、appid
- 这个文件一般不会发生变化
- app.json:全局配置
- page.json:页面的单独配置
- 每一个小程序页面也可以使用 .json 文件来对本页面的窗口表现进行配置
- 页面中配置项在当前页面会覆盖 app.json 的 window 中相同的配置项
小程序的生命周期函数
应用的生命周期
生命周期 | 说明 |
---|---|
onLaunch | 小程序初始化完成时触发,全局只触发一次 |
onShow | 小程序启动,或从后台进入前台显示时触发 |
onHide | 小程序从前台进入后台时触发 |
onError | 小程序发生脚本错误或 API 调用报错时触发 |
onPageNotFound | 小程序要打开的页面不存在时触发 |
onUnhandledRejection() | 小程序有未处理的 Promise 拒绝时触发 |
onThemeChange | 系统切换主题时触发 |
页面的生命周期
生命周期 | 说明 | 作用 |
---|---|---|
onLoad | 生命周期回调—监听页面加载 | 发送请求获取数据 |
onShow | 生命周期回调—监听页面显示 | 请求数据 |
onReady | 生命周期回调—监听页面初次渲染完成 | 获取页面元素(少用) |
onHide | 生命周期回调—监听页面隐藏 | 终止任务,如定时器或者播放音乐 |
onUnload | 生命周期回调—监听页面卸载 | 终止任务 |
组件的生命周期
生命周期 | 说明 |
---|---|
created | 生命周期回调—监听页面加载 |
attached | 生命周期回调—监听页面显示 |
ready | 生命周期回调—监听页面初次渲染完成 |
moved | 生命周期回调—监听页面隐藏 |
detached | 生命周期回调—监听页面卸载 |
error | 每当组件方法抛出错误时执行 |
描述下相关文件类型
微信小程序项目结构主要有四个文件类型
- WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构。内部主要是微信自己定义的一套组件
- WXSS (WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式
- js 逻辑处理,网络请求
- json 小程序设置,如页面注册,页面标题及tabBar
wxss和css不一样的地方
WXSS 和 CSS 类似,不过在 CSS 的基础上做了一些补充和修改
- 尺寸单位 rpx
rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。
如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。
换算成px 就是实际尺寸/2 = px;
- 使用 @import 标识符来导入外联样式。@import 后跟需要导入的外联样式表的相对路径,用;表示语句结束
@import '../plugins/wxParse/wxParse.wxss';
什么是rpx
可以根据屏幕宽度进行自适应,规定屏幕宽度为750rpx,建议开发中将 iPhone6 作为视觉稿的标准
- iPhone6 屏幕宽度为375px 750物理像素 所以 750rpx = 375px = 750物理像素
- 1rpx = 0.5px
- 因此如果想定义一个100px宽度的view 则需要设置width为 200rpx
小程序关联微信公众号确定用户的唯一性
如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过 unionid 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 unionid 是唯一的。
换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid 是相同的
bindtap和catchtap的区别
相同点:首先他们都是作为点击事件函数,就是点击时触发。在这个作用上他们是一样的,可以不做区分
不同点:他们的不同点主要是bindtap是不会阻止冒泡事件的,catchtap是阻值冒泡的
canvas自适应屏幕画海报并保存图片
先利用wx.getSystemInfo (获取系统信息)的API获取屏幕宽度
// 在onLoad中调用
const that = this
wx.getSystemInfo({
success: function (res) {
console.log(res)
that.setData({
model: res.model,
screen_width: res.windowWidth/375,
screen_height: res.windowHeight
})
}
})
在绘制方法中将参数乘以相对单位即可实现自适应
<canvas canvas-id="PosterCanvas" style="width:{{screen_width*375+'px'}}; height:{{screen_height*1.21+'px'}}"></canvas>
drawPoster(){
let ctx = wx.createCanvasContext('PosterCanvas'),that=this.data;
console.log('手机型号' + that.model,'宽'+that.screen_width*375,'高'+ that.screen_height)
let rpx = that.screen_width
//这里的rpx是相对不同屏幕宽度的相对单位,实际的宽度测量,就是实际测出的px像素值*rpx就可以了;之后无论实在iPhone5,iPhone6,iPhone7...都可以进行自适应。
ctx.setFillStyle('#1A1A1A')
ctx.fillRect(0, 0, rpx * 375, that.screen_height * 1.21)
ctx.fillStyle = "#E8CDAA";
ctx.setFontSize(29*rpx)
ctx.font = 'normal 400 Source Han Sans CN';
ctx.fillText('Hi 朋友', 133*rpx,66*rpx)
ctx.fillText('先领礼品再买车', 84*rpx, 119*rpx)
ctx.drawImage('../../img/sell_index5.png', 26*rpx, 185*rpx, 324*rpx, 314*rpx)
ctx.drawImage('../../img/post_car2x.png', 66 * rpx, 222 * rpx, 243 * rpx, 145 * rpx)
ctx.setFontSize(16*rpx)
ctx.font = 'normal 400 Source Han Sans CN';
ctx.fillText('长按扫描获取更多优惠', 108*rpx, 545*rpx)
ctx.drawImage('../../img/code_icon2x.png', 68 * rpx, 575 * rpx, 79 * rpx, 79 * rpx)
ctx.drawImage('../../img/code2_icon2x.png', 229 * rpx, 575 * rpx, 79 * rpx, 79 * rpx)
ctx.setStrokeStyle('#666666')
ctx.setLineWidth(1*rpx)
ctx.lineTo(187*rpx,602*rpx)
ctx.lineTo(187*rpx, 630*rpx)
ctx.stroke()
ctx.fillStyle = "#fff"
ctx.setFontSize(13 * rpx)
ctx.fillText('xxx科技汽车销售公司', 119 * rpx, 663 * rpx)
ctx.fillStyle = "#666666"
ctx.fillText('朝阳区·望京xxx科技大厦', 109 * rpx, 689 * rpx)
ctx.setFillStyle('#fff')
ctx.draw()
},
保存到相册很简单,就是在画完图片之后的draw回调函数里调用canvasToTempFilePath()生产一个零时内存里的链接,然后在调用saveImageToPhotosAlbum()就可以了;其中牵扯到授权,如果你第一次拒绝了授权,你第二次进入的时候在iphone手机上是不会再次提醒你授权的,这时就需要你手动调用了;以下附上代码!
ctx.draw(true, ()=>{
// console.log('画完了')
wx.canvasToTempFilePath()({
x: 0,
y: 0,
width: rpx * 375,
height: that.screen_height * 1.21,
canvasId: 'PosterCanvas',
success: function (res) {
// console.log(res.tempFilePath);
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: (res) => {
console.log(res)
},
fail: (err) => { }
})
}
})
})
拒绝授权后再次提醒授权的代码
mpvue.saveImageToPhotosAlbum({
filePath: __path,
success(res) {
mpvue.showToast({
title: '保存成功',
icon: 'success',
duration: 800,
mask:true
});
},
fail(res) {
if (res.errMsg === "saveImageToPhotosAlbum:fail:auth denied" || res.errMsg === "saveImageToPhotosAlbum:fail auth deny" || res.errMsg === "saveImageToPhotosAlbum:fail authorize no response") {
mpvue.showModal({
title: '提示',
content: '需要您授权保存相册',
showCancel: false,
success:modalSuccess=>{
mpvue.openSetting({
success(settingdata) {
// console.log("settingdata", settingdata)
if (settingdata.authSetting['scope.writePhotosAlbum']) {
mpvue.showModal({
title: '提示',
content: '获取权限成功,再次点击图片即可保存',
showCancel: false,
})
} else {
mpvue.showModal({
title: '提示',
content: '获取权限失败,将无法保存到相册哦~',
showCancel: false,
})
}
},
fail(failData) {
console.log("failData",failData)
},
complete(finishData) {
console.log("finishData", finishData)
}
})
}
})
}
}
});
上拉刷新下拉加载
const app = getApp()
Page({
data: {
list: 30
},
onLoad(options) {
this.setData({
list: 30
})
},
onPullDownRefresh() {
setTimeout(() => {
this.setData({
list: 30
})
wx.stopPullDownRefresh()
}, 1000)
},
onReachBottom() {
this.setData({
list: this.data.list + 30
})
}
})
{
"usingComponents": {},
"enablePullDownRefresh": true,
"onReachBottomDistance": 0
}
wx:if和hidden属性有什么区别
wx:if是 组件是否渲染
hidden指的是hidden属性是否添加
开发中选择:
- 如果操作很频繁 则使用hidden
- 如果不频繁 则使用 wx:if
wx:for为什么需要绑定key
为什么要绑定key:
- 当我们希望处于同一层的VNode 进行插入 删除 新增 节点时 可以更好的进行节点的复用 就需要key属性来判断
绑定key的方式有哪些:
- 字符串: 表示 for循环array中item的某个属性(property) 该property是列表中的唯一的字符串或数字
- 保留关键字 *this 表示item本身 此时item本身是唯一的字符串或数字
事件传递参数的方法
小程序中常用传递参数的方式是通过 data- 属性来实现,可以在逻辑代码中通过 “el.currentTarget.dataset.属性名称” 获取
target和currentTarget的区别?
target和currentTarget的区别 :
·
target指触发事件的元素
·
currentTarget指的是处理事件的元素,两者作用在同一个元素上无差别,小程序中常用currentTarget
页面和组件进行数据传递
页面和组件如何进行数据传递 :
·
向组件传递数据可以通过 properties 属性,支持String、Number、Boolean、Object、Array、null等类型
·
向组件传递样式可以通过定义externalClasses属性来实现
·
组件向外传递事件可以在组件内部通过this.triggerEvent将事件派发,页面可以通过bind绑定
网络请求的封装
class Class_Request {
request(options) {
return new Promise((resolve, reject) => {
wx.request({
...options,
success: (res) => {
resolve(res.data) //网络请求成功时回调
},
fail: reject //失败时回调
})
})
}
get(options) { //get方法
return this.request({...options, method: "get"})
}
post(options) { //post方法
return this.request({...options, method: "post"})
}
}
// 导出
export const API = new Class_Request()
小程序页面跳转
小程序中实现页面跳转有两种方式 :
通过navigator组件
通过wx的API进行页面跳转,常用 :
- wx.navigateTo():保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面
- wx.redirectTo():关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面
- wx.switchTab():跳转到 abBar 页面,并关闭其他所有非 tabBar 页面
- wx.navigateBack()关闭当前页面,返回上一页面或多级页面。可通过
- getCurrentPages() 获取当前的页面栈,决定需要返回几层
- wx.reLaunch():关闭所有页面,打开到应用内的某个页面
页面跳转数据传递
// Navigate
wx.navigateTo({
url: '../pageD/pageD?name=raymond&gender=male',
})
// Redirect
wx.redirectTo({
url: '../pageD/pageD?name=raymond&gender=male',
})
// pageB.js
...
Page({
onLoad: function(option){
console.log(option.name + 'is' + option.gender)
this.setData({
option: option
})
}
})
获取修改别的页面的数据
const pages = getCurrentPages() //获取实例方法
const prevPage = pages[pages.length - 2] //具体实例
prevPage.setData({info: "my name is wzl"}) //修改数据
小程序的登录流程
1.通过wx.login()获取code
2.将这个code发送给后端,后端会返回一个token,这个token将作为你身份的唯一标识
3.将token通过wx.setStorageSync()保存在本地存储
4.用户下次进入页面时,会先通过wx.getStorageSync() 方法判断token是否有值,如果有值,则可以请求其它数据,如果没有值,则进行登录操作
小程序常见的系统API
小程序常见系统API :
·
展示弹窗API : showToast、showModal、showLoading、showActionSheet
·
分享功能 :通过onShareAppMessage()实现
·
获取设备信息 : 通过wx.getSystemInfo()实现
·
获取用户位置信息 : 通过wx.getLocation()获取
·
本地数据存储 (常用两个):
- 同步存储数据 : wx.setStorageSync()
- 同步获取数据 : wx.getStorageSync()
云数据库的增删改查操作
// 1 获取数据库
const db = wx.cloud.database();
// 2 获取到操作的集合
const studentsColl = db.collection("students");
// 添加
studentsColl
.add({
data: {
name: "wmm",
age: 18,
height: 1.88,
address: {
name: "sx",
code: "033000",
},
hobbies: ["联盟", "吃鸡"],
},
})
// 删除数据
studentsColl
.doc("6d85a2b962fefabe1a635e252c570b60")
.remove()
.then((res) => {
console.log(res);
});
// 根据条件删除
const _ = db.command;
studentsColl
.where({
age: _.gt(25),
})
.remove()
.then((res) => {
console.log(res);
});
// 修改 某一条数据
studentsColl
.doc("0a4ec1f962ff32731a5056974fac5b24")
.update({
data: {
hobbies: ["c", "t", "lq"],
age: 30,
},
})
.then((res) => {
console.log(res);
});
// 2 set 新增 将原来的字段全部替换掉
studentsColl
.doc("8f75309d62ff32721546b5852c0431e6")
.set({
data: {
age: 31,
},
})
.then((res) => {
console.log(res);
});
// update 更新多条数据
const _ = db.command;
studentsColl
.where({
age: _.gt(25),
})
.update({
data: { age: 10 },
})
.then((res) => {
console.log(res);
});
// 1 方式一 根据id查询某条数据
lolColl
.doc("b69f67c062ff0a6311e0f21f02fd1047")
.get()
.then((res) => {
console.log(res);
});
// 2 方式二 查询多条数据
lolColl
.where({
nickname: "天才辅助杨小杨",
})
.get()
.then((res) => {
console.log(res);
});
// 3 方式三 查询指令, gt/lt
const _ = db.command;
lolColl
.where({
rid: _.gte(5000000),
})
.get()
.then((res) => {
console.log(res);
});
// 4 正则表达式
lolColl
.where({
nickname: db.RegExp({
regexp: "z",
options: "i",
}),
})
.get()
.then((res) => {
console.log(res);
});
// 5 方式五 获取整个集合中的数据
lolColl.get().then((res) => {
console.log(res);
});
// 6 分页 skip(offset)/ limit
let page = 1;
lolColl
.skip(page * 5)
.limit(5)
.get()
.then((res) => {
console.log(res);
});
// 7 排序 orderBy("rid")
// 升序 asc
// 降序 desc
lolColl
.skip(page * 5)
.limit(5)
.orderBy("rid", "asc")
.get()
.then((res) => {
console.log(res);
});
// 8 过滤字段
lolColl
.field({
_id: true,
hn: true,
nickname: true,
roomName: true,
rid: true,
})
.skip(page * 5)
.limit(5)
.orderBy("rid", "desc")
.get()
.then((res) => {
console.log(res);
});
云存储的上传、下载、删除
const imageRes = await wx.chooseMedia({
type: "image",
});
const imagePath = imageRes.tempFiles[0].tempFilePath;
const timestamp = Date.now();
const openid = "open_xx";
const extension = imagePath.split(".").pop();
const filename = `${timestamp}_${openid}_${extension}`;
const uploadRes = await wx.cloud.uploadFile({
filePath: imagePath,
cloudPath: `images/${filename}`,
});
下载
const result = await wx.cloud.downloadFile({
fileID:
" cloud://cloud1-0gs04p81a1eb23de.636c-cloud1-0gs04p81a1eb23de-1313399766/images/1660901337462_open_xx_png",
});
this.setData({
tempFilePath: result.tempFilePath,
});
删除
const res = await wx.cloud.deleteFile({
fileList: [
"cloud://cloud1-0gs04p81a1eb23de.636c-cloud1-0gs04p81a1eb23de-1313399766/21.png",
],
});
组件插槽
<!--第一步:封装组件,components/music/index.wxml-->
<view>
<view>默认内容</view>
<slot></slot>
</view>
<!--第二步:引入组件,pages/index/index.wxml-->
<f-music></f-music>
<f-music>
<view>我是定制的内容</view>
</f-music>
<!--第一步:封装组件,components/music/index.wxml-->
<view>
<view>默认内容</view>
<slot name="custom1"></slot>
<slot name="custom2"></slot>
</view>
<!--第三步:引入组件,pages/index/index.wxml-->
<f-music></f-music>
<f-music>
<view slot="custom1">我是定制的内容1</view>
<view slot="custom2">我是定制的内容2</view>
</f-music>
多个组件插槽需要进行配置
// 第二步:启用插槽,components/music/index.js
Component({
// 启用插槽
options: {
multipleSlots: true
}
})
插槽默认值
在使用小程序组件插槽的时候,我们发现这个插槽是不能给默认值的。
<view>
<view class="slot">
<slot></slot>
</view>
<!-- 插槽不传递值的时候,则作为默认值显示 默认情况下 我们不让其显示 -->
<view class="default">
<view>默认内容</view>
</view>
</view>
最简单的方式当然是使用一个布尔类型的变量,通过wx:if
和wx:else
来控制是显示插槽的值,还是显示组件内部的默认值,但是除此之外我们可以使用一个empty
伪类来解决。
// 默认插槽是否显示 如果默认插槽组件内是空的,也就是没有传组件,此时<slot/>
// 标签在渲染的时候,会消失,则slot标签的父容器此时为空
.slot:empty + .default {
// 插槽是空 则显示默认插槽
display: block;
}
.right {
// 默认情况 我们认为插槽会传值 则不显示
display: none;
}
}
分包加载
https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic.html
开发者通过在 app.json subpackages
字段声明项目分包结构:
{
// 主包也可以有自己的 pages,即最外层的 pages 字段。
"pages":[
// `tabBar` 页面必须在主包内
"pages/index",
"pages/logs"
],
"subpackages": [
// 声明 `subpackages` 后,将按 `subpackages` 配置路径进行打包,`subpackages` 配置路径外的目录将被打包到主包中
{
"root": "packageA",
"pages": [
"pages/cat",
"pages/dog"
]
}, {
"root": "packageB",
"name": "pack2",
"pages": [
"pages/apple",
"pages/banana"
]
}
]
}
https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/preload.html
开发者可以通过配置,在进入小程序某个页面时,由框架自动预下载可能需要的分包,提升进入后续分包页面时的启动速度。
{
"pages": ["pages/index"],
"subpackages": [
{
"root": "important",
"pages": ["index"],
},
{
"root": "sub1",
"pages": ["index"],
},
{
"name": "hello",
"root": "path/to",
"pages": ["index"]
},
{
"root": "sub3",
"pages": ["index"]
},
{
"root": "indep",
"pages": ["index"],
"independent": true
}
],
"preloadRule": {
"pages/index": {
"network": "all",
// 进入页面后预下载分包的 root 或 name。
"packages": ["important"]
},
"sub1/index": {
"packages": ["hello", "sub3"]
},
"sub3/index": {
"packages": ["path/to"]
},
"indep/index": {
// __APP__ 表示主包。
"packages": ["__APP__"]
}
}
}