微信公众号学习

时间:2022-01-30 21:02:48

一、微信公众号设计思路

微信公众号学习

二、微信公众号的分类

1、订阅号

1.简介 为媒体和个人提供一种新的信息传播方式,主要功能是在微信侧给用户传达资讯;(功能类似报纸杂志,提供新闻信息或娱乐趣事)

2.适用主要人群:个人、媒体。

3.群发次数 订阅号(认证用户、非认证用户)1天内可群发1条消息。

2、服务号

1.简介 为企业和组织提供更强大的业务服务与用户管理能力,主要偏向服务类交互(功能类似银行,12315,114等)

2.适用主要人群:企业、*或其他组织。

3.群发次数 服务号1个月(按自然月)内可发送4条群发消息。

三、微信公众号的开发

1、验证服务器的合法性

①测试账号的使用

开发/开发者工具 => 开发者文档 => 开始开发/接口测试号申请 => 进入微信公众帐号测试号申请系统

填写URL和Token

②验证服务器地址的有效性

①将token、timestamp、nonce三个参数进行字典序排序

②将三个参数字符串拼接成一个字符串进行sha1加密

③开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

 ()=>{
  return async(req,res,next) =>{
  const {signature,echostr,timestamp,nonce}=rquery.query\
  //获取自己配置的token
  const {token}=config
  const sha1Str= sha1([timestamp,nonce,token].sort().join(""))
  if(req.mothod==='GET'){
  if(sha1Str===signature){
  res.send(echostr)
  }else{
  res.send("error")
  }
  }
  }
 }

2、获取access_token

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。

2小时需要更新一次,提前5分钟刷新

 /*
  用来获取access_token
  */
 getAccessToken() {
    const url = `${api.access_token}&appid=${appID}&secret=${appsecret}`
    return new Promise((resolve, reject) => {
        rp({method: "GET", url, json: true}).then(res => {
            /*
            access_token: '23_E3GhXnvYKh6xxdPq0e5ECFgXI4DeU2FNNGbQUKRX1O-4a0KrVJGduEwnBgul730-9qEkxqvnE2KE9YPxUrNVIzvcnlBfTqapB2HQGw840fjoGXs3L7UN08GrqQDOGV2UlX3P01lqKo0uLtcuTGWiAHAFSI',
            expires_in: 7200
              */
            res.expires_in = Date.now() + (res.expires_in - 300) * 1000
            resolve(res)
        }).catch(error => {
            reject("30:" + 'getAccessToken()方法出错:' + error)
        })
    })
 
 }
 
 /*
 保存access_token
  */
 saveAccessToken(access_token) {
    return writeFileAsync(access_token, 'accessToken.txt')
 }
 
 /*
 读取access_token
  */
 readAccessToken() {
    return readFileAsync('accessToken.txt')
 
 }
 
 /*
 用来检测access_token是否有效
  */
 isValidAccessToken(data) {
    if (!data || !data.access_token || !data.expires_in) {
        //代表access_token无效
        return false
    }
    if (data.expires_in < Date.now()) {
        //代表access过期
        return false
    }
    return true
 }
 
 /*
 获取没有过期的access_token
  */
 fetchAccessToken() {
 
    //优化
    if (this.access_token && this.expires_in && this.isValidAccessToken(this)) {
 
        //说明之前保存过access_token,并且它是有效的, 直接使用
        return Promise.resolve({
            access_token: this.access_token,
            expires_in: this.expires_in
        })
    }
    return new Promise((resolve, reject) => {
        this.readAccessToken()
            .then(async res => {
                //本地有文件,需要判断是否过期
                if (this.isValidAccessToken(res)) {
                    //有效的
                    // resolve(res)
                    return Promise.resolve(res)
                } else {
                    const res = await this.getAccessToken()
                    await this.saveAccessToken(res)
                    //返回access_token
                    // resolve(res)
                    return Promise.resolve(res)
                }
            })
            .catch(async err => {
                const res = await
                    this.getAccessToken()
                await this.saveAccessToken(res)
                //返回access_token
                //resolve(res)
                return Promise.resolve(res)
            })
            .then(res => {
                //将access_token挂载到this上
                this.access_token = res.access_token;
                this.expires_in = res.expires_in;
 
                //返回res包装了一层promise对象(此对象为成功的状态)
                //是this.readAccessToken()最终的返回值
                resolve(res);
            })
    })
 }

