??上周二開(kāi)晨會(huì)分配任務(wù)的時(shí)候悠瞬,分配到了一個(gè)微信掃碼關(guān)注公眾號(hào)的需求。剛開(kāi)始以為只要截個(gè)公眾號(hào)二維碼的圖涯捻,然后按照UI出的設(shè)計(jì)稿把二維碼放到指定位置浅妆,再加上一波加邊框加陰影的操作提交就完事了。所以當(dāng)部門(mén)大佬問(wèn)多久能做完的時(shí)候障癌,我毫不猶豫地說(shuō):小Case啦凌外,兩天妥妥的!
??回到座位上本想著時(shí)間還早涛浙,先刷會(huì)微博康辑。刷著刷著看到廣州東站的宜家要搬遷的消息,活動(dòng)大減價(jià)全場(chǎng)5折起=瘟痢疮薇!忍不住點(diǎn)進(jìn)去宜家的網(wǎng)上商城,看了兩圈好想剁手買(mǎi)買(mǎi)買(mǎi)我注。這時(shí)按咒,部門(mén)大佬站了起來(lái),好像正往我這邊看但骨。算了算了励七,先關(guān)注宜家公眾號(hào)干活去吧,拿出手機(jī)準(zhǔn)備掃碼的時(shí)候整個(gè)人都懵了...
??誒嗽冒?誒誒呀伙?!早上的需求好像是要實(shí)現(xiàn)掃碼關(guān)注公眾號(hào)并登陸的添坊,但瀏覽器怎么會(huì)知道我掃碼了,而且掃的還是登陸用的二維碼...這怎么跟想象的不一樣箫锤,我還打包票說(shuō)兩天內(nèi)做完贬蛙,真的完了完了雨女。
??當(dāng)然,兩天后這掃碼的功能還是經(jīng)過(guò)測(cè)試按時(shí)提交阳准,并在線上穩(wěn)定運(yùn)行了一周氛堕。如何才能快速實(shí)現(xiàn),并盡量少踩坑野蝇,我想讼稚,有些思路和和代碼寫(xiě)下來(lái),以后可能會(huì)用得著绕沈。
??項(xiàng)目主要以Nodejs進(jìn)行開(kāi)發(fā)锐想,優(yōu)先選用Koa2 + ioredis等一些比較輕量級(jí)的模塊實(shí)現(xiàn),配合Alpine Linux制作Docker Image乍狐,最后得到的一個(gè)開(kāi)箱即用的Docker鏡像也僅僅只有33M赠摇。
??瀏覽項(xiàng)目的完整代碼可以點(diǎn)擊這里github,如果對(duì)你有幫助歡迎Star浅蚪。
先整理一下需求:
- 登錄入口實(shí)現(xiàn)掃描二維碼關(guān)注公眾號(hào)并登錄網(wǎng)站藕帜。已關(guān)注的直接跳轉(zhuǎn)登陸,未關(guān)注的等待用戶關(guān)注再跳轉(zhuǎn)惜傲;
- 新的公眾平臺(tái)掃碼登錄機(jī)制代替原有的微信開(kāi)放平臺(tái)的掃碼登錄洽故;
- 掃碼關(guān)注后需要根據(jù)情況返回不同的提示(歡迎)信息。
??但目前已上線的網(wǎng)站同時(shí)提供微信掃碼和手機(jī)/郵箱注冊(cè)登錄盗誊,新需求實(shí)際是想讓更多的用戶關(guān)注公眾號(hào)收津。完全按照需求上做的話就會(huì)變成強(qiáng)制用戶必須關(guān)注公眾號(hào),否則無(wú)法完成登錄浊伙∽睬铮考慮到市場(chǎng)上有不少同類(lèi)型產(chǎn)品,這種強(qiáng)制的行為可能會(huì)導(dǎo)致用戶反感嚣鄙,從而選擇其他產(chǎn)品吻贿。
經(jīng)討論需求改為:
- 保留現(xiàn)有的微信掃碼和手機(jī)/郵箱注冊(cè)登錄,完成注冊(cè)/登錄流程后哑子,新增一個(gè)掃碼關(guān)注公眾號(hào)的頁(yè)面舅列;
- 用戶掃碼關(guān)注,關(guān)注后利用unionid機(jī)制綁定賬戶卧蜓,讓手機(jī)/郵箱注冊(cè)的用戶以后可以直接微信掃碼登錄帐要;
- 點(diǎn)擊關(guān)注后,網(wǎng)站自動(dòng)跳轉(zhuǎn)進(jìn)入控制臺(tái)弥奸,或點(diǎn)擊暫不關(guān)注直接跳轉(zhuǎn)榨惠;
- 掃碼關(guān)注后需要根據(jù)情況返回不同的提示(歡迎)信息。
實(shí)現(xiàn)思路和步驟:
- 實(shí)現(xiàn)一個(gè)與微信公眾號(hào)平臺(tái)交互的API,接收并處理公眾號(hào)推送的事件(關(guān)注赠橙、掃碼和文字消息等)耽装;
- 實(shí)現(xiàn)一個(gè)生成二維碼的API供瀏覽器調(diào)用,API可通過(guò)參數(shù)聲明需要返回的格式期揪;
- 請(qǐng)求公眾平臺(tái) →【生成帶參數(shù)的二維碼】接口生成帶有場(chǎng)景值的二維碼掉奄,生成成功后記錄到數(shù)據(jù)庫(kù)并返回;
- 瀏覽器獲取二維碼信息后輪詢(xún)二維碼的掃描狀態(tài)凤薛,掃描成功后自動(dòng)跳轉(zhuǎn)姓建;
- 用戶掃碼后,公眾平臺(tái)會(huì)向1實(shí)現(xiàn)的API推送事件缤苫,如果是關(guān)注就獲取用戶信息速兔,然后記錄到數(shù)據(jù)庫(kù)。
第一步榨馁,搭建Koa的環(huán)境并接入微信公眾平臺(tái)
??提供的源碼里包含刪減過(guò)的 Koa2和 koa-router的代碼憨栽,也可以使用原版的代碼。建議使用Nodejs10以上版本翼虫,特別是Nodejs12屑柔,換了新的HTTP解析器(llhttp)性能直接提高了一倍。
安裝依賴(lài)
package.json
"dependencies": {
"debug": "^4.1.1",
"got": "^9.6.0",
"ioredis": "^4.10.0",
"mime-types": "^2.1.24",
"negotiator": "^0.6.2",
"xml2js": "^0.4.19",
"ylru": "^1.2.1"
}
如果是直接用官方的 Koa珍剑,mime-types掸宛,negotiator,ylru都不用安裝
目錄結(jié)構(gòu)
Koa APP的代碼結(jié)構(gòu)跟官方的栗子差不多招拙,就直接看吧
app.js
const http = require('http')
const Koa = require('./vendor/koa2/application')
const XMLParser = require('./middlewares/XMLParser')
const router = require('./routes/wechat')
const app = new Koa()
?
app.use(XMLParser) // 解析xml的中間件唧瘾,用于預(yù)處理微信公眾號(hào)推送的事件
?
app.use(router.routes())
app.use(router.allowedMethods())
?
http.createServer(app.callback()).listen(3000)
middlewares/XMLParser.js
const parseXML = require('xml2js').parseString
const debug = require('debug')('xml-parse')
?
const parse = (req, options = {}) => {
return new Promise((resolve, reject) => {
let xml = ''
req.on('data', chunk => { xml += chunk.toString('utf-8') })
.on('error', reject)
.on('end', () => parseXML(xml, options, (err, res) => {
if (err) reject(err)
resolve(res)
}))
})
}
?
module.exports = async (ctx, next) => {
// 這里先嘗試直接匹配,匹配失敗再到mime庫(kù)里查詢(xún)
if (ctx.request.type === 'text/xml' || ctx.is('xml')) {
try {
ctx.request.body = await parse(ctx.req)
} catch (e) {
debug(e.message)
}
}
await next()
}
routes/wechat.js
const Router = require('../vendor/koa-router')
const wechatController = require('../controllers/wechat')
?
?
const router = new Router({
prefix: '/wechat'
})
?
// 測(cè)試號(hào)配置接口信息時(shí)需要校驗(yàn)别凤,但傳輸?shù)臄?shù)據(jù)跟推送消息一樣饰序,所以放在同一個(gè)controller里處理
// conntroller的完整path是/wechat/event,這個(gè)后面配置測(cè)試號(hào)URL的時(shí)候會(huì)用到
router.get('/', ctx => ctx.body = 'hello wechat')
.get('/event', wechatController)
.post('/event', wechatController)
?
module.exports = router
先新建一個(gè)配置文件规哪,與app.js同目錄
config.js
WXMP的信息暫時(shí)留空求豫,到配置微信公眾平臺(tái)的時(shí)候再填寫(xiě)
const CACHE = {
host: 'localhost',
port: 6379
}
?
// WeChat Media Platform
const WXMP = {
appID: '',
appSecret: '',
token: ''
}
?
module.exports = {
CACHE,
WXMP
}
/controllers/wechat.js
const { WXMP } = require('../config');
const { SHA1 } = require('../utils/mUtils')
?
module.exports = async (ctx, next) => {
const token = WXMP.token
const { signature, nonce, timestamp, echostr } = ctx.query
?
/**
* https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319
* 1)將token、timestamp诉稍、nonce三個(gè)參數(shù)進(jìn)行字典序排序
* 2)將三個(gè)參數(shù)字符串拼接成一個(gè)字符串進(jìn)行sha1加密
* 3)開(kāi)發(fā)者獲得加密后的字符串可與signature對(duì)比蝠嘉,標(biāo)識(shí)該請(qǐng)求來(lái)源于微信
*/
const str = [token, timestamp, nonce].sort().join('')
?
const signVerified = SHA1(str) === signature
?
if (!signVerified) {
ctx.status = 404 // 可以不設(shè)為404,koa默認(rèn)的狀態(tài)值就是404
return
}
?
if (ctx.method === 'GET') ctx.body = echostr
else if (ctx.method === 'POST') {
// 實(shí)現(xiàn)思路里的第一步
// 推送的消息會(huì)以POST的方式進(jìn)到這里杯巨,暫時(shí)用不著蚤告,先放著
}
}
??來(lái)到這里,先測(cè)試一下Web服務(wù)是否能正常跑起來(lái)服爷。這里使用Postman直接發(fā)請(qǐng)求杜恰,也可以用瀏覽器訪問(wèn)http://localhost:3000/wechat/获诈。
接下來(lái)配置一下微信公眾平臺(tái)。
??線上的環(huán)境經(jīng)不起折騰箫章,還是用公眾號(hào)測(cè)試號(hào)進(jìn)行調(diào)試吧烙荷,如果你是剛開(kāi)始接觸微信公眾號(hào)開(kāi)發(fā)镜会,推薦使用測(cè)試號(hào)檬寂。
掃碼登陸后會(huì)看到這樣一個(gè)界面:
??appID和appsecret是系統(tǒng)生成的,只需要填寫(xiě)URL和Token戳表。把a(bǔ)ppID和appsecret貼到之前創(chuàng)建的config.js中桶至,token自己隨便輸入,保證兩個(gè)token一致即可匾旭。配置URL之前镣屹,我們需要一個(gè)域名和一個(gè)內(nèi)網(wǎng)穿透的環(huán)境。舊版本的微信web開(kāi)發(fā)工具提供一個(gè)類(lèi)似的方式价涝,讓微信服務(wù)器可以向我們?cè)趦?nèi)網(wǎng)的機(jī)器推送消息女蜈,新版沒(méi)有這功能我們就自己百度一個(gè)吧。
??粗略比較了一下色瘩,發(fā)現(xiàn)續(xù)斷內(nèi)網(wǎng)穿透很大方伪窖,只要9.9就有兩條8M永久使用的隧道(只比KFC會(huì)員價(jià)的原味雞貴4毛,尊貴的VIP吃雞怎么還這么貴(╯‵□′)╯︵┻━┻)居兆。注冊(cè)交了9.9入會(huì)費(fèi)覆山,裝上客戶端后進(jìn)行簡(jiǎn)單配置就可以了,體驗(yàn)挺棒的泥栖。設(shè)置過(guò)程中發(fā)現(xiàn)他家支持好多系統(tǒng)簇宽,像群暉、OpenWRT那些都有吧享,還有樹(shù)莓派魏割,看來(lái)吃塵多年的B+可以拿出來(lái)發(fā)揮余熱了。好像還有一些有趣的功能钢颂,可惜體驗(yàn)隧道不支持钞它。不過(guò)只要9.9,還要啥自行車(chē)呢甸陌,果斷開(kāi)干吧滓走!
??點(diǎn)擊保存稍等一會(huì)會(huì)得到一個(gè)外網(wǎng)訪問(wèn)地址,類(lèi)似http://sd8xxxxxxxxs.gzhttp.cn
??接著把外網(wǎng)訪問(wèn)地址+之前定義的path(http://sd8xxxxxxxxs.gzhttp.cn/wechat/event)填寫(xiě)到測(cè)試號(hào)接口配置的URL中译株,然后點(diǎn)擊提交吉执。這時(shí)我們已經(jīng)成功接入到微信公眾平臺(tái)了。
第二步牲尺,實(shí)現(xiàn)一個(gè)生成二維碼的API并完善與微信公眾號(hào)平臺(tái)交互的API
??開(kāi)始第二步之前卵酪,先來(lái)了解一下創(chuàng)建二維碼及用戶掃碼后公眾號(hào)給web服務(wù)推送消息的流程:
??首先在utils/wechat/文件夾中新建一個(gè)helper.js幌蚊,負(fù)責(zé)提供公眾號(hào)配置(用于下面創(chuàng)建wechat對(duì)象)和get/set access_token的兩個(gè)方法。
utils/wechat/helper.js
const { WXMP } = require('../../config')
const { redis } = require('../dbHelper')
?
const config = {
MP: {
appID: WXMP.appID,
appSecret: WXMP.appSecret,
token: WXMP.token,
getAccessToken: async () => {
let token = await redis.get('access_token')
return token
},
saveAccessToken: async (data = {}) => {
await redis.set('access_token', data.access_token ,'EX', data.expires_in)
}
}
}
?
module.exports = {
...config
}
??在utils/wechat/文件夾中新建一個(gè)wxmp.js溃卡,定義一個(gè)Wechat類(lèi)
...
const api = {
accessToken: 'token?grant_type=client_credential',
user: {
info: 'user/info?',
},
QRCodeTicket: 'qrcode/create?',
QRCode: 'showqrcode?'
}
?
class Wechat {
constructor (opts) {
// 這里的opts傳入的是上面定義的config
...
this.fetchAccessToken(true)
}
// 獲取access_token
async fetchAccessToken (init = false) {
let token = await this.getAccessToken()
?
if (!token) {
token = await this.updateAccessToken()
await this.saveAccessToken(token)
token = token.access_token
}
return token
}
?
async updateAccessToken () {
const url = api.accessToken + '&appid=' + this.appID + '&secret=' + this.appSecret
return await got(url)
}
?
// 提供一個(gè)統(tǒng)一操作的入口溢豆,第一個(gè)參數(shù)傳入操作函數(shù)名就可以拿到對(duì)應(yīng)的配置
async handle (operation, ...args) {
const token = await this.fetchAccessToken()
if (!token) return null
?
const options = this[operation](token, ...args)
let res = await wxGot(options)
return res
}
?
// 獲取用戶信息
getUserInfo (token, openID, lang) {
const url = `${api.user.info}access_token=${token}&openid=${openID}&lang=${lang || 'zh_CN'}`
?
return { url: url }
}
?
// 申請(qǐng)二維碼Ticket
getQRCodeTicket (token, sceneStr, timeout) {
return {
url: `${api.QRCodeTicket}access_token=${token}`,
method: 'post',
body: {
"expire_seconds": timeout || 60,
"action_name": "QR_STR_SCENE", // 臨時(shí)二維碼
"action_info": {
"scene": {
"scene_str": sceneStr
}
}
}
}
}
?
module.exports = Wechat
??我們繼續(xù)回到剛才的/routes/wechat.js,增加 “+” 標(biāo)識(shí)的代碼(新增獲取二維碼的路由)
const Router = require('../vendor/koa-router')
const wechatController = require('../controllers/wechat')
+ const { createQRCodeMB } = require('../controllers/wechat')
?
const router = new Router({
prefix: '/wechat'
})
?
// 測(cè)試號(hào)配置接口信息時(shí)需要校驗(yàn)瘸羡,但傳輸?shù)臄?shù)據(jù)跟推送消息一樣漩仙,所以放在同一個(gè)controller里處理
// conntroller的完整path是/wechat/event,這個(gè)后面配置測(cè)試號(hào)URL的時(shí)候會(huì)用到
router.get('/', ctx => ctx.body = 'hello wechat')
.get('/event', wechatController)
.post('/event', wechatController)
+ .get('/qrcode', createQRCodeMB)
?
module.exports = router
然后打開(kāi)/controllers/wechat.js犹赖,公眾號(hào)推送事件類(lèi)型可以參考這里
const { WXMP } = require('../config');
const { SHA1, fmtNormalXML, streamToBuffer, createTimestamp } = require('../utils/mUtils')
const { redis } = require('../utils/dbHelper')
const Wechat = require('../utils/wechat/wxmp')
const MPConfig = require('../utils/wechat/helper').MP
const got = require('got')
const qr = require('../vendor/qr')
const fs = require('fs')
const pathResolve = require('path').resolve
?
const MP = new Wechat(MPConfig)
?
module.exports = async (ctx, next) => {
...
if (ctx.method === 'GET') ctx.body = echostr
else if (ctx.method === 'POST') {
// 把數(shù)組形態(tài)的xmlObject轉(zhuǎn)換可讀性更高的結(jié)構(gòu)
const message = fmtNormalXML(ctx.request.body.xml)
? const msgType = message.MsgType
const msgEvent = message.Event
const userID = message.FromUserName
let eventKey = message.EventKey
let body = null
?
if (msgType === 'event') {
switch (msgEvent) {
// 關(guān)注&取關(guān)
case 'subscribe':
case 'unsubscribe':
body = await subscribe(message)
break
// 關(guān)注后掃碼
case 'SCAN':
body = '掃碼成功'
break
}
if (!!eventKey) {
// 有場(chǎng)景值(掃了我們生成的二維碼)
let user = await MP.handle('getUserInfo', userID)
let userInfo = `${user.nickname}(${user.sex ? '男' : '女'}, ${user.province}${user.city})`
if (eventKey.slice(0, 8) === 'qrscene_') {
// 掃碼并關(guān)注
// 關(guān)注就創(chuàng)建帳號(hào)的話可以在這里把用戶信息寫(xiě)入數(shù)據(jù)庫(kù)完成用戶注冊(cè)
eventKey = eventKey.slice(8)
console.log(userInfo + '掃碼并關(guān)注了公眾號(hào)')
} else {
// 已關(guān)注
console.log(userInfo + '掃碼進(jìn)入了公眾號(hào)')
}
?
// 更新掃碼記錄队他,供瀏覽器掃碼狀態(tài)輪詢(xún)
await redis.pipeline()
.hset(eventKey, 'unionID', user.unionid || '') // 僅unionid機(jī)制下有效
.hset(eventKey, 'openID', user.openid)
.exec()
}
}
}
}
?
async function subscribe (message) {
let userID = message.FromUserName
if (message.Event === 'subscribe') {
return '感謝您的關(guān)注'
} else {
// 用戶取消關(guān)注后我們不能再通過(guò)微信的接口拿到用戶信息,
// 如果要記錄用戶信息峻村,需要從我們自己的用戶記錄里獲取該信息麸折。
// 所以建議創(chuàng)建用戶時(shí)除了unionid,最好把openid也保存起來(lái)粘昨。
console.log(userID + '取關(guān)了')
}
}
?
const templetData = fs.readFileSync(pathResolve(__dirname, '../vendor/qrcode-templet.html'))
?
// 創(chuàng)建二維碼
async function createQRCodeMB (ctx, next) {
let userID = ctx.query.userID
let type = +ctx.query.type
let errno = 0
let responseDate = {}
let id = createTimestamp()
?
let res = await MP.handle('getQRCodeTicket', id)
?
if (res === null) errno = 1
else {
responseDate = {
expiresIn: res.expire_seconds,
id
}
?
let imgBuffer = await streamToBuffer(qr.image(res.url))
let imgSrc = imgBuffer.toString('base64')
?
if (type === 1) {
// 返回圖片
ctx.body = `<img src="data:image/png;base64,${imgSrc}" />`
} else if (type === 2) {
// 返回一個(gè)自帶查詢(xún)狀態(tài)和跳轉(zhuǎn)的網(wǎng)頁(yè)
let templetValue = `
<script>var imgSrc='${imgSrc}',id='${responseDate.id}',
timeout=${responseDate.expiresIn},width=100,height=100</script>`
?
ctx.body = templetValue + templetData.toString('utf-8')
} else {
// 返回圖片內(nèi)容
responseDate.imgSrc = imgSrc
}
}
?
if (!ctx.body) {
ctx.body = {
errno,
...responseDate
}
}
}
?
module.exports.createQRCodeMB = createQRCodeMB
??到這里應(yīng)該是可以接收到公眾號(hào)推送的掃碼事件和生成二維碼垢啼。
??保存后我們先測(cè)試一下,首先不帶參數(shù)訪問(wèn)http://localhost:3000/wechat/qrcode
??接著嘗試獲取二維碼圖片(使用參數(shù)type=1)并使用微信掃描二維碼:
??首次掃描二維碼會(huì)提示關(guān)注张肾,點(diǎn)擊關(guān)注后數(shù)據(jù)庫(kù)就會(huì)更新芭析,控制臺(tái)也會(huì)打印出類(lèi)似 “XXX掃碼并關(guān)注了公眾號(hào)“ 的日志。但這時(shí)候公眾號(hào)里應(yīng)該會(huì)提示 ”該公眾號(hào)提供的服務(wù)出現(xiàn)故障捌浩,請(qǐng)稍后再試“ 的提示放刨,因?yàn)槌绦虿](méi)有把提示信息正確得返回。下一步我們需要格式化返回的信息(即ctx.body的內(nèi)容)尸饺。
新增一個(gè)生成模板的文件/utils/tmpl.js
格式化給公眾號(hào)返回的消息进统,這里只簡(jiǎn)單使用util.format來(lái)格式化消息。
const util = require('util')
?
const msgTemplet = `
<xml>
<ToUserName><![CDATA[%s]]></ToUserName>
<FromUserName><![CDATA[%s]]></FromUserName>
<CreateTime>%d</CreateTime>
<MsgType><![CDATA[%s]]></MsgType>
$msgBody$
</xml>
`
?
const textMsg = `<Content><![CDATA[%s]]></Content>`
const imageMsg = `<Image><MediaId><![CDATA[%s]]></MediaId></Image>`
?
module.exports = (ctx, originMsg) => {
let type = (ctx && ctx.type) || 'text'
let msgTmpl = util.format(msgTemplet,
originMsg.FromUserName,
originMsg.ToUserName,
Math.floor(new Date().getTime() / 1000),
type
)
?
let body = ''
?
switch (type) {
case 'text':
body = util.format(textMsg, ctx)
break
case 'image':
break
default:
body = util.format(textMsg, '操作無(wú)效')
}
?
return msgTmpl.replace(/\$msgBody\$/, body)
}
接著我們?cè)赾ontrollers/wechat.js增加一下 ”+“ 標(biāo)記的代碼
const { WXMP } = require('../config');
const { SHA1, fmtNormalXML, streamToBuffer, createTimestamp } = require('../utils/mUtils')
+ const { tmpl } = require('../utils/wechat')
const { redis } = require('../utils/dbHelper')
const Wechat = require('../utils/wechat/wxmp')
...
module.exports = async (ctx, next) => {
const token = WXMP.token
const { signature, nonce, timestamp, echostr } = ctx.query
?
const str = [token, timestamp, nonce].sort().join('')
...
// 更新掃碼記錄浪听,供瀏覽器掃碼狀態(tài)輪詢(xún)
await redis.pipeline()
.hset(eventKey, 'unionID', user.unionid || '') // 僅unionid機(jī)制下有效
.hset(eventKey, 'openID', user.openid)
.exec()
}
}
?
+ ctx.type = 'application/xml'
+ ctx.body = tmpl(body || ctx.body, message)
}
}
?
async function subscribe (message) {
let userID = message.FromUserName
if (message.Event === 'subscribe') {
return '感謝您的關(guān)注'
} else {
console.log(userID + '取關(guān)了')
}
}
??保存后再獲取一次二維碼并掃描螟碎,微信上就能正確顯示提示信息了:
第三步,瀏覽器增加掃碼狀態(tài)輪詢(xún)
??這塊跟業(yè)務(wù)代碼關(guān)系比較密切迹栓,所以不做詳細(xì)介紹掉分。共通點(diǎn)就是通過(guò)二維碼返回id獲取unionid(openid)的記錄,然后按需處理克伊,最后以cookies或其他方式更新登錄狀態(tài)酥郭。
輪詢(xún)的代碼可以參考vendor/qrcode-templet.html
async function waitToSubscribe(id, timeout) {
let countdown = Math.ceil(timeout / 3);
return new Promise((resolve, reject) => {
const loop = async function() {
let res = await ky.default.get("/wechat/check", {
searchParams: { id }
}).json();
if (!res) return;
if (res.errno === 0) resolve("subscribe");
else if (res.errno === 2) reject("timeout");
else if (countdown-- > 0) self.QRCodeTimer = setTimeout(loop, 3000);
};
loop();
});
};
(async () => {
try {
await waitToSubscribe(id, timeout);
window.location.href = "/wechat/";
} catch (e) {
history.go(0);
}
})();
我們可以嘗試獲取集成好獲取狀態(tài)的二維碼網(wǎng)頁(yè)(使用參數(shù)type=2,實(shí)際使用時(shí)可以用iframe嵌套):
總結(jié):
??到這里愿吹,我們已經(jīng)實(shí)現(xiàn)了:
1. 與微信公眾號(hào)平臺(tái)交互的API不从,能夠接收并處理公眾號(hào)推送的事件;
2. 生成二維碼的API犁跪,并能分別以三種常用方式返回二維碼椿息;
3. 掃描二維碼后歹袁,微信上能正常顯示服務(wù)返回的提示信息,并成功記錄在數(shù)據(jù)庫(kù)中寝优;
4. 當(dāng)瀏覽器輪詢(xún)二維碼的掃描狀態(tài)并獲取到掃描結(jié)果后条舔,自動(dòng)跳轉(zhuǎn)。
??以上幾乎包含了公眾號(hào)開(kāi)發(fā)的完整流程乏矾,其他的功能可以參照公眾號(hào)開(kāi)發(fā)文檔上的說(shuō)明按需增加孟抗。這里有一點(diǎn)需要注意的,文中提到的unionid機(jī)制需要以公司身份申請(qǐng)正式的公眾號(hào)和微信開(kāi)放平臺(tái)妻熊,并在開(kāi)放平臺(tái)上完成公眾號(hào)綁定夸浅。同一個(gè)用戶在已綁定公眾號(hào)仑最、小程序扔役、網(wǎng)站應(yīng)用等程序里會(huì)使用同一個(gè)unionid來(lái)確定用戶的唯一性。
??像公眾號(hào)網(wǎng)頁(yè)授權(quán)警医、開(kāi)放平臺(tái)的網(wǎng)站應(yīng)用授權(quán)(類(lèi)似京東的掃碼登錄)和小程序的開(kāi)發(fā)亿胸,等有空的時(shí)候再更新。碼了這么多字预皇,差點(diǎn)忘了要去宜家掃貨侈玄,廣告上說(shuō)促銷(xiāo)商品數(shù)量有限,萬(wàn)一賣(mài)完了豈不是錯(cuò)過(guò)了幾個(gè)億::>_<::吟温,周末要找個(gè)時(shí)間過(guò)去看看才行序仙。
最后附上Dockerfile 和源碼地址
??預(yù)先拷貝文件到/build目錄,便于生成更小的Docker Image
cp -rf vendor docker/build/vendor
cp -rf utils docker/build/utils
cp -rf routes docker/build/routes
cp -rf middlewares docker/build/middlewares
cp -rf controllers docker/build/controllers
cp app.js docker/build/app.js
cp config.js docker/build/config.js
FROM alpine
COPY package.json /var/www/wechat-mp/
WORKDIR /var/www/wechat-mp
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk add nodejs npm \
&& npm install --production --registry=https://registry.npm.taobao.org \
&& npm cache clean -f \
&& rm package-lock.json \
&& apk del npm \
&& rm -rf ~/.npm \
&& rm -rf /var/cache/apk/* \
&& rm -rf /root/.cache \
&& rm -rf /tmp/*
COPY build/ /var/www/wechat-mp/
CMD node app.js</pre>