- 商户平台代金券发放 接口调用限制频率上限后会限流么?
我的需求是商户A替多个商户制券 发券,然后统一再进行抢券 这样的业务流程 [图片] 但是抢券的话tps预计在2000左右 这个一定会FREQUENCY_LIMIT_EXCEED报错么,如果报错后是否后续会限流,有什么解决办法么
05-29 - 「笔记」订阅消息体验踩坑
前言 10月12日夜晚社区发了公告小程序模板消息能力调整通知,正式发布了 一次性订阅消息 这一能力,所以第一时间进行了体验。 本文主要是补充一下官方未提供的使用方法,和使用中与模板消息用法的不同。 文档地址 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html 使用方法 [代码]wx.requestSubscribeMessage({ tmplIds: ["模板id1","模板id2"], success: function (res) { //成功 }, fail(err) { //失败 console.error(err); } }) [代码] 第一个坑 如果不勾选红色方框内的内容,用户每次触发订阅消息功能都会弹出授权窗口,如果用户勾选了则不会出现弹窗。 [图片] 第二个坑 目前开发者工具(v1.02.191012)不支持调试,只能通过真机调试。 [图片] 第三个坑 微信不会为开发者保存订阅次数,需要自己在后台记录用户触发的次数。 超过次数调用接口下发订阅消息会返回失败。 [图片] 第四个坑 发送模板格式和原来的模板消息格式不一致,特别是data内的内容,订阅消息的字段key是和数据类型有关,value的参数需要严格按照设置的类型提交,具体使用参考后台的模板详情。 模板消息的格式: [代码]"data": { "keyword1": { "value": "内容", "color": "#000" }, "keyword2": { "value": "内容", "color": "#000" } } [代码] 订阅消息的格式: [代码]"data": { "thing1": { "value": "内容" }, "number2": { "value": 20 } [代码] 第五个坑 订阅消息申请模板的时候,需要选择所属类目,而且只能是自己小程序相关类目,模板消息是不需要选择对应类目的。 如果删除小程序类目,则会把订阅消息模板一起删除,需谨慎操作。 [图片] 第六个坑 长期订阅消息只针对特定行业开放,所以普通开发者并无法使用。 结束 暂时就先总结这些,有其它坑再补充。
2019-10-13 - 我这mpvue的demo页的列表,咋就总不出来呢?真相原来是。。。
问题原帖: https://developers.weixin.qq.com/community/develop/doc/0004a24a948a40e3cda937c9657000 第一步,下载这位同学的代码到本地,然后npm install -> npm run dev -> 开发者工具导入项目 [图片] 这代码写的也没毛病啊,接着查看组件List的代码 [图片] 这也写的没毛病,真奇怪。 接着我新建了一个项目,执行vue create binnie-app, 然后编写相同的代码,运行之后可以显示列表,猜测mpvue可以是对list做处理了, 然后我把mpvue项目的importList 改成了 KList, 竟然能显示了,肯定是保留关键字了。最后搜索mpvue常见问题发现list果然在保留关键字中。 链接:http://mpvue.com/qa.html 总结:没用过mpvue的我,碰见这个问题,差点以为自己不会vue了。在这里建议大家用框架前,先查一下有什么常见问题或坑。 推荐使用binnie小姐姐同款的kbone(Web 与小程序同构解决方案) kbone参考文章: https://developers.weixin.qq.com/community/develop/article/doc/00020a7c280268018ea91fc3c51013 https://developers.weixin.qq.com/community/develop/article/doc/0008424ea884e83b47895f5c45b813
2019-12-29 - 【经验总结】如何解决微信支付返回签名错误
搜了一下,发现不少朋友卡在这里了,我自己也被卡了一天多才解决,下面分享下个人经验: 1.检查各参数是否正确,一定要对着微信网站数据过一遍,不要拿中间数据对比,一定要对着微信网站数据过一遍,不要拿中间数据对比,一定要对着微信网站数据过一遍,不要拿中间数据对比。中间别人给的数据,可能已经变了,而没有告知你,这个会浪费不少时间。 2.验证证书是不是对的,这个我也遇到了,拿到的第一版证书也是有问题,幸好后面补发给我了,要不又不知道要卡多久。。证书验证的方法官网有例子,大概命令如下: openssl x509 -noout -modulus -in wxpay_cert.pem openssl rsa -noout -modulus -in wxpay_key.pem 两个结果一样就OK的。 3.加密算法可参考官网例子https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/qian-ming-sheng-cheng,但这个里面有坑,比如哪个随机数,一定不要过长,官网的32位,其实是有问题的,建议拿时间戳(毫秒)试,位数应该有要求。 4.如果还是返回"message":"错误的签名,验签失败",这个时候,你可以拿官网给的两个工具试。链接地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml,一个是可以验签的,把自己拼的填进去然后比对工具跑出来的对不对,不对的话,就是加密算法有问题,对了,那就是有参数或者数据拼接格式不对,这个时候可以用第二个工具。第二个工具是个postman的配置,直接按照git上的readme流程走,就可以了,有3个参数需要填: private_key,商户私钥,位于文件[代码]apiclient_key.pem[代码]中。请见,商户API证书和什么是私钥?什么是证书?。mchid,商户号。serialNo,商户证书的证书序列号。请见什么是证书序列号。填好之后,直接发个测试的req,如果还是“错误的签名,验签失败”,那么基本就可以确定你上面3个参数有问题,因为中间计算签名都是微信自己算的,这个我自己测试过,是对的,还是靠谱的。这个时候就回到步骤1,老老实实核对参数,步骤2校验证书吧。
2021-02-24 - 小程序直播从开通到开播全过程——开发篇
本文因为社区编辑器markdown功能暂有问题,格式上比较混乱,大家将就看吧: 目前小程序支持的直播方式有两种,一种是纯原生方案(小程序提供推流拉流服务器,主播端和收播端页面都已提供好,你直接使用即可),一种是自己搭建推流服务器(只是使用小程序端提供的live-pusher和live-player组件而已,里面的直播页面和功能都自己独立开发!),这里说的是第一种方案: 一、准备工作 1、一个已经申请开通和正常使用的实实在在的小程序 PS:如果开通了直播功能,但是没有审核上架成功过,直播间分享出去的二维码点击会提示页面不存在!!!原因很简单,因为你新开发的直播页面正式版的小程序上并没有新加进去,必须要提审上架到正式版才能生效! 二、小程序直播准入门槛 微信小程序直播功能准入要求(官方文档链接>>) 一、类目要求: 1. 小程序开发者为国内非个人主体开发者; 2. 小程序开发者为下述类目品类,类目具体信息可参考《微信小程序开放的服务类目》: 1)电商平台:电商平台 2)商家自营:百货、食品、初级食用农产品、酒/盐、图书报刊/音像/影视/游戏/动漫、汽车/其他交通 工具的配件、服装/鞋/箱包、玩具/母婴用品(不含食品)、家电/数码/手机、美妆/洗护、珠宝/饰品/眼镜 /钟表、运动/户外/乐器、鲜花/园艺/工艺品、家居/家饰/家纺、汽车内饰/外饰、办公/文具、机械/电子 器件、电话卡销售、预付卡销售、宠物/农资、五金/建材/化工/矿产品; 3)教育:培训机构、教育信息服务、学历教育(学校)、驾校培训、教育平台、素质教育、婴幼儿教 育、在线教育、教育装备、出国移民、出国留学、特殊人群教育、在线视频课程; 4)金融业:证券/期货投资咨询、保险; 5)出行与交通:航空、地铁、水运、城市交通卡、打车(网约车)、顺风车(拼车)、出租车、路况、 路桥收费、加油/充电桩、城市共享交通、高速服务、火车、公交、长途客运、停车、代驾、租车; 6)房地产:房地产、物业管理、房地产经营、装修/建材; 7)生活服务:丽人、宠物(非医药类)、宠物医院/兽医、环保回收/废品回收、摄影/扩印、婚庆服务、 搬家公司、百货/超市/便利店、家政、营业性演出票务、生活缴费; 8)IT科技:硬件与设备、基础电信运营商、电信业务代理商、软件服务提供商、多方通信; 9)餐饮:餐饮服务场所/餐饮服务管理企业、点餐平台、外卖平台、点评与推荐、菜谱、餐厅排队; 10)旅游:旅游线路、旅游攻略、旅游退税、酒店服务、公寓/民宿、门票、签证、出境WiFi、景区服 务; 11)汽车:养车/修车、汽车资讯、汽车报价/比价、车展服务、汽车经销商/4S店、汽车厂商、汽车预售 服务; 12)体育:体育场馆服务、体育赛事、体育培训、在线健身 二、运营要求: 1、主体下小程序近半年没有严重违规 2、小程序近90天存在支付行为 以上2个运营条件和类目同时满足的前提下,下面3个条件满足其一即可 3、主体下公众号累计粉丝数大于100 4、主体下小程序近7日dau大于100 5、主体在微信生态内近一年广告投放实际消耗金额大于1w 以上准入要求于 2020 年 02 月 24 日进行公示生效。为营造良好健康的微信生态,腾讯公司有权对《微信 小程序直播功能准入要求》不时予以调整并公布,请予以关注。 腾讯公司 tip:如果你的小程序刚刚满足上面门槛,请T+2后刷新再试试。 三、进入小程序后台直播,创建直播间 [图片] 如果你的小程序满足了第二点。小程序后台会有一个直播的入口(没有的话自己找找原因) 点击进入后->创建直播间 按提示操作(要输入主播人的微信号,对方初次使用要活体检测+实名认证)即可成功创建直播间。(注意点:开播时间最早不能早于当前时间10分钟后) 创建成功后,会有一个开播码。注意这个开播码是给主播用的,主播开播的入口小程序码。主播可以扫码进入直播间开播。 [图片] 四、小程序端开发 完成上面3步算是完成主播端的配置了,接下来是收播端(观看直播的小程序端)的开发了。这个是要小程序开发者完成的。所以下面操作都在小程序开发端完成。下面就简单介绍开发逻辑和顺序,具体的要用到的API和接口都不细说,在后面相关链接里面可以点击官方链接查看!(小程序直播 | 微信开放文档)https://developers.weixin.qq.com/miniprogram/dev/framework/liveplayer/live-player-plugin.html) (1)引入直播插件(直接按官方介绍文档操作) 正常引入后开发者工具会弹出这个窗口,如果不弹出请认真,静下心来按照官方文档检查自己的引入代码: [图片] (2)开发后端(如果你没有小程序端自建直播列表和直播间入口的需求2、3、4都可以跳过,届时你的小程序直播间可以用分享方式进入) 后端目前官方只提供了2个接口。一个是获取直播间列表,一个是获取直播间直播完后的相关回放信息,其中第一个接口必须先完成。就是获取到直播间列表,列表里面有带返回直播间的roomid,小程序端必须需要接收到这方面的返回才能接下来的开发。 (3)进入直播页面 引入直播插件后并对接第二步的后端接口后,你可以直接编码进入直播页面了。像进入普通页面一样,可以通过wxml里面的navigator url="xxxx"的方式和js里的wx.navigateTo跳转页面代码进入直播页面。但是他这个url比较特殊,是下面这样的格式: url: `plugin-private://${provider}/pages/live-player-plugin?room_id=${roomId}&custom_params=${encodeURIComponent(JSON.stringify(customParams))}` provider:插件appid(1)小步里面获取到的 rommId:直播间id(2)小步里面获取列表后里面的roomId customParams:自定义的进入页面参数。(根据需要自己定义的传入直播间收播页面的参数) 进入直播间收播页面后的开发量为0,因为这个是由直播间插件接管并完成相关功能。 (4)几个注意点: 4.1、后端获取直播间列表接口几个跟官方文档介绍不一致的地方 [图片] 4.2、 livePlayer.getLiveStatus获取直播间状态这个API官方介绍:首次获取立马返回直播状态,往后间隔1分钟或更慢的频率去轮询获取直播状态。实际使用过程中建议也这么干,如果需要轮询直播间状态,建议间隔时间1分钟以上,如果少于这个值,基本上就是卡在这里后面的代码都不执行了。还有,有时候即使超过1分钟后再轮询,也会偶发性出现获取不到卡住的情况。解决方法,大家可以看看开发者工具里面的本地Storage相关的值,然后后面怎么做你懂的。。 4.3订阅组件subscribe的样式问题。不多说,你懂的,你加上去就能看到效果 4.4后端接口每日调用次数限制的问题。要做好相关缓存到本地的架构设计。 4.5运营上一定要注意,按要求直播。别整那些没用的,很容易被禁播的。 (5)回放功能开发 1.0.4版本后支持0开发的回放功能了。参考后面新增的专门介绍回放功能的使用教程。 五、跑路 这里的跑路是指代码写累了,带上口罩和吉娃娃去公园跑一圈路回来继续码。 最新:1.0.4版本后的回放功能说明,回放功能是这样的 1、后台开启该直播间的回放功能 [图片] 2、收播端还是原来的直播入口进行回放,小程序端是 plugin-private://${liveplayId}/pages/live-player-plugin?room_id=${roomId}&custom_params=${encodeURIComponent(JSON.stringify(customParams))}` 这里的页面链接,链接到回放页面。获取分享方式,分享出去的直播页面,点击后进入回放。 [图片] 还有一个口,点击原来的分享链接后的直播完成页面,也有一个查看回放的入口,如上图。 Tip:如果刚刚直播完可能需要稍等生成回放视频后再次进入相关页面才能看到回放。 相关链接: 小程序直播 | 微信开放文档(开发必看,而且要熟读,基本有所有你要的开发资料) https://developers.weixin.qq.com/miniprogram/dev/framework/liveplayer/live-player-plugin.html 微信小程序直播功能准入要求 | 微信开放文档 https://developers.weixin.qq.com/miniprogram/product/live/access-requirement.html “小程序直播”接入指引 | 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/0008ce654c4450244c1a7e5de5b801?highLine=%25E7%259B%25B4%25E6%2592%25AD%2520%25E6%25B1%25BD%25E8%25BD%25A6 相关知识科普: 小程序直播单日直播上限是50场,同时直播上限50场,单场的直播时长上限是12小时。
2020-06-23 - 持续更新:收藏整理官方隐藏的小程序功能/参数/方法/API
简介 一门热门的编程语言通常会留下一些带惊喜的「彩蛋」让你去自己深挖,小程序也是如此。此文是收集与整理官方未公布的一些彩蛋。希望大家多多提供线索。本文仅整理官方未公布的小程序相关包括但不限于:组件属性、功能、方法、API。 组件类 scroll-view里面的隐藏属性 throttle="{{false}}" 功能:关闭函数节流功能,可以更精准的bindScroll。但也更耗手机性能 [代码]app.json里面配置关闭同层渲染功能: "window": { "renderingMode": "seperated" } [代码] 收集中… 功能类 wx:for支持Object列表渲染 [代码]{ '2018-1-9':{ address: '....', name: '....' }, '2018-1-10':{ address: '....', name: '....' }, '2018-1-11':{ address: '....', name: '....' } } //wxml <view wx:for="{{obj}}" wx:for-index="key" wx:for-item="value"> {{key}} : {{value.address}} </view> [代码] 2.wx.chooseContact 作用:从手机通讯录中选择联系人并获取相关信息(iPhone下有兼容问题) [代码]参考代码: wx.chooseContact({ success:res=>{ that.customer.name = res.displayName; that.customer.contract = res.phoneNumber; }, complete: function(res) { } }); [代码] 收集中… 开发者工具类 完美支持各种插件安装,详见: 动手打造更强更好用的微信开发者工具-编辑器扩展篇 | 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000a8816e18748dd52f96f8975b413 历史版本的隐藏下载链接 升级开发者工具到最新,种种原因需要恢复旧版,旧版安装包找不到应急处理方法 | 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/0008c4833f4450f8e32ab0dbc51013 官方社区类 有几个隐藏的官方的美女甩锅采用非官方账号在社区带节奏。具体我就不点名了。🐶🐶🐶🐶 收集中… 其他未公布的隐藏功能: 给此文点赞后的码农写代码永无BUG ,CP设计的产品人见人爱,BOSS每年收入翻番! ↓↓↓↓
2020-06-26 - 云支付,pay_cb/functionName的正确打开姿势
纯代码:第一步就秒过: // pay_cb 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const _ = db.command exports.main = async (event, context) => { //回调信息备案 await db.collection('cloudPay').doc(event.outTradeNo).set({ data:event }) //其他业务逻辑 return { errcode:0, errmsg:'SUCCESS' } } 更多内容: [图片]
2020-10-25 - wx.request包装成云函数格式,以便将来一键迁移。
将wx.request包装成云函数格式: 云函数的调用格式是:wx.cloud.callFunction({name:'login',data:{x:1,y:2}}).then() 仿云函数的调用格式是:app.callFunction({name:'login',data:{x:1,y:2}}).then() 两者一模一样。 在app.js里: url: 'https://xxx.com/applet/', callFunction: function(args) { return new Promise((resolve, reject) => { wx.request({ url: this.url + args.name, method: 'POST', data: { sessionid: this.getSessionid(),//换取openid ...args.data }, success: resolve, fail: reject }) }) }, page里index.js app.callFunction({name:'login',data:{x:1,y:2}}).then(res=>{console.log(res)}) 或者await: let res = await app.callFunction({name:'login',data:{x:1,y:2}}) console.log(res) 传统后台:nginx+nodejs+koa2+mongo nginx定义一条路由location:'https://xxx.com/applet/' koa定义一条路由router:'login' [图片]
2020-10-20 - 数组再分的实现
实现数组再分 开发的时候遇到了一个比较少见的关于数组的操作,如何根据数组中的某个键的属性值是否相同,重新整合数组中的值,如同代码示例中的根据数组的name属性,将name相同的数组元素重新组合在一起。最初的设想是for循环找出每个元素之后,内嵌一个for循环进行比对,找出相应数据存放,结果是数组中的数据重复打印。于是从网上搜索并解决问题,以下便是代码示例。 代码示例 [代码]//实验代码 未分组变量 arrayDemo: [{ "name": "z", "age": 15, "high": 10, "phone": 12345678911 }, { "name": "q", "age": 15, "high": 10, "phone": 12345678910 }, { "name": "w", "age": 15, "high": 10, "phone": 12345678912 }, { "name": "e", "age": 15, "high": 10, "phone": 12345678913 }, { "name": "r", "age": 15, "high": 10, "phone": 12345678914 }, { "name": "z", "age": 15, "high": 10, "phone": 12345678915 }, { "name": "z", "age": 15, "high": 10, "phone": 12345678916 }, { "name": "f", "age": 15, "high": 10, "phone": 12345678917 } ] //调用数组排列方法 this.arrayGroup(arrayDemo); /** * 数组分组 */ arrayGroup: function(array) { var groups = []; //存放新数组 for (var i = 0; i < array.length; i++) { //遍历数组每一项 // 读取每条数据的名称 取出 分类的条件 var groupName = array[i].name; var groupValue = { // 符合分类条件的属性值组合成对象放入新数组中value属性中 'age': array[i].age, 'high': array[i].high, 'phone': array[i].phone } var groupItem = { //新数组中存放的对象 'name': '', value: [] } groupItem.name = groupName; groupItem.value.push(groupValue); if (i == 0) { //设置为基准值进行对比 groups.push(groupItem); } else { //遍历剩余数组项 var index = -1; //第二层循环找到属性值相同的数组成员并且放入新数组中 for (var k = 0; k < groups.length; k++) { if (groupName == groups[k].name) { index = k; break; } } if (index == -1) { //没有找到 groups.push(groupItem); } else { groups[k].value.push(groupValue); //将属性值存放到新数组中 } } } console.log('groups', groups); } [代码] 结果示例 [图片] 解决这个问题之后,鉴于花费在这个问题上的时间,重新搜集了有关数组的操作方法记录下来,以减少下次花费时间。 数组相关方法扩展 filter() filter()方法创建一个新数组, filter为数组中的每个元素调用一次 callback 函数,并利用调用callback函数返回true或等价于true的值的元素创建一个新数组。 [代码]filter语法: var newArray = arr.filter(callback(element[, index[, array]])[, thisArg]) /** * filterdemo 过滤掉所有小于10的数组项 */ filterDemo: function() { var filterArray = [12, 14, 8, 9, 100]; var filterResult = filterArray.filter(function(item) { if (item > 10) { return true; } }); console.log('filterResult', filterResult) } [代码] [图片] map() map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。 [代码]//map语法: var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg]) /** * map方法示例 所有数组成员乘以5 */ mapDemo: function() { var mapArray = [1, 2, 3, 4, 5]; var mapResult = mapArray.map(function(item) { return item * 5 }) console.log('mapResult', mapResult); } [代码] [图片] foreach() foreach()方法对数组中的每个元素执行一次提供的函数。 [代码]语法: array.forEach(function(currentValue, index, arr), thisValue) /** * forEach 代码示例 */ forEachDemo: function() { var arr = [1, 5, 8, 9] arr.forEach(function(item) { console.log('item', item) }) } /** * forEachDemo 无法使用break跳出循环 */ forEachDemo: function() { var arr = [1, 5, 8, 9] arr.forEach(function(item) { if (item == 1) { break } console.log('item', item) }) } /** * forEach 被调用的时候,不会直接改变调用它的对象,但是对象可能会被callback改变原数组为 var words = ['one', 'two', 'three', 'four'];调用foreach后 原数组被改变 */ forEachDemo: function() { var words = ['one', 'two', 'three', 'four']; words.forEach(function(word) { console.log(word); if (word === 'two') { words.shift(); } }); } [代码] [图片] foreach无法使用break跳出循环 [图片] forEach 被调用的时候,不会直接改变调用它的对象,但是对象可能会被callback改变 [图片] for-in for-in常用来遍历对象,以任意顺序去遍历一个对象可枚举的属性 [代码] /** * for-in 实例 输出对象的属性 */ forInDemo: function() { var obj = { name: 'ar', color: 'yellow', day: 'sunday', number: 6, age:15 } for (var key in obj) { console.log(obj[key]) } } [代码] [图片] 在合适的场景下选用合适的数组操作方法,可以使得复杂的代码变得更加的易读和简练,更容易让人理解。
2019-06-30 - 你(可能)不知道的web api
简介 作为前端er,我们的工作与web是分不开的,随着HTML5的日益壮大,浏览器自带的webapi也随着增多。本篇文章主要选取了几个有趣且有用的webapi进行介绍,分别介绍其用法、用处以及浏览器支持度,同时我也分别为这几个api都做了一个简单的demo(真的很简单,样式等于没有~)这几个api分别是: page lifecycle onlineState 利用deviceOrientation制作一个随着手机旋转的正方体 battery status custom event 利用execCommand完成一个简单的富文本 page lifecycle(网页生命周期) 介绍 我们可以用document.visibitilityState来监听网页可见度,是否卸载,但是在手机和电脑上都会现这种情况,就是比如说页面打开过了很久没有打开,这时你看在浏览器的tab页中看着是可以看到内容的,但是点进去却需要加载。chrome68添加了 freeze和 resume事件,来完善的描述一个网页从加载到卸载,包括浏览器停止后台进程,释放资源各种生命阶段。从一个生命周期阶段到另外一个生命周期阶段会触发不同的事件,比如onfocus,onblur,onvisibilitychange,onfreeze等等,通过这些事件我们可以响应网页状态的转换。具体的教程推荐大家看看阮一峰大神的教程。 用法 [代码]window.addEventListener('blur',() => {}) window.addEventListener('visibilitychange',() => { // 通过这个方法来获取当前标签页在浏览器中的激活状态。 switch(document.visibilityState){ case'prerender': // 网页预渲染 但内容不可见 case'hidden': // 内容不可见 处于后台状态,最小化,或者锁屏状态 case'visible': // 内容可见 case'unloaded': // 文档被卸载 } }); [代码] 用处 大家可以看下这个demo [图片] 所以说,这个API的用处就是用来响应我们网页的状态,比如说我们的页面是在播放视频或者是一个网页的游戏,你可以通过这个API来去做出对应的响应,暂停视频,游戏暂停等等。 浏览器支持度 page visibilituState [图片] online state(网络状态) 这个API就很简单了,就是获取当前的网络状态,同时也有对应的事件去响应网络状态的变化。 用法 [代码]window.addEventListener('online',onlineHandler) window.addEventListener('offline',offlineHandler) [代码] 用处 比如说我们的网站是视频网站,正在播放的时候,网络中断了,我们可以通过这个API去响应,给用户相应的提示等等。 浏览器支持度 [图片] Vibration(震动) 让手机震动~~~ 嗯,就这么简单。 用法 [代码]// 可以传入一个大于0的数字,表示让手机震动相应的时间长度,单位为ms navigator.vibrate(100) // 也可以传入一个包含数字的数组,比如下面这样就是代表震动300ms,暂停200ms,震动100ms,暂停400ms,震动100ms navigator.vibrate([300,200,100,400,100]) // 也可以传入0或者一个全是0的数组,表示暂停震动 navigator.vibrate(0) [代码] 用处 用来给用户一个提示,比如说数据校验失败,当然震动不止这点作用,大家自己去扩展吧~~~ 浏览器支持度 [图片] device orientation(陀螺仪) 通过绑定事件来获取设备的物理朝向,可以获取到三个数值,分别是: alpha:设备沿着Z轴的旋转角度 [图片] beta:设备沿着X轴的旋转角度 [图片] gamma:设备沿着Y轴的旋转角度 [图片] 用法 [代码]window.addEventListener('deviceorientation',e => { console.log('Gamma:',e.gamma); console.log('Beta:',e.beta); console.log('Alpha:',e.Alpha); }) [代码] 用处 这种自然是web VR 中的使用场景会相对较多。这是我写的一个小demo [图片] 浏览器支持度 [图片] battery status 这个API就使用来获取当前的电池状态 用法 [代码]// 首先去判断当前浏览器是否支持此API if ('getBattery' in navigator) { // 通过这个方法来获取battery对象 navigator.getBattery().then(battery => { // battery 对象包括中含有四个属性 // charging 是否在充电 // level 剩余电量 // chargingTime 充满电所需事件 // dischargingTime 当前电量可使用时间 const { charging, level, chargingTime, dischargingTime } = battery; // 同时可以给当前battery对象添加事件 对应的分别时充电状态变化 和 电量变化 battery.onchargingchange = ev => { const { currentTarget } = ev; const { charging } = currentTarget; }; battery.onlevelchange = ev => { const { currentTarget } = ev; const { level } = ev; } }) } else { alert('当前浏览器不支持~~~') } [代码] 用处 用来温馨的提示用户当前电量~~~ 浏览器支持度 这个浏览器的支持度很低。。。。 [图片] execCommand 执行命令 当将HTML文档切换成设计模式时,就会暴露出 execcommand 方法,然后我们可以通过使用这个方法来执行一些命令,比如复制,剪切,修改选中文字粗体、斜体、背景色、颜色,缩进,插入图片等等等等。 用法 用法也很简单,这里简单介绍几个,详细的介绍大家可以去MDN上看看。 这个API接受三个参数,第一个是要执行的命令,第二个参数mdn上说是Boolean用来表示是否展现用户界面,但我也没测试出来有什么不同,第三个参数就是使用对应命令所需要传递的参数。 [代码]// 一般不会直接去操作我们本身的HTML文档,可以去插入一个iframe然后通过contentDocument来获取、操作iframe中的HTML文档。 let iframe = document.createElement('ifram'); let doc = iframe.contentDocument; // 首先要将HTML文档切换成设计模式 doc.designMode = 'on'; // 然后就可以使用execCommand 这个命令了; // 执行复制命令,复制选中区域 doc.execCommand('copy') // 剪切选中区域 doc.execCommand('cut') // 全选 doc.execCommand('selectAll') // 将选中文字变成粗体,同时接下来输入的文字也会成为粗体, doc.execCommand('bold') // 将选中文字变成斜体,同时接下来输入的文字也会成为斜体, doc.execCommand('italic') // 设置背景颜色,,比如设置背景色为红色,就传入 'red'即可 doc.execCommand('backColor',true,'red') [代码] 用处 我用这些命令简单的写了一个富文本的demo,大家可以看一下Demo,效果如下: [图片] 浏览器支持度 CustomEvent (自定义事件) 大家都知道各种事件是如何绑定的,但是有时候这些事件不够用呢,custom event就可以解决这样的问题。 用法 [代码]let dom = document.querySelector('#app'); // 绑定事件, 传递过来的值可以通过ev.detail 来获取 dom.addEventListener('log-in',(ev) => { const { detail } = ev; console.log(detail); // hello }) // 派发事件,需要传入两个参数,一个是事件类型,另外一个是一个对象,detail就是传递过去的值 dom.dispatchEvent(new CustomEvent('log-in',{ detail:'hello' })) [代码] 用处 绑定自定义事件,最近很火的框架Omi,其中的自定义事件就是基于customEvent实现的。 浏览器支持度 [图片] 最后 就先介绍到这些,web api越来越多,当然每个人不可能全都熟记于心,这篇文章也只是简单介绍一下,还有很多有意思而且很重要的API,比如:web components, service worker,genric sensor等等,不过这些都有很多人在钻研,同时文档相对较多。 相信你看完这些至少已经知道这些API的大概用法了,如果有兴趣了解用法的话,可以去看下我写的demo,也可以去看看MDN文档去深入研究一下。 参考 MDN文档 阮一峰大神的博客 web-api-you-dont-know 视频演讲 http://www.zhangyunling.com/725.html Omi WeElement源码
2019-03-01 - Painter 一款轻量级的小程序海报生成组件
生成海报相信大家有的人都做过,但是canvas绘图的坑太多。大家可以试试这个组件。然后附上楼下大哥做的可视化拖拽生成painter代码的工具:链接地址https://developers.weixin.qq.com/community/develop/article/doc/000e222d9bcc305c5739c718d56813
2019-09-27 - Web打印探秘
概述 本文首先描述笔者所处的项目组的 Web 打印项目的需求背景,然后描述笔者在实践 Web 打印项目的过程中遇到了诸多问题,阐述 Web 打印的问题解决思路,最后给出了另外一种 Web 打印的需求解决方案,即使用[代码]Headless browser[代码]生成图片并打印的方案。预计阅读时间5 ~ 10分钟。 本文主要分下面几个方面: Web 构建打印模板需求 基本概念 打印设备接口 页面模型 引入打印样式 处理 Web打印 分页问题 去除浏览器默认的页眉页底 构建自定义的页眉页底 使用 Headless browser 生成图片的解决方案 Web 构建打印模板需求 产品经理小姐姐近期给笔者写了这样一个需求: 实现一个打印报告的模板页面,浏览器或客户端调用打印设备的接口打印出对应的报告。 对应报告支持报告模板配置,模板分几种,例如免费玩家用极简版、低保玩家用基本版、充值玩家用高级版、土豪玩家用顶配版。没错,充值才能变得更强。 需要实现分页功能,支持把对应的内容展示到对应的页。例如:内容A为基本信息,需要展示到第一页,低保玩家享受内容B,展示到第二页…土豪玩家享受所有的功能,展示到第n页。 展示产品配置的对应的页眉、页底。 于是,为了解决上述需求,笔者大概写了这样的一个模板,如下所示: [代码]<div class="page1"> <div>我是第一页1</div> <div>我是第一页2</div> <div>我是第一页3</div> </div> <div class="page2"> <div>我是第二页1</div> <div>我是第二页2</div> <div>我是第二页3</div> </div> <div class="page3"> <div>我是第三页1</div> <div>我是第三页2</div> <div>我是第三页3</div> </div> <div class="page4"> <div>我是第四页1</div> <div>我是第四页2</div> <div>我是第四页3</div> </div> [代码] 基本概念 打印设备接口 浏览器打印是一个很成熟的应用,最简单的打印直接调用[代码]window.print()[代码]或者是调用[代码]document.execCommand('print')[代码]。此时,浏览器会弹出打印预览的窗口,通过页面生成了[代码]pdf[代码]用于打印预览。如图所示,展示了谷歌首页的打印预览: [图片] 页面模型 和 CSS 盒子模型一样,页面盒子模型由外边距 (margin)、边框 (border)、内边距 (padding) 和 内容区域 (content area) 构成: [图片] 有以下两点可以注意: 打印页面时,只打印出页面的内容区域 页面默认有页眉页脚信息,展现到页面外边距范围 默认情况下,页面是从左到右、从上到下展示,如果需要更改打印设备的方向,可以通过设置根元素的 direction 和 writing-mode 属性来改变页面方向。 引入打印样式 可以通过三种方式引入打印样式: 使用 [代码]@media print[代码]: [代码]@media print { body { background-color:#FFFFFF; margin: 0mm; /* this affects the margin on the content before sending to printer */ } // ... } [代码] 内联样式使用[代码]media[代码]属性: [代码]<style type="text/css" media="print"> </style> [代码] 在 CSS 中使用 [代码]@import[代码]: [代码]@import url("print.css") print; [代码] HTML 中使用的[代码]link[代码]标签添加[代码]media[代码]属性: [代码]<link rel="stylesheet" media="print" href="print.css"> [代码] 处理 Web打印 分页问题 项目需求中首先遇到的问题是需要处理 Web打印 分页问题。即使该部分未占满一页纸的高度,也需要进行手动的分页。起初,我通过计算页面每个部分的高度,在对应页面部分的节点的高度下方预留一部分的外边距来实现,如下代码所示,通过查资料得知 A4纸的宽高比为 297 : 210 ,除去页面外边距(左右各 20mm )来算得每一部分需要预留的高度: [代码]const A4_HEIGHT_WIDTH_RATE = 297 / (210 - 2 * 13); // 打印区域长宽比:(A4纸高)比(A4纸宽减去左右侧20mm的边距) const PAGE_WIDTH = 680; // 页面宽度(像素值) const PAGE_HEIGHT = PAGE_WIDTH * A4_HEIGHT_WIDTH_RATE; // 页面高度 const $page1El = document.querySelector('.page1'); const page1Height = parseInt($page1El.clientHeight); // page1的高度是多少像素 const pageNum = Math.ceil(page1Height / PAGE_HEIGHT); // page1需要占多少页,超过1页的高度,就需要占2页,因此向上取整 const marginBottom = pageNum * PAGE_HEIGHT - page1Height; // 需要预留多少外边距 $page1El.style.marginBottom = `${marginBottom}px`; [代码] 但是,其实 CSS 早就支持了打印设备里的分页问题了,可以通过设置[代码]break-after: page;[代码] 或 [代码]page-break-after: always;[代码]实现在打印设备的分页: [代码].page1 { break-after: page; page-break-after: always; } // ... [代码] 去除浏览器默认的页眉页底 实现分页的效果后,发现页面打印会在页底出现当前页面的 url : [图片] 页面默认有页眉页脚信息,展现到页面外边距范围,通过去除 页面模型 的外边距,使得内容不会延伸到页面的边缘,再通过设置 [代码]body[代码] 元素的 [代码]margin[代码] 来保证 A4 纸打印出来的页面带有外边距: [代码]@media print { @page { margin: 0; } body { margin: 2cm; } } [代码] 现在打印出来的页面不再具有默认的页底: [图片] 构建自定义的页眉页底 通过将对应的页眉、页底元素的 [代码]position[代码] 设置为 [代码]fixed[代码] 可以固定对应节点到页面的任意一部分,它们也将在每个打印页面上重复。 [代码].header { position: fixed; top: 0; } .footer { position: fixed; bottom: 0; } [代码] 使用 Headless browser 生成图片的解决方案 上面说了那么多,都是在前端实现的 Web 打印的解决方案,但实际上,如果可以在后台直接通过 Web 页面,预先保存好的页面模板,通过拉取后台数据,并运行[代码]Headless browser[代码]生成一张截图,通过打印截图就可以解决这样的问题了,下面以 phantomjs 配合 pug为例,展示笔者使用[代码]Headless browser[代码]生成图片的简单解决方案: [代码]// 针对链接的截图服务 // 返回phantom实例的promise对象,为了获取对应的base64编码 function captureByUrl(url, data) { let instance; let page; const destroyInstance = () => { // 关闭页面 page.close(); // 退出实例 instance.exit(); }; return phantom.create() // 首先,创建phantom实例 .then((_instance) => { instance = _instance; return instance.createPage(); }) .then((_page) => { page = _page; if (data.width && data.height) { // 设置phantom截图页面的宽高值 page.property('viewportSize', { width: data.width, height: data.height }); } page.setting('userAgent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36'); return page.open(url); }) .then(() => { return page.renderBase64('PNG'); // 渲染对应图片,拿到base64字符串 }) .then((image) => { destroyInstance(); // 销毁phantom实例 return image; }, (error) => { destroyInstance(); // 销毁phantom实例 throw error; }); } [代码] 如上代码所示,使用 Headless browser 打开一个链接,通过[代码]renderBase64[代码]将对应页面的预览图截图生成base64字符串。 对应的,在服务端,可以通过读取预先写好的[代码]pug[代码]模板,传入对应数据生成对应页面预览图,再通过 Headless browser 生成截图保存到本地,即可实现 Web 打印在服务端的解决方案,如下代码所示,为服务端读取模板,并保存图片的部分代码: [代码]// 针对模板和数据的截图服务 function captureByTemplate(template, data) { const content = pug.compile(template)(Object.assign({ URL_PREFIX, }, data)); const contentInBase64 = new Buffer(content).toString('base64'); const url = `data:text/html;charset=utf8;base64,${contentInBase64}`; return captureByUrl(url, data); } captureByTemplate(fs.readFileSync('./print.pug', 'utf-8'), data) .then(base64Data => { fs.writeFile("out.png", base64Data, 'base64', function(err) { console.error(err); }); }) .catch(err => { console.error(err); }); [代码] 小结 本文为笔者在实践 Web 打印相关项目的项目总结,首先描述了 Web 打印项目一般需求,然后在打印设备下,页面模型的展现形式;然后描述了笔者在实践过程中遇到的一些常见问题,给出一些通用性的解决方案。最后,联想到 Headless browser 也可用于实现打印模板需求,笔者以 phantomjs 和 pug 模板为例进行了一个简单的实践。
2019-04-16 - 开启websocket服务端口,调试微信websocket接口方法
使用node环境,在vscode 工具中,创建 app.js 文件 ,代码如下,记得安装 npm install websocket 和 npm install http 模块 . 开启 websocket接口服务后,就可以去封装 官方提供的 wx.sendSocketMessage 等接口了。 [代码]const http = require("http"); const WebSocketServer = require("websocket").server; const httpServer = http.createServer((request, response) => { console.log("[" + new Date() + "] Received request for " + request.url); response.writeHead(404); response.end(); }); const wsServer = new WebSocketServer({ httpServer, autoAcceptConnections: true }); wsServer.on("connect", connection => { connection .on("message", message => { if (message.type === "utf8") { console.log(">> message content from client: " + message.utf8Data); connection.sendUTF(message.utf8Data); // 输出内容返回给前端接口调用 } }) .on("close", (reasonCode, description) => { console.log( "[" + new Date() + "] Peer " + connection.remoteAddress + " disconnected." ); }); }); httpServer.listen(8080, () => { console.log("[" + new Date() + "] Serveris listening on port 8080"); }); #小程序页面示例代码,请参考文档 const socketOpen = false const socketMsgQueue = [] wx.connectSocket({ // url: 'test.php', url :“ws://localhost:8080/” }) wx.onSocketOpen(function (res) { socketOpen = true for (let i = 0; i < socketMsgQueue.length; i++) { sendSocketMessage(socketMsgQueue[i]) } socketMsgQueue = [] }) function sendSocketMessage(msg) { if (socketOpen) { wx.sendSocketMessage({ data: msg }) } else { socketMsgQueue.push(msg) } }[代码]
2019-04-18 - canvas画图随记
最近画了一张分享图,在此记录一下遇到的问题及解决方法。 画布尺寸自适应 微信小程序尺寸为rpx,会自适应各种机型,但canvas的方法参数默认为px,所以需要对画布上的每一项参数乘以(画布宽度/设备屏幕宽度),将rpx换算成px,达到尺寸自适应的目的,所以将此系数设置为全局变量。代码如下: [代码]var app = getApp(); const device = wx.getSystemInfoSync(); const width = device.windowWidth;//设备屏幕宽度 const xs = width / 375; [代码] 调用: [代码]createCard: function() { var context = wx.createCanvasContext('myCanvas'); context.fillText('内容', 100 * xs , 100 * xs) } [代码] 长文本换行 由于fillText只能画一行,但很多情况下是需要将长文本自动换行展示的,这个时候则需要对文本进行处理。 方法:遍历该文本,计算出每一字宽度之和,当该宽度大于文本最大宽度时绘制当前截取部分,并将绘制高度加上行高,宽度置0,重新计算并绘制下一行。当只剩最后一字时,绘制剩余部分。 缺陷:当文本内有换行符时,绘制会换行,但当前计算宽度不会增加,导致格式混乱。所以需要在计算宽度之和前判断该字符是否为换行符,若果是,则绘制当前部分,开始下一行的计算。 完善:如果需要知道绘制文本的总高度,设置初始文本高度为0,在绘制一行时加上行高则可。代码如下: [代码] /** * context:当前画布对象 * text:文本内容 * leftWidth:文本左上角x坐标 * initHeight:文本左上角y坐标 * canvasWidth:一行文本最大宽度 */ drawText: function(context, text, leftWidth, initHeight, canvasWidth) { var lineWidth = 0; //文本宽度 var textHeight = 0; //文本总高度 var lastSubStrIndex = 0; //每次开始截取的字符串的索引 for (let i = 0; i < text.length; i++) { if (text[i] == "\n") { //如遇换行 context.fillText(text.substring(lastSubStrIndex, i), leftWidth, initHeight, canvasWidth); //绘制截取部分 initHeight += 17.5 * xs; //17.5为字体高度 lineWidth = 0; lastSubStrIndex = i + 1; //截取字符串时跳过换行符 textHeight += 17.5 * xs; } else { lineWidth += context.measureText(text[i]).width; //计算每个字的宽度之和 if (lineWidth > canvasWidth) { context.fillText(text.substring(lastSubStrIndex, i), leftWidth, initHeight, canvasWidth); initHeight += 17.5 * xs; lineWidth = 0; lastSubStrIndex = i; textHeight += 17.5 * xs; } } if (i == text.length - 1) { //绘制剩余部分 context.fillText(text.substring(lastSubStrIndex, i + 1), leftWidth, initHeight, canvasWidth); textHeight += 17.5 * xs; } } return textHeight; }, [代码] 调用: [代码] var text = '新建项目选择小程序项目,选择代码存放的硬盘路径,填入刚刚申请到的小程序的 AppID,给你的项目起一个好听的名字,最后,勾选 "创建 QuickStart 项目" (注意: 你要选择一个空的目录才会有这个选项),点击确定,你就得到了你的第一个小程序了,点击顶部菜单编译就可以在微信开发者工具中预览你的第一个小程序。'; context.setFontSize(15 * xs) that.drawText(context, text, 30 * xs, 100 * xs, 320 * xs) [代码] 高度自适应 如碰到画布高度需要根据内容高度不同而不同,或者某元素与可变化高度的元素固定距离的情况,则需要计算出可变化元素高度,再根据该高度进行计算其他高度。例如: [图片] 微信图标始终距离文本30px,而该文本高度可变,所以图标的左上角y轴坐标=文本y轴坐标+文本高度+下边距,代码如下: [代码]var textHeight = that.drawText(context, text, 30 * xs 100 * xs, 320 * xs) context.drawImage('/images/wx.png', 68 * xs, (100 + 30) * xs + textHeight, 80 * xs, 80 * xs) [代码] 注意:因为计算文本高度的方法里已经乘过系数,所以这里不需要乘。宽度自适应同理。 绘制圆角矩形框 由于没有绘制圆角矩形的方法,所以需要将圆角矩形分开绘制。 方法:将四个圆角当成四分之一圆绘制,然后分别画四条边,坐标如下图所示。 [图片] 代码: [代码] /** * context:当前画布对象 * x:圆角矩形左上角x坐标 * y:圆角矩形左上角y坐标 * w:宽度 * h:高度 * r:border-radius * color:填充颜色 */ roundRect(ctx, x, y, w, h, r, color) { ctx.beginPath() // 左上角 ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5) // 上边框 ctx.moveTo(x + r, y) ctx.lineTo(x + w - r, y) ctx.lineTo(x + w, y + r) // 右上角 ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2) // 右边框 ctx.lineTo(x + w, y + h - r) ctx.lineTo(x + w - r, y + h) // 右下角 ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5) // 下边框 ctx.lineTo(x + r, y + h) ctx.lineTo(x, y + h - r) // 左下角 ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI) // 左边框 ctx.lineTo(x, y + r) ctx.lineTo(x + r, y) //填充颜色 ctx.setFillStyle(color); ctx.fill() ctx.closePath() } [代码] 调用: [代码]that.roundRect(context, 15 * xs, 60 * xs, 350* xs, 200 * xs, 14 * xs, '#ffffff') [代码] 文本加粗 官方文档里有说到font的使用规则与css语法一致,有几个需要注意的地方,否则可能会导致设置无效。 [图片] 调用: [代码] context.font = "normal bold 27px sans-serif"; context.setFontSize(27 * xs) context.fillText('加粗字体', 100 * xs , 145 * xs) [代码] 效果: [图片] 注意:在真机上若没有写第一个normal参数,则不能成功设置。 字体大小可以在下面重新赋值。 如果没有效果可以注意console有没有如下图所示 设置无效的警告,原因很大可能是因为参数写的不对。 [图片] 圆形头像绘制 方法:在画布上剪切一个圆,然后在圆上画头像,最后恢复即可。有一个需要注意的地方,drawImage方法只能绘制本地图片,如果需要绘制网络图片需下载完成之后再画。代码如下: [代码] context.save() context.beginPath() context.arc(77 / 2 * xs + 150 * xs, 77 / 2 * xs + 73 * xs, 77 / 2 * xs, 0, Math.PI * 2, false) context.clip() var headimg = '/images/headimg.jpg' context.drawImage(headimg, 150 * xs, 73 * xs, 77 * xs, 77 * xs) context.restore() context.draw(); [代码] 遇到的问题:当图片为长方形时,强行将图片压缩为正方形会导致头像变形。 解决办法:image组件里,参数mode有一个值为aspectFill,即保持纵横比缩放图片,只保证图片的短边能完全显示出来,我们参考这种思路来截取图片。 [图片] 这里以宽比高长的图为例。如上图所示,圆为头像显示位置,线为中线,矩形框为一张宽大于高的图片。矩形左上角即为画图时的左上角坐标。截部分如图所示,得到图片宽高后,短边固定为头像尺寸,长边根据短边缩放比计算得到。图片宽=原图宽 /(头像高 / 原图高)。左上角的x轴坐标为:中线x坐标 - 图片宽 / 2。代码如下所示: [代码] context.save() context.beginPath() context.arc(77 / 2 * xs + 150 * xs, 77 / 2 * xs + 73 * xs, 77 / 2 * xs, 0, Math.PI * 2, false) context.clip() var headimg = '/images/headimg.jpg'; //头像路径 var headimgHeight = 0; var headimgWidth = 0; wx.getImageInfo({ src: headimg, success(res) { headimgHeight = res.height; //原图高度 headimgWidth = res.width; //原图宽度 //当宽 > 高时 if (headimgWidth > headimgHeight) { var width = headimgWidth / (headimgHeight / (77 * xs)); //图片宽度 var x = (150 + 77 / 2) * xs - width / 2; //x轴坐标 context.drawImage(headimg, x, 73 * xs, width, 77 * xs) } else { //当高>=宽时 var height = headimgHeight / (headimgWidth / (77 * xs)); //图片高度 var h = (73 + 77 / 2) * xs - height / 2; //y轴坐标 context.drawImage(headimg, 150 * xs, h, 77 * xs, height) context.restore() context.draw(); } } }) [代码] 注意:这里得到的图片宽高已经是px为单位,所以不乘系数。
2019-03-18 - 小程序保持长连接小经验
大家都知道,小程序的websocket在切入后台5秒左右,会断开链接,或者长时间无数据收发,也会切断链接。 然而心跳数据并不能完全保证链接的正常, 我在这里说一下我的经验,我初中毕业(真的),代码方面写的不好不要见笑。 首先,我为了知道当前的网络是否断了,使用了一个标志位比如 : var NetworkIsOK = false; 当首次链接打开时比如socket的 onOpen 事件!在这个事件回调中,将NetworkIsOK = true; 同理,如果网络出现错误,在错误的回调中将NetworkIsOK = false; 建立一个发送 发送区数据 暂存站 var SendBuffer =[]; 将socket的send 函数包装一下,比如取名SendToServer(data) 在SendToServer中,首先检查网络的状态,如果状态是正常的,则直接调用socket.send()发送数据, 如果不是正常的,则重新链接服务器,并在onOpen事件中检查 SendBuffer.length是否大于 0 ,如果有存入的缓冲数据,则依次发送掉, 以下是部分实际代码,请忽略我垃圾的编程水平! [代码]var[代码] [代码]app = getApp();[代码][代码]/**[代码][代码] [代码][代码]* 与服务器进行通信的所有操作在此进行[代码][代码] [代码][代码]*/[代码][代码]var[代码] [代码]Server = {};[代码][代码]Server.socket = [代码][代码]null[代码][代码]; [代码][代码]//socket连接句柄[代码][代码]Server.isOK = [代码][代码]false[代码][代码]; [代码][代码]//服务器连接状态处理数组[代码][代码]Server.event = []; [代码][代码]//事件注册处理数组[代码][代码]Server.url = [代码][代码]'wss://********'[代码][代码]; //服务器地址[代码] [代码]Server.SendBuffer=[]; [代码][代码]//数据包发送缓冲[代码][代码]/**[代码][代码] [代码][代码]* 初始化操作[代码][代码] [代码][代码]*/[代码][代码]Server.Init=[代码][代码]function[代码][代码](apps){[代码][代码] [代码][代码]console.log([代码][代码]'hello server'[代码][代码]);[代码][代码] [代码][代码]app = apps;[代码][代码] [代码][代码]console.log(app);[代码][代码] [代码][代码]var[代码] [代码]Timer;[代码][代码] [代码][代码]/**执行服务器连接逻辑 */[代码][代码] [代码][代码]Server.socket = wx.connectSocket({[代码][代码] [代码][代码]url:Server.url[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//132.232.87.229[代码][代码] [代码][代码]Server.socket.onOpen([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server open'[代码][代码]);[代码][代码] [代码][代码]Server.isOK=[代码][代码]true[代码][代码]; [代码][代码]//可以通信了[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'已连接服务器 '[代码][代码],[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Timer = setInterval([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]Server.startHeart();[代码][代码] [代码][代码]}, 1000 * 50);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onError([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server error'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接错误'[代码][代码],[代码][代码] [代码][代码]icon:[代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onClose([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server close'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接关闭'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onMessage([代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]//Server.socketPress.onMessage(res);[代码][代码] [代码][代码]var[代码] [代码]message = JSON.parse(res.data);[代码][代码] [代码][代码]if[代码][代码](!message) [代码][代码]return[代码][代码]; [代码][代码]//空数据[代码][代码] [代码][代码]var[代码] [代码]Operator = message.Operator;[代码][代码] [代码][代码]var[代码] [代码]event=Server.event[Operator];[代码][代码] [代码][代码]if[代码][代码](event){[代码][代码] [代码][代码]event(message); [代码][代码]//实际执行[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]} /**服务器重连**/[代码] [代码]Server.reLink=[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]var[代码] [代码]Timer;[代码][代码] [代码][代码]/**执行服务器连接逻辑 */[代码][代码] [代码][代码]Server.socket = wx.connectSocket({[代码][代码] [代码][代码]url:Server.url[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//132.232.87.229[代码][代码] [代码][代码]Server.socket.onOpen([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server open'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]true[代码][代码]; [代码][代码]//可以通信了[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'已连接服务器 '[代码][代码],[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.Login();[代码][代码]//重新注册登入[代码][代码] [代码][代码]Timer = setInterval([代码][代码]function[代码][代码](){[代码][代码] [代码][代码]Server.startHeart();[代码][代码] [代码][代码]},1000*50);[代码][代码] [代码][代码]//检查缓冲区是否有未发送数据[代码][代码] [代码][代码]while[代码] [代码](Server.SendBuffer.length>0){[代码][代码] [代码][代码]var[代码] [代码]data = Server.SendBuffer.pop();[代码][代码] [代码][代码]//将用户的code发往服务器[代码][代码] [代码][代码]Server.socket.send({[代码][代码] [代码][代码]data: data[代码][代码] [代码][代码]});[代码][代码] [代码][代码]console.log([代码][代码]'将缓存中的信息发送'[代码][代码],data)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onError([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server error'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接错误'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onClose([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server close'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接关闭'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onMessage([代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]//Server.socketPress.onMessage(res);[代码][代码] [代码][代码]var[代码] [代码]message = JSON.parse(res.data);[代码][代码] [代码][代码]if[代码] [代码](!message) [代码][代码]return[代码][代码]; [代码][代码]//空数据[代码][代码] [代码][代码]var[代码] [代码]Operator = message.Operator;[代码][代码] [代码][代码]var[代码] [代码]event = Server.event[Operator];[代码][代码] [代码][代码]if[代码] [代码](event) {[代码][代码] [代码][代码]event(message); [代码][代码]//实际执行[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 启动心跳[代码][代码] [代码][代码]*/[代码][代码]Server.startHeart=[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]if[代码] [代码](Server.isOK == [代码][代码]false[代码][代码]) {[代码][代码] [代码][代码]Server.reLink(); [代码][代码]//重连[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'Heart'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]};[代码][代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(sendData);[代码][代码] [代码][代码]// console.log(JsonData);[代码][代码] [代码][代码]//将用户的code发往服务器[代码][代码] [代码][代码]Server.socket.send({[代码][代码] [代码][代码]data: JsonData[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.addEvent([代码][代码]'HeartOK'[代码][代码],[代码][代码]function[代码][代码](message){[代码][代码] [代码][代码]// console.log('心跳OK',message);[代码][代码] [代码][代码]})[代码][代码]}[代码][代码]/**用户登陆 */[代码][代码]Server.Login=[代码][代码]function[代码][代码](ques){[代码][代码] [代码][代码]var[代码] [代码]userOpenId = wx.getStorageSync([代码][代码]'openId'[代码][代码]);[代码][代码] [代码][代码]console.log(app);[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'userLogin'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]userCode: app.userCode, [代码][代码]//用户code[代码][代码] [代码][代码]userInfo: app.userInfo, [代码][代码]//用户信息[代码][代码] [代码][代码]};[代码][代码] [代码][代码]if[代码] [代码](userOpenId) { [代码][代码]//如果缓存中的有效,就用缓存中的openId发过去给服务器[代码][代码] [代码][代码]sendData.userOpenId = userOpenId;[代码][代码] [代码][代码]console.log([代码][代码]'调用缓存openID'[代码][代码]);[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(sendData);[代码][代码] [代码][代码]console.log([代码][代码]"登陆数据:"[代码][代码], JsonData);[代码][代码] [代码][代码]// console.log(JsonData);[代码][代码] [代码][代码]//将用户的code发往服务器[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**如果是黑名单用户,禁止使用 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'BanLogin'[代码][代码],[代码][代码]function[代码][代码](message){[代码][代码] [代码][代码]if[代码] [代码](ques) {[代码][代码] [代码][代码]ques();[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 注册消息执行处理函数[代码][代码] [代码][代码]*/[代码][代码]Server.addEvent=[代码][代码]function[代码][代码](eventName,eventHandle){[代码][代码] [代码][代码]Server.event[eventName] = eventHandle;[代码][代码]}[代码][代码]/**注册登入成功处理事件 */[代码][代码]Server.addEvent([代码][代码]'LoginOK'[代码][代码],[代码][代码]function[代码][代码](message){[代码][代码] [代码][代码]console.log([代码][代码]'LOGIN OK'[代码][代码]);[代码][代码] [代码][代码]console.log(message);[代码][代码] [代码][代码]wx.setStorageSync([代码][代码]'openId'[代码][代码],message.openId);[代码][代码]});[代码][代码]/**注册登入失败处理事件 */[代码][代码]Server.addEvent([代码][代码]'LoginError'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]wx.showModal({[代码][代码] [代码][代码]content: [代码][代码]'登入失败了!部分功能可能无法使用,可能是网络原因,也可能是没有获得授权'[代码][代码],[代码][代码] [代码][代码]showCancel: [代码][代码]false[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]if[代码] [代码](res.confirm) {[代码][代码] [代码][代码]//console.log('用户点确定');[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]});[代码][代码]/**[代码][代码] [代码][代码]* 创建帖子[代码][代码] [代码][代码]*/[代码][代码]Server.CreateInvitation=[代码][代码]function[代码][代码](table,image,text,isRichText){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'CreateInvitation'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]isRichText:isRichText,[代码][代码]//是否为富文本[代码][代码] [代码][代码]Table:table,[代码][代码]//标题[代码][代码] [代码][代码]Image:image,[代码][代码]//图片地址[代码][代码] [代码][代码]Text:text,[代码][代码]//文本内容[代码][代码] [代码][代码]};[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 从服务器获取贴子[代码][代码] [代码][代码]*/[代码][代码]Server.GetInvitation=[代码][代码]function[代码][代码](id,mode,time,limt,skip,success){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'GetInvitation'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]id:id, [代码][代码]//使用帖子ID的查询方式[代码][代码] [代码][代码]mode:mode, [代码][代码]//操作模式[代码][代码] [代码][代码]time:time, [代码][代码]//时间查询时使用的时间[代码][代码] [代码][代码]limt:limt, [代码][代码]//分页查询时需要获取的贴子数量[代码][代码] [代码][代码]skip:skip, [代码][代码]//需要跳过的帖子数量[代码][代码] [代码][代码]};[代码][代码] [代码][代码]//var JsonData = JSON.stringify(sendData);[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**注册登入成功处理事件 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'GetInvitationOK'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]console.log([代码][代码]'GetInvitationOK'[代码][代码]);[代码][代码] [代码][代码]var[代码] [代码]data = message.data; [代码][代码] [代码][代码]var[代码] [代码]serverTime=message.serverTime;[代码][代码] [代码][代码]if[代码][代码](success) success(data,serverTime); [代码][代码]//回调执行[代码][代码] [代码][代码]});[代码][代码]}[代码][代码]/**[代码][代码] [代码][代码]* 创建帖子评论[代码][代码] [代码][代码]*/[代码][代码]Server.CreateInvitationComment=[代码][代码]function[代码][代码](_id,text,success){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'CreateInvitationComment'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]id:_id, [代码][代码]//使用帖子ID的查询方式[代码][代码] [代码][代码]text:text,[代码][代码]//评论内容[代码][代码] [代码][代码]};[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**注册登入成功处理事件 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'CreateInvitationCommentOK'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]console.log([代码][代码]'CreateInvitationCommentOK'[代码][代码]);[代码][代码] [代码][代码]var[代码] [代码]data = message.data;[代码][代码] [代码][代码]if[代码] [代码](success) success(data); [代码][代码]//回调执行[代码][代码] [代码][代码]});[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 给帖子点赞[代码][代码] [代码][代码]*/[代码][代码]Server.LoveInvitation=[代码][代码]function[代码][代码](_id,success){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'LoveInvitation'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]id:_id, [代码][代码]//使用帖子ID的查询方式[代码][代码] [代码][代码]};[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**注册登入成功处理事件 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'LoveInvitationOK'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]console.log([代码][代码]'LoveInvitationOK'[代码][代码]);[代码][代码] [代码][代码]if[代码] [代码](success) success(); [代码][代码]//回调执行[代码][代码] [代码][代码]});[代码][代码]}[代码][代码]/**[代码][代码] [代码][代码]* 向服务器发送数据,数据类型为任意数据,[代码][代码] [代码][代码]* 如果服务器断线,则自动重连服务器,数据被暂存,重连成功后将被发送[代码][代码] [代码][代码]*/[代码][代码]Server.SendData=[代码][代码]function[代码][代码](data){[代码][代码] [代码][代码]if[代码] [代码](Server.isOK == [代码][代码]false[代码][代码]) {[代码][代码] [代码][代码]Server.reLink(); [代码][代码]//重连[代码][代码] [代码][代码]//将未发送数据存入Buffer[代码][代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(data);[代码][代码] [代码][代码]Server.SendBuffer.push(JsonData);[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码][代码]else[代码][代码]{ [代码][代码]//否则直接给服务器发送数据[代码][代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(data);[代码][代码] [代码][代码]Server.socket.send({[代码][代码] [代码][代码]data: JsonData[代码][代码] [代码][代码]});[代码][代码] [代码][代码]}[代码][代码]}[代码][代码]//暴露接口[代码][代码]module.exports.Server = Server;[代码]
2019-03-18