从零开发微信公众号(PC)

框架使用Express,开发前先下载Express到项目目录

npm i express   //下载Express框架
npm i           //下载框架所需的依赖

内网穿透,因为微信服务器需要访问到你的服务器域名,每次开机测试都要运行穿透软件

(113条消息) sunny-ngrok 的配置及使用_zxc_user的博客-CSDN博客

password:LKbswxl1314?

id:163732335504

 还有运行之前一定要打开mongoose的服务也就是黑框:

 (157条消息) mongodb 学习_qq_51717117的博客-CSDN博客

微信的参考网页

微信公众平台 (qq.com) 测试接口

关注/取消关注事件 | 微信开放文档 (qq.com) 测试文档

app.js主文件

//app.js

const express = require('express');           //引入express框架
const app = express();                        //创建express实例app
app.use()                                     //这里可以使用中间件,从而简化主文件
app.listen(6177, () => {      
        console.log('server is running...')
    })                                        //开启server服务,6177是自定义端口

功能一:auth_check.js 验证服务器有效性

$对应微信开放文档=>开始开发/接入指南

单文件版(113条消息) 微信公众号_接口测试_验证服务器有效性_现实里的大梦想家的博客-CSDN博客
思路比较清晰,适合作为拆成模块的依据

模块化版

思路:

1.config定义配置对象写成模块,因为都是开发者固定的东西,且模块便于重复调用

//auth_check.js

module.exports = {
    token: '1234',                                //身份验证的参数,自定义即可
    appID: 'wx387707c142342e5f',                  //根据自己的开发者信息填写
    appsecret: 'ed1109d567b0bf87eba63747f6aa71b2' //根据自己的开发者信息填写
}

2.在auth_check.js文件中调用模块

验证思路:

微信服务器发来的东西即=>req.query

   {
       signature: '9bf986bba167e1d38434813148d9622cf1e143c0',
       //微信的加密签名,在自己服务器根据这个算出是否来自微信服务器
       echostr: '3329679419479420731',              //微信随机字符串
       timestamp: '1635763993',                     //微信发送请求的时间戳
       nonce: '378478086'                           //随机数字
   }

解密思路: 
将微信服务器发来的timestamp, nonce, token字典排序拼接成字符串sha1加密(所以要引入sha1), 如果和req.query中signature一样, 验证成功,返回给微信服务器

//auth_check.js

const sha1 = require('sha1')                            //引入解密模块
const config = require('./config')                      //引入配置模块config,注意路径
module.exports = () => {                                //暴露模块

    return async(req, res, next) => {
        //微信服务器向开发者服务器请求的参数是req.query
        const { signature, echostr, timestamp, nonce } = req.query 
        //对象的结构赋值,拿到大括号中想提取的内容
        const { token } = config//拿到自定义的token进行对来自微信服务器的signature解密
        const sha1str = sha1([timestamp, nonce, token].sort().join(''))//按思路解密

        if (req.method === 'GET') {
            if (sha1str == signature) {                     //对应得上,身份验证成功
                res.send(echostr)           //微信测试号收到后即知道服务器身份验证通过
            } else {                                      //没对应上,不是微信服务器
                res.end('err')
            }
        } else if (req.method === 'POST') {                          //post后面再说
            
        } else {                             //既不是get也不是post,微信识别不了,报错
            res.end('err')
        }
    }
}
/*微信服务器会发送两种请求到开发者服务器
GET
-验证服务器的有效性
POST
-微信会把用户输入的消息转发到开发者服务器上
*/

注意:在app.js中要引入这个身份验证中间件

const express = require('express');
const auth = require('./auth_check')          //身份验证主模块,里面包括config
const app = express();
app.use(auth())                               //使用身份验证中间件
app.listen(6177, () => {
        console.log('server is running...')
    })

功能二:获取Access_Token

$对应微信开发者文档=>开始开发/获取AccessToken

为什么需要Access_Token?       因为是微信调用全局接口的唯一凭据且有效期两小时

思路:

首次本地没有,向微信服务器请求access_token,保存(本地文件),第二次先去本地读取文件,判断是否过期,过期了:重新请求,覆盖之前文件;没过期:直接使用

流程:(调用模块化方法)

读取本地文件:readAccessToken

    本地文件存在:

         判断是否过期 isValidAccessToken

             getAccessToken,saveAccessToken

    本地文件不存在:

         请求 getAccessToken,saveAccessToken

//accesstoken.js

