微信公众号开发整理

想起来两年前刚入行的时候,接收的基本都是微信公众号项目的开发,后面重构豆腐web的时候,也单独对移动站接入了微信公众号。恰好最近又处理了一点微信公众号的东西,想起还没有对微信公众号的一些问题进行记录,因此这里稍作整理。

<!--more-->

这篇博文主要整理微信公众号开发中一些常见的问题,包括

  • 授权登录
  • 配置JSSDK
  • 单页面应用在微信公众号中的注意事项

参考

1. 微信登录

微信登录是典型的oAuth应用。关于oAuth,可以参考这篇博客:理解OAuth 2.0

跟pc上常见的第三方平台登录类似

  • 首先需要去对应的平台申请client_id和密钥client_sercet
  • 然后指定对应的回调地址,通过client_id,跳转到平台的登录界面
  • 用户完成登录后,平台会访问先前指定的回调地址,并附带对应的code
  • 通过code获得对应的access_token,然后可以根据access_token执行一些权限操作,比如访问用户基本资料啥的
  • 最后可以绑定平台账号和自己网站的用户账号,整个认证登录完成

1.1. 服务端的微信登录

将原理运用到生产中,一般用户访问对应的页面时,如果链接上没有携带对应的code参数,则会跳转到微信授权界面,用户点击运行授权之后,会携带code并跳转到指定的回调地址(一般直接在要求登录的路由上,根据是否携带code参数进行判断即可)。

Route::get("/qq", "AuthController@wxLogin"); // 路由

class AuthController extends CommonAuthController {
    public function wxLogin(Request $request)
    {
        $redirect_url = "https://xxx.com/wxLogin";

        $code = $request->input("code");

        if (!$code) {
            // 申请获取code后,对应平台回访问回调地址,并携带code
            // 内部封装的请求 https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
            AuthorizeWX::getInstance()->authorize($redirect_url);
        } else {
            // 根据code获取对应的access_token,然后根据token获取用户的信息
            $authResult = AuthorizeWX::getInstance()->getUserInfo($redirect_url, $code);

            return $this->oAuthResult($authResult);
        }
    }
}

获取到code之后,就可以根据code调用下面接口,换取网页授权的access_token,对应参数文档里面都有详细描述。

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

后续的接口都通过access_token进行,比如获取用户信息等

https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

由于access_token的获取次数限制,且存在有效期,因此一般会启用缓存来保存access_token。获取用户身份信息之后,就可以做其他的事情了,比如将用户的openid与站点的uid进行关联,识别唯一用户。

1.2. 单页面应用的登录

在前端获取code的方式与服务端基本一致,判断location.href是否存在code,然后拉起授权即可

function getUrlParams(){
   let params = query.split('&').reduce((acc, item) => {
        let [key, val] = item.split('=')
        acc[key] = val
        return acc
    }, {})
    return params
}

let parmas = getUrlParams()
if(!params.code){
  location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=${location.href}&response_type=code&scope=SCOPE&state=STATE`
}else {
  this.http.getUserInfo(params.code)
}

由于在前端请求access_token等接口存在跨域,可以考虑通过服务器接口中转一次请求,这样使用缓存也比较方便。

2. JSSDK的使用

使用JSDDK需要在公众号后台配置使用域名,然后获取对应的配置参数,然后才可以在前端进行调用。

一般的使用方式是,前端请求后台提供的配置参数,然后在页面上进行wx.config配置,最后调用对应接口

官方文档提供了一个demo,用于不同后台语言返回jssdk配置。

首先根据access_token获取ticket

https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken

然后根据接口返回的ticket,以及随机字符串、url、时间戳等生成对应的签名

 public function getSignPackage($url) {
    $jsapiTicket = $this->getJsApiTicket();
    // 注意 URL 一定要动态获取,不能 hardcode.
    $timestamp = time();
    $nonceStr = $this->createNonceStr();

    // 这里参数的顺序要按照 key 值 ASCII 码升序排序
    $string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr&timestamp=$timestamp&url=$url";

    $signature = sha1($string);

    $signPackage = array(
      "appId"     => $this->appId,
      "nonceStr"  => $nonceStr,
      "timestamp" => $timestamp,
      "url"       => $url,
      "signature" => $signature,
      "rawString" => $string
    );
    return $signPackage; 
  }

最后通过将配置传到前端,然后进行配置

这里需要注意的问题是:其中的url是需要使用jssdk的页面的链接(去除url后面的fragment),而不是接口的链接,这对于单页面应用的jssdk需要注意,一般的做法是将url通过参数的的形式进行传递

let url = encodeURIComponent(location.href.split("#")[0])
let params = {
    url
}
this.http.get(jssdkApi, {params}).then(config=>{
    wx.config({
        debug: true, 
        appId: '', // 必填,公众号的唯一标识
        timestamp: , // 必填,生成签名的时间戳
        nonceStr: '', // 必填,生成签名的随机串
        signature: '',// 必填,签名
        jsApiList: [] // 必填,需要使用的JS接口列表
    });
    wx.ready(function(){
        // 内部调用相关的jssdk接口
    });
})

如果是使用vue-router的history形式,在需要在路由改变时重新去获取对应的config配置。

3. 长按保存图片

这是一个经常遇见的需求,即生成用户自己的二维码图片,并长按分享并保存图片。

这个功能可以通过canvas绘制实现,实际上与微信开发没多大关系。

在开发中遇见的一个问题是:遇见通过canvas的toDataURL保存图片的场景,在iPhone等高清屏下,会出现生成的图片模糊的情形。这是因为devicePixelRatio的问题。 解决办法:将canvas画布绘制的大一些,但是canvas.style保持基本的容器尺寸

下面是一个项目的实际开发例子,用作参考。

let canvas = this.$refs.stage
let ctx = canvas.getContext('2d')
let self = this

function DPR() {
    if (window.devicePixelRatio && window.devicePixelRatio > 1) {
        return window.devicePixelRatio;
    }
    return 1;
}

let windowWidth = window.screen.width,
    windowHeight = window.screen.height

let imgWidth = 1083


// 放大倍数为dpr的2倍,倍数越大图片越清晰,当然图片尺寸越大
let scaleBy = DPR() * 2
canvas.width = windowWidth * scaleBy
canvas.height = px(imgWidth) * scaleBy

canvas.style.width = `${windowWidth}px`
canvas.style.height = `${px(imgWidth)}px`

function rem(size) {
    const radio = windowWidth / 750 * scaleBy
    return radio * size
}

function px(size) {
    const radio = windowWidth / 750
    return radio * size
}

// ...绘制
function drawBg() {
    let img = new Image()
    img.src = 'http://xxx.com/img/poster.png'

    return new Promise((resolve, rej) => {
        img.onload = function () {
            ctx.drawImage(img, 0, 0, rem(750), rem(1083))
            resolve(this)
        }
    })

}

function drawQrcode() {
    let img = new Image()
    img.src = 'http://xxx.com/img/qrcode.png'

    return new Promise((resolve, rej) => {
        img.onload = function () {
            // 根据设计图绘制对应布局
            ctx.drawImage(img, rem(256), rem(741), rem(233), rem(233))
            resolve(this)
        }
    })
}

drawBg().then(res => {
    return drawQrcode()
}).then(res => {
    // 绘制完成后进行缩放
    let imgData = canvas.toDataURL("image/jpeg");
    // 将canvas导出为base64编码的图片
    this.imgData = imgData
})

4. 小结

这里整理了微信开发中常见的几个问题,一般还有配置域名、关联开发者、发送模板消息等功能,直接参考文档进行就可以了。