正文
微信小程序开发,如果有后端服务,就牵扯到session认证、用户信息微信跨服务注册(小程序、公众号)。
用户信息微信跨服务注册,就是微信中只有一个用户标识,通过这一个标识,不论是在小程序、还是公众号,都知道你是你,而不会产生两个账号的问题, 这个标识就是unionid,需要保存在后端数据库。OpenID则是微信中不同入口的唯一标示,微信帮我们生成了这个应用的唯一id。为了将来打通不同应用, 最好把这两个字段都记录下来。
不过小程序并不能直接获取到openid,需要调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。 开发者服务器再调用 code2Session 接口,换取 用户唯一标识 OpenID 和 会话密钥 session_key。
看一下实际过程:
//app.js
App({
onLaunch: function () {
// 登录
wx.login({
success: res => {
console.log(res.code);
// 发送 res.code 到后台换取 openId, sessionKey, unionId
wx.request({
url: 'http://localhost/mi/getopenID.php',
data:{code: res.code},
success:(res)=>{
console.log(res.data.openid);
this.globalData.openID = res.data.openid;
}
})
}
})
}
})
后端处理:
// 声明CODE,获取小程序传过来的CODE
$code = $_GET["code"];
// 配置appid 此处填写你的appid
$appid = "";
//配置appscret 此处填写你的appsecret
$secret = "";
//api接口
$api = "https://api.weixin.qq.com/sns/jscode2session?appid={$appid}&secret={$secret}&js_code={$code}&grant_type=authorization_code";
//获取GET请求
function httpGet($url){
$curl = curl_init();
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 2);
curl_setopt($curl, CURLOPT_TIMEOUT, 500);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 2);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($curl, CURLOPT_URL, $url);
$res = curl_exec($curl);
curl_close($curl);
return $res;
}
//发送
$str = httpGet($api);
// 然后就可以看到返回的openid了,在一个json字符串中
echo $str;
wx.getSetting(Object object)
获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
// 获取用户信息
wx.getSetting({
success: res => {
if (res.authSetting['scope.userInfo']) {
// 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
wx.getUserInfo({
success: res => {
// 可以将 res 发送给后台解码出 unionId
this.globalData.userInfo = res.userInfo
// 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
// 所以此处加入 callback 以防止这种情况
if (this.userInfoReadyCallback) {
this.userInfoReadyCallback(res)
}
}
})
}
}
})
微信小程序不支持session,除了变相使用cookie实现session认证外,我们还可以用JWT实现前后端认证。
注意点
有时会碰到这个报错信息:
Cannot read property ‘globalData’ of undefined;at App onLaunch function
这是因为this具有作用域的概念,在函数的一开始就定义一个变量指向this:
var that = this;
需要使用的时候调用这个新定义的变量就可以了。
提示框
wx.showToast({
title: '刷新中',
icon: 'loading',
duration: 2000
})
wx.showToast({
title: '刷新成功',
icon: 'success',
duration: 1000
})
wx.showToast({
title: '服务异常',
icon: 'warn',
duration: 1000
})
异步处理
微信小程序的wx.request是异步请求,非阻塞,代码会继续向后执行,不等待wx.request请求返回。
如:
Page({
data: {
myData: ''
},
// loadMyData函数用于打印myData的值
loadMyData () {
console.log('获取到的数据为:' + this.data.myData)
},
// 生命周期函数onload用于监听页面加载
onload: function () {
wx.request({
url: 'https://api', // 某个api接口地址
success: res => {
console.log(res.data)
this.setData({
myData: res.data
})
console.log(this.data.myData)
}
})
// 调用之前的函数
this.loadMyData()
}
})
我们希望的是后面的代码在wx.request请求返回后再向下执行,这里就需要考虑异步处理的方式了。
方法一-嵌套
...
onload: function () {
wx.request({
url: 'https://api', // 某个api接口地址
success: res => {
console.log(res.data)
this.setData({
myData: res.data
})
console.log(this.data.myData)
// 把使用数据的函数写在回调函数success中
this.loadMyData()
}
})
}
但是如果逻辑复杂,需要多层异步操作,会出现怎么样的情况呢?
asyncFn1(function(){
//...
asyncFn2(function(){
//...
asyncFn3(function(){
//...
asyncFn4(function(){
//...
asyncFn5(function(){
//...
});
});
});
});
});
这就是恐怖的“回调地狱”(Callback Hell)。
方法二-回调
方法三-Promise
Promise是javascript中的原生函数,可见这里。
Promise这东西简单说来就是,它可以将异步的执行逻辑和结果处理分离,摒弃了一层又一层的回调嵌套,使得处理逻辑更加清晰。
现在我们就用Promise包装一下wx.request:
/**
* requestPromise用于将wx.request改写成Promise方式
* @param:{string} myUrl 接口地址
* @return: Promise实例对象
*/
const requestPromise = myUrl => {
// 返回一个Promise实例对象
return new Promise((resolve, reject) => {
wx.request({
url: myUrl,
success: res => resolve(res)
})
})
}
// 我把这个函数放在了utils.js中,这样在需要时可以直接引入
module.exports = requestPromise
现在再使用试试:
// 引用模块
const utilApi = require('../../utils/util.js')
Page({
...
// 生命周期函数onload用于监听页面加载
onLoad: function () {
utilApi.requestPromise("https://www.bilibili.com/index/ding.json")
// 使用.then处理结果
.then(res => {
console.log(res.data)
this.setData({
myData: res.data
})
console.log(this.data.myData)
this.loadMyData()
})
}
})
结果和使用回调函数一致。当有多个异步请求时,直接不断地.then(fn)去处理即可,逻辑清晰。
方法四-第三方封装
示例demo
<!--index.wxml-->
<view class="container">
<view class="userinfo">
<button wx:if="" open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 获取头像昵称 </button>
<block wx:else>
<image bindtap="bindViewTap" class="userinfo-avatar" src="" mode="cover"></image>
<text class="userinfo-nickname"></text>
</block>
</view>
<view class="usermotto">
<text class="user-motto"></text>
</view>
</view>
// index.js
//获取应用实例
const app = getApp()
Page({
data: {
motto: 'Hello World',
userInfo: {},
hasUserInfo: false,
canIUse: wx.canIUse('button.open-type.getUserInfo')
},
//事件处理函数
bindViewTap: function() {
wx.navigateTo({
url: '../logs/logs'
})
},
onLoad: function () {
if (app.globalData.userInfo) {
this.setData({
userInfo: app.globalData.userInfo,
hasUserInfo: true
})
} else if (this.data.canIUse){
// 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
// 所以此处加入 callback 以防止这种情况
app.userInfoReadyCallback = res => {
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
} else {
// 在没有 open-type=getUserInfo 版本的兼容处理
wx.getUserInfo({
success: res => {
app.globalData.userInfo = res.userInfo
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
}
},
getUserInfo: function(e) {
console.log(e)
app.globalData.userInfo = e.detail.userInfo
this.setData({
userInfo: e.detail.userInfo,
hasUserInfo: true
})
}
})
<!--logs.wxml-->
<view class="container log-list">
<block wx:for="" wx:for-item="log">
<text class="log-item">. </text>
</block>
</view>
// logs.js
const util = require('../../utils/util.js')
Page({
data: {
logs: []
},
onLoad: function () {
this.setData({
logs: (wx.getStorageSync('logs') || []).map(log => {
return util.formatTime(new Date(log))
})
})
}
})
// utils/util.js
const formatTime = date => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
}
const formatNumber = n => {
n = n.toString()
return n[1] ? n : '0' + n
}
module.exports = {
formatTime: formatTime
}
内层页面比app应用层加载的快
回调定义
首先要说明在小程序中,app.js和index.js是异步执行的,也就说明可能当你app.js中onlaunch的登录、通过code获取open_id之类的网络请求在执行的时候, index.js中的onload已经执行完了,如果你在index的onload中有需要用到openid的话,那就会无法执行调用了。
这个时候就需要通过用 请求的callback方法了,app.js的获取用户信息中举了例子:
// 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
// 所以此处加入 callback 以防止这种情况
if (this.userInfoReadyCallback) {
this.userInfoReadyCallback(res)
}
app.userInfoReadyCallback,在index.js中定义。
index.js先判断app中用户id是否存在,如果存在,说明app.js已加载完成,则不定义app.userInfoReadyCallback; 如果不存在,说明app.js还没有加载完成,则定义app.userInfoReadyCallback,等app.js执行到时再去执行userInfoReadyCallback, userInfoReadyCallback中定义的this是index页面,我们在userInfoReadyCallback中先去获取用户信息,然后再去执行index当前页的加载逻辑。
如果 this.userInfoReadyCallback 未定义,说明app.js比index.js加载的快,用户id信息已经在app.js中赋值过了; 如果 this.userInfoReadyCallback 定义了,说明index.js比app.js加载的快,用户id信息在app.js中还没有赋值,则执行userInfoReadyCallback。
代码详情:
app.js
onLaunch: function() {
wx.login({
success: res => {
// 发送 res.code 到后台换取 openId, sessionKey, unionId
console.log(res)
wx.request({
url: that.globalData.url + 'code_to_openid',
method:'post',
header: { 'Content-Type': 'application/x-www-form-urlencoded' },
data:{
code:res.code
},
success(res){
this.globalData.user_id = res.data.data.id
this.globalData.unionid = res.data.data.unionid
this.globalData.user_info = res.data.data
// callback,如果 userInfoReadyCallback 定义了就执行,否则跳过,非阻塞
if (this.userInfoReadyCallback) {
this.userInfoReadyCallback(res)
}
}
})
}
})
}
index.js
onLoad: function() {
let that = this
//查看是否有userid
if (!app.globalData.user_id) {
app.userInfoReadyCallback = res => {
//判断是否阅读过引导页
wx.request({
url: app.globalData.url + 'checkfirst',
method: 'POST',
header: { "Content-Type": "application/x-www-form-urlencoded" },
data: {
user_id: app.globalData.user_id
}, success(res) {
console.log(res)
if (res.data.resultCode == 0 || res.data.resultCode == ''){
wx.hideTabBar()
that.setData({
guide_show:true
});
that.getLists();
}
}
})
}
}
}
promise
还有种方式就是使用 promise ,我们在app.js中定义promise,然后在index.js中执行promise,成功后再执行当前页逻辑。
如:
app.js
var http = require('service/http.js')
App({
onLaunch: function() {
//调用API从本地缓存中获取数据
// var that = this;
},
getAuthKey: function () {
var that = this;
return new Promise(function (resolve, reject) {
// 调用登录接口
wx.login({
success: function (res) {
if (res.code) {
that.globalData.code = res.code;
//调用登录接口
wx.getUserInfo({
withCredentials: true,
success: function (res) {
that.globalData.UserRes = res;
that.globalData.userInfo = res.userInfo;
that.func.postReq('/api/v1/image/oauth', {
code: that.globalData.code,
signature: that.globalData.UserRes.signature,
encryptedData: that.globalData.UserRes.encryptedData,
rawData: that.globalData.UserRes.rawData,
iv: that.globalData.UserRes.iv
}, function (res) {
wx.setStorage({
key: "auth_key",
data: res.data.auth_key
})
var res = {
status: 200,
data: res.data.auth_key
}
resolve(res);
})
}
})
} else {
console.log('获取用户登录态失败!' + res.errMsg);
var res = {
status: 300,
data: '错误'
}
reject('error');
}
}
})
});
},
})
index.js
onLoad: function () {
app.getAuthKey().then(function (res) {
console.log(res);
if (res.status == 200){
var auth_key = res.data;
app.func.req('/api/v1/image/theme-list', {
page: 1,
auth_key: auth_key
}, function (res) {
var page = that.data.pageValue + 1;
that.setData({
images: res.data,
pageValue: page
});
});
}else{
console.log(res.data);
}
});
二维码
目前有 2 种类型的二维码:
- 临时二维码,是有过期时间的,最长可以设置为在二维码生成后的 30天后过期,但能够生成较多数量。 临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景
- 永久二维码,是无过期时间的,但数量较少(目前为最多10万个)。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。
可以借用EasyWeChat快速生成,参见 https://easywechat.com/5.x/basic-services/qrcode.html
实例:
<?php
namespace app\common\service;
use EasyWeChat\Factory;
use fast\Random;
use think\Env;
/**
* Class WechatService
* @package app\common\service
*/
class WechatService
{
public static $instance;
/**
* \EasyWeChat\OfficialAccount\Application|null
*/
protected $app = null;
/**
* WechatService constructor.
*/
private function __construct()
{
$config = [
'app_id' => Env::get('wx.appId'),
'secret' => Env::get('wx.secret'),
'token' => Env::get('wx.token'),
'response_type' => 'array',
];
$this->app = Factory::officialAccount($config);
}
/**
* @param array $options
* @return static
*/
public static function instance($options = [])
{
if (is_null(self::$instance)) {
self::$instance = new static($options);
}
return self::$instance;
}
/**
* 获取关注用户列表
* @param string $nextOpenId
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*/
public function getUserList($nextOpenId = '')
{
$userList = [];
try {
$userList = $this->app->user->list($nextOpenId);
} catch (\Exception $exception) {
$error = [
'msg' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine()
];
write_logs($error, 'wechat_h5', 'get_user_list');
}
return $userList;
}
/**
* 获取用户基本信息-单个/批量
* @param array $openIds
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*/
public function getUserInfo($openIds = [])
{
$userInfo = [];
try {
$userInfo = $this->app->user->select($openIds);
} catch (\Throwable $exception) {
$error = [
'msg' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine()
];
write_logs($error, 'wechat_h5', 'get_user_info');
}
return $userInfo;
}
/**
* 创建永久二维码
* 最多创建10W个二维码
*/
public function createForever($scene = '')
{
$result = $this->app->qrcode->forever($scene);
if (!key_exists('ticket', $result)) {
return '';
}
$destDir = ROOT_PATH . 'public';
$path = '/uploads/qrcode/wx/forever';
$saveDir = $destDir . $path;
if (!is_dir($saveDir)) {
mkdir($saveDir, 0755, true);
}
$ticket = $result['ticket'];
$url = $this->app->qrcode->url($ticket);
$fileName = Random::build('md5') . '.jpg';
$destPath = $saveDir . '/' . $fileName;
$content = file_get_contents($url); // 得到二进制图片内容
file_put_contents($destPath, $content); // 写入文件
return $path . '/' . $fileName;
}
/**
* 创建临时二维码
* 最多30天
*/
public function createTemporary($scene = '', $seconds = 30 * 24 * 3600)
{
$result = $this->app->qrcode->temporary($scene, $seconds);
if (!key_exists('ticket', $result)) {
return '';
}
$destDir = ROOT_PATH . 'public';
$path = '/uploads/qrcode/wx/temporary';
$saveDir = $destDir . $path;
if (!is_dir($saveDir)) {
mkdir($saveDir, 0755, true);
}
$ticket = $result['ticket'];
$url = $this->app->qrcode->url($ticket);
$fileName = Random::build('md5') . '.jpg';
$destPath = $saveDir . '/' . $fileName;
$content = file_get_contents($url); // 得到二进制图片内容
file_put_contents($destPath, $content); // 写入文件
return $path . '/' . $fileName;
}
}
FastAdmin自带的随机数生成类fast\Random
:
<?php
namespace fast;
/**
* 随机生成类
*/
class Random
{
/**
* 生成数字和字母
*
* @param int $len 长度
* @return string
*/
public static function alnum($len = 6)
{
return self::build('alnum', $len);
}
/**
* 仅生成字符
*
* @param int $len 长度
* @return string
*/
public static function alpha($len = 6)
{
return self::build('alpha', $len);
}
/**
* 生成指定长度的随机数字
*
* @param int $len 长度
* @return string
*/
public static function numeric($len = 4)
{
return self::build('numeric', $len);
}
/**
* 生成指定长度的无0随机数字
*
* @param int $len 长度
* @return string
*/
public static function nozero($len = 4)
{
return self::build('nozero', $len);
}
/**
* 能用的随机数生成
* @param string $type 类型 alpha/alnum/numeric/nozero/unique/md5/encrypt/sha1
* @param int $len 长度
* @return string
*/
public static function build($type = 'alnum', $len = 8)
{
switch ($type) {
case 'alpha':
case 'alnum':
case 'numeric':
case 'nozero':
switch ($type) {
case 'alpha':
$pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
break;
case 'alnum':
$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
break;
case 'numeric':
$pool = '0123456789';
break;
case 'nozero':
$pool = '123456789';
break;
}
return substr(str_shuffle(str_repeat($pool, ceil($len / strlen($pool)))), 0, $len);
case 'unique':
case 'md5':
return md5(uniqid(mt_rand()));
case 'encrypt':
case 'sha1':
return sha1(uniqid(mt_rand(), true));
}
}
/**
* 根据数组元素的概率获得键名
*
* @param array $ps array('p1'=>20, 'p2'=>30, 'p3'=>50);
* @param int $num 默认为1,即随机出来的数量
* @param bool $unique 默认为true,即当num>1时,随机出的数量是否唯一
* @return mixed 当num为1时返回键名,反之返回一维数组
*/
public static function lottery($ps, $num = 1, $unique = true)
{
if (!$ps) {
return $num == 1 ? '' : [];
}
if ($num >= count($ps) && $unique) {
$res = array_keys($ps);
return $num == 1 ? $res[0] : $res;
}
$max_exp = 0;
$res = [];
foreach ($ps as $key => $value) {
$value = substr($value, 0, stripos($value, ".") + 6);
$exp = strlen(strchr($value, '.')) - 1;
if ($exp > $max_exp) {
$max_exp = $exp;
}
}
$pow_exp = pow(10, $max_exp);
if ($pow_exp > 1) {
reset($ps);
foreach ($ps as $key => $value) {
$ps[$key] = $value * $pow_exp;
}
}
$pro_sum = array_sum($ps);
if ($pro_sum < 1) {
return $num == 1 ? '' : [];
}
for ($i = 0; $i < $num; $i++) {
$rand_num = mt_rand(1, $pro_sum);
reset($ps);
foreach ($ps as $key => $value) {
if ($rand_num <= $value) {
break;
} else {
$rand_num -= $value;
}
}
if ($num == 1) {
$res = $key;
break;
} else {
$res[$i] = $key;
}
if ($unique) {
$pro_sum -= $value;
unset($ps[$key]);
}
}
return $res;
}
/**
* 获取全球唯一标识
* @return string
*/
public static function uuid()
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0xffff)
);
}
}
下面两个可以看一下:
扫描普通二维码,进入微信小程序 https://blog.csdn.net/xiasohuai/article/details/124121258
小程序如何生成带参数二维码 https://cloud.tencent.com/developer/article/1173253
用PHP生成二维码可以使用类库:PHP QR Code,官网 https://phpqrcode.sourceforge.net
问题
pad横屏变大问题
微信一次版本升级后,微信小程序横屏,页面会变大,原本的rpx单位就不再适用。 微信小程序横屏后,把宽度固定为750rpx了,所以出现了这个问题。
解决办法是用 CSS3的单位 vmax,vmin。 具体使用,参见 https://www.jianshu.com/p/a655d63f8183
参考资料
wx.login 获取 用户的openid https://www.jianshu.com/p/1b35561dc322
auth.code2Session https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html#%E8%AF%B7%E6%B1%82%E5%9C%B0%E5%9D%80
小程序登录 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
UnionID 机制说明 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html
微信的OpenID究竟是什么 https://zhuanlan.zhihu.com/p/111355529
wx.getSetting(Object object)
用户信息 wx.getUserInfo(Object object) https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserInfo.html
boolean wx.canIUse(string schema) https://developers.weixin.qq.com/miniprogram/dev/api/base/wx.canIUse.html
微信小程序登录对接Django后端实现JWT方式验证登录 https://blog.csdn.net/weixin_33894640/article/details/93283473
小程序本地测试环境 https://blog.csdn.net/qq_35960620/article/details/83995765
小程序分页加载onReachBottom上拉触底加载–小程序走过的坑 https://blog.csdn.net/weixin_45938332/article/details/105968506?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.compare&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.compare
从wx.request谈谈异步处理 http://www.okeydown.com/html/2018/10-09/885.html
微信小程序异步处理 https://www.cnblogs.com/xjwy/p/7813859.html
Promise 对象 https://es6.ruanyifeng.com/#docs/promise
微信小程序-逻辑层 https://www.jianshu.com/p/56d742b6175c
App里的onLaunch,后面再执行页面Page里的onLoad执行顺序 https://www.debug8.com/javascript/t_7443.html
微信小程序-解决index页面onload比app.js onlaunch请求快 https://www.jianshu.com/p/2fce7f926427
小程序 解决App.js和index.js异步问题(Promise解决) https://blog.csdn.net/weixin_43789195/article/details/107698549
JavaScript Promise https://www.runoob.com/js/js-promise.html