const { appID, appsecret } = require('../config')    //引入一些配置信息
const { writeFile, readFile } = require('fs')    //使用node自带的文件模块,获取读写文件方法
const rp = require('request-promise-native') //npm i request库,request-promise-native库
class wechat {
    getAccessToken() {
        const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appID}&secret=${appsecret}` //请求地址
        return new Promise((resolve, reject) => {
            rp({ method: 'GET', url, json: true })
                .then(res => {
                    res.expires_in = Date.now() + (res.expires_in - 300) * 1000;
                    resolve(res)
                })
                .catch(err => {
                    reject('getAccessToken方法出问题' + err)
                })
        })
    }
    saveAccessToken(accessToken) {
        accessToken = JSON.stringify(accessToken) //将对象转化成json字符串
        return new Promise((resolve, reject) => {
            writeFile('./accessToken.txt', accessToken, (err) => {
                if (!err) {
                    console.log('save success')
                    resolve()
                } else {
                    reject('error:' + err)
                }
            })
        })

    }
    readAccessToken() {
        return new Promise((resolve, reject) => {
            readFile('./accessToken.txt', (err, data) => {
                if (!err) {
                    console.log('read success')
                    data = JSON.parse(data) //json字符串转换为json对象
                    resolve(data)
                } else {
                    reject('error:' + err)
                }
            })
        })
    }
    isValidToken(data) {
        if (!data && !data.access_token && !data.expires_in) { return false }
        //检测有效期
        if (data.expires_in < Date.now()) { return false } else { return true }
    }
    fetchAccessToken() {
        if (this.access_token && this.expires_in && this.isValidToken(this)) {
            //有有效的token
            return Promise.resolve({
                access_token: this.access_token,
                expires_in: this.expires_in
            })
        }
        return this.readAccessToken()
            .then(async res => { //判断是否过期
                if (this.isValidToken(res)) { return Promise.resolve(res) } else {
                    const res = await this.getAccessToken()
                    await this.saveAccessToken(res) //保存token
                    return Promise.resolve(res)
                }
            })
            .catch(async err => { //本地没有文件
                const res = await this.getAccessToken()
                await this.saveAccessToken(res) //保存token
                return Promise.resolve(res)
            })
            .then(res => { //挂载到this上
                this.access_token = res.access_token
                this.expires_in = res.expires_in
                return Promise.resolve(res) //最终的返回值
            })
    }
}

//测试用
const w = new wechat();
w.fetchAccessToken().then(res => { console.log(res) }) //这样就可以拿数据

功能三:获取用户发送的消息

$对应微信开发者文档=>基础消息能力       因为是post请求,所以在前面空着的post框架里写

微信服务器会发送两种类型的消息给开发者服务器

1.GET请求

-验证服务器的有效性

2.POST请求

-微信服务器会将用户发送的数据以POST请求的方式转发到开发者服务器上

//auth.js
//验证开发者服务器有效性
const config = require('../config')
const sha1 = require('sha1')
const { getUserDataAsync, praseXMLAsync, formatMessage } = require('../utils/tools')
module.exports = () => {
    return async(req, res, next) => {
        const { signature, echostr, timestamp, nonce } = req.query
        const { token } = config
        const sha1str = sha1([timestamp, nonce, token].sort().join(''))
        if (req.method === 'GET') { //微信服务器验证开发者服务器有效性时用GET
            if (sha1str == signature) {
                res.send(echostr)
            } else {
                res.end('error')
            }
        } else if (req.method === 'POST') { //验证消息来自于微信服务器
            if (sha1str !== signature) { //消息不是来自微信服务器
                res.end('error')
            } //----------------------------------
            const xmlData = await getUserDataAsync(req) //接受请求体中的流式数据
            const jsData = await praseXMLAsync(xmlData)
            const message = formatMessage(jsData) //格式化数据
            console.log(message) //接收到了用户发来的消息
            res.end('')
                //----------------------------------
        } else { //其他类型的请求都是无效的
            res.end('error')
        }
    }
}
//tools.js
const { parseString } = require('xml2js') //npm i xml2js
module.exports = {
    getUserDataAsync(req) {
        return new Promise((resolve, reject) => {
            let xmlData = ''
            req
                .on('data', data => { //将流式数据注入到回调函数中
                    xmlData += data.toString() //将buffer转换为字符串
                })
                .on('end', () => {
                    resolve(xmlData)
                })
        })
    },
    praseXMLAsync(xmlData) {
        return new Promise((resolve, reject) => {
            parseString(xmlData, { trim: true }, (err, data) => {
                if (!err) {
                    resolve(data)
                } else {
                    reject('praseXMLAsync' + err)
                }
            })
        })

    },
    formatMessage(jsData) {
        let message = {}
        jsData = jsData.xml //获取xml对象
        if (typeof jsData === 'object') { //判断是否是一个对象
            for (let key in jsData) {
                let value = jsData[key] //获取属性值
                if (Array.isArray(value) && value.length > 0) { //过滤掉空数据
                    message[key] = value[0]
                }
            }
        }
        return message
    }
}

收到消息: 

 

功能四:被动回复用户消息 

1.简易版

//auth.js被动回复文本消息
//验证开发者服务器有效性
const config = require('../config')
const sha1 = require('sha1')
const { getUserDataAsync, praseXMLAsync, formatMessage } = require('../utils/tools')
module.exports = () => {
    return async(req, res, next) => {
        const { signature, echostr, timestamp, nonce } = req.query
        const { token } = config
        const sha1str = sha1([timestamp, nonce, token].sort().join(''))
        if (req.method === 'GET') { //微信服务器验证开发者服务器有效性时用GET
            if (sha1str == signature) {
                res.send(echostr)
            } else {
                res.end('error')
            }
        } else if (req.method === 'POST') { //验证消息来自于微信服务器
            if (sha1str !== signature) { //消息不是来自微信服务器
                res.end('error')
            } //----------------------------------
            const xmlData = await getUserDataAsync(req) //接受请求体中的流式数据
            const jsData = await praseXMLAsync(xmlData)
            const message = formatMessage(jsData) //格式化数据
            console.log(message) //接收到了用户发来的消息
            let content = '我听不懂'
            if (message.MsgType === 'text') { //判断类型
                if (message.Content === '1') {
                    content = '嘿嘿'
                }
            }
            let replyMessage = `<xml>
            <ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>
            <FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
            <CreateTime>${Date.now()}</CreateTime>
            <MsgType><![CDATA[text]]></MsgType>
            <Content><![CDATA[${content}]]></Content>
            </xml>`
            res.send(replyMessage)
                //----------------------------------
        } else { //其他类型的请求都是无效的
            res.end('error')
        }
    }
}

功能单一,代码冗杂,改成模板工具函数进行调用

2.多文件版 ,可回复文本,语音,图片消息

//验证开发者服务器有效性
const config = require('../config')
const sha1 = require('sha1')
const { getUserDataAsync, praseXMLAsync, formatMessage } = require('../utils/tools')
const template = require('./template')
const reply = require('./reply')
module.exports = () => {
    return async(req, res, next) => {
        const { signature, echostr, timestamp, nonce } = req.query
        const { token } = config
        const sha1str = sha1([timestamp, nonce, token].sort().join(''))
        if (req.method === 'GET') { //微信服务器验证开发者服务器有效性时用GET
            if (sha1str == signature) {
                res.send(echostr)
            } else {
                res.end('error')
            }
        } else if (req.method === 'POST') { //验证消息来自于微信服务器
            if (sha1str !== signature) { //消息不是来自微信服务器
                res.end('error')
            } //----------------------------------
            const xmlData = await getUserDataAsync(req) //接受请求体中的流式数据
            const jsData = await praseXMLAsync(xmlData)
            const message = formatMessage(jsData) //格式化数据
            console.log(message) //接收到了用户发来的消息
            const options = reply(message)
            let replyMessage = template(options)
            res.send(replyMessage)
                //----------------------------------
        } else { //其他类型的请求都是无效的
            res.end('error')
        }
    }
}

template.js模板=>微信开发者文档有规定xml格式

//消息回复模板函数
module.exports = options => {
    let replymessage = `<xml>
    <ToUserName><![CDATA[${options.FromUserName}]]></ToUserName>
    <FromUserName><![CDATA[${options.ToUserName}]]></FromUserName>
    <CreateTime>${Date.now()}</CreateTime>
    <MsgType><![CDATA[${options.MsgType}]]></MsgType>`
    if (options.MsgType === 'text') {
        replymessage += `<Content><![CDATA[${options.Content}]]></Content>`
    } else if (options.MsgType === 'image') {
        replymessage += `<Image><MediaId><![CDATA[${options.MediaId}]]></MediaId></Image>`
    } else if (options.MsgType === 'voice') {
        replymessage += `<Voice><MediaId><![CDATA[${options.MediaId}]]></MediaId></Voice>`
    }
    replymessage += '</xml>'
    return replymessage
}

后面还加了图文消息的模板,因为少了一个<item>导致一直收不到消息

错误修正后,发现只能收到一条图文消息,查看官方文档:

 图文消息个数;当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息 

所以说用户发文本消息只能回复一条,而自定义菜单关联的事件可以恢复多条! 

 点击“赞我们” 即可收到三条图文消息(当然这个对应的不是很好,不过我已经很满意了)

reply.js

module.exports = (message) => {
    let options = {
        ToUserName: message.ToUserName,
        FromUserName: message.FromUserName,
        CreateTime: Date.now(),
        MsgType: 'text',
    }
    let content = '我不明白 ~'
    if (message.MsgType === 'text') {
        if (message.Content === '5') {
            content = '👉'
        }
    } else if (message.MsgType === 'image') {
        options.MsgType = 'image'
        options.MediaId = message.MediaId
    } else if (message.MsgType === 'voice') {
        options.MsgType = 'voice'
        options.MediaId = message.MediaId
        console.log(message.Recognition)
    }
    options.Content = content
    return options
}

至此微信公众号可以完成与用户的普通消息收发

事件消息和普通消息差不多 (/≧▽≦)/

 else if (message.MsgType === 'event') {
        if (message.Event === 'subscribe') {
            content = 'Welcome!'
        } else if (message.Event === 'unsubscribe') {
            console.log('lose a fan...')
        }
    }

                                          

微信小程序也开放了推送功能

小程序订阅消息 | 微信开放文档 (qq.com)

templateMessage.send | 微信开放文档 (qq.com)

功能五 自定义菜单

//accesstoken.js
createmenu(menu) {
        return new Promise(async(resolve, reject) => {
            const data = await this.fetchAccessToken()
            const url = `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${data.access_token}`
            const result = await rp({ method: 'POST', url, json: true, body: menu })
            resolve(result)
        })
    }
    deletemenu() {
        return new Promise(async(resolve, reject) => {
            const data = await this.fetchAccessToken()
            const url = `https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=${data.access_token}`
            const result = await rp({ method: 'GET', url, json: true })
            resolve(result)
        })
    }
//立即执行函数测试一下
(async() => {
    const w = new wechat()
    let result = await w.deletemenu()
    console.log(result)
    let date = await w.createmenu(menu)
    console.log(date)
    const data = await w.fetchTicket()
    console.log(data)
})()
//menu.js别忘了在accesstoken里引入
module.exports = {
    "button": [{
            "type": "view",
            "name": "影片",
            "url": "http://publicvv.free.idcfengye.com/movie"
        },
        {
            "name": "菜单",
            "sub_button": [{
                    "type": "view",
                    "name": "热门数据",
                    "url": "http://publicvv.free.idcfengye.com/search"
                },
                {
                    "type": "click",
                    "name": "赞我们",
                    "key": "V1001_GOOD"
                }
            ]
        }
    ]
}
//在事件里加上对应的reply.js
 else if (message.MsgType === 'event') {
        if (message.Event === 'subscribe') {
            content = 'Welcome!'
        } else if (message.Event === 'unsubscribe') {
            console.log('lose a fan...')
        } else if (message.Event === 'CLICK') {
            if (message.EventKey === 'V1001_TODAY_MUSIC') {
                content = '《新年好》⭐'
            } else if (message.EventKey === 'V1001_GOOD') {
                content = '谢谢支持😊'
            }
        }

                                          

功能六 JS-SDK接口获取

 一、获取ticket(和accesstoken差不多)

//accesstoken.js
getTicket() {
        return new Promise(async(resolve, reject) => {
            const data = await this.fetchAccessToken()
            console.log(data)
            const url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${data.access_token}&type=wx_card`
            rp({ method: 'GET', url, json: true })
                .then(res => {
                    resolve(res)
                })
                .catch(err => {
                    reject('getticket方法出问题' + err)
                })
        })
    }
    saveTicket(ticket) {
        ticket = JSON.stringify(ticket) //将对象转化成json字符串
        return new Promise((resolve, reject) => {
            writeFile('./ticket.txt', ticket, (err) => {
                if (!err) {
                    console.log('save success')
                    resolve()
                } else {
                    reject('error:' + err)
                }
            })
        })

    }
    readTicket() {
        return new Promise((resolve, reject) => {
            readFile('./ticket.txt', (err, data) => {
                if (!err) {
                    console.log('read success')
                    data = JSON.parse(data) //json字符串转换为json对象
                    resolve(data)
                } else {
                    reject('error:' + err)
                }
            })
        })
    }
    isValidTicket(data) {
        if (!data && !data.ticket && !data.expires_in) { return false }
        //检测有效期
        if (data.expires_in < Date.now()) { return false } else { return true }
    }
    fetchTicket() {
        if (this.ticket && this.ticket_expires_in && this.isValidTicket(this)) {
            //有有效的token
            return Promise.resolve({
                ticket: this.ticket,
                expires_in: this.expires_in
            })
        }
        return this.readTicket()
            .then(async res => { //判断是否过期
                if (this.isValidTicket(res)) { return Promise.resolve(res) } else {
                    const res = await this.getTicket()
                    await this.saveTicket(res) //保存token
                    return Promise.resolve(res)
                }
            })
            .catch(async err => { //本地没有文件
                const res = await this.getTicket()
                await this.saveTicket(res) //保存token
                return Promise.resolve(res)
            })
            .then(res => { //挂载到this上
                this.access_token = res.access_token
                this.ticket_expires_in = res.expires_in
                return Promise.resolve(res) //最终的返回值
            })
    }