3、自动回复消息

 else if (req.method === 'POST') {
    if (sha1Str != signature) {
    //验证失败
    res.end("error")
    }
         
    const xmlData = await getUserDataAsync(req)
    //将xml解析为js
    const jsData = await parseXMLAsync(xmlData)
 
    const message = await formatMessage(jsData)
    const options = await reply(message)
    let replyMessage = template(options)
    res.send(replyMessage)
 
 }
 
 /*
  获取用户发送的消息
 */
 getUserDataAsync(req) {
    return new Promise((resolve, reject) => {
        let xmlData = "";
        req
            .on('data', data => {
                //当流式数据传递过来的时候,会触发当前事件,会将数据注入到回调函数汇总
                //data为buffer类型
                data = data.toString()
                xmlData += data;
            })
            .on('end', () => {
                //当数据接收完毕时,会触发当前函数
                resolve(xmlData)
            })
    })
 
 }  
 
 
 const {parseString} = require("xml2js")
 /*
  将xml转换为js
 */
 parseXMLAsync(xmlData) {
    return new Promise((resolve, reject) => {
        parseString(xmlData, {trim: true}, (err, data) => {
            if (!err) {
                resolve(data)
            } else {
                reject("parseXMLAsync有误:" + err)
            }
        })
    })
 }
 
 formatMessage(jsData) {
    return new Promise((resolve, reject) => {
        let message = {}
        jsData = jsData.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]
                }
            }
        }
        resolve(message)
    })
 }
 
 [reply.js]
 const Theaters = require('../model/Theaters');
 const Trailers = require('../model/Trailers');
 const {url, qiniuImgUrl} = require('../config')
 /*
  处理用户发送的消息类型和内容,决定返回不同的内容给用户
  */
 module.exports = message => {
 
    return new Promise(async (resolve, reject) => {
 
        let options = {
            toUserName: message.FromUserName,
            fromUserName: message.ToUserName,
            createTime: Date.now(),
            msgType: 'text'
        }
        let content = '您在说什么,我听不懂?';
 
        if (message.MsgType === 'text') {
            //用户发送文字信息
            if (message.Content === '预告片') {
                const data = await Trailers.find({}, {posterKey: 1, _id: 0})
                //将回复内容初始化为空数据
                content = []
                options.msgType = 'news'
                content.push({
                    title: "最新热门电影预告片",
                    description: "点击查看最近热门预告片",
                    picUrl: `${qiniuImgUrl}${data[0].posterKey}`,
                    url: `${url}/trailer`
                })
            }
        } else if (message.MsgType === 'voice') {
            options.msgType = 'voice'
            options.mediaId = message.MediaId
            const text = message.Recognition.toString().replace('。', "")
            console.log(text)
            const data = await Theaters.findOne({title: text}, {
                title: 1,
                summary: 1,
                doubanId: 1,
                image: 1,
                _id: 0
            })
            //将回复内容初始化为空数据
            content = []
            options.msgType = 'news'
            content.push({
                title: data.title,
                description: data.summary,
                picUrl: data.image,
                url: `${url}/detail/${data.doubanId}`
            })
        } else if (message.MsgType === 'event') {
            if (message.Event === 'subscribe') {
                options.msgType = 'text'
                content = '欢迎您关注XXX电影公众号~ \n' +
                    '回复 预告片 查看最新热门电影预告片 \n' +
                    '回复 文本 搜索电影信息 \n' +
                    '回复 语音 搜索电影信息 \n' +
                    '也可以点击下面菜单按钮,来了解XXX电影公众号'
            } else if (message.Event === 'unsubscribe') {
                options.msgType = 'event'
                content = '取关了哈哈'
            } else if (message.Event === 'CLICK') {
                content = '您可以按照以下提示来进行操作~ \n' +
                    '回复 预告片 查看最新热门电影预告片 \n' +
                    '回复 文本 搜索电影信息 \n' +
                    '回复 语音 搜索电影信息 \n' +
                    '也可以点击下面菜单按钮,来了解XXX电影公众号'
            }
        }
        options.content = content
        resolve(options)
    })
 
 }

4、上传素材

公众号经常有需要用到一些临时性的多媒体素材的场景,例如在使用接口特别是发送消息时,对多媒体文件、多媒体消息的获取和调用等操作,是通过media_id来进行的。素材管理接口对所有认证的订阅号和服务号开放。

