- 程序员的万圣节-开源云开发小程序-丧尸头像
3万圣节马上就到啦,有没有想好今年的万圣节干点啥?幽默的程序员可不会这样普通的过节,接下来带你们看看,程序员写的万圣节小程序-丧尸头像。 名字听着有些可怕,但是功能很搞怪,适合万圣节主题,话不多说上图 [图片] 大家看出来了吧,左边是我,右边是生成的丧尸头像,好吓人。 下面给大家解析一下实现效果,首先我们要做的就是图片安全识别,不能上传违规的头像哦~ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { // console.log(event, "1111111111") if (event.type == 'imgSecCheck') { return imgSecCheck(event); } else if (event.type == 'msgSecCheck') { return msgSecCheck(event); } else { return ''; } } // 图片内容检测 async function imgSecCheck(event) { try { const res = await cloud.downloadFile({ fileID: event.value, }) const imgResult = await cloud.openapi.security.imgSecCheck({ media: { header: { 'Content-Type': 'application/octet-stream' }, contentType: 'image/png', value: res.fileContent } }) return imgResult; } catch (err) { return err; } } 然后我们再上传图片时调用图片安全检测 // 内容安全检测(图片)。判断图片是否合法,不含有色情,等内容。 function imgSecCheck(imgUrl) { wx.showLoading({ title: '检测图片中', }) return new Promise((resolve, reject)=>{ wx.cloud.callFunction({ name: 'contentCheck', data: { value: imgUrl, type:"imgSecCheck" }, success: res => { wx.hideLoading(); console.log(res, '检查结果') if (res.result.errCode == 0) { // 没问题 resolve(TIPS.SUCCESS); }else if (res.result.errCode == 87014) { wx.showToast({ title: '图片含有敏感违法内容!', icon: 'none' }); // 违法删除图片 wx.cloud.deleteFile({ fileList: [imgUrl] }).then(resu => { console.log(resu,'删除图片') }) }else{ TIPS.error(res) } }, fail: res => { console.log(res, '报错结果') wx.hideLoading(); TIPS.error(res) } }) }) } 上传图片变异接口调用 wx.uploadFile({ url: 'https://deepgrave-image-processor-no7pxf7mmq-uc.a.run.app/transform', filePath, name: 'image', header: { 'Content-Type': 'multipart/form-data' }, formData: { method: 'POST' //请求方式 }, success(res) { const data = res.data //do something wx.hideLoading() if (data == 'No face found') { return wx.showToast({ title: '未检测到人物图像', icon: 'none', duration: 2500 }) } _this.setData({ zombie: data }) }, fail: () => { wx.hideLoading() } }) 到此结果就出来了,功能很简单,玩儿法很特别。下面给大家上一个体验码: [图片] 开源链接:https://citizenfour.coding.net/public/zombie-head/zombie-head/git/files 作者:小码农
2020-11-02 - 至少含有数字/字母/字符2种组合正则示例
/** * @param {t} String * 长度为8-20个字符; =====> t.length >= 8 && t.length <= 20 * 不能使用空格、中文; =====> /^[^[\u4e00-\u9fa5\s]*]*$/.test(t) * 至少含有数字/字母/字符2种组合; ==> /[0-9]/g /[A-Za-z]/g /[-!\.~;_:@#\$%\^&\*\(\)\[\]\?+=]/g * 非法字符; =====> !/^[\w-!\.~;:@#\$%\^&\*\(\)\[\]\?+=]*$/.test(t) */ checkMyPwd: function (t) { // 解析说明模式 if (t.length >= 8 && t.length <= 20) { console.log("长度为8-20个字符") } if(!/^[^[\u4e00-\u9fa5\s]*]*$/.test(t)){ console.log("不能使用空格、中文") } if(/^[\w-!\.~;:@#\$%\^&\*\(\)\[\]\?+=]*$/.test(t)){ var num = !!t.match(/[0-9]/g), str = !!t.match(/[A-Za-z]/g), zi = !!t.match(/[-!\.~;_:@#\$%\^&\*\(\)\[\]\?+=]/g); var v = (num || str) && zi || num && str; console.log("至少含有数字/字母/字符2种组合 v=>",v) }else{ console.log("非法字符") } }
2020-10-14 - 视频无缝切换(ios黑一下问题也解决了),预加载图片资源等小程序二三点优化
缘由没啥别的理由,就是闲的。最近又开始写小程序了,不过心态不太一样了,因为其实大家都是js。你不能总是觉得小程序low,其实人家的思维逻辑都是互通的。 今天把一些性能优化的解决方案拿出来分享下。能比较有效的解决页面的一些卡顿和存在的一些问题 提示:博主用uniapp框架,但是原理都是一样的,一些uniapp的api,你把uni替换成wx就OK了 掘金原文地址:https://juejin.im/post/6881923388810461197/ 一、data数据的性能优化,基本操作这个其实和vue一个道理,所以大家不要把不参与渲染的数据定义在data里面了,不管是用了框架的还是原生小程序框架的。最好是大家把未定义渲染的数据放在created开始,进行约定规范,能大幅降低渲染成本。 再一个,如果你的数据不进行渲染操作,那么比如for循环赋值之类的操作,最好是先存储一下变量。最后再赋值回去。能减少无意间的性能开销,这个主要是针对vue,react框架这类框架。 举例: this.list.map() 改为 const list = this.list list.map() this.list = list 这样不只是性能优化,你的代码整洁度也能大幅上升 二、路由优化,能大幅降低性能问题首先微信小程序为了性能优化,对页面进行了10层最大缓存的处理,并且当10层缓存满了之后,navigateTo函数会直接报错,无法使用。除了我们一般经常需要注意页面层级问题外,其实还有别的方式来解决这个问题。当然一般的电商购物类的页面没有这么苛刻。但是这次公司的app是视频播放类app,本身视频播放就很吃性能,而且看似页面就那么几个,但是极度容易出现层级满了,然后页面跳转失效问题。 如果用reLaunch会释放资源,那么缓存失效,更卡 redirectTo呢依旧无法避免层级超过10层问题 所以就有了下面这个函数 /* { url: 'pages/index', type: 'navigate' //redirect分别对应navigateTo和redirectTo } */ const pageJump = ({ url, type = 'navigate' }) => { const pageStack = getCurrentPages() const pageLen = pageStack.length const home = pageLen + 1 //如果需要返回首页 // 获取页面所在栈位置 const page = u => { //获取指定页面所在数 for (let i = 0; i < pageStack.length; i++) { if (pageStack[i].route === u) { return i } } } const pa1 = page(pageStack[pageLen - 1].route) // 当前页数 const pa2 = page(url) // 需要跳转的页 //实际需要跳转的页数 const pageNum = pa1 - pa2 if (pageNum >= 0) { uni.navigateBack({ delta: pageNum }) return true } if (type === 'navigate') { uni.navigateTo({ url: '/' + url }) return true } if (type === 'redirect') { uni.redirectTo({ url: '/' + url }) return true } } 使用很简单,你直接传入对应的下一个页面的地址就行了,注意不要有前面的‘/’ 这样页面在跳转的时候就会优先找历史中缓存的页面,没有的情况下才会新开页面。有效避免了10层最大缓存问题,并且现在流畅性提高一个档次(因为会复用缓存)。 三、小程序模拟multipart/form-data数据提交和post大数据引发的问题这个不是性能问题,属于个人对于小程序框架的bug踩坑 1、首先小程序一般是不支持formdata方式的,我们改造之后变成可以做到 header部分 header: { 'content-type': 'multipart/form-data; boundary=XXX' }, data传参部分用这个函数处理一下 // 修改为formdata getformdata(obj = {}) { let result = '' for (let name of Object.keys(obj)) { let value = obj[name]; result += '\r\n--XXX' + '\r\nContent-Disposition: form-data; name=\"' + name + '\"' + '\r\n' + '\r\n' + value } return result + '\r\n--XXX--' } 这个当初要这样做是因为旷视的人脸识别接口需要base64数据并且需要是formdata方式进行上传 2、引发的小程序真机调试的bug 当post里面的data数据超过100kb会直接无法请求接口数据,导致接口函数直接没有任何执行效果。万分注意该问题 四、图片压缩问题除了小程序官方提供的图片压缩函数wx.compressImage,我们还可以用canvas进行图片压缩,同时我比较不推荐官方的wx.compressImage,因为这个api会导致相册新增图片。像我之前的人脸识别频繁拍照的,那真是灾难。多出来几千张照片 函数呢写好了, async compress({ w, h, canvaId, quality = 0.1, src = '' }) { const ctx = uni.createCanvasContext('compress') const [, img] = await uni.getImageInfo({ src }) const { path, width, height } = img ctx.drawImage(path, 0, 0, width, height, 0, 0, w, h) //描述图片到画布上 return new Promise((reslove, reject) => { ctx.draw(false, async () => { const [err, nimg] = await uni.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'compress', quality: quality }) if (err) { reject(err) return false } else { reslove(nimg.tempFilePath) } }) }) } 使用 const img = await this.compress({ w: windowWidth, // 希望的图片宽高 h: windowHeight, canvaId: 'compress', quality: 0.5, // 压缩质量 src: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1602222973914&di=80f148ded356938570ed00fe9f68b816&imgtype=0&src=http%3A%2F%2Fwww.deskcar.com%2Fdesktop%2Ffengjing%2F2007125102851%2F10.jpg' }) 注意html部分 <canvas class="compress" canvas-id="compress"></canvas> 这里我提供的函数没有对canvas的图片的宽高动态变化,这个大家注意下。在getImageInfo这个接口之后注意要给canvas赋值下图片本身的宽高。避免矿高比变形。 五、图片预加载这个其实就比较常用,特别是我们这边有几个比较大的前置图片,如果进入页面再加载那么背景图就无法里面加载好。 这里用 uni.getImageInfo({ src: ‘’ })就可以了,注意看下官方的api,是在success之后会得到一个本地的临时图片地址,我们拿这个地址去全局使用就行了 官方文档地址:https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.getImageInfo.html 六、ios和安卓多个视频交替切换,会黑一下的问题这个原因是因为视频播放之前需要进行解码,而解码是需要时间的,那么在这个之前,播放肯定是黑的。安卓还好,ios非常严重。产品大大说这个必须要改 1、我们这个是多个视频切换,那么我们增加背景视频或者背景图片避免黑屏尴尬 2、但是这样我们如果只是监听onplay是不行的,还是会比较明显的黑一下 并且这里引出一个bug,当多个视频之间相互切换的时候,上一个视频帧数据会影响下一个视频帧数据。如果是单纯的去监听api也是没有用的。 最后我的代码是这样的。做到了无缝切换的感觉 export default { data() { return { isplay: false, // 是否播放了 show: '', // 播放的视频地址 } }, created() { // 这里定义的变量不会参与渲染 this.check = false // 当前是否切换地址了 this.index = 0 this.src = [ ‘公司的视频,自己换’, ‘公司的视频,自己换’, ‘公司的视频,自己换’, ‘公司的视频,自己换’, ] }, methods: { next() { this.isplay = false this.show = '' if (this.index === this.src.length - 1) { this.index = 0 } // 模拟接口请求,500毫秒延迟 setTimeout(() => { this.show = this.src[this.index] //+ '?t=' + new Date().getTime() this.index = this.index + 1 this.check = true }, 500) }, onended() { this.isplay = false }, //监听视频播放时间更新 ontimeupdate(e) { const currentTime = e.detail.currentTime // 排除干扰因素 if (currentTime <= 0) return false // 只有当前未播放,切换视频地址情况下,并且时间开始为零点几才可以撤销遮罩视频 // 原理:多个视频切换播放的时候上一个视频播放帧会因为卡顿等问题延迟到下一个视频播放 if (!this.isplay && this.check && currentTime.toString().substring(0, 1) === '0') { this.isplay = true this.check = false } } } } 代码为uniapp的代码,但是我已经大幅简化了,拿去试一试吧 摸鱼结束吐槽下掘金的文章编辑器感觉越来越难用。代码块赋值黏贴没格式,然后放过来之后呢文章的格式全部不对。搞得我需要先放语雀里面重新编辑下在贴回来
2020-10-10 - #小程序云开发挑战赛#-小程序云开发挑战赛作品全收录-注定拿不到T恤的队伍
应用场景“小程序云开发挑战赛作品全收录”是小程序云开发大赛作品汇总,可按照参赛赛道查看作品即时浏览、点赞数,方便同学们使用哟! 目标用户官方的汇总页面太难用了,300多个链接看死人。 实现思路本小程序采用基于云开发的原生开发,用到了云数据库存储数据,使用云函数和小程序端进行数据交互。 A. 整体架构图如下: [图片] b. 爬虫 没啥好说的,golang爬虫获取社区参赛作品数据 c. 小程序端 大家都是大佬,就不介绍了吧 效果截图[图片] [图片] 小TIps: 1. 点击列头 ”编号“、”浏览“、”点赞“可以排序哟! 2. 部分手机可能顶部导航有些bug,请静待下个版本过审(下个版本可以跳转到社区文章页哟)。 作品体验二维码[图片] 团队简介本团队的核心成员来自注定拿不到T恤的队伍,在微信开放社区也积极活跃。 重要事情说三遍: 请素质三连! 请素质三连! 请素质三连! 不然小心你的作品在【小程序云开发作品全收录】里被硬编码!
2020-09-22 - 微信小程序通讯录组件及demo,支持汉字转拼音,排序类型,常规通讯录,全选、多选、单选通讯录。
提供支持通讯录基础组件,来源博主Homilier https://blog.csdn.net/Honiler/article/details/82929111 汉字拼音转换工具 https://github.com/hotoo/pinyin 微信原生代码片段https://developers.weixin.qq.com/s/A3maQemS7qkc 效果图[图片][图片][图片][图片][图片][图片][图片][图片] 使用index.json { "navigationBarTitleText": "教师通讯录", "enablePullDownRefresh": true, "usingComponents": { "alphabet-order-list": "/components/alphabet-order-list/alphabet-order-list" } }
2020-09-09 - 常见问题解答【个人汇总】
引言就目前微信用户在开发、学习和生活中所遇到的共性问题这一现象,整合官方和各渠道的共性解答而著 微信号使用问题登录异常1.你的微信登录环境存在异常,为了账号安全,本次登录已失效? 答:你好,提示“登录环境异常,本次登录失效,请重新登录”的问题,重新扫码登录即可,出现该提示一般是由于微信帐号被其它用户投诉导致(如:涉嫌频繁发送营销类广告骚扰信息等,请遵守微信使用规则,并规范个人行为,谢谢!),详情可查看社区朋友“citizen four”发布的一篇文章分析。 小商店相关问题资金结算2.小商店资金如何结算? 答:你好,如果是企业,会自动提现至对公账户,不支持提现到私人账户; 如果是个体工商户,可以选择对公或者对私(经营者个人银行卡)账户; 个人主体的直接结算到绑定的对私银行账户。 小商店目前不收取任何服务费或者技术服务费,仅对单笔交易收入扣除订单金额的千分之六作为支付费率。客户的每一笔货款都会直接到达商家自己的微信支付商户号账号中,到账的款项无特殊情况均为待结算状态,商家将在客户确认收货后,可对该笔款项进行提现至银行账户操作,如无特殊情况,款项一般会在24小时内到账。 个人&企业店铺的区别3.个人店铺、企业店铺的功能和收款差别? 答:你好,功能差异:目前企业店功能更加完善,支持直播、优惠券、数据中心等功能;个人店的功能也在同步完善中,敬请期待。 收款限额:个人店正常日收款上限额度为10万,若交易良好则自动提升至20-30万/日,若交易异常则自动下降至5万以下/日。信用卡单日收款上限不超过1千,信用卡单月收款不超过1万。 商铺曝光4.小商店如何进行曝光? 答:你好,小商店曝光渠道:任务栏、发现入口、客服消息、小程序码、搜索、对话分享、群分享、APP分享、模板消息、公众号Profile页/文章插入、LBS广告、微信广告; 商品曝光渠道:对话分享、群分享、APP分享、小程序互跳、公众号自定义菜单/文章插入、二维码、模板消息。 更多问题请阅读微信小商店常见问题汇总。 修改资料5.店铺名称、头像、简介、介绍可以修改吗? 小商店的名称和头像都可以修改。 个人店请至移动端店铺后台,点击“我-店铺信息” 企业店请至pc端后台,点击“店铺管理-基础信息” 修改次数限制如下: (1)店铺名称:企业/个体每年5次,个人每年3次 (2)店铺头像:企业/个体/个人每年5次 (3)店铺简称:企业/个体每年2次,个人每年3次 (4)店铺介绍:企业/个体/个人每月5次 装修功能6.小程序装修功能何时推出? 答:你好,目前小商店暂不支持自定义店铺首页功能;未来,小商店将开放店铺、商品、订单、物流、客服等一系列API接口供第三方介入,完善小商店能力,敬请期待。 小程序相关问题小程序盈利方式7.小程序如何才能盈利? 答:你好,小程序的盈利方式可参考这篇文章。 长期订阅模板8.如何申请长期订阅模板? 目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。可参考这个帖子中的政务民生类目申请长期订阅消息,请在正文内容处按照下面这种方式进行申请,等官方过来核实评估。长期订阅消息目前不支持线上申请新增模板,如模板库内无适用模板,请按照以下格式补全信息,后续官方会进行评估是否新增入库: 【小程序appid】 【小程序主体】 【申请模板类目】 【申请模板名称】 【使用场景】(详细描述下发消息场景、收到的用户类型,对行业判断很重要) 【模板字段】 【消息示例】(1~2个示例) 虚拟支付的界定9.IOS中小程序的哪些功能属于虚拟支付? 答:你好,若小程序售卖的优惠券涉及线下核销兑换且非虚拟产品,则不属于虚拟支付违规,平台对于违规小程序的处理是根据实际具体情况进行评估。如果对平台处理存在异议可发起线上申诉,会有专人处理。 小程序服务内容中涉及虚拟产品购买,ios平台规则不支持,任何为用户提供前往支付流程的路径/文案都将被拒绝,包含但不限于虚拟产品的购买指引流程,价格展示,已购产品展示,付费及购买的展示按钮、文案及图片,引导用户到微信公众号、H5、APP或是任何外链进行付费等,请整改后再重新提交审核。 ios小程序开发以下这6种情况是不属于虚拟支付,其他则判定为虚拟支付 ① 小程序在线课程直播。用户先买课程,后续在线上安排老师在小程序直播。补充选择教育-在线视频课程类目 ② 线上报名活动,线下培训的类型 ③ 充值加油卡加油,涉及预付卡销售服务,补充商家自营-预付卡销售类目 ④ 充值手机流量,补充IT科技-电信运营商类目 ⑤ 悬赏问答功能,需选择社交红包-社交红包类目,并完成新商户号申请后,再提交代码审核。 ⑥ 微信支付充值积分,签到积分等,积分兑换实物商品,兑换成功后,会直接给用户寄过去。有实际服务存在 此处附上官方文档中的虚拟支付行为和虚拟支付违规类型和整改示例。 人脸核身9.关于申请人脸核身识别的接口,小程序是否支持第三方人脸信息采集吗? 答:你好,小程序不支持采集生物特征,人脸核身只能使用微信的。 小程序涉及人脸识别功能,人脸识别功能涉及采集、存储用户生物特征,包括但不限于人脸照片、人脸视频、身份证加手持身份证和身份证照加拍免冠照等服务,此类型服务需使用微信原生人脸识别接口。因小程序目前不符合接入微信原生人脸识别接口的条件,所以需去掉小程序前端展示、小程序代码中的人脸采集功能后再重新提交审核。或建议切换符合主体和类目的来做人脸识别相关的功能服务。 具体可参考: 腾讯云微信小程序人脸接入 和这个 帖子; 微信人脸核身接口能力; 人脸核身功能服务的准入规范与案例解析。 域名拦截10.域名被微信拦截,怎么解决? 答:你好,在域名被拦截的提示页面,点击最下方申请恢复网址进行申述。 详细规则请参考《微信外部链接内容管理规范》。 公众号相关问题群发后显示异常11.微信公众号按照标签群发后一些用户收不到消息? 答:你好,如文章群发后用户收不到消息,请参考官方的解答。 视频审核问题12.公众号视频怎么一直在审核,视频原创失败该如何申述? 答:你好,详见官方公众号视频上传相关问题和群发指引。 公众号如何注销13.公众号注销方法? 答:你好,详见官方文档中的个人公众号注销方法和帐号注销问题汇总。 公众号永封申述 14.微信公众号被永久封禁,怎么办? 答:你好,可参照网友的方法进行申述,友情提醒:如果你被永封的情况和网友的情况出入较大,基本上会申述失败。 联系官方问题联系官方客服15.如何找到腾讯人工客服?(电话人工客服)? 答:你好,可参照网友提供的方法。
2020-10-15 - 用云函数这一利器改写了ai抠图
抠图效果 [图片] 引言 上次写了一篇用小程序实现ai抠图,就差一步可以能在小程序全盘使用第三方库去抠图,苦于不能将Buffer图片源转成base64赋给[代码][代码],上了node.js后端去实现,这两天突然想起可以用云函数去实现,果断用云函数代替自己写后端。 纯微信小程序端实现ai抠图代码如下: [代码]wx.chooseImage({ count: 1, // 默认9 sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有 sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 success: res => { var tempFilePaths = res.tempFilePaths const file = tempFilePaths[0] this.setData({ origin: file }) console.log(file) wx.uploadFile({ header: { 'X-Api-Key': 'your key' }, url: ' https://api.remove.bg/v1.0/removebg', filePath: file, name: 'image_file', success: res => { const data = res.data console.log(data) console.log('base64') const base64 = data.toString('base64') console.log(base64) let url = base64 this.setData({ url: url }) } }) } }) [代码] 只可惜上面的代码最终还是乱码的,不能赋值给image来渲染 于是后来,才动用node.js koa框架写了一个简单的后端实现。 使用云函数改写 koa就为了转发一下请求,这一跳板有点大材小用,于是乎想到用云函数,就免去买服务器,配后端环境,起koa项目。 建立云函数目录并配置 在项目根目录新建文件夹functions,并在project.config.json中增加如下设置 [代码]{ "cloudfunctionRoot": "./functions/" } [代码] 然后项目目录下会出现如下标识 [图片] 添加云函数 鼠标右击functions文件,创建云函数ps,安装依赖,[代码]npm install request-promise --save[代码] 具体代码如下 [代码]// 云函数入口文件 const rp = require('request-promise') // 云函数入口函数 exports.main = async (event, context) => { // const wxContext = cloud.getWXContext() const file = event.file const buffer = new Buffer.from(file, 'base64') const result = await rp.post({ url: 'https://api.remove.bg/v1.0/removebg', formData: { image_file: buffer, size: 'auto' }, headers: { 'X-Api-Key': 'wkMhcc4TRNFpxjL79Kf8mMU1' }, encoding: null }) const body = result const image = body.toString('base64') return { image } } [代码] 原理就是将文件的base64编码,再转换成buffer,再提交给remove.bg这个ai抠图api地址。于是现在只剩下一件事,就是将小程序端本来是用二进制文件上传的,要先将它改成用base64后,才能传递给云函数。 小程序端上传传base64编码 这里使用小程序的FileSystemManager.readFile方法将图片二进制文件,转成base64再提交给云函数。 相关文档: https://developers.weixin.qq.com/minigame/dev/api/file/FileSystemManager.readFile.html 于是完整的小程序前端代码如下 [代码]wx.getFileSystemManager().readFile({ filePath: file, //选择图片返回的相对路径 encoding: 'base64', //编码格式 success: res => { //成功的回调 wx.cloud.callFunction({ name: 'ps', data: { file: res.data }, success: (res) => { console.log(res) const data = res.result.image let url = 'data:image/png;base64,' + data this.setData({ url: url }) // //do something console.log(res) }, fail(err) { console.log(err) } }) } }) [代码] 先作base64编码,然后调用云函数,最后将云函数返回的base64图片资源渲染到[代码][代码],整个流程走完。 源码 https://gitee.com/laeser/demo-weapp 代码位于[代码]pages/ps-cloud[代码]文件夹下 关注我 [图片]
2020-08-10 - 论函数复用的几大姿势
开发过小程序的朋友们应该都遇到这样的情况,可能很多个页面有相同的函数,例如[代码]onShareAppMessage[代码],有什么最佳实践吗,应该如何处理呢? 本次开发技巧,我从以下几种解决办法剖析: 将它复制粘贴到每个地方(最烂的做法) 抽象成一个公共函数,每个[代码]Page[代码]都手动引用 提取一个behavior,每个页面手动注入 通过[代码]Page[代码]封装一个新的[代码]newPage[代码],以后每个页面都通过[代码]newPage[代码]注册 劫持Page函数,注入预设方法,页面仍可使用[代码]Page[代码]注册 复制粘贴大法 这是最直观,也是初学者最常用到的办法。也是作为工程师最不应该采取的办法。这有一个致命的问题,如果某一天,需要改动这个函数,岂不是要将所有的地方都翻出来改,所以这个办法直接否决。 抽象公共函数 这种方式,解决了复制粘贴大法的致命问题,不需要改动很多地方,只需要改动这个抽象出来的函数即可。但是其实,这个方式不便捷,每次新增页面都需要手动引入这个函数。 以下都通过[代码]onShareAppMessage[代码]方法举例。 假设在[代码]app.js[代码]通过[代码]global[代码]注册了[代码]onShareAppMessage[代码]方法: [代码]// app.js global.onShareAppMessage = function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } } [代码] 那么此时每次新增的Page都需要这样引入: [代码]// page.js Page({ ...global.onShareAppMessage, data: {} }) [代码] 这样的缺点也是非常明显的: 创建新页面时,容易遗忘 如果多个相同的函数,则需要每个独立引入,不方便 提取Behavior 将多个函数集成到一个对象中,每个页面只需要引入这个对象即可注入多个相同的函数。这种方式可以解决 抽象公共函数 提到的 缺点2。 大致的实现方式如下: 同样在[代码]app.js[代码]通过[代码]global[代码]注册一个[代码]behavior[代码]对象: [代码]// app.js global.commonPage = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onHide: function() { // do something } } [代码] 在新增的页面注入: [代码]// page.js Page({ data: {}, ...global.commonPage, }}) [代码] 缺点仍然是,新增页面时容易遗忘 封装新Page 封装新的[代码]Page[代码],然后每个页面都通过这个新的[代码]Page[代码]注册,而不是采用原有的[代码]Page[代码]。 同理,在[代码]app.js[代码]先封装一个新的[代码]Page[代码]到全局变量[代码]global[代码]: [代码]// app.js global.newPage = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return Page({...defaultSet, ...obj}) } [代码] 往后在每个页面都使用新的[代码]newPage[代码]注册: [代码]// page.js global.newPage({ data: {} }) [代码] 好处即是全新封装了[代码]Page[代码],后续只需关注是否使用了新的[代码]Page[代码]即可;此外大家也很清晰知道这个是采用了新的封装,避免了覆盖原有的[代码]Page[代码]方法。 我倒是觉得没什么明显缺点,要是非要鸡蛋里挑骨头的话,就是要显式调用新的函数注册页面。 劫持Page 劫持函数其实是挺危险的做法,因为开发人员可能会在定位问题时,忽略了这个被劫持的地方。 劫持[代码]Page[代码]的做法,简单的说就是,覆盖[代码]Page[代码]这个函数,重新实现[代码]Page[代码],但这个新的[代码]Page[代码]内部仍会调用原有的[代码]Page[代码]。说起来可能有点拗口,通过代码看就一目了然: [代码]// app.js let originalPage = Page Page = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return originalPage({ ...defaultSet, ...obj}) } [代码] 通过这种方式,不改变页面的注册方式,但可能会让不了解底层封装的开发者感到困惑:明明没注册的方法,怎么就自动注入了呢? 这种方式的缺点已经说了,优点也很明显,不改变任何原有的页面注册方式。 其实这个是一个挺好的思路,在一些特定的场景下,会有事半功倍的效果。
2020-03-23 - uni-app开发小程序总结
最近开发项目中使用 uni-app 开发了 微信小程序,整个体验下来还算流畅,下面做一些总结: HBuilderX安装 HBuilderX安装的时候选择标准版,不要下载APP开发版,至于uni-app编辑都可以在标准版里面通过插件安装或者是直接通过vue-cli命令行创建项目,另外就我个人使用之后,APP开发版编译小程序的时候,有时候会导致编译出来的小程序页面空白(只剩下[代码][代码])。 微信开发者工具 HBuilderX运行/发布微信小程序在编译完成之后会尝试打开微信开发者工具,实际上就是用命令行控制微信开发者工具(命令行 V2),这个需要在开发者工具的设置 -> 安全设置中开启服务端口。 创建项目 官方提供了HBuilderX可视化界面和vue-cli命令行两种方式创建项目,不过cli版如果想安装less、scss、ts等编译器,需自己手动npm安装。在HBuilderX的插件管理界面安装无效,那个只作用于HBuilderX创建的项目。 代码目录 以HBuilderX可视化界面创建的项目: components:组件文件,不用引入声明,直接使用。 pages:页面 index:首页 static:静态资源 store:全局状态管理中心 unpackage:打包文件 .gitignore:忽略文件,暂时忽略 [代码]unpackage[代码] 文件 APP.vue:应用配置,用来配置App全局样式以及监听 main.js:Vue初始化入口文件 manifest.json:配置应用名称、appid、logo、版本等打包信息 pages.json:配置页面路由、导航条、选项卡等页面类信息 uni.scss:整体控制应用的风格,https://uniapp.dcloud.io/collocation/uni-scss mixins:混入 utils:工具,uni.request 和 uni.uploadFile 方法封装 config:配置文件 index.js:类似于vue.config.js theme.js:配置在js中使用的主题配置 sitemap.json:配置页面是否被索引 package.json:npm相关,直接安装对应的库,和web一样引入使用 package-lock.json:npm相关 完整代码可查看https://github.com/fxss5201/uni-template 代码封装 [代码]config -> index.js 配置信息[代码] [代码]export default { loginExpiredCode: '', // 用户信息过期的code token: 'token', // 如果使用到用户信息,需要存储token时,设置此token值,表示token的key origin: process.env.NODE_ENV === 'development' ? '' : '', // 配置请求的域名 origin1: process.env.NODE_ENV === 'development' ? '' : '' // 用于设置多个域名 } [代码] [代码]utils -> request.js uni.request 和 uni.uploadFile 方法封装[代码] [代码]// uni.request 和 uni.uploadFile 方法封装 // 使用方式: // this.$request({ // url: '', // origin: 1, // 可选参数,用于设置使用项目中 config/index.js 中配置的哪个origin,值为对应的数字,未设置则使用默认的 origin // method: 'POST', // data: {} // }).then(res => { // console.log(res) // }).catch(err => { // console.log(err) // }).finally(_ => {}) import store from '../store' import config from '../config' export function request(options) { let origin if (typeof options.origin === 'number' && !isNaN(options.origin)) { origin = config[`origin${options.origin}`] } else { origin = config.origin } return new Promise((resolve, reject) => { uni.request({ url: `${origin}${options.url}`, // 统一配置域名信息 method: options.method, header: options.header || { 'content-type': 'application/json', 'token': store.state.token }, data: options.data || {}, success(res) { /** * https://developers.weixin.qq.com/miniprogram/dev/api/ui/interaction/wx.showToast.html * uni.showLoading 和 uni.showToast 同时只能显示一个 * 我们一般会在发起请求的时候 uni.showLoading ,此处需要先 uni.hideLoading() ,才方便后面的提示信息 */ uni.hideLoading() if (res.statusCode === 200) { // code 看公司及个人定义 if (res.data.code === 0) { resolve(res.data) } else { // 返回的信息需要报错错误的msg,进行uni.showToast uni.showToast({ title: res.data.msg, icon: 'none' }) // 此处再根据code统一做一些相关处理,比如登录过期等操作 if(res.data.code === config.loginExpiredCode) { // 删除本地Storage的token uni.removeStorageSync(config.token) // uni.showToast 默认显示 1500ms ,之后跳转到登录页面 setTimeout(() => { uni.reLaunch({ url: 'pages/login/index' }) }, 1500) } reject(res.data) } } else { // statusCode 不为 200 的时候先报网络出错加 statusCode uni.showToast({ title: `网络出错: ${res.statusCode}`, icon: 'none' }) reject(res.data) console.log(`网络出错:${res.data.path} -> ${res.data.status}`) } }, fail(err) { uni.hideLoading() uni.showToast({ title: '网络出错', icon: 'none' }) reject(err) } }) }) } export function upload(options) { let origin if (typeof options.origin === 'number' && !isNaN(options.origin)) { origin = config[`origin${options.origin}`] } else { origin = config.origin } return new Promise((resolve, reject) => { uni.uploadFile({ url: `${origin}${options.url}`, filePath: options.filePath, name: options.name, formData: options.formData || {}, header: { 'content-type': 'multipart/form-data', 'token': store.state.token }, success(result) { uni.hideLoading() if (result.statusCode === 200) { /** * https://uniapp.dcloud.io/api/request/network-file * data String 开发者服务器返回的数据 */ const res = JSON.parse(result.data) if (res.code === 0) { resolve(res) } else { uni.showToast({ title: res.msg, icon: 'none' }) if(res.code === config.loginExpiredCode) { uni.removeStorageSync(config.token) setTimeout(() => { uni.reLaunch({ url: 'pages/login/index' }) }, 1500) } reject(res) } } else { uni.showToast({ title: `网络出错: ${result.statusCode}`, icon: 'none' }) reject(res.data) console.log(`网络出错:${res.data.path} -> ${res.data.status}`) } }, fail(err) { uni.hideLoading() uni.showToast({ title: '网络出错', icon: 'none' }) reject(err) } }) }) } [代码] 网络监控 我是在全局放了网络监控,当断网的时间弹出断网弹窗禁止操作,再次联网的时间,关闭弹窗。 [代码]App.vue[代码] : [代码]export default { onLaunch: function() { console.log('App Launch'); uni.onNetworkStatusChange(({isConnected}) => { if (isConnected) { uni.hideToast() } else { uni.hideToast() uni.showToast({ title: '您已断网', icon: 'none', mask: true, duration: 6000000 }) } }) }, onShow: function() { console.log('App Show'); }, onHide: function() { console.log('App Hide'); } }; [代码] 更新提示 [代码] onLaunch: function() { // 更新版本提示 if (uni.canIUse('getUpdateManager')) { const updateManager = uni.getUpdateManager() updateManager.onCheckForUpdate(function (res) { if (res.hasUpdate) { updateManager.onUpdateReady(function () { uni.showModal({ title: '更新提示', content: '新版本已经准备好,请重启应用', showCancel: false, confirmColor: theme.showModalConfirmColor, success: function (res) { if (res.confirm) { updateManager.applyUpdate() } } }) }) updateManager.onUpdateFailed(function () { uni.showModal({ title: '已经有新版本了哟~', content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~', showCancel: false, confirmColor: theme.showModalConfirmColor }) }) } }) } else { uni.showModal({ title: '提示', content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试', showCancel: false, confirmColor: theme.showModalConfirmColor }) } } [代码] 至于测试可以在 添加编译模式 设置: [图片] 遇到的一些问题及解决方法 微信小程序用户授权弹窗,获取用户信息。用户拒绝授权时,引导用户去重新授权 关于小程序获取手机号解密失败的踩坑历程
2020-08-29 - 关于微信小商店开通遇到的一些问题和一些个人建议
很荣幸被内测,整体流程都跑了一遍。先给大家发一遍流程以及会遇到的一些问题。结尾有体验码(非广告,给没见过的同学看看)当你收到这个邀请说明,你被内测到了,可以注册自己的小商店了。此通知在【微信公众平台】,申请了,没有被内测到的同学,赶紧去看一下。没申请的同学赶紧点击链接去申请:https://wj.qq.com/s2/6720859/9c96/ [图片] 点击详情,直接跳转【小商店助手】小程序,点击【免费开店】,类型选择【企业/个体户】(个人类型还在内部测试,我们暂时还没有权限开通),然后登陆https://mp.weixin.qq.com(被内测的微信通知,去扫码,选择小商店登陆) 下面是首次登陆会出现的问题汇总(持续更新): 1、首次登陆店铺基础信息里只有账号信息(appid和原始id),找不到基础信息在哪里,你需要马上随便添加一个商品,选择发布,会告诉你完成店铺认证。 [图片] 2、填写铺信息比较简单,唯一感觉不熟悉的地方就是银行卡的省市区编号对照表,点击链接会下载一个表,自己找对应编号就可以了。https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/applyments/chapter4_1.shtml [图片] 3、店铺信息(步骤1-3)基本审核是在1个工作日内(我的是20个小时左右),小商店名字申请通过后,一年可以改5次,不收费。名字可以和认证的公众号名字一样,如果名字和已有小程序名字重复,则需要换个名字。 [图片] 4、审核完即可发布商品,商品一级类目主要有:个人护理、厨具、家具、家居日用、家用电器、家纺、家装建材、工业品、手机通讯、数码、服饰内衣、母婴、汽车用品、玩具乐器、电脑、办公、礼品、运动户外、宠物生活、箱包皮具、家庭清洁/纸品、鞋靴、农资园艺、钟表、美妆护肤。 5、每发布一个商品都需要微信官方审核一次,审核时间大约在2-3小时左右(我的是2个半小时左右)。店铺的分类管理点击发布立即生效,无需审核。 6、微信小商店结算账户是结算到微信支付商户平台,如果没有微信支付商户平台,默认会给你创建一个(如果有,还是会给你创建一个),费率大部分应该是6‰,这个商户支付比较简洁,没有营销功能,比如企付零、企付卡,只能提到公户。 7、微信小商店自带直播、好物圈、即时配送。可以手机直播,也可以OBS推流,有一个相当好的好处是,直播间开启后本场直播将有可能被官方推荐。个人感觉目前微信小店是内测期间,只要你敢直播,微信就敢给你推荐,哈哈(因为是内测,收到的内测资格人数不多,刨去审核中的,没时间创建的,还有一群憨憨,只要你是用心直播,肯定会被官方推荐,我自己想的,具体以官方为准) 8、微信小商店可以添加运营人员,微信搜索【小程序助手】,找到你的小商店,点成员管理,去添加。(暂时不要去添加账号密码,据说有人添加后变成普通小程序了,待官方确认) [图片] 9、微信小商店首页路径:pages/index/index;商品页路径:__plugin__/wx34345ae5855f892d/pages/productDetail/productDetail.html?productId=SpuId,SpuId去商品管理查看;优惠券页面:__plugin__/wx34345ae5855f892d/pages/userCoupon/userCoupon,个人中心页面暂时无法获取到,需要认证。 [图片] 10、如果微信小商店中没有你想出售的商品类目,准备好对应资质去提交审核。 [图片] 以上就是微信小店的全部流程了,下面我说说我个人建议。 1、小程序首页不支持自定义分享图片,这个可以考虑一下,因为某音就是后台设置的 2、首页、商品详情页分享朋友圈,官方应该考虑支持一下。 3、分类页面个人觉得应该出几套模板让商家选择,现在大部分用户习惯的分类是,左边栏右商品。 4、营销中心目前是灰色的,建议加一些拼团、砍价、分销、优惠券等这些常用功能。 5、既然电商梦还在,我觉得应该在微信搜索推送更多的流量进来,比如商品标题等。 6、上架商品,目前【颜色】是必选项,建议改成非必选项,比如我上架了一个定制手机壳,产品属性只有玻璃和磨砂,和颜色不搭边。 7、SUK价格参数得一个一个填写,我就上架了一个商品,花了将近20分钟,希望可以改进一下,支持批量修改、总感觉小商店稍微有些简单,却点啥。比如定义商城首页banner图关联商品、商品标签(新品、火爆等),可能是因为没有营销功能的原因吧。 以上就是我个人觉得一些不成熟的建议~下面我发个体验二维码,大家可以参考。如果要买,记得先联系客服(非广告,给没见过的同学看看) [图片]
2020-08-01 - 实战丨如何制作一个完整的外卖小程序(已开源)
最近微信小店开放了,赶着微信全面开放之前,把自己的小程序开源出来给大家使用~ 小程序效果 [图片] [图片] [图片] 开发心得 如何在项目中集成云开发 一开始项目并非基于云开发而开发的,目前考虑用云开发,因此,需要在项目中开启云开发的相关选项。 首先,在小程序文件夹中建立 [代码]cloud[代码] 文件夹,并在package文件中配置,建立用户登录的云函数并上传到微信小程序云中。相关的操作可以参考官方文档。 我在项目目录中添加了 [代码]cloud[代码] 和 [代码]miniprogram[代码] 两个目录,并在 [代码]project.config.json[代码] 文件夹进行配置 [代码]{ "miniprogramRoot": "./miniprogram" "cloudfunctionRoot": "./cloud/" } [代码] 开通云开发 配置完成后,可以点击控制台中的「云开发」来开通云开发。 [图片] 在云开发的界面中配置,并开通云开发。 [图片] 开通数据库集合 云开发不会自动创建数据库集合,因此,你需要手动创建集合。分别创建 店铺表Seller、分类表Category、商品表Food、订单表Order、地址表Address、用户表*_User*。 [图片] 数据操作 有了数据库的表后,就可以在代码中对数据进行操作了。 下方是我进行目录操作的代码。 [代码]const db = wx.cloud.database() const { showModal } = require('../../utils/utils') Page({ onLoad: function(options) { // 管理员认证 getApp().auth() if (options.objectId) { // 缓存数据 this.setData({ isEdit: true, objectId: options.objectId }) // 请求待编辑的分类对象 db.collection('Category') .doc(options.objectId) .get() .then(res => { // 获取分类信息 this.setData({ category: res.data }) }) } }, add: function(e) { var form = e.detail.value if (form.title == '') { wx.showModal({ title: '请填写分类名称', showCancel: false }) return } form.priority = Number.parseInt(form.priority) // 添加或者修改分类 // 修改模式 if (this.data.isEdit) { const category = this.data.category db.collection('Category') .doc(category._id) .update({ data: form }) .then(res => { console.log(res) showModal() }) } else { db.collection('Category') .add({ data: form }) .then(res => { console.log(res) showModal() }) } }, showModal() { // 操作成功提示并返回上一页 wx.showModal({ title: this.data.isEdit ? '修改成功' : '添加成功', showCancel: false, success: () => { wx.navigateBack() } }) }, delete: function() { // 确认删除对话框 wx.showModal({ title: '确认删除', success: res => { if (res.confirm) { const category = this.data.category db.collection('Category') .doc(category._id) .remove() .then(res => { console.log(res) wx.showToast({ title: '删除成功' }) wx.navigateBack() }) } } }) } }) [代码] 联表查询 在使用数据库时,难免要进行联表查询,云开发支持在云函数侧进行联表查询,你可以参考我的代码,来实现联表查询的功能。 [代码]const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() // 云函数入口函数 exports.main = async (event, context) => { const result = await db.collection('Food') .aggregate() .lookup({ from: 'Category', localField: 'category', foreignField: '_id', as: 'categories' }) .end() // .orderBy('priority', 'asc') // .get() console.log(result) return result.list } [代码] 文件上传 在小程序的操作中,难免会遇到需要进行图片上传的场景。在进行图片上传时,云开发提供了方便的云存储供我们查询数据。 在获取到文件的本地路径后,调用 [代码]wx.cloud.uploadFile[代码] 即可上传文件。 [代码]chooseImage() { wx.chooseImage({ count: 1, // 默认9 sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有 sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 success: res => { const tempFilePaths = res.tempFilePaths const file = tempFilePaths[0] const name = utils.random_filename(file) //上传的图片的别名,建议可以用日期命名 console.log(name) wx.cloud.uploadFile({ cloudPath: name, filePath: file, // 文件路径 }).then(res => { console.log(res) const fileId = res.fileID // 将文件id保存到数据库表中 db.collection('Seller').doc(this.data.seller._id) .update({ data: { logo_url: fileId } }).then(() => { wx.showToast({ title: '上传成功' }) // 渲染本地头像 this.setData({ new_logo: fileId }) }, err => { console.log(err) wx.showToast({ title: '上传失败' }) }) }) } }) } [代码] 微信支付逻辑的实现 作为一个商城,难免会有微信支付相关逻辑的实现。在这种情况下,可以借助云开发提供的微信支付云调用功能实现快速的 API 调用和接口的实现。 绑定商户 在使用云开发提供的微信支付时,需要先执行微信支付的绑定,在云开发控制台添加相应的商户号 [图片] 添加后微信会发来通知 [图片] 根据提示,开通账号即可。 [图片] 如果不绑定,将报“受理关系不存在”的错误 [图片] 函数代码调用 配置完成后,只需要在云函数中调用微信支付的接口,就可以实现相关调用的能力 [代码]const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { console.log('请求中') console.log(cloud.getWXContext().ENV) let { orderId, amount, body } = event const wxContext = cloud.getWXContext() const res = await cloud.cloudPay.unifiedOrder({ body: body, outTradeNo: orderId, spbillCreateIp: '127.0.0.1', subMchId: '1447716902', totalFee: amount, envId: 'dinner-cloud', functionName: 'pay_cb' }) return res.payment } [代码] 这里 [代码]functionName: 'pay_cb'[代码]指的就是支付成功后,微信支付那侧给我的回调信息,后面我们就用它来更新我们的订单状态 小程序端代码调用 调用云函数后,会获得微信支付所需要的各种参数, [图片] 这个时候,就可以在小程序端调用微信支付接口,进行支付,相关代码可以参考 [代码]const { result: payData } = res wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, signType: 'MD5', paySign: payData.paySign, success: res => { console.log('支付成功', res) wx.showModal({ title: '支付成功', showCancel: false, success: () => { // 跳转订单详情页 wx.navigateTo({ url: '/order/detail/detail?objectId=' + order._id }) } }) }, ... [代码] 微信支付回调处理 微信统一下单里一个pay_cb回调函数,它是一个云函数,后续微信支付的支付信息将会发送在这个函数中,相应的,我们需要编写处理的方法 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ // API 调用都保持和云函数当前所在环境一致 env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() // 云函数入口函数 exports.main = async (event, context) => { console.log('支付回调') console.log(event) console.log(cloud.getWXContext().ENV) const orderId = event.outTradeNo const resultCode = event.resultCode if (resultCode === 'SUCCESS') { const res = await db .collection('Order') .doc(orderId) .update({ data: { status: 1 } }) console.log(res) return { errcode: 0 } } } [代码] 总结 云开发体验下来,优点自不必多说,微信登录与支付原生支持,调用与调试都很方便,特别是不用启本地服务开发,真的好用; 这个小程序的源码我已经开源了,你可以访问社区官网 获取源码,自行使用~ 作者:黄秀杰,16年开始从事小程序开发与技术布道,同名个人公众号「黄秀杰」。 云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 产品文档:https://cloud.tencent.com/product/tcb 技术文档:https://cloudbase.net 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2020-07-29 - 常见的动画效果预览,点击按钮即可预览动画效果
常见的动画效果预览,点击按钮即可预览动画效果 demo 代码片段:https://developers.weixin.qq.com/s/hKwi0imL706Q 效果图如下 [图片] 欢迎交流
2019-02-19 - H5跳转小程序以及小程序分享朋友圈官方文档
本文背景近期微信官方新推出两项能力灰度 本文内容主要给用户展示新能力的官方对接文档 [图片] f [图片] f [图片] f H5跳转官方文档 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html 分享朋友圈官方文档 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html 本文总结由于目前我还没有具体接入这两个功能,暂时不能提供实际接入代码片段,这个后续补充。
2020-07-10 - 干货,小程序清除冗余css样式,清除多余css样式PHP版
写在前面的一些废话 最近微信小程序项目快要上线了,在开发工具的Audits里看了下,说是建议清除多余css样式,搜了下看没有相关的工具,无奈只能自己动手丰衣足食了。 项目快一个月了,上个月写css的妹子离职了,挺想她的,她喜欢粉色,并且我和她经常一起喝奶茶,真的很想她!不多说了先上代码。 PS:此处省略一千万字...... /* * 小程序清除冗余css样式 * by 尧曳网络 2020-06-29 * 步骤: * 1、创建一个php文件,复制以下代码进去保存,更改变量$wxmlFolder为你的小程序pages目录路径,$publicCssPath为公共css路径 * 2、确认wxml文件中的class、id属性=号两边是否有多余空格或是否单或双引号,视情况更改$pattern正则匹配变量值(可在工具搜索看看有哪些class或id不同的形式) * 3、建议在替换前确认所有已获得的css和id名称是否正确 * 4、提取结果只匹配“#”、“.”和":"伪类的样式,公共和引入的样式需要手动提取,最后将提取出的css保存为另外一个文件进行测试即可 */ $wxmlFolder = '******/pages/'; $publicCssPath = '******/app.wxss'; //提取样式 function extractCss($content, $pattern) { $css=""; preg_match_all($pattern, $content, $matches); foreach ($matches[1] as $item) { preg_match_all("/'(.+?)'/is", $item, $Vmatches); foreach ($Vmatches[1] as $Vitem) { $css.=$Vitem." "; } preg_match_all("/\"(.+?)\"/is", $item, $Vmatches); foreach ($Vmatches[1] as $Vitem) { $css.=$Vitem." "; } $item = preg_replace('/{{(.+?)}}/is', '', $item); $css.=$item." "; } return $css; } /*获得所有样式文件*/ function getAllWxmlFile($folder) { $files = scandir($folder); foreach ($files as $file) { if ($file != '.' && $file != '..') { if (is_dir($folder . '/' . $file)) { getAllWxmlFile($folder . '/' . $file); } else { $GLOBALS["wxmlList"][] = $folder . '/' .$file; } } } } /*获得所有样式*/ function getAllCss($folder) { global $wxmlList; global $wxml; global $css; getAllWxmlFile($folder); foreach ($wxmlList as $file) { if (strpos($file, '.wxml') !== false) { $wxml[]=$file; } } //取消注释查看文件列表 //echo json_encode($wxml); //die(); foreach ($wxml as $path) { $fs = fopen($path, "r"); $content = fread($fs, filesize($path)); fclose($fs); //class="" 双引号 $css[] = extractCss($content, '/class="(.+?)"/is'); //class='' 单引号 $css[] = extractCss($content, '/class=\'(.+?)\'/is'); //id="" 双引号 $css[] = extractCss($content, '/id="(.+?)"/is'); //id='' 单引号 $css[] = extractCss($content, '/id=\'(.+?)\'/is'); } $cssRes=""; foreach ($css as $item) { $cssRes.=$item." "; } $allCss=array(); $cssRes=explode(" ", $cssRes); foreach ($cssRes as $v) { $v = preg_replace('/\'/is', '', $v); $v = preg_replace('/{{(.+?)\?/is', '', $v); $v = preg_replace('/}}/is', '', $v); $v = preg_replace('/:/is', '', $v); if (!empty($v)) { if (!in_array($v, $allCss)) { $allCss[]=$v; } } } //取消注释查看css列表 //echo json_encode($allCss); //die(); return $allCss; } /*获得所有样式*/ function showAllCss($res) { $fs = fopen($GLOBALS["publicCssPath"], "r"); $publicCss = fread($fs, filesize($GLOBALS["publicCssPath"])); fclose($fs); //替换注释 $publicCss = preg_replace('/\/\*(.+?)\*\//is', '', $publicCss); //查找css并提取出来 $allCss = array(); foreach ($res as $item) { //":" $patternCss="/(\. *".$item.":(.+?) *{(.+?)})/is"; preg_match_all($patternCss, $publicCss, $matches); $_id=implode($matches[1]); if (!empty($_id)) { $allCss[]=$_id; } //"." $patternCss="/(\. *".$item." *{(.+?)})/is"; preg_match_all($patternCss, $publicCss, $matches); $_css=implode($matches[1]); if (!empty($_css)) { $allCss[]=$_css; } //"#" $patternCss="/(\# *".$item." *{(.+?)})/is"; preg_match_all($patternCss, $publicCss, $matches); $_id=implode($matches[1]); if (!empty($_id)) { $allCss[]=$_id; } } $allCssContentRes=array(); foreach ($allCss as $v) { $allCssContentRes[]=$v; } foreach ($allCssContentRes as $k=>$v) { $allCssContentRes[$k] = preg_replace('/\r\n/is', '', $v); $allCssContentRes[$k] = preg_replace('/\t/is', '', $allCssContentRes[$k]); } return $allCssContentRes; } $res = getAllCss($wxmlFolder); $allCss = showAllCss($res); echo implode($allCss); 至此,运行后看上去似乎已经输出了一些样式,将结果另存为文件,然后把原来公共样式文件里的一些公共样式需要手动加入一下就可以了。 说实话,1999年编程至今孤身一人,当年看着马云、同程、携程、京东等崛起,甚至错过了炒玉米、小游戏等,落魄潦倒不堪至今,也就只能写点代码慰藉心灵了。 真想她,她写的css是那么的优雅,我认为她就是编程界的一股清流,可现在她从北京去了杭州,不知道还能不能和她联系上,也许……。 未完待续-----
2020-06-30 - 小程序全局事件监听
代码片段 https://developers.weixin.qq.com/s/g0tZKYmj7OhD //全局存储 var event = {}; //接收消息 传入params={name:"string 监听事件名",success:"function 监听事件回调",tg:"当前页面或App.js 传入this即可"} const $on = function(params) { if (!params) { return false; } if (!params.name) { console.error("事件监听未设置名称 属性key=name"); return false; } if (!params.success) { console.error("事件监听未设置回调函数 属性key=success"); return false; } if (!params.tg) { console.error("事件监听未设置目标对象 属性key=tg"); return false; } if (event[params.name]) { var list = event[params.name]; list.push([params.tg, params.success]); } else { event[params.name] = [ [params.tg, params.success] ]; } pageStatus(params.tg); } //消息发送 传入{name:"string 发送消息事件名",data:"可选 可以是任意数据类型"} const $emit = function(params) { if (!params) { return false; } if (!params.name) { console.error("事件监听未设置名称 属性key=name"); return false; } if (event[params.name]) { var list = event[params.name]; list.forEach(item => { item[1].call(item[0], params.data); }) } } //移除监听 传入params={name:"string 监听事件名",tg:"当前页面或App.js 传入this即可"} //如未移除监听 页面onUnload时将会自动移除 const $remove = function(params) { if (!params) { return false; } if (!params.tg) { console.error("事件监听未设置目标对象 属性key=tg"); return false; } if (params.name && event[params.name]) { event[params.name] = event[params.name].filter(a => { return a[0] != params.tg; }) } else { for (let key in event) { event[key] = event[key].filter(a => { return a[0] != params.tg; }) } } } //页面onUnload触发监听 const pageStatus = function(self) { if (self["onUnload"]) { var s = self["onUnload"]; self["onUnload"] = function(a) { s.call(this, a); $remove({ tg: this }); } } else { self["onUnload"] = function() { $remove({ tg: this }); } } } exports.$on = $on; exports.$emit = $emit; exports.$remove = $remove; 此方法可挂载至全局getApp()调用 也可引入文件调用 写的不好 大家见谅
2020-06-09 - 小程序海报生成工具,可视化编辑直接生成代码使用,你的海报你自己做主
开门见山 工具地址 点我直达>>painter-custom-poster 由于挂载在github page上,打开速度会慢一些,请耐心等待或自行解决git网速问题 背景 在做小程序时候,我们经常会有一个需求,需要将小程序分享到朋友圈,但是朋友圈是不允许直接分享小程序,那我们还有其他的办法解决吗?答案肯定是有的,即 canvas 生成个性化海报分享图片到朋友圈 分析 小程序中有大量的生成图片需求,但是使用过 canvas 的人,都会发现一些难以预料的问题>>有关小程序的坑 直接在 canvas 上绘制图形,对于普通开发者来说代码会特别凌乱并且难以维护,经常会花费很久的时间去优化代码 不同的环境渲染问题,例如在开发者工具看起来好好的,一到 Android 真机,就出现图片不显示,位置不对应等等问题 解决 那可不可以开发一款生成海报的插件库呢? 首先,只需要提供一份简单的参数配置文件即可 解决掉小程序Canvas遇到的一些大大小小的坑 有严苛的测试环节,解决各种环境和各种机型遇到的问题,并提供稳定的线上版本 长期维护,并有专人更新迭代更新颖的功能 以上的要求当然是可以的,曾经的我也想尝试开发一款出来,但是后来尝试了几款现成的工具之后就放弃了,毕竟轮子这个东西,是需要不断维护更新的,另外已经有这么多优秀现成的插件了,我为何还要费力去写呢,贡献代码岂不更美哉,以下是我收集的几款 小程序生成图片库,轻松通过 json 方式绘制一张可以发到朋友圈的图片>>Painter 小程序组件-小程序海报组件>>wxa-plugin-canvas 微信小程序:一个 json 帮你完成分享朋友圈图片>>mp_canvas_drawer 我想干什么 唠了这么多,好像提供给大家插件就没我什么事情了…想走是不可能的 为了能够制作出更酷炫的海报,我思考了许久 虽然有了插件后,只需要提供配置代码就能够制作出一款海报来,但是我发现还是有些许问题 制作海报效率还是不够高,微调一个元素的大小和位置,就需要不断的修改保存代码,等待片刻,查看效果,真的烦 一个小小的位置调整可能就需要来回调整无数次,这种最简单的机械化劳动,这辈子是不可能的 拿着完美的稿子,递给设计师看,这个位置不对,这个线太粗,这个颜色太重…你信不信我打死你 对于一些精美复杂的海报,实现起来真的不太现实 那我需要怎么做呢,请点击这个链接体验>>painter-custom-poster 点击左侧例子展示中的任意一个例子,然后导入代码就能看到效果图,这下你应该能猜到了我的想法了 如何实现 刚开始我想用简单的html和css加拖动功能实现,通过简单尝试之后就放弃了,因为这个功能真的太复杂了,简单的工具肯定是不行的 中间这个计划停滞了很长时间,一度已经放弃 直到发现了这个库fabric.js,真的太太优秀了,赞美之词无以言表,唯一的缺点就是中文教程太少,必须生啃英文加谷歌翻译 fabric介绍,你可以很容易地创建任何一个简单的形状,复杂的形状,图像;将它们添加到画布中,并以任何你想要的方式进行修改:位置、尺寸、角度、颜色、笔画、不透明度等 How To Use 目前工具一共分成4部分 例子展示 用来将一些用户设计的精美海报显示出来,通过点击对应的例子并将代码导入画布中 画布区 显示真实的海报效果,画布里添加的元素,都可以直接用鼠标进行拖动,旋转,缩放操作 操作区 第一排四个按钮 复制代码 将画布的展示效果转化成小程序海报插件库所需要的json配置代码,目前我使用的是Painter库,默认会转化成这个插件的配置代码,将代码直接复制到card.js即可 查看代码 这个功能用不用无所谓,可以直观的看到生成的代码 导出json 将画布转化成fabric所需要的json代码,方便将自己设计的海报代码保存下来 导入json 将第3步导出的json代码导入,会在画布上显示已设计的海报样式 第二排五个按钮 画布 画布的属性参数 详解见下方 文字 添加文字的属性参数 详解见下方 矩形 添加矩形的属性参数 详解见下方 图片 添加图片的属性参数 详解见下方 二维码 添加二维码的属性参数 详解见下方 第三排 各种元素的详细设置参数 激活区 激活对象是指鼠标点击画布上的元素,该对象会被蓝色的边框覆盖,此时该对象被激活,可以执行拖动 旋转 缩放等操作 激活区只有对象被激活才会出来,用来设置激活对象的各种配置参数,修改value值后,实时更新当前激活对象的对应状态,点击其他区域,此模块将隐藏 快捷键 ‘←’ 左移一像素 ‘→’ 右移一像素 ‘↑’ 上移一像素 ‘↓’ 下移一像素 ‘ctrl + z’ 撤销 ‘ctrl + y’ 恢复 ‘delete’ 删除 ‘[’ 提高元素的层级 ‘]’ 降低元素的层级 布局属性 通用布局属性 属性 说明 默认 rotate 旋转,按照顺时针旋转的度数 0 width、height view 的宽度和高度 top、left 如 css 中为 absolute 布局时的作用 0 background 背景颜色 rgba(0,0,0,0) borderRadius 边框圆角 0 borderWidth 边框宽 0 borderColor 边框颜色 #000000 shadow 阴影 ‘’ shadow 可以同时修饰 image、rect、text 等 。在修饰 text 时则相当于 text-shadow;修饰 image 和 rect 时相当于 box-shadow 使用方法: [代码]shadow: 'h-shadow v-shadow blur color'; h-shadow: 必需。水平阴影的位置。允许负值。 v-shadow: 必需。垂直阴影的位置。允许负值。 blur: 必需。模糊的距离。 color: 必需。阴影的颜色。 举例: shadow:10 10 5 #888888 [代码] 渐变色支持 你可以在画布的 background 属性中使用以下方式实现 css 3 的渐变色,其中 radial-gradient 渐变的圆心为 中点,半径为最长边,目前不支持自己设置。 [代码]linear-gradient(-135deg, blue 0%, rgba(18, 52, 86, 1) 20%, #987 80%) radial-gradient(rgba(0, 0, 0, 0) 5%, #0ff 15%, #f0f 60%) [代码] !!!注意:颜色后面的百分比一定得写。 画布属性 属性 说明 默认 times 控制生成插件代码的宽度大小,比如画布宽100,times为2,生成的值为200 1 文字属性 属性名称 说明 默认值 text 字体内容 别跟我谈感情,谈感情伤钱 maxLines 最大行数 不限,根据 width 来 lineHeight 行高(上下两行文字baseline的距离) 1.3 fontSize 字体大小 30 color 字体颜色 #000000 fontWeight 字体粗细。仅支持 normal, bold normal textDecoration 文本修饰,支持none underline、 overline、 linethrough none textStyle fill: 填充样式,stroke:镂空样式 fill fontFamily 字体 sans-serif textAlign 文字的对齐方式,分为 left, center, right left 备注: fontFamily,工具中的第一个例子支持文字字体,但是导入小程序为什么看不到呢,小程序官网加载网络字体方法>> 加载字体教程>> 文字高度 是maxLines lineHeight2个字段一起计算出来的 图片属性 属性 说明 默认 url 图片路径 mode 图片裁剪、缩放的模式 aspectFill mode参数详解 scaleToFill 缩放图片到固定的宽高 aspectFill 图片裁剪显示对应的宽高 auto 自动填充 宽度全显示 高度自适应居中显示 Tips(一定要看哦~) 本工具不考虑兼容性,如发现不兼容请使用google浏览器 painter现在只支持这几种图形,所以暂不支持圆,线等 如果编辑过程,一个元素被挡住了,无法操作,请选择对象并通过[ ]快捷键提高降低元素的层级 文字暂不支持直接缩放操作,因为文字大小和元素高度不容易计算,可以通过修改激活栏目maxLines lineHeight fontSize值来动态改变元素 如发现导出的代码一个元素被另一个元素挡住了,请手动调整元素的位置,json数组中元素越往后层级显示就越高,由于painter没有提供层级参数,所以目前只能这样做 本工具导出代码全是以px为单位,为什么不支持rpx, 因为painter在rpx单位下,阴影和边框宽会出现大小计算问题,由于原例子没有提供px生成图片方案,可以下载我这里修改过的demo>>Painter即可解决 文本宽度随着字数不同而动态变化,想在文本后面加个图标根据文本区域长度布局, 请参考Painter文档这块教程直接修改源码 由于本工具开发有些许难度,如出现bug,建议或者使用上的问题,请提issue,源码地址>>painter-custom-poster 海报贡献 如果你设计的海报很好看,并且愿意开源贡献,可以贡献你的海报代码和缩略图,例子代码文件在example中,按顺序排列,例如现在库里例子是example2.js,那你添加example3.js和example3.jpg图片,事例可以参考一下文件夹中源码,然后在index.js中导出一下 导出代码 代码不要格式化,会报错,请原模原样复制到json字段里 生成缩略图 刚开始我想在此工具中直接生成图片,但是由于浏览器图片跨域问题导致报错失败 所以请去小程序中生成保存图片,图片质量设置0.2,并去tinypng压缩一下图片 找到painter.js,替换下边这个方法,可以生成0.2质量的图片,代码如下 [代码] saveImgToLocal() { const that = this; setTimeout(() => { wx.canvasToTempFilePath( { canvasId: 'k-canvas', fileType: 'jpg', quality: 0.2, success: function(res) { that.getImageInfo(res.tempFilePath); }, fail: function(error) { console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`); that.triggerEvent('imgErr', { error: error }); } }, this ); }, 300); } [代码] TODO 颜色值选择支持调色板工具 文字padding支持 缩放位置弹跳问题优化 假如需求大的话,支持其他几款插件库代码的生成 ~ 创作不易,如果对你有帮助,请给个星星 star✨✨ 谢谢 ~
2019-09-27 - 填坑来了。PC小程序的几个坑,大家mark吧。
坑一:必须勾选增强编译,否则PC小程序Windows打开白屏,MAC正常。 坑二:PC小程序大屏显示的配置:会强制大屏。 app.json "resizable": true, "window": { "pageOrientation": "portrait", }, 某个需要大屏显示的page.json { "pageOrientation": "landscape" } 坑三:以上配置在MAC上无效。(已证明在MAC一样有效) 坑三:app.json里pageOrientation设为auto时,page.json里的设置在MAC上无效。猜,auto时,需要手机有横屏动作才会触发。
2020-06-22 - 小程序日历组件推荐
小程序日历组件推荐 这几天在做民宿小程序,遇到一个需求就是 用户选择日期,来下单,由于民宿每天的价格都是变化的,所以要在日历上显示价格,找了很久,终于找到了 vant https://youzan.github.io/vant-weapp/#/calendar 1 [图片] 2 [图片] 3 具体使用方法如下所示 [图片] 4 具体小程序界面截图如下所示: 5 [图片] 6 7 现在说起来都是风轻云淡,但是找的过程又盲目,有着急,开发中,也遇到各种问题,好在,回过头去,vant calendar就是我要找的日历组件 8
2020-06-07 - 微信支付云调用拼夕夕版尝鲜踩坑教程 [拎包哥]
微信支付竟然跟云开发发生了关系,手残党又迎来福音喽! [图片] 但!本着跟着官方教程复制粘贴就能跑的心态,拎包哥慢慢发现事情并不简单。。。 [图片] 下面列出几个填过的小坑。 1. 退款API权限 [图片] 1.1 如果只是想进行微信支付,退款API权限不需要授权的, 只要在微信支付商家助手(公众号)上授权了JSAPI权限就可以进行微信支付。 1.2 如果要对退款API进行授权,就得在登录商户平台后,再打开 https://pay.weixin.qq.com/index.php/extend/product/submch ,才能看到“我授权的产品" 这他喵竟然是在首页你敢信? [图片] [图片] 所以产品中心根本就没有的,不要再瞎找了。 [图片] 2. 两个云函数 不要被官方文档所蒙蔽了,其实我们需要写两个云函数。 [图片] [图片] functionName就是官方文档没有写的第二个云函数的名字。如果你不写这个函数, 就会出现各种莫名其妙的报错,不要试图从这些报错上找出答案,都是扯淡。 functionName对应函数代码 exports.main = async (event, context) => { return(event,context) } 踩过这两个坑,再跟着教程走基本上就可以进行微信支付的云调用了。 欢迎大哥们批评指正错误。 最后感谢这些知识的来源: 1.微信问答 https://developers.weixin.qq.com/community/pay/doc/0002ce8b3007d89db65aa98f655c00 2.bilibili李东教学 https://www.bilibili.com/video/BV1uz411B7Kb ================点个赞,是拎包哥继续瞎逼逼的动力哦================= 番外坑 outTradeNo要放在exports.main里面,放在外面则会得到相同而不是随机的值。 老张哥或者各路大神如果你看到这个问题,请帮忙解释一下为啥,谢谢! exports.main = async(event,context)=>{ var randomNo = Math.random().toString(36).subStr(2,15) var timeStamp = parseInt(Date.now()/1000) + '' var outTradeNo = 'otn' + timeStamp + randomNo } ================2020/5/27更新=================
2020-05-27 - 小程序图片裁剪插件 image-cropper
之前的插件类目没有了导致搜不到了,重新发个文章。 image-cropper 一款高性能的小程序图片裁剪插件,支持旋转。 [图片] 优势 [代码]1.功能强大。[代码] [代码]2.性能超高超流畅,大图毫无卡顿感。[代码] [代码]3.组件化,使用简单。[代码] [代码]4.点击中间窗口实时查看裁剪结果。[代码] ㅤ 初始准备 1.json文件中添加image-cropper [代码] "usingComponents": { "image-cropper": "../image-cropper/image-cropper" }, "navigationBarTitleText": "裁剪图片", "disableScroll": true [代码] 2.wxml文件 [代码]<image-cropper id="image-cropper" limit_move="{{true}}" disable_rotate="{{true}}" width="{{width}}" height="{{height}}" imgSrc="{{src}}" bindload="cropperload" bindimageload="loadimage" bindtapcut="clickcut"></image-cropper> [代码] 3.简单示例 [代码] Page({ data: { src:'', width:250,//宽度 height: 250,//高度 }, onLoad: function (options) { //获取到image-cropper实例 this.cropper = this.selectComponent("#image-cropper"); //开始裁剪 this.setData({ src:"https://raw.githubusercontent.com/1977474741/image-cropper/dev/image/code.jpg", }); wx.showLoading({ title: '加载中' }) }, cropperload(e){ console.log("cropper初始化完成"); }, loadimage(e){ console.log("图片加载完成",e.detail); wx.hideLoading(); //重置图片角度、缩放、位置 this.cropper.imgReset(); }, clickcut(e) { console.log(e.detail); //点击裁剪框阅览图片 wx.previewImage({ current: e.detail.url, // 当前显示图片的http链接 urls: [e.detail.url] // 需要预览的图片http链接列表 }) }, }) [代码] 参数说明 属性 类型 缺省值 取值 描述 必填 imgSrc String 无 无限制 图片地址(如果是网络图片需配置安全域名) 否 disable_rotate Boolean false true/false 禁止用户旋转(为false时建议同时设置limit_move为false) 否 limit_move Boolean false true/false 限制图片移动范围(裁剪框始终在图片内)(为true时建议同时设置disable_rotate为true) 否 width Number 200 超过屏幕宽度自动转为屏幕宽度 裁剪框宽度 否 height Number 200 超过屏幕高度自动转为屏幕高度 裁剪框高度 否 max_width Number 300 裁剪框最大宽度 裁剪框最大宽度 否 max_height Number 300 裁剪框最大高度 裁剪框最大高度 否 min_width Number 100 裁剪框最小宽度 裁剪框最小宽度 否 min_height Number 100 裁剪框最小高度 裁剪框最小高度 否 disable_width Boolean false true/false 锁定裁剪框宽度 否 disable_height Boolean false true/false 锁定裁剪框高度 否 disable_ratio Boolean false true/false 锁定裁剪框比例 否 export_scale Number 3 无限制 输出图片的比例(相对于裁剪框尺寸) 否 quality Number 1 0-1 生成的图片质量 否 cut_top Number 居中 始终在屏幕内 裁剪框上边距 否 cut_left Number 居中 始终在屏幕内 裁剪框左边距 否 [代码]img_width[代码] Number 宽高都不设置,最小边填满裁剪框 支持%(不加单位为px)(只设置宽度,高度自适应) 图片宽度 否 [代码]img_height[代码] Number 宽高都不设置,最小边填满裁剪框 支持%(不加单位为px)(只设置高度,宽度自适应) 图片高度 否 scale Number 1 无限制 图片的缩放比 否 angle Number 0 (limit_move=true时angle=n*90) 图片的旋转角度 否 min_scale Number 0.5 无限制 图片的最小缩放比 否 max_scale Number 2 无限制 图片的最大缩放比 否 bindload Function null 函数名称 cropper初始化完成 否 bindimageload Function null 函数名称 图片加载完成,返回值Object{width,height,path,type等} 否 bindtapcut Function null 函数名称 点击中间裁剪框,返回值Object{src,width,height} 否 函数说明 函数名 参数 返回值 描述 参数必填 upload 无 无 调起wx上传图片接口并开始剪裁 否 pushImg src 无 放入图片开始裁剪 是 getImg Function(回调函数) [代码]Object{url,width,height}[代码] 裁剪并获取图片(图片尺寸 = 图片宽高 * export_scale) 是 setCutXY X、Y 无 设置裁剪框位置 是 setCutSize width、height 无 设置裁剪框大小 是 setCutCenter 无 无 设置裁剪框居中 否 setScale scale 无 设置图片缩放比例(不受min_scale、max_scale影响) 是 setAngle deg 无 设置图片旋转角度(带过渡效果) 是 setTransform {x,y,angle,scale,cutX,cutY} 无 图片在原有基础上的变化(scale受min_scale、max_scale影响) 根据需要传参 imgReset 无 无 重置图片的角度、缩放、位置(可以在onloadImage回调里使用) 否 GitHub https://github.com/wx-plugin/image-cropper/tree/master 如果有什么好的建议欢迎提issues或者提pr
2021-12-15 - 小程序图片懒加载终极方案
效果图 既然来了,把妹子都给你。 [图片] 定义懒加载,前端人都知道的一种性能优化方式,简单的来说,只有当图片出现在浏览器的可视区域内时,才设置图片正真的路径,让图片显示出来。这就是图片懒加载。 实现原理监听页面的[代码]scroll[代码]事件,判读元素距离页面的[代码]top[代码]值是否是小于等于页面的可视高度 判断逻辑代码如下 [代码]element.getBoundingClientRect().top <= document.documentElement.clientHeight ? 显示 : 默认[代码] 我们知道小程序页面的脚本逻辑是在JsCore中运行,JsCore是一个没有窗口对象的环境,所以不能在脚本中使用window,也无法在脚本中操作组件。 所以关于图片懒加载就需要在数据上面做文章了。 页面页面上面只需要根据数据的某一个字段来判断是否显示图片就可以了,字段为Boolean类型,当为false的时候显示默认图片就行了。 代码大概长成这样 <view wx:for="{{list}}" class='item item-{{index}}' wx:key="{{index}}"> <image class="{{item.show ? 'active': ''}}" src="{{item.show ? item.src : item.def}}"></image> </view> 布局跟简单,[代码]view[代码]组件里面有个图片,并循环[代码]list[代码],有多少就展示多少 [代码]image[代码]组件的[代码]src[代码]字段通过每一项的[代码]show[代码]来进行绑定,[代码]active[代码]是加了个透明的过渡 样式 image{ transition: all .3s ease; opacity: 0; } .active{ opacity: 1; } 逻辑 本位主要讲解懒加载,所以把数据写死在页面上了 数据结构如下: [图片] 我们使用两种方式来实现懒加载,准备好没有,一起来快乐的撸码吧。 WXML节点信息 小程序支持调用createSelectQuery创建一个[代码]SelectorQuery[代码]实例,并使用[代码]select[代码]方法来选择节点,并通过[代码]boundingClientRect[代码]来获取节点信息。 wx.createSelectorQuery().select('.item').boundingClientRect((ret)=>{ console.log(ret) }).exec() 显示结果如下 [图片] 悄悄告诉你,小程序里面有个[代码]onPageScroll[代码]函数,是用来监听页面的滚动的。 还有个[代码]getSystemInfo[代码]函数,可以获取获取系统信息,里面包含屏幕的高度。 接下来,思路就透彻了吧。还是上面的逻辑, 扒拉扒拉直接写代码就行了,这里只写下主要的逻辑,完整代码请戳文末github showImg(){ let group = this.data.group let height = this.data.height // 页面的可视高度 wx.createSelectorQuery().selectAll('.item').boundingClientRect((ret) => { ret.forEach((item, index) => { if (item.top <= height) { 判断是否在显示范围内 group[index].show = true // 根据下标改变状态 } }) this.setData({ group }) }).exec() } onPageScroll(){ // 滚动事件 this.showImg() } 至此,我们完成了一个小程序版的图片懒加载,只是思维转变了下,其实并没有改变实现方式。我们来学些新的东西吧。 节点布局相交状态 节点相交状态是啥?它是一个新的API,叫做[代码]IntersectionObserver[代码], 本文只讲解简单的使用,了解更多请猛戳没错,就是点我 小程序里面给它的定义是节点布局交叉状态API可用于监听两个或多个组件节点在布局位置上的相交状态。这一组API常常可以用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见。 里面设计的概念主要有五个,分别为 参照节点:以某参照节点的布局区域作为参照区域,参照节点可以有多个,多个话参照区域取它们的布局区域的交集目标节点:监听的目标,只能是一个节点相交区域:目标节点与参照节点的相交区域相交比例:目标节点与参照节点的相交比例阈值:可以有多个,默认为[0], 可以理解为交叉比例,例如[0.2, 0.5]关于它的API有五个,依次如下 1、[代码]createIntersectionObserver([this], [options])[代码],见名知意,创建一个IntersectionObserver实例 2、[代码]intersectionObserver.relativeTo(selector, [margins])[代码], 指定节点作为参照区域,margins参数可以放大缩小参照区域,可以包含top、left、bottom、right四项 3、[代码]intersectionObserver.relativeToViewport([margin])[代码],指定页面显示区域为参照区域 4、[代码]intersectionObserver.observer(targetSelector, callback)[代码],参数为指定监听的节点和一个回调函数,目标元素的相交状态发生变化时就会触发此函数,callback函数包含一个result,下面再讲 5、[代码]intersectionObserver.disconnect()[代码] 停止监听,回调函数不会再触发 然后说下callback函数中的result,它包含的字段为 [图片] 我们主要使用[代码]intersectionRatio[代码]进行判断,当它大于0时说明是相交的也就是可见的。 先来波测试题,请说出下面的函数做了什么,并且log函数会执行几次 1、 wx.createIntersectionObserver().relativeToViewport().observer('.box', (result) => { console.log('监听box组件触发的函数') }) 2、 wx.createIntersectionObserver().relativeTo('.box').observer('.item', (result) => { console.log('监听item组件触发的函数') }) 3、 wx.createIntersectionObserver().relativeToViewport().observer('.box', (result) => { if(result.intersectionRatio > 0){ console.log('.box组件是可见的') } }) duang,揭晓答案。 第一个以当前页面的视窗监听了[代码].box[代码]组件,log会触发两次,一次是进入页面一次是离开页面 第二个以[代码].box[代码]节点的布局区域监听了[代码].item[代码]组件,log会触发两次,一次是进入页面一次是离开页面 第三个以当前页面的视窗监听了[代码].box[代码]组件,log只会在节点可见的时候触发 好了,题也做了,API你也掌握了,相信你已经可以使用[代码]IntersectionObserver[代码]来实现图片懒加载了吧,主要逻辑如下 let group = this.data.group // 获取图片数组数据 for (let i in this.data.group){ wx.createIntersectionObserver().relativeToViewport().observe('.item-'+ i, (ret) => { if (ret.intersectionRatio > 0){ group[i].show = true } this.setData({ group }) }) } 最后 至此,我们使用两种方式实现了小程序版本的图片懒加载,可以发现,使用[代码]IntersectionObserver[代码]来实现不要太酸爽
2020-05-12 - 如何只使用一个云函数搞定一个庞大而复杂的系统
吐槽 翻遍社区的文章,关于云开发的干货,少之又少,大部分都还是官方文档的搬来搬去,没啥营养,是时候放出一点技术"干货"了(有经验的开发者都能想到的方案)! 正题 小程序云开发的云函数的最大限制是 [代码]50[代码] 个,假设每个接口都使用 [代码]1[代码] 个云函数的话,有 [代码]10[代码] 张表,每张表都有 [代码]增删改查[代码] 四个接口,那么就会有 [代码]40[代码] 个接口,再加上一些其他接口,差不多刚刚好够用,那如果有 [代码]20[代码] 张表,甚至更多的表、更多的接口呢?对于中小型的小程序来说足够使用,那如果一个非常庞大而复杂的系统该怎么办呢? 而且每一个云函数的运行环境是独立的,想要共享一些数据也不是特别方便,那么有没有什么办法突破这样的限制呢? 其实解决方案很简单,只需要一点点的 [代码]OOP[代码] 思想和利用 [代码]JavaScript[代码] 的特性,一个云函数就可以搞定所有的接口。 具体的实现请往下看。 思路 云函数的运行环境是 [代码]Nodejs[代码] , 那么使用的语言就是 [代码]JavaScript[代码] ,可以充分的利用 [代码]JavaScript[代码] 的特性。 [代码]JavaScript[代码] 中的 [代码]属性访问表达式[代码] 有两种语法 [代码]expression . identifier expression [ expression ] [代码] 第一种写法是一个表达式后跟随一个句点 [代码].[代码] 和一个标识符。表达式指定对象,标识符则指定需要访问的属性的名称。 第二种写法是使用方括号 [代码][][代码],方括号内是另一个表达式(这种方法适用于对象和数组)。第二个表达式指定要访问的属性的名称或者代表要访问数组元素的索引。 不管使用哪种形式的属性访问表达式,在 [代码].[代码] 和 [代码][][代码] 之前的表达式总是会首先计算。 虽然 [代码].[代码] 的写法更加简单,但这种方式只适用于要访问的属性名称的合法标识符,并需要准确知道访问的属性的名字,如果属性的名称是一个保留字或者包含空格和标点符号,或者是一个数字(对于数组来说),则必须使用方括号 [代码][][代码] 的写法。当属性名是通过运算得出的值而不是固定值的时候,这时也必须使用方括号 [代码][][代码] 写法。 感谢社区大神 @卢霄霄 提供参考资料,详见 [代码]JavaScript权威指南[代码] (犀牛书)4.4章节。 可以使用 [代码][][代码] 的形式来完成动态的属性访问。具体实现请往下看。 实现 上面说了太多废话了,下面直接开干吧。 新建云函数 在云开发目录中新建一个云函数,我这里命名为 [代码]cloud[代码]。 打开 [代码]index.js[代码] 文件你会看到下面这段代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') // 初始化 cloud cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } } [代码] 这个云函数仅作为入口使用,上面提到了云函数的运行环境是 [代码]Nodejs[代码] 那么 [代码]Nodejs[代码] 的特性也是可以使用的,这里主要用到的是全局对象 [代码]global[代码],详见文档 在文件中,写入一些必要的全局变量,主要还是云数据库方面的,方便后面使用。 在初始化后面插入代码 [代码]global.cloud = cloud global.db = cloud.database() global._ = db.command global.$ = _.aggregate [代码] 这样就可以在同一个云函数环境中直接访问这些全局变量。 创建公共类 然后新建一个文件夹,我这里命名为 [代码]controllers[代码] ,这个文件夹用于存放所有的接口。 在 [代码]controllers[代码] 中新建一个 [代码]base-controller.js[代码] 文件,创建一个叫做 [代码]BaseController[代码] 的类,用于提供一些公用的方法。 内容如下: [代码]class BaseController { /** * 调用成功 */ success (data) { return { code: 0, data } } /** * 调用失败 */ fail (erroCode = 0, msg = '') { return { erroCode, msg, code: -1 } } } module.exports = BaseController [代码] 看到这里大家可能有点没看懂在做什么,那么请继续往下看。 创建接口 假设创建一些要操作用户相关的的接口,可以在 [代码]controllers[代码] 文件夹中新建一个 [代码]user-controller.js[代码] 的文件,创建一个名为 [代码]UserController[代码] 的类,并继承上面的 [代码]BaseController[代码] 类,内容如下: [代码]const BaseController = require('./base-controller.js') class UserController extends BaseController { // ... } module.exports = UserController [代码] 可以在这个类中编写所有关于 [代码]user[代码] 的接口方法。 编写接口 假设要分页查询用户信息,可以在 [代码]UserController[代码] 类中创建一个 [代码]list[代码] 方法。 代码如下: [代码]async list (data) { const { pageIndex, pageSize } = data let result = await db.collection('users') .skip((pageIndex - 1) * pageSize) .limit(pageSize) .get() .then(result => this.success(result.data)) .catch(() => this.fail([])) return result } [代码] 由于上面已经定义了全局变量 [代码]db[代码] 所以在 [代码]UserController[代码] 中无需引入 [代码]wx-server-sdk[代码] 引入接口类 写到这里接口已经完成了,还需要再引入这些接口类才可以进行访问。在 [代码]index.js[代码] 中引入 [代码]user-controller.js[代码] [代码]const User = require('./controllers/user-controller.js') [代码] 然后创建一个 [代码]api[代码] 变量,[代码]new[代码] 一个 [代码]User[代码] 实例 [代码]const api = { user: new User() } [代码] 在 [代码]main[代码] 方法中调用 [代码]UserController[代码] 中的方法。 [代码]exports.main = async (event, context) => { const { data } = event let result = await api['user']['list'](data) return result } [代码] 写到这里基本已经完成了接口的调用,但想要一个云函数动态调用所有接口还需要做一些改动。 动态调用接口 刚开始的时候介绍了 [代码]属性访问表达式[代码],限制稍微改动一下 [代码]main[代码] 方法 [代码]exports.main = async (event, context) => { const { controller, action, data } = event const result = await api[controller][action](data) return result } [代码] 在小程序调用云函数时,需要传入 [代码]controller[代码]、[代码]action[代码] 和 [代码]data[代码] 参数即可 [代码]const result = await wx.cloud.callFunction({ name: 'cloud', data: { controller, action, data } }) [代码] 完整 [代码]index.js[代码] 文件的代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const User = require('./controllers/user-controller.js') const api = { user: new User() } // 初始化 cloud cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) global.cloud = cloud global.db = cloud.database() global._ = db.command global.$ = _.aggregate // 云函数入口 exports.main = async (event, context) => { exports.main = async (event, context) => { const { controller, action, data } = event const result = await api[controller][action](data) return result } } [代码] 其他实现 云开发官方团队打造的轮子 tcb-router
2020-05-26 - WXS仿拼多多横向滚动条ScrollView组件
Demo: 最终实现效果如下 [图片] 接下来要实现的就是下面的,滚动条,显示横向拖动的进度。 Component代码: index.js Component({ properties: { customStyle: { type:String, }, // 根据Slot中的元素计算出的ScrollView总宽度 rpx scrollViewWidth: { type: Number, value: 0 }, // ScrollView的总高度,调整高度来控制显示行数 rpx scrollViewHeight: { type: Number, value: 0 }, // 滚动条的宽度rpx scrollBarWidth: { type: Number, value: 160 }, // 滚动条的高度 rpx scrollBarHeight: { type: Number, value: 10 }, // 滚动条滑块的宽度 rpx scrollBarBlockWidth: { type: Number, value: 100 }, // 滚动条样式 scrollBarStyle: { type: String, }, // 滚动滑块样式 scrollBarBlockStyle: { type: String, } }, data: { windowWidth: 375, // px px2rpxRatio: 0, windowWidth2ScrollViewWidthRatio: 0, scrollBarBlockLeft: 0, }, lifetimes: { attached: function() { this.data.windowWidth = wx.getSystemInfoSync().windowWidth this.setData({ px2rpxRatio: Number(750 / this.data.windowWidth).toFixed(3) }) console.log('px2rpxRatio', this.data.px2rpxRatio) this.setData({ windowWidth2ScrollViewWidthRatio: Number(this.data.windowWidth * this.data.px2rpxRatio / this.data.scrollViewWidth).toFixed(3) }) let scrollBarBlockWidth = Number(this.data.scrollBarWidth * this.data.windowWidth2ScrollViewWidthRatio).toFixed() if(scrollBarBlockWidth >= this.data.scrollBarWidth) { scrollBarBlockWidth = this.data.scrollBarWidth } this.setData({ scrollBarBlockWidth: scrollBarBlockWidth }) }, }, }) index.wxml <wxs module="wxs" src="./index.wxs"></wxs> <view style="{{ customStyle }}"> <scroll-view scroll-x="{{ true }}" bind:scroll="{{ wxs.onScroll }}" data-px2rpxRatio="{{ px2rpxRatio }}" data-scrollViewWidth="{{ scrollViewWidth }}" data-scrollBarWidth="{{ scrollBarWidth }}"> <view class="scroll-view" style="height: {{ scrollViewHeight }}rpx"> <slot/> </view> </scroll-view> <view class="scrollbar" style="width: {{ scrollBarWidth }}rpx; height: {{ scrollBarHeight}}rpx; margin: 0 auto; margin-top: 10rpx; {{ scrollBarStyle }}"> <view class="scrollbar-block" id="scrollbar" style="width: {{ scrollBarBlockWidth }}rpx; left: {{scrollBarBlockLeft}}rpx; {{ scrollBarBlockStyle }}"></view> </view> </view> index.wxs var onScroll = function(e, ownerInstance) { var scrollBarBlockLeft = (e.detail.scrollLeft * e.currentTarget.dataset.px2rpxratio / (e.currentTarget.dataset.scrollviewwidth)) * e.currentTarget.dataset.scrollbarwidth ownerInstance.selectComponent('#scrollbar').setStyle({left: scrollBarBlockLeft + 'rpx'}) } module.exports = { onScroll: onScroll }; index.wxss .scroll-view{ width: 100%; box-sizing: border-box; display: flex; flex-direction: column; flex-wrap: wrap; justify-content: flex-start; align-items: flex-start; align-content: flex-start; } .scrollbar { margin: 0 auto; background: black; position: relative; border-radius: 4rpx; overflow: hidden; } .scrollbar-block { height: 100%; position: absolute; top: 0; background: darkred; border-radius: 4rpx; } 以上就是组件的代码部分,下面来看一看在Page页中如何使用,代码也很简单: index.js //index.js Page({ data: { data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] } }) index.wxml <x-scroll-bar scroll-view-width="1200" scroll-view-height="400"> <view wx:for="{{ data }}" class="block" > {{ item }} </view> </x-scroll-bar> index.wxss .block{ flex-shrink: 0; flex-grow: 0; background: black; width: 200rpx; height: 200rpx; line-height: 200rpx; text-align: center; color: white; } .block:nth-child(even) { background: red; } 以上全部内容,组件代码已经在我的Github,可直接访问下载: https://github.com/foolstack-omg/fool-weapp
2020-04-08 - 微信公众号发送客服信息a标签显示失败,如何解决?
微信公众号发送客服信息时(带a标签),PC端和ios显示正常,,安卓端a标签会直接显示html代码,例如:<a href=“...”>点击跳转</a>。
2020-05-15 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26 - 如何在小程序中快速实现环形进度条
在小程序开发过程中经常涉及到一些图表类需求,其中环形进度条比较属于比较常见的需求 [图片] [中间的文字部分需要自己实现,因为每个项目不同,本工具只实现进度条] 上图中,一方面我们我们需要实现动态计算弧度的进度条,还需要在进度条上加上渐变效果,如果每次都需要自己手写,那需要很多重复劳动,所以决定为为小程序生态圈贡献一份小小的力量,下面来介绍一下整个工具的实现思路,喜欢的给个star咯 https://github.com/lucaszhu2zgf/mp-progress 环形进度条由灰色底圈+渐变不确定圆弧+双色纽扣组成,首先先把页面结构写好: .canvas{ position: absolute; top: 0; left: 0; width: 400rpx; height: 400rpx; } 因为进度条需要盖在文字上面,所以采用了绝对定位。接下来先把灰色底圈给画上: const context = wx.createContext(); // 打底灰色曲线 context.beginPath(); context.arc(this.convert_length(200), this.convert_length(200), r, 0, 2*Math.PI); context.setLineWidth(12); context.setStrokeStyle('#f0f0f0'); context.stroke(); wx.drawCanvas({ canvasId: 'progress', actions: context.getActions() }); 效果如下: [图片] 接下来就要画绿色的进度条,渐变暂时先不考虑 // 圆弧角度 const deg = ((remain/total).toFixed(2))*2*Math.PI; // 画渐变曲线 context.beginPath(); // 由于外层大小是400,所以圆弧圆心坐标是200,200 context.arc(this.convert_length(200), this.convert_length(200), r, 0, deg); context.setLineWidth(12); context.setStrokeStyle('#56B37F'); context.stroke(); // 辅助函数,用于转换小程序中的rpx convert_length(length) { return Math.round(wx.getSystemInfoSync().windowWidth * length / 750); } [图片] 似乎完成了一大部分,先自测看看不是满圆的情况是啥样子,比如现在剩余车位是120个 [图片] 因为圆弧函数arc默认的起点在3点钟方向,而设计想要的圆弧的起点从12点钟方向开始,现在这样是没法达到预期效果。是不是可以使用css让canvas自己旋转-90deg就好了呢?于是我在上面的canvas样式中新增以下规则: .canvas{ transform: rotate(-90deg); } 但是在真机上并不起作用,于是我把新增的样式放到包裹canvas的外层元素上,发现外层元素已经旋转,可是圆弧还是从3点钟方向开始的,唯一能解释这个现象的是官方说:小程序中的canvas使用的是原生组件,所以这样设置css并不能达到我们想要的效果 [图片] 所以必须要在canvas画图的时候把坐标原点移动到弧形圆心,并且在画布内旋转-90deg [图片] // 更换原点 context.translate(this.convert_length(200), this.convert_length(200)); // arc原点默认为3点钟方向,需要调整到12点 context.rotate(-90 * Math.PI / 180); // 需要注意的是,原点变换之后圆弧arc原点也变成了0,0 真机预览效果达成预期 [图片] 接下来添加环形渐变效果,但是canvas原本提供的渐变类型只有两种: 1、LinearGradient线性渐变 [图片] 2、CircularGradient圆形渐变 [图片] 两种渐变中离设计效果最近的是线性渐变,至于为什么能够形成似乎是随圆形弧度增加而颜色变深的效果也只是控制坐标开始和结束的坐标位置罢了 const grd = context.createLinearGradient(0, 0, 100, 90); grd.addColorStop(0, '#56B37F'); grd.addColorStop(1, '#c0e674'); // 画渐变曲线 context.beginPath(); context.arc(0, 0, r, 0, deg); context.setLineWidth(12); context.setStrokeStyle(grd); context.stroke(); 来看一下真机预览效果: [图片] 非常棒,最后就剩下跟随进度条的纽扣效果了 [图片] 根据三角函数,已知三角形夹角根据公式radian = 2*Math.PI/360*deg,再利用cos和sin函数可以x、y,从而计算出纽扣在各部分半圆的坐标 const mathDeg = ((remain/total).toFixed(2))*360; // 计算弧度 let radian = ''; // 圆圈半径 const r = +this.convert_length(170); // 三角函数cos=y/r,sin=x/r,分别得到小点的x、y坐标 let x = 0; let y = 0; if (mathDeg <= 90) { // 求弧度 radian = 2*Math.PI/360*mathDeg; x = Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 90 && mathDeg <= 180) { // 求弧度 radian = 2*Math.PI/360*(180 - mathDeg); x = -Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 180 && mathDeg <= 270) { // 求弧度 radian = 2*Math.PI/360*(mathDeg - 180); x = -Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } else{ // 求弧度 radian = 2*Math.PI/360*(360 - mathDeg); x = Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } [图片] 有了纽扣的圆形坐标,最后一步就是按照设计绘制样式了 // 画纽扣 context.beginPath(); context.arc(x, y, this.convert_length(24), 0, 2 * Math.PI); context.setFillStyle('#ffffff'); context.setShadow(0, 0, this.convert_length(10), 'rgba(86,179,127,0.5)'); context.fill(); // 画绿点 context.beginPath(); context.arc(x, y, this.convert_length(12), 0, 2 * Math.PI); context.setFillStyle('#56B37F'); context.fill(); 来看一下最终效果 [图片] 最后我重新review了整个代码逻辑,并且已经将代码开源到https://github.com/lucaszhu2zgf/mp-progress,欢迎大家使用
2020-05-27 - 【笔记】云开发聚合实现分页,涉及跨表查询、逻辑计算、判断权限、数据格式化、限制输出
背景: 之前不会用聚合,因此把数据库结构分为了用户表、帖子表、喜欢表。小程序端请求一次列表,要根据帖子列表,循环查询用户表,并且还要做一系列的逻辑运算处理,计算当前帖子的权限、是否喜欢过、喜欢人数、是否有这个帖子管理权限等信息。 这样做有很多弊端: 处理速度慢,资源耗费严重,循环查询肯定慢且耗费资源,一个列表需要21次查询。需要写大量逻辑处理代码,如计算管理权限,喜欢数量、当前用户是否喜欢,格式处理等等。于是使用聚合进行了优化: 跨表查询数据格式化逻辑计算,权限判断、是否喜欢等数据统计,喜欢总人数权限判断,是否为管理员限制输出效果: 之前:上百行代码,多次查询,需要单独判断函数,处理时间在3000ms以上之后:几行代码,一次查询,直接查询时算出结果,处理时间在300ms以内 数据库结构 [图片] 代码实现: const { OPENID } = cloud.getWXContext(context) //构建查询条件 let query = null switch (Number(event.listType)) { case 0: query = db.collection('post').aggregate() .match({ //0我的 '_openid': OPENID }) .sort({ createTime: -1 }) .skip(20 * (event.pageNum - 1)) .limit(20) break; case 1: //1 随机 query = db.collection('post').aggregate() .match({ public: true, // feeling: _.gte(50) }) .sample({ size: 20 }) break; case 2: query = db.collection('post').aggregate() .match({ //2喜欢 likes: _.all([OPENID]) }) .sort({ createTime: -1 }) .skip(20 * (event.pageNum - 1)) .limit(20) break; case 4: query = db.collection('post').aggregate() .match({ //4指定 _id: event.id }) .sort({ createTime: -1 }) .skip(20 * (event.pageNum - 1)) .limit(20) break; } //使用聚合处理后续数据 let listData = await query .lookup({ from: "user", localField: "_openid", foreignField: "_id", as: "postList" })//联表查询用户表 .replaceRoot({ newRoot: $.mergeObjects([$.arrayElemAt(['$postList', 0]), '$$ROOT']) })//将用户表输出到根节点 .addFields({ day: $.dayOfMonth('$createTime'), month: $.month('$createTime'), year: $.year('$createTime'), isLike: $.in([OPENID, '$likes']), //是否喜欢 isLiked: $.in([OPENID, '$liked']), //是否喜欢过 isAdmin: $.eq([OPENID, 'oy0T-4yk7lCRFGDefpFC4Yvx_ppU']),//是否管理员 isAuthor: $.eq(['$_openid', OPENID]),//是否为作者 like: $.size('$likes'), //喜欢该帖子数 face: $.switch({ branches: [ { case: $.gte(['$feeling', 90]), then: 9 }, { case: $.gte(['$feeling', 80]), then: 8 }, { case: $.gte(['$feeling', 70]), then: 7 }, { case: $.gte(['$feeling', 60]), then: 6 }, { case: $.gte(['$feeling', 50]), then: 5 }, { case: $.gte(['$feeling', 40]), then: 4 }, { case: $.gte(['$feeling', 30]), then: 3 }, { case: $.gte(['$feeling', 20]), then: 2 }, { case: $.gte(['$feeling', 10]), then: 1 } ], default: 0 }) //根据心情值判断对应表情 }) .project({ postList: 0, userInfo: 0, liked: 0, likes: 0, city: 0, province: 0, country: 0, language: 0, nlp: 0, saveType: 0, }) //清楚掉不需要的数据 .end() return listData
2020-05-26 - 用户引导组件开发(2)遮罩层与突出元素
我们一步步来吧,包括我踩过的坑我也会还原一遍,让大家一起长长见识 遮罩层 遮罩层是最没技术难度的,写个css就可以了。 .mask { z-index: 1110; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); } 按然后再加个bindtap,控制点击之后消失,感觉直接就可以用了呢。这里有铅笔画不出蜡笔的味道提供的一个代码片段: https://developers.weixin.qq.com/s/cvqEYzmM7Ffn 突出元素 这个才是难点所在。 1、确定元素位置。 我们首先要找出这个元素的轮廓,这个我是通过boundingClientRect实现的。 const query = wx.createSelectorQuery(); query.select("#gameInfo").boundingClientRect(); //gameInfo就是我们所需要突出展示的元素ID,后续可以用config传入 query.exec(function(res) { console.log(res); } 输出如下: [图片] 可以看到这个办法很好地取到了所要突出的元素的位置。 2、画出镂空遮罩。 这里我参考了这篇文章:https://www.cnblogs.com/mxdmg/p/10427605.html 文章中的方法一就不说了,又麻烦又浪费空间。 方法二我试了下,展示效果确实不错,但是也如文中所说的,点遮罩的时候会点到底下的元素,肯定会影响效果。 方法三没试,因为感觉有跟二一样的缺点。 方法四也没试,因为一看就知道很麻烦。 方法五把我导向了mask-image,想说能否用这个css属性在遮罩层上挖个洞出来。关于这方面的应用我觉得这篇文章写得挺好的:https://www.zhangxinxu.com/wordpress/2017/11/css-css3-mask-masks/。然而一一试过后,发现这个属性在小程序中好像用不了(也有可能是我哪里出错,反正就是没法生效)。 呵呵。。。以上几种方式折腾了一个晚上。 后来想想,干脆抛开幻想抛开依赖,自力更生。 不搜了,自己土办法画一个。 [图片] 我用四个view作为遮罩,框出了需要突出的元素。 <view wx:if="{{showGuide}}" class="mask" bindtap="getHidden"> <view class="mask_block" style="width:100%;top:0px;left:0px;height:{{showArea.top}}px"></view> //上 <view class="mask_block" style="width:100%;top:{{showArea.bottom}}px;left:0px;height:100%"></view> //下 <view class="mask_block" style="height:{{showArea.height}}px;top:{{showArea.top}}px;left:0px;width:{{showArea.left}}px"></view> //左 <view class="mask_block" style="height:{{showArea.height}}px;top:{{showArea.top}}px;left:{{showArea.right}}px;width:100%"></view> //右 </view> 上面的showArea就是第一步中取到的元素位置。 这样一来,遮罩中的元素突出就解决了。 3、还有一个坑 本来以为这一步搞定的时候,发现这个镂空的位置竟然还会漂移!在开发工具和真机中的位置不一样,就算同在真机中,这次展示和下次展示的位置也有可能不一样! 这TM是薛定谔的镂空框! [图片] 这就是当时的神奇漂移。 经验告诉我,会出现这种不确定性的—— 十之八九都和元素的加载是否完成有关系 请好好记住这句话,可以节省你至少一天的调试时间。 是的,坑就在于步骤一的 query.select("#gameInfo").boundingClientRect(); 什么时候取的很关键。在小程序布局完成前,你可能会取到一个偏差值,导致了后面的踩坑。 我之前是在组件生命周期(还不清楚组件生命周期的看这里)中attached环节执行,不行。发现手册中还有一个ready,感觉和page中的onReady是一个东西,然而还是不行。。。 看来还是必须等页面的onReady触发之后才能取到完全正确的值。 所以我必须在主页面的onReady事件发生后再来做这个事情。 那么如何确定onReady呢?相当于我必须能够在onReady事件中去调用用户引导组件的初始化函数。。。想来想去,他们两者之间能够互动的也只有setData了。 也就是数据监听器,不懂的点这里。 幸好按计划本来也是要传入配置数组的。于是在pages中的onReady传入配置数组,然后在组件中对配置数组进行监听。 observers: { 'config': function(config) { if (config) { this.initMask(config); } } } 如此一来,遮罩层跟元素的突出展示就算完成了。 当然,这样的遮罩层还是有些不足的,例如只支持方形镂空,如果是圆形元素看着就不那么美观。。。好在我目前没有这方面需求,以后有遇到再说吧。
2020-05-10 - 用户引导组件开发(1)理下思路
个人开发者一枚。。之前为了偷懒,做的小程序都没有好好地做用户引导,导致新用户上手的不多。 所以这次下决心好好地做一个用户引导。以下是最终效果(当然,我目前还没上新版本,所以你就算找到这个小程序也看不到这个效果): [图片] 初步设想 做之前,先说说我想做成怎样的。 1、要井水不犯河水。我的几个小程序功能都已经做全了,我不想再为了这个用户引导功能去大改里面的逻辑,包括页面啥的。所以用户引导的功能实现最好能独立模块,与原来的代码尽量低耦合。 2、要能复用。用户引导可能会在各个小程序的不同页面中出现,所以必须能够复用。 因为这两个原因,做成组件是再好不过的了。把各个用户引导界面所需要展示的元素用配置数组的方式传给组件,再由组件去呈现出来。 配置数组 初步设计中,配置数组应该包括如下内容: 1、突出元素。即在遮罩层中需要突出展示的页面元素,如上面示意图中的按钮。配置数组应传入元素ID。 2、说明文字,为了美观,说明文字最好用气泡框。配置数组要传入文字内容,可能要允许多段文字,然后可以配置相关的位置大小样式。 3、说明图片。本人美工不行,但是如果是专业的团队应该会想用更生动的说明图片来替代说明文字。如以下这种的: [图片] 配置数组必须传入图片路径,且应该允许多张图片,并且可以自定义图片的位置大小等。 相关事件 遮罩层的相关事件其实很简单,就是点击。一点就没了。所以我们必须把这个事件再反馈出来。 大致想法就是这些,我们开工吧。
2020-05-10 - 云开发进阶:云函数bug终极必胜技
不管你的云函数本身碰到什么问题,只要不是代码的错误,试试以下几步吧: 乱拳打起: 1、选择一个云环境 [图片] 2、同步云函数列表 [图片] 3、wx.cloud.callFunction({name:'xxx'})执行看看。记得一定要先:(1)wx.cloud.init();(2)基础库选最高版本。 如果有两个云环境,需要: [图片] wx.cloud.init({ env:'env1-hkkgy'//改成正确的云环境ID }) 跳到第6步。没问题就走第7步,有问题就走第4步。 4、还有问题?云控制台删除云函数 [图片] 5、新建一个云函数 [图片] 6、上传云函数 [图片] 7、重复第三步。 以上七步可打乱顺序,随便执行,真正的乱拳走起。 8、还是不行,让子弹飞一会儿,等几个小时,重复以上步骤。 基本目前为止,这趟乱拳100%成功搞定云函数我碰到的任何问题。(网络不通除外) 9、如果还是不行, 终极大招就是:节衰顺便 10、某些其他错误:某某插件找不到:在package.json里声明依赖关系: "dependencies": { "wx-server-sdk": "~2.0.2" } [图片]
2020-10-20 - 小程序webview组件,小程序和webview交互,小程序内联h5页面,小程序webview内网页实现微信支付
小程序支持webview以后,我们开发的好多h5页面,就可以直接在小程序里使用了,比如我们开发的微信商城,文章详情页,商品详情页,就可以开发一套,多处使用了。我们今天来讲一讲。在小程序的webview里实现微信支付功能。因为微信不允许在小程序的webview里直接调起微信支付。所以我们这节课就要涉及到小程序和webview的交互了。 老规矩先看效果。 因为这里涉及的东西比较多,录gif太多,没法上传,我就录制了一段视频出来。 https://v.qq.com/x/page/t0913iprnay.html 原理 先说下实现原理吧,实现原理就是我们在webview的h5页面里实现下单功能,然后点击支付按钮,我们点击支付按钮的时候会跳转到小程序页面,把订单号,订单总金额,传递到小程序里,然后小程序里使用订单号和订单金额去调起微信支付,实现付款,付款成功或者失败时都会有回调。我们再把对应的回调传递给webview,刷新webview里的订单和支付状态。 一,定义webview显示h5页面 关于webview的使用,我就不做讲解了,官方文档里写的很清楚,用起来也很简单。https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html [图片] webview很简单,就是用一个webview组件,显示我们的网页。 二,定义h5页面 我这里启动一个本地服务器,用来展示一个简单的h5页面。 [图片] 上图是我在浏览器里显示的效果。 接下来我们在小程序的webview里显示这个页面,也很简单,只需要把我们的src定义为我们的本地网页链接就可以了。 [图片] 这里有一点需要注意 因为我们是本地链接,我们需要到开发者工具里把这一项给勾选。 [图片] 三,来看下h5页面代码 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>小程序内嵌webview</title> <style> .btn { font-size: 70px; color: red; } </style> </head> <body> <h1>我是webview里的h5页面</h1> <a id="desc" class="btn" onclick="jumpPay()">点击支付</a> <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script> <script> console.log(location.href); let payOk = getQueryVariable("payOk"); console.log("payOk", payOk) if(payOk){//支付成功 document.getElementById('desc').innerText="支持成功" document.getElementById('desc').style.color="green" }else{ document.getElementById('desc').innerText="点击支付" } //获取url里携带的参数 function getQueryVariable(variable) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split("="); if (pair[0] == variable) { return pair[1]; } } return (false); } function jumpPay() { let orderId = Date.now();//这里用当前时间戳做订单号(后面使用你自己真实的订单号) let money = 1;//订单总金额(单位分) let payData = {orderId: orderId, money: money}; let payDataStr = JSON.stringify(payData);//因为要吧参数传递给小程序,所以这里需要转为字符串 const url = `../wePay/wePay?payDataStr=${payDataStr}`; wx.miniProgram.navigateTo({ url: url }); // console.log("点击了去支付", url) console.log("点击了去支付") } </script> </body> </html> [代码] h5代码这里不做具体讲解,只简单说下。我们就是在点击支付按钮时,用当前时间戳做为订单号(因为订单号要保证唯一),然后传一个订单金额(单位分),这里节约起见,就传1分钱吧,花的是自己的钱,心疼。。。。 关键点说一下 1, 必须引入jweixin,才可以实现h5跳转小程序。 <script type=“text/javascript” src=“https://res.wx.qq.com/open/js/jweixin-1.3.2.js”></script> 2,跳转到小程序页面的方法 [代码]const url = `../wePay/wePay?payDataStr=${payDataStr}`; wx.miniProgram.navigateTo({ url: url }); [代码] 这里要和你小程序的页面保持一致。payDataStr是我们携带的参数 [图片] 四,小程序支付页 来看下我们的小程序支付页 [图片] 小程序支付页功能很简单,就是来接收我们h5传过订单号和订单金额。然后去调起微信支付,实现支付。支付成功和支付失败都有对应的回调。 [图片] 支付我们这里实用的小程序云开发来实现的支付,核心代码只有10行。由于支付不是本节的重点,所以这里不做具体讲解。感兴趣的同学可以去看我写的文章和我录的视频 小程序支付文章:https://www.jianshu.com/p/2b391df055a9 小程序支付视频:https://edu.csdn.net/course/play/25701/310742 下面把小程序接收参数和支付的完整代码贴出来给大家 [代码]Page({ //h5传过来的参数 onLoad: function(options) { console.log("webview传过来的参数", options) //字符串转对象 let payData = JSON.parse(options.payDataStr) console.log("orderId", payData.orderId) let that = this; wx.cloud.callFunction({ name: "pay", data: { orderId: payData.orderId, money: payData.money }, success(res) { console.log("获取成功", res) that.goPay(res.result); }, fail(err) { console.log("获取失败", err) } }) }, //微信支付 goPay(payData) { wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, signType: 'MD5', paySign: payData.paySign, success(res) { console.log("支付成功", res) //你可以在这里支付成功以后,再跳会webview页,并把支付成功状态传回去 wx.navigateTo({ url: '../webview/webview?payOk=true', }) }, fail(res) { console.log("支付失败", res) } }) } }) [代码] 代码里注释很清楚,这里有一点,就是我们支付成功后,需要告诉h5我们支付成功了,通知h5去刷新订单或者支付状态。 到这里我们就完整的实现了小程序webview展示h5页面,并且做到了h5和小程序的交互,实现了小程序webview的支付功能。 是不是很简单呢。 源码地址 1,关注“编程小石头”公号,回复“webview”即可获取源码 2,也可以到我github下载源码 https://github.com/qiushi123/xiaochengxu_demos [图片]
2019-08-15 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - 2020-08-16
- wepy已经凉透了吗?腾讯不要自己的亲儿子了?
wepy已经凉透了吗?腾讯的亲儿子现在没有维护了?社区也不给个介绍友链什么的?想初始化项目都下不动了,网上查了一圈还是无能为力啊[图片] 找了很久找到办法解决: 至今原因未知 第一时间我就想到了墙,但是我用的代理全局init都失败 解决方法(三选一,最后一个方法一定可以): 1:如果你是用的移动的网络,请更换至电信或者联通的网络重试 2:使用全局代理(墙) 3:手动下载 wepy 的初始化 template wepy 小程序模版的下载地址是 第一步:https://raw.githubusercontent.com/wepyjs/wepy_templates/${branch}/zips/${templateName}.zip 那么对应的 [代码]standard[代码] 模板的下载地址是(假设你要用 2.0.x 的版本) https://raw.githubusercontent.com/wepyjs/wepy_templates/2.0.x/zips/standard.zip(我测试需要配合外网,还要全局的情况下才下载成功) 第二步:下载到本地后解压 第三部:wepy init 本地路径/standard 项目名 即和 wepy init standard 项目名 效果相同 [图片]
2020-05-01 - 熟练使用微信开发工具的代码块功能提升编码效率,降低误码率
1. 写在前面 自从上次介绍了宇宙第一强集成IDE的微信开发者工具安装插件等隐藏功能后,很多开发者社区哥们老铁感兴趣,一些哥们还私我问我是否支持代码块功能。答案是:支持!而且很完美的支持(此处必须有感恩,感恩微信团队每天的辛苦开发,作为同开发过小编辑器的人知道其中的苦与乐,同时呼吁各位猿少些喷喷喷的负能量,多些宣传和赞美的正能量!)。代码块是小编非常非常常用的一个功能,基本从04年开始建站和开发过程用过的所有IDE中都必备的功能。熟练使用微信开发工具的代码块功能提升编码效率,降低高频代码段的误写率。 2. 什么是代码块 可能有些刚接触开发的哥们不了解什么是「代码块」,这里我仅仅说说我个人对「代码块」功能的理解:代码块,英文名字:Code snippets,代码块的作用就是把比较长的一段代码在编码时只需要输入简写后的几个字母,比如把高频的代码console.log();简写成「clg」,再比如把小程序的页面.js里面的onLoad(),onShow等常用代码简写成「page_init」几个字母,最早提出这个概念和想法的人我也不知道是谁,总之得感谢他。如果说现在很多的编程语言有「语法糖」的说法,那么我个人觉得「代码块」可以称为「代码糖/编码糖」!熟练使用编码糖一定能让你尝到编码中的甜味。他的优点非常明显:极速,0误差输入高频代码段。个人觉得无论你现在处在编码的哪个级别都应该熟练使用Code snippets来提升工作效率。 3. 如何使用 3.1 打开开发者工具的编辑器扩展目录 [图片] 3.2 创建相关文件夹 返回上一级目录到User目下(里面有Workspaces文件夹),创建/进入snippets,此目录mac下完整路径应为:"~/Library/Application Support/微信开发者工具/【当前开发者工具特征码】/Default/Editor/User/snippets" [图片] 3.3 新建/编辑代码块json文件(如上图) [图片] 格式如上面,1,2,3是我比较常用的代码糖,生效后输入图1里面的clg回车就是console.log()(可以用这个来检测你代码糖功能是否生效);并且光标自动定位到()里,如下图: [图片][图片] 3.4 附带自用all.code-snippets 下面附上我常用的粗陋的代码糖块文件all.code-snippets(可以直接复制使用,你可以自行自己添加删除。里面有自己写的java框架常用的代码块,php常用和html常用以及nginx比较常用的,个人习惯不一样建议全部干掉重来,只需按里面的格式来写就可以了,格式如下: 字段名 意义 备注 prefix 简写后的字符串 必填 body 原字符串,可以用转义符 必填 description 备注 可控 [代码]{ // Example: // "Print to console": { // "scope": "javascript,typescript", // "prefix": "log", // "body": [ // "console.log('$0');", // "$2" // ], // "description": "Log output to console" // } //java begin "valueOf": { "prefix": "val", "body": "valueOf($0)", }, "parseInt": { "prefix": "par", "body": "Integer.parseInt($0);", }, "parseLong": { "prefix": "parl", "body": "Long.parseLong($0);", }, "equ": { "prefix": "equ", "body": "equals(\"$0\")", }, "isnull": { "prefix": "isnull", "body": "Tools.myIsNull($0)", }, "setattr": { "prefix": "setattr", "body": "request.setAttribute(\"$0\",$2)", }, "getattr": { "prefix": "getattr", "body": "request.getAttribute(\"$0\")", }, "getparam": { "prefix": "getparam", "body": "request.getParameter(\"$0\")", }, "getpost": { "prefix": "getpost", "body": "TtMap postUrl = Tools.getUrlParam();\r\nTtMap post = Tools.getpostmap(request, true);// 过滤参数,过滤mysql的注入,url参数注入\r\n" }, "tcf": { "prefix": "tcf", "body": "try{\r\n}catch(Exception e){\r\n\tTools.logError(e.getMessage());\r\n}finally{\r\n\t$0.closeConn();\r\n}", }, "777": { "prefix": "777", "body": "Runtime.getRuntime().exec(\"chmod 777 -R \" + strFileFullPath);" }, "go-1": { "prefix": "go-1", "body": "javascript:history.go(-1);" }, "dbctrl": { "prefix": "!dbctrl", "body": "DbCtrl dbCtrl = new DbCtrl(\"$0\");\r\ntry{\r\n\tTtList list = dbCtrl.lists(\"\", \"\");\r\n\tTtMap info = dbCtrl.info($2);\r\n}catch(Exception e){\r\n\tTools.logError(e.getMessage());\r\n\tif (Config.DEBUGMODE) {\r\n\t\te.printStackTrace();\r\n\t}\r\n}finally{\r\n\tdbCtrl.closeConn();\r\n}", "description": "TT-DbCtrl" }, "dbtools": { "prefix": "dbtools", "body": "DbTools dbTools = new DbTools();\r\ntry{\r\n\tTtList list = dbTools.reclist(\"$0\");\r\n\tTtMap info = dbTools.recinfo(\"$2\");\r\n}catch(Exception e){\r\n\tTools.logError(e.getMessage());\r\n}finally{\r\n\tdbTools.closeConn();\r\n}" }, "!recinfo": { "prefix": "rrecinfo", "body": "Tools.recinfo(\"$0\")", "description": "TT-DbTools-recinfo" }, "!reclist": { "prefix": "rreclist", "body": "Tools.reclist(\"$0\")", "description": "TT-DbTools-reclist" }, "ss": { "prefix": "ss", "body": "String[] sS = new String[]{}; ", "description": "new String[]" }, "mss": { "prefix": "mss", "body": "TtMap info = new HashMap<>();", "description": "new Map<String,String>" }, "lss": { "prefix": "lss", "body": "TtList lmss = new ArrayList<>();", "description": "new TtList " }, "<%": { "prefix": "!s", "body": "<%\r\n\t$0\r\n%>", "description": "TT-JSP<%自动完成" }, "<%=": { "prefix": "!ss", "body": "<%=$0%>", "description": "TT-JSP<%=自动完成" }, "jsp_include": { "prefix": "!sss", "body": "<jsp:include page=\"<%=$0%>\">\r\n\t<jsp:param name=\"$3\" value=\"$2\"/>\r\n</jsp:include>" }, "jsp_include2": { "prefix": "<jsp", "body": "<jsp:include page=\"<%=$0%>\">\r\n\t<jsp:param name=\"$3\" value=\"$2\"/>\r\n</jsp:include>" }, "!jsp_requery": { "prefix": "!req", "body": "request.getParameter(\"$0\")", }, "!jsp_requery_getAttribute": { "prefix": "!req2", "body": "request.getAttribute(\"$0\")", }, "!": { "prefix": "!ssss", "body": "<%@page import=\"com.tt.tool\"%>\r\n<%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\"%>\r\n<%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%>\r\n<%@ taglib prefix=\"fmt\" uri=\"http://java.sun.com/jsp/jstl/fmt\"%>\r\n<%@ taglib uri=\"http://java.sun.com/jsp/jstl/functions\" prefix=\"fn\"%>\r\n<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\">\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\r\n <title>$0</title>\r\n</head>\r\n<body>\r\n\t$2\r\n</body>\r\n</html>", "description": "jsp-header" }, "div": { "prefix": "<div", "body": "<div id=\"$0\" name=\"$0\">\r\n\t\r\n</div>", }, "p": { "prefix": "<p", "body": "<p id=\"\" name=\"\"></p>", }, "a": { "prefix": "<a", "body": "<a href=\"$0\" id=\"\" name=\"\">$2</a>", }, "head": { "prefix": "<head", "body": "<head>\r\n\t$0\r\n</head>", }, "img": { "prefix": "<img", "body": "<img id=\"\" name=\"\" src=\"$0\">" }, "ul": { "prefix": "<ul", "body": "<ul id=\"\" name=\"\">\r\n\t\r\n</ul>" }, "li": { "prefix": "<li", "body": "<li id=\"\" name=\"\">\r\n\t\r\n</li>" }, "select": { "prefix": "<select", "body": "<select id=\"\" name=\"\">\r\n\t\r\n</select>" }, "option": { "prefix": "<option>", "body": "<option value=\"\">$0</option>" }, "input": { "prefix": "<input", "body": "<input type=\"$0\" id=\"$2\" name=\"$2\" value=\"$3\">" }, "form": { "prefix": "<form", "body": "<form id=\"info_form\" action=\"$${posturl}\" class=\"form-horizontal\" method=\"post\" enctype=\"multipart/form-data\">\r\n\t$2\r\n</form>" }, "!i": { "prefix": "!i", "body": "<%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\"%>\r\n<%@ page import=\"com.tt.tool.Tools\" %>\r\n<%@ page import=\"com.tt.data.TtMap\" %>\r\n<%@ page import=\"com.tt.data.TtList\" %>\r\n<%@ page import=\"com.tt.tool.JspTools\" %>\r\n<%@ page import=\"java.util.*\" %>\r\n<%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%>\r\n<%@ taglib prefix=\"fmt\" uri=\"http://java.sun.com/jsp/jstl/fmt\"%>\r\n<%@ taglib uri=\"http://java.sun.com/jsp/jstl/functions\" prefix=\"fn\"%>\r\n<%@ taglib prefix=\"Tools\" uri=\"/tld/manager\" %>\r\n", }, "loginfo": { "prefix": "loginfo", "body": "Tools.logInfo($0)", "description": "loginfo" }, "logerror": { "prefix": "logerror", "body": "Tools.logError($0)", "description": "loginfo" }, //javascript "!go-1": { "prefix": "!go-1", "body": "javascript:history.go(-1);", }, "!tag": { "prefix": "!tag", "body": "<%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%>\r\n<%@ taglib prefix=\"fmt\" uri=\"http://java.sun.com/jsp/jstl/fmt\"%>\r\n<%@ taglib uri=\"http://java.sun.com/jsp/jstl/functions\" prefix=\"fn\"%>\r\n<%@ taglib prefix=\"Tools\" uri=\"/tld/manager\" %>", }, "$$": { "prefix": "$$", "body": "$(function(){$0\r\n});", "description": "TT-jquery自动运行" }, "$(": { "prefix": "$f", "body": [ "$(function(){", "});" ], "description": "TT-jquery自动运行" }, "timer": { "prefix": "timer", "body": "inv = setInterval(\"$0showtimes();\",1000);", "description": "TT-JS每隔几秒自动运行", }, "!posturl": { "prefix": "!posturl", "body": "<%=Tools.urlKill(\"id$0\")%>", }, "!ifdebug": { "prefix": "ifd", "body": [ "if (Config.DEBUGMODE) {", "\te.printStackTrace();", "}" ], }, "reload":{ "prefix": "reload", "body": "location.reload(true);" }, //PHP "<?php": { "prefix": "php", "body": "<?php $0 ?>", "description": "Global-PHP插入标签" }, "nav":{ "prefix": "nav", "body": "<navigator url=\"car_yy/car_yy_ppuser/car_yy_ppuser\">", "description": "老子的微信小程序的跳转", }, "bind":{ "prefix": "bind", "body": "bindtap=\"$1\"", "description": "老子的bindtap" }, "wx_ra":{ "prefix": "wepy_ra", "body":"let result = await util.wxRequest(httpSet);", "description": "微信小程序阻塞等待请求" }, "wx_r":{ "prefix": "wepy_r", "body":["util.wxRequest(httpSet).then(function(result){\r\n\tif(result.result==\"success\"){$1\r\n\t}\r\n});"], "description": "微信小程序阻塞等待请求" }, "httpset":{ "prefix": "wepy_hs", "body": "let params= {};\r\nlet httpSet={\r\n\turl:'$1',\r\n\tparams:params,\r\n\tmethod:'GET',\r\n\tisShowLoading: false,\r\n}", "description": "httpSet" }, "wepy_global":{ "prefix": "wepy_global", "body": "wepy.$$instance.globalData.$1", }, "wepy_castfail":{ "prefix": "wepy_castfail", "body": "this.$$broadcast('alertHeaderWarning', '$1');" }, "wepy_castOK":{ "prefix": "wepy_castok", "body": "this.$$broadcast('alertHeader', '$1');" }, "wepy_castfail_height":{ "prefix": "wepy_castfailh", "body": "this.$$broadcast('alertHeaderWarningHeight', '$1',60);" }, "wepy_castOK_height":{ "prefix": "wepy_castokh", "body": "this.$$broadcast('alertHeaderHeight', '$1',60);" }, "clg": { "prefix": "clg", "body": "console.log($0);" }, "wepy_tt":{ "prefix": "wepy_tt", "body": "let that = this;" }, "wepy_currtarget":{ "prefix": "wepy_currt", "body": " e.currentTarget.dataset.$1", }, "wepy_gourl":{ "prefix":"wepy_gourl", "body":"@tap=\"goUrl\" data-url=\"$1\" data-memberflag=\"$2\"", }, "wepy_goback":{ "prefix": "wepy_goback", "body":"wepy.navigateBack();" }, "wepy_urlencode":{ "prefix": "wepy_urlencode", "body": "encodeURIComponent($1);" }, "wepy_urldecode":{ "prefix": "wepy_urldecode", "body": "decodeURIComponent($1);" }, "wepy_noimg":{ "prefix": "wepy_noimg", "body": "/images/imgload.png", }, "wepy_errmsg":{ "prefix": "wepy_errmsg", "body": "(result && result.errorMsg) ? result.errorMsg :\"删除失败!\"", }, "wepy_sets":{ "prefix": "wepy_sets", "body": "wepy.setStorageSync($1,$2);" }, "wepy_gets":{ "prefix": "wepy_gets", "body": "wepy.getStorageSync($1);" } } [代码] 4. 相关链接 持续更新:收藏整理官方隐藏的小程序功能/参数/方法/API 动手打造更强更好用的微信开发者工具-编辑器扩展篇 点击wxml里面的绑定事件/变量名/样式名直接跳转到对应文件代码 小程序编码时变量名中文转英文变量名工具,各种驼峰取名 微信开发者工具编辑器支持「书签」功能,快速跳转到指定文件指定行和列 愉快的编写和调试Java:体验新版开发者工具的编辑器扩展功能 5. 其他未公布的隐藏功能 当你觉得有用的时候,就点赞和收藏或者分享,觉得没用的话就投诉,不管点赞还是投诉后的码农写代码永无BUG ,CP设计的产品人见人爱,BOSS每年收入翻番! ↓↓↓↓↓↓__________________________________________________________________________投诉的话点…↓↓↓
2020-05-17 - 向官方道歉。接口security.imgSecCheck有问题?我已经解决。并附上最全代码,保证能用
首先,发布的帖子,关于“security.imgSecCheck 这个图片内容审核API 有问题”的帖子,我已经解决。 之前说接口不稳定,错怪了官方! 向官方道歉, 向官方道歉, 向官方道歉!!! 以下分享经验【3个重点】: 1.(重点)控制图片尺寸:图片尺寸不超过 750px x 1334px 2.(重点)检测的图片,一定要压缩后再上传。(图片大小限制:1M) 自己检测过。500k-1MB还是大了。建议在50kb 左右。 3.(重点)通过获取文件信息,图片以ArrayBuffer格式上传云函数进行检测。 wx.getFileSystemManager().readFileSync(图片临时文件) //文件二进制内容 ArrayBuffer 以上条件缺一不可 根据返回结果,执行你需要的代码 复制以下代码。直接可以使用。 ------------------------------------------------------------------------------------------------------------------------ 【xxx.html代码】 <canvas canvas-id='imageBox' class="imageBox" style="border:#000 5px solid; width:{{imageBoxMake_Width}}px; left:{{screenWidth * 1.2}}px;"></canvas> 【xxx.wxss代码】 /* 设定页面决对定位 */ page{ position: relative; } /* 设定画布相对定位 */ .imageBox{ position: absolute; z-index: 0; } 【xxx.js代码】 //图片内容安全-画布 imageBoxMake_Width: '', imageBoxMake_height: '', /**生 命周期函数--监听页面加载 */ onLoad: function(options) { console.log('进入introduction页面') this.setData({ //画布布局。让画布在屏幕之外 screenWidth: app.globalData.screenWidth, //画布距离屏幕左侧宽度 = 屏幕宽度 }) }, //【选择图片】方法 chooseImage() { var that = this //使手机发生较短时间的振动 wx.vibrateShort() wx.showActionSheet({ itemList: ['拍照', '相册',], success(res) { console.log(res.tapIndex) //拍照vounDemo if (res.tapIndex == 0) { wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], //['original', 'compressed'] sourceType: ['camera'], success(res) { // 图片临时地址 const imgFileURL = res.tempFilePaths[0] console.log('打印取到的图片') console.log(res) //canvas绘制并压缩图片,然后图片内容安全检测 that.imageBoxMake(imgFileURL) } }) } //手机相册 vounShow else if (res.tapIndex == 1) { wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album'], success(res) { // 图片临时地址 const imgFileURL = res.tempFilePaths[0] console.log('打印取到的图片') console.log(res) //canvas绘制并压缩图片,然后图片内容安全检测 that.imageBoxMake(imgFileURL) } }) } }, fail(res) { console.log(res.errMsg) } }) }, //绘制【内容安全图片图片】canvas imageBoxMake: function (imgFileURL) { wx.showLoading({ title: '正在压缩图片', //正在内容安全检测 }) console.log('开始imageBoxMake方法') var that = this var saveSize = 100 var canvasId = 'imageBox' //画布ID //创建画布 const ctx = wx.createCanvasContext(canvasId) ctx.save() //获得图片信息 wx.getImageInfo({ src: imgFileURL, //获得画芯图片 success(res) { //图片比例 var perWHcanvas = res.height / res.width that.setData({ //画布尺寸,处理为(100px)像素宽度。 imageBoxMake_Width: saveSize, imageBoxMake_height: saveSize * perWHcanvas, //高度等比 }) //绘制图 ctx.drawImage(res.path, 0, 0, res.width, res.height, 0, 0, saveSize, saveSize * perWHcanvas) ctx.restore() //绘制保存 ctx.draw(true) console.log('绘制【内容安全图片图片】canvas完成') setTimeout(function () { that.imageBoxIMG(saveSize, perWHcanvas, canvasId, imgFileURL) //压缩图片尺寸,并上传 }, 2000) } }) }, //压缩图片尺寸,并上传 imageBoxIMG: function (saveSize, perWHcanvas, canvasId, imgFileURL) { console.log('开始 imageBoxIMG 方法') var that = this wx.canvasToTempFilePath({ x: 0, y: 0, width: saveSize, height: saveSize * perWHcanvas, destWidth: saveSize, destHeight: saveSize * perWHcanvas, canvasId: canvasId, //这是canvasId fileType: 'jpg', //目标文件的类型 quality: 0.8, //图片的质量 success(res) { console.log('保存的图片临时路径' + res.tempFilePath) var canvasImage = res.tempFilePath wx.hideLoading() //隐藏 loading 提示框 const imageArrayBuffer = wx.getFileSystemManager().readFileSync(canvasImage) //文件二进制内容 ArrayBuffer that.imgSecCheck(imgFileURL, imageArrayBuffer) } }) }, //图片智能鉴黄 imgSecCheck: function (imgFileURL, imageArrayBuffer) { console.log('开始imgSecCheck方法') wx.showLoading({ title: '图片安全检测中', //正在内容安全检测 }) //初始化云开发及设置其环境 wx.cloud.init({ env: app.globalData.env, //注意:我这里是调用app.js里设置好的云环境。你们可以改为自己的云环境 traceUser: true }) wx.cloud.callFunction({ // 要调用的云函数名称 name: 'imgSecCheck', // 传递给云函数的event参数 data: { imageArrayBuffer: imageArrayBuffer } }).then(res => { console.log('打印云函数imgSecCheck返回结果为') console.log(res) wx.hideLoading()//隐藏 loading 提示框 if (res.result.errMsg == "openapi.security.imgSecCheck:ok") { //内容正常。这里可以执行你需要的【方法】代码 } else{ //内容检测结果为不安全 this.showModal() } }).catch(err => { //错误 console.log('打印err结果为 错误') console.log(err) wx.hideLoading() //隐藏 loading 提示框 //内容检测结果为不安全 this.showModal() }) }, //内容检测结果为不安提示 showModal: function () { wx.showModal({ title: '图片内容违规', content: '通过腾讯图片智能鉴黄检测到你发布的内容,可能包含涉黄、涉暴、涉政等有害信息。为营造安全绿色的平台,我们坚决拒绝上传危害内容、言论。若你多次上传危害内容,系统将自动封号哦,并同步到网络安全部', confirmText: '知道了', confirmColor: '#000000', showCancel: false, success(res) { if (res.confirm) { console.log('用户点击知道了') } else if (res.cancel) { console.log('用户点击取消') } } }) }, 【云函数代码】 // 云函数入口文件 imgSecCheckPro const cloud = require('wx-server-sdk') // 云函数入口函数 exports.main = async (event, context) => { //初始化云函数 cloud.init({ env: event.env }) try { return await cloud.openapi.security.imgSecCheck({ media: { contentType: 'image/png', value: Buffer.from(event.imageArrayBuffer) } }) } catch (err) { // 错误处理 // err.errCode !== 0 } } ------------------------------------------------------------------------------------------ 复制以上代码。直接可以使用。 希望对各位开发者有帮助。加油!
2020-05-01 - APP拉起小程序的新规则,不再需要开放平台关联小程序,也不做限制
最近几天,几个朋友的APP更新时在开放平台找不到关联小程序的入口了,然后在社区也有一些人私我问找不到开放平台关联小程序的口了,今天整理下统一回复: 新的跳转规则 对于已通过认证的开放平台账号,其移动应用可以跳转至任何合法的小程序,且不限制跳转的小程序数量。 对于未通过认证的开放平台账号,其移动应用仅可以跳转至同一开放平台账号下小程序。 注意:若移动应用未上架,则最多只能跳转小程序100次/天,用于满足调试需求。 Android示例 微信开放文档 https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Launching_a_Mini_Program/Android_Development_example.html Ios示例 微信开放文档 https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Launching_a_Mini_Program/iOS_Development_example.html 官方链接 功能介绍 | 微信开放文档 https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Launching_a_Mini_Program/Launching_a_Mini_Program.html 截图 [图片] [图片]
2020-05-10 - 小程序直播从开通到开播全过程——开发篇
本文因为社区编辑器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 - 借助CSS代码实现几个简单的渐变色的Demo
话不多说先上图: [图片] wxml页面代码: <view class="color"> <view class="linear-gradient color1">1</view> <view class="linear-gradient color2">2</view> <view class="linear-gradient color3">3</view> <view class="linear-gradient color4">4</view> <view class="linear-gradient color5">5</view> <view class="linear-gradient color6">6</view> <view class="linear-gradient color7">7</view> <view class="linear-gradient color8">8</view> <view class="linear-gradient color9">9</view> <view class="linear-gradient color10">10</view> </view> wxss模块代码: .color{ display: flex; flex-wrap: wrap; justify-content: space-around; padding: 5rpx; color: white; text-align: center; font-weight: bold; } .linear-gradient{ height: 200rpx; } .color1{ width: 48%; background-image: linear-gradient(45deg, #29bdd9 0%, #276ace 100%); } .color2{ width: 48%; background-image: linear-gradient(45deg, #ff9569 0%, #e92758 100%); } .color3{ margin-top: 10rpx; width: 48%; background-image: linear-gradient(45deg, #FF416C 0%, #FF4B2B 100%); } .color4{ margin-top: 10rpx; width: 48%; background-image: linear-gradient(45deg, #8A2387 0%, #E94057 50%, #F27121 100%); } .color5{ margin-top: 10rpx; width: 48%; background-image: linear-gradient(45deg, #ec008c 0%, #fc6767 100%); } .color6{ margin-top: 10rpx; width: 48%; background-image: linear-gradient(45deg, #2980B9 0%, #6DD5FA 50%, #FFFFFF 100%); } .color7{ margin-top: 10rpx; width: 48%; background-image: linear-gradient(45deg, #f12711 0%, #f5af19 100%); } .color8{ margin-top: 10rpx; width: 48%; background-image: linear-gradient(45deg, #C6FFDD 0%, #FBD786 50%, #f7797d 100%); } .color9{ margin-top: 10rpx; width: 48%; background-image: linear-gradient(45deg, #ee9ca7 0%, #ffdde1 100%); } .color10{ margin-top: 10rpx; width: 48%; background-image: linear-gradient(45deg, #12c2e9 0%, #c471ed 50%, #f7797d 100%); }
2020-04-26 - 云开发小程序如何导入题库数据系列二
云开发小程序数据批量导入工具 我将这篇文章编排到我的系列里面了,暂时作为该系列的第一篇文章 云开发小程序如何导入题库数据系列二 今天是周五,我花了两个小时把之前的一个工具完善了下。 该工具为云开发小程序导入数据专用,具体实现的功能有 1、上传excel 2、生成能满足云开发的JSON 目前已经生成了JSON文件,需要用户另存为,并通过云开发控制台导入。 先看看界面吧 1 [图片] 2 [图片] 3 [图片] 4 [图片] 5 [图片] 注意事项 1、excel的第一行是表头, 2、为编码输出内容乱码建议采用谷歌浏览器 备注: 文件格式不固定,可以任意表头的文件,但是大家不清楚可以参考下下面链接的文档 https://developers.weixin.qq.com/community/develop/article/doc/000866b4a28e58a1172aa6b0451413 如果有好的想法或者建议可以跟我提 工具链接 https://www.xiaomutong.com.cn/index20200501.html
2020-04-30 - 微信小程序查看原生组件的代码结构
大家在开发过程中大多数有遇到过想修改微信小程序的原生组件样式的时候吧 比如修改复选框的样式如下: [图片] 这几个 css 类官方明明没有提供,在网上百分之九十九的文章都是直接告诉你这么做,但是没人告诉你为啥这么做 有咩有和我一样第一次看到一脸懵逼的举个手😄 本着不抛弃不放弃的思想,终于在我不断探索下(其实是胡乱一蒙)找到了方法 进入复选框官方文档[图片] 2. 鼠标右键 审查示例代码(这里是个iframe) [图片] 3. 审查示例代码里的的复选框 [图片] 嘻嘻😁就能看到代码结构了 然后通过类名还能找到样式 完成!!!!
2020-03-30 - scroll-view向上滚动时,超出的不隐藏,该如何设置呢?
我想实现某小程序详情页面的效果:内容可以整体上滑遮住封面,下滑回到原位,下拉一下,封面有个缩放效果。 [图片] 我看到scroll-view有整体内容上滑,又有监听下拉bindrefresherrefresh,于是想选择使用scroll-view,但是没看到上滑超出的内容 不隐藏的设置选项。 请问这个功能,选用scroll-view来实现可以吗?
2020-03-26 - 微信小程序踩坑*2
注意,安卓手机很有可能只有两种字体粗细,我拿我几个同事的手机测试过了,发现安卓手机似乎只有两种字体粗细(可能数字不一样),而苹果较为细致,有多种字体粗细程度[图片][图片][图片][图片] 第一张是ios,其他都是安卓,除了最后一个安卓数字不一样,其他都是两种粗细,最后一行打错字了,应该是lighter
2020-03-26 - 微信小程序答题页——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 - 云开发批量上传图片,上传完图片再上传数据库 [即抄即用,拎包入住]
大家好,又是我拎包哥,今天我们来实现在云开发中批量上传图片。 经过Stephen哥的指正,我改用了Promise.all的方法来达到目的。 Promise.all的作用就是等待所包含的promise函数结束后再执行下一步逻辑,非常方便好用!const db = wx.cloud.database() const test = db.collection('test') Page({ onLoad() { this.imgList = [] wx.chooseImage({ success: (res) => { this.TFP = res.tempFilePaths } }) }, btn() { let promiseMethod = new Array(this.TFP.length) for (let i = 0; i < this.TFP.length; i++) { promiseMethod[i] = wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } Promise.all([...promiseMethod]).then(() => { test.add({ data: { imgList: this.imgList } }) }) } }) --------------------------------------我是分割线-------------------------------------- async await 要点: ctrl c + ctrl v这里用了await阻塞在wx.cloud.uploadFile前面,避免还没上传完图片就往数据库插入数组。减少了then里的代码,美观逼格高。嘻嘻嘻。await wx.cloud.uploadFile不能放在wx.chooseImage里,如果可以的话,请告诉我怎么做,谢谢!欢迎交流,指出错误,我立刻修改么么哒。 标准版 const db = wx.cloud.database() const test = db.collection('test') Page({ onLoad() { this.imgList = [] wx.chooseImage({ success: (res) => { this.TFP = res.tempFilePaths } }) }, async btn() { this.imgList = [] console.log(this.TFP) for (let i = 0; i < this.TFP.length; i++) { await wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } test.add({ data: { imgList: this.imgList } }) } }) 新手最爱一锅炖版(不推荐) 为什么不推荐呢,因为选择图片并不意味着要上传图片,用户还没进行最终的确定操作(不过可以用来了解async await)。 onLoad() { this.imgList = [] wx.chooseImage({ success: async res => { this.TFP = res.tempFilePaths for (let i = 0; i < this.TFP.length; i++) { await wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } test.add({ data: { imgList: this.imgList } }) } }) } [图片] ==========================end==========================
2020-05-17 - “小程序直播”接入指引
各位微信开发者: “小程序直播” 功能正在公测中,具体接入指引请参考《小程序直播产品介绍及操作指引》。 一、功能简介 小程序直播是微信官方在2020年2月公测推出的产品能力,帮助商家在自有小程序中实现直播互动与商品销售的闭环。 [图片] 二、商家准入要求 满足以下条件,即可开通小程序直播: ①属于小程序直播开放类目,具体见《微信小程序直播功能准入要求》 ②主体下小程序近半年没有严重违规; ③小程序近90天内,有过支付行为; 三、具体产品功能及操作指引 具体接入指引请参考《小程序直播产品介绍及操作指引》,以下为商家操作步骤: 1. 开通权限 1) 登录“小程序后台”(mp.weixin.qq.com),在左侧导航栏找到“小程序—功能—直播”,点击开通。 小程序直播需要基于小程序,如若开发者还未创建小程序,可按照《小程序接入指南》流程指引创建小程序并完成开发。 2) 符合上述开放范围的即可开通。 2.功能开发 小程序直播需要实现【直播组件】与【后台配置】两个部分,其中组件部分需要在小程序中进行配置开发。 具体开发文档,请参考《小程序直播开发文档》。 2.直播间配置 开发完成后,商家可通过小程序后台设置直播计划、开通、设置抽奖等操作。具体操作指引,请参考《小程序直播产品介绍及操作指引》。
2020-08-26 - 小程序 swiper的高度问题,如何用样式自适应?
小程序的swiper默认高度是150,可是我们在实际项目中很多时候是很难定这个高度的。而最好的办法就是根据内容的多少而展开。 我百度了下,很多人都是动态去改变这个高度的,这里就有个麻烦的地方,你需要通过js文件去计算,再通过变量去赋值。难到样式真的做不了么? 根据我上一篇列表里的图片可以自适应内容的高度,我觉得swiper也是可以的!!!于是做了个实验,请看以下的图 [图片] 难道做的不行么??卡住了..... 不应该啊 最后我一气之下,把height全部注释掉了!!!这时奇迹就出现了,可以了~~~~ 哈哈,可是这里又有一个问题,weiper是系统带有的样式,是不能删的。所以这时候,你只需要把高度改成 height: initial; [图片] 有这个需要的,可以自己手动试试,而以上的界面,等我做好,也会在我的小程序里展示,有兴趣的,可以留意我的小程序更新 [图片]
2020-03-03 - 「笔记」Web页面使用vConsole
前言 这几天在突然想搞(xian)点(de)东(dan)西(teng),于是折腾了下公众号相关的接口,然而发现某个api在模拟器上正常,手机上使用微信浏览器无法始终无法执行,但是H5页面又不像小程序一样可以远程调试,又看不到打印日志,于是想起来了社区大佬之前发过的vConsole。 vConsole 是什么? 或许很多人会觉得很陌生,但是开发过小程序的人来说看到下面这个就很熟悉了。 [图片] 小程序端和web端的区别? 小程序端: [图片] Web端: [图片] 从上面2个截图可以看出基本上功能差不多,用法上也没什么区别,Web端可以查看Network和Storage的信息,至于好不好用,自己试试就知道。 Vue项目使用vConsole [代码]npm install vconsole [代码] 在入口文件引入模块 [代码]import VConsole from 'vconsole' let vConsole = new VConsole() Vue.prototype.$vConsole = vConsole; [代码] 扩展功能:隐藏Network面板 因为Network信息有时候比较敏感,所以小程序端至今没有开放这个面板,但是理论上来说应该都是有的,可能是官方特意隐藏了,我们在H5页面上也可能会有这种需求,默认的话是都显示出来的,我们可以针对线上环境单独设置隐藏。 [代码]vConsole.removePlugin("network") [代码] 仓库地址 https://github.com/Tencent/vConsole
2020-03-20 - 教你解决showLoading 和 showToast显示异常的问题
问题描述 当wx.showLoading 和 wx.showToast 混合使用时,showLoading和showToast会相互覆盖对方,调用hideLoading时也会将toast内容进行隐藏。 触发场景 当我们给一个网络请求增加Loading态时,如果同时存在多个请求(A和B),如果A请求失败需要将错误信息以Toast形式展示,B请求完成后又调用了wx.hideLoading来结束Loading态,此时Toast也会立即消失,不符合展示一段时间后再隐藏的预期。 解决思路 这个问题的出现,其实是因为小程序将Toast和Loading放到同一层渲染引起的,而且缺乏一个优先级判断,也没有提供Toast、Loading是否正在显示的接口供业务侧判断。所以实现的方案是我们自己实现这套逻辑,可以使用Object.defineProperty方法重新定义原生API,业务使用方式不需要任何修改。 代码参考 [代码]// 注意此代码应该在调用原生api之前执行 let isShowLoading = false; let isShowToast = false; const { showLoading, hideLoading, showToast, hideToast } = wx; Object.defineProperty(wx, 'showLoading', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowToast) { // Toast优先级更高 return; } isShowLoading = true; console.log('--------showLoading--------') return showLoading.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'hideLoading', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowToast) { // Toast优先级更高 return; } isShowLoading = false; console.log('--------hideLoading--------') return hideLoading.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'showToast', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowLoading) { // Toast优先级更高 wx.hideLoading(); } isShowToast = true; console.error('--------showToast--------') return showToast.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'hideToast', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { isShowToast = false; console.error('--------hideToast--------') return hideToast.apply(this, param); // 原样移交函数参数和this } }); [代码] 调整后展示逻辑为: 优先级:Toast>Loading,如果Toast正在显示,调用showLoading、hideLoading将无效 调用showToast时,如果Loading正在显示,则先调用 wx.hideLoading 隐藏Loading
2019-10-30 - 腾讯云Linux服务器安装Mysql8并实现远程访问
上一节已经给大家讲解了我们java项目,也就是微信小程序后台项目部署到腾讯云服务器,但是呢,我们服务器肯定要装mysql数据库吧,要不然我们的数据放哪里呢,所以这一节来教大家如何在linux服务器里安装mysql数据库,并做一些数据库常见的配置。 传送门 《java项目部署到linux服务器,微信小程序后台部署到服务器》:https://juejin.im/post/5d6b206bf265da03ae788d01 一,首先还是登录到我们的服务器 服务器如何登录我在上一节已经讲过了,大家只需要去看我上一节课程即可。 [图片] 然后通过下面命令行,检测服务器上是否安装过mysql [代码]rpm -qa|grep mysql [代码] 如果安装过,可以通过下面命令卸载删除 [代码]rpm -e --nodeps mysql-libs [代码] 二,下载并安装mysql 1,检查服务器是否已经安装过mysql [代码]yum list installed mysql* [代码] 出现下图所示,代表没有安装过 [图片] 2,安装mysql源 [代码]sudo wget https://repo.mysql.com//mysql80-community-release-el7-1.noarch.rpm [代码] 3,下载mysql rpm源 [代码]wget http://repo.mysql.com/mysql57-community-release-el7-8.noarch.rpm [代码] [图片] 4安装下载好的rpm包 [代码]sudo rpm -ivh mysql80-community-release-el7-1.noarch.rpm [代码] [图片] 5安装mysql,发现提示,y到底 [代码]sudo yum install mysql-server [代码] [图片] [图片] 凡是让输入的地方,都输入y [图片] [图片] [图片] [图片] 有的地区服务器安装mysql比较慢,只能耐心等待了 [图片] 出现下面标志,代表安装成功 [图片] 6,查看下mysql的版本,确定是否安装成功 [代码]mysql -V [代码] [图片] 可以看出我们安装的mysql版本是5.7.27 7,运行mysql [代码]service mysqld start [代码] [图片] 查看是否启动 [代码]service mysqld status [代码] 出现下面箭头所示,代表已经启动mysql [图片] 8,取得mysql初始化随机密码 [代码]grep "password" /var/log/mysqld.log [代码] [图片] 可以看到我们的初始密码是 l>KbWhk6K&+Y 9,登录mysql [代码]mysql -uroot -p'l>KbWhk6K&+Y' [代码] 记得把l>KbWhk6K&+Y换成你自己的密码。下面代表登录成功。 [图片] 10,更改root密码 [代码]alter user user() identified by '你的新密码'; [代码] (“需要带数字,大写字母,小写字母,特殊符号”)如我设置密码为 Qc123456! [图片] 设置密码永不过期 [代码]ALTER USER 'root'@'localhost' PASSWORD EXPIRE NEVER; [代码] [图片] 一定要记得执行下面代码刷新MySQL的系统权限相关表 [代码]flush privileges; [代码] [图片] 三,设置mysql可以远程访问 默认情况下,我们的mysql只可以在服务器的本地访问,远程是没法访问的,今天就来教大家如何设置mysql的远程访问。 1,同样还是先登陆mysql,这时登陆记得用我们新设置的密码。 如我们的mysql -uroot -p’Qc123456!’ 这里的Qc123456!就是我的新密码 [图片] 2,登陆成功后用 show databases; 来显示所有的数据库 [图片] 3,use mysql; 来更改管理员信息 [图片] 4,select user,host from user; 查询所有的管理员 [图片] 5, update user set host = ‘%’ where user = ‘root’; 更新root用户的本地访问为% ,即代表可以远程访问。通常情况下我们的root用户应该只能本地访问,但是我们今天是为了学习方便,所以就设置root可以远程访问了。 [图片] 6,设置完,一定要记得 flush privileges;刷新下权限。 [图片] 7,再来看user表,root后面的信息就改变了。 [图片] 四,添加mysql数据库的子管理员 我们上面直接用root来操作数据库,有些危险,所以我们再来教大家添加一个子管理员。 [图片] 如上图所示: 1,添加用户名为xiaoshitou,密码为Xiaoshitou123!的子用户 [代码]create user 'xiaoshitou'@'%' identified with mysql_native_password by 'Xiaoshitou123!'; [代码] 2,设置xiaoshitou这个用户可供远程访问 [代码]grant all privileges on *.* to 'xiaoshitou'@'%' with grant option; [代码] 3,刷新权限 [代码]flush privileges; [代码] 可以看到我们的xiaoshitou用户的host也变成了 % [图片] 4,可以看到我们的xiaoshitou子用户也可以供远程访问了(远程访问前,要记得设置下服务器的安全组,下面第五步有讲) [图片] 后面的生产环境里我们也可以给这个xiaoshitou用户设置一些权限,比如只可以增加和修改mysql数据库,不可以删除数据。。。。 五,设置完以后不要忘记设置你服务器的安全组 出站和入站规则都要设置3306端口 [图片] [图片] 六,idea远程访问服务数据库。 1,进入mysql链接 [图片] 2,输入信息链接服务器mysql数据库 [图片] 3,查看链接效果 [图片] 到这里我们就完整的在linux服务器里安装好mysql了,并且可以通过远程访问到。 视频讲解 https://study.163.com/course/courseMain.htm?courseId=1209428915 有任何问题可以加我微信询问:2501902696(请备注编程)
2020-03-13 - ✨【进阶技能】小程序内wxParse读取的富文本中插入小程序商品链接,直接在文章中跳转商品详情,商品可插入文章任意位置
今天说一个进阶技能,怎么用wxParse读取的富文本中插入商品链接,首先你得懂得使用wxParse 和后台网页的富文本编辑器,这篇文章并不是基础讲解这俩个插件的使用方法,这里只作概述,如需详细了解,还请搜索这俩个插件的详细使用方法,弄懂之后再看本文,会比较容易理解 最终效果: 1.后台文本编辑器,添加商品链接效果: [图片] 2。小程序内文章中自动转为商品模块效果: [图片] *其中小程序的富文本页面的商品模块可以直接点击跳转到商品详情界面 实现流程: 一 插件简介及下载地址 1.wxParse:小程序中的HTML解析器,可以将HTML代码直接转换为小程序可以显示的代码,一般用于后台传入HTML富文本,小程序中直接显示文章 GitHub地址:https://github.com/icindy/wxParse 使用简介: [图片] 解压出来后,直接复制到小程序项目的根级目录, 在需要解析HTML的页面中加入代码 wxml:(除了目录地址,其他都是固定格式,不需要修改) import src="../../wxParse/wxParse.wxml" /> js:(这里的res.data.content就是我们后台传过来的HTML富文本代码,其他参数都不需要改) const WxParse = require('../../wxParse/wxParse.js'); onLoad: function (options) { WxParsewxParse('article', 'html', resdatacontent, that, 0); }, 2.Ueditor:网页的富文本编辑器插件,下载地址某度搜Ueditor官网就出来了,这种编辑器很多,关键不是TX的,就不发使用说明和下载地址了,怕给我文章封了,嘿嘿,自行查找吧 二 后台富文本编辑器修改 本文以Ueditor为例子,使用其他编辑器插件,可以看看实现原理 1.修改link插件文件:(说明:Ueditor有添加自定义按钮的方法,不过呢,因为我这里用的编辑器都是给小程序传的HTML编码,添加A标签的功能用不上,就直接修改了,有兴趣的可以额外添加一个按钮) [图片] 下面是完整代码,修改一下你的AJAX 调用接口地址后,直接复制,替换截图中的link.html文件就可以使用了 htmlhead title> scripttype"text/javascript"src"../internal.js"> --> tr td> 原title --> tr tdcolspan"2" labelfor"target"> ";// }else{// $G("msg").innerHTML = "";// } //这里的AJAX请求地址,需要改成你的接口地址 createRequest('你的AJAX接口地址.html?id='+$G('href').value.replace(/^\s+|\s+$/g, ''),getgoodinfo); //提示:这里的这个请求接口,用户用F12是可以看到的,所以调用的时候,一定要作加密认证,不要只传ID }; functionhrefStartWith(href,arr){ href = href.replace(/^\s+|\s+$/g, ''); forvar i=,ai;ai=arr[i++];){ if(href.indexOf(ai)==){ returntrue; } } returnfalse; } /获取ajax回传数据 unction getgoodinfo(){ if(http_request.readyState == ){ if(http_request.status==200){ if(http_request.responseText==){ alert("该ID商品不存在"); document.getElementById('title').value=; }else{ document.getElementById('text').value="商品链接:" + http_request.responseText.split(",")[]; //这里我是以 商品ID,商品名称,商品图标地址,商品价格 以‘,’英文逗号链接成的字符串,可以根据你的业务需要修改, document.getElementById('title').value=http_request.responseText; } }else{ alert("您请求的页面发生错误"); } } ar http_request = false; /生成ajax unction createRequest(url,func){ //Mozilla 等其他浏览器 if(window.XMLHttpRequest){ http_request = new XMLHttpRequest(); if(http_request.overrideMimeType){ http_request.overrideMimeType("text/xml"); } }elseif(windows.ActiveXObject){ try{ http_request = new ActiveXObject("Msxml2.XMLHTTP"); }catch(e){ try{ http_request = new ActiveXObject("Microsoft.XMLHTTP"); }catch(e){} } } if(!http_request){ alert("浏览器不支持访问"); returnfalse; } http_request.onreadystatechange = func; //发出http请求 http_request.open("GET",url,true); http_request.send(null); } 三 小程序端wxParse插件代码修改 1.修改html2json.js文件: [图片] 在该文件的146行添加如下代码:(这里我是以 商品ID,商品名称,商品图片地址,商品价格以‘,’英文逗号隔开组成字符串传过来的,可以根据你的业务需求更改识别方式) 如图: [图片] //将A标签的title转为商品信息 if (nodetag === 'a' && nodeattr.title!=) { //console.log(nodenode.attr.title) var title = nodeattr.title.split(",") nodeattr.title.split(",") nodegoodname = title[] nodegoodicon = title[] nodegoodprice = title[] } 2.修改wxParse.wxml文件: [图片] 在文件177行找到<!--a类型--> 进行修改,如图:[图片] 代码如下:(注释掉的是原本插件的代码,这里的商品模块CSS就自行设计编写吧,直接写到要调用wxParse的页面wxss文件中就行) blockwx:elif{{item.tag == 'a'}}viewbindtap"wxParseTagATap"class"wxParse-inline {{item.classStr}} wxParse-{{item.tag}}data-src{{item.attr.href}}style{{item.styleStr}}viewclass"goodbg row"imageclass"goodimg"src{{item.goodicon}}> 3.修改wxParse.js文件: 这个主要是修改一下点击跳转标签的路径,因为我这里是直接是把商品的ID传了过来,如果直接传了绝对路径这一步可以省略 [图片] [图片] 这里涂抹的地方换成你的小程序路径 到此,修改结束,就可以使用了! 后记 1.该方法可以改为跳转其他的页面路径也可以,稍微修改一下就行 2.后台的编辑器我没弄成可视化的商品跳转组件,这个如果有兴趣的同学搞出来了,给大家分享一下
2021-04-15 - 快人一步,了解小程序直播,读一篇就够了~
2020年新年伊始,受疫情影响,传统线下销售门店受到了巨大的波及,门店的被迫停业,不仅损失了大量利润的同时产生了高额的库存和必须面对的固定成本的开支; 一方面消费者被“困”家中足不出户,原本春节的预算花费遭到抑制,一方面是实体面临的疫情危机的经营困境,开辟新的获利经营模式,去盘活这一两难的局面成为了当务之急,非常时期用非常的办法。 在这个背景下,大批线下经营者纷纷主动转战线上战场,线上迎来爆发,商超类小程序相关的访问人数同比增长115%,生鲜果蔬类小程序同比增长168%,社区电商类小程序同比增长83%。 [图片] 时尚品牌菲安妮官方商城在3月7号—8号连续两天通过酷客多提供技术支持的小程序直播,其场均观看人数65000人,评论数量25000条,点赞291000取得了首播卓越的效果,直播打破销售记录。 接连两场的直播数据,这足以说明了问题,小程序直播虽然来得晚了,但却坐拥其他人没有的天时地利人和。 1、流量自有 依仗海量的微信用户,到小程序直播带来的流量与交易,全都沉淀在伙伴们自己的小程序中。 2、低门槛、快运营 无需直播资质,最快可一天完成开发,最快20分钟运营上手,轻松运用点赞、抽奖、优惠券派发等能力与用户进行互动。运营主动权自己把控,何时开播,开播频次自己说了算。 3、强社交互动、高转化 可最自然地承接微信的社交、通过一键分享直播间到微信聊天,传播便利、便于社群粉丝访问;还可通过订阅消息为用户提供一键订阅直播间的开播信息,方便用户回访;再配合公众号为直播间引流,粉丝互动强,交易转化高,大有后来居上之势力。 提问Qustions & 解答Answers 微信小程序直播长什么样? 小程序直播画面为竖屏形态,直播观看较为流畅。功能也很全面,大致分为四块: 1、预约观看 顾客通过小程序预约、观看直播并进行互动及购买; 2、一键分享 可将小程序直播分享给微信好友或群聊; 3、营销互动 点赞、评论、抽奖等,商家可在直播页推送商品,目前商城最多可添加2000个商品; 4、数据能力 直播数据实时更新,主播端和商家后台均有相关数据展示。 那商家怎么接入小程序直播? 目前处于公测阶段,仅限官方邀请的商家开通,收到邀请的商家开通小程序直播的流程是: 1、在小程序后台“功能-直播”申请直播能力并提审; 2、通过审核之后,可通过酷客多快速开发应用直播组件、创建直播间、配置商品。 [图片] ▲公测邀请 借助直播,商家可以让商品、服务通过直播镜头走到顾客面前,实现“店里没人,生意不减”。且直播沉淀下来的流量都属于商家,为日后销售打下基础。 小程序直播需要开发才能接入吗? 小程序直播是功能组件,需开发后才能投入使用。 我们支持多种接入模式: 商家:可自行开发或找服务商 服务商:开发者可快速了解直播能力,增加直播权限集,想成为小程序直播服务商,可点击获取详细指引 公众号、MCN机构及各类红人:可与品牌合作,或自行搭建小程序直播 怎么样才能参加小程序直播公测? 本次公测为邀请制,符合以下条件且收到邀请的伙伴即可开通: 1.满足小程序18个开放类目:(详细见下图) 2. 主体下小程序近半年没有严重违规 3. 小程序近90天内,有过支付行为 4. 主体下公众号累计粉丝数大于100人 5. 主体下小程序连续7日日活跃用户数大于100人 6. 主体在微信生态内近一年广告投放实际消耗金额大于1万元 注:条件中1、2、3必须满足,4、5、6满足其一即可。如满足以上条件还未收到邀请,请耐心等待,公测将尽快覆盖。 产品介绍—观众端口 [图片] ▲主图展示 小程序接入直播之后用户可以在商家自己的小程序中观看直播、购买商品、 在直播间内点赞评论抽奖,让用户与主播进行实时互动,完成“边看边买”的闭环。 产品介绍——主播端口 [图片] ▲主图展示 直播主播端,是一个独立的官方小程序,名称为小程序直播”,可通过扫码或小程序搜索进入,为主播提供直播录入功能。 主播可以在直播时查看用户点赞评讼互动情况、开奖、调节美颜、切换前后摄像头等,直播结束后可以查看本场直播数据。主播首次登录时,需要进行实名验证,确保主播身份与创建直播间时登记的主播微信身份一致。 产品介绍—后台管理端 [图片] 后台管理端是小程序直播功能在PC 端的管理后台/商家可以在后台创建直 播间、添加商品、设置抽奖、控制直播。 商家添加商品前需进行商品审核/审核 通过后商品将进入商品库,只有商品库 中的商品方能添加到直播间中。 小程序开播 操作流程 [图片] 登录小程序后台——点击左侧功能栏“直播”——点击“创建直播间” [图片] 添加商品库 [图片] [图片] 点击商品库——添加商品——提交商品审核——完成商品入库。〔已入库的商品上限为2000每天最多提交审添500件商品] 说明:商品入库前需经过审核,审核时长为1-7天。只有已入库的商品才能添加到直播间的商品列表中,建议商家将直播的商品提前录入到商品库中。 商品链接填写方法: 1、在右侧输入直播小程序任一项目成员的微信号,即可开启叔限。 2、该成员进入小程序中的商品详情页点击右上角菜单察看商品链接。 直播组件优势在哪里? 1、内容易于传播 过去微信生态中的公众号、社群、朋友圈图文、视频等内容繁琐、单一,现在通过小程序码海报或卡片,通过微信聊天、微信群、朋友圈分享直播简单高效。 2、带货更快捷 在线直播彻底打通了“人、货、场”,产品介绍更加生动直接,主播随时与顾客在线互动答疑,消费者边看边买,商家可利用微信的社交裂变优势,提高卖货效率和保持老客维系。 3、运营有据可循 小程序直播为商家和内容主提供精细化数据,建立用户画像,为商家优化产品、直播内容、社群运营等提供准确参考。 一觉醒来 全新的电商时代 [图片] 微信流量池、公众号、小程序商城、社群,几个关键词的联动,似乎已经足够值得期待,但远不止于此,微信小程序直播最大的意义在于,帮商家第一次真正完成了直播和小程序商城的融合。 微信19年月活用户接近12亿,小程序日活超3亿,累计创造8000多亿交易额,同比增长160%。从现有信息看,小程序交易额无疑将再度迎来高增长,继传统电商、社交电商、直播电商之后,将开启全新的电商时代,成为下一个风口。具体会带来什么样子的改变?如何抓住微信生态的红利,寻找二次起飞的机会?酷客多期待和您一起探索。 ——酷客多
2020-03-12 - 分享一个固定头和列的 table 组件的简单实现
本案案例基于 WePY 实现,大家可根据自身需要进行更改扩展。 代码地址>> 演示 [图片] 演示视频地址>> 实现原理 [图片] 橙色和紫色区域组成了横向滚动的 [代码]scroll-view[代码]。 红色虚线区域是纵向滚动的 [代码]scroll-view[代码]。但由于绿色区域设置了 [代码]pointer-events: none;[代码],即实际只能触摸橙色区域。通过在橙色区域绑定的 [代码]scroll[代码] 事件(纵向),实时设置绿色虚线区域的 [代码]scrollTop[代码]。 紫色区域是固定头部,绿色区域是固定列。左上角的绿色区域是横向与纵向共同固定的区域。 实现要点 绑定了 [代码]scroll[代码] 事件的 [代码]scroll-view[代码] 要指定 [代码]throttle: false[代码],否则回调函数有可能取不到最终位置的 [代码]scrollTop[代码] 值。官方文档目前未提及此属性,参考资料>>。 固定列需要设置 [代码]pointer-events: none;[代码],实现点击穿透。使得 [代码]tbody[代码] 能触发 [代码]scroll[代码] 事件,而不是为固定列也绑定 [代码]scroll[代码] 事件。 找出每列的最大单元格作为该列的宽度,当然你也可以显示设置。 peace out!👋 小程序 Bug 2019.09.03 更新 当将该组件至于 Popup 弹框,且该弹框通过 [代码]visibility: hidden/visible[代码] 切换,那么在 iOS 中,会使固定列([代码].table__fixed-columns[代码])的 [代码]pointer-events: none[代码] 失效。
2019-09-03 - 【优化】利用函数防抖和函数节流提高小程序性能
大家好,上次给大家分享了swiper仿tab的小技巧: https://developers.weixin.qq.com/community/develop/article/doc/000040a5dc4518005d2842fdf51c13 [代码]今天给大家分享两个有用的函数,《函数防抖和函数节流》 函数防抖和函数节流是都优化高频率执行js代码的一种手段,因为是js实现的,所以在小程序里也是适用的。 [代码] 首先先来理解一下两者的概念和区别: [代码] 函数防抖(debounce)是指事件在一定时间内事件只执行一次,如果在这段时间又触发了事件,则重新开始计时,打个很简单的比喻,比如在打王者荣耀时,一定要连续干掉五个人才能触发hetai kill '五连绝世'效果,如果中途被打断就得重新开始连续干五个人了。 函数节流(throttle)是指限制某段时间内事件只能执行一次,比如说我要求自己一天只能打一局王者荣耀。 这里也有个可视化工具可以让大家看一下三者的区别,分别是正常情况下,用了函数防抖和函数节流的情况下:http://demo.nimius.net/debounce_throttle/ [代码] 适用场景 函数防抖 搜索框搜索联想。只需用户最后一次输入完,再发送请求 手机号、邮箱验证输入检测 窗口resize。只需窗口调整完成后,计算窗口大小。防止重复渲染 高频点击提交,表单重复提交 函数节流 滚动加载,加载更多或滚到底部监听 搜索联想功能 实现原理 [代码] 函数防抖 [代码] [代码]const _.debounce = (func, wait) => { let timer; return () => { clearTimeout(timer); timer = setTimeout(func, wait); }; }; [代码] [代码] 函数节流 [代码] [代码]const throttle = (func, wait) => { let last = 0; return () => { const current_time = +new Date(); if (current_time - last > wait) { func.apply(this, arguments); last = +new Date(); } }; }; [代码] [代码] 上面两个方法都是比较常见的,算是简化版的函数 [代码] lodash中的 Debounce 、Throttle [代码] lodash中已经帮我们封装好了这两个函数了,我们可以把它引入到小程序项目了,不用全部引入,只需要引入debounce.js和throttle.js就行了,链接:https://github.com/lodash/lodash 使用方法可以看这个代码片段,具体的用法可以看上面github的文档,有很详细的介绍:https://developers.weixin.qq.com/s/vjutZpmL7A51[代码]
2019-02-22 - 【优化】小程序优化-代码篇
本文主要是从代码方面跟大家分享我自己在开发小程序的一些做法,希望能帮到一些同学。 前言 不知道大家有没有这种体会,刚到公司时,领导要你维护之前别人写的代码,你看着别人写的代码陷入了深深的思考:“这谁写的代码,这么残忍” [图片] 俗话说“不怕自己写代码,就怕改别人的代码”,一言不和就改到你吐血,所以为了别人好,也为了自己好,代码规范,从我做起。 项目目录结构 在开发之前,首先要明确你要做什么,不要一上来就是干,咱们先把项目结构搭好。一般来说,开发工具初始化的项目基本可以满足需求,如果你的项目比较复杂又有一定的结构的话就要考虑分好目录结构了,我的做法如下图: [图片] component文件夹是放自定义组件的 pages放页面 public放公共资源如样式表和公共图标 units放各种公共api文件和封装的一些js文件 config.js是配置文件 这么分已经足以满足我的需求,你可以根据自己的项目灵活拆分。 配置文件 我的项目中有个config.js,这个文件是用来配置项目中要用到的一些接口和其它私有字段,我们知道在开发时通常会有测试环境和正式环境,而测试环境跟正式环境的域名可能会不一样,如果不做好配置的话直接写死接口那等到上线的时候一个个改会非常麻烦,所以做好配置是必需的,文件大致如下: [图片] 首先是定义域名,然后在config对象里定义接口名称,getAPI(key)是获取接口方法,最后通过module暴露出去就可以了.引用的时候只要在页面引入 import domain from ‘…/…/config’;,然后wx.request的时候url的获取方式是domain.getAPI(’’) 代码健壮性、容错性 例子 代码的健壮性、容错性也是我们应该要考虑的一点,移动端的项目不像pc端的网络那么稳定,很多时候网络一不稳定就决定我们的项目是否能正常运行,而一个好的项目就一定要有良好的容错性,就是说在网络异常或其它因素导致我们的项目不能运行时程序要有一个友好的反馈,下面是一个网络请求的例子: [图片] 相信多数人请求的方式是这样,包括我以前刚接触小程序的时候也是这样写,这样写不是说不好,而是不太严谨,如果能够正常获取数据那还好,但是一旦请求出现错误那程序可以到此就没法运行下去了,有些比较好的会加上faill失败回调,但也只是请求失败时的判断,在请求成功到获取数据的这段流程内其实是还有一些需要我们判断的,一般我的做法是这样: [图片] 在请求成功后小程序会进行如下判断: 判断是否返回200,是则进行一下步操作,否则抛出错误 判断数据结构是否完整,是则进行一下步操作,否则抛出错误 然后就可以在页面根据情况进行相应的操作了。 定制错误提示码 可以看到上面的截图的错误打印后面会带一个gde0或gde1的英文代码,这个代码是干嘛用的呢,其实是用来报障的,当我们的小程序上线后可能会遇到一些用户发来的报障,一般是通过截图发给我们,之前没有做错误提示码的时候可能只是根据一句错误提示来定位错误,但是很多时候误提示语都是一样的,我们根本不知道是哪里错了,这样一来就不能很快的定位的错误,所以加上这样一个提示码,到时用户一发截图来,我们只要根据这个错误码就能很快的定位错误并解决了,错误提示码建议命名如下: 不宜过长,3个字母左右 唯一性 意义明确 像上面gde表示获取草稿失败,后面加上数字表示是哪一步出错。 模块化 我们组内的大神说过, 模块化的意义在义分治,不在于复用。 之前我以为模块化只是为了可以复用,其实不然,无论模块多么小也是可以模块化,哪怕只是一个简单的样式也一样,并是不为了复用,而是管理起来方便。 很多同学经常将一些公共的样式事js放在app.wxss和app.js里以便调用,这样做其实有一个坏处,就是维护性比较差,如果是比较小的项目还好,项目一大问题就来了。而且项目是会迭代的,不可能总是一个人开发,可能后面会交接给其他人开发,所以会造成的问题就是: app.wxss和app.js里的内容只会越来越多,因为别人不确定哪些是没用的也不敢删,只能往里加东西,造成文件臃肿,不利于维护。 app.wxss和app.js对于每个页面都有效,可读性方面比较差。 所以模块化的意义就出来了,将公共的部分进行模块化统一管理,也便于维护。 样式模块化 公共样式根据上面的目录结构我是放在public里的css里,每个文件命名好说明是哪个部分的模块化,比如下面这个就表示一个按钮的模块化 [图片] 前面说过模块化不在于大小,就算只是一个简单的样式也可以进行模块化,只要在用到的地方import一下就行了,就知道哪里有用到,哪里没有用到,清晰明了。 js模块化 js模块化这里分为两个部分的模块化,一部分是公共js的模块化,另一部分是页面js的模块化即业务与数据的拆分。 公共js模块化 比较常用的公共js有微信登录,弹窗,请求等,一般我是放在units文件夹里,这里经微信弹窗api为例: [图片] 如图是在小程序中经常会用到的弹窗提示,这里进行封装,定义变量,只要在页面中引入就能直接调用了,不用每次都写一大串。比如在请求的时候是这样用的 [图片] toast()就是封装的弹窗api,这样看起来是不是清爽多了! 业务与数据模块化 业务与数据模块化就是指业务和数据分开,互不影响,业务只负责业务,数据只负责数据,可以看到页面会比普通的页面多了一个api.js [图片] 这个文件主要就是用来获取数据的,而index.js主要用来处理数据,这样分工明确,相比以往获取数据和处理数据都在一个页面要好很多,而且我这里获取数据是返回一个promise对象的,也方便处理一些异步操作。 组件化 组件化相信大家都不陌生了,自从小程序支持自定义组件,可以说是大大地提高了开发效率,我们可以将一些公共的部分进行组件化,这部分就不详细介绍,大家可以去看文档。组件化对于我们的项目来说有很大的好处,而且组件化的可移植性强,从一个项目复用到另一个项目基本不需要做什么改动。 总结 这篇文章通过我自己的一些经验来给大家介绍如何优化自己的代码,主要有以下几点 分好项目目录结构 做好接口配置文件 代码健壮性、容错性的处理 定制错误提示码方便定位错误 样式模块化和js模块化 组件化 最后放上项目目录结构的代码片段,大家可以研究一下,有问题一起探讨:https://developers.weixin.qq.com/s/1uVHRDmT7j6l
2019-03-07 - 【圣诞节】给你的头像加个圣诞帽吧
圣诞节快来了,来给你的头像加个帽子吧 看着大伙都在弄这个,我自己也来试一哈,我分别用了两种方式来实现,一种是普通的方式,一种是wxs方式 普通方式 效果图如下: [图片] 思路 获取头像 选择素材 缩放,移动,旋转素材 生成canvas 生成图片,保存图片 实现方式 [图片] 首先是获取头像,这个不用说,大家应该都会的。 选择素材这里我准备了三张圣诞帽的素材,这个网上有很多,可以自己找下,然后我还做了一个选择手机相册的功能,如果你自己有素材的话也可以直接选择这个功能。 缩放,移动,旋转素材都是通过触摸函数去实现的,这里是先将布局做好,然后在标签上面绑定各个触摸事件,通过返回的值在标签的style里设置实现各个效果。 调整好了之后点击保存头像会获取所有参数并将头像画出来,再通过 [代码]wx.canvasToTempFilePath()[代码] 将canvas生成图片最后通过 [代码]wx.saveImageToPhotosAlbum()[代码] 保存图片。 主要代码 主要的函数就是下面这几个,代码片段我会放在文末,没有什么比较难的地方,就是要注意下计算的时候不要算错就行。 [图片] 需要注意的点 由于素材的大小可能会有不同,所以在重新选择素材的时候高度要重新设置一下,这里我用了一个方法来重置高度,主要是每次重新选择素材的时候就用 [代码]wx.getImageInfo()[代码] 这个api去获取图片素材的宽高,再计算出宽高比。 [图片] wxs实现方式 实现方式 思路跟普通方式是一样的,不同的是这里将绑定事件通过 [代码]wxs[代码] 去实现,直接设置标签的参数而不通过逻辑层去处理,在性能上会比较好一点,不过这种实现方式在进行旋转的时候最后生成的图片会有不准,后面会说到。 参数的获取是通过在标签上设置style,然后点击保存的时候用 [代码]wx.createSelectorQuery()[代码] 获取各个参数的 [图片] [图片] 获取旋转的值 由于 [代码]wx.createSelectorQuery()[代码] 并不能获取到 [代码]rotate[代码] 这个参数,所以我是通过下面这种方式来拿到旋转的值的,将旋转值以宽度的形式赋值给 [代码].vo-ro[代码] [图片] [图片] 但是我发现旋转之后生成的图片不是正确的,原因是旋转之后通过 [代码]wx.createSelectorQuery()[代码] 拿到的宽高并不是图片大小的宽高,而是旋转之后的宽高,按理来说不应该是这样的,即使通过样式旋转,它的宽高应该保持不变才对,这样就造成了参数上的错误,所以画出来的图片是不准确的。 因为加了旋转之后画出来的图片会不准确,暂时想不出别的方法,我把旋转的按钮先注释掉了,只支持缩放跟拖拽。 总结 两种方式,wxs性能要更好,但是效果没第一种的好,看你要哪种了,最后祝大家圣诞节快乐,祝你生活愉快 https://developers.weixin.qq.com/s/Cizd1RmY7qdg
2019-12-25 - 正确使用JavaScript数组
首先,我们可以简单地认为缩进就是代码复杂性的指标(尽管很粗略)。因为缩进越多代表我们的嵌套越多,因此代码就越复杂。今天就拿数组来做具体的例子,来展示以下如何抛弃循环,减少缩进,正确地使用JavaScript数组。 “…a loop is an imperative control structure that’s hard to reuse and difficult to plug in to other operations. In addition, it implies code that’s constantly changing or mutating in response to new iterations.” -Luis Atencio 循环 我们都知道,循环结构就是会无形地提高代码的复杂性。那我们现在看看在JavaScript上的循环是如何工作的。 在JavaScript上至少有四五种循环的方式,其中最基础的就是[代码]while[代码]循环了。讲例子前,先设定一个函数和数组: [代码]// oodlify :: String -> String function oodlify(s) { return s.replace(/[aeiou]/g, 'oodle'); } const input = [ 'John', 'Paul', 'George', 'Ringo', ]; [代码] 那么,如果我们现在要使用[代码]oodlify[代码]函数操作一下数组里每个元素的话,如果我们使用[代码]while[代码]循环的话,是这样子的: [代码]let i = 0; const len = input.length; let output = []; while (i < len) { let item = input[i]; let newItem = oodlify(item); output.push(newItem); i = i + 1; } [代码] 这里就有许多无谓的,但是又不得不做的工作。比如用[代码]i[代码]这个计数器来记住当前循环的位置,而且需要把[代码]i[代码]初始化成0,每次循环还要加一;比如要拿[代码]i[代码]和数组的长度[代码]len[代码]对比,这样才知道循环到什么时候停止。 这时为了让清晰一点,我们可以使用JavaScript为我们提供的[代码]for[代码]循环: [代码]const len = input.length; let output = []; for (let i = 0; i < len; i = i + 1) { let item = input[i]; let newItem = oodlify(item); output.push(newItem); } [代码] [代码]for[代码]循环的好处就是把与业务代码无关的计数逻辑放在了括号里面了。 对比起[代码]while[代码]循环虽有一定改进,但是也会发生类似忘记给计数器[代码]i[代码]加一而导致死循环的情况。 现在回想一下我们的最初目的:就只是给数组的每一个元素执行一下[代码]oodlify[代码]函数而已。其实我们真的不想关什么计数器。 因此,[代码]ES2015[代码]就为我们提供了一个全新的可以让我们忽略计数器的循环结构- [代码]for...of[代码]循环 : [代码]let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } [代码] 这个方式是不是简单多了!我们可以注意到,计数器和对比语句都没了。 如果我们这就满足的话,我们的目标也算完成了,代码的确是简洁了不少。 但是其实,我们可以对JavaScript的数组再深入挖掘一下,更上一层楼。 Mapping [代码]for...of[代码]循环的确比[代码]for[代码]循环简洁不少,但是我们仍然写了一些不必要的初始化代码,比如[代码]output[代码]数组,以及把每个操作过后的值push进去。 其实我们有办法写得更简单明了一点的。不过,现在我们来放大一下这个问题先: 如果我们有两个数组需要使用[代码]oodlify[代码]函数操作的话呢? [代码]const fellowship = [ 'frodo', 'sam', 'gandalf', 'aragorn', 'boromir', 'legolas', 'gimli', ]; const band = [ 'John', 'Paul', 'George', 'Ringo', ]; [代码] 很明显,我们就要这样循环两个数组: [代码]let bandoodle = []; for (let item of band) { let newItem = oodlify(item); bandoodle.push(newItem); } let floodleship = []; for (let item of fellowship) { let newItem = oodlify(item); floodleship.push(newItem); } [代码] 这的确可以完成我们的目标,但是这样写得有点累赘。我们可以重构一下以减少重复的代码。因此我们可以创建一个函数: [代码]function oodlifyArray(input) { let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } return output; } let bandoodle = oodlifyArray(band); let floodleship = oodlifyArray(fellowship); [代码] 这样是不是好看多了。但是问题来了,如果我们要使用其他函数来操作这个数组的话呢? [代码]function izzlify(s) { return s.replace(/[aeiou]+/g, 'izzle'); } [代码] 这时,我们前面创建的[代码]oodlifyArray[代码]函数帮不了我们了。不过如果我们这时创建[代码]izzlifyArray[代码]函数的话,代码不就又有许多重复的部分了吗? [代码]function oodlifyArray(input) { let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } return output; } function izzlifyArray(input) { let output = []; for (let item of input) { let newItem = izzlify(item); output.push(newItem); } return output; } [代码] 这两个函数是不是及其相似呢。 如果此时我们将其抽象成一个模式的话呢:我们希望传入一个数组和一个函数,然后映射每个数组元素,最后输出一个数组。这个模式就称为[代码]mapping[代码]: [代码]function map(f, a) { let output = []; for (let item of a) { output.push(f(item)); } return output; } [代码] 其实我们并不需要自己手动写[代码]mapping[代码]函数,因为JavaScript提供了内置的[代码]map[代码]函数给我们使用,此时我们的代码是这样的: [代码]let bandoodle = band.map(oodlify); let floodleship = fellowship.map(oodlify); let bandizzle = band.map(izzlify); let fellowshizzle = fellowship.map(izzlify); [代码] Reducing 此时[代码]map[代码]是很方便了,但是并不能覆盖我们所有的循环需要。 如果此时我们需要累计数组中的所有数组呢。我们假设有一个这样的数组: [代码]const heroes = [ {name: 'Hulk', strength: 90000}, {name: 'Spider-Man', strength: 25000}, {name: 'Hawk Eye', strength: 136}, {name: 'Thor', strength: 100000}, {name: 'Black Widow', strength: 136}, {name: 'Vision', strength: 5000}, {name: 'Scarlet Witch', strength: 60}, {name: 'Mystique', strength: 120}, {name: 'Namora', strength: 75000}, ]; [代码] 如果我们要找到[代码]strength[代码]最大的那个的元素的话,使用[代码]for...of[代码]循环是这样的: [代码]let strongest = {strength: 0}; for (hero of heroes) { if (hero.strength > strongest.strength) { strongest = hero; } } [代码] 如果此时我们想累计一下所有的[代码]strength[代码]的话,循环里面就是这样的了: [代码]let combinedStrength = 0; for (hero of heroes) { combinedStrength += hero.strength; } [代码] 这两个例子我们都需要初始化一个变量来配合我们的操作。合并两个例子的话就是这样的: [代码]function greaterStrength(champion, contender) { return (contender.strength > champion.strength) ? contender : champion; } function addStrength(tally, hero) { return tally + hero.strength; } // 例子 1 const initialStrongest = {strength: 0}; let working = initialStrongest; for (hero of heroes) { working = greaterStrength(working, hero); } const strongest = working; // 例子 2 const initialCombinedStrength = 0; working = initialCombinedStrength; for (hero of heroes) { working = addStrength(working, hero); } const combinedStrength = working; [代码] 此时我们可以抽象成这样一个函数: [代码]function reduce(f, initialVal, a) { let working = initialVal; for (item of a) { working = f(working, item); } return working; } [代码] 其实这个方法JavaScript也提供了内置函数,就是[代码]reduce[代码]函数。这时代码是这样的: [代码]const strongestHero = heroes.reduce(greaterStrength, {strength: 0}); const combinedStrength = heroes.reduce(addStrength, 0); [代码] Filtering 前面的[代码]map[代码]函数是将数组的全部元素执行同个操作之后输出一个同样大小的数组; [代码]reduce[代码]则是将数组的全部值执行操作之后,最终输出一个值。 如果此时我们只是需要提取几个元素到一个数组内呢?为了更好得解释,我们来扩充一下之前的例子: [代码]const heroes = [ {name: 'Hulk', strength: 90000, sex: 'm'}, {name: 'Spider-Man', strength: 25000, sex: 'm'}, {name: 'Hawk Eye', strength: 136, sex: 'm'}, {name: 'Thor', strength: 100000, sex: 'm'}, {name: 'Black Widow', strength: 136, sex: 'f'}, {name: 'Vision', strength: 5000, sex: 'm'}, {name: 'Scarlet Witch', strength: 60, sex: 'f'}, {name: 'Mystique', strength: 120, sex: 'f'}, {name: 'Namora', strength: 75000, sex: 'f'}, ]; [代码] 现在假设我们要做的两件事: 找到[代码]sex = f[代码]的元素 找到[代码]strength > 500[代码]的元素 如果使用[代码]for...of[代码]循环的话,是这样的: [代码]let femaleHeroes = []; for (let hero of heroes) { if (hero.sex === 'f') { femaleHeroes.push(hero); } } let superhumans = []; for (let hero of heroes) { if (hero.strength >= 500) { superhumans.push(hero); } } [代码] 由于有重复的地方,那么我们就把不同的地方抽取出来: [代码]function isFemaleHero(hero) { return (hero.sex === 'f'); } function isSuperhuman(hero) { return (hero.strength >= 500); } let femaleHeroes = []; for (let hero of heroes) { if (isFemaleHero(hero)) { femaleHeroes.push(hero); } } let superhumans = []; for (let hero of heroes) { if (isSuperhuman(hero)) { superhumans.push(hero); } } [代码] 此时就可以抽象成JavaScript内置的[代码]filter[代码]函数: [代码]function filter(predicate, arr) { let working = []; for (let item of arr) { if (predicate(item)) { working = working.concat(item); } } } const femaleHeroes = filter(isFemaleHero, heroes); const superhumans = filter(isSuperhuman, heroes); [代码] Finding [代码]filter[代码]搞定了,那么如果我们只要找到一个元素呢。 的确,我们同样可以使用[代码]filter[代码]函数完成这个目标,比如: [代码]function isBlackWidow(hero) { return (hero.name === 'Black Widow'); } const blackWidow = heroes.filter(isBlackWidow)[0]; [代码] 当然我们也同样会发现,这样的效率并不高。因为[代码]filter[代码]函数会过滤所有的元素,尽管在前面已经找到了应该要找到的元素。因此我们可以写一个这样的查找函数: [代码]function find(predicate, arr) { for (let item of arr) { if (predicate(item)) { return item; } } } const blackWidow = find(isBlackWidow, heroes); [代码] 正如大家所预期那样,JavaScript也同样提供了内置方法[代码]find[代码]给我们,因此我们最终的代码是这样的: [代码]const blackWidow = heroes.find(isBlackWidow); [代码] 总结 这些JavaScript内置的数组函数就是很好的例子,让我们学会了如何去抽象提取共同部分,以创造一个可以复用的函数。 现在我们可以用内置函数完成几乎所有的数组操作。分析一下,我们可以看出每个函数都有以下特点: 摒弃了循环的控制结构,使代码更容易阅读。 通过使用适当的方法名称描述我们正在使用的方法。 减少了处理整个数组的问题,只需要关注我们的业务代码。 在每种情况下,JavaScript的内置函数都已经将问题分解为使用小的纯函数的解决方案。通过学习这几种内置函数能让我们消除几乎所有的循环结构,这是因为我们写的几乎所有循环都是在处理数组或者构建数组或者两者都有。因此使用内置函数不仅让我们在消除循环的同时,也为我们的代码增加了不少地可维护性。 本文翻译自:JavaScript Without Loops
2020-03-11 - 官方小程序推荐
小程序推荐 1、在平时 开发,我们经常会遇到内容安全监测的服务,推荐官方小程序 如需体验、接入安全能力,请扫一扫如下小程序二维码进行详细了解。 珊瑚内容安全助手 [图片] 占位 2、在测试小程序时,时常遇到手机兼容性问题,可以扫码获取手机具体信息 [图片] 具体扫码截图如下所示 [图片] [图片] [图片]
2020-11-20 - 在线答题小程序题库批量导入经验
本经验面向用云开发做在线答题小程序的同学 占位 对于一个在线答题小程序,题库就是ta的灵魂,经过最近一段时间的摸索,总结出一套批量题库导入的方法,仅供大家参考, 本导入方案支持单选、多选、判断、填空、简答 方案实现过程中包括对excel原子数据进行加工,包括但不限于 1、对于单选、多选、判断,会从四个选项中提取内容组装成对象数组 2、对于填空、解答,会从答案中提取内容并解析成对象数组 本次导入模板如下 [图片] 具体导入代码如下所示 该方案采用PHP代码实现,借助PHPExcel解析excel 占位 [图片] 占位 [图片] 占位 [图片] 占位 [图片] 占位 [图片] 占位 目前该代码实现生成的JSON字符串已成功经过线上验证。
2020-03-10 - 关于微信授权获取昵称含Emoji表情引发的乱码问题总结
做过微信授权的小伙伴都可能会遇到获取用户昵称乱码问题,那是因为微信昵称中的含有SoftBank版本的Emoji表情。 如我的微信昵称: 正常显示为[图片]; 未处理Softbank及微信自定义表情显示为[图片]; 处理Softbank后显示为[图片]。 [图片] Emoji表情有很多种版本,其中包括Unified、DoCoMo、KDDI、SoftBank和Google,不同版本的Unicode代码并不一定相同。经研究,微信昵称中的Emoji表情截止目前(2019.12.10)已知支持三种版本: 1、SoftBank版本(网上一般称之为SB Unicode),如😂为E412; 2、Unified版本,如😂为1F602; 3、自定义表情版本,如[捂脸]。 举个例子,😂(喜极而泣)的各种编码如下: SoftBank:0000E412 Unified:0001F602(U+1F602) DoCoMo:0000E72A KDDI:0000EB64 Google:000FE334 UTF-8:F09F9882(%F0%9F%98%82) UTF-16BE:FEFFD83DDE02(\uD83D\uDE02) UTF-16LE:FFFE3DD802DE UTF-32BE:0000FEFF0001F602 UTF-32LE:FFFE000002F60100 Emoji表情代码表参阅:http://punchdrunker.github.io/iOSEmoji/table_html/index.html 对于SoftBank及微信自家定义的表情,需要做映射处理转换成标准的Unified版本的Emoji表情才能正常显示,否则就可能乱码。具体解决方案参见https://github.com/gzu-liyujiang/UnicodeEmoji SoftBank版本编码与Unified版本编码对应关系 { “E150”: “0001F68F”, “E030”: “0001F338”, “E151”: “0001F6BB”, “E152”: “0001F46E”, “E031”: “0001F531”, “E032”: “0001F339”, “E153”: “0001F3E3”, …省略… } SoftBank版本编码与标准Unicode编码对应关系 { “E150”: “\uD83D\uDE8F”, “E030”: “\uD83C\uDF38”, “E151”: “\uD83D\uDEBB”, “E152”: “\uD83D\uDC6E”, “E031”: “\uD83D\uDD31”, “E032”: “\uD83C\uDF39”, “E153”: “\uD83C\uDFE3”, …省略… } SoftBank版本编码与标准的Emoji字符表情的对应关系 { “E150”: “🚏”, “E030”: “🌸”, “E151”: “🚻”, “E152”: “👮”, “E031”: “🔱”, “E032”: “🌹”, “E153”: “🏣”, …省略… }
2019-12-11 - 关于微信昵称中Emoji表情乱码问题解决方案
问题描述 我相信每个人在刚接触小程序开发的时候都会遇到这个问题:小程序用户授权的时候,如果微信昵称里面带有Emoji表情的昵称,保存到数据库里,在界面显示的时候就乱码了。 当然微信昵称乱码只是一个最常遇到的场景,在其他包含Emoji表情的地方,该问题都会暴露出来。 如下图所示: 正常文本 [图片] 乱码文本 [图片] 问题解决方案 在本文我会总结两种解决方案,第一种解决方案是我在之前查阅相关文档,亲测实现的,当时在采用第二种解决方案的时候遇到了问题,采用的不得已方案,后面等第二种方案亲测实现后,才发现第二种方案是真香。 第一种解决Emoji表情乱码的方案 将包含该Emoji表情的信息encodeURIComponent编码,在使用的地方,在decodeURIComponent解码,这种方案已在小程序 “垃圾分类黑板报”中亲测,如下图所示: [图片] 这种方案不算好,仅仅是解决了乱码的问题,带来了代码的额外编码解码开支。 第二种解决Emoji表情乱码的方案 按照我采用Mysql数据库,PHP YII2框架来描述 两步走 在数据库连接的时候采用utf8mb4,这一点非常重要,很容易遗漏。 数据库编码和表格编码以及对应的存储字段都应采用utf8mb4编码 关于第一步在代码示例如下 [图片] 该方案在小程序"群黑板报"中,亲测可用。 [图片] 这种方案是最完美的方案,没有带来额外的代码开支,也是目前网上能搜到的推荐使用方案,但是在实操过程中,有时候会因为数据库版本或者个人设置导致不生效,但是该方案确实香。 部分文章推荐 下面推荐几篇文章用来普及下为什么正常情况下Emoji表情存储到数据库里面就会乱码了 关于微信授权获取昵称含Emoji表情引发的乱码问题总结 https://developers.weixin.qq.com/community/develop/article/doc/000c84d49d4058d35e99bbaef5b013
2019-12-11 - 小程序中如何实现表情组件
先上效果图(无图无真相) [图片] 1. 第一步准备表情包素材 我这里用的微博的表情包可以点击下面的链接查看具体JSON格式这里不展示 表情包文件weibo-emotions.js 2. 第二步编写表情组件(基于wepy2.0) 如果不会 wepy 可以先去了解下如果你会vue那非常容易上手 首先我们需要把表情包文件weibo-emotions.js中的JSON文件转换成我们需要的格式 [代码]emojis = [ { id: 编号, value: 表情对应的汉字含义 例如:[偷笑], icon: 表情相对图片路径, url: 表情具体图片路径 } ] [代码] 具体转换方法 [代码]function () { const _emojis = {} for (const key in emotions) { if (emotions.hasOwnProperty(key)) { const ele = emotions[key]; for (const item of ele) { _emojis[item.value] = { id: item.id, value: item.value, icon: item.icon.replace('/', '_'), url: weibo_icon_url + item.icon } } } } return _emojis } [代码] 编写组件的html代码 [代码]<template> <div class="emoji" style="height:{{height}}px;" :hidden="hide"> <scroll-view :scroll-y="true" style="height:{{height}}px;"> <div class="icons"> <div class="img" v-for="img in emojis" :key="img.id" @tap.stop="onTap(img.value)"> <img class="icon-image" :src="img.url" :lazy-load="true" /> </div> </div> <div style="height:148rpx;"></div> </scroll-view> <div class="btn-box"> <div class="btn-del" @tap.stop="onDel"> <div class="icon icon-input-del" /> </div> </div> </div> </template> [代码] html代码中的height变量为键盘的高度,通过props传入 编写组件的css代码 [代码].emoji { position: fixed; bottom: 0px; left: 0px; width: 100%; transition: all 0.3s; z-index: 10005; &::after { content: ' '; position: absolute; left: 0; top: 0; right: 0; height: 1px; border-top: 0.4px solid rgba(235, 237, 245, 0.8); color: rgba(235, 237, 245, 0.8); } .icons { display: flex; flex-wrap: wrap; .img { flex-grow: 1; padding: 20rpx; text-align: left; justify-items: flex-start; .icon-image { width: 48rpx; height: 48rpx; } } } scroll-view { background: #f8f8f8; } .btn-box { right: 0rpx; bottom: 0rpx; position: fixed; background: #f8f8f8; padding: 30rpx; .btn-del { background: #ffffff; padding: 20rpx 30rpx; border-radius: 10rpx; .icon { font-size: 48rpx; } } } .icon-loading { height: 100%; display: flex; justify-content: center; align-items: center; } } [代码] 这里是使用less来编写css样式的,flex布局如果你对flex不是很了解可以看看 这篇文章 组件JS代码比较少 [代码]import { weibo_emojis } from '../common/api'; import wepy from '@wepy/core'; wepy.component({ options: { addGlobalClass: true }, props: { height: Number, hide: Boolean }, data: { emojis: weibo_emojis, }, methods: { onTap(val) { this.$emit('emoji', val); }, onDel() { this.$emit('del'); } } }); [代码] 表情组件基本已经编写完成是不是很简单 那么编写好的组件怎么用呢? 其实也很简单 第一步把组件引入到页面 [代码]<config> { "usingComponents": { "emoji-input": "../components/input-emoji", } } </config> [代码] 第二步把组件加入到页面html代码中 [代码]<emoji-input :height="boardheight" @emoji="onInputEmoji" @del="onDelEmoji" :hide="bottom === 0" /> [代码] 第三步编写onInputEmoji,onDelEmoji方法 [代码] /** * 选择表情 */ onInputEmoji(val) { let str = this.content.split(''); str.splice(this.cursor, 0, val); this.content = str.join(''); if (this.cursor === -1) { this.cursor += val.length + 1; } else { this.cursor += val.length; } this.canSend(); }, /** * 删除表情 */ onDelEmoji() { let str = this.content.split(''); const leftStr = this.content.substring(0, this.cursor); const leftLen = leftStr.length; const rightStr = this.content.substring(this.cursor); const left_left_Index = leftStr.lastIndexOf('['); const left_right_Index = leftStr.lastIndexOf(']'); const right_right_Index = rightStr.indexOf(']'); const right_left_Index = rightStr.indexOf('['); if ( left_right_Index === leftLen - 1 && leftLen - left_left_Index <= 8 && left_left_Index > -1 ) { // "111[不简单]|23[33]"left_left_Index=3,left_right_Index=7,leftLen=8 const len = left_right_Index - left_left_Index + 1; str.splice(this.cursor - len, len); this.cursor -= len; } else if ( left_left_Index > -1 && right_right_Index > -1 && left_right_Index < left_left_Index && right_right_Index <= 6 ) { // left_left_Index:4,left_right_Index:3,right_right_Index:1,right_left_Index:2 // "111[666][不简|单]"right_right_Index=1,left_left_Index=3,leftLen=6 let len = right_right_Index + 1 + (leftLen - left_left_Index); if (len <= 10) { str.splice(this.cursor - (leftLen - left_left_Index), len); this.cursor -= leftLen - left_left_Index; } else { str.splice(this.cursor, 1); this.cursor -= 1; } } else { str.splice(this.cursor, 1); this.cursor -= 1; } this.content = str.join(''); }, [代码] 好了基本就完成了一个表情组件的编写和调用 如果你想看完整的代码请点击这里 如果你想体验可以扫下面的二维码自己去体验下 [图片] 下篇 我们写写怎么实现一个简单的富文本编辑器
2020-03-09 - 小程序笔记
1、通常情况下小程序的背景色backgroundColor要和页面的颜色设置成同一颜色。 2、justify-content 设置的是主轴上的对齐方式,而align-items 设置的是交叉轴上的对齐方式。通过观察flex-direction的值来判断 竖直方向还是水平方向哪一条是主轴。若flex-direction: column;那么竖直方向上为主轴,若flex-direction:row,那么水平方向上为主轴。 3、当一行内所有元素的宽度相加超过屏幕的宽度时,flex布局会将每一个元素进行压缩,以保证所有的元素都能显示在同一行内。为了让元素换行,可以使用flex-wrap: wrap。 4、小程序中的像素单位rpx可以根据设备的屏幕进行自适应。若一个字体设置成22px,那么不管设备是IP5还是IP6都会显示同样大的字,但是使用了rpx作为单位之后,在IP5上显示的字会小于IP6上的字。 5、在全局样式表(app.wxss)中定义的样式,只有font和color才会被组件继承,其他的样式都不会被组件继承(这样确保了组件的封闭性)。但是几乎所有的样式都可以被page给继承。 6、在设计组件时,尽量不要让组件产生一些无意义的空白。 7、使用bind:tap="onClick"来为页面元素绑定响应时间,此处为单击事件。 catch:tap可以阻止冒泡事件。 8、组件复用,代码分离。 9、一般不会将请求后台的代码写在组件中。如果组件中需要请求后台了应该是model的文件夹内创建相应的JS文件去请求后台,例如demo中的like.js。 10、在小程序的JS文件中,声明的data对象是该js文件中的私有变量,properties是公开的属性,外部可以访问,如果需要从外部传递进来,那么就需要将属性声明在properties中。不要在data和properties中声明相同的变量,那样会覆盖掉其中的一个变量。 11、修改data对象中的属性要使用this.setData方法来设置。 12、不要在properties的属性中,修改属性本身的值。 index: { type: Number, observer: function(newVal, oldVal, changedPath) { //当组件的值被改变时,会主动的调用observer //newVal:改变之后的值。 //oldVal:改变之前的值。 let val = newVal < 10 ? ‘0’ + newVal : newVal; this.setData({ index:val//错误的写法。会导致内存泄漏。 }); } } 13、在小程序中使用缓存的时候,要确定哪些部分是可以被缓存的,哪些是不能缓存的。(页面会实时发生变化的内容就是不能被缓存的) 14、小程序内置的标签是可以使用hidden=“{{true}}”属性来控制其显示与否的。但是开发者自己编写的组件就无法使用hidden属性来控制其显示或影藏(除非放在自己开发组件的所在WXML文件的view标签内)。 可以使用 wx:if="{{true}}" 来控制自己编写的组件的隐藏与否。 如果需要频繁的切换组件的显示或隐藏,那么微信官方推荐使用hidden,而如果不是频繁切换的话,那么微信官方推荐使用wx:if来控制组件的显示或隐藏。 15、在老板小程序中存在模板template这个概念,在template中可以提取共用的wxml和wxss内容实现组件的元素共用。新的小程序中,可以创建一个common.wxss文件。然后再所要引用的wxss文件中,使用@import “…/common.wxss”;将样式导进来 在小程序中播放音乐有两种方式,老的那种方式存在一定的bug,建议采用新的播放方式:背景音乐播放管理wx.getBackgroundAudioManage()来做。 16、小程序中的behavior可以多继承,当父类中存在一个属性,并且子类中也声明了该属性时,子类中的属性会覆盖掉父类中的那个属性(两个属性的名字相同,但是类型不同)。但是声明周期函数不会覆盖,而是以此执行。 17、使用 @import “…/common.wxss”; 可以为wxml文件引入公用的样式。 18、16和17分别解决了在小程序中复用js和wxss的问题,在wxml也可以通过模板的方式进行复用,但是在组件中复用wxml的话带来的意义并不是特别的大。 19、navi组件和music组件之间的通信,可以通过classic组件进行传递。子组件(navi)通过事件的方式将数据传递给父组件(classic),然后父组件再传递给另外一个子组件(music)。 20、wx:key 如果wx:for后面遍历的是一个object,可以使用object下的某一个属性来作为wx:key的取值,且该属性需要满足不重复且是数字或者是字符串。如果wx:for需要遍历的是一个数组或者字符串的话,那么wx:key后面的取值是*this。 21、在小程序中使用wx.navigateTo()进行小程序页面的跳转。让组件去进行页面的跳转会降低组件的通用性。如果在主页面中进行跳转,需要在组件的js文件中使用triggerEvent将组件内的参数传递到主页面,再在主页面中进行页面跳转。如何取舍?如果编写的组件不会和其他的项目进行共用,那么就可以在组件内部进行页面跳转。 22、组件之间的传参是通过properties中的属性进行传参的。而页面之间的传参是通过onload生命周期函数的options参数中。const id=option.id。就能接收到从外部传入的id了。 23、slot,插槽。感觉上像是一个占位符,可以在组件的外部向组件内部传递一个wxml标签。如果不传递,也不会有任何的显示。 在组件中需要声明属性 options:{ multipleSlots:true }, <view class=‘container’> <text>{{text}}</text> <slot name=“after”></slot> //这里预留一个插槽。 </view> 使用的时候需要将传入的标签,包裹在组件标签的内部: <block wx:for="{{comments}}"> //block用于循环,不是slot的相关知识点。 <v-tag text="{{item.content}}"> <text slot=“after”>{{’+’+item.nums}}</text> //after是插槽的名字。 </v-tag> </block> 24、在v-tag标签中加入属性 tag-class=“ex-tag”,然后在tag组件的js文件中写上externalClasses:[‘tag-class’],然后在再tag.wxml文件的view标签中增加class=‘tag-class’,这样就可以引入外部样式了。如果标签中存在多个样式,那么可能会造成冲突。样式加不上去。如果外部样式想要覆盖普通样式,可以使用!important就可以覆盖普通样式了。 .ex-tag{ background-color:#effbdd !important; } 25、WXS相当于在html标签中直接调用JS代码,可以用来写小程序的过滤器。小程序中的WXS只支持ES5的语法。WXS中文本并不默认解析转义字符例如 。当需要解析这些转义字符时,可以在调用过滤器的标签上添加属性decode。例如:<text class=‘content’ decode=’{{true}}’>{{util.format(book.summary)}}</text> 中的 decode=’{{true}}’ 26、下滑加载更多有两种实现方式:scroll-view和Page的onReachBottom,推荐使用onReachBottom。onReachBottom在组件中无法使用,所有要在Page中使用,并结合12点中的observer来实现下滑后,加载更多内容的动作。(在组件中定义一个属性,然后生成随机字符串或者随机数,在page中每次触发onReachBottom后,更改组件中属性的值,从而使用observer)。 27、小程序中,属性的名字尽量不要用驼峰命名法。在js中声明了一个属性openType,在wxml文件中使用的时候,使用连字符来调用。open-type=“xxxx”。 28、获取授权:老版本使用wx.getUserInfo来弹窗询问是否授权。新版本需要使用小程序中的组件button来获取授权。 在.wxml文件中,使用<button open-type=“getUserInfo” bindgetuserinfo=“getUser”>授权</button>
2020-03-09 - 让你的微信开发者工具编辑器支持「书签」功能,快速跳转到指定文件指定行和列
如果你用过Visual Studio,Intellij IDEA,Eclipse,甚至Delphi等相关编辑器的话相信你会用到里面一个非常方便的功能:书签功能。所谓的书签功能就是临时给某行/某段需要高频查看/修改的代码做一个mark,当你修改完其他代码需要专回来的时候就不用去人工找了,直接书签到这个mark处。 这里要说的实现微信开发者工具编辑器也有书签功能的方法是使用第三方编辑器扩展:alefragnani.bookmarks。 使用后的部分功能截图如下: [图片] 1、加入/取消当前行到书签列表 2、跳转上一个书签 3、跳转下一个书签 [图片] F1载入微信开发者工具编辑器的命令输入框,输入book模糊搜索书签插件的功能列表 [图片] 以列出所有书签功能为例,点击进入该功能后弹出当前工作区所有文件里面你设置的书签。 上下箭头移动到对应item后可以直接预览该处的书签 最后说一说安装这个第三方编辑器扩展的方法: 动手打造更强更好用的微信开发者工具-编辑器扩展篇 | 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000a8816e18748dd52f96f8975b413
2020-03-09 - 小程序的图片,一定要固定大小才能显示出来。那为什么携程的列表可以显示不同尺寸的大小呢?
最近在思考,写点什么呢?!想了很久,决定了 还是仿!!!这次我打算用原生的方式仿携程的酒店列表(未完)。 却无意中发现,携程的列表居然有不同大小的图片~~~~~~~~~~~~~~ 虽然,我不知道他的实现方式,是如何的。 但是我找到了解决的办法。也证实了我的想法。 那就是css里的flex。可能根据内容的伸缩改变图片的高度,代码简单如下 .p-item border-top: 6rpx solid #f5f5f5 background: #fff ss-display-flex(row nowrap, flex-start) overflow: hidden .item-pic flex: 1 image width: 240rpx height: 100% .item-content flex-grow: 1 padding: 25rpx 20rpx 15rpx 20rpx 实现效果: [图片] 真机效果可以搜索SAUI,或者扫以下码《实际项目之酒店列表》(刚推,正在审核。效果应该隔天才能看到) [图片]
2020-02-28 - 小程序取消橡皮筋回弹效果解决方案及坑总结
提到ios系统的橡皮筋效果,作为开发者是又爱又恨,有想要这个效果又有不想要的,无奈的是却没有一个简单的开关来设置这个效果是否开启。 最近在开发小程序时也遇到有关于ios橡皮筋回弹的问题,这里分两部分(取消橡皮筋回弹效果和因为这个效果遇到的坑)和大家分享一下。 取消IOS橡皮筋回弹效果的解决方案 1) 页面无滚动区域时,可通过页面json配置文件设置disableScroll:true禁止整个页面滚动,从而取消橡皮筋效果。 [代码]{ "disableScroll":true } [代码] 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo1 2) 页面有滚动区域,滚动区域通过view模拟实现,然后在页面json配置文件设置disableScroll:true禁止整个页面滚动,从而取消橡皮筋效果。 [代码]json文件配置 { "disableScroll":true } view元素模拟实现滚动样式 { height: calc(100vh - 120rpx); //高度必须是固定的值 overflow-y: auto; } [代码] 不足之处在于view元素模拟的滚动区域滚动时不够连贯,没有scroll-view那种原生丝滑般的感觉。 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo2 3) 页面有滚动区域,滚动区域使用scroll-view,这时通过disableScroll则无法实现,尝试设置一下scroll-view的scroll-y="{{false}}",上拉或下拉时居然不再触发橡皮筋的回弹啦,当然滚动区域也不能滚动。 小脑袋动一动,解决方法有啦! 通过设置一个变量scrollY动态控制滚动区域的滚动从而阻止回弹。 监听bindscrolltoupper\bindscrolltolower当scroll-view区域滚动到顶部或底部时候设置scrollY:false来关闭页面滚动,从而阻止回弹。 监听bindtouchstart\bindtouchmove 当用户反方向滑动的时候设置scrollY:true,再次开启页面滚动。 [代码]wxml滚动区域属性和事件处理,具体实现请点击测试代码链接 <scroll-view scroll-y="{{scrollY}}" class="list" upper-threshold="5" lower-threshold="5" bindscrolltoupper="bindscrolltoupper" bindscrolltolower="bindscrolltolower" bindtouchstart="touchstart" bindtouchmove="touchmove"> <view class="list-item" wx:for="{{list}}" wx:key="{{index}}">{{item}}</view> </scroll-view> [代码] 相对view模拟实现滚动区域,scroll-view滚动更丝滑,不过每次滚动到底部或顶部的时候,再反向滑动时由于再次开启scroll-view滚动会有操作卡顿的感觉,暂时没想到好的解决方法,有解决的大佬希望提供一下想法,一起学习下。 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo3 IOS橡皮筋效果遇到的坑 1) 操作左滑删除组件时上下移动,会触发橡皮筋效果导致页面抖动的问题 这个坑的严重程度看设计师的意愿了,反正我们团队目前是需要解决的,方案类似取消橡皮筋解决方案的第三种 在左滑的时候关闭scroll-view的滚动,取消时再次开启滚动 2) 如果页面顶部有置顶的横向滚动区域scroll-view,当页面滚动到底部时继续上拉会导致置顶头部消失,松开回弹后头部又会出现。 坑是社区里的朋友提出来的,我借了个iphone x 一预览,我嚓,还是真是个奇坑! 微信官方回复已复现正在解决中… 不想继续等下去的,暂时解决方法是 监听页面的滚动区域,当滚动到底部时设置顶部横向滚动scroll-view的scroll-x=false来解决。 写在最后 以上便是我在小程序开发中有关于ios橡皮筋回弹效果的分享,示例代码已上传github,可自行下载体验。 https://github.com/YuniorZen/minicode-debug/tree/master/minicode01 目前微信官方虽说已经着手解决(已两年)此类bug,但哪吒说 我命由我不由天,所以还是我们开发者多分享些解决方案自救来的快。 分享方案如有问题还望不吝指出,没有涉及到的坑也欢迎评论提出,一起学习和解决,后续也会基于此篇不断更新总结。
2021-01-14 - 极致的scroll-view的下拉刷新扩展组件
不敢说是最好的,但是感觉也应该是性能和体验比较极致的下拉刷新扩展了,老规矩,代码片段放最后了~ 2020.2.22 修复了小程序基础库v2.10.2带来的不能滚动的问题,最新代码片段见scroll-view-extends 原理 其实原理很简单,和普通H5以及市面上有的下拉刷新没有特别大的区别,都是基于[代码]touch[代码]手势检测事件来实现下拉刷新的。[代码]touchstart[代码]的时候记录当前触摸点,[代码]touchmove[代码]的时候开始计算移动方向和移动距离, [代码]touchend[代码]的时候计算是否要进行下拉刷新操作。如图所示: [图片] 实现方法 调研了一些实现方法,目前大部分都是通过js计算,然后setData来改变元素的[代码]transform[代码]值实现下拉刷新。考虑到性能问题,此处使用了[代码]wxs[代码]的响应式能力来实现整个计算逻辑,不用通过逻辑层和视图层通信,直接在视图层进行渲染。具体文档请参考wxs响应事件。 这里在[代码]list[代码]组件(由[代码]scroll-view[代码]组成)下抽出了一个[代码]scroll.wxs[代码]作为响应事件的事件处理函数集合,源码基本上就在[代码]scroll.wxs[代码]和[代码]list[代码]组件。 [代码]scroll.wxs[代码]定义了如下变量和函数: [代码]var moveStartPosition = 0 //开始位置 var moveDistance = 0 //移动距离 var moveRefreshDistance = 60 //达到刷新的阈值 var moveMaxDistance = 100 //最大可滑动距离 var isRefreshMaxDown = false //是否达到了最大距离, 用来判断是否要震动提示 var loading = false //是否正在loading ... ... module.exports = { touchStart: touchStart, //手指开始触摸事件 touchMove: touchMove, //手指移动事件 touchEnd: touchEnd, //手指离开屏幕事件 loadingTypeChange: loadingTypeChange, //请求状态变化监听,监听刷新请求开始和请求完成 triggerRefresh: triggerRefresh //主动触发刷新操作,比如点击页面上一个按钮,重新刷新list,这就需要用到这个方法 } [代码] [代码]touchStart[代码]和[代码]touchMove[代码]就不用说了,代码注释都很明白,普通的监听移动和处理逻辑。 [代码]touchEnd[代码]主要是判断移动距离是否达到了阈值,然后根据结果,调用监听实例的[代码]callMethod[代码]方法触发[代码]refreshStart[代码]或者[代码]refreshCancel[代码]方法,这两个方法都是写到[代码]list[代码]组件里面的,用来触发刷新方法或者取消刷新。 [代码]loadingTypeChange[代码]方法主要是监听刷新是否完成,以此来触发动画效果。 [代码]triggerRefresh[代码]通过监听主动触发的变量来处理。如果需要主动触发刷新,则调用[代码]list[代码]组件内部的[代码]forceRefresh[代码]方法,具体使用示例在[代码]index/index/js[代码]的[代码]onLoad[代码]函数有: [代码]this.selectComponent('.list').forceRefresh()[代码] [代码]scroll.wxs[代码]里面还有一个未导出的方法,叫[代码]drawTransitionY[代码],这个方法主要是因为[代码]ios12[代码]对于[代码]transition[代码]动画效果支持的不好,所以自己写了个Y轴方向的动画([代码]linear[代码]线性的),大佬们可以自己往上添加各种[代码]ease-in-out[代码]效果。 里面具体的实现可以查看代码注释哦~ 使用 好了,前面讲了实现的原理和方法,那么在代码里面,应该怎么直接使用呢?如下代码所示: [代码]<!-- 使用示例 --> <list class="list" refresh-loading="{{refreshLoading}}" loading="{{loading}}" bindrefresh="initList" bindloadmore="loadmore"> <!-- your code --> </list> [代码] [代码]refresh-loading[代码]属性用来通过外部loading态来控制刷新动画的开始结束,因为每当变化[代码]refresh-loading[代码]的值时,会将变化同步到组件内的[代码]showRefresh[代码]属性,[代码]wxs[代码]通过监听[代码]showRefresh[代码]来处理动画逻辑。 [代码]loading[代码]属性是上拉加载更多的时候触发的loading态展示,跟刷新无关 [代码]bindrefresh[代码]是刷新触发时绑定的函数,下拉刷新动画成功开始后触发这个函数 [代码]bindloadmore[代码]透传[代码]scroll-view[代码]的加载更多方法 当然,源码里面也包含了一个[代码]list-item[代码]组件,这个跟本文没太大关系,是用来做瀑布流长列表内容太多时的内存不足问题解决方案的,具体请看解决小程序渲染复杂长列表,内存不足问题 干货 最后,上代码片段, 小程序代码片段 github地址
2020-02-22 - 动手打造更强更好用的微信开发者工具-编辑器扩展篇
1. 写在前面 1.1 微信开发者工具现状 具备一些基本的通用IDE功能,但是第三方的支持扩展需要加强。 1.2 开发者工具自带的编辑器扩展功能 可能很多老铁没用过官方的微信开发者工具的编辑器扩展(我一般称为编辑器插件)。官方把这块功能也隐藏得很深,也没有相关文档介绍,但是预留了相关的入口。合理利用第三方编辑器插件,可以极大的提升开发效率。下面先来看看官方预留的编辑器插件入口: [图片] (图一) 2. 几个不错插件安装效果 2.1 标签高亮插件-vincaslt.highlight-matching-tag [图片] 功能:可以把当前行对应的标签开头和结尾高亮起来,让开发者一目了然 2.2 小程序开发助手插件-overtrue.miniapp-helper [图片] 功能:必须要说的这个是纯国产的插件,里面的代码片段功能很全,具体介绍:小程序开发助手 - Visual Studio Marketplace https://marketplace.visualstudio.com/items?itemName=overtrue.miniapp-helper 2.3 minapp插件-qiu8310.minapp-vscode [图片] 功能:这个是今天的明星插件,里面的跳转功能很强,可以在wxml里CMD+点击对应变量/方法和CSS样式名称直接跳转到对应的js/wxss文件对应的地方。具体的下面是官方介绍: 标签名与属性自动补全 根据组件已有的属性,自动筛选出对应支持的属性集合 属性值自动补全 点击模板文件中的函数或属性跳转到 js/ts 定义的地方(纯 wxml 或 pug 文件才支持,vue 文件不完全支持) 样式名自动补全(纯 wxml 或 pug 文件才支持,vue 文件不完全支持) 在 vue 模板文件中也能自动补全,同时支持 pug 语言 支持 link(纯 wxml 或 pug 文件才支持,vue 文件不支持) 自定义组件自动补全(纯 wxml 文件才支持,vue 或 pug 文件不支持) 模板文件中 js 变量高亮(纯 wxml 或 pug 文件才支持,vue 文件不支持) 内置 snippets 支持 emmet 写法 wxml 格式化 3. DIY添加适合自己的插件 3.1 添加插件功能简介 仔细研究过微信开发者工具的人可能知道或者了解,其实微信开发者工具编辑器跟微软的开源编辑器vsCode「颇有渊源」。再深入研究发现,vsCode的插件完全可以无缝移植到微信开发者工具编辑器里来,所以今天的内容就是移植vsCode的插件到微信开发者工具。咱们先看看微信开发者工具自带的「管理编辑器扩展」功能(图1标注为2的地方) [图片](图二) 3.2 插件添加具体步骤 3.2.1 安装插件,获取插件文件 安装vsCode并安装你需要移植的插件,必须要说的是vsCode的插件非常多,好的插件也很多。相关安装,搜索插件教程建议大家百度相关教程。或者直接下载vsCode亲自体验,插件安装过程还是非常简单的。 3.2.2 复制插件文件夹 找到vsCode相关插件的安装文件夹: 操作系统 安装路径 windows %USERPROFILE%.vscode\extensions macOS ~/.vscode/extensions Linux ~/.vscode/extensions 复制对应插件文件夹到微信开发者工具的「打开编辑器扩展目录」(图一标注为1的地方) 3.2.3 添加插件配置文件 新版开发者工具直接进入图形设置,扩展设置里勾选对应插件即可。如下图: [图片] 旧版操作方法:进入微信开发者工具的「管理编辑器扩展」功能页面,在尾端加入对应添加的插件名称。以以上3个介绍的插件为例,在原来的尾端加入: “vincaslt.highlight-matching-tag”, “overtrue.miniapp-helper”, “qiu8310.minapp-vscode” 3.2.4 见证奇迹 重启微信开发者工具,见证插件带来的编码便利吧! 4 需要注意的 vsCode的插件很多,小程序相关的也越来越多了,但是插件质量参差不齐,所以安装时建议选择「标星」star比较多的插件。
2020-05-02 - 点击wxml里面的绑定事件名/变量名/样式名直接跳转到对应js/wxss文件的相关代码
1. 简单说明 1.1 实现后的效果文字说明 这里要说的实现方法是使用微信开发者工具的编辑器扩展功能,安装并使用第三方编辑器扩展工具实现如题所示功能。实现点击wxml里面的相关方法,如bindtap=“XXX”,CMD+鼠标左键点击XXX(WINDOWS下为CTRL+鼠标左键)直接跳转到对应.js里面此方法名称处。 1.2 实现后的效果动图演示 [图片] 2. 实现方法/插件介绍 标签名与属性自动补全 根据组件已有的属性,自动筛选出对应支持的属性集合 属性值自动补全 点击模板文件中的函数或属性跳转到 js/ts 定义的地方(纯 wxml 或 pug 文件才支持,vue 文件不完全支持) 样式名自动补全(纯 wxml 或 pug 文件才支持,vue 文件不完全支持) 在 vue 模板文件中也能自动补全,同时支持 pug 语言 支持 link(纯 wxml 或 pug 文件才支持,vue 文件不支持) 自定义组件自动补全(纯 wxml 文件才支持,vue 或 pug 文件不支持) 模板文件中 js 变量高亮(纯 wxml 或 pug 文件才支持,vue 文件不支持) 内置 snippets 支持 emmet 写法 wxml 格式化 DIY添加适合自己的 3. 安装第三方编辑器扩展教程: 动手打造更强更好用的微信开发者工具-编辑器扩展篇
2020-05-02 - 社区每周|社区每周反馈与1月份社区突出贡献者奖励公示(02.03-02.07)
各位微信开发者: 以下是 02.03-02.07 我们在社区收到的问题反馈、需求的处理进度,以及2020年1月份社区突出贡献者奖励公示,希望同大家一同打造小程序生态。 上周问题反馈和处理进度(02.03-02.07)修复中的问题在开发者工具能正常显示背景色,在真机显示不出来(小游戏排行榜)的问题 查看详情 网页上Markdown语法错乱的问题 查看详情 未认证订阅号接口设置自定义菜单问题的问题 查看详情 注册小程序绑定新管理员,提示这些微信超过5个的问题 查看详情 PC端小程序 websocket 的 onMessage 数据格式不一致的问题 查看详情 小程序后台->开发->业务数据监控「查看开发文档」链接错误的问题 查看详情 企业微信小程序调用 InnerAudioContext 对象无法播放音频,报错 errCode:10001 的问题 查看详情 开发者工具云开发环境初始化报错,一直显示初始化中的问题 查看详情 链接不的问题准 查看详情 “wx:key”介绍里有错误,举例内容没有显示出来的问题 查看详情 文档中云函数端获取集合数目的问题 查看详情 文档内容出错的问题 查看详情 文档排版格式问题 查看详情 文档链接404的问题 查看详情 关于组件 detached 生命周期函数触发时期在文档中的错误的问题 查看详情 需求反馈需求评估中 关于 Laya 引擎插件的需求 查看详情 社区2020.01月度突出贡献者名单公示 以下为上个月(2020年01月)社区月度“突出贡献者”名单,具体用户主页可从社区首页右侧公示版块中进入: [图片] 为了鼓励大家持续为社区生产优质内容,以上社区月度突出贡献者将获得如下奖励: 一份微信官方正版周边礼物小程序极速审核奖励*注1:极速审核机制说明:获得突出贡献者前已绑定的--作为管理员或开发者的小程序,在获得突出贡献者后将可享受一个月极速审核的奖励。小程序将在2小时内审核完毕,工作时间:周一到周五,9点-21点;周六周日,9点-19点; *注2:2020年1月份由于春节假期原因,登录的企业用户较其它月份占比降低,故该月企业突出贡献者比例相应有所下降; *注3:受近期新冠肺炎疫情影响,实物奖励可能会延迟发货。 微信团队 2020.02.14
2020-02-14 - 有没有大佬有微信小程序商城文档的案例呀,开发遇到了问题,想借鉴一下,谢谢。?
[图片]
2020-02-17 - 小程序流量主运营技巧
前言(写给入坑的小白) 本文不涉及任何需要资质的小程序(如:视频类目)。小程序流量主是个人和小微企业主要变现途径之一,满1000人即可开通流量主(登录mp.weixin.qq.com,左侧边栏-推广-流量主-开通即可)。开通后,开发者可从流量主-广告位管理添加广告位,目前有6种广告位。 [图片] 正文(本文约很多字,分为四大主类,手里有1-10个小程序建议全部看完;手里有10个以上小程序,可跳过1、2、3,均为个人观点,不喜使劲喷) 一、小程序定位 小程序定位目前有以下四种,均不需要任何资质,个人(商城除外)/小微企业都可以做,由于本人不擅长文字表达,每个类型只选择一个做分析,谅解。 1、工具类 工具类有很多可以做:题库、技术文档、教程、去水印等。目前最火爆的应该属于疫情相关类的工具,关于疫情数据类小程序不做分析,没资质也没权利,主要说疫情周边可运营的工具。头像口罩,代表小程序:头像加口罩、戴个口罩吧、戴上口罩(每日搜索量约等于2000),可参考以下做法: [图片] 以下为近7日访问数据量 [图片] 盈利方式:流量主 延伸参考:如果仅做头像加口罩的话,那么疫情过后,这个小程序会直线下降,将无任何作用。如果开发者手里目前有类似小程序,可参考“头像加口罩”做法,逐渐去延伸头像周边功能,例: ①、头像加字:头像+数字、头像加V、头像加字、头像加圣诞帽、新年头像边框、头像加福、头像加明星等 ②、聊天背景图、壁纸:武汉加油、卡通、美女(不要漏点太多)、二次元、跑车、科技等 ③、趣味九宫格配图:类似朋友圈9张图,中间获取用户自己头像,周围8张图弄点能吸引用户的等 ④、文字秀:微信昵称下标、上标、个性昵称等 运营分析:如果参考以上4点做法,首先你的程序再疫情结束后,不至于直线下滑,最起码能留住一些用户(UI很重要) 个人建议:工具类的好处就是不需要去长时间盯着后台,建议有想法的开发者,可以入门5-10个左右工具类小程序(功能不要相同)。 推广方式:参考本文第四大板块内容 2、返利类 主流返利平台:淘宝、天猫、拼多多、京东、蘑菇街、唯品会、网易考拉,以下参考 [图片] 盈利方式:返利(主)+流量主(辅) [图片] 基础分析:每个人微信里都会有一个或多个微信群是给你们购物优惠券链接的,他们盈利方式主要是靠每个平台的返利,比如淘宝天猫的叫“阿里妈妈”、拼多多的叫“多多进宝”等 运营分析: ①、平台功能:提供所有优惠券、商品返利、代理入驻、提现(个人可做收款码、企业可对接微信支付到零钱) ②、招代理商、可以给代理商(兼职、宝妈)50%以上的返利 ③、除了商品优惠券之外,可以把返利分给一部分给到用户。首先,用户会花更少的钱买到商品;其次,用户买完东西还会赚点小钱,每个月可提现到微信零钱。这样用户会发生裂变,省钱+赚钱。 个人建议:开发者至少有一个类似的返利小程序,每个月只需运营一天,工作内容一是把用户的返利发给用户&代理商,二是自己去各大平台领取每个月的“工资” 推广方式:参考本文第四大板块内容 3、商城类(个人开发者可跳过) 商城类,本人运营的比较少,每天就10-20单左右,卖啥就不做广告了 盈利方式:差价 基础分析:如果自己手里有一些商品低价资源,可以做一个“综合服务商城类目”,然后去试着用广告主去推一下 运营分析: ①、平台功能:砍价、返利、拼团、回购、入驻、积分、抽奖、游戏营销 ②、广告主曝光&点击报价不要最低,也不要最高,理由就是最低的话,80%的钱会给你推到一些质量很差的微信用户,比如我。 ③、对接圈子,虽然圈子刚起步,不确定能不能做大,万一呢? 个人建议:企业一定要有一个自己的商城,哪怕没人买。这种东西怎么说呢,就好比一个企业站,虽然没什么用,但是得放那儿,万一客户要看呢? 推广方式:参考本文第四大板块内容 4、游戏类(非小游戏) 答题、成语、找茬等类似运营的比较多,可自行搜索,不要认为这是游戏,开发者就望而却步,在线教育类目是可以通过的,这个开发者很多都不知道。以下可参考: [图片] 盈利方式:流量主 基础分析:基本所有的模式都是闯关类型,这种类型的小程序,基本都是用户消磨时间用 运营分析:关卡尽量多,入门、初级、中级、高级,高级模式可以做类比循环,形成无限关卡模式,闯关奖励机制,签到机制等。这种类型的小程序比较方便运营,裂变起来也快。 个人建议:裂变模式一定要有,虽然微信会严格把控这方面功能,但是开发者可以做一些技巧,不要让用户强制或者主动去触发,这样微信对开发者还是很友好的。 推广方式:参考本文第四大板块内容 二、小程序开发 有实力的开发者,自己开发,云开发很快,会前端就可以了,没实力的去正规平台买源码,论坛源码也很多,有部分论坛还是嵌入了比特币勒索,自己做好防护。个人建议:开发者能开发尽量自己开发,后期迭代方便,不要像我一样,50多个小程序80%是买现成去运营的。反正各有各的好处,开发者可自行决定,运营者可选择直接购买源码直接上线运营,前提是自己看好功能是不是和自己要的一样。有些SAAS平台的开发者实力还是可以的,支持定制功能。此处不做广告,自行搜索或者询问朋友。 三、广告位位置及利润 开发者的每个页面广告位一定要分开!一定要分开!一定要分开!这样做的目的是为了分析每个广告位的利润,好去做调整,把收益最大化。 失败案例举例:小程序的主页、个人中心页用同一个banner广告位,这样做出来一点好处都没有,后台只能看到banner收益是多少,看不到是哪个页面收益。极端情况,收益全部再首页,个人中心页没有广告收益,这种情况开发者是不知道的,如果把广告位分开,这种情况可以去优化个人页面,或者主页面换成视频banner。广告位分析页面:流量主--数据统计--广告数据--广告指标明细--细分数据 [图片] [图片] 1、很多人表示,疫情期间流量主收入下滑。这个原因不是因为微信调整流量主收益,根本问题是自己的用户质量。举个例子,当你开通流量主之后,你的用户还是这1000个,假如你第一天收益为100,你很开心,1000用户就能赚100,你第二天就放弃推广了,这样的话,你的用户质量是会逐渐下滑,微信方完全可以认定为你这1000人都是自己的号,去刷广告费的。长此以往下去,你的流量主利润会无限趋向于0。举个栗子: [图片] 2、广告位位置一定要合理好看,但是不代表“流氓”,比如全明星代言的某游戏“元宝无限收一刀999”点哪儿哪充值。开发者需要注意的是小程序的质量,需要用户在每个页面停留的时长最起码30秒,这样一个完整的视频广告才能曝光完。 3、banner广告收益是按有效点击计算的。很多人好几千曝光,但是点击只有几个、十几个,这种情况需要不断去优化接入的场景/位置,提高用户点击意愿。个人技巧:banner广告位尽量不要太多,1-2个就可以。尽量多放几个视频广告位,这样曝光也有收益。格子广告没试过,用过都说不好~ 4、激励广告作为流量主最高收益是有一定道理的,用户为了获取某些奖励是必须观看完整的,所以给开发者建议:用户如果可以获得小程序内某些奖励,可以适当多放一些激励广告位。 5、所有的广告位都是根据用户年龄、爱好等参数去调取相应的广告,开发者不需要去考虑 6、广告收益个人认为:激励》视频》插屏》前贴》banner》格子(格子没试过,暂放倒数第一) 四、小程序推广 尽量做成年人主打的小程序,有些开发者觉得好玩儿,做一些儿童益智类的小程序,你是认为儿童有手机,还是认为家长愿意让孩子玩儿手机呢?这个很不解。没有鄙视的意思,也许是情怀吧~~毕竟我做小程序比较俗,就是为了赚钱。 主流推广方式:公众号引流、截流,由于涉及一些不合常规的内容,本文只说常规操作,剩下的自己领悟,或者可以联系我~ 首先小程序的名字至关重要,一个好的名字可以带来无限的流量,再加上裂变功能(邪恶的微笑)。起名字的时候可以用到的工具:搜索小程序-微信指数,查询关键字,尽量找稳定再1000万以上的搜索量,从关键字中摸索自己的小程序名字。这样用户搜索到你的小程序几率会很高~ 1、工具类核心玩儿法(适用于所有小程序推广):文章引流,截取关键字,火爆主题,比如2019年12月19日庆余年全集泄露、2020年疫情(不要发疫情数据内容,要发一些正能量的有内容文章去引流),我阅读过的文章最低的阅读量8000左右,最高的10万+,据说有好几百万的阅读量。如果你的文章写的好,结尾放一个小广告:为防止疫情蔓延,请给您的头像带上口罩~,啪,一个卡片小程序(或二维码),流量自己想~ 推广对象:18-30岁 2、返利类核心玩儿法: ①、可以参考工具类玩儿法 ②、各大微信群、QQ群,去推广,招代理等方式,或者去买一些基础流量,进行裂变,实际运营看下效果,好继续针对用户群体去推广,建立自己的群体系,群内发商品返利链接。微信好友没人?给你举个例子,我这篇文章发完,如果加个我的二维码,最起码能有100人加我,不是我文章写的有多好,是你永远不知道用户有什么样的目的和需求~ 推广对象:18-60岁 3、商城类核心玩儿法 ①、可参考返利类核心玩儿法,拥有自己的客户群体系,发一些自己的商品还是可以的,一定要带分销体系,你懂得~(最高3级,再高就是传销了) ②、广告主、目前效益个人感觉不明显,每次花1000块钱做广告,利润基本没有,和发广告的钱持平,而且用户留存也不是很高,可能是我的商品比较单一等各方面因素吧,不过赚流量还是不错的。 推广对象:18-30岁(以我的商城为例,还需看商城出售的内容) 4、游戏类核心玩儿法(非小游戏) ①、一个好的名字就够了。举例:精选商品橱窗(腾讯官方),微橱窗(我朋友的)。不得不说,这波流量很高,遗憾的是,他不是火爆的游戏类小程序~ [图片] ②、参考工具类玩儿法,文章引流截流 推广对象:18-40岁 五、小程序矩阵 矩阵一定要有,矩阵一定要有,矩阵一定要有,防截流,底配10个小程序。不是纯矩阵,是微信开发规定,每个小程序可以跳转10个小程序,开发者可以利用这个功能去添加自己的矩阵来获取更多的流量收益,保证自己的用户在自己的矩阵圈活动。 [图片] 写这篇文章主要是给大家传授经验,希望小白能学到点东西,入门后的朋友可领悟到更多运营方法,江湖之大,附月账单有缘再见 [图片]
2020-05-25 - 多张图片上传(源码分享+实现分析)
本篇文章以小程序中的代表【微信小程序】为例,分享一下在微信小程序中实现多图上传的源码实现。 代码片段(可导入微信WEB开发者工具体验):https://developers.weixin.qq.com/s/DHrt69mk7af3 两种不同实现方法的优缺点,请查看我的 博客原创文章,在文章中有详细的说明 小程序 多张图片上传 文章地址:https://blog.csdn.net/u013350495/article/details/104326088。 源码: const app = getApp() Page({ data: { // 已选择上传的本地图片地址 urlArr:['helang.jpg','1846492969.jpg','web.jpg'] }, onLoad: function () { }, // 多图上传-回调式 uploadCallback(){ let index = 0; // 当前位置,标识已上传到第几张图片 let newUrls = []; // 上传成功后的图片地址数组 // 图片上传方法 let upload = ()=>{ let nowUrl = this.data.urlArr[index]; //当前待上传的图片地址 wx.showLoading({ title: '正在上传', }); /* 无图片上传接口,收setTimeout 模拟延迟状态 项目中替换为 wx.uploadFile 即可 */ // 假设每 1000ms 上传一张图片 setTimeout(()=>{ // 此处为已上传成功后的回调函数内容 let resUrl = `服务器返回上传后的地址 ${nowUrl}`; //假设这是上传成功后返回的地址 newUrls.push(resUrl); // 将上传后的地址添加到成功数组中 // 判断图片是否已经全部上传完成 if (index >= (this.data.urlArr.length-1)){ send(); }else{ //未全部上传完时标识位置+1并再次调用上传方法 index++; upload(); } },1000); } // 发送方法,用作图片上传完后,得到图片地址提交给其它接口或其它操作 let send = () => { // 关闭加载提示 wx.hideLoading(); wx.showToast({ title: '上传成功', icon:'success' }) // 输出已经上传完的图片地址,请查看控制台结果 console.log(newUrls); } // 调用上传方法 upload(); }, // 多图上传-Promise uploadPromise(){ /* Promise 对象数组 */ let p_arr = []; /* 新建 Promise 方法,nowUrl参数为当前上传的图片地址 */ let new_p = (nowUrl) => { return new Promise((resolve, reject) => { /* 无图片上传接口,收setTimeout 模拟延迟状态 项目中替换为 wx.uploadFile 即可 */ // 假设每 1000ms 上传一张图片 setTimeout(()=>{ // 此处为已上传成功后的回调函数内容 let resUrl = `服务器返回上传后的地址 ${nowUrl}`; //假设这是上传成功后返回的地址 resolve(resUrl); },1000); }) } // 遍历数据,创建相应的 Promise 数组数据 this.data.urlArr.forEach((item, index) => { let nowUrl = this.data.urlArr[index]; //当前待上传的图片地址 p_arr.push(new_p(nowUrl)); }); wx.showLoading({ title: '正在上传', }); /* 所有图片上传完成后调用该方法 */ Promise.all(p_arr).then((res) => { // 关闭加载提示 wx.hideLoading(); wx.showToast({ title: '上传成功', icon: 'success' }) // 输出已经上传完的图片地址,请查看控制台结果 console.log(res); }); } })
2020-02-15 - 微信小程序自定义tabBar组件开发
1.新建template文件夹用于保存tabBar模板,template/template.wxml [代码]<template name="tabBar"> <view class="tabBar"> <block wx:for="{{tabBar}}" wx:for-item="item" wx:key="tabBar"> <view class="tabBar-item"> <navigator open-type="reLaunch" url="{{item.pagePath}}"> <view><image class="icon" src='{{item.iconPath}}'></image></view> <view class="{{item.current== 1 ? 'tabBartext' :''}}">{{item.text}}</view> </navigator> </view> </block> </view> </template> [代码] 2.创建template.wxss [代码].icon{ width:54rpx; height: 54rpx; } .tabBar{ width:100%; position: fixed; bottom:0; padding:10rpx; margin-left:-4rpx; background:#F7F7FA; font-size:24rpx; color:#8A8A8A; box-shadow: 3rpx 3rpx 3rpx 3rpx #aaa; z-index: 9999; } .tabBar-item{ float:left; width:20%; text-align: center; overflow: hidden; } /*当前字体颜色*/ .tabBartext{ color:red; } .navigator-hover{ background-color: rgba(0, 0, 0, 0); } [代码] 3.创建template.js,初始化数据 [代码]//初始化数据 function tabbarinit() { return [ { "current": 0, "pagePath": "/pages/dashboard/index", "iconPath": "/images/goback.png", "text": "返回商城" }, { "current": 0, "pagePath": "/pages/collage/index", "iconPath": "/images/collage1.png", "selectedIconPath": "/images/collage.png", "text": "拼团首页" }, { "current": 0, "selectedIconPath": "/images/list.png", "iconPath": "/images/list1.png", "pagePath": "/pages/collage-list/index", "text": "活动列表" }, { "current": 0, "selectedIconPath": "/images/collage-order.png", "iconPath": "/images/collage-order1.png", "pagePath": "/pages/collage-order/index", "text": "我的订单" }, { "current": 0, "selectedIconPath": "/images/group.png", "iconPath": "/images/group1.png", "pagePath": "/pages/group/index", "text": "我的团" } ] } //tabbar 主入口 function tabbarmain(bindName = "tabdata", id, target) { var that = target; var bindData = {}; var otabbar = tabbarinit(); otabbar[id]['iconPath'] = otabbar[id]['selectedIconPath']//换当前的icon otabbar[id]['current'] = 1; bindData[bindName] = otabbar that.setData({ bindData }); } module.exports = { tabbar: tabbarmain } [代码] 4.使用方法 先把样式文件载入app.wxss [代码]@import "/template/template.wxss"; [代码] 新建一个页面,比如index.wxml,引入模板 [代码]<import src="../../template/template.wxml"/> <template is="tabBar" data="{{tabBar:bindData.tabBar}}"/> [代码] index.js 初始化数据 [代码]var template = require('../../template/template.js'); Page({ onLoad: function () { template.tabbar("tabBar", 0, this)//0表示第一个tabbar }, }) [代码] 其它新建页面也跟index.wxml一样,初始化数据。 效果如图 [图片]
2020-04-09 - 小程序如何定位所在城市,如何发起周边搜索
app上常见的自动切换本地城市,还有见到有些小程序中个人账号可以获取位置服务,整理了一些封装方法 先封装request 为小程序get/post的promise封装 rq.js [代码]/* * url {String} 请求地址接口 * data {Object} 请求参数 * param {Object} request参数 * method {String} 指定使用post或者是get方法 */ export function request(url, data={}, param={}, method='POST') { return new Promise((resolve, reject)=>{ let postParam = { url, method, data, // timeout // dataType // responseType header: { 'content-type': 'application/json' // 默认值 }, success(res) { resolve(res) }, error: function (e) { reject(e) } } postParam = Object.assign(postParam, param) postParam.fail = postParam.error if (postParam.url) wx.request(postParam) }) } module.exports = { get(url, data, param){ return request(url, data={}, param={}, method='GET') }, post(){ return request.apply(null, arguments) } } [代码] 位置服务方法 需要开通小程序的位置服务功能,在小程序控制台 [图片] 简单的封装了三个位置服务的方法 所在地城市 地区搜索 范围搜索 [代码]// request的promise封装 const iKit = request('./rq.js') // key为开通位置服务所获取的查询值 // 下面这个key是随便写的 let key = 'JKDBZ-XZVLW-IAQR8-OROZ1-XR3G9-UYBD5' /* * 搜索地区... * 搜索地区的商圈, 如搜索 kfc 广州市 * key {String} 搜索内容 * region {String} 搜索区域 */ export function searchRegion(kw, region) { let opts = { keyword: encodeURI(kw), boundary: region ? `region(${encodeURI(region)}, 0)` : '', // 0 为限定范围搜搜索,1为开放范围搜素偶 page_size: 10, // 搜索结果分页最大条数 page_index: 1, // 指定分页 key } return new Promise((resolve, rej)=>{ iKit.get('https://apis.map.qq.com/ws/place/v1/search', opts).then(res=>{ resolve(res.data.data) }) }) } /* * 搜索附近的... * 以当前位置的经纬度搜索附近商圈,如附近的酒店,快餐等等 * key {String} 搜索内容 * params {Object} 搜索参数 包含三个参数 lat纬度,lng经度,distance范围(单位米) */ export function searchCircle(kw, params={}) { let {lat, lng, distance} = params if (!lat && !lng) return if (!distance) distance = 1000 // 搜索范围默认1000米 let opts = { keyword: encodeURI(kw), boundary: `nearby(${lat},${lng},${distance})`, orderby: '_distance', // 范围搜索支持排序,由近及远 page_size: 20, // 搜索结果分页最大条数 page_index: 1, // 指定分页 key } return new Promise((resolve, rej)=>{ iKit.get('https://apis.map.qq.com/ws/place/v1/search', opts).then(res=>{ resolve(res.data.data) }) }) } // 所在地的城市,省份等区域信息 /* * 所在地的城市,省份等区域信息 * 如当前地址所在省份、城市 * lat {Number} 纬度 * lng {Number} 经度 */ export function myCity(lat, lng) { return new Promise((resolve, rej)=>{ let opts = { location: `${lat},${lng}`, key } iKit.get(`https://apis.map.qq.com/ws/geocoder/v1/`, opts).then(res => { resolve(res.data.result) }) }) } [代码] 调用 [代码]wx.getLocation({ type: 'wgs84', success(location) { locationPosition = location // 所在地信息 myCity(location.latitude, location.longitude).then(res => { let myCityInfo = res.ad_info let {city, nation, province, city_code, adcode} = myCityInfo console.log({title: `国家: ${nation},省份: ${province},城市: ${city}`}) }) // 附近搜索 searchCircle('快餐', { lat: location.latitude, lng: location.longitude, distance: 500 }).then(res=>{ console.log(res) }) // 地区搜索 searchRegion('酒店', '广州').then(res=>{ console.log(res) }) } }) [代码] 关注小程序 [图片]
2020-02-10 - 新富文本组件
mp-html小程序富文本组件 news欢迎加入 QQ 交流群:699734691示例小程序添加获取组件包功能[图片] 功能介绍 支持在多个平台使用 支持丰富的标签(包括 table、video、svg 等) 支持丰富的事件效果(自动预览图片、链接处理等) 支持锚点跳转、长按复制等丰富功能 支持大部分 html 实体 丰富的插件(关键词搜索、内容编辑等) 效率高、容错性强且轻量化使用方法1. npm 方式 在项目根目录下执行 npm install mp-html 开发者工具中勾选 使用 npm 模块 并点击 工具 - 构建 npm 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "mp-html" } } 在需要使用页面的 wxml 文件中添加 <mp-html content="{{html}}" /> 在需要使用页面的 js 文件中添加 Page({ onLoad() { this.setData({ html: 'Hello World!' }) } }) 2. 源码方式 将源码中的代码包(dist/mp-weixin)拷贝到 components 目录下,更名为 mp-html 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "/components/mp-html/index" } } 后续步骤同上 获取github 链接:https://github.com/jin-yufeng/mp-html npm 链接:https://www.npmjs.com/package/mp-html 文档链接:https://jin-yufeng.gitee.io/mp-html
2022-03-04 - 教你怎么监听小程序的返回键
更新:2020年7月28日08:51:11 基础库2.12.0起,可以调用wx.enableAlertBeforeUnload监听原生右上角返回、物理返回以及wx.navigateBack时弹框提示 AIP详情请看: https://developers.weixin.qq.com/miniprogram/dev/api/ui/interaction/wx.enableAlertBeforeUnload.html //======================================== 怎么监听小程序的返回键? 应该有很多人想要监听用户的这个动作吧,但是很遗憾,小程序不会给你这个API的,那是不是就没辙了? 幸好我们还可以自定义导航栏,这样一来我们就可以监听用户的这一动作了。 什么?这你已经知道啦? 那好咱们就不说自定义导航栏的返回监听了,说一下物理返回和左滑?右滑?(不管了,反正是滑)返回上一页怎么监听。 监听物理返回 首先说一下这个监听方法的缺点,虽说是监听,但是还是无法真正意义上的监听并拦截来阻止页面跳转,页面还是会返回上一页,而后重新载入刚刚的页面,如果这不是你想要的,那可以不用往下看了 其次说一下用到什么东西: wx.onAppRoute、wx.showModal 最后是一些主要代码: 重写wx.showModal,主要是加个confirmStay参数和使wx.showModal Promise化 [代码]const { showModal } = wx; Object.defineProperty(wx, 'showModal', { configurable: false, // 是否可以配置 enumerable: false, // 是否可迭代 writable: false, // 是否可重写 value(...param) { return new Promise(function (rs, rj) { let { success, fail, complete, confirmStay } = param[0] param[0].success = (res) => { res.navBack = (res.confirm && !confirmStay) || (res.cancel && confirmStay) wx.setStorageSync('showBackModal', !res.navBack) success && success(res) rs(res) } param[0].fail = (res) => { fail && fail(res) rj(res) } param[0].complete = (res) => { complete && complete(res) (res.confirm || res.cancel) ? rs(res) : rj(res) } return showModal.apply(this, param); // 原样移交函数参数和this }.bind(this)) } }); [代码] 使用wx.onAppRoute实现返回原来的页面 [代码]wx.onAppRoute(function (res) { var a = getApp(), ps = getCurrentPages(), t = ps[ps.length - 1], b = a && a.globalData && a.globalData.pageBeforeBacks || {}, c = a && a.globalData && a.globalData.lastPage || {} if (res.openType == 'navigateBack') { var showBackModal = wx.getStorageSync('showBackModal') if (c.route && showBackModal && typeof b[c.route] == 'function') { wx.navigateTo({ url: '/' + c.route + '?useCache=1', }) b[c.route]().then(res => { if (res.navBack){ a.globalData.pageBeforeBacks = {} wx.navigateBack({ delta: 1 }) } }) } } else if (res.openType == 'navigateTo' || res.openType == 'redirectTo') { if (!a.hasOwnProperty('globalData')) a.globalData = {} if (!a.globalData.hasOwnProperty('lastPage')) a.globalData.lastPage = {} if (!a.globalData.hasOwnProperty('pageBeforeBacks')) a.globalData.pageBeforeBacks = {} if (ps.length >= 2 && t.onBeforeBack && typeof t.onBeforeBack == 'function') { let { onUnload } = t wx.setStorageSync('showBackModal', !0) t.onUnload = function () { a.globalData.lastPage = { route: t.route, data: t.data } onUnload() } } t.onBeforeBack && typeof t.onBeforeBack == 'function' && (a.globalData.pageBeforeBacks[t.route] = t.onBeforeBack) } }) [代码] 改造Page [代码]const myPage = Page Page = function(e){ let { onLoad, onShow, onUnload } = e e.onLoad = (() => { return function (res) { this.app = getApp() this.app.globalData = this.app.globalData || {} let reinit = () => { if (this.app.globalData.lastPage && this.app.globalData.lastPage.route == this.route) { this.app.globalData.lastPage.data && this.setData(this.app.globalData.lastPage.data) Object.assign(this, this.app.globalData.lastPage.syncProps || {}) } } this.useCache = res.useCache res.useCache ? reinit() : (onLoad && onLoad.call(this, res)) } })() e.onShow = (() => { return function (res) { !this.useCache && onShow && onShow.call(this, res) } })() e.onUnload = (() => { return function (res) { this.app.globalData = Object.assign(this.app.globalData || {}, { lastPage: this }) onUnload && onUnload.call(this, res) } })() return myPage.call(this, e) } [代码] 在需要监听的页面加个onBeforeBack方法,方法返回Promise化的wx.showModal [代码]onBeforeBack: function () { return wx.showModal({ title: '提示', content: '信息尚未保存,确定要返回吗?', confirmStay: !1 //结合content意思,点击确定按钮,是否留在原来页面,confirmStay默认false }) } [代码] 运行测试,Oj8K 是不是很简单,马上去试试水吧,效果图就不放了,静态图也看不出效果,动态图懒得弄,想看效果的自己运行代码片段吧 代码片段 https://developers.weixin.qq.com/s/hc2tyrmw79hg
2020-07-28 - [有点炫]自定义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 - 微信小程序中的日期格式在Android和iOS真机下兼容性问题的坑
问题提出 Android和iOS在日期格式上的处理方式有所不同,这个不同也影响到小程序的相关日期时间函数,最常用的应该是new Date()了。如下代码: date=new Date(‘2020-01-16 22:00:00’).getTime(); 上面这行代码在Android和开发者工具(windows/mac)以及开发者工具的真机调试模式(iOS/Android)都不会有问题,但是一上到体验版或者正式版的iOS上手机你获取到的数据就不是你想要的值了! 原因分析 因为iOS只支持2020/01/01 这种日期格式,不支持2020-01-01这样的格式,而现在很多后端处理日期的格式是2020-01-01,发送过来的,或者自己小程序前端生成的也是这种格式,new Date()就会出现兼容性问题。 解决方法 直接替换大法吧,把-换成/,封装成一个自己的方法来new Date()吧。具体代码不用我写你也会的。 总结 这个问题前天一个社区的朋友发了一个相关的帖子,最后查实就是这个原因导致。但是解决这个问题的过程花费了太多时间。因为这个问题很难查出问题所在,因为他在开发者工具和安卓机,以及开发者工具的「真机调试」模式下都不会出现,隐藏得很深。
2020-01-16 - 针对新手很容易出现理解误区的微信小程序订阅消息模块
1. 写在前面 微信小程序下架了模板消息功能,取而代之的是订阅消息功能。这个订阅消息目前又分为「一次性订阅」和「永久订阅」。使用订阅消息也有一段时间了,感觉对新手订阅消息很容易让新开发者进入一个理解的误区,这里觉得有必要说出来 2. 理解误区 很多新手认为,只要用户勾选了小程序端订阅消息弹出时底部的「总是保持以上选择…」后,就可以「为所欲为」的不限次数的推送订阅消息给用户了。如下图: [图片] 3. 正确理解 如果你使用的「一次性订阅」模板(目前发现绝大多数开发者都是只能用一次性的,因为永久性的订阅消息申请门槛太高),那么勾选底部的「总是…」这个并不代表以后可以直接推送了。官方原话wx.requestSubscribeMessage的介绍里是这样写的: 3.1 官方说明 wx.requestSubscribeMessage(Object object) 基础库 2.8.2 开始支持,低版本需做兼容处理。 调起客户端小程序订阅消息界面,返回用户订阅消息的操作结果。当用户勾选了订阅面板中的“总是保持以上选择,不再询问”时,模板消息会被添加到用户的小程序设置页,通过 wx.getSetting 接口可获取用户对相关模板消息的订阅状态。 注意事项 一次性模板 id 和永久模板 id 不可同时使用。 低版本基础库2.4.4~2.8.3 已支持订阅消息接口调用,仅支持传入一个一次性 tmplId / 永久 tmplId。 2.8.2 版本开始,用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。 2.10.0 版本开始,开发版和体验版小程序将禁止使用模板消息 fomrId。 3.2 重点关注 这里重点关注第7条:「用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。」这就意味着你需要在用户主动点击某个组件是触发调用wx.requestSubscribeMessage方法再次订阅,订阅后,你才可以「为所欲为」推送一次模板消息,注意只能一次。下次再想推送时,需要用户再次点击触发wx.requestSubscribeMessage。 4. 破局方案 目前订阅消息功能,就是这么个情况,所以针对这个情况的替代方案有以下 4.1 永久性订阅消息 如果能达到申请「永久性订阅」消息的模板的门槛,那自然是极好的,直接用永久性模板「为所欲为」。 4.2 使用服务号的模板消息替代 比较常用的是使用公众号服务号的模板消息代替小程序的订阅消息功能,公众号的模板消息功能限制就比订阅号好多了,基本上可以「为所欲为」的推送。但是这个方案有个致命的运营成本:必须要用户关注公众号,还有小程序要跟公众号同一主体并绑定在开放平台下。同时开发成本有所增加,要采用unionId机制来打通小程序跟公众号的openId。这个具体的实现方案,大家有兴趣的话可以讨论下。笔者目前就是用这种方案的。 5. 几个注意点 5.1 官方提示 订阅消息如果选择选择‘总是保持以上选择,"不再询问"后的设置问题: 目前是选择‘总是保持以上选择,"不再询问"后,可以在设置中开启或拒绝接收,但不会再次拉起授权弹窗 6. 长期性订阅消息 请参考官方最新文档: 小程序模板消息能力调整通知 | 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/00008a8a7d8310b6bf4975b635a401 长期性订阅消息 一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。 目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。 7.题外话 鉴于被戴上各种「刷赞,冲级,让社区点赞“通货膨胀”」等等一些恶毒字眼(最近多了个职业回复的「雅称」),各种帽子戴得,做一个开发爱好者积极分享和解决各种问题太难了,姑且不论咱写一篇文章需要截图多少,单单排版就得废掉俺多少时间哈,很受伤,所以本人决定在微信开放者社区封笔。你看到是俺最后一篇发表在微信开放社区的文章。如果你想继续查看俺的一些文章可以私聊我。我会在其他平台保持继续创作。bye-bye~ 8. 最最重要的来了 看完后觉得有用记得点赞~~ ↓点赞处↓
2020-09-04