小程序双线程模型架构的理解?
小程序分为视图层和逻辑层,视图层的相关任务全都在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:全局配置
- pages: 页面路径列表
- 用于指定小程序由哪些页面组成,每一项都对应一个页面的 路径(含文件名) 信息
- 小程序中所有的页面都是必须在pages中进行注册
- window: 全局的默认窗口展示
- 用户指定窗口如何展示, 其中还包含了很多其他的属性
- tabBar: 底部tab栏的展示
- pages: 页面路径列表
- 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/ltconst _ = 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)/ limitlet page = 1;lolColl.skip(page * 5).limit(5).get().then((res) => {console.log(res);});// 7 排序 orderBy("rid")
// 升序 asc// 降序 desclolColl.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__"]}}
}