- 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 小程序登录、用户信息相关接口调整说明
公告更新时间:2021年04月15日考虑到近期开发者对小程序登录、用户信息相关接口调整的相关反馈,为优化开发者调整接口的体验,回收wx.getUserInfo接口可获取用户授权的个人信息能力的截止时间由2021年4月13日调整至2021年4月28日24时。为优化用户的使用体验,平台将进行以下调整: 2021年2月23日起,若小程序已在微信开放平台进行绑定,则通过wx.login接口获取的登录凭证可直接换取unionID2021年4月28日24时后发布的小程序新版本,无法通过wx.getUserInfo与<button open-type="getUserInfo"/>获取用户个人信息(头像、昵称、性别与地区),将直接获取匿名数据(包括userInfo与encryptedData中的用户个人信息),获取加密后的openID与unionID数据的能力不做调整。此前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。新增getUserProfile接口(基础库2.10.4版本开始支持),可获取用户头像、昵称、性别及地区信息,开发者每次通过该接口获取用户个人信息均需用户确认。具体接口文档:《getUserProfile接口文档》由于getUserProfile接口从2.10.4版本基础库开始支持(覆盖微信7.0.9以上版本),考虑到开发者在低版本中有获取用户头像昵称的诉求,对于未支持getUserProfile的情况下,开发者可继续使用getUserInfo能力。开发者可参考getUserProfile接口文档中的示例代码进行适配。请使用了wx.getUserInfo接口或<button open-type="getUserInfo"/>的开发者尽快适配。开发者工具1.05.2103022版本开始支持getUserProfile接口调试,开发者可下载该版本进行改造。 小游戏不受本次调整影响。 一、调整背景很多开发者在打开小程序时就通过组件方式唤起getUserInfo弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。 二、调整说明通过wx.login接口获取的登录凭证可直接换取unionID 若小程序已在微信开放平台进行绑定,原wx.login接口获取的登录凭证若需换取unionID需满足以下条件: 如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用2月23日后,开发者调用wx.login获取的登录凭证可以直接换取unionID,无需满足以上条件。 回收wx.getUserInfo接口可获取用户个人信息能力 4月28日24时后发布的新版本小程序,开发者调用wx.getUserInfo或<button open-type="getUserInfo"/>将不再弹出弹窗,直接返回匿名的用户个人信息,获取加密后的openID、unionID数据的能力不做调整。 具体变化如下表: [图片] 即wx.getUserInfo接口的返回参数不变,但开发者获取的userInfo为匿名信息。 [图片] 此外,针对scope.userInfo将做如下调整: 若开发者调用wx.authorize接口请求scope.userInfo授权,用户侧不会触发授权弹框,直接返回授权成功若开发者调用wx.getSetting接口请求用户的授权状态,会直接读取到scope.userInfo为true新增getUserProfile接口 若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,该接口从基础库2.10.4版本开始支持,该接口只返回用户个人信息,不包含用户身份标识符。该接口中desc属性(声明获取用户个人信息后的用途)后续会展示在弹窗中,请开发者谨慎填写。开发者每次通过该接口获取用户个人信息均需用户确认,请开发者妥善保管用户快速填写的头像昵称,避免重复弹窗。 插件用户信息功能页 插件申请获取用户头像昵称与用户身份标识符仍保留功能页的形式,不作调整。用户在用户信息功能页中授权之后,插件就可以直接调用 wx.login 和 wx.getUserInfo 。 三、最佳实践调整后,开发者如需获取用户身份标识符只需要调用wx.login接口即可。 开发者若需要在界面中展示用户的头像昵称信息,可以通过<open-data>组件进行渲染,该组件无需用户确认,可以在界面中直接展示。 在部分场景(如社交类小程序)中,开发者需要在获取用户的头像昵称信息,可调用wx.getUserProfile接口,开发者每次通过该接口均需用户确认,请开发者妥善处理调用接口的时机,避免过度弹出弹窗骚扰用户。 微信团队 2021年4月15日
2021-04-15 - [有点炫]自定义navigate+分包+自定义tabbar
自定义navigate+分包+自定义tabbar,有需要的可以拿去用用,可能会存在一些问题,根据自己的业务改改吧 大家也可以多多交流 代码片段:在这里 {"version":"1.1.5","update":[{"title":"修复 [复制代码片段提示] 无法使用的问题","date":"2020-06-15 09:20","imgs":[]}]} 更新日志: 2019-11-25 自定义navigate 也可以调用wx.showNavigationBarLoading 和 wx.hideNavigationBarLoading 2019-11-25 页面滚动条显示在自定义navigate 和 自定义tabbar上面的问题(点击“体验custom Tabbar” [图片] [图片] 其他demo: 云开发之微信支付:代码片段
2020-06-15 - 大数据量swiper滑动优化
问题场景 事情是这样的,我做了一个在线答题小程序,有一个顺序练习模块,每次顺序练习,都要把整个题库过一遍,每个题库题目数量不一,有的几百,有的上千,为方便讨论,我们假定某个题库1000道题目, 具体答题模块是用swiper来实现的,当swiper的数组很大的时候,setData会有三四秒的延迟,我们都知道setData的效率,但是没想到这么厉害。 问题描述问题不是swiper本身,而是setData https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips.html 官方资料https://developers.weixin.qq.com/miniprogram/dev/component/swiper.html 功能演示 [视频] https://developers.weixin.qq.com/community/develop/article/doc/00040235334788f8651a168d951413 这个问题困扰了我几个周时间, 不敢梳头,因为白头发会掉了一地,不要问我为什么是白头发 不敢照镜子,因为黑眼圈更重了; 不敢出门走路,因为问题没有解决,抬不起头来。 [图片] 昨天我在群里发了一个有偿征求优化方案,小伙伴很积极,讨论了一个晚上,第二天就有朋友把优化的方案,递给我,今天中午验证有效,亲测。 [图片] 现在好了,问题得到圆满解决,心情都不一样了,原来处处都是美 [图片] 解决方案大家都能想到方案就是分页,虽然总数组长度为1000,但是每次渲染到swiper的可能只有3、5、7不等的小数组,这样通过setData传递到UI层的时候才不会卡。 但是分页的逻辑要我们自己来控制,怎么判断左滑动,怎么判断右滑动,滑动边界问题,很多细节,总之我为了这个问题花了一个周末没有解决, 就是在大方向明确的情况下,还是写不出来。 具体的实现方案晚点我整理下发出来,同时也希望想挑战的同学锻炼下。 在这里特别感谢群里的两位小伙伴 社区相关帖子 https://developers.weixin.qq.com/community/develop/article/doc/000ecafb3486f07000c92c3225c013 https://developers.weixin.qq.com/community/develop/doc/000e4c77da47208296f8b0b4c51800 感谢@~~娃娃 @~~ dinner
2020-03-25 - 微信小程序答题页——swiper渲染优化及swiper分页实现
前言 swiper的加载太多问题,网上资料好像没有一个特别明确的,就拿这个答题页,来讲讲我的解决方案 这里实现了如下功能和细节: 保证swiper-item的数量固定,加载大量数据时,大大优化渲染效率记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验示例动图 [图片] 截图 [图片] [图片] 问题原因 当swiper-item数量很多的时候,会出现性能问题 我实现了一个答题小程序,在一次性加载100个swipe-item的时候,低端手机页面渲染时间达到了2000多ms 也就是说在进入答题页的时候,会卡顿2秒多去加载这100个swiper-item 思考问题 那我们能不能让他先加载一部分,然后滑动以后再去改变item的数据,让swiper一直保持一定量的swiper-item? 注意到官方文档有这么两个属性可以利用,我们可以开启衔接滑动,然后再bindchange方法中去修改data [图片] 1、保证swiper-item的数量固定,加载大量数据时,优化渲染效率 假设我们请求到的数据的为list,实际渲染的数据为swiperList 我们现在给他就固定3个swiper-item,前后滑动的时候去替换数据 正向滑动的时候去替换滑动后的下一页数据,反向滑动的时候去替换滑动后的上一页数据 当我们知道了要替换的条件,我们便可以去替换数据了 但是我们应该考虑到临界值的问题,如果当前页是list第一项和最后一项该怎么办,向左向右滑是不是得禁止啊 这边是判断没数据会让它再弹回去 2、记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页 有很多时候,我们是从某一项直接进来的,比如说上次答题答到了第五题,我这次进来要直接做第六题 那么我们需要去初始化这个swiperList,让它当前页、上一页、下一页都有数据 3、答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验 从答题卡选择index,那就不仅仅是滑动上下页了,它可以跳转到任何页,所以也采用类似初始化swiperList的方法 swiper切换动画我这边是默认250ms,但是发现有时候从答题卡点击回来,你在答题卡点击的下一项不知道会从左还是从右滑过来 体验真的很差,一开始不知道怎么禁掉动画,其实在跳转到答题卡页的时候把duration设为0就可以了 然后在答题卡页的unload方法中恢复 关键点: 在固定3个swiper-item的同时,要保证我们可以有办法来替代微信自带swiper的current属性和change方法 swiper-limited-load使用方法及说明: 将components中的swiper-limited-load复制到您的项目中在需要的页面引用此组件,并且创建自己的自定义组件item-view在初始化数据时,为你的list的每一项指定index属性具体可以参照项目目录start-swiper-limited-load中的用法说明:其它属性和swiper无异,你们可以自己单独添加你们需要的属性总结 一开始很头疼,为什么微信小程序提供的这个swiper,没去考虑这方面 然后在网上和社区找也没有一个特别好的解决方案。 后来想想,遇到需求就静下来解决吧。 项目地址:https://github.com/pengboboer/swiper-limited-load 如果错误,欢迎指出。 如有新的需求也可以提出来,如果有时间的话,我会帮你们完善。 如果能帮到你们,记得给一个star,谢谢。 ---补充 有很多朋友在评论区提到了分页的需求,抽时间写了一个分页的Demo和大家分享一下。 还是以答题为例,比如我们一共有500条数据,一页20条,可能需要如下功能,乍一看不就加了个分页,挺简单的,其实实现起来挺麻烦的,下面说一下思路和一些需要特别注意的点: 1、从其他页面跳转到答题页时,不光只能默认在第一题,可以是任意一题,比如第80题。 跳转到任意一题,那么需要我们根据index算出该数据在第几页,然后需要请求该页数据,最后显示对应的index。我的思路更注重用户体验,不可能是上滑或者下滑才开始去请求数据,一定是要用户滑动前提前请求好数据。所以起码要保证左右两侧在初始化那一刻都有数据。如果此题和它的上一题下一题都在同一页,那么我们只需要请求一页数据(第15题,那么只需请求第1页数据)。如果此题和它的上一题或者下一题不在同一页,那么我们可能需要请求两页数据。(第20题,那么需要请求第1页和第2页数据) 2、左滑、右滑没数据时,都可以加载新数据。直到滑到第一题或者最后一题。 如果我们初始化时是第24题,那么我们左滑到第21题时,就应该去请求第一页的数据。那么用户在看完21题时,再滑到20题,可能就根本不会感知到通过网络请求了数据。但是如果用户此刻滑动特别快:滑到21题时请求了网络,请求还没成功,就又向左滑了。那么我们需要限制用户的滑动,给用户一个提示:数据正在加载中。 3、从答题卡点击任意一题可以跳转到相应的题目,并且左右滑动显示正常数据 比如我们初始化是跳转到了第80题,不一会点击答题卡又要跳转到200题,一会又跳转到150题。各种无序操作,你也不知道用户要往哪里点。 一开始是想着维护一个主list,点到哪道题往list中添加这道题所在的当页的数据,但是还得判断这一页或者左滑右滑请求新一页的数据得往list的哪个位置添加。这来回来去乱七八糟的判断就很麻烦了,很容易出bug。而且list长度太长了以后insert的性能也不好。 后来就去想,要不答题卡点击任意一题都清空旧的list,然后请求新的数据,左右滑动没数据了再请求新的数据呗。但是这样很浪费资源,并且用户体验也不好,用户已经从第1题答到第200题了,这时用户从答题卡选择了一个25题,还得重新请求网络。而且200道题的数据都没了,那再选个26题,再重新请求网络?网络有延时不说,还浪费资源。 最后转念一想,这时候就需要弄一个缓存了。所以最终的解决方法就出来了:我们维护一个map,在网络请求成功后,在map中保存对应页的数据,同时我们维护一个主list来显示对应的题目。当我们在答题卡选择某一题目,就清空list,然后判断map中有没有该页的数据,如果有就直接拿来,没有就再去网络请求。这个处理方式,写法相对来说简单,不需要乱七八糟的判断,也不浪费资源,用户体验也很不错。 总结 以上就是一些思路和要注意的地方。这个Demo断断续续花了好几天时间写出来的。可能我说的比较啰嗦比较细,只是想让需要用到这个分页Demo的同学能理解我是如何实现的。 如果觉得能帮到你,记得给一个star,谢谢。同时如果这个demo有bug或者你们有新想法,欢迎提出来。
2021-01-07 - 搞懂微信支付 v3 接口规则-【附Java源码】
简介 为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付API v3。 其实还要一个主要因素是「为了符合监管的要求」。 主要是为了符合监管的要求,保证更高的安全级别。《中华人民共和国电子签名法》、《金融电子认证规范》及《非银行支付机构网络支付业务管理办法》中规定 “电子签名需要第三方认证的,由依法设立的电子认证服务提供者提供认证服务。”,所以需使用第三方 CA 来确保数字证书的唯一性、完整性及交易的不可抵赖性。 支付宝支付也是如此,从之前的「普通公钥方式」新增了 「公钥证书方式」。今天的主角是微信支付 Api v3 这里就不展开讲支付宝支付了。 微信支付 Api v3 接口规则 官方文档 v2 与 v3 的区别 V3 规则差异 V2 JSON 参数格式 XML POST、GET 或 DELETE 提交方式 POST AES-256-GCM加密 回调加密 无需加密 RSA 加密 敏感加密 无需加密 UTF-8 编码方式 UTF-8 非对称密钥SHA256-RSA 签名方式 MD5 或 HMAC-SHA256 微信支付 Api-v2 版本详细介绍请参数之前博客 微信支付,你想知道的一切都在这里 干货多,屁话少,下面直接进入主题,读完全文你将 Get 到以下知识点 如何获取证书序列号 非对称密钥 SHA256-RSA 加密与验证签名 AES-256-GCM 如何解密 API 密钥设置 请登录商户平台进入【账户中心】->【账户设置】->【API安全】->【APIv3密钥】中设置 API 密钥。 具体操作步骤请参见:什么是APIv3密钥?如何设置? 获取 API 证书 请登录商户平台进入【账户中心】->【账户设置】->【API安全】根据提示指引下载证书。 具体操作步骤请参见:什么是API证书?如何获取API证书? 按照以上步骤操作后你将获取如下内容: apiKey API 密钥 apiKey3 APIv3 密钥 mchId 商户号 apiclient_key.pem X.509 标准证书的密钥 apiclient_cert.p12 X.509 标准的证书+密钥 apiclient_cert.pem X.509 标准的证书 请求签名 如何生成签名参数?官方文档 描述得非常清楚这里就不啰嗦了。 示例代码 构造签名串 [代码] /** * 构造签名串 * * @param method {@link RequestMethod} GET,POST,PUT等 * @param url 请求接口 /v3/certificates * @param timestamp 获取发起请求时的系统当前时间戳 * @param nonceStr 随机字符串 * @param body 请求报文主体 * @return 待签名字符串 */ public static String buildSignMessage(RequestMethod method, String url, long timestamp, String nonceStr, String body) { return new StringBuilder() .append(method.toString()) .append("\n") .append(url) .append("\n") .append(timestamp) .append("\n") .append(nonceStr) .append("\n") .append(body) .append("\n") .toString(); } [代码] 构造 HTTP 头中的 Authorization [代码]/** * 构建 v3 接口所需的 Authorization * * @param method {@link RequestMethod} 请求方法 * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath key.pem 证书路径 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @return {@link String} 返回 v3 所需的 Authorization * @throws Exception 异常信息 */ public static String buildAuthorization(RequestMethod method, String urlSuffix, String mchId, String serialNo, String keyPath, String body, String nonceStr, long timestamp, String authType) throws Exception { // 构建签名参数 String buildSignMessage = PayKit.buildSignMessage(method, urlSuffix, timestamp, nonceStr, body); // 获取商户私钥 String key = PayKit.getPrivateKey(keyPath); // 生成签名 String signature = RsaKit.encryptByPrivateKey(buildSignMessage, key); // 根据平台规则生成请求头 authorization return PayKit.getAuthorization(mchId, serialNo, nonceStr, String.valueOf(timestamp), signature, authType); } /** * 获取授权认证信息 * * @param mchId 商户号 * @param serialNo 商户API证书序列号 * @param nonceStr 请求随机串 * @param timestamp 时间戳 * @param signature 签名值 * @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048 * @return 请求头 Authorization */ public static String getAuthorization(String mchId, String serialNo, String nonceStr, String timestamp, String signature, String authType) { Map<String, String> params = new HashMap<>(5); params.put("mchid", mchId); params.put("serial_no", serialNo); params.put("nonce_str", nonceStr); params.put("timestamp", timestamp); params.put("signature", signature); return authType.concat(" ").concat(createLinkString(params, ",", false, true)); } [代码] 拼接参数 [代码] public static String createLinkString(Map<String, String> params, String connStr, boolean encode, boolean quotes) { List<String> keys = new ArrayList<String>(params.keySet()); Collections.sort(keys); StringBuilder content = new StringBuilder(); for (int i = 0; i < keys.size(); i++) { String key = keys.get(i); String value = params.get(key); // 拼接时,不包括最后一个&字符 if (i == keys.size() - 1) { if (quotes) { content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"'); } else { content.append(key).append("=").append(encode ? urlEncode(value) : value); } } else { if (quotes) { content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"').append(connStr); } else { content.append(key).append("=").append(encode ? urlEncode(value) : value).append(connStr); } } } return content.toString(); } [代码] 从上面示例来看我们还差两个参数 serial_no 证书序列号 signature 使用商户私钥对待签名串进行 SHA256 with RSA 签名 如何获取呢?不要着急,容我喝杯 「89年的咖啡」提提神。 获取证书序列号 通过工具获取 openssl x509 -in apiclient_cert.pem -noout -serial 使用证书解析工具 https://myssl.com/cert_decode.html 通过代码获取 [代码]// 获取证书序列号 X509Certificate certificate = PayKit.getCertificate(FileUtil.getInputStream("apiclient_cert.pem 证书路径")); System.out.println("输出证书信息:\n" + certificate.toString()); System.out.println("证书序列号:" + certificate.getSerialNumber().toString(16)); System.out.println("版本号:" + certificate.getVersion()); System.out.println("签发者:" + certificate.getIssuerDN()); System.out.println("有效起始日期:" + certificate.getNotBefore()); System.out.println("有效终止日期:" + certificate.getNotAfter()); System.out.println("主体名:" + certificate.getSubjectDN()); System.out.println("签名算法:" + certificate.getSigAlgName()); System.out.println("签名:" + certificate.getSignature().toString()); /** * 获取证书 * * @param inputStream 证书文件 * @return {@link X509Certificate} 获取证书 */ public static X509Certificate getCertificate(InputStream inputStream) { try { CertificateFactory cf = CertificateFactory.getInstance("X509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream); cert.checkValidity(); return cert; } catch (CertificateExpiredException e) { throw new RuntimeException("证书已过期", e); } catch (CertificateNotYetValidException e) { throw new RuntimeException("证书尚未生效", e); } catch (CertificateException e) { throw new RuntimeException("无效的证书", e); } } [代码] SHA256 with RSA 签名 获取商户私钥 [代码] /** * 获取商户私钥 * * @param keyPath 商户私钥证书路径 * @return 商户私钥 * @throws Exception 解析 key 异常 */ public static String getPrivateKey(String keyPath) throws Exception { String originalKey = FileUtil.readUtf8String(keyPath); String privateKey = originalKey .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s+", ""); return RsaKit.getPrivateKeyStr(RsaKit.loadPrivateKey(privateKey)); } public static String getPrivateKeyStr(PrivateKey privateKey) { return Base64.encode(privateKey.getEncoded()); } /** * 从字符串中加载私钥 * * @param privateKeyStr 私钥 * @return {@link PrivateKey} * @throws Exception 异常信息 */ public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception { try { byte[] buffer = Base64.decode(privateKeyStr); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); return keyFactory.generatePrivate(keySpec); } catch (NoSuchAlgorithmException e) { throw new Exception("无此算法"); } catch (InvalidKeySpecException e) { throw new Exception("私钥非法"); } catch (NullPointerException e) { throw new Exception("私钥数据为空"); } } [代码] 私钥签名 [代码]/** * 私钥签名 * * @param data 需要加密的数据 * @param privateKey 私钥 * @return 加密后的数据 * @throws Exception 异常信息 */ public static String encryptByPrivateKey(String data, String privateKey) throws Exception { PKCS8EncodedKeySpec priPkcs8 = new PKCS8EncodedKeySpec(Base64.decode(privateKey)); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); PrivateKey priKey = keyFactory.generatePrivate(priPkcs8); java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA"); signature.initSign(priKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signed = signature.sign(); return StrUtil.str(Base64.encode(signed)); } [代码] 至此微信支付 Api-v3 接口请求参数已封装完成。 执行请求 [代码]/** * V3 接口统一执行入口 * * @param method {@link RequestMethod} 请求方法 * @param urlPrefix 可通过 {@link WxDomain}来获取 * @param urlSuffix 可通过 {@link WxApiType} 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath apiclient_key.pem 证书路径 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @param file 文件 * @return {@link String} 请求返回的结果 * @throws Exception 接口执行异常 */ public static Map<String, Object> v3Execution(RequestMethod method, String urlPrefix, String urlSuffix, String mchId, String serialNo, String keyPath, String body, String nonceStr, long timestamp, String authType, File file) throws Exception { // 构建 Authorization String authorization = WxPayKit.buildAuthorization(method, urlSuffix, mchId, serialNo, keyPath, body, nonceStr, timestamp, authType); if (method == RequestMethod.GET) { return doGet(urlPrefix.concat(urlSuffix), authorization, serialNo, null); } else if (method == RequestMethod.POST) { return doPost(urlPrefix.concat(urlSuffix), authorization, serialNo, body); } else if (method == RequestMethod.DELETE) { return doDelete(urlPrefix.concat(urlSuffix), authorization, serialNo, body); } else if (method == RequestMethod.UPLOAD) { return doUpload(urlPrefix.concat(urlSuffix), authorization, serialNo, body, file); } return null; } [代码] 网络请求库默认是使用的 Hutool 封装的一套 Java 工具集合来实现 GET 请求 [代码]/** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doGet(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.post(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] POST 请求 [代码] /** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doPost(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.post(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] DELETE 请求 [代码]/** * delete 请求 * * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doDelete(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.delete(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] 上传文件 [代码] /** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @param file 上传的文件 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doUpload(String url, String authorization, String serialNumber, String jsonData, File file) { return HttpRequest.post(url) .addHeaders(getUploadHeaders(authorization, serialNumber)) .form("file", file) .form("meta", jsonData) .execute(); } [代码] 构建 Http 请求头 [代码]private Map<String, String> getBaseHeaders(String authorization) { String userAgent = String.format( "WeChatPay-IJPay-HttpClient/%s (%s) Java/%s", getClass().getPackage().getImplementationVersion(), OS, VERSION == null ? "Unknown" : VERSION); Map<String, String> headers = new HashMap<>(3); headers.put("Accept", ContentType.JSON.toString()); headers.put("Authorization", authorization); headers.put("User-Agent", userAgent); return headers; } private Map<String, String> getHeaders(String authorization, String serialNumber) { Map<String, String> headers = getBaseHeaders(authorization); headers.put("Content-Type", ContentType.JSON.toString()); if (StrUtil.isNotEmpty(serialNumber)) { headers.put("Wechatpay-Serial", serialNumber); } return headers; } private Map<String, String> getUploadHeaders(String authorization, String serialNumber) { Map<String, String> headers = getBaseHeaders(authorization); headers.put("Content-Type", "multipart/form-data;boundary=\"boundary\""); if (StrUtil.isNotEmpty(serialNumber)) { headers.put("Wechatpay-Serial", serialNumber); } return headers; } [代码] 构建 Http 请求返回值 从响应的 HttpResponse 中获取微信响应头信息、状态码以及 body [代码]/** * 构建返回参数 * * @param httpResponse {@link HttpResponse} * @return {@link Map} */ private Map<String, Object> buildResMap(HttpResponse httpResponse) { Map<String, Object> map = new HashMap<>(); String timestamp = httpResponse.header("Wechatpay-Timestamp"); String nonceStr = httpResponse.header("Wechatpay-Nonce"); String serialNo = httpResponse.header("Wechatpay-Serial"); String signature = httpResponse.header("Wechatpay-Signature"); String body = httpResponse.body(); int status = httpResponse.getStatus(); map.put("timestamp", timestamp); map.put("nonceStr", nonceStr); map.put("serialNumber", serialNo); map.put("signature", signature); map.put("body", body); map.put("status", status); return map; } [代码] 至此已完成构建请求参数,执行请求。接下来我们就要实现响应数据的解密以及响应结果的验证签名 对应的官方文档 证书和回调报文解密 签名验证 验证签名 构建签名参数 [代码]/** * 构造签名串 * * @param timestamp 应答时间戳 * @param nonceStr 应答随机串 * @param body 应答报文主体 * @return 应答待签名字符串 */ public static String buildSignMessage(String timestamp, String nonceStr, String body) { return new StringBuilder() .append(timestamp) .append("\n") .append(nonceStr) .append("\n") .append(body) .append("\n") .toString(); } [代码] 证书和回调报文解密 官方文档文末有完整的源码这里就不贴了。贴一个示例大家参数一下 [代码]try { String associatedData = "certificate"; String nonce = "80d28946a64a"; String cipherText = "DwAqW4+4TeUaOEylfKEXhw+XqGh/YTRhUmLw/tBfQ5nM9DZ9d+9aGEghycwV1jwo52vXb/t6ueBvBRHRIW5JgDRcXmTHw9IMTrIK6HxTt2qiaGTWJU9whsF+GGeQdA7gBCHZm3AJUwrzerAGW1mclXBTvXqaCl6haE7AOHJ2g4RtQThi3nxOI63/yc3WaiAlSR22GuCpy6wJBfljBq5Bx2xXDZXlF2TNbDIeodiEnJEG2m9eBWKuvKPyUPyClRXG1fdOkKnCZZ6u+ipb4IJx28n3MmhEtuc2heqqlFUbeONaRpXv6KOZmH/IdEL6nqNDP2D7cXutNVCi0TtSfC7ojnO/+PKRu3MGO2Z9q3zyZXmkWHCSms/C3ACatPUKHIK+92MxjSQDc1E/8faghTc9bDgn8cqWpVKcL3GHK+RfuYKiMcdSkUDJyMJOwEXMYNUdseQMJ3gL4pfxuQu6QrVvJ17q3ZjzkexkPNU4PNSlIBJg+KX61cyBTBumaHy/EbHiP9V2GeM729a0h5UYYJVedSo1guIGjMZ4tA3WgwQrlpp3VAMKEBLRJMcnHd4pH5YQ/4hiUlHGEHttWtnxKFwnJ6jHr3OmFLV1FiUUOZEDAqR0U1KhtGjOffnmB9tymWF8FwRNiH2Tee/cCDBaHhNtfPI5129SrlSR7bZc+h7uzz9z+1OOkNrWHzAoWEe3XVGKAywpn5HGbcL+9nsEVZRJLvV7aOxAZBkxhg8H5Fjt1ioTJL+qXgRzse1BX1iiwfCR0fzEWT9ldDTDW0Y1b3tb419MhdmTQB5FsMXYOzqp5h+Tz1FwEGsa6TJsmdjJQSNz+7qPSg5D6C2gc9/6PkysSu/6XfsWXD7cQkuZ+TJ/Xb6Q1Uu7ZB90SauA8uPQUIchW5zQ6UfK5dwMkOuEcE/141/Aw2rlDqjtsE17u1dQ6TCax/ZQTDQ2MDUaBPEaDIMPcgL7fCeijoRgovkBY92m86leZvQ+HVbxlFx5CoPhz4a81kt9XJuEYOztSIKlm7QNfW0BvSUhLmxDNCjcxqwyydtKbLzA+EBb2gG4ORiH8IOTbV0+G4S6BqetU7RrO+/nKt21nXVqXUmdkhkBakLN8FUcHygyWnVxbA7OI2RGnJJUnxqHd3kTbzD5Wxco4JIQsTOV6KtO5c960oVYUARZIP1SdQhqwELm27AktEN7kzg/ew/blnTys/eauGyw78XCROb9F1wbZBToUZ7L+8/m/2tyyyqNid+sC9fYqJoIOGfFOe6COWzTI/XPytCHwgHeUxmgk7NYfU0ukR223RPUOym6kLzSMMBKCivnNg68tbLRJHEOpQTXFBaFFHt2qpceJpJgw5sKFqx3eQnIFuyvA1i8s2zKLhULZio9hpsDJQREOcNeHVjEZazdCGnbe3Vjg7uqOoVHdE/YbNzJNQEsB3/erYJB+eGzyFwFmdAHenG5RE6FhCutjszwRiSvW9F7wvRK36gm7NnVJZkvlbGwh0UHr0pbcrOmxT81xtNSvMzT0VZNLTUX2ur3AGLwi2ej8BIC0H41nw4ToxTnwtFR1Xy55+pUiwpB7JzraA08dCXdFdtZ72Tw/dNBy5h1P7EtQYiKzXp6rndfOEWgNOsan7e1XRpCnX7xoAkdPvy40OuQ5gNbDKry5gVDEZhmEk/WRuGGaX06CG9m7NfErUsnQYrDJVjXWKYuARd9R7W0aa5nUXqz/Pjul/LAatJgWhZgFBGXhNr9iAoade/0FPpBj0QWa8SWqKYKiOqXqhfhppUq35FIa0a1Vvxcn3E38XYpVZVTDEXcEcD0RLCu/ezdOa6vRcB7hjgXFIRZQAka0aXnQxwOZwE2Rt3yWXqc+Q1ah2oOrg8Lg3ETc644X9QP4FxOtDwz/A=="; AesUtil aesUtil = new AesUtil(wxPayV3Bean.getApiKey3().getBytes(StandardCharsets.UTF_8)); // 平台证书密文解密 // encrypt_certificate 中的 associated_data nonce ciphertext String publicKey = aesUtil.decryptToString( associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), cipherText ); // 保存证书 FileWriter writer = new FileWriter(wxPayV3Bean.getPlatformCertPath()); writer.write(publicKey); // 获取平台证书序列号 X509Certificate certificate = PayKit.getCertificate(new ByteArrayInputStream(publicKey.getBytes())); return certificate.getSerialNumber().toString(16).toUpperCase(); } catch (Exception e) { e.printStackTrace(); } [代码] 验证签名 [代码]/** * 验证签名 * * @param signature 待验证的签名 * @param body 应答主体 * @param nonce 随机串 * @param timestamp 时间戳 * @param certInputStream 微信支付平台证书输入流 * @return 签名结果 * @throws Exception 异常信息 */ public static boolean verifySignature(String signature, String body, String nonce, String timestamp, InputStream certInputStream) throws Exception { String buildSignMessage = PayKit.buildSignMessage(timestamp, nonce, body); // 获取证书 X509Certificate certificate = PayKit.getCertificate(certInputStream); PublicKey publicKey = certificate.getPublicKey(); return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); } /** * 公钥验证签名 * * @param data 需要加密的数据 * @param sign 签名 * @param publicKey 公钥 * @return 验证结果 * @throws Exception 异常信息 */ public static boolean checkByPublicKey(String data, String sign, PublicKey publicKey) throws Exception { java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA"); signature.initVerify(publicKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); return signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8))); } [代码] 至此微信支付 Api-v3 接口已介绍完,如有疑问欢迎留言一起探讨。 完整示例 SpringBoot 参考资料 你真的了解 HTTPS 吗? WechatPay-API-v3
2021-03-02 - #小程序直播# 使用微信小程序直播功能服务,你应当阅读并遵守《微信小程序直播功能服务条款》(以下简称“本条款”)
欢迎你使用微信小程序直播功能! 为使用微信小程序直播功能服务(以下简称“本服务”或“本功能”),你应当阅读并遵守《微信小程序直播功能服务条款》(以下简称“本条款”)。请你务必审慎阅读、充分理解各条款内容,特别是免除或限制责任的相应条款,以及开通或使用某项服务的单独协议,并选择接受或不接受。限制、免责条款可能以加粗形式提示你注意。 除非你已阅读并接受本条款所有条款,否则你无权使用本服务。你对本服务的登录、查看、设置、发布、使用等行为即视为你已阅读并同意本条款的约束。 你有违反本协议的任何行为时,腾讯有权依照违反情况,随时单方限制、中止或终止向你提供本服务,并有权追究你的相关责任。 如果你未满18周岁,请在法定监护人的陪同下阅读本条款及其他上述协议,并特别注意未成年人使用条款。 一、协议效力及范围 1.1本条款是你与腾讯公司之间关于你使用本功能所订立的协议。 1.2为使用本功能,你应当阅读并遵守本条款。本条款是《微信公众平台服务协议》和《微信小程序平台服务条款》、《微信小程序平台运营规范》的补充条款,是其不可分割的一部分。如本条款与上述协议内容存在冲突的,以本条款为准。 1.3本条款适用于在腾讯微信公众平台申请开通并使用本功能进行直播的微信小程序开发者。 二、术语定义 2.1微信小程序直播功能:是腾讯根据本条款向小程序开发者提供的实时视频直播技术服务,开发者可以通过使用本功能,向用户进行实时视频直播。 2.2用户:指使用开发者小程序的微信用户。 2.3开发者:指符合腾讯公司要求的申请并使用本功能的非个人主体小程序开发者。 三、服务的开通及使用 3.1你可根据本条款的规定和本功能相关运营规范、指引文档,向腾讯申请开通本功能。在你提交申请之后,腾讯有权基于微信以及平台有序运营、健康发展等因素对申请予以审核,并有权独立判断、决定是否向你提供本功能以及所提供的具体功能范围。 3.2满足《微信小程序直播功能准入要求》的小程序开发者可以向腾讯申请开通本功能。腾讯有权自行决定本功能相关的准入要求并不时予以调整。个人主体、海外主体小程序开发者暂不支持开通本功能,后续如有调整将另行告知。《微信小程序直播功能准入要求》见本条款附录三。 3.3你使用本功能、提供相关服务等行为须符合相关法规的要求,具有相关合法资质或相关行为已经过相关政府部门的审核批准。腾讯有权要求你按照本功能的设置提交相关资质材料以供审核。腾讯有权在你提交材料无法通过审核的情况下,自行决定是否允许你重新提交相关资质或者直接终止对你提供服务。 3.4开通本服务,你即特此授予腾讯一项非独家的、免费的、永久的、全球性的许可,以存储、使用、复制和显示全部或部分通过本服务上传、发布并展示的内容(包括但不限于你通过本服务所上传、发布的文本、音频、视频、图像、文件、网页、商标、标识以及其包含的其他知识产权等),以便腾讯向你提供本功能相关技术服务并向最终用户进行展示。 四、行为规范 4.1开发者在使用本功能时,不得利用本功能从事以下行为,包括但不限于: (1)发布、传送、传播、储存违反国家法律、危害国家安全统一、社会稳定、破坏民族团结、宣扬邪教和封建迷信、违反公序良俗、社会道德以及散布淫秽、色情、赌博、暴力、恐怖或教唆犯罪等内容; (2)发布、传送、传播、储存侮辱或者诽谤他人、或侵害他人名誉权、肖像权、知识产权、商业秘密等合法权利的内容; (3)发布、传送、传播骚扰信息、垃圾广告信息; (4)发布、传送、传播谣言、虚假信息、诈骗信息或其他不实信息; (5)使用绝对化用语或发布、传送、传播其他违反广告法相关规定的内容; (6)未经许可,发布、传送、传播新闻、游戏、电影、电视剧、综艺节目、体育赛事、境外节目等内容; (7)销售、或通过本功能发布、推广(包括但不限于主播宣传推广、或在商品页面、评论区等信息发布区块展示)根据国家法律法规或腾讯微信平台相关协议、规则要求禁止销售的商品及其信息。《禁售商品目录》见本条款附件一; (8)通过本功能发布外部网站信息或商品,包括但不限于社交、导购、团购、促销、购物平台等外部网站或APP的名称、超链接、二维码、LOGO、联系帐号等信息; (9)通过本功能引导包括但不限于第三方平台或账户、银行或其他线下交易方式; (10)扰乱微信平台运营秩序的行为,包括但不限于刷单、数据造假、评论造假、提供虚假资质等; (11)不得销售或通过本功能发布、推广三无产品、质量不符合国家标准的产品或假冒商品; (12)其他违反法律法规、政策及公序良俗、社会公德、引起公众不适或宣传不健康思想、生活方式等行为; (13)其他侵犯其他用户或第三方合法权益的行为。 4.2开发者通过本功能发布或传播的内容应有合法来源,并保证相关内容为你所有或你已获得相关权利人合法授权。你通过本功能发布或传播的内容(包括但不限于网页、文字、图片、音频、图表、标识等)均由你自行承担责任。 4.3开发者对其主播使用本功能发布或传播的内容、实施的行为承担责任。开发者须确保其主播严格遵守本功能《主播行为规范》,如主播违反平台规则,腾讯将视违规情况对开发者以及主播进行相应处理。《主播行为规范》见本条款附件二。 4.4你清楚了解并同意,你对于本服务的申请和使用的合法性自行承担保证责任,腾讯对你接入本服务的小程序内的服务、内容和数据不承担任何责任和义务,但腾讯保留依法对你提供前述服务和内容的行为进行审核和监督的权利。 4.5开发者应对其使用本服务销售、推广的商品承担责任,包括但不限于产品质量责任、解决消费者交易纠纷的责任等。因你使用本服务产生的任何纠纷、责任等,以及你违反相关法律法规或相关微信平台规则(包括但不限于可适用的《微信公众平台服务协议》、《微信小程序平台服务条款》、《微信小程序平台运营规范》等,简称“微信平台规则”)引发的任何后果,均由你独立承担责任、赔偿损失,与腾讯无关。如侵害到腾讯或他人权益的,你须自行承担全部责任和赔偿一切损失。 五、投诉与处理 如开发者违反本条款或其他与本功能相关的规则,腾讯将视具体违规程度对开发者予以不同程度的处理措施,包括但不限于删除违规商品或信息、采取警告并下线直播、删除直播内容、冻结直播权限、清退帐户等措施。 六、遵守当地法律监管 6.1你在使用本功能的过程中应当遵守当地相关的法律法规,并尊重当地的道德和风俗习惯。如果你的行为违反了当地法律法规或道德风俗,你应当为此独立承担责任。 6.2你应避免因使用本服务而使腾讯卷入政治和公共事件,否则腾讯有权暂停或终止对你的服务。 七、免责声明 7.1你知悉并同意:签署本条款,并不代表腾讯保证必然向你提供本服务。腾讯有权根据产品计划和运营情况独立决定本服务开启、暂停、终止的具体时间、试点时长以及本服务的具体内容,前述决定不经通知即生效,腾讯不对此向你承担任何责任。腾讯不保证你对本功能的使用不会中断或不出现错误。你同意,腾讯可能不经通知不时移除本服务接入的内容或暂停、终止提供本服务。腾讯不保证本功能能够免于网络故障、损失、攻击、病毒、干扰、黑客或其他安全侵入。 7.2你同意,你自愿承担使用本功能的风险。本功能以及通过本功能提供给你的所有产品、服务均按现状提供,腾讯不对此进行任何明示或暗示的保证,包括特定适用性、不侵权等。 7.3在使用本功能中,如你违反本条款或本条款项下承诺或有其他非法、超授权、不合理使用本功能的行为而导致任何诉讼、索赔、争议,你承诺保障与本服务有关的腾讯、腾讯员工、关联公司、代理商、承包商等免受其害,并保证将代为解决,承担相关责任和所有支出(包括律师费、相关赔偿、罚金等)。 八、用户个人信息保护 8.1 本服务将适用《微信公众平台服务协议》关于“用户个人信息保护”的约定。同时你也同意,你将遵守《微信小程序平台服务条款》关于“用户个人信息保护”的相关规定。 九、其它 9.1你使用本服务即视为你已阅读并同意接受本条款的约束,腾讯有权在必要时予以修改、调整。你可以在相关服务页面查阅最新版本的条款。本条款变更后,如果你继续使用本功能,即视为你已接受修改后的协议。如果你不接受修改后的协议,应当停止使用本功能。 9.2本条款签订地、争议解决管辖方式,均与《微信公众平台服务协议》一致,即为中华人民共和国广东省深圳市南山区,由本条款签订地有管辖权的人民法院管辖。 9.3本条款的成立、生效、履行、解释及纠纷解决,适用中华人民共和国大陆地区法律(不包括冲突法)。 9.4这是一份动态更新的文档,我们会根据新出现的问题、相关法律法规更新或产品运营的需要来对其内容进行修改并更新,制定新的规则,保证微信用户的体验。你应能反复查看以便获得最新信息,请定期了解更新情况。 腾讯公司 附件一禁售商品目录 根据相关法律法规,禁止通过互联网销售以下商品: 一级类目 二级类目(包括但不限于) 1.枪支、刀具及军警用品 1.1枪支、弹药、军火及仿制品; 1.2管制器具(弩、电击器、防狼喷雾等)、管制类刀具及飞镖; 1.3 1.1及1.2的配件、附属产品、仿制品及仿制品的衍生工艺品; 1.4警用、军用制服、标志、设备及制品; 1.5国家机关制服机器相关配件类商品。 2.易燃易爆、有毒化学品、毒品 2.1易燃易爆物品、烟花爆竹; 2.2毒品、危险化学品、制毒原料及致瘾性药物; 2.3毒品吸食工具及配件、介绍制作易燃易爆品方法的相关教程及书籍; 2.4农药。 3.危害国家安全、破坏社会稳定的商品 3.1危害国家安全、破坏国家统一、宣扬恐怖主义和极端主义、破坏民族团结、种族歧视、破坏国家宗教政策、宣扬邪教的商品; 3.2涉及机密的文件资料,如国家机关的内部资料等; 3.3破网、翻墙软件及VPN代理服务; 3.4法律法规禁止或者不适宜在国内出版发行的书籍、音像制品、视频、文件资料; 3.5国家禁止的集邮票品以及未经邮政行业管理部门批准制作的集邮品,以及一九四九年之后发行的包含“中华民国”字样的邮品。 4.淫秽、色情低俗类用品 4.1含有色情淫秽低俗暴力信息的图片、读物、动漫、游戏、音像制品及视频; 4.2色情陪聊服务,色情网站论坛的账号及邀请码,用于传播色情信息的软件、种子文件、网盘资源; 4.3催情类商品,原味内衣及相关商品(如二手男女内衣等穿过的贴身衣物); 4.4境外成人秀商品,如泰国成人秀、天皇秀等。 5.涉及隐私、人身安全类用品 5.1用于监听、窃取隐私或机密的软件及设备; 5.2用于非法摄像、录音、取证等用途的设备; 5.3身份证、护照、社会保障卡等依法可以用于身份证明的证件; 5.4盗取或破解账号密码的软件、工具、教程及产物; 5.5个人隐私信息及企业内部数据;提供个人手机定位、电话清单查询、银行账户查询等服务; 5.6已报废、达到国家强制报废标准、非法拼装或通过走私、盗取、抢劫、诈骗等违法犯罪手段获得的车辆及其“五大总成”; 5.6汽车安全带扣等具有交通安全隐患的汽车配件类商品; 5.7妨害交通安全秩序的产品,如汽车解码器、万能钥匙、汽车反雷达测速仪、电子狗、汽车隐性喷雾、卷帘式车牌架、雷达干扰器、牌照遮盖设备、交通信号灯变换器等相关产品; 5.8 载人航天器、航空配件、模型图纸类商品。 6.药品及医疗器械 6.1处方药; 6.2依据《中华人民共和国药品管理法》认定的假药、劣药; 6.3精神类、麻醉类、有毒类、放射类、兴奋剂类、计生类药品、血清、疫苗、血液制品、医疗用毒性药品、有毒中药材;国家公示已查处、药品监督管理局认定禁止生产、使用的药品; 6.4 注射类美白针剂、溶脂针剂、填充针剂、瘦身针剂等用于人体注射的美容针剂类商品。 7.单证卡券类 7.1正在流通的人民币及外币、伪造变造的货币及设备; 7.2境内运营商的上网资费卡或资费套餐及SIM卡,及未经实名登记便可登录使用的通话、上网类商品及服务; 7.3伪造变造国家机关或特定机构颁发的文件、证书、公章、防伪标签等; 7.4伪造变造银行发行或承办的银行卡、信用卡、医保卡、证券卡、公积金卡等; 7.5尚可使用或用于报销的票据,尚可使用的外贸单证; 7.6邮局包裹、EMS专递、快递等物流单据凭证及单号; 7.7未公开发行的国家级正式考试答案。 8.动植物、动植物器官及动物捕杀工具 8.1《国家重点保护野生动物名录》、《濒危野生动植物种国际贸易公约》附录中包含的动物及其相关制品,如象牙、玳瑁、犀牛角制品等; 8.2已灭绝动物与现有国家二级以上保护动物的化石,如恐龙化石等; 8.3《国家重点保护野生植物名录》、《濒危野生动植物种国际贸易公约》附录中包含的植物,如银杉、红豆杉、发菜等; 8.4《国家保护的有益的或者有重要经济、科学研究价值的陆生野生动物名录》中包含的动物及其相关制品,如松鼠、缅甸陆龟、平胸龟的活体、内脏、任何肢体、皮毛、标本或其他制成品等; 8.5猫狗肉、猫狗皮毛、鱼翅、熊胆等其他有违公益或不利于环境保护的动植物及其制品; 8.6宠物活体; 8.7动物捕杀工具类。 9.虚拟商品 9.1虚拟货币,如比特币、Q币; 9.2以支付、社交、媒体为主要功能的互联网用户账号类商品,如腾讯及旗下公司提供的各项服务账号及服务,腾讯视频VIP、QQ账号、微信帐号、微信读书年卡等; 9.3游戏卡充值,外挂、私服相关的网游类商品,未经国家备案的网络游戏、游戏点卡、货币等相关服务类商品; 9.4航空公司积分、里程;航空公司积分/里程兑换的机票; 9.5代账号注册、代认证服务、炒作人气、代刷流量、代发布/删除特定信息、代拉新推广、顶帖删帖服务、代网络投票等; 9.6用于兑换商品实物或服务的定额卡券、储值卡券、储值服务或将购买款项分期返还的交易,如美发卡、超市卡等。 10.特许商品 10.1食用盐; 10.2烟草专卖品、烟草专用机械及烟草制品,例如: 1)烟草、雪茄类制品; 2)香烟、烟盒、烟标等,包括电子香烟和戒烟产品; 3)烟草替代品及辅助工具、无烟烟草制品(如电子烟、IQOS、鼻烟); 4)将烟丝和烟纸加工成成品烟的烟草专用机械; 10.3军需、国家机关专供、特供等商品; 10.4未经许可发布的奥林匹克运动会、世界博览会、亚洲运动会等特许商品。 11.其他 11.1赌博博彩类商品、抽奖类商品; 11.2非法传销类商品; 11.3非法所得及非法用途软件、工具或设备,如作弊工具、偷电设备、群发设备、蹭网卡、蹭网器、拨号器、撬锁工具及相关教程等; 11.4封建迷信类商品和服务; 11.5其他国家明确禁止销售的商品。 除上述禁售商品外,禁止通过本功能销售、推广以下商品: 一级类目 二级类目(包括但不限于) 1.成人用品类 1.1两性用品及周边相关的化妆品、服装服饰; 1.2情趣家具; 1.3避孕套。 2.保健品、各种疾病治疗、美容美体类商品 2.1描述壮阳或存在其他夸大功效的词的保健食品; 2.2目前带“健”字批号的保健药品; 2.3保健化妆品,如保健香水、漱口水等; 2.4保健用品,如健身器、按摩器、磁水器、健香袋、衣服鞋帽、垫毯、艾炙贴、针灸贴等; 2.5针灸、纹身、美容整形、减脂类产品; 2.6美体塑形用品及工具,如塑身按摩贴、塑身贴、塑身带、增高用品、胸部护理。 3.投资、金融类 3.1证券、基金、期货投资、保险、理财等金融相关商品; 3.2 POS机或第三方支付设备; 3.3三级分销、招代理、招商加盟、店铺买卖; 3.4国内/海外投资房产、炒房、互联网金融(高利贷、私人贷款、贷款推广等)。 4.仿冒品、疑似假货、假货类 4.1外观侵权、商标侵权及高仿、假冒、盗版商品; 4.2存在制假风险的品牌配件类商品,如品牌化妆品空瓶、品牌奶粉空罐、品牌酒空瓶等。 5.古董文物、收藏品 5.1古董文物; 5.2名人字画。 6.宗教类 6.1宣传宗教教义及活动的用品; 6.2宗教用品、宗教工艺品销售宗教用品,包括佛珠、佛龛、佛香、僧袍其他法器等宗教用品、或纪念品。 7.三无产品 根据规定需要有中文厂名,中文厂址、电话、许可证号、产品标志、生产日期、中文产品说明书、如有必要时还需要有限定性或提示性说明等等,凡是缺少的均视为不合格产品。上述要求缺少其中之一,均可视为“三无产品”。 8.药品及医疗器械 8.1非处方药; 8.2兽药; 8.3医疗器械。 9.其他 9.1各类服务; 9.2自用闲置转让、二手类商品、游戏装备; 9.3殡葬用品用具。 附件二主播行为规范 一、【目的及原则】 为维护微信小程序直播功能(以下简称“本功能”)的正常使用和运营,保障微信用户的合法权益,制定本主播行为规范(以下简称“本规范”)。 二、【适用范围】 本规范适用于使用本功能进行直播的主播,包括但不限于主播的行为、主播的昵称、头像及发布的直播内容等。 三、【主播注册规范】 未成年人不得作为主播注册并使用本功能。主播需根据腾讯微信平台规则申请主播认证,并提供真实、合法的身份材料进行实名认证。主播已清楚了解并同意,使用本功能即视为已同意腾讯收集主播的相关个人信息用于本功能的实名身份验证。禁止被列入网络表演(直播)行业主播黑名单的主播申请本功能的主播认证。一经发现,平台将视为开发者严重违规进行处理。 四、【发布信息规范】 主播不得通过本功能发布以下涉及政治或违法的信息或内容,包括但不限于:(1)违反宪法确定的基本原则的; (2)危害国家安全,泄露国家秘密,颠覆国家政权,破坏国家统一的; (3)损害国家荣誉和利益的; (4)煽动民族仇恨、民族歧视,破坏民族团结的; (5)破坏国家宗教政策,宣扬邪教和封建迷信的; (6)散布谣言,扰乱社会秩序,破坏社会稳定的; (7)散布淫秽、色情、赌博、暴力、恐怖或者教唆犯罪的; (8)侮辱或者诽谤他人,侵害他人合法权益的; (9)煽动非法集会、结社、游行、示威、聚众扰乱社会秩序; (10)以非法民间组织名义活动的; (11)含有法律、行政法规和国家规定禁止的其他内容。 2. 主播不得歪曲、丑化、亵渎、否定英雄烈士的事迹和精神,或针对自然灾害、历史事件等发表相关不当言论。3. 主播不得发布诋毁、损害微信平台形象的言论或发布与本平台相关的不实信息、恶意信息。4. 主播应同时遵守《微信小程序直播功能服务条款》中开发者行为规范,如主播违反该行为规范,腾讯将视违规情况对开发者以及主播进行相应处理。 五、【着装规范】 主播服装不得过透过露,需衣着大方得体、干净整洁。不得穿着以下类型服装进行直播: 1. 女主播不得穿着大尺度裸露背部、裸露内衣、穿着露沟或容易露沟服装、短裙或短裤下摆高于臀下线的服装;不得仅着比基尼及类似内衣的服装或不穿内衣,不能露出内衣或内裤(安全裤)。2. 男主播不得仅着下装或穿着内裤、紧身裤的服装直播。3. 严禁穿着以下特殊类型服装进行直播:(1)带有中华人民共和国(包含港澳台)国家机关人员、军队工作制服(警服、军服、检察院、法院、工商、城管制服等); (2)情趣制服、情趣内衣、暴露装、透视装、肉色紧身衣、内衣外穿、半截抹胸、渔网袜、吊带袜、三角短裤或呈现三角状短裤、低腰超短裤、有性暗示文字或图片的服装等。 六、【行为规范】 1. 主播不得展示吸烟、酗酒、赌博等不良行为,不得使用粗口等不雅言论。2. 主播不得由上至下拍摄胸部或由下至上拍摄腿部、臀部等敏感部位;不得长时间聚焦腿部、脚部等敏感部位。3. 主播不得进行带有性暗示的抚摸、拉扯、舔咬、拍打等动作,或使用道具引起观众对性敏感部位的注意;利用身体上的敏感部位进行游戏,包括但不限于:猜内裤的颜色、猜内衣的颜色、剪丝袜、直播脱/穿丝袜、撕扯或剪衣服等。4. 主播不得做出带有暴露风险的动作,包括但不限于:弯腰、高抬腿、双腿分开、劈叉、下腰、倒立、频繁切换坐姿等。5. 主播不得直播过程中做与直播内容或产品无关的事情,包括但不限于:玩手机、吃饭、睡觉、或长时间空置镜头、不进行直播等。6. 主播不得进行具有挑逗性或诱导性的肢体动作或表演,如脱衣舞等。7. 主播不得以诱惑、挑逗性质的声音、语言吸引观众,包括且不限于模仿动物发情时的叫声、使用直接或者隐晦性暗示词语。8. 主播不得展示或露出纹身。9. 主播不得以暴力工具枪支(含仿真枪)、刀具、赌博工具、性用品、内衣等涉及暴力或不雅内容的物品作为表演道具,或进行任何形式的高危表演。10. 主播不得在违规的直播地点进行直播,包括但不限于:路上、机动车(包括汽车与电动车等所有上路行驶车辆)内或行驶期间、床上或其他私密环境、涉黄、涉毒场所以及敏感时期、敏感场景中或出现反党反动的环境中。11. 主播不得在镜头前展示反国家反党标志的物品。12. 主播不得进行任何侵犯他人隐私,危及公众利益或类似行为,包括但不限于未经当事人同意,公开他人姓名、住址、电话等个人资料及其他隐私信息。13. 主播直播推荐、展示内衣、丝袜或类似品类物品过程中,不得进行试穿或以任何形式使用真人模特进行展示,或播放、展示真人模特试穿的视频或照片。14. 主播在直播过程中如需要试穿或更换服装,以对服饰类物品进行展示,不得出现裸露身体性敏感部位、内衣内裤露点、动作引诱等情色画面。15. 主播在直播过程中展示、推广物品,不得出现血腥、暴力、恶心等引起观众视听不适的画面或内容。16. 主播不得销售或通过本功能推广根据国家法律法规或腾讯微信平台相关协议、规则要求禁止销售的商品。17. 同一场次直播中,直播间主播人数不得超过2名。 七、【投诉及处理】 如腾讯发现或收到他人举报主播存在违反本规范的行为,腾讯将视具体违规程度对主播及开发者予以不同程度的处理措施,包括但不限于主播禁播、采取警告并下线直播、删除直播内容、冻结直播权限、清退帐户等措施。 八、【动态文档】 这是一份动态更新的文档,我们会根据新出现的问题、相关法律法规更新或产品运营的需要来对其内容进行修改并更新,制定新的规则,保证用户的体验。主播应能反复查看以便获得最新信息,请定期了解更新情况。 腾讯公司 附件三微信小程序直播功能准入要求 一、类目要求: 1.小程序开发者为国内非个人主体开发者; 2.小程序开发者为“商家自营”类目下的以下12个垂直品类,具体可参考《微信小程序开放的服务类目》: 商家自营-百货; 商家自营-食品; 商家自营-初级食用农产品; 商家自营-服装/鞋/箱包; 商家自营-酒/盐; 商家自营-玩具/母婴用品(不含食品); 商家自营-家电/数码/手机; 商家自营-美妆/洗护; 商家自营-珠宝/饰品/眼镜/钟表; 商家自营-运动/户外/乐器; 商家自营-家居/家饰/家纺; 商家自营-办公/文具 二、运营要求: 小程序开发者主体近半年内(自然年)未出现严重违规行为,如涉黄涉政、假冒伪劣、欺诈等。此外,除了参考小程序本身的支付行为和用户访问情况之外,也会综合参考小程序开发者主体的支付数据、广告数据和公众号粉丝数等情况,对小程序开发者申请微信小程序直播能力进行综合评估。 以上准入要求于2019年12月23日进行公示生效。为营造良好健康的微信生态,腾讯公司有权对《微信小程序直播功能准入要求》不时予以调整并公布,请予以关注。 腾讯公司
2020-03-10 - 小程序不同页面的异步回调,callback和promise的使用讲解
发个扫盲贴,大神请绕道。最近好多同学问我如何再请求数据成功后直接使用数据。我们通常的做法就是在请求成功后,再调用我们定义的方法,进而使用数据。如下代码 [代码] onLoad() { let that=this wx.cloud.database().collection("users").get({ success(res) { that.setData(res.data) }, fail(res) { } }) }, showData(dataList) { //.........做数据处理 }, [代码] 我们这样写其实也没什么不对,但是如果数据请求和使用是在两个不同的页面呢。 比如我们在app.js里请求位置,获取用户信息。然后在首页index.js里要使用这些数据,那么我们这么写就有问题了。下面就来教大家两种方式来很好的解决这个问题。 一,通过callback回调。 先看下代码,然后我再具体给大家讲解下原理。 app.js里定义如下方法 [图片] 然后再index.js 里这么使用 [图片] 这时候,其实就可以看到了,我们在首页index.js里调用了app.js里的请求数据的方法,并且可以在index.js里直接使用数据。 原理讲解 原理其实很简单,就是我们在app.js里的获取数据的方法里定义一个参数。而这个参数和普通参数唯一不同的地方,就是这个参数是个function方法 [图片] 我们上图的callback参数,其实就是下图 function(result){} [图片] 把function方法作为一个参数传递进去的目的,就是为了下面的回调。 [图片] 我们这个callBack参数,可以在请求数据成功或者失败的时候作为一个方法调用。这样就可以把请求到的数据,回传回去了。 讲的有点绕,不知道大家有没有被绕晕。这在java开发中,其实就相当于监听者模式。说白了就是在一个页面里监听另外一个页面的动作,如获取数据成功,当监听到数据获取成功这个动作以后,就可以直接把数据回传回来了。 如果觉得这种方法有点绕,不好使用,我们就用下面的这个第二种方式。 二,promise promise的好处就是可以不用那个层层传递,不用那么绕。 还是先看代码,后面结合代码给大家讲下原理 app.js里定义如下方法 [图片] index.js里这么调用 [图片] 用句通俗的话说,就是通过promise让我们的数据请求和使用看上去是在同一个页面完成。怎么实现的呢 1,在app.js里把数据请求封装到promise里,然后把promise返回到我们的首页index.js里 2,在首页里使用这个promise 实现数据的获取和使用。 在具体些就是下面这几步 promise基础用法 [图片] 1、new 一个Promise对象 2、请求数据的异步代码写在promise的函数中 3、promise接受两个参数,一个resolve(已成功success),一个reject(已失败fail) 4、promise有三种状态pendding(进行中,当new了promise就是pendding的状态)、fulfilled(已成功)、rejected(已失败),当成功的时候调用resolve将状态改为已成功,当失败的时候调用reject将状态改为已失败,一旦状态发生改变之后,状态就凝固了,后面就无法改变状态了,成功会将成功的数据返回,失败会将失败的信息返回。 5、在需要获取数据的地方通过promise.then()的方式获取,这里面接受两个参数,都是匿名函数,第一个是接受成功的函数,第二个是失败时候的函数 [图片] 好了,到这里我们两种不同页面的异步回调就给大家将完了。代码就完整的给大家贴出来吧,方便大家日后使用 app.js [代码]//app.js App({ //第二种,通过promise promiseGetData() { let promise = new Promise(function(success, fail) { wx.cloud.database().collection("users").get({ success(res) { success(res) }, fail(res) { fail(res) } }) }) return promise; }, //第一种,通过callback的方式来实现回调 callBackGetData(callBack) { wx.cloud.database().collection("users").get({ success(res) { callBack(res) }, fail(res) { callBack(res) } }) }, }) [代码] index.js [代码]// 异步调用,callback const app = getApp() Page({ clickBtn() { //按钮点击 //callback方式 // app.callBackGetData(function(result) { // console.log("dataList", result) // }) //promise方法 let promise = app.promiseGetData() promise.then((res) => { //获取成功的结果,res中存着获取成功时的数据 console.log("成功", res) }, (error) => { // 获取数据失败时 console.log("失败", error) }) }, }) [代码]
2019-11-14 - 阅读 9小时搞定微信小程序开发 源码总结(小书架)。
目录与页面模块 读代码首先应该认真阅读文档的[代码]README.md[代码]。 看下小书架页面模块、目录结构清晰,虽然模块不多,实际业务开发中首先应该构思拆解业务模块,确定目录结构。小程序包大小超过 2M 需要分包,所以从一开始确定目录结构的时候就要考虑进去,不然线上跑起来再去分包会有点小麻烦。 目录结构 [代码]├── config │ └── config.js ├── images ├── pages │ ├── books │ │ ├── books.js │ │ ├── books.json │ │ ├── books.wxml │ │ └── books.wxss │ ├── comment │ │ ├── comment.js │ │ ├── comment.js │ │ ├── comment.js │ │ └── comment.wxss │ ├── detail │ │ ├── detail.js │ │ ├── detail.js │ │ ├── detail.js │ │ └── detail.wxss │ ├── my │ │ ├── my.js │ │ ├── my.js │ │ ├── my.js │ │ └── my.wxss │ └── myBooks │ ├── myBooks.js │ ├── myBooks.js │ ├── myBooks.js │ └── myBooks.wxss ├── utils │ └── util.js ├── app.js ├── app.json ├── app.wxss └── project.config.json [代码] 各页面模块 页面 描述 books 首页/书籍列表页 comment 评论页面 detail 书籍详情页 my 个人中心页 myBooks 已购书籍页 接口封装 小书架没有对 wx.request 封装。考虑到是入门教程,没做处理也是正常。接口请求路径封装在 config.js [代码]// 服务器域名 const baseUrl = 'http://127.0.0.1:[your port]/'; // 获取书籍信息接口地址(可选择全部或单个书籍) const getBooksUrl = baseUrl + 'api/book/getBooks'; //... module.exports = { getBooksUrl: getBooksUrl //... }; [代码] 也可以根据个人习惯进行封装,这里一定要写注释以及考虑后期维护 [代码]let returnCancel = (memberId, refundId) => http.post(`api/return/goods/cancel`, { memberId: memberId, refundId: refundId}) export default { returnCancel }; [代码] 页面 book book.js 代码干净、整洁。注释很详细,虽然这是入门教程,但我们开发的时候也要养成这样的好习惯。 data [代码]data: { bookList: [], // 书籍列表数组 indicatorDots: false, // 是否显示轮播指示点 autoplay: false, // 是否自动播放轮播 sideMargin: '100rpx', // 幻灯片前后边距 showLoading: true // 是否显示loading态 //... }, [代码] onLoad getBookList 方法获取所有书籍列表,不要把所有的 wx.request 都写在load里面,简单封装下可维护性大大提高 [代码]/** * 获取所有书籍列表 */ getBookList: function() { let that = this; wx.request({ url: api.getBooksUrl, data: { is_all: 1 }, success: function(res) { let data = res.data; // console.log(data); if (data.result === 0) { setTimeout(function() { that.setData({ bookList: data.data, showLoading: false }); }, 800); } }, error: function(err) { console.log(err); } }); }, onLoad: function(options) { let that = this; that.getBookList(); }, [代码] loading处理 [代码]<block wx:if="{{showLoading}}"> <view class="donut-container"> <view class="donut"></view> </view> </block> // 默认 true 。getBookList 方法成功回调里设为 false。没有错误处,接口请求失败的话应该也做下处理的 [代码] comment comment.js 封装了检查用户输入的方法,实际业务中如果输入较多的话,可以提炼到 unit.js。封装了 wx.showToast [代码]// 检查输入是否为空,起名称注意语义话 checkEmpty: function(input) { return input === ''; }, /** * 检查用户是否输入了非法字符 */ checkIllegal: function(input) { let patern = /[`#^<>:"?{}\/;'[\]]/im; let _result = patern.test(input); return _result; }, /** * 检查用户输入 */ checkUserInput: function() { /* * 检测用户输入 * 1. 是否包含非法字符 * 2. 是否为空 * 3. 是否超出长度限制 */ let that = this; let comment = that.data.comment; let showToastFlag = false; let toastWording = ''; if (that.checkEmpty(comment)) { showToastFlag = true; toastWording = '输入不能为空'; } else if (that.checkIllegal(comment)) { showToastFlag = true; toastWording = '含有非法字符'; } else if (comment.length > 140) { showToastFlag = true; toastWording = '长度超出限制'; } if (showToastFlag) { that.showInfo(toastWording); return false; } else { return true; } }, [代码] 封装toast [代码]showInfo: function(info, icon = 'none', callback = () => {}) { wx.showToast({ title: info, icon: icon, duration: 1500, mask: true, success: callback }); }, [代码] detail 简单的返回刷新处理以及下载进度条 [代码]// 从上级页面返回时 重新拉去评论列表 backRefreshPage: function() { let that = this; that.setData({ commentLoading: true }); that.getPageData(); }, /** * 生命周期函数--监听页面显示 */ onShow: function() { if (wx.getStorageSync('isFromBack')) { wx.removeStorageSync('isFromBack') this.backRefreshPage(); } } [代码] 进度条(这个还是很少见的需求,很可爱) [代码]<!-- 下载进度条 --> <view class="loading-container" wx:if="{{downloading}}"> <progress percent="{{downloadPercent}}" stroke-width="6" activeColor="#1aad19" backgroundColor="#cdcdcd" show-info /> </view> [代码] my 主要是检查登陆。myBooks没有什么亮眼的操作,就不上场了。 [代码]data: { userInfo: {}, // 用户信息 hasLogin: wx.getStorageSync('loginFlag') ? true : false // 是否登录,根据后台返回的skey判断 }, [代码] app.js 主要负责检查处理登陆信息 [代码]App({ // 小程序启动生命周期 onLaunch: function () { let that = this; // 检查登录状态 that.checkLoginStatus(); }, // 检查本地 storage 中是否有登录态标识 checkLoginStatus: function () { let that = this; let loginFlag = wx.getStorageSync('loginFlag'); if (loginFlag) { // 检查 session_key 是否过期 wx.checkSession({ // session_key 有效(为过期) success: function () { // 直接从Storage中获取用户信息 let userStorageInfo = wx.getStorageSync('userInfo'); if (userStorageInfo) { that.globalData.userInfo = JSON.parse(userStorageInfo); } else { that.showInfo('缓存信息缺失'); console.error('登录成功后将用户信息存在Storage的userStorageInfo字段中,该字段丢失'); } }, // session_key 过期 fail: function () { // session_key过期 that.doLogin(); } }); } else { // 无登录态 that.doLogin(); } }, // 登录动作 doLogin: function (callback = () => {}) { let that = this; wx.login({ success: function (loginRes) { if (loginRes.code) { /* * @desc: 获取用户信息 期望数据如下 * * @param: userInfo [Object] * @param: rawData [String] * @param: signature [String] * @param: encryptedData [String] * @param: iv [String] **/ wx.getUserInfo({ withCredentials: true, // 非必填, 默认为true success: function (infoRes) { console.log(infoRes,'>>>') // 请求服务端的登录接口 wx.request({ url: api.loginUrl, data: { code: loginRes.code, // 临时登录凭证 rawData: infoRes.rawData, // 用户非敏感信息 signature: infoRes.signature, // 签名 encryptedData: infoRes.encryptedData, // 用户敏感信息 iv: infoRes.iv // 解密算法的向量 }, success: function (res) { console.log('login success'); res = res.data; if (res.result == 0) { that.globalData.userInfo = res.userInfo; wx.setStorageSync('userInfo', JSON.stringify(res.userInfo)); wx.setStorageSync('loginFlag', res.skey); callback(); } else { that.showInfo(res.errmsg); } }, fail: function (error) { // 调用服务端登录接口失败 that.showInfo('调用接口失败'); console.log(error); } }); }, fail: function (error) { // 获取 userInfo 失败,去检查是否未开启权限 wx.hideLoading(); that.checkUserInfoPermission(); } }); } else { // 获取 code 失败 that.showInfo('登录失败'); console.log('调用wx.login获取code失败'); } }, fail: function (error) { // 调用 wx.login 接口失败 that.showInfo('接口调用失败'); console.log(error); } }); }, // 检查用户信息授权设置 checkUserInfoPermission: function (callback = () => { }) { wx.getSetting({ success: function (res) { if (!res.authSetting['scope.userInfo']) { wx.openSetting({ success: function (authSetting) { console.log(authSetting) } }); } }, fail: function (error) { console.log(error); } }); }, // 获取用户登录标示 供全局调用 getLoginFlag: function () { return wx.getStorageSync('loginFlag'); }, // app全局数据 globalData: { userInfo: null } }); [代码] 中规中矩的小程序,入门还是可以的,代码简洁干净。新手的话撸一遍还是可以的。这样不知道算不算侵权,侵删。
2019-11-15 - 小程序请求数据双向混合加密和防篡改+防重放攻击的实现
前言 大家好,借着中秋放假明日又要上班的这个晚上,平常又没空,趁这个时间点就决定来一篇。 [图片] 我们都知道微信小程序的服务端API上,官方可谓是做足了心思,对用户的数据进行加密,虽然对我们开发者来说似乎是一种麻烦,但是从长远角度来看,是十分有必要的,用户隐私高于一切。 那么在小程序的开发过程中与后端对接接口时是否有想过这样的问题呢? HTTPS真的百分百安全吗? 数据被成功抓包后安全吗? 数据被篡改后还有效吗? 请求被重放后安全吗? 世界上没有绝对安全的系统,但我们可以让它被破解的成本变高。 本篇文章专业性并不高,如果存在错误请大家为我指出来,以免误导别人,谢谢! 以下我们将小程序端称为C端,服务端称为S端,服务端代码是Node.js,仅供参考,但原理都一样,后端可以是其它语言。 思考 围绕着以上的问题,探究一下问题的答案? 本部分只对问题做思考,具体实现请参考下面的实现部分。 HTTPS真的百分百安全吗?只能说是相对安全,当然,在微信小程序的沙箱环境里,HTTPS通信会更加安全,否则官方可能会要求我们对请求加密了对吧。但百密一疏,C端是否存在漏洞,假设C端安全了难道S端就安全嘛?细思恐极,退一万步讲,百分百安全是不存在的,由于篇幅问题相关漏洞大家可以搜索探究。 假设数据被中间人成功抓包,如果数据是明文传输,那么将导致数据泄露,因此对数据进行加密是必要的,但应该如何加密呢?如何做到密钥安全?C端和S端如何进行数据的加解密?RSA和AES加密应该使用哪种加密明文?如何充分发挥RSA和AES两个加密算法的特长? 假设数据被篡改,如果因为请求数据被篡改而导致严重后果,那么很大程度上其实是代码设计有问题,正常设计中安全应被摆在重要的位置,至少不应该出现购买某样商品时是通过C端发送商品价格给S端调起支付那样(只需篡改商品价格即可支付极少的钱购买商品),如果代码设计并不存在严重问题,那么数据被篡改也是不可忽视的,我们需要进行数据签名,让不同的数据拥有唯一的MD5哈希值,如果数据被篡改,通过哈希值即可判断数据是否被篡改,当然也可能会有人问,既然数据被篡改,那攻击者会不会重新生成一个哈希值代替?是的。但是我们有接下来会说到的key,key会作为签名的数据变量之一,由于攻击者并不知道key值因此无法重新签名,key值建议不是固定值而是周期性更换的随机值,例如随着用户的登录态而产生并随之抹除。 假设请求被重放,大部分时候由于数据被加密和防篡改处理过,攻击者并无法直接获得数据或篡改,但如果通过劫持到的登录认证请求的原始数据并重新发起该请求,则攻击者将可能获得重要的认证数据,使得系统将攻击者作为正常用户处理,后续的请求攻击者仍能伪装成正常的用户进行后续攻击。 期望效果 在开始实现之前先看一下完成后的效果,以下是截取了Network中其中一个GET请求发送的数据: [图片] 实际上发送的数据是如下图所示: [图片] 然后是该请求返回的数据: [图片] 实际上返回的数据是如下图所示: [图片] 整个流程: [图片] 实现 由于整个流程在C端的实现上顺序反过来的,因此下面的步骤也将是反向而行。 工具库下载 工欲善其事,必先利其器!这三个库是经过修改压缩的,支持在小程序上使用并且体积可观(总共69.1KB,如果不涉及密码哈希处理只需要前两个库,体积只有65.3KB),接下来的实现操作将会使用到,建议大家可以根据实际情况对功能进行二次封装: CryptoJS.js:点击下载 RSA.js:点击下载 SHA256.js(可选):点击下载 在线的各类加解密工具(可选,可以收藏起来,平时测试挺有用):点击访问 请求数据防重放 要防范请求重放攻击,首先需要了解Unix时间戳 timestamp概念,和时间戳不一样的是它的单位是秒,事实上这个需求也只需要秒级即可。除此之外还将用到另一个值:nonce,它是一个随机产生并只能被使用一次的值,长度自定,请求越频繁长度需要越长(降低同一时间产生相同nonce的几率),C端发送请求时需要将timestamp和生成的nonce加入发送的参数中。那么,如何将两者结合呢?我这为防重放的目标下了一个简单的定义。 同样的请求只能发生一次,且请求必须在规定时间内发出。 何为同样的请求?不是指两次发送的参数一致就是一样的,而是连timestamp和nonce也一样才算是同样的请求。 那么S端如何确认其是同样的请求呢? S端每次接收到一个请求,都会将该请求的nonce存入缓存并保持60秒(这个阈值不一定是60秒,可以根据实际需要定义),时间过后该值将被移除,建议S端采用Redis存储nonce,这样可省去检测和移除nonce的代码。如果S端发现当前请求的nonce存在于已存储的nonce之中,则此请求发生重复,那么timestamp有何用? 如果只使用nonce我们只能保证该请求60秒内不会重复,但60秒后依然任人宰割,这不是要的结果。所以timestamp将用来限制时间,S端时间戳减去C端发送请求的时间戳,得到的差值为N秒,如果N秒大于60秒则此请求过期,那么则可以保证,60秒内因为nonce相同而被判为请求重放,60秒后因为时间差超过而被判为请求已过期,因此确保了请求不会被重放。。 以下展示三种情况: [代码]C端时间戳:1568487720 //2019/9/15 03:02:00 C端NONCE:5rKbMs2Fm3 C端发送请求 -> S端接收请求 S端时间戳:1568487722 //2019/9/15 03:02:02 CS端时间差:1568487722 - 1568487720 = 2 C端NONCE是否存在于缓存:false 【重放校验通过】 [代码] [代码]C端时间戳:1568487722 //2019/9/15 03:05:22 C端NONCE:IzFEs52bAC C端发送请求 -> S端接收请求 S端时间戳:1568487922 //2019/9/15 03:02:00 CS端时间差:1568487922 - 1568487722 = 200 C端NONCE是否存在于缓存:false 【重放校验不通过,请求已过期,因为时间差超过60秒】 [代码] [代码]C端时间戳:1568487720 //2019/9/15 03:02:00 C端NONCE:IxwPHQU0nA C端发送请求 -> S端接收请求 S端时间戳:1568487722 //2019/9/15 03:02:02 CS端时间差:1568487722 - 1568487720 = 2 C端NONCE是否存在于缓存:true 【重放校验不通过,此请求为重放请求,因为nonce已经存在,此请求已经完成,不可重复】 [代码] timestamp和nonce将作为参数参与下面部分的签名。 请求数据防篡改 C端数据签名 首先通过对参数按照参数名进行字典排序(调过一些第三方API的朋友应该明白),假设当前需要传输的参数如下: [代码]{ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3" } [代码] 进行字典排序,参数名顺序应为: [代码]const keys = Object.keys(data); //获得参数名数组 keys.sort(); //字典排序 console.log(key); //["a", "b", "c", "nonce", "timestamp"]; [代码] 参数字典排序后应和参数一起拼接为字符串,至于使用什么拼接符就要与S端商量了,如果参数值是一个数组或一个对象(如c为[1,2,3])那么可以将数据值转为JSON字符串再拼接。以上参数拼接后字符串如下: [代码]a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720 [代码] 下一步是计算拼接字符串的MD5哈希值了嘛? 不是。因为这样拼接的字符串很容易被攻击者伪造签名并篡改数据,这样就失去了签名的意义了。也就是说缺了一个key值。key值又从何而来?上面思考部分有提到建议从登录后发放,并且这个key与该用户的登录态绑定,登录态有效期间,将使用这个key进行请求的签名与验签,至于如何鉴别用户,我们会在最终发送给S端的参数加入一个sessionId作为登录态唯一标识,这里不加是因为这部分数据是需要参与后续的加密的,而sessionId不参与加密。 但是可能又会有一个问题,登录前没有key怎么实现的登录请求?事实上,登录请求并不怕篡改,因为攻击者自己也不知道账号密码,所以无需提供key用于登录请求的签名。 提到登录密码这个需要注意一点,密码不能明文传输,请计算哈希值后传输,S端比对账户密码哈希值即可确认是否正确,同样S端非特殊情况也不能明文存储密码,建议SHA-1或更高级的SHA-256计算后的值,MD5值可能被使用彩虹表(一种为各种常见密码建立的MD5映射表)破解。SHA256计算的库已在上面工具库下载提供。 拼接上我们登录时随机生成的key: [代码]a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720&key=gUelv79KTcFaCkVB [代码] 接下来计算32位MD5哈希值 为什么上面说MD5会被破解而这里却用MD5计算?因为此处计算MD5的目的并不是为了隐藏明文数据,而只是用于数据校验 此处引入了CryptoJS.js [代码]const CryptoJS = require('./CryptoJS'); const signStr = "a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720&key=gUelv79KTcFaCkVB"; const sign = CryptoJS.MD5(signStr).toString(); //a42af0962de99e698d27030c5c9d3b0e [代码] 这么一来我们的数据签名阶段就完成了,然后需要把签名加入参数之中,将和参数一起传输,需要注意的是传输参数无需在意参数名排序。以下是当前的参数处理结果: [代码]{ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" } [代码] 其实从上面这两部分内容来看,会发现防重放和防篡改是相辅相成的,就像两兄弟一样,少了谁都干不好这件事。 S端验证签名 既然有签名那必定也有验签,验签流程其实就是重复C端的前面流程并比对CS两端得出的签名值是否一致。S端取得请求数据后(假设数据未加密,暂时不讨论解密),将除了sign之外的参数名进行字典排序,sign用一个临时变量存下,然后排好序的参数和C端一样拼接得到字符串。 接下来根据上面部分提到的未加密参数sessionId获得用户登录态并获取到该状态的临时key拼接到字符串末尾,接下来进行MD5计算即可得到S端方获得的签名,此时与请求中携带的sign比较是否一致则可确定签名是否有效,如果不一致返回签名错误。具体流程请参考以下: [代码]C端发送请求 -> S端接收请求 //请求参数为data const _sign = data.sign; //排序并拼接除sign外的参数 let signStr = a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720 const sessionId = ...; //用户的sessionId const sessionData = getUserSessionData(sessionId);//根据sessionId查询用户登录态sessionData并取出key,并拼接到字符串尾部 const key = sessionData.key signStr += key; //MD5计算并比对,代码仅供参考 const sign = crypto.md5(signStr); if(sign !== _sign) { //签名错误! } [代码] 请求数据加解密 有了上面部分的参数处理铺垫后,接下来就该开始本文章最核心的加解密,在这之前我们先了解AES和RSA两种加密算法。了解概念后我们再来思考一下两者的一些特性。 AES加解密需要密钥,除某些模式外还需要提供初始化向量,可使用密钥解出明文,是对称加密算法。 RSA加解密需要一对密钥,分别为公钥和私钥,公钥加密,私钥解密,是非对称加密算法。 两者在加密长文本性能上AES占优势。 根据这些让我们发现,他们可以形成互补关系,RSA加解密安全性高但长文本处理性能不及AES。AES加解密长文本性能优于RSA但需要明文密钥和向量加解密,密钥的安全性成问题。 那么在C端生成随机的AES密钥和向量,使用密钥和向量使用aes-128-cbc加密模式(也可以根据实际需要采用其它的模式)加密真正需要传输的参数(参数则是经过防重放+防篡改处理的参数)得到encryptedData,然后将该密钥使用RSA公钥加密得到encryptedKey,下面我顺便把AES加密的向量也一起加密了得到encryptedIV,这样就完美的互补了对方的缺点,既能够较快的完成数据加密又能保证密钥安全性,两全其美。 整个个加解密流程如下图所示: [图片] 下面的四个小章节将逐一描述流程实现: C端加密数据 C端加密后的数据应如下(并非固定格式,根据自己需要定制): [代码]{ "sessionId": "xxxxxxxx", "encryptedData": "xxxxxx", "encryptedKey": "xxxxxx", "encryptedIV": "xxxxxxx" } [代码] 但RSA公钥又是怎么发放到C端的呢?答案是在登录认证的时候服务器下发的,登录成功时服务器会创建RSA密钥对并把公钥发放给C端,私钥存在服务器上该用户的登录态数据中。流程如下所示: [代码]C端发起登录请求 -> S端接收登录请求 S端登录认证是否通过 true S端生成RSA密钥对 - publicKey , privateKey S端查询相关用户信息 S端生成登录态信息 -> 向登录态信息存入privateKey私钥,登录态信息类似如下 "xxxxxx": { id: "xxxxx", authData: { key: "xxxxxx", privateKey: "xxxxxx" } } S端返回登录态唯一标识sessionId和publicKey公钥以及相关用户信息 -> C端 C端存储登录态信息于本地,后续请求将使用服务器提供的公钥进行加密 [代码] 其中privateKey就是该用户当前登录态所使用的解密私钥,C端通过公钥加密后的AES密钥数据只能用该私钥解密。 如果希望登录阶段的请求也加密,那么可以手动生成一个RSA密钥对,然后客户端放置一个固定的公钥,服务器也使用一个固定的私钥进行登录阶段的加解密。 具体实现如下: 此处引入了CryptoJS.js和RSA.js [代码]const CryptoJS = require('./CryptoJS'); const RSA = require('./RSA.js'); //假设当前已登录成功并获得S端下发的RSA公钥且已存入本地存储 //createRandomStr为生成随机大小写英文和数字的字符串 const aesKey = createRandomStr(16); //生成AES128位密钥 16字节=128位 const aesIV = createRandomStr(16); //生成初始化向量IV const raw = JSON.stringfly({ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" }); const encryptedData = CryptoJS.AES.encrypt(data: raw, key: aesKey, { iv: aesIV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); //使用CBC模式和Pkcs7填充加密 const authData = wx.getStorageSync("authData"); //读取本地存的RSA公钥 RSA.setPublicKey(authData.publicKey); //设置RSA公钥 const encryptedKey = RSA.encrypt(aesKey); //RSA加密AES加密密钥 const encryptedIV = RSA.encrypt(aesIV); //RSA加密AES加密初始化向量,是否加密向量可由自己决定 //最后的处理结果 const result = { sessionId: authData.sessionId, encryptedData, encryptedKey, encryptedIV }; [代码] S端解密数据 [代码]const crypto = require('crypto'); const cryptojs = require('crypto-js'); const { sessionId, encryptedData, encryptedKey, encryptedIV } = requestData; const authData = getUserSessionData(sessionId); //获取用户登录态数据 const privateKey = authData.privateKey; const aesKey = crypto.privateDecrypt({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, encryptedKey); //使用私钥解密得到AES密钥 const aesIV = crypto.privateDecrypt({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, encryptedIV); //使用私钥解密得到AES iv向量 const key = cryptojs.enc.Base64.parse(aesKey); const iv = cryptojs.enc.Utf8.parse(aesIV); const decryptedData = cryptojs.AES.decrypt(encryptedData, key, { iv, mode: cryptojs.mode.CBC, padding: cryptojs.pad.Pkcs7 }); //采用与加密统一的模式和填充进行解密 const { "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" } = decryptedData; //至此解密得到C端传来的数据 [代码] S端加密数据 当S端处理完C端的请求后应加密响应数据,那么加密响应数据应该使用什么密钥呢?既然C端已经将加密的密钥发送过来了,那么干脆将C端使用的AES密钥拿来加密响应数据就可以了。加密的数据传回C端后,C端只需使用该请求所使用的AES加密密钥进行解密即可。响应的加密数据如下: [代码]const cryptojs = require('crypto-js'); const responseData = JSON.stringify(...); //此为S端需要返回给C端的数据 const aesKey = ...; //此为之前C端用来加密数据的AES密钥 const aesIV = createRandomStr(16); //生成初始化向量IV const encryptedData = cryptojs.AES.encrypt(data, aesKey, { iv: aesIV, mode: cryptojs.mode.CBC, padding: cryptojs.pad.Pkcs7 }); //加密响应数据 const encryptedResponse = { "encryptedData": encryptedData, "iv": aesIV }; //得到加密后的响应数据并返回给C端 [代码] C端解密数据 C端接收到S端的响应数据后应对加密的数据进行解密,此次解密就是单纯的AES解密了,使用发起请求时用于加密数据的AES密钥配合响应数据的iv向量对encryptedData进行解密,得到解密后的数据即为S端真正的响应数据。实现过程如下: 此处引入了CryptoJS.js [代码]const CryptoJS = require('./CryptoJS'); const key = ...; //之前用于加密请求参数的AES加密密钥 const { encryptedData, iv } = responseData; const aesIV = CryptoJS.enc.Utf8.parse(iv); const aesKey = CryptoJS.enc.Utf8.parse(key); const decryptedData = CryptoJS.AES.decrypt(encryptedData, aesKey, { iv: aesIV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); //AES解密响应数据 const { ... } = decryptedData; //得到解密后的响应数据 [代码] 结语 第一次写这么长的文章,可能存在大量纰漏,如果大佬发现问题欢迎指出(`・ω・´)我会马上修改。 也许小程序的运行环境会比我想象中的更安全 也许HTTPS也会比我想象中的更安全 也许Web服务器引擎也比我想象中的更安全 但,安全不总是先行一步的吗?
2019-10-24 - 【周刊-1】三年大厂面试官-面试题精选及答案
前言 在阿里和腾讯工作了6年,当了3年的前端面试官,把期间我和我的同事常问的面试题和答案汇总在我 Github 的 Weekly-FE-Interview 中。希望对大家有所帮助。 如果你在bat面试的时候遇到了什么不懂的问题,欢迎给我提issue,我会把题目汇总并将面试要点和答案写好放在周刊里,大家一起共同进步和成长,助力大家进入自己理想的企业。 项目地址是:https://github.com/airuikun/Weekly-FE-Interview 常见面试题精选 以下是十道大厂一面的时候常见的面试题,如果全部理解并且弄透,在一面或者电话面的时候基本上能中1~2题。小伙伴可以先不急着看答案,先自己尝试着思考一下和自己实现一下,然后再看答案。 第 1 题:http的状态码中,499是什么?如何出现499,如何排查跟解决 解析:第 1 题:http的状态码中,499是什么?如何出现499,如何排查跟解决 第 2 题:讲解一下HTTPS的工作原理 解析:第 2 题:讲解一下HTTPS的工作原理 第 3 题:讲解一下https对称加密和非对称加密。 解析:第 3 题:讲解一下https对称加密和非对称加密 第 4 题:如何遍历一个dom树 解析:第 4 题:如何遍历一个dom树 第 5 题:new操作符都做了什么 解析:第 5 题:new操作符都做了什么 第 6 题:手写代码,简单实现call 解析:第 6 题:手写代码,简单实现call 第 7 题:手写代码,简单实现apply 解析:第 7 题:手写代码,简单实现apply 第 8 题:手写代码,简单实现bind 解析:第 8 题:手写代码,简单实现bind 第 9 题: 简单实现项目代码按需加载,例如import { Button } from ‘antd’,打包的时候只打包button 解析:第 9 题: 简单实现项目代码按需加载,例如import { Button } from ‘antd’,打包的时候只打包button 第 10 题:简单手写实现promise 解析:第 10 题:简单手写实现promise 结语 本人还写了一些前端进阶知识的文章,如果觉得不错可以点个star。 blog项目地址是:https://github.com/airuikun/blog 我是小蝌蚪,腾讯高级前端工程师,跟着我一起每周攻克几个前端技术难点。希望在小伙伴前端进阶的路上有所帮助,助力大家进入自己理想的企业。
2019-04-08 - setData 学问多
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ vm, newData, }) => { newState = { ...newState, ...newData, }; clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, }); newState = null }, 0); }; [代码] 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) }) [代码] 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); [代码] 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList }); [代码] 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList }) [代码] 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) }); [代码] 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList }); [代码] 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() } module.exports = { reverseArr: reverseArr } </wxs> <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 问题解决! 参考资料 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
2019-04-11 - 好的经验要分享:chooseImage转base64
好的经验必须要分享:chooseImage后转base64 现在网上各种帖子的解决方案存在各种各样的问题,不说了,直接贴代码,手机亲测,没有问题 const fileManager = wx.getFileSystemManager(); [图片]
2018-08-31