微信公众平台开发教程(五)详解自定义菜单


Posted in Javascript onDecember 02, 2016

一、概述:

如果只有输入框,可能太简单,感觉像命令行。自定义菜单,给我们提供了很大的灵活性,更符合用户的操作习惯。在一个小小的微信对话页面,可以实现更多的功能。菜单直观明了,不仅能提供事件响应,还支持URL跳转,如果需要的功能比较复杂,我们大可以使用URL跳转,跳转至我们的网页即可。

注意:自定义菜单,只有服务号才有此功能

接着我们详细介绍,如何实现自定义菜单?

二、详细步骤:

1、首先获取access_token

access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token。正常情况下access_token有效期为7200秒,重复获取将导致上次获取的access_token失效。

公众号可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在开发模式中获得(需要已经成为开发者,且帐号没有异常状态)。注意调用所有微信接口时均需使用https协议。

接口调用请求说明

http请求方式: GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

参数说明

参数 是否必须 说明
grant_type 获取access_token填写client_credential
appid 第三方用户唯一凭证
secret 第三方用户唯一凭证密钥,既appsecret

返回说明

正常情况下,微信会返回下述JSON数据包给公众号:

{"access_token":"ACCESS_TOKEN","expires_in":7200}
参数 说明
access_token 获取到的凭证
expires_in 凭证有效时间,单位:秒

返回说明

正常情况下,微信会返回下述JSON数据包给公众号:

{"access_token":"ACCESS_TOKEN","expires_in":7200}

参数 说明
access_token 获取到的凭证
expires_in 凭证有效时间,单位:秒

错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):

{"errcode":40013,"errmsg":"invalid appid"}

2、创建自定义菜单

自定义菜单能够帮助公众号丰富界面,让用户更好更快地理解公众号的功能。开启自定义菜单后,公众号界面如图所示:

微信公众平台开发教程(五)详解自定义菜单

目前自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。一级菜单最多4个汉字,二级菜单最多7个汉字,多出来的部分将会以“...”代替。请注意,创建自定义菜单后,由于微信客户端缓存,需要24小时微信客户端才会展现出来。建议测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。

目前自定义菜单接口可实现两种类型按钮,如下:

click:

用户点击click类型按钮后,微信服务器会通过消息接口推送消息类型为event 的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值与用户进行交互;

view:

用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的url值 (即网页链接),达到打开网页的目的,建议与网页授权获取用户基本信息接口结合,获得用户的登入个人信息。

接口调用请求说明

http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN

请求示例

{
  "button":[
  { 
   "type":"click",
   "name":"今日歌曲",
   "key":"V1001_TODAY_MUSIC"
  },
  {
   "type":"click",
   "name":"歌手简介",
   "key":"V1001_TODAY_SINGER"
  },
  {
   "name":"菜单",
   "sub_button":[
   { 
    "type":"view",
    "name":"搜索",
    "url":"http://www.soso.com/"
   },
   {
    "type":"view",
    "name":"视频",
    "url":"http://v.qq.com/"
   },
   {
    "type":"click",
    "name":"赞一下我们",
    "key":"V1001_GOOD"
   }]
  }]
 }

参数说明

参数 是否必须 说明
button 一级菜单数组,个数应为1~3个
sub_button 二级菜单数组,个数应为1~5个
type 菜单的响应动作类型,目前有click、view两种类型
name 菜单标题,不超过16个字节,子菜单不超过40个字节
key click类型必须 菜单KEY值,用于消息接口推送,不超过128字节
url view类型必须 网页链接,用户点击菜单可打开链接,不超过256字节

 返回结果

正确时的返回JSON数据包如下:

{"errcode":0,"errmsg":"ok"}

错误时的返回JSON数据包如下(示例为无效菜单名长度):

{"errcode":40018,"errmsg":"invalid button name size"}

3、查询菜单

使用接口创建自定义菜单后,开发者还可使用接口查询自定义菜单的结构。

请求说明

http请求方式:GET
https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN

返回说明

对应创建接口,正确的Json返回结果:

{"menu":{"button":[{"type":"click","name":"今日歌曲","key":"V1001_TODAY_MUSIC","sub_button":[]},{"type":"click","name":"歌手简介","key":"V1001_TODAY_SINGER","sub_button":[]},{"name":"菜单","sub_button":[{"type":"view","name":"搜索","url":"http://www.soso.com/","sub_button":[]},{"type":"view","name":"视频","url":"http://v.qq.com/","sub_button":[]},{"type":"click","name":"赞一下我们","key":"V1001_GOOD","sub_button":[]}]}]}}

