- 如何优雅的调用云函数?
前言 这是在做《#小程序云开发挑战赛#-情侣券-想做就做》这个项目过程中学习到了一个新知识。 能够优雅的调用云函数,极大程度的减少维护成本。 在此之前我都是业务代码层面直接调用云函数,而且一个云函数就一个方法。 如下: 首页 [代码]wx.cloud.callFunction({ name: 'login' }).then(res => { wx.setStorageSync('openid', res.result.openid) }) [代码] login 云函数 [代码]const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = (event, context) => { const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, env: wxContext.ENV, } } [代码] 这种代码写起来很爽,但是要维护起来的时候就很头疼,云函数越写越多,找起来就比较麻烦,无法快速定位。 如何解决呢? 从两个维度来解决: 从小程序代码来说,可以加一个api层来管理所有云函数的调用。 从云函数代码来说,可以在一个云函数里面做个简单路由写多个方法。 1.云函数调用统一管理 从之前的顺序:小程序业务层=>云函数 改成 小程序业务层=>API层=>云函数 业务层:调用API [代码]// 先导入api的user类 import { login } from 'api/user.js' login().then(res => { wx.setStorageSync('openid', res.result.openid) }) [代码] API层:调用云函数 [代码]async function login() { return wx.cloud.callFunction({ // 云函数名称 name: 'user', // 传给云函数的参数 data: { action: 'login' } }); } module.exports = { login:login } [代码] 上面案例中我们只用了login方法,其实在实战中user里面会有所有的相关操作,引入也可以引入多个方法。 所有的云函数都会在api层进行管理,这样改云函数就不需要去业务代码里面去找了。 业务层只负责传参与回调,调用过程不需要关心,同时还可以支持多个页面复用api方法。 2. 一个云函数多个方法 从上面api层的案例不难看出,data里面传入了一个action参数,这个参数就是你要调用的方法名称。 云函数: [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init(); // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() // 简单路由 if (event.action && userHelper[event.action]) { const result = await userHelper[event.action](wxContext, event) return result } return { message: 'This action was not found', error: -1, } } const db = cloud.database(); const userHelper = { async getOpenId(context, params) { return { openid: context.OPENID, } } } [代码] 通过userHelper来管理所有的方法,你如果需要添加方法可以直接在userHelper下面添加即可,action传入方法名称就可以调用了。 这样便于查找与管理,同一类业务之需要创建一个云函数即可。 还有一点就是减少云函数数量,云开发不同的收费套餐是有云函数数量限制的。 套餐配合可见:配额说明 总结 全部写完了,如果觉得不错就给我的《#小程序云开发挑战赛#-情侣券-想做就做》点个赞,谢谢。
2020-09-22 - 【干货】如何做好小程序数据埋点
背景:在接触过上百家头部客户中,诊断和参与了数百次的数据体系搭建工作。几乎80%的App都没有科学的埋点规划,只采集显性数据,而更深层的与事件、参数相关的隐性数据,都没有采集到。埋点规划并不难!但为什么大部分企业都做的不太好?埋点规划需要整合产品、运营、技术和业务等跨部门的需求,运营同学不太懂技术、技术同学不太懂业务、产品同学不太懂埋点,这问题该如何解?在友盟+《战疫求生,开发者的危与机》直播公开课上,友盟+业务专家张跃梳理一套方法论。带你从埋点的坑、结构化的埋点方案、高效的事件管理入手进行结构化埋点。 一、 在埋点前,先带你避开埋点的深坑 第一坑:遗漏,指的是埋点采集不全面,有可能重要的数据并没有采集到,会对数据分析造成比较直接的影响,出现这个问题的原因是前期数据分析需求不清晰。 第二坑:杂乱。指的是数据采集比较零散,可以理解为前期并没有进行事件结构化的设计,通常是想到一个需求,就把这个需求提供给技术进行埋点。这种称之为“扁平化”的埋点方式,例如:某一个位置或者某一个功能的点击行为,就当做一个事件进行采集,看上去采集和查看很容易,但随着时间跟需求的增加,当采集了大量零散的事件之后,需要在统计工具中通过分组分析时,就会比较麻烦。 第三坑:低效。不同于杂乱,杂乱是任何行为数据都会直接当事件去进行采集,没有利用参数去进行结构化的设计。低效指的是在事件设计的时候,会去做结构化处理。但事件设计的参数逻辑会有问题,通常都是以大的页面这种框架的思维去进行设计。 举个例子:部分客户在设计时,会按照页面的思路去进行事件采集,页面上有推荐位,还有很多功能按钮的点击,那么就会把这个页面所有的点击行为都归到一个事件,并且点击具体的按钮和内容都当做参数传回来。但这里埋着两个雷区:1、在分析数据时,例如想了解整个用户浏览内容的情况,或者是想了解某个功能(搜索引擎)整体使用情况,按照如上设计,内容和功能的采集都分布在每一个事件中了,这样后面再归类、分析就非常不方便。2、当产品结构产生变化时,原有事件调整概率会比较大,因为之前都是按页面结构去设计,页面的调整直接影响事件采集。 第四坑:无用。指的是数据虽然采集了,但分析时根本用不上,这个问题主要有2个原因导致,一是前期需求不太清晰,另一个是之前的采集需求都是由不同人提出的,由于中间人员变动,很多采集需求就不清楚了,并且也不敢下掉,因为并不清楚这个事件是否还有人使用。 第五坑:复用。指的是事件重复采集,或者是需求重复,这个同样是与多个人提需求有关,并没有一个人去做整合管理,或者是说,没有一个工具去帮忙我们做管理。 二、如果想要避免这些坑,就需要坚守五个原则: 1、需求清晰。 2、合理设计。 3、实施规范。 4、结果可验 5、规范管理。 三、 埋点方法论——五步一全(ODEIIC),需要多角色参与统筹决策 第一、需求梳理。在梳理埋点设计的时候,通常会以产品、运营和市场以及KPI三个视角去切入。通常,产品关注的核心业务点会聚焦在内容和功能上,运营和市场关注的业务点在拉新、留存、促活和转化上,KPI视角会聚焦在转化与收入上,但也需要根据客户的实际情况而定。 同时,会把不同视角的业务需求再转化成需要关注的核心数据,如产品运营在内容上所需要关注用户浏览、内容的转发或者是偏好,针对功能使用会关注注册、登录、搜索等这些功能的使用情况。 [图片] 业务需求拆解成核心数据后,针对每一个核心数据进行维度的细分,如内容方面:会按照标题、频道或者是标签,进行拆分分析。那么我们针对功能方面,会按照功能使用情况以及步骤的转化去进行分析。通过要分析的关键点,就可以把细分维度拆出来,最后还会再加上一些通用的维度,例如可以对单个用户或者某一个地区的用户进行深度分析。 以产品视角的需求样例,产品通常情况下会聚焦内容与功能上的使用,但在需求收集时都是分散和抽象的,例如:业务需要分析内容偏好和推荐效果以及内容受欢迎的程度。那在这个环节就需要先做需求拆解,也就是说要去找到能分析这个需求的核心数据与能够帮助判断业务变化的一些指标,细分维度在这里的作用更多的是做需求详细的拆解,可以理解为是去做核心数据的多维度明细展示,那么目的就是从更细的维度去满足业务分析需求; 总结:先要找到能满足这个需求的核心数据,再找到核心数据分析时所需要涉及的细分维度,如图: [图片] 第二、事件设计。可以通过这3个步骤去完成事件的结构化设计,第一个步骤是要了解产品结构,也就是先要了解分析的范围是什么,例如需要知道对哪些页面或者哪些功能有分析需求;第二步,就是要针对这些锁定的范围,去明确我们要分析用户的行为有哪些;第三步,要把这些行为,落实到具体的分析维度上; 后面会通过指标体系、分析需求、分析方法这3个角度,在去结合这三个步骤,进行事件结构化设计的详细说明。 [图片] 在介绍按照指标体系去进行结构化事件设计前,我们先看下指标体系的样例,通常会按照这几个模块去搭建指标体系,分为:概况、营销、用户价值、运营和核心功能。 1、概况可以理解日常关注的核心数据,比如:新增、启动、日活、周活、月活以及会员数据、注册数据以及使用黏性、使用时长、留存等,还包括技术、产品较为关注的稳定性数据。总的来说:就是将核心或常看的数据放在概况的大板块中。 [图片] 2、营销。通常会把广告数据,例如:广告的曝光、点击率以及广告点击排行,媒体排行、展示排行信息会放在第二个板块。 3、用户价值。通常会把新用户的次留、成本以及用户回本周期模型和生命周期模型放在用户价值模块。 4、运营。主要关注内容与转化,通常会分析内容的热度,任务的交互与会员的转化,针对会员还会分析会员新增、会员累计、会员续费等维度。 5、核心功能。是产品岗位较为关注的,例如:导航位、导航按钮,被用户点击的情况、使用的情况,对应核心功能,比如说搜索功能或者是注册功能,整个功能的入口、被点击的情况和转化率等相关的这些数据会放到这个板块。 从指标体系到事件设计 [图片] 如何通过指标体系去进行结构化设计?指标体系可以理解为指标与报表的一个组合,整个指标体系对应到产品结构上,可以分为对产品页面和产品功能的分析需求。下面先从产品页面的角度去进行事件设计说明: [图片] 第一步,会先锁定页面的范围,比如产品里有活动页、内容页、如果是视频App的话会有播放页,小说App会有阅读或者是听书页面。 第二步,范围圈定后就需要找分析行为,用户看到内容是否有点击行为,进入页面后的浏览行为,以及是否有分享、评论等行为。 第三步,确定了要分析的行为后,就需要进行分析维度的细化,如要分析用户浏览(浏览完成行为)内容都有哪些,还想分析用户是哪个入口(来源)进入到页面等等,这些都是针对用户行为要分析的维度; 按照这三步梳理清楚后,事件设计中与产品页面相关的事件和参数就能整理出来了,如页面范围对应的“内容页”和分析行为对应的“点击”行为,就能够清楚我们要采集的事件为“内容点击”,在根据这个事件需要分析的维度是页面名称、页面分类以及页面来源,这个事件所需要的参数也就找到了。 下图中是以内容页和活动页梳理的结构化事件样例。 [图片] 以产品功能的角度去进行事件设计说明: [图片] 同样,第一步先找到要分析哪些功能。比如:搜索、登录、注册、会员、付费、签到等,第一步找到监测功能的范围。 第二步在找行为,功能层面的行为比内容会稍微简单一些,主要是点击行为或者是完成状态。 第三步是维度,例如:搜索功能,想分析搜索入口的点击情况,搜索的关键词是什么,针对登录与充值的话,需要分析帐号登录的类型、充值的方式等等。 页面功能所产出的结构化事件样例[图片] 以搜索引擎为例:搜索引擎监测的行为是点击和完成,通常会用两个事件进行监测,搜索引擎功能在很多页面都会有入口, 通常会建议在这里增加一个参数叫搜索位置,可以辨识用户点击哪些搜索位的按钮,另外可增加参数叫用户ID,去了解具体是哪些用户进行的点击。 重点说一下功能按钮点击事件。通常情况下,会将核心要分析的功能都抽离成单独的事件进行统计。如登录、注册、付费或者是会员购买等,这些属于核心要关注的功能,并且会为这些核心功能事件单独设计要分析的参数; 但如扫一扫、加载更多以及一些Tab键,只需要监测用户点击即可 ,不需要监测功能背后的参数信息。通常会将这些点击行为放在一个事件下,定义名称叫功能按钮点击,会通过“按钮名称“与“所属页面”等参数去锁定用户点的具体按钮是哪个。 小结,通过指标体系去进行事件设计,就能够把大部分需要采集的页面与功能都能覆盖到,并且可以满足后期看数据的需求。 从分析需求到事件设计 [图片] 先引用小说行业的一个需求举例,近期上架了新书,要分析新书对用户的吸引力如何。那么第一步,就要把需求进行转义,也就是需要知道哪些数据和维度,能证明用户对新书的吸引力。 针对这个需求,分析思路是:今天新上架的小说,用户看了多少章节和时间,明天会不会继续来看,可以通过这几个维度去判断出新书吸引力。那么在落实到事件设计的三个步骤中,第一步采集的页面范围是小说页面,第二步采集的行为就是阅读,这两步对应出我们需要采集的事件就是小说阅读,第三步需要分析的维度就是阅读章节、阅读时长、小说名称以及上线日期,这些维度就可以转化成参数在事件中设计进来。 另外,一般做内容事件时,通常还会增加来源参数,比如:来源页面、来源版块、来源位置,这些参数可以帮我们定位到用户是从那些入口获取到内容的,便于后期去分析各入口的导流效率。 从分析方法到事件设计 [图片] 这部分指的是根据核心目标,在利用一套分析方法去解决问题时,如何找到解决问题环节中所需要采集的事件。 比如,目标锁定是要提升用户留存或者是提升付费转化率,那么,首先要找到不同的人群,针对人群找差异(功能使用、内容偏好的差异),找到不同的人群在功能使用、以及转化路径的差异后,在去找问题,如某一些功能对于非留存用户或者是非付费用户体验不好或推荐的内容用户不感兴趣,找到问题后,就需要进行优化,并进行验证;针对分析方法中的每个环节,其实都能对应到需要分析的事件,如找问题的环节会对入口的点击、完成的情况,内容浏览的来源等等进行事件采集,在分人群环节,会对用户的付费行为进行事件采集等等。 通过每个环节找到对应需要分析的行为后,就可以把相关信息以事件或者是参数的形式,补充到现有结构化埋点方案中了。 按照指标、需求、方法这3个角度去做了事件设计方法的介绍,总体可归纳为:有了指标体系与分析需求,整个结构化埋点方案的框架就能设计出来了。分析方法更多的作用是做分析思路上的贯穿,可以帮我们发现埋点设计中缺少或者遗漏的环节,整体上我们就可以理解为,指标体系+分析需求+分析方法这三部分的结合,才能得到一个非常贴合业务的埋点方案。 [图片] 小结:“事件采集“就是要知道谁在什么时候做了什么事情,设计思路可以分为三步,首先,了解产品结构(产品结构的范围,页面结构、功能结构)其次,了解用户行为(点击行为、完成行为、曝光行为等)最后,行为可以细分哪些维度,按照三步结构化事件就可以设计完成了。 同时,总结三个避坑的Tips: 一,需求。如果前期需求不是很明确时,可以先把这个指标体系梳理起来,比如:核心关注的指标,采集方案是可以满足暂时看数的需求,后期可以根据对分析需求的升级再去补充。 二,归类。在事件设计时要合理的进行归类,尽量用一个事件满足多个分析需求。比如,了解用户都是从哪些入口获取内容的,和内容浏览的热度排行。是可以通过一个事件来实现的,只需要通过内容名称和来源页面两个参数,就能够满足这两个需求了。 三,范围,在参数设计中两个范围需要注意,即来源和点击按钮,内容采集会涉及三个来源:来源页面、来源板块和来源位置,是为了去锁定到底内容从哪里点过来,开发也会要求将入口信息梳理清楚,从而进行埋点的开发工作。点击按钮,将按钮都归属到一个事件中,将参数设置为按钮名称,梳理出具体的按钮采集的范围给到开发,才能去进行后续的埋点。 埋点设计不是简单的事件与参数的结合,而是需要贴合业务、贴合分析场景去进行设计。 结构化事件设计完成后,下一步就是要交付给技术进行开发,下图为一个资讯行业的事件埋点模版,可以参照这个模板去进行梳理并提交给技术。友盟+开发者数据银行产品中的智能采集平台就可以按照这个模板,直接帮我们生成对应的埋点方案,并协助我们进行后续的事件管理。 [图片] 三、埋点实施 市场上主流支持的四种埋点方式,分别是代码埋点、服务端埋点、可视化埋点和全埋点。 代码埋点,支持事件与参数这种结构化的使用方式,弊端是想增加或修改事件,都需要重新发版,用户更新后才能采集。 服务端埋点,通常用于业务数据的采集,例如:付费成功、用户注册等,这个场景会选择用服务埋点进行采集。 可视化埋点和全埋点,都是解决整个App前端操作的一些点击行为,例如说某些按钮、页面,每一个点击都能监测。但差异点在于可视化埋点只能看到圈定后的数据,那么全埋点则是在圈定时,历史数据也能去追溯。 但这两个埋点的弊端是散点采集,每一个点击行为都是一个事件,在数据分析时,事件的量级会较大,不易于分析,而且它只能是取这种点击行为的事件,并不能把参数带过来,你可以理解为它就是一个纯扁平化的一个事件采集。 针对需求的不同,数据采集方式应该是结合使用的,以友盟+为例,友盟+现在支持两种埋点方式,代码埋点和可视化埋点,开发者可以结合使用,去满足事件方案的采集需求。 [图片] 四、看板校验。埋点后可通过三种方式验证,一、打印日志,开启debug去打印Log,去验证触发事件log是否有上报,这种方式需要技术来配合验证。 二、集成测试,以友盟+为例,只需要让技术注册一个测试设备,就可在你这个测试设备上去启用你的App,在去触发事件,产品、运营的同学就可直接测试埋点情况。三,也可以使用市场上智能验证的工具,以友盟+为例,可先注册设备,自动去识别整个埋点的情况,且日志是实时的,可产出事件的验证报告。 五、智能验证,可以帮您智能验证这些事件的点是否采集了,是否有遗漏,最后会定期给出体检报告,详细的明细都会有。在友盟+的智能采集页面就可以智能验证埋点,只需要注册一个测试设备,这个测试设备填加完之后会实时把客户这些埋点的数据进行验证,到底是成功还是异常,以及测试的时间是什么都会有详细的数据。 综上所述:一个公司的埋点要可见、可控、可管,如果一家公司不清楚自己的埋点结构,便是在错误的数据上长期持续经营业务,越走越错。合理的埋点方案,可以使埋点能够智能调试和验证,大幅降低埋点采集的成本,从而最终达成数据质量的根本性提升。
2020-08-14 - 基于云开发的商城小程序开发之路系列之准备篇
本系列文章主要记录基于云开发 的商城小程序的开发过程以及开发过程中遇到的问题 写在之前:正式接触微云开发是在19年10月份,刚开始只是把它当做一个新的知识去了解,偶尔也会写个代码片段测试单个功能。随着了解的深入慢慢的喜欢上了这种新的开发模式,然后就按照商城的需求一点一点的找云开发的替代方案。我记得当时遇到的问题包括:定时发送模板消息 、支付回调地址 、商城后台管理等等这一些列问题,总终这些问题都一一得到解决。故而在新项目开始之际记录开发 过程中的点点滴滴,一来重新梳理思路 逻辑,二来供广大开发者参考以便指点雅正。 开发准备1,新建小程序填入APPID 使用云开发 [图片] 2,开通 云开发 [图片] [图片] 创建环境,等待初始化 [图片] 3,回调IDE 删除无用文件 [图片] 移除多余文件后空白项目 [图片] 4,添加小程序常用工具了,搭建简单的开发框架 [图片] config.js const userUtils = require("../utils/userUtils.js") const user = require("../mode/user.js") module.exports = { no_data: "cloud://xxxxx/no_data.png", faild: "cloud://xxxxx/net_error.png", net_error: "cloud://xxxxx/net_error.png", no_order: "cloud://xxxxx/res/no_order.png", pageSize: 10, minClickTime: 500, order_out_time: 60 * 1000 * 30, ok: "cloud.callFunction:ok", env: "xxxxx",//环境id tmpid_order_cancel: "xxxxx", //订单取消模板消息id tmpid_order_send: "xxxxx", //订单发货 } user .js module.exports = { USER_ID: "user_id", HEIGHT: "height", MAIN_HEIGHT: "main_height", USER_NAME: "user_name", AVATAR: "avatar", SX: "sx", } file.js let path="" function setPath(locationPath) { path=locationPath } function uploadFile(fileList, index, afterUpSu, posi) { const imgItem = fileList[index] if (imgItem.is_up) { toNext(fileList, index, afterUpSu, posi) } else { if (!imgItem.location_path) { toNext(fileList, index, afterUpSu, posi) } else { wx.compressImage({ src: imgItem.location_path, // 图片路径 quality: 70, // 压缩质量 success: function(res) { imgItem.temppath = res.tempFilePath upImgToCloud(fileList, index, afterUpSu, posi) }, fail: function(res) { upImgToCloud(fileList, index, afterUpSu, posi) }, complete: function(res) {}, }) } } } function upImgToCloud(fileList, index, afterUpSu,posi) { const imgItem = fileList[index] const cloudPath = path+ getMadomChart(10) + new Date().getTime() + ".jpg" wx.cloud.uploadFile({ cloudPath: cloudPath, filePath: imgItem.temppath || imgItem.location_path, // 文件路径 success: res => { // get resource ID console.log("上传图片 success==" + JSON.stringify(res)) const fileID = res.fileID imgItem.file_id = fileID imgItem.is_up = true wx.cloud.getTempFileURL({ fileList: [{ fileID: fileID }] }).then(urlRes => { console.log("getTempFileURL success==" + JSON.stringify(urlRes)) imgItem.url = urlRes.fileList[0].tempFileURL toNext(fileList, index, afterUpSu, posi) }).catch(error => { console.log("getTempFileURL catch==" + JSON.stringify(error)) toNext(fileList, index, afterUpSu, posi) }) }, fail: err => { // handle error console.log("上传图片 err==" + JSON.stringify(err)) toNext(fileList, index, afterUpSu, posi) } }) } function getMadomChart(length) { const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" let result = ''; for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]; return result; } /** * 下一张 */ function toNext(fileList, index, afterUpSu, posi) { let nextIndex = 0 if (index < fileList.length - 1) { nextIndex = index + 1 uploadFile(fileList, nextIndex, afterUpSu, posi) } else { afterUpSu(fileList, posi) } } module.exports = { setPath: setPath, uploadFile: uploadFile, } userUtils.js let utils = require("util.js") const user = require("../mode/user.js") const log = require("../utils/log.js") const config = require("../config/config.js") let userModel = {} /** * 获取窗口高度以及 sx(像素与rpx的比) */ function getHeight() { let oldHight let mainHeight const that = this console.log("mainHeight getHeight==") try { oldHight = getDataByKey(user.HEIGHT) mainHeight = getDataByKey(user.MAIN_HEIGHT) } catch (e) { oldHight = 0 mainHeight = 0 } if (oldHight > mainHeight && mainHeight > 0) { return } wx.getSystemInfo({ success: function(res) { let height = res.windowHeight console.log("mainHeight==" + mainHeight) console.log("height==" + height) if (mainHeight > 0) { } else { setDataBykey(user.MAIN_HEIGHT, height) } let width = res.windowWidth let sx = width / 750 setDataBykey("sx", sx) if (height > oldHight) { setDataBykey(user.HEIGHT, height) } }, }) } function setDataBykey(key, value) { userModel[key] = value try { wx.setStorageSync(key, value) } catch (e) { wx.setStorage({ key: key, data: value }) } } function getDataByKey(key) { if (userModel[key]) { return userModel[key] } else { try { return wx.getStorageSync(key) } catch (e) { wx.getStorage({ key: key, success(res) { return res.data } }) } return "" } } function saveUserMsg(res, page, doWhat) { const detail = res.detail if (detail && detail.errMsg != "getUserInfo:fail auth deny") { setDataBykey(user.AVATAR, detail.userInfo.avatarUrl || "") setDataBykey(user.USER_NAME, detail.userInfo.nickName || "") page.setData({ avatar: detail.userInfo.avatarUrl, nickName: detail.userInfo.nickName }) wx.cloud.callFunction({ name: "updata_userinfo", data: detail.userInfo }) if (doWhat) { doWhat() } } else { wx.showToast({ title: '用户信息获取失败', icon: "none" }) } } function login() { console.log("login") const data = {} data.req_type = "login" data.data = {} wx.cloud.callFunction({ name: "user_manage", data: data }).then(res => { if (res.errMsg == config.ok) { if (res.result.code == 200) { const openid = res.result.data.openid const unionid = res.result.data.unionid userUtils.setDataBykey(user.USER_ID, openid) userUtils.setDataBykey(user.UNIONID, unionid) } } }) } module.exports = { getHeight: getHeight, saveUserMsg: saveUserMsg, getDataByKey: getDataByKey, setDataBykey: setDataBykey, login: login, } utils.js const config=require("../config/config.js") let imgPath = "" function formatNumber(n) { n = n.toString() return n[1] ? n : '0' + n } function formatTime(date, format) { var formateArr = ['YY', 'MM', 'DD', 'hh', 'mm', 'ss']; var returnArr = []; returnArr.push(date.getFullYear()); returnArr.push(formatNumber(date.getMonth() + 1)); returnArr.push(formatNumber(date.getDate())); returnArr.push(formatNumber(date.getHours())); returnArr.push(formatNumber(date.getMinutes())); returnArr.push(formatNumber(date.getSeconds())); for (var i in returnArr) { format = format.replace(formateArr[i], returnArr[i]); } return format; } /** * 判断权限 * * scope 权限名字 * contentMsg 权限被拒绝时提示文字 * doWhat 授权成功后续操作 * */ function getAuthorize(scope, contentMsg, doWhat) { var that = this if (wx.getSetting) { wx.getSetting({ success(res) { if (!res.authSetting[scope]) { wx.authorize({ scope: scope, success() { // 用户已经同意小程序使用录音功能,后续调用 wx.startRecord 接口不会弹窗询问 doWhat() }, fail: function(error) { wx.showModal({ title: '提示', content: contentMsg + "权限未开启", confirmText: "去打开", cancelText: "暂不", success: function(res) { if (res.confirm) { wx.navigateTo({ url: '../openAuthorize/index?auth=' + contentMsg, }) } } }) } }) } else { doWhat() } }, }) } } /** * 设置图片下载地址 */ function setImgPath(downLoadPath) { imgPath = downLoadPath } function saveImg() { this.getAuthorize('scope.writePhotosAlbum', "相册", downLoadPhoto) } /** * 下载图片 */ function downLoadPhoto() { if (wx.showLoading) { wx.showLoading({ title: '正在保存', mask: true }) } var that = this wx.saveImageToPhotosAlbum({ filePath: imgPath, success: function (res) { if (wx.hideLoading) { wx.hideLoading() } wx.showModal({ title: '提示', content: '图片已保存', confirmText: "我知道了" }) }, fail: function (error) { if (wx.hideLoading) { wx.hideLoading() } } }) } /** * 比较两个版本 */ function compareVersion(v1, v2) { v1 = v1.split('.') v2 = v2.split('.') var len = Math.max(v1.length, v2.length) while (v1.length < len) { v1.push('0') } while (v2.length < len) { v2.push('0') } for (var i = 0; i < len; i++) { var num1 = parseInt(v1[i]) var num2 = parseInt(v2[i]) if (num1 > num2) { return 1 } else if (num1 < num2) { return -1 } } return 0 } /** * 检查更新 */ function checkUpData() { if (wx.getUpdateManager) { const updateManager = wx.getUpdateManager() updateManager.onCheckForUpdate(function(res) { // 请求完新版本信息的回调 console.log(res.hasUpdate) }) updateManager.onUpdateReady(function() { wx.showModal({ title: '更新提示', content: '新版本已经准备好,是否重启应用?', success: function(res) { if (res.confirm) { // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 updateManager.applyUpdate() } } }) }) updateManager.onUpdateFailed(function() { // 新的版本下载失败 }) } } function isPhone(phone) { if (!(/^1[3456789]\d{9}$/.test(phone))) { return false; } return true } function initCloud() { if (!wx.cloud) { console.error('请使用 2.2.3 或以上的基础库以使用云能力') } else { wx.cloud.init({ // env 参数说明: // env 参数决定接下来小程序发起的云开发调用(wx.cloud.xxx)会默认请求到哪个云环境的资源 // 此处请填入环境 ID, 环境 ID 可打开云控制台查看 // 如不填则使用默认环境(第一个创建的环境) env: config.env, traceUser: true, success:function(res) { } }) } } module.exports = { formatTime: formatTime, initCloud: initCloud, getAuthorize: getAuthorize, compareVersion: compareVersion, checkUpData: checkUpData, setImgPath: setImgPath, saveImg: saveImg, isPhone: isPhone, } 5,建立集合 [图片] 6,小程序初始化 //app.js const utils = require("utils/util.js") const userUtils = require("utils/userUtils.js") const user = require("mode/user.js") const config = require("config/config.js") App({ onLaunch: function () { userUtils.getHeight() utils.initCloud() utils.checkUpData() this.globalData = {} userUtils.login() }, }) 到此小程序基本框架搭建完毕,下篇聊一下云开发之后台管理
2020-01-11 - 将小程序原生异步函数promisify后,在async/await中使用
目前,小程序中支持使用async/await有三种模式: 1、不勾选es6转es5,不勾选增强编译;该模式是纯es7的async/await,需要基础库高版本。 2、勾选es6转es5,勾选增强编译;一般是因为调用了第三方的es5插件,通过增强编译支持async/await。 3、勾选es6转es5,不勾选增强编译;手工引入runtime.js支持async/await。 据最近更新情况,原生的函数已经大部分同时原生支持同步化了,不需要本方案转化了,直接加上await即可;比如wx.chooseImage、wx.showModal。。。具体有哪些,可以自己试。 如果只是wx.request的同步化,可参考: https://developers.weixin.qq.com/community/develop/article/doc/0004cc839407a069f77a416c056813 app.js代码: function promisify(api) { return (opt, ...arg) => { return new Promise((resolve, reject) => { api(Object.assign({}, opt, { success: resolve, fail: reject }), ...arg) }) } } App({ globalData: {}, chooseImage: promisify(wx.chooseImage), request: promisify(wx.request), getUserInfo: promisify(wx.getUserInfo), onLaunch: function () { }, }) 某page的index.js代码: const app = getApp() testAsync: async function(){ let res = await app.chooseImage() console.log(res) res = await app.request({url:'url',method:'POST',data:{x:0,y:1}}) console.log(res) }, [图片]
2020-10-20 - 从0到1开发一个小程序cli脚手架(二) --版本发布/管理篇
接上文 《从0到1开发一个小程序cli脚手架(一)–创建页面/组件模版篇》 github地址:https://github.com/jinxuanzheng01/xdk-cli 觉得有用的朋友帮忙给项目一个star,谢谢 上文大家应该大致学会了怎么搭建一个cli脚手架,包括实现了一个快速生成启动模版的功能,本质上作为脚手架应该可以做更多的事情,本篇文章会实现一些新的功能,例如:自动发布体验版,版本号控制,环境变量控制 痛点 不知道大家有没有一天发多次版本或者一天给多个小程序发版的经历,按照微信正常的发布流程,需要: 修改版本号/版本描述 修改发布环境 点击微信开发者工具上传体验版 提交审核 确认环境/版本 点击发布 其中所有的1,2步为手工修改config文件,第5步是确认手工修改config文件的正确性,毕竟人总会犯错,作者表示就干过线下环境发布到测试环境的事情,而且这是在做了第5步的情况下,很遗憾没有仔细核对 为了不再次发生同样的事情导致引咎辞职,那么有没有更好的方法呢 ?自然是有的,既然人不可靠,那么直接把它流程化自然就可以了 准备工作 最好阅读了上篇文章《从0到1开发一个小程序cli脚手架(一)–创建页面/组件模版篇》,并搭建了一个简单的demo 需要了解微信小程序提供的一些cli能力, 点击这里 Let‘ go 后续的很多流程上的实操代码为了缩短篇幅会以伪代码的形式来进行描述,强烈推荐先阅读上文,如果需要更详细的实操代码请去github仓库查看 实际效果图:[图片] 梳理流程 识别命令行 询问问题,拿到版本号和版本描述 调用微信提供的cli能力,进行体验版上传 是不是发现非常简单,事实也是如此,整个功能做下来也就60行代码 ~ 目录结构 项目结构分为入口文件,配置文件 [代码]- lib - publish-weapp.js - index.js - config.js - node_modules - package.json [代码] config.js用来记录一些基础常量和默认项的配置,例如项目路径,执行路径等 [代码]module.exports = { // 根目录 root: __dirname, // 执行命令目录路径 dir_root: process.cwd(), // 小程序项目路径 entry: './', // 项目编译输出文件夹 output: './', } [代码] 识别命令行 在入口文件(index.js)处利用第三方库 commander 识别命令行参数,同时作为路由进行任务分发, [代码]#!/usr/bin/env node const version = require('./package').version; // 版本号 /* = package import -------------------------------------------------------------- */ const program = require('commander'); // 命令行解析 /* = task events -------------------------------------------------------------- */ const publishWeApp = require('./lib/publish-weapp'); // 发布体验版 program .command('publish') .description('发布微信小程序体验版') .action((cmd, options) => publishWeApp(); /* = main entrance -------------------------------------------------------------- */ program.parse(process.argv) [代码] 创建交互命令 接下来是一个QA环节,这时候需要根据自己的实际需要安排自己需要的命令,可以回想一下要解决的问题: 修改版本号/版本描述 修改发布环境 根据cli自动上传体验版 大概得出这样一个队列: [代码]function getQuestion({version, versionDesc} = {}) { return [ // 确定是否发布正式版 { type: 'confirm', name: 'isRelease', message: '是否为正式发布版本?', default: true }, // 设置版本号 { type: 'list', name: 'version', message: `设置上传的版本号 (当前版本号: ${version}):`, default: 1, choices: getVersionChoices(version), filter(opts) { if (opts === 'no change') { return version; } return opts.split(': ')[1]; }, when(answer) { return !!answer.isRelease } }, // 设置上传描述 { type: 'input', name: 'versionDesc', message: `写一个简单的介绍来描述这个版本的改动过:`, default: versionDesc }, ] } [代码] 最后获得的json对象,是这个样子: [代码]{isRelease: true, version: '1.0.1', versionDesc: '这是一个体验版'} [代码] 确定是否发布正式版 主要是因为体验版并非完全是发行版,公司内部测试的时候也是需要发布测试用体验版的,但是又涉及只有正式版本修改本地版本文件,那么就只能多此一问作为区分了 设置版本号 / 设置版本信息 [图片] 设置上述如图的 体验版上的版本号和描述信息,这里有个case是只有第一个问题选择发布正式版才会让你设置版本号,测试版本使用默认版本号0.0.0,这也是区分体验版的一种方式 [代码] when(answer) { return !!answer.isRelease } [代码] 这里只设置了三个问题:是否发布正式版,设置版本号,设置上传描述,当然你也可以设置自己想做的其他事情 上传体验版 翻了下小程序的文档,大概犹如下几个关键点: 找到cli工具 小程序cli并非全局安装,需要自己去索引路径,命令行工具所在位置:macOS: [代码]<安装路径>/Contents/MacOS/cli[代码] Windows: [代码]<安装路径>/cli.bat[代码] mac的 安装路径 如果是默认安装的话,是/Applications/wechatwebdevtools.app/, 外加cli的位置是: /Applications/wechatwebdevtools.app/Contents/Resources/app.nw/bin/cli windows 的话作者表示没有这个环境,只能大家自己探索了 拼凑上传命令 官方文档给了非常详细的描述: [图片] [代码]# 上传路径 /Users/username/demo 下的项目,指定版本号为 1.0.0,版本备注为 initial release cli -u 1.0.0@/Users/username/demo --upload-desc 'initial release' # 上传并将代码包大小等信息存入 /Users/username/info.json cli -u 1.0.0@/Users/username/demo --upload-desc 'initial release' --upload-info-output /Users/username/info.json [代码] 编写上传逻辑 基本流程: 获取cli -> 获取当前版本配置 -> 问题队列(获取上传信息)-> 执行上传(cli命令)-> 修改本地版本文件 -> 成功提示 [代码]// ./lib/publish-weapp.js 文件 module.exports = async function(userConf) { // cli路径 const cli = `/Applications/wechatwebdevtools.app/Contents/Resources/app.nw/bin/cli`; // 版本配置文件路径 const versionConfPath = Config.dir_root + '/xdk.version.json'; // 获取版本配置 const versionConf = require(versionConfPath); // 开始执行问题队列 anser case: {isRelease: true, version: '1.0.1', versionDesc: '这是一个体验版'} let answer = await inquirer.prompt(getQuestion(versionConf)); versionConf.version = answer.version || '0.0.0'; versionConf.versionDesc = answer.versionDesc; //上传体验版 let res = spawn.sync(cli, ['-u', `${versionConf.version}@${Config.output}`, '--upload-desc', versionConf.versionDesc], { stdio: 'inherit' }); if (res.status !== 0) process.exit(1); // 修改本地版本文件 (当为发行版时) !!answer.isRelease && await rewriteLocalVersionFile(versionConfPath, versionConf); // success tips Log.success(`上传体验版成功, 登录微信公众平台 https://mp.weixin.qq.com 获取体验版二维码`); } // 修改本地版本文件 function rewriteLocalVersionFile(filepath, versionConf) { return new Promise((resolve, reject) => { fs.writeFile(filepath, jsonFormat(versionConf), err => { if(err){ Log.error(err); process.exit(1); }else { resolve(); } }) }) } [代码] 注意这里需要在项目根目录下创建一个xdk.version.json的版本文件,详细配置说明见仓库 大致是这样的结构: [代码]// xdk.version.json { "version": "0.12.2", "versionDesc": "12.2版本" } [代码] 到这里基本就完成了上传的所有步骤,可以当前项目下键入[代码]xdk-cli publish[代码]查看一下程序是否正常运行,注意上传出错记得检查是否处于登录状态 扩展:关于版本号自增 可以看到在版本号环节使用的是list类型,而非input类型,这是因为手写版本号有写错的风险, 还是让我做选择题吧 emmm… 最终效果: [图片] 就不在这里展开了,可以下, 搜索[代码]getVersionChoices[代码]函数: https://github.com/jinxuanzheng01/xdk-cli/blob/master/lib/publish-weapp.js 解决打包工具的问题 你会发现是运行cli命令就直接发布了,那么用gulp,webpack等工具项目因为需要上传dist目录而非src的原因,需要先进行打包再进行发布: gulp -> xdk-cli publish ,将一个动作拆成了两个 遗忘了一个怎么办?纠结了 不知道大家对微信开发者工具的上传钩子还有没有印象,要实现的大概就是这么个东西, 一个上传前预处理的钩子 [图片] 这个实现起来也很简单,首先给用户的配置文件(xdk.config.js)开一个可配置项: [代码]// xdk.config.js { // 发布钩子 publishHook: { async before(answer) { this.spawnSync('gulp'); return Promise.resolve(); }, async after(answer) { this.log('发布后的钩子执行了~'); return Promise.resolve(); } } } [代码] 在publish-weapp.js中去识别钩子即可: [代码] // 前置钩子函数 await userConf.publishHook.before.call(originPrototype, answer); //上传体验版 let res = spawn.sync(cli, ['-u', `${versionConf.version}@${Config.output}`, '--upload-desc', versionConf.versionDesc], { stdio: 'inherit' }); // 后置钩子函数 await userConf.publishHook.after.call(originPrototype, answer); [代码] 当然,你需要判断下钩子是否存在,类型判断,包括需要返回给用户配置里一些基本的方法和属性,例如:日志输出,命令行执行等 我这边以gulp为例,可以看到在publsih前先执行[代码]gulp[代码],后进行发布,最后log出一行提示信息 完全符合预期 解决环境变量切换问题 解决了自动上传的问题,接下来就要解决环境变量切换的问题了,这里还要借用下刚才写的钩子函数: [代码]{ // 发布钩子 publishHook: { async before(answer) { this.spawnSync('gulp', [`--env=${answer.isRelease ? 'online' : 'stage'}`]); return Promise.resolve(); }, async after(answer) { this.log.success('发布后的钩子执行了~'); return Promise.resolve(); } } } [代码] 以gulp为例,根据之前的回答的结果answer.isRelease,判断是否为使用正式环境 如果你没有使用编译工具,也可以提出一个env的config文件,使用fs模块直接重写该环境变量 下面是一串伪代码: [代码]目录结构 - app (小程序项目) - page - app.json ... - env.js - xdk.config.js - xdk.version.js - envConf.js // envConf.js module.exports = { ['online']: { appHost1: 'https://app.xxxxxxxxx.com', appHost2: 'https://app.xxxxxxxxx.com', }, ['stage']: { appHost1: 'https://stage.xxxxxxxxx.com', appHost2: 'https://stage.xxxxxxxxx.com', } }; // xdk.config.js publishHook: { async before(answer) { let config = require('envConf.js')[answer.isRelease ? 'online' : 'stage']; fs.writeFile('./app/env.js', jsonFormat(jsonConf), (err) => { if(err){ this.log.error('写入失败') }else { this.log.success('写入成功); } }); return Promise.resolve(); } [代码] 打包工具的问题还有环境变量的问题,我都没有写在cli里面,是因为不同项目之间对环境变量的写法,格式都不尽相同,具体要怎么做还是要留给开发者自己来确定吧,这样看起来更灵活一些 最后 总之利用脚手架解决了从发布到上线的一连串问题,使得不再担心因为切换环境导致线上bug,也不再担心写版本号写错的问题,确认线上环境这个环境也变成了一个非强需求的事情 整个上线流程也只需要:xdk-cli publish -> 提交审核即可,而且整个代码也并不复杂,publish-weapp.js整个文件算上注释也就60行代码,何乐不为呢? 注:下篇会讲一下如何做自定义指令,帮助小伙伴可以更自由的适配不同的项目~
2019-08-05 - 3分钟教你学会使用路线规划小程序插件
路线规划小程序插件是腾讯位置服务开发的一款为用户规划驾车、公交、步行路线方案的插件。开发者可以直接在小程序内使用这个插件,从而为自己的用户提供多种出行方案选择。 路线规划插件的功能 路线规划插件能为用户规划驾车出行路线(如下图1所示),并且当行车起点和行车终点之间可以规划出多个方案时会展示多个方案及方案耗时。这些不同方案体现了不同的策略,例如根据实时路况时间最短、红绿灯数较少、少收费等策略。 同时驾车路线在地图中会通过不同路线的颜色直观反映道路的拥堵情况,例如红色路线表示那段道路拥堵,这就能够让用户提前规避拥堵路段。 路线规划插件也能为用户规划步行出行路线(如下图2所示),不仅显示了步行路线距离和耗时信息,还显示了用户步行过程中,走过的天桥、人行横道数量,更人性化的显示了步行消耗了多少卡路里。 [图片] [图片] 路线规划插件还能为用户规划公交出行路线(如下图所示),提供多种公交和地铁出行方案,并且用户可以根据自己的实际情况进行方案排序,例如时间短优先排序、少步行优先排序、少换乘优先排序。出行方案上也会有时间短这样的标志信息说明方案特点。 [图片] 路线规划插件的应用场景 路线规划插件应用场景非常丰富,可以直接接入到餐饮、电影等各种类型的小程序中,让消费者在小程序中就能获得到达门店的路线规划方案,方便去门店消费。 设想一个场景,小王周末想要吃一顿大餐,于是打开了某家餐厅小程序,当小王决定去这家餐厅时,不需要再打开地图软件去规划出行路线,通过我们的路线规划插件,在这家餐厅的小程序中就能直接规划小王目前的位置到餐厅的出行路线。小王可以选择开车去餐厅,如果今天车牌号限行,那么小王也可以选择公共交通出行,如果到餐厅的距离很近,那么小王可以选择步行方式到达餐厅。 小程序只需要使用路线规划插件就能拥有这些全面精准规划路线能力。看了这些功能,是不是想马上体验呢?别急!接下来就介绍路线规划插件的使用方法。 路线规划插件的使用方法1、申请路线规划插件在微信公众平台中, “微信小程序官方后台-设置-第三方设置-插件管理” 里点击 “添加插件”(如下图所示),搜索 “腾讯位置服务路线规划” ,选择添加插件,小程序开发者就可以在小程序内使用该插件了。 [图片] 2、申请key调用路线规划插件需要申请腾讯位置服务的服务账号,key是开发者的唯一标识,申请key请点击这里。申请key的具体步骤如下: 2.1 填写申请信息[图片] 2.2 创建key成功[图片] 2.3 授权小程序appid开通微信小程序服务:控制台 -> key管理 -> 设置(使用该功能的key)-> 勾选“微信小程序” -> 填写“授权 APP ID” ->保存。 [图片] 2.4 勾选“WebService API”及“白名单”微信小程序插件需要使用WebService API的部分服务,所以使用该功能的key需要具备相应的权限。 [图片] 如果开发者之前是腾讯位置服务的用户并申请过key,则可以跳过上面2.1、2.2的步骤,直接进行2.3、2.4步骤的设置。 3、在小程序中引入路线规划插件只需要在小程序的app.json文件做如下配置就可以在小程序中引入路线规划插件: [代码]// app.json[代码][代码]{[代码][代码] [代码][代码]"plugins"[代码][代码]: {[代码][代码] [代码][代码]"routePlan"[代码][代码]: {[代码][代码] [代码][代码]"version"[代码][代码]: [代码][代码]"1.0.0"[代码][代码],[代码][代码] [代码][代码]"provider"[代码][代码]: [代码][代码]"wx50b5593e81dd937a"[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"permission"[代码][代码]: {[代码][代码] [代码][代码]"scope.userLocation"[代码][代码]: {[代码][代码] [代码][代码]"desc"[代码][代码]: [代码][代码]"你的位置信息将用于小程序定位"[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码]}[代码]4、在小程序中调用路线规划插件在小程序中调用路线规划插件也非常简单: [代码]let plugin = requirePlugin([代码][代码]'routePlan'[代码][代码]);[代码][代码]let key = [代码][代码]''[代码][代码]; [代码][代码]//使用在腾讯位置服务申请的key[代码][代码]let referer = [代码][代码]''[代码][代码]; [代码][代码]//调用插件的小程序的名称[代码][代码]let startPoint = JSON.stringify({ [代码][代码]//起点[代码][代码] [代码][代码]'name'[代码][代码]: [代码][代码]'中国技术交易大厦'[代码][代码],[代码][代码] [代码][代码]'latitude'[代码][代码]: 39.984154,[代码][代码] [代码][代码]'longitude'[代码][代码]: 116.30749[代码][代码]});[代码][代码]let endPoint = JSON.stringify({ [代码][代码]//终点[代码][代码] [代码][代码]'name'[代码][代码]: [代码][代码]'北京西站'[代码][代码],[代码][代码] [代码][代码]'latitude'[代码][代码]: 39.894806,[代码][代码] [代码][代码]'longitude'[代码][代码]: 116.321592[代码][代码]});[代码][代码]wx.navigateTo({[代码][代码] [代码][代码]url: [代码][代码]'plugin://routePlan/route-plan?key='[代码] [代码]+ key + [代码][代码]'&referer='[代码] [代码]+ referer + [代码][代码]'&endPoint='[代码] [代码]+ endPoint[代码][代码]});[代码]如以上示例代码所示,只需要传4个参数,就能为小程序用户提供驾车、公交、步行路线规划信息了。这4个参数含义如下: key,开发者的唯一标识,第2步申请的key referer,调用插件的小程序的名称 startPoint,起点名称和坐标信息,如果不传起点参数,则起点默认当前用户的真实位置 endPoint,终点名称和坐标信息 怎么样?看了上面的使用方法是不是觉得很简单呢?腾讯位置服务开发路线规划插件的目的就是为了减少开发者开发成本,解放开发者生产力,所以才把这些复杂的路线规划业务封装成了插件,方便小程序开发者使用。 那么还犹豫什么呢?立即点击这里去体验使用吧! 另外,腾讯位置服务还推出了地铁图小程序插件,为用户提供查看各城市地铁线路的功能,还能帮用户检索到最优点地铁出行线路及每个站队的详情信息。 后续,腾讯位置服务还会开发更多的关于地图相关的小程序插件,还请各位开发者持续关注我们的服务商主页!
2019-08-05 - BookChat v2.1 发布,开源的书籍阅读微信小程序
BookChat 介绍 微信叫[代码]WeChat[代码],所以我们叫[代码]BookChat[代码]. [代码]BookChat[代码] - 面向程序员的开源书籍和文档阅读学习小程序,同时也是一款基于 Apache 2.0 开源协议进行开源的通用书籍阅读微信小程序。 升级日志 实现微信登录和绑定 移除小程序的普通注册(微信一键登录更便捷) 调整内容显示样式,优化内容阅读体验 列表页增加搜索入口 优化登录提示和登录跳转 小程序体验码 体验一下,相信你会喜欢,没有理由 [图片] 开源地址 Gitee: https://gitee.com/truthhun/BookChat GitHub: https://github.com/TruthHun/BookChat
2019-07-18