- 社区每周 | 上两周社区问题反馈以及功能优化更新(07.08-07.19)
各位微信开发者: 以下是上两周我们在社区收到的问题反馈、需求的处理进度,希望同大家一同打造小程序生态。 「微信开放社区」小程序更新、搜索能力优化、支持扫码分享-小程序更新 社区小程序部分版块已支持完整的阅读、评论与回复功能,欢迎扫码体验。 [图片] -搜索能力优化 大幅度优化搜索结果匹配度,并支持对搜索结果的内容类型、排序方式、时间范围进行二次筛选。 [图片] -支持扫码分享 社区公告、文章、问答等内容支持在页面底部“复制链接”或在扫码打开小程序进行分享。 [图片] 上两周问题反馈和处理进度(07.08-07.19)修复中的问题video 全屏下结束播放时显示控制条的问题 查看详情 安卓 video 切换视频源后 removeChild 报错的问题 查看详情 iOS video 控件全屏时未适配 iPhoneX 系列机型的问题 查看详情 开发者工具 video 组件 catchtap 事件可捕获的问题 查看详情 video 组件拖动进度条后,等待状态一直显示 loading 的问题 查看详情 安卓下弹出手机号授权框后会触发 onHide 的问题 查看详情 在 tabbar 页面下拉刷新会显示上一个 tab 页的问题 查看详情 调用 navigateBack 返回时小程序闪退的问题 查看详情 iOS 下切换自定义 tabBar 会抖动的问题 查看详情 video 组件循环播放进度条不重置的问题 查看详情 map 组件 工具上 moveToLocation 没有回调的问题 查看详情 修复 vConsole 某些字符打印不出来的问题 查看详情 video 组件全屏时由横屏变为竖屏的问题 查看详情 苹果微信升级7.0.5后下载pdf文件无法打开的问题 查看详情 wx.downloadFile 相同代码在不同手机上测试下载的后缀不一样的问题 查看详情 wx.downloadFile 图片不显示的问题 查看详情 movable-view 拖动和点击的问题 查看详情 canvasGetImageData 真机调用失败的问题 查看详情 wx.previewImage 图片预览问题的问题 查看详情 video 视频组件安卓端拖动进度条和全屏的问题 查看详情 自定义 tabbar 的问题 查看详情 windows 快递打印软件打印格式有问题 查看详情 微信安卓 7.0.4 以及 7.0.5 版本,websocket 建立连接成功后报错的问题 查看详情 微信绑定小程序管理员数量的问题 查看详情 一个页面多个input输入框,第一个无光标的问题 查看详情 需求反馈迭代跟进中video 底部进度条允许隐藏的需求 查看详情 video 增加 controls 显示隐藏事件的需求 查看详情 分享图支持云文件 ID 的需求 查看详情 基础组件 UI 更新样式的需求 查看详情 map 组件普通区域点击获取经纬度的需求 查看详情 需求评估中小程序 UI 设计规范资源更新的需求 查看详情 小程序 web-view 链接 h5 页面,获取页面加载时间的需求 查看详情 获取屏幕亮度,设置屏幕亮度的需求 查看详情 支持上传pdf excel 等附件的需求 查看详情 建议云开发存储中的文件夹可以重命名 查看详情 小程序内引用微信内置数字、价格符号字体的需求 查看详情 微信团队 2019.07.24
2019-07-24 - 怎么提交材料
- 当前 Bug 的表现(可附上截图) 小程序后台改名失败。提示如下: 审核不通过:请提交与主体名称一致的个人手持身份证材料。 找不到任何提交材料的入口,到底怎么样才能完成小程序改名。
2019-07-20 - 蓝牙分包写入
前段时间收到一个蓝牙设备,需求是控制板子上面的电机,这对于从未接触过硬件的小白来说无疑是一个挑战,然而我是一个喜欢挑战的蓝人,于是开始了我的研究。 我开始各种搜罗Demo,查看文档及各大论坛。 终于,黄天不负苦心人,我成功的连接上了设备,并且获取到了他的服务及特征值,当然,每台外设可用的服务及特征值都是不一样的,而且有些是不可用的,什么 read、write、notify、indicate 要根据自己的操作需求去看哪个特征值支持。每个服务下面都有不同的特征值,每个特征值下面又分出来几个不同的特征值列表,接下来就是根据自己需求筛选了。 支持列表: read:读取低功耗蓝牙设备的特征值的二进制数据值 write:向低功耗蓝牙设备特征值中写入二进制数据 notify || indicate:启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值 这些都选好后该开始向蓝牙设备写入指令,让他动起来了(想想就有些小激动) 不过根据官方提供的方式转换指令并写入的时候,意外发生了! 我在回调里面打印成功与否的时候,显示成功,但是设备却没人任何反应,于是我又开始找原因(设备通过某个App试过,写入指令后是正常的),又开始了我的搜寻之旅。 之后发现原因是因为我使用的指令转换后超过了20字节,在Api文档中 https://developers.weixin.qq.com/miniprogram/dev/api/wx.writeBLECharacteristicValue.html 标注了建议每次写入不超过20字节,但是也不是强制性的呀,导致我很懵逼,明明返回的是成功。 [图片] 事到如今,只能接着寻找解决方法了,总不能跟他这么耗着呀。接着我的搜寻之旅吧!(废话那么多,重点该来了) 终于,找到种分包的方式,把超过20字节的指令分批发送(当然,没有超过20字节的话,也不影响使用),这边需要注意一下,不能直接连发,需要有一个延迟,然后~完美写入指令,我的小电机动起来了。 整体的流程如下: 打开蓝牙模块 => 搜索蓝牙 => 获取所有已发现的设备 => 连接蓝牙设备 => 获取蓝牙设备的所有服务 => 获取蓝牙设备服务下的所有特征值 => 向蓝牙设备写入指令 => 完成 =>关闭蓝牙模块 行了,不哔哔了。我结合官方提供的Demo修改了一下,添加了一个分包写入,已经打包成代码片段,可以直接使用。 代码片段中服务和特征值这两个地方我写成了固定的,根据自己的需求可以修改下。 [图片] [图片] 核心代码: 延时定时器 格式转换 判断并分包写入 [图片] [图片] 代码片段: https://developers.weixin.qq.com/s/oFJc70mI7o8K 如有不对的地方或者更好的解决方案,还望大佬们及时提出,希望对你们有所帮助。
2019-05-05 - 小程序图片上传,存储,获取,显示
我们在做小程序开发时,难免会遇到图片上传的功能,我们如果自己搭建图片服务器的话,成功太大了,并且还要写后台程序来接收上传的图片,还要有存储服务器。好在小程序云开发为我们提供了云存储的功能,这样我们就可以轻松的实现小程序图片的上传和存储。 老规矩,先看效果图 [图片] 本节知识点 1,小程序图片的选取 2,小程序图片的上传 3,小程序图片的存储 4,获取云端图片并显示 下面就来具体讲解下具体实现步骤 图片的选择和上传 index.wxml文件如下 [代码] <view class='item_root' bindtap='chuantupian'> <text>{{zhaopian}}</text> <view class='right_arrow' /> </view> [代码] index.js文件如下 [代码] //上传图片 chuantupian() { let that = this; let timestamp = (new Date()).valueOf(); wx.chooseImage({ success: chooseResult => { wx.showLoading({ title: '上传中。。。', }) // 将图片上传至云存储空间 wx.cloud.uploadFile({ // 指定上传到的云路径 cloudPath: timestamp + '.png', // 指定要上传的文件的小程序临时文件路径 filePath: chooseResult.tempFilePaths[0], // 成功回调 success: res => { console.log('上传成功', res) wx.hideLoading() wx.showToast({ title: '上传图片成功', }) if (res.fileID) { that.setData({ zhaopian: '图片如下', imgUrl: res.fileID }) } }, }) }, }) }, [代码] 到这里其实我们就可以实现图片的选取和上传功能了。 下面讲讲具体是如何实现的 首先我们通过wx.chooseImage来获取相册里的图片 再获取照片成功后,我们用当前时间戳命名图片,然后使用 wx.cloud.uploadFile方法来实现图片的上传 在上传成功后,会有如下回调。下图中的filenId就是我们在云存储中的路径,可以直接用这个路径来获取图片并显示的。 [图片] 到这里我们就轻松的实现了小程序图片上传的功能,是不是很简单。 有任何关于编程的问题都可以加我微信2501902696(备注编程开发) 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 视频讲解地址:https://edu.csdn.net/course/play/9604/281187 [图片]
2019-06-11 - 小程序WIFI接口和WebSocket创建局域网数据通讯
#小程序跟设备建立局域网通讯,进行数据传输; 1、让手机连接设备热点,需要用到微信 WI-FI 模块; 2、websocket支持局域网,需要用到 wx.websocket; 3、实践演变后,开放如下接口; 4、调试开启服务文件 ./utils/server.js (请自行使用node安装); [图片] 代码引入片段 : https://developers.weixin.qq.com/s/nZa78Zmu7k7R 代码引入片段更新: https://developers.weixin.qq.com/s/RiQ0POm47Xcx 更新接口规范,兼容,app.json 新增代码 “permission”: { “scope.userLocation”: { “desc”: “你的位置信息将用于小程序位置接口的效果展示” } } [图片] 引入代码调试,请查看 server.js 更新 npm install http
2019-11-12 - 商家真正理解小程序与公众号的区别,直营落地将指日可待!
对于很多商家来说,都没有真正理解小程序和公众号的本质区别。从而导致了直营迟迟不能落地。 [图片] 从官网到APP,到公众号,再到现在如火如荼的小程序;商家从来没有放弃过直营。过中坎坷,不堪回首。最终还是脱离不了中心化平台的魔爪!如:住宿行业的携程、外卖行业的美团、电商行业的淘宝等;商家一年到头所挣的大部分利润都被平台吸干。提起都是泪!还是说回小程序和公众号踏实点。 我们先来普及一下小程序和公众号表面区别: 一、功能 小程序功能核心是服务,公众号功能核心是内容。 二、体验 由于公众号附加功能是基于第三方H5开发,而小程序是基于微信原生开发。所以在速度和安全上,小程序比公众号都有先天优势。有个类比非常恰当,公众号就像网页,小程序就像原生APP。 理解了小程序和公众号的技术本源后。我们就比较好理解为什么小程序体验这么好了。 为什么小程序打开快?因为微信都帮小程序加载好了。 为什么不用安装?因为微信已经安装了。 为什么不用注册?因为微信账号已经注册了。 为什么小程序安全?因为都在微信体系监管之下。 三、流量红利 1、小程序流量入口达50多个。 如:小程序特有的快捷下拉、发现页、小程序搜索、服务通知、小程序群标签、文章内嵌等。 [图片] [图片] 小程序50多个入口,7个主要入口、7个小程序自身入口、6个搜索入口、7个公众号入口、10个二维码入口、9个微信场景入口、其他8个入口。 2、2017-2018流量入口排行表。 我们再看看阿拉丁总结的2017-2018年的流量入口排行,就非常清晰怎么布局、获取本来属于你的流量红利。 [图片] [图片] 上图5个可能第2个APP分享有点陌生,下图普及一下: [图片] 这个APP分享可以说打通了很多APP流通的桥梁。这个入口也算是小程序范畴的一匹黑马。 3、小程序搜索权重高。 在微信搜一搜小程序有较高权重。也就是被搜索到的几率非常大,尤其用过的小程序。一般都排在前三!想找不到都难。 [图片] 四、O2O O2O除了大家熟悉的搜索附近小程序。小程序码即扫即用,打通线上线下,创造多元化收益。 [图片] 五、裂变 裂变是近2年火起来的热词,通俗一点解析就是老带新。如:你的微信有几百好友,你就能影响几百人。而这几百人他们也有几百人。这样循环下去,数量是非常庞大的。 小程序的列表工具大家应该都不陌生:砍价、优惠券转赠、分销、精品店等。不管在微信群、公众号、文章页、对话框等。你能想到的任何地方,都可以看到这些裂变信息。之所以有这样的铺天盖地的现象,就是因为小程序的裂变工具效果好。公众号的裂变就大打折扣了。 [图片] 上面五点好像总结得挺全面的,但这都是表面想象。 而真正理解小程序和公众号的本质区别,那离直营落地不远了。 说得简单直白一点,小程序就是用来解决成交的,公众号不是! 虽然小程序很多功能公众号也有。但各环节就差那么一点,公众号解决不了 成交的问题。因为成交是一个流程,流程中不允许半点瑕疵。 成交流程每一步必须做到畅快淋漓,直到付完全款。不然成交率谁也无法保证效果。 用个形象的比喻,成交必须像坐上滑梯,一溜到底。 我们回想一下,小程序需要安装吗?小程序需要关注吗?小程序需要填表注册吗? 小程序需要绑定银行卡再付款吗? 这些成交过程中的场景每一环节都有大量客户流失。现在流量如此昂贵,走到最后付款能达到百分之几已经算非常不错的成交率。而小程序的先天基因良好!它就像是为了优化成交而存在的。 [图片] 都明白了吗?小程序是用来成交,不是用来搞流量的。 理解了本质,其它都迎刃而解。 [图片] 当商家把成交流程打通后,需要流量变现大有人在。 那时还怕平台要挟?化被动为主动,商家挑选流量主将成为主流。 成功的商家直营将遍地开花,我们拭目以待!
2019-05-15 - 试了一下小程序的websocket,提供个简单的代码吧
[代码]var[代码] [代码]util = require([代码][代码]'../../../utils/util.js'[代码][代码]);[代码] [代码]var[代码] [代码]socketOpen = [代码][代码]false[代码][代码];[代码] [代码]var[代码] [代码]frameBuffer_Data, session, SocketTask;[代码] [代码]var[代码] [代码]url = [代码][代码]'wss://wss......./Chat'[代码][代码];[代码] [代码]Page({[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 页面的初始数据[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]data: {[代码] [代码] [代码][代码]toView: [代码][代码]'green'[代码][代码],[代码] [代码] [代码][代码]windowH: [代码][代码]"1000"[代码][代码],[代码] [代码] [代码][代码]user_input_text: [代码][代码]''[代码][代码], [代码][代码]//用户输入文字[代码] [代码] [代码][代码]inputValue: [代码][代码]''[代码][代码],[代码] [代码] [代码][代码]returnValue: [代码][代码]''[代码][代码],[代码] [代码] [代码][代码]addImg: [代码][代码]false[代码][代码],[代码] [代码] [代码][代码]//格式示例数据,可为空[代码] [代码] [代码][代码]allContentList: [],[代码] [代码] [代码][代码]num: 0,[代码] [代码] [代码][代码]wo: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]ta: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]youImg: [代码][代码]""[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]scrollToTop() {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]this[代码][代码].setAction({[代码] [代码] [代码][代码]scrollTop: that.windowH[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]bindKeyInput: [代码][代码]function[代码][代码](e) {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]inputValue: e.detail.value[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]// 提交文字[代码] [代码] [代码][代码]submitTo: [代码][代码]function[代码][代码](e) {[代码] [代码] [代码][代码]let that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]content = that.data.inputValue;[代码] [代码] [代码][代码]var[代码] [代码]data = {[代码] [代码] [代码][代码]// body: that.data.inputValue,[代码] [代码] [代码][代码]"name"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"content"[代码][代码]: content,[代码] [代码] [代码][代码]"type"[代码][代码]: 5,[代码] [代码] [代码][代码]"toName"[代码][代码]: that.data.ta[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//静态显示[代码] [代码] [代码][代码]this[代码][代码].data.allContentList.push({[代码] [代码] [代码][代码]"id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"hx_id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"wo"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"ta"[代码][代码]: that.data.ta,[代码] [代码] [代码][代码]"content"[代码][代码]: content,[代码] [代码] [代码][代码]"voice_url"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"fileurl"[代码][代码]: [代码][代码]null[代码][代码],[代码] [代码] [代码][代码]"create_date"[代码][代码]: util.formatTime[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]allContentList: [代码][代码]this[代码][代码].data.allContentList,[代码] [代码] [代码][代码]inputValue: [代码][代码]''[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]//至底[代码] [代码] [代码][代码]that.bottom();[代码] [代码] [代码][代码]//socket[代码] [代码] [代码][代码]if[代码] [代码](socketOpen) {[代码] [代码] [代码][代码]// 如果打开了socket就发送数据给服务器[代码] [代码] [代码][代码]that.sendSocketMessage(data);[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]//通过 WebSocket 连接发送数据,需要先 wx.connectSocket,并在 wx.onSocketOpen 回调之后才能发送。[代码] [代码] [代码][代码]sendSocketMessage: [代码][代码]function[代码][代码](msg) {[代码] [代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]console.log([代码][代码]'通过 WebSocket 连接发送数据'[代码][代码], JSON.stringify(msg))[代码] [代码] [代码][代码]// debugger[代码] [代码] [代码][代码]SocketTask.send([代码] [代码] [代码][代码]{[代码] [代码] [代码][代码]data: JSON.stringify(msg)[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]function[代码] [代码](res){[代码] [代码] [代码][代码]console.log([代码][代码]'已发送'[代码][代码], res)[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码])[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]addImg: [代码][代码]function[代码][代码](e) {[代码] [代码] [代码][代码]var[代码] [代码]a = [代码][代码]this[代码][代码].data.addImg;[代码] [代码] [代码][代码]if[代码] [代码](a == [代码][代码]true[代码][代码]) {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]addImg: [代码][代码]false[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]addImg: [代码][代码]true[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]tap() {[代码] [代码] [代码][代码]for[代码] [代码](let i = 0; i < order.length; ++i) {[代码] [代码] [代码][代码]if[代码] [代码](order[i] === [代码][代码]this[代码][代码].data.toView) {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]toView: order[i + 1],[代码] [代码] [代码][代码]scrollTop: (i + 1) * 200[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]break[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]tapMove() {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]scrollTop: [代码][代码]this[代码][代码].data.scrollTop + 10[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面加载[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onLoad: [代码][代码]function[代码][代码](options) {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]that.setData({[代码] [代码] [代码][代码]ta: options.tel,[代码] [代码] [代码][代码]youImg: options.youImg,[代码] [代码] [代码][代码]wo: util.phone[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]// 获取设备高度[代码] [代码] [代码][代码]wx.getSystemInfo({[代码] [代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码] [代码] [代码][代码]var[代码] [代码]clientHeight = res.windowHeight,[代码] [代码] [代码][代码]clientWidth = res.windowWidth,[代码] [代码] [代码][代码]rpxR = 750 / clientWidth;[代码] [代码] [代码][代码]var[代码] [代码]helfH = clientHeight * 1 * rpxR;[代码] [代码] [代码][代码]//var textH = helfH - 100;[代码] [代码] [代码][代码]that.setData({[代码] [代码] [代码][代码]windowH: helfH[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]//加载历史数据[代码] [代码] [代码][代码]// util.apiHtml +http://localhost:9193[代码] [代码] [代码] [代码] [代码][代码]//修改已读[代码] [代码] [代码] [代码] [代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面初次渲染完成[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onReady: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码] [代码] [代码] [代码] [代码][代码]SocketTask.onOpen(res => {[代码] [代码] [代码][代码]socketOpen = [代码][代码]true[代码][代码];[代码] [代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 连接打开事件。'[代码][代码], res)[代码] [代码] [代码][代码]//发送登陆信息[代码] [代码] [代码][代码]var[代码] [代码]data = {[代码] [代码] [代码][代码]// body: that.data.inputValue,[代码] [代码] [代码][代码]"Name"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"content"[代码][代码]: [代码][代码]"login"[代码][代码],[代码] [代码] [代码][代码]"type"[代码][代码]: 4[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]that.sendSocketMessage(data);[代码] [代码] [代码][代码]//循环发送心跳[代码] [代码] [代码][代码]setInterval([代码] [代码] [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]var[代码] [代码]ping = { [代码][代码]"type"[代码][代码]: [代码][代码]"ping"[代码] [代码]};[代码] [代码] [代码][代码]that.sendSocketMessage(ping);[代码] [代码] [代码][代码]}, 20000[代码] [代码] [代码][代码]); [代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]SocketTask.onClose(onClose => {[代码] [代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 连接关闭事件。'[代码][代码], onClose)[代码] [代码] [代码][代码]socketOpen = [代码][代码]false[代码][代码];[代码] [代码] [代码][代码]this[代码][代码].webSocket()[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]SocketTask.onError(onError => {[代码] [代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 错误。错误信息'[代码][代码], onError)[代码] [代码] [代码][代码]socketOpen = [代码][代码]false[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]SocketTask.onMessage(onMessage => {[代码] [代码] [代码][代码]console.log([代码][代码]"onMessage:::::"[代码][代码]+onMessage.data);[代码] [代码] [代码][代码]if[代码] [代码](onMessage.data.indexOf([代码][代码]"上线"[代码][代码]) != -1 || onMessage.data.indexOf([代码][代码]"下线"[代码][代码]) != -1) {[代码] [代码] [代码][代码]return[代码][代码];[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]console.log([代码][代码]'监听WebSocket接受到服务器的消息事件。服务器返回的消息'[代码][代码], JSON.parse(onMessage.data))[代码] [代码] [代码][代码]var[代码] [代码]onMessage_data = JSON.parse(onMessage.data)[代码] [代码] [代码][代码]if[代码] [代码](onMessage_data.toName == that.data.wo && onMessage_data.name == that.data.ta) {[代码] [代码] [代码][代码]// addmsglist1(msg1.name, msg1.content)[代码] [代码] [代码][代码]that.data.allContentList.push({[代码] [代码] [代码][代码]"id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"hx_id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"wo"[代码][代码]: that.data.ta,[代码] [代码] [代码][代码]"ta"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"content"[代码][代码]: onMessage_data.content,[代码] [代码] [代码][代码]"voice_url"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"fileurl"[代码][代码]: [代码][代码]null[代码][代码],[代码] [代码] [代码][代码]"create_date"[代码][代码]: util.formatTime[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]that.setData({[代码] [代码] [代码][代码]allContentList: that.data.allContentList[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]that.bottom();[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]// 获取hei的id节点然后屏幕焦点调转到这个节点 [代码] [代码] [代码][代码]bottom: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]scrollTop: 1000000[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面显示[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onShow: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]if[代码] [代码](!socketOpen) {[代码] [代码] [代码][代码]this[代码][代码].webSocket()[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]webSocket: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]// 创建Socket[代码] [代码] [代码][代码]SocketTask = wx.connectSocket({[代码] [代码] [代码][代码]url: url,[代码] [代码] [代码][代码]data: [代码][代码]'data'[代码][代码],[代码] [代码] [代码][代码]header: {[代码] [代码] [代码][代码]'content-type'[代码][代码]: [代码][代码]'application/json'[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]method: [代码][代码]'post'[代码][代码],[代码] [代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码] [代码] [代码][代码]console.log([代码][代码]'WebSocket连接创建'[代码][代码], res)[代码] [代码] [代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]fail: [代码][代码]function[代码][代码](err) {[代码] [代码] [代码][代码]wx.showToast({[代码] [代码] [代码][代码]title: [代码][代码]'网络异常!'[代码][代码],[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]console.log(err)[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]})[代码] [代码] [代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面隐藏[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onHide: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]SocketTask.close([代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]console.log(res);[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面卸载[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onUnload: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]SocketTask.close([代码][代码]function[代码][代码](res){[代码] [代码] [代码][代码]console.log(res);[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 页面相关事件处理函数--监听用户下拉动作[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onPullDownRefresh: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 页面上拉触底事件的处理函数[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onReachBottom: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 用户点击右上角分享[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onShareAppMessage: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]}[代码] [代码]})[代码]
2019-05-16 - 【高校开发者】双生日记开发经验分享
双生日记开发经验分享 Hello,我是双生日记的 Founder & Developer Airing。该项目的小程序端获得了 2018 C4——微信小程序应用开发赛的一等奖,而 iOS 端则获得了 2018 C4——移动应用创新赛的一等奖,目前累计注册用户已达 1 万+,并仍在不断开发维护中~ 本文将简要概括我们团队在产品的整个研发流程中的所做的工作,更侧重于介绍产品研发与团队管理的方法。 我将整个产品的研发分为以下四步: 立项 设计 开发 维护 [图片] 可以看到,以上四步形成了项目流程的闭环,使得产品能够良性发展。接下来我来具体谈谈这四步工作的内容。 1. 立项 项目立项是所有环节最开始的部分,我觉得也是最重要的部分,它的工作内容类似于“产品经理”的职责。虽然我是 Founder,但产品的探讨还是与大家共同完成的。具体而言,这个环节有两个内容: 产品脑暴 文档整理 1.1 产品脑暴 首先,我会先在团队中提出我的想法,并创建一个讨论区供大家讨论。我们是一个非常大的兴趣团队,虽然参与双生研发的只有寥寥三人,但是在产品脑暴的时候,团队的成员都提出了各自的见解与建议。例如,下图是我们在团队研发中讨论的内容。 [图片] 这里我们团队用的是产品是“语雀”,当然工具是随意的,用腾讯文档我觉得也非常方便,重要的是一定要形成电子版记录材料,如果只是在微信群里讨论或者线下简单聊聊,那讨论了、忘记了,那就相当低效,约等于没有讨论。 1.2 文档整理 第二步,整理脑暴的文档并撰写相关的研发文档,具体来说,包括但不限于: 需求文档 产品文档 模型文档 接口文档 [图片] PS. 这是我们团队的文档库,仅供参考:零熊 | 语雀 [图片] 2. 设计 设计工具我们用的 Sketch,但是不会把源文件直接发给研发同学,因为正版 Sketch 挺贵的,而且只支持 mac 系统。这里我们使用的工具是蓝湖,开始用的也是语雀的画板,但是发现实在是太难用了…另外,在蓝湖中的设计稿是可以分享的,并且邀请团队里的同学进行点评。 设计稿的内容具体包括: 规范 原型 UI 切图 规范重点是色彩规范、组件规范、和字体规范。原型更多的是交互说明,这里我们只是用批注的方式在 ui 上详细说明了一下交互,但如果直接用 flinto 去做也是可以的。flinto 的好处是更加直观,但是开发人员不一定能 get 到设计同学的全部内容。 [图片] 3. 开发 本部分分享的是产品研发的核心环节:项目开发。本环节我分享的内容会稍微多一些,但也略微零散,主要包含三个内容: 规划记录 开发工具 建议事项 3.1 规划记录 在开发之前,我习惯于自己先列一个 todolist 去罗列出项目中的各个需求点或技术点,从整体上会有一个直观的感受,也方便我去安排和规划自己的开发任务。这里我使用的工具是 Notion,我先按照重要的模块把产品分割成了 8 个部分,然后再在每个部分里写各自的 todolist,以免单文件 todolist 过长。 [图片] [图片] 当然,todolist 不单单记录待办事项而已,它更多的是承担一个开发日记的作用。我个人倾向于把开发中遇到的难点问题及解决方法,或者用到的资源顺手记录下来。我认为开发是一个学习和成长的过程,而不仅仅只是完成业务需求。 随手记录是方便日后整理为博客或者再遇到类似的问题可以快速定位,若不记录则很容易忘记。因此,做开发日记对学习的成效是非常大的。 [图片] 3.2 开发工具 针对微信小程序开发,我建议对开发很熟悉的同学可以尝试去使用 VS Code + 扩展 + 真机的模式进行开发,个人觉得这套流程既高效又不会出错。“高效”是 VS Code 自身的高效,而“出错”指的则是模拟器有时候效果与真机不同。 这里顺便安利一下我自己的 VS Code 配置: [图片] 我喜欢把资源管理器放在右边,有两个原因:一是左边是人的注意区,故应该放代码编辑器;二是我随时按 Cmd + B 可以隐藏资源管理器而同时不改变编辑器的位置,如果放左边,隐藏的时候编辑器会有一个位移,眼睛会很不舒服。 对于扩展,我这里用了几个比较有意思的: Color Highlight:颜色值高亮可视化为颜色本身,方便前端样式开发 TODO Highlight:高亮 TODO 与 FIXME miniapp:小程序标签与属性自动补全 Bracket Pair Colorizer:括号着色配对,这个特别方便。 Image preview:方便在代码里预览 uri 上的图片,我是用来看看自己资源路径有没有引错。 REST Client:HTTP 测试,方便开发、分享与 mock。 主题我用的是 Winter is Coming Theme + Material Icon Theme,我个人觉得黑色默认也非常好看。 3.3 注意事项 如果是协同开发我推荐搭配 Git History + Eslint 插件,当然如果自己开发,也免不了 Eslint。Git Commit 规范我们用的是这套: Commit 提交规范 [图片] 开发的时候也别忘记埋点,做一些打点统计,需要打点的地方根据项目需要检测的内容来定。如 PV、UV 这些小程序自带帮你统计了你可以不用打,但其他项目还是要统计的,或者直接规划好 Nginx 的日志,再对日志做分析也是 ok 的~ 如果前后端分离开发,前端同学可以自己接 mockjs 做一套符合接口文档规范的 mock 接口。 4. 维护 对于用户的反馈,我们智能筛选后自动提交到 github issue,再针对 issue 进行 label 和优先级分配。这是我们项目开源的主地址:oh-bear/2life。 [图片] 可以看到 issue 是比较杂乱的,所以还需要 github 的 project 去做一个任务画板。 [图片] by the way,安利一下小工具Devhub,可以很方便的检测自己负责项目的 issue。 [图片] 针对这些 issue,可以做一个阶段性的文档,回归到“立项”步骤,进行下一个小版本的开发。 [图片] 可以发现,我始终没有去选择使用甘特图软件,虽然甘特图更加直观,但是我不太喜欢把任务排的满满的、紧紧的,这样会不自觉地产生工作压力。最重要的是,我们毕竟不是工作嘛,只是一个兴趣开发,所以还是遵循自己的喜好来便好~ 好了,这次的分享就到这里。我是 Airing,我的个人博客是:https://me.ursb.me,欢迎大家来访交流~
2019-05-14 - 小程序审核:名称规则调整及优化建议
微信小程序的开发者们在发布小程序之前,需要给每个小程序起好一个名称,设置好logo、简介及类目信息,然后才能进行代码提审。 这些账号的基础信息是用户对一个小程序的初步认知,平台希望这些信息能够给予用户准确预期每个小程序的功能和内容,也希望每个小程序的名称是具有独特性的,减少误导、混淆用户理解的可能性。 因此有以下几点名称规则调整及优化建议,希望开发者可以了解并遵循,后续可能会在名称审核、代码审核或已发布运营时,收到平台的审核通知或建议修改通知。 l 微信小程序的名称建议是品牌、商标或具有辨识度的短词 在微信小程序发布后,用户会通过搜索、传播等场景使用小程序的功能和服务。选择品牌、商标或具有辨识度的短词作为小程序的名称,可以帮助用户快速建立与小程序的记忆与关系。 名称为“印象笔记”会比“笔记”能更好的让用户记忆和理解,并使用“印象”品牌词进行搜索或传播。 [图片] l 不能直接使用功能性描述、广义归纳类的词汇作为名称 功能性描述的词汇虽然可传达出小程序所要提供的相关功能,但直接使用此类词汇,缺乏显著性且不具有识别、区分小程序的作用。 如名称为“牙科”的账号,用户能直接理解小程序的内容可能与“牙科”相关,但对这个小程序账号是没有建立认知和关系的。 [图片] l 不能同时使用地域性词汇与广义归纳类词汇进行命名 使用“地域+广义归纳类”词汇进行命名,同样存在指向性过于宽泛的问题。 如以下案例, 名称包含的地域词:“四川”、归纳类词:“系统门窗”,并不会让用户对账号的功能产生有效的预期。以下示例是不允许的: [图片] 但在政务民生类服务场景,“广州地铁”、“广州公积金”可以被用户理解并接受属于政府或相关机构提供的服务和资讯,因此“地域+广义归纳类”词汇可被政府民生场景使用。 l 不建议使用具有营销属性的词汇进行命名 营销属性词汇如:免费、促销、清仓、打折等,鉴于活动规则普遍附有限制条件,在名称中使用,通常带有过于夸大的表达效果,并非是小程序的核心功能和用户的全部认知,存在误导的可能性。以下示例是不允许的: [图片] l 商品销售类小程序,需要包含品牌名称,不直接使用产品分类进行命名 商品销售类小程序,若仅使用产品分类进行命名,无法建立易于记忆、易于传播的品牌形象。如名称为“女鞋男鞋女包批发生产”,用户并不会因“女鞋”“男鞋”“女包”就会进行选择,反而“品牌+女装”才是用户真正需要的。以下示例是不允许的。 [图片] l 不在名称中堆砌热门搜索关键词 名称中堆砌多个关键词,过于冗长的热门搜索关键词堆砌名称无法建立品牌记忆,目的普遍是在搜索结果上获得更多的曝光。以下示例是不允许的: [图片] 结语 此次名称规则的调整和优化建议,平台希望开发者们能相应地调整自己的小程序名称。如未满足名称规范,平台会在后续的小程序名称设置、名称审核、代码审核流程进行规则提示和修改要求。并会在搜索场景优化需求基础上,对无辨识度的账号名称进行搜索策略调整。 微信小程序完整的名称规则可具体参照《微信小程序平台运营规范》2. 基本信息规范,除以上规则与示例之外,当然还包括了不得使用违法违规的名称、不得使用侵权他人的品牌词进行命名、在命中敏感类目如医疗词时提交主体资质等规则。 平台希望小程序名称的优化调整,可以使得每个被开发者精心创造出来的小程序都能让用户在使用时、搜索时、传播时逐渐接受并建立对产品品牌的认知。
2023-08-17 - 拥抱更底层技术——从CSS变量到Houdini
0. 前言 平时写CSS,感觉有很多多余的代码或者不好实现的方法,于是有了预处理器的解决方案,主旨是write less &do more。其实原生css中,用上css变量也不差,加上bem命名规则只要嵌套不深也能和less、sass的嵌套媲美。在一些动画或者炫酷的特效中,不用js的话可能是用了css动画、svg的animation、过渡,复杂动画实现用了js的话可能用了canvas、直接修改style属性。用js的,然后有没有想过一个问题:“要是canvas那套放在dom上就爽了”。因为复杂的动画频繁操作了dom,违背了倒背如流的“性能优化之一:尽量少操作dom”的规矩,嘴上说着不要,手倒是很诚实地[代码]ele.style.prop = <newProp>[代码],可是要实现效果这又是无可奈何或者大大减小工作量的方法。 我们都知道,浏览器渲染的流程:解析html和css(parse),样式计算(style calculate),布局(layout),绘制(paint),合并(composite),修改了样式,改的环节越深代价越大。js改变样式,首先是操作dom,整个渲染流程马上重新走,可能走到样式计算到合并环节之间,代价大,性能差。然后痛点就来了,浏览器有没有能直接操作前面这些环节的方法呢而不是依靠js?有没有方法不用js操作dom改变style或者切换class来改变样式呢? 于是就有CSS Houdini了,它是W3C和那几个顶级公司的工程师组成的小组,让开发者可以通过新api操作CSS引擎,带来更多的自由度,让整个渲染流程都可以被开发者控制。上面的问题,不用js就可以实现曾经需要js的效果,而且只在渲染过程中,就已经按照开发者的代码渲染出结果,而不是渲染完成了再重新用js强行走一遍流程。 关于houdini最近动态可点击这里 上次CSS大会知道了有Houdini的存在,那时候只有cssom,layout和paint api。前几天突然发现,Animation api也有了,不得不说,以后很可能是Houdini遍地开花的时代,现在得进一步了解一下了。一句话:这是css in js到js in css的转变 1. CSS变量 如果你用less、sass只为了人家有变量和嵌套,那用原生css也是差不多的,因为原生css也有变量: 比如定义一个全局变量–color(css变量双横线开头) [代码]:root { --color: #f00; } [代码] 使用的时候只要var一下 [代码].f{ color: var(--color); } [代码] 我们的html: [代码]<div class="f">123</div> [代码] 于是,红色的123就出来了。 css变量还和js变量一样,有作用域的: [代码]:root { --color: #f00; } .f { --color: #aaa } .g{ color: var(--color); } .ft { color: var(--color); } [代码] html: [代码] <div className="f"> <div className="ft">123</div> </div> <div className=""> <div className="g">123</div> </div> [代码] 于是,是什么效果你应该也很容易就猜出来了: [图片] css能搞变量的话,我们就可以做到修改一处牵动多处的变动。比如我们做一个像准星一样的四个方向用准线锁定鼠标位置的效果: [图片] 用css变量的话,比传统一个个元素设置style优雅多了: [代码]<div id="shadow"> <div class="x"></div> <div class="y"></div> <div class="x_"></div> <div class="y_"></div> </div> [代码] [代码] :root{ --x: 0px; --y: 0px; } body{ margin: 0 } #shadow{ width: 50%; height: 600px; border: #000 1px solid; position: relative; margin: 0; } .x, .y, .x_, .y_ { position: absolute; border: #f00 2px solid; } .x { top: 0; left: var(--x); height: 20px; width: 0; } .y { top: var(--y); left: 0; height: 0; width: 20px; } .x_ { top: 600px; left: var(--x); height: 20px; width: 0; } .y_ { top: var(--y); left: 100%; height: 0; width: 20px; } [代码] [代码]const style = document.documentElement.style shadow.addEventListener('mousemove', e => { style.setProperty(`--x`, e.clientX + 'px') style.setProperty(`--y`, e.clientY + 'px') }) [代码] 那么,对于github的404页面这种内容和鼠标位置有关的页面,思路是不是一下子就出来了 2. CSS type OM 都有DOM了,那CSSOM也理所当然存在。我们平时改变css的时候,通常是直接修改style或者切换类,实际上就是操作DOM来间接操作CSSOM,而type om是一种把css的属性和值存在attributeStyleMap对象中,我们只要直接操作这个对象就可以做到之前的js改变css的操作。另外一个很重要的点,attributeStyleMap存的是css的数值而不是字符串,而且支持各种算数以及单位换算,比起操作字符串,性能明显更优。 接下来,基本脱离不了window下的CSS这个属性。在使用的时候,首先,我们可以采取渐进式的做法: [代码]if('CSS' in window){...}[代码] 2.1 单位 [代码]CSS.px(1); // 1px 返回的结果是:CSSUnitValue {value: 1, unit: "px"} CSS.number(0); // 0 比如top:0,也经常用到 CSS.rem(2); //2rem new CSSUnitValue(2, 'percent'); // 还可以用构造函数,这里的结果就是2% // 其他单位同理 [代码] 2.2 数学运算 自己在控制台输入CSSMath,可以看见的提示,就是数学运算 [代码]new CSSMathSum(CSS.rem(10), CSS.px(-1)) // calc(10rem - 1px),要用new不然报错 new CSSMathMax(CSS.px(1),CSS.px(2)) // 顾名思义,就是较大值,单位不同也可以进行比较 [代码] 2.3 怎么用 既然是新的东西,那就有它的使用规则。 获取值[代码]element.attributeStyleMap.get(attributeName)[代码],返回一个CSSUnitValue对象 设置值[代码]element.attributeStyleMap.set(attributeName, newValue)[代码],设置值,传入的值可以是css值字符串或者是CSSUnitValue对象 当然,第一次get是返回null的,因为你都没有set过。“那我还是要用一下getComputedStyle再set咯,这还不是和之前的差不多吗?” 实际上,有一个类似的方法:[代码]element.computedStyleMap[代码],返回的是CSSUnitValue对象,这就ok了。我们拿前面的第一部分CSS变量的代码测试一波 [代码]document.querySelector('.x').computedStyleMap().get('height') // CSSUnitValue {value: 20, unit: "px"} document.querySelector('.x').computedStyleMap().set('height', CSS.px(0)) // 不见了 [代码] 3. paint API paint、animation、layout API都是以worker的形式工作,具体有几个步骤: 建立一个worker.js,比如我们想用paint API,先在这个js文件注册这个模块registerPaint(‘mypaint’, class),这个class是一个类下面具体讲到 在html引入CSS.paintWorklet.addModule(‘worker.js’) 在css中使用,background: paint(mypaint) 主要的逻辑,全都写在传入registerPaint的class里面。paint API很像canvas那套,实际上可以当作自己画一个img。既然是img,那么在所有的能用到图片url的地方都适合用paint API,比如我们来自己画一个有点炫酷的背景(满屏随机颜色方块)。有空的话可以想一下js怎么做,再对比一下paint API的方案。 [图片] [代码]// worker.js class RandomColorPainter { // 可以获取的css属性,先写在这里 // 我这里定义宽高和间隔,从css获取 static get inputProperties() { return ['--w', '--h', '--spacing']; } /** * 绘制函数paint,最主要部分 * @param {PaintRenderingContext2D} ctx 类似canvas的ctx * @param {PaintSize} PaintSize 绘制范围大小(px) { width, height } * @param {StylePropertyMapReadOnly} props 前面inputProperties列举的属性,用get获取 */ paint(ctx, PaintSize, props) { const w = (props.get('--w') && +props.get('--w')[0].trim()) || 30; const h = (props.get('--h') && +props.get('--h')[0].trim()) || 30; const spacing = +props.get('--spacing')[0].trim() || 10; for (let x = 0; x < PaintSize.width / w; x++) { for (let y = 0; y < PaintSize.height / h; y++) { ctx.fillStyle = `#${Math.random().toString(16).slice(2, 8)}` ctx.beginPath(); ctx.rect(x * (w + spacing), y * (h + spacing), w, h); ctx.fill(); } } } } registerPaint('randomcolor', RandomColorPainter); [代码] 接着我们需要引入该worker: [代码]CSS && CSS.paintWorklet.addModule('worker.js');[代码] 最后我们在一个class为paint的div应用样式: [代码].paint{ background-image: paint(randomcolor); width: 100%; height: 600px; color: #000; --w: 50; --h: 50; --spacing: 10; } [代码] 再想想用js+div,是不是要先动态生成n个,然后各种计算各种操作dom,想想就可怕。如果是canvas,这可是canvas背景,你又要在上面放一个div,而且还要定位一波。 注意: worker是没有window的,所以想搞动画的就不能内部消化了。不过可以靠外面的css变量,我们用js操作css变量可以解决,也比传统的方法优雅 4. 自定义属性 支持情况 点击这里查看 首先,看一下支持度,目前浏览器并没有完全稳定使用,所以需要跟着它的提示:[代码]Experimental Web Platform features” on chrome://flags[代码],在chrome地址栏输入[代码]chrome://flags[代码]再找到[代码]Experimental Web Platform features[代码]并开启。 [代码]CSS.registerProperty({ name: '--myprop', //属性名字 syntax: '<length>', // 什么类型的单位,这里是长度 initialValue: '1px', // 默认值 inherits: true // 会不会继承,true为继承父元素 }); [代码] 说到继承,我们回到前面的css变量,已经说了变量是区分作用域的,其实父作用域定义变量,子元素使用该变量实际上是继承的作用。如果[代码]inherits: true[代码]那就是我们看见的作用域的效果,如果不是true则不会被父作用域影响,而且取默认值。 这个自定义属性,精辟在于,可以用永久循环的animation驱动一次性的transform。换句话说,我们如果用了css变量+transform,可以靠js改变这个变量达到花俏的效果。但是,现在不需要js,只要css内部消化,transform成为永动机。 [代码]// 我们先注册几种属性 ['x1','y1','z1','x2','y2','z2'].forEach(p => { CSS.registerProperty({ name: `--${p}`, syntax: '<angle>', inherits: false, initialValue: '0deg' }); }); [代码] 然后写个样式 [代码]#myprop, #myprop1 { width: 200px; border: 2px dashed #000; border-bottom: 10px solid #000; animation:myprop 3000ms alternate infinite ease-in-out; transform: rotateX(var(--x2)) rotateY(var(--y2)) rotateZ(var(--z2)) } [代码] 再来看看我们的动画,为了眼花缭乱,加了第二个改了一点数据的动画 [代码]@keyframes myprop { 25% { --x1: 20deg; --y1: 30deg; --z1: 40deg; } 50% { --x1: -20deg; --z1: -40deg; --y1: -30deg; } 75% { --x2: -200deg; --y2: 130deg; --z2: -350deg; } 100% { --x1: -200deg; --y1: 130deg; --z1: -350deg; } } @keyframes myprop1 { 25% { --x1: 20deg; --y1: 30deg; --z1: 40deg; } 50% { --x2: -200deg; --y2: 130deg; --z2: -350deg; } 75% { --x1: -20deg; --z1: -40deg; --y1: -30deg; } 100% { --x1: -200deg; --y1: 130deg; --z1: -350deg; } } [代码] html就两个div: [代码] <div id="myprop"></div> <div id="myprop1"></div> [代码] 效果是什么呢,自己可以跑一遍看看,反正功能很强大,但是想象力限制了发挥。 自己动手改的时候注意一下,动画关键帧里面,不能只存在1,那样子就不能驱动transform了,做不到永动机的效果,因为我的rotate写的是 rotateX(var(–x2))。接下来随意发挥吧 最后 再啰嗦一次 关于houdini最近动态可点击这里 关于houdini在浏览器的支持情况 ENJOY YOURSELF!!!
2019-03-25