4、删除菜单

使用接口创建自定义菜单后,开发者还可使用接口删除当前使用的自定义菜单。

请求说明

http请求方式:GET
https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN

返回说明
对应创建接口,正确的Json返回结果:

{"errcode":0,"errmsg":"ok"}

5、事件处理

用户点击自定义菜单后,如果菜单按钮设置为click类型,则微信会把此次点击事件推送给开发者,注意view类型(跳转到URL)的菜单点击不会上报。

推送XML数据包示例:

<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[CLICK]]></Event>
<EventKey><![CDATA[EVENTKEY]]></EventKey>
</xml>

参数说明:

参数 描述
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,CLICK
EventKey 事件KEY值,与自定义菜单接口中KEY值对应

三、实例讲解

还接着上一篇文章讲。微信公众账号开发教程(三) 实例入门:机器人(附源码)

我们将在上一篇文章基础上,添加自定义菜单功能。

1、获取access_token

首先需要得到AppId和AppSecret

当你成为开发者后,自然能够在,开发者模式,便可看到这两个值,可以重置。

然后调用按照二.1中所示,进行操作。

注意:access_token有过期时间,如果过期,需要重新获取。

代码如下:

private static DateTime GetAccessToken_Time;
  /// <summary>
  /// 过期时间为7200秒
  /// </summary>
  private static int Expires_Period = 7200;
  /// <summary>
  /// 
  /// </summary>
  private static string mAccessToken;
  /// <summary>
  /// 
  /// </summary>
  public static string AccessToken
  {
   get
   {
    //如果为空,或者过期,需要重新获取
    if (string.IsNullOrEmpty(mAccessToken) || HasExpired())
    {
     //获取
     mAccessToken = GetAccessToken(AppID, AppSecret);
    }

    return mAccessToken;
   }
  }
  /// <summary>
  /// 
  /// </summary>
  /// <param name="appId"></param>
  /// <param name="appSecret"></param>
  /// <returns></returns>
  private static string GetAccessToken(string appId, string appSecret)
  {
   string url = string.Format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}", appId, appSecret);
   string result = HttpUtility.GetData(url);

   XDocument doc = XmlUtility.ParseJson(result, "root");
   XElement root = doc.Root;
   if (root != null)
   {
    XElement access_token = root.Element("access_token");
    if (access_token != null)
    {
     GetAccessToken_Time = DateTime.Now;
     if (root.Element("expires_in")!=null)
     {
      Expires_Period = int.Parse(root.Element("expires_in").Value);
     }
     return access_token.Value;
    }
    else
    {
     GetAccessToken_Time = DateTime.MinValue;
    }
   }

   return null;
  }
  /// <summary>
  /// 判断Access_token是否过期
  /// </summary>
  /// <returns>bool</returns>
  private static bool HasExpired()
  {
   if (GetAccessToken_Time != null)
   {
    //过期时间,允许有一定的误差,一分钟。获取时间消耗
    if (DateTime.Now > GetAccessToken_Time.AddSeconds(Expires_Period).AddSeconds(-60))
    {
     return true;
    }
   }
   return false;
  }

2、设置菜单

菜单需根据需要,按照实际要求进行设定。

这里我们提供天气查询功能,将常用的城市列出来,点击即可查询。

然后还提供了友情链接,这里提供了view类型的菜单,直接可以跳转至URL页面,为跳转做个好的演示。

具体菜单如下:

{
 "button": [
  {
   "name": "链接", 
   "sub_button": [
    {
     "type": "view", 
     "name": "搜索", 
     "url": "http://www.baidu.com/"
    }, 
    {
     "type": "view", 
     "name": "视频", 
     "url": "http://v.qq.com/"
    }, 
    {
     "type": "click", 
     "name": "赞一下我们", 
     "key": "BTN_GOOD"
    }
   ]
  }, 
  {
   "name": "查询天气", 
   "sub_button": [
    {
     "type": "click", 
     "name": "武汉", 
     "key": "BTN_TQ_WUHAN"
    }, 
    {
     "type": "click", 
     "name": "上海", 
     "key": "BTN_TQ_SHANGHAI"
    }, 
    {
     "type": "click", 
     "name": "北京", 
     "key": "BTN_TQ_BEIJING"
    }
   ]
  }, 
  {
   "type": "click", 
   "name": "帮助", 
   "key": "BTN_HELP"
  }
 ]
}

3、管理菜单

因为菜单的变更没有那么频繁,因此通过txt文件来设置菜单,并通过管理界面来管理菜单。

主要的管理功能:

