前言
现在的微信用户太强大了,公司的一些运营的活动啊,总是希望能够定制微信的自定义分享,但是需要jsSDK
,我们又没做公众号开发,只能闲暇的时候看看怎么弄了。
以前还能投机取巧些说分享图片取第一张图片,现在微信升级之后,好像是在6.8.5版本之后,如果不及接入jsSDK的话,就默认使用微信官方的一个锁的那个默认图,怎么说呢,体验会不是很好。
申请微信公众号
公众号分什么订阅号,服务号,个人号,企业号,不同需求不同功能嘛,都有开发者模式。但是我作为个人开发者是申请不到服务号的,怎么办呢,腾讯微我们准备了测试账号,通过这个账号,我们可以获得微信服务号的所有功能和接口调用权限。
申请地址
申请之后你会拿到一个appid
和appsecret
,这个其实差不多就是你的公众号的秘钥一样,在拿access_token
需要用到的。
准备工作
其实微信官方文档说的很清楚。
微信是一个app,我们h5是一个网页,那我们怎么在app里面分享、拍照上传等动作呢,这时候就需要一个h5和app的桥,叫jsSDK。那使用这个jsSDK的前提是什么?
步骤一:绑定域名
登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。
备注:登录后可在“开发者中心”查看对应的接口权限。
测试账号的话直接拉倒下面就可以看到填写得地方了。注意,我这里卡了好久。首先,先解释下什么叫二级域名
这里我举个例子就能明白了:
- .com 顶级域名
- baidu.com 一级域名
- www.baidu.com 二级域名
- bbs.baidu .com 二级域名
- tieba.baidu .com 二级域名
设置js安全域名后,公众号开发者可以在该域名下调用微信的jsSDK,js接口的安全域名要求是一级或一级以上,可以填写三个。
如果调用js的域名是二级域名,而在JS接口安全域名里面没有配置该二级域名,那么可以直接配置成主域名。比如二级域名是weixin.test.com,那么JS接口安全域名可以配置成test.com
我个人建议是填写一级域名,因为这么多的运营活动,你才三个坑。
另外截止到现在为止,公众平台接口调用仅支持80端口。
步骤二:引入JS文件
在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.2.0.js
备注:支持使用 AMD/CMD 标准模块加载方法加载
步骤三:通过config接口注入权限验证配置
所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支持pushState的H5新特性,所以使用pushState来实现web app的页面会导致签名失败,此问题会在Android6.2中修复)。
1 2 3 4 5 6 7 8
| wx.config({ debug: true, appId: '', timestamp: '', nonceStr: '', signature: '', jsApiList: [] });
|
等等,上面的这些啥玩意儿?
appId
就是上面提到的,timestamp时间戳自己生成的,nonceStr一个随机字符串自己生成的,signature签名,这怎么来?
签名算法
签名生成规则如下:
参与签名的字段包括noncestr
(随机字符串), 有效的jsapi_ticket
, timestamp
(时间戳), url
(当前网页的URL,不包含#及其后面部分) 。
对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。
这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。
即signature=sha1(string1)
。 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| noncestr = 'Wm3WZYTPz0wzccnW'; jsapi_ticket = 'sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg'; timestamp = '1414587457'; url = 'http://mp.weixin.qq.com?params=value'; ``` 步骤1. 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1: jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg&noncestr=Wm3WZYTPz0wzccnW×tamp=1414587457&url=http://mp.weixin.qq.com?params=value 步骤2. 对string1进行sha1签名,得到signature:0f9de62fce790f9a083d5c99e95740ceb90c27ed 注意事项 - 签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。 - 签名用的url必须是调用JS接口页面的完整URL。 - 出于安全考虑,开发者必须在服务器端实现签名的逻辑。 `jsapi_ticket`又是啥? ##### jsapi_ticket `jsapi_ticket`是公众号用于调用微信JS接口的临时票据。 正常情况下,`jsapi_ticket`的有效期为7200秒,通过`access_token`来获取。 由于获取`jsapi_ticket`的`api`调用次数非常有限,频繁刷新`jsapi_ticket`会导致`api`调用受限,影响自身业务,开发者必须在自己的服务全局缓存`jsapi_ticket` 。 说白了就是调用下微信的接口,`https: ```javascript //返回的json数据示例 { "errcode": 0, "errmsg": "ok", "ticket": "bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA", "expires_in": 7200 }
|
access_token
又是啥?
access_token
access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。
开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。
access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
公众平台的API调用所需的access_token的使用及生成方式说明:
- 建议公众号开发者使用中控服务器统一获取和刷新Access_token,其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致access_token覆盖而影响业务;
- 目前Access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器对外输出的依然是老access_token,此时公众平台后台会保证在刷新短时间内,新老access_token都可用,这保证了第三方业务的平滑过渡;
- Access_token的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样便于业务服务器在API调用获知access_token已超时的情况下,可以触发access_token的刷新流程。
其实也是请求微信的一个接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
正常的返回json { "access_token":"ACCESS_TOKEN", "expires_in":7200 } 失败的返回json { "errcode":40013, "errmsg":"invalid appid" }
|
好了,这样一步一步过来就能拿到调用jsSDK需要的必备参数了。完美,皆大欢喜!
步骤四:通过ready接口处理成功验证
1 2 3
| wx.ready(function(){ });
|
步骤五:通过error接口处理失败验证
1 2 3
| wx.error(function(res){ });
|
接口调用说明
1 2 3 4 5 6 7 8 9 10 11
| 所有接口通过wx对象(也可使用jWeixin对象)来调用,参数是一个对象,除了每个接口本身需要传的参数之外,还有以下通用参数: 1. success:接口调用成功时执行的回调函数。 2. fail:接口调用失败时执行的回调函数。 3. complete:接口调用完成时执行的回调函数,无论成功或失败都会执行。 4. cancel:用户点击取消时的回调函数,仅部分有用户取消操作的api才会用到。 5. trigger: 监听Menu中的按钮点击时触发的方法,该方法仅支持Menu中的相关接口。 备注:不要尝试在trigger中使用ajax异步请求修改本次分享的内容,因为客户端分享操作是一个同步操作,这时候使用ajax的回包会还没有返回。 以上几个函数都带有一个参数,类型为对象,其中除了每个接口本身返回的数据之外,还有一个通用属性errMsg,其值格式如下: 调用成功时:"xxx:ok" ,其中xxx为调用的接口名 用户取消时:"xxx:cancel",其中xxx为调用的接口名 调用失败时:其值为具体错误信息
|
实施
这个题目,不知道该叫啥,哈哈。
之前不是说js安全域名不能填本地的ip嘛,那怎么办,我本地开发,我又是一个前端。我需要服务端的人支持,运维的人支持,服务端的人每次初始化的时候给我签名,时间戳啥啥啥的。
但是我又想自己搞清楚,要不然一塌糊涂,做缓存,调接口,我node也可以呀。重要的是我怎么把我的内网地址能让外网访问。
之前QQ浏览器自带了一个微信调试这样的一个功能,它其中有个功能就是服务端调试,把你的内网穿透,能映射让外网访问。这样就可以在测试公众号上填写js安全域名了。
很可惜,我说的是之前。所以,百度啊百度,百度了一个叫花生壳的工具,有这样的一个功能。
注册一个账号,并且去申请一个免费的壳域名,设置一下,映射到本地的你起的这个服务的网址上,这样就做到了内网穿透。具体的大家可以百度花生壳内网穿透的教程。
做这个的时候还是挺心酸的,我用的是mac,花生壳还没有mac版的客户端,我又跑去找到封尘以久的联想,然后…
我申请到一个ashasmile.imwork.net
,然后在js安全域名填了imwork.net
。
代码
上面的都是一些概念啊,流程啊,下面开始晒代码了啊。
基本配置 /config/wechat.config.js
1 2 3 4 5 6 7 8 9
| module.exports = { grant_type: 'client_credential', appid: 'wx5f87160ac8f64d3d', secret: 'ad89c2d17bfeddce0905e6761b42a3d2', noncestr:'Wm3WZYTPz0wzccnW', accessTokenUrl:'https://api.weixin.qq.com/cgi-bin/token', ticketUrl:'https://api.weixin.qq.com/cgi-bin/ticket/getticket', cache_duration:1000*60*60*24 }
|
生成签名算法 /sign/signature.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| var request = require('request'), cache = require('memory-cache'), sha1 = require('sha1'), config = require('../config/wechat.config');
exports.sign = function (url,callback) { var noncestr = config.noncestr, timestamp = Math.floor(Date.now()/1000), jsapi_ticket; if(cache.get('ticket')){ jsapi_ticket = cache.get('ticket'); console.log('1' + 'jsapi_ticket=' + jsapi_ticket + '&noncestr=' + noncestr + '×tamp=' + timestamp + '&url=' + url); callback({ noncestr:noncestr, timestamp:timestamp, url:url, jsapi_ticket:jsapi_ticket, signature:sha1('jsapi_ticket=' + jsapi_ticket + '&noncestr=' + noncestr + '×tamp=' + timestamp + '&url=' + url) }); }else{ request(config.accessTokenUrl + '?grant_type=' + config.grant_type + '&appid=' + config.appid + '&secret=' + config.secret ,function(error, response, body){ if (!error && response.statusCode == 200) { var tokenMap = JSON.parse(body); request(config.ticketUrl + '?access_token=' + tokenMap.access_token + '&type=jsapi', function(error, resp, json){ if (!error && response.statusCode == 200) { var ticketMap = JSON.parse(json); cache.put('ticket',ticketMap.ticket,config.cache_duration); console.log('jsapi_ticket=' + ticketMap.ticket + '&noncestr=' + noncestr + '×tamp=' + timestamp + '&url=' + url); callback({ noncestr:noncestr, timestamp:timestamp, url:url, jsapi_ticket:ticketMap.ticket, signature:sha1('jsapi_ticket=' + ticketMap.ticket + '&noncestr=' + noncestr + '×tamp=' + timestamp + '&url=' + url) }); } }) } }) } }
|
客户端js /static/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
| document.getElementById('refresh').onclick = function(){location.reload();}
wx.config({ debug: true, appId: appId, timestamp: timestamp, nonceStr: nonceStr, signature: signature, jsApiList: ['checkJsApi', 'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareWeibo', 'hideMenuItems', 'showMenuItems', 'hideAllNonBaseMenuItem', 'showAllNonBaseMenuItem', 'translateVoice', 'startRecord', 'stopRecord', 'onRecordEnd', 'playVoice', 'pauseVoice', 'stopVoice', 'uploadVoice', 'downloadVoice', 'chooseImage', 'previewImage', 'uploadImage', 'downloadImage', 'getNetworkType', 'openLocation', 'getLocation', 'hideOptionMenu', 'showOptionMenu', 'closeWindow', 'scanQRCode', 'chooseWXPay', 'openProductSpecificView', 'addCard', 'chooseCard', 'openCard'] });
wx.ready(function(){ document.querySelector('#checkJsApi').onclick = function () { wx.checkJsApi({ jsApiList: [ 'getNetworkType', 'previewImage' ], success: function (res) { alert(JSON.stringify(res)); } }); };
document.querySelector('#onMenuShareAppMessage').onclick = function () { wx.onMenuShareAppMessage({ title: '互联网之子', desc: '在长大的过程中,我才慢慢发现,我身边的所有事,别人跟我说的所有事,那些所谓本来如此,注定如此的事,它们其实没有非得如此,事情是可以改变的。更重要的是,有些事既然错了,那就该做出改变。', link: location.href, imgUrl: 'http://demo.open.weixin.qq.com/jssdk/images/p2166127561.jpg', trigger: function (res) { alert('用户点击发送给朋友'); }, success: function (res) { alert('已分享'); }, cancel: function (res) { alert('已取消'); }, fail: function (res) { alert(JSON.stringify(res)); }
}); alert('已注册获取“发送给朋友”状态事件'); };
var images = { localId: [], serverId: [] }; document.querySelector('#chooseImage').onclick = function () { wx.chooseImage({ success: function (res) { images.localId = res.localIds; alert('已选择 ' + res.localIds.length + ' 张图片'); } }); }; document.querySelector('#previewImage').onclick = function () { wx.previewImage({ current: 'http://img5.douban.com/view/photo/photo/public/p1353993776.jpg', urls: [ 'http://img3.douban.com/view/photo/photo/public/p2152117150.jpg', 'http://img5.douban.com/view/photo/photo/public/p1353993776.jpg', 'http://img3.douban.com/view/photo/photo/public/p2152134700.jpg' ] }); };
document.querySelector('#getLocation').onclick = function () { wx.getLocation({ success: function (res) { alert(JSON.stringify(res)); }, cancel: function (res) { alert('用户拒绝授权获取地理位置'); } }); };
document.querySelector('#scanQRCode0').onclick = function () { wx.scanQRCode(); };
});
wx.error(function(res){ JSON.stringify(res) });
|
node起服务 /server.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| var express = require('express'), jade = require('jade');
var app = express();
app.set('view engine', 'jade'); app.set('views', './views');
app.locals.basedir = './'
var port = 4567 ; app.use(express.logger()); app.use(express.compress()); app.use(express.bodyParser()); app.use(express.methodOverride()); app.use(express.cookieParser());
app.use(express.static('./static'));
app.listen(port, function() { console.log('服务启动成功!请访问 http://localhost:' + port); });
require('./router/index').init(app);
|
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| { "name": "wechat-sdk-demo", "version": "0.0.0", "description": "a demo with wechat js-sdk based on nodejs", "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "https://github.com/liaobin312716/wechat-sdk-demo.git" }, "keywords": [ "wechat", "js-sdk", "node" ], "author": "liaobin", "license": "MIT", "dependencies": { "express": "~3.17.5", "jade": "^1.11.0", "memory-cache": "^0.1.4", "request": "^2.58.0", "sha1": "^1.1.1" }, "bugs": { "url": "https://github.com/liaobin312716/wechat-sdk-demo/issues" }, "homepage": "https://github.com/liaobin312716/wechat-sdk-demo" }
|
router /route/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| exports.init = function (app) { var wechat_cfg = require('../config/wechat.cfg'); var http = require('http'); var cache = require('memory-cache'); var sha1 = require('sha1'); var signature = require('../sign/signature'); app.get('/',function(req,res){ var url = req.protocol + '://' + req.host + req.originalUrl; console.log(url); signature.sign(url,function(signatureMap){ signatureMap.appId = wechat_cfg.appid; res.render('index',signatureMap); }); }); };
|
前端静态资源css /static/index.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| body{-webkit-tap-highlight-color: rgba(0,0,0,0);} h3{text-align: center;color: red;} .panel{text-align: center;} .panel h5{ text-align: left; color: #888; margin: 0; } .panel button{ width: 90%; height: 40px; line-height: 40px; margin: 5px auto; border: none; border-radius: 5px; background: #e2e2e2; }
|
前端静态资源 html /views/index.jade
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| doctype html html(lang='en') head meta(http-equiv='Content-Type',content='text/html; charset=utf-8') meta(name='viewport',content='width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no,minimal-ui') meta(name='apple-mobile-web-app-status-bar-style',content='black') title 微信SDK demo link(rel='stylesheet',type="text/css",href='index.css') body h3 微信SDK demo div.panel h5 1.判断是否支持特定JS-API button#checkJsApi checkJsApi br br h5 2.分享接口 button#onMenuShareAppMessage 发送给朋友 br br h5 3.图像接口 button#chooseImage 选择图片 br button#previewImage 预览图片 br br h5 4.位置接口 button#getLocation 获取位置 br br h5 5.微信扫一扫 br button#scanQRCode0 扫一扫 br button#refresh 刷新当前页面 script(src='http://res.wx.qq.com/open/js/jweixin-1.0.0.js') script(type='text/javascript'). var signature = '#{signature}'; var nonceStr = '#{noncestr}';] var timestamp = '#{timestamp}'; var appId = '#{appId}'; var jsapi_ticket = '#{jsapi_ticket}'; script(src='index.js')
|
总结
比较重要的是生成签名的算法,微信官方也给出了各个语言的版本示例,java
、php
、node
等。
参考资料:
微信公众平台技术文档
基于nodejs 的微信 JS-SDK 简单应用
微信公众平台接口调试工具
jsSDKdemo