生成js-sdk使用的签名:

1.组合参与签名的四个参数:jsapi_ticket(临时票据)、noncestr(随机字符串)、timestamp(时间戳)、url(当前服务器地址)

2.将其进行字典排序,以‘&’拼接在一起

3.进行sha1加密,最终生成signature 

概述 | 微信开放文档 (qq.com)

//npm i ejs 引入模板引擎
app.set('views', './views')
app.set('view engine', 'ejs')
//配置模板引擎
app.get('/search', async(req, res) => {
//通过config接口注入权限验证配置
    const noncestr = num.randomStr(36)
    const timestamp = Date.now()
    const { ticket } = await AccessTicket.fetchTicket()
    const arr = [`jsapi_ticket=${ticket}`, `noncestr=${noncestr}`, `timestamp=${timestamp}`, `url=${url}/search`]
    const str = arr.sort().join('&')
    console.log(str)
    const signature = sha1(str)
    res.render('search', {
        signature,
        noncestr,
        timestamp
    })
})
//随机字符串代码
module.exports = {
        randomNum(min = 0, max = 100, len = 0) {
            return Number((min + (max - min) * Math.random()).toFixed(len));
        },
        randomStr(len = 8) {
            let str = '';
            let list = '0123456789abcdefghijklmnopqrstuvwxyz';
            for (let i = 0; i < len; i++) {
                let index = this.randomNum(0, 35);
                let word = list[index];
                if (isNaN(word) && this.randomNum() < 50) {
                    word = word.toUpperCase();
                }
                str += word;
            }
            return str;
        }
    }