1)从文件加载菜单

2)创建菜单。即将菜单通知微信服务端,并更新至微信客户端

3)查询菜单。获取当前系统的菜单。

4)删除菜单。从微信服务器删除菜单,也可以删除后再创建。

实现代码如下:

public class MenuManager
 {
  /// <summary>
  /// 菜单文件路径
  /// </summary>
  private static readonly string Menu_Data_Path = System.AppDomain.CurrentDomain.BaseDirectory + "/Data/menu.txt";
  /// <summary>
  /// 获取菜单
  /// </summary>
  public static string GetMenu()
  {
   string url = string.Format("https://api.weixin.qq.com/cgi-bin/menu/get?access_token={0}", Context.AccessToken);

   return HttpUtility.GetData(url);
  }
  /// <summary>
  /// 创建菜单
  /// </summary>
  public static void CreateMenu(string menu)
  {
   string url = string.Format("https://api.weixin.qq.com/cgi-bin/menu/create?access_token={0}", Context.AccessToken);
   //string menu = FileUtility.Read(Menu_Data_Path);
   HttpUtility.SendHttpRequest(url, menu);
  }
  /// <summary>
  /// 删除菜单
  /// </summary>
  public static void DeleteMenu()
  {
   string url = string.Format("https://api.weixin.qq.com/cgi-bin/menu/delete?access_token={0}", Context.AccessToken);
   HttpUtility.GetData(url);
  }
  /// <summary>
  /// 加载菜单
  /// </summary>
  /// <returns></returns>
  public static string LoadMenu()
  {
   return FileUtility.Read(Menu_Data_Path);
  }
 }

 4、基本方法

上面的代码,其实我们对一些公共功能做了封装。如进行get请求、POST提交等操作,读取文件等。

这里我们提供进行get、Post提交的方法案例代码,如果使用,建议优化。

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Web;

namespace Yank.WeiXin.Robot.Utility
{
 /// <summary>
 /// Http帮助类
 /// </summary>
 class HttpUtility
 {
  /// <summary>
  /// 发送请求
  /// </summary>
  /// <param name="url">Url地址</param>
  /// <param name="data">数据</param>
  public static string SendHttpRequest(string url, string data)
  {
   return SendPostHttpRequest(url,"application/x-www-form-urlencoded",data); 
  }
  /// <summary>
  /// 
  /// </summary>
  /// <param name="url"></param>
  /// <returns></returns>
  public static string GetData(string url)
  {
   return SendGetHttpRequest(url,"application/x-www-form-urlencoded"); 
  }

  /// <summary>
  /// 发送请求
  /// </summary>
  /// <param name="url">Url地址</param>
  /// <param name="method">方法(post或get)</param>
  /// <param name="method">数据类型</param>
  /// <param name="requestData">数据</param>
  public static string SendPostHttpRequest(string url,string contentType,string requestData)
  {
   WebRequest request = (WebRequest)HttpWebRequest.Create(url);
   request.Method = "POST";
   byte[] postBytes = null;
   request.ContentType = contentType;
   postBytes = Encoding.UTF8.GetBytes(requestData);
   request.ContentLength = postBytes.Length;
   using (Stream outstream = request.GetRequestStream())
   {
    outstream.Write(postBytes, 0, postBytes.Length);
   }
   string result = string.Empty;
   using (WebResponse response = request.GetResponse())
   {
    if (response != null)
    {     
     using (Stream stream = response.GetResponseStream())
     {
      using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
      {
       result = reader.ReadToEnd();
      }
     }
     
    }
   }
   return result;
  }

  /// <summary>
  /// 发送请求
  /// </summary>
  /// <param name="url">Url地址</param>
  /// <param name="method">方法(post或get)</param>
  /// <param name="method">数据类型</param>
  /// <param name="requestData">数据</param>
  public static string SendGetHttpRequest(string url, string contentType)
  {
   WebRequest request = (WebRequest)HttpWebRequest.Create(url);
   request.Method = "GET";
   request.ContentType = contentType;
   string result = string.Empty;
   using (WebResponse response = request.GetResponse())
   {
    if (response != null)
    {
     using (Stream stream = response.GetResponseStream())
     {
      using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
      {
       result = reader.ReadToEnd();
      }
     }
    }
   }
   return result;
  }
 }
}
using System;
using System.Xml.Linq;
using Newtonsoft.Json;

namespace Yank.WeiXin.Robot.Utility
{
 class XmlUtility
 {
  /// <summary>
  /// 
  /// </summary>
  /// <param name="json"></param>
  /// <param name="rootName"></param>
  /// <returns></returns>
  public static XDocument ParseJson(string json,string rootName)
  {
   return JsonConvert.DeserializeXNode(json, rootName);
  }
 }
}