①上传临时素材

 uploadTemporaryMeterial(type, fileName) {
    //获取文件的绝对路径
    const filePath = resolve(__dirname, '../media', fileName)
    return new Promise(async (resolve1, reject) => {
        try {
            //获取access_token
            const data = await this.fetchAccessToken()
            //定义请求的地址
            const url = `${api.temporary.upload}access_token=${data.access_token}&type=${type}`
 
            const formData = {
                media: createReadStream(fileName)
            }
            //以form表单的方式发送请求
            const result = rp({method: 'POST', url, json: true, formData})
            //将数据返回给用户
            resolve(result)
        } catch (e) {
            reject('uploadTemporaryMaterial方法出了问题:' + e);
        }
 
    })
 }

② 获取临时素材

 getTemporaryMeterial(type, mediaId, fileName) {
    //获取文件的绝对路径
    const filePath = resolve(__dirname, '../media', fileName);
    return new Promise(async (resolve1, reject) => {
        //获取access_token
        const data = await this.fetchAccessToken()
        //定义请求地址
        let url = `${api.temporary.get}access_token=${data.access_token}&media_id=${mediaId}`
        if (type === 'video') {
            //视频文件只支持http协议
            url = url.replace('https://', 'http://');
            //发送请求
            const data = await rp({method: 'GET', url, json: true});
            //返回出去
            resolve(data);
        } else {
            //其他类型文件
            request(url)
                .pipe(createWriteStream(filePath))
                .once('close', resolve)   //当文件读取完毕时,可读流会自动关闭,一旦关闭触发close事件,从而调用resolve方法通知外部文件读取完毕了
        }
    })
 }

③上传永久素材

 uploadPermanentMaterial (type, material, body) {
 
    return new Promise(async (resolve, reject) => {
        try {
            //获取access_token
            const data = await this.fetchAccessToken();
            //请求的配置对象
            let options = {
                method: 'POST',
                json: true
            }
 
            if (type === 'news') {
                //上传图文消息
                options.url = `${api.permanment.uploadNews}access_token=${data.access_token}`;
                options.body = material;
            } else if (type === 'pic') {
                //上传图文消息中的图片
                options.url = `${api.permanment.uploadImg}access_token=${data.access_token}`;
                options.formData = {
                    media: createReadStream(join(__dirname, '../media', material))
                }
            } else {
                //其他媒体素材的上传
                options.url = `${api.permanment.uploadOthers}access_token=${data.access_token}&type=${type}`;
                options.formData = {
                    media: createReadStream(join(__dirname, '../media', material))
                }
                //视频素材,需要多提交一个表单
                if (type === 'video') {
                    options.body = body;
                }
            }
 
            //发送请求
            const result = await rp(options);
            //将返回值返回出去
            resolve(result);
        } catch (e) {
            reject('uploadPermanentMaterial方法出了问题:' + e);
        }
 
    })
 }

④获取永久素材

  
 getPermanentMaterial (type, mediaId, fileName) {
    return new Promise(async (resolve, reject) => {
        try {
            //获取access_token
            const data = await this.fetchAccessToken();
            //定义请求地址
            const url = `${api.permanment.get}access_token=${data.access_token}`;
 
            const options = {method: 'POST', url, json: true, body: {media_id: mediaId}};
            //发送请求
            if (type === 'news' || 'video') {
                const data = await rp(options);
                resolve(data);
            } else {
                request(options)
                    .pipe(createWriteStream(join(__dirname, '../media', fileName)))
                    .once('close', resolve)
            }
        } catch (e) {
            reject('getPermanentMaterial方法出了问题:' + e);
        }
 
    })
 
 }