//ejs模板html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>

<body>
    <h1 id='search'>这是一个搜索页面</h1>
    <script type="text/javascript" src='http://res.wx.qq.com/open/js/jweixin-1.6.0.js '></script>
    <script type="text/javascript">
        wx.config({
            debug: true,
            appId: 'wx387707c142342e5f',
            timestamp: '<%=timestamp%>',
            nonceStr: '<%=noncestr%>',
            signature: '<%=signature%>',
            jsApiList: ['updateAppMessageShareData', 'startRecord', 'stopRecord', 'translateVoice']
        });
        wx.ready(function() {
            //验证接口是否有权限
            wx.checkJsApi({
                jsApiList: ['updateAppMessageShareData', 'startRecord', 'stopRecord', 'translateVoice'],
                success: function(res) {
                    console.log(res)
                }
            });
            //语音识别

        });
        wx.error(function(res) {
            // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
        });
    </script>
</body>

</html>

新下载了一个库BootCDN - Bootstrap 中文网开源项目免费 CDN 加速服务 zepto

复制   <script src="https://cdn.bootcdn.net/ajax/libs/zepto/1.0rc1/zepto.min.js"></script>

然后用到了jQuery的东西,实现点击文字,返回热门电影信息功能

豆瓣api暂时无法使用,用了另外一个网址qpi