5、事件处理

设置了菜单,这下需要处理事件了。跟我们之前设计ASPX或者WinForm一样,都要绑定按钮的事件。这里只是通过XML消息将请求传递过来。

通过“2、设置菜单"中具体的菜单内容,我们便已经知道需要进行哪些事件处理了。对于按钮类型为view的,无须处理,它会自动跳转至指定url.

需要处理的点击事件:

1)赞一下

2)查询某城市的天气,北京、上海、武汉

3)帮助

这个还要沿用上章中的事件处理器EventHandler来扩展处理。

具体的实现代码吧:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Yank.WeiXin.Robot.Messages;

namespace Yank.WeiXin.Robot.Handlers
{
 class EventHandler : IHandler
 {
  /// <summary>
  /// 请求的xml
  /// </summary>
  private string RequestXml { get; set; }
  /// <summary>
  /// 构造函数
  /// </summary>
  /// <param name="requestXml"></param>
  public EventHandler(string requestXml)
  {
   this.RequestXml = requestXml;
  }
  /// <summary>
  /// 处理请求
  /// </summary>
  /// <returns></returns>
  public string HandleRequest()
  {
   string response = string.Empty;
   EventMessage em = EventMessage.LoadFromXml(RequestXml);
   if (em != null)
   {
    switch (em.Event.ToLower())
    {
     case ("subscribe"):
      response = SubscribeEventHandler(em);
      break;
     case "click":
      response = ClickEventHandler(em);
      break;
    }
   }

   return response;
  }
  /// <summary>
  /// 关注
  /// </summary>
  /// <param name="em"></param>
  /// <returns></returns>
  private string SubscribeEventHandler(EventMessage em)
  {
   //回复欢迎消息
   TextMessage tm = new TextMessage();
   tm.ToUserName = em.FromUserName;
   tm.FromUserName = em.ToUserName;
   tm.CreateTime = Common.GetNowTime();
   tm.Content = "欢迎您关注***,我是大哥大,有事就问我,呵呵!\n\n";
   return tm.GenerateContent();
  }
  /// <summary>
  /// 处理点击事件
  /// </summary>
  /// <param name="em"></param>
  /// <returns></returns>
  private string ClickEventHandler(EventMessage em)
  {
   string result = string.Empty;
   if (em != null && em.EventKey != null)
   {
    switch (em.EventKey.ToUpper())
    {
     case "BTN_GOOD":
      result = btnGoodClick(em);
      break;
     case "BTN_TQ_BEIJING":
      result = searchWeather("beijing", em);
      break;
     case "BTN_TQ_SHANGHAI":
      result = searchWeather("shanghai", em);
      break;
     case "BTN_TQ_WUHAN":
      result = searchWeather("wuhai", em);
      break;
     case "BTN_HELP":
      result = btnHelpClick(em);
      break;
    }
   }

   return result;
  }
  /// <summary>
  /// 赞一下
  /// </summary>
  /// <param name="em"></param>
  /// <returns></returns>
  private string btnGoodClick(EventMessage em)
  {
   //回复欢迎消息
   TextMessage tm = new TextMessage();
   tm.ToUserName = em.FromUserName;
   tm.FromUserName = em.ToUserName;
   tm.CreateTime = Common.GetNowTime();
   tm.Content = @"谢谢您的支持!";
   return tm.GenerateContent();
  }
  /// <summary>
  /// 帮助
  /// </summary>
  /// <param name="em"></param>
  /// <returns></returns>
  private string btnHelpClick(EventMessage em)
  {
   //回复欢迎消息
   TextMessage tm = new TextMessage();
   tm.ToUserName = em.FromUserName;
   tm.FromUserName = em.ToUserName;
   tm.CreateTime = Common.GetNowTime();
   tm.Content = @"查询天气,输入tq 城市名称\拼音\首字母";
   return tm.GenerateContent();
  }
  /// <summary>
  /// 查询天气
  /// </summary>
  /// <param name="cityName"></param>
  /// <param name="em"></param>
  /// <returns></returns>
  private string searchWeather(string cityName, EventMessage em)
  {
   TextMessage tm = new TextMessage();
   tm.Content = WeatherHelper.GetWeather(cityName);
   //进行发送者、接收者转换
   tm.ToUserName = em.FromUserName;
   tm.FromUserName = em.ToUserName;
   tm.CreateTime = Common.GetNowTime();
   return tm.GenerateContent();
  }
 }
}