5、菜单

 /*
  菜单栏的组成
 */
 const {url}=require('../config')
 module.exports = {
    "button": [
        {
            "type":"view",
            "name":"预告片????",
            "url":`${url}/trailer`
        },
        {
            "type":"view",
            "name":"语音识别????",
            "url":`${url}/search`
        },
        {
            "name": "戳我????",
            "sub_button": [
                {
                    "type": "view",
                    "name": "官网☀",
                    "url": `https://www.baidu.com`
                },
                {
                    "type": "click",
                    "name": "帮助????",
                    "key": "help"
                }
            ]
        }]
 }
 
 
 /*
  创建菜单栏
  */
 createMenu() {
    return new Promise(async (resolve, reject) => {
        try {
            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)
        } catch (e) {
            reject("createMenu方法出错:" + e)
        }
    })
 }
 
 /*
  删除菜单栏
  */
 deleteMenu() {
    return new Promise(async (resolve, reject) => {
        try {
            const data = await this.fetchAccessToken();
            const url = `https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=${data.access_token}`
            const result = rp({method: 'GET', url, json: true})
            resolve(result)
        } catch (e) {
            reject("deleteMenu方法出错:" + e)
        }
 
    })
 
 }
 /*
  获取菜单的配置
 */
 getMenu () {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/menu/get?access_token=${res.access_token}`;
          rp({method: 'GET', json: true, url})
            .then(res => resolve(res))
            .catch(err => reject('getMenu方法出了问题:' + err))
        })
 }  
 /*
  创建自定义菜单
 */
 createMyMenu (body) {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/menu/addconditional?access_token=${res.access_token}`;
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject('createMyMenu方法出了问题:' + err))
      })
    })
 }
 /*
 删除自定义菜单
 */
 deleteMyMenu (body) {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/menu/delconditional?access_token=${res.access_token}`;
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject('deleteMyMenu方法出了问题:' + err))
        })
    })
 }
 
 /*
 测试个性化菜单匹配结果
 */
 testMyMenu (body) {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/menu/trymatch?access_token=${res.access_token}`;
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject(' testMyMenu方法出了问题:' + err))
        })
    })
 }

6、用户管理

 //创建标签
 createTag (body) {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/tags/create?access_token=${res.access_token}`;
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject('createTag方法出了问题:' + err))
        })
    })
 }
 
 //获取标签
 getTag () {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/tags/get?access_token=${res.access_token}`;
          rp({method: 'GET', json: true, url})
            .then(res => resolve(res))
            .catch(err => reject('getTag方法出了问题:' + err))
        })
    })
 }
 
 //更新标签
 updateTag (body) {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/tags/update?access_token=${res.access_token}`;
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject('updateTag方法出了问题:' + err))
        })
    })
 }
 
 //删除标签
 deleteTag (body) {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/tags/delete?access_token=${res.access_token}`;
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject('deleteTag方法出了问题:' + err))
        })
    })
 }
 
 //获取标签下的粉丝列表
 getTagUsers (body) {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/user/tag/get?access_token=${res.access_token}`;
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject('getTagUsers方法出了问题:' + err))
        })
    })
 }
 
 //批量为用户打标签
 batchUserTags (body) {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/tags/members/batchtagging?access_token=${res.access_token}`;
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject('batchUserTags方法出了问题:' + err))
        })
    })
 }
 
 //批量为用户取消标签
 unBatchUserTags (body) {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/tags/members/batchuntagging?access_token=${res.access_token}`;
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject('unBatchUserTags方法出了问题:' + err))
        })
    })
 }
 
 //获取用户下所有的标签
 getUserTags (body) {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/tags/getidlist?access_token=${res.access_token}`;
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject('getUserTags方法出了问题:' + err))
        })
    })
 }
 
 //获取所有用户列表
 getUsers (next_openid) {
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          let url = `https://api.weixin.qq.com/cgi-bin/user/get?access_token=${res.access_token}`;
          if (next_openid) {
            url += '&next_openid=' + next_openid;
          }
          rp({method: 'GET', json: true, url})
            .then(res => resolve(res))
            .catch(err => reject('getUsers方法出了问题:' + err))
        })
    })
 }
 

