目录

微信小程序云开发获取关联公众号的文章列表

目录

微信小程序云开发获取关联公众号的文章列表

最近收到一个需求是:小程序获取关联公众号的文章并且显示。

首先想到的是,需要客户去买服务器和域名,进行域名备案,然后为客户开发一个网站,能够通过公众平台的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 的文档显示个人小程序不能使用,但实际上是可以使用的。 。

参考

云开放的官方文档