Node微信公众号开发 前文整合优化

1,057 阅读7分钟

本篇主要用于将 Node微信公众号开发 cheerio网页抓取和memory-cache缓存模块 涉及的内容整合入公众号,使代码上显得更整洁美观。


优化 accessToken.js

该文件用于获取公众号凭证 access_token ,之前的代码如下:

var fs = require('fs')
var request = require('./request') // 将 request 封装成 Promise

module.exports = (config) => { // config 为配置文件,其中包含 appid 、appScrect ……
    return new Promise((resolve, reject) => {    
        var currentTime = new Date().getTime() // 获取当前时间戳    
        var url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + config.appId + '&secret=' + config.appScrect // 发起请求

        // 判断是否需要更新 accessToken
        if (config.setAccessToken.accessToken === '' || config.setAccessToken.time < currentTime) {
            // 若本地文件不存在凭证或凭证过期则更新本地重新凭证
            // config.setAccessToken 为本地文件键名
            request.get(url).then((response) => {
                var result = JSON.parse(response)
                config.setAccessToken.accessToken = result.access_token
                config.setAccessToken.time = new Date().getTime() + (parseInt(result.expires_in) - 200) * 1000 // 计算过期时间
                fs.writeFile('./config.json', JSON.stringify(config), (error) => {
                    if (error) {
                        reject(error)
                    }
                    resolve(response.access_token) // 导出 access_token
                })
            })
        } else {
            // 若凭证未过期直接导出本地 access_token
            resolve(config.setAccessToken.accessToken)
        }
    })
}

以上实现方式是将请求结果通过 fs 模块直接写入本地文件存储,这样会导致代码略显臃肿(我暂时无法评估优化的利与弊,至少从代码量上看),而且在动态写入的过程中会导致配置文件文本格式被压缩不宜阅读。所以我将 access_token 的保存方式由本地保存修改未缓存储存,当然这里使用的是前文提到的 memory-cache 缓存组件。(之所以想到用 memory-cache 缓存模块是通过 基于nodejs 的微信 JS-SDK 简单应用 的启发)

memory-cache 用法回顾:

  • 存储缓存。参数分别为:键名、键值、到期时间、回调函数

cache.put('houdini', 'disappear', 100, (key, value) => { console.log(key + ' did ' + value) })

  • 读取缓存。参数为目标缓存的键名

cache.get('foo')

以下是我的具体“优化”操作:

// 首先将 fs 模块替换为 memory-cache
var cache = require('memory-cache')

// 之后通过 cache.get 去判断是否需要更新 access_token
if (!cache.get('accessToken')) { // 如果 access_token 不存在则将请求结果保存进缓存
    request.get(url).then((response) => {
        var result = JSON.parse(response)
        cache.put('accessToken', result.access_token, (result.expires_in - 200) * 1000)
        resolve(result.access_token) // 导出 access_token
    })
} else { // 否则直接导出缓存中的 access_token
    resolve(cache.get('accessToken'))
}

通过一番改造,代码量从 51 行生生压缩到了 22 行!个人甚是满意。


封装爬虫爬取页面数据

之前封装的 reply.js 是通过请求 wp-json 接口获得站点数据,从而实现诸如“自动被动回复”类的功能,这不免是在某些程度上降低了站点安全性。

cheerio 用法回顾:

const cheerio = require('cheerio') // 首先,引入模块 const = cheerio.load(html) // 获取整个 html 节点
const eleUl =.find('#fruits') // 找到 id 为 fruits 的节点

// cheerio 模块对节点的使用方法 jQuery 如出一辙

在对 reply.js 进行优化前,我需要先封装一个专门用于爬取数据的页面,我将其命名为 reptile.js ,这里用到了前文的 cheerio 爬虫模块。

reptile.js 内包含两个爬取方法,分别用于爬取文章列表以及爬取正文。代码如下:

var cheerio = require('cheerio') // 引入模块
var config = require('./config') // 引入配置模块
var request = require('./request') // 将 request 封装成 Promise

exports.list = (callback, page) => { …… } // 回调函数用于向外抛出结果,page 为列表页 path
exports.detail = (url, callback) => { …… } // 回调函数用于向外抛出结果,url 为详情页 path

exports.list

因为站点文章列表和分类文章列表链接格式有区别,我做了区别处理,先看下我的路径:

文章列表:

www.sfatpaper.com/page/1/

www.sfatpaper.com/page/2/

分类文章列表:

www.sfatpaper.com/diary/page/…

www.sfatpaper.com/travel/page…

www.sfatpaper.com/code/page/3…

首先我将公共部分 https://www.sfatpaper.com/ 提取到配置文件中,之后根据路由的配置对不同页面进行请求响应:

// router.js
function routerTo (res, page) {
    reptile.list((data) => {
        res.render('list.html', { // 渲染页面
            data: data.list
        })
    }, page)
}
router.get('/history', (req, res) => { // 历史文章
    routerTo(res, 'page/1')
})
router.get('/travel', (req, res) => { // 旅行日记
    routerTo(res, 'travel/page/1')
})
router.get('/diary', (req, res) => { // 日常杂记
    routerTo(res, 'diary/page/1')
})
router.get('/code', (req, res) => { // 码农笔记
    routerTo(res, 'code/page/1')
})
// reptile.js - reptile.list
// config.reptilePath 即配置文件中的 https://www.sfatpaper.com/
request.get(config.reptilePath + page).then((response) => {
    var html = ''
    html += response // 将页面数据存入变量 html
    var $ = cheerio.load(html)

    function reptileData () {
        var arr = [] // 用于存放文章数据
        $('article').each((index, element) => { // 根据节点 $('article') 循环出所有文章
            // 拆分路径的作用主要用于百度站长平台移动适配的链接匹配功能
            var str = $(element).find('.entry-title a').attr('href').split('/')

            arr.push({
                sort: $(element).find('.meta-category a').text(), // 文章分类
                title: $(element).find('.entry-title a').text(), // 文章标题
                year: str[3],
                month: str[4],
                path: str[5],
                excerpt: $(element).find('.entry-content p').text(), // 文章简介
                thumbnail: $(element).find('.post-thumbnail img').attr('src') // 文章封面图
            })
        })
        return arr // 返回爬取最终数组,此时 arr 为创建好的数组对象
    }
    callback(reptileData())
})
// list.html
// 这里需要重新将对应文章的链接拼接成正确的地址
{{each data}}
<a href="http://m.sfatpaper.com/detail?year={{$value.year}}&month={{$value.month}}&path={{$value.path}}" class="weui-media-box weui-media-box_appmsg">
    <div class="weui-media-box__hd">
        <img class="weui-media-box__thumb" src="{{$value.thumbnail}}">
    </div>
	<div class="weui-media-box__bd">
    	<h4 class="weui-media-box__title">{{$value.title}}</h4>
		<p class="weui-media-box__desc">{{$value.excerpt}}</p>
	</div>
</a>
{{/each}}

到此位置,列表页的渲染工作就到此为止。此时我们可以通过点击直接获取到对应路径的参数,从而定位爬取文章页面。

exports.detail

// router.js
router.get('/detail', (req, res) => {
    var query = req.query // 获取到请求参数,从而拼接对应文章路径
	// config.reptilePath 即配置文件中的 https://www.sfatpaper.com/
    posts.detail(config.reptilePath + query.year + '/' + query.month + '/' + query.path, (detail) => {
        res.render('detail.html', { // 后端处理数据
            title: detail.title,
            excerpt: detail.excerpt,
            content: detail.content,
            thumbnail: detail.thumbnail
        })
    })
})
// reptile.js - reptile.detail
request.get(config.reptilePath + url).then((response) => {
    var html = ''
    html += response // 将页面内容存入变量 html
    var $ = cheerio.load(html)'
    
    var postDetail = {
        title: $('.entry-title').text(), // 文章标题
        excerpt: $('*[name="description"]').attr('content'), // 文章简介
        content: $('.entry-content').html(), // 文章正文
        thumbnail: $('.entry-content img').eq(0).attr('src') // 文章正文首图
    }
    
    callback(postDetail)
})

这里有一点暂时没有想到好的办法,那就是文章缩略图和文章简介都显示与原站点列表页,通过层层传递的方式很繁琐,所以我将简介存到了页面 description 中,而图片则直接选择调取文章内容首图而非列表页的缩略图。


优化被动关键词自动回复

我的自定义回复文件命名为 reply.js ,具体详情还请移步至前文,这里主要优化的地方是最新文章回复的功能,这个功能可以通过回复字母“n”以及点击菜单“文章”->"最新文章"触发。

先来看下我之前的实现方法吧(以下代码只针对是获取最新文章功能):

// config.webJson 原 wp-json 接口地址
request.get(config.webJson).then((data) => {
    var contentArr = []
    var items = JSON.parse(data) // 将数据转化为对象数组
    
    contentArr.push({
        Title: item[0].title.rendered, // 文章标题
        Description: item[0].excerpt.rendered.replace(/<[^>]+>/g, ''), // 文章简介(replace 解决数据中 html 标签显示问题)
        PicUrl: item[0].thumbnail, // 文章缩略图
        Url: item[0].link // 文章链接
    })

    // replyType.js 整合了所有回复的种类,包括图文回复、文本回复、图片回复 ……
    resultXml = replyType.graphicMsg(result, contentArr) // 向 replyType.js 抛出最新文章数据
})

而现在我利用之前开发好的 reptile.js 模块,我们可以省去对 JSON.parse 数据转化工作(爬取来的内容为纯文本,带 html 标签),对于数据的调用也更为灵活,不会再拘泥于接口中繁杂的层级命名。

代码优化如下:

reptile.list((data) => {
    var contentArr = [{
        Title: data[0].title, // 文章标题
        Description: data[0].excerpt, // 文章简介
        PicUrl: data[0].thumbnail, // 文章缩略图
        Url: config.routerPath + 'detail?year=' + data[0].year + '&month=' + data[0].month + '&path=' + data[0].path // 文章链接(拼接路径,用法如前文)
    }]
    resultXml = replyType.graphicMsg(result, contentArr) // 向 replyType.js 抛出最新文章数据
}, 'page/1') // 始终获取第一页的第一篇文章

怎么样,是不是看着多少顺眼了些呢?


前文还提到了另外一个模块 cron ,它是一个定时器用来定时去执行某样任务。起初是想利用该模块实现主动发送最新文章的功能,但由于种种原因该功能暂时被我放弃了。所以在这里也没有太多可以介绍的,若想要了解它还请异步至 cron


文章已同步我的个人博客:《Node微信公众号开发 前文整合优化


相关文章:


资料参考:

本文由博客一文多发平台 OpenWrite 发布!