7、群发消息

 
 //定义根据标签群发消息
 sendAllByTag (type, tag_id, content, is_to_all = false, send_ignore_reprint = 0) {
    /*
      type: 媒体数据类型
      tag_id: 指定标签
      content: 媒体消息内容
      is_to_all: 是否保存历史消息记录中
      send_ignore_reprint:   图文消息被判定为转载时,是否继续群发。 1为继续群发(转载),0为停止群发。 该参数默认为0。
      */
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=${res.access_token}`;
          let body = {
            filter: {
              is_to_all,
              tag_id
            }
          }
          //判断群发的消息类型
          if (type === 'text') {
            body.text = {
              content
            }
          } else if (type === 'mpnews') {
            body.send_ignore_reprint = send_ignore_reprint;
            body[type] = {
              media_id: content
            }
          } else {
            body[type] = {
              media_id: content
            }
          }
          body.msgtype = type;
          console.log(body);
          //发送请求
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject('sendAllByTag方法出了问题:' + err))
        })
    })
 }
 
 //定义根据用户列表openid群发消息
 sendAllByUsers (type, openid_list, content, send_ignore_reprint = 0, title, description) {
    /*
      type: 媒体数据类型
      openid_list: 指定用户列表
      content: 媒体消息内容
      send_ignore_reprint:   图文消息被判定为转载时,是否继续群发。 1为继续群发(转载),0为停止群发。 该参数默认为0。
      title: 视频文件的标题
      description: 视频文件的描述
      */
    return new Promise((resolve, reject) => {
      this.fetchAccessToken()
        .then(res => {
          const url = `https://api.weixin.qq.com/cgi-bin/message/mass/send?access_token=${res.access_token}`;
          let body = {
            touser: openid_list
          }
          //判断群发的消息类型
          if (type === 'text') {
            body.text = {
              content
            }
          } else if (type === 'mpnews') {
            body.send_ignore_reprint = send_ignore_reprint;
            body[type] = {
              media_id: content
            }
          } else if (type === 'mpvideo') {
            body[type] = {
              media_id: content,
              title: title,
              description: description
            }
          } else {
            body[type] = {
              media_id: content
            }
          }
          body.msgtype = type;
          // console.log(body);
          //发送请求
          rp({method: 'POST', json: true, url, body})
            .then(res => resolve(res))
            .catch(err => reject('sendAllByUsers方法出了问题:' + err))
        })
    })
 }

四、微信公众号交互流程

微信公众号学习

五、JS-SDK

微信JS-SDK ( JavaScript Software Development Kit )是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包。

①获取jsapi-ticket

 /*
  用来获取jsapi_ticket
 */
 getJsapiTicket() {
    return new Promise(async (resolve, reject) => {
        const data = await this.fetchAccessToken()
        const url = `${api.ticket}&access_token=${data.access_token}`
        rp({method: "GET", url, json: true}).then(res => {
            /*
                "errcode":0,
                "errmsg":"ok",
                "ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
                "expires_in":7200
              */
            resolve({
                ticket: res.ticket,
                expires_in: Date.now() + (res.expires_in - 300) * 1000
            })
        }).catch(err => {
            reject("30:" + 'getJsapiTicket()方法出错:' + err)
        })
    })
 }
 
 /*
 保存jsapi_ticket
  */
 saveJsapiTicket(ticket) {
    return writeFileAsync(ticket, 'ticket.txt')
 }
 
 /*
 读取jsapi_ticket
  */
 readJsapiTicket() {
    return readFileAsync('ticket.txt')
 
 }
 
 /*
 用来检测jsapi_ticket是否有效
  */
 isValidJsapiTicket(data) {
    if (!data || !data.access_token || !data.expires_in) {
        //代表access_token无效
        return false
    }
    if (data.expires_in < Date.now()) {
        //代表access过期
        return false
    }
    return true
 }
 
 /*
 获取没有过期的jsapi_ticket
  */
 fetchJsapiTicket() {
 
    //优化
    if (this.ticket && this.ticket_expires_in && this.isValidJsapiTicket(this)) {
        //说明之前保存过access_token,并且它是有效的, 直接使用
        return Promise.resolve({
            ticket: this.ticket,
            expires_in: this.ticket_expires_in
        })
    }
    return new Promise((resolve, reject) => {
        this.readJsapiTicket()
            .then(async res => {
                //本地有文件,需要判断是否过期
                if (this.isValidJsapiTicket(res)) {
                    return Promise.resolve(res)
                } else {
                    const res = await
                        this.getJsapiTicket()
                    await this.saveJsapiTicket(res)
                    //返回access_token
                    // resolve(res)
                    return Promise.resolve(res)
                }
            })
            .catch(async err => {
                const res = await this.getJsapiTicket()
 
                await this.saveJsapiTicket(res)
                return Promise.resolve(res)
            })
            .then(res => {
                this.ticket = res.ticket
                this.ticket_expires_in = res.expires_in
                resolve(res)
            })
    })
 }
 
 writeFileAsync(data, fileName) {
    data = JSON.stringify(data)
    const filePath=resolve(__dirname,fileName)
    return new Promise((resolve, reject) => {
        writeFile(filePath, data, err => {
            if (!err) {
                resolve()
            } else {
                reject()
            }
        })
    })
 },
 readFileAsync(fileName) {
    const filePath=resolve(__dirname,fileName)
    return new Promise((resolve, reject) => {
        readFile(filePath, (err, data) => {
            if (!err) {
                data = JSON.parse(data)
                resolve(data)
            } else {
                reject('读取文件失败!' + err)
            }
        })
    })
 }    

