小程序经常需要向服务器传递数据或者从服务器拉取数据,这个时候可以使用wx.request这个API,在本章节会重点讨论wx.request的使用和注意事项。
2.4.4.1 wx.request接口
如果需要从https://test.com/getinfo接口拉取用户信息,JS示例代码如下:
wx.request({
url: 'https://test.com/getinfo',
success: function (res) {
console.log(res)// 服务器回包信息
}
})
有关wx.request的详细参数解释如表2-xx所示。
参数名 | 类型 | 必填 | 默认值 | 描述 |
---|---|---|---|---|
url | String | 是 | 开发者服务器接口地址 | |
data | Object/String | 否 | 请求的参数 | |
header | Object | 否 | 设置请求的header,header中不能设置Referer,默认header['content-type'] = 'application/json' | |
method | String | 否 | GET | (需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT |
dataType | String | 否 | json | 回包的内容格式,如果设为json,会尝试对返回的数据做一次JSON解析 |
success | Function | 否 | 收到开发者服务成功返回的回调函数,其参数是一个Object。 | |
fail | Function | 否 | 接口调用失败的回调函数 | |
complete | Function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
表2-xx wx.request详细参数
2.4.4.2 服务器接口
url参数是当前发起请求的服务器接口地址。小程序宿主环境要求request发起的网络请求必须是https协议请求,因此开发者服务器必须提供HTTPS服务的接口,同时为了保证小程序不乱用任意域名的服务,wx.request请求的域名需要在小程序管理平台进行配置,如果小程序正式版使用wx.request请求未配置的域名,在控制台会有相应的报错。
一般在开发阶段时,处于开发阶段的服务器接口还没有部署到现网的域名下,经常会通过另一个域名来进行开发调试,考虑到这一点,为了方便开发者进行开发调试,开发者工具、小程序的开发版和小程序的体验版在某些情况下允许wx.request请求任意域名。
由于一直在迭代更新小程序,那么就会有一个问题:在新版小程序发布时的某段时间内,会有部分用户使用旧版本的小程序。如果接口需要支持新的特性需要修改返回的数据格式,那么接口的参数和返回字段至少向前兼容一个版本。举个例子,假设前边的https://test.com/getinfo接口返回的JSON数据为:{"username":"zhangsan","sex":"man"},在新版本中,需要把sex字段的值改成用0或者1来表示性别男或者女。为了保持接口向前兼容,不应该直接改sex字段值的类型,而是返回的JSON数据中再多定义一个字段sexNumber,这样旧版本通过这个接口拿到的数据格式依旧是能够正常工作的。
2.4.4.3 请求参数
通过wx.request这个API,有两种方法把数据传递到服务器:
(1)通过url参数
(2)通过data参数
举个例子:需要从服务器获取id为1的用户信息,同时把当前小程序的版本带给服务器,让服务器实现新旧版逻辑兼容,两种方法的示例代码如下:
// 通过url参数传递数据
wx.request({
url: 'https://test.com/getinfo?id=1&version=1.0.0',
success: function (res) {
console.log(res)// 服务器回包信息
}
})
// 通过data参数传递数据
wx.request({
url: 'https://test.com/getinfo',
data: { id: 1, version: '1.0.0' },
success: function (res) {
console.log(res)// 服务器回包信息
}
})
两种实现方式在HTTP GET请求的情况下表现几乎是一样的,需要留意的是url是有长度限制的,其最大长度是1024字节,同时url上的参数需要拼接到字符串里,参数的值还需要做一次urlEncode。当向服务器端发送的数据超过1024字节时,就要采用HTTP POST的方式,此时传递的数据就必须要使用data参数,基于这个情况,一般建议需要传递数据时,使用data参数来传递。
再来单独看看POST请求的情况,并不是所有的请求都是按照键值对key=value的形式传递到后台服务器的,有时候需要传递一些比较复杂的数据结构到后台的时候,用JSON格式就会更加合适。此时可以在wx.request的header参数设置content-type头部为application/json,小程序发起的请求的包体内容就是data参数对应的JSON字符串,示例代码如下:
// 请求的包体为 {"a":{"b":[1,2,3],"c":{"d":"test"}}}
wx.request({
url: 'https://test.com/postdata',
method: 'POST',
header: { 'content-type': 'application/json' },
data: {
a: {
b: [1, 2, 3],
c: { d: "test" }
}
},
success: function (res) {
console.log(res)// 服务器回包信息
}
})
2.4.4.4 收到回包
通过wx.request发送请求后,服务器处理请求并返回HTTP包,小程序端接收到回包后会触发success回调,同时回调会带上一个Object信息,详细参数如表2-xx所示。
参数名 | 类型 | 描述 |
---|---|---|
data | Object/String | 开发者服务器返回的数据 |
statusCode | Number | 开发者服务器返回的 HTTP 状态码 |
header | Object | 开发者服务器返回的 HTTP Response Header |
表2-xx wx.request的success返回参数
尤其注意,只要成功收到服务器返回,无论HTTP状态码是多少都会进入success回调。因此开发者自己通过对回包的响应码进行判断后再执行后续的业务逻辑。
success回调的参数data字段类型是根据header['content-type']决定的,默认header['content-type']是'application/json',在触发success回调前,小程序宿主环境会对data字段的值做JSON解析,如果解析成功,那么data字段的值会被设置成解析后的Object对象,其他情况data字段都是String类型,其值为HTTP回包包体。
2.4.4.5 一般使用技巧
2.4.4.5.1 设置超时时间
小程序发出一个HTTPS网络请求,有时候网络会存在一些异常或者服务器存在问题,在经过一段时间后仍然没有收到网络回包,这时把这一段等待的最长时间称为请求超时时间。小程序request默认超时时间是60秒,一般来说不需要这么长的一个等待时间才收到回包,可能在等待3秒后还没收到回包就需要给用户一个明确的服务不可用的提示。在小程序项目根目录里边的app.json可以指定request的超时时间,示例代码如下:
{
"networkTimeout": {
"request": 3000
}
}
2.4.4.5.2 请求前后的状态处理
大部分场景可能是这样的,用户点击一个按钮,界面出现“加载中...”的Loading界面,然后发送一个request请求到后台,后台返回成功直接进入下一个业务逻辑处理,后台返回失败或者网络异常等情况则显示一个“系统错误”的Toast,同时一开始的Loading界面会消失。下面给出一个常见的wx.request的示例代码:
var hasClick = false;
Page({
tap: function () {
if (hasClick) {
return
}
hasClick = true
wx.showLoading()
wx.request({
url: 'https://test.com/getinfo',
method: 'POST',
header: { 'content-type': 'application/json' },
data: {},
success: function (res) {
if (res.statusCode === 200) {
console.log(res.data)// 服务器回包内容
}
},
fail: function (res) {
wx.showToast({ title: '系统错误' })
},
complete: function (res) {
wx.hideLoading()
hasClick = false
}
})
}
})
为了防止用户极快的速度触发两次tap回调,还加了一个hasClick的“锁”,在开始请求前检查是否已经发起过请求,如果没有才发起这次请求,等到请求返回之后再把锁的状态恢复回去。
2.4.4.6 排查异常的方法
在使用wx.request接口会经常遇到无法发起请求或者服务器无法收到请求的情况,下面罗列一些排查这类问题的一般方法:
(1)检查手机网络状态以及wifi连接点是否工作正常。
(2)检查小程序是否为开发版或者体验版,因为开发版和体验版的小程序不会校验域名。
(3)检查对应请求的HTTPS证书是否有效,同时TLS的版本必须支持1.2及以上版本,可以在开发者工具的console面板输入showRequestInfo()查看相关信息。
(4)域名不要使用IP地址或者localhost,并且不能带端口号,同时域名需要经过ICP备案。
(5)检查app.json配置的超时时间配置是否太短,超时时间太短会导致还没收到回包就触发fail回调。
(6)检查发出去的请求是否302到其他域名的接口,这种302的情况会被视为请求别的域名接口而导致无法发起请求。
2.4.4.7 wx.request结合Java使用
创建名称为httpServer的Spring Boot项目。
实体类Userinfo示例代码如下:
package com.ghy.www.httpserver.entity; public class Userinfo { private Integer id; private String username; private String password; public Userinfo() { } public Userinfo(int id, String username, String password) { this.id = id; this.username = username; this.password = password; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
控制层代码如下:
package com.ghy.www.httpserver.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.ghy.www.httpserver.entity.Userinfo; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.io.PrintWriter; @RestController public class TestController { //无参数_get/post @RequestMapping(value = "noParam") public String noParam(HttpServletRequest request) { String methodName = request.getMethod(); System.out.println("noParam run " + methodName); return "我是返回值_noParam_" + methodName; } //有参数_参数来自get/post提交 @RequestMapping(value = "hasParam_urlParam1") public String hasParam_urlParam1(int id, String username, String password, HttpServletRequest request) { System.out.println("id=" + id); System.out.println("username=" + username); System.out.println("password=" + password); String methodName = request.getMethod(); System.out.println("hasParam_urlParam1 run " + methodName); return "我是返回值_hasParam_urlParam1_" + methodName; } //有参数_参数来自get/post提交 @RequestMapping(value = "hasParam_urlParam2") public String hasParam_urlParam2(Userinfo userinfo, HttpServletRequest request) { System.out.println("id=" + userinfo.getId()); System.out.println("username=" + userinfo.getUsername()); System.out.println("password=" + userinfo.getPassword()); String methodName = request.getMethod(); System.out.println("hasParam_urlParam2 run " + methodName); return "我是返回值_hasParam_urlParam2_" + methodName; } //有参数_参数来自post的payload提交 @RequestMapping(value = "hasParam_urlParam3") public String hasParam_urlParam3(@RequestBody Userinfo userinfo, HttpServletRequest request) { System.out.println("id=" + userinfo.getId()); System.out.println("username=" + userinfo.getUsername()); System.out.println("password=" + userinfo.getPassword()); String methodName = request.getMethod(); System.out.println("hasParam_urlParam3 run " + methodName); return "我是返回值_hasParam_urlParam3_" + methodName; } //返回值类型为json对象 @RequestMapping(value = "returnJSONObject", produces = "application/json") @ResponseBody public Userinfo returnJSONObject(HttpServletRequest request) { Userinfo returnUserinfo = new Userinfo(); returnUserinfo.setId(6); returnUserinfo.setUsername("返回的账号6"); returnUserinfo.setPassword("返回的密码6"); String methodName = request.getMethod(); System.out.println("returnJSONObject run " + methodName); return returnUserinfo; } //返回值类型为json字符串 @RequestMapping(value = "returnJSONString1", produces = "text/html;charset=utf-8") @ResponseBody public String returnJSONString1(HttpServletRequest request) throws JsonProcessingException { Userinfo returnUserinfo = new Userinfo(); returnUserinfo.setId(7); returnUserinfo.setUsername("返回的账号7"); returnUserinfo.setPassword("返回的密码7"); String methodName = request.getMethod(); System.out.println("returnJSONString1 run " + methodName); String jsonString = new ObjectMapper().writeValueAsString(returnUserinfo); return jsonString; } //返回值类型为json字符串 @RequestMapping(value = "returnJSONString2") public void returnJSONString2(HttpServletRequest request, HttpServletResponse response) throws IOException { String methodName = request.getMethod(); System.out.println("returnJSONString2 run " + methodName); Userinfo userinfo = new Userinfo(); userinfo.setId(8); userinfo.setUsername("返回的账号8"); userinfo.setPassword("返回的密码8"); String returnJSONString = new ObjectMapper().writeValueAsString(userinfo); response.setCharacterEncoding("utf-8"); response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.print(returnJSONString); out.flush(); out.close(); } //测试loading效果 @RequestMapping(value = "loadingTest") public String loadingTest() throws InterruptedException { System.out.println("loadingTest run begin " + System.currentTimeMillis()); Thread.sleep(5000); System.out.println("loadingTest run end " + System.currentTimeMillis()); return "loadingTest"; } //测试异步和同步 @RequestMapping(value = "test1") public String test1() throws InterruptedException { System.out.println("test1 begin " + System.currentTimeMillis()); Thread.sleep(5000); System.out.println("test1 end " + System.currentTimeMillis()); return "test1ok"; } //测试异步和同步 @RequestMapping(value = "test2") public String test2() throws InterruptedException { System.out.println("test2 begin " + System.currentTimeMillis()); Thread.sleep(5000); System.out.println("test2 end " + System.currentTimeMillis()); return "我是返回值_test1"; } }
创建名称为wxRequest的小程序项目。
WXML示例代码如下:
<view><button bind:tap="noParam_get" style="width: 100%;">无参数_get</button></view>
<view><button bind:tap="noParam_post" style="width: 100%;">无参数_post</button></view>
<view>--------------------------------------------------------</view>
<view><button bind:tap="hasParam1_get" style="width: 100%;">有参数_get_多参数</button></view>
<view><button bind:tap="hasParam1_post" style="width: 100%;">有参数_post_多参数</button></view>
<view>--------------------------------------------------------</view>
<view><button bind:tap="hasParam2_get" style="width: 100%;">有参数_get_单实体</button></view>
<view><button bind:tap="hasParam2_post" style="width: 100%;">有参数_post_formdata单实体</button></view>
<view>--------------------------------------------------------</view>
<view><button bind:tap="hasParam3_post" style="width: 100%;">有参数_post_payload单实体</button></view>
<view>--------------------------------------------------------</view>
<view><button bind:tap="returnJSONObject" style="width: 100%;">返回jsonObject</button></view>
<view><button bind:tap="returnJSONString1" style="width: 100%;">返回jsonString1</button></view>
<view><button bind:tap="returnJSONString2" style="width: 100%;">返回jsonString2</button></view>
<view>--------------------------------------------------------</view>
<view><button bind:tap="success_complete" style="width: 100%;">success_complete</button></view>
<view><button bind:tap="fail_complete" style="width: 100%;">fail_complete</button></view>
<view>--------------------------------------------------------</view>
<view><button bind:tap="loadingTest" style="width: 100%;">loadingTest</button></view>
<view>--------------------------------------------------------</view>
<view><button bind:tap="asyncTest" style="width: 100%;">测试异步</button></view>
<view><button bind:tap="syncTest" style="width: 100%;">测试同步</button></view>
JS示例代码如下:
var hasClick = false;
Page({
noParam_get() {
wx.request({
url: 'http://localhost:8080/noParam',
method: "GET",
success: function (res) {
console.log("res内容:");
console.log(res);
}
});
},
noParam_post() {
wx.request({
url: 'http://localhost:8080/noParam',
method: "POST",
success: function (res) {
console.log("res内容:");
console.log(res);
}
})
},
hasParam1_get() {
wx.request({
url: 'http://localhost:8080/hasParam_urlParam1',
method: "GET",
data: { id: 101, username: '101username', password: '101password' },
success: function (res) {
console.log("res内容:");
console.log(res);
}
});
},
hasParam1_post() {
wx.request({
url: 'http://localhost:8080/hasParam_urlParam1',
data: { id: 102, username: '102username', password: '102password' },
header: { "Content-Type": "application/x-www-form-urlencoded" },
method: "POST",
success: function (res) {
console.log("res内容:");
console.log(res);
}
})
},
hasParam2_get() {
wx.request({
url: 'http://localhost:8080/hasParam_urlParam2',
method: "GET",
data: { id: 103, username: '103username', password: '103password' },
success: function (res) {
console.log("res内容:");
console.log(res);
}
});
},
hasParam2_post() {
wx.request({
url: 'http://localhost:8080/hasParam_urlParam2',
data: { id: 104, username: '104username', password: '104password' },
header: { "Content-Type": "application/x-www-form-urlencoded" },
method: "POST",
success: function (res) {
console.log("res内容:");
console.log(res);
}
})
},
hasParam3_post() {
wx.request({
url: 'http://localhost:8080/hasParam_urlParam3',
data: { id: 105, username: '105username', password: '105password' },
method: "POST",
header: { 'content-type': 'application/json' },
success: function (res) {
console.log("res内容:");
console.log(res);
}
})
},
returnJSONObject() {
wx.request({
url: 'http://localhost:8080/returnJSONObject',
method: "GET",
success: function (res) {
console.log("res内容:");
console.log(res);
}
})
},
returnJSONString1() {
wx.request({
url: 'http://localhost:8080/returnJSONString1',
method: "GET",
dataType: "text",
success: function (res) {
console.log("res内容:");
console.log(res);
}
})
},
returnJSONString2() {
wx.request({
url: 'http://localhost:8080/returnJSONString2',
method: "GET",
dataType: "text",
success: function (res) {
console.log("res内容:");
console.log(res);
}
})
},
success_complete() {
wx.request({
url: 'http://localhost:8080/noParam',
method: "GET",
success: function (res) {
console.log("success res内容:");
console.log(res);
},
fail: function (res) {
console.log("fail res内容:");
console.log(res);
},
complete: function (res) {
console.log("complete res内容:");
console.log(res);
}
})
},
fail_complete() {
wx.request({
url: 'http://www.noURLnoURLnoURLnoURLnoURLnoURLnoURLnoURL.com',
method: "GET",
dataType: "text",
success: function (res) {
console.log("success res内容:");
console.log(res);
},
fail: function (res) {
console.log("fail res内容:");
console.log(res);
},
complete: function (res) {
console.log("complete res内容:");
console.log(res);
}
})
},
loadingTest() {
if (hasClick) {
console.log("当前请求还未完成,不能再次发起请求!");
return;
}
hasClick = true;
wx.showLoading();
wx.request({
url: 'http://localhost:8080/loadingTest',
method: "GET",
success: function (res) {
if (res.statusCode === 200) {
console.log("success res内容:");
console.log(res);
}
},
fail: function (res) {
console.log("fail res内容:");
console.log(res);
wx.showToast({ title: '系统错误' })
},
complete: function (res) {
console.log("complete res内容:");
console.log(res);
wx.hideLoading();
hasClick = false;
}
});
},
asyncTest() {
wx.request({
url: 'http://localhost:8080/test1',
method: "GET",
success: function (res) {
console.log("test1 res内容:" + res);
}
});
wx.request({
url: 'http://localhost:8080/test2',
method: "GET",
success: function (res) {
console.log("test2 res内容:" + res);
}
});
},
async syncTest() {
var res = await this.test1_request();
if (res.data == "test1ok") {
await this.test2_request();
}
},
test1_request() {
return new Promise((resolve, reject) => {
// 异步请求
wx.request({
url: "http://localhost:8080/test1",
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
})
},
test2_request() {
return new Promise((resolve, reject) => {
// 异步请求
wx.request({
url: "http://localhost:8080/test2",
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
})
}
})
已有的互联网产品在接入小程序时会面临一些和登录态相关的问题,比如怎么获取微信的登录态,怎么把微信帐号和自己的帐号进行打通。所以在本章节会介绍一下如何把微信登录应用到自己的小程序中。
先来看看微信登录的七个步骤的整个过程,如图2-xx所示。
图2-xx 微信登录的整个过程
下面依次分解一下图中的七个步骤,其中,第1步到第4步分别用一个小节来讲述,第5步到第7步都和SessionId相关,放在1节一起讨论。
2.4.5.1 获取微信登录凭证code:第1步
首先说到登录,可能很正常地想到一个做法:通过wx.login直接拿到微信用户的id编号,再把这个id传到自己的后台,从而知道是哪个微信用户在使用自己的服务。而真正微信登录的流程中并不是通过wx.login直接获取微信用户的id,那直接获取微信用户id的做法有什么问题呢?假设现在有个接口,通过wx.request请求https://test.com/getUserInfo?id=1拉取到微信用户id为1的业务侧的个人信息,那么黑客就可以通过遍历所有的id,把整个业务侧的个人信息数据全部拉走,如果还有其他接口也是依赖这样的方式去实现的话,那么黑客就可以伪装成任意身份来操作任意账户下的数据,想想这样做会给业务带来多大的安全风险。
为了避免这样的风险,wx.login是生成一个带有时效性的凭证,就像是一个会过期的临时身份证一样,在wx.login调用时,会先在微信后台生成一张临时的身份证,其有效时间仅为5分钟。然后把这个临时身份证返回给小程序方,这个临时的身份证把它称为微信登录凭证code。如果5分钟之内小程序的后台不拿着这个临时身份证来微信后台服务器换取微信用户id的话,那么这个身份证就会被作废,需要再调用wx.login重新生成登录凭证。
由于这个临时身份证5分钟之后会过期,如果黑客要冒充一个用户的话,那么他就必须在5分钟之内穷举所有的身份证id,然后去开发者服务器换取真实的用户身份。显然,黑客这么做要付出非常大的成本才能获取到一个用户信息,同时,开发者服务器也可以通过一些技术手段检测到5分钟之内频繁从某个ip发送过来的登录请求,从而拒绝掉这些请求。
2.4.5.2 发送code到开发者服务器:第2步
在wx.login的success回调中拿到微信登录凭证code,紧接着会通过wx.request把这个登录凭证code传到开发者服务器,目的是为了后续可以换取微信用户身份id。如果当前微信用户还没有绑定当前小程序业务的用户身份,那么在这次请求中应该顺便把用户输入的帐号密码一起传到开发者服务器后台,然后开发者服务器后台就可以校验账号密码之后再和微信用户id进行绑定,小程序端的示例代码如下所示。
Page({
tapLogin: function () {
wx.login({
success: function (res) {
if (res.code) {
wx.request({
url: 'https://test.com/login',
data: {
username: 'zhangsan', // 用户输入的账号
password: 'pwd123456', // 用户输入的密码
code: res.code
},
success: function (res) {
// 登录成功
if (res.statusCode === 200) {
console.log(res.data.sessionId)// 服务器回包内容
}
}
})
} else {
console.log('获取用户登录态失败!' + res.errMsg)
}
}
});
}
})
2.4.5.3 到微信服务器换取微信用户身份openid:第3步
开发者后台拿到了前边wx.login()所生成的微信登录凭证code,此时就可以拿这个code到微信服务器换取微信用户身份。微信服务器为了确保拿code过来换取身份信息的人就是刚刚对应的小程序开发者,到微信服务器的请求要同时带上AppId和AppSecret,这两个信息在小程序管理平台的开发设置界面里可以看到,由此可以看出,AppId和AppSecret是微信鉴别开发者身份的重要信息,AppId是公开信息,泄露AppId不会带来安全风险,但是AppSecret是开发者的隐私数据,不应该泄露。如果发现泄露需要到小程序管理平台进行重置AppSecret,而code在成功换取一次信息之后也会立即失效,即便凭证code生成时间还没过期。
开发者服务器和微信服务器通信也是通过HTTPS协议,微信服务器提供的接口地址是:
https://api.weixin.qq.com/sns/jscode2session?appid=<AppId>&secret=<AppSecret>&js_code=<code>&grant_type=authorization_code
URL的query部分的参数中的<AppId>,<AppSecret>,<code>就是前文所提到的三个信息,请求参数合法的话,接口会返回表2-xx中的字段。
字段 | 描述 |
---|---|
openid | 微信用户的唯一标识 |
session_key | 会话密钥 |
unionid | 用户在微信开放平台的唯一标识符。本字段在满足一定条件的情况下才返回。 |
暂时只需要关注前两个字段即可,openid就是前文一直提到的微信用户id,可以用这个id来区分不同的微信用户。session_key则是微信服务器给开发者服务器颁发的身份凭证,开发者可以用session_key请求微信服务器的其他接口来获取一些其他的信息,由此可以看到,session_key不应该泄露或者下发到小程序前端。
可能会好奇,为什么要设计session_key?如果每次都通过小程序前端wx.login()生成微信登录凭证code去微信服务器请求信息,步骤太多造成整体耗时比较严重,因此对于一个比较可信的服务端,给开发者服务器颁发一个时效性更长的会话密钥就显得很有必要了。session_key也存在过期时间,因为篇幅关系不在此展开,可以参考小程序的官方文档关于session_key的相关介绍。
2.4.5.4 绑定微信用户身份openid和业务用户身份:第4步
在前面章节中提到过:如果业务侧用户还没有绑定微信侧的身份时,会让用户填写业务侧的用户名密码,这两个值会和微信登录凭证一起请求开发者服务器的登录接口,此时开发者后台通过校验用户名密码就拿到了业务侧的用户身份id,通过code到微信服务器获取微信侧的用户身份openid。微信会建议开发者把这两个信息的对应关系存起来,把这个对应关系称之为“绑定”。
有了这个绑定信息,小程序在下次需要用户登录的时候就可以不需要输入账号密码,因为通过wx.login()获取到code之后,可以拿到用户的微信身份openid,通过绑定信息就可以查出业务侧的用户身份id,这样静默授权的登录方式显得非常便捷。
2.4.5.5 业务登录凭证SessionId:第5步到第7步
微信侧返回的session_key是开发者服务器和微信服务器的会话密钥,同样道理,开发者服务器和开发者的小程序应该也有会话密钥,在本教程中就把它称之为SessionId。用户登录成功之后,开发者服务器需要生成会话密钥SessionId,在服务端保持SessionId对应的用户身份信息,同时把SessionId返回给小程序。小程序后续发起的请求中携带上SessionId,开发者服务器就可以通过服务器端的Session信息查询到当前登录用户的身份,这样就不需要每次都重新获取code,省去了很多通信消耗。在后面章节还会提到如何利用本地数据缓存的能力把SessionId存储起来,以便在它还没过期的时候能重复利用,以提高通信的性能。
本地数据缓存是小程序存储在当前设备上的数据,本地数据缓存有非常多的用途,可以利用本地数据缓存来存储用户在小程序上产生的操作,在用户关闭小程序重新打开时可以恢复之前的状态。还可以利用本地缓存存储一些服务端非实时的数据以提高小程序获取数据的速度,在特定的场景下可以提高页面的渲染速度,减少用户的等待时间。
2.4.6.1 读写本地数据缓存
小程序提供了读写本地数据缓存的接口,通过wx.getStorage/wx.getStorageSync读取本地缓存,通过wx.setStorage/wx.setStorageSync写数据到缓存,其中Sync后缀的接口表示是同步接口,执行完毕后会立马返回。
使用wx.getStorage/wx.getStorageSync读取本地数据缓存的示例代码如下:
wx.getStorage({
key: 'key1',
success: function (res) {
// 异步接口在success回调才能拿到返回值
var value1 = res.data
},
fail: function () {
console.log('读取key1发生错误')
}
})
try {
// 同步接口立即返回值
var value2 = wx.getStorageSync('key2')
} catch (e) {
console.log('读取key2发生错误')
}
wx.getStorage/wx.getStorageSync详细参数说明如表2-xx所示。
参数名 | 类型 | 必填 | 描述 |
---|---|---|---|
key | String | 是 | 本地缓存中指定的 key |
success | Function | 否 | 异步接口调用成功的回调函数,回调参数格式: {data: key对应的内容} |
fail | Function | 否 | 异步接口调用失败的回调函数 |
complete | Function | 否 | 异步接口调用结束的回调函数(调用成功、失败都会执行) |
表2-xx 参数解释
使用wx.setStorage/wx.setStorageSync写入本地数据缓存的示例代码如下:
// 异步接口在success/fail回调才知道写入成功与否
wx.setStorage({
key: "key",
data: "value1"
success: function () {
console.log('写入value1成功')
},
fail: function () {
console.log('写入value1发生错误')
}
})
try {
// 同步接口立即写入
wx.setStorageSync('key', 'value2')
console.log('写入value2成功')
} catch (e) {
console.log('写入value2发生错误')
}
wx.setStorage/wx.setStorageSync详细参数说明如表2-xx所示。
参数名 | 类型 | 必填 | 描述 |
---|---|---|---|
key | String | 是 | 本地缓存中指定的 key |
data | Object/String | 是 | 需要存储的内容 |
success | Function | 否 | 异步接口调用成功的回调函数 |
fail | Function | 否 | 异步接口调用失败的回调函数 |
complete | Function | 否 | 异步接口调用结束的回调函数(调用成功、失败都会执行) |
表2-xx 参数解释
创建名称为localStorage的小程序项目。
WXML示例代码如下:
<button bind:tap="setStorage">setStorage</button>
<button bind:tap="setStorageSync">setStorageSync</button>
<button bind:tap="getStorage">getStorage</button>
<button bind:tap="getStorageSync">getStorageSync</button>
JS示例代码如下:
Page({
setStorage() {
// 异步接口在success/fail回调里才知道写入成功或者失败
wx.setStorage({
key: "key",
data: "我是异步值_value1",
success: function () {
console.log('写入value1成功');
},
fail: function () {
console.log('写入value1发生错误');
},
complete: function () {
console.log('执行完成');
}
})
},
setStorageSync() {
try {
// 同步接口立即写入
wx.setStorageSync('key', '我是同步值_value2');
console.log('写入value2成功')
} catch (e) {
console.log('写入value2发生错误')
}
},
getStorage() {
wx.getStorage({
key: 'key',
success: function (res) {
// 异步接口在success回调里才能拿到返回值
var value = res.data
console.log("异步取值:" + value);
},
fail: function () {
console.log('读取key发生错误')
},
complete: function () {
console.log('执行完成');
}
})
},
getStorageSync() {
try {
// 同步接口立即返回值
var value = wx.getStorageSync('key');
console.log("同步取值:" + value);
} catch (e) {
console.log('读取key发生错误')
}
}
})
2.4.6.2 缓存限制和隔离
小程序宿主环境会管理不同小程序的数据缓存,不同小程序的本地缓存空间是分开的,每个小程序的缓存空间上限为10MB,如果当前缓存已经达到10MB,再通过wx.setStorage写入缓存会触发fail回调。
小程序的本地缓存不仅仅通过小程序这个维度来隔离空间,考虑到同一个设备可以登录不同的微信用户,宿主环境还对不同用户的缓存进行了隔离,避免用户间的数据隐私泄露。
由于本地缓存是存放在当前设备里,用户更换设备之后无法从另一个设备读取到当前设备的数据,因此用户的关键信息不建议只存在本地缓存,应该把数据存放到服务器端进行持久化存储。
2.4.6.3 利用本地缓存提前渲染界面
讨论一个需求:需要实现一个购物商城的小程序,首页是展示一堆商品的列表。一般的实现方法就是在页面onLoad回调之后通过wx.request向服务器发起一个请求去拉取首页商品列表的数据,等待wx.request的success回调之后把数据通过setData渲染到界面上,示例代码如下:
Page({
onLoad: function () {
var that = this
wx.request({
url: 'https://test.com/getproductlist',
success: function (res) {
if (res.statusCode === 200) {
that.setData({
list: res.data.list
})
}
}
})
}
})
设想一下当用户退出小程序再进来时,界面仍然会有白屏现象,因为需要等待拉取商品列表的请求回来才能渲染商品列表。当然还可以再做一些体验上的优化,例如在发送请求之前可能会在界面上显示一个Loading提示用户正在加载中,但是并没有解决这个延迟渲染的现象,这个时候可以利用本地缓存来提前渲染界面。
在拉取商品列表之后把列表存储在本地缓存里,在onLoad发起请求之前,先检查是否有缓存过列表,如果有的话直接渲染界面,然后等到wx.request的success回调之后再覆盖本地缓存重新渲染新的列表,示例代码如下:
Page({
onLoad: function () {
var that = this
var list = wx.getStorageSync("list")
if (list) { // 本地如果有缓存列表,提前渲染
that.setData({
list: list
})
}
wx.request({
url: 'https://test.com/getproductlist',
success: function (res) {
if (res.statusCode === 200) {
list = res.data.list
that.setData({ // 再次渲染列表
list: list
})
wx.setStorageSync("list", list) // 覆盖缓存数据
}
}
})
}
})
这种做法可以让用户体验到小程序加载的非常快,但是还要留意这个做法的缺点,如果小程序对渲染的数据实时性要求非常高的话,用户看到一个旧数据的界面会非常困惑。因此一般在对数据实时性/一致性要求不高的页面采用这个方法来做提前渲染,用以优化小程序的使用体验。
2.4.6.4 缓存用户登录态SessionId
前面章节说到处理用户登录态的一般方法通常是用户在没有主动退出登录之前,用户的登录态会一直保持一段时间,就无需用户频繁地输入账号密码。如果把SessionId记录在JS中的某个内存变量,当用户关闭小程序再进来小程序时,之前内存的SessionId已经丢失,此时就需要利用本地缓存的能力来持久化存储SessionId。
示例代码如下:
//page.js
var app = getApp()
Page({
onLoad: function () {
// 调用wx.login获取微信登录凭证
wx.login({
success: function (res) {
// 拿到微信登录凭证之后去自己服务器换取自己的登录凭证
wx.request({
url: 'https://test.com/login',
data: { code: res.code },
success: function (res) {
var data = res.data
// 把 SessionId 和过期时间放在内存中的全局对象和本地缓存里边
app.globalData.sessionId = data.sessionId
wx.setStorageSync('SESSIONID', data.sessionId)
// 假设登录态保持1天
var expiredTime = +new Date() + 1 * 24 * 60 * 60 * 1000
app.globalData.expiredTime = expiredTime
wx.setStorageSync('EXPIREDTIME', expiredTime)
}
})
}
})
}
})
在重新打开小程序的时候,把上一次存储的SessionId内容取出来,恢复到内存。
示例代码如下:
//app.js
App({
onLaunch: function (options) {
var sessionId = wx.getStorageSync('SESSIONID')
var expiredTime = wx.getStorageSync('EXPIREDTIME')
var now = +new Date()
if (now - expiredTime <= 1 * 24 * 60 * 60 * 1000) {
this.globalData.sessionId = sessionId
this.globalData.expiredTime = expiredTime
}
},
globalData: {
sessionId: null,
expiredTime: 0
}
})
2.4.6.5 微信登陆结合Java使用
创建名称为wxloginClient的小程序项目。
创建名称为wxloginServer的JavaWeb项目。
具体代码请参考这2个项目。
PC的程序和手机的程序有很大体验不一样的地方,尤其是在信息输入这个体验上差别非常大。PC端可以有键盘、鼠标等等外设来辅助用户输入很多复杂的信息,而用户要在一个小小的手机屏幕上输入复杂信息的效率是很低的。小程序的宿主环境提供了非常多的操作设备能力来帮助用户在特定场景下实现高效的输入,例如:扫码、操控蓝牙等等能力。当然也有很多设备能力不是为了解决输入低效问题而设计的,它们更多的是解决用户侧一些体验问题,例如:获取设备网络状态;调整屏幕亮度等等,在本章节会介绍其中几种常见的操作设备能力的场景。
2.4.7.1 利用微信扫码能力
为了让用户减少输入,可以把复杂的信息编码成一个二维码,利用宿主环境提供的wx.scanCode这个API调用微信的扫一扫功能,用户扫码之后,wx.scanCode的success回调会收到这个二维码所对应的字符串信息。
例如餐厅点餐的小程序,给餐厅中的每个餐桌编号1-100号,把这个数字编码到二维码中,扫码获得编号之后,就可以知道是哪一桌点的菜,大大提高点餐体验和效率。
利用wx.scanCode实现微信扫码的示例代码如下:
//page.js
Page({
//点击“扫码订餐”的按钮,触发tapScan回调
tapScan: function () {
//调用wx.login获取微信登录凭证
wx.scanCode({
success: function (res) {
varnum = res.result//获取到的num就是餐桌的编号
}
})
}
})
还有很多场景可以结合微信扫码能力做到很好的体验,例如通过扫描商品上的二维码做一个商品展示的小程序;通过扫描共享单车上的二维码去开启单车。可以多思考如何利用这个扫码能力去替代一些繁琐的输入操作,让小程序变得更加便捷。
创建名称为scanCode的小程序项目。
WXML示例代码如下:
<button bind:tap="beginScan">开始扫码</button>
<text>showCode:{{showCode}}</text>
JS示例代码如下:
Page({
data: {
showCode: ""
},
beginScan() {
wx.scanCode({
success: (res) => {
const varnum = res.result;
this.setData({ "showCode": varnum + " test text" });
}
})
}
})
2.4.7.2 获取网络状态
手机连接到互联网有几种方式:Wifi、2G、3G、4G,包括很快到来的5G,每种方式的上传速度和下载速度差异很大,它们的计费方式的差异也导致用户在使用互联网服务的时候有不同的使用习惯。
Wifi相对于其他几种网络连接方式,其速度会更快。Wifi一般都是免费供用户使用,通过移动数据网络是需要根据使用流量进行计费的。考虑到这样一个场景,小程序需要下载一些文档,然后通过小程序的能力去预览这个文档,这些文档可能文件体积比较大,对于某些用户来说,他们并不想耗费太多的数据流量去预览文档。考虑到这样的情况,可以通过小程序提供的获取网络状态的能力,做一些更友好的体验提示。
利用wx.getNetworkType获取网络状态的示例代码如下:
//page.js
Page({
//点击“预览文档”的按钮,触发tap回调
tap: function () {
wx.getNetworkType({
success: function (res) {
//networkType字段的有效值:
//wifi/2g/3g/4g/unknown(Android下不常见的网络类型)/none(无网络)
if (res.networkType == 'wifi') {
//从网络上下载pdf文档
wx.downloadFile({
url: 'http://test.com/somefile.pdf',
success: function (res) {
//下载成功之后进行预览文档
wx.openDocument({
filePath: res.tempFilePath
})
}
})
} else {
wx.showToast({ title: '当前为非Wifi环境' })
}
}
})
}
})
创建名称为getNetworkType的小程序项目。
WXML示例代码如下:
<button bind:tap="getNetworkTypeMethod">获得网络类型</button>
<text>NetworkType:{{networkType}}</text>
JS示例代码如下:
Page({
data: {
networkType: ""
},
getNetworkTypeMethod() {
wx.getNetworkType({
success: (res) => {
console.log(res.networkType);
if (res.networkType == 'wifi') {
this.setData({ "networkType": "当前为Wifi环境" });
} else {
this.setData({ "networkType": "当前为非Wifi环境 " + res.networkType });
}
}
})
}
})
某些情况下,手机连接到网络的方式会动态变化,例如手机设备连接到一个信号不稳定的Wifi热点,导致手机会经常从Wifi切换到移动数据网络。小程序宿主环境也提供了一个可以动态监听网络状态变化的接口wx.onNetworkStatusChange,让开发者可以及时根据网络状况去调整小程序的体验,wx.onNetworkStatusChange这个接口的使用场景留给读者来思考。
创建名称为onNetworkStatusChange的小程序项目。
WXML示例代码如下:
<text>NetworkType:{{networkType}}</text>
JS示例代码如下:
Page({
data: {
networkType: ""
},
onLoad() {
wx.onNetworkStatusChange((result) => {
this.setData({ "networkType": result.networkType + " " + result.isConnected });
});
}
})
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。