- 云开发还可以这么玩系列之二----小手一点,一键部署!
首先回顾一下上期的分享 云开发还可以这么玩!从红包外卖券薅羊毛说起... 其中聊到了如何使用 tcb cli 及tcb framework,并对其进行扩展,以更高效、更便捷的进行项目的切换及部署。 今日偶逛社区,意外发现有同学提出了这样一个问题: 小程序云开发的环境切换,如何快速把云函数、存储导入到新环境? 看到这个帖子,我不禁又陷入了沉思: [图片] 仔细看看,其实需求的解决方案在上期的分享中已经阐述的很清楚了。难道我的帖子没人看?难道我的SEO做的不够好? 然而,回头再思考一下,通过修改配置文件、扩展cli命令的方式,其实并不是一种特别友好的方式。一是有一定的开发门槛,二是对操作人员,黑气吗黑的命令行也不是人见人爱...... 那么有没有一种方案既能做到配置简单,又对想要快速部署的新手甚至一窍不懂的小白友好呢? 答案是肯定的。 来,首先快速体验一下快速部署的魅力! 这是一个简单的博客小程序,基本的分类、文章、检索、浏览。 简陋,但是五脏俱全。 [图片] 你也想快速拥有并部署一个?没问题,请做以下准备工作: 1 一个开通了云开发并创建了云环境的小程序,复制他的appId备用; 2 小程序后台,开发管理-开发设置-小程序上传管理中,生成小程序上传密钥,下载密钥文件备用,同时关闭代码上传ip保护; 接下来,点击下方按钮,按照步骤进行(请在pc浏览器下进行此操作,移动端体验并不好): [图片] 选择云环境: [图片] 输入小程序appid,访问链接上传密钥文件生成base64字符串,并填写: [图片] 点击“完成”,稍等片刻,等待构建部署完成,见证奇迹的时刻到了! 进入腾讯云控制台,云函数及db创建部署完成: [图片] 进入小程序管理后台,版本管理。会发现已经上传了小程序版本: [图片] 还等什么?扫码体验吧! 那么,这一切是怎么做到的?这个神奇的“部署到云开发”的按钮背后究竟隐藏着什么秘密?走进云开发即将为你揭晓...(跑题了) 有请今天的主角登场: 云开发一键部署按钮 正如一键部署的介绍中说: 一键部署按钮可以让公开的 Git 项目一键部署到云开发 CloudBase 上,大大简化用户部署的门槛,方便用户快速使用和体验应用。一键部署功能支持 Github,Gitlab,Coding,Gitee 等 Git 仓库地址。 一键部署其实就是在tcb framework的基础上,通过引入git项目,读取配置,从而能够快速的部署项目到云。 更多详细的,请大家参阅文档,有一个基础的认识。接下来针对此博客小程序来剖析一下详细的配置,相关git代码文末将放出。 1 DB插件配置: [图片] 部署的时候会自动生成分类(categories), 文章(articles),及数据库迁移记录(mirgates)三个集合,如果已存在,则会跳过创建; 2 云函数插件配置: [图片] api是整个博客的云函数接口,migrate是每次deploy时候执行的db迁移脚本,方便进行数据初始化、数据清洗。 3 小程序部署插件配置: [图片] 简单易懂。唯一需要注意的是{{env.WX_APPID}}和{{env.WX_CI_KEY}}。 没错,这就是之前准备工作中提到的小程序appid和小程序上传密钥了。 这里用到的是模板变量的概念:模板变量 所谓模板变量,实际是通过读取项目下的.env, .env.local,或者是用户自定义的.env.xxx配置文件,注入变量到cloudbaserc中,这样就可以方便的进行模式切换。 同时,也可以通过framework配置中requirement的environment配置,在一键部署的时候,通过用户的输入注入到配置文件中,实现自定义: [图片] [图片] 4 framework的hook配置: [图片] 所谓hook,即是在部署前后执行的钩子动作,可以进行一些预编译,或者在部署完成后执行相关的云函数进行一些操作。 这里postdeploy在部署完成后执行了migrate云函数,进行了db数据的一些初始化工作,感兴趣的可以看一下源码。 至此,一个可以完美支持一键部署的云开发小程序就配置ok了,可以进行一键部署了。 看,是不是很方便? tcb Framework及一键部署,想象的空间还有很多,当然也存在一定的局限性,优点缺点都有待各位看官去进一步挖掘了。 无论如何,值得你去用! 本项目源码见:https://github.com/yautah/cloudblog,欢迎指正、star、fork、pr,请勿用于商业用途,不然挖地三尺也要追则到底。
2021-01-14 - 小程序怎么支付到个人零钱?
体验码(已上线,非具体需求用户请勿支付,如误操作,请联系客服商家动态微信退款) [图片] 下面看下演示 [图片] 开通步骤: 1、首先你得有个营业执照 2、打开微信-我-支付-收付款-二维码收款-收款小账本(或者直接搜索小程序:收款小账本) [图片] 3、补充经营信息,开启商家服务,需要上传营业执照 4、审核通过后,继续收款小账本首页,下单助手、商家动态等去完善店铺信息 5、信息完善后点击【收款小账本】头部店铺名称,然后再点击商家小程序即可打开小商店助手,然后进入我的小程序,就能打开啦 6、剩下的就是店铺活动,各种信息完善了 Tips:此小程序名称格式必须是是【主营业务|地理区域】,然后店铺名称可被全网搜索到
2020-11-22 - 只有三行代码的神奇云函数的功能之五:获取群id
这是一个神奇的网站,哦不,神奇的云函数,它只有三行代码:(真的只有三行哦) 云函数:login index.js: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event) => { return { ...event, ...cloud.getWXContext() } } 神奇功能之五:获取群id: 将小程序分享到某群里,可获得该群的群id, page.js: onShareAppMessage: function () { wx.showShareMenu({ withShareTicket: true }) let path = '/pages/xxx/xxx' let title = 'title' let imageUrl = `http://xxx.com/100.jpg` return {title,imageUrl,path} }, app.js: onLaunch: function (options) { options.shareTicket && this.getOpenGId(options.shareTicket) }, getOpenGId: function (shareTicket) { wx.getShareInfo({ shareTicket, success: function (res) { wx.cloud.callFunction({ name: 'login', data: { weRunData: wx.cloud.CloudID(res.cloudID) }, success: res=> { console.log(res.result.weRunData.data.openGId); } }) } }) }, 需要说明一下的是:从群里点击分享卡片进入小程序,必须是重启的小程序,不能是已经打开的小程序,否则得不到shareTicket。 其他功能: 神奇功能之一:获取openid: https://developers.weixin.qq.com/community/develop/article/doc/00080c6e3746d8a940f9b43e55fc13 神奇功能之二:不用授权获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/000a0c6b580338e947f9db0c65b813 神奇功能之三:100%成功获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/00066a967c4e384949f93fe1151413 神奇功能之四:获取电话号码: https://developers.weixin.qq.com/community/develop/article/doc/0006a8ec7ac860c94bf90a34f5d813 [图片]
2020-10-20 - 开源闯关答题小程序
全民闯关答题小程序是使用云开发的在线答题小程序,无需搭建服务器,无需域名即可使用云端能力。 V2.0版已完成功能 答题闯关挑战答题发放红包(可设置几题发放红包,每题设置红包数额)答题等级设置(自定义等级名称和关卡)观看激励视频获取金币(限制每天观看次数)排行榜显示红包兑换物品设置更多软件推广集成banner 视频广告,插屏广告,激励视频广告等展示截图 [图片][图片][图片][图片] 现将V1.0初始版进行开源学习,欢迎大家学习,如果上线需要经过作者许可哦 ~ 开发不易、创作不易。 码云地址: https://gitee.com/yingwuniao/chuangguan 技术文档下次会发文章补上
2020-10-10 - 个人小程序怎么推广
一、搜索优化三板斧 在制定搜索优化策略前,我们要清楚小程序的搜索入口有:微信搜索、小程序垂搜、购物单搜索以及搜一搜,这4个入口结合小程序排序规则,就衍生出以下3种推广方式: 01 取个容易被搜到的名称先占位 小程序的取名需要结合产品特点,也要考虑到用户对所提供服务的称呼习惯和搜索习惯,把最关键和最常使用的词语放在名称的前面,尽量避免复生僻词语,也不宜太长,不然影响用户记忆。 另外,由于小程序的名称是唯一的,商家应该尽早注册小程序名称,不然就会出现名字被已被注册的局面。不过,出现这个尴尬局面也不用慌,可以在名称前面或者后面加上标志性的英文字母,比如「i麦当劳」、「肯德基+」、「YH永辉生活+」、等。 02 提升小程序商品的搜索排行 微信近期上线新功能,搜索栏边购物商城,小程序商品均可接入。据微信官方透露,这商品的销量和评价有直接关系,那么商家可以通过打造小程序爆款商品,引导用户给好评的方式提高商品的搜索排名。 二、巧用微信免费端口 公众号和小程序都是微信生态内极为重要的组成部分,在微信官方的持续迭代下,两者在流量上基本可以实现互通有无。 03 公众号绑定/解绑小程序的消息通知 只要公众号关联成功小程序,就会自动向用户推送一条关联通知,用户点击卡片即可进入小程序。这里要划重点了:这条关联通知不限次数、不占用推送次数。 也就是说,你可以重复「绑定- 解绑」这个动作,当然前提是不引起用户的反感。因此我的建议是,当公众号新增粉丝到一定量后,操作一次,来达到同时引导新老用户使用小程序的目的,「百果园」就是活用「绑定- 解绑」这个动作的典型例子。 04 小程序和公众号菜单结合 当你的小程序和公众号关联后,你可以选择将小程序放置在公众号菜单栏内,无论新老用户只要看到点击菜单栏,即可实现把公众号粉丝引流到小程序中。 05 公众号文章插入小程序码/卡片 有了前面和公众号的关联,公众号文章中就可以直接插入小程序卡片,这种形式的插入十分直观,特别适合做内容电商的企业。 他们基本都是采用「公众号+电商小程序」引导用户购买的形式,黎贝卡于在17年12月份上线自有品牌的小程序,短短7分钟告罄;第二次上线更是只用了59秒就告罄,如此成绩实在令人艳羡。 但如果你的小程序没有和公众号关联,还可以采取这种更简单粗暴的方法:直接插入小程序码,引导用户长按扫码进入小程序(效果比前一种卡片式的嵌入要差)。 06 小程序跳转(免费互推) 小程序跳转有两种玩法,一种是同一企业之间的小程序跳转,另一种是不同企业之间的资源互换。 前者的相互跳转,其实是企业服务的互补和完善,它一般会这么做:将每个产品线各做一个07 支付后消息通知 商家可以在微信支付推送的支付成功通知页面设置「进入商家小程序」链接,引导消费者使用微信支付付款后进入小程序,形成二次触达。 08「附近的小程序」 「附近的小程序」可以说是微信收割线下场景的大杀器。用户到达一个地点想要服务时就可以使用这个功能去发现附近的小程序,完全不用借助中间商,就可以帮助线下商户更好获取用户。 以麦当劳小程序为例,通过「附近的小程序」访问麦当劳小程序的用户中,新用户占比高达94%。在麦当劳小程序所有新用户中,通过「附近的小程序」入口进入的用户占比达 23%,是小程序新用户的主要来源。 那么怎么用好这个新功能呢?主要有两个小技巧: 第一、添加门店客服。商家在添加门店时,可以认证微信客服,待门店通过审核上线后,再使用小程序所绑定的微信登陆客服后台并保持在线状态,即可实现客服外显至门店列表页。这样一来,用户更容易看到你的门店,也更方便联系你,从而为你的小程序获得更多服务机会。 第二、在店铺页上配置发放卡券。如果商家开通了微信买单、会员或优惠服务,会展现在店铺信息页。这样用户在选择特定分类下附近的小程序时,可以通过点击标签筛选开通了以上服务的商家,完成拉新转化和招募会员的同时,打通了「微信卡包」这个一级入口。 三、利用微信进行社交裂变 坐拥10亿用户的微信俨然是一个巨大的流量池,为小程序的社交裂变提供了支持。 09 社交立减金+卡包 在小程序支付成功后,商家可向用户赠送「社交立减金」,用户可以分享给多位好友或微信群。当「立减金」被好友领取后会放入卡包「我的票券」中,用户下次再到这个小程序内消费支付就能进行核销,比如星巴克、周黑鸭、屈臣氏等商家都上线了该功能,这是打通「微信卡包」这个一级入口的另一种推广方式, 据2018年1月微信公开课PRO公布的数据,已上线社交立减金的小程序已经取得不错的效果:社交立减金裂变率达到150%,每次分享机会可以至少带来1个新的客人,同时获客成本可以低至1.7元,券核销率超过20%。 10 红包裂变 玩红包裂变的套路一般是这样的,以「拼多多」为例,你打开它的小程序,会弹出一个霸屏的大红包,当你领取后,它会引导你将红包分发到微信群,这样你就可以再领红包。它实质上就是一个借助原有用户经小程序通过微信群裂变首个用户的过程。 11 功能玩法裂变 除了用立减金、红包这种简单直接的利益刺激,商家还会对它们进行变形,于是出现比如拼团、砍价、抽奖等玩法。玩法很多,就看你怎么能不能玩出新花样来。 以电商小程序为例,商家可以做拼团、分销、秒杀这些功能扩散到有匹配度的群里做社群裂变;同时可以挑选一款产品作为拼团、秒杀的一个亮点,结合公众号推文做活动裂变;另外还可以开启分销系统,做朋友圈海报进行朋友圈裂变,三种裂变方式一起作用的效果基本不会差。 12 小程序和H5的组合打法 今年我们看到各种刷屏H5,十分火爆,这给了我们新的思路:通过H5引爆活动吸引流量进入,然后用小程序进行用户沉淀,这种组合打法或许会成为接下来的一种小趋势。 四、微信生态内投放广告 以上给大家介绍了一些不怎么花钱的推广方式,下面我们来聊点花钱的——投广告。首先你在公众号后台找到「广告主」一栏,点击进入,然后就可以根据你的需求进行一一设置。 13 公众号文中广告 现在公众号新增了文中广告位,支持小程序落地页投放,只要点击广告外层的图片或按钮即可跳转到指定的小程序。只要公众号的流量足够大,一大波广告会自动寻找到你;另外如果线下商家需要推广,也可以利用这种方式和知名的本地号做推广。 14 公众号底部广告 在公众号底部广告处也支持了小程序的落地页投放,与在公众号文中的广告位类似,用户点击广告外层图片即跳转指定小程序。该功能将对电商类小程序有很大的好处,其成交率将比之前的H5推广要高得多。但该功能没有对门店推广开放,而且对线下商家的意义不大。 15 朋友圈小程序广告 朋友圈广告支持小程序投放,广告投放落地页以小程序作承载,点击广告即可进入小程序页面。另外,根据微信官方最新消息,朋友圈广告上线新能力——多门店投放,广告主可实现单条广告支持承载不超过100家线下门店,同时广告系统可以智能拉取并向用户展示与其最近的门店,引导用户到店转化,这对于做本地推广的线下商家来说是一个绝佳的好消息 16 小程序内广告 现在企业也能在其他小程序内对自家的小程序进行广告投放,本质上也是一次小程序间的跳转,不过这种要花广告费而已。 五、微信生态外投放广告 除了在微信生态内进行广告投放,当然还有在微信生态外进行广告投放。 17 媒体曝光 不管小程序推广方式多么“丰富多彩”,寻求互联网行业媒体的曝光,永远都是一种经济且效果较为可观的方式,而「小打卡」小程序初期的用户增长,很大程度上就是依靠这种方法实现的。 18 APP分享 在APP内分享内容到微信群或好友时,分享出来的也可以以小程序的方式进行展示,比如大众点评、智行火车票等APP分享出来的就是小程序,很多核心的用户不需要使用APP,也可以使用小程序触达。 19 WiFi推广 通过WiFi推广小程序是建立在此前公众号推广的基础上,通过微信连WiFi关注公众号后,落地页跳转小程序,进而为小程序导流。其场景地位主要在线下实体门店、餐饮店、服装,商超等,可以帮助更多商户解放线下流量,更快拥抱新零售。 20 海报、宣传单等物料的线下扫码 线下扫码是小程序最基础的获客方式之一,通过微信扫描小程序码,即可进入小程序,简单快捷。线下门店可以采用这种简单的方式,通过与线下营销活动相结合,以易拉宝、宣传单或相片架等方式进行宣传,吸引用户主动扫码参与活动,典型如喜茶,它就是通过门店物料,赠送相片架等方式实现用户快速增长。 21 委托第三方运营公司 将小程序委托给第三方运营公司,然后通过第三方运营公司下的众多微信社群进行转发流通促成大量激活,但是这个方法比较粗放,因为投放的用户群不一定都是小程序的目标用户,转化率可能会比较低。 最后,我们用一张思维导图总结下小程序的21种推广方式: [图片]
2020-10-09 - 微信小程序swiper的自适应高度
小程序组件swiper需要指定固定高度,但在某些场景中我们需要动态设置swiper的高度以完整展示swiper中的内容,比如高度不同的图片,笔者最近项目中的日历组件(31号有时会多出一行)等等,如何使swiper组件自适应高度呢? 翻阅了一些网上的例子,一般的解决方法是通过设置style.height来解决 [代码]<swiper style="{{style}}" > <swiper-item></swiper-item> </swiper> [代码] [代码] Page({ data: { style: '' }, onReady(){ this.setData({style: 'height: 100px'}) } }) [代码] 问题:状态丢失 直接设置样式可以动态设置高度,但这样做的不好之处在于会重新渲染结构,导致之前设置的状态丢失,比如我们在日历中选中的日期 我们的需求是,1. 动态设置swiper高度,2. 不丢失之前的状态 一番折腾过后,发现这条路是个死胡同,不能解决问题。 解决: CSS变量 后来发现使用css变量也能够动态改变样式,抱着试一试的想法 模板 [代码]<view class="box" style="{{boxStyle}}"> <swiper class="container"> <swiper-item></swiper-item> </swiper> </view> [代码] 样式 [代码].box{ --box-height: 400px; --append-height: 0; width: 100vw; height: calc(var(--box-height) + var(--append-height)) } .container{ height: 100%; width: 100%; } [代码] js [代码]Page({ data: { boxStyle: '' }, onReady(){ if (...) { this.setData({boxStyle: '--append-height: 50px'}) } else { this.setData({boxStyle: '--append-height: 0'}) } } }) [代码] 上述设置,居然能够完美的实现项目需求,现在项目正在上线中,等待测试出bug,哈哈 欢迎关注github项目 关注下面的小程序查看最新的DEMO示例 [图片]
2020-06-24 - 小程序流量主运营技巧
前言(写给入坑的小白) 本文不涉及任何需要资质的小程序(如:视频类目)。小程序流量主是个人和小微企业主要变现途径之一,满1000人即可开通流量主(登录mp.weixin.qq.com,左侧边栏-推广-流量主-开通即可)。开通后,开发者可从流量主-广告位管理添加广告位,目前有6种广告位。 [图片] 正文(本文约很多字,分为四大主类,手里有1-10个小程序建议全部看完;手里有10个以上小程序,可跳过1、2、3,均为个人观点,不喜使劲喷) 一、小程序定位 小程序定位目前有以下四种,均不需要任何资质,个人(商城除外)/小微企业都可以做,由于本人不擅长文字表达,每个类型只选择一个做分析,谅解。 1、工具类 工具类有很多可以做:题库、技术文档、教程、去水印等。目前最火爆的应该属于疫情相关类的工具,关于疫情数据类小程序不做分析,没资质也没权利,主要说疫情周边可运营的工具。头像口罩,代表小程序:头像加口罩、戴个口罩吧、戴上口罩(每日搜索量约等于2000),可参考以下做法: [图片] 以下为近7日访问数据量 [图片] 盈利方式:流量主 延伸参考:如果仅做头像加口罩的话,那么疫情过后,这个小程序会直线下降,将无任何作用。如果开发者手里目前有类似小程序,可参考“头像加口罩”做法,逐渐去延伸头像周边功能,例: ①、头像加字:头像+数字、头像加V、头像加字、头像加圣诞帽、新年头像边框、头像加福、头像加明星等 ②、聊天背景图、壁纸:武汉加油、卡通、美女(不要漏点太多)、二次元、跑车、科技等 ③、趣味九宫格配图:类似朋友圈9张图,中间获取用户自己头像,周围8张图弄点能吸引用户的等 ④、文字秀:微信昵称下标、上标、个性昵称等 运营分析:如果参考以上4点做法,首先你的程序再疫情结束后,不至于直线下滑,最起码能留住一些用户(UI很重要) 个人建议:工具类的好处就是不需要去长时间盯着后台,建议有想法的开发者,可以入门5-10个左右工具类小程序(功能不要相同)。 推广方式:参考本文第四大板块内容 2、返利类 主流返利平台:淘宝、天猫、拼多多、京东、蘑菇街、唯品会、网易考拉,以下参考 [图片] 盈利方式:返利(主)+流量主(辅) [图片] 基础分析:每个人微信里都会有一个或多个微信群是给你们购物优惠券链接的,他们盈利方式主要是靠每个平台的返利,比如淘宝天猫的叫“阿里妈妈”、拼多多的叫“多多进宝”等 运营分析: ①、平台功能:提供所有优惠券、商品返利、代理入驻、提现(个人可做收款码、企业可对接微信支付到零钱) ②、招代理商、可以给代理商(兼职、宝妈)50%以上的返利 ③、除了商品优惠券之外,可以把返利分给一部分给到用户。首先,用户会花更少的钱买到商品;其次,用户买完东西还会赚点小钱,每个月可提现到微信零钱。这样用户会发生裂变,省钱+赚钱。 个人建议:开发者至少有一个类似的返利小程序,每个月只需运营一天,工作内容一是把用户的返利发给用户&代理商,二是自己去各大平台领取每个月的“工资” 推广方式:参考本文第四大板块内容 3、商城类(个人开发者可跳过) 商城类,本人运营的比较少,每天就10-20单左右,卖啥就不做广告了 盈利方式:差价 基础分析:如果自己手里有一些商品低价资源,可以做一个“综合服务商城类目”,然后去试着用广告主去推一下 运营分析: ①、平台功能:砍价、返利、拼团、回购、入驻、积分、抽奖、游戏营销 ②、广告主曝光&点击报价不要最低,也不要最高,理由就是最低的话,80%的钱会给你推到一些质量很差的微信用户,比如我。 ③、对接圈子,虽然圈子刚起步,不确定能不能做大,万一呢? 个人建议:企业一定要有一个自己的商城,哪怕没人买。这种东西怎么说呢,就好比一个企业站,虽然没什么用,但是得放那儿,万一客户要看呢? 推广方式:参考本文第四大板块内容 4、游戏类(非小游戏) 答题、成语、找茬等类似运营的比较多,可自行搜索,不要认为这是游戏,开发者就望而却步,在线教育类目是可以通过的,这个开发者很多都不知道。以下可参考: [图片] 盈利方式:流量主 基础分析:基本所有的模式都是闯关类型,这种类型的小程序,基本都是用户消磨时间用 运营分析:关卡尽量多,入门、初级、中级、高级,高级模式可以做类比循环,形成无限关卡模式,闯关奖励机制,签到机制等。这种类型的小程序比较方便运营,裂变起来也快。 个人建议:裂变模式一定要有,虽然微信会严格把控这方面功能,但是开发者可以做一些技巧,不要让用户强制或者主动去触发,这样微信对开发者还是很友好的。 推广方式:参考本文第四大板块内容 二、小程序开发 有实力的开发者,自己开发,云开发很快,会前端就可以了,没实力的去正规平台买源码,论坛源码也很多,有部分论坛还是嵌入了比特币勒索,自己做好防护。个人建议:开发者能开发尽量自己开发,后期迭代方便,不要像我一样,50多个小程序80%是买现成去运营的。反正各有各的好处,开发者可自行决定,运营者可选择直接购买源码直接上线运营,前提是自己看好功能是不是和自己要的一样。有些SAAS平台的开发者实力还是可以的,支持定制功能。此处不做广告,自行搜索或者询问朋友。 三、广告位位置及利润 开发者的每个页面广告位一定要分开!一定要分开!一定要分开!这样做的目的是为了分析每个广告位的利润,好去做调整,把收益最大化。 失败案例举例:小程序的主页、个人中心页用同一个banner广告位,这样做出来一点好处都没有,后台只能看到banner收益是多少,看不到是哪个页面收益。极端情况,收益全部再首页,个人中心页没有广告收益,这种情况开发者是不知道的,如果把广告位分开,这种情况可以去优化个人页面,或者主页面换成视频banner。广告位分析页面:流量主--数据统计--广告数据--广告指标明细--细分数据 [图片] [图片] 1、很多人表示,疫情期间流量主收入下滑。这个原因不是因为微信调整流量主收益,根本问题是自己的用户质量。举个例子,当你开通流量主之后,你的用户还是这1000个,假如你第一天收益为100,你很开心,1000用户就能赚100,你第二天就放弃推广了,这样的话,你的用户质量是会逐渐下滑,微信方完全可以认定为你这1000人都是自己的号,去刷广告费的。长此以往下去,你的流量主利润会无限趋向于0。举个栗子: [图片] 2、广告位位置一定要合理好看,但是不代表“流氓”,比如全明星代言的某游戏“元宝无限收一刀999”点哪儿哪充值。开发者需要注意的是小程序的质量,需要用户在每个页面停留的时长最起码30秒,这样一个完整的视频广告才能曝光完。 3、banner广告收益是按有效点击计算的。很多人好几千曝光,但是点击只有几个、十几个,这种情况需要不断去优化接入的场景/位置,提高用户点击意愿。个人技巧:banner广告位尽量不要太多,1-2个就可以。尽量多放几个视频广告位,这样曝光也有收益。格子广告没试过,用过都说不好~ 4、激励广告作为流量主最高收益是有一定道理的,用户为了获取某些奖励是必须观看完整的,所以给开发者建议:用户如果可以获得小程序内某些奖励,可以适当多放一些激励广告位。 5、所有的广告位都是根据用户年龄、爱好等参数去调取相应的广告,开发者不需要去考虑 6、广告收益个人认为:激励》视频》插屏》前贴》banner》格子(格子没试过,暂放倒数第一) 四、小程序推广 尽量做成年人主打的小程序,有些开发者觉得好玩儿,做一些儿童益智类的小程序,你是认为儿童有手机,还是认为家长愿意让孩子玩儿手机呢?这个很不解。没有鄙视的意思,也许是情怀吧~~毕竟我做小程序比较俗,就是为了赚钱。 主流推广方式:公众号引流、截流,由于涉及一些不合常规的内容,本文只说常规操作,剩下的自己领悟,或者可以联系我~ 首先小程序的名字至关重要,一个好的名字可以带来无限的流量,再加上裂变功能(邪恶的微笑)。起名字的时候可以用到的工具:搜索小程序-微信指数,查询关键字,尽量找稳定再1000万以上的搜索量,从关键字中摸索自己的小程序名字。这样用户搜索到你的小程序几率会很高~ 1、工具类核心玩儿法(适用于所有小程序推广):文章引流,截取关键字,火爆主题,比如2019年12月19日庆余年全集泄露、2020年疫情(不要发疫情数据内容,要发一些正能量的有内容文章去引流),我阅读过的文章最低的阅读量8000左右,最高的10万+,据说有好几百万的阅读量。如果你的文章写的好,结尾放一个小广告:为防止疫情蔓延,请给您的头像带上口罩~,啪,一个卡片小程序(或二维码),流量自己想~ 推广对象:18-30岁 2、返利类核心玩儿法: ①、可以参考工具类玩儿法 ②、各大微信群、QQ群,去推广,招代理等方式,或者去买一些基础流量,进行裂变,实际运营看下效果,好继续针对用户群体去推广,建立自己的群体系,群内发商品返利链接。微信好友没人?给你举个例子,我这篇文章发完,如果加个我的二维码,最起码能有100人加我,不是我文章写的有多好,是你永远不知道用户有什么样的目的和需求~ 推广对象:18-60岁 3、商城类核心玩儿法 ①、可参考返利类核心玩儿法,拥有自己的客户群体系,发一些自己的商品还是可以的,一定要带分销体系,你懂得~(最高3级,再高就是传销了) ②、广告主、目前效益个人感觉不明显,每次花1000块钱做广告,利润基本没有,和发广告的钱持平,而且用户留存也不是很高,可能是我的商品比较单一等各方面因素吧,不过赚流量还是不错的。 推广对象:18-30岁(以我的商城为例,还需看商城出售的内容) 4、游戏类核心玩儿法(非小游戏) ①、一个好的名字就够了。举例:精选商品橱窗(腾讯官方),微橱窗(我朋友的)。不得不说,这波流量很高,遗憾的是,他不是火爆的游戏类小程序~ [图片] ②、参考工具类玩儿法,文章引流截流 推广对象:18-40岁 五、小程序矩阵 矩阵一定要有,矩阵一定要有,矩阵一定要有,防截流,底配10个小程序。不是纯矩阵,是微信开发规定,每个小程序可以跳转10个小程序,开发者可以利用这个功能去添加自己的矩阵来获取更多的流量收益,保证自己的用户在自己的矩阵圈活动。 [图片] 写这篇文章主要是给大家传授经验,希望小白能学到点东西,入门后的朋友可领悟到更多运营方法,江湖之大,附月账单有缘再见 [图片]
2020-05-25 - 【笔记】云开发通过客服消息实现自动回复进群,同时兼容客服小助手
小程序不具备小程序内扫描二维码的能力,因此如果要实现关注公众号或加用户群功能大家一般都利用微信客服功能的自动回复来实现。此时如果自己去实现微信客服自动回复,客服小助手就不能用了,很令人纠结。经过我的研究,借助云开发找到了一个方案,可以实现当用户想获取微信群走自动回复的接口,真正咨询时,直接到客服小助手进行回复。 效果如下 [图片] 原理解析 云开发在做消息推送配置的时候可以配置消息类型,这个时候如果我们只配置一种类型(小程序卡片),那么就只有卡片才会被云函数接管做自动回复,其他消息类型(图片、文字)则正常走小程序客服的通道。 实现步骤 1.小程序端设置按钮属性open-type="contact",用于用户点击时带上定义的卡片跳到客服消息界面。 申请加入 2.新建云端的函数,设置config.json定义权限,如下config.json { "permissions": { "openapi": [ "customerServiceMessage.send", "customerServiceMessage.uploadTempMedia", "customerServiceMessage.setTyping" ] } } 3.写云函数端代码,如下 if (event.Title == "我要进用户群"||event.Title =="关注公众号"||event) { //设置输入状态 cloud.openapi.customerServiceMessage.setTyping({ touser: OPENID, command: 'Typing' }) //从云储存中拉取图片数据 const qunimg = await cloud.downloadFile({ fileID: "cloud://pm-hsfip.706d-pm-hsfip-1259751853/img/qun.png", }) //上传图片素材到媒体库,并获取到图片id const qunmedia = await cloud.openapi.customerServiceMessage.uploadTempMedia({ type: 'image', media: { contentType: 'image/png', value: qunimg.fileContent } }) //向用户发送群二维码 await cloud.openapi.customerServiceMessage.send({ touser: OPENID, msgtype: 'image', image: { mediaId: qunmedia.mediaId, } }) //取消状态设置 cloud.openapi.customerServiceMessage.setTyping({ touser: OPENID, command: 'CancelTyping' }) } 4.设置消息推送,路径如下 云开发-设置-全局设置-云函数接收消息推送 中添加消息类型为miniprogrampage,绑定云函数为新建的云函数。 [图片] 5.微信公众平台绑定客服[图片] 注意事项 如果按照本教程 客服小助手无法收到消息 或 无法自动回复,可以先将以上消息推送配置删除,然后再微信后台绑定客服后,再重新进行消息推送配置。
2020-07-29 - 小程序如何生成邀请码
在线答题小程序最近做了重大更新,推出了V4.0版本,在该版本中增加了会员邀请制功能,只要受邀请用户才可以进来参与答题,这样对于答题活动以及企业内部培训而言,是非常有必要的。 那么答题小程序如何生成邀请码? 具体看下面截图,我标记的①②③④,也就是需要四步走,我下面分别介绍下 [图片] 3 第一步:打开云开发控制台,切换到数据库面板 第二步:选择数据库里面的somecode集合 第三步:点击右侧【添加记录】按钮,并点击确定 第四步:_id里面的值便是一个有效的邀请码 4 友情提示: 一个邀请码只能用一次,用完即销毁,不可重复使用 小程序界面截图 1 [图片] 2 [图片] 3 [图片] 4
2020-05-20 - 定制小程序开发有哪些优势
小程序信息千变万化,机遇也稍纵即逝,小程序的从业者想要在小程序行业里发展,就要时刻关注行业动态,下面千优小编为您带来“定制小程序开发有哪些优势”的资讯分享。 需要小程序开发的咨询,吴生:微电18319783819 [图片] 在这移动互联网时代,小程序凭借着即用即走、流量大、开发难度低的优势吸引了不少人的关注,随着小程序的更新迭代,让愈来愈多的行业加入了小程序开发的队伍中。不过在开发小程序的道路上,大家会碰到非常多的选择,其中就包括小程序选择哪种开发方法,当前比较常见的开发方法就是定制小程序开发和模板小程序开发,不过绝大部分企业都选择了定制小程序开发,那么定制小程序开发都有哪些优势? 一、优化用户体验在了解用户的一些使用习惯后,可以灵活地定制小程序,提高用户体验,增加用户的黏粘度。虽然市场上有不少提供小程序模板的服务,但由模板开发的小程序未免少了些个性,也不肯定贴合用户使用习惯。定制开发小程序不仅可以满足用户个性化需求,还能在产品、服务上进行优化,将优质的体验带给用户,用户对其产生深刻印象后,自然能让你的小程序与其他商家的小程序区别开来。 二、提高企业形象不同企业有着不同的企业文化,小程序的设计自然要与企业文化相结合,通用性的小程序已经非常难适用企业不断发展的管理需求,定制微信小程序不仅可以符合企业文化,还能从另一面来突显企业的个性,有利于企业提升产品品牌形象。 三、开发成本低定制开发小程序的价钱不高,因为基于微信端开发,因此无需考虑版本问题,对于商家来说节约了一笔不小的开发成本,大大缩短的开发周期,商家在短时间拿到小程序后能够更快地进行营销工作。 模板小程序有模板小程序的优势,可是它毕竟不能够创造独一无二的小程序,可是定制小程序开发却可以满足商家一切的需求,所以大家更喜爱于定制小程序开发。以上就是小编分享的全部内容了,希望对大家有所帮助。 以上资讯就是小编为大家收集的,关于“定制小程序开发有哪些优势”的信息分享,现在您对小程序生态发展是不是有更深入的了解呢,更多关于小程序的资讯或有小程序开发的需求,请继续关注千优网,一个集小程序开发、发布、推广的综合性平台!
2020-05-09 - 用户引导组件开发(1)理下思路
个人开发者一枚。。之前为了偷懒,做的小程序都没有好好地做用户引导,导致新用户上手的不多。 所以这次下决心好好地做一个用户引导。以下是最终效果(当然,我目前还没上新版本,所以你就算找到这个小程序也看不到这个效果): [图片] 初步设想 做之前,先说说我想做成怎样的。 1、要井水不犯河水。我的几个小程序功能都已经做全了,我不想再为了这个用户引导功能去大改里面的逻辑,包括页面啥的。所以用户引导的功能实现最好能独立模块,与原来的代码尽量低耦合。 2、要能复用。用户引导可能会在各个小程序的不同页面中出现,所以必须能够复用。 因为这两个原因,做成组件是再好不过的了。把各个用户引导界面所需要展示的元素用配置数组的方式传给组件,再由组件去呈现出来。 配置数组 初步设计中,配置数组应该包括如下内容: 1、突出元素。即在遮罩层中需要突出展示的页面元素,如上面示意图中的按钮。配置数组应传入元素ID。 2、说明文字,为了美观,说明文字最好用气泡框。配置数组要传入文字内容,可能要允许多段文字,然后可以配置相关的位置大小样式。 3、说明图片。本人美工不行,但是如果是专业的团队应该会想用更生动的说明图片来替代说明文字。如以下这种的: [图片] 配置数组必须传入图片路径,且应该允许多张图片,并且可以自定义图片的位置大小等。 相关事件 遮罩层的相关事件其实很简单,就是点击。一点就没了。所以我们必须把这个事件再反馈出来。 大致想法就是这些,我们开工吧。
2020-05-10 - 单张、多张图片上传(图片转base64格式)实践经验
定义初始数据: data: { imgList: [], // 图片集合 baseImg: [], // base64图片集合 maxImg: 8, // 图片上传最高数量(根据需求设置) } 第一步:从本地相册选择图片或使用相机拍照(wx.chooseImage) // 选择图片 selectPictures: function() { const that = this; // 最多上传图片数量 if (that.data.imgList.length < that.data.maxImg) { wx.chooseImage({ count: that.data.maxImg - that.data.imgList.length, // 最多可以选择的图片张数(最大数量-当前已上传数量=当前可上传最大数量) sizeType: "compressed", success: function(res) { for (let i = 0; i < res.tempFilePaths.length; i++) { that.data.imgList.push(res.tempFilePaths[i]); } // 显示图片(同步渲染到页面) that.setData({ imgList: that.data.imgList }) } }) } else { wx.showToast({ title: "最多上传" + that.data.maxImg + "张照片!" }) } } count:最多可以选择的图片张数(默认9) sizeType:所选的图片的尺寸(original-原图,compressed-压缩图) sourceType:选择图片的来源(album-从相册选图,camera-使用相机) 第二步:将图片本地路径转为base64图片格式(wx.getFileSystemManager().readFile) // 图片转base64 conversionAddress: function() { const that = this; // 判断是否有图片 if (that.data.imgList.length !== 0) { for (let i = 0; i < that.data.imgList.length; i++) { // 转base64 wx.getFileSystemManager().readFile({ filePath: that.data.imgList[i], encoding: "base64", success: function(res) { that.data.baseImg.push('data:image/png;base64,' + res.data); //转换完毕,执行上传 if (that.data.imgList.length == that.data.baseImg.length) { that.upCont(that.data.textCont, that.data.baseImg); } } }) } } else { wx.showToast({ title: "请先选择图片!" }) } } filePath:要读取的文件的路径 (本地路径) encoding:指定读取文件的字符编码(ascii,base64,binary,hex......) 第三步:执行上传,把图片数组传输给后端即可 // 执行上传 upCont: function (baseImg) { const that = this; wx.request({ url: "上传地址", method: "POST", data: { imglist: baseImg }, success: function (res) { if (res.data.code == 200) { wx.showModal({ title: "提示", content: "提交成功,棒棒哒!" }) // 清空当前数据 that.data.imgList = []; } else { wx.showModal({ title: "提示", content: "上传失败!" }) } } }) } 删除功能:被选中图片移除当前图片数组 // 删除图片(选中图片移除) delImg: function(e) { const that = this; const index = e.currentTarget.dataset.index; // 当前点击图片索引 that.data.imgList.splice(index, 1); that.setData({ imgList: that.data.imgList }) } tips:点击提交按钮后可以增加显示loading提示框:wx.showLoading(),返回结果后隐藏loading提示框:wx.hideLoading(),此方法可以避免重复点击! 完整代码: 1.js代码(直接复制文中代码即可) 2.wxml <view class="img-list"> <view class="txt">图片 {{imgList.length}} / {{maxImg}}</view> <view class="list"> <!-- 图片展示列表 --> <view class="li" wx:for="{{imgList}}" wx:key="index"> <image class="file" src="{{item}}"></image> <!-- 删除图片 --> <image class="close" src="/images/close.png" data-index="{{index}}" bindtap="delImg"></image> </view> <!-- 添加图片 --> <view class="li" bindtap="selectPictures"> <image class="file" src="/images/upload.jpg"></image> </view> </view> </view> <view class="btn" bindtap="conversionAddress">提 交</view> 3.wxss .img-list{ width: 700rpx; margin: 0 auto;} .img-list .txt{ width: 680rpx; padding: 40rpx 0 20rpx; margin: 0 auto; color: #b2b2b2;} .img-list .list{ width: 700rpx; overflow: hidden;} .img-list .list .li{ width: 160rpx; margin: 10rpx 0 0 10rpx; height: 160rpx; border: 1rpx solid #fff; float: left; position: relative;} .img-list .list .li:last-child{ border: 1rpx solid #f7f7f7;} .img-list .list .li .file{ display: block; width: 160rpx; height: 160rpx;} .img-list .list .li .close{ position: absolute; top: 0; right: 0; width: 44rpx; height: 44rpx; background: #fff;} .btn{ background: #f60; width: 680rpx; border-radius: 10rpx; line-height: 88rpx; color: #fff; text-align: center; margin: 50rpx auto 0;} 效果图: [图片]
2020-03-12 - 如何使用Node.js的Buffers
为什么要有Buffers? 在纯[代码]JavaScript[代码]开发中,unicode编码的字符串也够好用的了,并不需要直接处理二进制数据(straight binary data)。在浏览器环境,大部分数据都是字符串的形式,这是足够的。然而,Node.js是服务器环境,必须要处理TCP流还有文件系统的读取和写入流,这就让[代码]JavaScript[代码]需要处理纯二进制数据了。 其实,要解决这个问题直接使用字符串也是可以的,这也是Node.js一开始的做法。然而,这样的做法有许多问题,也很慢。 所以,记住了,别使用二进制字符串(binary strings),用buffers代替它! 什么是Buffers? 在Node.js里,Buffers是专门设计来处理原始二进制数据的,是Buffer这个类的实例。 每个buffer在V8引擎外都有内存分配。Buffer操作起来和包含数字的数组一样,但是不像数组那样自由设置大小的。并且buffer拥有一系列操作二进制数据的方法。 另外,buffer里的“数字”代表的是byte并且限制大小是0到255(2^8-1) 在哪里可以看到buffers 一般情况,buffer经常可以在读取二进制数据流的时候看到,比如[代码]fs.createReadStream[代码] 用法: 创建buffer 有许多方法可以生成新的buffers: [代码]var buffer = new Buffer(8); [代码] 这个buffer是未初始化的,且包含8个字节(bytes)。 [代码]var buffer = new Buffer([ 8, 6, 7, 5, 3, 0, 9]); [代码] 这个buffer用一个数组的内容来初始化。记住了,数组里的数字表示的是字节(bytes) [代码]var buffer = new Buffer("I'm a string!", "utf-8") [代码] 通过第二个参数来指定编码(默认是utf-8)的字符串来初始化buffer。utf-8是在Node.js里最常用的编码,但是buffer还支持其他编码: “ascii”:这个编码方式很快,但是只限制ascii字符集。而且这个编码会将null转换成空格,而不像utf-8编码。 “ucs2”:一种双字节,小端存储的编码。可以编码一个unicode的子集。 “base64”:Base64字符串编码。 “binary”:这个“二进制字符串”前面提到过,这个编码即将被弃用,避免使用这个。 写入buffer 创建一个buffer: [代码]> var buffer = new Buffer(16); [代码] 开始写入字符串: [代码]> buffer.write("Hello", "utf-8") 5 [代码] [代码]buffer.write[代码]的第一个参数是写入buffer的字符串,而第二个参数是这个字符串的编码方式。如果字符串的编码是utf-8,那么这个参数是多余的。 [代码]buffer.write[代码]返回5,这代表我们写入了5个字节到这个buffer。事实上,“Hello“这个字符串也刚好是5个字符。这是因为刚好每个字符都是8位(bits)。这对补全字符串很重要: [代码]> buffer.write(" world!", 5, "utf-8") 7 [代码] 当[代码]buffer.write[代码]有3个参数的时候,第二个参数代表是偏移量,或者说是buffer开始写入的位置。 读取buffer toString: 这个方法可能是读取buffer最通用的方法了,因为很多buffer都包含文本: [代码]> buffer.toString('utf-8') 'Hello world!\u0000�k\t' [代码] 再一次,第一个参数代表编码方式。这里可以看到并没有用完整个buffer。幸运的是,我们知道写入了多少字节到这个buffer,我们可以简单地增加参数去割开这个字符串: [代码]> buffer.toString("utf-8", 0, 12) 'Hello world!' [代码] 独立字节: 你可以看到用类似数组的语法来设置独立位(individual bits) [代码]> buffer[12] = buffer[11]; 33 > buffer[13] = "1".charCodeAt(); 49 > buffer[14] = buffer[13]; 49 > buffer[15] = 33 33 > buffer.toString("utf-8") 'Hello world!!11!' [代码] 在这个例子里,手动地设置剩余的字节,这样就代表了“utf-8”编码的“!”和“1“字符了。 更多有趣用法 Buffer.isBuffer(object) 这个方法是检测一个对象是否是buffer,类似于[代码]Array.isArray[代码] Buffer.byteLength(string, encoding) 通过这个方法,你可以获取字符串(默认utf-8编码)的字节数。这个长度和字符串的长度(string length)不一样,因为很多字符需要更多的字节,例如: [代码]> var snowman = "☃"; > snowman.length 1 > Buffer.byteLength(snowman) 3 [代码] 这个unicode的雪人只有两个字符,却占了3个字节。 buffer.length 这个是buffer的长度,也代表分配了多少内存。这个不等于buffer内容的大小,因为buffer有可能是没满的,比如: [代码]> var buffer = new Buffer(16) > buffer.write(snowman) 3 > buffer.length 16 [代码] 在这个例子里,我们只写入了3个字符,但是长度依然是16,因为这是已经初始化了的。 buffer.copy(target, targetStart=0, sourceStart=0, sourceEnd=buffer.length) [代码]buffer.copy[代码]允许拷贝一个buffer的内容到另一个buffer。 第一个参数表示目标buffer,就是要写入内容的buffer。 另外一个参数是指定需要拷贝到目标buffer的开始位置。看个例子: [代码]> var frosty = new Buffer(24) > var snowman = new Buffer("☃", "utf-8") > frosty.write("Happy birthday! ", "utf-8") 16 > snowman.copy(frosty, 16) 3 > frosty.toString("utf-8", 0, 19) 'Happy birthday! ☃' [代码] 在这个例子,拷贝了含有3个字节长度的“snowman”buffer到“forsty”buffer。 其中forsty一开始写入了前16个字节,而snowman有3个字节长,因此结果就是19个字节长。 buffer.slice(start, end=buffer.length) 这个方法的API可以说和[代码]Array.prototype.slice[代码]是一样的。 不过其中一个特别重要的区别是:这个slice方法不是简单地返回一个新的buffer,也不仅仅是内存中子集的引用。这个slice会改变原来的buffer!举例: [代码]> var puddle = frosty.slice(16, 19) > puddle.toString() '☃' > puddle.write("___") 3 > frosty.toString("utf-8", 0, 19) 'Happy birthday! ___' [代码] 完。 原文链接:https://docs.nodejitsu.com/articles/advanced/buffers/how-to-use-buffers/
2020-03-11 - 【云开发·云函数】分层(layer)实现代码复用,云函数之间共用底层文件
导读 当我们有很多云函数时,其中可能很多 [代码]中间件[代码] 、 [代码]工具[代码] 是共用的。 把这些 [代码]中间件[代码] 、 [代码]工具[代码] 复制到每个云函数下,是一件很麻烦的事情,而且如果发生了更新,还要重新复制。最重要的是,这种方式,不符合科学的设计模式。 云函数里的 层(layer) 就可以很好的解决我们这个问题。它是把我们共通需要的 [代码]中间件[代码] 、 [代码]工具[代码] 等文件放在一个公共的路径下,使用过程中只需要引用进来即可。 注意: 层暂时还没出现在小程序官方文档,但是可以在 [代码]腾讯云 - 云函数[代码]找到文档。 实操 这里我们使用 [代码]Nodejs8.9[代码] 来做示范 1. 先确认小程序已经开通了云开发 2. 登录腾讯云 打开登录界面 选择【其他登录方式 - 微信公众号 - 扫码选择对应的小程序(不是公众号)】. [图片] 注意:小程序云开发和腾讯云的账户体系其实是有关联的,可以把腾讯云面板看作更高级的云开发面板 3. 编写层代码 编写以下代码,并保存为 [代码]demo.js[代码] [代码]exports.main = async (event) => { return "hello,layer." } [代码] 把 [代码]demo.js[代码] 打包成 [代码]demo.zip压缩包[代码] 4. 创建分层 打开云函数面板. 点击左侧菜单的『层』 地域选择『上海』 点击『新建按钮』 层名称:demo 提交方法:本地上传zip包 层代码:选择上一步打包的 [代码]demo.zip[代码] 压缩包 运行环境:Nodejs8.9 [图片] 点击提交 注意: 有些压缩软件压缩后,会多一层文件夹,需要注意下,压缩包内打开应该直接就是文件 [代码]demo.js[代码] 没有多一层文件夹。 5. 小程序·云开发面板创建云函数 创建云函数 [图片] 修改 [代码]云函数 demo 下的 index.js[代码] 代码并上传 [代码]var hello = require("demo.js") exports.main = (event, context, callback) => { return hello.main() }; [代码] 6. 云函数绑定层 回到 腾讯云 云函数面板,选择左侧菜单 [代码]函数服务[代码] 注意左上角的 命名空间 选择小程序云开发的命名空间,地域:上海 选择刚创建的 [代码]demo[代码] 云函数 点击 [代码]层管理[代码] 绑定刚创建的 [代码]demo[代码] 层 7. 调试 还是 [代码]demo[代码] 的云函数里,点击 [代码]函数代码[代码] 项 点击测试,返回结果 [代码]"hello,layer."[代码] 既成功 [图片] Q&A 问:为什么云函数里可以直接[代码]require("demo.js")[代码] 引入 [代码]demo.js[代码] ? 答:绑定层后,会根据顺序把层文件放在系统目录 [代码]/opt[代码] 下,然后该目录在 Node.js 环境下又在环境变量里。当引入 [代码]demo.js[代码] 在当前目录找不到时,会尝试在环境变量内查找。所以云函数可以找到该文件。 再问:如果层的文件名和云函数列表的文件名冲突,岂不是会引入错误? 答:是的,这时候可以使用绝对路径确保引入的是层文件 [代码]require("/opt/demo.js")[代码] 再再问:如果云函数绑定了多个层,每个层里都有 [代码]demo.js[代码] 会怎么样? 答:会根据层的顺序,逐个覆盖文件,最终 [代码]/opt/demo.js[代码] 文件是最后一层的文件。 再再再问:环境变量除了这个路径,还有哪些路径? 答:这里有一份各语言的环境变量路径,可以参考下,具体查看文档 关环境变量 路径 THONPATH [代码]/var/user:/opt[代码] ASSPATH [代码]/var/runtime/java8:/var/runtime/java8/lib/*:/opt[代码] DE_PATH [代码]/var/user:/var/user/node_modules:/var/lang/node6/lib/node_modules:/opt:/opt/node_modules[代码] 问:云函数只支持 [代码]Nodejs8.9[代码] 版本吗? 答:在小程序云开发面板里,确实只能创建。但是可以通过服务端 SDK 创建以下语言支持。 Python 3.6 Python 2.7 Nodejs 10.15 Nodejs 8.9 Nodejs 6.10 Php 7.2 Php 5.6 Java 8 Golang 1 持续更新… 参考资料 腾讯云·云函数文档
2020-05-26 - 前端加载优化及实践
大家都知道产品体验的重要性,而其中最重要的就是加载速度,一个产品如果打开都很慢,可能也就没有后面更多的事情了。这篇文章是我最近项目中的一些加载优化总结,欢迎大家一起讨论交流。 内容包括: 性能指标及数据采集 性能分析方法 性能优化方法 性能优化具体实践 第一部分:性能指标及数据采集 要优化性能首先需要有一套用来评估性能的指标,这套指标应该是是可度量、可线上精确采集分析的。现在来一起看看如何选择性能指标吧。 1. 性能指标 加载的过程是一个用户的感知变化的过程。所以我们的页面性能指标也是要以用户感知为中心的。下面是google定义了几个以用户感知为中心的性能指标。 1.1 以用户感知为中心的性能指标 首先确定页面视觉的变化传递给用户的感知变化关键点: 感知点 说明 发生了吗? 浏览是否成功。 有用了吗? 是否有足够的内容呈现给用户。 可用了吗? 用户是否可以和页面交互了。 好用吗? 用户和应用交互是否流畅自然。 我这里讲的是加载优化,所以第四点暂时不讨论。下面是感知点相关的性能指标。 First paint(FP) and first contentful paint(FCP) FP: Webview跳转到应用的首次渲染时间。 FCP:Webview首次渲染内容的时间:文本,图像(包括背景图像),非白色画布或SVG。这是用户第一次消费内容的时间。 Chrome支持用Paint Timing API获取这两个值: [代码] performance.getEntriesByType("paint") [代码] First meaningful paint(FMP) 首次绘制有效内容的时间,用来表明这个应用是否绘制了有效内容。比如天气应用可以看到天气了,商品列表可以看到商品了。 Time to Interactive(TTI) 应用可交互时间,这时应用渲染完成且可以响应用户输入的时间。这种情况下JS已经加载完成且主线程处于空闲状态。 Speed index 速度指标:代表填充页面内容的速度。要想降低速度指标分数,您需要让加载速度从视觉上显得更快,也就是渐进式展示。 上面指标对应的感知点如下: 感知点 说明 发生了吗? FP/FCP 有用了吗? FMP 可用了吗? TTI Speed index是个整体效果指标所以没有对应上面的任何一个,但也同时对应任何一个。 对于实际项目中我们选取指标要便于采集,下面是针对我的实际项目(APP内的单页面应用)选取的性能指标。 1.2 实际项目选取的性能指标 Webview加载时间 反应Webview性能。这样就可以更真实的知道我们应用的加载情况。 页面下载时间 反应浏览成功时间。 应用启动时间 反应应用启动完成时间,这个时候页面初始化完成,是JS首次执行完成的时间,应用所需异步请求都已经发出去了。 首次有效绘制内容时间 已经有足够的内容呈现给用户,是首屏所需重要接口返回且DOM渲染完成的时间,这个时间由程序员自行判断。 应用加载完成时间 应用完整的呈现给了用户,这个时候页面中所有资源都已经下载好,包括图片等资源。 这里我们的性能指标确定了,下面看看这些数据怎么采集吧。 2. 数据采集 performance.timing为我们提供页面加载每个过程的精确时间,如下图: [图片] 是不是很完美,这足够了?还不够,我们还需要加上原生APP为我们提供的点击我们应用的时间和我们自己确定的FMP才够完美。 下面是每个指标的获取方法: 公用代码部分 [代码]let performance = window.performance || window.msPerformance || window.webkitPerformance; if (performance && performance.timing) { let t = performance.timing; let navigationStart = t.navigationStart; //跳转开始时间 let enterTime = ""; //app提供的用户点击应用的时间,需要和app沟通传递方式 //... 性能指标部分 } [代码] Webview加载时间 [代码] let webviewLoaded = navigationStart - enterTime; [代码] 注意:enterTime应该是客户端ms时间戳,不是服务器时间。 页面下载时间 [代码] let pageDownLoadedTime = t.responseEnd - navigationStart; [代码] 应用启动时间 [代码]let appStartTime = t.domContentLoadedEventStart - navigationStart; [代码] 首次有效绘制内容时间 这里我们需要在有效绘制后调用 [代码]window._fmpTime = +(new Date())[代码]获取当前时间戳。 [代码]let fmpTime = window._fmpTime - navigationStart; [代码] 应用加载完成时间 [代码]let domCompleteTime = t.domComplete - navigationStart; [代码] 最后在document load以后使用上面代码就可以收集到性能数据了,然后就可以上报给后台了。 [代码]if (document.readyState == 'complete') { _report(); } else { window.addEventListener("load", _report, false); } [代码] 这样就封装了一个简单性能数据采集上报组件,这是非常通用的可以用在类似项目中使用只要按照标准提供enterTime和window._fmpTime就可以。 3. 数据分析 有了上面的原始数据,我们需要一些统计方法来观察性能效果和变化趋势,所以我们选取下面一些统计指标。 平均值 注意在平均值计算的时候要设置一个取值范围比如:0~10s以防脏数据污染。 平均值的趋势用折线图展示: [图片] 分布占比 可以清晰的看到用户访问时间的分布,这样你就可以知道有多少用户是秒开的了。 分布占比可以使用折线图、堆积图、饼状图展示: [图片] [图片] [图片] 第二部分:性能分析方法 上面有了性能指标和性能数据,现在我们来学习一下性能分析的一些方法,这样我们才能知道性能到底哪里不行、为什么不行。 1. 影响性能的外部因素 分析性能最重要的一点要确定外部因素。经常会有这种情况,有人反应页面打开速度很慢,而你打开速度很快,其实可能并不是页面性能不好,只是外部因素不同而已。 所以做好性能优化不能只考虑外部因素好的情况,也要让用户能在恶劣条件(如弱网络情况)下也有满足预期的表现。下面看看影响性能的外部因素主要有哪些。 1.1 网络 网络可以说是最影响页面性能最重要的外部因素了,网络的主要指标有: 带宽:表示通信线路传送数据的能力,即在单位时间内通过网络中某一点的最高数据率,单位有bps(b/s)、Kbps(kb/s)、Mbps(mb/s)等。常说的百兆带宽100M就是100Mbps,理论下载最大速度12.5MB/s。 时延:Delay,指数据从网络的一端传送到另一端所需的时间,反应的网络畅通程度。 往返时间RTT:Round-Trip Time,是指从发送端发送数据到接收端接受到确认的总时间。我们经常用的ping命令就是用这个指标表明我们和目标主机的网络顺畅程度。比如我们要对比几个翻墙代理哪里个好,我们就可以ping一下,看看这几个代理哪个RTT低来作出选择。 [图片] 这三个主要指标中后面两个类似,在Chrome中模拟网络主要用设置带宽和网络延迟(往返时间RTT出现最小延迟)来模拟网络。我们电脑一般用的是WI-FI(百兆),那么我们模拟网络,主要模拟常见3G(1兆)、4G(10兆)网络就好,这样我们就覆盖了三个级别的网络情况了。 可以在Chrome的NetWork面板直接选取Chrome模拟好的网络,这个项目network-emulation-conditions中有默认模拟网络的速度。 [图片] 如果默认不满足,你也可以自己配置网络参数,在设置面板的Throttling。 [图片] 上面设置的3G接近100KB/s,4G 0.5MB/s。你可以根据自己的需要来调整这个值,这两个值的差异应该能很好两种不同的网络情况了。设置模拟网络只要能覆盖不同的带宽情况就好,也不用那么真实因为真实情况很复杂。网络部分就介绍完了,接着看其他因素。 1.2 用户机器性能 经常会有这种情况,一个应用在别人手机上打开速度那么快、那么流畅,为啥到我这里就不行了呢?原因很简单人家手机好,自然有更好的配置、更多的资源让程序运行的更快。 Chrome现在非常强大你可以通过performance面板来模拟cpu性能。也可以让你看到应用在低性能机器上的表现。 [图片] 1.3 用户访问次:首次访问、2次访问、发版本访问 用户访问次数也是分析性能的重要外部因素,当用户第一次访问要请求所有资源,后面在访问因为有些资源缓存了访问速度也会不同。当我们开发者又发版本,会更新部分资源,这样访问速度又会跟着变。因为缓存的效果存在,所以这三种情况要分开分析。同时也要注意我们是否要支持用户离线访问。 通过在Chrome中的Network面板中选中Disable cache就可以强制不缓存了,来模拟首次访问。 [图片] 1.4 因素对选取 上面的外部因素虽然只有3种但相乘也有不少情况,为了简化我们性能分析,要选取代表性的因素去分析我们的性能。下面是指导因素对: 网络:WIFI 3G 4G 用户访问状态:首次 2次 这样有6种情况不算特别多,也能很好反应我们应用在不同情况下的性能。 2. devtools具体分析性能 通过devtools可以观察在不同外部因素下代码具体加载执行情况,这个工具是我们性能分析中最重要的工具,加载优化这里我们主要关注两个面板:Network、Performance。 先看Network面板的列表页: [图片] 这是网络请求的列表,右击表头可以增删属性列,根据自己需要作出调整。 下面我介绍网络列表中的几个重点属性: Protocol:网络协议,h2说明你的请求是http2协议的了。 Initiator:可以查到这个资源是哪里引用的。 Status:网络状态码。 Waterfall:资源加载瀑布流。 下面在看看Network面板中单个请求的详情页: [图片] 这里可以看到具体的请求情况,Timing面板是用来观察这次网络的请求时间占用的具体情况,对我们性能分析非常重要。具体每个时间段介绍可以点击Explanation。 虽然Network面板可以让我看到了网络请求的整体和单个请求的具体情况,但Network面板整体请求情况看着并不友好,而且也只有加载情况没有浏览器线程的执行情况。下面看看强大的Performance面板的吧。 [图片] 这里可以清晰看到浏览器如何加载资源如何解析html、解析css、执行js和渲染绘制的。 Performance简直太强大了,所以请你务必要掌握它的使用,这里篇幅有限,只能介绍了个大概,建议到google网站仔细学习一下。 3. Lighthouse整体分析性能 使用Lighthouse可以对应用做整体性能分析评分,并且会给我们专业的指导建议。我们可以安装Lighthouse插件或者安装Lighthouse npm包来使用它。 检测结果中可以看到很多性能指标的分值和建议。你也可以去测试下你的应用表现。 4. 线上用户统计分析性能 虽然使用devtools和Lighthouse可以知道页面的性能情况,但我们还要观察用户的真实访问情况,这才能真实反映我们应用的性能。线上数据采集分析,第一步部分已经介绍过了,这里就不在多说了。优化完看看自己对线上数据到底造成了什么影响。 上面介绍了性能分析的方法,可以很好帮你去分析性能,有了性能分析的基础,下面我们在来看看怎么做性能优化吧。 第三部分:性能优化方法 1. 微观:优化单次网络请求时间 在性能分析知道Network面板可以看到单次网络请求的详情 [图片] 从图可以看出请求包括:DNS时间、TCP时间、SSL时间(https)、TTFB时间(服务器处理时间)、ContentLoaded内容下载时间,所以有下面公式: [代码]requestTime = DNS + TCP + SSL+ TTFB +ContentLoaded [代码] 所以只要我们降低这里面任意一个值就可以降低单次网络请求的时间了。 2. 宏观:优化整体加载过程 加载过程的优化就是不断让第一部分的性能指标感知点提前的过程。通过关键路径优化、渐进式展示、内容效率优化手段,来优化资源调度。 2.1 加载过程 在介绍页面加载过程,先看看渲染绘制过程: [图片] Javascript:操作DOM和CSSOM。 样式计算:根据选择器应用规则并计算每个元素的最终样式。 布局:浏览器计算它要占据的空间大小及其在屏幕的位置。 绘制:绘制是填充像素的过程。 合成。由于页面的各部分可能被绘制到多层,合成是将他们按正确顺序绘制到屏幕上,正确渲染页面。 渲染其实是很复杂的过程这里只简单了解一下,想深入了解可以看看这篇文章。 了解了渲染绘制过程,在学习加载过程的时候就可以把它当作黑盒了,黑盒只包括渲染过程从样式计算开始,因为上面的Javascript主要是用来输入DOM、CSSOM。 浏览器加载过程: Webview加载 下载HTML 解析HTML:根据资源优先级加载资源并构建DOM树 遇到加载同步JS资源暂停DOM构建,等待CSSOM树构建 CSS返回构建CSSOM树 用已经构建的DOM、CSSOM树进行渲染绘制 JS返回执行继续构建DOM树,进行渲染绘制 当HTML中的JS执行完成,DOM树第一次完整构建完成触发:domContentLoaded 当所有异步接口返回后渲染制完成,并且外部加载完成触发:onload 注意点: CSSOM未构建好页面不会进行任何渲染 脚本在文档的何处插入,就在何处执行 脚本会阻塞DOM构建 脚本执行要等待CSSOM构建完成后执行 下面看看如何在加载过程提前感知点。 2.2 优化关键路径 把关键路径定义为:从页面请求到应用启动完成这个过程,也就是到JS执行完domContentLoaded触发的过程。 主要指标有: 关键资源: 影响应用启动完成的资源。 关键资源的数量:这个过程中加载的资源数据。 关键路径长度:关键资源请求的串行长度。 关键字节的数量:关键资源大小总和。 [图片] 上图关键资源有:html、css、3个js。关键资源数量:5个。关键字节的数量:5个资源的总大小。关键路径长度:2,html+剩余其他资源。 关键优化路径优化,就是要降低关键路径长度、关键字节的数量,在http1时代还要降低关键资源的数量,现在http2资源数不用关心。 2.3 优化内容效率 主要是关注的应用加载完成这个时间点,由首页加载完成所需的资源量决定。我们要尽量减少加载资源的大小,避免不必要加载的资源,比如做一些图片压缩懒加载尽快让应用加载完成。 主要指标有: 应用加载完成字节数:应用加载完成,所需的资源大小。 这个指标可以从Chrome上观察到,不过要剔除prefetch的资源。这个指标一般不太稳定,因为页面展示的内容不太相同,所以最好在相同内容相同情况下对比。 2.4 渐进式展示 从上面的加载过程中,可以知道渲染是多次的。那样我们可以先让用户看到一个Loading提示、先展示首屏内容。Loading主要优化的是FP/FCP这两个指标,先展示首屏主要是优化FMP。 3. 缓存:优化多次访问 缓存重点强调的是二次访问、发版访问、离线访问情况下的优化。 通过缓存有效减少二次访问、发版访问所要加载资源,甚至可以让应用支持离线访问,而且是对弱网络环境是最有效的手段,一定要善于使用缓存这是你性能优化的利器。 4. 优化手段 优化手段我归纳为5类:small(更小)、pre(更早)、delay(更晚)、concurrent(并发)、cache(缓存)。性能优化就是将这5种手段应用于上面的优化点:网络请求优化、关键路径优化、内容效率优化、多次访问优化。 5. 构建自己可动态改变的优化方法表和检查表 Checklist包括两部分,一个优化方法表,另外一个优化方法检查表。优化方法表是让我们对我们的性能优化方法有个评估和认识,优化方法检查表的好处是,可以清晰的知道你的项目用了哪些优化方法,还有哪些可以尝试做进一步优化,同时作为一个新项目的指导。 优化名:优化方法的名字。 优化介绍:对优化方法做简单的介绍。 优化点:网络请求优化、关键路径优化、内容效率优化、多次访问优化。 优化手段:small、pre、delay、concurrent、cache。 本地效果:选取合适的因素对,进行效果分析,确定预期作用大小。 线上效果:线上效果对比,确定这个优化方案的有效性及实际作用大小。 这样我们就能大概了解了这个效果的好处。我们新引入了一种优化方法都要按这张表的方法进行操作。 优化方法表: 名称 内容 优化名 JS压缩 优化介绍 压缩JS 优化点 关键路径优化 优化手段 small 本地效果 具体本地效果对比 线上效果 线上数据效果 上面是以JS压缩为例的优化方法表。 优化方法检查表: 分类 优化点 是否使用 不适用 问题说明 small JS压缩 √ pre preload/prefetch √ 不需要 通过这张表就能看出我们使用了哪些方法,还有哪些没使用,哪些方法不适用我们。可以很方便的应用于任何一个新项目。 第四部分:性能优化具体实践 现在就看看我在项目中的具体实践吧,项目中使用的技术栈是:Webpack3+Babel7+Vue2,下面我按照优化手段介绍: 1. small(更小) scope-hoisting scope-hoisting(作用域提升):Webpack分析出模块之间的依赖关系,把可以合并到一起模块合并到一起,但不造成冗余,因此只有被一个地方引用的代码可以合并到一起。这样做函数声明会变少,可以让代码更小、执行更快。 这个功能从Webpack3开始引入,依赖于ES2015模块的静态分析,所以要把Babel的preset要设置成[代码]"modules": false[代码]: [代码] ... [ "@babel/preset-env", { "modules": false ... [代码] Webpack3要引入ModuleConcatenationPlugin插件,Webpack4 product模式已经预置该插件: [代码]... new webpack.optimize.ModuleConcatenationPlugin(), ... [代码] [图片] 如上图,不压缩的JS中可以文件中看到CONCATENATED MODULE这就说明生效了。 tree-shaking 摇树:通常用于描述移除JavaScript上下文中的未引用代码,在webpack2中开始内置。依赖于ES2105模块的静态分析,所以我们使用babel同样要设置成 [代码]"modules": false[代码]。 [图片] 如上图,不压缩的JS中可以文件中看到unused harmony这就说明摇树成功了。 code-splitting(按需加载) 代码分片,将代码分离到不同的js中,进行并行加载和按需加载。 代码分片主要有两种: 按需加载:动态导入 vendor提取:业务代码和公共库分离 这里只介绍按需加载部分,动态导入Webpack提供了两个类似的技术。1. Webpack特定的动态导入require.ensure。2.ECMAScript提案[代码]import()[代码]。这里我只介绍我使用的[代码]import()[代码]这种方法。因为是推荐方法。 代码如下: Babel配置支持动态导入语法: [代码]... "@babel/plugin-syntax-dynamic-import", ... [代码] 代码中使用: [代码]... if(isDevtools()){ import(/* webpackChunkName: "devtools" */'./comm/devtools').then((devtools)=>{ let initDevtools = devtools.default; initDevtools(); }); } ... [代码] polyfill按需加载 我们代码是ES2015以上版本的要真正能在浏览器上能使用要通过babel进行编译转化,还要使用polyfill来支持新的对象方法,如:Promise、Array.from等。对于不同环境来说需要polyfill的对象方法是不一样的,所以到了Babel7支持了按需加载polyfill。 下面是我项目中的配置,看完以后我会介绍一下几个关键点: [代码]module.exports = function (api) { api.cache(true); const sourceType = "unambiguous"; const presets = [ [ "@babel/preset-env", { "modules": false, "useBuiltIns": "usage", // "debug": true, "targets": { "browsers": ["Android >= 4.0", "ios >= 8"] } } ] ]; const plugins= [ "@babel/plugin-syntax-dynamic-import", "@babel/plugin-transform-strict-mode", "@babel/plugin-proposal-object-rest-spread", [ "@babel/plugin-transform-runtime", { "corejs": false, "helpers": true, "regenerator": false, "useESModules": false } ] ]; return { sourceType, presets, plugins } } [代码] @babel/preset-env preset是预置的语法转化插件的集合。原来有很多preset如:@babel/preset-es2015。直到出现了@babel/preset-env,它可以根据目标环境来动态的选择语法转化插件和polyfill,统一了preset众多的局面。 [代码]targets[代码]:是我们用来设置环境的,我的应用支持移动端所以设置了上面那样,这样就可以只加载这个环境需要的插件了。如果不设置[代码]targets[代码]通过@babel/preset-env引入的插件是 @babel/preset-es2015、@babel/preset-es2016和@babel/preset-es2017插件的集合。 [代码]"useBuiltIns": "usage"[代码]:将useBuiltIns设置为usage就会根据执行环境和代码按需加载polyfill。 @babel/plugin-transform-runtime 和polyfill不同,@babel/plugin-transform-runtime可以在不污染全局变量的情况下,使用新的对象和方法,并且可以移除内联的Babel语法转化时候的辅助函数。 我们这里只用它来移除辅助函数,不需要它来帮我处理其他对象方法,因为我们在开发应用不是做组件不怕全局污染。 sourceType:“unambiguous” 一个文件混用了ES2015模块导入导出和CJS模块导入导出。需要设置[代码]sourceType:"unambiguous"[代码],需要让babel自己猜测类型。如果你的代码都很合规不用加这个的。 压缩:js、css js、css压缩应该最基本的了。我在项目中使用的是[代码]UglifyJsPlugin[代码]和[代码]optimize-css-assets-webpack-plugin[代码],这里不做过多介绍。 压缩图片 通过对图片压缩来进行内容效率优化,可以极大的提前应用加载完成时间,我在项目中做了下面两件事。 广告图片,限制大小50K以内。原来基本会上传超过100K的广告图。 项目中图片使用的[代码]img-loader[代码]对图片进行压缩。 HTTP2支持,去掉css中base64图片 先看看HTTP1.1中的问题: 同一域名浏览器做了TCP连接数的限制,如:Chrome中只能有6个。 一个TCP连接只能同时处理一个请求响应。 在看看HTTP2的优势: 二进制分帧:HTTP2的性能增强的核心在于新的二进制分帧层。帧是最小传输单位,帧组成消息,数据以消息形式发送。 多路复用:所有请求在一个连接上完成,可以支持多数据流混合传输,在接收端拼接。 头部压缩:使用HPACK对头部压缩,网络中可以传递更少的数据。 服务端推送:服务端可以主动向客户端推送资源。 有了HTTP2我们在也不用担心资源数量,不用在考虑减少请求了。像:base64图片打到css、合并js、域名分片、精灵图都不要去做了。 这里我把原来base64压缩图片从css中去除了。 2. pre(更早) preload prefetch preload:将资源加载和执行分离,你可以根据你的需要指定要强制加载的资源,比如后面css要用到一个字体文件就可以在preload中指定加载,这样提高了页面展示效果。建议把首页展示必须的资源指定到preload中。 prefetch:用来告诉浏览器我将来会用到什么资源,这样浏览器会在空闲的时候加载。比如我在列表页将详情页js设置成prefetch,这样在进入详情页的时候速度就会快很多,因为我提前加载好了。 这里我用的是来使用[代码]preload-webpack-plugin[代码]preload和prefetch的。 代码: [代码]... const PreloadWebpackPlugin = require('preload-webpack-plugin'); ... new PreloadWebpackPlugin({ rel: 'prefetch', include: ['devtools','detail','VideoPlayer'] }), ... [代码] dns-prefetch preconnect dns-prefetch:在页面中请求该域名下资源前提前进行dns解析。preconnect:比dns-prefetch更近一步连TCP和SSL都为我们处理好了。 使用注意点:1. 考虑到兼容性问题,我们对一个域名两个都设置 2. 对于应用中不一定会使用的域名我们设置dns-prefetch就好以防占用资源。 代码如下: [代码]... <link rel="preconnect" href="//game.gtimg.cn"> ... <link rel="dns-prefetch" href="//game.gtimg.cn"> ... [代码] 3. delay(更晚) lazyload 对图片进行懒加载,我使用的是[代码]vue-lazyload[代码]。 代码如下: [代码]... import VueLazyload from 'vue-lazyload' ... Vue.use(VueLazyload, { preLoad: 1.3, error: '...', loading: '...', attempt: 1 }); ... <div class='v-fullpage' v-lazy:background-image="item.roomPic" :key="item.roomPic"></div> ... [代码] 这里的:key特别注意,如果你的列表数据是动态变化的一定要设置,否则图片是最开始一次的。 code-splitting(按需加载) code-splitting(按需加载)前面已经介绍过这里只是强调下它的delay作用,不使用的部分先不加载。 4. concurrent(并发) HTTP2 HTTP2前面已经应用在了css体积减少,这里主要强调它的多路复用。需要大家看看自己的项目是否升级到HTTP2,是否所有资源都是HTTP2的,如果不是的,需要推进升级。 code-splitting(vendor提取) vendor提取是把业务代码和公共库分离并发加载,这样有两个好处: 下次发版本这部分不用在加载(缓存的作用)。 JS并发加载:让先到并在前面的部分先编译执行,让加载和执行并发。 Webpack配置: [代码] ... entry:{ "bundle":["./src/index.js"], "vendor":["vue","vue-router","vuex","url","fastclick","axios","qs","vue-lazyload"] }, ... new webpack.optimize.CommonsChunkPlugin({ name: "vendor", minChunks: Infinity }), new webpack.optimize.CommonsChunkPlugin({ name: 'manifest' }), ... [代码] 5. cache(缓存) HTTP缓存 HTTP缓存对我们来说是非常有用的。 下面介绍下HTTP缓存的重点: Last-Modified/ETag:用来让服务器判断文件是否过期。 Cache-Control:用来控制缓存行为。 max-age: 当请求头设置max-age=deta-time,如果上次请求和这次请求时间小于deta-time服务端直接返回304。当响应头设置max-age=deta-time,客户端在小于deta-time使用客户端缓存。 强制缓存:这主要把不经常变化的文件设置强制缓存,这样就不需要在发起HTTP请求了。通过设置响应头Cache-Control的max-age设置。 如果像缓存很久设置一个很大的值,如果不想缓存设置成:Cache-Control:no-cahce。 协商缓存:如果没有走强制缓存就要走协商缓存,服务器根据Last-Modified/ETag来判断文件是否变动,如果没变动就直接返回304。 这里我们做的就是让运维调整资源的强制缓存时间,前端在结合文件hash命名就可以进行资源更新了。 ServiceWorker ServiceWorker是Web应用和浏览器之间的代理服务器,可以用来拦截网络来进行资源缓存、离线体验,还可以进行推送通知和后台同步。功能非常强大,我们这里使用的是资源缓存功能,看看和HTTP缓存比有什么优势: 功能多:支持离线访问、资源缓存、推送通知、后台同步。 控制力更强:缓存操作+络拦截功能都由开发者控制,可以做出很多你想做的事情比如动态缓存。 仅HTTPS下可用,更安全。 看看我在项目中的使用: js使用HTTP缓存和ServiceWorker双重缓存在cacheid变化后依然可以缓存。 不得对service-worker.js缓存,因为我们要用这个更新应用。在Chrome中看到请求的cache-control被默认设置了no-cache。 我们项目中使是Google的Workbox,Webpack中插件是 workbox-webpack-plugin。 [代码]... const WorkboxPlugin = require('workbox-webpack-plugin'); ... new WorkboxPlugin.GenerateSW({ cacheId: 'sw-wzzs-v1', // 缓存id skipWaiting: true, clientsClaim: true, swDest: './html/service-worker.js', include: [/\.js(.*)$/,/\.css$/], importsDirectory:'./swmainfest', importWorkboxFrom: 'local', ignoreUrlParametersMatching: [/./] }), ... [代码] localStorage localStorage项目中主要做接口数据缓存。通常localStorage是没有缓存时间的我们将其封装成了有时间的缓存,并且在应用启动的时候对过期的缓存清理。 code-splitting(vendor提取) 这里在提vendor提取主要是说明它发版本时候的缓存价值,前面介绍过了。 6. 整体优化效果评价 经过上面的优化,看看效果提升吧。 主要增长点来源: 关键路径资源:698.6K降低到538.6K降低22.9% 内容效率提升:广告图由原来的基本100K以上降低到现在50K以下,页面内图片全部走强制缓存。 缓存加快多次访问速度:js+css强制缓存加ServiceWorker。 线上数据效果: 页面下载时间: 平均值下降:25.74%左右 应用启动完成时间: 平均值下降:33.45%左右 秒开占比提高:23.42%左右 应用加载完成时间: 平均值下降:48.02%左右 第六部分:总结 以上就是我在加载优化方面的一些总结,希望对您有所帮助,个人理解有限,欢迎一起讨论交流。
2019-03-11 - 深入解析JS的异步机制
1. JavaScript定义 JavaScript 是一种单线程编程语言,这意味着同一时间只能完成一件事情。也就是说,JavaScript 引擎只能在单一线程中处理一次语句。 优点:单线程语言简化了代码编写,因为你不必担心并发问题,但这也意味着你无法在不阻塞主线程的情况下执行网络请求等长时间操作。 缺点:当从 API 中请求一些数据。根据情况,服务器可能需要一些时间来处理请求,同时阻塞主线程,让网页无法响应。 2. 异步运行机制 CallBack,setTimeOut,ajax 等都是通过**事件循环(event loop)**实现的。 2.1 什么是Event Loop? 主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。 2.2 流程整体示意图 [图片] 2.3 总结异步运行到整体机制 主线程在运行的时候,将产生堆(heap)和栈(stack),栈中的代码会调用各种外部API,它们将在"任务队列"中根据类型不同,分类加入到相关任务队列中,如各种事件等。只要栈中的代码执行完毕,主线程就会去读取"任务队列",根据任务队列的优先级依次执行那些事件所对应的回调函数。这就是整体的事件循环。 2.4 任务队列的优先级 微任务队列中的所有任务都将在宏队列中的任务之前执行。也就是说,事件循环将首先在执行宏队列中的任何回调之前清空微任务队列。 ** 举例: ** [代码] console.log('Script start'); setTimeout(() => { console.log("setTimeout 1"); }, 0); setTimeout(() => { console.log("setTimeout 2"); }, 0); new Promise ((resolve, reject) => { resolve("Promise 1 resolved"); }) .then(res => console.log(res)) .catch(err => console.log(err)); new Promise ((resolve, reject) => { resolve("Promise 2 resolved"); }) .then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script end'); [代码] 运行结果是: Script start Script end Promise 1 Promise 2 setTimeout 1 setTimeout 2 通过上述例子可以看到无论宏队列的位置在何方,只要微队列尚未清空,一定会先清空微队列后,在去执行宏队列。下面介绍微队列任务中比较典型的几个API,通过相关举例,让你更深入理解JS的异步机制。 3. 微任务队列 3.1 Promise(ES6) Promise,就是一个对象,用来传递异步操作的消息。 3.1.1 基础用法: [代码] var promise = new Promise(function(resolve, reject) { //异步处理逻辑 //处理结束后,调用resolve返回正常内容或调用reject返回异常内容 }) promise.then(function(result){ //正常返回执行部分,result是resolve返回内容 }, function(err){ //异常返回执行部分,err是reject返回内容 }) .catch(function(reason){ //catch效果和写在then的第二个参数里面一样。另外一个作用:在执行resolve的回调时,如果抛出异常了(代码出错了),那么并不过报错卡死JS,而是会进入到这个catch方法中,所以一般用catch替代then的第二个参数 }); [代码] 缺点: 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。再次,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 优点: Promise能够简化层层回调的写法,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。 3.1.2 用法注意点 - 顺序: [代码] new Promise((resolve, reject) => { resolve(1); console.log(2); }).then(r => { console.log(r); }); [代码] 运行结果是: 2 1 说明: 立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。也就是resolve(1)和console.log(2)是属于同步任务,需要全部执行完同步任务后,再去循环到resolve的then中。 3.1.3 用法注意点 - 状态: [代码] const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000); }); const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000); }); const p3 = new Promise(function (resolve, reject) { setTimeout(() => resolve(new Error('fail')), 1000); }); p2 .then(result => console.log("1:", result)) .catch(error => console.log("2:",error)); p3 .then(result => console.log("3:", result)) .catch(error => console.log("4:",error)); [代码] 运行结果是: 3: Error: fail at setTimeout (async.htm:182) 2: Error: fail at setTimeout (async.htm:174) 说明: p1是一个 Promise,3 秒之后变为rejected。p2和p3的状态是在 1 秒之后改变,p2 resolve方法返回的是 p1, p3 resolve方法返回的是 抛出异常。但由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。 而p3返回的是自身的resolve,所以触发then中指定的回调函数。 3.1.4 用法注意点 - then链的处理: [代码] var p1 = p2 = new Promise(function (resolve){ resolve(100); }); p1.then((value) => { return value*2; }).then((value) => { return value*2; }).then((value) => { console.log("p1的执行结果:",value) }) p2.then((value) => { return value*2; }) p2.then((value) => { return value*2; }) p2.then((value) => { console.log("p2的执行结果:",value) }) [代码] 运行结果是: p2的执行结果: 100 p1的执行结果: 400 说明: p2写法中的 then 调用几乎是在同时开始执行的,而且传给每个 then 方法的 value 值都是 100。而p1中写法则采用了方法链的方式将多个 then 方法调用串连在了一起,各函数也会严格按照 resolve → then → then → then 的顺序执行,并且传给每个 then 方法的 value 的值都是前一个promise对象通过 return 返回的值。 ###3.1.4 用法注意点 - catch的处理: [代码] var p1 = new Promise(function (resolve, reject){ reject("test"); //throw new Error("test"); 效果同reject("test"); //reject(new Error("test")); 效果同reject("test"); resolve("ok"); }); p1 .then(value => console.log("p1 then:", value)) .catch(error => console.log("p1 error:", error)); p2 = new Promise(function (resolve, reject){ resolve("ok"); reject("test"); }); p2 .then(value => console.log("p2 then:", value)) .catch(error => console.log("p2 error:", error)); [代码] 运行结果是: p2 then: ok p1 error: test 说明: Promise 的状态一旦改变,就永久保持该状态,不会再变了。不会即抛异常又会正常resolve。 3.2 async/await(ES7) 3.2.1 async基础用法: async 用于申明一个 function 是异步的,返回的是一个 Promise 对象。 [代码] async function testAsync() { return "hello async"; } var result = testAsync(); console.log("1:", result); testAsync().then(result => console.log("2:", result)); async function mytest() { //"hello async"; } var result1 = mytest(); console.log("3:", result1); [代码] 运行结果是: 1: Promise {<resolved>: “hello async”} 3: Promise {<resolved>: undefined} 2: hello async 说明: async返回的是一个Promise对象,可以用 then 来接收,如果没有返回值的情况下,它会返回 Promise.resolve(undefined),所以在没有 await 的情况下执行 async 函数,它会立即执行,并不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。 3.2.2 await基础用法: await 只能出现在 async 函数中,用于等待一个异步方法执行完成(实际等的是一个返回值,强调 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果)。 [代码] function getMyInfo() { return Promise.resolve("hello 2019!"); } async function testAsync() { return "hello async"; } async function mytest() { return Promise.reject("hello async"); } async function test() { try { const v1 = await getMyInfo(); console.log("getV1"); const v2 = await testAsync(); console.log("getV2"); const v3 = await mytest(); console.log(v1, v2, v3); } catch (error) { console.log("error:", error); } } test(); [代码] 运行结果是: getV1 getV2 error: hello async 说明: await等到的如果是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。 放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。 3.2.3 async/await的优势: 很多情况下,执行下一步操作是需要依赖上一步的返回结果的,如果当嵌套层次较多的时候,(举例3层的时候): [代码] const getRequest = () => { return promise1().then(result1 => { //do something return promise2(result1).then(result2 => { //do something return promise3(result1, result2) }) }) } [代码] 从上例可以看到嵌套内容太多。此时如果用async写法,可写成如下: [代码] const getRequest = async () => { const result1 = await promise1(); const result2 = await promise2(result1); return promise3(result1, result2); } [代码] 说明: async / await 使你的代码看起来像同步代码,它有效的消除then链,让你的代码更加简明,清晰。 总结:以上就是对JS的异步机制及相关应用的整体总结,如有需要欢迎交流~
2019-03-12 - [填坑手册]小程序新版订阅消息+云开发实战与跳坑
[图片] 老版本的订阅消息在2020年1月10日就下线了,相信不少人在接入新版本订阅系统的时候,或多或少会遇到一些问题,这里智库君跟大家介绍下新版订阅的机制和不需要node/后端的情况下 独立完成功能开发。 一、新版订阅的机制 其实开发过程不难,但是要理清楚它里面的机制,智库君还是花了一些时间的,也踩了不少坑 先来看下官方介绍: [图片] 可以设置多个订阅选项 感叹号里面可以看到详情 有个默认不被选中的“总是”选项 这些就是新不同的地方,智库君在开发的时候也有很多疑问,点了“总是”再点“取消”按钮会怎样?部分选择订阅会怎样?下面为大家一一梳理 (1)部分选中 [图片] 比如现在有三个选项 A,B,C,用户**“部分选中”**返回的情况: [图片] 这里用真机调试可以看到,有个返回值状态为“reject”。 如果我们反复几点点击同一个订阅后,这些值是如何计算的呢? 举例: [图片] 从这里看出,微信系统会自动记录用户点击的次数,并且做累加记录,如果用户只允许2次发送,而开发者发送了3次,最后一次将会被拒绝。 (2)点击“总是保持以上选择,不再询问”的情况 [图片] 当用户点击“总是”之后,同一个类型的订阅将不再弹出,那如果有多个订阅选项呢? 举例 订阅AAA 三个订阅模板为 X Y Z 订阅BBB 二个订阅模板为 Y W 这时候如果“订阅AAA”按钮选择了“总是”,那么再点击“订阅BBB”按钮,将只会弹出一个选项“W”,不会有 “Y” 的模板,因为在之前 “订阅AAA” 按钮中已经包含了。 [代码]wx.requestSubscribeMessage({ tmplIds: ["MECDDOdhbC3SrQmMY5XrfqiIGbMTzpEN8Z7ScXJfcd0", "iSb2NIlNnnO60wlI-8Wx5Pe82jR7TRdwjotSXtM1-ww"], success(res) { console.log(res); } }) [代码] 显示内容仅一个选项: [图片] 这里需要注意,“总是”选项是全局有效,不区分页面,选中“总是”的 W,X,Y,Z的模板,在全局任意页面中再次调用,再次调用将不再会显示! [图片] 返回值无提示用户是否选中“总是”。 (3)用户点击“总是”后,获取状态 [图片] [代码]wx.getSetting({ withSubscriptions: true, success(res) { console.log(res.authSetting) // res.authSetting = { // "scope.userInfo": true, // "scope.subscribeMessage": true // } console.log(res.subscriptionsSetting) // res.subscriptionsSetting = { // SYS_MSG_TYPE_INTERACTIVE: 'accept', // SYS_MSG_TYPE_RANK: 'accept', // zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE: 'reject', // ke_OZC_66gZxALLcsuI7ilCJSP2OJ2vWo2ooUPpkWrw: 'ban', // } } }); [代码] [图片] 这里可以调用wx.getSetting方法,但是需要注意:如果用户第一次选“总是”后点击“取消”按钮或者订阅模板全部是未选中/reject的,那将获取不到状态(这里可能是BUG,期待官方未来修复)。 (4)用户点击“总是”后,让用户手动修改 前面说到用户点击“总是”后,系统将不再弹窗,但是我们可以通过**“wx.openSetting”**引导用户手动修改。 [代码]wx.openSetting({ success(res) { console.log(res.authSetting) // res.authSetting = { // "scope.userInfo": true, // "scope.userLocation": true // } } }) [代码] [图片] [图片] 当然用户自己也可以修改 [图片] 总结 【重点】选择“总是”,很多人认为就可无限发送订阅消息,这个是错误的,勾选和不勾选唯一的区别就是每次触发订阅的时候会不会弹授权窗口!!! 用户点击次数系统会自动累加,直接影响后台发送通知的次数。 用户选择“总是”后,小程序界面不再弹窗,但仍然有回调/callback。 任意订阅模板在用户选中“总是”(包括接受/拒绝2个状态)后,全局有效,就算其他订阅包含“此模板”也不再显示/弹出 当用户选择“总是”中“accept/选中/接受”的状态后,可以在wx.getSetting查询到用户是否选择“总是”。 当用户选择“总是”中“reject/未选中/拒绝”的状态后,返回值“无感知”(这里可能是BUG) 二、功能开发 使用微信自带的云开发,可以在没有node/后端开发支持下,完成整个订阅流程的开发。 (1)微信后台设置订阅模板和获取模板ID 1、打开小程序后台,找到订阅消息设置 [图片] 2、在公共模板库找模板或者自己申请新模板,建议能用现成模板用现成的,因为申请周期可能较长,且容易被拒 [图片] 3、选好模板后,点击详情 [图片] 4、查看模板内容和发送DATA的结构 [图片] 5、复制模板ID (2)配置云函数 [图片] [图片] 1、新建getOpenId云函数,用于获取用户的openID [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } } [代码] 2、新建订阅推送通知云函数 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() //订阅推送通知 exports.main = async (event, context) => { try { const result = await cloud.openapi.subscribeMessage.send({ touser: event.openid, //接收用户的openId page: 'pages/my/index', //订阅通知 需要跳转的页面 data: { //设置通知的内容 thing1: { value: '小程序订阅填坑' }, thing2: { value: '智库方程式' }, thing3: { value: '一起学习,一起进步' } }, templateId: '5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac' //模板id }) console.log(result) return result } catch (err) { console.log(err) return err } } [代码] 写完云函数记得右键部署下!!! (3)小程序代码部分 [代码]<!------------html -------------> <button bindtap="getOpenId" type='primary'>获取openId</button> <view class="subBtn" catch:tap="sub">订阅AAA</view> <view class="subBtn" catch:tap="send">订阅推送测试</view> <view class="subBtn" catch:tap="setting">设置“总是”后,跳转修改</view> [代码] [代码]//JS 部分 //获取用户的openid getOpenId() { wx.cloud.callFunction({ name: "getOpenId" }).then(res => { let openid = res.result.openid console.log("获取openid成功", openid) }).catch(res => { console.log("获取openid失败", res) }) }, //发送模板消息给指定的openId用户 send(openid){ wx.cloud.callFunction({ name: "sendSub", data: { openid: openid } }).then(res => { console.log("发送通知成功", res) }).catch(res => { console.log("发送通知失败", res) }); }, //消息订阅 sub: function () { wx.requestSubscribeMessage({ tmplIds: ["5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac"], success(res) { console.log("订阅授权成功:"+res); }, fail(res){ console.log("订阅授权失败:" + res); } }) }, //帮助用户跳转修改订阅状态 setting:function(){ wx.openSetting({ success(res) { console.log(res.authSetting) // res.authSetting = { // "scope.userInfo": true, // "scope.userLocation": true // } } }) }, [代码] (4)测试流程 点击发送通知后,获得这样的效果: [图片] [图片] 获得对应返回值: [图片] 当errCode为0时,即发送通知成功。 当errCode为43101,说明用户只授权了一次,但是你发送了2次,超过用户授权次数。 [图片] 三、进阶与思考 1、当你有多个订阅模板同时需要用户选择时,你可以通过以下代码记录,用户哪些选了,哪些没选。 [代码]wx.requestSubscribeMessage({ tmplIds: ["5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac", "OBB_Z10eh_Inm9p8EU6Ml_NS_mijXgTz3T07cxgKvX0","5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac"], success(res) { //console.log(res); if (res.errMsg == "requestSubscribeMessage:ok") { let acceptArray = []; //用户授权模板列表 for (let i = 0; i < tmplIds.length; i++) { const element = tmplIds[i]; if (res[element] == "accept") { acceptArray.push(element); } }; console.log(acceptArray); if (acceptArray.length > 0) { //执行下一步函数 } } } }) [代码] 2、一个关于是否需要记录用户对某个“订阅模板授权的次数”,以控制后台“发送的次数”,智库君在实战中认为,其实没有必要,顶多就是你发送返回一个错误码,微信之所有记录用户授权次数,也是为了保护用户不被骚扰。 3、你只需要记录用户点击了哪些需要授权的模板就行,为了是用户点击订阅后,改变按钮的状态,避免订阅按钮反复弹窗的问题,同时当检测到用户点错“总是”按钮后,可以自动跳转到“设置”界面。 4、这次智库君主要给大家简单介绍了下订阅全流程。后面大家可以根据自己的需要,添加和改进这些代码。比如: 配置云函数中的node函数,实现定时发送 配置云函数中的数据库,实现内容的自定义发送 最后,希望这篇文章能帮助到大家,一起学习,一起进步! (官方文档地址:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html) 往期回顾: [打怪升级]小程序自定义头部导航栏“完美”解决方案 [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二)
2021-09-13 - 云开发批量上传图片,上传完图片再上传数据库 [即抄即用,拎包入住]
大家好,又是我拎包哥,今天我们来实现在云开发中批量上传图片。 经过Stephen哥的指正,我改用了Promise.all的方法来达到目的。 Promise.all的作用就是等待所包含的promise函数结束后再执行下一步逻辑,非常方便好用!const db = wx.cloud.database() const test = db.collection('test') Page({ onLoad() { this.imgList = [] wx.chooseImage({ success: (res) => { this.TFP = res.tempFilePaths } }) }, btn() { let promiseMethod = new Array(this.TFP.length) for (let i = 0; i < this.TFP.length; i++) { promiseMethod[i] = wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } Promise.all([...promiseMethod]).then(() => { test.add({ data: { imgList: this.imgList } }) }) } }) --------------------------------------我是分割线-------------------------------------- async await 要点: ctrl c + ctrl v这里用了await阻塞在wx.cloud.uploadFile前面,避免还没上传完图片就往数据库插入数组。减少了then里的代码,美观逼格高。嘻嘻嘻。await wx.cloud.uploadFile不能放在wx.chooseImage里,如果可以的话,请告诉我怎么做,谢谢!欢迎交流,指出错误,我立刻修改么么哒。 标准版 const db = wx.cloud.database() const test = db.collection('test') Page({ onLoad() { this.imgList = [] wx.chooseImage({ success: (res) => { this.TFP = res.tempFilePaths } }) }, async btn() { this.imgList = [] console.log(this.TFP) for (let i = 0; i < this.TFP.length; i++) { await wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } test.add({ data: { imgList: this.imgList } }) } }) 新手最爱一锅炖版(不推荐) 为什么不推荐呢,因为选择图片并不意味着要上传图片,用户还没进行最终的确定操作(不过可以用来了解async await)。 onLoad() { this.imgList = [] wx.chooseImage({ success: async res => { this.TFP = res.tempFilePaths for (let i = 0; i < this.TFP.length; i++) { await wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } test.add({ data: { imgList: this.imgList } }) } }) } [图片] ==========================end==========================
2020-05-17 - 干货:如何借助小程序云开发实现小程序支付功能(含源码)
正文共:5081 字 13 图 预计阅读时间:13 分钟 我们在做小程序支付相关的开发时总会遇到这些难题 1.小程序调用微信支付时必须要有自己的服务器 2.有自己的备案域名 3.有自己的后台开发 这就导致我们做小程序支付时的成本很大 本节就来教大家如何使用小程序云开发实现小程序支付功能的开发,不用搭建自己的服务器,不用有自己的备案域名,只需要简简单单的使用小程序云开发。 老规矩先看效果图: [图片] 本节知识点 1.云开发的部署和使用 2.支付相关的云函数开发 3.商品列表 4.订单列表 5.微信支付与支付成功回调 [图片] 支付成功给用户发送推送消息的功能会在后面讲解 下面就来教大家如何借助云开发使用小程序支付功能 支付所需要用到的配置信息 1.小程序appid 2.云开发环境id 3.微信商户号 4.商户密匙 一、准备工作 1.已经申请小程序,获取小程序 AppID 和 Secret 在小程序管理后台中——【设置】 →【开发设置】 可以获取微信小程序 AppID 和 Secret。 [图片] 2.微信支付商户号,获取商户号和商户密钥在微信支付商户管理平台中——【账户中心】→【商户信息】 可以获取微信支付商户号。 [图片] 在【账户中心】 ‒> 【API安全】 可以设置商户密钥。 [图片] 这里特殊说明下——个人小程序是没有办法使用微信支付的,所以如果想使用微信支付功能必须是非个人账号(当然个人可以办个体户工商执照来注册非个人小程序账号) 3.微信开发者 IDE https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 4.开通小程序云开发功能 https://edu.csdn.net/course/play/9604/204526 二、商品列表的实现 效果图如下 由于本节重点是支付的实现所以这里只简单贴出关键代码 [图片] wxml布局如下: [代码]<view class="container"> <view class="good-item" wx:for="{{goods}}" wx:key="*this" ontap="getDetail" data-goodid="{{item._id}}"> <view class="good-image"> <image src="{{pic}}"></image> </view> <view class="good-detail"> <view class="title">商品: {{item.name}}</view> <view class="content">价格: {{item.price / 100}} 元 </view> <button class="button" type="primary" bindtap="makeOrder" data-goodid="{{item._id}}" >下单</button> </view> </view></view> [代码] 我们所需要做的就是借助云开发获取云数据库里的商品信息然后展示到商品列表,关于云开发获取商品列表并展示本节不做讲解(感兴趣的同学可以翻看作者历史博客,有写过的) [图片] 三、支付云函数的创建 首先看下我们支付云函数都包含那些内容 [图片] 简单先讲解下每个的用处 config下的index.js是做支付配置用的,主要配置支付相关的账号信息 lib是用的第三方的支付库,这里不做讲解 重点讲解的是云函数入口 index.js 下面就来教大家如何去配置 1.配置config下的index.js, 这一步所需要做的就是把小程序appid、云开发环境ID、商户id、商户密匙填进去。 [图片] 2.配置入口云函数 [图片] 详细代码如下 代码里注释很清楚了这里不再做单独讲解: [代码]const cloud = require('wx-server-sdk') cloud.init()const app = require('tcb-admin-node');const pay = require('./lib/pay');const { mpAppId, KEY } = require('./config/index');const { WXPayConstants, WXPayUtil } = require('wx-js-utils'); const Res= require('./lib/res'); const ip = require('ip');/** * * @param {obj} event * @param {string} event.type 功能类型 * @param {} userInfo.openId 用户的openid */exports.main = async function(event, context) { const { type, data, userInfo } = event; onst wxContext = cloud.getWXContext() const openid = userInfo.openId; app.init(); const db = app.database (); const goodCollection = db.collection('goods'); const orderCollection = db.collection('order');// 订单文档的status 0 未支付 1 已支付 2 已关闭 switch (type) { // [在此处放置 unifiedorder 的相关代码] case 'unifiedorder': { // 查询该商品 ID 是否存在于数据库中,并将数据提取出来 const goodId = data.goodId let goods = await goodCollection.doc(goodId).get(); if (!goods.data.length) { return new Res ({ code: 1, message: '找不到商品' }); } // 在云函数中提取数据,包括名称、价格才更合理安全, // 因为从端里传过来的商品数据都是不可靠的 let good = goods.data[0]; // 拼凑微信支付统一下单的参数 const curTime = Date.now(); const tradeNo =`${goodId}-${curTime}`; const body = good.name; const spbill_create_ip = ip.address() || '127.0.0.1'; // 云函数暂不支付 http 触发器,因此这里回调 notify_url 可以先随便填。 const notify_url = 'http://www.qq.com'; // '127.0.0.1'; const total_fee = good.price; const time_stamp = '' + Math.ceil(Date.now() / 1000); const out_trade_no = `${tradeNo}`; const sign_type = WXPayConstants.SIGN_TYPE_MD5; let orderParam = { body, spill_create_ip, notify_url, out_trade_no, total_fee, openid, trade_type: 'JSAPI', timeStamp: time_stamp, }; // 调用 wx-js-utils 中的统一下单方法 const { return_code, ...restData } = await pay.unifiedOrder(orderParam); let order_id = null; if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { const { prepay_id, nonce_str } = restData; // 微信小程序支付要单独进地签名,并返回给小程序端 const sign = WXPayUtil.generateSignature ({ appId: mpAppId, nonceStr: nonce_str, package: `prepay_id=${prepay_id}`, signType: 'MD5', timeStamp: time_stamp }, KEY); let orderData = { out_trade_no, time_stamp, nonce_str, sign, sign_type, body, total_fee, prepay_id, sign, status: 0, // 订单文档的status 0 未支付 1 已支付 2 已关闭 _openid: openid, }; let order = await orderCollection.add(orderData); order_id = order.id; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { out_trade_no, time_stamp, order_id, ...restData } }); } // [在此处放置 payorder 的相关代码] case 'payorder': { // 从端里出来相关的订单相信 const { out_trade_no, prepay_id, body, total_fee } = data; // 到微信支付侧查询是否存在该订单,并查询订单状态,看看是否已经支付成功了。 const { return_code, ...restData } = await pay.orderQuery({ out_trade_no }); // 若订单存在并支付成功,则开始处理支付 if (restData.trade_state === 'SUCCESS') { let result = await orderCollection .where({ out_trade_no }) .update({ status: 1, trade_state: restData.trade_state, trade_state_desc: restData.trade_state_desc }); let curDate = new Date(); let time = `${curDate.getFullYear()}-${curDate.getMonth() + 1}-${curDate.getDate()} ${curDate.getHours()}:${curDate.getMinutes()}:${curDate.getSeconds()}`; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } case 'orderquery': { const { transaction_id, out_trade_no } = data; // 查询订单 const { data: dbData } = await orderCollection .where({ out_trade_no }).get(); const { return_code, ...restData } = await pay.orderQuery({ transaction_id, out_trade_no }); return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { ...restData, ...dbData[0] } }); } case 'closeorder': { // 关闭订单 const { out_trade_no } = data; const { return_code, ...restData } = await pay.closeOrder({ out_trade_no }); if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { await orderCollection .where({ out_trade_no }) .update({ status: 2, trade_state: 'CLOSED', trade_state_desc: '订单已关闭' }); } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } } } [代码] 其实我们支付的关键功能都在上面这些代码里面了 [图片] 再来看下支付的相关流程截图 [图片] 上图就涉及到了我们的订单列表、支付状态、支付成功后的回调 今天就先讲到这里后面会继续给大家讲解支付的其他功能——比如支付成功后的消息推送也是可以借助云开发实现的 [图片] 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心!
2019-09-19 - 云开发·云调用生成小程序码
云开发·云调用生成小程序码 小程序云开发已经支持云调用,开放了很多接口,一直想要的获取小程序码也支持了。这下轻量的小程序也可以有自定义小程序码的功能。 1. 需求 获得一个带参数的小程序码,传播出去以后,用户扫码进入指定页面,根据参数做不同的处理。本文只讲小程序码生成、存储、展示部分。参数处理不多介绍,可以查看 项目代码 了解更多。 2. 开通云开发 新建小程序可以从开发工具的云开发模板初始化项目,根据云开发操作指引新建项目即可。 但是这里有个问题,已发布小程序的页面才能生成小程序码。如果现有的小程序没有开通云开发,需要做以下几步: 开发工具开通云开发,设定云开发的环境; 将原来的代码(除了[代码]project.config.json[代码]以外的所有文件)放到新建的 [代码]miniprogram[代码] 目录; 新增 [代码]cloudfunctions[代码] 目录; [代码]app.json[代码] 新增配置 [代码]"cloud": true[代码]; [代码]project.config.json[代码] 配置 [代码]"miniprogramRoot":"miniprogram/"[代码] 和 [代码]"cloudfunctionRoot":"cloudfunctions/"[代码]; 修改小程序基础库版本,最低要 2.3.0 [代码]"libVersion": "2.3.0"[代码]。 3. 生成小程序码 下面可以开始写代码开发了,开始之前,建议先看完官方教程。特别是开发工具的使用步骤,开发和调试时如果遇到奇怪的问题,可以尝试重启开发工具、重装开发工具,也可以去微信开放社区发帖。(重启和重装都是我在社区中发现的答案,能解决各种不应该存在的问题)。 3.1 准备文件 在 [代码]cloudfunctions[代码]目录右键新建Node.js云函数 [代码]getqr[代码]。 生成小程序码需要单独指定权限。在 [代码]getqr[代码] 目录新建 [代码]config.json[代码] ,里面写以下内容: [代码]{ "permissions": { "openapi": [ "wxacode.getUnlimited" ] } } [代码] 小程序码的获取方式有三种,这里只用到了接口 getUnlimited,选择这个接口的原因是漂亮的圆形小程序码,数量无限制。具体区别可以去 获取小程序码官方文档查看详情。 正常情况下,这个时候云函数可以部署测试了。如果遇到部署不成功、各种权限问题,可以尝试本地部署上传所有文件、重启试试。 3.2 生成小程序码 生成小程序码的代码如下,可以指定页面和页面参数 [代码]scene[代码],还有小程序码的尺寸。 注意这里的 [代码]scene[代码] 有限制: 最大32个可见字符; 只支持数字,大小写英文以及部分特殊字符:[代码]!#$&'()*+,/:;=?@-._~[代码]; 注意参数格式:下面实例代码生成小程序码后,扫码获得 [代码]pages/demo/demo?scene=id%3D6[代码] 。 [代码]try { const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/demo/demo', scene: 'id=6', width: 240, }) console.log(result) return result } catch (err) { console.log(err) return err } [代码] 直接调用,比服务端调用少了 access_token 参数。 3.3 上传到云存储 返回值中的 buffer 就是图片内容,直接上传到云存储: [代码]const uploadResult = await cloud.uploadFile({ cloudPath: 'shareqr/' + qr_name_hash + '.jpg', fileContent: result.buffer, }); [代码] 我在云存储新建了 [代码]shareqr[代码] 目录保存小程序码; 图片名根据参数取md5摘要; [代码]getUnlimited[代码] 返回的图像是 [代码]jpeg[代码] 格式,后缀硬编码写 [代码].jpg[代码]。 3.4 获取图片临时路径 直接上代码 [代码]getURLReault = await cloud.getTempFileURL({ fileList: [uploadResult.fileID] }); fileObj = getURLReault.fileList[0] return fileObj [代码] 3.5 直接从存云存储获取 生成过以后图片已经保存在云存储,用同样的参数第二次调用没必要再生成一次,去掉一次网络请求,可以节省不少时间。 前面说到文件名使用请求参数摘要,知道了目录和文件名,再加上文件bucket前缀就可以拼出来 [代码]fileID[代码],用[代码]fileID[代码] 可以查询云存储的文件。 比如我刚刚生成的 fileID 是 [代码]cloud://dev-xxxx.8888-dev-xxxx/qr/44ea42f05091c3bec771123e6e8cd4c2.jpg[代码], 前缀就是 [代码]cloud://dev-xxxx.8888-dev-xxxx/[代码]。再拼上目录、文件名、后缀就是 [代码]fileID[代码]。 注:此处的 [代码]fileID[代码]拼接方法并不是来自官方文档,只是在使用中发现这个前缀不会变。还需要官方解释说明[代码]fileID[代码]规则。 如果会改变,就需要再用云数据库存储[代码]fileID[代码],更麻烦一些。 3.6 云函数完整代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk'); const crypto = require('crypto'); const bucketPrefix = 'cloud://dev-xxxx.8888-idc-4d11a4-1257831628/qr/'; // env: 'dev-xxxx' // 云函数入口函数 exports.main = async (event, context) => { const full_path = event.page + '?' + event.scene; const qr_name_hash = crypto.createHash('md5').update(full_path).digest('hex'); const temp_id = bucketPrefix + qr_name_hash + '.jpg'; // return { // full_path, // qr_name_hash, // temp_id // } try { // 先尝试获取文件,存在就直接返回临时路径 let getURLReault = await cloud.getTempFileURL({ fileList: [temp_id] }); // return getURLReault; let fileObj = getURLReault.fileList[0]; if (fileObj.tempFileURL != '') { fileObj.fromCache = true; return fileObj; } // 生成小程序码 const wxacodeResult = await cloud.openapi.wxacode.getUnlimited({ scene: event.scene, page: event.page, width: 280 //二维码的宽度,单位 px,最小 280px,最大 1280px }) // return wxacodeResult; if (wxacodeResult.errCode != 0) { // 生成二维码失败,返回错误信息 return wxacodeResult; } // 上传到云存储 const uploadResult = await cloud.uploadFile({ cloudPath: 'qr/' + qr_name_hash + '.jpg', fileContent: wxacodeResult.buffer, }); // return uploadResult; if (!uploadResult.fileID) { //上传失败,返回错误信息 return uploadResult; } // 获取图片临时路径 getURLReault = await cloud.getTempFileURL({ fileList: [uploadResult.fileID] }); fileObj = getURLReault.fileList[0]; fileObj.fromCache = false; // 上传成功,获取文件临时url,返回临时路径的查询结果 return fileObj; } catch (err) { return err } } [代码] 4. 小程序页面调用 调用页面就比较简单了,在小程序新建一个 [代码]pages/share/share[代码] 在 [代码]onLoad[代码] 函数调用云函数。 [代码]// 使用前记得先初始化云函数,一版放到 app.js onLaunch() 中 // wx.cloud.init({env: 'dev-8888'}) wx.cloud.callFunction({ name: 'getqr', data: { page: 'pages/demo/demo', scene: 'id=6', } }).then(res => { console.log(res.result); if (res.result.status == 0) { _this.setData({ qr_url: res.result.tempFileURL }) }else{ wx.showToast({ icon: 'none', title: '调用失败', }) } }).catch(err => { console.error(err); wx.showToast({ icon: 'none', title: '调用失败', }) }) [代码] 至此完整的调用过程已经全部完成,详细代码可以到 项目代码 查看。 代码中还对入口页面和share页面的参数做了包装,云函数可以直接使用,小程序可以稍做修改适应自己业务。 写在最后 小程序云开发已经开放了很多功能,除了这次提到的生成小程序码,云调用还可以发送模板消息。有需要的开发者又一个理由可以快速上线新功能了。 云开发还开放了[代码]HTTP API[代码],也就是用自己的服务器调用云函数。以前看完云开发介绍文章最大的疑问就是,你说的都很好,可是后台数据怎么管理呢?不能跟自己的服务器结合,只能放一些轻量的小程序。有了 [代码]HTTP API[代码] 以后就可以用自己的服务器做管理后台了。这时候你要问,都用上服务器了,还需要云开发做什么。首先,云开发免费;其次,免费功能已经够强,就差不能做Web管理后台了;最后,获取access_token(小程序及小游戏调用不要求IP地址在白名单内。)
2020-07-10 - 小程序服务商必须教会商户哪些操作?(支付篇)
作为一个职称的小程序服务商,必须给商户普及微信支付商家版的7个操作。 一、开通支付 1、进入管理后台的“申请支付”菜单,按向导完成账号注册。 2、完成商户号、密钥和证书配置。 3、手机端微信支付后台登录。 一般在开通微信支付商家版的过程,手机端微信支付后台都已经绑定。进入方法:微信搜索“微信支付商家助手”,进入公众号右下角菜单 我的账号 > 我是商家 。 [图片] 手机端商家版微信支付,除了能收到顾客的收款通知外,还有收款记录、经营分析、员工管理。尤其是经营分析里的用户画像功能,值得重点研究一下。 二、退款 1、退款功能开通 点击管理后台的“申请退款”菜单。 通知工作人员后,会在1个工作日内完成审核并指引完成操作。 商户登录自己的微信支付后台接受退款功能开通邀请。 2、退款操作 [图片] 当退款事件产生后,可以到酒店管理后台 订房订单>小程序订单 点 退款 按钮。输入退款金额点确定即可。金额返回时间根据支付方式而定,微信零钱支付的是即时返回。 3、退款查询 [图片] 商家版微信支付后台的交易中心 > 退款查询。 三、员工账号管理 1、账号权限。 [图片] 默认情况下,有基本账户、财务、管理员、客服、技术操作员5个角色。也可以自定义新的角色。新角色可以自行配置权限!操作权限可以详细到菜单下某个功能。 2、如何开通员工账号。 [图片] 进入商家微信支付后台,账户中心>员工账号管理 > 员工列表 > 新增账号。 四、交易订单查询 [图片] 商家微信支付后台,交易中心 > 交易订单。可以根据时间查询、订单号查询。高级查询还可以根据场景查询,支付方式查询、金额范围查询、交易状态查询等。 五、提现说明 提现分为自动提现和手动提现,默认情况下为自动提现。也就是在每天早上9:00会把前一天的资金自动提现到绑定的对公账号。 这是最快捷的提现方案,但有个场景需要注意。就是提现完成后没有足够金额的情况下,有退款发生,就会产生退款失败。 提供两种解决方案: 1、关闭自动提现功能。 [图片] 关闭方法:产品中心 > 我的产品 > 资金解决方案 > 自动提现。 2、自动提现的情况下,设置预留退款金额。 根据商家自己的实际经营情况,预估一天最多的退款金额。在设置自动提现时设置预留金额。预留金额仍然不够的情况下,还可以通过充值解决。充值方面微信支付还很贴心地做了一个指定微信充值。设置完成后在第二天零点生效。 [图片] 提现的官方详细说明: https://kf.qq.com/faq/161223NJBr2u161223Mfeqei.html 六、收款通知 [图片] 顾客支付成功后,除了总管理员能收到收款通知。授权的店员也会收到微信收款商业版的通知。 七、财务对账 在商家微信支付后台里的交易中心,有交易、退款、充值、提现等数据汇总报表。并提供excel格式下载和强大的类目选择功能。 [图片]
2019-09-26 - JavaScript常用设计模式示例与应用
JavaScript常用设计模式实例与应用 前言 1. 什么是设计模式 小时候打游戏,我们总是追求快速完美通关;上下班交通,我们总是会选择最方便便捷乘车路线。我们总是追求一件事情的最优美便捷的解决方案,也就是其所谓的最佳实践。 一个设计模式就是一个可重用的方案,可应用于在软件设计中常见的问题,在本次分享主题中,就是编写JavaScript的web应用程序中常见的问题,设计模式的另一种解释就是一个我们如何解决问题的模板。那些在许多不同但类似的情况下使用的模板。 2. 为什么要学习设计模式 JavaScript是一门以原型为基础,面向对象的,动态数据类型语言。在把函数视为第一公民,支持函数式编程的同时也不排斥面向对象的开发方式,甚至在ES6+的标准中还引入了面向对象的一些原生支持。这使得JavaScript成为一门功能强大的语言同时也导致了编程风格的碎片化,同一个功能实现的多样性。对于一些传统的、强面向对象的设计模式会有各种类型的实现,有时候会让人感觉牵强。但是这些并不妨碍我们使用JavaScript来表达设计模式的理念、所要解决的问题以及它的核心思想,这才是我们所要关注的核心。 设计模式可以让我们站在巨人的肩膀上,获得前人的经验,保证我们以优雅的方式组织我们的代码并满足我们解决问题所需要的条件。 内容 一、设计原则 设计原则是指导思想,是我们在程序设计中尽可能要遵守的准则。设计模式就是这些设计原则的一些具体实现,所要达到的目标就是高内聚低耦合。在这里我简单介绍一些六大设计原则中的单一职责原则(SPR)、开放封闭原则(OCP)、最少知识原则(LKP)。 1. 单一职责原则 单一职责原则指的是一个类应该仅有一个引起它变化的原因,也就是说一个对象只做一件事情。这样做可以让我们对对象的维护变得简单,如果一个对象拥有多种职责,职责之间相互耦合,对一个职责的修改势必会影响到其他职责。也就是说,一个对象负责的职责越多,耦合越强,对模块的修改就越危险。 2. 开放封闭原则 开放封闭原则指的是一个模块应该在对扩展开放,而对修改封闭。当需要修改增加需求的时候,应该尽量通过扩展新代码的方式,而不是修改已有的代码。因为修改已有代码会给依赖原有代码的模块带来隐患,从而需要把依赖原有代码的模块重新测试一遍,加重测试成本。 3. 最少知识原则 最少知识原则指的是一个类应该对自己需要耦合或调用的类了解得尽可能少,调用者或依赖着仅需要知道他所需要的方法即可,其他的概不关心。因为类与类之间的关系越密切,耦合性越高,当一个类发生改变时,对另一个类的影响也越大。通常我们减少对象之间的联系的方法是引入一个第三者来帮助通信,阻隔对象之间的直接通信,从而减少耦合。 二、设计模式的分类 设计模式可以被分成几个不同的种类: 创建型设计模式 创建型设计模式关注的是对象创建的机制方法,一般会把对象的创建和使用分离,从而帮助创建类的实例对象。属于这一类的设计模式主要有:构造器模式、工厂模式、单例模式、建造者模式等。 结构型设计模式 结构型设计模式关注对象组成以及不同对象之间的关系。这类模式有助于在系统的某一部分发生变化时减少对整个系统结构的改变。主要包括:代理模式、享元模式、外观模式、适配器模式、装饰者模式等。 行为型设计模式 行为型设计模式关注对象之间的通信,描述对象之间如何相互协作。主要包括:发布订阅模式,策略模式,状态模式,迭代器模式,命令模式,职责链模式,中介者模式等。 三、设计模式示例 1. 单例模式 单例模式(Singleton Pattern)属于创建型设计模式,它限制一个类只能有一个实例化对象,并提供一个访问它的全局访问点。 单例模式可能是最简单的设计模式了,虽然简单,但在实际项目开发中是很常用的一种模式。 单例模式中有几个需要知道的概念: Singleton:特定的类,也就是我们需要访问的类,访问者要拿到的就是它的实例。 Instance: 单例,是特定类的唯一实例。 getInstance: 获取单例的方法。 代码示例 [代码]var GameManager = (function () { // 单例 var instance; function init() { // 私有变量和方法 var _saveData = { name: 'glenn', level: 1 }; function _privateMethod(){ console.log( "I am private function" ); } return { // 公有变量和方法 levelUp: function(){ _saveData.level ++; }, getCurLevel: function(){ return _saveData.level; }, getName: function(){ return _saveData.name; }, publicProperty: "this is a public prop", }; }; return { // 如果存在获取此单例实例,如果不存在创建一个单例实例 getInstance: function () { if ( !instance ) { instance = init(); } return instance; } }; })(); // 使用: var singleA = GameManager.getInstance(); singleA.levelUp(); var singleB = GameManager.getInstance(); console.log( singleA.getCurLevel() === singleB.getCurLevel() ); // true [代码] 在本例中,GameManager是一个单例类,我们首先使用立即调用函数IIFE把希望隐藏的单例示例instance隐藏起来,在init方法中定义该单例类的公有和私有方法变量,然后返回一个对象,把获取单例实例的方法getInstance暴露出去。在getInstance方法中,通过JavaScript的闭包特性把单例实例instance存进闭包中,在第一次获取实例时才初始化单例,并在之后的获取操作中返回的都是这个相同的实例。 可以看到,在使用单例的代码中,我们调用了两次getInstance获取的两个对象singleA和singleB指向的是同一个对象。 源码中的单例模式 以 ElementUI 为例,ElementUI中的全屏Loading蒙层使用服务的形式调用的使用方式示意: [代码]Vue.prototype.$loading = service; this.$loading({ fullscreen: true }); [代码] 我们可以看看这个loading在ElementUI2.9.2源码中是如何实现的。 下面是为了方便观看省略了部分代码后的源码 [代码]import Vue from 'vue'; import loadingVue from './loading.vue'; const LoadingConstructor = Vue.extend(loadingVue); //... //单例 let fullscreenLoading; LoadingConstructor.prototype.originalPosition = ''; LoadingConstructor.prototype.originalOverflow = ''; LoadingConstructor.prototype.close = function() { //... }; const addStyle = (options, parent, instance) => { //... }; const Loading = (options = {}) => { //... //判断示例是否已经初始化 if (options.fullscreen && fullscreenLoading) { return fullscreenLoading; } //一系列的初始化操作 let parent = options.body ? document.body : options.target; let instance = new LoadingConstructor({ el: document.createElement('div'), data: options }); addStyle(options, parent, instance); if (instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed') { addClass(parent, 'el-loading-parent--relative'); } if (options.fullscreen && options.lock) { addClass(parent, 'el-loading-parent--hidden'); } parent.appendChild(instance.$el); Vue.nextTick(() => { instance.visible = true; }); //把初始化出来的实例缓存下来 if (options.fullscreen) { fullscreenLoading = instance; } return instance; }; export default Loading; [代码] 这里的单例是fullscreenLoading,缓存在闭包中。当用户调用时传入的options中fullscreen为true且之前已经创建并初始化过单例的情况下直接返回之前创建的单例,否则继续执行后面的初始化操作,并把创建的单例赋值给闭包中的fullscreenLoading后返回新创建的单例。 这是一个典型的单例模式应用,通过复用之前创建的全屏加载蒙层单例,不仅减少了重复实例化过程带来的额外开销,还保证了页面中不会出现重复的全屏加载蒙层。 单例模式的应用场景 当项目中需要一个公共的状态管理时,我们可以引入单例模式来确保访问的一致性。 当项目中存在一些同一时间只会出现一个且会重复出现的对象时,我们可以引入单例模式避免重复创建对象产生的多余开销,例如项目中的弹窗,消息框提醒等。 2. 外观模式 外观模式又叫门面模式,属于结构型模式,它将子系统的一系列复杂的接口集成起来组成一个更高级别的更舒适的高层接口,从而隐藏其真正的潜在复杂性,对外提供一个一致的外观。 外观模式让外界减少对子系统的直接交互,从而降低耦合,让外界可以轻松使用子系统,其本质是封装交互,简化调用。 代码示例 [代码]var module = (function() { var _sportsman = { speed: 5, height: 10, set : function(key, val) { this[key] = val; }, run : function() { console.log('运动呀正在以'+this.speed+'米每秒的速度向前跑着。'); }, jump: function(){ console.log( "运动员往上跳了"+this.height+'米'); } }; return { facade : function( args ) { args.speed != undefined && _sportsman.set('speed', args.speed); args.height != undefined && _sportsman.set('height', args.height); args.run && _sportsman.run(); args.jump && _sportsman.jump(); } }; }()); // Outputs: 运动呀正在以10米每秒的速度向前跑着。 // 运动员往上跳了5米 module.facade( {run: true, speed: 10, jump: true, height: 5} ); [代码] 这是表达外观模式一个简单的例子。在本例中,调用module的门面方法facede会触发运动员对象_sportsman中的一系列私有方法。但在这一次,用户不需要关心运动员对象内部方法的实现,就可以让运动员动起来。 源码中的外观模式 当我们使用Jquery的$(document).ready()来给浏览器加载完成添加事件回调时,Jquery会调用源码中的私有方法: [代码]// ... bindReady: function() { //... // Mozilla, Opera and webkit nightlies currently support this event if ( document.addEventListener ) { // Use the handy event callback document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); // A fallback to window.onload, that will always work window.addEventListener( "load", jQuery.ready, false ); // If IE event model is used } else if ( document.attachEvent ) { // ensure firing before onload, // maybe late but safe also for iframes document.attachEvent( "onreadystatechange", DOMContentLoaded ); // A fallback to window.onload, that will always work window.attachEvent( "onload", jQuery.ready ); // If IE and not a frame // continually check to see if the document is ready var toplevel = false; try { toplevel = window.frameElement == null; } catch(e) {} if ( document.documentElement.doScroll && toplevel ) { doScrollCheck(); } } } // ... [代码] 由于IE9之前的IE版本浏览器以及Opera7.0之前的Opera浏览器不支持addEventListener方法,在需要适配这些浏览器的项目中,我们需要自己手动判断浏览器版版本来决定使用什么事件绑定方法以及事件。而如果使用了Jquery库中提供的这个外观方法,用户则不需要关心浏览器的兼容问题,使用一致的外观接口$(document).ready()就可以实现监听浏览器加载完成事件的功能,从而简化了使用。 除了抹平浏览器的兼容性问题之外,Jquery还有一些其他的外观模式的应用: 比如设置或获取dom结点的内容和属性时使用的text()、html()和val()方法时,Jquery判断调用方法是否有传参数来确定是设置还是获取操作。这里Jquery把设置和获取操作对外提供了同一个外观接口,使调用简化了不少。 再比如Jquery的ajax的API[代码]$.ajax(url[,settings])[代码],当我们需要设置以JSONP的形式发送请求时,只需要传入[代码]dataType: 'jsonp'[代码]设置,jquery会进行额外的操作帮我们启动JSONP流程,而不需要调用者添加额外的代码。 外观模式的适用场景 维护设计粗糙和难以理解的上古系统,或者非常复杂的一些系统时,可以为这些系统设置一个外观模块,给外界提供清晰的接口,以后的新系统只需要与外观接口交互即可。 构建多层系统时,可以使用外观模式来将系统分层,让外观接口成为每一层的入口,简化层间调用,给层间松耦。 团队协作时,可以将各自负责的模块建立合适的外观,简化其他同事的使用,节约沟通时间。 发布订阅者模式 发布 - 订阅模式(Publish-Subscribe Pattern, pub-sub)又叫观察者模式(Observer Pattern),属于行为型模式,它定义了一种一对多的关系,让多个订阅者对象同时监听某一个发布者,或者叫主题对象,这个主题对象的状态发生变化时就会通知所有订阅自己的订阅者对象,使得它们能够自动更新自己。 发布 - 订阅模式有几个主要概念: Publisher:发布者,当消息发生时负责通知对应订阅者 Subscriber:订阅者,当消息发生时被通知的对象 SubscriberMap:以type为主键存储数组,每个数组存储所有对应type的订阅者 type: 消息类型,订阅者可以订阅的不同消息类型 subscribe:该方法可以将订阅者添加到SubscriberMap中对应的数组中 unSubscribe:该方法为SubscriberMap中删除订阅者 notify:该方法遍历通知SubscriberMap中对应type的所有订阅者 代码示例 [代码]var Publisher = (function() { var _subsMap = {} // 存储订阅者 return { /* 消息订阅 */ subscribe(type, cb) { if(_subsMap[type]){ if (!_subsMap[type].includes(cb)){ _subsMap[type].push(cb); } }else{ _subsMap[type] = [cb]; } }, /* 消息退订 */ unsubscribe(type, cb) { if(!_subsMap[type] || !_subsMap[type].includes(cb))return; var idx = _subsMap[type].indexOf(cb); _subsMap[type].splice(idx, 1); }, /* 消息发布 */ notify(type) { if (!_subsMap[type])return; var args = Array.prototype.slice.call(arguments, 1); _subsMap[type].forEach(function(cb){ cb.apply(this, args); }) } } })() Publisher.subscribe('运动鞋', function(message){console.log('111' + message)}); // 订阅运动鞋 Publisher.subscribe('运动鞋', function(message){console.log('222' + message)}); Publisher.subscribe('帆布鞋', function(message){console.log('333' + message)}); // 订阅帆布鞋 Publisher.notify('运动鞋', ' 运动鞋到货了 ~') // 打电话通知买家运动鞋消息 Publisher.notify('帆布鞋', ' 帆布鞋售罄了 T.T') // 打电话通知买家帆布鞋消息 [代码] 这是一个发布-订阅模式的通用代码实现,Publisher就是一个发布者,这里使用了立即调用函数IIFE方式来将不希望被外界调用的_subsMap隐藏。订阅者采用回调函数的形式,在消息发布时使用JavaScript的apply、call函数使发布的消息参数可以传到订阅者回调函数中去。 源码中的发布-订阅模式 我们使用Jquery的API可以轻松实现消息的订阅、发布以及退订操作: [代码]function eventHandler() { console.log('自定义方法') } /* ---- 事件订阅 ---- */ $('#app').on('myevent', eventHandler) // 发布 $('#app').trigger('myevent') // 输出:自定义方法 /* ---- 取消订阅 ---- */ $('#app').off('myevent') $('#app').trigger('myevent') // 没有输出 [代码] 对应api源码参见: event.js 其中add方法为on接口的内部直接绑定方法,remove方法对应off接口的内部实现。 发布-订阅模式的优缺点 发布-订阅模式最大优点就是解耦: 时间上的解耦:注册事件后,订阅者不需要持续关注发布者的动态,当事件触发时,发布者会通知对应的订阅者,调用对应的回调函数。 对象间的解耦: 发布者不需要提前知道事件的订阅者有哪些,当事件发生时直接遍历对应的订阅者回调函数来通知订阅者,从而解耦了发布者和订阅者之间的联系,使它们之间互不持有。 发布-订阅模式也有一些缺点: 增加消耗:创建结构和缓存订阅者两个过程都会消耗计算和内存资源,即时订阅后没有触发过,订阅者使用会存在内存中。 增加复杂度 :订阅者被缓存在一起,如果多个订阅者和发布者层层嵌套,那么程序将变得难以追踪和调试,参考一下 Vue 调试的时候你点开原型链时看到的那堆 deps/subs/watchers 们… 总结 设计模式能够让我站在巨人的肩膀上,享受其他开发者们长期以来在一些有挑战性问题上的解决方案以及优秀的架构。 对我们来讲,知道有这些设计模式是很重要的,但更重要的是应该知道怎样以及什么时候去使用它们。遵守设计原则,使用设计模式是好事,但是过犹不及,在实际项目中我们不能刻板的遵守这些设计原则以及使用设计模式,在想使用每个模式前先去了解下它的优缺点。要真正的理解模式能给你带来什么好处需要花时间去尝试,以实际情况中模式给你的程序带来的好处作为标准来选择。
2019-10-31 - 小程序顶部自定义导航组件实现原理及坑分享
为什么使用自定义导航 对比默认导航栏,我们会更需要: 统一Android、IOS手机对于页面title的展示样式及位置 更丰富的导航栏定制效果,如添加home图标等 左上角返回事件的监听处理 统一实现双击返回顶部功能 自定义导航组件实现思路 自定义导航组件实现的核心是需要计算导航栏的真实高度 这里以官方文档->扩展能力中的Navigation组件为例分析实现思路。当使用"navigationStyle": "custom"时,默认导航被移除,页面的开始位置变成了屏幕顶部,这时我们需要实现的导航栏是在状态栏下面。 导航栏的真实高度=状态栏高度+导航栏内容。 [图片] 使用wx.getSystemInfo获取到statusBarHeight便是导航栏的高度,但是导航栏内容高度呢? 有人可能觉得导航栏内容高度顾名思义就是导航栏内容高度啊,内容撑起还用管嘛!要,必须要! 因为右上角胶囊按钮是原生加载的,我们的导航栏内容需要正好贴在胶囊的下方且垂直居中。 导航栏内容高度=(胶囊按钮的顶部距离 - 状态高度)*2 + 胶囊高度 [图片] 如何计算胶囊的数据呢?幸运的是我们有 wx.getMenuButtonBoundingClientRect() 获取胶囊按钮的布局位置信息,那么动态计算导航栏的内容高度就很方便啦。 好了,以上就是动态计算的核心思路,我们再来看官方Navigation组件高度是怎么实现的 [代码]page{--height:44px;--right:190rpx;} .weui-navigation-bar .android{--height:48px;--right:222rpx} .weui-navigation-bar__inner{ position:fixed;top:0;left:0;z-index:5001;display:flex;align-items:center; height:var(--height);padding-right:var(--right);width:calc(100% - var(--right)) } [代码] 导航栏内容的高度是通过- -height这个css变量提前声明好的,安卓机型会重新覆盖为新的css变量值,目前没发现有适配问题。 官方就是官方啊,具体尺寸都知道,那就不用一番计算周折啦,直接拿来主义即可。 导航的布局位置已经搞定啦,剩下就是写具体的内容,不同业务实现需求不同这里就不一一赘述了。 完善官方顶部导航组件 本着拿来主义,直接使用官方Navigation组件,但在实际业务开发中还是遇到不少需要自定义的需求,就比如: loadding样式没实现 标题内容超出没有出现省略号 和原生顶部的样式不兼容,导致单个页面引入时跳转有明显差异出现 没有双击返回顶部功能开关功能 引入页面需要获取导航栏的高度,来控制其他元素距离顶部的位置, 不能根据页面栈数据动态显示隐藏back按钮, 针对以上需求,我们对官方的组件进行二次完善开发,满足常规的自定义需求绰绰有余,直接引入开箱即用。 源码使用示例 https://github.com/YuniorZen/minicode-debug/tree/master/minicode02 [图片] 使用说明 [代码]/*自定义头部导航组件,基于官方组件Navigation开发。*/ <navigation-bar title="会员中心" bindgetBarInfo="getBarInfo"></navigation-bar> [代码] 组件属性列表 属性 类型 描述 bindgetBarInfo function 组件实例载入页面时触发此事件,首参为event对象,event.detail携带当前导航栏信息,如导航栏高度 event.detail.topBarHeight bindback function 点击back按钮触发此事件响应函数 backImage string back按钮的图标地址 homeImage string home按钮的图标地址 ext-class string 添加在组件内部结构的class,可用于修改组件内部的样式 title string 导航标题,如果不提供为空 background string 导航背景色,默认#ffffff color string 导航字体颜色 dbclickBackTop boolean 是否开启双击返回顶部功能,默认true border boolean 是否显示顶部导航下边框分割线,默认false loading boolean 是否显示标题左侧的loading,默认false show boolean 显示隐藏导航,隐藏的时候navigation的高度占位还在,默认true left boolean 左侧区域是否使用slot内容,默认false center boolean 中间区域是否使用slot内容,默认false Slot name 描述 left 左侧slot,在back按钮位置显示,当left属性为true的时候有效 center 标题slot,在标题位置显示,当center属性为true的时候有效 自定义顶部导航目前存在的坑 弹窗的背景蒙层无法覆盖原生胶囊按钮 页面下拉刷新的圆点会被自定义导航遮盖 如果要自定义顶部导航,以上问题避免不了,只能忍着接受。 目前还没遇到完美的解决方案,针对下拉刷新圆点被遮挡的问题微信官方还在需求开发中,如果你有好的想法欢迎留言反馈,一起学习交流。
2019-10-31 - 零基础该如何运营微信小程序
微信小程序基于腾讯的强大平台,具有天然的营销优势!今天给大家讲解一下怎么玩转小程序。 玩转小程序都离不开三种方式 1. 小程序+自媒体 配合企业或者品牌的微信公众号获得流量、进行转化变现。 2. 小程序+社群 大量流量来源于社群,所以很多优秀的团队通过小程序来完成裂变后变现。 3. 小程序+线下门店 利用小程序打通线下实体门店,形成销售闭环。其流量来源于自身产品、业务及小程序活动搭配,小程序为门店赋能,提升门店开单效率。 [图片] 玩转小程序的运营思路 1.精准定位 根据自身产品的特点,发展阶段,用户画像等来分析小程序需要运营的思维及模式,找准目标用户人群。根据消费者的消费习惯及特点,构划发展方向。 2.快速测试市场、反馈、迭代更新 定位目标人群之后,开始上线产品,通过使用小程序的一些强大功能进行测试,分析,进行活动设计,观察市场及用户反应。 通过市场数据分析,可以准确无误地反馈用户的消费习惯及消费爱好。通过这些数据终让商家了解每一个客户的消费需求,以及对小程序以及产品的优化调整。 3.培养消费者习惯 小程序是与用户沟通的一个有效渠道,重要的是要有意识地在固定的时间发布产品及相关活动。如果产品发布时间固定,长期关注的用户会养成定时查看的习惯。 4.保持线上门店的活跃性 社群的活跃性是必不可少的。而做好线上门店的运营工作一个重要内容就是保持日常的店铺活跃,让用户感受到一个真实、快速、高效率的消费氛围。节假日、促销活动版面实时更新,及时关注回复用户评论。这些步骤都要持续循序渐进的进行,提升用户的信任感。 5.线下导流不可少 5G新零售时代,消费者消费阵地后还是会转向线下,而且,线下消费场景是用户真实的需求场景,也是固定、忠实的流量来源地。合理运用好小程序二维码海报,让用户自发扫描关注,做好线下推广活动,比如扫码送礼品,扫码签到获得积分等方式。 本文由素材火https://www.sucaihuo.com/整理发布,转载请保留本信息!
2019-10-27 - 小程序实战1-项目总览
实战成果通过3周业余时间的开发,完成了我的车管家小程序开发,并提交审核发布出来了。希望通过接下来的系列博客,带领大家完成一个这样小程序开发,并提交微信审核发布出来。 主要功能:添加爱车基本信息 记录爱车加油、修车、维修和保养等信息 统计分析各个活动费用和次数 设置保养和保险时间 上传爱车图片 界面预览[图片] 二维码试用[图片] 小程序定位首先看看宝玉前辈对2019年前端趋势的预测。 [图片] 国外的PWA(Progressive Web Application技术)在国内的不流行,主要是国外Google搜索方便,所以以Chrome移动版本作为入口;而反观国内,各个浏览器都不是主要入口,反倒是App和小程序。所以2019年,国内开发领域,小程序不可获取。 大前端趋势是: 超级App(微信、微博、抖音、支付宝)和大型游戏(绝地求生、王者荣耀等)采用原生的开发技术。同时支持iOS和Android两大平台 其他-基于小程序,也是支持iOS和Android,未来有可能小程序在PC端微信客户端也可以使用 题外话 腾讯占有了社交入口就占有大部分用户,从以前的浏览器,搜索到App、微信,时代在变,场景也在变。微信把IM、资讯,支付、理财都做了,现在头痛的是抖音。所有2019年,微信肯定要基于[时刻视频]出大动作,对标头条的抖音。 小程序前端技术WXML:类似HTML,但是和HTML还不太一样。(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构。 WXSS:类似CSS,(WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式。 WXS:WeiXin Script是小程序的一套脚本语言,结合 [代码]WXML[代码],可以构建出页面的结构。 小程序后台技术-云开发[图片] 小程序-云开发 腾讯云开发是未来移动应用开发、小程序开发、Web开发后台BaaS服务,对标的是Google Firebase。 [图片] [图片]
2019-01-09