微信小程序云开发获取关联公众号的文章列表
优采云 发布时间: 2020-08-13 00:36最近收到一个需求是:小程序获取关联公众号的文章并且显示。
首先想到的是,需要顾客去买服务器和域名,进行域名备案,然后为顾客开发一个网站,能够通过公众平台的appid与key来获取公众号的素材列表,最后开发一个插口给小程序使用。一般顾客选购服务器和域名就是个困局,劝退了很多人。
但是我查看了陌陌小程序的官方文档,发现有一个云开发功能,其中有云数据库、云存储、云函数三种功能。类似于Serverless,我们不需要去买服务器和域名了,直接使用小程序的云开发功能能够做好多事情。我就想试试能不能用云开发功能做获取公众号文章的事情。
云数据库是一个类似于Redis的Key-Value数据库,使用陌陌提供的一些查询API,能够进行链式操作,有点像.Net的Entity Framework。
云存储主要是拿来做上传与下载文件使用,可以拿来做相册等应用。
云函数是提供了一个Node.Js的运行环境,我们就能布署自己的代码,也才能使用npm配置引入其他的开源软件包。云函数只能通过陌陌小程序框架里特有的方法调用,并且返回结果。与我们订购服务器自建服务不同的是,我们不清楚究竟有几个实例在运行,每次执行不一定在同一个实例中,所以我们不太适宜做一些在显存中缓存数据、保持状态的工作,应当及时把数据存入云数据库。云开发的云函数的独到优势在于与陌陌登陆信令的无缝整合。当小程序端调用云函数时,云函数的传入参数中会被注入小程序端用户的 openid,开发者可以直接使用该 openid。
在做这个功能之前,首先是新建小程序,将公众平台与小程序关联,公众平台个人订阅号也是可以的。
然后在小程序的项目中,project.config.json文件中加入一个配置项,指定云函数的储存目录。
"cloudfunctionRoot": "clouds/"
新建这个clouds目录,在其中构建一个文件夹,比如我们命名为fetch,以后调用云函数也用这个名称,然后在其中新建index.js,作为云函数的入口。一个云函数的基本结构是:
// 云函数入口文件
const cloud = require('wx-server-sdk');
const request = require('request');
cloud.init();
// 云函数入口函数
exports.main = async (event, context) => {
return {200: 40400, msg: 'success'};
}
我们可以象MVC和Servlet等Web框架一样,要求客户端传一个action参数,然后按照不同的action参数,选择对应的处理函数,也可以创建多个云函数。代码类似于:
var action = event.action;
if(!action){
return {code: 20000, msg: 'hello, world'};
}
//dispatcher
var ret;
if(action === 'role'){
ret = await registerAndReturnUser(event);
} else if(action === 'userlist'){
ret = await fetchUserList(event);
} else if(action === 'updaterole'){
ret = await updateUser(event);
}
当然了我们可以使用npm install --save安装任何的npm开源包,像往常一样构建package.json,每次更新了云函数,都应当在云函数文件夹上点击右键,选择“上传并布署(云端安装依赖)”。
小程序后端调用云函数的代码是,无需晓得真实的URL,只需按云函数名称调用即可:
wx.cloud.callFunction({
// 云函数名称
name: 'fetch',
// 传给云函数的参数
data: {
keywords: this.data.keywords,
page: this.data.page,
limit: this.data.limit
},
success: function (res) {
console.log(res);
},
fail: err => {
console.log(err);
}
});
获取公众号的永久素材列表,可以参考这个文档。需要一个AccessToken,而AccessToken的过期时间是2小时,每天限制申请的次数,腾讯也建议在第三方服务器保存这个AccessToken。我们就可以把这个AccessToken存入云数据库中。申请AccessToken的文档在。
然后我们编撰一段代码获取AccessToken,然后查看云函数的日志,或者是我们主动加上throw句子,抛出异常,在小程序调试控制台也可以见到返回结果,经常会看见40164错误,这个错误会提示我们某个IP不在白名单中,我们就可以把这个IP加入微信公众号配置的白名单中。但是问题来了,小程序云开发的出口IP是浮动的,可能是一个集群在提供服务,不一定那个IP起作用。但是他又是有限的,当我们加了十几个IP时,发现常常可以命中我们的白名单列表,所以我们应当多试几次,把尽可能多的IP找下来。所以我就编撰了这样一段程序,当获取AccessToken失败,并且错误是40164时侯,首先在数据库中储存这个IP,等我们有时间了就把这种IP加入白名单,反复几次我们的白名单里就有一二十个IP了。这时候发觉我们的命中率上升了,请求好几次才有一次不命中的。所以我就编撰恳求失败之后立刻再恳求一次,最多恳求五次,类似于“再哈希”的算法,假设我们有50%的命中率,连续5次不命中的机率是3%左右,经过实验发觉,一般都能在五次之内成功获取AccessToken。
这段代码大致如下:
var getWechatAccessToken = async function(counter) {
//先检查数据库,如果token过期,返回空,继续执行网络请求
var tokenInDb = await this.getAccessTokenFromDB();
if (tokenInDb) {
return tokenInDb['accessToken'];
}
var token_url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + this.appid + '&' + 'secret=' + this.secret;
var result = await rp({
url: token_url,
method: 'GET'
});
var obj = (typeof result.body === 'object') ? result.body : JSON.parse(result.body);
if (obj.errcode && obj.errcode === 40164) {
var ip = obj.errmsg.split(' ')[2];
//保存IP到数据库
await this.insertIp(ip);
//try again and save ip to database;
counter += 1;
if (counter < 5) {
return await this.getWechatAccessToken(counter);
}
}
//保存AccessToken到数据库
if (obj['access_token']) {
this.saveTokenToDb(obj['access_token']);
return obj['access_token'];
}
return null;
}
我们储存了申请AccessToken的时间,可以按照这个时间选择使用这个数据库中储存的Token,或者是重新获取新的Token。接下来就是使用获取素材列表的插口获取所有的公众号文章。我们一直把它们存入数据库,这样上次小程序恳求,就可以直接从云数据库中访问。获取公众号素材列表有个限制是一次只能获取20条,我们须要先查出总量,然后分页获取。同样的是,查询云数据库一次最多获取100条,我们也是须要先查出总量,再分页查询所有的。
async fetchNewsFromServer(token, offset) {
var url = 'https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=' + token;
var body = {
"type": "news",
"offset": offset,
"count": 20
};
var result = await rp({
url: url,
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body)
});
return result;
}
//采集公众号的所有文章
async collectDataFromServer(token, dbcount) {
lastFetchTime = new Date();
var countUrl = 'https://api.weixin.qq.com/cgi-bin/material/get_materialcount?access_token=' + token;
var countResult = await rp({
url: countUrl,
method: 'GET',
});
var countObj = (typeof countResult.body === 'object') ? countResult.body : JSON.parse(countResult.body);
var total = 0;
if (countObj) {
total = countObj['news_count'];
if (total === dbcount) {
return;
}
} else {
return;
}
//每次获取20个
var fetchTimes = parseInt((total - 1) / 20) + 1;
var i;
var newsList = [];
for (i = 0; i < fetchTimes; i++) {
var offset = i * 20;
var newsResult = await this.fetchNewsFromServer(token, offset);
var news = (typeof newsResult.body === 'object') ? newsResult.body : JSON.parse(newsResult.body);
if (!news || !news.item_count) {
continue;
}
var j = 0;
var items = news.item;
for (j = 0; j < news.item_count; j++) {
newsList.push(items[j]);
}
}
//先清空原有的数据再存储新的
await this.clearTable('posts');
for (var k = 0; k < newsList.length; k++) {
var toSave = newsList[k];
var toSaveObj = {
mediaid: toSave.media_id,
url: toSave.content.news_item[0].url,
pic: toSave.content.news_item[0].thumb_url,
title: toSave.content.news_item[0].title
};
await db.collection('posts').add({
data: toSaveObj
});
}
}
然后就是小程序客户端通过云函数获取数据,在后端展示了,我们可以获取文章的链接,在WebView中展示。WebView只能显示关联公众号的文章和小程序后台填写的域名的网址。WebView的文档显示个人小程序不能使用,但实际上是可以使用的。。
参考
云开放的官方文档