6、效果图

终于大工告成,最后来看下效果图吧

微信公众平台开发教程(五)详解自定义菜单

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
拖动一个HTML元素
Dec 22 Javascript
IE中document.createElement的iframe无法设置属性name的解决方法
Sep 14 Javascript
基于javascript实现漂亮的页面过渡动画效果附源码下载
Oct 26 Javascript
web前端开发upload上传头像js示例代码
Oct 22 Javascript
详解微信小程序——自定义圆形进度条
Dec 29 Javascript
微信小程序 image组件binderror使用例子与js中的onerror区别
Feb 15 Javascript
详解用vue-cli来搭建vue项目和webpack
Apr 20 Javascript
Vuejs在v-for中,利用index来对第一项添加class的方法
Mar 03 Javascript
微信小程序开发之改变data中数组或对象的某一属性值
Jul 05 Javascript
vue实现登录页面的验证码以及验证过程解析(面向新手)
Aug 02 Javascript
JS数组方法slice()用法实例分析
Jan 18 Javascript
vue开发中遇到的问题总结
Apr 07 Javascript
基于jQuery实现滚动切换效果
Dec 02 #Javascript
遍历json 对象的属性并且动态添加属性的实现
Dec 02 #Javascript
Vue.js组件tree实现无限级树形菜单
Dec 02 #Javascript
js中的eval()函数把含有转义字符的字符串转换成Object对象的方法
Dec 02 #Javascript
Vue.js第四天学习笔记
Dec 02 #Javascript
微信公众平台开发教程(四) 实例入门:机器人回复(附源码)
Dec 02 #Javascript
基于javascript实现按圆形排列DIV元素(三)
Dec 02 #Javascript
You might like
一个可以找出源代码中所有中文的工具
2006/10/25 PHP
php下获取http状态的实现代码
2014/05/09 PHP
PHP配置把错误日志以邮件方式发送方法(Windows系统)
2015/06/23 PHP
CI框架文件上传类及图像处理类用法分析
2016/05/18 PHP
Laravel框架路由和控制器的绑定操作方法
2018/06/12 PHP
JavaScript加强之自定义callback示例
2013/09/21 Javascript
jQuery向上遍历DOM树之parents(),parent(),closest()之间的区别
2013/12/02 Javascript
javascript二维数组转置实例
2015/01/22 Javascript
jQuery实现在最后一个元素之前插入新元素的方法
2015/07/18 Javascript
Node.js文件操作方法汇总
2016/03/22 Javascript
JavaScript中Form表单技术汇总(推荐)
2016/06/26 Javascript
JS中判断null的方法分析
2016/11/21 Javascript
BootStrap Table 设置height表头与内容无法对齐的问题
2016/12/28 Javascript
bootstrap下拉菜单使用方法解析
2017/01/13 Javascript
bootstrap中的 form表单属性role=&quot;form&quot;的作用详解
2017/01/20 Javascript
实现微信小程序的wxml文件和wxss文件在webstrom的支持
2017/06/12 Javascript
解决JSON.stringify()自动将中文转译成unicode的问题
2018/01/05 Javascript
vue动画效果实现方法示例
2019/03/18 Javascript
原生JavaScript实现滑动拖动验证的示例代码
2019/12/06 Javascript
[01:07:02]DOTA2-DPC中国联赛 正赛 iG vs PSG.LGD BO3 第三场 2月26日
2021/03/11 DOTA
scrapy自定义pipeline类实现将采集数据保存到mongodb的方法
2015/04/16 Python
Python中tell()方法的使用详解
2015/05/24 Python
Python 3.x 连接数据库示例(pymysql 方式)
2017/01/19 Python
python自动化测试无法启动谷歌浏览器问题
2019/10/10 Python
关于Flask项目无法使用公网IP访问的解决方式
2019/11/19 Python
python词云库wordCloud使用方法详解(解决中文乱码)
2020/02/17 Python
《忆江南》教学反思
2014/04/07 职场文书
市级文明单位申报材料
2014/05/07 职场文书
企业贷款委托书格式
2014/09/12 职场文书
商品陈列协议书
2014/09/29 职场文书
教学督导岗位职责
2015/04/10 职场文书
初中军训感想
2015/08/07 职场文书
创业计划书之韩国烧烤店
2019/09/19 职场文书
Python面向对象之内置函数相关知识总结
2021/06/24 Python
SpringBoot+Redis实现布隆过滤器的示例代码
2022/03/17 Java/Android
Python matplotlib安装以及实现简单曲线的绘制
2022/04/26 Python