iiiiiii1/douban-imdb-api: 一个基于豆瓣、IMDB、烂番茄评分的电影电视剧双语(中英)数据api接口 (github.com)

获取的信息太多,只选了data[0] 肖申克的救赎,以下是网址返回内容第一条:

{
"data":
[{"createdAt":1605355459692,
"updatedAt":1605355459692,
"id":"5f968bfcee3680299115bbe6",
"poster":"https://wmdb.querydata.org/movie/poster/1603701754760-c50d8a.jpg",
"name":"肖申克的救赎",
"genre":"犯罪/剧情",
"description":"20世纪40年代末,小有成就的青年银行家安迪(蒂姆·罗宾斯 Tim Robbins 饰)因涉嫌杀害妻子及她的情人而锒铛入狱。在这座名为鲨堡的监狱内,希望似乎虚无缥缈,终身监禁的惩罚无疑注定了安迪接下来...",
"language":"英语",
"country":"美国",
"lang":"Cn",
"shareImage":"https://wmdb.querydata.org/movie/poster/1605355459683-5f968bfaee3680299115bb97.png",
"movie":"5f968bfaee3680299115bb97"
}],
"createdAt":1603701756481,
"updatedAt":1603701756481,
"id":"5f968bfaee3680299115bb97",
"originalName":"The Shawshank Redemption",
"imdbVotes":2297852,
"imdbRating":"9.3",
"rottenRating":"91",
"rottenVotes":75,
"year":"1994",
"imdbId":"tt0111161",
"alias":"月黑高飞(港) / 刺激1995(台) / 地狱诺言 / 铁窗岁月 / 消香克的救赎",
"doubanId":"1292052",
"type":"Movie",
"doubanRating":"9.7",
"doubanVotes":2170679,
"duration":8520,
"dateReleased":"1994-09-10"
}

 

 $('#search').tap(function() { //点击就搜索电影
                var url = 'https://api.wmdb.tv/api/v1/top?type=Imdb&skip=0&limit=50&lang=Cn'
                $.getJSON(url,
                    function(data) {
                        alert(data[0].data[0].name)
                    })
            })

 

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width= device-width,intial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <h1 id='search'>这是一个搜索页面</h1>
    <ul id="list">

    </ul>
    <script type="text/javascript " src='http://res.wx.qq.com/open/js/jweixin-1.6.0.js '></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/zepto/1.0rc1/zepto.min.js "></script>
    <script type="text/javascript ">
        wx.config({
            debug: true,
            appId: 'wx387707c142342e5f',
            timestamp: '<%=timestamp%>',
            nonceStr: '<%=noncestr%>',
            signature: '<%=signature%>',
            jsApiList: ['updateAppMessageShareData', 'startRecord', 'stopRecord', 'translateVoice']
        });
        wx.ready(function() {
            //验证这些接口在用户那里是否有权限
            wx.checkJsApi({
                jsApiList: ['updateAppMessageShareData'],
                success: function(res) {
                    console.log(res)
                }
            });
            //设置标志位,都不大能用了
            $('#search').tap(function() { //点击就搜索电影
                var url = 'https://api.wmdb.tv/api/v1/top?type=Imdb&skip=0&limit=50&lang=Cn'
                $.getJSON(url,
                    function(data) {
                        var html = ''
                        data.forEach(function(item) {
                            var da = item.data[0]
                            html += '<h2>' + da.name +
                                '</h2>' +
                                '<p>genre: ' + da.genre + '</p>' +
                                '<div>' +
                                '<img src ="' + da.poster + '" alt="图片">' +
                                '</div>'
                            $('#list').html(html)
                        })


                    })
            })
        });
        wx.error(function(res) {
            // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
        });
    </script>
</body>

</html>

易点微信编辑器_微信公众号排版_公众号软文编辑_公众号图文编辑_微信图文排版工具_微信内容排版软件_微信公众平台素材编辑-易点编辑器 (wxeditor.com)

LKbswxl1314? 

功能七 爬取热门电影信息

官网:puppeteer/puppeteer: Headless Chrome Node.js API (github.com)

npm i puppeteer --save-dev

下载爬虫包,爬虫就是用代码的方式模拟人的手动点击获取信息

去到想去的网页,右击检查查看代码

