微信小程序项目结构主要有四个文件类型
主要文件
app.json
必须要有这个文件,如果没有这个文件,项目无法运行,因为微信框架把这个作为配置文件入口,整个小程序的全局配置。包括页面注册,网络设置,以及小程序的
window 背景色,配置导航条样式,配置默认标题
app.js 必须要有这个文件,没有也是会报错!但是这个文件创建一下就行
什么都不需要写以后我们可以在这个文件中监听并处理小程序的生命周期函数、声明全局变量
app.wxss 可选
微信小程序采用 JavaScript
、WXML
、WXSS
三种技术进行开发,本质就是一个单页面应用,所有的页面渲染和事件处理,都在一个页面内进行,但又可以通过微信客户端调用原生的各种接口
微信的架构,是数据驱动的架构模式,它的 UI 和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现
小程序分为两个部分 webview
和 appService
。其中 webview
主要用来展现 UI ,appService
有来处理业务逻辑、数据及接口调用。它们在两个进程中运行,通过系统层 JSBridge 实现通信,实现 UI 的渲染、事件的处理
小程序直接 this.data 的属性是不可以同步到视图的,必须调用:
this.setData({
// 这里设置
})
WXSS 和 CSS 类似,不过在 CSS 的基础上做了一些补充和修改
rpx 是响应式像素,可以根据屏幕宽度进行自适应。规定屏幕宽为 750rpx。如在 iPhone6 上,屏幕宽度为 375px,共有 750 个物理像素,则 750rpx = 375px = 750 物理像素
/** index.wxss **/
@import './base.wxss';
.container{
color: red;
}
// app.js
App({
// 全局变量
globalData: {
userInfo: null
}
})
使用的时候,直接使用 getApp() 拿到存储的信息
//pageA.js
// 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
})
}
})
需要注意的问题:
wx.navigateTo
和 wx.redirectTo
不允许跳转到 tab 所包含的页面
onLoad
只执行一次
onLoad
页面加载时触发。一个页面只会调用一次,可以在 onLoad
的参数中获取打开当前页面路径中的参数
onShow()
页面显示/切入前台时触发
onReady()
页面初次渲染完成时触发。一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互
onHide()
页面隐藏/切入后台时触发。 如 navigateTo
或底部 tab 切换到其他页面,小程序切入后台等
onUnload()
页面卸载时触发。如 redirectTo
或 navigateBack
到其他页面时
详见 生命周期回调函数
参考 这里
网络请求小程序提供了wx.request
, 仔细看一下 api
,这不就是n年前的 $.ajax
吗,好古老啊。
// 官方例子
wx.request({
url: 'test.php', //仅为示例,并非真实的接口地址
data: {
x: '' ,
y: ''
},
header: {
'content-type': 'application/json' // 默认值
},
success: function(res) {
console.log(res.data)
}
})
小程序支持ES6,那么就应该支持Promise 了,很开心~, 话不多说直接上代码吧
Promise封装
const baseUrl = 'https://api.it120.cc';
const http = ({
url = '', param = {
}, ...other } = {
}) => {
wx.showLoading({
title: '请求中,请耐心等待..'
});
let timeStart = Date.now();
return new Promise((resolve, reject) => {
wx.request({
url: getUrl(url),
data: param,
header: {
'content-type': 'application/json' // 默认值 ,另一种是 "content-type": "application/x-www-form-urlencoded"
},
...other,
complete: (res) => {
wx.hideLoading();
console.log(`耗时${
Date.now() - timeStart}`);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
} else {
reject(res)
}
}
})
})
}
const getUrl = (url) => {
if (url.indexOf('://') == -1) {
url = baseUrl + url;
}
return url
}
// get方法
const _get = (url, param = {
}) => {
return http({
url,
param
})
}
const _post = (url, param = {
}) => {
return http({
url,
param,
method: 'post'
})
}
const _put = (url, param = {
}) => {
return http({
url,
param,
method: 'put'
})
}
const _delete = (url, param = {
}) => {
return http({
url,
param,
method: 'put'
})
}
module.exports = {
baseUrl,
_get,
_post,
_put,
_delete
}
// 使用
const api = require('../../utils/api.js')
// 单个请求
api.get('list').then(res => {
console.log(res)
}).catch(e => {
console.log(e)
})
// 一个页面多个请求
Promise.all([
api.get('list'),
api.get(`detail/${
id}`)
]).then(result => {
console.log(result)
}).catch(e => {
console.log(e)
})
登陆问题
做一个应用,肯定避免不了登录操作。用户的个人信息啊,相关的收藏列表等功能都需要用户登录之后才能操作。一般我们使用token
做标识。
小程序并没有登录界面,使用的是 wx.login
。 wx.login
会获取到一个 code
,拿着该 code
去请求我们的后台会最后返回一个token
到小程序这边,保存这个值为 token
每次请求的时候带上这个值。
一般还需要把用户的信息带上比如用户微信昵称,微信头像等,这时候就需要使用 wx.getUserInfo
,这里涉及到一个用户授权的问题
带上用户信息就够了嘛? too young too simple!我们的项目不可能只有小程序,相应的微信公众平台可能还有相应的App,我们需要把账号系统打通,让用户在我们的项目中的账户是同一个。这就需要用到微信开放平台提供的 UnionID 。
登陆
//app.js
App({
onLaunch: function () {
console.log('App onLaunch');
var that = this;
// 获取商城名称
wx.request({
url: 'https://api.it120.cc/'+ that.globalData.subDomain +'/config/get-value',
data: {
key: 'mallName'
},
success: function(res) {
wx.setStorageSync('mallName', res.data.data.value);
}
})
this.login();
this.getUserInfo();
},
login : function () {
var that = this;
var token = that.globalData.token;
// 如果有token
if (token) {
// 检查token是否有效
wx.request({
url: 'https://api.it120.cc/' + that.globalData.subDomain + '/user/check-token',
data: {
token: token
},
success: function (res) {
// 如果token失效了
if (res.data.code != 0) {
that.globalData.token = null;
that.login(); // 重新登陆
}
}
})
return;
}
// 【1】调用微信自带登陆
wx.login({
success: function (res) {
// 【2】 拿到code去访问我们的后台换取其他信息
wx.request({
url: 'https://api.it120.cc/'+ that.globalData.subDomain +'/user/wxapp/login',
data: {
code: res.code
},
success: function(res) {
// 如果说这个code失效的
if (res.data.code == 10000) {
// 去注册
that.registerUser();
return;
}
// 如果返回失败了
if (res.data.code != 0) {
// 登录错误
wx.hideLoading();
// 提示无法登陆
wx.showModal({
title: '提示',
content: '无法登录,请重试',
showCancel:false
})
return;
}
// 【3】 如果成功后设置token到本地
that.globalData.token = res.data.data.token;
// 保存用户信息
wx.setStorage({
key: 'token',
data: res.data.data.token
})
}
})
}
})
},
// 注册?? [这个看需求]
registerUser: function () {
var that = this;
wx.login({
success: function (res) {
var code = res.code; // 微信登录接口返回的 code 参数,下面注册接口需要用到
wx.getUserInfo({
success: function (res) {
var iv = res.iv;
var encryptedData = res.encryptedData;
// 下面开始调用注册接口
wx.request({
url: 'https://api.it120.cc/' + that.globalData.subDomain +'/user/wxapp/register/complex',
data: {
code:code,encryptedData:encryptedData,iv:iv}, // 设置请求的 参数
success: (res) =>{
wx.hideLoading();
that.login();
}
})
}
})
}
})
},
// 获取用户信息
getUserInfo:function() {
wx.getUserInfo({
success:(data) =>{
this.globalData.userInfo = data.userInfo;
wx.setStorage({
key: 'userInfo',
data: data.userInfo
})
return this.globalData.userInfo;
}
})
},
globalData:{
userInfo:null,
subDomain:"34vu54u7vuiuvc546d",
token: null
}
})
getUserInfo: function () {
// 先调用wx.getSetting 获取用户权限设置
wx.getSetting({
success(res) {
console.log('1');
if (!res.authSetting['scope.userInfo']) {
wx.authorize({
scope: 'scope.userInfo',
success() {
// 用户已经同意小程序使用录音功能,后续调用 wx.getUserInfo接口不会弹窗询问
wx.getUserInfo({
success: (data) => {
this.globalData.userInfo = data.userInfo;
wx.setStorage({
key: 'userInfo',
data: data.userInfo
})
return this.globalData.userInfo;
}
})
}
})
} else {
console.log(2);
}
}
})
},
授权2
小程序登录流程
这里引用下官方的一张登录流程图,我就按照登录流程图来讲下我的理解。
第一步
客户端(小程序)获取当前微信登录用户的登录凭证(code)
可通过wx.login api获得。这里有地方需要注意
1.wx.login不会弹授权弹框
2.wx.login换取的code只能使用一次,如果需要新code只能重新调用wx.login接口
wx.login({
success:(res)=>{
let code= res.code
}
})
第二步
通过上一步获得的临时登录凭证传给服务器端获取openid
和session_key.
服务器端需要通过appid
、appsecret
、(这里的数据可以从小程序管理后台获得)code
(第一步获取到的code)向微信服务端发送请求获取seeeion_key
和openid
。为了安全。建议将获得的session_key
加密后再传给客户端。
第三步
客户端获得加密后的登录态后把登录态存在本地以便后面进行业务请求。由于小程序中不存在cookie
机制。所以可以把登录态存储在storage
中。
以上就是微信官方登录流程图的一个大体过程。
但是在实际应用中可能要复杂点?我们接下来看。
这里看一下微信官方的说明
通过 wx.login
接口获得的用户登录态拥有一定的时效性。
用户越久未使用小程序,用户登录态越有可能失效。
反之如果用户一直在使用小程序,则用户登录态一直保持有效。
具体时效逻辑由微信维护,对开发者透明。
开发者只需要调用 wx.checkSession
接口检测当前用户登录态是否有效。
这说明如果用户一直在使用小程序。登录态就不会过期。反之就会过期。这里可以通过wx.checkSession api
来判断登录态是否过期。
接下来上代码。来看下在应用中的登录态维护。
目前在小程序中需要拉起微信登录授权的弹框。需要在wxml
文件中调用button
组件来调用:如下
<button bindgetuserinfo="getInfo" hover-class="none" open-type="getUserInfo"></button>
这样用户点击按钮的时候会弹出授权获取用户信息的弹窗。用户点击允许我们就可以拿到数据进行登录并进行业务请求。 如果点击拒绝可以获取不需要登录可查看的数据请求,并安利用户拒绝后的结果。重新引导用户进行授权。
下面是用户非首次进入应用的一个登录态维护(首次进入通过button
来授权。所以success
回调是不会执行的。直接fail的回调。)
// 小程序启动判断用户是否授权,根据是否授权来请求不同的业务数据
wx.getSetting({
success: (res) => {
//用户已授权
if (res.authSetting['scope.userInfo']) {
// 判断登录态是否过期
wx.checkSession({
// 登录态未过期,直接进行业务请求
success: (res) => {
//业务请求代码。。。
},
// 登录态已过期 。重新调用wx.login进行登录换取code
fail: (res) => {
// 可以在这里进行重新登录后的回调
wx.login({
success: function(res) {
let code = res.code;
}
})
}
})
}
// 为授权
else {
// 执行未授权的业务代码
}
}
})
附上登录态过期的回调。
/**
* 登录失败后重新登录
*/
getToken: function(fn) {
let that = this;
let getLogin = new Promise((resolve, reject) => {
//登录获取code
wx.login({
success: function(res) {
var code = res.code;
that.globalData.code = code;
resolve([fn, code]);
},
fail: function(res) {
reject();
}
})
});
getLogin.then(([fn, code]) => {
return new Promise((resolve, reject) => {
//使用该api需要在页面通过button组件触发授权弹窗
wx.getUserInfo({
success: function(res) {
//这里的iv,encryptedData等数据是用来服务器端进行解密的。
let requestData = {
"Data": {
"IV": res.iv,
"EncryptedData": res.encryptedData,
"JsCode": code,
},
}
//发送请求
wx.request({
url: that.apiList.login.getLogin,
data: requestData,
method: "POST",
success: function(res) {
//获取到自定义登录态存入storage
if (res.data && res.data.Success) {
that.globalData.token = res.data.Data.Key;
wx.setStorageSync('LoginSessionKey', res.data.Data.Key);
resolve(fn);
} else {
reject();
}
},
fail: function() {
Hq.tipMaskNoneIcon('您的网络开小差了');
}
})
}
})
});
}).then((fn) => {
that.getCountryInfo(fn);
}, function() {
}))
},
//执行fn回调函数
getCountryInfo: function(fn) {
if (typeof fn == 'function') {
//登录成功后进行业务请求。
fn();
} else {
Hq.afterSend();
}
},
以上就是我的一些理解。有语句不通,逻辑不清晰的地方,请不吝留言赐教!
1、提高页面加载速度
2、用户行为预测
3、减少默认 data 的大小
4、组件化方案
优势
缺点
小程序支持大部分 ES6 语法
如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过 unionid
来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 unionid
是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid
是相同的
参考 这里:(https://juejin.im/post/5a781c756fb9a063606eb742)
下拉刷新和上拉加载是业务上一个很常见的需求,在微信小程序里,提供了下拉刷新的方法 onPullDownRefresh
。而实现上拉加载相对来说就比较不方便了。
下拉刷新
虽然微信的官方文档有很多坑,但下拉刷新介绍的还是很全面的。在这里稍稍带过。
config
中的 window
配置 enablePullDownRefresh
.Page
中定义 onPullDownRefresh
钩子函数。到达下拉刷新条件后,该钩子函数执行,发起请求方法。wx.stopPullDownRefresh
停止下拉刷新。config
config = {
pages: [
'pages/index'
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#ccc',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: '#000',
enablePullDownRefresh: true
}
}
page
onPullDownRefresh() {
wepy.showNavigationBarLoading()
setTimeout(()=>{
this.getData = '数据拿到了'
wepy.stopPullDownRefresh()
wepy.hideNavigationBarLoading()
this.$apply()
},3000)
}
你会发现下拉的过程有些僵硬。这实际上是没有添加背景色的原因,加上背景色后再试试。
现在感觉好多了吧。下拉刷新有现成的配置和方法,很容易实现,可上拉加载就不同了。
上拉加载
首先看一下要实现的效果,这是3g端的上拉加载。小程序要实现同样的效果。
首先功能有
这里有两个实现的方案。一个是 page
自带的下拉触底钩子事件 onReachBottom
能做的只是下拉到底部的时候通知你触底了,一个是 scroll-view
标签自带事件。现在用两个方法分别实现一下上拉加载。
上拉触底事件 onReachBottom
模板
<template>
<view class="loading"></view>
<view class="container"
@touchmove="moveFn"
@touchstart="startFn"
@touchend="endFn"
style="transform:translate3d(0,{
{childTop}}px,0)">
<repeat for="{
{list}}"
key="index"
index="index"
item="item">
<view>{
{
item }}<text>{
{
index}}</text></view>
</repeat>
</view>
</template>
钩子函数
data = {
getData: '',
top: 0,
lastTop: 0,
canDrag: false,
list: []
}
onReachBottom() {
this.canDrag = true
}
methods = {
moveFn(ev) {
let nowY = ev.changedTouches[0].clientY
nowY = nowY-this.lastTop
if(nowY > 0 )
this.canDrag = false
if( nowY<=0 && this.canDrag ) {
this.top = nowY
}
if( -this.top>= this.maxTop )
this.top = -this.maxTop
},
startFn(ev) {
this.lastTop = ev.changedTouches[0].clientY
},
endFn() {
if(this.top <= -this.maxTop) {
this.text = "去请求数据了"
setTimeout(()=>{
this.text = "请求回来了"
this.canDrag = false
this.list.push(...["数据","数据","数据"])
this.$apply()
this.top = 0;
return
},1000)
}
},
gotoTop() {
wepy.pageScrollTo({
scrollTop: 0
})
}
}
滚动容器实现上拉加载
scroll-view
: 可滚动视图区域。
它的具体用法不赘述,看官方文档就行了。这里提解决上述问题的方法即可。
bindscrolltolower
类比原生全局钩子 onReachBottom
模板
<scroll-view scroll-y
id="content"
@scroll="scroll"
@scrolltolower="lower"
scroll-top="{
{gotoTopNum}}"
lower-threshold="100"
style="transform:translate3d(0,{
{childTop}}px,0)">
<view class="sty-search"
@touchmove="moveContent"
@touchstart="startContent"
@touchend="endContent">...</view>
</scroll-view>
以上就是最终的模板,你可能在想为什么这么复杂。虽然复杂,但每个属性都是有用的,当然这其中有几个坑在等着我们。
首先节点分为滚动容器和子容器。
Q:为什么滚动容器里嵌套一个子容器,并且将拖动的三个方法绑定在它上面。
A:这是第一个坑,因为 scroll-view 容器不能绑定 touchmove
事件,那如果绑定了会怎么样呢?不会怎么样,事件钩子不会调用。(这个坑在官方文档查不出来,当时绑定了不调用,在社区找到了解决方法,就是将touchmove
事件绑定到子容器)
再来看代码
methods = {
async lower() {
this.canDrag = true
},
scroll (ev) {
this.scrollTop = ev.detail.scrollTop
if (ev.detail.deltaY > 0) {
this.canDrag = false
}
let nowSet = this.documentHeight+this.scrollTop-this.contentHeader
let num = Math.ceil(nowSet/this.listHeight) - 1
num = Math.floor(num / this.pageBean.pageSize) + 1
num = (num > this.pageBean.pageNo) ? this.pageBean.pageNo : num
if(num != this.page) {
this.page = num
this.$apply()
}
},
startContent(ev) {
this.lastTop = ev.changedTouches[0].clientY
if(!this.documentHeight){
this.documentHeight = wx.getSystemInfoSync().windowHeight
}
/* 这句是解决回到顶部的bug */
if (this.gotoTopNum || this.gotoTopNum==0) {
this.gotoTopNum = undefined }
},
moveContent (ev) {
let {
pageNo,
pageSize,
totalCount
} = this.pageBean
let nowY = ev.changedTouches[0].clientY
nowY = nowY-this.lastTop
if (this.canDrag && nowY) {
this.state = 1;
if (nowY <= -this.maxMove) {
nowY = -this.maxMove
}
if (nowY <= 0) {
this.childTop = nowY
}
}
},
async endContent(ev) {
let {
pageNo,
pageSize,
totalCount
} = this.pageBean
if (this.childTop === -this.maxMove) {
/* 状态 */
if (pageNo >= this.maxPage || pageNo * pageSize >= totalCount) {
this.state = 0
} else {
this.pageBean.pageNo++
await this.fillData()
this.childTop = 0
this.canDrag = false
this.$apply()
}
}
/* 如果没超过刷新高度则重置 */
this.childTop = 0
},
gotoTop() {
this.gotoTopNum = 0
},
}
Q: 为什么要在 touchStart
的时候 将 gotoTopNum
置为 undefined
?
A: 因为这个页面有一个回到顶部的功能,当回到顶部时,gotoTopNum
置为0,再次下翻时,虽然实际的 scrollTop
改变了,但是 gotoTopNum
还为0,再次点击回到顶部时,因为数据未改变,视图层就不会去更新。所以在 touchStart
的时候给 gotoTopNum
一个无效的值,再次点击回到顶部时,视图层也就更新了。
END…了吗…
并没有。
真机测试
实现的上拉加载在模拟器上跑的很流畅,不存在问题。可是。
如果是苹果机的话(暂时测试iphone5 和 iPhone7),存在这样一个问题,上拉或下拉回弹效果,这个效果会影响上拉的距离。
这个问题想了很久,目前不能优雅的解决。
所以就找产品经理修改了需求,去掉了上拉动画效果 所以最终的效果就变成:
总结
1.在微信小程序里操作节点是昂贵的,比在浏览器里操作还昂贵(这是通过比较上拉加载功能在3g端和微信小程序的流畅度得来的),在 1.4.0 版本发布之后,虽然给出了很多操作节点的方法,比如得到一个节点的宽高、或者通过 id 选择器得到一个节点。请尽量减少这些方法的调用频率( 函数节流 )或 缓存结果
2.动手之前先动脑!!!不然会走很多弯路…
相同点:首先他们都是作为点击事件函数,就是点击时触发。在这个作用上他们是一样的,可以不做区分
不同点:他们的不同点主要是bindtap是不会阻止冒泡事件的,catchtap
是阻值冒泡的
wx.navigateTo()
, wx.redirectTo()
, wx.switchTab()
, wx.navigateBack()
, wx.reLaunch()
的区别wx.navigateTo()
:保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar
页面wx.redirectTo()
:关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar
页面wx.switchTab()
:跳转到 abBar
页面,并关闭其他所有非 tabBar
页面wx.navigateBack()
关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages()
wx.reLaunch()
:关闭所有页面,打开到应用内的某个页面