- 地理位置接口新增与相关流程调整
一、地理位置接口新增说明 由于精确地理位置接口只允许部分类目的小程序申请使用,为了满足开发者在更多场景使用地理位置接口,自 2022 年 7 月 14 日起,新增获取模糊地理位置接口(wx.getFuzzyLocation)。同时为保障用户合法权益,该接口调用前需进行准入开通申请,该接口准入规则与 wx.chooseLocation 一致。 wx.getFuzzyLocation 接口说明: 1、该接口返回的是经过模糊处理的经纬度坐标; 2、该接口支持返回 wgs84 或 gcj02 两种类型的坐标; 3、该接口需要用户授权 scope.userFuzzyLocation。 二、app.json 的配置指引 为了开发者能够正常使用获取模糊地理位置等接口,以及后续对于代码提审环节的优化(见「三、地理位置接口使用流程」),自 2022 年 7 月 14 日起,开发者在使用地理位置相关接口时(共计 8 个,见表1),需要提前在 app.json 中进行配置。 1、需配置的接口列表 [图片] 表1 2、配置规则 1)在代码中使用的地理位置相关接口(共计 8 个,见表1),开发者均需要在 app.json 中 requiredPrivateInfos 配置项中声明,代码格式如下: [图片] 2)表1中模糊位置信息(序号1)和精确位置信息(序号2-5)是互斥的,即声明了模糊位置信息就无法声明精确位置信息。若同时声明模糊位置信息和精确位置信息,则在编译代码时出现错误; 3)注意:自 2022 年 7 月 14 日后发布的小程序,如果未在 app.json 中声明表1中的相关接口,则小程序调用这些接口(表1)时会出现错误,在 2022 年 7 月 14 日之前发布的小程序不受影响; 4)对于第三方开发者,需要在上传代码时通过参数在 ext.json 中声明其需调用的地理位置相关接口,配置规则和普通小程序的配置规则相同。 三、地理位置接口使用流程 自 2022 年 7 月 14 日起,开发者如需在最新版本发布后使用地理位置相关接口,除需完成接口权限开通外,还需在 app.json(或ext.json)配置环节,具体如下: 1、接口权限开通 以下 8 个接口需完成准入开通流程:wx.getFuzzylocation、wx.getLocation、wx.onLocationChange、wx.chooseAddress、wx.choosePoi、wx.chooseLocation、wx.startLocationUpdate、wx.startLocationUpdateBackground 1)普通开发者:需要在 “小程序管理后台 -「开发」-「开发管理」-「接口设置」” 中完成权限申请; 2)第三方开发者:可通过 apply_privacy_interface 接口完成权限申请。 2、app.json(或 ext.json)配置 1)普通开发者:需在 app.json 中声明其需调用的地理位置相关接口,具体配置流程见「二、app.json 的配置指引」; 2)第三方开发者:需要在上传代码时通过参数在 ext.json 中声明其需调用的地理位置相关接口(配置方式:可通过 commit 接口配置)。 同时,为了提升开发者体验,平台在代码提审环节会协助开发者对地理位置接口进行检测,如检测出代码中包含未完成准入开通的地理位置接口,平台将再次提醒开发者确认是否需使用相关接口。 1)普通开发者:若无需使用,开发者可在提审时确认不使用该接口,即可正常进行代码提审。小程序审核通过且新版本发布完成后,平台将对小程序确认不使用的接口关闭使用权限; 2)第三方开发者:若无需使用,可在提审时通过参数声明不使用该接口(声明方式:可通过 submit_audit 接口配置),即可正常进行代码提审,审核通过后发布上线,将对其声明不使用的接口关闭使用权限。 以上调整将仅对所有小程序生效。 微信团队 2022年6月1日
2023-09-26 - 手机号授权,有时需要验证码
正常逻辑,一段时间内未验证的手机号,就需要重新验证。
2020-10-16 - 公众平台服务号、订阅号、企业微信、小程序的相关说明
1、公众号类型功能介绍 [图片] 2、服务号、订阅号功能区别 [图片] 3、小程序介绍 主要优势 1、用户可便捷地获取服务,无需安装或下载即可使用; 2、具有更丰富的功能和出色的使用体验; 3、封装一系列接口能力,帮助快速开发和迭代。 [图片] 4、企业微信介绍 [图片]
11-13 - 修改微信用户信息后,getUserInfo信息什么时候更新?
更新时间:2小时
2020-09-04 - 【干货】如何做好小程序数据埋点
背景:在接触过上百家头部客户中,诊断和参与了数百次的数据体系搭建工作。几乎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 - 小程序登录、用户信息相关接口调整说明
公告更新时间:2021年04月15日考虑到近期开发者对小程序登录、用户信息相关接口调整的相关反馈,为优化开发者调整接口的体验,回收wx.getUserInfo接口可获取用户授权的个人信息能力的截止时间由2021年4月13日调整至2021年4月28日24时。为优化用户的使用体验,平台将进行以下调整: 2021年2月23日起,若小程序已在微信开放平台进行绑定,则通过wx.login接口获取的登录凭证可直接换取unionID2021年4月28日24时后发布的小程序新版本,无法通过wx.getUserInfo与<button open-type="getUserInfo"/>获取用户个人信息(头像、昵称、性别与地区),将直接获取匿名数据(包括userInfo与encryptedData中的用户个人信息),获取加密后的openID与unionID数据的能力不做调整。此前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。新增getUserProfile接口(基础库2.10.4版本开始支持),可获取用户头像、昵称、性别及地区信息,开发者每次通过该接口获取用户个人信息均需用户确认。具体接口文档:《getUserProfile接口文档》由于getUserProfile接口从2.10.4版本基础库开始支持(覆盖微信7.0.9以上版本),考虑到开发者在低版本中有获取用户头像昵称的诉求,对于未支持getUserProfile的情况下,开发者可继续使用getUserInfo能力。开发者可参考getUserProfile接口文档中的示例代码进行适配。请使用了wx.getUserInfo接口或<button open-type="getUserInfo"/>的开发者尽快适配。开发者工具1.05.2103022版本开始支持getUserProfile接口调试,开发者可下载该版本进行改造。 小游戏不受本次调整影响。 一、调整背景很多开发者在打开小程序时就通过组件方式唤起getUserInfo弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。 二、调整说明通过wx.login接口获取的登录凭证可直接换取unionID 若小程序已在微信开放平台进行绑定,原wx.login接口获取的登录凭证若需换取unionID需满足以下条件: 如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用2月23日后,开发者调用wx.login获取的登录凭证可以直接换取unionID,无需满足以上条件。 回收wx.getUserInfo接口可获取用户个人信息能力 4月28日24时后发布的新版本小程序,开发者调用wx.getUserInfo或<button open-type="getUserInfo"/>将不再弹出弹窗,直接返回匿名的用户个人信息,获取加密后的openID、unionID数据的能力不做调整。 具体变化如下表: [图片] 即wx.getUserInfo接口的返回参数不变,但开发者获取的userInfo为匿名信息。 [图片] 此外,针对scope.userInfo将做如下调整: 若开发者调用wx.authorize接口请求scope.userInfo授权,用户侧不会触发授权弹框,直接返回授权成功若开发者调用wx.getSetting接口请求用户的授权状态,会直接读取到scope.userInfo为true新增getUserProfile接口 若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,该接口从基础库2.10.4版本开始支持,该接口只返回用户个人信息,不包含用户身份标识符。该接口中desc属性(声明获取用户个人信息后的用途)后续会展示在弹窗中,请开发者谨慎填写。开发者每次通过该接口获取用户个人信息均需用户确认,请开发者妥善保管用户快速填写的头像昵称,避免重复弹窗。 插件用户信息功能页 插件申请获取用户头像昵称与用户身份标识符仍保留功能页的形式,不作调整。用户在用户信息功能页中授权之后,插件就可以直接调用 wx.login 和 wx.getUserInfo 。 三、最佳实践调整后,开发者如需获取用户身份标识符只需要调用wx.login接口即可。 开发者若需要在界面中展示用户的头像昵称信息,可以通过<open-data>组件进行渲染,该组件无需用户确认,可以在界面中直接展示。 在部分场景(如社交类小程序)中,开发者需要在获取用户的头像昵称信息,可调用wx.getUserProfile接口,开发者每次通过该接口均需用户确认,请开发者妥善处理调用接口的时机,避免过度弹出弹窗骚扰用户。 微信团队 2021年4月15日
2021-04-15 - 自定义菜单使用说明
1、自定义菜单介绍 公众帐号可以在会话界面底部设置自定义菜单,菜单项可按需设定,并可为其设置响应动作。用户可以通过点击菜单项,收到您设定的响应,如收取消息、跳转链接。 [图片] 2、自定义菜单开通方法 进入微信公众平台->功能->自定义菜单->开启即可。 3、微信公众平台自定义菜单设置方法 进入微信公众平台->功能->自定义菜单->添加菜单->点击“+”添加子菜单->设置动作->发布。 1)最多创建3个一级菜单,一级菜单名称名字不多于4个汉字或8个字母。 2)每个一级菜单下的子菜单最多可创建5个,子菜单名称名字不多于8个汉字或16个字母。 3)在子菜单下可设置动作。 发送信息:可发送信息类型包括文字、图片、语音、视频和图文消息等。但未认证订阅号暂时不支持文字类型。 跳转到网页:所有公众帐号均可在自定义菜单中直接选择素材库中的图文消息作为跳转到网页的对象。认证订阅和服务号还可直接输入网址。 温馨提示:编辑中的菜单不会马上被用户看到,点击发布后,会在24小时后在手机端同步显示,粉丝不会收到更新提示,若多次编辑,以最后一次保存为准。
2019-11-21 - 商城系统开发使用微信服务号和小程序那个好?
“我们开发公众号不是为了媒体,我们的本意不是传播内容,我们要提供服务,但服务号没有达到预期,我们在讨论一个新的形态,叫应用号。平时不发东西,他安静的存在在那,低频的需求不需要安装App,微信尝试让更多App以轻量便捷的形态在微信中存在,就是应用号。”这是张小龙在2016年初宣布对“应用号”(小程序前身)的设想。这就很好理解了,小程序的出现是为了弥补服务号设计上的不足!但是公众号也具有很多优势是小程序不具备的。 微信服务号和小程序都可以使用微信授权登录(获取用户微信头像、昵称、)、微信支付、带参数二维码、微信红包等功能。 [图片]小程序 我们来说说服务号:2012年8月23日微信公众平台正式上线,此时还没有订阅号、服务号的区分,在2013年8月5号微信公众号被分为订阅号和服务号。订阅号定位是媒体资讯发布。服务号定位是提供服务,终极目标是代替使用频率不高的APP,很显然这个目标没有实现,所以才有了“小程序”的诞生。 服务号有以下特点:● 微信服务号通过扫服务号二维码点击“立即关注”关注服务号,也可以通过群发文章点击文章顶部蓝字进入服务号关注页面关注! ● 服务号每周可以群发一次文章(文章最多八篇)。 ● 微信服务号和聊天信息排列在一起 ● 服务号可以借助自定义菜单,通过H5实现应用开发 ● 服务号里所有功能都是通过H5开发的,H5是HTML5的简称,也会被叫为TouchWeb或者Web App,通俗点讲就是为触屏手机设计的网页。微信相当于是一个浏览器,用户通过服务号菜单,点击连接进入浏览器打开网页浏览。相对原生小程序,H5的运行速度、整体体验没有小程序好! 服务号优势:● 每次系统升级优化不用重新发布版本,无需审核。用户每次访问都是当前最新版本 ● 每月可群发4次消息,能够有效激活用户 ● 模板消息没有限制,可以在多次使用 ● 服务号页面可以分享至朋友圈 ● 服务号可以调用第三方支付 [图片] 再说说小程序:小程序是在微信基础上开发的一种简单应用程序,它可以调用微信的昵称和头像等账号信息,以及微信的一些基本功能,摄像头、录音、地图、扫一扫、支付等功能。 小程序有以下特点: ● 小程序不用关注,扫码就能使用。进入小程序界面不像服务号有聊天窗口,而是类似APP一样直接进入应用。 ● 使用过的小程序可以通过下拉微信首页找到,也可以在“发现”小程序列表中找到。常用小程序可以放在“我的小程序”中,位置固定使用起来更方便。 ● 小程序没有群发消息功能,页面不能分享到朋友圈。但是可以像服务号一样分享到微信群或者微信好友。 ● 小程序开发需要借助“微信开发者工具”,每次更新需要通过“微信开发者工具”上传代码。可以访问和存储本地文件,但有大小限制(上传的程序包体积不能超过2M,在用户手机中缓存大小不能超过10M,永久存储不能超过10M)。 ● 小程序模板消息推送有限制,用户在该小程序中有支付或者提交表单的动作,那么开发者可以在7天之内向他推送有限条模板消息(每发起1提交表单的动作,可以下发1条消息;每发起1次支付,可以发3条消息,多次提交下发条数独立,相互不影响); 小程序的优势: ● 小程序打开之后直接进入应用界面,跟App无异,流程更直接,开发的自由度也会更大。而小白用户也更容易理解。 ● 不需要连接服务器的小程序,还可以在没有网络的情况下离线使用,并能记住上一次使用最后停留的页面。这个特性很适合各种工具应用,这是所有H5都没法做到的。 ● 所以从营销角度考虑,服务号具有一定优势,服务号能够有效锁粉,并且可以通过消息群发进行刺激二次消费。但是从用户体验角度考虑,小程序更具优势,接近APP的用户体验,页面流畅度和友好度都优于服务号。 ● 很多用户在选择使用服务号和小程序的时候,就不知道该怎么选择,其实也有很好的解决办法;可以在公众号中关联小程序。公众号中的图文消息、自定义菜单、模板消息等功能中可以关联小程序。 关联规则:1、所有公众号都可以关联小程序。 2、一个公众号可关联10个同主体的小程序,3个不同主体的小程序。 3、一个小程序可关联500个公众号。 4、公众号一个月可新增关联小程序13次,小程序一个月可新增关联500次。 这样,可以通过小程序开发应用,再通过公众号关联;使用公众号进行营销推广,在线购物等应用可以通过小程序进行。 [图片] 上期轻编程给大家分享的开源版CRMEB推出的打通版商城,可以实现微信服务号、小程序商城数据同步;也就是一套商城系统,用户可以同时在微信服务号或者小程序中下单,产品、用户数据同步。这样再也不用纠结是使用微信服务号还是小程序了。(需要详细了解这套程序的可以私信轻编程,一起交流学习...)
2020-08-21 - (4)获取用户信息
背景 我们发现大部分小程序都会使用 [代码]wx.getUserInfo[代码] 接口,来获取用户信息。原本设计这个接口时,我们希望开发者在真正需要用户信息的情况下才去调取这个接口,但很多开发者会直接调用这个接口,导致用户在使用小程序的时候产生困扰,归结起来有几点: 开发者在小程序首页直接调用 [代码]wx.getUserInfo[代码] 进行授权,弹框获取用户信息,会使得一部分用户点击“拒绝”按钮。 在开发者没有处理用户拒绝弹框的情况下,用户必须授权头像昵称等信息才能继续使用小程序,会导致某些用户放弃使用该小程序。 用户没有很好的方式重新授权,尽管我们增加了[代码]设置[代码]页面,可以让用户选择重新授权,但很多用户并不知道可以这么操作。 此外,我们发现开发者默认将 [代码]wx.login[代码] 和 [代码]wx.getUserInfo[代码] 绑定使用,这个是由于我们一开始的设计缺陷和实例代码导致的([代码]wx.getUserInfo[代码] 必须通过 [代码]wx.login[代码] 在后台生成 [代码]session_key[代码]后才能调用)。同时,我们收到开发者的反馈,希望用户进入小程序首页便能获取到用户的 [代码]unionId[代码],以便识别到用户是否以前关注了同主体公众号或使用过同主体的App 。 为了解决以上问题,针对获取用户信息我们更新了三个能力: 1.使用组件来获取用户信息 2.若用户满足一定条件,则可以用[代码]wx.login[代码] 获取到的[代码]code[代码]直接换到[代码]unionId[代码] 3.[代码]wx.getUserInfo[代码] 不需要依赖 [代码]wx.login[代码] 就能调用得到数据 获取用户信息组件介绍 [代码][代码] 组件变化: [代码]open-type [代码]属性增加 [代码]getUserInfo[代码] :用户点击时候会触发 [代码]bindgetuserinfo[代码] 事件。 新增事件 [代码]bindgetuserinfo[代码] :当 [代码]open-type[代码]为 [代码]getUserInfo[代码] 时,用户点击会触发。可以从事件返回参数的 [代码]detail[代码] 字段中获取到和 [代码]wx.getUserInfo[代码] 返回参数相同的数据。 示例: [代码]<button open-type="getUserInfo" bindgetuserinfo="userInfoHandler"> Click me button>[代码]和 [代码]wx.getUserInfo[代码] 不同之处在于: 1.API [代码]wx.getUserInfo[代码] 只会弹一次框,用户拒绝授权之后,再次调用将不会弹框; 2.组件 [代码][代码][代码][代码] 由于是用户主动触发,不受弹框次数限制,只要用户没有授权,都会再次弹框。 通过获取用户信息的组件,就可以解决用户再次授权的问题。 直接获取unionId开发者申请 [代码]userinfo[代码] 授权主要为了获取 [代码]unionid[代码],我们鼓励开发者在不骚扰用户的情况下合理获得[代码]unionid[代码],而仅在必要时才向用户弹窗申请使用昵称头像。为此,凡使用“获取用户信息组件”获取用户昵称头像的小程序,在满足以下全部条件时,将可以静默获得 [代码]unionid[代码]: 1.在微信开放平台下存在同主体的App、公众号、小程序。 2.用户关注了某个相同主体公众号,或曾经在某个相同主体App、公众号上进行过微信登录授权。 这样可让其他同主体的App、公众号、小程序的开发者快速获得已有用户的数据。 不依赖登录的用户信息获取某些工具类的轻量小程序不需要登录行为,但是也想获取用户信息,那么就可以在 [代码]wx.getUserInfo[代码] 的时候加一个参数 [代码]withCredentials: false[代码] 直接获取到用户信息,可以少一次网络请求。 这样可以在不给用户弹窗授权的情况下直接展示用户的信息。 最佳实践 1.调用 [代码]wx.login[代码] 获取 [代码]code[代码],然后从微信后端换取到 [代码]session_key[代码],用于解密 [代码]getUserInfo[代码]返回的敏感数据。 2.使用 [代码]wx.getSetting[代码] 获取用户的授权情况 1) 如果用户已经授权,直接调用 API [代码]wx.getUserInfo[代码] 获取用户最新的信息; 2) 用户未授权,在界面中显示一个按钮提示用户登入,当用户点击并授权后就获取到用户的最新信息。 3.获取到用户数据后可以进行展示或者发送给自己的后端。 One More Thing 除了获取用户方案介绍之外,再聊一聊很多初次接触微信小程序的开发者所不容易理解的一些概念: 1.关于OpenId和UnionId [代码]OpenId[代码] 是一个用户对于一个小程序/公众号的标识,开发者可以通过这个标识识别出用户。 [代码]UnionId[代码] 是一个用户对于同主体微信小程序/公众号/APP的标识,开发者需要在微信开放平台下绑定相同账号的主体。开发者可通过[代码]UnionId[代码],实现多个小程序、公众号、甚至APP 之间的数据互通了。 同一个用户的这两个 ID 对于同一个小程序来说是永久不变的,就算用户删了小程序,下次用户进入小程序,开发者依旧可以通过后台的记录标识出来。 2.关于 getUserInfo 和 login 很多开发者会把 [代码]login[代码] 和 [代码]getUserInfo[代码] 捆绑调用当成登录使用,其实 [代码]login[代码] 已经可以完成登录,[代码]getUserInfo[代码] 只是获取额外的用户信息。 在 [代码]login[代码] 获取到 [代码]code[代码] 后,会发送到开发者后端,开发者后端通过接口去微信后端换取到 [代码]openid[代码] 和[代码]sessionKey[代码](现在会将 [代码]unionid[代码] 也一并返回)后,把自定义登录态 [代码]3rd_session[代码]返回给前端,就已经完成登录行为了。而 [代码]login[代码] 行为是静默,不必授权的,用户不会察觉。 [代码]getUserInfo[代码] 只是为了提供更优质的服务而存在,比如展示头像昵称,判断性别,开发者可通过 [代码]unionId[代码] 和其他公众号上已有的用户画像结合来提供历史数据。因此开发者不必在用户刚刚进入小程序的时候就强制要求授权。 可以在官方的文档中看到 [代码]login[代码] 的最佳实践: [图片] Q & A Q1: 为什么 login 的时候不直接返回 openid,而是要用这么复杂的方式来经过后台好几层处理之后才能拿到? A: 为了防止坏人在网络链路上做手脚,所以小程序端请求开发者服务器的的请求都需要二次验证才是可信的。因为我们采取了小程序端只给 [代码]code[代码] ,由服务器端拿着 [代码]code[代码] 和 [代码]AppSecrect[代码] 去微信服务器请求的方式,才会给到开发者对应的[代码]openId[代码] 和用于加解密的 [代码]session_key。[代码] Q2: 既然用户的[代码]openId[代码] 是永远不变的,那么开发者可以使用[代码]openId[代码] 作为用户的登录态么? A: 不行,这是非常危险的行为。因为 [代码]openId[代码] 是不变的,如果有坏人拿着别人的 [代码]openId[代码] 来进行请求,那么就会出现冒充的情况。所以我们建议开发者可以自己在后台生成一个拥有有效期的 [代码]第三方session[代码] 来做登录态,用户每隔一段时间都需要进行更新以保障数据的安全性。 Q3: 是不是用户每次打开小程序都需要重新[代码]login[代码]? A: 不必,可以将登录态存入[代码]storage[代码]中,用户再次登录就可以拿[代码]storage[代码] 里的登录态做正常的业务请求,只有当登录态过期了之后才需要重新[代码]login[代码] 。这样子做一则可以减少用户等待时间,二则可以减少网络带宽。 目前微信的[代码]session_key[代码] 有效期是三天,所以建议开发者设置的登录态有效期要小于这个值。
2018-08-17 - 粉丝接收订阅号消息列表改版情况说明
一、订阅号消息列表改版情况 为了优化粉丝的阅读体验与效率,鼓励订阅号内容的优化和创作,平台已对订阅号的消息列表进行了改版。改版后,订阅号列表里依然是粉丝订阅关注的订阅号群发消息。 改版后的变化: 1.减少了操作步骤:粉丝不需要进入订阅号后再点击文章阅读; 2.提高阅读的体验和效率:群发消息以“标题+头图”的形式直接展示在消息列表里,消息按照发布的时间顺序排列。 3. 原置顶帐号改为“星标”图案展示:粉丝此前置顶的订阅号名称前会出现“星标”图案,提醒粉丝阅读,消息依然按照时间排序,不再置于最顶。 二、如何还原原有阅读模式: 若粉丝不希望通过新的形式进行阅读,可在订阅号消息列表右上角点击进入订阅号会话列表,按照原来的习惯进行阅读。
2019-11-21 - 如何在文章中添加超链接?
请登录公众平台->素材管理->图文消息->正文->选中需要加链接的文字或者是图片(也可以不选择文本直接插入链接或者是历史图文消息)->点击超级链接图标->选择一篇图文消息或者输入需要跳转的链接即可。 [图片] [图片] [图片][图片] 温馨提示:所有的公众号都可以设置跳转群发图文,包括本公众号所有群发的文章和其他公众号所有群发的文章,但未开通微信支付功能无法设置跳转其他的链接。
2020-06-05 - 注销指引流程
1、公众号注销流程图 [图片] 2、个人类型帐号注销方法 第一步: 注销入口:公众号设置->原始ID->注销帐号,点击后即可进入; 第二步:发起注销申请 1)同意协议; 2)注册者本人扫码确认; 3)进入冻结期(冻结时长依据粉丝数而定。粉丝数≤1K,冻结7天;粉丝数大于1k≤1w,冻结15天;粉丝数大于1W≤10w,冻结30天;粉丝数10w以上,冻结60天); 4)管理员确认注销(7天); 5)注销成功。 个人类型注销方法,具体请点击这里了解。 3、组织类型帐号注销方法 第一步: 注销入口:公众号设置->原始ID->注销帐号,点击后即可进入; 第二步:发起注销申请 1)同意协议; 2)主体验证(填写组织对公账户信息); 3)小额打款验证(10天内需打款验证); 4)进入冻结期(冻结时长依据粉丝数而定。粉丝数≤1K,冻结7天;粉丝数大于1k≤1w,冻结15天;粉丝数大于1W≤10w,冻结30天;粉丝数10w以上,冻结60天); 5)管理员确认注销(7天); 6)注销成功。 组织类型注销方法,具体请点击这里了解。 4、组织类型如何修改打款信息? 在发起注销申请后页面会自动拉取主体信息,对公账户信息需自行填写。若您不小心填写错误,建议等待验证失败或者超时未验证后再次重新填写。 5、组织类型支付验证打款后,多久验证成功? 若打款账户信息和金额正确,1个工作日内会验证成功;打款的金额一般为3到10个工作日内原路退款,请留意。
2019-11-21 - (6)微信登录能力优化
小程序和小游戏内的用户登录,我们推荐使用以下两种方式获取用户信息: ▷ 按钮组件的登录方式,用户主动点击按钮可以拉起用户授权弹框,获取用户头像、昵称等信息; ▷ 在不获取用户信息的情况下,可展示用户头像昵称。 用户在没有任何操作的情况直接弹出授权的登录方式将逐渐不再支持,受影响的有 wx.getUserInfo 接口,以及 wx.authorize 接口传入 scope="scope.userInfo" 的情况。 1 为什么平台要做接口调整? 我们提供了 wx.login 和 wx.getUserInfo 接口,用于获取用户的 openID 和基本信息。 推出这两个接口的初衷是希望:当用户使用小程序时,只有访问到真正需要登录的页面,才需要授权并登录。 但在实际应用中,我们发现很多开发者在打开小程序时直接弹出授权框,如果用户点击拒绝授权,无法使用小程序。 在没有任何提示和背景的情况下,直接弹框想要获得用户信息授权,用户脑子里可能会闪过几个哲学问题: 你是谁? 我在哪里? 我为什么要同意? …… 相当一部分用户下意识会点击“拒绝“授权——这样不合理的登录流程既造成了用户的困扰,还使得用户流失。 用户通过小程序可以快速获取服务,因此在访问小程序的第一个页面非常重要。 对于一个互联网产品而言,第一个页面决定了用户对这个产品的认知,用户会选择是否继续使用这个产品。 一个优秀的互联网产品,能够给用户留下一个好的第一印象,用户可以快速了解你的产品,接收到你想要传递的服务信息,从而产生相应的操作行为。 一个优秀的小程序会吸引用户在小程序里进行探索,完成你期望他们去做的事,比如会员注册、商品购买等。 试想一下如果一个品牌的商品官网,一进入要求用户登录才能查看产品信息是什么感觉呢? 因此良好的用户登录体验非常重要。 2 如何设计登录流程? 用户打开小程序时,看第一眼的时候,开发者需要专注以下两个目标: ▷ 精准快速地传达产品理念,开发者要让用户能够快速了解自己的产品和服务; ▷ 将用户流量进行转化,让用户能方便操作或者交易。 一般而言,用户打开小程序后看到的第一个页面,先不要直接弹出授权框,第一个页面可以包含以下内容: ▷ 展示你的小程序功能(如产品、服务、活动等) ,让用户清晰地知道小程序是做什么用的,这些内容可以是你的精选内容; ▷ 激发用户的探索欲,通过描述或者图片吸引用户注意力; ▷ 按照自己的产品目标,给用户提供清晰明确的下一步操作(查看详情、购买等)。 如果某些特殊小程序在使用前一定需要用户登录,或者已经进行到需要用户登录的操作时,可以将 button 组件(其中 open-type 属性指定为 getUserInfo)放置到页面中,页面上可以大致说明以下要点: 为什么需要我授权? 需要我什么信息? 授权后我得到什么好处呢? 接下来在页面上放置一个明显的登录按钮, 建议这个页面上不要有额外的点击区域,以免分散用户注意力,让用户专注于登录这件事情。 3 简单的开发建议 1 当用户打开小程序时访问第一个页面时,先通过 wx.login,获取用户 openID 。这时无需弹框授权,开发者拿到 openID 可以建立自身的帐号 ID。 2 在第一步中,拿到 openID 后,判断是新用户还是老用户。如果是老用户,可以直接登录;如果是新用户,可先在小程序首页展示你的信息服务,让用户对这个小程序有大概的了解,再引导用户进行下一步的操作。 3 当需要获取用户头像昵称的时候,对用户展示一个登录页面,这个页面只有一个最重要的操作,引导用户进行登录。 小程序中,在页面中加入一个 button 按钮,并将 open-type 属性设置为 getUserInfo 。 以小程序为例: 微信登录 对于功能较简单的小程序或者小游戏而言,如果不是必须要获得用户的头像昵称,建议可先通过wx.login 拿到 openID 后,使用 open-data 方式或者开放数据域的方式展示用户信息,整个过程都无需用户授权。 Tips: 1、在用户登录后,开发者需要存储用户的 unionID,而且建议只把 unionID 作为互通的用户标识,不要直接使用 unionID 作为用户 ID。因为一旦小程序迁移到其他的开放平台下,unionID 是会改变的,而 openID 是不变的。 2、用 button 组件的方式获得用户授权后,调用 wx.getUserInfo 就可以直接获取用户信息。这个的意义在于获取过一次之后,用户有可能改昵称头像,因此为了及时同步,最好是定期获取用户信息。 这里两个小提示: ▷ 定期使用 wx.getUserInfo 获取并更新用户的信息; ▷ 如果用户授权过一次之后,又在设置中关掉了授权(或者本地删除了小程序),那这时再调用 wx.getUserInfo 也是不会成功的,需要重新获得授权。 相关开发文档参考: ▷ 小程序 1、小程序 wx.login 2、button 组件,并将 open-type 指定为 getUserInfo 类型,获取用户基本信息 3、open-data 展示用户基本信息 ▷ 小游戏 1、小游戏 wx.login 2、用户信息按钮 UserInfoButton 3、开放数据域下的展示用户信息
2018-08-17 - 微信登录能力介绍
为了便于用户便捷使用App、网站、移动端网页、小程序的服务,微信提供不同的技术方案,便于开发者在不同终端平台的服务中接入微信登录。 通过这个教程,开发者可以了解平台提供的针对各终端平台的微信登录能力,并可以根据实际使用场景合理选择接入方式。 以下为几类型微信登录的功能说明: 类型授权域/接口 用户侧使用流程 接入流程 App 接入微信SDK,并调用snsapi_userinfo (1)在App内选择使用微信登录 (2)拉起微信客户端,打开用户授权页,完成登录授权(1)注册微信开放平台(open.weixin.qq.com)帐号,并完成开发者资质认证 (2)申请【App移动应用】并审核通过后可以使用,查看开发文档网站应用 snsapi_login (1)用户使用微信“扫一扫”,在PC端扫码 (2)客户端打开授权页,完成登录授权 (1)注册微信开放平台(open.weixin.qq.com)帐号,并完成开发者资质认证 (2)申请【网站应用】并审核通过后可以使用,查看开发文档 微信客户端内H5使用公众号的登录能力: snsapi_base snsapi_userinfosnsapi_base:静默授权 snsapi_userinfo: (1)用户在H5内点击登录,唤起授权弹窗 (2)用户侧完成登录授权(1)注册微信公众号,选择“服务号”类型,并完成微信认证 (2)在公众号管理后台设置回调域名 (3)接入微信登录能力,查看开发文档 小程序wx.login wx.getUserInfowx.login:静默授权,开发者可获取openid wx.getUserInfo: (1)用户在小程序内点击组件,唤起登录窗口(2)用户侧完成登录授权 (1)注册小程序 (2)接入微信登录功能,查看开发文档,查看登录流程设计指引 开发者在不同使用场景下接入微信登录,应该注册符合要求的帐号并使用对应的登录能力。 【常见问题】 Q1: 在调用微信登录时,出现了“此帐号并没有这些scope的权限,错误码:10005”,是什么原因? A:对于场景与帐号属性、能力项不对应时(如在移动端网页中使用网站应用的AppID调用登录能力),将会出现以下的错误提示:此帐号并没有这些scope的权限,错误码:10005 [图片] Q2:我的服务同时有App、官网、公众号、小程序,那我怎么打通用户数据? A:对于多平台的服务,若开发者希望能识别用户身份,例如:希望用户在小程序内也能查看到在App内购买的商品订单,则可以通过平台提供的UnionID机制来实现用户身份识别。
2019-03-06 - (14)腾讯视频插件
想要在自己的电商小程序中增加商品介绍视频,但自己搭建视频服务开发成本太大? 想要在餐饮服务中添加菜品视频,但苦于带宽成本过高? 想要在门票预订服务中用视频打动用户,但视频资质申请流程太长? 腾讯视频插件来帮你~ 今天,我们跟大家分享腾讯视频插件的故事。 腾讯视频插件提供了完整视频播放能力,方便开发者可以给用户提供更好的视频体验。 插件基于腾讯视频的CDN节点和成熟视频方案,可以做到为不同地点的用户提供更流畅的播放服务,更清晰的视频,更稳定的播放质量。只需要视频的vid,开发者即可直接在小程序内播放腾讯视频上的内容! 腾讯视频插件使用场景 场景一:电商类小程序 除了对商品的语言描述,实惠的价格,精美的图片,也需要生动直观、360度动态介绍商品,比单一的图片更能引起消费者的好感。 现在,在商品介绍里直接链入相关商品视频。比如,买衣服的电商小程序,可以把电视或电影中明星穿过的同款视频链入,在满足用户感官享受的同时,提升转化率。 场景二:文娱推荐类小程序 不局限于苍白的文字描述,推荐类小程序也可在小程序中添加电影视频、预告片,为用户带来完美的购票、观影体验。开发者无需独立开发视频功能,直接使用“腾讯视频”插件,即可实现视频播放功能。 [图片] 场景三:资讯类小程序 “腾讯视频”插件还可以是内容创作者的一大利器,以游戏攻略类小程序为例。小程序就可以直接用视频的形式展现,一方面清晰明了地展示攻略流程,同时也增加了用户在小程序内的停留时长,进一步引导用户做更多动作。 或者生活居家小技巧类的小程序,比如想教大家一种打结的方式,纯文字的形式即很难说清楚,又浪费内容运营者的时间,这时候直接使用插件链入视频,就能很快地教会大家,给予用户更好的内容体验。 轻松接入腾讯视频插件 “腾讯视频”提供了这么多的功能,使用起来也同样很方便: 1 在“小程序管理后台→设置→第三方服务→插件管理”中查找插件名称“腾讯视频”,并申请使用。 [图片] 2 参考详情页中的 [开发文档],在小程序代码中使用插件。 了解完“腾讯视频”插件后,还想看看其他小程序是如何实际运用的?一起来体验一下 腾讯+ ! [图片] 页面视频服务是由腾讯视频插件提供
2018-08-17 - 论函数复用的几大姿势
开发过小程序的朋友们应该都遇到这样的情况,可能很多个页面有相同的函数,例如[代码]onShareAppMessage[代码],有什么最佳实践吗,应该如何处理呢? 本次开发技巧,我从以下几种解决办法剖析: 将它复制粘贴到每个地方(最烂的做法) 抽象成一个公共函数,每个[代码]Page[代码]都手动引用 提取一个behavior,每个页面手动注入 通过[代码]Page[代码]封装一个新的[代码]newPage[代码],以后每个页面都通过[代码]newPage[代码]注册 劫持Page函数,注入预设方法,页面仍可使用[代码]Page[代码]注册 复制粘贴大法 这是最直观,也是初学者最常用到的办法。也是作为工程师最不应该采取的办法。这有一个致命的问题,如果某一天,需要改动这个函数,岂不是要将所有的地方都翻出来改,所以这个办法直接否决。 抽象公共函数 这种方式,解决了复制粘贴大法的致命问题,不需要改动很多地方,只需要改动这个抽象出来的函数即可。但是其实,这个方式不便捷,每次新增页面都需要手动引入这个函数。 以下都通过[代码]onShareAppMessage[代码]方法举例。 假设在[代码]app.js[代码]通过[代码]global[代码]注册了[代码]onShareAppMessage[代码]方法: [代码]// app.js global.onShareAppMessage = function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } } [代码] 那么此时每次新增的Page都需要这样引入: [代码]// page.js Page({ ...global.onShareAppMessage, data: {} }) [代码] 这样的缺点也是非常明显的: 创建新页面时,容易遗忘 如果多个相同的函数,则需要每个独立引入,不方便 提取Behavior 将多个函数集成到一个对象中,每个页面只需要引入这个对象即可注入多个相同的函数。这种方式可以解决 抽象公共函数 提到的 缺点2。 大致的实现方式如下: 同样在[代码]app.js[代码]通过[代码]global[代码]注册一个[代码]behavior[代码]对象: [代码]// app.js global.commonPage = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onHide: function() { // do something } } [代码] 在新增的页面注入: [代码]// page.js Page({ data: {}, ...global.commonPage, }}) [代码] 缺点仍然是,新增页面时容易遗忘 封装新Page 封装新的[代码]Page[代码],然后每个页面都通过这个新的[代码]Page[代码]注册,而不是采用原有的[代码]Page[代码]。 同理,在[代码]app.js[代码]先封装一个新的[代码]Page[代码]到全局变量[代码]global[代码]: [代码]// app.js global.newPage = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return Page({...defaultSet, ...obj}) } [代码] 往后在每个页面都使用新的[代码]newPage[代码]注册: [代码]// page.js global.newPage({ data: {} }) [代码] 好处即是全新封装了[代码]Page[代码],后续只需关注是否使用了新的[代码]Page[代码]即可;此外大家也很清晰知道这个是采用了新的封装,避免了覆盖原有的[代码]Page[代码]方法。 我倒是觉得没什么明显缺点,要是非要鸡蛋里挑骨头的话,就是要显式调用新的函数注册页面。 劫持Page 劫持函数其实是挺危险的做法,因为开发人员可能会在定位问题时,忽略了这个被劫持的地方。 劫持[代码]Page[代码]的做法,简单的说就是,覆盖[代码]Page[代码]这个函数,重新实现[代码]Page[代码],但这个新的[代码]Page[代码]内部仍会调用原有的[代码]Page[代码]。说起来可能有点拗口,通过代码看就一目了然: [代码]// app.js let originalPage = Page Page = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return originalPage({ ...defaultSet, ...obj}) } [代码] 通过这种方式,不改变页面的注册方式,但可能会让不了解底层封装的开发者感到困惑:明明没注册的方法,怎么就自动注入了呢? 这种方式的缺点已经说了,优点也很明显,不改变任何原有的页面注册方式。 其实这个是一个挺好的思路,在一些特定的场景下,会有事半功倍的效果。
2020-03-23 - 使用第三方腾讯视频插件播放视频,是否需要添加 文娱-视频 类目?
若小程序已接入文娱-视频插件,暂不需补充文娱-视频类目。审核将以实际提交的小程序服务内容为准。
2019-12-27 - 如何实现一个简单的swiper效果
简单的siwper效果,又是逛社区发现老哥提的想要该效果,那么有需要,咱们就得上啊: 效果图: [图片] 上代码: wxml [代码]<view class="container"> <swiper duration="200" previous-margin="140rpx" next-margin="140rpx" bindchange="currentHandle" circular="{{true}}" class="swiper-out"> <block wx:for="{{punchList}}" wx:key="*this"> <swiper-item class="swp-item {{current === index ?'active-item': ''}}"> <view class="slide-image" style=" background: url({{item.bannerUrl}}) no-repeat center center;background-size: 100% 100%;" id="{{index}}"></view> </swiper-item> </block> </swiper> <view class="swp-dot"> <view class="square-12 m-r-8 {{current === index ?'active': ''}}" wx:for="{{punchList}}" wx:key="{{index}}"></view> </view> </view> [代码] JS [代码]const app = getApp() Page({ data: { punchList: [{ "bannerUrl": "https://qiniu-image.qtshe.com/1536067857379_122.png" }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068379879_115.png", }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068319939_230.png", }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068074140_695.png", }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068213758_796.png", }], current: 0 }, currentHandle(e) { let { current } = e.detail this.setData({ current }) } }) [代码] wxss [代码].container { display: flex; flex-direction: column; align-items: center; justify-content: space-between; height: 100vh; } .slide-image { height: 600rpx; width: 400rpx; margin-top: 20rpx; overflow: hidden; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 10rpx; } .swiper-out { width: 750rpx; height: 660rpx; margin-top: 60rpx; } .active-item .slide-image { box-shadow: 0 5rpx 20rpx 3rpx rgba(0, 0, 0, 0.15); } .swp-item { width: 400rpx; display: flex; flex-direction: column; align-items: center; padding-top: 4rpx; opacity: 0.6; } .active-item { opacity: 1; } .swp-dot { display: flex; justify-content: center; flex: 1; margin-top: 18rpx; } .m-r-8 { margin-right: 8rpx; } .m-l-8 { margin-right: 8rpx; } .square-12 { width: 12rpx; height: 12rpx; background-color: #d8d8d8; border-radius: 6rpx; transition: width 0.2s linear; } .active { background-color: #3c3c3c; width: 36rpx; transition: width 0.2s linear; } [代码] 代码放完了,看下效果吧。代码片段如下: https://developers.weixin.qq.com/s/DCK6HJmw7kaZ
2019-12-19 - 小程序如何做到永久推送?
我在之前也写过类似文章,可以参考下 https://developers.weixin.qq.com/community/develop/article/doc/000aee3b708b585e5489af0025b013 问题场景 大家都知道小程序模板消息已于2020年1月10号正式下线了 那么如何保证用户的永久触达呢,对我而言,平时接收系统的报警信息,以及日常用户的反馈信息,这些肯定希望是永久推送,而小程序目前的订阅消息是不会对绝大部分类目开放永久推送的。 相关资料 1、一次性订阅消息:用户订阅一次后,开发者可下发一条消息,不限时间。若用户勾选了“总是保持以上选择,不再询问”且点击了允许,那么以后都默认同意订阅这条消息。用户不再做多次选择,开发者也避免了更繁琐的提醒。 2、长期性订阅消息:用户订阅一次后,可长期下发多条消息。目前长期性订阅消息向政务、医疗、交通、金融、教育等线下公共服务开放,后续将综合评估行业需求和用户体验持续完善。(长期订阅消息只针对特定行业开放,所以普通开发者并无法使用)。 技术方案 现在改为:微信公众号-模板消息,相关技术文档如下所示: https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html 这种方案肯定面临下面的问题: 1、需要引导老用户主动关注微信公众号 2、那么直接在小程序打开引导关注公众号页是否合规呢? 经过了解这部分确定是合规的: 在服务内容和类目一致、无其他违规内容的情况下,引导关注公众号不违规。 所以这个方案是 可行的。 案例 腾讯的微信记账本便是这样操作 截图如下 [图片] [图片] 用户触达形式 公众号模板消息推送给用户形式和小程序订阅消息推送给用户是不一样的,一个在公众号内部,一个在服务通知里面,具体可见下图所示 公众号模板消息推送 [图片] 小程序订阅消息 [图片] 总结 这个方案虽然叫小程序永久推送,但是其实是曲线实现的永久推送,切换到公众号上去,但是作为触达用户的一种方式,这已经足够了。
2020-01-13 - 小程序页面通信、数据刷新、事件总线 、event bus 终极解决方案之 iny-bus
#### 背景介绍 在各种小程序中,我们经常会遇到 这种情况 有一个 列表,点击列表中的一项进入详情,详情有个按钮,删除了这一项,这个时候当用户返回到列表页时, 发现列表中的这一项依然存在,这种情况,就是一个 `bug`,也就是数据不同步问题,这个时候测试小姐姐 肯定会找你,让你解决,这个时候,你也许会很快速的解决,但过一会儿,测试小姐姐又来找你说,我打开了 四五个页面更改了用户状态,但我一层一层返回到首页,发现有好几个页面数据没有刷新,也是一个 bug, 这个时候你就犯愁了,怎么解决,常规方法有下面几种 #### 解决方法 1. 将所有请求放到 生命周期 `onShow` 中,只要我们页面重新显示,就会重新请求,数据也会刷新 2. 通过用 `getCurrentPages` 获取页面栈,然后找到对应的 页面实例,调用实例方法,去刷新数据 3. 通过设置一个全局变量,例如 App.globalData.xxx,通过改变这个变量的值,然后在对应 onShow 中检查,如果值已改变,刷新数据 4. 在打开详情页时,使用 redirectTo 而不是 navigateTo,这样在打开新的页面时,会销毁当前页面, 返回时就不会回到这个里面,自然也不会有数据不同步问题 #### 存在的问题 1. 假如我们将 所有 请求放到 onShow 生命周期中,自然能解决所有数据刷新问题,但是 onShow 这个生命周期,有两个问题 第一个问题,它其实是在 onLoad 后面执行的,也就是说,假如请求耗时相同,从它发起请求到页面渲染, 会比 onLoad 慢 第二个问题,那就是页面隐藏、调用微信分享、锁频等等都会触发执行,请求放置于 `onShow` 中就会造成 大量不需要的请求,造成服务器压力,多余的资源浪费、也会造成用户体验不好的问题 2. 通过 `getCurrentPages` 获取页面栈,然后找到对应的 页面实例,调用实例方法,去刷新数据,这也 不失为一个办法,但是就如微信官方文档所说 > 不要尝试修改页面栈,会导致路由以及页面状态错误。 > 不要在 App.onLaunch 的时候调用 `getCurrentPages()`,此时 page 还没有生成。 同时、当需要通信的页面有两个、三个、多个呢,这里去使用 `getCurrentPages` 就会比较困难、繁琐 3. 通过设置全局变量的方法,当需要使用的地方比较少时,可以接受,当使用的地方多的时候,维护起来 就会很困难,代码过于臃肿,也会有很多问题 4. 使用 redirectTo 而不是 navigateTo,从用来体验来说,很糟糕,并且只存在一个页面,对于 tab 页面,它也无能为力,不推荐使用 #### 最佳实践 在 Vue 中, 可以通过 new Vue() 来实现一个 event bus作为事件总线,来达到事件通知的功能,在各大 框架中,也有自身的事件机制实现,那么我们完全可以通过同样的方法,实现一个事件中心,来管理我们的事件, 同时,解决我们的问题。iny-bus 就是这样一个及其轻量的事件库,使用 typescript 编写,100% 测试覆 盖率,能运行 js 的环境,就能使用 传送门 [源码](https://github.com/landluck/iny-bus) [NPM](https://www.npmjs.com/package/iny-bus) [文档](https://landluck.github.io/iny-bus/docs/) #### 简单使用 iny-bus 使用及其简单,在需要的页面 onLoad 中添加事件监听, 在需要触发事件的地方派发事件,使监 听该事件的每个页面执行处理函数,达到通信和刷新数据的目的,在小程序中的使用可以参考以下代码 [代码] // 小程序[代码] [代码] import bus from [代码][代码]'iny-bus'[代码] [代码] // 添加事件监听[代码] [代码] // 在 onLoad 中注册, 避免在 onShow 中使用[代码] [代码] onLoad () {[代码] [代码] this[代码][代码].eventId = bus.on([代码][代码]'事件名'[代码][代码], (a, b, c, d) => {[代码] [代码] // 支持多参数[代码] [代码] console.log(a, b, c, d)[代码] [代码] this[代码][代码].setData({ a [代码]}) [代码] // 调用页面请求函数,刷新数据[代码] [代码] this[代码][代码].refreshPageData()[代码] [代码] })[代码] [代码] // 添加只需要执行一次的 事件监听[代码] [代码] this[代码][代码].eventIdOnce = bus.once([代码][代码]'事件名'[代码][代码], () => {[代码] [代码] // do some thing[代码] [代码] })[代码] [代码] }[代码] [代码] // 移除事件监听,该函数有两个参数,第二个事件id不传,会移除整个事件监听,传入ID,会移除该[代码] [代码] // 页面的事件监听,避免多余资源浪费, 在添加事件监[代码][代码]/// 听后,页面卸载(onUnload)时建议移除[代码] [代码] onUnload () {[代码] [代码] bus.remove([代码][代码]'事件名'[代码][代码], [代码][代码]this[代码][代码].eventId)[代码] [代码] }[代码] [代码] // 派发事件,触发事件监听处更新视图[代码] [代码] // 支持多参传递[代码] [代码] onClick () {[代码] [代码] bus.emit([代码][代码]'事件名'[代码][代码], a, b, c)[代码] [代码] }[代码] 更详细的使用和例子可以参考 [Github iny-bus 小程序代码](https://github.com/landluck/iny-bus/tree/master/examples) #### iny-bus 具体实现 * 基本打包工具,这里使用非常优秀的开源库 [typescript-library-starter](https://github.com/alexjoverm/typescript-library-starter),具体细节不展开 * 测试工具 使用 facebook 的 [jest](https://github.com/facebook/jest) * build ci 使用 [travis-ci](https://www.travis-ci.org/) * 测试覆盖率上传使用 [codecov](https://codecov.io/) * 具体的其他细节大家可以看源码中的 [package.json](https://github.com/landluck/iny-bus/blob/master/package.json),这里就一一展开讲了 iny-bus 的核心代码,其实就这么多,总的来说,非常少,但是能解决我们在小程序中遇到的大量 通信 和 数据刷新问题,是采用 各大平台小程序 原生开发时,页面通信的不二之选,同时,100% 的测试覆盖率,确保了 iny-bus 在使用中的稳定性和安全性,当然,每个库都是从简单走向复杂,功能慢慢完善,如果 大家在使用或者源码中发现了bug或者可以优化的点,欢迎大家提 pr 或者直接联系我 最后,如果 iny-bus 给你提供了帮助或者让你有任何收获,请给 作者 点个赞,感谢大家 [点赞](https://github.com/landluck/iny-bus)
2019-08-04 - 自定义菜单设置小程序相关问题
1、自定义菜单怎么设置“跳转小程序”? 跳转小程序:用户可将已发布且公众号已关联的小程序添加至一级菜单和二级菜单内,用户点击后即可跳转至小程序页面。 [图片] 温馨提示:备用网页是必填项,旧版微信客户端无法支持小程序,用户点击菜单时将会打开备用网页(认证的公众号可设置外链)。 2、设置菜单时提示“小程序未关联,菜单设置无效,请检查”? 目前平台规定自定义菜单设置跳转的小程序都需要进行关联。设置自定义菜单时,请确认之前菜单设置的小程序是否全部已关联,若菜单内容存在未关联的小程序,则会出现提示“小程序未关联,菜单设置无效,请检查”。 [图片] 小程序关联方法:【小程序】->【小程序管理】->【添加】-【关联小程序】,公众号管理员扫码确认后,请输入小程序AppID,发送邀请后,需小程序管理员微信号确认成功即可绑定。 [图片] 温馨提示: 1)公众号可关联同一主体的10个小程序,不同主体的10个小程序;1个小程序可关联最多500个公众号。 2)公众号关联小程序不要求已发布,但未发布的小程序不可展示在公众号资料页、图文消息、自定义菜单等场景(设置未发布小程序后路径显示空白)。 [图片]
2020-06-05 - 一个通用request的封装
小程序内置了[代码]wx.request[代码],用于向后端发送请求,我们先来看看它的文档: wx.request(OBJECT) 发起网络请求。使用前请先阅读说明。 OBJECT参数说明: 参数名 类型 必填 默认值 说明 最低版本 url String 是 - 开发者服务器接口地址 - data Object/String/ArrayBuffer 否 - 请求的参数 - header Object 否 - 设置请求的 header,header 中不能设置 Referer。 - method String 否 GET (需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT - dataType String 否 json 如果设为json,会尝试对返回的数据做一次 JSON.parse - responseType String 否 text 设置响应的数据类型。合法值:text、arraybuffer 1.7.0 success Function 否 - 收到开发者服务成功返回的回调函数 - fail Function 否 - 接口调用失败的回调函数 - complete Function 否 - 接口调用结束的回调函数(调用成功、失败都会执行) - success返回参数说明: 参数 类型 说明 最低版本 data Object/String/ArrayBuffer 开发者服务器返回的数据 - statusCode Number 开发者服务器返回的 HTTP 状态码 - header Object 开发者服务器返回的 HTTP Response Header 1.2.0 这里我们主要看两点: 回调函数:success、fail、complete; success的返回参数:data、statusCode、header。 相对于通过入参传回调函数的方式,我更喜欢promise的链式,这是我期望的第一个点;success的返回参数,在实际开发过程中,我只关心data部分,这里可以做一下处理,这是第二点。 promisify 小程序默认支持promise,所以这一点改造还是很简单的: [代码]/** * promise请求 * 参数:参考wx.request * 返回值:[promise]res */ function requestP(options = {}) { const { success, fail, } = options; return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success: res, fail: rej, }, )); }); } [代码] 这样一来我们就可以使用这个函数来代替wx.request,并且愉快地使用promise链式: [代码]requestP({ url: '/api', data: { name: 'Jack' } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 注意,小程序的promise并没有实现finally,Promise.prototype.finally是undefined,所以complete不能用finally代替。 精简返回值 精简返回值也是很简单的事情,第一直觉是,当请求返回并丢给我一大堆数据时,我直接resolve我要的那一部分数据就好了嘛: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { res(r.data); // 这里只取data }, fail: rej, }, )); }); [代码] but!这里需要注意,我们仅仅取data部分,这时候默认所有success都是成功的,其实不然,wx.request是一个基础的api,fail只发生在系统和网络层面的失败情况,比如网络丢包、域名解析失败等等,而类似404、500之类的接口状态,依旧是调用success,并体现在[代码]statusCode[代码]上。 从业务上讲,我只想处理json的内容,并对json当中的相关状态进行处理;如果一个接口返回的不是约定好的json,而是类似404、500之类的接口异常,我统一当成接口/网络错误来处理,就像jquery的ajax那样。 也就是说,如果我不对[代码]statusCode[代码]进行区分,那么包括404、500在内的所有请求结果都会走[代码]requestP().then[代码],而不是[代码]requestP().catch[代码]。这显然不是我们熟悉的使用方式。 于是我从jquery的ajax那里抄来了一段代码。。。 [代码]/** * 判断请求状态是否成功 * 参数:http状态码 * 返回值:[Boolen] */ function isHttpSuccess(status) { return status >= 200 && status < 300 || status === 304; } [代码] [代码]isHttpSuccess[代码]用来决定一个http状态码是否判为成功,于是结合[代码]requestP[代码],我们可以这么来用: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { const isSuccess = isHttpSuccess(r.statusCode); if (isSuccess) { // 成功的请求状态 res(r.data); } else { rej({ msg: `网络错误:${r.statusCode}`, detail: r }); } }, fail: rej, }, )); }); [代码] 这样我们就可以直接resolve返回结果中的data,而对于非成功的http状态码,我们则直接reject一个自定义的error对象,这样就是我们所熟悉的ajax用法了。 登录 我们经常需要识别发起请求的当前用户,在web中这通常是通过请求中携带的cookie实现的,而且对于前端开发者是无感知的;小程序中没有cookie,所以需要主动地去补充相关信息。 首先要做的是:登录。 通过[代码]wx.login[代码]接口我们可以得到一个[代码]code[代码],调用后端登录接口将code传给后端,后端再用code去调用微信的登录接口,换取[代码]sessionKey[代码],最后生成一个[代码]sessionId[代码]返回给前端,这就完成了登录。 [图片] 具体参考微信官方文档:wx.login [代码]const apiUrl = 'https://jack-lo.github.io'; let sessionId = ''; /** * 登录 * 参数:undefined * 返回值:[promise]res */ function login() { return new Promise((res, rej) => { // 微信登录 wx.login({ success(r1) { if (r1.code) { // 获取sessionId requestP({ url: `${apiUrl}/api/login`, data: { code: r1.code, }, method: 'POST' }) .then((r2) => { if (r2.rcode === 0) { const { sessionId } = r2.data; // 保存sessionId sessionId = sessionId; res(r2); } else { rej({ msg: '获取sessionId失败', detail: r2 }); } }) .catch((err) => { rej(err); }); } else { rej({ msg: '获取code失败', detail: r1 }); } }, fail: rej, }); }); } [代码] 好的,我们做好了登录并且顺利获取到了sessionId,接下来是考虑怎么把sessionId通过请求带上去。 sessionId 为了将状态与数据区分开来,我们决定不通过data,而是通过header的方式来携带sessionId,我们对原本的requestP稍稍进行修改,使得它每次调用都自动在header里携带sessionId: [代码]function requestP(options = {}) { const { success, fail, } = options; // 统一注入约定的header let header = Object.assign({ sessionId: sessionId }, options.header); return new Promise((res, rej) => { ... }); } [代码] 好的,现在请求会自动带上sessionId了; 但是,革命尚未完成: 我们什么时候去登录呢?或者说,我们什么时候去获取sessionId? 假如还没登录就发起请求了怎么办呢? 登录过期了怎么办呢? 我设想有这样一个逻辑: 当我发起一个请求的时候,如果这个请求不需要sessionId,则直接发出; 如果这个请求需要携带sessionId,就去检查现在是否有sessionId,有的话直接携带,发起请求; 如果没有,自动去走登录的流程,登录成功,拿到sessionId,再去发送这个请求; 如果有,但是最后请求返回结果是sessionId过期了,那么程序自动走登录的流程,然后再发起一遍。 其实上面的那么多逻辑,中心思想只有一个:都是为了拿到sessionId! 我们需要对请求做一层更高级的封装。 首先我们需要一个函数专门去获取sessionId,它将解决上面提到的2、3点: [代码]/** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { login() .then((r1) => { res(r1.data.sessionId); }) .catch(rej); } } else { res(sessionId); } }); } [代码] 好的,接下来我们解决第1、4点,我们先假定:sessionId过期的时候,接口会返回[代码]code=401[代码]。 整合了getSessionId,得到一个更高级的request方法: [代码]/** * ajax高级封装 * 参数:[Object]option = {},参考wx.request; * [Boolen]keepLogin = false * 返回值:[promise]res */ function request(options = {}, keepLogin = true) { if (keepLogin) { return new Promise((res, rej) => { getSessionId() .then((r1) => { // 获取sessionId成功之后,发起请求 requestP(options) .then((r2) => { if (r2.rcode === 401) { // 登录状态无效,则重新走一遍登录流程 // 销毁本地已失效的sessionId sessionId = ''; getSessionId() .then((r3) => { requestP(options) .then(res) .catch(rej); }); } else { res(r2); } }) .catch(rej); }) .catch(rej); }); } else { // 不需要sessionId,直接发起请求 return requestP(options); } } [代码] 留意req的第二参数keepLogin,是为了适配有些接口不需要sessionId,但因为我的业务里大部分接口都需要登录状态,所以我默认值为true。 这差不多就是我们封装request的最终形态了。 并发处理 这里其实我们还需要考虑一个问题,那就是并发。 试想一下,当我们的小程序刚打开的时候,假设页面会同时发出5个请求,而此时没有sessionId,那么,这5个请求按照上面的逻辑,都会先去调用login去登录,于是乎,我们就会发现,登录接口被同步调用了5次!并且后面的调用将导致前面的登录返回的sessionId过期~ 这bug是很严重的,理论上来说,登录我们只需要调用一次,然后一直到过期为止,我们都不需要再去登录一遍了。 ——那么也就是说,同一时间里的所有接口其实只需要登录一次就可以了。 ——也就是说,当有登录的请求发出的时候,其他那些也需要登录状态的接口,不需要再去走登录的流程,而是等待这次登录回来即可,他们共享一次登录操作就可以了! 解决这个问题,我们需要用到队列。 我们修改一下getSessionId这里的逻辑: [代码]const loginQueue = []; let isLoginning = false; /** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { loginQueue.push({ res, rej }); if (!isLoginning) { isLoginning = true; login() .then((r1) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().res(r1); } }) .catch((err) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().rej(err); } }); } } else { res(sessionId); } }); } [代码] 使用了isLoginning这个变量来充当锁的角色,锁的目的就是当登录正在进行中的时候,告诉程序“我已经在登录了,你先把回调都加队列里去吧”,当登录结束之后,回来将锁解开,把回调全部执行并清空队列。 这样我们就解决了问题,同时提高了性能。 封装 在做完以上工作以后,我们都很清楚的封装结果就是[代码]request[代码],所以我们把request暴露出去就好了: [代码]function request() { ... } module.exports = request; [代码] 这般如此之后,我们使用起来就可以这样子: [代码]const request = require('request.js'); Page({ ready() { // 获取热门列表 request({ url: 'https://jack-lo.github.io/api/hotList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 request({ url: 'https://jack-lo.github.io/api/latestList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); }, }); [代码] 是不是很方便,可以用promise的方式,又不必关心登录的问题。 然而可达鸭眉头一皱,发现事情并不简单,一个接口有可能在多个地方被多次调用,每次我们都去手写这么一串[代码]url[代码]参数,并不那么方便,有时候还不好找,并且容易出错。 如果能有个地方专门记录这些url就好了;如果每次调用接口,都能像调用一个函数那么简单就好了。 基于这个想法,我们还可以再做一层封装,我们可以把所有的后端接口,都封装成一个方法,调用接口就相对应调用这个方法: [代码]const apiUrl = 'https://jack-lo.github.io'; const req = { // 获取热门列表 getHotList(data) { const url = `${apiUrl}/api/hotList` return request({ url, data }); }, // 获取最新列表 getLatestList(data) { const url = `${apiUrl}/api/latestList` return request({ url, data }); } } module.exports = req; // 注意这里暴露的已经不是request,而是req [代码] 那么我们的调用方式就变成了: [代码]const req = require('request.js'); Page({ ready() { // 获取热门列表 req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 req.getLatestList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); } }); [代码] 这样一来就方便了很多,而且有一个很大的好处,那就是当某个接口的地址需要统一修改的时候,我们只需要对[代码]request.js[代码]进行修改,其他调用的地方都不需要动了。 错误信息的提炼 最后的最后,我们再补充一个可轻可重的点,那就是错误信息的提炼。 当我们在封装这么一个[代码]req[代码]对象的时候,我们的promise曾经reject过很多的错误信息,这些错误信息有可能来自: [代码]wx.request[代码]的fail; 不符合[代码]isHttpSuccess[代码]的网络错误; getSessionId失败; … 等等的一切可能。 这就导致了我们在提炼错误信息的时候陷入困境,到底catch到的会是哪种[代码]error[代码]对象? 这么看你可能不觉得有问题,我们来看看下面的例子: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 假如上面的例子中,我想要的不仅仅是[代码]console.log(err)[代码],而是想将对应的错误信息弹窗出来,我应该怎么做? 我们只能将所有可能出现的错误都检查一遍: [代码]req.getHotList({ page: 1 }) .then((res) => { if (res.code !== 0) { // 后端接口报错格式 wx.showModal({ content: res.msg }); } }) .catch((err) => { let msg = '未知错误'; // 文本信息直接使用 if (typeof err === 'string') { msg = err; } // 小程序接口报错 if (err.errMsg) { msg = err.errMsg; } // 自定义接口的报错,比如网络错误 if (err.detail && err.detail.errMsg) { msg = err.detail.errMsg; } // 未知错误 wx.showModal({ content: msg }); }); [代码] 这就有点尴尬了,提炼错误信息的代码量都比业务还多几倍,而且还是每个接口调用都要写一遍~ 为了解决这个问题,我们需要封装一个方法来专门做提炼的工作: [代码]/** * 提炼错误信息 * 参数:err * 返回值:[string]errMsg */ function errPicker(err) { if (typeof err === 'string') { return err; } return err.msg || err.errMsg || (err.detail && err.detail.errMsg) || '未知错误'; } [代码] 那么过程会变成: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { const msg = req.errPicker(err); // 未知错误 wx.showModal({ content: msg }); }); [代码] 好吧,我们再偷懒一下,把wx.showModal也省去了: [代码]/** * 错误弹窗 */ function showErr(err) { const msg = errPicker(err); console.log(err); wx.showModal({ showCancel: false, content: msg }); } [代码] 最后就变成了: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch(req.showErr); [代码] 至此,一个简单的wx.request封装过程便完成了,封装过的[代码]req[代码]比起原来,使用上更加方便,扩展性和可维护性也更好。 结尾 以上内容其实是简化版的[代码]mp-req[代码],介绍了[代码]mp-req[代码]这一工具的实现初衷以及思路,使用[代码]mp-req[代码]来管理接口会更加的便捷,同时[代码]mp-req[代码]也提供了更加丰富的功能,比如插件机制、接口的缓存,以及接口分类等,欢迎大家关注mp-req了解更多内容。 以上最终代码可以在这里获取:req.js。
2020-08-04 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - 下载文件后缀名为unknown的解决方法
貌似这个问题有很多玩家遇到,微信官方说法是根据服务器响应的header中的Content-Type来决定下载到本地的文件的后缀的, 但是这个特性支持的特别不好,如果下载后,文件后缀名为.unknown就不好搞了。 怎么解决的呢? 思路: 利用wx.downloadFile下载文件,获取到tempFilePath临时文件。这个临时文件路径不要改动。 利用wx.getFileSystemManager获取到FileSystemManager对象,利用该对象的saveFile方法,把临时文件保存为本地文件,保存成功后,其success函数回调会返回savedFilePath本地路径。这个路径会把上面的临时文件移动到这个本地路径中,但是后缀名仍然为unknown。这一步的目的是把临时文件保存为本地文件。 本地文件已经有了,我们就可以对本地文件进行任意的操作。利用FileSystemManager对象的copyFile,把上面的后缀为unknown的本地文件,复制到另外的本地文件。这个时候你可以任意定义复制到的文件的后缀。 有一点坑的是:上面第一步、第二步中的文件的路径(tempFilePath、savedFilePath)我们都是知道的,但是第三部中的复制到的目标路径需要开发者自己定义路径。这个时候我们需要用到wx.env.USER_DATA_PATH常量。 这个常量是微信为每个小程序小游戏搞得目录地址,在这个路径下面你可以新建、删除等文件或者文件夹。有这个知识储备,你可以先调用FileSystemManager对象的mkdir方法,在wx.env.USER_DATA_PATH常量路径下新建一个文件夹,然后你再调用第三步的copyFile就可以了。 注: 第二步和第三步可以合并。 wx.downloadFile下载文件可以利用filePath指定下载的目标路径,这样可以省略上面的第二步操作,直接利用这个filePath进行第三部的复制操作,把后缀为unknown的filePath文件,复制到指定的文件后缀的目标文件中,然后就可以使用该文件了。 下面的代码是用TypeScript写的,但是逻辑思路是一样的。 (这是随便写的一个demo,代码有点乱堆,各位大佬轻喷~) [代码]private beginLoad(): void {[代码][代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]if[代码] [代码](Laya.Browser.onWeiXin) {[代码][代码] [代码][代码]var[代码] [代码]fileMgr: FileSystemManager = wx.getFileSystemManager();[代码] [代码] [代码] [代码] //利用access方法判断文件是否可用[代码] [代码] [代码][代码]fileMgr.access({[代码][代码] [代码][代码]path: wx.env.USER_DATA_PATH + [代码][代码]'/music/music.wav'[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'access res:'[代码][代码], res);[代码][代码] [代码][代码]AudioMgr.playMusic(wx.env.USER_DATA_PATH + [代码][代码]'/music/music.wav'[代码][代码]);[代码][代码] [代码][代码]},[代码] [代码] //失败,不可用,则下载文件到本地[代码] [代码] [代码][代码]fail: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'access file fail.'[代码][代码], res);[代码][代码] [代码][代码]wx.downloadFile({[代码][代码] [代码][代码]url: [代码][代码]'http://fjdx.sc.chinaz.com/Files/DownLoad/sound1/201808/10453.wav'[代码][代码],[代码][代码] [代码][代码]header: [代码][代码]''[代码][代码],[代码][代码] [代码][代码]filePath: [代码][代码]''[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'down load file success.'[代码][代码], res);[代码][代码] [代码][代码]that.saveMusicFile(res.tempFilePath);[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'down load file fail.'[代码][代码], res);[代码][代码] [代码][代码]},[代码][代码] [代码][代码]});[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码] //保存临时文件到本地路径 [代码] [代码][代码]private saveMusicFile(tempPath): void {[代码][代码] [代码][代码]var[代码] [代码]fileMgr: FileSystemManager = wx.getFileSystemManager();[代码] [代码] //这一步是把保存的文件列表删除掉。本地文件最大50M。很容易超过限制[代码] [代码] [代码][代码]fileMgr.getSavedFileList({[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](savedFiles) {[代码][代码] [代码][代码]console.log([代码][代码]'saved files:'[代码][代码], savedFiles);[代码][代码] [代码][代码]var[代码] [代码]fileList = savedFiles.fileList as Array<any>;[代码][代码] [代码][代码]for[代码][代码]([代码][代码]var[代码] [代码]i=0; i<fileList.length; i++) {[代码][代码] [代码][代码]fileMgr.removeSavedFile({[代码][代码] [代码][代码]filePath: fileList[i].filePath,[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](r) {[代码][代码] [代码][代码]console.log([代码][代码]'remove save file. success.'[代码][代码], r);[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](r) {[代码][代码] [代码][代码]console.log([代码][代码]'remove save file.'[代码][代码], r);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](savedFiles) {[代码][代码] [代码][代码]console.log([代码][代码]'saved files fail.'[代码][代码], savedFiles);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码] [代码] [代码] [代码] //把临时文件保存到本地文件中[代码] [代码] [代码][代码]fileMgr.saveFile({[代码][代码] [代码][代码]tempFilePath: tempPath,[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](data) {[代码][代码] [代码][代码]console.log([代码][代码]'save file:'[代码][代码], data);[代码] [代码] [代码] [代码] //新建dir目录,把保存的本地文件,复制到指定的文件夹下[代码] [代码] [代码][代码]fileMgr.mkdir({[代码][代码] [代码][代码]dirPath: wx.env.USER_DATA_PATH + [代码][代码]'music'[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'mkdir success:'[代码][代码], res);[代码] [代码] [代码] [代码] //复制文件到目标文件夹下指定的后缀的文件中[代码] [代码] [代码][代码]fileMgr.copyFile({[代码][代码] [代码][代码]srcPath: data.savedFilePath,[代码][代码] [代码][代码]destPath: wx.env.USER_DATA_PATH + [代码][代码]'/music/music.wav'[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](result) {[代码][代码] [代码][代码]console.log([代码][代码]'copy file :'[代码][代码], result);[代码] [代码] //复制成功后,就可以进行后续逻辑处理了[代码] [代码] [代码][代码]AudioMgr.playMusic(wx.env.USER_DATA_PATH + [代码][代码]'/music/music.wav'[代码][代码]);[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](result) {[代码][代码] [代码][代码]console.log([代码][代码]'copy file fail>'[代码][代码], result);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'mkdir fail res:'[代码][代码], res);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](data) {[代码][代码] [代码][代码]console.log([代码][代码]'save file fail:'[代码][代码], data);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]}[代码]
2018-08-14