const puppeteer = require('puppeteer')
const url = 'https://movie.douban.com/cinema/nowplaying/nanjing/'
module.exports = async() => {
    //1.打开浏览器
    const browser = await puppeteer.launch({
            //args: [],
            headless: false
        })
        //2.创建一个标签页
    const page = await browser.newPage()
        //3.跳转到指定网页
    await page.goto(url, {
            waitUntil: 'networkidle2' //等待网络空闲时跳转加载页面
        })
        //4.等待网址加载完成,爬取数据
    const result = await page.evaluate(() => {
        //对加载好的页面进行dom操作
        //获取所有热门电影的li
        let result = []
        const $list = $('#nowplaying>.mod-bd>.lists>.list-item')
        for (let i = 0; i < 3; i++) {
            const liDom = $list[i]
            let title = $(liDom).data('title')
            let score = $(liDom).data('score')
            let region = $(liDom).data('region')
            let href = $(liDom).find('.poster>a').attr('href')
            let image = $(liDom).find('.poster>a>img').attr('src')
            result.push({
                title,
                score,
                region,
                href,
                image
            })
        }
        return result

    })
    console.log(result)
        //5.将浏览器关闭
    await browser.close()
}

 

把影片类型和影片简介也加上

在href的网站里重新访问,将类型和简介挂载到结果上

const puppeteer = require('puppeteer')
const url = 'https://movie.douban.com/cinema/nowplaying/nanjing/'
module.exports = async() => {
    //1.打开浏览器
    const browser = await puppeteer.launch({
            //args: [],
            headless: false
        })
        //2.创建一个标签页
    const page = await browser.newPage()
        //3.跳转到指定网页
    await page.goto(url, {
            waitUntil: 'networkidle2' //等待网络空闲时跳转加载页面
        })
        //4.等待网址加载完成,爬取数据
    const result = await page.evaluate(() => {
        //对加载好的页面进行dom操作
        //获取所有热门电影的li
        let result = []
        const $list = $('#nowplaying>.mod-bd>.lists>.list-item')
        for (let i = 0; i < 3; i++) {
            const liDom = $list[i]
            let title = $(liDom).data('title')
            let score = $(liDom).data('score')
            let region = $(liDom).data('region')
            let href = $(liDom).find('.poster>a').attr('href')
            let image = $(liDom).find('.poster>a>img').attr('src')
            result.push({
                title,
                score,
                region,
                href,
                image
            })
        }
        return result
    })
    console.log(result)
    for (var i = 0; i < result.length; i++) {
        let item = result[i]
        let url = item.href //获取电影详情页面的网址
        await page.goto(url, {
            waitUntil: 'networkidle2'
        })
        let itemresult = await page.evaluate(() => {
            let genre = []
            const $genre = $('[property="v:genre"]')
            for (let j = 0; j < $genre.length; j++) {
                genre.push($genre[j].innerText)
            }
            const summary = $('[property="v:summary"]').html().replace(/\s+/g, '')

            return { genre, summary }
        })
        item.genre = itemresult.genre //挂载新属性
        item.summary = itemresult.summary
    }
    console.log(result)
        //5.将浏览器关闭
    await browser.close()
}

新建数据库进行数据持久化保存

PS C:\Users\Vxl\Desktop\work\Gongzhongh> npm i mongoose

新的数据库相关目录结构: 

 model文件夹中定义存储的数据表类型

db连接mongoose数据库

save保存爬取的数据到数据库中

index整合以上功能完成数据持久化

立即执行函数有个奇怪的报错:(157条消息) TypeError: require(...)(...) is not a function_d1063270962的博客-CSDN博客

mongoose官方文档:

猫鼬 v6.2.5:入门 (mongoosejs.com)

写代码的时候要注意好逻辑关系,如果爬取数据返回的是一个数组,在保存的时候就要对这个数组进行遍历依次拿到每一个结构体再进行数据存储。

db/index.js
const mongoose = require('mongoose')
module.exports = new Promise((resolve, reject) => {
    mongoose.connect('mongodb://localhost:27017/gongzhonghao', { useNewUrlParser: true })
    mongoose.connection.once('open', err => {
        if (!err) {
            console.log('数据库连接成功')
            resolve()
        }
    })
})
model/theater.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema;
//创建约束对象
const theaterSchema = new Schema({
        title: String,
        score: Number,
        region: String,
        image: String,
        genre: [String],
        summary: String,
        postKey: String, //图片要上传到七牛中返回的key值
        createTime: {
            type: Date,
            defult: Date.now()
        }
    })
    //创建模型对象
const Theater = mongoose.model('Theater', theaterSchema)
    //暴露
module.exports = Theater
server/save/save.js
const Theater = require('../../model/Theater')
module.exports = async(data) => {
    const result = await Theater.create({
        title: data.title,
        score: data.score,
        region: data.region,
        image: data.image,
        genre: data.genre,
        summary: data.summary,
    })
    console.log('数据保存成功' + result)
}
server/index.js
const theatercrawler = require('./crawler/theaters');
const db = require('../db');
const saveTheater = require('./save/save');
(async() => {
    await db; //连接数据库
    const data = await theatercrawler() //爬取数据
        //返回一个有三个结构体元素的数组
    for (var i = 0; i < 3; i++) { //就设定为三条
        await saveTheater(data[i])
    }

})()

  

 通过图文消息将电影数据回复给用户

