个人案例
- 食典录
每时每刻,都有一道唤醒回忆的菜
食典录扫码体验
- 解决微信 H5 中 iOS 设备上 input 元素多显示搜索图标的问题
在 WebKit 内核的浏览器中,当 input 元素的类型为 "search" 时,通常会显示一个搜索图标在输入框中。这个图标是浏览器内置的默认行为,旨在提供更好的搜索体验。然而,如果你希望移除或修改这个搜索图标,可以采取以下方法: CSS 样式调整:使用 CSS 来调整搜索图标的显示样式。通过使用自定义样式覆盖默认样式,可以隐藏或修改搜索图标的外观。例如,通过设置 background-image 属性为 none,或者通过设置 background 属性为其他图标来替换默认的搜索图标。代码如下 input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { display: none; } JavaScript 操作:通过 JavaScript 操作 DOM 元素,可以动态地修改 input 元素的属性来控制搜索图标的显示与隐藏。可以使用 JavaScript 选择器选中输入框,并设置相应的属性来解决多个搜索图标显示的问题。 const searchInput = document.querySelector("input[type='search']"); searchInput.setAttribute("display", "none"); 在 WebKit 内核的浏览器中,当 input 元素的类型为 "search" 时,通常会显示一个搜索图标在输入框中。这个图标是浏览器内置的默认行为,旨在提供更好的搜索体验。然而,如果你希望移除或修改这个搜索图标,可以采取以下方法: CSS 样式调整:使用 CSS 来调整搜索图标的显示样式。通过使用自定义样式覆盖默认样式,可以隐藏或修改搜索图标的外观。例如,通过设置 background-image 属性为 none,或者通过设置 background 属性为其他图标来替换默认的搜索图标。代码如下 input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { display: none; } JavaScript 操作:通过 JavaScript 操作 DOM 元素,可以动态地修改 input 元素的属性来控制搜索图标的显示与隐藏。可以使用 JavaScript 选择器选中输入框,并设置相应的属性来解决多个搜索图标显示的问题。 const searchInput = document.querySelector("input[type='search']"); searchInput.setAttribute("display", "none");
2023-05-24 - 码农进阶中的思维变化
一、引子 时光如逝,2023年眨眼已经过去快六分之一了,近来跟同事和朋友聊的时候发现,有小伙伴对未来的发展方向和能力提升方式都有一些迷茫和不知所措,同时跟一些大牛们讨论过如何向上向下汇报、如何推动项目、如何成长,也随之有了一些感悟,借此机会写下来,希望能跟大家能一起探讨探讨。 二、概述 本篇文章的主题是思维变化,即讨论研发人员如何在思维上改变自己的工作和学习习惯从而提升自己的整体水平。 三、思维是什么,对我们有什么影响 思维是人类所具有的高级认识活动。按照信息论的观点,思维是对新输入信息与脑内储存知识经验进行一系列复杂的心智操作过程。分析与综合 是最基本的思维活动。以上来自百度百科哈哈:)。从“思维”两个字中,我们也可以领悟到一些东西,“思”即是思考,比较容易理解,关键在“维”字,“维”有角度、维度的意思。言归正传,对一个码农来说日常的工作就是代码开发,而思维方式会决定我们的代码水平和研发能力的提升。 思维如何影响我们呢,举个经典例子:一家大公司引进了一条肥皂生产线。这条生产线能将肥皂从原材料的加入直到包装装箱自动完成。 但是销售部门反映有的肥皂盒是空的,经理要求工程师们解决这个问题。于是成立一个以几名博士为核心、十几名研究生为骨干的团队。在耗费数十万后,工程师们在生产线上一套X光机和高分辨率监视器,当机器对X光图像进行识别后,一条机械臂会自动将空盒从生产线上拿走。 另外一家乡镇企业也遇到了同样的情况,管理生产线的小工找来一台电风扇,摆在生产线旁。装肥皂的盒子逐一在风扇前通过,只要有空盒子便会被吹离生产线,掉在箩筐里。 [图片] 就从本事例上来说确实一套高科技的检测流程还不如一台电风扇,不同的思维方式导致花费的代价不同。 大家别慌,让我们再换个角度探讨一下,假设我们现在生产的装肥皂的盒子进行了改良更精美也更重了,电风扇吹不动了怎么办;假设我们另一个做罐头的生产线也需要检测罐头里是否有桃子怎么办。 再换个角度看上面的例子,有没有感觉像是我们开发一个很小的项目时,投入了大量的人力物力做了一个apm系统来保障这个小项目的线上正常运行和安排一个同学每天去线上看一眼是不是项目还在正常运行类似,那么大家觉得这个apm系统应该不应该开发?对于这个问题,我们先不着急把答案定下来,看完下面的分析我在再来讨论。 四、研发能力的评判 对于研发的能力,各厂都有自己的职级划分,这里我们举个例子吧(一家之言,大家轻拍) 入门阶段:在他人指导下能够完成比较简单的任务 编码达人:代码的质量和效率都很好,能独立完成任务 独当一面:作为核心骨干能够负责中小型项目的研发管理 技术专家:具备架构设计能力,有实现大型系统的能力 领域专家:行业的领军人物,某个头部系统或者产品的领头人 以上不同的级别对应不同的能力,而我们的成长应该包括两个方面,一个是知识,另一个是思维,两者相辅相成。有了一定的知识会改变我们的思维模式;有了一定的思维模式时,会自动去学习欠缺的知识。知识的学习已经又很多教程了,下面我们先从思维模式上聊一聊。 五、聊聊几种研发中的思维 1、框架思维 软件开发是一种知识活动,因此知识的聚集和积累是至关重要的。框架能够采用一种结构化的方式对某个特定的业务领域进行描述,也就是将这个领域相关的技术以代码、文档、模型等方式固化下来。 2、架构思维 一个系统的运行模式是怎样进行,前后端如何协作、数据如何处理、前端如何展示通用逻辑如何公共和抽象、开发调试部署的流程、功能的可扩展性、服务的稳定性设计、高并发的设计、程序的安全性设计、生态建设等等,我们可以将这些通用的设计从架构层面上进行考虑和实现,而业务开发只需关注业务的逻辑。 3、懒人思维 软件的目标,是某些工作自动化,从而让某些人可以更懒。重复的事情一定不要自己手工重复完成,侧重于自动化。思考如何把这些原来需要很麻烦的事情,自动化执行。比如使用脚手架进行项目的初始化、CI/CD减少项目运维的工作、自动化测试减少测试的工作量。 4、全局思维 任何一个岗位都有其上下游的链接。比如研发的上面有市场,下面有生产。当你写客户软件时,你得站在客户的角度看看方不方便使用、系统稳定不稳定、体验有好不友好;当你设计架构的时候,你得考虑软件工程师方不方便使用;当你设计整个开发流程时,你得考虑团队的效能。更进一步,你的这个技术方案对于公司整体技术方案的适配性怎么样,也应该考虑考虑。 5、系统性的思维 当你在写代码的时候,你很容易就认为只要你按照需求实现了指定的功能,你的代码就写完了。如果想写出真正有影响力的代码,你需要从整个系统去理解你的工作: 1).你的代码和其他人写的代码在功能上是什么关系? 2).你有没有好好测试你的代码?或者其他人是否很容易测试你的代码? 3).为了部署你的代码,线上生产环境的代码是不是需要改动? 4).新的代码会不会影响到已经运行的代码? 5).在新的功能下,你的目标用户的行为是不是你期望的? 6).你的代码有没有产生商业上的影响? 7).你的代码是否兼容老数据?兼容不同的入口场景? 6、创新思维 是指一种能够激发创造力和创新灵感的思考方式。创新思维通常包括挑战常规思维方式的能力、在问题解决中采用多种不同的角度和方法、发掘新的机会、将不同的元素或概念组合起来以创造新的东西等能力。技术的更新迭代很快、软件产品也越来越多,各行各业、时时刻刻都在有创新,别人创造出来了,我们不学习就会落后,只有保持进步和创新才能不被这个时代所抛弃。 以上总共介绍了6种思维(如有遗漏欢迎补充),对我们研发来说,如何通过思维上的改变来提升自己的能力呢? 六、能力进阶与思维改变 对于不同阶段的人,进阶的路线也是不一样的,这里我们还是以之前的职级为例,探讨一下如何通过思维的改变来完成能力的进阶,希望能给对自己的成长路线不太明朗的同学有所帮助。 1、假如你是一个加入到码农行业的同学:希望你能有“框架思维”,在稳固基础知识的同时,能够养成良好的思维习惯,做任务前能够了解何种技术可以实现你的需求,完成任务时做好总结并形成文档,反思自己做的好的地方与不好的地方,将解决过程和避坑经验进行归档,方便后来人的查阅和学习,在日益的积累中,你代码的质量和效率都得到很好的提升; 2、假如你是一个编码达人:希望你能有“系统性的思维”,工作中要不断的思考你负责的模块在整个项目中是属于哪部分,你的程序是如何运行,模块之间是如何互相调用的,项目周期的每个阶段需要做那些事情,了解你的角色以及项目负责人的角色需要做哪些事情。日常工作中要以积极的态度去推动项目的进行、遇到技术卡点问题要多从原理层面进行分析。强烈的责任心、良好业务能力、出众的技术能力是你成为项目负责人所不可或缺的因素。 3、假如你已能独当一面:希望你能在日常工作中不仅仅局限于业务的研发,在代码开发的过程中能有“架构思维”。针对需求的实现,要关注:架构设计的是否合理、性能上是否有不必要的浪费、安全漏洞是否有统一解决方案或防御、公共能力的抽象和使用是否合理、核心业务逻辑的流程是否合理、库表设计是否有空间的浪费等等;技术选型上参考以往类似实现怎么做的,是否还适用、是否要有改进、依赖哪些能力等; 4、假如你是一个技术专家:你需要有“全局思维”、“懒人思维”。你已经拥有架构能力,可以设计项目的架构,但与此同时也更需要关注系统的兼容性、数据迁移方案、可拓展性、稳定性,以及架构提供的能力是否可以让开发者不必关注底层基础能力、公共能力而只专注于业务开发;在开发流程上是否可以做到精简,减少项目上线的流程;通过自动化的检测代码安全、逻辑漏洞、文件格式化、测试等提高开发效率保证运行质量;提供的公共化能力是否有相应的文档建设、测试用例、生态能力;完善基建平台的能力,比如监控系统运行情况的apm、实时更新应用配置的配置中心、应用部署运维的平台、公司内部的管理/工具平台等等;产品的功能是否做到了人无我有、人有我优,交互和性能的体验是否做到了行业领尖,如何做到超越;这些都是我们走向行业顶尖所需要的基本能力; 5、“创新思维”具体对我们研发来说怎么理解呢?我先抛个砖:已有的事物,去研究实现的过程,叫学习或者模仿;知道一些技术或理论,去制造未出现的东西,叫创新。创新比较难,相对而言模仿比较简单,因为我们有行业的标准作为参考,比如“小程序”,自从微信有了这个产品之后,大家竞相模仿;但是相反我们会开发native的app却不一定能创新性的开发出“小程序”。不过创新其实无处不在,我举个例子,假设我们学会来使用游戏引擎Cocos,用它我们可以实现一个“小猪挖土豆”(查了下还真没有)的游戏,这个算是一种创新;如果有实力换种实现方式重新写个游戏引擎,这也是一种创新。保持一种思维习惯,说不定哪天突然灵光一闪,就走上人生巅峰了。 最后呢,我们再看下之前说的一个小项目,我们有没有必要花费大量的人力物力去建一个apm系统?这是一个开放的话题,假设的条件不同答案也不相同,但是我相信很多人已经有了自己的想法了。 七、小结: 谈思维是一件很空洞的事情 ,因为思维实在看不见,摸不清。它不像知识,知之为知之,不知为不知。经常听到人说,你说的我都懂,可我就是做不到,这就是一种思维习惯。所以思维不在于你知道还是不知道,而是一个思维惯性,思考问题的时候,多去提醒自己去往这个维度上想一想,时间久了也就成自然了。ps:如有不合适的地方,请指正~
2023-02-24 - 车牌号正则表达式
新能源车 号码共8位,组成: 1.省份简称 [代码][京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领] [代码] 2.发牌机关代号字母[代码]A-Z[代码]; 3.序号位有6位数,分为小型车和大型车; 小型车。第1位:仅限字母[代码]ABCDEFGHJK[代码],第2位字母或者数字,后4位纯数字; [代码][DABCEFGHJK]([A-HJ-NP-Z0-9])[0-9]{4} [代码] 其中[代码]ABCDE[代码]代表纯电动车;[代码]FGHJK[代码]代表插电混动车或燃料电池车。 大型车。前5位纯数字,第6位仅限字母[代码]DF[代码]。 [代码][0-9]{5}[DF] [代码] 正则表达式: [代码][京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[DF])|([DABCEFGHJK]([A-HJ-NP-Z0-9])[0-9]{4})) [代码] 注意:序号位不存在字母I和O防止和1、0混淆; 普通车 组成: 省份简称 + 机关代号字母 + 5位字母或数字; 其中最后一位还包括汉字[代码]挂学警港澳使领[代码] 正则表达式: [代码][京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领] [代码] 代码示例 [代码]function isVehicleNumber(vehicleNumber) { const newReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[DF])|([DABCEFGHJK]([A-HJ-NP-Z0-9])[0-9]{4}))$/ const oldReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]$/ if (vehicleNumber.length === 7) { return oldReg.test(vehicleNumber); } else if (vehicleNumber.length === 8) { return newReg.test(vehicleNumber); // 京AA12345 } else { return false; } } [代码]
2022-10-11 - 微搭低代码xChatGPT,五步搭建AI聊天机器人小程序
本文将向您展示如何使用低代码工具,在30分钟左右搭建一个基于 ChatGPT 的聊天机器人小程序。让拥有OpenAI账号的朋友能随时随地体验ChatGPT,也希望低代码xAI技术普惠更多人。 [图片] ChatGPT最近大火,让原本已经沉寂许久的AI领域再次被唤醒狂欢。但是还是有很多朋友受限于OpenAI对国内用户的限制,无法愉快地体验这项革命性的AIGC技术。 众所周知,ChatGPT 是一个基于 GPT-3 的聊天机器人模型,能够通过分析提问内容生成流畅的自然语言结果,我们除了可以在 OpenAI 的ChatGPT官网上体验,也可以通过调用官方API来获取 ChatGPT 机器人模型进行训练和体验。本文将向您展示如何使用低代码工具,在30分钟左右搭建一个基于 ChatGPT 的聊天机器人小程序。一方面能让拥有OpenAI账号的朋友能随时随地体验ChatGPT;另一方面,也希望通过教程学习搭建出AI聊天小程序,去分享给更多人,把前沿的AI技术普惠到更广泛的群体,一起体验AIGC技术所带来的便利。 我们这次就以腾讯云微搭低代码作为搭建平台,来演示一下应该如何快速开发一个基于ChatGPT的聊天机器人应用,即便是新手开发者也可以试试哦 一、准备工作在开始搭建聊天机器人之前,您需要做以下准备: 微信小程序账号:如果您还没有微信小程序账号,可以在微信公众平台注册(如果没有小程序,也可以发布为移动端H5应用)开通腾讯云微搭低代码:微搭低代码是腾讯云官方推出的一款快速搭建应用的低代码开发工具,可以直接访问腾讯云微搭官网免费开通注册OpenAI账号:OpenAI账号注册也是免费的,不过OpenAI有地域限制,这里网上教程关键词搜索一大把,就不做赘述了。注册成功后,可以登录OpenAI的个人中心来获取[代码]API KEY[代码]本教程适用人群和应用类型: 适用人群:初级开发者(操作门槛较低,有一定技术背景的非开发者也可以体验)应用类型:小程序 或 H5应用(基于微搭一码多端特性,也可以直接发布为Web应用,点击文末链接可体验作者微搭搭建的Web版GPT聊天机器人)二、搭建聊天机器人首先,一个常见的聊天对话机器人应用界面效果,如下图所示: [图片] 通过应用界面可以看到,它主要由如下几个部分组成: 一个对话聊天界面一个API数据查询接口界面UI与后端数据的联动渲染那现在,我们就参照上面的几个模块,正式开始通过微搭低代码工具,分5个步骤来依次拆解搭建: 1.对应用界面进行样式配置[图片] 我们先拖入一个滚动容器和一个普通容器,一个用来展示聊天的上下文对话,一个用来展示输入框和发送按钮。然后依次拖入图中大纲树所示的组件,组件相应的层级关系可以参考上图中的大纲树结构。 接下来针对上述的组件分别进行样式的配置,我们默认使用样式面板的弹性(Flex)布局,包含接收消息和发送消息两个普通容器,可以分别选择样式面板中的弹性布局中的左对齐,如下图所示: [图片] 接着可以分别配置图片和文本两个组件的高度和宽度大小以及内外间距,以达到想要的视觉效果。 完成聊天上下文对话框的样式配置之后,可以进行底部多行输入框和按钮这个普通容器的样式配置,样式配置方式与上面的发送消息容器一样使用弹性布局并选择“平分”的方式布局,如下图所示: [图片] 完成布局配置之后,由于底部输入框按钮等是固定位置的,故需要额外配置一下定位属性,选中底部的“普通容器”,在样式面板底部,做如下配置即可: [图片] 以上,通过进一步微调一些样式细节如组件背景色以及间距等后,即可达到上文提到的应用界面效果了。 可以看到整个页面的配置过程是完全可视化操作的,不需要写一行代码。当然,如果样式配置不是很熟悉,或者有疑问的朋友,也可以等我们的视频教程,手把手教你用微搭低代码来配置AI聊天应用。 2. 配置数据变量和数据源API第2步,开始进行数据的绑定和数据源的配置: a. 新建一个数组对象变量chatList,用于存储聊天记录 [图片] 首先配置一个变量,如命名为chatLlist聊天记录这么一个变量,一个对象数组,默认值为如下所示,当然大家也可以基于这个结构任意修改。 [ { "res": "你好,欢迎体验ChatGPT聊天机器人,你可以直接输入你感兴趣的任何问题向我提问", "req": "红孩儿是牛魔王的亲儿子吗?", "index": 1 }, { "res": "不是,红孩儿是牛魔王的养子。据西游记中的记载,牛魔王是一个孤独的怪物,他在深山里住了很久,没有子女,却有一个养子——红孩儿,红孩儿的父母去世时,牛魔王便收养了他。", "req": "那谁教他的三味真火", "index": 2 } ] 接着把这个数组变量的初始值跟我们的这个页面的内容分别进行绑定。首先我们选择一个父级的普通容器,在属性配置的循环展示绑定为刚刚新建的数组变量。然后在里面的子节点中,如文本组件,分别绑定这个数组中的成员变量,他们的配置如下图所示: [图片] [图片] 这一步数据绑定完成之后,接下来就可以去配置请求远程数据的数据源API了。 b. 配置一个数据源APIs(用于请求Chat GPT接口) API的配置相对比较简单,主要参考OPENAI的官方文档,文档中可以看到文本对话接口对应的请求参数信息如下: curl https://api.openai.com/v1/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{"model": "text-davinci-003", "prompt": "Say this is a test", "temperature": 0, "max_tokens": 7}' 分别把上方的CURL请求头信息对应到HTTP的请求中即可,配置项参考如下: [图片] 我们通过数据源中的【方法测试】,得到API的返回结果如下,点击【出参映射】即可完成出参结构的配置: { "id": "cmpl-GERzeJQ4lvqPk8SkZu4XMIuR", "object": "text_completion", "created": 1586839808, "model": "text-davinci:003", "choices": [ { "text": "\n\nThis is indeed a test", "index": 0, "logprobs": null, "finish_reason": "length" } ], "usage": { "prompt_tokens": 5, "completion_tokens": 7, "total_tokens": 12 } } 其中的[代码]API_KEY[代码]是在完成OPENAI账号注册之后,在其个人中心中获取即可,至于OPENAI的账号注册方式,大家动动手搜索一下,网上教程很多就不啰嗦了。 3. 给发送按钮绑定请求事件我们在第1步中已经在页面中放置了输入框、按钮和文本展示等组件。接下来,我们需要给输入框配置相关的事件响应逻辑,来获取用户输入的消息内容,参考的关键配置如下: [图片] 然后,给按钮绑定事件来处理输入框中用户发送的消息,选择按钮组件,在右侧事件面板中配置如下逻辑,即 点击按钮 时触发API请求,并将获取到的API返回结果渲染在页面中。 [图片] 4. 将API返回数据 与 在页面中进行渲染展示第4步,将返回值用“变量赋值”方法加入到chatList数组中 [图片] 这里我们需要在数据中增加一条新的消息,采用表达式绑定的方式进行原有的[代码]ChatList[代码]变量进行解构后再赋值,表达式参考如下: [ ...$page.dataset.state.chatList, { req: $page.dataset.state.text, res: "" } ] 5. 完成开发,进行应用发布前端界面和后端数据逻辑都配置开发完成后,可在应用编辑器的右上角点击“发布”按钮,我们可以选择发布到 已绑定的小程序,也可以直接发布移动端的H5应用,如下所示: [图片] 至此,一个基础的AI聊天机器人应用搭建就完成了。 三、进一步完善基于上述步骤搭建完聊天机器人小程序后,你还可以进一步完善它的功能。 例如,您可以在小程序中添加聊天记录功能,让用户可以查看过往的聊天记录。您也可以使用其他自然语言处理技术;例如语音识别和文本分类,来使聊天机器人更加智能。 如需要存储聊天历史记录的话,可以在数据源中配置一个“聊天历史记录”数据模型,参考模型配置如下: [图片] 总之,使用微搭低代码搭建聊天机器人小程序,对于熟悉低代码或者喜欢钻研能力的朋友来说,确实是一件非常简单而有趣的事情。当然如果确实对界面样式配置不是很熟悉,或者其他有疑问的朋友,也可以关注漫话开发者公众号后续视频教程,手把手教你用微搭低代码来配置AI聊天机器人应用。 通过本教程的介绍,你已经基本熟悉了如何使用微搭低代码快速搭建基于 ChatGPT 的聊天机器人了,有任何疑问也欢迎关注漫话开发者公众号留言。 四、附录Q/A腾讯云微搭低代码平台,连接微信和企微用户,帮助企业快速定制和构建移动协同办公应用,让信息和流程流转更高效。3分钟可视化搭建和发布小程序、H5、Web等多端应用。快速搭建企业专属的业务管理平台,表单流程等办公和管理类应用,提供企业级账号和权限管控等能力。在搭建聊天机器人应用过程中,你可能会遇到一些问题,下面是常见问题的解决方法:i. 机器人无法回复:这可能是因为 ChatGPT 机器人模型无法理解用户的问题。可以尝试使用更加具体的问题,或者尝试使用不同的自然语言处理模型。ii. 机器人回复不流畅:这可能是因为 ChatGPT 机器人模型生成的回复不够流畅或在服务器在境外所致。可以尝试调整模型的「[代码]temperature[代码]」参数,使生成的回复更加流畅。iii. 机器人回复内容不准确:这可能是因为 ChatGPT 机器人模型无法理解用户的问题,或者因为模型没有学习到足够的知识。可以尝试使用更加具体的问题,或者尝试使用不同的自然语言处理模型。iv. 如果遇到其他低代码配置问题,可以尝试在微搭社区或通过网上搜索中寻求帮助。
2023-02-10 - H5或者小程序页面能调用聊天工具栏的相关接口实现发送自定义卡片连接到聊天会话框么?
举个应用场景的例子: 外部联系人跟医生私聊问诊的情况下,医生给出诊断意见,有很多场景是需要给用户开具处方的。 理想的场景下,医生通过企业微信的某个入口【譬如:工具栏应用】进入小程序或者H5的开方流程,开完处方后可以调用某个API,直接将处方的URL以消息的形式发给用户或者群里。
2020-09-22 - wx-open-launch-weapp在javascript不显示是怎么回事?
wx-open-launch-weapp在javascript不显示是怎么回事? 代码是按照官网文档写的,直接把官网文档代码粘贴进去也不显示 <wx-open-launch-weapp id="launch-btn" username="gh_xxxxxxxx" path="pages/home/index.html?user=123&action=abc" > <template> <style>.btn { padding: 12px }</style> <button class="btn">打开小程序</button> </template> </wx-open-launch-weapp>
2020-07-22 - ios下开始录音会导致ui卡顿?
在开发者工具和安卓下正常,只有在ios下开始录音的时候会导致页面的css动画卡顿一下,请问这个问题有解吗? [图片] https://developers.weixin.qq.com/s/YPYwK4mL7Jkl
2020-09-03 - RecorderManager.onStop返回的duration好像不准确?
结束录音接口 RecorderManager.onStop 返回的 duration 好像会把暂停的时间也计算进去,有大佬跟我一样吗?还是我太菜写错了,求指出问题 [图片] 代码片段:https://developers.weixin.qq.com/s/wX7nAJme7Pcf
2019-11-08 - 多形态小程序日历组件,轻松搞定项目需求
小程序日历组件 小程序日历组件,支持多种模式,简单易用好上手。 4种日历模式 3种日期选择方式 支持自定义节假日 支持自定义日期内容 懒加载保证渲染性能 支持农历 支持根据指定日期自动生成 支持跨无数据月份 [图片] [图片] [图片] [图片] [图片] 日历组件基础配置 wxml模板 [代码]<ui-calendar dataSource="{{config}}" /> [代码] 配置日历组件 [代码]Pager({ data: { source: { $$id: 'calendar', mode: 1, // 纵向日历 type: 'range', // 区域选择 tap: 'onTap', // page响应事件 total: 365, // 指定日历总天数 data: [], // 按给定日期计算total值,自动构建日历 rangeCount: 28, // 区选区间28天 rangeMode: 2, // 区选模式 rangeTip: ['入住', '离店'], // 区选提示 festival: true, // 开启节假日显示 alignMonth: false, // 月份对齐,swiper切换时 lunar: false, // 是否显示农历 date: [], // 指定日期显示的内容 value: ['2019-12-24', '2020-01-05'], // 默认值 toolbox: { monthHeader: true, // 是否显示月头 discontinue: false, // 自动构建时,是否省略无数据的月份 }, methods: { // 响应 tap事件 onTap(e, param, inst) { // param.date 选中的当前日期 // 当区选模式时 // param.range === 'start' 区选第一天 // param.range === 'end' 区选最后一天 } } } } }) [代码] github地址:https://github.com/webkixi/aotoo-xquery 小程序demo演示 [图片]
2020-06-30 - 如何实现一个自定义数据版省市区二级、三级联动
社区可能有其他的方案了,但是再分享下吧,给有需要的童鞋。 效果图: [图片] 额,这个视频转GIF因为社区上传不了大图,所以剪了一部分,具体的效果还是直接工具打开代码片段预览吧~ 第一步:你的页面JSON引入该组件: [代码]{ "usingComponents": { "city-picker": "/components/cityPicker/index" } } [代码] 第二步:你的页面WXML引入该组件 [代码]<city-picker visible="{{visible}}" column="2" bind:close="handleClick" bind:confirm="handleConfirm" /> [代码] 第三步:你的页面JS调用 [代码]// 显示/隐藏picker选择器 handleClick() { this.setData( visible: !this.data.visible }) }, // 用户选择城市后 点击确定的返回值 handleConfirm(e) { const { detail: { provinceName = '', provinceId = '', cityName, cityId='', areaName = '', areaId = '' } = {} } = e this.setData({ cityId, cityName, areaId, areaName, provinceId, provinceName }) } [代码] 组件属性 属性 默认值 描述 visible false 是否显示picker选择器 column 3 显示几列,可选值:1,2,3 values [0, 0, 0] 必填,默认回填的省市区下标,可选择具体省市区后查看AppData的regionValue字段 close function 点击关闭picker弹窗 confirm function 点击选择器的确定返回值 confirm: 属性 默认值 描述 provinceName 北京市 省份名称 provinceId 110000 省份ID cityName 市辖区 城市名称 cityId 110100 城市ID areaName 东城区 区域名称 areaId 110000 区域Id 至于怎么获取你想默认城市的下标,可以滑动操作下选中省市区后,点击确定后查看appData里的regionValue的值。 以上就是一个自定义数据版本的省市区二级、三级联动啦,老规矩,结尾放代码片段。 https://developers.weixin.qq.com/s/F9k9cTmT7LAz
2022-07-20 - 分享用户头像叠加循环渲染
[代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"flex-Center avatorBox"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"avatorBox-a"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{images}}"[代码] [代码]wx:key [代码][代码]style[代码][代码]=[代码][代码]'transform:translateX({{-index*25}}rpx)'[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]image[代码] [代码]class[代码][代码]=[代码][代码]"avator"[代码] [代码]src[代码][代码]=[代码][代码]'{{item}}'[代码] [代码]mode[代码][代码]=[代码][代码]'aspectFill'[代码][代码]></[代码][代码]image[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]view[代码][代码]>[代码][代码]</[代码][代码]view[代码][代码]>[代码][图片]
2019-12-02 - 小程序之日历签到积分
[图片] 该示例为纯手写代码,暂无插件,不多说直接上代码 我们的实现思路: JS部分 1、获取当前年月 const date = new Date(); cur_year = date.getFullYear(); cur_month = date.getMonth() + 1; const weeks_ch = [‘日’, ‘一’, ‘二’, ‘三’, ‘四’, ‘五’, ‘六’]; this.setData({ cur_year, cur_month, weeks_ch, }) 2、获取当月共多少天 getThisMonthDays: function (year, month) { return new Date(year, month, 0).getDate() }, 3、获取当月第一天星期几 getFirstDayOfWeek: function (year, month) { return new Date(Date.UTC(year, month - 1, 1)).getDay(); }, 4、计算当月1号前空了几个格子,把它填充在days数组的前面 calculateEmptyGrids: function (year, month) { var that = this; //计算每个月时要清零 that.setData({ days: [] }); const firstDayOfWeek = this.getFirstDayOfWeek(year, month); if (firstDayOfWeek > 0) { for (let i = 0; i < firstDayOfWeek; i++) { var obj = { date: null, isSign: false } that.data.days.push(obj); } this.setData({ days: that.data.days }); //清空 } else { this.setData({ days: [] }); } }, 5、绘制当月天数占的格子,并把它放到days数组中 calculateDays: function (year, month, sign) { var that = this; var isSign; const thisMonthDays = this.getThisMonthDays(year, month); for (var i = 1; i <= thisMonthDays; i++) { var obj = { date: i, isSign: ‘’ } for (var j = 0; j < sign.length; j++) { if (i == parseInt(sign[j].create_time.substr(8, 2))) { obj.isSign = true; break; } } that.data.days.push(obj); } this.setData({ days: that.data.days }); }, 6、切换控制年月,上一个月,下一个月 handleCalendar: function (e) { const handle = e.currentTarget.dataset.handle; const cur_year = this.data.cur_year; const cur_month = this.data.cur_month; if (handle === ‘prev’) { let newMonth = cur_month - 1; let newYear = cur_year; if (newMonth < 1) { newYear = cur_year - 1; newMonth = 12; } this.signRecord(newYear, newMonth); this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘cnext.png’ }) } else { if (cur_month + 1 > month) { this.setData({ imgType: ‘next.png’ }) } else { let newMonth = cur_month + 1; let newYear = cur_year; if (newMonth > 12) { newYear = cur_year + 1; newMonth = 1; } this.signRecord(newYear, newMonth); if (cur_month + 1 == month) { this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘next.png’ }) } else { this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘cnext.png’ }) } } } }, wxml部分: <view class=‘all’> <view class=“bar”> <!-- 上一个月 --> <view class=“previous” bindtap=“handleCalendar” data-handle=“prev”> <image src=‘https://www.***.com/weChatImg/pre.png’></image> </view> <!-- 显示年月 --> <view class=“date”>{{cur_year || “–”}} / {{filter.fill(cur_month) || “–”}}</view> <!-- 下一个月 --> <view class=“next” bindtap=“handleCalendar” data-handle=“next”> <image src=‘https://www.***.com/weChatImg/{{imgType}}’></image> </view> </view> <view class=‘xxian’> <image src=‘weChatImg/huan.png’></image> <image src=‘weChatImg/huan.png’></image> </view> <!-- 显示星期 --> <view class=“week”> <view wx:for="{{weeks_ch}}" wx:key="{{index}}" data-idx="{{index}}">{{item}}</view> </view> <view class=‘days’> <!-- 列 --> <view class=“columns” wx:for="{{days.length/7}}" wx:for-index=“i” wx:key=“i”> <view wx:for="{{days}}" wx:for-index=“j” wx:key=“j”> <!-- 行 --> <view class=“rows” wx:if="{{j/7 == i}}"> <view class=“rows” wx:for="{{7}}" wx:for-index=“k” wx:key=“k”> <!-- 每个月份的空的单元格 --> <view class=‘cell’ wx:if="{{days[j+k].date == null}}"> <text decode="{{true}}"> </text> </view> <!-- 每个月份的有数字的单元格 --> <view class=‘cell’ wx:else> <!-- 当前日期已签到 --> <view wx:if="{{days[j+k].isSign == true}}" style=‘color:#acacac’ class=‘cell’> {{days[j+k].date}} <image src=‘https://www.***.com/weChatImg/sgin.png’></image> </view> <!-- 当前日期未签到 --> <view wx:else> <text>{{days[j+k].date}}</text> </view> </view> </view> </view> </view> </view> </view> </view> 相信大家通过以上思路,再结合自己的需求应该可以自己做出符合自己心目中的日历插件或者签到
2019-09-11 - 小程序毫秒级倒计时的一种优雅实现
在小程序上用毫秒级定时器来刷新页面或组件,会引发性能问题。 使用 [代码]canvas[代码] 又对字体样式无法进行很好的控制。 一种实现方案 使用秒级的定时器刷新页面或组件实现秒级的倒计时,制作毫秒倒计时的 GIF 并把图放到秒级倒计时后面。 毫秒倒计时 GIF:[图片] 小程序效果如下图所示: [图片] HAVE FUN 😃
2019-09-20 - 微信小程序开发new Date()在IOS注意点
一般我们服务端接口返回的时间形式为: 2019-07-19 11:25:21 需要前端转换成时间戳,直接使用 [代码]new Date("2019-07-19 11:25:21") [代码] 在微信小程序开发中IOS上获取不到对应的时间对象。 解决方案: [代码]Date.parse("2019-07-19 11:25:21".replace(/-/g,"/")); [代码]
2019-09-23 - 搭建一个https网站的全过程
概述:本着学习的目的,做了这个分享。自己切切实实的做完了整个流程,发现其中的坑也是蛮多的,当然自己的收获对应也是蛮多的。写下这个流程一方面为了加深自己的印象、可以在将来回顾一下,另一方面也是为了给有需要的人提供帮助~ 一、服务器准备 [代码]为了演示方便,我购买了一台腾讯云服务器 [代码] [图片] 安装的操作系统是centos 二、域名准备 1、域名注册 可以从万网上进行注册:https://wanwang.aliyun.com/ 2、域名备案 备案流程略复杂,这里只列了一个步骤简介 入口:https://beian.aliyun.com/ [图片] 域名的备案时间较长,建议大家提前准备起来。 3、域名解析 域名备案通过之后,为我们的网站准备一个子域名,入口:https://dns.console.aliyun.com/?spm=a2c1d.8251892.aliyun_sidebar.daliyun_sidebar_dns.37575b76kNuXEO#/dns/domainList [图片] 点击上图的解析设置 [图片] 将记录值填写我们刚刚买的服务器的公网ip 三、申请ssl证书 我是从腾讯提供的证书服务里申请的,腾讯申请入口 https://console.cloud.tencent.com/ssl 点击“申请免费证书” [图片] 这里选择了手动Dns验证 [图片] 申请完成后会有个表格,是说明如何Dns校验的,要求域名下添加一条解析记录 [图片] Dns校验 首先,在域名下添加一条txt记录 [图片] 然后,单机自助诊断旁边的“查询” [图片] 下载证书 [图片] 点击图中的下载即可 四、服务器安装软件 我用ssh连接服务器 登录服务器:ssh root@212.129.*.* , 然后输入密码(从腾讯云管理后台进行密码的设置和获取)进入服务器,然后安装软件,如下~ 1、git:用于代码管理 2、nvm:用于管理node版本 3、node:用于启动web服务 4、pm2:用于守护node进程 安装git [代码]yum install git -y [代码] 下载nvm [代码]git clone git://github.com/creationix/nvm.git ~/nvm [代码] 设置nvm 自动运行; [代码]echo "source ~/nvm/nvm.sh" >> ~/.bashrc source ~/.bashrc [代码] 查询node版本 [代码]nvm list-remote [代码] 安装node.js [代码]nvm install v10.16.3 [代码] 使用nodejs [代码]nvm use v10.16.3 [代码] 使用npm安装pm2 [代码]npm install -g pm2 [代码] 五、下载一个web项目 & 使用 pm2 启动 这里我使用了自己的一个github上的项目:https://github.com/myronliu/ssr-koa-react-redux.git 1、git clone https://github.com/myronliu/ssr-koa-react-redux.git 2、cd ssr-koa-react-redux 3、npm install 4、pm2 start server.js 服务启动之后如下图: [图片] 现在我们可以用IP(服务器的公网ip) + 端口号来进行访问了 [图片] 六、安装nginx 步骤 1: 添加 yum 源 Nginx 不在默认的 yum 源中,可以使用 epel 或者官网的 yum 源,本例使用官网的 yum 源。 sudo rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm 安装完 yum 源之后,可以查看一下。 sudo yum repolist 步骤 2: 安装 yum 安装 Nginx,非常简单,一条命令: sudo yum install nginx 常用nginx命令介绍 设置开机启动 $ sudo systemctl enable nginx 启动服务 $ sudo systemctl start nginx 停止服务 $ sudo systemctl restart nginx 重新加载,因为一般重新配置之后,不希望重启服务,这时可以使用重新加载。 $ sudo systemctl reload nginx 步骤3: 打开防火墙端口 sudo firewall-cmd --permanent --zone=public --add-service=http sudo firewall-cmd --permanent --zone=public --add-service=https sudo firewall-cmd --reload [图片] 步骤4: 配置nginx 找到并查看配置文件/etc/nginx/nginx.conf [图片] 步骤5: 配置我们自己的conf [图片] 编写为 [图片] 步骤6: 现在可以使用域名访问我们的网站了,目前是http协议的 [图片] 七、安装证书 将第三步下载的证书里选择Nginx下的两个文件上传到服务器的/etc/nginx/conf.d文件夹下 [图片] Mac上利用scp上传 scp /Users/xxx/Downloads/todo.xxx.com/Nginx/1_todo.xx.com_bundle.crt root@212.129.*.*:/etc/nginx/conf.d [图片] 八、创建https的conf文件 进入到/etc/nginx/conf.d下,执行命令:touch https.conf && vi https.conf [图片] 测试nginx配置 & 重启 [图片] 九、访问https协议的站点 [图片] 备注: 如有问题,欢迎指出! 如有侵权,联系删除~
2019-09-24 - 小程序使用防抖函数的简单方法
废话不多说,上代码: Page构造器内部使用,不需要使用外部模块。 [代码]onLoad: function (options) { console.log(options); this.debounce = this.debounce();// 防抖函数,在此处初始化 // 若不初始化,函数主体不执行 } // debounce函数,就是事件触发的函数,名字可以随意取名 debounce : function () { var timeOut = null; return () => { clearTimeout(timeOut); timeOut = setTimeout(() => { // 事件函数中要执行的代码块 // 改写原函数异常方便、简洁 }, 300); } } [代码] 如果这个有问题,欢迎指点。
2019-11-12 - 微信小程序授权处理(wx.authorize()和wx.openSetting() 使用整理)
权限处理流程分析及整合 权限申请方式 通过 button 的 open-type 方式来获取相应的权限及信息。 通过 wx.authorize()和wx.openSetting() 两个接口来申请相应的权限。 特别注意的是:需要授权 scope.userLocation、scope.userLocationBackground 时必须配置地理位置用途说明 通过button申请权限 通过button获取权限对于开发者来说相对简单一些,开发者只需要绑定相应的回调函数即可,权限申请的成功和失败都可以在回调函数中处理。 其实我们可以把通过button申请权限的方式当作普通的信息获取接口,只不过这个接口必须通过button组件来调用而已。 通过wx.authorize()来申请权限 通过wx.authorize()来申请权限的方式是比较繁琐的。因为它的状态比较多,大致可分为: 如果用户未接受或拒绝过此权限,会弹窗询问用户,用户点击同意后方可调用接口; 如果用户已授权,可以直接调用接口; 如果用户已拒绝授权,则不会出现弹窗,而是直接进入接口 fail 回调。请开发者兼容用户拒绝授权的场景 针对这三种状态,开发者的处理方式也有所不同。 用户未接受或拒绝过此权限,会弹窗询问用户,用户点击同意 —— 可调用相应接口。 用户未接受或拒绝过此权限,会弹窗询问用户,用户点击拒绝 —— 打开设置页面。 如果用户已授权 —— 可调用相应接口。 用户已拒绝授权 —— 打开设置页面。 上述情况的2/4是需要开发者结合 wx.openSetting() 来帮助用户进行二次授权的。就是这个 openSetting 打开设置页面的过程让开发者比较抓狂。2.3.0 版本开始,用户发生点击行为后,才可以跳转打开设置页。也就是说 openSetting 只能通过 button 组件和点击(bindtap)的回调函数里来调用。然而我们在申请权限的时候无法保证用户一定会在第一次申请的时候同意,所以一定是需要配合openSetting来使用的。那我们在每个调用openSetting的地方都加一个button也是很不别扭的。当下来说我们最好的处理方案就是把wx.authorize()和wx.openSetting()结合起来使用,并把他们放在一个bindtap/catchtap(或其他手势事件)的回调函数里。接下来我们重点来整合一下这种方式的权限处理。 wx.authorize()来申请权限整合 权限工具类代码逻辑预览 以下是整合的工具类代码,我们暂时命名其为 authorizer.js,可直接复制使用。 [代码]const authsName = { "scope.userInfo": "用户信息",// 请用button获取该信息 "scope.userLocation": "地理位置", "scope.userLocationBackground": "后台定位", "scope.address": "通讯地址", "scope.invoiceTitle": "发票抬头", "scope.invoice": "获取发票", "scope.werun": "微信运动步数", "scope.record": "录音功能", "scope.writePhotosAlbum": "保存到相册", "scope.camera": "摄像头", } var scope = null; var success = null; var fail = null; var denyBack = null; var deniedFun = null; /** * 申请某个权限 * _scope 权限名称 * _success 成功回调 * _fail 失败回调 * _denyBack 申请权限时用户 拒绝 后的回调 * _deniedFun 之前申请过该权限但被拒绝了,该情况下调用此函数 */ function authorize(_scope, _success, _fail, _denyBack, _deniedFun) { resetData(); scope = _scope; success = _success; fail = _fail; denyBack = _denyBack; deniedFun = _deniedFun; if (!scope) { return; } // 判断权限状态 wx.getSetting({ success: function(res) { let currentScope = res.authSetting[scope]; if (currentScope == undefined || currentScope == null) { // 之前没有申请或该权限 wx.authorize({ scope: scope, success: function(res) { authSuccess(res); }, fail: function(err) { authDeny(); } }); } else if (currentScope == false) { // 之前申请过该权限但被拒绝了, 如果配置 deniedFun 函数,则有执行 deniedFun 方法, // 由调用者决定改中情况下如何处理。 if (authDenied()) { return; } // 如果没有配置 deniedFun 函数,走默认逻辑,打开设置界面 wx.showModal({ title: '权限申请', content: '点击 “确定” 按钮,打开 “' + authsName[scope] + '” 的权限设置界面', cancelText: '取消', confirmText: '确定', success(res) { if (res.confirm) { wx.openSetting({ success: function(res) { let cScope = res.authSetting[scope]; if (cScope) { authSuccess(); } else { authFail(); } }, fail: function(res) { authFail(); } }); } } }); } else { // 已经获得该权限 authSuccess(); } }, fail: function() { authFail(); } }); } /** * 权限申请成功 */ function authSuccess(res) { if (success && typeof success == 'function') { success(res); } resetData(); } /** * 权限申请失败 */ function authFail() { if (fail && typeof fail == 'function') { fail(); } resetData(); } /** * 拒绝使用该权限 */ function authDeny() { if (denyBack && typeof denyBack == 'function') { denyBack(); } resetData(); } /** * 之前申请过该权限但被拒绝了 */ function authDenied() { if (deniedFun && typeof deniedFun == 'function') { deniedFun(); resetData(); return true; } else { return false; } } /** * 重置参数 */ function resetData() { scope = null; success = null; fail = null; denyBack = null; deniedFun = null; } module.exports = { authorize: authorize, } [代码] 工具类的使用 wxm代码: [代码] <view bindtap="authApply">权限申请</view> [代码] js代码: [代码]/** * 权限申请--微信步数 */ authApply(e) { authorizer.authorize("scope.werun", function(res) { console.log('success', res); wx.getWeRunData({success: function(res){ console.log('WeRunData', res) }}); }, function(err) { console.log('denyback', err); }, function(err) { console.log('deniedBack', err); }); }, [代码] 总结 微信小程序的权限申请有两种方式; 分析了一下wx.authorize这种方式发生的几种情况以及相应的处理; 整理了一份wx.authorize使用的通用方法,且此方法须在某个手势事件的回调函数中。 文中如有不准确或不对的描述还请不吝指出。
2019-11-18 - 小程序顶部导航栏,可滑动,可动态选中放大
最近在研究小程序顶部导航栏时,学到了一个不错的导航栏,今天就来分享给大家。 老规矩,先看效果图 [图片] 可以看到我们实现了如下功能 1,顶部导航栏 2,可以左右滑动的导航栏 3,选中条目放大 原理其实很简单,我这里把我研究后的源码发给大家吧。 wxml文件如下 [代码]<!-- 导航栏 --> <scroll-view scroll-x class="navbar" scroll-with-animation scroll-left="{{scrollLeft}}rpx"> <view class="nav-item" wx:for="{{tabs}}" wx:key="id" bindtap="tabSelect" data-id="{{index}}"> <view class="nav-text {{index==tabCur?'tab-on':''}}">{{item.name}}</view> </view> </scroll-view> [代码] wxss文件如下 [代码]/* 导航栏布局相关 */ .navbar { width: 100%; height: 90rpx; /* 文本不换行 */ white-space: nowrap; display: flex; box-sizing: border-box; border-bottom: 1rpx solid #eee; background: #fff; align-items: center; /* 固定在顶部 */ position: fixed; left: 0rpx; top: 0rpx; } .nav-item { padding-left: 25rpx; padding-right: 25rpx; height: 100%; display: inline-block; /* 普通文字大小 */ font-size: 28rpx; } .nav-text { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; letter-spacing: 4rpx; box-sizing: border-box; } .tab-on { color: #fbbd08; /* 选中放大 */ font-size: 38rpx !important; font-weight: 600; border-bottom: 4rpx solid #fbbd08 !important; } [代码] js文件如下 [代码]// pages/test2/test2.js Page({ data: { tabCur: 0, //默认选中 tabs: [{ name: '等待支付', id: 0 }, { name: '待发货', id: 1 }, { name: '待收货', id: 2 }, { name: '待签字', id: 3 }, { name: '待评价', id: 4 }, { name: '五星好评', id: 5 }, { name: '差评订单', id: 6 }, { name: '编程小石头', id: 8 }, { name: '小石头', id: 9 } ] }, //选择条目 tabSelect(e) { this.setData({ tabCur: e.currentTarget.dataset.id, scrollLeft: (e.currentTarget.dataset.id - 2) * 200 }) } }) [代码] 代码里注释很明白了,大家自己跟着多敲几遍就可以了。后面会更新更多小程序相关的知识,请持续关注。
2019-11-22 - 做了一个颜色选择器
edit at 11/12 代码传到了:https://github.com/eclipseglory/zasi-components , DEMO演示在文章结尾 小程序没有提供color-picker类似的组件,只能自己做。 可传统的RGB颜色选择器,真的腻了,而且在手机上也不是很操作,就跑网上搜了一圈,发现有一种圆环形的(基于HSV)我很喜欢: [图片] 我自诩对canvas2d和webgl很熟悉,做个这玩意儿很轻松,开始做!没想到痛苦开始了。 从上周5开始,一共做了三个版本: 1.纯canvas版本 2.canvas+组件版本 3.纯组件版本 纯canvas版本这个版本做了整整一天! [图片] 由于canvas绘制性能问题,特别是因为没有requestAnimationFrame可以调用,别说在真机上测试特别不流畅,就是在模拟器上也小卡小卡的。而且,在纯的canvas进行触摸定位等事件响应处理,计算起来太麻烦,bug不断,只能放弃了。 混合版本因为wxs模块是提供requestAnimationFrame接口的,所以我就想,使用canvas作为底部颜色环,上面就直接用view作为指针,这样,事件触发和处理比起纯canvas要简单得多,而且还能利用rAF回调页面接口去绘制其他canvas。 的确,我的想法得到了证实,这个混合版本比起第一个要流畅得多! 可就要完工的时候,我却发现,在真机上,cover-view的鼠标事件有很大问题,坐标值飘忽不定,也就是说拖动指针会发生鬼畜般的抖动!加上我不知道怎么debug到wxs模块中,于是跟个sb一样fix,找了半天也没找到问题在哪儿,直到我搜索时,返现有人也遇到和我一样的问题,我才安心了:这是小程序的问题。 动手改!既然cover-view有不行,那就不用它。 实际上canvas在该组件中的作用无非就是绘制一个圆环而已,如果我利用离屏canvas事先画好,然后保存成图片,再用image加载它,这样就可以避免使用canvas来显示圆环了,也就可以不用cover-view放到其顶部! 想法是好的,可是到了真机上,绘制保存出来的图片时好时坏: [图片] 只能放弃,又耽误我一天。 无canvas版本刚才说了,canvas在该组件中的作用,仅仅是绘制一个颜色环而已,除此之外真没什么用。 那我就用css模拟一个类似圆环就好了,精确到每一度一个颜色一点意义没有。 所以就利用css的background-image属性,做了4个四分之一圆弧,然后拼在一起,得到了一个彩色原版,再用一个小的view遮挡,让它们只露出一部分,圆环就做好了。 之前的代码都不用改,直接用新作的圆环views替换canvas的标签即可。主体框架和功能,不到一天就完成了,不得不说,比起纯的canvas绘制,要方便太多太多。 这是截图: [图片] 代码片段这里是 演示DEMO,要使用的话,复制里面的组件出来用就好。 有些代码我混淆过,但不耽误使用。 有问题找我
2019-11-12 - 小程序10行代码实现微信头像挂红旗,国庆节个性化头像
最近朋友圈里经常有看到这样的头像 [图片] 既然这么火,大家要图又这么难,作为程序员的自己当然要自己动手实现一个。 老规矩,先看效果图 [图片] 仔细研究了下,发现实现起来并不难,核心代码只有下面10行。 [代码] wx.canvasToTempFilePath({ x: 0, y: 0, width: num, height: num, destWidth: num, destHeight: num, canvasId: 'shareImg', success: function(res) { that.setData({ prurl: res.tempFilePath }) wx.hideLoading() }, fail: function(res) { wx.hideLoading() } }) [代码] 一,首先要创建一个小程序 至于如何创建小程序,我这里就不在细讲了,我也有写过创建小程序的文章,也有路过相关的学习视频,去翻下我历史文章找找就行。 二,创建好小程序后,我们就开始来布局 布局很简单,只有下面几行代码。 [代码]<!-- 画布大小按需定制 这里我按照背景图的尺寸定的 --> <canvas canvas-id="shareImg"></canvas> <!-- 预览区域 --> <view class='preview'> <image src='{{prurl}}' mode='aspectFit'></image> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="1">生成头像1</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="2">生成头像2</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="3">生成头像3</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="4">生成头像4</button> <button type='primary' bindtap='save'>保存分享图</button> </view> [代码] 实现效果图如下 [图片] 三,使用canvas来画图 其实我们实现微信头像挂红旗,原理很简单,就是把头像放在下面,然后把有红旗的相框盖在头像上面 [图片] 下面就直接把核心代码贴给大家 [代码]let promise1 = new Promise(function(resolve, reject) { wx.getImageInfo({ src: "../../images/xiaoshitou.jpg", success: function(res) { console.log("promise1", res) resolve(res); } }) }); let promise2 = new Promise(function(resolve, reject) { wx.getImageInfo({ src: `../../images/head${index}.png`, success: function(res) { console.log(res) resolve(res); } }) }); Promise.all([ promise1, promise2 ]).then(res => { console.log("Promise.all", res) //主要就是计算好各个图文的位置 let num = 1125; ctx.drawImage('../../'+res[0].path, 0, 0, num, num) ctx.drawImage('../../' + res[1].path, 0, 0, num, num) ctx.stroke() ctx.draw(false, () => { wx.canvasToTempFilePath({ x: 0, y: 0, width: num, height: num, destWidth: num, destHeight: num, canvasId: 'shareImg', success: function(res) { that.setData({ prurl: res.tempFilePath }) wx.hideLoading() }, fail: function(res) { wx.hideLoading() } }) }) }) [代码] 来看下画出来的效果图 [图片] 四,头像加红旗画好以后,我们就要想办法把图片保存到本地了 [图片] 保存图片的代码也很简单。 [代码]save: function() { var that = this wx.saveImageToPhotosAlbum({ filePath: that.data.prurl, success(res) { wx.showModal({ content: '图片已保存到相册,赶紧晒一下吧~', showCancel: false, confirmText: '好哒', confirmColor: '#72B9C3', success: function(res) { if (res.confirm) { console.log('用户点击确定'); } } }) } }) } [代码] 来看下保存后的效果图 [图片] 到这里,我的微信头像就成功的加上了小红旗了。 [图片] 源码我也已经给大家准备好了,有需要的同学在文末留言即可。 [图片] 后面我准备录制一门视频课程出来,来详细教大家实现这个功能,敬请关注。
2019-09-26 - 【开箱即用】分享一个3D环物展示的解决方案
概述 有时候我们需要立体展示一个物体时,可能需要用到以下效果。当然实现的效果可能有很多,这里就为大家介绍一个大神写的方案,希望能帮到大家! 利用小程序开放的接口模拟简单的3D环物功能。只需传入物品序列照片数组即可。 [图片] 截图来自小程序“白海豚保护区” 一、小程序插件 AppID:wx0f253bdf656bfa08 基础库要求:>= 2.4.3 文档链接:https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx0f253bdf656bfa08 图片要求:网络URL链接 [图片] 二、兼容云开发 由于插件已经有段时间没有更新了,笔者在开发时又用到了小程序的云开发储存图片资源。拜原作者开源所赐,为了兼容云开发,我在开源的小程序插件代码中进行了部分修改。 开源代码:https://github.com/hiteochew/DimensionalShow-wxapp-plugin 修改方法: 将原代码中 downloadFile 方法替换为以下代码即可。 [代码]// 文件位置:plugin/api/util.js // 代码位置:第 124 行 function downloadFile(src) { return new Promise((resolve, reject) => { //云储存 wx.cloud.downloadFile({ fileID: src }).then(res => { resolve(res.tempFilePath); }).catch(error => { reject(err); }) }) } [代码] 结语 欢迎社区三连——关注点赞收藏!
2019-10-20 - 【开箱即用】分享几个好看的波浪动画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 - 小程序“订阅消息”终于来了!
全新小程序订阅消息,七天后也能触达用户。 作者丨Suvi & Tsai [图片] 就在刚刚,微信发布了小程序模板消息能力调整的通知。此前的模板消息接口将停止使用,而我们翘首以盼的小程序订阅消息接口将正式上线! 新上线的小程序订阅消息,同时支持一次性和长期性订阅消息,用户可以主动订阅或退订。所有消息由“服务通知”下发,并附带小程序外链入口,支持用户点击消息进入小程序。 订阅消息会带来哪些商业机会?它和小程序模板消息又有什么不同? 01 订阅消息 vs 模板消息 订阅消息,顾名思义,就是小程序可向用户推送消息。但我们所熟知的小程序模板消息,似乎也能做到这一点。那么,这二者,有什么区别? 模板消息,是小程序触达用户的一个重要入口,它基于用户的主动行为被触发。用户在小程序内完成特定交互行为(主要是支付)后,小程序可在后续7天内向用户推送1-3条模板消息。 [图片] ▲模板消息 模板消息内含小程序入口,且能够搭载优惠券、促销活动等营销信息,能够帮助商家大大提高运营能力。但是模板消息也有其局限性。 首先,模板消息虽然是由用户的行为触发,但实际上用户是被动接受者,事先对推送内容并不知情。由于用户事先没有选择权,所以贸然推送的消息很有可能对用户构成骚扰,起到反效果。尽管推送次数有限,不至于形成“轰炸骚扰”,但是依然有可能造成负面的用户体验。 其次,模板消息推送的时间限制过于严格,只有7天。对模板消息推送时效设限,在预防过度营销、优化用户体验等方面,有其合理性。但对于一些服务周期较长的小程序来说,7天的限制使得它们无法提供完整的服务。 例如机票类小程序,如想在用户出行前一天推送一条延误风险提醒,很有可能无法实现——用户从订票到出行,这之间的时间间隔大概率会超过7天。 [图片] ▲订阅消息示例 | 来源:微信公开课 而这两点局限性,新上线的「订阅消息」,或许就能解决。 02 订阅消息能做什么 能够让用户像关注公众号一样,自主按需订阅通知提醒,是「订阅消息」一大亮点。很多时候,“服务”与“骚扰”之间,只隔着一层很薄的“窗户纸”。同一条推送,愿意看的用户,认为这是服务周到的体现;而没兴趣的用户,收到提醒之后只想拒收。 在信息爆炸的当下,正确的营销策略,并非一厢情愿地给用户推送“想让他看的”,而是有选择性的给用户提供内容,给他看“他想看的”。用微信官方的说法,这就是所谓的该收到的用户收到消息,让不该收到的用户不收到。 许多小程序运营者有一个误区,生怕“浪费”任何一个触达用户的机会。例如,有的小程序会在用户首次下单后赠送优惠券,并通过后续的消息推送持续提醒用户用券。这样的策略本无可厚非,但如果将“7天3次”的机会全部用掉,且全都是营销提醒,并不是最明智的做法。 事实上,用户是否愿意回购,第一次提醒后大概率就能见分晓。更多的用户并非“忘记了”,而是根本没有产生消费欲望。反复单一的券过期提醒,并不能有效地刺激消费,还会产生反效果。 [图片] ▲一次性订阅消息示例 | 来源:微信公开课 「订阅消息」上线之后,这种“一厢情愿”的营销方式有望得到改善。商户可以在小程序内对用户进行“订阅引导”,了解用户真正的兴趣点,确保下一次通知推送给用户的消息能够吸引对方回到小程序中,从而真正做到“不浪费”每一次触达用户的机会。 例如,对于商城小程序来说,可以引导用户订阅心仪的商品的“降价通知”;内容类小程序,可以引导用户订阅其感兴趣的话题等;游戏类小程序,也可以引导用户对游戏动态进行订阅。 此外,模板消息的另一个局限性,就是推送的有效期只有7天。对于类似机票、保险等特殊行业的小程序,7天根本不够用:7天内,找不到合理的推送节点;7天之后,已经丧失了推送机会。 「订阅消息」没有时效限制。在经过用户的明确授权之后,商家小程序可以在任意时间给用户推送一条订阅消息,“航班延误提醒”等功能也因此有了用武之地。更宽松的时效限制、辅以合适的推送内容,这使得订阅消息有望在召回“沉睡用户”、提高小程序留存转化方面发挥巨大作用。 03 推送与骚扰的“平衡” 作为一款国民级超级app,微信的每一项新更改都会有着深远的影响。而订阅消息,就像一把双刃剑:运用得当,是商家的营销利器,微信、商户、用户皆大欢喜;但如果被滥用,则会降低用户体验,给微信用户带来骚扰。 所以,在「订阅消息」能力上,微信又一次展现了它的克制。从模板消息,到一次性订阅消息,微信的尝试非常谨慎,其至今都没有开放通知推送的条数限制就是一条例证。 微信官方团队也坦言,如何既赋予开发者灵活丰富的通知营销能力,又能够最大程度地杜绝滥发消息,是他们还在思考和探索的课题。未来,微信也将在这方面进行更多的尝试。 不过,对于小程序运营者们来说,倒不必思考太多。真正有效、有用的消息推送,就是在对的时机、推送对的内容,「订阅消息」显然已经能够满足这两点。
2019-10-14 - 【优化】小程序优化-代码篇
本文主要是从代码方面跟大家分享我自己在开发小程序的一些做法,希望能帮到一些同学。 前言 不知道大家有没有这种体会,刚到公司时,领导要你维护之前别人写的代码,你看着别人写的代码陷入了深深的思考:“这谁写的代码,这么残忍” [图片] 俗话说“不怕自己写代码,就怕改别人的代码”,一言不和就改到你吐血,所以为了别人好,也为了自己好,代码规范,从我做起。 项目目录结构 在开发之前,首先要明确你要做什么,不要一上来就是干,咱们先把项目结构搭好。一般来说,开发工具初始化的项目基本可以满足需求,如果你的项目比较复杂又有一定的结构的话就要考虑分好目录结构了,我的做法如下图: [图片] 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 - 微信小程序获取用户unionId
[图片] unionId 一个微信开放平台下的相同主体的App、公众号、小程序的unionid是相同的,这样就可以锁定是不是同一个用户 微信针对不同的用户在不同的应用下都有唯一的一个openId, 但是要想确定用户是不是同一个用户,就需要靠unionid来区分 同一个微信开放平台下的相同主体的 App、公众号、小程序,如果用户已经关注公众号,或者曾经登录过App或公众号,则用户打开小程序时,开发者可以直接通过 wx.login 获取到该用户UnionID,无须用户再次授权 (解读:用户如果没有登录过app,也没有登录过公众号,也没有关注过公众号的情况下,小程序中通过 wx.login 是获取不到 unionid的) UnionId 机制文档:https://developers.weixin.qq.com/miniprogram/dev/api/unionID.html UnionID获取途径 绑定了开发者帐号的小程序,可以通过下面3种途径获取UnionID。 调用接口wx.getUserInfo,从解密数据中获取UnionID。注意本接口需要用户授权,请开发者妥善处理用户拒绝授权后的情况。 如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号。开发者可以直接通过wx.login获取到该用户UnionID,无须用户再次授权。 如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用。开发者也可以直接通过wx.login获取到该用户UnionID,无须用户再次授权 用到的API wx.login(obj) wx.getUserInfo(obj) 注意:getUserInfo此接口有调整,使用该接口将不再出现授权弹窗,请使用<button open-type=“getUserInfo”></button> 坑: 我们一般都是先获取到微信的 unionid,然后再通过 unionid 去登录自己的网站,就可以关联到用户在自己网站上的 user_id,但是在小程序登录中,有时候可以获取到 unionid,有时候获取不到,在获取不到 unionid 的情况下,用户无法正常登录网站。 原因:同一个微信开放平台下的相同主体的 App、公众号、小程序,如果用户已经关注公众号,或者曾经登录过App或公众号,则用户打开小程序时,开发者可以直接通过 wx.login 获取到该用户UnionID,无须用户再次授权 (解读:用户如果没有登录过app,也没有登录过公众号,也没有关注过公众号的情况下,小程序中通过 wx.login 是获取不到 unionid的) 所有就有两种情况: 一般情况,用户登录过关联的其他公众号 使用 wx.login 获取code,传到后端,code换openid,unionId [代码] //1.login wx.login({ success: function(data) { wx.request({ url: openIdUrl, data: { code: data.code }, success: function(res) { self.globalData.openid = res.data.openid }, fail: function(res) { console.log('拉取用户openid失败,将无法正常使用开放接口等服务', res) } }) }, fail: function(err) { console.log('wx.login 接口调用失败,将无法正常使用开放接口等服务', err) callback(err) } }) [代码] 用户没有用过关联的公众号等 这时候 wx.login 就获取不到 unionId 了。需要使用 wx.getUserInfo 解决思路:通过带登录态的 wx.getUserInfo 获取到用户的加密数据 encryptedData 和加密算法的初始向量iv,然后将 encryptdata、iv 以及 code传给后端,后端再去通过接收到的encryptedData、iv以、code 以及之前的 session_key 解密出用户的 openid、unionid 等 [代码]wx.getUserInfo({ withCredentials:false, success:(obj)=>{ wx.request({ url: openIdUrl, data: { code: data.code, encryptedData : obj.encryptedData, iv : obj.iv, }, success: function(res) { self.globalData.openid = res.data.openid }, fail: function(res) { console.log('拉取用户openid失败,将无法正常使用开放接口等服务', res) } }) } }) [代码] 实际项目中,需要将两种情况整合使用 两种方案: 第一种:( 前端判断是否有 unionid )wx.login 向后端上传 code 并且后端返回数据以后,前端判断返回值中是否有 unionid 或者 unionid 是否为 null,null 的情况下去调用带有用户登录态的wx.getUserInfo(),然后再将微信返回的 encryptedData 和 iv 返回给后端,后端解密出相应的信息后再返回给前端; 第二种:( 后端判断是否有 unionid )前端调用 wx.login(), wx.getUserInfo() ,把 code,encryptedData 和 iv 返回给后端,后端在拿到前端 code 之后去请求微信的接口拿 unionid,如果返回的 unionid 为空,再用的 encryptedData、iv以及之前的 session_key 解密出 unionid,后端解密出相应的信息后再返回给前端。 有什么问题可以访问我的群 https://developers.weixin.qq.com/community/develop/article/doc/000ac401b845c83e3b19246865d013
2019-08-29 - 小打卡前端周刊(2019-08-12)
爷爷,你关注的“周”刊更新啦! 前端资讯 React 发布 v16.9 版本,它包含一些新特性,bug 修复以及新添加的废弃特性警告来为未来主版本的发布做好准备。详细信息 在 JavaScript 中使用异步的生成器(async generator function),异步的生成器函数是很特殊的,因为你即可以使用 [代码]await[代码] 也可以使用 [代码]yield[代码] ,但是它的实际用处是什么呢?详细信息 使用 Vue.js 3 来面向未来编程!即将到来的 Vue.js 3 将会提供 基于函数的 API 。那么它解决了什么问题并且它是如何提高代码逻辑性的?详细信息 工具推荐 Vuestic Admin 2.0: 一个设计极佳的 Vue.js 后台模板 Resemble.js: 一个用于图片分析与比较的库,这里是 Demo 每周文章:【翻译】ES2019 有啥新玩意? 本文译自JavaScript: What’s new in ES2019 译者 郭梓梁,首次发布于 MeloGuo Blog,转载请保留以上链接 [图片]作为最受欢迎并且是 web 开发的主要编程语言之一,JavaScript 在不断地进化并且每次迭代它都会带来新鲜的内部变化。让我们看看 ES2019 中那些能快速放入日常使用的新特性: Array.flat() 你现在可以指定深度来递归的拍平嵌套数组。深度默认为 1 ,如果你想全部拍平可以使用 [代码]Infinity[代码]。这个方法不会修改原数组而是会创建一个新的: [代码]const arr1 = [1, 2, [3, 4]]; arr1.flat(); // [1, 2, 3, 4] const arr2 = [1, 2, [3, 4, [5, 6]]]; arr2.flat(2); // [1, 2, 3, 4, 5, 6] const arr3 = [1, 2, [3, 4, [5, 6, [7, 8]]]]; arr3.flat(Infinity); // [1, 2, 3, 4, 5, 6, 7, 8] [代码] 如果你在数组中有一个空的槽使用此方法也会被移除 [代码]const arr4 = [1, 2, , 4, 5]; arr4.flat(); // [1, 2, 4, 5] [代码] Array.flatMap() 这是一个结合了基础的 map 函数然后使用 [代码]Array.flat()[代码] 进行深度为 1 的拍平的方法: [代码]const arr1 = [1, 2, 3]; arr1.map(x => [x * 4]; // [[4], [8], [12]] arr1.flatMap(x => [x * 4]); // [4, 8, 12] [代码] 另一个更有用的例子: [代码]const sentence = ["This is a", "regular", "sentence"]; sentence.map(x => x.split(" ")); // [["This","is","a"],["regular"],["sentence"]] sentence.flatMap(x => x.split(" ")); // ["This","is","a","regular", "sentence"] [代码] String.trimStart() and String.trimEnd() 除了 [代码]String.Trim()[代码] 可以移除字符串两端的空字符串,现在又有了两个方法可以分别移除左、右两端的空白字符串: [代码]const test = " hello "; test.trim(); // "hello"; test.trimStart(); // "hello "; test.trimEnd(); // " hello"; [代码] Object.fromEntries 一个将对象的键-值对转换为数组的新方法。它的效果与已经熟知的函数 [代码]Object.Entries[代码] 正相反,它用于简化将对象转换为数组的操作。在转换完成后,你将会得到一个数组,但是现在你可以返回操作过的数组再变回对象。让我们试着用一个给对象的所有属性的值平方操作的例子: [代码]const obj = { prop1: 2, prop2: 10, prop3: 15 }; let array = Object.entries(obj); // [["prop1", 2], ["prop2", 10], ["prop3", 15]] [代码] 让我们使用简单的 [代码]map[代码] 来求值的平方 [代码]array = array.map(([key, value]) => [key, Math.pow(value, 2)]); // [["prop1", 4], ["prop2", 100], ["prop3", 225]] [代码] 我们转换了对象的值但是我们留下了一个数组。将这个数组传入 [代码]Object.fromEntries[代码] ,转换回成一个对象: [代码]const newObj = Object.fromEntries(array); // {prop1: 4, prop2: 100, prop3: 225} [代码] Optional Catch Binding 全新的提案允许你完全忽略 [代码]catch()[代码] 的参数,在许多情况下你根本不想使用这个参数。 [代码]try { //... } catch (er) { //handle error with parameter er } try { //... } catch { //handle error without parameter } [代码] Symbol.description 你现在可以访问 [代码]Symbol[代码] 的 [代码]description[代码] 属性来获取值来替代使用 [代码]toString()[代码] 方法: [代码]const testSymbol = Symbol("Desc"); testSymbol.description; // "Desc" [代码] Function.toString() 调用函数上的 [代码]toString()[代码] 方法现在会返回函数定义时实际的样子,包括空格和注释。之前的样子: [代码]function /* foo comment */ foo() {} foo.toString(); // "function foo() {}" [代码] 现在是这样: [代码]foo.toString(); // "function /* foo comment */ foo() {}" [代码] JSON.parse() improvements 行分隔符 [代码]{\u2028}[代码] 和段落分隔符 [代码]{\u2029}[代码] 现在能够正确的被解析而不是报 [代码]SyntaxError[代码] 了。
2019-08-12 - 分享扫码打开小程序操作实战
这是我们工作过程中的经验总结,现在分享给大家 说明:生成能打开小程序的二维码分为两种:1. 服务端调用接口生成;2. 前端生成二维码 一、服务端调用接口生成 https://api.weixin.qq.com/wxa/getwxacode?access_token=ACCESS_TOKEN 适用于需要的码数量较少的业务场景 生成小程序码,可接受 path 参数较长。 永久有效,可生成的码数量限制为 100,000 https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN 适用于需要的码数量极多的业务场景。 可接受页面参数较短,生成个数不受限。 永久有效,数量暂无限制 调用分钟频率受限(5000次/分钟) ,如需大量小程序码,预生成 https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=ACCESS_TOKEN 适用于需要的码数量较少的业务场景。 生成二维码,可接受 path 参数较长。 生成个数受限,可生成的码数量限制为 100,000 在小程序中接受参数方式:page onLoad函数中接受 [代码]Page({ onLoad(query) { // 需要使用 decodeURIComponent 才能获取到生成二维码时传入的 scene const scene = decodeURIComponent(query.scene) } }) [代码] 二、前端生成二维码 操作步骤: 1. 在小程序后台(mp.weixin.qq.com)配置链接 https://mp.weixin.qq.com/wxopen/devprofile?action=get_profile&token=1318106189&lang=zh_CN [图片] 2. 点击添加: [图片] 3. 添加完成后点击上线 4. 在JS中用第三方库将拼接好的URL(比如: https://xxxxx/homeindex?orderId=123)生成二维码 小程序获取二维码中携带的参数: app.js的 onshow/onLoad函数中获取参数,options.query.q是生成二维码的完成URL,后截取URL获取相应参数 [代码]if (!!options.query && !!options.query.q) { let url = decodeURIComponent(options.query.q) if (url.indexOf('orderId') > -1) { let index = url.slice(url.indexOf('orderId')) if (index.indexOf('&') > -1) { let orderObj = index.slice(0, index.indexOf('&')) this.globalData.orderId = orderObj.split('=')[1] } else { this.globalData.orderId = index.split('=')[1] } } } [代码]
2019-08-16 - 【周刊-1】三年大厂面试官-面试题精选及答案
前言 在阿里和腾讯工作了6年,当了3年的前端面试官,把期间我和我的同事常问的面试题和答案汇总在我 Github 的 Weekly-FE-Interview 中。希望对大家有所帮助。 如果你在bat面试的时候遇到了什么不懂的问题,欢迎给我提issue,我会把题目汇总并将面试要点和答案写好放在周刊里,大家一起共同进步和成长,助力大家进入自己理想的企业。 项目地址是:https://github.com/airuikun/Weekly-FE-Interview 常见面试题精选 以下是十道大厂一面的时候常见的面试题,如果全部理解并且弄透,在一面或者电话面的时候基本上能中1~2题。小伙伴可以先不急着看答案,先自己尝试着思考一下和自己实现一下,然后再看答案。 第 1 题:http的状态码中,499是什么?如何出现499,如何排查跟解决 解析:第 1 题:http的状态码中,499是什么?如何出现499,如何排查跟解决 第 2 题:讲解一下HTTPS的工作原理 解析:第 2 题:讲解一下HTTPS的工作原理 第 3 题:讲解一下https对称加密和非对称加密。 解析:第 3 题:讲解一下https对称加密和非对称加密 第 4 题:如何遍历一个dom树 解析:第 4 题:如何遍历一个dom树 第 5 题:new操作符都做了什么 解析:第 5 题:new操作符都做了什么 第 6 题:手写代码,简单实现call 解析:第 6 题:手写代码,简单实现call 第 7 题:手写代码,简单实现apply 解析:第 7 题:手写代码,简单实现apply 第 8 题:手写代码,简单实现bind 解析:第 8 题:手写代码,简单实现bind 第 9 题: 简单实现项目代码按需加载,例如import { Button } from ‘antd’,打包的时候只打包button 解析:第 9 题: 简单实现项目代码按需加载,例如import { Button } from ‘antd’,打包的时候只打包button 第 10 题:简单手写实现promise 解析:第 10 题:简单手写实现promise 结语 本人还写了一些前端进阶知识的文章,如果觉得不错可以点个star。 blog项目地址是:https://github.com/airuikun/blog 我是小蝌蚪,腾讯高级前端工程师,跟着我一起每周攻克几个前端技术难点。希望在小伙伴前端进阶的路上有所帮助,助力大家进入自己理想的企业。
2019-04-08 - 【周刊-2】三年大厂面试官-前端面试题(偏难)
前言 在阿里和腾讯工作了6年,当了3年的前端面试官,把阿里和腾讯常问的面试题与答案汇总在我的Github中。希望对大家有所帮助,助力大家进入自己理想的企业。 项目地址是:https://github.com/airuikun/Weekly-FE-Interview 如果你在阿里和腾讯面试的时候遇到了什么不懂的问题,欢迎给我提issue,我会把答案和考点都列出来,公布在下一期的面试周刊里。 面试题精选 大家如果去阿里和腾讯面试过,就会发现,在网上刷了很多的前端面试题,但是去大厂面试的时候还是一头雾水,那是因为那些在网上一搜就能搜出来的题,大厂的面试官基本看不上,他们都会问一些开放题,在回答开放题的过程中,就能摸清你知识技能的广度和深度,所以本期会加入几道我在面试候选人常用的开放题,供大家学习和思考。 我把下面每道题的难度高低,和对标了阿里和腾讯的多少职级,都写上去了,大家可以参考一下自己是什么职级。 第 1 题:如何劫持https的请求,提供思路 难度:阿里p6+ ~ p7、腾讯t23 ~ t31 很多人在google上搜索“前端面试 + https详解”,把答案倒背如流,但是问到如何劫持https请求的时候就一脸懵逼,是因为还是停留在https理论性阶段。 想告诉大家的是,就算是https,也不是绝对的安全,以下提供一个本地劫持https请求的简单思路。 模拟中间人攻击,以百度为例 先用OpenSSL查看下证书,直接调用openssl库识别目标服务器支持的SSL/TLS cipher suite [代码] openssl s_client -connect www.baidu.com:443 [代码] 用sslcan识别ssl配置错误,过期协议,过时cipher suite和hash算法 [代码] sslscan -tlsall www.baidu.com:443 [代码] 分析证书详细数据 [代码] sslscan -show-certificate --no-ciphersuites www.baidu.com:443 [代码] 生成一个证书 [代码] openssl req -new -x509 -days 1096 -key ca.key -out ca.crt [代码] 开启路由功能 [代码] sysctl -w net.ipv4.ip_forward=1 [代码] 写转发规则,将80、443端口进行转发给8080和8443端口 [代码] iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080 iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8443 [代码] 最后使用arpspoof进行arp欺骗 如果你有更好的想法或疑问,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/11 第 2 题:前端如何进行seo优化 难度:阿里p5、腾讯t21 合理的title、description、keywords:搜索对着三项的权重逐个减小,title值强调重点即可;description把页面内容高度概括,不可过分堆砌关键词;keywords列举出重要关键词。 语义化的HTML代码,符合W3C规范:语义化代码让搜索引擎容易理解网页 重要内容HTML代码放在最前:搜索引擎抓取HTML顺序是从上到下,保证重要内容一定会被抓取 重要内容不要用js输出:爬虫不会执行js获取内容 少用iframe:搜索引擎不会抓取iframe中的内容 非装饰性图片必须加alt 提高网站速度:网站速度是搜索引擎排序的一个重要指标 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/12 第 3 题:前后端分离的项目如何seo 难度:阿里p6 ~ p6+、腾讯t22 ~ t23 使用prerender。但是回答prerender,面试官肯定会问你,如果不用prerender,让你直接去实现,好的,请看下面的第二个答案。 先去 https://www.baidu.com/robots.txt 找出常见的爬虫,然后在nginx上判断来访问页面用户的User-Agent是否是爬虫,如果是爬虫,就用nginx方向代理到我们自己用nodejs + puppeteer实现的爬虫服务器上,然后用你的爬虫服务器爬自己的前后端分离的前端项目页面,增加扒页面的接收延时,保证异步渲染的接口数据返回,最后得到了页面的数据,返还给来访问的爬虫即可。 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/13 第 4 题:简单实现async/await中的async函数 难度:阿里p6 ~ p6+、腾讯t22 ~ t23 async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里 [代码]function spawn(genF) { return new Promise(function(resolve, reject) { const gen = genF(); function step(nextF) { let next; try { next = nextF(); } catch (e) { return reject(e); } if (next.done) { return resolve(next.value); } Promise.resolve(next.value).then( function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); } ); } step(function() { return gen.next(undefined); }); }); } [代码] 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/14 第 5 题:1000-div问题 难度:阿里p5 ~ p6、腾讯t21 ~ t22 一次性插入1000个div,如何优化插入的性能 使用Fragment [代码] var fragment = document.createDocumentFragment(); fragment.appendChild(elem); [代码] 向1000个并排的div元素中,插入一个平级的div元素,如何优化插入的性能 先display:none 然后插入 再display:block 赋予key,然后使用virtual-dom,先render,然后diff,最后patch 脱离文档流,用GPU去渲染,开启硬件加速 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/15 第 6 题:(开放题)2万小球问题:在浏览器端,用js存储2万个小球的信息,包含小球的大小,位置,颜色等,如何做到对这2万条小球信息进行最优检索和存储 难度:阿里p7、腾讯t31 你面试阿里和腾讯,能否上p7和t31,就看你对开放题能答有多深和多广。 这题目考察你如何在浏览器端中进行大数据的存储优化和检索优化。 如果你仅仅只是答用数组对象存储了2万个小球信息,然后用for循环去遍历进行索引,那是远远不够的。 这题要往深一点走,用特殊的数据结构和算法进行存储和索引。 然后进行存储和速度的一个权衡和对比,最终给出你认为的最优解。 我提供几个能触及阿里p7和腾讯t31级别的思路: 用ArrayBuffer实现极致存储 哈夫曼编码 + 字典查询树实现更优索引 用bit-map实现大数据筛查 用hash索引实现简单快捷的检索 用IndexedDB实现动态存储扩充浏览器端虚拟容量 用iframe的漏洞实现浏览器端localStorage无限存储,实现2千万小球信息存储 这种开放题答案不唯一,也不会要你现场手敲代码去实现,但是思路一定要行得通,并且是能打动面试官的思路,如果大家有更好的idea,欢迎大家到我的github里补充:https://github.com/airuikun/Weekly-FE-Interview/issues/16 第 7 题:(开放题)接上一题如何尽可能流畅的实现这2万小球在浏览器中,以直线运动的动效显示出来 难度:阿里p6+ ~ p7、腾讯t23 ~ t31 这题考察对大数据的动画显示优化,当然方法有很多种。 但是你有没有用到浏览器的高级api? 你还有没有用到浏览器的专门针对动画的引擎? 或者你对3D的实践和优化,都可以给面试官展示出来。 提供几个思路: 使用GPU硬件加速 使用webGL 使用assembly辅助计算,然后在浏览器端控制动画帧频 用web worker实现javascript多线程,分块处理小球 用单链表树算法和携程机制,实现任务动态分割和任务暂停、恢复、回滚,动态渲染和处理小球 如果大家有更好的idea,欢迎大家到我的github里补充:https://github.com/airuikun/Weekly-FE-Interview/issues/17 第 8 题:(开放题)100亿排序问题:内存不足,一次只允许你装载和操作1亿条数据,如何对100亿条数据进行排序。 难度:阿里p6+ ~ p7、腾讯t23 ~ t31 这题是考察算法和实际问题结合的一个问题 众所周知,腾讯玩的是社交,用户量极大。很多场景的数据量都是百亿甚至千亿级别。 那么如何对这些数据进行高效的操作呢,可以通过这题考察出来。 以前老听说很多人问,前端学算法没有用,考算法都是垃圾,面不出候选人的能力 其实。。。老哥实话告诉你,当你在做前端需要用到crc32、并查集、字典树、哈夫曼编码、LZ77之类东西的时候 已经是涉及到框架实现和极致优化层面了 那时你就已经到了另外一个前端高阶境界了 所以不要抵触算法,可能只是我们目前的眼界和能力,还没触及到那个层级 我前面已经公布了两道开放题的答案了,相信大家已经有所参悟。我觉得在思考开放题的过程中,会有很多意想不到的成长,所以我建议这道题大家可以尝试自己思考一下。本题答案会在周五公布到我的github上。 对应的github地址为:https://github.com/airuikun/Weekly-FE-Interview/issues/18 第 9 题:(开放题)a.b.c.d和a[‘b’][‘c’][‘d’],哪个性能更高 难度:阿里p7 ~ p7+、腾讯t31 ~ t32 别看这题,题目上每个字都能看懂,但是里面涉及到的知识,暗藏杀鸡 这题要往深处走,会涉及ast抽象语法树、编译原理、v8内核对原生js实现问题 直接对标阿里p7 ~ p7+和腾讯t31 ~ t32职级,我觉得这个题是这篇文章里最难的一道题,所以我放在了开放题中的最后一题 大家多多思考,本题答案会在周五公布到我的github上 对应的github地址为:https://github.com/airuikun/Weekly-FE-Interview/issues/19 第 10 题:git时光机问题 难度:阿里p5 ~ p6+、腾讯t21 ~ t23 现在大厂,已经全部都是用git了,基本没人使用svn了 很多面试候选人对git只会commit、pull、push 但是有没有使用过reflog、cherry-pick等等,这些都很能体现出来你对代码管理的灵活程度和代码质量管理。 针对git时光机经典问题,我专门写了一个文章,轻松搞笑通俗易懂,大家可以看一下,放松放松,同时也能学到对git的时光机操作《git时光机》 结语 本人还写了一些前端进阶知识的文章,如果觉得不错可以点个star。 blog项目地址是:https://github.com/airuikun/blog 我是小蝌蚪,腾讯高级前端工程师,跟着我一起每周攻克几个前端技术难点。希望在小伙伴前端进阶的路上有所帮助,助力大家进入自己理想的企业。 交流 欢迎关注我的微信公众号,微信扫下面二维码或搜索“前端屌丝”,讲述了一个前端屌丝逆袭的心路历程,共勉。 [图片]
2019-04-17 - 如何实现小程序的强制更新
大家都知道小程序提交审核发布以后是不会马上更新版本的,用户需要下次使用才会更新到新的版本,这就是冷更新。 那么如果要做到及时生效怎么办呢?这时候就要做处理了,将下面的代码添加到app.js,提交审核,发布就会生效了 [代码]onLaunch: [代码][代码]function[代码] [代码](options) {[代码] [代码] [代码][代码]this[代码][代码].autoUpdate()[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]autoUpdate: [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码] [代码] [代码][代码]// 获取小程序更新机制兼容[代码] [代码] [代码][代码]if[代码] [代码](wx.canIUse([代码][代码]'getUpdateManager'[代码][代码])) {[代码] [代码] [代码][代码]const updateManager = wx.getUpdateManager()[代码] [代码] [代码][代码]//1. 检查小程序是否有新版本发布[代码] [代码] [代码][代码]updateManager.onCheckForUpdate([代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]// 请求完新版本信息的回调[代码] [代码] [代码][代码]if[代码] [代码](res.hasUpdate) {[代码] [代码] [代码][代码]//检测到新版本,需要更新,给出提示[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'更新提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'检测到新版本,是否下载新版本并重启小程序?'[代码][代码],[代码] [代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]if[代码] [代码](res.confirm) {[代码] [代码] [代码][代码]//2. 用户确定下载更新小程序,小程序下载及更新静默进行[代码] [代码] [代码][代码]self.downLoadAndUpdate(updateManager)[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码](res.cancel) {[代码] [代码] [代码][代码]//用户点击取消按钮的处理,如果需要强制更新,则给出二次弹窗,如果不需要,则这里的代码都可以删掉了[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'温馨提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'本次版本更新涉及到新的功能添加,旧版本可能无法正常访问哦'[代码][代码],[代码] [代码] [代码][代码]showCancel: [代码][代码]false[代码][代码],[代码][代码]//隐藏取消按钮[代码] [代码] [代码][代码]confirmText: [代码][代码]"确定更新"[代码][代码],[代码][代码]//只保留确定更新按钮[代码] [代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]if[代码] [代码](res.confirm) {[代码] [代码] [代码][代码]//下载新版本,并重新应用[代码] [代码] [代码][代码]self.downLoadAndUpdate(updateManager)[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码] [代码] [代码][代码]// 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 下载小程序新版本并重启应用[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]downLoadAndUpdate: [代码][代码]function[代码] [代码](updateManager) {[代码] [代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码] [代码] [代码][代码]wx.showLoading();[代码] [代码] [代码][代码]//静默下载更新小程序新版本[代码] [代码] [代码][代码]updateManager.onUpdateReady([代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]wx.hideLoading()[代码] [代码] [代码][代码]//新的版本已经下载好,调用 applyUpdate 应用新版本并重启[代码] [代码] [代码][代码]updateManager.applyUpdate()[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]updateManager.onUpdateFailed([代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]// 新的版本下载失败[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'已经有新版本了哟'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'新版本已经上线啦,请您删除当前小程序,重新搜索打开哟'[代码][代码],[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码]
2019-06-07 - 微信小程序之swiper轮播图片高度自适应
微信小程序中使用swiper组件可以实现图片轮播效果,但是默认swiper高度是固定的150px,如果项目中图片大于固定高度就会被隐藏,所以本篇文章要实现轮播图片的高度自适应。 效果图: [图片] wxml代码: [代码]<[代码][代码]swiper[代码] [代码]class[代码][代码]=[代码][代码]"t-swiper"[代码] [代码]indicator-dots[代码][代码]=[代码][代码]"{{indicatordots}}"[代码] [代码]indicator-active-color[代码][代码]=[代码][代码]"{{color}}"[代码] [代码]autoplay[代码][代码]=[代码][代码]"{{autoplay}}"[代码] [代码]interval[代码][代码]=[代码][代码]"{{interval}}"[代码] [代码]duration[代码][代码]=[代码][代码]"{{duration}}"[代码] [代码]style[代码][代码]=[代码][代码]"height:{{height}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{img}}"[代码] [代码]wx:key[代码][代码]=[代码][代码]""[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]swiper-item[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]image[代码] [代码]src[代码][代码]=[代码][代码]"{{item}}"[代码] [代码]mode[代码][代码]=[代码][代码]"widthFix"[代码] [代码]bindload[代码][代码]=[代码][代码]'goheight'[代码] [代码]/>[代码][代码] [代码][代码]</[代码][代码]swiper-item[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]block[代码][代码]>[代码][代码]</[代码][代码]swiper[代码][代码]>[代码]wxss代码: [代码].t-swiper image {[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]100%[代码][代码];[代码][代码]}[代码]js代码: [代码]Page({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]img: [[代码][代码] [代码][代码]'img/1.jpg'[代码][代码],[代码][代码] [代码][代码]'img/2.jpg'[代码][代码],[代码][代码] [代码][代码]'img/3.jpg'[代码][代码] [代码][代码]],[代码][代码] [代码][代码]indicatordots: [代码][代码]true[代码][代码],[代码][代码] [代码][代码]//是否显示面板指示点[代码][代码] [代码][代码]autoplay: [代码][代码]true[代码][代码],[代码][代码] [代码][代码]//是否自动切换[代码][代码] [代码][代码]interval: 5000,[代码][代码] [代码][代码]//自动切换时间间隔[代码][代码] [代码][代码]duration: 500,[代码][代码] [代码][代码]//滑动动画时长[代码][代码] [代码][代码]color: [代码][代码]'#ffffff'[代码][代码],[代码][代码] [代码][代码]//当前选中的指示点颜色[代码][代码] [代码][代码]height: [代码][代码]''[代码][代码] [代码][代码]//swiper高度[代码][代码] [代码][代码]},[代码][代码] [代码][代码]goheight: [代码][代码]function[代码] [代码](e) {[代码][代码] [代码][代码]var[代码] [代码]width = wx.getSystemInfoSync().windowWidth[代码][代码] [代码][代码]//获取可使用窗口宽度[代码][代码] [代码][代码]var[代码] [代码]imgheight = e.detail.height[代码][代码] [代码][代码]//获取图片实际高度[代码][代码] [代码][代码]var[代码] [代码]imgwidth = e.detail.width[代码][代码] [代码][代码]//获取图片实际宽度[代码][代码] [代码][代码]var[代码] [代码]height = width * imgheight / imgwidth + [代码][代码]"px"[代码][代码] [代码][代码]//计算等比swiper高度[代码][代码] [代码][代码]this[代码][代码].setData({[代码][代码] [代码][代码]height: height[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码]})[代码]
2019-07-14 - 基于 wxss 的自适应 canvas 海报生成组件
自适应 canvas 海报生成 wxss 盒子模型
2018-11-06 - image-cropper 💯小程序图片裁剪组件
先看Demo[图片]
2019-04-15 - setData 学问多
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ vm, newData, }) => { newState = { ...newState, ...newData, }; clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, }); newState = null }, 0); }; [代码] 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) }) [代码] 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); [代码] 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList }); [代码] 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList }) [代码] 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) }); [代码] 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList }); [代码] 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() } module.exports = { reverseArr: reverseArr } </wxs> <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 问题解决! 参考资料 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
2019-04-11 - Web直播,你需要先知道这些
转自IMWeb社区,原文链接 Web直播,你需要先知道这些 直播知识小科普 一个典型的直播流程:录制->编码->网络传输(推流->服务器处理->CDN分发)->解码->播放 IPB:一种常用的视频压缩方案,用I帧表示关键帧,B帧表示前向差别帧,P帧表示双向差别帧 GOP (Group of Pictures):GOP 越长(I帧之间的间隔越大),B 帧所占比例越高,编码的率失真性能越高。虽然B帧压缩率高,但解码时CPU压力会更大。 音视频直播质量好坏的主要指标:内容延时、卡顿(流畅度)、首帧时长 音视频直播需要克服的主要问题:网络环境、多人连麦、主辅路、浏览器兼容性、CDN支持等 MSE(Media Source Extensions):W3C 标准API,解决 HTML5 的流问题(HTML5 原生仅支持播放 mp4/webm 非流格式,不支持 FLV),允许JavaScript动态构建 [代码]<video>[代码] 和 [代码]<audio>[代码] 的媒体流。可以用MediaSource.isTypeSupported() 判断是否支持某种MINE类型。在ios Safari中不支持。 [图片] 文件格式/封装格式/容器格式:一种承载视频的格式,比如flv、avi、mpg、vob、mov、mp4等。而视频是用什么方式进行编解码的,则与Codec相关。举个栗子,MP4格式根据编解码的不同,又分为nMP4、fMP4。nMP4是由嵌套的Boxes 组成,fMP4格式则是由一系列的片段组成,因此只有后者不需要加载整个文件进行播放。 Codec:多媒体数字信号编码解码器,能够对音视频进行压缩(CO)与解压缩( DEC ) 。CODEC技术能有效减少数字存储占用的空间,在计算机系统中,使用硬件完成CODEC可以节省CPU的资源,提高系统的运行效率。 常用视频编码:MPEG、H264、RealVideo、WMV、QuickTime。。。 常用音频编码:PCM、WAV、OGG、APE、AAC、MP3、Vorbis、Opus。。。 现有方案比较 RTMP协议 基于TCP adobe垄断,国内支持度高 浏览器端依赖Flash进行播放 2~5秒的延迟 RTP协议 Real-time Transport Protocol,IETF于1996提出的一个标准 基于UDP 实时性强 用于视频监控、视频会议、IP电话 CDN厂商、浏览器不支持 HLS 协议 Http Live Streaming,苹果提出的基于HTTP的流媒体传输协议 HTML5直接支持(video),适合APP直播,PC断只有Safari、Edge支持 必须是H264+AAC编码 因为传输的是切割后的音视频片段,导致内容延时较大 [图片] flv.js Bilibli开源,解析flv数据,通过MSE封装成fMP4喂给video标签 编码为H264+AAC 使用HTTP的流式IO(fetch或stream)或WebSocket协议流式的传输媒体内容 2~5秒的延迟,首帧比RTMP更快 WebRTC协议 [图片] 1、Google力推,已成为W3C标准 2、现代浏览器支持趋势,X5也支持(微信、QQ) [图片] 3、基于UDP,低延迟,弱网抗性强,比flv.js更有优势 方案 CPU占用 帧率 码率 延时 首帧 flv.js 0.4 30 700kbit/s 1.5s 2s WebRTC 1.9 30 700kbit/s 0.7s 1.5s 4、支持Web上行能力 5、编码为H264+OPUS 6、提供NAT穿透技术(ICE) **实际情况下,当用户数量很大时,对推流设备的性能要求很高,复杂的权限管理也难以实现,采用P2P的架构基本不可行。对于个别用户提供上行流、海量用户只进行拉流的场景,腾讯课堂实现了一种P2S的解决方案。**进一步学习可阅读jaychen的系列文章《WebRTC直播技术》。 [图片] 小程序+直播 技术方案 基于RTMP,官方说底层使用HTTP/2的一种内部传输机制,但又说是基于UDP的,这就搞不懂了。。。 live-pusher 和 live-player 没有限制第三方云服务 可直接使用腾讯云视频直播能力,只需配置好推流url、播放url即可 推流url: [图片] 播放url: [图片] 下面是我根据官网教程搭建的一个音视频小程序,搭建过程简单,同一个局域网下直播体验也很流畅(读者也可直接搜索腾讯视频云小程序进行体验): [图片] 前端核心代码还是相当简洁的: live-pusher组件:设置好url推流地址(仅支持 flv, rtmp 格式)等参数即可,使用bindstatechange获取播放状态变化 [图片] live-player组件:设置后src音视频地址(仅支持 flv, rtmp 格式)等参数即可,使用bindstatechange获取播放状态变化 [图片] 能否和WebRTC同时使用? 对于腾讯课堂的应用场景,老师上课推流采用的是RTMP协议,考虑到WebRTC目前只能用于PC端拉流,那么在移动端能否让用户可以直接通过小程序来观看直播课呢?我觉得在技术层面可行的,接入小程序直播对于扩大平台影响力、社交圈分享、提高收费转化都会产生很大的帮助。难点在于复杂的权限控制、多路音视频流、多人连麦等问题,比如权限控制只能单独放到房间控制逻辑中完成,而音视频流本身缺乏这种校验;主辅路的切换还需要添加单独的信令控制,同时在小程序中加入相应的判断逻辑。 补充:最近看到已经有小程序的webrtc方案了,基于live-player、live-pusher组件,加入腾讯云强大的音视频后台服务,官方提供了一套封装度更高的自定义组件方案 —— <webrtc-room> ,甚至可以和Chrome打通。详情可以参考WebRTC 互通、webrtc-room [图片] 参考文章 HTTP 协议入门 使用flv.js做直播 面向未来的直播技术-WebRTC【视频、PPT】 小程序音视频能力技术负责人解读“小程序直播” 小程序开发简易教程 小程序音视频解读
2019-03-26 - 路由的封装
小程序提供了路由功能来实现页面跳转,但是在使用的过程中我们还是发现有些不方便的地方,通过封装,我们可以实现诸如路由管理、简化api等功能。 页面的跳转存在哪些问题呢? 与接口的调用一样面临url的管理问题; 传递参数的方式不太友好,只能拼装url; 参数类型单一,只支持string。 alias 第一个问题很好解决,我们做一个集中管理,比如新建一个[代码]router/routes.js[代码]文件来实现alias: [代码]// routes.js module.exports = { // 主页 home: '/pages/index/index', // 个人中心 uc: '/pages/user_center/index', }; [代码] 然后使用的时候变成这样: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { wx.navigateTo({ url: routes.uc, }); }, }); [代码] query 第二个问题,我们先来看个例子,假如我们跳转[代码]pages/user_center/index[代码]页面的同时还要传[代码]userId[代码]过去,正常情况下是这么来操作的: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { const userId = '123456'; wx.navigateTo({ url: `${routes.uc}?userId=${userId}`, }); }, }); [代码] 这样确实不好看,我能不能把参数部分单独拿出来,不用拼接到url上呢? 可以,我们试着实现一个[代码]navigateTo[代码]函数: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, query }) { const queryStr = Object.keys(query).map(k => `${k}=${query[k]}`).join('&'); wx.navigateTo({ url: `${url}?${queryStr}`, }); } Page({ onReady() { const userId = '123456'; navigateTo({ url: routes.uc, query: { userId, }, }); }, }); [代码] 嗯,这样貌似舒服一点。 参数保真 第三个问题的情况是,当我们传递的参数argument不是[代码]string[代码],而是[代码]number[代码]或者[代码]boolean[代码]时,也只能在下个页面得到一个[代码]string[代码]值: [代码]// pages/index/index.js Page({ onReady() { navigateTo({ url: routes.uc, query: { isActive: true, }, }); }, }); // pages/user_center/index.js Page({ onLoad(options) { console.log(options.isActive); // => "true" console.log(typeof options.isActive); // => "string" console.log(options.isActive === true); // => false }, }); [代码] 上面这种情况想必很多人都遇到过,而且感到很抓狂,本来就想传递一个boolean,结果不管传什么都会变成string。 有什么办法可以让数据变成字符串之后,还能还原成原来的类型? 好熟悉,这不就是json吗?我们把要传的数据转成json字符串([代码]JSON.stringify[代码]),然后在下个页面把它转回json数据([代码]JSON.parse[代码])不就好了嘛! 我们试着修改原来的[代码]navigateTo[代码]: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, data }) { const dataStr = JSON.stringify(data); wx.navigateTo({ url: `${url}?jsonStr=${dataStr}`, }); } Page({ onReady() { navigateTo({ url: routes.uc, data: { isActive: true, }, }); }, }); [代码] 这样我们在页面中接受json字符串并转换它: [代码]// pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(options.jsonStr); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这里其实隐藏了一个问题,那就是url的转义,假如json字符串中包含了类似[代码]?[代码]、[代码]&[代码]之类的符号,可能导致我们参数解析出错,所以我们要把json字符串encode一下: [代码]function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } // pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(decodeURIComponent(options.encodedData)); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这样使用起来不方便,我们封装一下,新建文件[代码]router/index.js[代码]: [代码]const routes = require('./routes.js'); function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { routes, navigateTo, extract, }; [代码] 页面中我们这样来使用: [代码]const router = require('../../router/index.js'); // page home Page({ onLoad(options) { router.navigateTo({ url: router.routes.uc, data: { isActive: true, }, }); }, }); // page uc Page({ onLoad(options) { const json = router.extract(options); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] route name 这样貌似还不错,但是[代码]router.navigateTo[代码]不太好记,[代码]router.routes.uc[代码]有点冗长,我们考虑把[代码]navigateTo[代码]换成简单的[代码]push[代码],至于路由,我们可以使用[代码]name[代码]的方式来替换原来[代码]url[代码]参数: [代码]const routes = require('./routes.js'); function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const url = routes[name]; wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { push, extract, }; [代码] 在页面中使用: [代码]const router = require('../../router/index.js'); Page({ onLoad(options) { router.push({ name: 'uc', data: { isActive: true, }, }); }, }); [代码] navigateTo or switchTab 页面跳转除了navigateTo之外还有switchTab,我们是不是可以把这个差异抹掉?答案是肯定的,如果我们在配置routes的时候就已经指定是普通页面还是tab页面,那么程序完全可以切换到对应的跳转方式。 我们修改一下[代码]router/routes.js[代码],假设home是一个tab页面: [代码]module.exports = { // 主页 home: { type: 'tab', path: '/pages/index/index', }, uc: { path: '/pages/a/index', }, }; [代码] 然后修改[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; if (route.type === 'tab') { wx.switchTab({ url: `${route.path}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${route.path}?encodedData=${dataStr}`, }); } [代码] 搞定,这样我们一个[代码]router.push[代码]就能自动切换两种跳转方式了,而且之后一旦页面类型有变动,我们也只需要修改[代码]route[代码]的定义就可以了。 直接寻址 alias用着很不错,但是有一点挺麻烦得就是每新建一个页面都要写一个alias,即使没有别名的需要,我们是不是可以处理一下,如果在alias没命中,那就直接把name转化成url?这也是阔以的。 [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : name; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 在页面中使用: [代码]Page({ onLoad(options) { router.push({ name: 'pages/user_center/a/index', data: { isActive: true, }, }); }, }); [代码] 注意,为了方便维护,我们规定了每个页面都必须存放在一个特定的文件夹,一个文件夹的当前路径下只能存在一个index页面,比如[代码]pages/index[代码]下面会存放[代码]pages/index/index.js[代码]、[代码]pages/index/index.wxml[代码]、[代码]pages/index/index.wxss[代码]、[代码]pages/index/index.json[代码],这时候你就不能继续在这个文件夹根路径存放另外一个页面,而必须是新建一个文件夹来存放,比如[代码]pages/index/pageB/index.js[代码]、[代码]pages/index/pageB/index.wxml[代码]、[代码]pages/index/pageB/index.wxss[代码]、[代码]pages/index/pageB/index.json[代码]。 这样是能实现功能,但是这个name怎么看都跟alias风格差太多,我们试着定义一套转化规则,让直接寻址的name与alias风格统一一些,[代码]pages[代码]和[代码]index[代码]其实我们可以省略掉,[代码]/[代码]我们可以用[代码].[代码]来替换,那么原来的name就变成了[代码]user_center.a[代码]: [代码]Page({ onLoad(options) { router.push({ name: 'user_center.a', data: { isActive: true, }, }); }, }); [代码] 我们再来改进[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : `pages/${name.replace(/\./g, '/')}/index`; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 这样一来,由于支持直接寻址,跳转home和uc还可以写成这样: [代码]router.push({ name: 'index', // => /pages/index/index }); router.push({ name: 'user_center', // => /pages/user_center/index }); [代码] 这样一来,除了一些tab页面以及特定的路由需要写alias之外,我们也不需要新增一个页面就写一条alias这么麻烦了。 其他 除了上面介绍的navigateTo和switchTab外,其实还有[代码]wx.redirectTo[代码]、[代码]wx.navigateBack[代码]以及[代码]wx.reLaunch[代码]等,我们也可以做一层封装,过程雷同,所以我们就不再一个个介绍,这里贴一下最终简化后的api以及原生api的映射关系: [代码]router.push => wx.navigateTo router.replace => wx.redirectTo router.pop => wx.navigateBack router.relaunch => wx.reLaunch [代码] 最终实现已经在发布在github上,感兴趣的朋友可以移步了解:mp-router。
2019-04-26 - 「分享」高性能双列瀑布流极简实现(附示例)❤️
前言 在日常开发过程中,经常会有双列瀑布流场景的需求出现,如商品列表、文章列表等,本文将简单介绍这种情景下如何高效、精准的实现双列瀑布流场景,支持刷新、加载更多等,实现效果如下。 [图片] [图片] 开发思路 瀑布流视图有一种参差的美感,常规列表布局如 flex wrap 等由于存在行高度限制,无法让第二行的 item 对齐上一行最矮处,因此,瀑布流布局时采用双列 scrollview 的 flex 布局。 参差布局的实现,采用代码计算左右两列的高度,然后对左右两列总高度进行比较,新加入的 item 总是排在总高度较小的那列后面。 计算时可以尽可能的缓存高度,例如左右两列高度在每次计算时都缓存起来,有新的 item 加入列表时直接增加左右两列高度即可,不需要重新从头计算。 index.js [代码]const tplWidth = (750 - 24 - 8) / 2; const tplHeight = 595; // plWidth * 1.66 newPhotos.forEach(photo => { const { height, width } = photo let photoHeight = tplWidth if (height > width) { photoHeight = tplHeight photo.display = 'long' } else { photo.display = 'short' } if (leftHeight < rightHeight) { leftList.push(photo) leftHeight += photoHeight } else { rightList.push(photo) rightHeight += photoHeight } }) [代码] index.wxml [代码]<!-- list --> <view class='list'> <!-- left --> <view class='left-list'> <block wx:for="{{leftList}}" wx:key="{{item._id}}"> <cell photo="{{item}}" bindclick='onCellClicked' /> </block> </view> <!-- right --> <view class='right-list'> <block wx:for="{{rightList}}" wx:key="{{item._id}}"> <cell photo="{{item}}" bindclick='onCellClicked' /> </block> </view> </view> [代码] index.css [代码].list { display: flex; flex: 1; position: relative; flex-direction: row; justify-content: space-between; padding-left: 12rpx; padding-right: 12rpx; padding-top: 8rpx; } .left-list { display: flex; position: relative; flex-direction: column; width: 359rpx; } .right-list { display: flex; position: relative; flex-direction: column; width: 359rpx; } [代码]
2019-02-27 - 发送短信验证码后60秒倒计时
微信小程序发送短信验证码后60秒倒计时功能,效果图: [图片] 完整代码 index.wxml [代码]<!--index.wxml-->[代码][代码]<view class=[代码][代码]"container"[代码][代码]>[代码][代码] [代码][代码]<view class=[代码][代码]"section"[代码][代码]>[代码][代码] [代码][代码]<text>手机号码:</text>[代码][代码] [代码][代码]<input placeholder=[代码][代码]"请输入手机号码"[代码] [代码]type=[代码][代码]"number"[代码] [代码]maxlength=[代码][代码]"11"[代码] [代码]bindinput=[代码][代码]"inputPhoneNum"[代码] [代码]auto-focus />[代码][代码] [代码][代码]<text wx:if=[代码][代码]"{{send}}"[代码] [代码]class=[代码][代码]"sendMsg"[代码] [代码]bindtap=[代码][代码]"sendMsg"[代码][代码]>发送</text>[代码][代码] [代码][代码]<text wx:if=[代码][代码]"{{alreadySend}}"[代码] [代码]class=[代码][代码]"sendMsg"[代码] [代码]>{{second+[代码][代码]"s"[代码][代码]}}</text>[代码][代码] [代码][代码]</view>[代码][代码]</view>[代码] index.wxss [代码]/**index.wxss**/[代码][代码].userinfo {[代码][代码] [代码][代码]display[代码][代码]: flex;[代码][代码] [代码][代码]flex-[代码][代码]direction[代码][代码]: column;[代码][代码] [代码][代码]align-items: [代码][代码]center[代码][代码];[代码][代码]}[代码][代码].section {[代码][代码]display[代码][代码]: flex;[代码][代码]margin[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]padding[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]border-bottom[代码][代码]: [代码][代码]1[代码][代码]rpx [代码][代码]solid[代码] [代码]#CFD8DC[代码][代码];[代码][代码]}[代码][代码] [代码] [代码]text {[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]200[代码][代码]rpx;[代码][代码]}[代码][代码] [代码] [代码]button {[代码][代码] [代码][代码]margin[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]}[代码][代码] [代码] [代码].sendMsg {[代码][代码] [代码][代码]font-size[代码][代码]: [代码][代码]12[代码][代码];[代码][代码] [代码][代码]margin-right[代码][代码]: [代码][代码]0[代码][代码];[代码][代码] [代码][代码]padding[代码][代码]: [代码][代码]0[代码][代码];[代码][代码] [代码][代码]height[代码][代码]: inherit;[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]80[代码][代码]rpx;[代码][代码]}[代码]index.js [代码]//index.js[代码][代码]//获取应用实例[代码][代码]const app = getApp()[代码][代码] [代码] [代码]Page({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]send: true,[代码][代码] [代码][代码]alreadySend: false,[代码][代码] [代码][代码]second: [代码][代码]60[代码][代码],[代码][代码] [代码][代码]disabled: true,[代码][代码] [代码][代码]phoneNum: [代码][代码]''[代码][代码] [代码][代码]},[代码][代码] [代码][代码]// 手机号部分[代码][代码] [代码][代码]inputPhoneNum: function (e) {[代码][代码] [代码][代码]let phoneNum = e.detail.value[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]phoneNum: phoneNum[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]sendMsg: function () {[代码][代码] [代码][代码]var phoneNum = this.data.phoneNum;[代码][代码] [代码][代码]if(phoneNum == [代码][代码]''[代码][代码]){[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'请输入手机号码'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码],[代码][代码] [代码][代码]duration: [代码][代码]2000[代码][代码] [代码][代码]})[代码][代码] [代码][代码]return ;[代码][代码] [代码][代码]}[代码][代码] [代码][代码]//此处省略发送短信验证码功能[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]alreadySend: true,[代码][代码] [代码][代码]send: false[代码][代码] [代码][代码]})[代码][代码] [代码][代码]this.timer()[代码][代码] [代码][代码]},[代码][代码] [代码][代码]showSendMsg: function () {[代码][代码] [代码][代码]if (!this.data.alreadySend) {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]send: true[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]hideSendMsg: function () {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]send: false,[代码][代码] [代码][代码]disabled: true,[代码][代码] [代码][代码]buttonType: [代码][代码]'default'[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]timer: function () {[代码][代码] [代码][代码]let promise = new Promise((resolve, reject) => {[代码][代码] [代码][代码]let setTimer = setInterval([代码][代码] [代码][代码]() => {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]second: this.data.second - [代码][代码]1[代码][代码] [代码][代码]})[代码][代码] [代码][代码]if (this.data.second <= [代码][代码]0[代码][代码]) {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]second: [代码][代码]60[代码][代码],[代码][代码] [代码][代码]alreadySend: false,[代码][代码] [代码][代码]send: true[代码][代码] [代码][代码]})[代码][代码] [代码][代码]resolve(setTimer)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码], [代码][代码]1000[代码][代码])[代码][代码] [代码][代码]})[代码][代码] [代码][代码]promise.then((setTimer) => {[代码][代码] [代码][代码]clearInterval(setTimer)[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码]})[代码]完整的短信验证码登录实例参考: https://blog.csdn.net/zuoliangzhu/article/details/81219900
2019-04-17 - 开启websocket服务端口,调试微信websocket接口方法
使用node环境,在vscode 工具中,创建 app.js 文件 ,代码如下,记得安装 npm install websocket 和 npm install http 模块 . 开启 websocket接口服务后,就可以去封装 官方提供的 wx.sendSocketMessage 等接口了。 [代码]const http = require("http"); const WebSocketServer = require("websocket").server; const httpServer = http.createServer((request, response) => { console.log("[" + new Date() + "] Received request for " + request.url); response.writeHead(404); response.end(); }); const wsServer = new WebSocketServer({ httpServer, autoAcceptConnections: true }); wsServer.on("connect", connection => { connection .on("message", message => { if (message.type === "utf8") { console.log(">> message content from client: " + message.utf8Data); connection.sendUTF(message.utf8Data); // 输出内容返回给前端接口调用 } }) .on("close", (reasonCode, description) => { console.log( "[" + new Date() + "] Peer " + connection.remoteAddress + " disconnected." ); }); }); httpServer.listen(8080, () => { console.log("[" + new Date() + "] Serveris listening on port 8080"); }); #小程序页面示例代码,请参考文档 const socketOpen = false const socketMsgQueue = [] wx.connectSocket({ // url: 'test.php', url :“ws://localhost:8080/” }) wx.onSocketOpen(function (res) { socketOpen = true for (let i = 0; i < socketMsgQueue.length; i++) { sendSocketMessage(socketMsgQueue[i]) } socketMsgQueue = [] }) function sendSocketMessage(msg) { if (socketOpen) { wx.sendSocketMessage({ data: msg }) } else { socketMsgQueue.push(msg) } }[代码]
2019-04-18 - Painter 一款轻量级的小程序海报生成组件
生成海报相信大家有的人都做过,但是canvas绘图的坑太多。大家可以试试这个组件。然后附上楼下大哥做的可视化拖拽生成painter代码的工具:链接地址https://developers.weixin.qq.com/community/develop/article/doc/000e222d9bcc305c5739c718d56813
2019-09-27 - 【技巧】利用canvas生成朋友圈分享海报
前言 大家好,上次给大家讲了函数防抖和函数节流 https://developers.weixin.qq.com/community/develop/article/doc/000a645d8b8ba0d8722863ef45bc13 今天给大家分享一下利用canvas生成朋友圈分享海报 由于小程序的限制,我们不能很方便地在微信内直接分享小程序到朋友圈,所以普遍的做法是生成一张带有小程序分享码的分享海报,再将海报保存到手机相册,有两种方法可以生成分享海报,第一种是让后台生成然后返回图片链接,这一种方法比较简单,只需要传后台所需要的参数就行了,今天给大家介绍的是第二种方法,用canvas生成分享海报。 效果 [图片] 主要步骤 把海报样式用标签先写好,方便画图时可以比对 用canvas进行画图,canvas要注意定好宽高 canvas利用wx.canvasToTempFilePath这个api将canvas转化为图片 将转化好的图片链接放入image标签里 再利用wx.saveImageToPhotosAlbum保存图片 坑点 用canvas进行画图的时候要注意画出来的图的大小一定要是你用标签写好那个样式的两倍大小,比如你的海报大小是400600的大小,那你用canvas画的时候大小就要是8001200,宽高可以写在样式里,如果你画出来的图跟你海报图是一样的大小的话生成的图片是会很模糊的,所以才需要放大两倍。 画图的时候要注意尺寸的转化,如果你是用rpx做单位的话,就要对单位进行转化,因为canvas提供的方法都是经px为单位的,所以这一点要注意一下,px转rpx的公式是w/750z2,w是手机屏幕宽度screenWidth,可以通过wx.getSystemInfo获取,z是你需要画图的单位,2就是乘以两倍大小。 图片来源问题,因为canvas不支持网络图片画图,所以你的图片要么是固定的,如果不是固定的,那就要用wx.downloadFile下载后得到一个临时路径才行 小程序码问题,小程序需要后台请求接口后返回一个二进制的图片,因为二进制图片canvas也是不支持的,所以也是要用wx.downloadFile下载后得到一个临时路径,或者可以叫后台直接返回一个小程序码的路径给你 这里保存的时候是有个授权提醒的,如果拒绝的话再次点击就没有反应了,所以这里我做了一个判断是否有授权的,如果没有就弹窗提醒,确认的话会打开设置页面,确认授权后再次返回就行了,这里有个坑注意下,就是之前拒绝后再进入设置页面确认授权返回页面时保存图片会不成功,官方还没解决,我是加了个setTimeOut处理的,详情可以看这里 https://developers.weixin.qq.com/community/develop/doc/000c46600780f0fa68d7eac345a400 代码实现 [图片] 这里图片我先用的是网上的链接,实际项目中是后台返回的数据,这个可以自行处理,这里只是为了演示方便,生成临时路径的方法我这里是分别定义了一个方法,其实可以合成一个方法的,只是生成小程序码时如果要传入参数要注意一下。 绘图方法是drawImg,这里截一部分,详细的可以看代码片段 [图片] 不足 由于在实际项目中返回的图片宽高是不固定的,但是canvas画出来的又需要固定宽高,所以分享图会有图片变形的问题,使用drawImage里的参数也不能解决,如果各位有比较好的方案可以一起讨论一下。 代码片段 https://developers.weixin.qq.com/s/3pcsjDmS7M5Y
2019-02-22 - canvas画图随记
最近画了一张分享图,在此记录一下遇到的问题及解决方法。 画布尺寸自适应 微信小程序尺寸为rpx,会自适应各种机型,但canvas的方法参数默认为px,所以需要对画布上的每一项参数乘以(画布宽度/设备屏幕宽度),将rpx换算成px,达到尺寸自适应的目的,所以将此系数设置为全局变量。代码如下: [代码]var app = getApp(); const device = wx.getSystemInfoSync(); const width = device.windowWidth;//设备屏幕宽度 const xs = width / 375; [代码] 调用: [代码]createCard: function() { var context = wx.createCanvasContext('myCanvas'); context.fillText('内容', 100 * xs , 100 * xs) } [代码] 长文本换行 由于fillText只能画一行,但很多情况下是需要将长文本自动换行展示的,这个时候则需要对文本进行处理。 方法:遍历该文本,计算出每一字宽度之和,当该宽度大于文本最大宽度时绘制当前截取部分,并将绘制高度加上行高,宽度置0,重新计算并绘制下一行。当只剩最后一字时,绘制剩余部分。 缺陷:当文本内有换行符时,绘制会换行,但当前计算宽度不会增加,导致格式混乱。所以需要在计算宽度之和前判断该字符是否为换行符,若果是,则绘制当前部分,开始下一行的计算。 完善:如果需要知道绘制文本的总高度,设置初始文本高度为0,在绘制一行时加上行高则可。代码如下: [代码] /** * context:当前画布对象 * text:文本内容 * leftWidth:文本左上角x坐标 * initHeight:文本左上角y坐标 * canvasWidth:一行文本最大宽度 */ drawText: function(context, text, leftWidth, initHeight, canvasWidth) { var lineWidth = 0; //文本宽度 var textHeight = 0; //文本总高度 var lastSubStrIndex = 0; //每次开始截取的字符串的索引 for (let i = 0; i < text.length; i++) { if (text[i] == "\n") { //如遇换行 context.fillText(text.substring(lastSubStrIndex, i), leftWidth, initHeight, canvasWidth); //绘制截取部分 initHeight += 17.5 * xs; //17.5为字体高度 lineWidth = 0; lastSubStrIndex = i + 1; //截取字符串时跳过换行符 textHeight += 17.5 * xs; } else { lineWidth += context.measureText(text[i]).width; //计算每个字的宽度之和 if (lineWidth > canvasWidth) { context.fillText(text.substring(lastSubStrIndex, i), leftWidth, initHeight, canvasWidth); initHeight += 17.5 * xs; lineWidth = 0; lastSubStrIndex = i; textHeight += 17.5 * xs; } } if (i == text.length - 1) { //绘制剩余部分 context.fillText(text.substring(lastSubStrIndex, i + 1), leftWidth, initHeight, canvasWidth); textHeight += 17.5 * xs; } } return textHeight; }, [代码] 调用: [代码] var text = '新建项目选择小程序项目,选择代码存放的硬盘路径,填入刚刚申请到的小程序的 AppID,给你的项目起一个好听的名字,最后,勾选 "创建 QuickStart 项目" (注意: 你要选择一个空的目录才会有这个选项),点击确定,你就得到了你的第一个小程序了,点击顶部菜单编译就可以在微信开发者工具中预览你的第一个小程序。'; context.setFontSize(15 * xs) that.drawText(context, text, 30 * xs, 100 * xs, 320 * xs) [代码] 高度自适应 如碰到画布高度需要根据内容高度不同而不同,或者某元素与可变化高度的元素固定距离的情况,则需要计算出可变化元素高度,再根据该高度进行计算其他高度。例如: [图片] 微信图标始终距离文本30px,而该文本高度可变,所以图标的左上角y轴坐标=文本y轴坐标+文本高度+下边距,代码如下: [代码]var textHeight = that.drawText(context, text, 30 * xs 100 * xs, 320 * xs) context.drawImage('/images/wx.png', 68 * xs, (100 + 30) * xs + textHeight, 80 * xs, 80 * xs) [代码] 注意:因为计算文本高度的方法里已经乘过系数,所以这里不需要乘。宽度自适应同理。 绘制圆角矩形框 由于没有绘制圆角矩形的方法,所以需要将圆角矩形分开绘制。 方法:将四个圆角当成四分之一圆绘制,然后分别画四条边,坐标如下图所示。 [图片] 代码: [代码] /** * context:当前画布对象 * x:圆角矩形左上角x坐标 * y:圆角矩形左上角y坐标 * w:宽度 * h:高度 * r:border-radius * color:填充颜色 */ roundRect(ctx, x, y, w, h, r, color) { ctx.beginPath() // 左上角 ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5) // 上边框 ctx.moveTo(x + r, y) ctx.lineTo(x + w - r, y) ctx.lineTo(x + w, y + r) // 右上角 ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2) // 右边框 ctx.lineTo(x + w, y + h - r) ctx.lineTo(x + w - r, y + h) // 右下角 ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5) // 下边框 ctx.lineTo(x + r, y + h) ctx.lineTo(x, y + h - r) // 左下角 ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI) // 左边框 ctx.lineTo(x, y + r) ctx.lineTo(x + r, y) //填充颜色 ctx.setFillStyle(color); ctx.fill() ctx.closePath() } [代码] 调用: [代码]that.roundRect(context, 15 * xs, 60 * xs, 350* xs, 200 * xs, 14 * xs, '#ffffff') [代码] 文本加粗 官方文档里有说到font的使用规则与css语法一致,有几个需要注意的地方,否则可能会导致设置无效。 [图片] 调用: [代码] context.font = "normal bold 27px sans-serif"; context.setFontSize(27 * xs) context.fillText('加粗字体', 100 * xs , 145 * xs) [代码] 效果: [图片] 注意:在真机上若没有写第一个normal参数,则不能成功设置。 字体大小可以在下面重新赋值。 如果没有效果可以注意console有没有如下图所示 设置无效的警告,原因很大可能是因为参数写的不对。 [图片] 圆形头像绘制 方法:在画布上剪切一个圆,然后在圆上画头像,最后恢复即可。有一个需要注意的地方,drawImage方法只能绘制本地图片,如果需要绘制网络图片需下载完成之后再画。代码如下: [代码] context.save() context.beginPath() context.arc(77 / 2 * xs + 150 * xs, 77 / 2 * xs + 73 * xs, 77 / 2 * xs, 0, Math.PI * 2, false) context.clip() var headimg = '/images/headimg.jpg' context.drawImage(headimg, 150 * xs, 73 * xs, 77 * xs, 77 * xs) context.restore() context.draw(); [代码] 遇到的问题:当图片为长方形时,强行将图片压缩为正方形会导致头像变形。 解决办法:image组件里,参数mode有一个值为aspectFill,即保持纵横比缩放图片,只保证图片的短边能完全显示出来,我们参考这种思路来截取图片。 [图片] 这里以宽比高长的图为例。如上图所示,圆为头像显示位置,线为中线,矩形框为一张宽大于高的图片。矩形左上角即为画图时的左上角坐标。截部分如图所示,得到图片宽高后,短边固定为头像尺寸,长边根据短边缩放比计算得到。图片宽=原图宽 /(头像高 / 原图高)。左上角的x轴坐标为:中线x坐标 - 图片宽 / 2。代码如下所示: [代码] context.save() context.beginPath() context.arc(77 / 2 * xs + 150 * xs, 77 / 2 * xs + 73 * xs, 77 / 2 * xs, 0, Math.PI * 2, false) context.clip() var headimg = '/images/headimg.jpg'; //头像路径 var headimgHeight = 0; var headimgWidth = 0; wx.getImageInfo({ src: headimg, success(res) { headimgHeight = res.height; //原图高度 headimgWidth = res.width; //原图宽度 //当宽 > 高时 if (headimgWidth > headimgHeight) { var width = headimgWidth / (headimgHeight / (77 * xs)); //图片宽度 var x = (150 + 77 / 2) * xs - width / 2; //x轴坐标 context.drawImage(headimg, x, 73 * xs, width, 77 * xs) } else { //当高>=宽时 var height = headimgHeight / (headimgWidth / (77 * xs)); //图片高度 var h = (73 + 77 / 2) * xs - height / 2; //y轴坐标 context.drawImage(headimg, 150 * xs, h, 77 * xs, height) context.restore() context.draw(); } } }) [代码] 注意:这里得到的图片宽高已经是px为单位,所以不乘系数。
2019-03-18 - CSS 火焰?不在话下
正文从下面开始。 今天的小技巧是使用纯 CSS 生成火焰,逼真一点的火焰。 嗯,长什么样子?在 CodePen 上输入关键字 [代码]CSS Fire[代码],能找到这样的: [图片] 或者这样的: [图片] 我们希望,仅仅使用 CSS ,效果能再更进一步吗?能不能是这样子: [图片] 如何实现 嗯,我们需要使用 [代码]filter[代码] + [代码]mix-blend-mode[代码] 的组合来完成。 很多 CSS 华而不实的效果都是 [代码]filter[代码] + [代码]mix-blend-mode[代码],很有意思,但是业务中根本用不上,当然多了解了解总没坏处。 如上图,整个蜡烛的骨架, 除去火焰的部分很简单,掠过不讲。主要来看看火焰这一块如何生成,并且如何赋予动画效果。 Step 1: filter blur && filter contrast 模糊滤镜叠加对比度滤镜产生的融合效果。 单独将两个滤镜拿出来,它们的作用分别是: [代码]filter: blur()[代码]: 给图像设置高斯模糊效果。 [代码]filter: contrast()[代码]: 调整图像的对比度。 但是,当他们“合体”的时候,产生了奇妙的融合现象。 先来看一个简单的例子: [图片] 仔细看两圆相交的过程,在边与边接触的时候,会产生一种边界融合的效果,通过对比度滤镜把高斯模糊的模糊边缘给干掉,利用高斯模糊实现融合效果。 利用上述 [代码]filter blur & filter contrast[代码],我们要先生成一个类似火焰形状的三角形。(略去过程) 这里类似火焰形状的三角形的具体实现过程,在这篇文章有详细的讲解:你所不知道的 CSS 滤镜技巧与细节 [图片] 父元素添加 [代码]filter: blur(5px) contrast(20)[代码],会变成这样: [图片] Step 2: 火焰粒子动画 看着已经有点样子了,接下来是火焰动画,我们先去掉父元素的 [代码]filter: blur(5px) contrast(20)[代码] ,然后继续 。 这里也是利用了 [代码]filter[代码] 的融合效果,我们在上述火焰中,利用 SASS 随机均匀分布大量大小不一的圆形棕色 div ,隐匿在火焰三角内部,大概是这样: [图片] 接下来,我们再利用 SASS,给中间每个小圆赋予一个从下往上逐渐消失的动画,并且均匀赋予不同的 [代码]animation-delay[代码],看起来会是这样: [图片] OK,最重要的一步,我们再把父元素的 [代码]filter: blur(5px) contrast(20)[代码] 打开,神奇的火焰效果就出来了: [图片] Step 3: mix-blend-mode 润色 当然,上述效果已经很不错了。经过各种尝试,调整参数,最后我发现加上 [代码]mix-blend-mode: screen[代码] 混合模式,效果更好,得到头图上面的最终效果如下: [图片] 完整源码在我的 CodePen 上:CodePen Demo – CSS Fire 另外一些效果 当然,掌握了这种方法后,这种生成火焰的技巧也可以迁移到其他效果去。下图是我鼓捣到另外一个小 Demo,当 hover 到元素的时候,产生火焰效果: [图片] CodePen Demo – Hover Fire 嗯,这些其实都是对滤镜及混合模式的一些搭配运用。按照惯例,肯定有人会留言喷了,整这些花里胡哨的有什么用,性能又不好,业务中敢上不把你的腿给打骨折。 [图片] 于我而言,虚心接受各种批评质疑及各种不同的观点,当然我是觉得搞技术一方面是实用,另一方面是兴趣使然,自娱自乐。希望喷子绕道~ 回到正题,了解了这种黏糊糊湿答答的技巧后,还可以折腾出其他很多有意思的效果,当然可能需要更多的去尝试,如下面使用一个标签实现的滴水效果: [图片] CodePen Demo – 单标签实现滴水效果 值得注意的细节点 动画虽然美好,但是具体使用的过程中,仍然有一些需要注意的地方: CSS 滤镜可以给同个元素同时定义多个,例如 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] ,但是滤镜的先后顺序不同产生的效果也是不一样的; 也就是说,使用 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] 和 [代码]filter: brightness(1.5) contrast(150%) blur(5px)[代码] 处理同一张图片,得到的效果是不一样的,原因在于滤镜的色值处理算法对图片处理的先后顺序。 滤镜动画需要大量的计算,不断的重绘页面,属于非常消耗性能的动画,使用时要注意使用场景。记得开启硬件加速及合理使用分层技术; [代码]blur()[代码] 混合 [代码]contrast()[代码] 滤镜效果,设置不同的颜色会产生不同的效果,这个颜色叠加的具体算法暂时没有找到很具体的规则细则,使用时比较好的方法是多尝试不同颜色,观察取最好的效果; 细心的读者会发现上述效果都是基于黑色底色进行的,动手尝试将底色改为白色,效果会大打折扣。 最后 本文只是简单的介绍了整个思路过程,许多 CSS 代码细节,调试过程没有展现出来。主要几个 CSS 属性默认大家已经掌握了大概,阅读后可以自行去了解补充更多细节: [代码]filter[代码] [代码]mix-blend-mode[代码] 更多精彩 CSS 技术文章汇总在我的 Github – iCSS ,持续更新,欢迎点个 star 订阅收藏。 好了,本文到此结束,希望对你有帮助 😃 如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。 最后,新开通的公众号求关注,形式希望是更短的篇幅,质量更高一些的技巧类文章,包括但不局限于 CSS: [图片]
2019-04-26 - 实现小程序canvas拖拽功能
组件地址 https://github.com/jasondu/wx-comp-canvas-drag 实现效果 [图片] 如何实现 使用canvas 使用movable-view标签 由于movable-view无法实现旋转,所以选择使用canvas 需要解决的问题 如何将多个元素渲染到canvas上 如何知道手指在元素上、如果多个元素重叠如何知道哪个元素在最上层 如何实现拖拽元素 如何缩放、旋转、删除元素 看起来挺简单的嘛,就把上面这几个问题解决了,就可以实现功能了;接下来我们一一解决。 如何将多个元素渲染到canvas上 定义一个DragGraph类,传入元素的各种属性(坐标、尺寸…)实例化后推入一个渲染数组里,然后再循环这个数组调用实例中的渲染方法,这样就可以把多个元素渲染到canvas上了。 如何知道手指在元素上、如果多个元素重叠如何知道哪个元素在最上层 在DragGraph类中定义了判断点击位置的方法,我们在canvas上绑定touchstart事件,将手指的坐标传入上面的方法,我们就可以知道手指是点击到元素本身,还是删除图标或者变换大小的图标上了,这个方法具体怎么判断后面会讲解。 通过循环渲染数组判断是非点击到哪个元素到,如果点击中了多个元素,也就是多个元素重叠,那第一个元素就是最上层的元素啦。 ###如何实现拖拽元素 通过上面我们可以判断手指是否在元素上,当touchstart事件触发时我们记录当前的手指坐标,当touchmove事件触发时,我们也知道这时的坐标,两个坐标取差值,就可以得出元素位移的距离啦,修改这个元素实例的x和y,再重新循环渲染渲染数组就可以实现拖拽的功能。 如何缩放、旋转、删除元素 这一步相对比较难一点,我会通过示意图跟大家讲解。 我们先讲缩放和旋转 [图片] 通过touchstart和touchmove我们可以获得旋转前的旋转后的坐标,图中的线A为元素的中点和旋转前点的连线;线B为元素中点和旋转后点的连线;我们只需要求A和B两条线的夹角就可以知道元素旋转的角度。缩放尺寸为A和B两条线长度之差。 计算旋转角度的代码如下: [代码]const centerX = (this.x + this.w) / 2; // 中点坐标 const centerY = (this.y + this.h) / 2; // 中点坐标 const diffXBefore = px - centerX; // 旋转前坐标 const diffYBefore = py - centerY; // 旋转前坐标 const diffXAfter = x - centerX; // 旋转后坐标 const diffYAfter = y - centerY; // 旋转后坐标 const angleBefore = Math.atan2(diffYBefore, diffXBefore) / Math.PI * 180; const angleAfter = Math.atan2(diffYAfter, diffXAfter) / Math.PI * 180; // 旋转的角度 this.rotate = currentGraph.rotate + angleAfter - angleBefore; [代码] 计算缩放尺寸的代码如下: [代码]// 放大 或 缩小 this.x = currentGraph.x - (x - px); this.y = currentGraph.y - (x - px); [代码]
2019-02-20 - 【微信小游戏】截图功能
废话不多说,开始 1.得到当前界面canvas。因为笔者用laya开发,所以介绍在laya中得到当前界面canvas的方法。 [代码]var canvas = Laya.stage.drawToCanvas(Laya.stage.width, Laya.stage.height, 0, 0).getCanvas(); [代码] 2.调用toTempFilePathSync方法将当前 Canvas 保存为一个临时文件。 [代码]var tempFilePath = canvas.toTempFilePathSync({ x: 0, y: 0, width: 100, height: 100, destWidth: 100, destHeight: 100, fileType: 'jpg', quality: 1.0 }); [代码] 参数详情可通过toTempFilePathSync了解。 3.此时,tempFilePath已经相当于一个图片链接(tips 若需分享,可以tempFilePath直接当做图片链接调用分享接口即可)。通过wx.authorize向用户发起保存图片授权。如果已经授权直接返回成功。 [代码]wx.authorize({ scope: 'scope.writePhotosAlbum',//这里表示相册权限 success: function (res) { //如果有授权过会直接返回成功,我们在这里就可以直接 调用保存图片的接口 Browser.window.wx.saveImageToPhotosAlbum({ filePath: tempFilePath, success: function (res): void { Browser.window.wx.showToast({ title: "截图已保存至相册,快快分享到朋友圈吧", icon: "none", image: "", duration: 1500 }) }, fail: function (): void { } }); }, fail: function () { //没有授权过,我们调用showModal弹出弹窗,告诉用户请求给予我们授权。 var alertParam = { 'title': '微信授权', 'content': '保存到相册需要您的授权,请给予我们授权', 'showCancel': true, 'canelColor': '#666', 'confirmText': '去授权', 'confirmColor': '#666', success: function (ssa) { //打开设置界面,设置界面只会出现小程序已经向用户请求过的权限。我们请求过保存图片的权限,所以里面会有相册的按钮,允许后点返回。则执行下面的success方法。调用保存图片的接口 if (ssa.confirm == true) { wx.openSetting({ success: function () { Browser.window.wx.saveImageToPhotosAlbum({ filePath: tempFilePath, success: function (res): void { Browser.window.wx.showToast({ title: "截图已保存至相册,快快分享到朋友圈吧", icon: "none", image: "", duration: 1500 }) }, fail: function (): void { } }); } }) } } }; //这里调用弹窗。 wx.showModal(alertParam); } }) [代码]
2019-02-25 - 背景图片设置
开发微信小程序时,不能直接在wxss文件里引用本地图片,运行时会报错:“本地资源图片无法通过WXSS获取,可以使用网络图片,或者 base64,或者使用<image/>标签。” 这里主要介绍使用<image>标签的方法 网上有很多方法,笔者也尝试了不少,期间也遇到一些问题。最后总结一下,只需2步: 1、在wxml文件中添加一个<image>标签: [代码]<!--页面根标签--> <view class="content"> <!--pics文件夹下的background.jpg文件--> <image class='background' src="../../pics/background.jpg" mode="aspectFill"></image> <!--页面其它部分--> </view> [代码] 2、在wxss文件中添加: [代码]page{ height:100%; } .background { width: 100%; height: 100%; position:fixed; background-size:100% 100%; z-index: -1; } [代码] 要说明的是z-index: -1,可以让图片置于最底层,不会影响其它部分。
2019-02-21 - 关于小程序接入给赞赞赏功能(给赞小程序已于2020年5月5号暂停小程序服务)
给赞小程序已于2020年5月5号暂停小程序服务,本文章仅起纪念意义 - 关于小程序接入给赞赞赏功能 背景:由于个人小程序没有支付功能,无法让用户直接通过调用支付接口直接打赏,喵咪咪科技专门开发了一款打赏小程序:‘给赞’:它可以每个用户创建一个独立的打赏账户,还可以编辑各种打赏样式风格,废话不多说上图; 1. 觉得有用可以先打赏个啦!没事就算1分钱也是支持!(长按识别) [图片] 2. 首先微信搜索‘给赞’小程序完成授权注册(也可直接扫上面我的赞赏码直接进入后点击左下角:我也要收赞赏 即可),然后再关注‘给赞’公众号:回复‘路径’二字如图,系统会自动返回一个小程序带参跳转地址,将这个地址放在跳转路径里面即可; [图片] [图片] 3.前端页面代码如下图: app_id : wx18a2ac992306a5a4 路径:path=“pages/apps/largess/detail?id=cTq9h%2BdgXUU%3D” 你到时候自己填入自己的就行了,我比较懒就不打码了别填错了; [图片] 4.运行示意图(闪图有些粗糙): [图片] 5.体验程序: [代码]a.此生余时(生命话题)(长按识别) [代码] [图片] [代码] b.心疑(恋爱话题)(长按识别) [代码] [图片] 6. 到此收工,确实觉得有用可以打赏个啦!没事就算1分钱也是支持!(长按识别) [图片]
2020-08-16 - 小程序canvas的那些事
背景 业务场景需要在小程序内生成活动的分享海报,图片中的某些数据需动态展示。可行的方案有️二: 服务端合成:直接返回给前端图片URL 客户端合成:客户端利用canvas绘制 在当前业务场景下,使用客户端合成会优于服务端合成,避免造成不必要的服务器CPU浪费。 下面主要谈谈**客户端(canvas)**合成的过程。 实现思路 小程序端发起请求,获取需动态展示的数据; 利用canvas绘制画布; 导出图片保存到相册。 小技巧&那些坑 理想很丰满,现实很骨感。 实现思路很简单,然而,在实现过程中,发现会趟一些坑,也有一些小技巧,遂记录下来,以供参考。 promise化 画布的绘制依赖系统信息(自适应和优化图片清晰度)和动态数据。故画布需要在所有前置条件都准备完成时,方可绘制。为了提高代码优雅度和维护性,建议用promise化,避免回调地狱(Callback Hell)。 [代码] let promise1 = new Promise((resolve, reject) => { this.getData(resolve, reject) }); let promise2 = new Promise((resolve, reject) => { this.getSystemInfo(resolve, reject) }); Promise.all([promise1, promise2]).then(() => { this.drawCanvas() }).catch(err => { console.log(err) }); [代码] 自适应 1、为了在各个机型下保持大小自适应,需要计算出缩放比: [代码] getSystemInfo(resolve, reject) { try { const res = wx.getSystemInfoSync() //缓存系统信息 systemInfo = res //这里视觉基于iPone6(375*667)设计,2x图视觉,可以填写750,1x图视觉,可以填写375 zoom = res.windowWidth / 750 * 1 resolve() } catch (e) { // Do something when catch error reject("获取机型失败") } } [代码] 2、绘制时进行按缩放比进行缩放,如: [代码]ctx.drawImage(imgUrl, x * zoom, y * zoom, w * zoom, h * zoom) [代码] 绘制网络图片 经测试,绘制CDN图片需要先将图片下载到本地,在进行绘制: [代码]wx.downloadFile({ url: imgUrl, success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, 326 * zoom, 176 * zoom, 14 * zoom, 14 * zoom) } } }) [代码] 绘制base64图片 因为业务上某些原因,依赖的图片数据,后端只能以base64格式返回给前端,而小程序在真机上无法直接绘制(开发工具OK)。 解决思路(存在兼容性问题,fileManager**基础库 1.9.9 **开始支持): 1、调用fileManager.writeFile存储base64到本地; 2、绘制本地图片。 实现代码如下: [代码]// 先获得一个文件实例 fileManager = wx.getFileSystemManager() // 把图片base64格式转存到本地,用于canvas绘制 fileManager.writeFile({ filePath: `${wx.env.USER_DATA_PATH}/qrcode.png`, data: self.data.qrcode, encoding: 'base64', success: () => { //此处需先调用wx.getImageInfo,方可绘制成功 wx.getImageInfo({ src: `${wx.env.USER_DATA_PATH}/qrcode.png`, success: () => { //绘制二维码 ctx.drawImage(`${wx.env.USER_DATA_PATH}/qrcode.png`, 207 * zoom, 313 * zoom, 148 * zoom, 148 * zoom) ctx.draw() } }) } }) [代码] 保存到本地相册 wx.saveImageToPhotosAlbum这个API需用户授权,故开发者需做好拒绝授权的兼容。此处实现对拒绝授权的场景进行引导。 [代码]canvas2Img(e) { wx.getSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum'] === undefined) { //呼起授权界面 wx.authorize({ scope: 'scope.writePhotosAlbum', success() { save() } }) } else if (res.authSetting['scope.writePhotosAlbum'] === false) { //引导拒绝过授权的用户授权 wx.showModal({ title: '温馨提示', content: '需要您授权保存到相册的权限', success: res => { if (res.confirm) { wx.openSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { save() } } }) } } }) } else { save() } } }) function save() { wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg', success: res => { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () => { wx.showModal({ content: '保存成功', showCancel: false, confirmText: '确认' }) }, fail: res => { if (res.errMsg !== 'saveImageToPhotosAlbum:fail cancel') { wx.showModal({ content: '保存到相册失败', showCancel: false }) } } }) }, fail: () => { wx.showModal({ content: '保存到相册失败', showCancel: false }) } }) } [代码] 导出清晰图片 wx.canvasToTempFilePath有destWidth(输出的图片的宽度)和destHeight(输出的图片的高度)属性。若此处以canvas的宽高去填写的话,在高像素手机下,导出的图片会模糊。 原因:destWidth和destHeight单位是物理像素(pixel),canvas绘制的时候用的是逻辑像素(物理像素=逻辑像素 * density),所以这里如果只是使用canvas中的width和height(逻辑像素)作为输出图片的长宽的话,生成的图片width和height实际上是缩放了到canvas的 1 / density大小了,所以就显得比较模糊了。 这里应该乘以设备像素比,实现如下: [代码]wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg' )} [代码] 特殊字体的绘制 研究发现,目前小程序canvas无法支持设置特殊字体,而业务生成的海报,又期望以特殊字体去呈现,最终取了个折中方案——保留数字部分的特殊样式。 实现方式为:把0-9这10个数字单独切图,用ctx.drawImage API,以图片形式去绘制。 [代码]drawNum(num, x, y, w, h) { return new Promise(function (resolve, reject) { //这里存储0-9的图片CDN链接 let numMap = [] wx.downloadFile({ url: numMap[num], success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, x * zoom, y * zoom, w * zoom, h * zoom) resolve() } }, fail: () => { reject() } }) }) } [代码] 安卓机型图片绘制锯齿化问题 测试发现,同样的绘制方案,在安卓下,调用ctx.drawImage方法,图片会出现锯齿问题。测试还发现,原像素越高,锯齿化程度降低(但业务上使用太大像素的素材也不合理),这里需要客户端底层进行优化,目前没有找到合适的解决方案。 总结 个人觉得,目前小程序canvas就底层能力上相比web还有一些不足。所以应注意两点: 提前从业务出发,考虑当前实现的可行性,以便采取更优方案(如特殊字体,像素要求等); 若绘制canvas导出图片是个高频场景,可参考html2canvas进行封装,以便提高效能(SelectorQuery节点查询需1.9.90以上)。 ps:之前有想过利用web-view方式,在传统网页去绘制,然后通过web-view和小程序的通信来实现的方式。时间原因,并未尝试,感兴趣同学可以尝试下。
2019-03-07 - 不可思议的纯 CSS 实现鼠标跟随效果
直接进入正题,鼠标跟随,顾名思义,就是元素会跟随着鼠标的移动而作出相应的运动。大概类似于这样: [图片] 通常而言,CSS 负责表现,JavaScript 负责行为。而鼠标跟随这种效果属于行为,要实现通常都需要借助 JS。 当然,本文的重点,就是介绍如何在不借助 JS 的情况下使用 CSS 来模拟实现一些鼠标跟随的行为动画效果。 原理 以上面的 Demo 为例子,要使用 CSS 实现鼠标跟随,最重要的一点就是: 如何实时监测到当前鼠标处于何处? OK,其实很多 CSS 效果,都离不开 障眼法 二字。要监测到当前鼠标处于何处,我们只需要在页面上铺满元素即可: 我们使用 100 个元素,将整个页面铺满,hover 的时,展示颜色,核心 SCSS 代码如下: [代码]<div class="g-container"> <div class="position"></div> <div class="position"></div> <div class="position"></div> <div class="position"></div> ... // 100个 </div> [代码] [代码].g-container { position: relative; width: 100vw; height: 100vh; } .position { position: absolute; width: 10vw; height: 10vh; } @for $i from 0 through 100 { $x: $i % 10; $y: ($i - $x) / 10; .position:nth-child(#{$i + 1}) { top: #{$y * 10}vh; left: #{$x * 10}vw; } .position:nth-child(#{$i + 1}):hover { background: rgba(255, 155, 10, .5) } } [代码] 可以得到这样的效果: [图片] 好的,如果把每个元素的 hover 效果去掉,那么这个时候操作页面,其实是没有任何效果的。但同时,通过 [代码]:hover[代码] 伪类,我们又是可以大概得知当前鼠标是处于页面上哪个区间的。 好继续,我们再给页面添加一个元素(圆形小球),将它绝对定位到页面中间: [代码]<div class="g-ball"></div> [代码] [代码].ball { position: absolute; top: 50%; left: 50%; width: 10vmax; height: 10vmax; border-radius: 50%; transform: translate(-50%, -50%); } [代码] 最后,我们借助 [代码]~[代码] 兄弟元素选择器,在 hover 页面的时候(其实是 hover 一百个隐藏的 div),通过当前 hover 到的 div,去控制小球元素的位置。 [代码]@for $i from 0 through 100{ $x: $i % 10; $y: ($i - $x) / 10; .position:nth-child(#{$i + 1}):hover ~ .ball { top: #{$y * 10}vh; left: #{$x * 10}vw; } } [代码] 至此,一个简单的纯 CSS 实现鼠标跟随的效果就实现了,方便大家理解,看看下面这张图就明白了: [图片] 完整的DEMO,你可以戳这里看看:CodePen Demo – CSS实现鼠标跟随 存在的问题 就上面的 Demo 来看,还是有很多瑕疵的,譬如 精度太差 只能控制元素运动到 div 所在空间,而不是精确的鼠标所在位置,针对这一点,我们可以通过增加隐藏的 div 的数量来优化。譬如将 100 个平铺 div 增加到 1000 个平铺 div。 运动不够丝滑 效果看起来不够丝滑,这个可能需要通过合理的缓动函数,适当的动画延时来优化。 燥起来吧 嗯。原理掌握了,下面我们来看看,使用这个技巧还能鼓捣出什么有意思的效果。 CSS鼠标跟随按钮效果 一开始,我在 CodePen 上看到了下面这个效果,使用了 [代码]SVG + CSS + JS[代码] 实现,我就想着,仅用 CSS,能不能 copy 一下: [图片] CodePen Demo – Gooey mouse follow 好吧,理想很丰满,现实很骨感。仅仅使用 CSS,还是有诸多限制。 但是我们还是可以使用上述介绍的方法实现鼠标跟随 利用 CSS 滤镜 [代码]filter: blur() contrast()[代码] 模拟元素融合,具体可以看看这篇文章:你所不知道的 CSS 滤镜技巧与细节 好,看看仅仅使用 CSS 的破产版模拟效果: [图片] 有点太太太奇怪了,可以稍微收敛点效果,通过调整颜色,滤镜强度(就是各种尝试…),得到一个稍微好一丢丢丢的类似效果: [图片] Demo 戳我,CodePen Demo – CSS鼠标跟随按钮效果 全屏鼠标跟随动画 OK,继续,下面来点更炫的。嗯,就是那种华而不实的。😅 如果我们控制的不止一个元素,而是多个元素。多个元素之间的动画效果再设定不同的 transition-delay ,顺序延迟运动。哇哦,想想就很激动。譬如这样: [图片] CodePen Demo – 鼠标跟随动画 PURE CSS MAGIC MIX 如果我们能更有想象力一点,那么可以再碰撞出多一点的火花: [图片] 这个效果是我非常喜欢的一位日本 CodePen 作者 Yusuke Nakaya 的作品,源代码: Demo – Only CSS: Water Surface 鼠标跟随指示 当然,不一定要指示元素运动。使用 div 铺满页面捕捉元素当前位置的技巧,还可以运用在其他一些效果上,譬如指示出鼠标运动轨迹: [图片] 默认的铺满背景的 div 的 [代码]transition-duration: 0.5s[代码] 当 hover 到元素背景 div 的时候,改变当前 hover 到的 div 的 [代码]transition-duration: 0s[代码],并且 hover 的时候赋予背景色,这样当前 hover 到的 div 会立即展示 当鼠标离开 div,div 的 [代码]transition-duration[代码] 变回默认状态,也就是 [代码]transition-duration: 0.5s[代码],同时背景色消失,这样被离开的 div 的背景色将慢慢过渡到透明,造成虚影的效果 CodePen Demo – cancle transition 最后 其实还有很多有意思的用法,感兴趣的同学可以自己动手,更多的去尝试,组合。 经常有人会问我,这些奇奇怪怪的用法实际业务中用得上吗?到底有用没用。额,我的看法是也许业务中真的用不上或者应用场景极为有限,但是多了解一些,能在遇到问题的时候多点选择,多一些思考的空间,更好的发散思维,至少是无害吧。 更多你可能想都想不到的有趣的 CSS 你可以来这里瞧瞧: CSS-Inspiration – CSS灵感 更多精彩 CSS 技术文章汇总在我的 Github – iCSS ,持续更新,欢迎点个 star 订阅收藏。 好了,本文到此结束,希望对你有帮助 😃 如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。
2019-03-08 - swiper组件添加左右箭头
前言:小程序官方swiper组件并未提供带左右箭头功能,但有些时候还是想把左右箭头添加上,今天连胜老师就给大家分享一下自己的实现方式。 思路很简单:在swiper组件内部添加两个image组件,绑定点击事件,动态改变swiper中的current值。不废话,主要看代码: WXML: <swiper indicator-dots="{{swiper.indicatorDots}}" autoplay="{{swiper.autoplay}}" interval="{{swiper.interval}}" duration="{{swiper.duration}}" current='{{swiper.current}}' circular="true"> <block wx:for="{{swiper.imgUrls}}"> <swiper-item> <image src="{{item}}" class="slide-image" width="355" height="150"/> </swiper-item> </block> <image class='prev arrow' bindtap='prevImg' src='/images/prev.jpg' /> <image class='next arrow' bindtap='nextImg' src='/images/next.jpg' /> </swiper> WXSS: swiper{ position: relative; height: 300rpx; } swiper image{ display: block; width: 100%; height: 300rpx; cursor: pointer; } swiper .arrow{ width: 30rpx; height: 46rpx; } swiper .prev{ position: absolute; left: 0; top: 50%; transform: translate(0, -50%); cursor: pointer; } swiper .next{ position: absolute; right: 0; top: 50%; transform: translate(0, -50%); } JS: //index.js Page({ data: { swiper: { imgUrls: [ '/images/swiper01.jpg', '/images/swiper02.jpg', '/images/swiper03.jpg' ], indicatorDots: true, autoplay: false, interval: 5000, duration: 1000, current: 0, } }, prevImg: function(){ var swiper = this.data.swiper; var current = swiper.current; swiper.current = current>0 ? current-1 : swiper.imgUrls.length-1; this.setData({ swiper: swiper, }) }, nextImg: function () { var swiper = this.data.swiper; var current = swiper.current; swiper.current = current < (swiper.imgUrls.length - 1) ? current + 1 : 0; this.setData({ swiper: swiper, }) } }) 看一下完成之后的效果: [图片] 貌似还不错,有用到这功能的同学,直接copy代码运行即可~ 欢迎交流技术问题,群人数已满100,可添加小助手: [图片]
2018-02-27 - 微信小程序swiper高度动态适配
ps:没有在[代码]swiper[代码]中添加[代码]scroll-view[代码]是为了可以使用页面的下拉刷新,最终方法直接跳到方案四。(含代码片段) 初始方案 [代码]swiper[代码]高度固定,[代码]swiper-item[代码]默认绝对定位且宽高100%,每个[代码]swiper-item[代码]中内容由固定高度的child组成,然后根据child数量动态计算[代码]swiper[代码]高度,初始方案(由于rpx针对屏幕宽度进行自适应,[代码]child_height[代码]使用[代码]rpx[代码]方便child正方形情况下自适应): [代码]swiper_height = child_height * child_num[代码]屏幕效果仅在宽度375的设备(ip6、ipⅩ)完美契合,其他设备都底部会出现多余空隙,并且在上拉加载过程中,随着内容增加,底部空隙也逐渐变大。 [图片] 方案二 开始以为是[代码]rpx[代码]适配显示问题,后通过文档中描述的WXSS 尺寸单位转化rpx为px([代码]child_height[代码]使用[代码]rpx[代码]): [代码]swiper_height = child_height * child_num * ( window_width / 750 )[代码]然后并无变化,我们可以看到[代码]child_height[代码]在不同宽度屏幕下,显示的宽高尺寸是不一样的([代码]px[代码]单位),那就尝试使用box在各个屏幕的实际高度进行计算[代码]swiper[代码]高度,box的高度可以单独在页面中增加一个固定标签,该标签样式和box宽高保持一致并且隐藏起来,然后在[代码]page[代码]的[代码]onload[代码]中通过wx.createSelectorQuery()获取标签实际高度[代码]baseItemHeight[代码]([代码]px[代码]单位): [代码]swiper_height = baseItemHeight * child_num[代码]结果显示原本的ip6、ipⅩ没有问题,另外宽带小于375的ip5上也ok,但是在大于375的设备上还是出现空隙,比如ip的plus系列。 方案三 之前的方案都无法计算出合适的[代码]swiper[代码]高度,那就换个思路,比如去计算空隙的高度。 [代码]swiper[代码]底部有一个load标签显示“加载更多”,该标签紧贴box其后,通过[代码]wx.createSelectorQuery()[代码]来获取[代码]bottom[代码],然而你会发现[代码]bottom[代码]是标签的[代码]height[代码]加[代码]top[代码]的和。计算底部空隙(暂时忽略“加载更多”标签高度): [代码]space_height = swiper_height - load_top[代码]刚计算完可以看到在静止状态下,计算出[代码]space_height[代码]拿去修改[代码]swiper_height[代码]显示空隙刚好被清掉了,但是接着就发现在动过程中获取到的[代码]bottom[代码]是不固定的,也就是说数值可能不准确导致[代码]space_height[代码]计算错误,显示效果达不到要求。 方案四 基于上述方案,[代码]swiper[代码]底部的load标签绝对定位[代码]bottom:0[代码],同时在[代码]swiper[代码]底部添加一个高度为0并且尾随内容box其后的标签(mark),然后获取这两个标签的top值之差: [代码]space_height = load_top - mark_top[代码][图片] 代码片段 这次获取到的空隙高度用于再计算[代码]swiper[代码]高度完美契合,美滋滋!!!
2018-05-23 - List遍历方法
1.for直接遍历 [代码]for[代码] [代码]([代码][代码]var[代码] [代码]i = 0; i < array.length; i++) {[代码][代码] [代码][代码]console.log(array[i]);[代码][代码]}[代码] 2.forEach函数遍历 [代码]array.forEach([代码][代码]function[代码] [代码](item) {[代码][代码] console.log(item);[代码][代码]});[代码] 3.map方法 [代码]array.map((list) => {[代码][代码] [代码][代码]console.log(list)[代码][代码]})[代码]4.for...in遍历 [代码]for[代码] [代码](let i [代码][代码]in[代码] [代码]array) {[代码][代码] [代码][代码]console.log(array[i]);[代码][代码]}[代码]
2018-11-08 - 【技巧】利用canvas生成朋友圈分享海报
大家好,上次给大家讲了函数防抖和函数节流https://developers.weixin.qq.com/community/develop/doc/0002c892fb80a8326bf70f56d5bc04 今天给大家分享一下利用canvas生成朋友圈分享海报 由于小程序的限制,我们不能很方便地在微信内直接分享小程序到朋友圈,所以普遍的做法是生成一张带有小程序分享码的分享海报,再将海报保存到手机相册,有两种方法可以生成分享海报,第一种是让后台生成然后返回图片链接,这一种方法比较简单,只需要传后台所需要的参数就行了,今天给大家介绍的是第二种方法,用canvas生成分享海报。 首先先来看下效果: [图片] 主要步骤: 1. 把海报样式用标签先写好,方便画图时可以比对 2. 用canvas进行画图,canvas要注意定好宽高 3. canvas利用wx.canvasToTempFilePath这个api将canvas转化为图片 4. 将转化好的图片链接放入image标签里 5. 再利用wx.saveImageToPhotosAlbum保存图片 这里有几个坑点需要注意下: 1. 用canvas进行画图的时候要注意画出来的图的大小一定要是你用标签写好那个样式的两倍大小,比如你的海报大小是400*600的大小,那你用canvas画的时候大小就要是800*1200,宽高可以写在样式里,如果你画出来的图跟你海报图是一样的大小的话生成的图片是会很模糊的,所以才需要放大两倍。 2. 画图的时候要注意尺寸的转化,如果你是用rpx做单位的话,就要对单位进行转化,因为canvas提供的方法都是经px为单位的,所以这一点要注意一下,px转rpx的公式是w/750*z*2,w是手机屏幕宽度screenWidth,可以通过wx.getSystemInfo获取,z是你需要画图的单位,2就是乘以两倍大小。 3. 图片来源问题,因为canvas不支持网络图片画图,所以你的图片要么是固定的,如果不是固定的,那就要用wx.downloadFile下载后得到一个临时路径才行 4. 小程序码问题,小程序需要后台请求接口后返回一个二进制的图片,因为二进制图片canvas也是不支持的,所以也是要用wx.downloadFile下载后得到一个临时路径,或者可以叫后台直接返回一个小程序码的路径给你 5. 这里保存的时候是有个授权提醒的,如果拒绝的话再次点击就没有反应了,所以这里我做了一个判断是否有授权的,如果没有就弹窗提醒,确认的话会打开设置页面,确认授权后再次返回就行了,这里有个坑注意下,就是之前拒绝后再进入设置页面确认授权返回页面时保存图片会不成功,官方还没解决,我是加了个setTimeOut处理的,详情可以看这里https://developers.weixin.qq.com/community/develop/doc/000c46600780f0fa68d7eac345a400 代码实现: [图片] 这里图片我先用的是网上的链接,实际项目中是后台返回的数据,这个可以自行处理,这里只是为了演示方便,生成临时路径的方法我这里是分别定义了一个方法,其实可以合成一个方法的,只是生成小程序码时如果要传入参数要注意一下。 绘图方法是drawImg,这里截一部分,详细的可以看代码片段 [图片] 不足: 由于在实际项目中返回的图片宽高是不固定的,但是canvas画出来的又需要固定宽高,所以分享图会有图片变形的问题,使用drawImage里的参数也不能解决,如果各位有比较好的方案可以一起讨论一下。 代码片段: https://developers.weixin.qq.com/s/3pcsjDmS7M5Y 系甘先,得闲饮茶
2019-01-23 - view里放多个元素横向排列自动换行
.js文件 data: { msg: [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], // 只是占位 }, .wxml文件 <view class="view"> <block wx:for="{{msg}}" wx:key="" wx:for-item=""> <view class='item'>{{index}}</view> </block> </view> .wxss文件 .view { width: 100%; height: 500rpx; display: flex; flex-wrap: wrap; align-content: flex-start; } .item { width: 50rpx; height: 50rpx; margin: 10rpx; background-color: greenyellow; text-align: center; } [图片] PS:本文只是记录个人学到的东西,以免还要到处搜索。希望对读者有帮助~
2019-01-27 - 小程序消息推送实现,含实现代码,和常见问题
最近需要开发微信和小程序的推送功能,需要用java后台实现推送,自己本身java和小程序都做,所以就自己动手实现下小程序的模版推送功能推送。 实现思路1 小程序获取用户openid,收集formid传给java后台 2 java推送消息给指定小程序用户 老规矩,还是先看效果图[图片] 微信收到小程序推送.png 我的这个是跑腿抢单推送,当用户新下单时,会给跑腿员推送消息。 下面开始讲解实现步骤 一,微信小程序管理后台开通模版推送[图片] 模版消息.png 这里的模版id很重要,接下来我们推送的都是这个模版。 二,java后台实现推送所需字段1 看微信官方推送消息所需要的字段 [图片] 实现推送所需要的字段 [图片] 官方示例 2 有了官方说明,我门接下来就去拿到官方所需要的这些字段,来组装请求数据就可以了。 三,下面讲解实现步骤我的java后台是基于springboot开发的,如果你不了解spring boot,建议你先去了解下springboot再回来接着学习。 还有RestTemplate是我们java后台做get和post请求必须的,我们和微信服务器交互就用的RestTemplate 1 首先根据官方推送所需字段组装java-bean 这里用到两个javabean [代码]/* * 小程序推送所需数据 * qcl 微信:2501902696 * */@Datapublic class WxMssVo { private String touser;//用户openid private String template_id;//模版id private String page = "index";//默认跳到小程序首页 private String form_id;//收集到的用户formid private String emphasis_keyword = "keyword1.DATA";//放大那个推送字段 private Map<String, TemplateData> data;//推送文字}[代码][代码]/* * 设置推送的文字和颜色 * qcl 微信:2501902696 * */@Datapublic class TemplateData { //keyword1:订单类型,keyword2:下单金额,keyword3:配送地址,keyword4:取件地址,keyword5备注 private String value;//,,依次排下去// private String color;//字段颜色(微信官方已废弃,设置没有效果)}[代码]到这里请求推送的数据就组装好了,解下来我们去实现推送功能。 奥不对,还有一个重要的字段需要获取到:access_token access_token的获取 [代码]/* * 获取access_token * appid和appsecret到小程序后台获取,当然也可以让小程序开发人员给你传过来 * */ public String getAccess_token(String appid, String appsecret) { //获取access_token String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" + "&appid=" + appid + "&secret=" + appsecret; String json = restTemplate.getForObject(url, String.class); AccessToken accessToken = new Gson().fromJson(json, AccessToken.class); return accessToken.getAccess_token(); }[代码]这次是真正的可以来请求微信服务器来实现消息推送了 [代码]/* * 微信小程序推送单个用户 * */ public String pushOneUser(String openid, String formid) { //获取access_token String access_token = getAccess_token(ConstantUtils.SCHOOL_APPID, ConstantUtils.SCHOOL_APPSECRET); String url = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send" + "?access_token=" + access_token; //拼接推送的模版 WxMssVo wxMssVo = new WxMssVo(); wxMssVo.setTouser(openid);//用户openid wxMssVo.setTemplate_id("LzeDP0G5PLgHoOjCMfhu44wfUluhW11Zeezu3r_dC24");//模版id wxMssVo.setForm_id(formid);//formid Map<String, TemplateData> m = new HashMap<>(5); //keyword1:订单类型,keyword2:下单金额,keyword3:配送地址,keyword4:取件地址,keyword5备注 TemplateData keyword1 = new TemplateData(); keyword1.setValue("新下单待抢单"); m.put("keyword1", keyword1); TemplateData keyword2 = new TemplateData(); keyword2.setValue("这里填下单金额的值"); m.put("keyword2", keyword2); wxMssVo.setData(m); TemplateData keyword3 = new TemplateData(); keyword3.setValue("这里填配送地址"); m.put("keyword3", keyword3); wxMssVo.setData(m); TemplateData keyword4 = new TemplateData(); keyword4.setValue("这里填取件地址"); m.put("keyword4", keyword4); wxMssVo.setData(m); TemplateData keyword5 = new TemplateData(); keyword5.setValue("这里填备注"); m.put("keyword5", keyword5); wxMssVo.setData(m); ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, wxMssVo, String.class); log.error("小程序推送结果={}", responseEntity.getBody()); return responseEntity.getBody(); }[代码]openid可以让小程序开发人员给你传过来,也可以自己获取。 formid需要小程序开发给你传过来,你也可以把formid存到数据库里,什么时候需要直接拿出来用就可以了。 注意:formid必须和用户openid对应。 下面贴出来完整代码[代码]package com.qcl.paotuischool.wechat;import com.google.gson.Gson;import com.qcl.userwechat.bean.AccessToken;import com.qcl.utils.ConstantUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Service;import org.springframework.web.client.RestTemplate;import java.util.HashMap;import java.util.Map;import lombok.extern.slf4j.Slf4j;/** * Created by qcl on 2018/9/11. * 微信小程序推送服务, * 包含获取access_token的服务 */@Service @Slf4j public class WxPushServiceQcl { //用来请求微信的get和post @Autowired private RestTemplate restTemplate; /* * 微信小程序推送单个用户 * */ public String pushOneUser(String openid, String formid) { //获取access_token String access_token = getAccess_token(ConstantUtils.SCHOOL_APPID, ConstantUtils.SCHOOL_APPSECRET); String url = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send" + "?access_token=" + access_token; //拼接推送的模版 WxMssVo wxMssVo = new WxMssVo(); wxMssVo.setTouser(openid);//用户openid wxMssVo.setTemplate_id("LzeDP0G5PLgHoOjCMfhu44wfUluhW11Zeezu3r_dC24");//模版id wxMssVo.setForm_id(formid);//formid Map<String, TemplateData> m = new HashMap<>(5); //keyword1:订单类型,keyword2:下单金额,keyword3:配送地址,keyword4:取件地址,keyword5备注 TemplateData keyword1 = new TemplateData(); keyword1.setValue("新下单待抢单"); m.put("keyword1", keyword1); TemplateData keyword2 = new TemplateData(); keyword2.setValue("这里填下单金额的值"); m.put("keyword2", keyword2); wxMssVo.setData(m); TemplateData keyword3 = new TemplateData(); keyword3.setValue("这里填配送地址"); m.put("keyword3", keyword3); wxMssVo.setData(m); TemplateData keyword4 = new TemplateData(); keyword4.setValue("这里填取件地址"); m.put("keyword4", keyword4); wxMssVo.setData(m); TemplateData keyword5 = new TemplateData(); keyword5.setValue("这里填备注"); m.put("keyword5", keyword5); wxMssVo.setData(m); ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, wxMssVo, String.class); log.error("小程序推送结果={}", responseEntity.getBody()); return responseEntity.getBody(); } /* * 获取access_token * appid和appsecret到小程序后台获取,当然也可以让小程序开发人员给你传过来 * */ public String getAccess_token(String appid, String appsecret) { //获取access_token String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" + "&appid=" + appid + "&secret=" + appsecret; String json = restTemplate.getForObject(url, String.class); AccessToken accessToken = new Gson().fromJson(json, AccessToken.class); return accessToken.getAccess_token(); } }[代码]在需要做推送的地方调用WxPushServiceQcl类中的pushOneUser方法,并传入openid, formid参数即可。 [图片] java控制台打印.png 这是我推送成功后打印的log 下面来讲小程序端开发需要做些什么可以看出,我们的formid有效期是7天,并且一个form_id只能使用一次,所以我们小程序端所需要做的就是尽可能的多拿些formid,然后传个后台,让后台存到数据库中,这样7天有效期内,想怎么用就怎么用了。 官方下发条件 所以接下来要讲的就是小程序开发怎么尽可能多的拿到formid了。 image.png 看下官方提供的,只有在表单提交时把report-submit设为true时才能拿到formid,比如这样 [代码] <form report-submit='true' > <button form-type='submit'>获取formid</button> </form>[代码]所以我们就要在这里下功夫了,既然只能在form组件获取,我们能不能把我们小程序里用到最多的地方用form来伪装呢。 [图片] 红框里是用户常点的 我的小程序是跑腿小程序,消息也主要推送给跑腿员的,而跑腿员点击最多的也就是这两个条目,所以我们就用from组件来伪装这两个条目,让用户在点击的同时就可以收集到用的formid。 [代码] <view class='button_item'> <form class="form_item" bindsubmit='gorRunnerLobby' report-submit='true' data-type="1"> <button class="button" form-type='submit'> <text>抢单大厅</text> <text class='runner_desc'>(兼职也可月入万元)</text> </button> </form> <view class='right_arrow' /> </view>[代码]这样就可以在用户点击条目时,收集到用户formid了 image.png 由于上面的botton有默认样式,所以我们就通过修改css来去除botton默认样式。 [代码].button_item { width: 100%; display: flex; flex-direction: row; align-items: center; padding: 2px 20px; background: white; border-bottom: 1px solid gainsboro; }/* 主要通过这里去除botton默认样式 */.button { width: 100%; background: white; border: none; text-align: left; padding: 6px 0px; margin: 0px; line-height: 1.5; }/* 主要通过这里去除botton边框 */.button::after { border: none; }/* 用button伪装获取formid */.form_item { width: 100%; }[代码]到这里我们小程序端也圆满完成自己的任务了。 这样我们java后台和小程序开发就可以开开心心的完成微信小程序的消息推送功能了。 如果有java或小程序相关的问题可以加我微信交流学习2501902696(备注小程序或java)
2018-09-12 - 小程序-canvas绘制文字实现自动换行
[代码] [代码][代码]const context = wx.createCanvasContext([代码][代码]'myCanvas'[代码][代码])[代码] [代码] [代码][代码][代码][代码]//这是要绘制的文本[代码] [代码] [代码][代码]var[代码] [代码]text = [代码][代码]'这是一段文字用于文本自动换行文本长度自行设置欢迎大家指出缺陷'[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]chr =text.split([代码][代码]""[代码][代码]);[代码][代码]//这个方法是将一个字符串分割成字符串数组[代码] [代码] [代码][代码]var[代码] [代码]temp = [代码][代码]""[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]row = [];[代码] [代码] [代码][代码]context.setFontSize(18)[代码] [代码] [代码][代码]context.setFillStyle([代码][代码]"#000"[代码][代码])[代码] [代码] [代码][代码]for[代码] [代码]([代码][代码]var[代码] [代码]a = 0; a < chr.length; a++) {[代码] [代码] [代码][代码]if[代码] [代码](context.measureText(temp).width < 250) {[代码] [代码] [代码][代码]temp += chr[a];[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]else[代码] [代码]{[代码] [代码] [代码][代码]a--; [代码][代码]//这里添加了a-- 是为了防止字符丢失,效果图中有对比[代码] [代码] [代码][代码]row.push(temp);[代码] [代码] [代码][代码]temp = [代码][代码]""[代码][代码];[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]row.push(temp); [代码] [代码][代码] [代码] [代码][代码]//如果数组长度大于2 则截取前两个[代码] [代码] [代码][代码]if[代码] [代码](row.length > 2) {[代码] [代码] [代码][代码]var[代码] [代码]rowCut = row.slice(0, 2);[代码] [代码] [代码][代码]var[代码] [代码]rowPart = rowCut[1];[代码] [代码] [代码][代码]var[代码] [代码]test = [代码][代码]""[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]empty = [];[代码] [代码] [代码][代码]for[代码] [代码]([代码][代码]var[代码] [代码]a = 0; a < rowPart.length; a++) {[代码] [代码] [代码][代码]if[代码] [代码](context.measureText(test).width < 220) {[代码] [代码] [代码][代码]test += rowPart[a];[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]else[代码] [代码]{[代码] [代码] [代码][代码]break[代码][代码];[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]empty.push(test);[代码] [代码] [代码][代码]var[代码] [代码]group = empty[0] + [代码][代码]"..."[代码][代码]//这里只显示两行,超出的用...表示[代码] [代码] [代码][代码]rowCut.splice(1, 1, group);[代码] [代码] [代码][代码]row = rowCut;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]for[代码] [代码]([代码][代码]var[代码] [代码]b = 0; b < row.length; b++) {[代码] [代码] [代码][代码]context.fillText(row[b], 10, 30 + b * 30, 300);[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]context.draw()[代码]
2018-12-25