个人案例
- 【云开发·云函数】分层(layer)实现代码复用,云函数之间共用底层文件
导读 当我们有很多云函数时,其中可能很多 [代码]中间件[代码] 、 [代码]工具[代码] 是共用的。 把这些 [代码]中间件[代码] 、 [代码]工具[代码] 复制到每个云函数下,是一件很麻烦的事情,而且如果发生了更新,还要重新复制。最重要的是,这种方式,不符合科学的设计模式。 云函数里的 层(layer) 就可以很好的解决我们这个问题。它是把我们共通需要的 [代码]中间件[代码] 、 [代码]工具[代码] 等文件放在一个公共的路径下,使用过程中只需要引用进来即可。 注意: 层暂时还没出现在小程序官方文档,但是可以在 [代码]腾讯云 - 云函数[代码]找到文档。 实操 这里我们使用 [代码]Nodejs8.9[代码] 来做示范 1. 先确认小程序已经开通了云开发 2. 登录腾讯云 打开登录界面 选择【其他登录方式 - 微信公众号 - 扫码选择对应的小程序(不是公众号)】. [图片] 注意:小程序云开发和腾讯云的账户体系其实是有关联的,可以把腾讯云面板看作更高级的云开发面板 3. 编写层代码 编写以下代码,并保存为 [代码]demo.js[代码] [代码]exports.main = async (event) => { return "hello,layer." } [代码] 把 [代码]demo.js[代码] 打包成 [代码]demo.zip压缩包[代码] 4. 创建分层 打开云函数面板. 点击左侧菜单的『层』 地域选择『上海』 点击『新建按钮』 层名称:demo 提交方法:本地上传zip包 层代码:选择上一步打包的 [代码]demo.zip[代码] 压缩包 运行环境:Nodejs8.9 [图片] 点击提交 注意: 有些压缩软件压缩后,会多一层文件夹,需要注意下,压缩包内打开应该直接就是文件 [代码]demo.js[代码] 没有多一层文件夹。 5. 小程序·云开发面板创建云函数 创建云函数 [图片] 修改 [代码]云函数 demo 下的 index.js[代码] 代码并上传 [代码]var hello = require("demo.js") exports.main = (event, context, callback) => { return hello.main() }; [代码] 6. 云函数绑定层 回到 腾讯云 云函数面板,选择左侧菜单 [代码]函数服务[代码] 注意左上角的 命名空间 选择小程序云开发的命名空间,地域:上海 选择刚创建的 [代码]demo[代码] 云函数 点击 [代码]层管理[代码] 绑定刚创建的 [代码]demo[代码] 层 7. 调试 还是 [代码]demo[代码] 的云函数里,点击 [代码]函数代码[代码] 项 点击测试,返回结果 [代码]"hello,layer."[代码] 既成功 [图片] Q&A 问:为什么云函数里可以直接[代码]require("demo.js")[代码] 引入 [代码]demo.js[代码] ? 答:绑定层后,会根据顺序把层文件放在系统目录 [代码]/opt[代码] 下,然后该目录在 Node.js 环境下又在环境变量里。当引入 [代码]demo.js[代码] 在当前目录找不到时,会尝试在环境变量内查找。所以云函数可以找到该文件。 再问:如果层的文件名和云函数列表的文件名冲突,岂不是会引入错误? 答:是的,这时候可以使用绝对路径确保引入的是层文件 [代码]require("/opt/demo.js")[代码] 再再问:如果云函数绑定了多个层,每个层里都有 [代码]demo.js[代码] 会怎么样? 答:会根据层的顺序,逐个覆盖文件,最终 [代码]/opt/demo.js[代码] 文件是最后一层的文件。 再再再问:环境变量除了这个路径,还有哪些路径? 答:这里有一份各语言的环境变量路径,可以参考下,具体查看文档 关环境变量 路径 THONPATH [代码]/var/user:/opt[代码] ASSPATH [代码]/var/runtime/java8:/var/runtime/java8/lib/*:/opt[代码] DE_PATH [代码]/var/user:/var/user/node_modules:/var/lang/node6/lib/node_modules:/opt:/opt/node_modules[代码] 问:云函数只支持 [代码]Nodejs8.9[代码] 版本吗? 答:在小程序云开发面板里,确实只能创建。但是可以通过服务端 SDK 创建以下语言支持。 Python 3.6 Python 2.7 Nodejs 10.15 Nodejs 8.9 Nodejs 6.10 Php 7.2 Php 5.6 Java 8 Golang 1 持续更新… 参考资料 腾讯云·云函数文档
2020-05-26 - 微信小程序云开发数据库脚手架
介绍 1、封装、简化、模型化数据库操作 2、支持查看执行的sql语句 基础库 大于2.8.1 代码片段DEMO 小程序demo: https://developers.weixin.qq.com/s/oXiGFFmI7mia 说明文档: https://www.yuque.com/docs/share/80cbef90-f262-4d2a-b245-079bc462d5e3 如何使用 一、小程序端 1、下载wx-cloud-db-falsework npm i wx-cloud-db-falsework 2、将wx-cloud-db-falsework.min.js拷贝到小程序项目中,如小程序根目录下的lib文件夹内 [图片] 3、在app.js中引入wx-cloud-db-falsework.min.js,使用重写过的Page,和原生Page一致,只是增加了use属性 [代码]// 根目录/app.js import { page } from './lib/wx-cloud-db-falsework.min' Page = page App({ onLaunch: function () {} } [代码] 4、创建模型,如: Order,根目录/model/order.js,集合名,默认为model名,首字母会强制小写 [代码]// 根目录/model/order.js import { Model, Controller } from '../lib/wx-cloud-db-falsework.min' class Order extends Model { constructor(o) { let t = super(o) return t } } // collectionName 集合名,默认为model名,如Order默认为order,首字母会小写 // collectionKey 集合主键,默认_id const Odr = Order.init({ env: '环境ID', cloud: wx.cloud [, collectionName = '集合名称', , collectionKey = '主键字段名'] }) export { Odr as Order, Controller } [代码] 5、创建控制器,如: Order的controller,根目录/controller/order.js [代码]// 根目录/controller/order.js import { Order, Controller as Base } from '../model/order' class Controller extends Base { constructor(o){ super(o) } // 在控制器里可以自己封装一些业务功能方法 getById(id){ return Order.find(id) } } let controller = new Controller(Order) export { controller } [代码] 6、在页面中通过use配置需要使用的控制器,控制器实例会挂载在页面实例上,直接通过 this.控制器实例 即可访问,页面: 根目录/pages/index/index.js [代码]// 根目录/pages/index/index.js import { DB } from '../../lib/wx-cloud-db-falsework.min' Page({ use:{ Order: require('../../controller/order.js') }, data:{}, onLoad: async function() { // 直接通过 this.控制器实例 即可访问控制器的封装的方法 await this.Order.getById(1).then(res=>{ console.log(res) // res 可为 DB.DbError实例 或者 DB.DbRes实例 // 为DB.DbError实例,表示数据库操作异常 // 为DB.DbRes实例,表示数据库操作正常,而DB.DbRes又包括了DB.DbResultJson和DB.DbResultArray子实例 // DB.DbResultJson实例说明结果为JSON对象,DB.DbResultArray实例则说明结果为Array对象 }) // 页面中也可以通过 this.(控制器实例名+'Md')访问模型的方法 let row = await this.OrderMd.find('xxxx') // 页面中也可以通过 this.(控制器实例名+'Dd')访问原生db对象,进行原生操作 let doc = await this.OrderDb.collection('xxx').doc(xx).get() } } [代码] 二、云函数端 1、配置云函数package.json,添加wx-cloud-db-falsework [代码]"dependencies": { "wx-cloud-db-falsework": "latest", "wx-server-sdk": "^2.1.2" } [代码] 2、配置云函数package.json,添加wx-cloud-db-falsework [代码]const cloud = require('wx-server-sdk') let { Model, DB } = require('wx-cloud-db-falsework') # order模型 class Order extends Model { constructor(o) { let t = super(o) return t } } exports.main = async (event, context) => { const Odr = Order.init({ env: '环境ID', cloud }) return await Odr.add({ _id: (new Date().getTime())+'add'}) } [代码] 数据库操作 添加记录 以上述Order模型为例,在页面中添加记录 [代码]// 使用model.add(data)添加记录, data为数据json对象或array对象 // 添加一条记录 let res = await this.OrderMd.add({ a:1, b:2 }) // 返回DbResultJson对象 {_id:'xxxx'} // 添加多条记录 res = await this.OrderMd.add([{ a:1, b:2 }, { a:3, b:4 }]) // 返回DbResultArray对象 [{_id:'xxxx'},{_id:'xxxx'}] // 支持原生参数 res = await this.OrderMd.add({ data:{ a:1, b:2 } }) res = await this.OrderMd.add({ data: [{ a:1, b:2 }, { a:3, b:4 }] }) // 支持回调函数 this.OrderMd.add({ data:{ a:1, b:2 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.add({ data:[{ a:1, b:2 }, { a:3, b:4 }], success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 删除记录 以上述Order模型为例,在页面中删除记录 [代码]// 使用model.remove(where)删除记录, where为删除添加,可为空 // 根据条件删除记录 let res = await this.OrderMd.where({ _id: 'xxx', abc: 123 }).remove() res = await this.OrderMd.remove({ _id: 'xxx', abc: 123 }) // 根据主键删除 res = await this.OrderMd.doc('xxx').remove() // 返回DB.DbError对象或DB.DbRes对象 {removed:Number} // 支持回调函数 this.OrderMd.where({ _id: 'xxx', abc: 123 }).remove({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.remove({ where:{ _id: 'xxx', abc: 123 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 查询记录 查询一条记录 以上述Order模型为例,在页面中查询记录 [代码]// 使用model.find()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ _id: 'xxx', abc: this.OrderMd._.eq(123) }).find() res = await this.OrderMd.find({ _id: 'xxx', abc: this.OrderMd._.eq(123) }) // 根据主键查询记录 res = await this.OrderMd.doc('xxx').find() res = await this.OrderMd.find('xxx') // 返回DB.DbError对象或DB.DbRes对象 {_id:'xxx', ....} // 使用model.where().get()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ _id: 'xxx' }).get() // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}] // 根据主键查询记录 res = await this.OrderMd.doc('xxx').get() // 返回DB.DbError对象或DB.DbRes对象 {_id:'xxx', ....} // 支持回调函数 this.OrderMd.find({ where:{ _id: 'xxx', abc: 123 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ _id: 'xxx', abc: 123 }).find({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ _id: 'xxx' }).get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.doc('xxx').get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 特殊支持: [代码]// json字符串条件 let res = await this.OrderMd.find(`{ _id: 'xxx' }`) res = await this.OrderMd.where(`{ _id: 'xxx' }`).find() // 使用command时需要加this res = await this.OrderMd.find(`{ _id: this._.eq('xxx') }`) res = await this.OrderMd.where(`{ _id: this._.eq('xxx') }`).find() [代码] 查询多条记录 [代码]// 使用model.findAll()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ abc: 'xxx', abc: this.OrderMd._.eq(123) }).findAll() res = await this.OrderMd.findAll({ abc: 'xxx', abc: this.OrderMd._.eq(123) }) // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}] // 使用model.where().get()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ _id: 'xxx' }).get() // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}] // 支持回调函数 this.OrderMd.findAll({ where:{ abc: 'xxx' }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ abc: 'xxx' }).findAll({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ _id: 'xxx' }).get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.doc('xxx').get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 特殊支持: [代码]// json字符串条件 let res = await this.OrderMd.findAll(`{ abc: 'xxx' }`) res = await this.OrderMd.where(`{ abc: 'xxx' }`).findAll() // 使用command时需要加this res = await this.OrderMd.findAll(`{ abc: this._.eq('xxx') }`) res = await this.OrderMd.where(`{ abc: this._.eq('xxx') }`).findAll() [代码] 更新记录 以上述Order模型为例,在页面中查询记录 [代码]// 使用model.update()更新记录 // 根据条件更新记录 let res = await this.OrderMd.where({ _id: 'xxx', abc: this.OrderMd._.eq(123) }).update({ cde: 456 }) res = await this.OrderMd.update({ cde: 456 }, { _id: 'xxx', abc: this.OrderMd._.eq(123) }) // 根据主键更新记录 res = await this.OrderMd.doc('xxx').update({ cde: 456 }) // 返回DB.DbError对象或DB.DbRes对象 {updated:Number} // 支持回调函数 this.OrderMd.where({ _id: 'xxx', abc: this.OrderMd._.eq(123) }).update({ data:{ cde: 456 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 聚合操作 以上述Order模型为例,在页面中查询记录 [代码]// 使用model.aggregate()开始聚合操作 let res = await this.OrderMd.aggregate() .addFields({ test: 1 }) .match({ test: 1 }) ... // 其他聚合操作 .end() // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}, ...] [代码] 数据监听 以上述Order模型为例,在页面中监听数据 [代码]// 使用model.wcthis(ops)监听记录, let res = await this.OrderMd.wcthis({ onChange(res){ // 监听操作 }, onError(err){ // 监听出错 } }).where({ _id: 'xxx' }).update({ cde: 456 }) // 使用原生watch方法监听记录, await this.OrderMd.where({ _id: 'xxx' }).watch({ onChange(res){ // 监听操作 }, onError(err){ // 监听出错 } }) let res = this.OrderMd.where({ _id: 'xxx' }).update({ cde: 456 }) [代码] 数据库操作结果 数据库操作结果返回 DB.DbError实例 或者 DB.DbRes实例 add结果 res = model.add(…) 添加一条数据返回 { _id: ‘xxx’ } ,添加多条时返回 [{ _id: ‘xxx’ }, { _id: ‘xxx’ }] 返回的结果可以直接调用update,remove方法,如: [代码]// 添加一条记录 let res = await this.OrderMd.add({ a:1, b:2 }) // 返回DbResultJson对象 {_id:'xxxx'} if(res instanceof DB.DbRes){ // 更新操作,更新刚新增的这条记录 let back = res.update({c: 555}) // 删除操作,删除刚新增的这条记录 back = res.remove() } // 添加多条记录 res = await this.OrderMd.add([{ a:1, b:2 }, { a:3, b:4 }]) // 返回DbResultArray对象 [{_id:'xxxx'},{_id:'xxxx'}] if(res instanceof DB.DbRes){ // 更新操作,更新刚新增的这几条记录 let back = res.update({c: 555}) // 删除操作,删除刚新增的这几条记录 back = res.remove() } [代码] remove结果 res = model.remove() 删除记录返回 { removed:Number } [代码]// 删除记录 let res = await this.OrderMd.remove({ a:1, b:2 }) // 返回DbResultJson对象 {removed:Number} if(res instanceof DB.DbRes){ if(res.removed){ // 删除成功 }else{ // 删除失败 } }else{ consloe.errr(res) } [代码] find/findAll/get结果 res = model.find() 返回 { _id: ‘xxx’, … } res = model.findAll/get() 返回 [{ _id: ‘xxx’, … }, { _id: ‘xxx’, … }, …] 返回的结果可以直接调用update,remove方法,如: [代码]// 添加一条记录 let res = await this.OrderMd.doc('xxx').find() // 返回DbResultJson对象 {_id:'xxxx'} if(res instanceof DB.DbRes){ // 更新操作,更新这条记录 // let back = res.update({c: 555}) // 删除操作,删除这条记录 let back = res.remove() } // 添加多条记录 res = await this.OrderMd.where({abc:123}).findAll() res = await this.OrderMd.where({abc:123}).get() // 返回DbResultArray对象 [{ _id: 'xxx', ... }, { _id: 'xxx', ... }, ...] if(res instanceof DB.DbRes){ // 更新操作,更新这几条记录 // let back = res.update({c: 555}) // 删除操作,删除这几条记录 let back = res.remove() } [代码] update结果 res = model.update() 更新数据返回 { updated: Number } [代码]// 更新记录 let res = await this.OrderMd.where({ a:1, b:2 }).update({c:555}) res = await this.OrderMd.doc('xxx').update({c:555}) // 返回DbResultJson对象 {update:Number} if(res instanceof DB.DbRes){ if(res.update){ // 更新成功 }else{ // 更新失败 } }else{ consloe.errr(res) } [代码] 查看SQL语句 以上述Order模型为例,在页面中查询记录 数据库操作结果返回 DB.DbRes 对象时,可查看执行的SQL语句 [代码]var res = await this.OrderMd.add({ a:1, b:2 }) console.log(res.sql) var res = await this.OrderMd.remove({ a:1, b:2 }) console.log(res.sql) var res = await this.OrderMd.doc('xxx').find() console.log(res.sql) var res = await this.OrderMd.where({ a:1, b:2 }).update({c:555}) console.log(res.sql) var res = await this.OrderMd.aggregate() .addFields({ test: 1 }) .match({ test: 1 }) .end() console.log(res.sql) [代码] ps: 可以拿去练练手,不保证无BUG
2020-07-18 - 腾讯地图JavaScript API GL实现文本标记的碰撞避让
以下内容转载自Crape的文章《web页面上的旋转矩形碰撞》 作者:Crape 链接:https://juejin.im/post/5eede991e51d45740950c946 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 前言 本文主要是总结一下web页面中的旋转矩形的碰撞检测,碰撞算法本身并不难,只是需要注意web坐标系在计算中的影响。碰撞检测应该是在游戏等场景中很常见且基础的功能,本文记录了在JavaScript API GL遇到了这类碰撞问题的调研和实现的过程。 需求场景 用户在地图上实现MultiLabel文本标注覆盖物时,会由于两个label坐标过近,或者地图的旋转、缩放产生的变化而相互重叠。目前label的背景色均为透明且暂时还不支持配置,文字重叠之后识别度下降很多,就计划先实现label之间的避让功能。检测到两个label碰撞时,根据优先级选择隐藏其中的一个,保证文字的可读性。 确定算法 在JSAPI GL中,label并不是在三维空间中的,而是绘制在屏幕上的,只是会根据用户视角的移动实时计算出label在屏幕坐标中所处的位置,然后在每一帧中进行绘制。label实际上就是一行文字,我们可以把它用一个矩形包围起来,当做整体计算,因为每个字之间的相对位置并不会变,这样一来label的碰撞检测实际上可以转化为二维空间内的矩形碰撞。 一般的横平竖直的矩形检测碰撞很简单,只要想清楚有哪些情况即可,不在这里赘述。但是用户可以对label进行旋转和偏移操作,普通的检测方法就不适用了,如果强行把label用一个大的水平矩形包裹起来再计算,精度损失会很多,所以调研了一下旋转矩形的碰撞检测方法。 比较常见的一种方式是通过分离轴定律(SAT:Separating Axis Theorem)来计算,分离轴定义:两个凸多边形物体,如果能找到一个轴,使得两个物体在该轴上的投影互不重叠,那么这两个物体就没有发生碰撞,这条轴可以称为分离轴。 一般不会遍历所有角度的轴,而是检测垂直于多边形每条边的轴,因为在这些轴上我们可以取到极值。对于矩形来说可以进一步简化,因为一个矩形的4条轴内有2个是重复的,所以只需要检测矩形互相垂直的两条边对应的轴就可以了。 进行判断的具体方式有两种:一是把每个矩形的4个顶点投影到一个轴上,算出该矩形最长的连线距离,判断两个矩形的投影是否重叠;二是将两个矩形的半径距离投影到轴上,然后把两个矩形中心点的连线投影到通一个轴上,判断两个矩形的半径投影之和与中心点连线投影的大小。 本文采用第二种方式计算,首先搞清楚投影的概念,引入向量来进行计算: [图片] 我们可以用单位向量来表示垂直于边线的轴,这样一个向量在轴线上的投影长度可以用该向量与投影轴上的单位向量的点积来表示。如上图,A点坐标为(xa, ya),OB为线段OA在x轴上的投影,x轴的单位向量为(1, 0),OA · x轴单位向量 = (xa, ya) · (1, 0) = xa * 1 + ya * 0 = xa。 [代码]// 如果用数组[x ,y]表示一个向量,则两个向量的点积结果可以表示为 function dot(vectorA, vectorB) { return Math.abs(vectorA[0] * vectorB[0] + vectorA[1] * vectorB[1]); } [代码] 然后就是如何表示矩形两个轴的单位向量,假设矩形以自身的中心点为原点,逆时针旋转θ,其两条相邻边的轴的单位向量如下图所示: [图片] 单位圆的半径为1,所以单位向量OA为 (cosθ, sinθ),另一条边的单位向量与OA垂直,为(-sinθ, cosθ),这两个单位向量的点积为0。但这里有一个非常重要的注意点:web页面中的坐标系与我们平时使用的坐标系不同,x轴正方向不变,y轴的正方向向下。我在最开始实现算法的过程中忽略了这个问题,导致碰撞结果不对,调试了半天才发现原因。在实际计算中,我们所使用的坐标都是web屏幕坐标系下的,轴的正方向与常用的不同,所以两个单位向量应该分别表示为 (cosθ, -sinθ), (sinθ, cosθ),如下图所示: [图片] 然后就是计算矩形的半径投影,首先明确下半径投影的概念,可以理解为矩形中心点到一个顶点的向量,在轴上的投影长度。其实就是,矩形在X轴上最远处的交点,数学上意义就是2条检测轴的投影之和。 [图片] 两个矩形检测的过程中,以其中一个矩形的检测轴为坐标系,投影另外一个矩形的检测轴。如上图所示,蓝色线段为左边矩形的半径投影,黄色线段为右边矩形检测轴。我们需要把右边2条检测轴投影到蓝色线段所在X轴的单位向量(即左边矩形的检测轴单位向量),得到投影比例,然后乘以检测轴长度(即矩形长、宽的一半),可计算出右边矩形的半径投影。红色线段则是两个矩形中心点的连线,同样需要计算它在蓝色线段所在X轴的投影长度,如果中心点连线的投影长度大于两个矩形的半径投影之和,那么在这条轴上两个矩形没有碰撞,否则发生碰撞。 检测最终是否碰撞,需要对四个分离轴都检测一次,在任何一个轴上没有碰撞,则两个矩形就没有碰撞。 实现 实际实现的过程中进行了简单的旋转矩形类,可根据实际业务需求调整,例如添加缩放、偏移等参数 [代码]class Rect { constructor(options) { const {center, height, width, angle} = options; this.centerPoint = [center.x, center.y]; this.halfHeight = height / 2; this.halfWidth = width / 2; this.setRotation(angle); } getProjectionRadius(axis) { // 计算半径投影 const projectionAxisX = this.dot(axis, this.axisX); const projectionAxisY = this.dot(axis, this.axisY); return this.halfWidth * projectionAxisX + this.halfHeight * projectionAxisY; } dot(vectorA, vectorB) { // 向量点积 return Math.abs(vectorA[0] * vectorB[0] + vectorA[1] * vectorB[1]); } setRotation(angle) { // 计算两个检测轴的单位向量 const deg = (angle / 180) * Math.PI; this.axisX = [Math.cos(deg), -Math.sin(deg)]; this.axisY = [Math.sin(deg), Math.cos(deg)]; return this; } isCollision(check) { const centerDistanceVertor = [ this.centerPoint[0] - check.centerPoint[0], this.centerPoint[1] - check.centerPoint[1] ]; const axes = [ // 两个矩形一共4条检测轴 this.axisX, this.axisY, check.axisX, check.axisY ]; for (let i = 0, len = axes.length; i < len; i++) { if (this.getProjectionRadius(axes[i]) + check.getProjectionRadius(axes[i]) <= this.dot(centerDistanceVertor, axes[i])) { return false; // 任意一条轴没碰上,就是没碰撞 } } return true; } } [代码] 使用时每个矩形实例化一个Rect类,然后调用实例上的[代码]isCollision[代码]方法,参数传入另一个矩形的实例,最后返回一个[代码]boolean[代码]类型的碰撞结果。 总结 封装的这个类比较简单,没有涉及到里面参数改变的问题,有需要的话可以再完善。实现过程中注意下web坐标系的问题就可以了。矩形应该是最简单的一种,其他凸多边形的检测会复杂一些,有兴趣的话可以自己尝试一下。 本文参考以下blog: https://blog.csdn.net/tom_221x/article/details/38457757 https://aotu.io/notes/2017/02/16/2d-collision-detection/index.html 画图工具为 GeoGebra sketch 实际效果可以在腾讯位置服务官网的示例中尝试https://lbs.qq.com/webDemoCenter/glAPI/glMarker/labelCollision 产品推广 Javascript API GL是基于WebGL技术打造的3D版地图API,3D化的视野更为自由,交互更加流畅。 提供丰富的功能接口,包括点、线、面绘制,自定义图层、个性化样式及绘图、测距工具等,使开发者更加容易的实现产品构思。 充分发挥GPU的并行计算能力,同时结合WebWorker多线程技术,大幅度提升了大数据量的渲染性能。最高支持百万级点、线、面绘制,同时可以保持高帧率运行。 同步推出基于Javascript API GL的 位置数据可视化API库,欢迎体验。
2020-07-10 - 【笔记】云开发通过客服消息实现自动回复进群,同时兼容客服小助手
小程序不具备小程序内扫描二维码的能力,因此如果要实现关注公众号或加用户群功能大家一般都利用微信客服功能的自动回复来实现。此时如果自己去实现微信客服自动回复,客服小助手就不能用了,很令人纠结。经过我的研究,借助云开发找到了一个方案,可以实现当用户想获取微信群走自动回复的接口,真正咨询时,直接到客服小助手进行回复。 效果如下 [图片] 原理解析 云开发在做消息推送配置的时候可以配置消息类型,这个时候如果我们只配置一种类型(小程序卡片),那么就只有卡片才会被云函数接管做自动回复,其他消息类型(图片、文字)则正常走小程序客服的通道。 实现步骤 1.小程序端设置按钮属性open-type="contact",用于用户点击时带上定义的卡片跳到客服消息界面。 申请加入 2.新建云端的函数,设置config.json定义权限,如下config.json { "permissions": { "openapi": [ "customerServiceMessage.send", "customerServiceMessage.uploadTempMedia", "customerServiceMessage.setTyping" ] } } 3.写云函数端代码,如下 if (event.Title == "我要进用户群"||event.Title =="关注公众号"||event) { //设置输入状态 cloud.openapi.customerServiceMessage.setTyping({ touser: OPENID, command: 'Typing' }) //从云储存中拉取图片数据 const qunimg = await cloud.downloadFile({ fileID: "cloud://pm-hsfip.706d-pm-hsfip-1259751853/img/qun.png", }) //上传图片素材到媒体库,并获取到图片id const qunmedia = await cloud.openapi.customerServiceMessage.uploadTempMedia({ type: 'image', media: { contentType: 'image/png', value: qunimg.fileContent } }) //向用户发送群二维码 await cloud.openapi.customerServiceMessage.send({ touser: OPENID, msgtype: 'image', image: { mediaId: qunmedia.mediaId, } }) //取消状态设置 cloud.openapi.customerServiceMessage.setTyping({ touser: OPENID, command: 'CancelTyping' }) } 4.设置消息推送,路径如下 云开发-设置-全局设置-云函数接收消息推送 中添加消息类型为miniprogrampage,绑定云函数为新建的云函数。 [图片] 5.微信公众平台绑定客服[图片] 注意事项 如果按照本教程 客服小助手无法收到消息 或 无法自动回复,可以先将以上消息推送配置删除,然后再微信后台绑定客服后,再重新进行消息推送配置。
2020-07-29 - 实现一个本地数据库
问题背景 对于一些变化性不强但可能多次使用的数据(如一些列表、文章信息等),如果每次都进行网络请求,不仅减慢了速度,也加重了服务器负担;这时往往需要通过 [代码]setStorage[代码] 和 [代码]getStorage[代码] 进行本地缓存,但也存在一些问题,一个是 [代码]storage[代码] 只能通过 [代码]key-value[代码] 的形式进行管理,无法进行更复杂的数据库操作,另外每次都从本地 [代码]storage[代码] 中读写效率不高 改进方式 改进读写方式 在 [代码]init[代码] 的时候读取本地 [代码]storage[代码],之后所有读写都在这个变量中进行操作,不必每次读取本地 [代码]storage[代码];在进行写入操作后,定期写回本地 [代码]storage[代码],也可以减少写入次数 存储结构 一个集合由一个 [代码]object[代码] 组成,里面的每一个键值对表示一个记录,通过 [代码]id[代码] 查询效率高([代码]id[代码] 可以自动生成,默认为 [代码]4[代码] 位由数字字母组成的随机值),通过 [代码]where[代码] 查询则需要遍历 实现数据库的 api 为减少上手难度,所有 [代码]api[代码] 都参照了 云数据库 的设置,几乎所有的方法都可以直接使用(但由于本地数据库存取都较快,所有方法都是同步方法,直接返回结果),通过这些 [代码]api[代码],可以大大便利数据的查询和设置 例程 [代码]const localDB = require('utils/localDB.js') const _ = localDB.command localDB.init() // 初始化 var articles = localDB.collection('articles') if(!articles) articles = localDB.createCollection('articles') // 不存在则先创建 // 按文章 id 查找 var doc = articles.doc('xxx') if(doc) { var data = doc.get() // 取得数据 } else { // 网络请求获取 data data._timeout = Date.now() + 15 * 24 * 3600000 // 设置过期时间为 15 天 articles.add(data) // 添加到本地数据库 } // 按类型查找 var data = articles.where({ type: 'xxx' }).get() // 正则查找 var data = articles.where({ title: /xxx/ // 标题中含有 xxx 的 }).get() // 分页查找 var page2 = articles.skip(10).limit(10).get() // 按时间查找 var data = articles.where({ date: _.gte('20200501').and(_.lte('20200510')) // 大于等于 20200501 小于等于 20200510 }).get() // 结果排序 var data = articles.orderBy('date', 'desc').get() // 按日期降序排序 // 清理过期数据 articles.where({ _timeout: _.lt(Date.now()) // 过期时间早于当前的 }).remove() [代码] github MpLocalDB
2020-05-09 - 如何只使用一个云函数搞定一个庞大而复杂的系统
吐槽 翻遍社区的文章,关于云开发的干货,少之又少,大部分都还是官方文档的搬来搬去,没啥营养,是时候放出一点技术"干货"了(有经验的开发者都能想到的方案)! 正题 小程序云开发的云函数的最大限制是 [代码]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 - 微信小程序使用GoEasy实现websocket实时通讯
不需要下载安装,便可以在微信好友、微信群之间快速的转发,用户只需要扫码或者在微信里点击,就可以立即运行,有着近似APP的用户体验,使得微信小程序成为全民热爱的好东西~ 同时因为微信小程序使用的是Javascript语法,对前端开发人员而言,几乎是没有学习成本和技术门槛的。对于大部分场景,都可以使用小程序快速开发实现,不论是开发周期还是开发成本都低的让人笑哭,所以受到了技术开发团队的各种追捧~ 但如果要在小程序里快速的实现一个即时通讯功能,就有点尴尬,因为微信官方提供的只是一个底层的websocket api,要在项目中直接使用,还需要做很多额外的工作,比如首先就需要搭建自己的websocket服务~ 那有没有简单的方式呢? 当然是有的! 今天小编就手把手的教您用GoEasy在微信小程序里,最短的时间快速实现一个websocket即时通讯Demo。 [图片] 本demo已经完成了真机下的小程序的测试,完整源代码开源到oschina的码云上,clone后,只需要将代码里的appkey换成自己的common key,就可以体验了, 源码网址:https://gitee.com/goeasy-io/GoEasyDemo-wxapp-Helloworld 1、获取appkey GoEasy官网(https://www.goeasy.io/)上注册账号,创建一个应用,拿到您的appkey。 [图片] GoEasy提供了两种类型的appkey: Common key: 即可以接收消息,也可以发送消息,与Subscribe Key最大的区别就是有写权限,可以发消息。适用于有消息发送需求的客户端和服务端开发。 Subscribe key: 只能接收消息,不可以发送消息,与Common Key最大的区别就是没有写权限,只能收消息。可以用于一些没有发送需求的客户端。 2、获取GoEasy SDK 下载 https://cdn.goeasy.io/download/goeasy-1.0.11.js [代码]import GoEasy from './goeasy-1.0.11'; [代码] 3、初始化GoEasy对象 [代码]var self = this; this.goeasy = GoEasy({ host: 'hangzhou.goeasy.io', appkey: "您的appkey", onConnected: function () { console.log("GoEasy connect successfully."); self.unshiftMessage("连接成功."); }, onDisconnected: function () { console.log("GoEasy disconnected.") self.unshiftMessage("连接已断开."); }, onConnectFailed: function (error) { console.log(error); self.unshiftMessage("连接失败,请检查您的appkey和host配置"); } }) [代码] 根据您在GoEasy后台创建应用时选择的区域,来传入不同的Host,如果您创建GoEasy应用时,选择了杭州,那么host:“hangzhou.goeasy.io”。选择了新加坡,host:“singapore.goeasy.io”。 如果您的大部分用户都是在国内,创建应用时,记得选择杭州,以便获得更快的通讯速度。 4、小程序端接收消息 [代码]var self = this; this.goeasy.subscribe({ channel: "my_channel", onMessage: function (message) { self.unshiftMessage(message.content); }, onSuccess: function () { self.unshiftMessage('订阅成功.'); } }); [代码] 很多朋友会问channel从哪里来,如何创建,应该传入什么呢? 根据您的业务需求来设定,channel可以为任意字符串,除了不能包含空格,和不建议使用中文外,没有任何限制,只需要和消息的发送端保持一致,就可以收到消息。channel可以是您直播间的uuid,也可以是一个用户的唯一表示符,可以任意定义,channel不需要创建,可以随用随弃。 5、小程序端发送消息: 发送时,需要注意channel一定要和subscribe的channel完全一致,否则无法收到。 [代码]this.goeasy.publish({ channel: "my_channel", message: self.data.message, onSuccess: function () { self.setData({ message: '' }); //清空发送消息内容 console.log("send message success"); }, onFailed: function (error) { self.unshiftMessage('发送失败,请检查您的appkey和host配置.'); } }); [代码] 本代码源码下载:https://gitee.com/goeasy-io/GoEasyDemo-wxapp-Helloworld 特别强调: 在运行之前,一定要在微信公众号平台配置socket合法域名,否则无法建立连接。具体步骤: 访问https://mp.weixin.qq.com,进入微信公众平台|小程序 -> 设置 -> 开发设置 -> 服务器域名 socket合法域名-> 添加GoEasy的地址: wx-hangzhou.goeasy.io(记得wx-开头) 若您创建GoEasy应用时选择了新加坡区域则添加地址:wx-singapore.goeasy.io 答疑时间: 1、我的服务器端可以给小程序发送消息吗?都支持些哪些语言? 当然可以,任何语言都可以通过调用GoEasy的Rest API发送消息,同时为了大家方便,GoEasy的官方文档里,也准备了Java, C#,NodeJS,PHP,Ruby和Python等常见语言调用REST API的代码,这里获取更多详情:https://www.goeasy.io/cn/doc/server/publish.html 2、GoEasy可以发送图片,语音和视频吗? 当然可以,您可以通过推送文件路径的方式来实现文件的发送。 按照行业惯例,不论MSN,微信,QQ对于图片和视频,通常的做法都是,只推送文件路径,而不会推送文件本身。你如果有注意的话,当您接受图片和视频的时候,收到消息后,等一会儿才能看,就是因为发送的时候,只发送了路径。 3、GoEasy和微信小程序官方的websocket API有什么区别和优势? 小程序官方的websocket API主要是用来与您的websocket服务通讯,所以使用小程序websocket的前提是,首先要搭建好您自己的websocket服务,然后与之通讯。这是一个纯技术的API,在建立网络连接后,还有很多的工作需要自己来完成,比如: 需要自己实现心跳机制,来维护网络连接,来判断客户端的网络连接状态; 需要自己实现断网自动重连; 需要自己维护消息列表,确保遇到断网重连后,消息能够补发; 需要自己维护一个客户端列表; 等等很多细致而繁杂的工作,比如websocket的安全机制和性能优化; 此之外服务端也有很多工作需要自己完成,有兴趣自己搭建websocket的话,可以参考这篇技术分享《搭建websocket消息推送服务,必须要考虑的几个问题》 而GoEasy是一个成熟稳定的企业级websocket PAAS服务平台,开发人员不需要考虑websocket服务端的搭建,只需要几行代码,就可以轻松实现客户端与客户端之间,服务器与客户端之间的的websocket通信,不需要考虑性能,安全,高可用集群的问题,只需要全力专注于开发自己的业务功能就好了。 GoEasy已经内置websocket中必备的心跳,断网重连,消息补发,历史消息和客户端上下线提醒等特性,开发人员也不需要自己搭建websocket服务处理集群高可用,安全和性能问题。GoEasy已经稳定运行了5年,支持千万级并发,成功支撑过很多知名企业的重要活动,安全性和可靠性都是久经考验。 4、GoEasy在小程序的开发中主要用在那些场景呢? 从应用场景上来说,所有需要websocket通信的场景,GoEasy都可以完美支持: 聊天,IM,直播弹幕,用户上下线提醒, 在线用户列表 扫码点菜,扫码登录, 扫码支付, 扫码签到, 扫码打印 事件提醒,工单,订单实时提醒 在线拍卖, 在线点餐,在线选座 实时数据展示,实时监控大屏, 金融实时行情显示,设备监控系统 实时位置跟踪,外卖实时跟踪,物流实时跟踪 远程画板,远程医疗,游戏,远程在线授课 5、GoEasy的文档为什么这么简单?简单到我都不知道如何使用 简单还不好吗?GoEasy从研发的第一天,就把追求API的极简作为我们的工作重点。严格控制接口的数量,就是是为了降低开发人员的学习成本,其实就是为了让您爽啊!但这并不影响GoEasy完美支持所有的websocket即时通讯需求。 GoEasy官网:https://www.goeasy.io GoEasy系列教程: 搭建websocket消息推送服务,必须要考虑的几个问题 websocket IM聊天教程-教你用GoEasy快速实现IM聊天 Websocket直播间聊天室教程-GoEasy快速实现聊天室 微信小程序使用GoEasy实现websocket实时通讯 Uniapp使用GoEasy实现websocket实时通讯 IM聊天教程:发送图片/视频/语音/表情
2020-05-21 - 从开发者工具 1.02.2005111 起,可以接入云支付了
官方这次的更新有点低调啊,下边是云支付的简单介绍: [图片] 小程序官方文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/wechatpay.html 以下为云支付API文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/open/pay/Cloud.CloudPay.html
2020-05-13 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - 如何在小程序中快速实现环形进度条
在小程序开发过程中经常涉及到一些图表类需求,其中环形进度条比较属于比较常见的需求 [图片] [中间的文字部分需要自己实现,因为每个项目不同,本工具只实现进度条] 上图中,一方面我们我们需要实现动态计算弧度的进度条,还需要在进度条上加上渐变效果,如果每次都需要自己手写,那需要很多重复劳动,所以决定为为小程序生态圈贡献一份小小的力量,下面来介绍一下整个工具的实现思路,喜欢的给个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 - 实现一个高亮的代码编辑框
实现效果 [图片] 实现思路 说到富文本编辑,首先想到的自然是 [代码]editor[代码] 组件了,然而 [代码]editor[代码] 组件设置 [代码]html[代码] 的方法只有 [代码]setContents[代码],但是这个方法是用来初始化内容的,每次设置都会使得光标变到开头,如果用这个方法,每输一个字符光标都会跳到开头,无法使用。 因此设想了一种新的方案,将编辑和显示分开;底层放置一个 [代码]rich-text[代码] 用于显示高亮后的代码,上层放置一个编辑器,将颜色设置为透明,字体和大小与底层一致;当编辑器输入字符时,通过高亮处理后显示在底层的 [代码]rich-text[代码] 上;这样就实现了一个高亮的代码编辑框 编辑器选择 小程序中一共有 3 种输入框,[代码]input[代码]、[代码]textarea[代码] 和 [代码]editor[代码],其中 [代码]input[代码] 只能输入单行文本,并不适合此场景;[代码]textarea[代码] 可以编辑多行文本,本是个不错的方案,然而一方面 [代码]textarea[代码] 是原生组件,会受到一些限制,另一方面,似乎在真机上给 [代码]textarea[代码] 设置字体无法生效,用默认的字体又有点丑;因此最终还是选用了 [代码]editor[代码] 高亮方案 这里选择了轻量且强大的 prismjs 代码实现 [代码]rich-text[代码] 通过 [代码]absolute[代码] 布局固定在 [代码]editor[代码] 下方,[代码]editor[代码] 被设置成透明颜色(除光标外) [代码]<view class="editor-view"> <rich-text class="highlight" nodes="{{code}}" /> <editor id="editor" class="editor" placeholder="请输入 html" bindinput="input" /> </view> [代码] 每输入一个字符,在 [代码]js[代码] 中进行高亮处理后在 [代码]rich-text[代码] 中显示 [代码]const Prism = require("./prism.js"); Page({ input(e) { // markdown 则改为 Prism.highlight(e.detail.text, Prism.languages.markdown, "markdown") this.setData({ code: "<pre style=\"color:#ccc;font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;\">" + Prism.highlight(e.detail.text, Prism.languages.html, "html") + "</pre>" }) } }) [代码] 应用场景 markdown 编辑器 [代码]markdown[代码] 编辑器作为一种简单的富文本编辑器,通过这样的方式实现就可以比较美观 富文本编辑完成后进行微调 在 [代码]editor[代码] 中编辑完成后,可以通过编辑 [代码]html[代码] 进行样式的微调 性能 在模拟器中是非常流畅的,在真机上稍有延缓,个人觉得是可以接受的(可以适当限制内容长度,过长的内容不进行高亮处理) 立即体验 代码片段
2020-02-21 - [有点炫]自定义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 - 微信开放社区正确的提问方式
调侃 某萌新:为什么我在社区提问的问题都没人回答啊 某大佬:社区的问题质量是越来越低了,真不想看社区的问题了 如何提问 今天,在下就来告诉大家如何优雅的提问: 在提问之前,先想好自己遇到的问题是什么,让想要帮助你的人,知道你的问题所在 提问语言简洁明了,详细说明重现步骤及可能存在的问题 附上问题截图,比如调试工具报错信息、提审被驳回信息 写明代码环境,如工具版本、手机版本、基础库版本等 如果调用接口报错,请直接把接口的官方文档地址贴出来,标明调用接口地址,附上请求的参数以及返回的数据 必要时请提供代码截图或代码片段 点击查看如何创建代码片段 最重要的一点:提问时请务必注意礼貌。这一点特别重要,因为这关乎到回答者决定要不要回答以及以怎么样的态度来回答 依然是重要的一点:请务必给认真回答协助解决问题的回答者一个“赞”,这会更加激起他们回答的积极性 千万不要投机,把问题提到“文章”版块,这样会被直接隐藏。不仅不会加快被解答的速度,还会耽误解答机会 示例 (仅为示例,不代表该问题实际存在) 标题:picker组件多级联动会出现选项为 null 的情况? 内容: 使用picker组件,设定为多级联动,仅滚动一级栏后确定,后面的栏目获取到的值为 null,能麻烦帮忙看下吗?最新版开发者工具,基础库版本 2.9.2 代码片段:xxxxxxxxxxxxxxxx 继续调侃 某萌新:诶?真的诶,这样提问,好多问题都被解决了 某大佬:这问题问得是真的好,多来点这样的问题,回答也舒坦
2020-01-03 - 发布后的小程序能HTTP访问局域网服务器么?
最近开发了一款小程序,但是外网服务器还没买,所以就临时让小程序访问局域网服务器,但是我印象中小程序是不支持HTTP的呀,为啥我发布后的小程序能访问我的局域网服务器
2019-12-27 - 云开发的函数 GBs 到底是如何计算的?
在云开发中,数据库的存储容量、文件存储的容量都是十分容易理解的,让开发者最为迷惑的,莫过于云函数的资源使用量 GBs。GBs 是个什么鬼?他到底是如何计算的?今天,我们就来说一说。 [图片] 传统云主机的计费模式 想要说明白云函数的计费模式,那我们就要看一看与之类似,我们很熟悉的传统的包月型云主机的计费模式。 [图片] 在传统的包月型云主机中,我们需要支付整月的费用,以保留我们对于自己所使用的服务器实例的使用权,在这个过程中,无论你的服务是否使用了云主机,你都需要支付相应的费用。但实际情况是,我们绝大多数的业务都只在全天的某个特定时段有流量,其他的绝大多数的时间都没有什么流量,比如说,一个企业门户网站,往往流量都在上班时间,下班时间几乎没有流量。 [图片] 但,传统云主机这种包月的计费模式,要求我们必须为整段时间付费,你必须要支付整段时间的费用,即使你并没有使用这些资源! 这种包月的计费模式使得用户为自己所不需要的资源支付了费用,因此,后续的人们发明了按量计费,并将按量计费的粒度不断缩小,从按日计费,缩短到按小时计费,缩短到按秒,甚至是按毫秒进行计费。随着粒度不断变细,我们支付的费用,越来越趋向于我们自己所使用的资源的费用。 云函数的计费模式 在上面我们说到,在传统云主机上,我们提供的是整段时间的计费,随着技术的先进,我们渐渐有了粒度不断变细的按量计费。 但是,在 Serverless 的范式下,用户对于云函数背后的服务器主机是无感的,这时就需要一个东西来进行用量的评估,从而完成计费。在云函数中,最重要的两个要素就是:内存用量 和 运行时间。一般来说,云函数的用量我们按照 GB 来计算,比如一个函数的内存用量是 256MB,则其内存用量表示为 0.256GB ;而时间则是以 S 来计算,如果一个函数用了 2s ,则运行时间就是 2s 。 [图片] 这个时候,我们的函数的具体资源用量就可以简单的用两个数值相乘得出 [图片] 举例说明 这里我们举个例子来说明计算。 [图片] 这里有一个函数,在日志中的右上角,我们可以看到函数的执行时间和内存使用量,分别是 1.43ms 和 27.42MB,则我们的函数资源用量就是 [代码]0.00143 x 0.02742 = 0.0000392106[代码],也就是说,我这个情况使用了 0.0000392106 GBs 的资源用量。显然,这样的用量是十分小的。 云函数的资源用量计算有什么用? 了解云函数的资源用量计算,就能够让你发现你自己的资源的具体用量,并以此得到具体的资源消耗情况,能够让你更加轻松的了解到自己的钱都花到哪里去了。更重要的是,知道在哪里查看执行时间和内存使用量可以指引你优化自己的云函数性能。 [图片]
2019-12-20 - 小程序的getElementsById,就像一把梭
使用selectComponent可以抓取自定义组件实例对象,但在层层嵌套结构的业务场景中,id的设置繁复,js/wxml开发界面频繁的切换,查找、维护、调整的开发工作很是让人抓狂啊 好想封装一个getElementsById方法给小程序,像在web开发中那样能够方便的获取页面元素。在父子子子子级间轻松调用,好想念jquery开发的一把梭时代! 实现如下需求: 任何绑定id的自定义组件都能够方便抓取实例对象(任何嵌套层级均可调用) 通过数据配置 思路 实现不难,我们可以将所有自定义组件在create生命周期方法时将[代码]this[代码]挂载到一个全局变量中,[代码]detached[代码]生命周期时销毁该实例(不然爆内存) 实现 准备一个全局变量 [代码]app._elements = {} [代码] 挂载/销毁方法 一个全局的挂载、销毁方法,方便将实例对象注册、注销在app._elements上 [代码]app.mount = function(id, context){ app._elements[id] = context } app.unmount = function(id){ app._elements[id] = null } [代码] getElementsById 定义全局[代码]getElementsById[代码]在Page中能够方便调用 [代码]app.getElementsById = function(id){ let res = app._elements[id] if (!res) { // 兼容selectComponent return wx.selectComponent('#'+id) || wx.selectComponent('.'+id) } return res } [代码] 自定义组件 ui-component组件 [代码]const app = getApp() Component({ options: { multipleSlots: true, // 在组件定义时的选项中启用多slot支持 addGlobalClass: true }, properties: { dataSource: { type: Object, }, }, data: {}, behaviors: [], lifetimes: { created: function() { }, attached: function() { this.id = this.data.dataSource.$$id // 专用$$id来指定唯一名称 }, ready: function() { app.mount(this.id, this) }, detached: function(){ app.unmount(this.id) } }, methods: { active(clsName){ /* do something */ } }) [代码] 应用 下面开始在Page中使用[代码]getElementsById[代码]来抓取自定义组件实例 wxml [代码]<ui-component dataSource="{{config}}" /> [代码] js [代码]Page({ data: { config: { $$id: 'component-id', title: 'some text' } }, onReady(){ // 我们应该在onReady中来调用,onLoad时,页面组件结构并没有渲染完成 const $ele = app.getElementsById('component-id') $ele.active('.active') } }) [代码] 至此,基本思路已经实现,现在即兼容了selectComponent方法,又简化了写模板id的麻烦。不知道大家有没有了解小程序组件是可以递归嵌套自己的(模板不能递归嵌套)。因此聪明的你应该可以想到通过数据嵌套去实现组件嵌套,进而实现结构嵌套,这样我们就能够实现很复杂的页面结构,当然小程序目前建议是结构应该在30层左右,然并卵,反正它能够正常显示,哈哈 github地址:https://github.com/webkixi/aotoo-xquery 小程序demo演示,下列小程序基于xquery的个人开发,公司的就不放了 xquery [图片] saui [图片] 嘟嘟倒计时 [图片]
2019-12-21 - 小程序的智能裁剪接口应该怎么用
在小程序的服务端接口中,有一类是图像处理接口,其中有一个接口是让大家觉得用起来很头疼的,就是 aiCrop — 图片智能裁剪这个接口。 这个接口根据官方的描述,其能力是 “本接口提供基于小程序的图片智能裁剪能力。”,但是,根据文档中给出的结果,似乎也并没有返回图片的 Buffer 流,那么这个接口真正应该怎么用呢?背后又有什么坑呢?今天我就给你讲一讲。 前置条件 你需要已经注册好小程序,并开通小程序云开发(本次演示将基于小程序云开发制作) 业务流程 [图片] 流程说明 用户侧选择图片,并生成临时文件路径(如果是网络图片,需要下载到本地,并修改云函数,改为直接传递 网络图片地址) 将图片上传的云存储中,并拿到 FileID 将 FileID 传递到云函数中,云函数获取到对应的临时 URL 将临时文件 URL 传递到微信的 AI 剪切接口 AI 接口将裁剪结果返回到云函数 云函数将裁剪结果返回到小程序 小程序基于返回结果进行渲染。 服务端代码 这里我们创建一个云函数来完成图片的裁剪,你需要创建一个新的云函数,其中[代码]index.js[代码]的代码如下 [代码]// index.js const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { let fileId = event.file; // 获取文件的临时连接 let tempUrl = await cloud.getTempFileURL({ fileList: [fileId] }) let newUrl = tempUrl.fileList[0].tempFileURL; // 对图片进行裁剪 let cropResult = await cloud.openapi.img.aiCrop({ imgUrl:newUrl, ratios:'1,2.35,0.5,0.25,3.25'//裁剪比例 }) return cropResult } [代码] 以及在该函数目录下创建一个 [代码]config.json[代码] 文件,内容如下 [代码]{ "permissions": { "openapi": [ "img.aiCrop" ] } } [代码] 这样就完成了云函数部分的内容。 上面这段代码帮助我们获取 FileID 对应的文件临时路径,并将其传递给微信接口进行调用。 小程序端调用代码 在小程序端,我们主要是选择文件,将其上传到云端,并调用云函数进行裁剪,在取得返回值后在小程序端进行渲染。 小程序的页面 JS 代码如下 [代码]Page({ /** * 由于此数据仅在逻辑层使用,因此定义一个tempData 进行存储 */ tempData:{ path:null, }, onClick() { /** * 选择文件 */ wx.chooseImage({ success: res => { /** * 获取文件路径,并传递给 tempData */ let file = res.tempFiles[0].path this.tempData.path = file console.log("[info]:开始上传文件") /** * 上传文件到云存储 */ wx.cloud.uploadFile({ filePath: file, cloudPath: "test.jpg" }).then(res => { /** * 调用云函数 */ console.log("[info]:开始调用云端裁剪") wx.cloud.callFunction({ name: "aicrop", data: { file: res.fileID } }).then(res => { /** * 调用裁剪 */ console.log("[info]:云端裁剪成功 ", res) this.crop(res.result); }).catch(err => { console.error("[error]:函数调用错误", err) }) }).catch(err => { console.error("[error]:文件上传错误", err) }) }, fail: err => { console.error("[error]:文件选择错误", err) } }) }, crop(cropOps) { /** * 获取 Context */ let ctx = wx.createCanvasContext('aiCrop', this); /** * 判断是否成功裁剪 */ if (cropOps.results.length == 0) { return } /** * 计算裁剪的值 */ let crop = cropOps.results[0]; let width = crop.cropRight - crop.cropLeft let height = crop.cropBottom - crop.cropTop /** * 绘制图像 */ ctx.drawImage(this.tempData.path, crop.cropLeft, crop.cropTop, width, height, 0, 0, 300, 300); ctx.draw() } }) [代码] 对应页面的 WXML 页面结构如下 [代码]<button bindtap="onClick">Crop MY IMAGE</button> <canvas canvas-id="aiCrop" style="width:300px;height:300px;"></canvas> [代码]
2019-12-22 - 谜之wxs,uni-app如何用它大幅提升性能
小程序里有几个谜一样的存在,微信的WXS、支付宝的SJS、百度的Filter。 很多开发者都不明白为什么要造这种语言脚本的轮子出来,甚至很多开发者根本不知道它们的存在。 其实几大小程序平台创造它们,都是为了解决性能问题,但不得不吐槽下,设计的实在是很难用,文档也语焉不详。 [代码]uni-app[代码]支持将[代码]WXS[代码]、[代码]SJS[代码]、[代码]Filter[代码]编译到这3家小程序平台,同时还在App和H5实现了[代码]WXS[代码]的解析。为什么做这些事?也是为了性能。 [代码]uni-ui[代码]库新版中的[代码]swiperaction[代码]组件,就是列表项向左滑动时拉出几个挤压式联动的菜单按钮,这种流畅的跟手动画,正是借助于[代码]WXS[代码]机制实现的。 微信为何要创造WXS WXS(WeiXin Script)是微信创造的一套脚本语言,它的官方说法是:“WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致”。 那微信为何要脱离 JavaScript ,单独创造一套语言呢?这要从微信小程序的底层逻辑(运行环境)讲起。 小程序的运行环境分为逻辑层和视图层,分别由2个线程管理,其中: WXML 模板和 WXSS 样式工作在视图层,界面使用 WebView 进行渲染 JavaScript代码工作在逻辑层,运行在JsCore或v8里 小程序在视图层与逻辑层两个线程间提供了数据传输和事件系统。这样的分离设计,带来了显而易见的好处: 逻辑和视图分离,即使业务逻辑计算非常繁忙,也不会阻塞渲染和用户在视图层上的交互 但同时也带来了明显的坏处: 视图层(webview)中不能运行JS,而逻辑层JS又无法直接修改页面DOM,数据更新及事件系统只能靠线程间通讯,但跨线程通信的成本极高,特别是需要频繁通信的场景 什么是需要频繁通讯的场景?最典型的例子就是用户持续交互的情况,比如触摸、滚动等。我们以侧滑菜单为例,假设在页面上滑动A元素,要求B元素跟随移动,一次滑动操作(touchmove)的响应过程如下: touchmove 事件从视图层(Webview)传递到逻辑层,中间会由微信客户端(Native)做中转 [图片] 逻辑层处理 touchmove 事件,计算需移动的位置,然后再通过 setData 传递到视图层,中间同样会由微信客户端(Native)做中转 [图片] 一次 touchmove 的响应需要经过 视图层、Native、逻辑层三者之间2个完整来回的通信,通信的耗时开销较大,用户的交互就会出现延时卡顿的情况。 除了滚动、拖动交互外,在for循环里对数据做格式修改,也会造成逻辑层和视图层频繁通讯。 其实这类通信损耗问题,在业内由来已久,react native和weex都有类似问题,weex提供了bindingx来解决。 但对于小程序来讲,这类问题解决起来更容易。其实视图层的webview,是有js环境的,只不过过去不给开发者开放。 如果在视图层的js直接处理滚动或拖动交互、直接处理数据格式,就能避免大量通信损耗。 但对于小程序平台而言,大量开放webview里的js编写,违反了它的初衷,比如开发者会直接操作dom,影响性能体验。所以小程序平台提出一种新规范,限制webview里可运行的js的能力。这就是wxs、sjs、filter的由来。 从本质来讲,wxs、sjs、filter是一种被限制过的、运行在视图层webview里的js。它并不是真的发明了一种新语言。 WXS特征及适用场景 WXS具备如下特征: WXS是可以在视图层(webview)中运行的JS WXS无法修改业务数据,仅能设置当前组件的[代码]class[代码]和[代码]style[代码] WXS是被限制过的JavaScript,可以进行一些简单的逻辑运算 WXS可以监听touch事件,处理滚动、拖动交互 故可以得出WXS的适用场景,主要包括: 用户交互频繁、仅需改动组件样式(比如布局位置),无需改动数据内容的场景,比如侧滑菜单、索引列表、滚动渐变等 纯粹的逻辑计算,比如文本、日期格式化,通过WXS可以模拟实现Vue框架的过滤器,如下是一个通过wxs便捷实现首字母大写的示例: [代码]<wxs module="m1"> //首字母大写 var capitalize = function(value) { if (!value) return '' value = value.toString() return value.charAt(0).toUpperCase() + value.slice(1) } module.exports = { capitalize: capitalize } </wxs> <view class="content"> <view class="text-area"> <!-- title 为当前页面 data 中定义的初始数据 --> <text class="title">{{m1.capitalize(title)}}</text> </view> </view> [代码] uni-app如何支持WXS [代码]uni-app[代码]遵循Vue单文件组件(SFC)规范,组件/样式/脚本是写在一个[代码].vue[代码]文件中的,但微信小程序是多文件分离(wxml/wxss/js/json)的,所以在微信端的主要工作是扩展[代码]vue-template-compiler[代码],解析[代码]template/style/script[代码]节点,并正确生成到对应的[代码]wxml/wxss/js[代码]文件中,具体编译工作如下图: [图片] Tips-1:关于[代码]<wxs>[代码]标签重构为[代码]<script lang="wxs">[代码]的说明: 因[代码].vue[代码]文件中的[代码]<wxs>[代码]标签及内嵌WXS代码,在主流前端开发工具(vscode/HBuilderX等)中,均无法实现语法提示、代码高亮及格式化,故[代码]uni-app[代码]将[代码]<wxs module="m1">[代码]重构为[代码]<script module="m1" lang="wxs">[代码],便捷实现了语法提示、代码高亮等,如下为vscode/HBuilderX中对于[代码]<wxs>[代码]标签重构前后的代码高亮对比,明显重构为[代码]<script lang="wxs">[代码]后,开发体验更佳: [图片] Tips-2:鉴于Vue的自定义标签规范,我们建议将[代码]<wxs>[代码]([代码]<script lang="wxs">[代码])和[代码]template[代码]平级编写 编译器的具体解析扩展工作,这里不详述,仅给出[代码]wxs[代码]生成的示例代码,让大家有个直观理解: [代码]createFilterTag (filterTag, { content, attrs }) { content = content.trim() if (content) { //<wxs>标签内直接编写 wxs 代码 return `<${filterTag} module="${attrs.module}"> ${content} </${filterTag}>` } else if (attrs.src) { //外联 .wxs 文件 return `<${filterTag} src="${attrs.src}" module="${attrs.module}"></${filterTag}>` } } [代码] 在保证编译正确的情况下,微信小程序运行时会正确解析并执行[代码]WXS[代码]脚本,框架[代码]runtime[代码]无需干预。 基于 WXS 提升性能体验的实现示例 下面的gif图是借助 WXS 实现的一个[代码]swipeaction[代码]示例,列表项向左滑动时拉出几个挤压式联动的菜单按钮,跟手动画、回弹动画都很自然流畅。 [图片] 这里简单给出主要实现思路: 在 wxml 中引用 wxs 文件,并绑定 touch 事件 [代码]<template> <view class="uni-swipe_content"> <!-- 可滑动的菜单项容器,绑定touch事件 --> <view :data-position="pos" class="move-hock" @touchstart="swipe.touchstart" @touchmove="swipe.touchmove" @touchend="swipe.touchend" @change="change"> <view class="uni-swipe_box"> <slot /> </view> <view class="uni-swipe_button-group move-hock"> <!-- 滑动后,右侧挤压式的联动菜单按钮--> <view v-for="(item,index) in options" :data-button="btn" :key="index" class="button-hock"> {{ item.text }} </view> </view> </view> </view> </template> <script module="swipe" lang="wxs" src="./index.wxs"></script> [代码] 在 wxs 文件中,处理 touch 事件逻辑,通过 translateX 移动元素位置 [代码]function touchstart(e, ins) { //记录开始位置及动画状态 var pageX = e.touches[0].pageX; .... } function touchmove(e, ownerInstance) { var instance = e.instance; var pageX = e.touches[0].pageX;//获取当前移动位置 //计算偏移位置 var x = Math.max(-instance.getState().position[1].width, Math.min((value), 0)); //设置左侧元素移动位置 instance.setStyle({transform: 'translateX(' + x + 'px)'}) //循环右侧挤压式联动菜单 var btnIns = ownerInstance.selectAllComponents('.button-hock'); for (var i = 0; i < btnIns.length; i++) { ... //设置每个联动菜单的移动位置 btnIns[i].setStyle({transform: 'translateX(' + (arr[i - 1] + value * (arr[i - 1] / position[1].width)) + 'px)'}) ... } } function touchend(e, ownerInstance) { var instance = e.instance; var state = instance.getState() //根据当前移动位置,实现菜单项的自动展开或回弹 move(state.left, -40, instance, ownerInstance) } [代码] 该示例的完整源码参考github 更多平台的兼容性 [代码]uni-app[代码]的App端也是一个小程序引擎,所以想要在App端实现流畅的跟手拖动,也需要实现类似wxs的机制。 其实H5平台倒不存在逻辑层和视图层通讯折损的问题,但为了平台兼容性拉齐,[代码]uni-app[代码]在H5端也实现了wxs机制。 这样编写wxs代码,在[代码]uni-app[代码]中可同时运行在App端、H5端、微信小程序端。 因百度小程序的Filter过滤器、支付宝小程序的SJS和微信小程序的WXS在语法上差异较大,[代码]uni-app[代码]只支持单独编写百度小程序的Filter过滤器和支付宝小程序的SJS,这两种脚本无法跨多端,仅支持自有平台。开发者若需使用,可分别编写[代码]wxs/filter/sjs[代码]脚本,然后依次通过[代码]script[代码]引用,[代码]uni-app[代码]编译器会根据目标平台,分别编译发行,如下为示例代码: 示例代码要有条件编译 [代码]<!-- App/H5/微信小程序平台调用wxs脚本 --> <script module="utils" lang="wxs" src="./utils.wxs"></script> <!-- 百度小程序平台调用filter.js脚本 --> <script module="utils" lang="filter" src="./utils.filter.js"></script> <!-- 支付宝小程序平台调用sjs脚本 --> <script module="utils" lang="sjs" src="./utils.sjs"></script> [代码] 后续 用运行在视图层的js解决通讯阻塞,可能很多人都没意识到。希望本文能给大家解惑,解开WXS之谜。 其实小程序的性能体验优化,仍然有大量空间。DCloud团队在这个领域研究了6年,清楚当前的优势,也清楚当前的问题。我们会继续分享这些问题及对应的解决方案,为小程序产业发展贡献力量。 本文涉及的[代码]uni-ui[代码]的[代码]swiperaction[代码]组件,代码开源在https://github.com/dcloudio/uni-ui,[代码]uni-app[代码]框架代码开源在 https://github.com/dcloudio/uni-app,欢迎大家 star 或提交 pr。
2019-12-17 - wxm2canvas 小程序中绘制 canvas 的小工具
小程序中绘制 canvas 的小工具
2018-12-08 - 小程序订阅消息开发指南
2019年10月12日微信开放了小程序订阅消息的功能。按官方的说法,目前的模板消息在实现小程序服务闭环上存在缺陷: 1. 部分开发者在用户无预期或未进行服务的情况下发送与用户无关的消息,对用户产生了骚扰;2. 模板消息需在用户访问小程序后的 7 天内下发,不能满足部分业务的时间要求模板消息确实存在上述的硬伤,不利于小程序的用户留存和用户体验。为了解决这些问题,微信官方推出了用户订阅消息功能。我在微慕专业版上加了订阅消息的功能,并验证了这个功能。这个功能是否能都达到官方的预期,这个我感觉不那么乐观。这里我先说我的感受:目前的订阅消息还不完善,后续还有很大的优化空间。 目前,官方只开放了“一次性订阅消息”,尚未开放“长期性订阅消息”,因此我只尝试了“一次性订阅消息”。 一次性订阅消息:用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。 订阅消息推送位置:服务通知 订阅消息下发条件:用户自主订阅 订阅消息卡片跳转能力:点击查看详情可跳转至该小程序的页面 以下我简单说明订阅消息的开发过程和使用体验。 一.订阅消息的开发1.获取订阅消息的模板ID 在微信小程序的管理后台,在左侧“功能”菜单,选择“订阅消息”,然后点击“添加” [图片] 然后选择你需要的消息模板,并配置关键词。 [图片] 配置完成后,如下图所示。 [图片] 值得关注的是,在配置好的模板详情页面里的“详细内容”很重要,这个就是开发订阅消息时需要遵循的消息格式,这个格式和模板消息有细微的差别 根据微慕小程序的需要,我选用了“新的评论提醒”和“内容更新提醒”这两个消息模版。前者用于提醒发表话题或文章的作者,有新的话题或文章评论,增强作者与读者之间的交流互动;后者是提醒订阅用户,小程序有新的文章发布,引导用户回归小程序。 订阅消息申请模板的时候,需要选择所属类目,只能选择当前小程序相关的类目模板,对于模板消息不需要选择对应类目。如果删除小程序类目,就会把订阅消息模板一起删除。因此删除类目要小心谨慎。 [图片] 2.触发用户订阅,获取下发的权限 触发用户订阅,微信小程序提供的api是: [代码]wx.requestSubscribeMessage[代码],用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。 注意:微信小程序开发工具尚不支持此功能,在开发工具触发订阅的api,会提示: requestSubscribeMessage:fail 开发者工具暂时不支持此 API 调试,请使用真机进行开发 调用api的代码示例如下: [代码]wx.requestSubscribeMessage({[代码] [代码]tmplIds: ["模板A","模板B"],[代码] [代码]success: function (res) {[代码] [代码]//成功[代码] [代码]},[代码] [代码]fail(err) {[代码] [代码]//失败[代码] [代码]console.error(err);[代码] [代码]}[代码] [代码]})[代码] wx.requestSubscribeMessage(Object object) 的回调函数[代码]object.success [代码]参数有两个:errMsg和TEMPLATE_ID; 接口调用成功时errMsg值为’requestSubscribeMessage:ok’。TEMPLATE_ID是动态的键,即模板id,值包括’accept’、’reject’、’ban’。’accept’表示用户同意订阅该条id对应的模板消息,’reject’表示用户拒绝订阅该条id对应的模板消息,’ban’表示已被后台封禁。例如 { errMsg: “requestSubscribeMessage:ok”, zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE: “accept”} 表示用户同意订阅zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE这条消息。 个人觉得这个动态键不是特别合理,代码处理起来有些麻烦,如果改成静态键的json格式比较方便处理,例如: [代码]{[代码] [代码] errMsg:"requestSubscribeMessage:ok",[代码] [代码] result: [[代码] [代码] { templateId:"zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE",[代码] [代码]status:"accept"[代码] [代码]}[代码] [代码] ][代码] [代码]}[代码] 在手机上调用此api方法会调出订阅消息的界面,如下图所示: [图片] 关于这个订阅消息的授权有几点要注意: 1) 在确认提示框里,如果用户选择“取消”表示拒绝(取消)订阅消息,选择“允许”表示用户订阅一次消息。 2) 如果用户不勾选“总是保持以上选择,不再询问”,那么每次用户触发都会弹出提示框。 3) 如果用户勾选“总是保持以上选择,不再询问”,那么将再也不会唤起这个对话框。同时,如果选择“取消”,那么以后每次调用这个api的时候,都会自动拒绝;如果选择“允许”,那么以后每次调用此api,都会自动允许授权。 目前小程序没有提供获取用户是否授权订阅消息的方法。通过wx.openSetting 方法无法获取用户是否授权消息订阅的信息,scope 列表没有订阅消息的内容。 如果想从自动拒绝转换到自动自动运行,需要打开小程序的设置去配置。设置方法:点击小程序右上角的三个点,打开如下对话框 [图片] 然后选择“设置”,在设置项里选择“订阅消息” [图片] [图片] 4)对于同一种消息,用户可以订阅多次,订阅多少次,就会收到多少次订阅消息,这个订阅次数是否有上限,官方没有说明,初步判断是不限的。但是,微信不会提供订阅的次数,因此需要在小程序的后端服务里存储用户订阅的次数。因此,我在微慕小程序专业版里,提供了一个给用户多次订阅的设置,并记录用户订阅的次数。 [图片] 如果用户需要某个消息服务,可以订阅多次,当然也可以在点击“订阅”的对话框里选择“取消”,“取消”一次也就减少一次订阅。 5)对于支付的场景,也需要用户确认是否订阅,这个我觉得不合理,支付后给用户一个订单推送消息应该是刚性需求,不需要再询问一遍用户是否订阅。 2.调用接口下发订阅消息 订阅消息下发的接口是小程序后台服务端调用:subscribeMessage.send,此方法类似下发模板消息的方法,详细调用说明见参考官方的链接: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html 订阅消息的下发接口方法和模板消息稍有不同, 模板消息的json格式如下 [代码]"data": {[代码] [代码]"keyword1": {[代码] [代码]"value": "内容1",[代码] [代码]"color": "#000"[代码] [代码]},[代码] [代码]"keyword2": {[代码] [代码]"value": "内容2",[代码] [代码]"color": "#000"[代码] [代码]}[代码] [代码]}[代码] 而订阅消息的json格式如下: [代码]"data": {[代码] [代码]"thing1": {[代码] [代码]"value": "内容"[代码] [代码]},[代码] [代码]"number2": {[代码] [代码]"value": 20[代码] [代码]}[代码] 订阅消息的字段key是和数据类型有关,value的参数需要严格按照设置的类型提交,如果不按类型提交,会导致发送失败。同时如果是文本型的内容,字数也有限制,超过限制也会发送失败,但具体字数是多少,官方没有给出,同时中英文混合计算的长度也有差异,据我目前测试25个中文字符是可以的。希望官方能给出具体的字符长度限制的明确数字。 如果调用下发的次数大于用户的订阅次数,调用接口下发订阅消息会返回失败。报如下错误 [图片] 二.订阅消息使用心得1.订阅消息虽然把订阅的授权的交给了用户,但是也增加了用户使用难度,同时,一次性订阅只能收到一次,操作起来比较繁琐,如果不是刚需用户可能会首次就拒绝了这个服务,要想重新获取授权,需要用户自己打开小程序设置里去配置,颇为麻烦,小程序没有提供更简便的方法去唤起。 2.小程序的服务商为了获得更多给用户发送订阅消息的次数,肯定会想方设法去埋点引诱用户去点击订阅,这种诱导估计也是违规。 3.用户使用门槛和学习比较高,比如某个预约的服务,原来的场景是用户只要有提交表单,小程序就可以推送消息给用户,但是现在需要用户主动去订阅,无形中多了一步,如果用户不熟悉订阅消息或者直接点了“取消”,小程序就没法通知到用户了,用户可能因此错失服务,对商家和用户都是损失。 4.微信小程序将采用订阅消息,并逐步取消模板消息,虽然微信官方试图在方便用户和不打扰用户这两种选择里去寻求平衡,但订阅消息目前的模式恐怕无法达到这个期望,至少在我看来,无论对小程序的服务商,还是小程序的用户,都感到不方便。 update:2020年5月18日,日前订阅消息已经支持微信小程序开发工具。
2020-05-18 - ma-skeleton 小程序骨架屏,利用自动生成骨架屏
小程序骨架屏,利用SelectorQuery自动生成骨架屏
2019-04-12 - 如何写出优雅的深复制
前言 无论在项目开发或者学习中,深拷贝已经是一个老生常谈的话题了,但是在实际中,如何优雅地写出深拷贝是我们值得思考的一个问题 内容 深拷贝 与 浅拷贝 深拷贝 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象 浅拷贝 如果一个对象有着原始对象属性值的一份精确拷贝。如果这个对象属性是基本类型,那么拷贝的就是基本类型的值,如果属性是引用类型,那么拷贝的就是内存地址。 区别 其实深拷贝和浅拷贝的主要区别就是其在内存中的存储类型不同。 堆和栈都是内存中划分出来用来存储的区域。 栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap)则是动态分配的内存,大小不定也不会自动释放。 对于js中的基本数据类型,他们的值被以键值对的形式保存在栈中。 [图片] 与基本类型不同的是,引用类型的值被保存在堆内存中,对象的引用被保存在栈内存中,而且我们不可以直接访问堆内存,只能访问栈内存。所以我们操作引用类型时实际操作的是对象的引用。 [图片] 了解相关的基础知识后,我们话不多说,直奔主图 简单版 在不使用第三方库的情况下,我们想要深拷贝一个对象,用的最多的就是下面这个方法。 [代码] JSON.parse(JSON.stringify()); [代码] 这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。 基础版 如果是浅拷贝对象时,我们可以很容易的就写出代码 [代码]function clone(obj) { let cloneObj = {}; for (const key in obj) { cloneObj [key] = obj[key]; } return cloneObj ; }; [代码] 对于浅拷贝而言,只需要简单地将对象的每一个属性进行复制即可。然而,对于深拷贝而言,我们拷贝对象的话是需要知道目标对象的属性是否是基本数据类型以及对象的深度。这些我们可以通过递归的方法来实现。 [代码]/* * 作用: 深复制对象属性 */ function clone(obj) { if (typeof obj=== 'object') { let cloneObj = {}; for (const key in obj) { cloneObj [key] = clone(obj[key]); } return cloneObj ; } else { return obj; } }; [代码] 这时候,我们实现了一个基础的深复制,那么问题来了,对于数组,该如何实现呢? 加深版 在上面的版本中,我们的初始化结果只考虑了普通的object,下面我们只需要把初始化代码稍微一变,就可以兼容数组了: [代码]function clone(obj) { if (typeof obj=== 'object') { let cloneObj = Array.isArray(obj) ? [] : {}; for (const key in target) { cloneObj[key] = clone(obj[key]); } return cloneObj; } else { return obj; } }; [代码] 在判断目标对象是引用类型时,则通过Array.isArray方法判断是否是数组,如果是则赋值为空数组,否则赋值为空对象。 循环引用 当对象子属性的值是父对象时,则递归的方法将不再适用。原因就是对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况,这将导致递归进入死循环导致栈内存溢出。 为了解决这个问题,我们可以通过WeakMap这种数据结构来实现。首先我们通过WeakMap来存储当前对象和拷贝对象的对应关系。当需要拷贝当前对象时,先去WeakMap中找,有则返回,无则set。 [代码]WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。 [代码] [代码]function clone(target, weakMap = new WeakMap()) { if (typeof target === 'object') { let cloneTarget = Array.isArray(target) ? [] : {}; if (weakMap.get(target)) { return weakMap.get(target); } weakMap.set(target, cloneTarget); for (const key in target) { cloneTarget[key] = clone(target[key], weakMap); } return cloneTarget; } else { return target; } }; [代码] 考虑一下性能,while循环的性能要比for on的要好,因此改造一下 [代码]function forEach(array, iteratee) { let index = 0; while (index < array.length) { iteratee(index, array[index]); index++; } return array; } function clone(target, weakMap = new WeakMap()) { if (typeof target === 'object') { const isArray = Array.isArray(target); let cloneTarget = Array.isArray(target) ? [] : {}; if (weakMap.get(target)) { return weakMap.get(target); } weakMap.set(target, cloneTarget); const keyList = isArray ? undefined : Object.keys(target); forEach( keyList || target , function(key, value){ if(keyList){ // 对象而言,其值才是他的key key = value; } cloneTarget[key] = clone(target[key], weakMap); }) return cloneTarget; } else { return target; } }; [代码] 其他数据类型 综上我们考虑到的只是普通的object以及array俩种数据类型,但引用类型并不单只有这俩个,还有很多。。。 判断是否是引用类型 在判断是否是引用类型时,我们可以通过typeof字段,此时我们还需要考虑typeof可能返回’function’字符串以及对象有可能是null的情况,因此可写出判断函数如下所示 [代码]function isObject(target) { const type = typeof target; return target !== null && (type === 'object' || type === 'function'); } [代码] 获取数据类型 我们可以使用toString来获取准确的引用类型: [代码]function getType(target) { return Object.prototype.toString.call(target); } [代码] [图片] [图片] 根据上面的返回的字符串,我们可以抽离出一些常用的数据类型以便后面使用: [代码]const mapTag = '[object Map]'; const setTag = '[object Set]'; const arrayTag = '[object Array]'; const objectTag = '[object Object]'; const boolTag = '[object Boolean]'; const dateTag = '[object Date]'; const errorTag = '[object Error]'; const numberTag = '[object Number]'; const regexpTag = '[object RegExp]'; const stringTag = '[object String]'; const symbolTag = '[object Symbol]'; [代码] 在上面的集中类型中,我们简单将他们分为两类: 可以继续遍历的类型 不可以继续遍历的类型 可继续遍历的类型 上面我们已经考虑的object、array都属于可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,另外还有Map,Set等都是可以继续遍历的类型 这时候我们需要一个通过对象原型上的constructor属性获取构造函数,从而对要复制的对象进行初始化。方法如下: [代码]function getInit(target) { const Ctor = target.constructor; return new Ctor(); } [代码] 下面我们改写一下clone函数,让他兼容map,set。 [代码]const mapTag = '[object Map]'; const setTag = '[object Set]'; const arrayTag = '[object Array]'; const objectTag = '[object Object]'; const deepTag = [mapTag, setTag, arrayTag, objectTag]; function clone(target, weakMap = new WeakMap()) { // 克隆基本数据类型 if (!isObject(target)) { return target; } // 初始化 const type = getType(target); let cloneTarget; if (deepTag.includes(type)) { cloneTarget = getInit(target, type); } // 防止循环引用 if (weakMap.get(target)) { return weakMap.get(target); } weakMap.set(target, cloneTarget); // 克隆set if (type === setTag) { target.forEach(value => { cloneTarget.add(clone(value,weakMap)); }); return cloneTarget; } // 克隆map if (type === mapTag) { target.forEach((value, key) => { cloneTarget.set(key, clone(value,weakMap)); }); return cloneTarget; } // 克隆对象和数组 const keys = type === arrayTag ? undefined : Object.keys(target); forEach(keys || target, (value, key) => { if (keys) { key = value; } cloneTarget[key] = clone(target[key], weakMap); }); return cloneTarget; } [代码] 不可继续遍历的类型 其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理: Bool、Number、String、String、Date、Error这几种类型我们都可以直接用构造函数和原始数据创建一个新对象: [代码]function cloneOtherType(targe, type) { const Ctor = targe.constructor; switch (type) { case boolTag: case numberTag: case stringTag: case errorTag: case dateTag: return new Ctor(targe); case regexpTag: return cloneReg(targe); case symbolTag: return cloneSymbol(targe); default: return null; } } function cloneSymbol(targe) { return Object(Symbol.prototype.valueOf.call(targe)); } //克隆正则 function cloneReg(targe) { const reFlags = /\w*$/; const result = new targe.constructor(targe.source, reFlags.exec(targe)); result.lastIndex = targe.lastIndex; return result; } [代码] 克隆函数 对于克隆函数,实际上是没有太大的意义。。。因为不同的俩个对象使用同一个函数是没有任何问题的。 首先,我们可以通过prototype来区分下箭头函数和普通函数,箭头函数是没有prototype的。 我们可以直接使用eval和函数字符串来重新生成一个箭头函数,注意这种方法是不适用于普通函数的。 [代码]function cloneFunction(func) { const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); if (func.prototype) { // 普通函数 const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if (body) { if (param) { const paramArr = param[0].split(','); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); } } else { return null; } } else { // 箭头函数 return eval(funcString); } } [代码] 总结 综上,我们围绕深复制进行了解析,了解到了应该如何写出优雅的深复制,在实际开发中,可以根据不同的场景,合理的选择如何书写深复制。
2019-11-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 - 「笔记」订阅消息体验踩坑
前言 10月12日夜晚社区发了公告小程序模板消息能力调整通知,正式发布了 一次性订阅消息 这一能力,所以第一时间进行了体验。 本文主要是补充一下官方未提供的使用方法,和使用中与模板消息用法的不同。 文档地址 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html 使用方法 [代码]wx.requestSubscribeMessage({ tmplIds: ["模板id1","模板id2"], success: function (res) { //成功 }, fail(err) { //失败 console.error(err); } }) [代码] 第一个坑 如果不勾选红色方框内的内容,用户每次触发订阅消息功能都会弹出授权窗口,如果用户勾选了则不会出现弹窗。 [图片] 第二个坑 目前开发者工具(v1.02.191012)不支持调试,只能通过真机调试。 [图片] 第三个坑 微信不会为开发者保存订阅次数,需要自己在后台记录用户触发的次数。 超过次数调用接口下发订阅消息会返回失败。 [图片] 第四个坑 发送模板格式和原来的模板消息格式不一致,特别是data内的内容,订阅消息的字段key是和数据类型有关,value的参数需要严格按照设置的类型提交,具体使用参考后台的模板详情。 模板消息的格式: [代码]"data": { "keyword1": { "value": "内容", "color": "#000" }, "keyword2": { "value": "内容", "color": "#000" } } [代码] 订阅消息的格式: [代码]"data": { "thing1": { "value": "内容" }, "number2": { "value": 20 } [代码] 第五个坑 订阅消息申请模板的时候,需要选择所属类目,而且只能是自己小程序相关类目,模板消息是不需要选择对应类目的。 如果删除小程序类目,则会把订阅消息模板一起删除,需谨慎操作。 [图片] 第六个坑 长期订阅消息只针对特定行业开放,所以普通开发者并无法使用。 结束 暂时就先总结这些,有其它坑再补充。
2019-10-13 - 如何实现一个自定义导航栏
自定义导航栏在刚出的时候已经有很多实现方案了,但是还有大哥在问,那这里再贴下代码及原理: 首先在App.js的 onLaunch中获取当前手机机型头部状态栏的高度,单位为px,存在内存中,操作如下: [代码]onLaunch() { wx.getSystemInfo({ success: (res) => { this.globalData.statusBarHeight = res.statusBarHeight this.globalData.titleBarHeight = wx.getMenuButtonBoundingClientRect().bottom + wx.getMenuButtonBoundingClientRect().top - (res.statusBarHeight * 2) }, failure() { this.globalData.statusBarHeight = 0 this.globalData.titleBarHeight = 0 } }) } [代码] 然后需要在目录下新建个components文件夹,里面存放此次需要演示的文件 navigateTitle WXML 文件如下: [代码]<view class="navigate-container"> <view style="height:{{statusBarHeight}}px"></view> <view class="navigate-bar" style="height:{{titleBarHeight}}px"> <view class="navigate-icon"> <navigator class="navigator-back" open-type="navigateBack" wx:if="{{!isShowHome}}" /> <navigator class="navigator-home" open-type="switchTab" url="/pages/index/index" wx:else /> </view> <view class="navigate-title">{{title}}</view> <view class="navigate-icon"></view> </view> </view> <view class="navigate-line" style="height: {{statusBarHeight + titleBarHeight}}px; width: 100%;"></view> [代码] WXSS文件如下: [代码].navigate-container { position: fixed; top: 0; width: 100%; z-index: 9999; background: #FFF; } .navigate-bar { width: 100%; display: flex; justify-content: space-around; } .navigate-icon { width: 100rpx; height: 100rpx; display: flex; justify-content: space-around; } .navigate-title { width: 550rpx; text-align: center; line-height: 100rpx; font-size: 34rpx; color: #3c3c3c; font-weight: bold; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } /*箭头部分*/ .navigator-back { width: 36rpx; height: 36rpx; align-self: center; } .navigator-back:after { content: ''; display: block; width: 22rpx; height: 22rpx; border-right: 4rpx solid #000; border-top: 4rpx solid #000; transform: rotate(225deg); } .navigator-home { width: 56rpx; height: 56rpx; background: url(https://qiniu-image.qtshe.com/20190301home.png) no-repeat center center; background-size: 100% 100%; align-self: center; } [代码] JS如下: [代码]var app = getApp() Component({ data: { statusBarHeight: '', titleBarHeight: '', isShowHome: false }, properties: { //属性值可以在组件使用时指定 title: { type: String, value: '青团公益' } }, pageLifetimes: { // 组件所在页面的生命周期函数 show() { let pageContext = getCurrentPages() if (pageContext.length > 1) { this.setData({ isShowHome: false }) } else { this.setData({ isShowHome: true }) } } }, attached() { this.setData({ statusBarHeight: app.globalData.statusBarHeight, titleBarHeight: app.globalData.titleBarHeight }) }, methods: {} }) [代码] JSON如下: [代码]{ "component": true } [代码] 如何引用? 需要引用的页面JSON里配置: [代码]"navigationStyle": "custom", "usingComponents": { "navigate-title": "/pages/components/navigateTitle/index" } [代码] WXML [代码]<navigate-title title="青团社" /> [代码] 按上面步骤操作即可实现一个自定义的导航栏。 如何实现通栏的效果默认透明以及滚动更换title为白色背景,如下图所示: [图片] [图片] [图片] [图片] 最后代码片段如下: https://developers.weixin.qq.com/s/wi6Pglmv7s8P。 以下为收集到的社区老哥们的分享: @Yunior: 小程序顶部自定义导航组件实现原理及坑分享 @志军: 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能 @✨o0o有脾气的酸奶💤 [有点炫]自定义navigate+分包+自定义tabbar @安晓苏 分享一个自适应的自定义导航栏组件
2020-03-10 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: Android、IOS手机对于页面title的展示不一致,安卓title的显示不居中 页面的title只支持纯文本级别的样式控制,不能够做更丰富的title效果 左上角的事件无法监听、定制 路由导航单一,只能够返回上一页,深层级页面的返回不够友好 探索 小程序自定义导航栏已开放许久>>了解一下,相信不少小伙伴已使用过这个功能,同时不少小伙伴也会发现一些坑: 机型多如牛毛:自定义导航栏高度在不同机型始终无法达到视觉上的统一 调皮的胶囊按钮:导航栏元素(文字,图标等)怎么也对不齐那该死的胶囊按钮 各种尺寸的全面屏,奇怪的刘海屏,简直要抓狂 一探究竟 为了搞明白原理,我先去翻了官方文档,>>飞机,点过去是不是很惊喜,很意外,通篇大文尽然只有最下方的一张图片与这个问题有关,并且啥也看不清,汗汗汗… 我特意找了一张图片来 [图片] 分析上图,我得到如下信息: Android跟iOS有差异,表现在顶部到胶囊按钮之间的距离差了6pt 胶囊按钮高度为32pt, iOS和Android一致 动手分析 我们写一个状态栏,通过wx.getSystemInfoSync().statusBarHeight设置高度 Android: [图片] iOS:[图片] 可以看出,iOS胶囊按钮与状态栏之间距离为:4px, Android为8px,是不是所有手机都是这种情况呢? 答案是:苹果手机确实都是4px,安卓大部分都是7和8 也会有其他的情况(可以自己打印getSystemInfo验证)如何快速便捷算出这个高度,请接着往下看 如何计算 导航栏分为状态栏和标题栏,只要能算出每台手机的导航栏高度问题就迎刃而解 导航栏高度 = 胶囊按钮高度 + 状态栏到胶囊按钮间距 * 2 + 状态栏高度 注:由于胶囊按钮是原生组件,为表现一致,其单位在各种手机中都为px,所以我们自定义导航栏的单位都必需是px(切记不能用rpx),才能完美适配。 解决问题 现在我们明白了原理,可以利用胶囊按钮的位置信息和statusBarHeight高度动态计算导航栏的高度,贴一个实现此功能最重要的方法 [代码]let systemInfo = wx.getSystemInfoSync(); let rect = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null; //胶囊按钮位置信息 wx.getMenuButtonBoundingClientRect(); let navBarHeight = (function() { //导航栏高度 let gap = rect.top - systemInfo.statusBarHeight; //动态计算每台手机状态栏到胶囊按钮间距 return 2 * gap + rect.height; })(); [代码] gap信息就是不同的手机其状态栏到胶囊按钮间距,具体更多代码实现和使用demo请移步下方代码仓库,代码中还会有输入框文字跳动解决办法,安卓手机输入框文字飞出解决办法,左侧按钮边框太粗解决办法等等 胶囊信息报错和获取不到 问题就在于 getMenuButtonBoundingClientRect 这个方法,在某些机子和环境下会报错或者获取不到,对于此种情况完美可以模拟一个胶囊位置出来 [代码]try { rect = Taro.getMenuButtonBoundingClientRect ? Taro.getMenuButtonBoundingClientRect() : null; if (rect === null) { throw 'getMenuButtonBoundingClientRect error'; } //取值为0的情况 if (!rect.width) { throw 'getMenuButtonBoundingClientRect error'; } } catch (error) { let gap = ''; //胶囊按钮上下间距 使导航内容居中 let width = 96; //胶囊的宽度,android大部分96,ios为88 if (systemInfo.platform === 'android') { gap = 8; width = 96; } else if (systemInfo.platform === 'devtools') { if (ios) { gap = 5.5; //开发工具中ios手机 } else { gap = 7.5; //开发工具中android和其他手机 } } else { gap = 4; width = 88; } if (!systemInfo.statusBarHeight) { //开启wifi的情况下修复statusBarHeight值获取不到 systemInfo.statusBarHeight = systemInfo.screenHeight - systemInfo.windowHeight - 20; } rect = { //获取不到胶囊信息就自定义重置一个 bottom: systemInfo.statusBarHeight + gap + 32, height: 32, left: systemInfo.windowWidth - width - 10, right: systemInfo.windowWidth - 10, top: systemInfo.statusBarHeight + gap, width: width }; console.log('error', error); console.log('rect', rect); } [代码] 以上代码主要是借鉴了拼多多的默认值写法,android 机子中 gap 值大部分为 8,ios 都为 4,开发工具中 ios 为 5.5,android 为 7.5,这样处理之后自己模拟一个胶囊按钮的位置,这样在获取不到胶囊信息的情况下,可保证绝大多数机子完美显示导航头 吐槽 这么重要的问题,官方尽然没有提供解决方案…竟然提供了一张看不清的图片??? 网上有很多ios设置44,android设置48,还有根据不同的手机型号设置不同高度,通过长时间的开发和尝试,本人发现以上方案并不完美,并且bug很多 代码库 Taro组件gitHub地址详细用法请参考README 原生组件npm构建版本gitHub地址详细用法请参考README 原生组件简易版gitHub地址详细用法请参考README 由于本人精力有限,目前只计划发布维护好这2种组件,其他组件请自行修改代码,有问题请联系 备注 上方2种组件在最下方30多款手机测试情况表现良好 iPhone手机打电话和开热点导致导航栏样式错乱,问题已经解决啦,请去demo里测试,这里特别感谢moments网友提出的问题 本文章并无任何商业性质,如有侵权请联系本人修改或删除 文章少量部分内容是本人查询搜集而来 如有问题可以下方留言讨论,微信zhijunxh 比较 斗鱼: [图片] 虎牙: [图片] 微博: [图片] 酷狗: [图片] 知乎: [图片] [图片] 知乎是这里边做的最好的,但是我个人认为有几个可以优化的小问题 打电话或者开启热点导致样式错落,这也是大部门小程序的问题 导航栏下边距太小,看起来不舒服 搜索框距离2侧按钮组距离不对等 自定义返回和home按钮中的竖线颜色重了,并且感觉太粗 如果您看到了此篇文章,请赶快修改自己的代码,并运用在实践中吧 扫码体验我的小程序: [图片] 创作不易,如果对你有帮助,请移步Taro组件gitHub原生组件gitHub给个星星 star✨✨ 谢谢 测试信息 手机型号 胶囊位置信息 statusBarHeight 测试情况 iPhoneX 80 32 281 369 48 88 44 通过 iPhone8 plus 56 32 320 408 24 88 20 通过 iphone7 56 32 281 368 24 87 20 通过 iPhone6 plus 56 32 320 408 24 88 20 通过 iPhone6 56 32 281 368 24 87 20 通过 HUAWEI SLA-AL00 64 32 254 350 32 96 24 通过 HUAWEI VTR-AL00 64 32 254 350 32 96 24 通过 HUAWEI EVA-AL00 64 32 254 350 32 96 24 通过 HUAWEI EML-AL00 68 32 254 350 36 96 29 通过 HUAWEI VOG-AL00 65 32 254 350 33 96 25 通过 HUAWEI ATU-TL10 64 32 254 350 32 96 24 通过 HUAWEI SMARTISAN OS105 64 32 326 422 32 96 24 通过 XIAOMI MI6 59 28 265 352 31 87 23 通过 XIAOMI MI4LTE 60 32 254 350 28 96 20 通过 XIAOMI MIX3 74 32 287 383 42 96 35 通过 REDMI NOTE3 64 32 254 350 32 96 24 通过 REDMI NOTE4 64 32 254 350 32 96 24 通过 REDMI NOTE3 55 28 255 351 27 96 20 通过 REDMI 5plus 67 32 287 383 35 96 28 通过 MEIZU M571C 65 32 254 350 33 96 25 通过 MEIZU M6 NOTE 62 32 254 350 30 96 22 通过 MEIZU MX4 PRO 62 32 278 374 30 96 22 通过 OPPO A33 65 32 254 350 33 96 26 通过 OPPO R11 58 32 254 350 26 96 18 通过 VIVO Y55 64 32 254 350 32 96 24 通过 HONOR BLN-AL20 64 32 254 350 32 96 24 通过 HONOR NEM-AL10 59 28 265 352 31 87 24 通过 HONOR BND-AL10 64 32 254 350 32 96 24 通过 HONOR duk-al20 64 32 254 350 32 96 24 通过 SAMSUNG SM-G9550 64 32 305 401 32 96 24 通过 360 1801-A01 64 32 254 350 32 96 24 通过
2019-11-17 - 小程序顶部自定义导航组件实现原理及坑分享
为什么使用自定义导航 对比默认导航栏,我们会更需要: 统一Android、IOS手机对于页面title的展示样式及位置 更丰富的导航栏定制效果,如添加home图标等 左上角返回事件的监听处理 统一实现双击返回顶部功能 自定义导航组件实现思路 自定义导航组件实现的核心是需要计算导航栏的真实高度 这里以官方文档->扩展能力中的Navigation组件为例分析实现思路。当使用"navigationStyle": "custom"时,默认导航被移除,页面的开始位置变成了屏幕顶部,这时我们需要实现的导航栏是在状态栏下面。 导航栏的真实高度=状态栏高度+导航栏内容。 [图片] 使用wx.getSystemInfo获取到statusBarHeight便是导航栏的高度,但是导航栏内容高度呢? 有人可能觉得导航栏内容高度顾名思义就是导航栏内容高度啊,内容撑起还用管嘛!要,必须要! 因为右上角胶囊按钮是原生加载的,我们的导航栏内容需要正好贴在胶囊的下方且垂直居中。 导航栏内容高度=(胶囊按钮的顶部距离 - 状态高度)*2 + 胶囊高度 [图片] 如何计算胶囊的数据呢?幸运的是我们有 wx.getMenuButtonBoundingClientRect() 获取胶囊按钮的布局位置信息,那么动态计算导航栏的内容高度就很方便啦。 好了,以上就是动态计算的核心思路,我们再来看官方Navigation组件高度是怎么实现的 [代码]page{--height:44px;--right:190rpx;} .weui-navigation-bar .android{--height:48px;--right:222rpx} .weui-navigation-bar__inner{ position:fixed;top:0;left:0;z-index:5001;display:flex;align-items:center; height:var(--height);padding-right:var(--right);width:calc(100% - var(--right)) } [代码] 导航栏内容的高度是通过- -height这个css变量提前声明好的,安卓机型会重新覆盖为新的css变量值,目前没发现有适配问题。 官方就是官方啊,具体尺寸都知道,那就不用一番计算周折啦,直接拿来主义即可。 导航的布局位置已经搞定啦,剩下就是写具体的内容,不同业务实现需求不同这里就不一一赘述了。 完善官方顶部导航组件 本着拿来主义,直接使用官方Navigation组件,但在实际业务开发中还是遇到不少需要自定义的需求,就比如: loadding样式没实现 标题内容超出没有出现省略号 和原生顶部的样式不兼容,导致单个页面引入时跳转有明显差异出现 没有双击返回顶部功能开关功能 引入页面需要获取导航栏的高度,来控制其他元素距离顶部的位置, 不能根据页面栈数据动态显示隐藏back按钮, 针对以上需求,我们对官方的组件进行二次完善开发,满足常规的自定义需求绰绰有余,直接引入开箱即用。 源码使用示例 https://github.com/YuniorZen/minicode-debug/tree/master/minicode02 [图片] 使用说明 [代码]/*自定义头部导航组件,基于官方组件Navigation开发。*/ <navigation-bar title="会员中心" bindgetBarInfo="getBarInfo"></navigation-bar> [代码] 组件属性列表 属性 类型 描述 bindgetBarInfo function 组件实例载入页面时触发此事件,首参为event对象,event.detail携带当前导航栏信息,如导航栏高度 event.detail.topBarHeight bindback function 点击back按钮触发此事件响应函数 backImage string back按钮的图标地址 homeImage string home按钮的图标地址 ext-class string 添加在组件内部结构的class,可用于修改组件内部的样式 title string 导航标题,如果不提供为空 background string 导航背景色,默认#ffffff color string 导航字体颜色 dbclickBackTop boolean 是否开启双击返回顶部功能,默认true border boolean 是否显示顶部导航下边框分割线,默认false loading boolean 是否显示标题左侧的loading,默认false show boolean 显示隐藏导航,隐藏的时候navigation的高度占位还在,默认true left boolean 左侧区域是否使用slot内容,默认false center boolean 中间区域是否使用slot内容,默认false Slot name 描述 left 左侧slot,在back按钮位置显示,当left属性为true的时候有效 center 标题slot,在标题位置显示,当center属性为true的时候有效 自定义顶部导航目前存在的坑 弹窗的背景蒙层无法覆盖原生胶囊按钮 页面下拉刷新的圆点会被自定义导航遮盖 如果要自定义顶部导航,以上问题避免不了,只能忍着接受。 目前还没遇到完美的解决方案,针对下拉刷新圆点被遮挡的问题微信官方还在需求开发中,如果你有好的想法欢迎留言反馈,一起学习交流。
2019-10-31 - 在京东购物小程序有属于自家店铺,自家开发的小程序,点击购买,能跳转到京东购物小程序里进行购买流程吗?
自家开发的小程序,希望能点击购买,跳转到京东购物小程序里的自家店铺。目前达到效果,能跳转到京东小程序,但跳转不了自家店铺,报参数错误。这个需求有人做过吗?
2019-10-09 - 小程序识别身份证,银行卡,营业执照,驾照
最近老是有同学问我小程序ocr识别的问题,就趁机研究了下,实现了小程序识别身份证,银行卡,驾照,营业执照,图片文字的功能。今天来给大家讲讲详细的实现流程。 先画一张流程图出来 [图片] 第一次看到这个流程图,可能有点萌,什么云开发,云函数。。。。 不要着急,我们接下来会一步步带大家实现。 先看下我们的页面和效果图。 [图片] 功能其实很简单,就是我们点对应的按钮后,去拍照或者去相册选择对应的图片。然后把图片上传到云存储,会有一个对应的图片url,然后把这个图片url传递到云函数,然后云函数里使用小程序的开发ocr能力,来识别图片,返回对应的信息回来。如下图所示,我们识别银行卡(身份证什么的就不演示了,涉及到石头哥个人隐私) [图片] 接下来就是代码的实现了。 一,首先要创建一个云开发的小程序项目 这里我前面文章有讲解过,就不再细说了,不会的同学去翻看下我之前的文章。或者看下我录制的 讲解视频 这里有一点需要注意的给大家说下 [图片] 二,创建一个简单的小程序页面 1,index.wxml如下 [图片] 2,index.js完整代码如下 [代码]Page({ //身份证 shenfenzheng() { this.photo("shenfenzheng") }, //银行卡 yinhangka() { this.photo("yinhangka") }, //行驶证 xingshizheng() { this.photo("xingshizheng") }, //拍照或者从相册选择要识别的照片 photo(type) { let that = this wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { // tempFilePath可以作为img标签的src属性显示图片 let imgUrl = res.tempFilePaths[0]; that.uploadImg(type, imgUrl) } }) }, // 上传图片到云存储 uploadImg(type, imgUrl) { let that = this wx.cloud.uploadFile({ cloudPath: 'ocr/' + type + '.png', filePath: imgUrl, // 文件路径 success: res => { console.log("上传成功", res.fileID) that.getImgUrl(type, res.fileID) }, fail: err => { console.log("上传失败", err) } }) }, //获取云存储里的图片url getImgUrl(type, imgUrl) { let that = this wx.cloud.getTempFileURL({ fileList: [imgUrl], success: res => { let imgUrl = res.fileList[0].tempFileURL console.log("获取图片url成功", imgUrl) that.shibie(type, imgUrl) }, fail: err => { console.log("获取图片url失败", err) } }) }, //调用云函数,实现OCR识别 shibie(type, imgUrl) { wx.cloud.callFunction({ name: "ocr", data: { type: type, imgUrl: imgUrl }, success(res) { console.log("识别成功", res) }, fail(res) { console.log("识别失败", res) } }) } }) [代码] 上面代码注释讲解的很清楚了,再结合我们的流程图,相信你可以看明白。 [图片] 三,重头戏来了,识别的核心代码是下面这个云函数 [图片] 云函数的完整代码也给大家贴出来 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async(event, context) => { let { type, imgUrl } = event switch (type) { case 'shenfenzheng': { // 识别身份证 return shenfenzheng(imgUrl) } case 'yinhangka': { // 识别银行卡 return yinhangka(imgUrl) } case 'xingshizheng': { // 识别行驶证 return xingshizheng(imgUrl) } default: { return } } } //识别身份证 async function shenfenzheng(imgUrl) { try { const result = await cloud.openapi.ocr.idcard({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } } //识别银行卡 async function yinhangka(imgUrl) { try { const result = await cloud.openapi.ocr.bankcard({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } } //识别行驶证 async function xingshizheng(imgUrl) { try { const result = await cloud.openapi.ocr.vehicleLicense({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } } [代码] 其实没什么特别的,就是用一个switch方法,根据用户传入的不同的type值,来实现不同的识别效果。 如用传入的type是‘ yinhangka’,我们就调用银行卡识别 [代码]try { const result = await cloud.openapi.ocr.bankcard({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } [代码] 进而把识别的结果返回给小程序端,如下图 [图片] 到这里我们就完整的实现了,小程序识别身份证,银行卡,行驶证的功能。至于别的更多的ocr识别,可以去看小程序官方文档,结合着我的这篇文章,相信你也可以轻松实现更多的图片识别。 [图片] 源码其实在上面都已经贴给大家了,如果你觉得不完整,想要完整的源码可以在文章底部留言或者私信我。
2019-10-30 - 教你解决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 - 实战分享: 小程序云开发玩转订阅消息(一)
[图片] 微信官方为提升小程序模板消息能力的使用体验,对模板消息的下发条件进行了调整。原有的小程序模板消息接口于 2020 年 1 月 10 日下线,届时将无法使用旧的小程序模板消息接口发送模板消息,取而代之的是新的一次性订阅消息和长期订阅消息。 订阅消息给小程序开发者带来了更好的触达用户的能力,在具体实施过程中,开发者如何把模板消息换成新的订阅消息,是否需要购买服务器来实现服务器鉴权,怎样才能在用户订阅之后一段时间后,给用户发送长期或一次性订阅消息呢? 小程序·云开发最近支持了通过云调用免 access_token 发送订阅消息,还新增支持了在定时触发器中实现云调用,这些能力可以帮助开发者轻松玩转小程序订阅消息。 我们今天会利用小程序·云开发进行一个小程序中实现订阅开课提醒的实战,帮助大家了解如何基于小程序·云开发快速接入小程序订阅消息。 [图片]整体时序图[图片]开课提醒订阅消息时序图环境准备注册小程序帐号[1]开通云开发服务[2]获取订阅消息模板 ID在微信小程序管理后台中,新增一个订阅消息的模板,这里我们新增了一个开课提醒的模板。 [图片]新增模板引导用户订阅微信小程序提供了[代码]wx.requestSubscribeMessage[代码] 接口来发起申请订阅权限界面。 [图片]微信申请订阅权限界面在 "订阅开课提醒" 的按钮上绑定 tap 事件,事件处理器我们这里用的 [代码]onSubscribe[代码] index.wxml [代码]<button class="btn" data-item="{{ item }}" bindtap="onSubscribe" hover-class="btn-hover" > 订阅开课提醒 </button> [代码]在 [代码]onSubscribe[代码] 函数内,我们会调用微信 API [代码]wx.requestSubscribeMessage[代码] 申请发送订阅消息权限,当用户在弹窗同意订阅之后,我们会收到 [代码]success[代码] 回调,将订阅的课程信息调用云函数 [代码]subscribe[代码] 存入云开发数据库,云函数 [代码]subscribe[代码] 的实现在下文会讲。 index.js [代码]onSubscribe: function(e) { // 获取课程信息 const item = e.currentTarget.dataset.item; // 调用微信 API 申请发送订阅消息 wx.requestSubscribeMessage({ // 传入订阅消息的模板id,模板 id 可在小程序管理后台申请 tmplIds: [lessonTmplId], success(res) { // 申请订阅成功 if (res.errMsg === 'requestSubscribeMessage:ok') { // 这里将订阅的课程信息调用云函数存入云开发数据 wx.cloud .callFunction({ name: 'subscribe', data: { data: item, templateId: lessonTmplId, }, }) .then(() => { wx.showToast({ title: '订阅成功', icon: 'success', duration: 2000, }); }) .catch(() => { wx.showToast({ title: '订阅失败', icon: 'success', duration: 2000, }); }); } }, }); [代码][代码] },[代码] 文章字数超出 50000 字,后半部分链接 《实战分享: 小程序云开发玩转订阅消息(二)》
2019-10-23 - 初试小程序接入three.js
看着小程序下的canvas日渐完善,特别是2.7.0库下新增了WebGL,终于可以摆脱原来用wx.createCanvasContext创建的2d上下文(不知为何在使用魔改后three.js中的CanvasRenderer渲染画面就是很慢,捕获JavaScript Profiler看着就是慢在draw方法上)。 不过理想很丰满,现实很骨感,想要在小程序上用three.js依然要来个大改造。让我们开始吧 官方文档里提供了一段如何获取WebGL Context的代码: [代码]Page({[代码][代码] [代码][代码]onReady() {[代码][代码] [代码][代码]const query = wx.createSelectorQuery()[代码][代码] [代码][代码]query.select([代码][代码]'#myCanvas'[代码][代码]).node().exec((res) => {[代码][代码] [代码][代码]const canvas = res[0].node[代码][代码] [代码][代码]const gl = canvas.getContext([代码][代码]'webgl'[代码][代码])[代码][代码] [代码][代码]console.log(gl)[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码]})[代码]我们就从这里入手 首先先写个wxml: [代码]<[代码][代码]canvas[代码] [代码]type[代码][代码]=[代码][代码]"webgl"[代码] [代码]id[代码][代码]=[代码][代码]"webgl"[代码] [代码]width[代码][代码]=[代码][代码]"{{canvasWidth||(320*2)}}"[代码] [代码]height[代码][代码]=[代码][代码]"{{canvasHeight||(504*2)}}"[代码] [代码]style[代码][代码]=[代码][代码]'width:{{canvasStyleWidth||"320px"}};height:{{canvasStyleHeight||"504px"}};'[代码] [代码]bindtouchstart[代码][代码]=[代码][代码]'onTouchStart'[代码] [代码]bindtouchmove[代码][代码]=[代码][代码]'onTouchMove'[代码] [代码]bindtouchend[代码][代码]=[代码][代码]'onTouchEnd'[代码][代码]></[代码][代码]canvas[代码][代码]>[代码]其中width和height是设置画布大小的,style中的width和height是设置画布的实际渲染大小的 然后js: [代码]onReady:[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]var[代码] [代码]query = wx.createSelectorQuery().select([代码][代码]'#webgl'[代码][代码]).node().exec((res) => {[代码][代码] [代码][代码]var[代码] [代码]canvas = res[0].node;[代码][代码] [代码][代码]requestAnimationFrame = canvas.requestAnimationFrame;[代码][代码] [代码][代码]canvas.width = canvas._width;[代码][代码] [代码][代码]canvas.height = canvas._height;[代码][代码] [代码][代码]canvas.style = {};[代码][代码] [代码][代码]canvas.style.width = canvas.width;[代码][代码] [代码][代码]canvas.style.height = canvas.height;[代码][代码] [代码][代码]self.init(canvas);[代码][代码] [代码][代码]self.animate();[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码]先模拟dom构造一个canvas对象,然后传入init方法中,我们在这里创建场景、相机、渲染器等 [代码]init: [代码][代码]function[代码] [代码](canvas) {[代码][代码]...[代码][代码] [代码][代码]camera = [代码][代码]new[代码] [代码]THREE.PerspectiveCamera(20, canvas.width / canvas.height, 1, 10000);[代码][代码] [代码][代码]scene = [代码][代码]new[代码] [代码]THREE.Scene();[代码][代码]...[代码][代码] [代码][代码]renderer = [代码][代码]new[代码] [代码]THREE.WebGLRenderer({ canvas: canvas, antialias: [代码][代码]true[代码] [代码]});[代码][代码] [代码][代码]}[代码]这样一个最基础的三维场景就搭好了,然后继续执行animate方法,开始渲染场景 [代码]animate:[代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]requestAnimationFrame([代码][代码]this[代码][代码].animate);[代码][代码] [代码][代码]this[代码][代码].render();[代码][代码] [代码][代码]}[代码]接下来尝试跑一下three.js提供的例子 webgl_geometry_colors : [图片] 锯齿问题比较严重,暂时没找到解决办法,但总体来说还是可以的,至少场景渲染出来了 由于暂时没想到如何改造CanvasTexture,我把例子中的 [代码]var[代码] [代码]canvas = document.createElement( [代码][代码]'canvas'[代码] [代码]);[代码][代码]canvas.width = 128;[代码][代码]canvas.height = 128;[代码][代码]var[代码] [代码]context = canvas.getContext( [代码][代码]'2d'[代码] [代码]);[代码][代码]var[代码] [代码]gradient = context.createRadialGradient( canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2 );[代码][代码]gradient.addColorStop( 0.1, [代码][代码]'rgba(210,210,210,1)'[代码] [代码]);[代码][代码]gradient.addColorStop( 1, [代码][代码]'rgba(255,255,255,1)'[代码] [代码]);[代码][代码]context.fillStyle = gradient;[代码][代码]context.fillRect( 0, 0, canvas.width, canvas.height );[代码][代码]var[代码] [代码]shadowTexture = [代码][代码]new[代码] [代码]THREE.CanvasTexture( canvas );[代码]替换成 webgl_geometries 例子中的TextureLoader [代码]var[代码] [代码]shadowTexture = [代码][代码]new[代码] [代码]THREE.TextureLoader().load(canvas,[代码][代码]'../../textures/UV_Grid_Sm.jpg'[代码][代码]);[代码]可能有人会发现load方法中传入的参数多了一个canvas,因为小程序提供的api没法直接创建Image对象,仅有一个Canvas.createImage()方法可以创建Image对象。因此我们还需要改造一下TextureLoader中的load方法,先看一下原版中的load方法: [代码]Object.assign( TextureLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]var[代码] [代码]texture = [代码][代码]new[代码] [代码]Texture();[代码] [代码] [代码][代码]var[代码] [代码]loader = [代码][代码]new[代码] [代码]ImageLoader( [代码][代码]this[代码][代码].manager );[代码][代码] [代码][代码]loader.setCrossOrigin( [代码][代码]this[代码][代码].crossOrigin );[代码][代码] [代码][代码]loader.setPath( [代码][代码]this[代码][代码].path );[代码] [代码] [代码][代码]loader.load( url, [代码][代码]function[代码] [代码]( image ) {[代码]其中实际调用了ImageLoader来加载图片,在看看ImageLoader: [代码]Object.assign( ImageLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]if[代码] [代码]( url === undefined ) url = [代码][代码]''[代码][代码];[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].path !== undefined ) url = [代码][代码]this[代码][代码].path + url;[代码] [代码] [代码][代码]url = [代码][代码]this[代码][代码].manager.resolveURL( url );[代码] [代码] [代码][代码]var[代码] [代码]scope = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]cached = Cache.get( url );[代码] [代码] [代码][代码]if[代码] [代码]( cached !== undefined ) {[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]setTimeout( [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( cached );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}, 0 );[代码] [代码] [代码][代码]return[代码] [代码]cached;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]var[代码] [代码]image = document.createElementNS( [代码][代码]'http://www.w3.org/1999/xhtml'[代码][代码], [代码][代码]'img'[代码] [代码]);[代码] [代码] [代码][代码]function[代码] [代码]onImageLoad() {[代码] [代码] [代码][代码]image.removeEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.removeEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]Cache.add( url, [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]function[代码] [代码]onImageError( event ) {[代码] [代码] [代码][代码]image.removeEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.removeEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onError ) onError( event );[代码] [代码] [代码][代码]scope.manager.itemError( url );[代码][代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]image.addEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.addEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( url.substr( 0, 5 ) !== [代码][代码]'data:'[代码] [代码]) {[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].crossOrigin !== undefined ) image.crossOrigin = [代码][代码]this[代码][代码].crossOrigin;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]image.src = url;[代码] [代码] [代码][代码]return[代码] [代码]image;[代码] [代码] [代码][代码]},[代码]document.createElementNS这种东西肯定是没法存在的,没办法,把canvas传进来用createImage方法创建Image对象,改造后: [代码]Object.assign( ImageLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( canvas,url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]if[代码] [代码]( url === undefined ) url = [代码][代码]''[代码][代码];[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].path !== undefined ) url = [代码][代码]this[代码][代码].path + url;[代码] [代码] [代码][代码]url = [代码][代码]this[代码][代码].manager.resolveURL( url );[代码] [代码] [代码][代码]var[代码] [代码]scope = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]cached = Cache.get( url );[代码] [代码] [代码][代码]if[代码] [代码]( cached !== undefined ) {[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]setTimeout( [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( cached );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}, 0 );[代码] [代码] [代码][代码]return[代码] [代码]cached;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//var image = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'img' );[代码][代码] [代码][代码]console.log([代码][代码]this[代码][代码], canvas);[代码][代码] [代码][代码]var[代码] [代码]image = canvas.createImage();[代码] [代码] [代码][代码]function[代码] [代码]onImageLoad() {[代码] [代码] [代码][代码]//image.removeEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.removeEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = [代码][代码]function[代码] [代码]() { };[代码][代码] [代码][代码]image.onerror = [代码][代码]function[代码] [代码]() { };[代码] [代码] [代码][代码]Cache.add( url, [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]function[代码] [代码]onImageError( event ) {[代码] [代码] [代码][代码]//image.removeEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.removeEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = [代码][代码]function[代码] [代码]() { };[代码][代码] [代码][代码]image.onerror = [代码][代码]function[代码] [代码]() { };[代码] [代码] [代码][代码]if[代码] [代码]( onError ) onError( event );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码][代码] [代码][代码]scope.manager.itemError( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//image.addEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.addEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = onImageLoad;[代码][代码] [代码][代码]image.onerror = onImageError;[代码] [代码] [代码][代码]if[代码] [代码]( url.substr( 0, 5 ) !== [代码][代码]'data:'[代码] [代码]) {[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].crossOrigin !== undefined ) image.crossOrigin = [代码][代码]this[代码][代码].crossOrigin;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]image.src = url;[代码] [代码] [代码][代码]return[代码] [代码]image;[代码] [代码] [代码][代码]},[代码]然后TextureLoader的load方法也改一下传参顺序: [代码]Object.assign( TextureLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( canvas,url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]var[代码] [代码]texture = [代码][代码]new[代码] [代码]Texture();[代码] [代码] [代码][代码]var[代码] [代码]loader = [代码][代码]new[代码] [代码]ImageLoader( [代码][代码]this[代码][代码].manager );[代码][代码] [代码][代码]loader.setCrossOrigin( [代码][代码]this[代码][代码].crossOrigin );[代码][代码] [代码][代码]loader.setPath( [代码][代码]this[代码][代码].path );[代码] [代码] [代码][代码]loader.load( canvas,url, [代码][代码]function[代码] [代码]( image ) {[代码]OK! 这个例子代码我放在https://github.com/leo9960/xcx_threejs,大家可以接着研究一下。潜力还是比较大的,比如我拿它搞了个全景展示 [图片] ---------------------------------------------------------------------- 2019.5.26 新上传了全景展示的范例,基于panolens.js,欢迎围观
2019-05-26 - map里的marker,点击弹出的信息窗怎么自定位类似于图中的效果?
[图片]
2019-10-11 - 如何用小程序实现类原生APP下一条无限刷体验
1.背景 如今信息流业务是各大互联网公司争先抢占的一个大面包,为了提高用户的后续消费,产品想出了各种各样的方法,例如在微视中,用户可以无限上拉出下一条视频;在知乎中,也可以无限上拉出下一条回答。这样的操作方式用户体验更好,后续消费也更多。最近几年的时间,微信小程序已经从一颗小小的萌芽成长为参天大树,形成了较大规模的生态,小程序也拥有了一个很大的流量入口。 2.demo体验 那如何才能在小程序中实现类原生APP效果的下一条无限刷体验? 这篇文章详细记录了下一条无限刷效果的实现原理,以及细节和体验优化,并将相关代码抽象成一个微信小程序代码片段,有需要的同学可查看demo源码。 线上效果请用微信扫码体验: [图片] 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a 3.实现原理 出于性能和兼容性考虑,我们尽量采用小程序官方提供的原生组件来实现下一条无限刷效果。我们发现,可以将无限上拉下一篇的文章看作一个竖向滚动的轮播图,又由于每一篇文章的内容长度高于一屏幕高度,所以需要实现文章内部可滚动,以及文章之间可以上拉和下拉切换的功能。 在多次尝试后,我们最终采用了在[代码]<swiper>[代码]组件内部嵌套一个[代码]<scroll-view>[代码]组件的方式实现,利用[代码]<swiper>[代码]组件来实现文章之间上拉和下拉切换的功能,利用[代码]<scroll-view>[代码]来实现一篇文章内部可上下滚动的功能。 所以页面的dom结构如下所示: [代码]<swiper class='scroll-swiper' circular="{{false}}" vertical="{{true}}" bindchange="bindChange" skip-hidden-item-layout="{{true}}" duration="{{500}}" easing-function="easeInCubic" > <block wx:for="{{articleData}}"> <swiper-item> <scroll-view scroll-top="0" scroll-with-animation="{{false}}" scroll-y > content </scroll-view> </swiper-item> </block> </swiper> [代码] 4.性能优化 我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。例如减少代码包体积,使用分包,渲染性能优化等。下面主要讲一下渲染性能优化。 4.1 dom优化 由于页面需要无限上拉刷新,所以要在[代码]<swiper>[代码]组件中不断的增加[代码]<swiper-item>[代码],这样必然会导致页面的dom节点成倍数的增加,最后非常卡顿。 为了优化页面的dom节点,我们利用[代码]<swiper>[代码]的[代码]current[代码]和[代码]<swiper-item>[代码]的[代码]index[代码]来做优化,控制是否渲染dom节点。首先,仅当[代码]index <= current + 1[代码]时渲染[代码]<swiper-item>[代码],也就是页面中最多预先加载出下一条,而不是将接口返回的所有后续数据都渲染出来;其次,对于用户已经消费过的之前的[代码]<swiper-item>[代码],不能直接销毁dom节点,否则会导致[代码]<swiper>[代码]的[代码]current[代码]值出现错乱,但是我们可以控制是否渲染[代码]<swiper-item>[代码]内部的子节点,我们设置了仅当[代码]current <= index + 1 && index -1 <= current[代码]时才会渲染[代码]<swiper-item>[代码]中的内容,也就是仅渲染当先文章,及上一篇和下一篇的文章内容,其他文章的dom节点都被销毁了。 这样,无论用户上拉刷新了多少次,页面中最多只会渲染3篇文章的内容,避免了因为上拉次数太多导致的页面卡顿。 4.2 分页时setData的优化 setData工作原理 [图片] 小程序的视图层目前使用[代码]WebView[代码]作为渲染载体,而逻辑层是由独立的 [代码]JavascriptCore[代码] 作为运行环境。在架构上,[代码]WebView[代码] 和 [代码]JavascriptCore[代码] 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 [代码]evaluateJavascript[代码] 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 [代码]JS[代码] 脚本,再通过执行 [代码]JS[代码] 脚本的形式传递到两边独立环境。 而 [代码]evaluateJavascript[代码] 的执行会受很多方面的影响,数据到达视图层并不是实时的。 每次 [代码]setData[代码] 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关。 [代码]setData[代码] 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互。 [代码]setData[代码] 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。 避免不当使用setData [代码]data[代码] 应仅包括与页面渲染相关的数据,其他数据可绑定在this上。使用 [代码]data[代码] 在方法间共享数据,会增加 setData 传输的数据量,。 使用 [代码]setData[代码] 传输大量数据,通讯耗时与数据正相关,页面更新延迟可能造成页面更新开销增加。仅传输页面中发生变化的数据,使用 [代码]setData[代码] 的特殊 [代码]key[代码] 实现局部更新。 避免不必要的 [代码]setData[代码],避免短时间内频繁调用 [代码]setData[代码],对连续的setData调用进行合并。不然会导致操作卡顿,交互延迟,阻塞通信,页面渲染延迟。 避免在后台页面进行 [代码]setData[代码],这样会抢占前台页面的渲染资源。可将页面切入后台后的[代码]setData[代码]调用延迟到页面重新展示时执行。 优化示例 无限上拉刷新的数据会采用分页接口的形式,分多次请求回来。在使用分页接口拉取到下一刷的数据后,我们需要调用[代码]setData[代码]将数据写进[代码]data[代码]的[代码]articleData[代码]中,这个[代码]articleData[代码]是一个数组,里面存放着所有的文章数据,数据量十分庞大,如果直接[代码]setData[代码]会增加通讯耗时和页面更新开销,导致操作卡顿,交互延迟。 为了避免这个问题,我们将[代码]articleData[代码]改进为一个二维数组,每一次[代码]setData[代码]通过分页的 [代码]cachedCount[代码]标识来实现局部更新,具体代码如下: [代码]this.setData({ [`articleData[${cachedCount}]`]: [...data], cachedCount: cachedCount + 1, }) [代码] [代码]articleData[代码]的结构如下: [图片] 4.3 体验优化 解决了操作卡顿,交互延迟等问题,我们还需要对动画和交互的体验进行优化,以达到类原生APP效果的体验。 在文章间上拉切换时,我们使用了[代码]<swiper>[代码]组件自带的动画效果,并通过设置[代码]duration[代码]和[代码]easing-function[代码]来优化滚动细节和动画。 当用户阅读文章到底部时,会提示下一篇文章的标题等信息,而在页面上拉时,由于下一篇文章的内容已经加载出来了,这样在滑动过程中会出现两个重复的标题。为了避免这种情况出现,我们通过一个占满屏幕宽高的空白[代码]<view>[代码]来将下一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]hidden="{{index !== current && index !== current + 1}}"[代码]来隐藏这个空白[代码]<view>[代码],并对这个空白[代码]<view>[代码]的高度变化增加动画,来实现下一篇文章从屏幕底部滚动到屏幕顶部的效果: [代码].fake-scroll { height: 100%; width: 100%; transition: height 0.3s cubic-bezier(0.167,0.167,0.4,1); } [代码] [图片] 而当用户想要上拉查看之前阅读过的文章时,我们需要给用户一个“下滑查看上一条”提示,所以也可以采用同上的方式,通过一个占满屏幕宽高的提示语[代码]<view>[代码]来将上一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]wx:if="{{index + 1 === current}}"[代码]来隐藏这个提示语[代码]<view>[代码],并对这个提示语[代码]<view>[代码]的透明度变化增加动画,来实现下拉时提示“下滑查看上一条”的效果: [代码].fake-previous { height: 100%; width: 100%; opacity: 0; transition: opacity 1s ease-in; } .fake-previous.show-fake-previous { opacity: 1; } [代码] 至此,这个类原生APP效果的下一条无限刷体验的需求的所有要点和细节都已实现。 记录在此,欢迎交流和讨论。 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a
2019-06-25 - 叠式轮播图
开发工具和iOS测过,android我没测过。。哈哈哈哈哈 https://developers.weixin.qq.com/s/kh8HhjmA7A4D 注释不知道写啥,简单描述了下 [图片]
2018-11-30 - iconfont硬核,支持多色彩、支持自定义颜色
目前市面上很多教程叫我们如何把iconfont的字体整到小程序中,基本千篇一律,都有一个特点,就是需要用字体文件。 但是用字体文件意味着只能设置一种颜色了(单色)。这是个硬伤~~~ 所以,今天笔者花了一天时间,做了一个支持多色彩、支持自定义颜色的iconfont开源库。你一定会喜欢 [代码]<iconfont name="alipay" /> <iconfont name="alipay" color="{{['red', 'orange']}}" size="300" /> [代码] [图片] 特性: 1、纯组件 2、不依赖字体文件 3、支持px和rpx两种格式 4、原样渲染多色彩图标 4、图标颜色可定制 地址:https://github.com/iconfont-cli/mini-program-iconfont-cli 喜欢的小伙伴记得给个star呦。
2019-09-26 - 复制任意微信小程序页面路径
以下以微信小程序“虎牙直播”为例,演示如何复制微信小程序页面的路径。 1.进入小程序的“关于虎牙直播”页面 [图片] 2.点击右上角的“…”进入“更多资料”页面 [图片] [图片] [图片] 3.复制AppID:wx74767bf0b684f7d3 4.进入小程序后台输入appid并搜索,然后点下一步 [图片] 5.鼠标移动到“获取更多页面路径”,在弹出窗口输入当前登陆的小程序的任意开发者微信号,然后点击开启,出现顶部的“开启入口成功”就可以使用手机访问“虎牙直播”任意页面进行复制了 [图片] 6.某个直播间的页面路径:pages/main/liveRoom/index.html?anchorUid=1678113423&source=search[图片] PS:复制出来的页面路径在小程序里使用的时候记得删除 .html 才能正常访问。
2020-01-16