1.从数据库中查询数据

2.在处理用户消息回复区域返回数据(在文件中引入Theater)

(157条消息) 使用mongoose模块中的find方法查不到mongoDB数据_牛先森家的博客-CSDN博客

(157条消息) mongoose踩坑: Cannot overwrite xxxx model once compiled._jolieLi2019888的博客-CSDN博客

很详细的mongoose博客:(157条消息) 后端-Node(express)连接mongodb到前端-访问接口将数据显示页面(一条龙)_简单Cere-CSDN博客_express 连接mongodb 目前还没有完成数据从数据库中的读取

无语了...解决了,原来是没有建立数据库连接...😊

实现电影详情页面

用户发送 “热门” 返回三条电影图文消息,其中有个参数是电影详情网址

//item.doubanid由数据库提取
url: `http://publicvv.free.idcfengye.com/detail/${item.doubanId}`

我们用id这种唯一标识来对应每个电影的详情页面

接下来撰写这个详情页面

在router中加入电影详情的路由

router.get('/detail/:id', async(req, res) => {
    const { id } = req.params //拿到网址中的doubanid
    if (id) { //找到这个id的所有数据,引入Theaters
        const data = await Theater.findOne({ doubanId: id }, { createTime: 0 }) //可以设置不要哪些数据
        res.render('detail', { data }) //页面渲染过去,将后台的数据传给前端
    } else {
        res.end('error')
    }
})

前端页面

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <header>
        <a class="header_title" href="/movie">硅谷电影</a>
        <a class="header_search" href="/search">搜索</a>
        <!-- a,p标签和h标签有什么区别啊 -->
        <div class="page">
            <h1 class="title">
                <%= data.title %>
            </h1>
            <section class="info">
                <div class="left">
                    <p class="rating">
                        <span>评分:</span>
                        <strong><%= data.score %></strong>
                        <span class="ratingNum">52996人评价</span>
                    </p>
                    <p class="meta">
                        <%= data.runtime%> /
                            <% data.genre.forEach(function (item) { %>
                                <%= item %> /
                                    <% })%>
                    </p>
                </div>
                <div class="right">
                    <a href="javascript:">
                    <img src="<%= data.image %>" alt="<%= data.title %>">
                    </a>
                </div>
            </section>
            <section class="intro">
                <h2>
                    <%= data.title %> 剧情简介
                        <div class="bd">
                            <p>
                                <%= data.summary %>
                            </p>
                        </div>
                </h2>
            </section>
        </div>
    </header>
</body>

</html>

数据库可能还需要改一下,没有设置id等等

(159条消息) jQuery获取指定ul下的li_bigface_girl的博客-CSDN博客_jquery获取ul下所有li

终于获取到了,就是多了一个$的问题 😀

const $list = $('#nowplaying>.mod-bd>.lists>.list-item')
const liDom = $list[i]
            let doubanid = liDom.id

 功能九 预告片实现

去有预告片素材的网页爬取预告片数据

又是一个低级的错误:TypeError: Assignment to constant variable.

把变量赋给了常量,因为没有写let关键词...

 除了没获取到视频背景图片其他都拿到了

const puppeteer = require('puppeteer')
const url = 'https://movie.douban.com/coming'
module.exports = async() => {
    const browser = await puppeteer.launch({
        headless: false
    })
    const page = await browser.newPage()
    await page.goto(url, { //去即将上映网页即有预告片的网页上进行爬取
        waitUntil: 'networkidle2'
    })
    const result = await page.evaluate(() => {
        let result = []
        const $trs = $('.coming_list>tbody>tr')
        for (let i = 0; i < $trs.length; i++) {
            const trDom = $trs[i]
            let num = parseInt($(trDom).find('td').last().html())
            if (num > 20000) {
                let href = $(trDom).find('a').attr('href')
                result.push(href)
            }
        }
        return result //result里放着各个电影的详情网址
    })
    let arr = []
    for (var i = 0; i < result.length; i++) {
        let hrefurl = result[i]
        await page.goto(hrefurl, { //在电影详情页里爬更多数据
            waitUntil: 'networkidle2'
        })
        let itemresult = await page.evaluate(() => {
            let title = $('[property="v:itemreviewed"]').html()
            let director = $('[rel="v:directedBy"]').html()
            let casts = []
            for (var j = 0; j < 3; j++) {
                casts.push($('[rel="v:starring"]')[j].innerText)
            }
            let genre = []
            const $genre = $('[property="v:genre"]')
            for (let j = 0; j < $genre.length; j++) {
                genre.push($genre[j].innerText)
            }
            const summary = $('[property="v:summary"]').html().replace(/\s+/g, '')
            const runtime = $('[property="v:runtime"]').html()
            const releaseDate = $('[property="v:initialReleaseDate"]')[0].innerText
            const href = $('.related-pic-video').attr('href') //爬取预告片网址
            const cover = $('.related-pic-video').attr('background-image') //这个好像没拿到
            return {
                title,
                director,
                casts,
                genre,
                summary,
                runtime,
                releaseDate,
                href,
                cover
            }
        })
        arr.push(itemresult)
    }
    //预告电影链接
    for (let i = 0; i < arr.length; i++) {
        let item = arr[i]
        let url = item.href
        await page.goto(url, {
            waitUntil: 'networkidle2'
        })
        item.link = await page.evaluate(() => { //将预告片地址挂载到每个{}里
            const link = $('video>source').attr('src')
            return link
        })
    }
    await browser.close()
    console.log(arr)
        //5.将浏览器关闭
    return arr
}

