- 手把手教你备案微信小程序(非个人主体备案)
备案材料准备 在提交备案前,请务必提前准备好备案所需材料,以免由于材料更新问题,导致备案需延期提交。下面将会带大家详细了解备案材料的要求,这样后续在提交时就能避免因为材料问题而导致失败。 材料示例及注意事项 [图片] 注:所有上传材料大小应不超过2M,分辨率不低于720* 1280 ,仅支持JPG、JPEG、PNG 格式 若想查看更多小程序备案材料示例,详情可查看文档 备案信息填写 备案材料准备好后,就可以前往【小程序管理后台-设置-小程序备案】提交备案申请了。下面将会详细教大家如何进行备案信息的填写,一共分为五个部分:主办单位信息填写、主体负责人信息填写、小程序信息填写、小程序管理员信息填写和上传其他信息材料。 1.主办单位信息填写 [图片] [图片] 填写说明 常见报错/问题 解决方案 ①选择地区:选择与证件地址相一致的省市区信息 该主体已在XX完成备案,请修改备案省份或注销备案主体重新备案 请核实该主体是否有在其他省份备案过,由于同主体在所有平台的备案省份必须保持一致,需修改备案省份或注销备案主体重新备案 ②主办者性质:默认与小程序主体认证信息相一致 / / ③证件类型:默认与小程序主体认证信息相一致 / / ④上传证件:按要求提供最新版证件 营业执照有效期不足 请联系工商部门更新证件有效期 ⑤企业名称:填写证件相对应名称信息 主办者与小程序主体不一致 请核实填写企业名称是否与小程序主体名称、上传营业执照名称相一致 ⑤企业名称:填写证件相对应名称信息 营业执照名称为空或者* 号 请联系工商部门更新企业名称信息 ⑥证件住所:填写证件相对应经营场所信息 【主体证件住所】工商数据对比不通过 请参考文档进行排查 ⑦证件号码:填写证件相对应统一社会信用代码信息 未查询到企业信息,请检查主体证件号是否有误 请核实填写的是否为统一社会信用代码,若无,请联系工商部门更新证件信息,不能填写其他如工商注册号等 ⑧通讯地址:填写当前主体所在的实际通讯地址(无需填写省、市、区) 通讯地址未能精确到门牌号 若无具体门牌号,需要在备注中说明情况 ⑨备注(选填):针对主体信息进行补充说明,如有可填写 / / 注:若为新建企业或近期有做信息变更,可能会存在企业工商数据更新延迟的情况,建议过段时间(5~15个工作日)再进行重试,否则无法正常发起验证流程。 2.主体负责人信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①证件类型:选择主体负责人证件类型信息 / / ②上传证件:按要求提供最新版证件 / / ③负责人名称:通过上传证件自动识别,有误可自行修改 主体负责人与法定代表人不一致,且备案所在地不支持法定代表人授权 请核实填写的主体负责人是否为法人,需与营业执照信息一致,由于所属地区不支持授权,只能填写法人信息 ④负责人证件号:通过上传证件自动识别,有误可自行修改 【主体负责人证件号码】企业工商四要素核验失败 请核实填写的主体负责人名称、证件号信息是否正确 ⑤证件有效期:通过上传证件自动识别,有误可自行修改 / / ⑥手机号:主体负责人手机号码 【主体负责人手机号码】不允许被多人使用 请核实填写的手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑦验证码:主体负责人手机号码收到的对应验证码 验证码不正确 请核实验证码是否已失效,验证码有效期为10分钟 ⑧应急手机号:主体负责人的应急电话 【主体负责人应急联系方式】不允许被多人使用 请核实填写的应急手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑨邮箱地址:主体负责人的电子邮箱 / / 3.小程序信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①服务内容标识:根据小程序实际运营内容选择合适的即可 小程序服务内容类型数目不能超过5个 服务内容标识是通信管局对各个行业的分类,平台部分行业类目与管局行业类目名称不完全不一致,建议根据备案小程序实际运营内容尽可能选择对应的服务内容标识,最多选择5个。 ②互联网信息服务前置审批项:根据小程序实际运营内容判断是否需要进行前置审批 如从事XXX业务,请上传前置审批文件 小程序实际运营内容涉及前置审批项,需上传对应的审批文件 ②互联网信息服务前置审批项:根据小程序实际运营内容判断是否需要进行前置审批 前置审批项必须选择“以上都不涉及” 小程序实际运营内容不涉及前置审批项,需要选择"以上都不涉及” ③备注(必填):具体描述小程序实际经营内容,主要服务内容 请在小程序备注按格式填写 请核实是否有根据备注格式进行填写,仅自行补充带星号内容即可。 4.小程序管理员信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①证件类型:选择小程序负责人证件类型信息(目前仅支持身份证) / / ②上传证件:按要求提供最新版证件(目前仅支持身份证) / / ③负责人名称:通过上传证件自动识别,有误可自行修改 【小程序负责人姓名】负责人与小程序管理员不一致 请核实小程序是否未完善管理员实名信息,需参考指引文档进行补充 ④负责人证件号:通过上传证件自动识别,有误可自行修改 【小程序负责人证件号码】负责人与小程序管理员不一致 请核实小程序是否未完善管理员实名信息,需参考指引文档进行补充 ⑤证件有效期:通过上传证件自动识别,有误可自行修改 / / ⑥手机号:小程序负责人手机号码 【小程序负责人手机号码】不允许被多人使用 请核实填写的手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑦验证码:小程序负责人手机号码收到的对应验证码 验证码不正确 请核实验证码是否已失效,验证码有效期为10分钟 ⑧应急手机号:小程序负责人的应急电话 【小程序负责人应急联系方式】不允许被多人使用 请核实填写的应急手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑨邮箱地址:小程序负责人的电子邮箱 / / ⑩负责人人脸核身:小程序管理员(小程序负责人)需使用微信APP扫码,完成人脸核身 当前场景仅支持居民身份证 核实小程序管理员证件是否为大陆居民身份证,目前港澳台管理员无法进行人脸核身,建议先更换小程序管理员为中国大陆地区的人员,作为备案小程序负责人。 5.上传其他信息材料 [图片] 填写说明 常见报错/问题 解决方案 互联网信息服务承诺书:1. 广东地区:下载页面提供的模版文件,填写完整后上传提交;2. 非广东地区:点击阅读确认后提交 承诺书需加盖公章,但个体户没有公章 若个体工商户无公章,需要主体负责人手写日期+签名+盖手印+身份证号码,同时请在主体备注处备注“个体工商户无公章”。注:江苏、宁夏、福建地区,须刻章后提交备案,不接受负责人手印。 以上信息都填写完毕,就可点击提交,后续的备案审核流程可参考: [图片] 如有其他相关疑问,欢迎随时参与社区讨论。
05-28 - 新版微信web-view嵌入的h5页面,audio组件播放音乐在锁屏后自动停止播放
自从用户更新到新的版本后,web-view嵌入的h5页面,audio组件播放音乐在锁屏后自动停止播放,但是在之前的老版本中是可以播放的,麻烦问一下,怎么兼容?
2020-11-17 - webview 嵌入的界面里的音频,在有些手机息屏后无法继续播放,如何实现可以息屏的时候继续播放?
在小程序里的webview外链的界面https://m.qingxuetang.com/x/entry/hY_pWrl4lfs里,播放音频,有些手机息屏后可以继续播放,有些不可以。安卓手机小米10就不可以继续播放。
2020-11-24 - 小程序image如何实现全屏适配所有机型?
有没有办法让一张image实现全屏适配各种宽高不同的屏幕呢? 代码片段 https://developers.weixin.qq.com/s/gi7ZEjmC7YhE [图片][图片][图片][图片][图片][图片]
2020-05-13 - 微信小程序头像昵称实战篇
2022-08-25 api文档地址: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html 目前的api变更后,得到的地址为 临时地址, 这个是文档没有说明的, 最佳实践,是需要把得到的地址上传到自己的服务器,然后用服务器返回的地址作为 真实头像的永久地址. 核心点说明: //获取到api返回的新地址路径 onChooseAvatar(e) { this.avatarUrl = e.detail.avatarUrl console.log('e.detail', e.detail) // this.updateUserInfo(); this.uploadFile(); }, /* 上传 头像 转 话格式*/ uploadFile(){ uni.uploadFile({ url: config.webUrl + '/upload/uploadImages',//后台接口 filePath: this.avatarUrl,// 上传图片 url name:'image', // formData: this.formData, header: { 'content-type': 'multipart/form-data', 'token': uni.getStorageSync('token') }, // header 值 success: res => { let obj = JSON.parse(res.data) console.log('obj', obj) if (obj.code == 1) { let imgUrl = obj.data.full_path; this.userImg = imgUrl; this.updateUserInfo(); } else { uni.showToast({ icon: 'none', title: '图片太大,请重新选择!' }); } }, fail: e => { this.$toast('上传失败') } }); }, 这里需要注意, wx.uploadFile 返回的是字符串类型,需要前端自己处理一下数据结构: [图片] 完整代码如下: import config from "@/common/config.js"; import {debounce} from '@/utils/debounce.js' export default { data() { return { defaultAvatarUrl: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0', avatarUrl: '', nick_name: '', userImg: '', } }, onLoad() { let userInfo = uni.getStorageSync('userInfo') || {}; let { nick_name,img_url } = {...userInfo}; this.userImg = img_url; this.nick_name = nick_name; }, methods: { onChooseAvatar(e) { this.avatarUrl = e.detail.avatarUrl console.log('e.detail', e.detail) // this.updateUserInfo(); this.uploadFile(); }, inputWord: debounce(function(e){ this.nick_name = e.detail.value console.log('this.nick_name.length',this.nick_name.length) let str = this.nick_name.trim(); if(str.length==0){ this.$toast('请输入合法的昵称') return } if((/[^/a-zA-Z0-9\u4E00-\u9FA5]/g).test(str)){ this.$toast('请输入中英文和数字') return } this.updateUserInfo() }, 1500), /* 上传 头像 转 话格式*/ uploadFile(){ uni.uploadFile({ url: config.webUrl + '/upload/uploadImages',//后台接口 filePath: this.avatarUrl,// 上传图片 url name:'image', // formData: this.formData, header: { 'content-type': 'multipart/form-data', 'token': uni.getStorageSync('token') }, // header 值 success: res => { let obj = JSON.parse(res.data) console.log('obj', obj) if (obj.code == 1) { let imgUrl = obj.data.full_path; this.userImg = imgUrl; this.updateUserInfo(); } else { uni.showToast({ icon: 'none', title: '图片太大,请重新选择!' }); } }, fail: e => { this.$toast('上传失败') } }); }, updateUserInfo(){ let self = this; uni.showLoading({}); let params = { img_url: this.userImg, nick_name: this.nick_name.trim(), } self.$http.post('updateUserInfo', params).then(res => { uni.hideLoading() if (res.data.code == 1) { self.$toast('修改成功!') }else { self.$toast(res.data.msg) } }) }, } } 请一键三连,争取升个级,谢谢各位道友! 补充一下,如果api不生效注意切换一下版本库: 我本地用的2.26.1 [图片] 实际效果图: [图片] [图片]
2022-11-24 - 可靠关闭小程序轮询定时器的方法
小程序页面实现轮询的定时器转到别的页面时并不会停止,还在幕后运行。用下述方法可彻底清除它: 1.在App.js中设置一全局变量: globalData{ timerSwitch: ""} 2.在该页面: onShow: function(){ App.globalData.timerSwitch="1" }, onLoad: function(){ //其他函数也是同样写法 var that=this ........//要做的事 if(App.globalData.timerSwitch=='1') var timer=setTimeout(function(){ that.onLoad();console.log('定时器在运行') clearTimeout(timer) },1000)//1000毫秒 }, 3.在相邻的页面中: onShow: function(){ App.globalData.timerSwitch="" }, 这样做,当该页面返回或转到相邻页面时,定时器timer就嘎然而止了(在console上不再出现'定时器在运行'了!) 讨论:当然某些情况下不用全局变量也行,比如用本页data:{ timerSwitch:””},然后在onHide: function(){This.data.timerSwitch=""},停用定时器。但不可靠,有时好几个页面的定时器在幕后群魔乱舞!
2022-11-13 - 微信小程序中安全区域计算和适配
前言 自从iphoneX问世之后,因为iphoneX、iphoneXR和后续全面屏手机设备,因为物理Home键被底部小黑条代替了,这时候很多前端小伙伴在开发的过程都会遇到 “全面屏”和“非全面屏”的兼容性问题,普遍问题就是底部按钮或者选项卡与底部黑线重叠 解释 根据官方解释: 安全区域指的是一个可视窗口范围,处于安全区域的内容不受圆角(corners)、齐刘海(sensor housing)、小黑条(Home Indicator)的影响。 具体区域如图展示 [图片] 适配方案 当前有效的解决方式有几种 使用已知底部小黑条高度34px/68rpx来适配 使用苹果官方推出的css函数env()、constant()适配 使用微信官方API,getSystemInfo()中的safeArea对象进行适配 使用已知底部小黑条高度34px/68rpx来适配 这种方式是根据实践得出,通过物理方式测出iPhone底部的小黑条(Home Indicator)高度是34px,实际在开发者工具选中真机获取到高度也是34px,所以直接根据该值,设置margin-bottom、padding-bottom、height也能实现。同时这样做要有一个前提,需要判断当前机型是需要适配安全区域的机型。 但是这种方案相对来说是不推荐使用的。比较是一个比较古老原始的方案 使用苹果官方推出的css函数env()、constant()适配 这种方案是苹果官方推荐使用env(),constant()来适配,开发者不需要管数值具体是多少。 env和constant是IOS11新增特性,有4个预定义变量: safe-area-inset-left:安全区域距离左边边界的距离 safe-area-inset-right:安全区域距离右边边界的距离 safe-area-inset-top:安全区域距离顶部边界的距离 safe-area-inset-bottom :安全距离底部边界的距离 具体用法如下: Tips: constant和env不能调换位置 [代码] padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/ padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/ [代码] 其实利用这个能解决大部分的适配场景了,但是有时候开发需要自定义头部信息,这时候就没办法使用css来解决了 使用微信官方API,getSystemInfo()中的safeArea对象进行适配 通过 wx.getSystemInfo获取到各种安全区域信息,解析出具体的设备类型,通过设备类型做宽高自适应,话不多说,直接上代码 代码实现 [代码] const res = wx.getSystemInfoSync() const result = { ...res, bottomSafeHeight: 0, isIphoneX: false, isMi: false, isIphone: false, isIpad: false, isIOS: false, isHeightPhone: false, } const modelmes = result.model const system = result.system // 判断设备型号 if (modelmes.search('iPhone X') != -1 || modelmes.search('iPhone 11') != -1) { result.isIphoneX = true; } if (modelmes.search('MI') != -1) { result.isMi = true; } if (modelmes.search('iPhone') != -1) { result.isIphone = true; } if (modelmes.search('iPad') > -1) { result.isIpad = true; } let screenWidth = result.screenWidth let screenHeight = result.screenHeight // 宽高比自适应 screenWidth = Math.min(screenWidth, screenHeight) screenHeight = Math.max(screenWidth, screenHeight) const ipadDiff = Math.abs(screenHeight / screenWidth - 1.33333) if (ipadDiff < 0.01) { result.isIpad = true } if (result.isIphone || system.indexOf('iOS') > -1) { result.isIOS = true } const myCanvasWidth = (640 / 375) * result.screenWidth const myCanvasHeight = (1000 / 667) * result.screenHeight const scale = myCanvasWidth / myCanvasHeight if (scale < 0.64) { result.isHeightPhone = true } result.navHeight = result.statusBarHeight + 46 result.pageWidth = result.windowWidth result.pageHeight = result.windowHeight - result.navHeight if (!result.isIOS) { result.bottomSafeHeight = 0 } const capsuleInfo = wx.getMenuButtonBoundingClientRect() // 胶囊热区 = 胶囊和状态栏之间的留白 * 2 (保持胶囊和状态栏上下留白一致) * 2(设计上为了更好看) + 胶囊高度 const navbarHeight = (capsuleInfo.top - result.statusBarHeight) * 4 + capsuleInfo.height // 写入胶囊数据 result.capsuleInfo = capsuleInfo; // 安全区域 const safeArea = result.safeArea // 可视区域高度 - 适配横竖屏场景 const screenHeight = Math.max(result.screenHeight, result.screenWidth) const height = Math.max(safeArea.height, safeArea.width) // 状态栏高度 const statusBarHeight = result.statusBarHeight // 获取底部安全区域高度(全面屏手机) if (safeArea && height && screenHeight) { result.bottomSafeHeight = screenHeight - height - statusBarHeight if (result.bottomSafeHeight < 0) { result.bottomSafeHeight = 0 } } // 设置header高度 result.headerHeight = statusBarHeight + navbarHeight // 导航栏高度 result.navbarHeight = navbarHeight [代码]
2022-11-04 - 贴一个小程序端db.collection 操作的封装。
封装在utils里 const collectionGet = async function (collectionName, getData = {}) { let { where, skip, limit } = getData; where = where ? where : {}; skip = skip ? skip : 0; limit = limit ? limit : 5; wx.showLoading({ title: '加载ing...', }) try { const res = await db.collection(collectionName).where(where).skip(skip).limit(limit).get(); return res['data']; } catch (error) { return error; } finally{ wx.hideLoading({ success: (res) => {}, }) } } 页面调用很方便: const result_get=await utils.collectionGet("compositions",{ where:{ _id:docid } });
2022-06-16 - 小程序跨页面通信解决方案
场景介绍 在小程序开发过程中,难免会遇到一种情况,当A页面需要用户设置数据 点击进入B页面,在B页面设置成功后返回并将设置的值传递给A页面。但是wx.navigateBack()并不支持返回传参。这种情况下就可以使用onfire.js,onfire.js 是一个很简单的事件分发的 Javascript 库(仅仅 0.9kb),简洁实用。 下载地址 onfire.js下载地址 https://www.bootcdn.cn/onfire.js/ https://gitee.com/mirrors/onfire-js 如何使用 将onfire.js下载下来并放置在开发项目某个目录下,例如根目录lib文件夹内。 在使用页面对应的js文件中引入该文件。 代码如下: A页面 [代码]<!--index.wxml--> <view class="order"> <view class="order-alert">设置您的联系方式</view> <view class="order-mobile" bindtap="setMobile"> <view class="order-mobile__caption">联系方式</view> <view class="order-mobile__content"> <text class="valign-mid"> <text>{{ mobile }}</text> </text> <text class="iconfont order-mobile__content__more"></text> </view> </view> </view> [代码] [代码]//index.js const onfire = require('../../lib/onfire.js') Page({ data: { mobile: '' }, onLoad: function () { // 绑定事件,当名为EventPhoneChange的事件发生的时 onfire.on('EventPhoneChange', e => { this.setData({ mobile: e }) }) }, // 设置手机号 setMobile: function () { wx.navigateTo({ url: '../phone/phone?mobile=' + this.data.mobile, }) }, onUnload: function () { // 取消事件绑定 onfire.un("EventPhoneChange"); } }) [代码] B页面 [代码]<!--phone.wxml--> <view class="phone"> <input class="phone-input" placeholder="手机号码" bindinput="bindinput" value="{{mobile}}"></input> <button class="phone-setting" bindtap="tapSetting">设置</button> </view> [代码] [代码]// phone.js const onfire = require('../../lib/onfire.js') Page({ data: { mobile: '' }, onLoad: function (e) { this.setData({ mobile: e.mobile }) }, tapSetting: function () { let mobile = this.data.mobile; if (!mobile) { wx.showToast({ title: '请填写手机号!', icon: 'none', duration: 2000, }) return; } // 触发名为EventPhoneChange的事件,并且携带变量mobile值。 onfire.fire('EventPhoneChange', mobile) wx.navigateBack() }, bindinput: function (e) { this.setData({ mobile: e.detail.value.trim() }) } }) [代码] 其效果图如下: 图一 [图片] 图二 [图片] 图三 [图片] 相关API 关于onfire.js的API 1.on(event_name, callback) 绑定事件,参数为event_name和callback, 当有名字为event_name的事件发生的时候,callback方法将会被执行。这个方法会返回一个eventObj,这个可以用于使用un(eventObj)方法来取消事件绑定。 2.one(event_name, callback) 绑定(订阅)事件,参数为 event_name with callback. 当被触发一次之后失效。只能被触发一次,一次之后自动失效。 3.fire(event_name, data) 触发名字为event_name的事件,并且赋予变量data为callback方法的输入值。 4.un(eventObj / eventName / function)取消事件绑定。可以仅仅取消绑定一个事件回调方法,也可以直接取消全部的事件; 5.clear() 清空所有事件。 总结 个人对于onfire.js的理解就是一个全局通知,从B页面发出一个通知带上Key和Value,在其他某一个页面监听这个Key值来获取传出来的Value。 其他解决方案 该方案来自摩拜单车小程序 https://juejin.im/post/5da80767f265da5b5f7584dc
2019-12-20 - 如何打开线上版本小程序的调试模式
生产版本的小程序如果出现问题,可以调试一下正式版看看,调试方式如下: 方式1、https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/wx.setEnableDebug.html [代码]wx.setEnableDebug(Object object) 基础库 1.4.0 开始支持,低版本需做兼容处理。 设置是否打开调试开关。此开关对正式版也能生效。 参数 Object object 属性 类型 默认值 必填 说明 enableDebug boolean 是 是否打开调试 success function 否 接口调用成功的回调函数 fail function 否 接口调用失败的回调函数 complete function 否 接口调用结束的回调函数(调用成功、失败都会执行) 示例代码 // 打开调试 wx.setEnableDebug({ enableDebug: true }) // 关闭调试 wx.setEnableDebug({ enableDebug: false }) Tips [代码] 方式2、先在开发版或体验版打开调试,再切到正式版就能看到vConsole
2019-11-29 - 关于开放社区需要优化的几点建议?
根据大家反馈微信社区待完善以下几点 1、点赞涨了但是不知道是谁点的 2、收藏加了但是不知道被谁收藏的 欢迎盖楼
2019-11-20 - Wxml2Canvas -- 快速生成小程序分享图通用方案
Wxml2Canvas库,可以将指定的wxml节点直接转换成canvas元素,并且保存成分享图,极大地提升了绘制分享图的效率。目前被应用于微信游戏圈、王者荣耀、刺激战场助手等小程序中。 github地址:https://github.com/wg-front/wxml2canvas 一、背景 随着小程序应用的日渐成熟,多处场景需要能够生成分享图便于用户进行二次传播,从而提升小程序的传播率以及加强品牌效应。 对于简单的分享图,比如固定大小的背景图加几行简短文字构成的分享小图,我们可以利用官方提供的canvas接口将元素直接绘制, 虽然繁琐了些,但能满足基本要求。 对于复杂的分享图,比如用户在微信游戏圈发表完话题后,需要将图文混排的富文本内容生成分享图,对于这种长度不定,内容动态变化的图片生成需求,直接利用官方的canvas接口绘制是十分困难的,包括但不限于文字换行、表情文字图片混排、文字加粗、子标题等元素都需要一一绘制。又如王者荣耀助手小程序,需要将十人对局的详细战绩绘制成分享图,包含英雄数据、装备、技能、对局结果等信息,要绘制100多张图片和大量的文字信息,如果依旧使用官方的接口一步一步绘制,对开发者来说简直就是一场噩梦。我们急需一种通用、高效的方式完成上述的工作。 在这样的背景下,wxml2cavnas诞生了,作为一种分享图绘制的通用方案,它不仅能快速的绘制简单的固定小图,还能直接将wxml元素真实地转换成canvas元素,并且适配各种机型。无论是复杂的图文混排的富文本内容,还是展现形式多样的战绩结果页,都可以利用wxml2cavnas完美地快速绘制并生成所期望的分享图片。 二、Wxml2Canvas介绍及示例 1. 介绍 Wxml2Cavnas库,是一个生成小程序分享图的通用方案,提供了两种绘制方式: 封装基础图形的绘制接口,包括矩形、圆形、线条、图片、圆角图片、纯文本等,使用时只需要声明元素类型并提供关键数据即可,不需要再关注canvas的具体绘制过程; wxml直接转换成canvas元素,使用时传入待绘制的wxml节点的class类名,并且声明绘制此节点的类型(图片、文字等),会自动读取此节点的computedStyle,利用这些数据完成元素的绘制。 2. 生成图示例 下面是两张极端复杂的分享图。 2.1 游戏圈话题 [图片] 点击查看完整长图 2.2.2 王者荣耀战绩 [图片] 点击查看完整大图 三、小程序的特性及局限 小程序提供了如下特性,可供我们便捷使用: measureText接口能直接测量出文本的宽度; SelectorQuery可以查询到节点对应的computedStyle。 利用第一条,我们在绘制超长文本时便于文本的省略或者换行,从而避免文字溢出。 利用第二条,我们可以根据class类名,直接拿到节点的样式,然后将style转换成canvas可识别的内容。 但是和html的canvas相比,小程序的canvas局限性很多。主要体现在如下几点: 不支持base64图片; 图片必须下载到本地后才能绘制到画布上; 图片域名需要在管理平台加入downFile安全域名; canvas属于原生组件,在移动端会置于最顶层; 通过SelectorQuery只能拿到节点的style,而无法获取文本节点的内容以及图片节点的链接。 针对以上问题,我们需要将base64图片转换jpg或png格式的图片,实现图片的统一下载逻辑,并且离屏绘制内容。针对第五条,好在SelectorQuery可以获取到节点的dataset属性,所以我们需要在待绘制的节点上显示地声明其类型(imgae、text等),并且显示地传入文本内容或图片链接,后文会有示例。 四、Wxml2Canvas使用方式 1. 初始化 首先在wxml中创建canvas节点,指定宽高: [代码] <canvas canvas-id="share" style="height: {{ height * zoom }}px; width: {{ width * zoom }}px;"> </canvas> [代码] 引入代码库,创建DrawImage实例,并传入如下参数: [代码] let DrawImage = require('./wxml2canvas/index.js'); let zoom = this.device.windowWidth / 375; let width = 375; let height = width * 3; let drawImage = new DrawImage({ element: 'share', // canvas节点的id, obj: this, // 在组件中使用时,需要传入当前组件的this width: width, // 宽高 height: height, background: '#161C3A', // 默认背景色 gradientBackground: { // 默认的渐变背景色,与background互斥 color: ['#17326b', '#340821'], line: [0, 0, 0, height] }, progress (percent) { // 绘制进度 }, finish (url) { // 画完后返回url }, error (res) { console.log(res); // 画失败的原因 } }); [代码] 所有的数字参数均以iphone6为基准,其中参数width和height决定了canvas画布的大小,规定值是在iphone6机型下的固定数值; zoom参数的作用是控制画布的缩放比例,如果要求画布自适应,则应传入 windowWidth / 375,windowWidth为手机屏幕的宽度。 2. 传入数据,生成图片 执行绘制操作: [代码] drawImage.draw(data, this); [代码] 执行绘制时需要传入数据data,数据的格式分为两种,下面展开介绍。 2.1 基础图形 第一种为基础的图形、图文绘制,直接使用官方提供接口,下面代码是一个基本的格式: [代码] let data = { list: [{ type: 'image', url: 'https://xxx', class: 'background_image', // delay: true, x: 0, y: 0, style: { width: width, height: width } }, { type: 'text', text: '文字', class: 'title', x: 0, y: 0, style: { fontSize: 14, lineHeight: 20, color: '#353535', fontFamily: 'PingFangSC-Regular' } }] } [代码] 如上,type声明了要元素的类型,有image、text、rect、line、circle、redius_image(圆角图)等,能满足绝大多数情况。 class类名指定了使用的样式,需要在style中写出,符合css样式规范。 delay参数用来异步绘制元素,会把此元素放在第二个循环中绘制。 x,y用来指定元素的起始坐标。 将css样式与元素分离的目的是便于管理与复用。 此种方式每个元素都相互独立,互不影响,能够满足自由度要求高的情况,可控性高。 2.2 wxml转换 第二种方式为指定wxml元素,自动获取,下面是示例: [代码] let data = { list: [{ type: 'wxml', class: '.panel .draw_canvas', limit: '.panel' x: 0, y: 0 }] } [代码] 如上,type声明为wxml时,会查找所有类名为draw_canvas的节点,并且加入到绘制队列中。 class传入的第一个类名限定了查询的范围,可以不传,第二个用来指定查找的节点,可以定义为任意不影响样式展现的通用类名。 limit属性用来限定相对位置,例如,一个文本的位置(left, top) = (50, 80), class为panel的节点的位置为(left, top) = (20, 40),则文本canvas上实际绘制的位置(x, y) = (50 - 20, 80 -40) = (30, 40)。如果不传入limit,则以实际的位置(x, y) = (50, 80)绘制。 由于小程序节点元素查询接口的局限,无法直接获取节点的文本内容和图片标签的src属性,也无法直接区分是文本还是图片,但是可以获取到dataset,所以我们需要在节点上显示地声明data-type来指明类型,再声明data-text传入文字或data-url传入图片链接。下面是个示例: [代码] <view class="panel"> <view class="panel__img draw_canvas" data-type="image" data-url="https://xxx"></view> <view class="panel__text draw_canvas" data-type="text" data-text="文字">文字</view> </view> [代码] 如上,会查询到两个节点符合条件,第一个为image图片,第二个为text文本,利用SelectorQuery查询它们的computedStyle,分别得到left、top、width、height等数据后,转换成canvas支持的格式,完成绘制。 除此之外,下面的示例功能更加丰富: [代码] <view class="panel"> <view class="panel__text draw_canvas" data-type="background-image" data-radius="1" data-shadow="" data-border="2px solid #000"></view> <view class="panel__text draw_canvas" data-type="text" data-background="#ffffff" data-padding="2 3 0 0" data-delay="1" data-left="10" data-top="10" data-maxlength="4" data-text="这是个文字">这是个文字</view> </view> [代码] 如上,第一个data-type为background-image,表示读取此节点的背景图片,因为可以通过computedStyle直接获取图片链接,所以不需要显示传入url。声明data-radius属性,表示要将此图绘成乘圆形图片。data-border属性表示要绘制图片的边框,虽然也可以通过computedStyle直接获取,但是为了避免非预期的结果,还是要声明传入,border格式应符合css标准。此外,图片的box-shadow等样式都会根据声明绘制出来。 第二个文本节点,声明了data-background,则会根据节点的位置属性给文字增加背景。 data-padding属性用来修正背景的位置和宽高。data-delay属性用来延迟绘制,可以根据值的大小,来控制元素的层级,data-left和data-top用来修正位置,支持负值。data-maxlength用来限制文本的最大长度,超长时会截取并追加’…’。 此外,data-type还有inline-text,inline-image等行内元素的绘制,其实现较为复杂,会在后文介绍。 五、Wxml2Canvas实现原理 1. 绘制流程 整个绘制流程如下: [图片] 因为小程序的限制,只能在画布上绘制本地图片,所以统一先对图片提前下载,然后再绘制,为了避免图片重复下载,内部维护一个图片列表,会对相同的图片链接去重,减少等待时间。 2. 基本图形的实现 基础图形的绘制比较简单,内部实现只是对基础能力的封装,使用者不用再关注canvas的绘制过程,只需要提供关键数据即可,下面是一个图片绘制的实现示例: [代码] function drawImage (item, style) { if(item.delay) { this.asyncList.push({item, style}); }else { if(item.y < 0) { item.y = this.height + item.y * zoom - style.height * zoom; }else { item.y = item.y * zoom; } if(item.x < 0) { item.x = this.width + item.x * zoom - style.width * zoom; }else { item.x = item.x * zoom; } ctx.drawImage(item.url, item.x, item.y, style.width * zoom, style.height * zoom); ctx.draw(true); } } [代码] 如上,x,y值坐标支持传入负值,表示从画布的底部和右侧计算位置。 3. Wxml转Canvas元素的实现 3.1 computedStyle的获取 首先需要获取wxml的样式,代码示例如下: [代码] query.selectAll(`${item.class}`).fields({ dataset: true, size: true, rect: true, computedStyle: ['width', 'height', ...] }, (res) => { self.drawWxml(res); }) [代码] 3.2 块级元素的绘制 对于声明为image、text的元素,默认为块级元素,它们的绘制都是独立进行的,不需要考虑其他的元素的影响,以wxml节点为圆形的image为例,下面是部分代码: [代码] if(sub.dataset.type === 'image') { let r = sub.width / 2; let x = sub.left + item.x * zoom; let y = sub.top + item.y * zoom; let leftFix = +sub.dataset.left || 0; let topFix = +sub.dataset.top || 0; let borderWidth = sub.borderWidth || 0; let borderColor = sub.borderColor; // 如果是圆形图片 if(sub.dataset.radius) { // 绘制圆形的border if(borderWidth) { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setStrokeStyle(borderColor) ctx.setLineWidth(borderWidth) ctx.stroke() ctx.closePath() } // 绘制圆形图片的阴影 if(sub.boxShadow !== 'none') { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setFillStyle(borderColor); setBoxShadow(sub.boxShadow); ctx.fill() ctx.closePath() } // 最后绘制圆形图片 ctx.save(); ctx.beginPath(); ctx.arc((x + r), (y + r) - limitTop, r, 0, 2 * Math.PI); ctx.clip(); ctx.drawImage(url, x + leftFix * zoom, y + topFix * zoom, sub.width, sub.height); ctx.closePath(); ctx.restore(); }else { // 常规图片 } } [代码] 如上,块级元素的绘制和基础图形的绘制差异不大,理解起来也很容易,不再多述。 3.3 令人头疼的行内元素的绘制 当wxml的data-type声明为inline-image或者inline-text时,我们认为是行内元素。行内元素的绘制是一个难点,因为元素之前存在关联,所以不得不考虑各种临界情况。下面展开细述。 3.3.1 纯文本换行 对于长度超过一行的行内元素,需要计算出合适的换行位置,下图所示的是两种临界情况: [图片] [图片] 如上图所示,第一种情况为最后一行只有一个文字,第二种情况最后一行的文字长度和宽度相同。虽然长度不同,但都可通过下面代码绘制: [代码] let lineNum = Math.ceil(measureWidth(text) / maxWidth); // 文字行数 let sinleLineLength = Math.floor(text.length / lineNume); // 向下取整,保证多于实际每行字数 let currentIndex = 0; // 记录文字的索引位置 for(let i = 0; i < lineNum; i++) { let offset = 0; // singleLineLength并不是精确的每行文字数,要校正 let endIndex = currentIndex + sinleLineLength + offset; let single = text.substring(currentIndex, endIndex); // 截取本行文字 let singleWidth = measureWidth(single); // 超长时,左移一位,直至正好 while(singleWidth > maxWidth) { offset--; endIndex = currentIndex + sinleLineLength + offset; single = text.substring(currentIndex, endIndex); singleWidth = measureWidth(single); } currentIndex = endIndex; ctx.fillText(single, item.x, item.y + i * style.lineHeight); } // 绘制剩余的 if(currentIndex < text.length) { let last = text.substring(currentIndex, text.length); ctx.fillText(last, item.x, item.y + lineNum * style.lineHeight); } [代码] 为了避免计算太多次,首先算出大致的行数,求出每行的文字数,然后移位索引下标,求出实际的每行的字数,再下移一行继续绘制,直到结束。 3.3.2 非换行的图文混排 [图片] 上图是一个包含表情图片和加粗文字的混排内容,当使用Wxml2Canvas查询元素时,会将第一行的内容分为五部分: 文本内容:这是段文字; 表情图片:发呆表情(非系统表情,image节点展现); 表情图片:发呆表情; 文本内容:这也; 加粗文本内容:是一段文字,这也是文字。 对于这种情况,执行查询computedStyle后,会返回相同的top值。我们把top值相同的元素聚合在一起,认为它们是同一行内容,事实也是如此。因为表情大小的差异以及其他影响,默认规定top值在±2的范围内都是同一行内容。然后将top值的聚合结果按照left的大小从左往右排列,再一一绘制,即可完美还原此种情况。 3.3.3 换行的图文混排 当混排内容出现了换行情况时,如下图所示: [图片] 此时的加粗内容占据了两行,当我们依旧根据top值归类时,却发现加粗文字的left值取的是第二行的left值。这就导致加粗文字和第一部分的文字的top值和left值相同,如果直接绘制,两部分会发生重叠。 为了避免这种尴尬的情况,我们可以利用加粗文字的height值与第一部分文字的height值比较,显然前者是后者的两倍,可以得知加粗部分出现了换行情况,直接将其放在同组top列表的最后位置。换行的部分根据lineHeight下移绘制,同时做记录。 最后一部分的文本内容也出现了换行情况,同样无法得到真正的起始left值,并且其top值与上一部分换行后的top值相同。此时应该将他的left值追加加粗换行部分的宽度,正好得到真正的left值,最后再绘制。 大多数的行内元素的展现形式都能以上述的逻辑完美还原。 六、总结 基于基础图形封装和wxml转换这两种绘制方式,可以满足绝大多数的场景,能够极大地减少工作量,而不需要再关注内部实现。在实际使用中,二者并非孤立存在,而更多的是一起使用。 [图片] 如上图所示,对于列表内容我们利用wxml读取绘制,对于下部的白色区域,不是wxml节点内容,我们可以使用基础图形绘制方式实现。二者的结合更加灵活高效。 目前Wxml2Canvas已经在公司内部开源,不久会放到github上,同时也在不断完善中,旨在实现更多的样式展现与提升稳定性和绘制速度。 如果有更好的建议与想法,请联系我。
2019-02-28 - 【优化】小程序优化-代码篇
本文主要是从代码方面跟大家分享我自己在开发小程序的一些做法,希望能帮到一些同学。 前言 不知道大家有没有这种体会,刚到公司时,领导要你维护之前别人写的代码,你看着别人写的代码陷入了深深的思考:“这谁写的代码,这么残忍” [图片] 俗话说“不怕自己写代码,就怕改别人的代码”,一言不和就改到你吐血,所以为了别人好,也为了自己好,代码规范,从我做起。 项目目录结构 在开发之前,首先要明确你要做什么,不要一上来就是干,咱们先把项目结构搭好。一般来说,开发工具初始化的项目基本可以满足需求,如果你的项目比较复杂又有一定的结构的话就要考虑分好目录结构了,我的做法如下图: [图片] component文件夹是放自定义组件的 pages放页面 public放公共资源如样式表和公共图标 units放各种公共api文件和封装的一些js文件 config.js是配置文件 这么分已经足以满足我的需求,你可以根据自己的项目灵活拆分。 配置文件 我的项目中有个config.js,这个文件是用来配置项目中要用到的一些接口和其它私有字段,我们知道在开发时通常会有测试环境和正式环境,而测试环境跟正式环境的域名可能会不一样,如果不做好配置的话直接写死接口那等到上线的时候一个个改会非常麻烦,所以做好配置是必需的,文件大致如下: [图片] 首先是定义域名,然后在config对象里定义接口名称,getAPI(key)是获取接口方法,最后通过module暴露出去就可以了.引用的时候只要在页面引入 import domain from ‘…/…/config’;,然后wx.request的时候url的获取方式是domain.getAPI(’’) 代码健壮性、容错性 例子 代码的健壮性、容错性也是我们应该要考虑的一点,移动端的项目不像pc端的网络那么稳定,很多时候网络一不稳定就决定我们的项目是否能正常运行,而一个好的项目就一定要有良好的容错性,就是说在网络异常或其它因素导致我们的项目不能运行时程序要有一个友好的反馈,下面是一个网络请求的例子: [图片] 相信多数人请求的方式是这样,包括我以前刚接触小程序的时候也是这样写,这样写不是说不好,而是不太严谨,如果能够正常获取数据那还好,但是一旦请求出现错误那程序可以到此就没法运行下去了,有些比较好的会加上faill失败回调,但也只是请求失败时的判断,在请求成功到获取数据的这段流程内其实是还有一些需要我们判断的,一般我的做法是这样: [图片] 在请求成功后小程序会进行如下判断: 判断是否返回200,是则进行一下步操作,否则抛出错误 判断数据结构是否完整,是则进行一下步操作,否则抛出错误 然后就可以在页面根据情况进行相应的操作了。 定制错误提示码 可以看到上面的截图的错误打印后面会带一个gde0或gde1的英文代码,这个代码是干嘛用的呢,其实是用来报障的,当我们的小程序上线后可能会遇到一些用户发来的报障,一般是通过截图发给我们,之前没有做错误提示码的时候可能只是根据一句错误提示来定位错误,但是很多时候误提示语都是一样的,我们根本不知道是哪里错了,这样一来就不能很快的定位的错误,所以加上这样一个提示码,到时用户一发截图来,我们只要根据这个错误码就能很快的定位错误并解决了,错误提示码建议命名如下: 不宜过长,3个字母左右 唯一性 意义明确 像上面gde表示获取草稿失败,后面加上数字表示是哪一步出错。 模块化 我们组内的大神说过, 模块化的意义在义分治,不在于复用。 之前我以为模块化只是为了可以复用,其实不然,无论模块多么小也是可以模块化,哪怕只是一个简单的样式也一样,并是不为了复用,而是管理起来方便。 很多同学经常将一些公共的样式事js放在app.wxss和app.js里以便调用,这样做其实有一个坏处,就是维护性比较差,如果是比较小的项目还好,项目一大问题就来了。而且项目是会迭代的,不可能总是一个人开发,可能后面会交接给其他人开发,所以会造成的问题就是: app.wxss和app.js里的内容只会越来越多,因为别人不确定哪些是没用的也不敢删,只能往里加东西,造成文件臃肿,不利于维护。 app.wxss和app.js对于每个页面都有效,可读性方面比较差。 所以模块化的意义就出来了,将公共的部分进行模块化统一管理,也便于维护。 样式模块化 公共样式根据上面的目录结构我是放在public里的css里,每个文件命名好说明是哪个部分的模块化,比如下面这个就表示一个按钮的模块化 [图片] 前面说过模块化不在于大小,就算只是一个简单的样式也可以进行模块化,只要在用到的地方import一下就行了,就知道哪里有用到,哪里没有用到,清晰明了。 js模块化 js模块化这里分为两个部分的模块化,一部分是公共js的模块化,另一部分是页面js的模块化即业务与数据的拆分。 公共js模块化 比较常用的公共js有微信登录,弹窗,请求等,一般我是放在units文件夹里,这里经微信弹窗api为例: [图片] 如图是在小程序中经常会用到的弹窗提示,这里进行封装,定义变量,只要在页面中引入就能直接调用了,不用每次都写一大串。比如在请求的时候是这样用的 [图片] toast()就是封装的弹窗api,这样看起来是不是清爽多了! 业务与数据模块化 业务与数据模块化就是指业务和数据分开,互不影响,业务只负责业务,数据只负责数据,可以看到页面会比普通的页面多了一个api.js [图片] 这个文件主要就是用来获取数据的,而index.js主要用来处理数据,这样分工明确,相比以往获取数据和处理数据都在一个页面要好很多,而且我这里获取数据是返回一个promise对象的,也方便处理一些异步操作。 组件化 组件化相信大家都不陌生了,自从小程序支持自定义组件,可以说是大大地提高了开发效率,我们可以将一些公共的部分进行组件化,这部分就不详细介绍,大家可以去看文档。组件化对于我们的项目来说有很大的好处,而且组件化的可移植性强,从一个项目复用到另一个项目基本不需要做什么改动。 总结 这篇文章通过我自己的一些经验来给大家介绍如何优化自己的代码,主要有以下几点 分好项目目录结构 做好接口配置文件 代码健壮性、容错性的处理 定制错误提示码方便定位错误 样式模块化和js模块化 组件化 最后放上项目目录结构的代码片段,大家可以研究一下,有问题一起探讨:https://developers.weixin.qq.com/s/1uVHRDmT7j6l
2019-03-07 - 获取用户信息昵称,emoji显示有问题
- 当前 Bug 的表现(可附上截图) [图片] 昵称获取有问题,第一个图标应该是 🐷 [图片] - 预期表现 - 复现路径 获取有emoji图标的用户昵称,部分会有问题 - 提供一个最简复现 Demo
2018-12-21 - 怎么实现图片和单选框的绑定
就是点击图片后相应单选框会自动打勾,[图片]
2019-07-25