②JS-SDK的使用

 [页面路由]
 router.get('/search', async (req, res) => {
 
    /*
        生成js-sdk使用的签名:
            1、需要的参数jsapi_ticket(临时票据)、noncestr(随机字符串),timestamp(时间戳)、url(当前服务器的地址)
            2、将其进行字典序排序,以'&'拼接在一起
            3、进行sha1加密
      */
    //获取随机字符串
    const noncestr = Math.random().toString().split('.')[1]
    //获取时间戳
    const timestamp = Date.now()
    const {ticket} = await wechatApi.fetchJsapiTicket();
    const arr = [
        `jsapi_ticket=${ticket}`,
        `noncestr=${noncestr}`,
        `timestamp=${timestamp}`,
        `url=${url}/search`
    ]
    const str = arr.sort().join('&');
    const signature = sha1(str);
    res.render('search', {
        signature, noncestr, timestamp
    })
 })
 
 [录音和录音识别功能的实现]
 <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
  <script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
  <script type="text/javascript">
    /*
      1. 绑定域名
        - 在接口测试号页面上填写js安全域名接口
      2. 引入js文件
        - http://res.wx.qq.com/open/js/jweixin-1.2.0.js
      3. 通过config接口注入权限验证配置
      */
 
    window.onload = function () {
      wx.config({
        debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
        appId: 'wx4bbacf0b506f1309', // 必填,公众号的唯一标识
        timestamp: '<%= timestamp %>', // 必填,生成签名的时间戳
        nonceStr: '<%= noncestr %>', // 必填,生成签名的随机串
        signature: '<%= signature %>',// 必填,签名
        jsApiList: [
          'onMenuShareQQ',
          'onMenuShareQZone',
          'startRecord',
          'stopRecord',
          'translateVoice'
        ] // 必填,需要使用的JS接口列表
      });
 
      //测试开发时使用
      /*wx.checkJsApi({
        jsApiList: ['onMenuShareQQ',
          'onMenuShareQZone',
          'startRecord',
          'stopRecord',
          'translateVoice'], // 需要检测的JS接口列表,所有JS接口列表见附录2,
        success: function(res) {
          // 以键值对的形式返回,可用的api值true,不可用为false
          // 如:{"checkResult":{"chooseImage":true},"errMsg":"checkJsApi:ok"}
          console.log(res);
        },
        fail: function (err) {
          console.log(err);
        }
      });*/
 
      //config信息验证后会执行ready方法
      wx.ready(function(){
        // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
        //标志位:正在录音
        var isRecord = false;
        //绑定事件监听
        document.getElementById('btn').addEventListener('touchend', function () {
          if (isRecord) {
            //目前状态是正在录音中,结束录音
            wx.stopRecord({
              success: function (res) {
                var localId = res.localId; //它会自动将录音上传到微信服务器中,返回一个id来标识录音文件
                //语音识别
                wx.translateVoice({
                  localId: localId, // 需要识别的音频的本地Id,由录音相关接口获得
                  isShowProgressTips: 1, // 默认为1,显示进度提示
                  success: function (res) {
                    console.log(res.translateResult);
                    isRecord = false;
                    const url='/search/byName'
                    //查询相应的电影信息
                    $.get(url, {reqData:res.translateResult},function (data) {
                        console.log('查询的数据:'+data)                      
                      //   //分享功能
                      //   //默认情况下可以分享,分享以后用户看图文消息,没有图片,消息标题是链接
                      //   //使用微信分享接口,就可以自己设置图片,设置标题、描述
                        wx.onMenuShareQQ({
                          title: data.subjects[0].title, // 分享标题
                          desc: `评分:${data.subjects[0].rating.average}`, // 分享描述
                          link: data.subjects[0].alt, // 分享链接
                          imgUrl: data.subjects[0].images.small, // 分享图标
                          success: function () {
                            // 用户确认分享后执行的回调函数
                            alert('分享成功');
                          },
                          cancel: function () {
                            // 用户取消分享后执行的回调函数
                            alert('分享失败');
                          }
                        });
                   
                      // } else {
                      //   alert('暂时没有相关的电影信息');
                      // }
                    })
 
                  }
                });
              }
            });
          } else {
            //开始录音
            wx.startRecord();
            isRecord = true;
          }
 
        })
      });
 
      //config信息验证失败会执行error函数
      wx.error(function(res){
        // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
      });
    }
 
 
 
 
 
  </script>