发送模板消息

sendmodelmes() { //发送模板消息
        return new Promise(async(resolve, reject) => {
            const ACCESS_TOKEN = await this.fetchAccessToken()
            url = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${ACCESS_TOKEN}`
            const result = await rp({ method: 'POST', url, json: true, body: modelstruct })
            resolve(result)
        })
    }

获取公众号用户列表

getuserlist() {
        return new Promise(async(resolve, reject) => {
            const ACCESS_TOKEN = await this.fetchAccessToken()
            url = `https://api.weixin.qq.com/cgi-bin/user/get?access_token=${ACCESS_TOKEN}`
            rp({ method: 'GET', url, json: true })
                .then(res => {
                    resolve(res)
                })
                .catch(err => {
                    reject('getuserlist方法出问题' + err)
                })
        })
    }

公众号页面素材

Scale  
https://2.flexiple.com/scale/home

Vektors 
https://www.vektors.pro/

Pixeltrue  
https://www.pixeltrue.com/free-illustrations

getillustrations 
https://www.getillustrations.com/illustration-packs

oblikstudio免费插画包 
https://gumroad.com/oblikstudioWeareSkribbl  
https://weareskribbl.com/

Aracreator  
https://www.aracreator.com/

Niceillustrations 
https://niceillustrations.com/free-illustrations/

Open Peeps
https://www.openpeeps.com/

VectorCreator
https://icons8.com/vector-creator

Fresh Folk
https://fresh-folk.com/

Iconscout
https://iconscout.com/free-illustrations

Draw Kit
https://www.drawkit.io/

Humaaans
https://www.humaaans.com

Gallery.manypixels
https://gallery.manypixels.co/

Mixkit Art 
https://mixkit.co/art/

Isoflat
https://isoflat.com/

IRA Design
https://iradesign.io/

Undraw
https://undraw.co/illustrations

Lukaszadam
https://lukaszadam.com/illustrations
设计网页

模板在线设计制作-稿定设计 (gaoding.com)

文档管理:工作台 · 语雀 (yuque.com)

 

继“Java开发微信朋友圈PC版系统-架构1.0”之后,debug这段时间日撸夜撸,终于赶在春节放假前给诸位带来了这一系统的架构2.0版本,特此分享给诸位进行学习,以掌握、巩固更多的技术栈以及项目和产品开发经验,同时也为即将到来的金三银四跳槽季做准备! 言归正传,下面仍然以问答的方式介绍下本门课程的相关内容! (1)问题一:这是一门什么样的课程? 很明显,本门课程是建立在架构1.0,即 第1门课程 的基础上发布的,包含了架构1.0的内容,即它仍然是一门项目、产品实战课,基于Spring Boot2.X + 分布式中间件开发的一款类似“新浪微博”、“QQ空间”、“微信朋友圈”PC版的互联网社交软件,包含完整的门户网前端 以及 后台系统管理端,可以说是一套相当完整的系统! (2)问题二:架构2.0融入了哪些新技术以及各自有什么作用? 本课程对应着系统架构2.0,即第2阶段,主要目标:基于架构1.0,优化系统的整体性能,实现一个真正的互联网社交产品;其中,可以学习到的技术干货非常多,包括:系统架构设计、Spring Boot2.X、缓存Redis、多线程并发编程、消息中间件RabbitMQ、全文搜索引擎Elastic Search、前后端消息实时通知WebSocket、分布式任务调度中间件Elastic Job、Http Restful编程、Http通信OKHttp3、分布式全局唯一ID、雪花算法SnowFlake、注册中心ZooKeeper、Shiro+Redis 集群Session共享、敏感词自动过滤、Java8 等等; A.  基于Elastic Search实现首页列表数据的初始化加载、首页全文检索;B.  基于缓存Redis缓存首页朋友圈“是否已点赞、收藏、关注、评论、转发”等统计数据;整合Shiro实现集群部署模式下Session共享;C.  多线程并发编程并发处理系统产生的废弃图片、文件数据;D.  基于Elastic Job切片作业调度分布式多线程清理系统产生的废弃图片;E.  基于RabbitMQ解耦同步调用的服务模块,实现服务模块之间异步通信;F.  基于WebSocket实现系统后端 与 首页前端 当前登录用户实时消息通知;G.  基于OKHttp3、Restful风格的Rest API实现ES文档、分词数据存储与检索;H.  分布式全局唯一ID 雪花算法SnowFlake实现朋友圈图片的唯一命名;I.  ZooKeeper充当Elastic Job创建的系统作业的注册中心;J.  为塑造一个健康的网络环境,对用户发的朋友圈、评论、回复内容进行敏感词过滤;K.  大量优雅的Java8  Lambda编程、Stream编程;  (3)问题三:系统运行起来有效果图看吗?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值