个人案例
- 场景零售
场景零售
场景零售扫码体验
- 小程序深度合成服务在用证明获取指引
step1:在微信服务市场选择深度合成服务接入 路径:微信服务市场—接口和插件—深度合成,选择深度合成服务接入 微信服务市场链接:微信服务市场 [图片] step2:获取深度合成服务订单完成凭证,将该凭证上传小程序类目资质审核 [图片]
2023-12-22 - 微信小程序可以播放二进制音频流吗?
在项目中开发中,遇到这样的需求:有一段文字,需要通过后台接口转成语音传到前端进行播放。 因为文字是实时生成的,为保证实时性,需要在生成文字的过程中,转为一段一段的音频流通过websocket传递到前端,前端拿到音频流后立即开始播放,接收到后续的音频流后追加到播放音频里继续播放,达到实时生成文字,实时转换音频流,前端实时播放的效果。 类似于腾讯元宝小程序。这个怎么实现
06-13 - 这个小程序开场画面好漂亮,要怎么实现呀?
[视频] 这个小程序开场画面好漂亮,要怎么实现呀?
2020-12-03 - 小程序“本地组件”(原自定义交易组件本地生活平台)接入指引(不定期更新,文明交流勿喷)
本地组件 之前曾写过“新版自定义交易组件接入指引”,广受好评,解决了很多开发者接入时遇到的问题,当然,其中也不乏一些喂不饱的白眼狼,得到便利还反咬一口,无耻至极。 本文主要介绍本地组件产品的业务流程及内测期间各流程的注意事项,文档篇幅较长,如无需查看完整文档可以使用浏览器自带页面搜索功能进行关键字搜索(快捷键Ctrl+F ),组件目前还在内测中,如无法获取内测权限请耐心等待正式上线(因未给某人提供申请方式,大佬都被喷退群了,太令人失望啦)。 本文由Memory的小跟班编写,内容如有错误请指正,勿喷。 良言一句三冬暖,恶语伤人六月寒。 1、产品介绍 本地生活服务行业的商家,可通过"本地组件" (无需新申请专用商户号)将小程序中的商品(兑换券)上架到视频号橱窗,用户在视频号购买兑换券后可在线上/线下二次核销,实现到店核销(自提)/到家配送(同城)的业务场景。 业务流程图 [图片] 用户交互示意 [图片] 2、开通方式 2.1 权限开通 商家符合接入要求或收到内测邀请后需要准备好以下资料:商家名称、视频号名称、小程序名称、小程序APPID等填写报名申请表,此处不作报名表单提供,请自行通过自有资源获取,提交表单后,正常审核时效为1个工作日。 2.2 资质要求 内测期间,商家需要发邮件提交准入资质材料用于资质审核;组件正式上线后可通过MP后台「交易组件」入口开通的「本地组件」提交对应资质审核 2.2.1 商超购百行业(如:万达、天虹) 本规则适用于入驻视频号橱窗的所有需要使用本地组件的商超购百商家 商家类型 资质类型 资质要求 商超购百 营业执照 营业执照 商超购百 商标 1.资质主体与入驻主体(小程序)一致 <br> 2.需提供35类服务商标 <br> 3.若为关联公司或代运营公司入驻,需提供上述商标权利人的委托函或授权书 商超购百 门店数量要求 1.线下业务:以服务类商标命名的实体门店>1家 <br> 2.线上业务:以服务类商标命名的官方网站、独立app或其他平台 商超购百 门店品牌授权要求 提供不少于3个,在经营销售的合作或授权经销协议(协议方需为京东/天猫官方旗舰店或旗舰店品牌) 商超购百 承诺函 承诺在视频号橱窗中所售商品均为正规渠道授权经销商品,保证商品质量,遇到售后和纠纷问题协助视频号团队友好协商处理。 2.2.2 酒旅景区行业(如:万豪、维也纳、迪士尼、长隆) 本规则适用于入驻视频号橱窗的所有需要使用本地组件的酒店/景区商家,暂不支持旅行社 商家类型 资质类型 资质要求 酒旅商家 营业执照 营业执照 酒旅商家 商标 1.资质主体与入驻主体(小程序)一致 <br> 2.品牌商标 <br> 3.若为关联公司或代运营公司入驻,需提供上述商标权利人的委托函或授权书 酒旅商家 其他资质 「单体酒店」需提供:1. 卫生许可证 <br> 2.特种行业许可证 <br> 3.如涉及自助餐券需提交餐饮服务许可(如已三证合一可提交食品经营许可)<br> 提供四星级/五星级酒店证书 <br> 「酒店/度假村集团」需提供:1. 集团旗下一家酒店的资质(同单体酒店)<br> 2. 集团与该提供资质的酒店之间的管理关系证明并附集团旗下所有酒店名单 <br> 「全国/全球连锁主题乐园」主体需提供:1. 娱乐经营许可证 <br>「景区」需提供(任选其一):1.全国旅游景区质量等级评定委员会出具的《旅游景区质量等级证书》 <br> 2. 旅游局景区等级评定委员会红头文件 <br> 3.提供4A以上景区证书 酒旅商家 承诺函 承诺在视频号橱窗中开展相关业务,保证为视频号用户提供商品购买后的线下履约能力,遇到售后和纠纷问题协助视频号团队友好协商处理。 2.2.3 餐饮行业(如:麦当劳、肯德基、喜茶、瑞幸) 本规则适用于入驻视频号橱窗的所有需要使用本地组件的餐饮商家 商家类型 资质类型 资质要求 餐饮商家 营业执照 营业执照 餐饮商家 商标 1.资质主体与入驻主体(小程序)一致 <br> 2.品牌商标 <br> 3.若为关联公司或代运营公司入驻,需提供上述商标权利人的委托函或授权书 餐饮商家 门店数量要求 以上述商标命名的实体门店,全国实体店数量≥100家(美团/点评门店信息截图或百度/高德POI信息截图) 餐饮商家 其他资质 1.餐饮店主体: 需提供申请主体的「食品经营许可证(经营范围需包含餐饮制作相关项目)扫描件」 <br> 2. 非餐饮主体需提供:a) 《餐饮平台与门店的管理关系声明》 b) 《餐饮门店运营资质和责任承诺函》(含旗下所有门店名单) c)卡券使用门店的「食品经营许可证(经营范围需包含餐饮制作相关项目)扫描件」 餐饮商家 承诺函 承诺在视频号橱窗中开展相关业务,保证为视频号用户提供商品购买后的线下履约能力,遇到售后和纠纷问题协助视频号团队友好协商处理。 2.2.4 其他行业(如:电影、演出、健身、体检、买菜等) 本规则适用于提供本地生活服务的商家,如电影、演出、健身、体检、买菜、按摩、KTV等线下服务商家。 商家类型 资质类型 资质要求 到店综合 营业执照 营业执照 到店综合 商标 1.资质主体与入驻主体(小程序)一致 <br> 2.品牌商标 <br> 3.若为关联公司或代运营公司入驻,需提供上述商标权利人的委托函或授权书 到店综合 门店数量要求 1.线上平台经营型商家(美团、携程、猫眼电影等)需提供官网,需要有开设以35类服务类型品牌商标命名的官方网站、独立APP或在其他平台上经营卖场型店铺。 <br> 2.线下经营商家(如爱康国宾体检)以上述品牌商标命名的实体门店,全国实体店数量≥100家(美团/点评门店信息截图或百度/高德POI信息截图) 到店综合 其他资质 1.餐饮店主体: 需提供申请主体的「食品经营许可证(经营范围需包含餐饮制作相关项目)扫描件」 <br> 2. 非餐饮主体需提供:a) 《餐饮平台与门店的管理关系声明》 b) 《餐饮门店运营资质和责任承诺函》(含旗下所有门店名单) c)卡券使用门店的「食品经营许可证(经营范围需包含餐饮制作相关项目)扫描件」 到店综合 承诺函 承诺在视频号橱窗中开展相关业务,保证为视频号用户提供商品购买后的线下履约能力,遇到售后和纠纷问题协助视频号团队友好协商处理。 3、协议相关 本地组件正式上线后,商家在开通本地组件时,会查看和签订本地组件协议,协议将定义本地组件业务模式、技术服务费比例、对账周期、打款周期、打款账号等内容. 本地组件正式上线后,预计会按「月」为周期结算,收取商家在视频号场景下支付GMV*1%的技术服务费。 4、文档接口 暂不在本文提供,后续更新接入时的一些问题 5、带货测试 上架商品,商品审核,保证金提交,直播卖货,支付,订单发货,退货等环节测试。 6、对账打款测试 双方进行账单对账,技术服务费对账,商家请求打款,技术服务费打款测试。 内测期问,预计月初通过邮件将账单发给商家对接人进行对账,商家完成小额打款测试, 正式上线后,平台预计在每个月1号出对账单,商家在5个工作日确认对账单无误后,在20个工作日内完成打款。 [图片] 7、异常情况测试 保证金延迟提交会限制带货能力,佣金延迟打款限制带货能力等一场逻辑测试。 8、 FAQ 8.1 视频号小店、自定义交易组件、新版自定义交易组件、视频号交易组件与本地组件的区别 能力 商户号 业务类型 发货类型 技术服务费 本地组件 原小程序商户号 团购兑换券 本地配送/到店自提 支付口径/统一比例 自定义交易组件 原小程序商户号 商品/兑换券 快递/同城/到店 无 新版自定义交易组件 新申请二级商户号 商品/兑换券 快递/同城/到店 结算口径/分类目 视频号交易组件 新申请二级商户号 商品 快递/免物流 结算口径/分类目 视频号小店 新申请二级商户号 商品 快递/免物流 结算口径/分类目 自定义交易组件将会在4月底下限,届时通过自定义交易组件上架的商品将会失效,若商家在视频号继续经营卖货,需要接入「本地组件」或者「视频号交易组件/视频号小店」 8.2 本地组件的商家,如何售卖电商物流发货商品? 目前一个视频号只能关联一个视频号小店 或一个组件,内测期间,用新的视频号开通视频号小店,上架标准电商发货商品,上传至“优选联盟”。商家视频号通过优选联盟分销电商商品,区别本地生活售卖券,(也可以通过带货团长来实现)未来会支持视频号可同时关联本地组件和视频号小店。 8.3 本地组件的商家,如何缴纳保证金,缴纳多少? 本地组件商家保证金沿用已发布的橱窗保证金规则,自营账户交3万元保证金,其他推广员交100元保证金。具体规则可参考「视频号橱窗保证金条款 」 未完待续······
2023-04-21 - 【实战记录】使用新版接口获取手机号过程中,对小程序的基础库版本的了解
描述: 老版文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/deprecatedGetPhoneNumber.html 新版文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html 如题所示,最近项目中对获取手机号的接口进行升级,这里先展示下老版的写法 wxml部分: <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"></button> js部分: getphonenumber(e) { wx.login({ success (res) { if (res.code) { //发起网络请求 wx.request({ url: 'https://example.com/onLogin', data: { code: res.code }, success (res) { const data = { ..., encryptedData: e.detail.encryptedData, iv: e.detail.iv, sessionKey: res.data.session_key } wx.request({ url: 'https://example.com/getphonenumber', data, success (res) { // todo } }) } }) } } }) } 新版写法: getphonenumber(e) { wx.request({ url: 'https://example.com/getphonenumber', data: { code: e.detail.code }, success (res) { // todo } }) } 分析: 1、如文档所说旧版用户使用不当会存在下图所示问题,本身项目定位也是做自己的产品,也本着长期发展的目标,决定进行此次更新。 [图片] 2、新版代码简洁逻辑清晰,但若在项目中替换的话,还需要考虑到用户版本库使用情况,原因是:从基础库 2.21.2 开始,对获取手机号的接口进行了安全升级,这时需要登录下我们的小程序管理后台,设置-》基本设置-》基础库最低版本设置;查看下当前小程序用户版本库使用的占比情况,如下图所示,由于我们当前的小程序自2.21.3以下没有用户再使用,故直接设置了2.21.2,这样设置后,假设后面有低版本用户访问我们小程序的话,微信侧会提示用户去更新微信版本 [图片] 3、考虑如果其他同学项目发现有部分用户的版本是低于2.21.2的,这时可能就需要做兼容处理了。 小结: 获取手机号虽然api更简洁了,但也注意不能滥用哦~,好吧就到这里了,如有描述的不到位的地方,欢迎大家指正哈~ [图片]
2022-06-05 - 基于Minium框架的小程序自动化落地实践
致谢 本文的技术实现细节,要特别感谢微信团队中的严烨,乃华,冯永鹏等小伙伴的热心帮忙和技术指导。 Minium框架简介 框架优点 微信小程序官方推出的小程序自动化框架,是为小程序专门开发的自动化框架, 提供了 Python 和 JavaScript 版本。 支持一套脚本,iOS & Android & 模拟器,三端运行 提供丰富的页面跳转方式,看不到也能去得到 可以获取和设置小程序页面数据,让测试不止点点点 支持往 AppSerive 注入代码片段 可以使用 minium 来进行函数的 mock, 可以直接跳转到小程序某个页面并设置页面数据, 做针对性的全面测试 框架缺点 暂不支持H5页面的调试; 暂不支持插件内wx接口调用; 技术选型 minium支持Python 和 JavaScript 版本,而且有专门的团队定期维护,遇到问题可以在微信开发者社区进行提问,因此选择了minium。 Minium框架原理 minium提供一个基于unittest封装好的测试框架,利用这个简单的框架对小程序测试可以起到事半功倍的效果。 测试基类Minitest会根据测试配置进行测试,minitest向上继承了unittest.TestCase,并做了以下改动: 加载读取测试配置 在合适的时机初始化minium.Minium、minium.App和minium.Native 根据配置打开IDE,拉起小程序项目和或自动打开真机调试 拦截assert调用,记录检验结果 记录运行时数据和截图,用于测试报告生成 使用MiniTest可以大大降低小程序测试成本。 Properties 名称类型默认值说明 appminium.AppNoneApp实例,可直接调用minium.App中的方法 miniminium.MiniumNoneMinium实例,可直接调用minium.Minium中的方法 nativeminium.NativeNoneNative实例,可直接调用minium.Native中的方法 环境搭建 搭建Minium Doc 微信技术团队伙伴已告知,无需进行本地搭建,本地搭建的都是错的并且还是旧的文档) 无需构建,直接访问官方文档(https://minitest.weixin.qq.com/#/) 安装软件 下载并安装python 3.8+,地址:https://www.python.org/downloads/release/python-390/ 下载并安装微信开发者工具,地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 安装minium 下载 https://minitest.weixin.qq.com/minium/Python/dist/minium-latest.zip,然后执行 第一步:解压,解压路径自己记住(我这里解压到 C:\minium-1.2.6) [图片] 第二步:切换到文件目录下 第三步:执行即可 (如下图) [图片] 第四步:测试 [图片] 特别说明 如果新手通过本地解压再执行,遇到以下问题 [图片] 请直接使用以下命令,即可一次性成功安装 pip3 install https://minitest.weixin.qq.com/minium/Python/dist/minium-latest.zip [图片] [图片] 配置微信开发者工具 安装微信开发者工具(我本机使用的版本是1.06.2205312),并打开安全模式: 设置 -> 安全设置 -> 服务端口: 打开 [图片] 在工具栏菜单中点击设置,选择项目设置,切换到“本地设置”,将调试基础库选择大于2.7.3的库; [图片] 开启微信工具安全设置中的 CLI/HTTP (提供了命令行和HTTP两种调用方式)调用功能。 开启被测试项目的自动化端口号 [代码]"path/to/cli" auto --project "path/to/project" --auto-port 9420 [代码] 路径说明 path/to/project: 指代填写存放小程序源码的目录地址,文件夹中需要包含有 project.config.json 文件 [图片] path/to/cli: 指代开发者工具cli命令路径 [图片] 与下图一致证明开启成功 [代码]"C:/Program Files (x86)/Tencent/微信web开发者工具/cli" auto --project "C:/WeChatProjects/miniprogram-1" --auto-port 9420 [代码] [图片] 配置信息 代码结构目录 [图片] 模拟器Config.json [代码]{ "project_path": "C:\\WeChatProjects\\miniprogram-1", "dev_tool_path": "C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat", "debug_mode": "debug", "test_port": 9420, "platform": "ide", "app": "wx", "assert_capture": true, "request_timeout":60, "remote_connect_timeout": 300, "auto_relaunch": true } [代码] 真机Config.json [代码]{ "project_path": "C:\\WeChatProjects\\xxx_chinamobile-pmc_migration2\\unpackage\\dist\\build\\mp-weixin", "dev_tool_path": "C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat", "debug_mode": "debug", "test_port": 9420, "platform": "Android", "app": "wx", "enable_app_log": true, "device_desire": { "serial": "d310bf55" }, "assert_capture": true, "request_timeout":60, "remote_connect_timeout": 300, "auto_relaunch": true } [代码] Suite.json [代码]{ "pkg_list": [ { "case_list": [ "test_*" ], "pkg": "testcase.*_test" } ] } [代码] 测试用例 first_test.py [代码]# !/usr/bin/python # -*- coding: utf-8 -*- """ @File : first_test.py @Create Time: 2022-06-01 16:17 @Description: """ import minium class FirstTest(minium.MiniTest): def test_get_system_info(self): sys_info = self.mini.get_system_info() print("FirstTest: ", sys_info) self.assertIn("SDKVersion", sys_info) if __name__ == "__main__": import unittest loaded_suite = unittest.TestLoader().loadTestsFromTestCase(FirstTest) result = unittest.TextTestRunner().run(loaded_suite) print(result) [代码] 主程序入口 run.py [代码]# !/usr/bin/python # -*- coding: utf-8 -*- """ @File : run.py @Create Time: 2022-06-01 17:21 @Description: """ import os # 运行执行class文件中的指定用例 cmd0 = "minitest -m testcase.first_test --case test_get_system_info -c config.json -g" # 运行执行testcase文件中的指定用例 cmd1 = "minitest -m testcase.first_test -c config.json -g" # 按照suite配置执行用例 cmd2 = "minitest -s suite.json -c config.json -g" os.system(cmd0) [代码] 执行用例 [图片] 测试报告 测试结果存储在outputs下,运行命令 [代码]python -m http.server 12345 -d outputs [代码] 然后在浏览器上访问 http://localhost:12345 即可查看报告 [图片] 下载项目源代码 申请GitLab项目代码管理权限,下载小程序源代码 [图片] [图片] [图片] 可选项(注意!此部分内容只适用于我司内部产品) 下载Node.js 地址:http://nodejs.cn/download/current/ [图片] 下载并安装编译软件HBuilderX 下载HBuilderX 地址:https://dcloud.io/hbuilderx.html 安装HBuilderX [图片] [图片] 编译源代码 [图片] 报错的解决方案,进入源代码目录下,执行NPM INSTALL [图片] 编译并发布小程序 因为公司的小程序是通过Vue.js进行编写,发布项目需要先通过HBuilderX工具进行编译。 [图片] [图片] [图片] [图片] [图片] 执行测试脚本 [图片] 输出测试报告 用例执行成功 [图片] 用例执行失败 [图片] [图片] 用例执行异常 [图片] [图片] Nginx简介 Nginx (engine x) 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器。Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru 站点开发的。 它也是一种轻量级的Web服务器,可以作为独立的服务器部署网站(类似Tomcat)。它高性能和低消耗内存的结构受到很多大公司青睐,如淘宝网站架设。 Nginx配置 Minium框架的Nginx配置 [代码] server { listen 80; server_name your.domain.com; location / { alias /path/to/dir/of/report; index index.html; } } [代码] [图片] Nginx下载 下载Nginx 下载地址:https://nginx.org/en/download.html [图片] Nginx安装部署 下载完成后,解压缩,运行cmd,使用命令进行操作,不要直接双击nginx.exe,不要直接双击nginx.exe,不要直接双击nginx.exe 使用命令到达nginx的加压缩后的目录 [代码]cd C:\nginx-1.22.0 [代码] [图片] 启动nginx服务,启动时会一闪而过是正常的 [代码]start nginx [代码] [图片] 打开任务管理器在进程中看不到nginx.exe的进程(双击nginx.exe时会显示在这里),需要打开详细信息里面能看到隐藏的nginx.exe进程 [图片] 如果都没有可能是启动报错了查看一下日志,在nginx目录中的logs文件夹下error.log是日志文件 [图片] [图片] 修改配置文件,进入解压缩目录,直接文件夹点击进去即可,不需要从dos操作 [图片] 在conf目录下找到nginx.conf使用txt文本打开即可,找到server这个节点,修改端口号,如果有需求可以修改主页目录没有就不用修改 [图片] [图片] 修改完成后保存,使用以下命令检查一下配置文件是否正确,后面是nginx.conf文件的路径,successful就说明正确了 [代码]nginx -t -c /nginx-1.22.0/conf/nginx.conf [代码] [图片] 如果程序没启动就直接start nginx启动,如果已经启动了就使用以下命令重新加载配置文件并重启 [代码]nginx -s reload [代码] [图片] 关闭nginx服务使用以下命令,同样也是一闪而过是正常的,看一下是否进程已消失即可。 [代码]nginx -s stop #强制停止Nginx服务:常用 nginx -s quit #优雅地停止Nginx服务(即处理完所有请求后再停止服务) [代码] Nginx常见命令 [代码]nginx -s reopen #重启Nginx nginx -s reload #重新加载Nginx配置文件,然后以优雅的方式重启Nginx nginx -s stop #强制停止Nginx服务:常用 nginx -s quit #优雅地停止Nginx服务(即处理完所有请求后再停止服务) nginx -t #检测配置文件是否有语法错误,然后退出 nginx -?,-h #打开帮助信息 nginx -v #显示版本信息并退出 nginx -V #显示版本和配置选项信息,然后退出 nginx -t #检测配置文件是否有语法错误,然后退出 nginx -T #检测配置文件是否有语法错误,转储并退出 [代码] 在线查看 打开浏览器,访问 http://ip:port/,在线查看报告是否可以正常展示 [图片] 常见问题 (1)端口号被占用 解决方法:https://blog.csdn.net/qq_32265203/article/details/110088489 (2)nginx文件夹路径含中文 解决方法:中文路径改为英文路径,或者换一个不包含中文的路径 其他错误就详细看log中的描述 解决方法:百度 优化配置 [代码]#user nobody; #==工作进程数,一般设置为cpu核心数 worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { #==最大连接数,一般设置为cpu*2048 worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; #==客户端链接超时时间 keepalive_timeout 65; #gzip on; #当配置多个server节点时,默认server names的缓存区大小就不够了,需要手动设置大一点 server_names_hash_bucket_size 512; #server表示虚拟主机可以理解为一个站点,可以配置多个server节点搭建多个站点 #每一个请求进来确定使用哪个server由server_name确定 server { #站点监听端口 listen 8800; #站点访问域名 server_name localhost; #编码格式,避免url参数乱码 charset utf-8; #access_log logs/host.access.log main; #location用来匹配同一域名下多个URI的访问规则 #比如动态资源如何跳转,静态资源如何跳转等 #location后面跟着的/代表匹配规则 location / { #站点根目录,可以是相对路径,也可以使绝对路径 root html; #默认主页 index index.html index.htm; #转发后端站点地址,一般用于做软负载,轮询后端服务器 #proxy_pass http://10.11.12.237:8080; #拒绝请求,返回403,一般用于某些目录禁止访问 #deny all; #允许请求 #allow all; add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; #重新定义或者添加发往后端服务器的请求头 #给请求头中添加客户请求主机名 proxy_set_header Host $host; #给请求头中添加客户端IP proxy_set_header X-Real-IP $remote_addr; #将$remote_addr变量值添加在客户端“X-Forwarded-For”请求头的后面,并以逗号分隔。 如果客户端请求未携带“X-Forwarded-For”请求头,$proxy_add_x_forwarded_for变量值将与$remote_addr变量相同 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #给请求头中添加客户端的Cookie proxy_set_header Cookie $http_cookie; #将使用代理服务器的主域名和端口号来替换。如果端口是80,可以不加。 proxy_redirect off; #浏览器对 Cookie 有很多限制,如果 Cookie 的 Domain 部分与当前页面的 Domain 不匹配就无法写入。 #所以如果请求 A 域名,服务器 proxy_pass 到 B 域名,然后 B 服务器输出 Domian=B 的 Cookie, #前端的页面依然停留在 A 域名上,于是浏览器就无法将 Cookie 写入。 #不仅是域名,浏览器对 Path 也有限制。我们经常会 proxy_pass 到目标服务器的某个 Path 下, #不把这个 Path 暴露给浏览器。这时候如果目标服务器的 Cookie 写死了 Path 也会出现 Cookie 无法写入的问题。 #设置“Set-Cookie”响应头中的domain属性的替换文本,其值可以为一个字符串、正则表达式的模式或一个引用的变量 #转发后端服务器如果需要Cookie则需要将cookie domain也进行转换,否则前端域名与后端域名不一致cookie就会无法存取 #配置规则:proxy_cookie_domain serverDomain(后端服务器域) nginxDomain(nginx服务器域) proxy_cookie_domain localhost .testcaigou800.com; #取消当前配置级别的所有proxy_cookie_domain指令 #proxy_cookie_domain off; #与后端服务器建立连接的超时时间。一般不可能大于75秒; proxy_connect_timeout 30; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } #当需要对同一端口监听多个域名时,使用如下配置,端口相同域名不同,server_name也可以使用正则进行配置 #但要注意server过多需要手动扩大server_names_hash_bucket_size缓存区大小 server { listen 80; server_name www.abc.com; charset utf-8; location / { proxy_pass http://localhost:10001; } } server { listen 80; server_name aaa.abc.com; charset utf-8; location / { proxy_pass http://localhost:20002; } } } [代码] 创建数据库和数据表 数据库:XXXXX [图片] 数据表:device [图片] 存储数据信息 [图片] SQL 语句如下 [代码]CREATE TABLE `device` ( `id` int(11) NOT NULL AUTO_INCREMENT, `execid` int(11) NOT NULL, `device_number` int(11) DEFAULT NULL, `project_name` varchar(255) DEFAULT NULL, `case_field` varchar(255) DEFAULT NULL, `case_content` varchar(255) DEFAULT NULL, `triggertime` datetime DEFAULT NULL, `showtime` datetime DEFAULT NULL, `offlinetime` datetime DEFAULT NULL, `starttime` datetime DEFAULT NULL, `endtime` datetime DEFAULT NULL, `status` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=gb18030; [代码] 创建 Flask 服务 新建 db_func.py [代码]# !/usr/bin/python # -*- coding: utf-8 -*- """ @Author : Charles @File : db_func.py @Create Time: 2022-06-21 09:18 @Description: """ import time import pymysql def connect_db(): url = "127.0.0.1" name = "root" pwd = "123456" dataBase = "XXXXX" return pymysql.connect(host=url, port=3306, user=name, passwd=pwd, db=dataBase) def query_database(db, sql): cursor = db.cursor() try: if isinstance(sql, str): cursor.execute(sql) result = list(cursor.fetchall()) else: result = [] for sq in sql: cursor.execute(sq) result.append(list(cursor.fetchall())) except Exception as err: result = ''.join(('An db query exception happened: ', str(err))) # db.close() # 关闭数据库连接 return result def update_db(db, sql): cursor = db.cursor() try: if isinstance(sql, str): cursor.execute(sql) db.commit() else: print('sql 不是一个合格的字符串:{}'.format(sql)) except Exception as err: result = ''.join(('An db update exception happened: ', str(err))) db.rollback() print(result) # 数据库数据插入更新 def db_insert(db, sql): cursor = db.cursor() i = 0 try: cursor.execute(sql) db.commit() result = 'db insert success' except Exception as err: db.rollback() result = 'An db insert exception happened: ' + str(err) + ' ' + str(i) db.close() # 关闭数据库连接 return result def close_db(db): try: db.close() except Exception as err: result = ''.join(('An db closed exception happened: ', str(err))) def get_current_time(): return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) [代码] 新建 project_index.py [代码]# !/usr/bin/python # -*- coding: utf-8 -*- """ @Author : Charles @File : project_index.py @Create Time: 2022-06-21 09:53 @Description: """ import datetime import time from flask import * from common import db_func app = Flask(__name__) @app.route('/') def display(): """ 最新一轮回归测试的整体页面展示 :return: """ db = db_func.connect_db() # 如果实时数据正常显示,就把设备掉线时间默认设置为"1970-01-01 08:00:00" ,实时数据加载时长=数据显示时间-设备被触发时间 # 如果设备显示掉线页面,就把实时数据时间默认设置为"1970-01-01 08:00:00" ,实时数据加载时长=设备掉线时间-设备被触发时间 sql_all = 'SELECT device_number, project_name, case_field, case_content, ' \ '(CASE WHEN offlinetime ="1970-01-01 08:00:00" THEN TIMESTAMPDIFF(SECOND,triggertime,showtime) ' \ 'WHEN showtime ="1970-01-01 08:00:00" THEN TIMESTAMPDIFF(SECOND,triggertime,offlinetime) else 0 ' \ 'END) as duration, triggertime, showtime, offlinetime, starttime, endtime, status FROM device ' \ 'where execid in (select max(execid) from device) ;' # 如果实时数据正常显示,就把设备掉线时间默认设置为"1970-01-01 08:00:00" ,实时数据加载时长=数据显示时间-设备被触发时间 # 如果设备显示掉线页面,就把实时数据时间默认设置为"1970-01-01 08:00:00" ,实时数据加载时长=设备掉线时间-设备被触发时间 sql_fail = 'SELECT device_number, project_name, case_field, case_content, ' \ '(CASE WHEN offlinetime ="1970-01-01 08:00:00" THEN TIMESTAMPDIFF(SECOND,triggertime,showtime) ' \ 'WHEN showtime ="1970-01-01 08:00:00" THEN TIMESTAMPDIFF(SECOND,triggertime,offlinetime) else 0 ' \ 'END) as duration, starttime, endtime, status FROM device ' \ 'where status="fail" and execid in (select max(execid) from device) ;' pass_total_duration = 'SELECT CAST(SUM(TIMESTAMPDIFF(SECOND,triggertime,showtime)) AS CHAR) AS duration FROM device ' \ 'WHERE case_field="当前楼层" AND STATUS="pass" AND TIMESTAMPDIFF(SECOND,triggertime,showtime) <= "10" ' \ 'AND execid IN (SELECT MAX(execid) FROM device) ' fail_total_duration = 'SELECT CAST(SUM(TIMESTAMPDIFF(SECOND,triggertime,offlinetime)) AS CHAR) AS duration FROM device ' \ 'WHERE case_field="当前楼层" AND STATUS="fail" AND TIMESTAMPDIFF(SECOND,triggertime,offlinetime) > "10" ' \ 'AND execid IN (SELECT MAX(execid) FROM device) ' device_total_number = 'SELECT COUNT(*) FROM device WHERE case_field="当前楼层" AND execid IN (SELECT MAX(execid) FROM device) ' device_pass_number = 'SELECT COUNT(*) FROM device WHERE case_field="当前楼层" AND STATUS="pass" AND execid IN (SELECT MAX(execid) FROM device) ' device_fail_number = 'SELECT COUNT(*) FROM device WHERE case_field="当前楼层" AND STATUS="fail" AND execid IN (SELECT MAX(execid) FROM device) ' db_res_all = db_func.query_database(db, sql_all) de_res_fail = db_func.query_database(db, sql_fail) db_pass_total_duration = db_func.query_database(db, pass_total_duration) db_device_total_number = db_func.query_database(db, device_total_number) db_device_pass_number = db_func.query_database(db, device_pass_number) db_device_fail_number = db_func.query_database(db, device_fail_number) # Average Loading Time if db_device_pass_number[0][0] == 0: average_loading_time = "%.2f" % 0 else: average_loading_time = "%.2f" % (int(db_pass_total_duration[0][0]) / int(db_device_pass_number[0][0])) print("Average_loading_time: ", average_loading_time) # Averavge Loading Time Less Than 10s if db_device_pass_number[0][0] == 0: average_less_than_10s = "%.2f" % 0 else: average_less_than_10s = "%.2f" % (int(db_pass_lessthan_total_duration[0][0])/int(db_device_pass_number[0][0])) print("Averavge Loading Time Less Than 10s: ", average_less_than_10s) # Percentage of Loading Time Less Than 10s if db_device_pass_number[0][0] == 0: percentage_less_than_10s = "{:.2%}".format(0) else: percentage_less_than_10s = "{:.2%}".format(int(db_device_lessthan_pass_number[0][0])/int(db_device_pass_number[0][0])) print("Percentage of Loading Time Less Than 10s: ", percentage_less_than_10s) # Percentage of Loading Time Over 10s if db_device_pass_number[0][0] == 0: percentage_over_10s = "{:.2%}".format(0) else: percentage_over_10s = "{:.2%}".format(int(db_device_over_pass_number[0][0])/int(db_device_pass_number[0][0])) print("Percentage of Loading Time Over 10s: ", percentage_over_10s) # 获得SQL语句查询内容 db_res = [average_loading_time, average_less_than_10s, percentage_less_than_10s, percentage_over_10s, db_res_fail, db_res_pass_over_10s, db_res_all] if db_res: return render_template("/koneview_page.html", content=db_res) else: pass if __name__ == "__main__": app.run(host='xx.xx.xx.xx', port=5500, debug=True) [代码] 创建 HMTL 网页 新建 project_page.html [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>MyProject Monitor Platform</title> <link rel="icon" href="https://www.myproject.cn/zh/Images/myproject-logo-76x52_tcm156-8930.png"> <script src="static/lib/jquery-1.11.1.min.js" type="text/javascript"></script> <div class="navi-brand"> <a class="logo" onclick="zhugeTrack('顶栏按钮点击',{'按钮名称':'LOGO'});" href="https://www.myproject.com/en/" target="_blank"> <img src="/static/images/myprojectLOGO.png" style="text-align: left; width: 265px; height: 50px; position: absolute; left: 62px; top: 30px; margin-left: -60px;margin-top: -20px;"></a> </div> <style type="text/css"> .bg{ background-image: url("https://pic4.zhimg.com/80/v2-052324a9dae7a6feb2e5ddf6f68ad8c6_720w.jpg"); background-repeat: repeat; } em{ color: red; font-style: normal; } table{ border-spacing: 0; width: 100%; border: 1px solid black; } thead th { border: 1px solid black; /*text-align: center;*/ width: auto; color: blueviolet; } th{ border: 1px solid black; text-align: left; width: auto; background-color: #69ABD6; } td{ border: 1px solid black; text-align: left; } </style> </head> <body class="bg"> <table style="margin-bottom: -1px" class="left" id="table_top_header"> <h1 align="center" style="color:#0071B9 ; font-size:30px">MyProject Monitor Platform</h1> <h3 align="left" style="color:#0071B9 ; font-size:20px">Basic Information</h3> <tr> <th>Owner</th> <th>Project Name</th> <th>Running Environment</th> <th>Running System</th> <th>Running Equipment</th> <th>Testing Phase</th> <th>Monitor Frequency</th> <th>Test Report</th> </tr> <tr> <td>Charles</td> <td>MyProject</td> <td>Production</td> <td>Windows 10 64bit</td> <td>Wechat DevTools</td> <td>Regression Test</td> <td>One day/time</td> <td><a style="background: #eee4a6" href="http://ip:port/" target="_blank">Minium</a></td> </tr> </table> <table style="margin-bottom: -1px" class="left" id="table_analysis"> <tbody> <h3 align="left" style="color:#0071B9 ; font-size:20px">LiveData Analysis</h3> <tr> <th>Average Loading Time(sec)</th> <th>Percentage of Loading Time Less Than 10s</th> <th>Percentage of Loading Time Over 10s</th> </tr> <div > <div id="ChangelistTable0"> <tr> <td class="text-center" > <div> {% if content[2] %} <span style="background: #e8e272"> <a>{{content[2]}}</a> </span> {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[3] %} <span style="background: #7ad9f4"> <a>{{content[3]}}</a> </span> {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[4] %} <span style="background: #e999d4"> <a>{{content[4]}}</a> </span> {% else %} {% endif %} </div> </td> </tr> </div> </div> </tbody> </table> <table style="margin-bottom: -1px" class="left" id="table_header"> <tbody> <h3 align="left" style="color:#0071B9 ; font-size:20px">TestCase Failed</h3> <tr> <th>No</th> <th>Device Number</th> <th>Project Name</th> <th>Monitor Field</th> <th>Live Data</th> <th>Duration(s)</th> <th>Latest Case Executed Time</th> <th>Latest Case Ended Time</th> <th>Latest Case Result</th> </tr> <div > <div id="ChangelistTable1"> {% for i in range(content[1]| length) %} <tr> <td> {{ i+1 }} </td> <td class="text-center" > <div> {% if content[1][i][0] %} {{ content[1][i][0]| safe }} {% else %} {% endif %} </div> </td> <td> <div> {% if content[1][i][1] %} {{ content[1][i][1]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][2] %} {{ content[1][i][2]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][3] %} {{ content[1][i][3]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][4] %} {% if content[1][i][4]<=10 %} <span style="background: lightseagreen"> <a>{{content[1][i][4]}}</a> </span> {% else %} <span style="background: lightcoral"> <a>{{content[1][i][4]}}</a></span> {% endif %} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][5] %} {{ content[1][i][5]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][6] %} {{ content[1][i][6]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][7] %} {% if content[1][i][7]=='pass' %} <span style="background: mediumseagreen"> <a>PASS</a> </span> <!-- <span style="background: #50d2c8"> <a href={{content[0][i][6]}} target="_blank">Pass</a> </span>--> {% elif content[1][i][7]=='fail' %} <span style="background: orangered"> <a>FAIL</a> </span> <!-- <span style="background: red"> <a href={{content[0][i][6]}} target="_blank">Fail</a> </span>--> {% else %} <span style="background: sandybrown"> <a>Scheduled</a> </span> {% endif %} {% else %} {% endif %} </div> </td> </tr> {% endfor %} </div> </div> </tbody> </table> <table style="margin-bottom: -1px" class="left" id="table_list"> <tbody> <h3 align="left" style="color:#0071B9 ; font-size:20px">TestCase Description</h3> <tr> <th>No</th> <th>Device Number</th> <th>Project Name</th> <th>Monitor Field</th> <th>Live Data</th> <th>Duration(s)</th> <th>Latest Triggered Time</th> <th>Latest Showed Time</th> <th>Latest Dropped Time</th> <th>Latest Case Executed Time</th> <th>Latest Case Ended Time</th> <th>Latest Case Result</th> </tr> <div > <div id="ChangelistTable"> {% for i in range(content[0]| length) %} <tr> <td> {{ i+1 }} </td> <td class="text-center" > <div>{{ content[0][i][0]| safe }}</div> </td> <td> <div>{{ content[0][i][1]| safe }}</div> </td> <td class="text-center" > <div>{{ content[0][i][2]| safe }}</div> </td> <td class="text-center" > <div> {% if content[0][i][3] %} {{ content[0][i][3]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][4] %} {% if content[0][i][4]<=10 %} <span style="background: lightseagreen"> <a>{{content[0][i][4]}}</a> </span> {% else %} <span style="background: lightcoral"> <a>{{content[0][i][4]}}</a></span> {% endif %} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][5] %} {{ content[0][i][5]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][6] %} {{ content[0][i][6]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][7] %} {{ content[0][i][7]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][8] %} {{ content[0][i][8]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][9] %} {{ content[0][i][9]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][10] %} {% if content[0][i][10]=='pass' %} <span style="background: mediumseagreen"> <a>PASS</a> </span> <!-- <span style="background: #50d2c8"> <a href={{content[0][i][6]}} target="_blank">Pass</a> </span>--> {% elif content[0][i][10]=='fail' %} <span style="background: orangered"> <a>FAIL</a> </span> <!-- <span style="background: red"> <a href={{content[0][i][6]}} target="_blank">Fail</a> </span>--> {% else %} <span style="background: sandybrown"> <a>Scheduled</a> </span> {% endif %} {% else %} {% endif %} </div> </td> </tr> {% endfor %} </div> </div> </tbody> </table> </body> </html> [代码] 在线查看报告 打开http://IP:PORT/,在线查看网页报告 [图片] 项目迁移到Jenkins平台 [图片] 创建一个新的Node节点 [图片] 节点名称为Application,标签为Monitoring [图片] 标签为Monitoring下的所有节点列表 [图片] 节点Application上的项目列表 [图片] 节点Monitoring_01上的项目列表 [图片] 测试机上部署Agent服务 将Jenkins平台上自动生成的jar包,放入对应的测试机上 [图片] 输入Jenkins平台上自动生成的命令,进行命令行启动 [代码]java -jar agent.jar -jnlpUrl http://XX.XX.XXX.XXX:8080/computer/Applications/jenkins-agent.jnlp -secret XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -workDir "C:\JenkinsWorkSpace\247" [代码] 验证Agent服务,是否成功连到Jenkins平台上 [图片] Jenkins上进行项目的配置部署 [图片] [图片] [图片] [图片] [图片] 通过Publish HTML reports插件,查看报告 [图片] [图片] 通过http://x.x.x.x:xxxx/ ,查看报告 [图片] 参考资料 https://git.weixin.qq.com/groups/minitest https://blog.csdn.net/jiangjunsss/article/details/120228371 https://blog.csdn.net/baguenaudier/article/details/124478687 https://blog.csdn.net/weixin_49546967/article/details/119858529 https://minitest.weixin.qq.com/#/minium/Python/readme https://developers.weixin.qq.com/community/develop/article/doc/0000cae3a58748ed7f2c8975351413 https://www.cnblogs.com/taiyonghai/p/9402734.html https://www.cnblogs.com/zpcdbky/p/15339213.html https://blog.csdn.net/qq_32265203/article/details/110088489 https://blog.csdn.net/qq_43813373/article/details/123116268
2022-08-22 - 小程序自动化实践方案总结:Minium + 云测
本文旨在为大家在着手做小程序自动化前提供参考,少踩坑。里面不会涉及细节的实践操作,主要讲解流程让大家对小程序自动化有个整体的印象。本文中存在的不足还望指出,多谢~😄 背景:由于每次迭代我们需要手动进行大量小程序用例回归,其中很多都是简单的组件操作,为了提升回归效率将更多的人力放在复杂场景回归和探索式测试中,因此希望通过小程序自动化来实现简单场景的回归用例。 要进行小程序自动化落地,需要先对下面2点进行了解: 全方位了解微信官方提供的小程序自动化能力的特点(云测、录制回放、Minium、虚拟账号)熟悉自己项目小程序的特性:是否支持一键登录、项目代码编译后元素class属性值是否会变、自己的公司是否是第三方服务商在了解小程序自动化能力之后,结合自己小程序特性,选择合适的方案进行落地。 下面陆续讲述的内容会涉及上面2点。 一、小程序自动化能力简介 1、云测整体流程 云测平台可以基于你上传的用例创建相应的测试计划,然后选择小程序不同版本(开发版、体验版、线上版)、手机系统类型(安卓和ios)、测试账号等根据测试计划创建任务,跑测完毕可以通过短信和邮件通知测试结果。 [图片] [图片] 2、录制回放 如果想要把录制回放的用例上传到云测,必须使用虚拟账号进行录制。这里有篇腾讯团队内部实践的文章。 3、Minium 要进行Minium用例编写,前提是搭建好小程序开发环境,这个可以询问自己项目组的前端开发。下图是以我们组内实践为例画的流程图,让大家对如何运行Minium脚本有个概念。图内的切换小程序,若你们不是第三方服务商则不涉及;云盾是我们公司的用例管理执行平台,同理也可以替换为你们自己的用例平台,将跑测完的结果同步到用例平台。 编写用例也是采用的PO模式,在微信开发中工具上进行元素定位,整体思路与写web自动化差异不大,这里可以参考有赞的文章,就不赘述了。 特别注意:若使用的是wxss选择器定位元素,一定要确认元素的class属性值会不会在每次编译小程序的时候发生改变,若会的话强烈建议使用xpath进行定位。 [图片] 4、虚拟账号 虚拟账号常用在录制回放和云测跑测试任务的时候。 若小程序必须登录才能使用,且符合截图中标记的这3项之一,就不能通过云测平台来跑自己录制或编写的用例了。 [图片] 5、持续集成 目前提供了在云测平台创建测试任务和获取测试任务结果的接口,所以可以搭配jenkins使用。举个例子:调用接口创建测试任务----调用接口查询任务结果----将结果同步到用例平台或定制化测试结果邮件 二、方案选择 1、3种自动化方式对比 [图片] 2、方案选择 [图片] 页面多少决定选择录制还是minium,是否适用虚拟账号决定能否用云测,用例执行时间决定本地还是云测运行更方便(当然全部跑本地也行啊~) 选择云测平台跑测的一个好处是基于真机去跑,据了解后面可能会支持付费选择手机机型和付费增加跑测时间。还有一点就是不用自己去倒腾本地配置真机运行(😂我现在都还没实践本地真机运行)。 3、举我们产品这个例子 我们作为小程序服务提供商小程序页面有上百个,绝大多数场景对账号没有要求,落地方案是:minium+云测。 所以每次迭代发布,只需运行一下测试任务的脚本便可坐等测试结果就好。 三、基于项目编写Minium用例 1、目录结构 [图片] 2、用例编写流程 若小程序页面不是非配置化的,可以省去配置这一步;若不需要同步测试结果到用例平台,可以省去最后一步。 [图片] 3、基于下面这个页面编写用例,步骤如下。 分别在page、logic、case目录下,分别创建allHouseListPage、AllHouseListLogic、AllHouseListCaseAllHouseListPage继承BasePage类,编写页面中各个组件的元素定位和操作方法AllHouseListLogic编写操作流程AllHouseListCase继承minium.MiniTest类,因为是固定在该页面执行用例,每个组件操作的前置步骤都是先进入该页面,所以在setUp里面添加进入该页面的前置操作(举一反三,其它页面的case的前置操作也是如此)。[图片] 4、tips 页面跳转后最好多用wait_for等待元素加载完,不然用例出现找不到元素的报错定位的元素必须绑定了事件,才能触发操作不熟悉怎么写的时候,可以先通过录制脚本同步到云测平台后,查看录制脚本进行参考 四、快速Monkey 只有服务商才有快速Monkey入口,这个我目前用的少,个人见解是每次有小的改动发了bg分支时,可以挑选一些流量大的甲方爸爸小程序进行冒烟测试。
2022-08-08 - 小程序自动销毁后的使用体验优化
2023年12月24日更新 经过测试发现,官方提供的onSaveExitState有一些问题,不太符合官方文档提出的预期,大家暂时慎重使用onSaveExitState功能,用其他方案吧。 ======================================================================== 假设用户在小程序内进行一个答题的活动,或者进行一个测试,这个活动或测试的时间比较长,大概需要10分钟的时间。当用户答题进行到一半的时候,来了一个重要的电话,电话打了十几分钟,回来之后想着继续进行操作,发现小程序是重新打开的状态。之前答题答了5分钟,白费了。这样,用户需要重新进行答题。 问题场景分析 用户离开小程序时间太久(官方说30分钟以上,但测试十几分钟分钟以上)或者手机内存不够用的时候,小程序会被销毁,也就是完全终止运行了。此时用户再想进入小程序进行之前的操作,只能重新操作一遍。 解决方案 以本场景为例,如果用户正在答题,在用户退出小程序的时候,将当前页面的答题进度数据进行一个保存,当用户再重新进入小程序的时候,检查是否有答题进行一半的数据。如果有,自动跳转到答题的页面,并且在onload中恢复退出之前状态的数据,让用户继续进行答题的操作。 微信小程序有一个非常好用的回调函数onSaveExitState。 退出状态onSaveExitState 每当小程序可能被销毁之前,页面回调函数 [代码]onSaveExitState[代码] 会被调用。如果想保留页面中的状态,可以在这个回调函数中“保存”一些数据,下次启动时可以通过 [代码]exitState[代码] 获得这些已保存数据。 代码示例: { "restartStrategy": "homePageAndLatestPage" } Page({ onLoad: function() { var prevExitState = this.exitState // 尝试获得上一次退出前 onSaveExitState 保存的数据 if (prevExitState !== undefined) { // 如果是根据 restartStrategy 配置进行的冷启动,就可以获取到 prevExitState.myDataField === 'myData' } }, onSaveExitState: function() { var exitState = { myDataField: 'myData' } // 需要保存的数据 return { data: exitState, expireTimeStamp: Date.now() + 24 * 60 * 60 * 1000 // 超时时刻 } } }) onSaveExitState 返回值可以包含两项: 字段名 类型 含义 data Any 需要保存的数据(只能是 JSON 兼容的数据) expireTimeStamp Number 超时时刻,在这个时刻后,保存的数据保证一定被丢弃,默认为 (当前时刻 + 1 天) 一个更完整的示例:在开发者工具中预览效果 注意事项如果超过 [代码]expireTimeStamp[代码] ,保存的数据将被丢弃,且冷启动时不遵循 [代码]restartStrategy[代码] 的配置,而是直接从首页冷启动。[代码]expireTimeStamp[代码] 有可能被自动提前,如微信客户端需要清理数据的时候。在小程序存活期间, [代码]onSaveExitState[代码] 可能会被多次调用,此时以最后一次的调用结果作为最终结果。在某些特殊情况下(如微信客户端直接被系统杀死),这个方法将不会被调用,下次冷启动也不遵循 [代码]restartStrategy[代码] 的配置,而是直接从首页冷启动。
2023-12-24 - 小程序销毁的时机
小程序会被销毁的三大场景: 1 当钱小程序进入后台后,如果很长时间-目前是 30 分钟-后没有再次进入,小程序会被销毁。 2 当小程序占用系统资源过高,会被系统销毁或被微信客户端主动回收。 3 在 iOS 上,当微信客户端在一定时间间隔内连续收到系统内存告警时,会根据一定的策略,主动销毁小程序,并提示用户 (运行内存不足,请重新打开该小程序)。 如果小程序中有过多占用内存的场景,建议使用 wx.onMemoryWarning 监听内存告警事件,进行必要的内存清理。
2022-05-21 - 微信小程序适配企业微信的通讯录组件 ww-open-data 【踩坑记录】
微信原生小程序中使用 第一步:在 app.json 中配置通讯录插件 [代码]"plugins": { "contactPlugin": { "version": "1.2.3", "provider": "wx5917c8c26f85c588" } } [代码] 第二步:在使用 ww-open-data 组件的页面 json文件中引入插件 [代码]"usingComponents": { "ww-open-data": "plugin://contactPlugin/ww-open-data" } [代码] uni-app 小程序中使用 第一步:在 manifest.json 微信模块中配置通讯录插件 [代码]"mp-weixin" : { "plugins": { "contactPlugin": { "version": "1.2.3", "provider": "wx5917c8c26f85c588" } } } [代码] 第二步:在 pages.json 中全局引入插件 [代码]"globalStyle": { "usingComponents": { "ww-open-data":"plugin://contactPlugin/ww-open-data" } } [代码] 踩坑记录 Q1:微信插件接口报错 【result: -300】 A1:小程序未在对应的企业下安装测试【具体安装步骤见下方:(企业微信安装测试小程序)】 Q2:微信插件接口报错 【result: {errCode: 1, humanMessage: “数据不存在”}】 A2:一定要按照官方企微通讯录文档中的版本使用,【我用的是 1.2.3/1.2.1这两个版本都可以】文档地址:https://developer.work.weixin.qq.com/document/path/91958#3-小程序方案 Q3:微信开发工具提示【查询失败,请退出重试】 [图片] A3:同 Q1 解决方式相同 企业微信安装测试小程序 第一步:在企微管理后台进入服务商后台 第二步:在服务商后台关联微信小程序 [图片] 第三步:在关联小程序中安装测试企业 [图片] 注意 1、只有在关联小程序中安装的测试企业,测试企业中的通讯录便可在微信小程序中正常显示 2、本次适配微信小程序开发工具使用小程序模式,而非企业微信小程序模式 文档地址 企微官方文档
2022-05-19 - 使用 directCommit 直接提交至待审核列表,然后提交审核时提示先提交代码?
开发工具提交代码 [图片] [图片] 然后提交审核时候,接口返回 [图片] [图片] 不知道什么原因导致的,其他小程序都可以,唯独一个小程序不行。求开发小哥哥帮忙定位问题
2021-04-25 - 第三方平台存在获取已授权小程序的当前版本信息与待审核列表的接口吗?
如果没有的话,通过directCommit直接上传到审核列表的小程序如何追踪其版本信息呢? 具体问题: 1.提审的时候如何确定是正确的待审核版本。假设多人都有过directCommit上传操作 2.发版后如何知道当前的版本 需要人工维护吗?
2020-12-24 - 07.Taro框架获取用户手机号解决方案
流程图 [图片] 核心代码 PhoneAuth 组件 [代码]import Taro from '@tarojs/taro'; import React, { useCallback, useEffect, useState } from 'react'; import { View, Button } from '@tarojs/components'; import { PLATFORM_TYPE } from '@/constant/index'; import tools from '@/utils/tools'; import './index.less'; // 工具方法 const PhoneManager: TaroMiniApp.IUtilsPhoneManager = { // 老版本: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/deprecatedGetPhoneNumber.html async oldVersionGetPhoneNumber(evt: TaroMiniApp.TGetPhoneNumberEvtDetail): Promise<TaroMiniApp.TaroApiResult> { return Taro.login() .then((codeRes: Taro.login.SuccessCallbackResult) => { // console.log(codeRes, evt); return this.getPhoneNumber(evt, codeRes.code) }) }, // 新版本: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html async newVersionGetPhoneNumber(evt: TaroMiniApp.TGetPhoneNumberEvtDetail): Promise<any> { console.log(evt); return this.getPhoneNumber(evt); }, // 获取支付宝绑定的手机号 async handleGetAliPayPhoneNumber(): Promise<TaroMiniApp.TaroApiResult> { try { const res = await (Taro as any).getPhoneNumber(); const data = JSON.parse(res.response); if (data.response.code !== 0) { return Promise.reject({ code: data.response.code, errMsg: 'getPhoneNumber:fail', data: { error: data.response }, }) } // TODO: 支付宝 return this.getPhoneNumber(data); } catch (error) { return Promise.reject({ code: 10, errMsg: 'getPhoneNumber:fail', data: { error: error }, }) } }, // TODO: 对接后端服务 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html getPhoneNumber(evtDetail: TaroMiniApp.TGetPhoneNumberEvtDetail, code?: string): Promise<TaroMiniApp.TaroApiResult> { // console.log(evtDetail, code); return Promise.resolve({ code: 0, errMsg: evtDetail?.detail?.errMsg, data: { phone: `121212-${code}` }, }) }, // 处理获取手机号事件 async handleGetPhoneNumberEvt(evt?: TaroMiniApp.TGetPhoneNumberEvtDetail): Promise<TaroMiniApp.TGetPhoneNumberResult> { if (PLATFORM_TYPE === 'WEAPP') { if (evt?.detail?.errMsg.indexOf('deny') !== -1) { return Promise.reject({ errMsg: evt?.detail.errMsg, code: 10, }) } // 获取系统信息 const { SDKVersion } = tools.getSystemInfo(); // console.log(SDKVersion, tools.compareVersion(SDKVersion, '2.21.2')); // 版本号对比 if (tools.compareVersion(SDKVersion, '2.21.2') === -1) { return this.oldVersionGetPhoneNumber(evt); } // new version return this.newVersionGetPhoneNumber(evt); } if (PLATFORM_TYPE === 'ALIPAY') { return this.handleGetAliPayPhoneNumber(); } const res = { code: 10, errMsg: 'getPhoneNumber: noSupport', data: {}, }; return Promise.resolve(res); }, } const PhoneAuth = (props: TaroMiniApp.IComponentsPhoneAuthProps) => { const [phoneNumberAuth, setPhoneNumber] = useState(false); useEffect(() => { if (PLATFORM_TYPE === 'ALIPAY') { Taro.getSetting() .then((res: Taro.getSetting.SuccessCallbackResult) => { setPhoneNumber((res.authSetting as any).phoneNumber); }); } }, []); const handleGetPhoneNumber = useCallback(async (evt) => { if (PLATFORM_TYPE === 'WEAPP') { PhoneManager.handleGetPhoneNumberEvt(evt).then((res) => { props.onSuccess(res) }).catch((err) => { props.onFail(err) }) return; } if (PLATFORM_TYPE === 'ALIPAY') { PhoneManager.handleGetPhoneNumberEvt().then((res) => { props.onSuccess(res) }).catch((err) => { props.onFail(err) }) } }, []); const getManualPhoneNumber = useCallback(async () => { PhoneManager.handleGetPhoneNumberEvt().then((res) => { props.onSuccess(res) }).catch((err) => { props.onFail(err) }) }, []); const onAuthError = useCallback((err) => { props.onFail({ code: 11, errMsg: 'getPhoneNumber: deny', data: err.detail }) }, []); const btnTxt = props.children || props.btnText || '手机号登录'; if (PLATFORM_TYPE !== 'ALIPAY' && PLATFORM_TYPE !== 'WEAPP') { return <View>不支持</View> } return ( <View className="phoneAuthContainer"> { PLATFORM_TYPE === 'WEAPP' && ( <Button style={`${props.btnStyle}`} className={`${props.uesSlot ? 'resetBtn' : ''}`} openType="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber} >{btnTxt}</Button> ) } {/* 支付宝已授权用户 */} { PLATFORM_TYPE === 'ALIPAY' && !phoneNumberAuth && ( <Button style={`${props.btnStyle}`} className={`${props.uesSlot ? 'resetBtn' : ''}`} openType="getAuthorize" scope='phoneNumber' onGetAuthorize={handleGetPhoneNumber} onError={onAuthError} >{btnTxt}</Button> ) } {/* 支付宝未授权用户 */} { PLATFORM_TYPE === 'ALIPAY' && phoneNumberAuth && ( <Button style={`${props.btnStyle}`} className={`${props.uesSlot ? 'resetBtn' : ''}`} onClick={getManualPhoneNumber} >{btnTxt}</Button> ) } </View> ); } export default PhoneAuth; [代码] 如何使用 [代码]import React, { useCallback } from 'react'; import { View, } from '@tarojs/components'; // 引入的 PhoneAuth 组件 import { PhoneAuth } from '@/components/index'; import './index.less'; export default function PhoneAuthDemo() { const handleSuccess = useCallback((res) => { console.log('success:', res); }, []); const handleFail = useCallback((err) => { console.log('fail:', err); }, []); const resetBtnStyle = "display: flex; background: #eee"; return ( <View className="page"> <View className='wrap'> <PhoneAuth onSuccess={handleSuccess} onFail={handleFail} /> </View> <View className='wrap'> <PhoneAuth onSuccess={handleSuccess} onFail={handleFail} btnText="自定义文本" /> </View> <View className='wrap'> <PhoneAuth onSuccess={handleSuccess} onFail={handleFail} btnStyle={resetBtnStyle} uesSlot> <View>我是自定义 slot</View> </PhoneAuth> </View> </View> ) } [代码]
2022-03-03 - #小程序 小程序和公众号内长按识别哪些码是有效的
Tip:2021-05-21 测试了小程序图片长按识别个人微信码、群聊码、企业微信码可以直接添加。 须知:以下结果均在微信IOS最新版(8.0.2)测试所得!!! 视频号二维码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片] 个人赞赏码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 官方回复小程序因策略调整不能识别:https://developers.weixin.qq.com/community/develop/doc/0008ea7edb8f4845c39be413456c00?highLine=%25E8%25B5%259E%25E8%25B5%258F%25E7%25A0%2581%25E8%25AF%2586%25E5%2588%25AB [图片] 个人微信号二维码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片] 个人收款二维码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片] 公众号(订阅号)二维码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片] 小程序码 公众号内长按识别结果:可以 小程序内长按识别结果:可以 小程序内webview(公众号文章):可以 小程序内webview(自定义H5):可以 小程序客服消息长按识别:可以 [图片] 小商店码 公众号内长按识别结果:可以 小程序内长按识别结果:可以 小程序内webview(公众号文章):可以 小程序内webview(自定义H5):可以 小程序客服消息长按识别:可以 [图片] 企业微信码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片] 普通网址二维码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片]
2021-05-21 - 小程序中图片二维码、小程序码,长按识别支持的情况
因为看到最近还有人刷到这篇文章还有收藏的,所以特别说明一下: 以下是2021年5月31日时候测试的结果,并不一定与现在的情况相符。现在啥情况,我也不知道,已经不咋做小程序了。所以大家实际使用时候,请大家还是再测测。 上面这段话更新于2021年10月11日 下面是原文 ==================================================================================================================== 最近小程序中的图片支持长按识别了,总结一下几种情况下: 测试时间:2021-5-31 微信版本:8.0.6 当前时间最新 image标签 + show long press menu <image src="https://img.qr.com/qr.jpg" style="width: 100%;" mode="widthFix" show-menu-by-longpress="{{true}}"></image> ✅ 识别小程序码 - ✅ 跳转小程序 ✅ 识别群二维码 - ❌ 跳转到加群页面 ✅ 识别名片二维码 - ❌ 跳转到加好友页面 ❌ 识别小程序二维码 wx.previewImage ✅ 识别小程序码 - ✅ 跳转小程序 ✅ 识别群二维码 - ✅ 跳转到加群页面 ✅ 识别名片二维码 - ✅ 跳转到加好友页面 ❌ 识别小程序二维码 web-view ✅ 识别小程序码 - ✅ 跳转小程序 ✅ 识别群二维码 - ✅ 跳转到加群页面 ✅ 识别名片二维码 - ✅ 跳转到加好友页面 ❌ 识别小程序二维码 总结,目前微信已经开放了在小程序中长按识别。但是似乎还有一些bug,image标签可以识别到,但是点了没反应。
2021-10-11 - 小程序中如何实现并发控制?
小程序中如何实现并发控制? 一、性能之网络请求数 wx.request、wx.uploadFile、wx.downloadFile 的最大并发限制是 10 个; 小程序中,短时间内发起太多请求会触发小程序并发请求数量的限制同时太多请求也可能导致加载慢等问题,应合理控制请求数量,甚至做请求的合并等。 上传多图使用Promise.all并发处理,很容易触发限制,导致请求失败。 故需做并发控制。实现并发控制此处将对p-limit和async-pool进行研究 二、p-limit并发控制的实现 2.1 p-limit的使用 [代码]const limit = pLimit(1); const input = [ limit(() => fetchSomething('foo')), limit(() => fetchSomething('bar')), limit(() => doSomething()) ]; // 一次只运行一个promise const result = await Promise.all(input); console.log(result); [代码] pLimit(concurrency) 返回一个[代码]limit[代码]函数。 concurrency(Number): 表示并发限制,最小值为1,默认值为Infinity limit(fn, …args) 返回通过调用[代码]fn(...args)[代码]返回的promise fn(Function): 表示Promise-returning/async function args: 表示传递给fn的参数 limit.activeCount 当前正在运行的promise数量 limit.pendingCount 等待运行的promise数量(即它们的内部fn尚未被调用)。 limit.clearQueue() 丢弃等待运行的pending promises。 2.2 p-limit 的实现 要想理解p-limit必须先掌握浏览器中event-loop的执行顺序: [代码]console.log('script start') async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } // 以上两个async函数可改写为以下代码 // new Promise((resolve, reject) => { // console.log('async2 end') // // Promise.resolve() 将代码插入微任务队列尾部 // // resolve 再次插入微任务队列尾部 // resolve(Promise.resolve()) // }).then(() => { // console.log('async1 end') // }) // 如果 await 后面跟着 Promise 的话,async1 end 需要等待三个 tick 才能执行到 async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end') // script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout [代码] 先介绍下宏任务和微任务具体有哪些内容, 微任务包括 [代码]process.nextTick[代码] ,[代码]promise[代码] ,[代码]MutationObserver[代码],其中 [代码]process.nextTick[代码] 为 Node 独有。 宏任务包括 [代码]script[代码] , [代码]setTimeout[代码] ,[代码]setInterval[代码] ,[代码]setImmediate[代码] ,[代码]I/O[代码] ,[代码]UI rendering[代码]。 Event Loop 执行顺序如下所示: 首先执行同步代码,这属于宏任务 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行 执行所有微任务 当执行完所有微任务后,如有必要会渲染页面 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 [代码]setTimeout[代码] 中的回调函数 [代码]function pLimit(concurrency) { if(!((Number.isInteger(concurrency)||concurrency===Number.POSITIVE_INFINITY)&&concurrency>0)) { throw new TypeError('Expected `concurrency` to be a number from 1 and up'); } // 用一个queue队列维护所有Promise异步函数 const queue=[]; let activeCount=0; const next=() => { // 某异步函数完成后,需要将activeCount-- activeCount--; if(queue.length>0) { // 再次从队列中出队并执行异步函数,activeCount维持在concurrency queue.shift()(); } }; const run=async (fn,resolve,args) => { // activeCount++; // 进一步将fn封装为异步函数并运行 const result=(async () => fn(...args))(); // 此处返回generator函数 的resolve值,即Promise.all resolve(result); try { // 等待result异步函数完成(例如某请求完成) await result; } catch {} next(); }; const enqueue=(fn,resolve,args) => { queue.push(run.bind(undefined,fn,resolve,args)); // setTimeout(()=>{ // // 正在运行的Promise数量activeCount始终不大于concurrency,从而达到控制并发的目的 // if(activeCount<concurrency&&queue.length>0) { // // 队列出队并执行改函数 // queue.shift()(); // } // },0); (async () => { // 这个函数需要等到下一个微任务再比较 `activeCount` 和 `concurrency` // 因为 `activeCount` 在 run 函数出列和调用时异步更新。 // if 语句中的比较也需要异步进行,以获取 `activeCount` 的最新值。 await Promise.resolve(); // 正在运行的Promise数量activeCount始终不大于concurrency,从而达到控制并发的目的 if (activeCount < concurrency && queue.length > 0) { // 队列出队并执行改函数 queue.shift()(); } })(); }; const generator = (fn,...args) => new Promise(resolve => { enqueue(fn,resolve,args); }); Object.defineProperties(generator,{ // 正在运行的Promise数量 activeCount: { get: () => activeCount, }, // 等待运行的Promise数量 pendingCount: { get: () => queue.length, }, // 清空queue队列中的异步函数 clearQueue: { value: () => { while(queue.length!=0) { pueue.shift(); } }, }, }); return generator; } [代码] 三、asyncPool并发控制的实现 async-pool 这个库提供了 ES7 和 ES6 两种不同版本的实现,在分析其具体实现之前,我们来看一下它如何使用。 3.1 asyncPool 的使用 [代码]function asyncPool(poolLimit, array, iteratorFn){ ... } [代码] 该函数接收 3 个参数: [代码]poolLimit[代码](Number):表示限制的并发数; [代码]array[代码](Array):表示任务数组; [代码]iteratorFn[代码](Function):表示迭代函数,用于实现对每个任务项进行处理,该函数会返回一个 Promise 对象或异步函数。 [代码]asyncPool[代码]在有限的并发池中运行多个promise-returning & async functions。一旦其中一个承诺被拒绝,它就会立即拒绝。当所有 Promise 完成时,它就会resolves。它尽快调用迭代器函数(在并发限制下)。例如: [代码]const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i)); await asyncPool(2, [1000, 5000, 3000, 2000], timeout); // Call iterator (i = 1000) // Call iterator (i = 5000) // Pool limit of 2 reached, wait for the quicker one to complete... // 1000 finishes // Call iterator (i = 3000) // Pool limit of 2 reached, wait for the quicker one to complete... // 3000 finishes // Call iterator (i = 2000) // Itaration is complete, wait until running ones complete... // 5000 finishes // 2000 finishes // Resolves, results are passed in given array order `[1000, 5000, 3000, 2000]`. [代码] 通过观察以上的注释信息,我们可以大致地了解 [代码]asyncPool[代码] 函数内部的控制流程 3.2 asyncPool 实现 [代码]async function asyncPool(poolLimit, array, iteratorFn) { const ret = []; // 存储所有的异步任务 const executing = []; // 存储正在执行的异步任务 for (const item of array) { // 调用iteratorFn函数创建异步任务 const p = Promise.resolve().then(() => iteratorFn(item, array)); ret.push(p); // 保存新的异步任务 // 当poolLimit值小于或等于总任务个数时,进行并发控制 if (poolLimit <= array.length) { // 当任务完成后,从正在执行的任务数组中移除已完成的任务 const e = p.then(() => executing.splice(executing.indexOf(e), 1)); executing.push(e); // 保存正在执行的异步任务 if (executing.length >= poolLimit) { await Promise.race(executing); // 等待较快的任务执行完成 } } } return Promise.all(ret); } [代码] 在以上代码中,充分利用了 [代码]Promise.all[代码] 和 [代码]Promise.race[代码] 函数特点,再结合 ES7 中提供的 [代码]async await[代码] 特性,最终实现了并发控制的功能。利用 [代码]await Promise.race(executing);[代码] 这行语句,我们会等待 正在执行任务列表 中较快的任务执行完成之后,才会继续执行下一次循环。 四、实践运用 使用p-limit [代码]const limit=pLimit(5); const fn=() => { return axios({ method: 'get', url: 'https://www.fastmock.site/mock/883c2b5177653e4ef705b7ffdc680af1/daily/story', }) .then(function(response) { return response.data; }); }; const input=[]; for(let i=0;i<50;i++) { input.push(limit(fn)) } Promise.all(input).then(res => { console.log('res',res) }); [代码] [图片] 使用asyncPool [代码]const fn=() => { return axios({ method: 'get', url: 'https://www.fastmock.site/mock/883c2b5177653e4ef705b7ffdc680af1/daily/story', }) .then(function(response) { return response.data; }); } const input=[]; for(let i=0;i<50;i++) { input.push(fn); } const timeout= f => new Promise(resolve => setTimeout(() => resolve(f()))); asyncPool(5,input,timeout).then(res => { console.log('res',res); }) [代码] [图片]
2021-12-09 - 教你怎么监听小程序的返回键
更新:2020年7月28日08:51:11 基础库2.12.0起,可以调用wx.enableAlertBeforeUnload监听原生右上角返回、物理返回以及wx.navigateBack时弹框提示 AIP详情请看: https://developers.weixin.qq.com/miniprogram/dev/api/ui/interaction/wx.enableAlertBeforeUnload.html //======================================== 怎么监听小程序的返回键? 应该有很多人想要监听用户的这个动作吧,但是很遗憾,小程序不会给你这个API的,那是不是就没辙了? 幸好我们还可以自定义导航栏,这样一来我们就可以监听用户的这一动作了。 什么?这你已经知道啦? 那好咱们就不说自定义导航栏的返回监听了,说一下物理返回和左滑?右滑?(不管了,反正是滑)返回上一页怎么监听。 监听物理返回 首先说一下这个监听方法的缺点,虽说是监听,但是还是无法真正意义上的监听并拦截来阻止页面跳转,页面还是会返回上一页,而后重新载入刚刚的页面,如果这不是你想要的,那可以不用往下看了 其次说一下用到什么东西: wx.onAppRoute、wx.showModal 最后是一些主要代码: 重写wx.showModal,主要是加个confirmStay参数和使wx.showModal Promise化 [代码]const { showModal } = wx; Object.defineProperty(wx, 'showModal', { configurable: false, // 是否可以配置 enumerable: false, // 是否可迭代 writable: false, // 是否可重写 value(...param) { return new Promise(function (rs, rj) { let { success, fail, complete, confirmStay } = param[0] param[0].success = (res) => { res.navBack = (res.confirm && !confirmStay) || (res.cancel && confirmStay) wx.setStorageSync('showBackModal', !res.navBack) success && success(res) rs(res) } param[0].fail = (res) => { fail && fail(res) rj(res) } param[0].complete = (res) => { complete && complete(res) (res.confirm || res.cancel) ? rs(res) : rj(res) } return showModal.apply(this, param); // 原样移交函数参数和this }.bind(this)) } }); [代码] 使用wx.onAppRoute实现返回原来的页面 [代码]wx.onAppRoute(function (res) { var a = getApp(), ps = getCurrentPages(), t = ps[ps.length - 1], b = a && a.globalData && a.globalData.pageBeforeBacks || {}, c = a && a.globalData && a.globalData.lastPage || {} if (res.openType == 'navigateBack') { var showBackModal = wx.getStorageSync('showBackModal') if (c.route && showBackModal && typeof b[c.route] == 'function') { wx.navigateTo({ url: '/' + c.route + '?useCache=1', }) b[c.route]().then(res => { if (res.navBack){ a.globalData.pageBeforeBacks = {} wx.navigateBack({ delta: 1 }) } }) } } else if (res.openType == 'navigateTo' || res.openType == 'redirectTo') { if (!a.hasOwnProperty('globalData')) a.globalData = {} if (!a.globalData.hasOwnProperty('lastPage')) a.globalData.lastPage = {} if (!a.globalData.hasOwnProperty('pageBeforeBacks')) a.globalData.pageBeforeBacks = {} if (ps.length >= 2 && t.onBeforeBack && typeof t.onBeforeBack == 'function') { let { onUnload } = t wx.setStorageSync('showBackModal', !0) t.onUnload = function () { a.globalData.lastPage = { route: t.route, data: t.data } onUnload() } } t.onBeforeBack && typeof t.onBeforeBack == 'function' && (a.globalData.pageBeforeBacks[t.route] = t.onBeforeBack) } }) [代码] 改造Page [代码]const myPage = Page Page = function(e){ let { onLoad, onShow, onUnload } = e e.onLoad = (() => { return function (res) { this.app = getApp() this.app.globalData = this.app.globalData || {} let reinit = () => { if (this.app.globalData.lastPage && this.app.globalData.lastPage.route == this.route) { this.app.globalData.lastPage.data && this.setData(this.app.globalData.lastPage.data) Object.assign(this, this.app.globalData.lastPage.syncProps || {}) } } this.useCache = res.useCache res.useCache ? reinit() : (onLoad && onLoad.call(this, res)) } })() e.onShow = (() => { return function (res) { !this.useCache && onShow && onShow.call(this, res) } })() e.onUnload = (() => { return function (res) { this.app.globalData = Object.assign(this.app.globalData || {}, { lastPage: this }) onUnload && onUnload.call(this, res) } })() return myPage.call(this, e) } [代码] 在需要监听的页面加个onBeforeBack方法,方法返回Promise化的wx.showModal [代码]onBeforeBack: function () { return wx.showModal({ title: '提示', content: '信息尚未保存,确定要返回吗?', confirmStay: !1 //结合content意思,点击确定按钮,是否留在原来页面,confirmStay默认false }) } [代码] 运行测试,Oj8K 是不是很简单,马上去试试水吧,效果图就不放了,静态图也看不出效果,动态图懒得弄,想看效果的自己运行代码片段吧 代码片段 https://developers.weixin.qq.com/s/hc2tyrmw79hg
2020-07-28 - 【教程】小程序呼出菜单的悬浮框封装
最终效果如图: [图片] [图片] 教程 通过设定一个圆形悬浮框在右下角后,绑定一个点击事件,决定菜单栏是呼出还是收拢,通过一个变量isShow来判断另外两个菜单要不要显示出来 [代码]WXML: <image src="/images/openIT.png" class="buttom" animation="{{animMain}}" bindtap="showOrHide"></image> JS: data: { isShow: false,//是否已经弹出 animMain: {},//旋转动画 animAdd: {},//item位移,透明度 animDelLots: {},//item位移,透明度 animEnd: {},//item位移,透明度, isLogin:false }, //点击弹出或者收起 showOrHide: function () { if (this.data.isShow) { //缩回动画 this.takeback(); this.setData({ isShow: false }) } else { //弹出动画 this.popp(); this.setData({ isShow: true }) } }, //弹出动画 popp: function () { //main按钮顺时针旋转 var animationMain = wx.createAnimation({ duration: 500, timingFunction: 'ease-out' }) var animationDelLots = wx.createAnimation({ duration: 500, timingFunction: 'ease-out' }) var animationAdd = wx.createAnimation({ duration: 500, timingFunction: 'ease-out' }) var animationEnd = wx.createAnimation({ duration: 500, timingFunction: 'ease-out' }) animationMain.rotateZ(180).step(); animationDelLots.translate(0, -240 / 750 * systemInfo.windowWidth).opacity(1).step(); animationAdd.translate(0, -360 / 750 * systemInfo.windowWidth).opacity(1).step(); animationEnd.translate(0, -120 / 750 * systemInfo.windowWidth).opacity(1).step(); this.setData({ animMain: animationMain.export(), animDelLots: animationDelLots.export(), animAdd: animationAdd.export(), animEnd: animationEnd.export(), }) }, //收回动画 takeback: function () { //main按钮逆时针旋转 var animationMain = wx.createAnimation({ duration: 500, timingFunction: 'ease-out' }) var animationDelLots = wx.createAnimation({ duration: 500, timingFunction: 'ease-out' }) var animationAdd = wx.createAnimation({ duration: 500, timingFunction: 'ease-out' }) var animationEnd = wx.createAnimation({ duration: 500, timingFunction: 'ease-out' }) animationMain.rotateZ(0).step(); animationDelLots.translate(0, 0).rotateZ(0).opacity(0).step(); animationAdd.translate(0, 0).rotateZ(0).opacity(0).step(); animationEnd.translate(0, 0).rotateZ(0).opacity(0).step(); this.setData({ animMain: animationMain.export(), animDelLots: animationDelLots.export(), animAdd: animationAdd.export(), animEnd: animationEnd.export(), }) }, [代码] 该JS代码中运用到了animation中的动画,具体使用方式可查看小程序文档进行进一步学习。 文档链接:https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/wx.createAnimation.html 剩余的WXML代码: [代码]<!--miniprogram/components/menu/menu.wxml--> <view class="drawer_screen" bindtap="showOrHide" wx:if="{{isShow}}" catchtouchmove="myCatchTouch"></view> <view > <view animation="{{animEnd}}" class="block"> <view wx:if="{{isShow}}" class="block-text" style="color:white;">队员招募</view> <image src="/images/findMe.png" class=" small" bindtap="findTeam"></image> </view> <view animation="{{animDelLots}}" class="block"> <view wx:if="{{isShow}}" class="block-text" style="color:white;">寻找团队</view> <image src="/images/findPer.png" class=" small" bindtap="findCaptain"></image> </view> <image src="/images/openIT.png" class="buttom" animation="{{animMain}}" bindtap="showOrHide"></image> </view> [代码]
2021-11-20 - [拆弹时刻]分包大作战,主包调用分包资源问题和若干报错处理
[图片] 由于一个项目主包要爆了,但是项目的地址二维码已经出去,不能修改,所以必须将一些JS库移到分包中,在这过程中踩了不少坑。 一些错误提示误导性很强,所以我将遇到的都一一罗列出来。 1、显示component组件引入错误,其实是配置问题 错误提示:VM7771 WAService.js:2 Component is not found in path “packageA/ec-canvas/ec-canvas” (using by “pages/smallTools/token”)(env: Windows,mp,1.05.2109262; lib: 2.14.1) [图片] 解决方法: [代码]"usingComponents": { "ec-canvas": "../../packageA/ec-canvas/ec-canvas", "mp-msg": "weui-miniprogram/msg/msg" }, "componentPlaceholder": { "ec-canvas":"view" } [代码] JSON配置中没有写:<code>componentPlaceholder</code>,这种常常出现在Components引用时候,因为调用异步,所以需要占位! 2、because they are in diffrent subPackages错误提示 这种错误,一般有两种可能性。 (1)引用路径错误,不支持import直接引用 Error: should not require …/…/packageA/ec-canvas/echarts in pages/smallTools/token.js without a callback, because they are in diffrent subPackages [图片] [代码]//比如: import * as echarts from '../../packageA/ec-canvas/echarts'; [代码] (2)基础库版本太低,比如2.17.3版本以上 VM10490 WAService.js:2 Please do not register multiple Pages in packageA/smallTools/token.js(env: Windows,mp,1.05.2109262; lib: 2.14.1) [图片] [代码]let echarts; require.async('../../packageA/ec-canvas/echarts.js').then(pkg => { echarts = pkg; console.log(pkg); }) [代码] 出现不支持文档中的 <code> require.async </code>,主要原因是基础库版本过低。 3、主包/分包A调用其他分包B的资源,必须写在function中 VM10490 WAService.js:2 Please do not register multiple Pages in packageA/smallTools/token.js(env: Windows,mp,1.05.2109262; lib: 2.14.1) [图片] 一般可能是引入方法没有写在function中,而直接头部引用 [代码]//错误位置 require('../subPackageB/utils.js', utils => { console.log(utils.whoami) // Wechat MiniProgram }) page({ onLoad(){ //正确位置 } }) [代码] 4、主包引用分包JS正确的写法(示例) 官方的几个方法都可以,但要注意一些细节,这里展示一个echart图表的实战demo。 [代码]//配置JSON: { "navigationBarBackgroundColor": "#1757c4", "navigationBarTextStyle": "white", "enablePullDownRefresh": true, "usingComponents": { "ec-canvas": "../../packageA/ec-canvas/ec-canvas", "mp-msg": "weui-miniprogram/msg/msg" }, //这个一定要写 "componentPlaceholder": { "ec-canvas":"view" } }; //JS: let echarts; page({ async onLoad(){ //注意:要写在方法里面 require('../../packageA/ec-canvas/echarts.js', pkg => { echarts = pkg; }); //同步方法 require.async('../commonPackage/index.js').then(pkg => { echarts = pkg; // 'common' }) } }) [代码] 单个JS的引用注意点比较多,其他component直接调用即可。 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic.html (和你一样,我用的时候,压根没一个个字仔细看哈~) 如有疑问请留言~ 觉得有用,请点个赞哦,让我继续分享更有动力~
2022-02-08 - 如何从零实现上拉无限加载瀑布流组件
代码已优化请查看另外一篇文章 https://developers.weixin.qq.com/community/develop/article/doc/00026c521ece40c2d2db97f7156013 小程序瀑布流组件 前言:为了实现这个组件也花费了些时间,以前也做过瀑布流的功能,不过是利用 js 去 计算图片的高度,然后通过 css 的绝对定位去改变位置。不过这种要提前加载完一个列 表的图片,然后通过排列的算法生成排序的数组。总之就是太复杂了,后来在网上也看到 纯 css 实现,比如 flex 两列布局,columns 等,不做过多的阐述,下面分享下自己项 目中实现的瀑布流过程。 Css Grid 布局 Css3 变量属性 Js 动态修改 css 变量属性 Wxs 小程序脚本语言 Wxml 节点 Api Component 自定义组件 效果图 代码片段 [图片] Css Grid 网格布局实现多列多行布局 [代码]<view class="c-waterfall"> <view wx:for="{{ 10 }}" wx:key="item" class="view-container" > {{ item }} </view> </view> [代码] [代码].c-waterfall { display: grid; grid-template-columns: repeat(2, 1fr); grid-auto-flow: row dense; grid-auto-rows: 10px; grid-gap: 10px; } .view-container { width: 100%; grid-row: auto / span 20; } [代码] Css3 变量,可以通过[代码]js动态[代码]改变 [代码].c-waterfall { --grid-span: 10; --grid-column: 2; --grid-gap: 10px; --grid-rows: 10px; width: 100%; display: grid; grid-template-columns: repeat(var(--grid-column), 1fr); grid-auto-flow: row dense; grid-auto-rows: var(--grid-rows); grid-gap: var(--grid-gap); } .view-container { width: 100%; grid-row: auto / span var(--grid-span); } [代码] 动态修改 css 变量,实现遍历的节点都有独立的样式 [代码]<view class="c-waterfall" style="{{ style }}"> <view wx:for="{{ 10 }}" wx:key="item" class="view-container style="grid-row: auto / span var(--grid-row-{{ index }})" > {{ item }} </view> </view> [代码] [代码]Page({ data: { span: 20, style: '' }, onReady() { this.setData({ style: '--grid-row-0: 10;--grid-row-1: 10;' // 0-9... }) } }) [代码] 显然通过这种方式去修改emmm,有点不尽人意,当view渲染的时候,通过[代码]index[代码]下标给每个view都设置独立的[代码]grid-row[代码]样式,然后在修改view父级的style,将[代码]--grid-row-xxx[代码]变量写进去实现子类继承,虽然比直接去修改每个view的样式要优雅些,但是一旦views的节点多了,100个、1000个、没上限呢,那这个父级的style真的惨不忍睹。。比如100个view,那么style将会是下面这样,所以需要换个思路还是得单独去设置view的样式。 [代码]const views = [...99].map((v, k) => `--grid-row-${k}: 10;`) console.log(views) // ["--grid-row-0: 10;", "--grid-row-1: 10;", ... "--grid-row-2: 10;", "--grid-row-3: 10;", "--grid-row-98: 10;", "--grid-row-99: 10;"] [代码] 通过Wxs脚本语言来修改view的样式,相比较通过[代码]setData[代码]去修改view的样式,wxs的性能绝对比js强。 WXS 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行。 WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。 WXS 的运行环境和其他 JavaScript 代码是隔离的,WXS 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的API。 WXS 函数不能作为组件的事件回调。 由于运行环境的差异,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备上二者运行效率无差异。 一般在对wxs的使用场景上大多数用来做[代码]computed[代码]计算,因为在[代码]wxml[代码]模板语法里只能进行简单的三元运算,所以一些复杂的运算、逻辑判断等都会放到wxs里面去处理,然后返回给wxml。 [代码]// index.wxs var format = function(string) { return string + 'px' } module.exports = { format: format } [代码] [代码]<!-- index.wxml --> <wxs src="./index.wxs" module="wxs"></wxs> <view>{{ wxs.format('100') }}</view> <view>{{ wxs.format(span) }}</view> <button bind:tap="modifySpan">修改span的值</button> [代码] [代码]// index.js page({ data: { span }, modifySpan() { this.setData({ span: '200' }) } }) [代码] 通过WXS响应事件来修改视图层[代码]Webview[代码],跳过逻辑层[代码]App Service[代码],减少性能开销,比如一些频繁响应的事件监听,滚动条位置,手指滑动位置等,通过wxs来做视图层的修改,大大提升了流畅度。 通过wxs响应原生组件的事件,[代码]image[代码]组件的[代码]bind:load[代码]事件 [代码]<!-- index.html --> <wxs src="./index.wxs" module="wxs"></wxs> <image class="image" src="https://hbimg.huabanimg.com/ccf4a904deaebc25990a47471c61ea1c765694f82633b-71iPZs_/fw/480/format/webp" bind:load="{{ wxs.loadImg }}" /> [代码] [代码]// index.wxs var loadImg = function(event, ownerInstance) { // image组件load加载完返回图片的信息 var image = event.detail // 获取image的实例 var imageDom = ownerInstance.selectComponent('.image') // 设置image的样式 imageDom.setStyle({ height: image.height + 'px', background: 'red' // ... }) // 给image添加class imageDom.addClass('.loaded') // 更多的功能请参考文档 // https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html } module.exports = { loadImg: loadImg } [代码] wxs监听data的值 [代码]<!-- index.html --> <wxs src="./index.wxs" module="wxs"></wxs> <view class="container"> <view change:text="{{ wxs.changeText }}" text="{{ text }}" class="text" data-options="{{ options }}" > {{ text }} </view> <view class="child-node"> this is childNode </view> <!-- 某个自定义组件 --> <test-component class="other-node" /> </view> [代码] [代码]// index.wxs var changeText = function(newValue, oldValue, ownerInstance, instance) { // 获取修改后的text var text = newValue // 获取data-options var options = instance.getDataset() // 获取当前页面的任意节点实例 var childNode = instance.selectComponent('.container .child-node') // 修改childNode样式 childNode.setStyle({ color: 'gree' }) // 获取页面的自定义组件 var otherNode = instance.selectComponent('.container .other-node') // 获取自定义组件内的节点实例 // 通过css选择器 > var otherChildNode = instance.selectComponent('.container .other-node >>> .other-child-node') // 获取自定义组件内部节点的样式 var style = otherChildNode.getComputedStyle(['width', 'height']) // 更多功能看文档 } module.exports = { changeText: changeText } [代码] 通过[代码]createSelectorQuery[代码]获取节点的信息,用来后续计算[代码]grid-row[代码]的参数 [代码]Page({ onReady() { wx.createSelectorQuery(this) .select('.view-container') .fields({size: true}) .exec((res) => { console.log(res) // [{width: 375, height: 390}] }) } }) [代码] 创建waterfall自定义组件 waterfall组件的职责,做成组件有什么好处,不做成组件又有什么好处,以及通过抽象节点来实现多组件复用。 prop的基本设置参数 [代码]Component({ properties: { views: Array, // 需要渲染的瀑布流视图列表 options: { // 瀑布流的参数定义 type: Object, default: { span: 20, // 节点高度比 column: 2, // 显示几列 gap: [10, 10], // xy轴边距,单位px rows: 2, // 网格的高度,单位px }, } } }) [代码] 组件内部默认的样式 [代码].c-waterfall { --grid-span: 10; --grid-column: 2; --grid-gap: 10px; --grid-rows: 10px; width: 100%; display: grid; grid-template-columns: repeat(var(--grid-column), 1fr); grid-auto-flow: row dense; grid-auto-rows: var(--grid-rows); grid-gap: var(--grid-gap); } .view-container { width: 100%; grid-row: auto / span var(--grid-span); } [代码] 组件的骨架 [代码]<wxs src="./index.wxs" module="wx" ></wxs> <!-- 样式承载节点 --> <view class="c-waterfall" change:loadStatus="{{ wx.load }}" loadStatus="{{ childNode }}" data-options="{{ options }}" style="{{ wx.setStyle(options) }}" > <!-- 抽象节点 --> <selectable class="view-container" id="view-{{ index }}" wx:for="{{ views }}" wx:key="item" value="{{ item }}" index="{{ index }}" bind:load="load" > </selectable> </view> [代码] 抽象节点 [代码]{ "component": true, "usingComponents": {}, "componentGenerics": { "selectable": true } } [代码] 抽象节点应该遵循什么 [代码]Component({ properties: { value: Object, // 组件自身需要的数据 index: Number, // 下标值 }, methods: { load(event) { // load节点响应事件 this.triggerEvent('load', { ...this.data, // value必填参数 {width,height} value: { ...event.detail }, }) }, }, }) [代码] 组件wxs响应事件 [代码].c-waterfall[代码]样式承载节点,主要是设置options传入的参数 [代码] var _getGap = function (gaps) { return gaps .map(function (v) { return v + 'px' }) .join(' ') } var setStyle = function (options) { if (!options) return var style = [ '--grid-span: ' + options.span || 10, '--grid-column: ' + options.column || 2, '--grid-gap: ' + _getGap(options.gap || [10, 10]), '--grid-rows: ' + (options.rows || 10) + 'px', ] return style.join(';') } [代码] 获取瀑布流样式承载节点实例 [代码] var _getWaterfall = function (dom) { var waterfallDom = dom.selectComponent('.c-waterfall') return { dom: waterfallDom, options: waterfallDom.getDataset().options, } } [代码] 获取事件触发的节点实例 [代码] var _getView = function (index, dom) { var viewDom = dom.selectComponent('.c-waterfall >>> #view-' + index) return { dom: viewDom, style: viewDom.getComputedStyle(['width', 'height']), } } [代码] 获取虚拟节点自定义组件load节点实例,初始化渲染时,节点是未知的,比如image组件,图片的宽高是未知的,需要等到image加载完成才会知道宽高,该节点用于存放异步视图展示,然后通过事件回调计算出节点高度。 [代码] var _getLoadView = function (index, dom) { return { dom: dom.selectComponent( '.c-waterfall >>> #view-' + index + '>>>.waterfall-load-node' ), } } [代码] 获取虚拟节点自定义组件other节点实例,初始化渲染就存在节点,比如一些文字就放在该节点,具体由组件的创造者去自定义。 [代码] var _getOtherView = function (index, dom) { var other = dom.selectComponent( '.c-waterfall >>> #view-' + index + '>>> .waterfall-load-other' ) return { dom: other, style: other.getComputedStyle(['height', 'width']), } } [代码] 已知瀑布流样式承载节点的宽度,等load节点异步视图回调时,获取到load节点的实际高度,比如一张400*800的图片,如果要显示在一个宽度180px的视图里,注意:[代码]image[代码]组件会有默认高度240px,或者用户自己设置了高度。如果要实现瀑布流,还是需要通过计算图片的宽高比例得到图片在视图中宽高,然后再通过计算grid布局的span值实现填充。 [代码] var fix = function (string) { if (typeof string === 'number') return string return Number(string.replace('px', '')) } var computedContainerHeight = function (node, view) { var vW = fix(view.width) var nW = fix(node.width) var nH = fix(node.height) var scale = nW / vW return { width: vW, height: nH / scale, } } [代码] 通过公式计算span的值,这个公式也是花了我不少时间去研究的,对grid布局使用也不多,很多潜在用法并不知道,所以通过大量的随机数据对比查找规律所在。[代码]gap为数组[x, y][代码],我们要取y计算,已知gap、rows求视图中节点高度[代码](gap[y] + rows) * span - gap[y] = height[代码],有了求height的公式,那么求span就简单了,[代码](height + gap[y]) / (gap[y] + rows) = span[代码],最终视图里的高度会跟计算出来的结果几个像素的误差,因为[代码]grid-row[代码]设置span不能为小数,只能为整数,而我们瀑布流的高度是未知的,通过计算有多位浮点数,所以只能向上取整了导致有几个像素的误差。 [代码] var computedSpan = function (height, options) { var rows = options.rows var gap = options.gap[1] var span = Math.ceil((height + gap) / (gap + rows)) return span } [代码] 最后我们能得到[代码]span[代码]的值了,只需要将[代码]load完成的视图修改样式即可[代码] [代码] var load = function (node, oldNode, dom) { if (!node.value) return false var index = node.index var waterfall = _getWaterfall(dom) // 获取虚拟组件,通过index下标确认是哪个,获取宽度高度 var view = _getView(index, dom) var otherView = _getOtherView(index, dom) var otherViewHeight = fix(otherView.style.height) // 计算虚拟组件的高度,其实就是计算图片在当前视图节点里的宽高比例 // image组件的mode="widthFix"也是这样计算的额 var virtualStyle = computedContainerHeight(node.value, view.style) // span取值,此处计算的高度应该是整个虚拟节点视图的高度 // load事件回调里,我们只传了load视图节点的宽高 // 后续通过selectComponent获取到了other视图节点的高度 var span = computedSpan( otherViewHeight + virtualStyle.height, waterfall.options ) // 设置虚拟组件的样式 view.dom.setStyle({ 'grid-row': 'auto / span ' + span, }) // 获取重新渲染后的虚拟组件高度 var viewHeight = view.dom.getComputedStyle(['width', 'height']) viewHeight = fix(viewHeight.height) // 上面说了因为浮点数的计算会导致有几个像素的误差 // 为了视图美观,我们将load视图节点的高度设置成虚拟视图节点的总高度减去静态节点的高度 var loadView = _getLoadView(index, dom) loadView.dom.setStyle({ width: virtualStyle.width + 'px', height: parseInt(viewHeight - otherViewHeight) + 'px', opacity: 1, visibility: 'visible', }) return false } module.exports = { load: load, setStyle: setStyle, } [代码] 抽离成虚拟节点自定义组件的利弊 利: 符合观察者模式的设计模式 降低代码耦合度 扩展性强 代码清晰 弊: 节点增加,如果视图节点过多会造成小程序性能警告 样式编写不便捷,需要写过多的判断代码去实现外部样式覆盖 wxs只能监听原生组件的事件,所以image的load事件触发时本可以直接去修改页面视图节点样式,不需要传回给父组件,然后父组件setData下标,wxs监听事件触发在去修改视图样式,多了一次setData的开销。 合: 时间有限没有扩展样式覆盖了,可以开启自定义组件的外部样式引入 节点过多的问题,在我自己电脑上,开发工具插入100个组件时,出现了卡顿,样式错乱,真机上目前还没发现上限。 后续想实现长列表功能,有回收机制,这样视图内的节点有限了,降低了性能开销,因为之前版本的长列表组件是通过[代码]createSelectorQuery[代码]获取节点信息,然后记录高度,通过创建[代码]createIntersectionObserver[代码]监听视图节点是否在视图来判断是否渲染。但是瀑布流有异步视图,初次渲染的高度跟异步加载完的高度是不一样,所以创建监听事件高度会不准确,若等到load完再创建监听事件,父级容器的高度又要经过计算,因为子节点会去填充空白区域实现瀑布流,目前项目中为了避免节点过大造成性能警告,加了item的个数限制,如果超过100或者1000个就清空数组,类似分页的功能。不过上面总结的思路可以去试试。 等把功能完善了,发布npm依赖包安装。 后续有时间会将项目里比较实用的组件抽离出来。。 自定义tabbar 自定义navbar 长列表 下拉刷新 上拉加载 购物车sku … Demo page调用页面 [代码]<view class="container"> <waterfall wx:if="{{ _type === 0 }}" generic:selectable="test-view" views="{{ views }}" options="{{ options }}" /> <waterfall wx:else generic:selectable="image-view" views="{{ images }}" options="{{ options }}" /> </view> <view class="btns"> <button bind:tap="loadView">模拟节点</button> <button bind:tap="loadImage">远程图片</button> </view> [代码] [代码]Page({ data: { views: [], loading: false, options: { span: 30, column: 2, gap: [10, 10], rows: 2, }, images: [], _page: 1, _type: 0, }, onLoad() { // 生成随机数据 // this.generateViews() // this.getHuaBanList() }, loadView() { this.data._page = 1 this.setData({ images: [], _type: 0 }) this.generateViews() }, loadImage() { this.data._type = 1 this.setData({ views: [], _type: 1 }) this.getHuaBanList() }, getHuaBanList() { let { images, _page } = this.data wx.request({ url: `https://huaban.com/search/?q=随机&page=${_page}&per_page=10&wfl=1`, header: { accept: 'application/json', 'accept-language': 'zh-CN,zh;q=0.9', 'x-request': 'JSON', 'x-requested-with': 'XMLHttpRequest', }, success: (res) => { res.data.pins.map((v) => { images.push({ url: `https://hbimg.huabanimg.com/${v.file.key}_/fw/480/format/webp`, title: v.raw_text, }) }) this.setData({ images, _page: ++_page }) wx.hideLoading() }, }) }, generateViews() { const { views } = this.data for (let i = 0; i < 10; i++) { views.push({ width: this._randomNum(150, 500) + 'px', height: this._randomNum(200, 600) + 'px', }) } this.setData({ views, }) }, _randomNum(minNum, maxNum) { switch (arguments.length) { case 1: return parseInt(String(Math.random() * minNum + 1), 10) break case 2: return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10) break default: return 0 break } }, onReachBottom() { let { loading, _type } = this.data if (!loading) { wx.showLoading({ title: 'loading...', }) loading = true setTimeout(() => { _type === 0 ? this.generateViews() : this.getHuaBanList() wx.hideLoading() loading = false }, 1000) } }, }) [代码] [代码]{ "usingComponents": { "waterfall": "/components/waterfall/index", "test-view": "/components/test-view/index", "image-view": "/components/image-view/index" } } [代码] 模拟load异步的自定义组件 [代码]<view class="c-test-view"> <view class="waterfall-load-node"> {{value.width}}*{{value.height}} </view> <view class="waterfall-load-other">模拟加载图片</view> </view> [代码] [代码]Component({ properties: { value: Object, index: Number, }, lifetimes: { ready() { const { index } = this.data const timer = 1000 + 300 * String(index).charAt(index.length - 1) setTimeout(() => this.load(), timer) }, }, methods: { load() { this.triggerEvent('load', { ...this.data, }) }, }, }) [代码] [代码].c-test-view { width: 100%; height: 100%; display: flex; flex-flow: column; justify-content: center; align-items: center; background: white; } .c-test-view .waterfall-load-node { height: 50%; flex-grow: 1; transition: all 0.3s; display: inline-flex; flex-flow: column; justify-content: center; align-items: center; background: #eeeeee; width: 100%; opacity: 0; } .c-test-view .waterfall-load-other { width: 100%; height: 80rpx; display: inline-flex; justify-content: center; align-items: center; background: cornflowerblue; color: white; } [代码] 随机获取花瓣网图片的自定义组件 [代码]<view class="c-image-view"> <view class="waterfall-load-node"> <image class="load-image" src="{{ value.url }}" bind:load="load" /> </view> <view class="waterfall-load-other">{{ value.title }}</view> </view> [代码] [代码]Component({ properties: { value: Object, index: Number, }, lifetimes: { ready() {}, }, methods: { load(event) { this.triggerEvent('load', { ...this.data, value: { ...event.detail }, }) }, }, }) [代码] [代码].c-image-view { width: 100%; display: inline-flex; flex-flow: column; background: white; border-radius: 10px; overflow: hidden; height: 100%; } .c-image-view .waterfall-load-node { width: 100%; height: 50%; display: inline-flex; flex-grow: 1; background: gainsboro; transition: opacity 0.3s; opacity: 0; overflow: hidden; visibility: hidden; } .c-image-view .waterfall-load-node .load-image { width: 100%; height: 100%; overflow: hidden; } .c-image-view .waterfall-load-other { font-size: 30rpx; background: white; min-height: 60rpx; padding: 10px; display: flex; align-items: center; } [代码] 代码片段 https://developers.weixin.qq.com/s/Q02FETmW7ind
2021-03-19 - 小程序使用Grid和css变量实现瀑布流布局
前言 要实现如下瀑布流效果,动态图片,动态高度 [图片] 我知道使用JS能够实现完美瀑布流,但小程序不比web,坑点会比较多,因此我决定先使用CSS看能不能解决,最后实在不行在使用JS来实现 根据网上的教程尝试使用css的方式(column和flex)实现效果,但排列顺序都是竖排而不是横排,不符合产品需求,实现效果如下 [图片] GRID瀑布流 如此看来只剩grid这一条路了,还好成功了 基础版 下列摘自网上使用GRID实现瀑布流的实例 模板 [代码]
1/view> 2/view> 3/view> 4/view> 5/view> 6/view> ... /view> [代码] wxss [代码].waterfall { display: grid; grid-template-columns: repeat(2, 1fr); // 指定两列,自动宽度 grid-gap: 0.25em; // 横向,纵向间隔 grid-auto-flow: row dense; // 是否自动补齐空白 grid-auto-rows: 20px; // base高度,grid-row基于此运算 } .waterfall .item { width: 100%; background: #222; color: #ddd; } .waterfall .item:nth-of-type(3n+1) { grid-row: auto / span 5; } .waterfall .item:nth-of-type(3n+2) { grid-row: auto / span 6; } .waterfall .item:nth-of-type(3n+3) { grid-row: auto / span 8; } [代码] 效果 [图片] 基础版的问题 看看上面的css是如何使用grid实现 [代码].waterfall .item:nth-of-type(3n+1) { grid-row: auto / span 5; } [代码] 上述代码指定[代码]1,4,7,10...[代码]等item的高度,[代码]auto[代码]为grid自动设置该item的起始位置,[代码]span 5[代码]则指定该item的高度为[代码]grid-auto-rows * 5[代码], [代码]grid-auto-rows[代码]在CSS的设定中为20px,在源码中我做了说明,它是一个基础高度 [代码].waterfall .item:nth-of-type(3n+2) { grid-row: auto / span 6; } [代码] 上述代码指定[代码]2,8,11,14...[代码]等item的高度,[代码]auto[代码]为grid自动设置该item的起始位置,[代码]span 6[代码]则指定该item的高度为[代码]grid-auto-rows * 6[代码] [代码].waterfall .item:nth-of-type(3n+3) { grid-row: auto / span 8; } [代码] 上述代码指定[代码]3,6,12,15...[代码]等item的高度,[代码]auto[代码]为grid自动设置该item的起始位置,[代码]span 8[代码]则指定该item的高度为[代码]grid-auto-rows * 8[代码] 基础版虽然看上去基本符合我们的产品需求,但由css可以知道,它的问题是__高度固定__,但业务上我们并不确切知道每个item的高度及所包含的图片的高度。所以接下来我们要解决__动态高度__设定的问题,让每一个item都自动计算自己的高度,而不是通过CSS来手动指定 css变量登场 微信小程序swiper的自适应高度 小程序中使用css var变量,使js可以动态设置css样式属性 上面两篇文章是之前写得关于css变量的一些巧妙的用法,css变量确实能够解决很多之前很棘手的问题,此时我脑海里面迸发出了一个绝佳的IDEA 仔细观察这段css [代码].waterfall .item:nth-of-type(3n+2) { grid-row: auto / span 6; } [代码] 唯一不确定的就是[代码]6[代码],对,它应该是一个变量而不是一个恒量,它应该是一个与高度关联的比值,而我们可以通过css变量动态设置这个比值,它大概应该长这样 [代码]page{ --item-span: x // 需要使用setData设置x值 } .waterfall .item { grid-row: var(--item-span); } [代码] 考虑到需要设置每个item的高度,应该为每一个item设定独立的样式 [代码].waterfall{ --item-span-1: x; // 使用setData设置x值 --item-span-2: y // 使用setData设置y值 } .waterfall .item-1 { grid-row: var(--item-span-1); } .waterfall .item-2 { grid-row: var(--item-span-2); } [代码] 原理 到此我们就可以讲通如何实现的原理了 注意:下面的例子使用内联样式代替上面的样式设定,因为内联样式可以由JS动态输出 模板 [代码] .../view> /view> .../view> /view> /page> [代码] Page [代码]Page({ data: { waterStyle: '', items: [...] }, onReady(){ let query = wx.createSelectorQuery().in(this) query.selectAll('.waterfall .item').boundingClientRect(ret=>{ let styleStr = ''; ret.forEach((ele, ii) => { let height = ele.height let span = parseInt(height/ 20) // 20 = grid-auto-row styleStr += `--item-span-${ii}: auto / span ${span};` }) this.setData({ waterStyle: styleStr }) }) } }) // 注释一 // 所有item的css变量合集 waterStyle /* xn在onReady方法中计算得出 --item-span-1: auto/span x1; --item-span-2: auto/span x2; --item-span-3: auto/span x3; ... */ [代码] 使用内联样式而不是.item-n [代码][代码]item元素使用内联样式,因为我们不确定item的数量。 高度计算 通过query我们可以获取所有item子元素的rect属性(长宽高…),计算height属性与grid-auto-row的比值,即我们需要的设定值 waterStyle 参考上述代码的注释一(动态计算每一个item的span比值),通过setData方法设置生效(设置在父级[代码]view.waterfall[代码]上),如此grid会自动设置每一个item子元素的位置 优化 以上基本将如何使用grid实现瀑布流的原理阐述了一遍,实际代码中需要注意[代码]grid-auto-row[代码]值的设定,在我们的项目中省略了此项css属性的设置,即span比值实际是由[代码]height/grid-gap[代码]得出,反而效果更好,具体原因我也一脸懵逼,如果有知道的留言告诉我 [图片] 2020-08-15 - 我们小程序需要做一个按期从微信自动扣款功能,用户在同意自动扣款协议后,每月定期从微信里面扣钱,能吗?
求大佬回复
2021-09-10 - 企业微信的第三方应用如何调试?
使用微信开发者工具调试,提示:未绑定企业号开发者。 企业微信管理后台——我的企业——微信插件——开发者工具 也已经勾选了。 但是还是调试不了,这是为啥呢? 有什么解决办法么?
2020-05-25 - 小程序在web-view中跳转小程序,提示“当前小程序无法打开***小程序”
每个小程序可跳转的其他小程序数量限制为不超过10个,在web-view中打开的小程序也包含在这个限制中。 从 基础库2.4.0 版本以及指定日期(参考公告)开始,开发者提交新版小程序代码时,如使用了跳转其他小程序功能,则需要在代码配置中声明将要跳转的小程序名单,限定不超过 10 个,否则将无法通过审核。该名单可在发布新版时更新,不支持动态修改。配置方法详见 小程序全局配置。
2019-11-11 - 小程序调试新方案——使用WeConsole监控console/network/api/component/storage
[图片] 一、背景与简介 在传统的 PC Web 前端开发中,浏览器为开发者提供了体验良好、功能丰富且强大的开发调试工具,比如常见的 Chrome devtools 等,这些调试工具极大的方便了开发者,它们普遍提供查看页面结构、监听网络请求、管理本地数据存储、debugger 代码、使用 Console 快速显示数据等功能。 但是在近几年兴起的微信小程序的前端开发中,却少有类似的体验和功能对标的开发调试工具出现。当然微信小程序的官方也提供了类似的工具,那就是 vConsole,但是相比 PC 端提供的工具来说确实无论是功能和体验都有所欠缺,所以我们开发了 weconsole 来提供更加全面的功能和更好的体验。 基于上述背景,我们想开发一款运行在微信小程序环境上,无论在用户体验还是功能等方面都能媲美 PC 端的前端开发调试工具,当然某些(如 debugger 代码等)受限于技术在当前时期无法实现的功能我们暂且忽略。 我们将这款工具命名为[代码]Weimob Console[代码],简写为[代码]WeConsole[代码]。 项目主页:https://github.com/weimobGroup/WeConsole 二、安装与使用 1、通过 npm 安装 [代码]npm i weconsole -S [代码] 2、普通方式安装 可将 npm 包下载到本地,然后将其中的[代码]dist/full[代码]文件夹拷贝至项目目录中; 3、引用 WeConsole 分为[代码]核心[代码]和[代码]组件[代码]两部分,使用时需要全部引用后方可使用,[代码]核心[代码]负责重写系统变量或方法,以达到全局监控的目的;[代码]组件[代码]负责将监控的数据显示出来。 在[代码]app.js[代码]文件中引用[代码]核心[代码]: [代码]// NPM方式引用 import 'weconsole/init'; // 普通方式引用 import 'xxx/weconsole/init'; [代码] 引入[代码]weconsole/init[代码]后,就是默认将 App、Page、Component、Api、Console 全部重写监控!如果想按需重写,可以使用如下方式进行: [代码]import { replace, restore, showWeConsole, hideWeConsole } from 'weconsole'; // scope可选值:App/Page/Component/Console/Api // 按需替换系统变量或函数以达到监控 replace(scope); // 可还原 restore(scope); // 通过show/hide方法控制显示入口图标 showWeConsole(); [代码] 如果没有显式调用过[代码]showWeConsole/hideWeConsole[代码]方法,组件第一次初始化时,会根据小程序是否[代码]开启调试模式[代码]来决定入口图标的显示性。 在需要的地方引用[代码]组件[代码],需要先将组件注册进[代码]app/page/component.json[代码]中: [代码]// NPM方式引用 "usingComponents": { "weconsole": "weconsole/components/main/index" } // 普通方式引用 "usingComponents": { "weconsole": "xxx/weconsole/components/main/index" } [代码] 然后在[代码]wxml[代码]中使用[代码]<weconsole>[代码]标签进行初始化: [代码]<!-- page/component.wxml --> <weconsole /> [代码] [代码]<weconsole>[代码]标签支持传入以下属性: [代码]properties: { // 组件全屏化后,距离窗口顶部距离 fullTop: String, // 刘海屏机型(如iphone12等)下组件全屏化后,距离窗口顶部距离 adapFullTop: String, } [代码] 4、建议 如果不想将 weconsole 放置在主包中,建议将组件放在分包内使用,利用小程序的 分包异步化 的特性,减少主包大小 三、功能 1、Console 界面如图 1 实时显示[代码]console.log/info/warn/error[代码]记录; [代码]Filter[代码]框输入关键字已进行记录筛选; 使用分类标签[代码]All, Mark, Log, Errors, Warnings...[代码]等进行记录分类显示,分类列表中[代码]All, Mark, Log, Errors, Warnings[代码]为固定项,其他可由配置项[代码]consoleCategoryGetter[代码]产生 点击[代码]🚫[代码]按钮清空记录(不会清除[代码]留存[代码]的记录) [代码]长按[代码]记录可弹出操作项(如图 2): [代码]复制[代码]:将记录数据执行复制操作,具体形式可使用配置项[代码]copyPolicy[代码]指定,未指定时,将使用[代码]JSON.stringify[代码]序列化数据,将其复制到剪切板 [代码]取消置顶/置顶显示[代码]:将记录取消置顶/置顶显示,最多可置顶三条(置顶无非是想快速找到重要的数据,当重要的数据过多时,就不宜用置顶了,可以使用[代码]标记[代码]功能,然后在使用筛选栏中的[代码]Mark[代码]分类进行筛选显示) [代码]取消留存/留存[代码]:留存是指将记录保留下来,使其不受清除,即点击[代码]🚫[代码]按钮不被清除 [代码]取消全部留存[代码]:取消所有留存的记录 [代码]取消标记/标记[代码]:标记就是将数据添加一个[代码]Mark[代码]的分类,可以通过筛选栏快速分类显示 [代码]取消全部标记[代码]:取消所有标记的记录 [图片] 图 1 [图片] 图 2 2、Api 界面如图 3 实时显示[代码]wx[代码]对象下的相关 api 执行记录 [代码]Filter[代码]框输入关键字已进行记录筛选 使用分类标签[代码]All, Mark, Cloud, xhr...[代码]等进行记录分类显示,分类列表由配置项[代码]apiCategoryList[代码]与[代码]apiCategoryGetter[代码]产生 点击[代码]🚫[代码]按钮清空记录(不会清除[代码]留存[代码]的记录) [代码]长按[代码]记录可弹出操作项(如图 4): [代码]复制[代码]:将记录数据执行复制操作,具体形式可使用配置项[代码]copyPolicy[代码]置顶,未指定时,将使用系统默认方式序列化数据(具体看实际效果),将其复制到剪切板 其他操作项含义与[代码]Console[代码]功能类似 点击条目可展示详情,如图 5 [图片] 图 3 [图片] 图 4 [图片] 图 5 3、Component 界面如图 6 树结构显示组件实例列表 根是[代码]App[代码] 二级固定为[代码]getCurrentPages[代码]返回的页面实例 三级及更深通过[代码]this.selectOwnerComponent()[代码]进行父实例定位,进而确定层级 点击节点名称(带有下划虚线),可显示组件实例详情,以 JSON 树的方式查看组件的所有数据,如图 7 [图片] 图 6 [图片] 图 7 4、Storage 界面如图 8 显示 Storage 记录 [代码]Filter[代码]框输入关键字已进行记录筛选 点击[代码]🚫[代码]按钮清空记录(不会清除[代码]留存[代码]的记录) [代码]长按[代码]操作项含义与[代码]Console[代码]功能类似 点击条目后,再点击[代码]❌[代码]按钮可将其删除 点击[代码]Filter[代码]框左侧的[代码]刷新[代码]按钮可刷新全部数据 点击条目显示详情,如图 9 [图片] 图 8 [图片] 图 9 5、其他 界面如图 10 默认显示 系统信息 可通过[代码]customActions[代码]配置项进行界面功能快速定制,也可通过[代码]addCustomAction/removeCustomAction[代码]添加/删除定制项目 几个简单的定制案例如下,效果如图 11: [代码]import { setUIRunConfig } from 'xxx/weconsole/index.js'; setUIRunConfig({ customActions: [ { id: 'test1', title: '显示文本', autoCase: 'show', cases: [ { id: 'show', button: '查看', showMode: WcCustomActionShowMode.text, handler(): string { return '测试文本'; } }, { id: 'show2', button: '查看2', showMode: WcCustomActionShowMode.text, handler(): string { return '测试文本2'; } } ] }, { id: 'test2', title: '显示JSON', autoCase: 'show', cases: [ { id: 'show', button: '查看', showMode: WcCustomActionShowMode.json, handler() { return wx; } } ] }, { id: 'test3', title: '显示表格', autoCase: 'show', cases: [ { id: 'show', button: '查看', showMode: WcCustomActionShowMode.grid, handler(): WcCustomActionGrid { return { cols: [ { title: 'Id', field: 'id', width: 30 }, { title: 'Name', field: 'name', width: 70 } ], data: [ { id: 1, name: 'Tom' }, { id: 2, name: 'Alice' } ] }; } } ] } ] }); [代码] [图片] 图 10 [图片] 图 10 四、API 通过以下方式使用 API [代码]import { showWeConsole, ... } from 'weconsole'; showWeConsole(); [代码] replace(scope:‘App’|‘Page’|‘Component’|‘Api’|‘Console’) 替换系统变量或函数以达到监控,底层控制全局仅替换一次 restore(scope:‘App’|‘Page’|‘Component’|‘Api’|‘Console’) 还原被替换的系统变量或函数,还原后界面将不在显示相关数据 showWeConsole() 显示[代码]WeConsole[代码]入口图标 hideWeConsole() 隐藏[代码]WeConsole[代码]入口图标 setUIConfig(config: Partial<MpUIConfig>) 设置[代码]WeConsole[代码]组件内的相关配置,可接受的配置项及含义如下: [代码]interface MpUIConfig { /**监控小程序API数据后,使用该选项进行该数据的分类值计算,计算后的结果显示在界面上 */ apiCategoryGetter?: MpProductCategoryMap | MpProductCategoryGetter; /**监控Console数据后,使用该选项进行该数据的分类值计算,计算后的结果显示在界面上 */ consoleCategoryGetter?: MpProductCategoryMap | MpProductCategoryGetter; /**API选项卡下显示的数据分类列表,all、mark、other 分类固定存在 */ apiCategoryList?: Array<string | MpNameValue<string>>; /**复制策略,传入复制数据,可通过数据的type字段判断数据哪种类型,比如api/console */ copyPolicy?: MpProductCopyPolicy; /**定制化列表 */ customActions?: WcCustomAction[]; } /**取数据的category字段值对应的prop */ interface MpProductCategoryMap { [prop: string]: string | MpProductCategoryGetter; } interface MpProductCategoryGetter { (product: Partial<MpProduct>): string | string[]; } interface MpProductCopyPolicy { (product: Partial<MpProduct>); } /**定制化 */ interface WcCustomAction { /**标识,需要保持唯一 */ id: string; /**标题 */ title: string; /**默认执行哪个case? */ autoCase?: string; /**该定制化有哪些情况 */ cases: WcCustomActionCase[]; } const enum WcCustomActionShowMode { /**显示JSON树 */ json = 'json', /**显示数据表格 */ grid = 'grid', /** 固定显示<weconsole-customer>组件,该组件需要在app.json中注册,同时需要支持传入data属性,属性值就是case handler执行后的结果 */ component = 'component', /**显示一段文本 */ text = 'text', /**什么都不做 */ none = 'none' } interface WcCustomActionCase { id: string; /**按钮文案 */ button?: string; /**执行逻辑 */ handler: Function; /**显示方式 */ showMode?: WcCustomActionShowMode; } interface WcCustomActionGrid { cols: DataGridCol[]; data: any; } [代码] addCustomAction(action: WcCustomAction) 添加一个定制化项目;当你添加的项目中需要显示你自己的组件时: 请将 case 的[代码]showMode[代码]值设置为[代码]component[代码] 在[代码]app.json[代码]中注册名称为[代码]weconsole-customer[代码]的组件 定制化项目的 case 被执行时,会将执行结果传递给[代码]weconsole-customer[代码]的[代码]data[代码]属性 开发者根据[代码]data[代码]属性中的数据自行判断内部显示逻辑 removeCustomAction(actionId: string) 根据 ID 删除一个定制化项目 getWcControlMpViewInstances():any[] 获取小程序内 weconsole 已经监控到的所有的 App/Page/Component 实例 log(type = “log”, …args) 因为 console 被重写,当你想使用最原始的 console 方法时,可以通过该方式,type 就是 console 的方法名 on/once/off/emit 提供一个事件总线功能,全局事件及相关函数定义如下: [代码]const enum WeConsoleEvents { /**UIConfig对象发生变化时 */ WcUIConfigChange = 'WcUIConfigChange', /**入口图标显示性发生变化时 */ WcVisableChange = 'WcVisableChange', /**CanvasContext准备好时,CanvasContext用于JSON树组件的界面文字宽度计算 */ WcCanvasContextReady = 'WcCanvasContextReady', /**CanvasContext销毁时 */ WcCanvasContextDestory = 'WcCanvasContextDestory', /**主组件的宽高发生变化时 */ WcMainComponentSizeChange = 'WcMainComponentSizeChange' } interface IEventEmitter<T = any> { on(type: string, handler: EventHandler<T>); once(type: string, handler: EventHandler<T>); off(type: string, handler?: EventHandler<T>); emit(type: string, data?: T); } [代码] 五、后续规划 优化包大小 单元测试 体验优化 定制化升级 基于网络通信的界面化 weconsole 标准化 支持 H5 支持其他小程序平台(支付宝/百度/字节跳动) 六、License WeConsole 使用 MIT 协议. 七、声明 生产环境请谨慎使用。
2021-07-14 - 云开发静态网站托管的网页H5跳转小程序页面鉴权问题
文档地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/staticstorage/jump-miniprogram.html [图片] 按照文档描述部署在静态网站托管会自动鉴权,但是只有默认域名有鉴权,如果在静态网站绑定自定义域名则无法自动鉴权,导致无法在微信内置浏览器内显示开放标签进行跳转小程序。 后台已经配置自定义域名 [图片] 正常链接:https://dev-21758e78331b20e4-1304618850.tcloudbaseapp.com/jump-mp.html?openlink=A52E6D29F28B85C2A01A63C045679CA4 [图片] 异常链接:https://tcb.zhiqubuluo.com/jump-mp.html?openlink=A52E6D29F28B85C2A01A63C045679CA4 [图片]
2021-01-10 - 微信小程序新能力:URL Scheme,可从短信跳转小程序
最近小程序上线了一个超级流量的新入口:URL Scheme。通过小程序页面的URL Scheme,可以在短信、邮件或微信外部的网页中打开小程序。 那么如何实现呢?官方文档已经写的很清楚啦,这里简单介绍一下。 首先,获取URL Scheme,通过服务端接口可以获取打开小程序任意页面的URL Scheme,支持生成到期失效和永久有效的URL Scheme。 [图片] 然后,通过短信群发平台将获取的URL Scheme + 营销文案发送到用户的手机上。 最后,用户收到短信后,直接点击URL Scheme唤起微信,跳转到对应小程序页面,就是这么简单。 除此之外,还可以通过邮件或外部浏览器打开跳转小程序。 由于部分操作系统仍不支持直接识别URL Scheme,因此直接将Scheme发送给用户可能存在无法打开小程序的情况。 为此,我们可以先准备一个H5页面,再从H5页面跳转到URL Scheme实现打开小程序。 [代码]location.href = 'weixin://dl/business/?ticket= *TICKET*' [代码] H5的示例代码我已经更新到Github,可以复用起来,基于官方的案例做了些改动,增加PC端打开时生成二维码方便手机扫码使用。 这次新能力的更新将使微信小程序不再局限于微信内部的流量,天花板被掀开啦。 而且短信和邮件营销的触达成本非常低,营销成本的压低也会催生出很多新的流量玩法,我们敬请期待吧。
2021-01-08 - 小程序自定义TabBar后如何实现keep-alive
小程序自定义TabBar后如何实现keep-alive 前段时间写了小程序实现TabBar创意动画和小程序开发技巧后,有小伙伴提问到,自定义[代码]TabBar[代码]是可以做很多交互,但点击切换[代码]TabBar[代码]页面,都会伴随着组件的销毁和重建,这点确实会影响性能。这里就提供一个方案来实现“[代码]keep-alive[代码]”。如有更好的方案,欢迎评论区交流。欢迎点赞和收藏~ 自定义TabBar方案 虽然在之前文章提到过了,本次采用[代码]组件化实现[代码] 我们可以新建一个[代码]home[代码]文件夹,在[代码]home/index.wxml[代码]中写一个tabBar,然后把[代码]TabBar[代码]页面写成组件,然后点击TabBar切换相应的组件展示就可以。代码如下: wxml部分 [代码]<!-- home页面 --> <view id='index'> <!-- 自定义头部 --> <head name='{{name}}' bgshow="{{bgshow}}" backShow='false'></head> <!-- 首页 --> <index change='{{activeIndex==0}}'></index> <!-- 购物车 --> <cart change='{{activeIndex==1}}'></cart> <!-- 订单 --> <order change='{{activeIndex==2}}'></order> <!-- 我的 --> <my change='{{activeIndex==2}}'></my> <!-- tabbar --> <view class="tab ios"> <view class="items {{activeIndex==index?'active':''}}" wx:for="{{tab}}" bindtap="choose" data-index='{{index}}' wx:key='index' wx:for-item="items"> <image wx:if="{{activeIndex==index}}" src="{{items.activeImage}}"></image> <image wx:else src="{{items.image}}"></image> <text>{{items.name}}</text> </view> </view> </view> [代码] home页面的ts [代码]Page({ data: { activeIndex:0, tab:[ { name:'商品', image:'../../images/index.png', activeImage:'../../images/index-hover.png', }, { name:'购物车', image:'../../images/cart.png', activeImage:'../../images/cart-hover.png', }, { name:'订单', image:'../../images/order.png', activeImage:'../../images/order-hover.png', }, { name:'我的', image:'../../images/my.png', activeImage:'../../images/my-hover.png', } ] }, // 切换事件 choose(e:any){ const _this=this; const {activeIndex}=_this.data; if(e.currentTarget.dataset.index==activeIndex){ return }else{ _this.setData({ activeIndex:e.currentTarget.dataset.index }) } }, }) [代码] 上面代码不难理解,点击以后改变[代码]activeIndex[代码]从而控制每个组件的渲染和销毁,这样付出的代价还是比较大的,需要我们进一步的优化。 如何实现keep-alive 我们知道,这里主要是避免组件反复创建和渲染,有效提升系统性能。 实现思路 1.在[代码]tab[代码]每个选项增加两个值:[代码]status[代码]和[代码]show[代码],[代码]show[代码]控制组件是否需要渲染,[代码]status[代码]控制组件[代码]display[代码] 2.初始化时候设置首页的[代码]status[代码]和[代码]show[代码],其他都为[代码]false[代码] 3.当我们切换时:把上一个[代码]tab[代码]页面的[代码]status[代码]改为[代码]false[代码],然后把当前要切换页面的[代码]tab[代码]数据中的[代码]status[代码]和[代码]show[代码]都改为[代码]true[代码],最后再更新一下[代码]activeIndex[代码]的值。 wxml代码: [代码] <!-- 首页 --> <view wx:if="{{tab[0].show}}" hidden="{{!tab[0].status}}"> <index></index> </view> <!-- 购物车 --> <view wx:if="{{tab[1].show}}" hidden="{{!tab[1].status}}"> <cart></cart> </view> <!-- 订单 --> <view wx:if="{{tab[2].show}}" hidden="{{!tab[2].status}}"> <order></order> </view> <!-- 我的 --> <view wx:if="{{tab[3].show}}" hidden="{{!tab[3].status}}"> <my></my> </view> [代码] ts代码 [代码]Page({ data: { activeIndex:0, //当前选中的index tab:[ { name:'商品', image:'../../images/index.png', activeImage:'../../images/index-hover.png', status:true,//控制组件的display show:true, //控制组件是否被渲染 }, { name:'购物车', image:'../../images/cart.png', activeImage:'../../images/cart-hover.png', status:false, show:false, }, { name:'订单', image:'../../images/order.png', activeImage:'../../images/order-hover.png', status:false, show:false, }, { name:'我的', image:'../../images/my.png', activeImage:'../../images/my-hover.png', status:false, show:false, } ] }, choose(e:any){ const _this=this; const {activeIndex}=_this.data; //如果点击的选项是当前选中,就不执行 if(e.currentTarget.dataset.index==activeIndex){ return }else{ //修改上一个tab页面的status let prev='tab['+activeIndex+'].status', //修改当前选中元素的status status='tab['+e.currentTarget.dataset.index+'].status', //修改当前选中元素的show show='tab['+e.currentTarget.dataset.index+'].show'; _this.setData({ [prev]:false, [status]:true, [show]:true, activeIndex:e.currentTarget.dataset.index,//更新activeIndex }) } }, }) [代码] 这样基本就大功告成了,来看一下效果: [图片] 当我们点击切换时候,如果当前组件没有渲染就会进行渲染,如果渲染过后进行切换只是改变[代码]display[代码],完美实现了需求,大功告成! 实际业务场景分析 在实际使用中还有两种种情况: 情况1:比如某些数据并不希望他首次加载后就数据保持不变,当切换页面时候希望数据进行更新,比如笔者做的电商小程序,在首页点击商品加入购物车,然后切换到购物车,每次切换时候肯定需要再次进行请求。 情况2:像个人中心这种页面,数据基本请求一次就可以,没必要每次切换请求数据,这种我们不需要进行改进。 我们给组件传递一个值:[代码]status[代码],然后在组件中监听这个值的变化,当值为[代码]true[代码]时候,去请求接口更新数据。具体代码如下: wxml代码(只列举关键部分): [代码]<!-- 首页 --> <view wx:if="{{tab[0].show}}" hidden="{{!tab[0].status}}"> <index change='{{tab[0].status}}'></index> </view> <!-- 购物车 --> <view wx:if="{{tab[1].show}}" hidden="{{!tab[1].status}}"> <cart change='{{tab[0].status}}'></cart> </view> [代码] 首页组件/购物车组件[代码]ts[代码]代码: [代码]Component({ /** * 组件的属性列表 */ properties: { change: { type: String,//类型 value: ''//默认值 }, }, observers: { //监听数据改变进行某种操作 'change': function(change) { if(change=='true'){ console.log('更新首页数据'+change) } } }, }) [代码] 来看一下最终效果: [图片] 结尾 目前能想到的实现方法就是这样,如果你有更好的方法,欢迎评论区交流,文章如有错误问题欢迎指正。
2021-06-21 - 小程序官方压测工具上线通知
“小程序压测”工具是微信小程序团队为开发者提供小程序压力测试解决方案,帮助企业最大限度真实还原业务的海量高并发等复杂场景,提早发现并解决问题,大幅降低传统测试成本,高效检验和管理业务性能,为业务保驾护航。 一、应用场景 适用于促销抢购、限量秒杀、直播卖货等流量峰值明显的业务,可模拟海量高并发,提前发现流量峰值问题、降低传统测试成本二、能力优势 真实模拟:微信独家小程序爬虫技术真实模拟微信用户打开小程序的行为,实现全链路压测的功能报告详细:压测报告包含页面加载、网络请求的多种结果指标,帮助开发者更好发现瓶颈、定位问题操作简单:全界面化操作,可灵活配置并发数、压测时间长,支持选择不同版本进行压测,更好地适配不同业务场景三、使用指南 工具免费使用,详情请在PC端访问:https://fuwu.weixin.qq.com/service/detail/000c24bafd0bb8e3794cbfa505c015 操作指南请访问:https://developers.weixin.qq.com/doc/oplatform/service_market/buyer_guideline/tool/use.html
2021-06-17 - 小程序内嵌二维码长按识别内测QA
小程序内嵌二维码长按识别内测QA Q1:支持识别的码类型与场景如何? A1:小程序内一直支持小程序码的长按识别,公众号二维码仅在小程序内嵌公众号文章场景下识别。 此次放开内测识别的码包括:微信个人码、企业微信个人码、普通群码与互通群码,支持的场景包括: 调用previewImage接口后,长按图片出现菜单:iOS 8.0.6&安卓8.0.3以上版本支持调用previewMedia接口后,长按图片出现菜单:iOS 8.0.6&安卓8.0.3以上版本支持<image>组件将 show-menu-by-longpress属性设置为true后,长按图片出现菜单:iOS 8.0.8&安卓8.0.7以上版本支持(未发布)<web-view>组件中长按图片出现菜单:iOS 8.0.6&安卓8.0.3以上版本支持 Q2:使用该能力时需要注意什么? A2:请勿使用利诱等方式诱导用户添加好友或者加入群聊,页面内容需要遵循小程序运营规范,若发现违反规范的行为将封禁识别能力。 Q3:为什么有些图片长按没有弹出菜单? A3:在小程序中<image>组件需要将 show-menu-by-longpress属性设置为true后才可以直接长按出现菜单。 同时<image>支持识别微信个人码、企微个人码、普通群码、互通群码的能力目前在iOS下存在问题需要客户端进行修复(预计8.0.8版本);安卓8.0.3版本未在此场景下支持,预计8.0.7版本完成支持。 Q4:为什么有些图片长按会出现菜单,也会出二维码的跳转入口,但是点击后不跳转? A4:此问题已知,是iOS的跳转出现了问题,将在8.0.8版本修复 Q5:为什么企业微信群码有时可以识别有时无法识别? A5:请确认是否为企业微信活码,企业微信活码不支持识别,暂无放开计划 Q6:为什么H5中的图片长按不出现菜单,反而出现一个系统的共享/添加到“照片”/拷贝菜单? A6:此处是iOS WebView的特性,可参考此链接进行禁用:https://developers.weixin.qq.com/community/develop/doc/000a20560c89a8f7555a0b16051400
2021-06-09 - 微信小程序去掉左上角返回键
问题原因: 在小程序某个页面鉴权不通过时,需要将小程序跳转到登陆页面,让用户重新输入账号密码,此时该页面顶部会显示返回按钮,用户可以点击返回上一个页面,因此想去掉返回按钮 解决办法: 使用wx.reLaunch({url: "/pages/login/login"});方法跳转到登陆页面login.wxml,然后在login.js中的onShow方法中,使用wx.hideHomeButton();方法将返回键隐藏。 注: 必须先使用wx.reLaunch({url: "/pages/login/login"});跳转,然后用wx.hideHomeButton();才能隐藏,直接使用wx.hideHomeButton();没有效果,测试的手机系统版本IOS14.4,微信版本8.0.6
2021-06-04 - 小程序内调起客服,客服发送的消息卡片某些情况无法正常跳转
描述现象: 用户在小程序内点击button调起客服聊天后,客服发送了跳转本小程序某页面的消息卡片,用户点击消息卡片没有打开新的小程序页面,只触发了一个返回小程序的操作,无页面跳转,交互上跟wx.navigateBack一样; 用户只能将小程序收起/关闭,再点击聊天消息中客服发送的消息卡片才能正常跳转。 原因猜测: 小程序试图做了优化,跳转目标的小程序在前台的情况下就只返回而不打开新的,但没考虑目标页面的问题,导致只是简单的返回了一下,造成功能缺失。 相同情况的其他开发者反馈参考:https://developers.weixin.qq.com/community/develop/doc/000664e81a0e00e7684a2033f56400?highLine=%25E5%25AE%25A2%25E6%259C%258D%25E6%25B6%2588%25E6%2581%25AF%25E5%258D%25A1%25E7%2589%2587
2020-06-11 - 【小程序直播】如何开通小程序直播?
前不久,我们开始公测小程序直播能力。作为微信官方提供的商家经营工具,小程序直播具有流量自有、低门槛快运营和强社交互动高转化的优势,可以帮助商家实现用户互动与商品销售的闭环。 消息一公布,我们就收到大家的咨询。现就小程序直播的申请开通、功能与运营两个模块,将被问得最多的问题一一解答。 小程序直播申请开通问题 Q1:我要怎样才能接入小程序直播? 首先,你必须要有一个自己的小程序;其次,你的小程序接收到了微信的公测邀请。 Q2:我在哪里可以查看是否接受到公测邀请? 可登陆小程序后台,点击左侧“功能 - 直播”功能查看是否收到公测邀请。 Q3:怎么样才能参加小程序直播公测? 本次公测为邀请制,符合以下条件且收到邀请的商家即可开通: 1.满足小程序18个开放类目: [图片] 2. 主体下小程序近半年没有严重违规; 3. 小程序近90天内,有过支付行为; 4. 主体下公众号累计粉丝数大于100人; 5. 主体下小程序连续7日日活跃用户数大于100人; 6. 主体在微信生态内近一年广告投放实际消耗金额大于1万元。 注:条件中1、2、3必须满足,4、5、6满足其一即可。如满足以上条件还未收到邀请,请耐心等待,公测将尽快覆盖。如未满足条件,可尽快补齐相关条件。 Q4:小程序直播需要开发才能接入吗? 小程序直播是功能组件,需开发后才能投入使用。我们支持多种接入模式: 商家:可自行开发或找服务商,最快一天开发完成,运营20分钟即可上手; 服务商:开发者可快速了解直播能力,增加直播权限集,想成为小程序直播服务商,可点击获取详细指引; 公众号、MCN机构及各类红人:可与品牌合作,或自行搭建小程序直播。 小程序直播功能与运营问题 Q1:直播间的直播的时间是否有限制? 每个直播间不能直播超过12小时。 Q2:直播当中能更换上下架商品吗? 可以。 Q3:目前支持多少个列表商品? 已入库的商品上限为2000,每天最多提交审核500件商品。 商品入库前需经过审核,审核时长为1—7个工作日。只有已入库的商品才能添加到直播间的商品列表中,建议将直播间的商品提前录入到商品库中。 Q4:如何获得直播回放? 我们提供了回放源视频能力,请查看小程序直播开发文档 —【获取回放源视频】,可点击查看文档。后续我们将提供无需开发的直播回放功能。 Q5:小程序直播时,为何扫描分享的小程序码,显示页面不存在? 小程序需在代码中引入直播组件,并提审更新小程序版本,否则小程序码不生效。 Q6:支持一个小程序,同时开多个直播间进行同时直播吗? 可以的,同一个小程序可以支持50个直播间同时直播,一天的直播上限也是50场。 Q7:为什么我后台没有小程序发布的地方? 如果小程序发布权限授权给第三方,发版需要在第三方提交。 Q8:我是服务商,如何查询是否开通了小程序直播的服务商权限? 一般在提交申请后,不超过7个工作日,完成权限审核及开通。 权限开通后,商家可在微信开放平台—第三方平台的权限集中勾选“小程序直播权限”,且开发工具中不会报“此插件未授权”的错误。 Q9:小程序直播组件开发遇到技术问题怎么办? 技术相关问题,请到小程序社区发帖咨询,社区有专业的技术答疑,可点击跳转到社区。 产品与运营相关问题,关注“微信行业助手”公众号,回复【直播】+资讯问题,我们会尽快回复你的问题。 小程序直播新能力预告 Q1:小程序直播是否支持分享带自定义参数? 我们已支持分享带自定义参数,请查看小程序直播开发文档 —【获取直播间相关参数及开发者自定义参数接口】可点击查看文档;商家可根据这些参数建立用户、直播间、商品之间的映射关系。 Q2:小程序直播支持摄像机等专业设备推流吗? 已经在开发中,计划3月内支持。
2020-04-28 - 03.getUserInfo和getUserProfile 对比
最近动态 wx.getUserProFile() 在2.16.0成功回调有iv、encryptedData,具体看这里https://developers.weixin.qq.com/community/develop/doc/000c04d0490118d8a6ebf675a56c00 调整背景 很多开发者在打开小程序时就通过组件方式唤起 getUserInfo 弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。详情可以点击官方调整链接(https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801) 调整前后API功能的对比[图片] [图片] 能力检测 两个前提条件: 1.开发者工具版本不低于 1.05.21030222.基础库版本不低于 2.10.4[图片] 代码片段: https://developers.weixin.qq.com/s/odMs3wmX7Ko3 测试过程 step1: 在开发工具设置清除全部缓存step2: 点击 getUserInfo 按钮,会弹出用户授权,允许后会得到这些信息,见截图[图片] step3: 在终端输入下面代码,也可以获取上面截图数据(今天还不到截止时间,还能获取完整的用户头像和昵称)wx.getUserInfo({ complete: (res) => { console.log(res) } }) step4: 点击 getUserProfile 按钮,会弹出用户授权,允许后会得到这些信息,见截图(只有用户昵称和头像信息)[图片] step5: 通用在终端输入下面代码,获取不到任何信息,符合`若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,且开发者每次通过该接口获取用户个人信息均需用户确认`wx.getUserProfile({ complete: (res) => { console.log(res) } }) step6: 可以重复点击 getUserInfo 按钮和 getUserProfile 按钮进行测试。功能对比讲解 1.4月13日前未发布的,wx.getUserInfo 能力 wx.getUserInfo(Object object) 会返回 encryptedData、signature、rawData,通过将返回的数据传递给服务器,服务端能解析出用户的身份标识,即 unionId(unionId 获取机制:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html) 【对我们业务来说】 从 wx.getUserInfo 就是要两样东西:unionId和用户信息(头像和昵称)。 但从 2021年2月23日起,可以通过 wx.login 接口获取的登录凭证可直接换取 unionID,可以替代一部分wx.getUserInfo 的功能了。 2.新增 getUserProfile 能力 wx.getUserProfile 能获取到头像和昵称,可以替代 wx.getUserInfo 的另外一部分功能。 3.小结 从这里是不是可以得出,wx.login + wx.getUserProfile 基础可以替代之前的 4月13日前未发布的,wx.getUserInfo 能力。其实不然,如果真是这样的,官方是不是没必要这样搞,咱们接着看。 4.wx.getUserInfo 和 wx.getUserProfile 区别 1.功能上是 wx.getUserInfo 不在返回用户授权的头像昵称,只返回匿名信息,但 wx.getUserProfile 会返回用户授权的头像昵称。2.wx.getUserInfo 授权成功后,当下次调用时,可以直接获取授权成功返回数据,不需要每次都需要用户确认,但 wx.getUserProfile 每次都需要用户确认允许后才能拿到用户信息3.对于业务来说,可以通过 wx.getUserProfile 获取用户信息和昵称后,要存在自己服务器,不能像之前那样每次都通过 wx.getUserInfo 方式获取,否则体验会比较差疑问 1.4月13日后发布的新版本小程序,如果用户未更新到新版本,此时调用 wx.getUserInfo 会不会返回用户授权的头像昵称(如果不确定,业务可能需要兼容处理)2.4月13日后发布的新版本小程序,用户更新到新版本,调用 wx.getUserInfo 返回匿名的头像昵称支持服务器解密吗? 常见问题汇总 1.wx.canIUse 判断getUserProfile结果是false,可以通过直接判断 wx.getUserProfile 即可,类似问题可以查看官方知识库(https://developers.weixin.qq.com/community/develop/doc/000cac40cf0eb8d3e429647c351c09?_at=1614912876047)
2021-04-02 - 动态更新swiper,更新的swiper-item显示空白。
在swiper-item设置了背景图的情况下会出现此bug。
2019-08-29 - 小程序支付用户不点击“完成”的处理方案
小程序支付,用户在付费成功后,没有点击“完成”,就没有支付成功回调触发,只能通过notify_url来接收异步回调通知,处理流程,当然这是正常的处理流程。 以下是另类的处理流程: 1、不处理notify_url; 2、在wx.requestPayment之前,将流程进度缓存,比如wx.setStorageSync('out_trade_no',out_trade_no); 3、那么,如果用户点击了“完成”或者取消支付,则必然会触发wx.requestPayment的回调success或者fail,则清除缓存,wx.removeStorageSync('out_trade_no'); 4、如果用户没有点击“完成”,则用户下次打开小程序,一定是冷启动即重启小程序,因为如果是热启动,将还是停留在支付界面,用户可以继续点击“完成”,继续业务流程; 5、因为是冷启动,所以在pages/index/index的onLoad里常驻一个进程,检查是否有支付未完成缓存,如果有,则按照out_trade_no查询订单支付状态,再继续支付流程。 以上只是另类做法,仅供参考。 虽然不符合支付的标准流程,但是可以不需要专门的进程来负责notify_url。
2021-03-22 - 微信小程序swiper的自适应高度(借用别人的)
原文:https://developers.weixin.qq.com/community/develop/article/doc/00024611008208e3516a165075b813 在既要保持状态,同时又要动态设置swiper高度的要求下,最好是通过css来解决问题,这是第一印象,这样既能保持无渲染(刷新),然后高度又能定制 小程序组件swiper需要指定固定高度,但在某些场景中我们需要动态设置swiper的高度以完整展示swiper中的内容,比如高度不同的图片,笔者最近项目中的日历组件(31号有时会多出一行)等等,如何使swiper组件自适应高度呢? 翻阅了一些网上的例子,一般的解决方法是通过设置style.height来解决 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,哈哈
2021-01-09 - 企业微信 外部联系人unionid转换 ?
/cgi-bin/externalcontact/unionid_to_external_userid 外部联系人unionid转换 微信公众号能调用吗
2020-11-28 - 如何通过 微信的union_id 获取到 企业微信的外部联系人的userid?
我现在想实现,公司的标签系统和微信的标签系统打通,因为之前收集过了用户的 微信union_id,现在想在企业微信上给外部联系人打标签,需要企业微信的user_id,请问如何通过微信union_id 兑换 企业微信的外部联系人的userid?
2020-08-11 - 小程序视频录制
小程序视频录制分析 从功能层面是好实现的,但是在微信合规审核层面是否能过关是另一马,特别是人脸的视频录制。 [图片]~ 参考文档 1、微信小程序可以做视频录制吗?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/0000a2201f0ae0b9e8f98ca605c800 2、小程序可以做一个视频录制的功能吗?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/000e86a87486700ee75be7c6556800
2021-01-26 - “聊天素材支持小程序打开” 这个能力怎么在真机测试呢?
最近官方在内测 “聊天素材支持小程序打开” 这个功能,目前有几个疑问: “更多方式打开”这个入口是不是只有小程序上线之后才能看到?目前用体验版测试无法看到这个入口。(用的安卓手机,最新版微信,基础库版本是2.14.3)模拟器里测试成功,但是审核的时候提示功能无效,由于无法进行真机调试,没办法定位问题。forwardMaterials 这个值到底是数组还是对象,模拟器上返回的是数组,但是文档里面写的是对象。forwardMaterials 里返回的 path 值可以传给 getImageInfo 这个接口来获取图片宽高吗?模拟器里可以,不知道真机是什么效果。
2021-01-12 - 聊天素材支持小程序打开示例
上代码片段。 链接:https://developers.weixin.qq.com/s/qrcIHnmL7tnL onLoad: function () { var q=wx.getLaunchOptionsSync() var n = wx.getAccountInfoSync(); if(n.miniProgram.envVersion=="release"){ //线上版本中的功能 if(q.scene!=1173){ //校验场景值是否正确。如果不是从微信聊天中的素材打开,则弹窗提示 wx.showModal({ showCancel:false, title:"提示", content:"场景值错误,无法使用本页面的功能" }) return } if(q.scene==1173){ //如果场景值正确,则执行正常的功能(对聊天素材的处理) var qq=q.forwardMaterials[0] console.log(qq.path) //这里的qq.path为聊天素材文件(图片/视频)的本地临时路径 } } if(n.miniProgram.envVersion!="release"){ //填写提审时给审核人员看的功能,建议与线上版本中的功能一致 } }, "supportedMaterials": [ { "materialType": "image/*", "name": "用${nickname}打开", "desc": "可进行压缩图片等操作。", "path": "dktp/dktp" } ]
2021-01-15 - H5免鉴权跳转小程序常见问题解答
看到很多开发者在H5免鉴权跳转小程序这处于懵逼状态!!!! 我下边解释一下什么叫免鉴权????? 下方是官方文档内对免鉴权跳转能力的解释 [图片] 注意第二段话 静态网站网页在微信客户端打开时 也就相当于什么,相当于在微信中打开这个开放按钮时候才会免鉴权,此时是通过什么跳转的呢 没错是通过上边代码中 username="小程序原始账号 ID(gh_ 开头的)" path="要跳转到的页面路径" 此时根据这两个参数跳转的,此时和云函数半毛钱关系没得!!!! 此时相当于什么 相当与小程序内的 wx.navigateToMiniProgram(Object object) 此时你想说我就想单纯实现微信内H5的跳转,且想在路径传参怎么解决??? js获取静态网站url后的参数,原生js去替换username与path的值呀 let launchBtn = document.getElementById('launch-btn') launchBtn.setAttribute("path", "XXXXXXXXXX"); //HTML 属性 launchBtn.setAttribute("username", "XXXXXXXXXX"); //HTML 属性 在微信以外的渠道中都需要走云函数去请求拿到 openlink 或自建网站鉴权调用接口获取openlink 那些发帖想在小程序A云函数跳转B小程序的别想了不可以 在uniapp里腾讯云函数搞的也别想了,那边没有内置小程序的sdk,调用不动云函数的 乖乖去云开发里上传静态网站,并打开允许访问,云函数打开未登录允许调用 或者自建网站鉴权获取 下边发一条我自己开发的H5跳小程序链接,你们可以去测试 https://u.imvp.top/?s=jlqwyBFN ——本链接由微信小程序【链接工具】生成 [图片] 看到这里有人问我,我这个链接后边的参数是干什么的?这个参数是控制跳转哪一篇文章的加密id。 在任何情况下访问网站我都会去解析真实对应的文章链接是什么? 微信内我会将真实链接拼接在wx-open-launch-weapp属性内 if(res.result.url){ launchBtn.setAttribute("path", `/pages/basics/web_view.html?url=${encodeURIComponent(res.result.url)}`); //HTML 属性 } [图片] 此时文章链接已经拼接在属性path上了。微信内点击也会跳转到指定位置, 非微信内我会拿到openlink 重定向Url唤醒微信,实现外链跳转。 云函数端代码同样采用了官方示例代码,增加了openlink 入库绑定对应文章链接与加密参数,免得多次生成浪费! 2021年1月26日补充 因为目前URL Scheme进入小程序仅可进入正式版本,无法进入测试版,自己在开发时候专门做了参数埋点,上次测试后才二次对接参数提版,为此我将参数格式说明一下 //小程序端首页onLoad onLoad(options) { if(options.s=='u'){ //openlink解析后的参数标志位 uni.navigateTo({ url:`/pages/basics/web_view?id=${options.id}` }) } } //云函数端,生成openlink const result = await cloud.openapi.urlscheme.generate({ jumpWxa: { path: `/pages/index/index`, // 替换自己的url路径 query: id?'s=u&id='+id:'', // s=u 作为我自己的参数标志位 }, // 如果想不过期则置为 false,并可以存到数据库 isExpire: false, // 一分钟有效期 expireTime: parseInt(Date.now() / 1000 + 60), }) //存储跳转链接 saveOpenlink(id,result.openlink) return { ...result, //urlscheme返回的所有参数 主要使用result.openlink s:id, //加密文章ID url:articleData.data.url //文章链接 } //html端 供小程序环境访问跳转使用 let launchBtn = document.getElementById('launch-btn') launchBtn.setAttribute("path", `/pages/basics/web_view.html?url=${encodeURIComponent(res.result.url)}`); //HTML 属性
2021-01-26 - 小程序的防盗思考
之前写了一篇《用webpack编译小程序》的文章,有人留言问到关于小程序防盗,加密,混淆等等问题,我想了一下午,发现这个真的是一个问题(呵呵),这篇文章来分享下我的思考 https://juejin.im/post/5b0e431f51882515497d979f 上文是一篇关于小程序反编译的介绍,作者通过一顿猛如虎的操作,成功的扒下了一个小程序的源码(好像是滴滴)。文末作者提到了一点安全相关的问题,为增加了反编译者的阅读难度可以通过babel来压缩混淆js文件。 我想通过各种框架(wepy, mpvue等)来编写的小程序,框架应该都经过了babel来处理输出,反编译者拿到的应该都是uglifyjs后的文件,之所以仍然有这些问题,应该还是有相当一部分的同学使用的原生小程序语法来进行的开发的吧! 盗版 如果我是一个盗版者,我这么做的目的当然是为了增加我的收益,毕竟成本小,我会找很多比较热门,流量高的项目,扒下来,加上我的广告(嘿嘿),我不太在乎小程序的质量,只要它能正常运行。 镜像盗版 镜像盗版是成本最小的一种方式,扒一个小程序,clone成n个项目,上线,收钱。镜像盗版不理会源代码是否混淆或者加密,不需要弄懂逻辑,仅仅需要小程序能正常运行,即便不完整。 数据盗版 这一类是比较有追求的盗版,因为搭建一套数据服务成本较高,因此扒一个小程序,将api抽离出来,自己开发界面,避开被人投诉,神不知鬼不觉的利用别人的数据服务(想到这,有点激动)。 数据保护 不论那种盗版,最终都需要将数据呈现给用户,因此如何保护数据是防盗的一个关键点。 关键字防盗方案 PC/H5我们有一种经济的操作方法,将关键字秘钥(如公钥,keyword)非周期性通过后端php/node/java等服务渲染输出(明文),浏览器端则通过算法生成token来解开相关数据或接口。注意非周期性这个描述(周期性输出也可以,但破解成本变小了)。 小程序并不能如上操作,但小程序可以通过非周期性更新版本,来输出不同的关键字秘钥(如写死在app.globalData中),这必然增加了盗版者的破解成本,当正版更新秘钥后,盗版小程序不更新将不能正常获得数据,也就断了盗版的根 云函数方案 这个是我重点想介绍的。云端小程序面向的是没钱,没资源又想搞点啥挣点外快的开发者,正规小程序当然都必须有自己的后端数据服务,但并不妨碍使用云端服务来提供支持。 云端很安全,这是我的推论,我也深信这是正确的。之所以安全,我想应该是微信app一定封装了一些和腾讯云间通信的某种不可告人的秘密吧。试想一下,如果不同小程序之间的云服务被串了,我的小程序用你的云资源,这。。这。。这,估计腾讯自己都会玩不下去了。 基于云端很安全的前提,我们可以把关键的秘钥数据存放在云端,每一次小程序初始化时去云端将关键秘钥取下来生成接口访问token,这样即便盗版者扒到了小程序,但云服务的隔离性使得盗版者不能获取关键秘钥,从而盗版者成功的扒到了一套UI 云服务的缺点是每天有访问次数上限,我记得是50万次/天,这完全不够啊~~ , 嗯,老铁,666,双击点赞 结束 以上种种,本人并没有实践过,仅限于分享,我们现在主要做开源,盗版,不存在的啊~~~ 如果你想了解下我们的架构,可以看这里 https://github.com/webkixi/aotoo-hub 如果你想使用我们的架构, 不怕死的看这里 https://www.agzgz.com 如果你还想看看我们的小程序,骚一骚下面这个 [图片]
2019-06-06 - 小程序如何接入客服消息
1 这几天想给我的答题小程序增加一个反馈的功能,这样用户就可以直接反馈我一些意见,由于是个人小程序不能出现UGC,所以折中想到客服消息这个模式, 那么如何接入小程序客服消息呢? 首先在小程序后台配置客服人员,比如把我添加到客服里面,如下图所示 [图片] 2 3 接入代码 [图片] 4 小程序界面 [图片] 5 [图片] 6 [图片] 7 其实很简单就一行代码, 具体官方文档如下所示: https://developers.weixin.qq.com/miniprogram/introduction/custom.html#%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/customer-message/customer-message.html 但是在接入后,用户发了消息,作为客服的微信号并未收到通知,其实是需要扫码特定的小程序(客服小助手)才可以的,这一点非常容易遗漏,需要格外关注 大家可以扫码体验下 [图片] 1 2020-05-11补充 小程序审核通过之后,客服消息发的会话,会在服务通知里面有提醒,这样确实就方便很多了 1 [图片] 2 [图片] 3 [图片] 2
2020-05-10 - 关于自定义客服会话contact同时兼容自动和人工客服使用
首先,我们知道,可以通过button中的属性open-type="contact",实现小程序用户和小程序所有者客服对话。
2020-08-12 - 微信新增功能「小程序外链」,短信营销场景如何落地实现营销闭环
在微信小程序支持外部网站跳转到小程序的功能后,我们第一时间上线了 PC 端的小程序外链生成器和小程序桌面快捷方式生成器,受到了广大小程序开发者和产品运营的称赞和应用。为了让大家以更高的效率、更低的成本,获得更显著的营销效果,本周我们给大家带来两个好消息。 小程序外链支持移动端配置,只需一部手机,随时随地进行多渠道引流; 支持短信内容带小程序外链,快速设置短信内容,一键完成消息发送。 如果你不是很了解小程序外链,可查看文章《两步实现桌面图标、短信和邮件外链跳转到小程序,轻松召回沉睡用户》。 小程序外链 小程序外链是微信小程序近期上线的新功能,支持从外部渠道(邮件、短信、网页、其它 App、手机桌面快捷方式等)快速跳转到微信小程序平台上。 小程序外链的应用场景 短信、邮件内容营销 在短信、邮件内容中附上小程序链接,用户点击链接直接跳转到微信小程序指定页面上,有效降低用户操作门槛,提高营销效果。 网页(贴吧、社区、论坛等) 支持从网页(如贴吧、社区、论坛等)链接跳转到微信小程序,实现多渠道营销引流。 其它 App(QQ、抖音等) 支持在 QQ、抖音等 App 的聊天窗口中,通过小程序外链极速打开微信小程序,无需切换微信客户端。 手机桌面快捷方式 通过「小程序桌面快捷方式生成器」可以快速将小程序添加到手机桌面上,用户无需打开微信客户端,点击图标一秒访问小程序。 你可以将已生成的小程序外链(URL)和「小程序桌面快捷方式生成器」分享给你的用户,引导他们将你的小程序添加到手机桌面上,快捷的访问方式有利于提高小程序活跃度,减少用户流失。 [图片] 小程序外链生成器使用步骤 移动端 关注知晓云公众号,点击菜单「知晓云」-「小程序外链」跳转到小程序链页面,如果你是首次使用知晓云服务,则需注册账号、创建企业和授权微信小程序,如已有账号,则登录账号即可,首次使用小程序外链功能需更新授权小程序。 点击「添加外链」按钮生成小程序外链(URL),点击「复制」按钮,即可将外链应用到邮件、短信等营销场景中。你还可以针对不同渠道、不同小程序页面创建不同的外链,实现营销渠道的效果分析。 [图片] PC 端 登录知晓云控制台,选择你要设置的应用,点击「微信」-「小程序链」-「添加」按钮,输入相关信息生成小程序外链(URL)。 [图片] 短信推送添加小程序外链 知晓云已支持短信内容快速插入小程序外链,一键推送短信功能。生成小程序外链后,你可以直接使用系统已为你准备好的短信模板,无需复杂的配置工作、无需审核、一键发送,一站式完成用户触达。 你可以在「知晓云控制台-运营-短信」中配置短信内容,并选择你已生成的小程序外链,即可一键发送给你的用户。 温馨提示: 已经在知晓云授权过的小程序,需要更新授权,在授权页面中勾选「获取 URL Scheme 权限」 单个小程序的外链数量无上限; 目前小程序外链暂不支持个人版的小程序; 小程序未上线则无法生成外链; 添加小程序桌面快捷方式目前仅支持 iOS 自带浏览器和安卓的小米自带浏览器、谷歌浏览器。 更新预告 小程序外链功能还在不断迭代中,如果你有其他想法,欢迎你随时跟我们交流。 微信小程序视频播放器; 支持快手小程序、车载小程序,全平台支持能力再升级; 知晓推送支持 iOS 和 Android 。
2021-01-19 - 小程序直播插件可以动态引入吗?
请问小程序直播插件可以动态引入吗?比如先判断小程序有没有开通直播,开通了则引入插件代码,没开通就不引入 "plugins": { "live-player-plugin": { "version": "1.2.5", "provider": "wx2b03c6e691cd7370" } }
2021-01-13 - 小程序eval替代方案:eval5 1.4.0-1.4.5 发布日志
eval5是基于TypeScript编写的JavaScript解释器,100%支持ES5语法。 支持浏览器、node.js、小程序等 JavaScript 运行环境 。 项目地址: https://github.com/bplok20010/eval5 使用场景 浏览器环境中需要沙盒环境来执行JavaScript代码 浏览器环境控制代码执行时长 不支持eval/Function的JavaScript运行环境,如:微信小程序 示例 更新日志 1.4.5 修复with语句中函数调用时丢失this信息 1.4.4 修复在未使用try-catch情况下出现异常时导致下次调用evaluate时的变量声明错乱问题。 1.4.3 修复 WithStatement 中赋值不生效问题。 rootContext创建调整为:Object.create(options.rootContext),防污染。 1.4.2 新增内置对象:URIError RangeError SyntaxError ReferenceError 修复 assignment 表达式触发对象的getter方法调用 1.4.1 修复再次执行事超时机制失效问题 修复函数表达式赋值时引起的返回值错乱问题 1.4.0 解释器内部eval/Function重写 新增参数 options.rootContext 新增参数 options.globalContextInFunction 移除Interpreter.rootContext 运行原理 eval5先将源码编译得到树状结构的抽象语法树(AST)。 抽象语法树由不同的节点组成,每个节点的type标识着不同的语句或表达式,例如: 1+1的抽象语法树 [代码]{ "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": { "type": "BinaryExpression", "operator": "+", "left": { "type": "Literal", "value": 1, "raw": "1" }, "right": { "type": "Literal", "value": 1, "raw": "1" } } } ], "sourceType": "script" } [代码] 根据节点type编写不同的处理模块并得到最终结果。例如:根据1+1的语法树我们可以写出一下解释器代码: [代码]function handleBinaryExpression(node) { switch( node.operator ) { case '+': return node.left.value + node.right.value; case '-': return node.left.value - node.right.value; } } [代码] [图片] 示例 在线体验 更多示例 以下是解析echarts4效果示例: [图片]
2020-04-23 - 小程序eval/Function终极替代方案:eval5
由于小程序内部禁用了eval、Function导致在一些场景下无法动态执行脚本,小程序又只支持JavaScript开发,如果想要在在前端动态执行脚本就得实现对于的脚本解释器。 eval5是完全基于JavaScript编写的JavaScript解释器,支持ECMA5语法 项目地址:https://github.com/bplok20010/eval5 实现原理: 使用acorn或esprima编译器对JavaScript代码进行编译并得到抽象语法树(AST)用JavaScript解析语法树并得到计算结果例如:1+1的解析 一、使用acorn编译后得到的语法树,语法树由不同的节点组成各个节点的type代表着不同的语句或表达式类型: { "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": { "type": "BinaryExpression", "operator": "+", "left": { "type": "Literal", "value": 1, "raw": "1" }, "right": { "type": "Literal", "value": 1, "raw": "1" } } } ], "sourceType": "script" } 二、得到语法树后,我们可以根据不同的节点类型实现不同的处理函数,例如: function handleBinaryExpression(node) { switch( node.operator ) { case '+': return node.left.value + node.right.value; case '-': return node.left.value - node.right.value; } } 如何使用 一、使用npm安装: npm install --save eval5 二、使用打包好的eval5.js 使用示例: // npm install --save eval5 import {Function,evaluate} from 'eval5'; //or 'path/eval5.js' const sum = new Function('a', 'b', 'return a+b'); sum(100,200) evaluate('1+1') eval5基于小程序编写的示例:eval5-wx-demo github地址 [图片]
2020-02-19 - 手机号授权,有时需要验证码
正常逻辑,一段时间内未验证的手机号,就需要重新验证。
2020-10-16 - 云开发短信跳小程序(自定义开发版)教程
写在前面如果你想要自主开发,但没有云开发相关经验,可以采用演示视频来学习本教程: [视频] 一、能力介绍境内非个人主体的认证的小程序,开通静态网站后,可以免鉴权下发支持跳转到相应小程序的短信。短信中会包含支持在微信内或微信外打开的静态网站链接,用户打开页面后可一键跳转至你的小程序。 这个链接的网页在外部浏览器是通过 URL Scheme 的方式来拉起微信打开主体小程序的。 总之,短信跳转能力的实现分为两个步骤,「配置拉起网页」和「发送短信」。本教程将介绍如何执行操作完成短信跳转小程序的能力。 如果你想要无需写代码就能完成短信跳转小程序的能力,可以参照无代码版教程进行逐步实现。 二、操作指引1、网页创建首先我们需要构建一个基础的网页应用,在任何代码编辑器创建一个 html 文件,在教程这里命名为 index.html 在这个 html 文件中输入如下代码,并根据注释提示更换自己的信息: window.onload = function(){ window.web2weapp.init({ appId: 'wx999999', //替换为自己小程序的AppID gh_ID: 'gh_999999',//替换为自己小程序的原始ID env_ID: 'tcb-env',//替换小程序底下云开发环境ID function: { name:'openMini',//提供UrlScheme服务的云函数名称 data:{} //向这个云函数中传入的自定义参数 }, path: 'pages/index/index.html' //打开小程序时的路径 }) } 以上引入的 web2weapp.js 文件是教程封装的有关拉起微信小程序的极简应用,我们直接引用即可轻松使用。 如果你想进一步学习和修改其中的一些WEB展示信息,可以前往 github 获取源码并做修改。 有关于网页拉起小程序的更多信息可以访问官方文档 如果你只想体验短信跳转功能,在执行完上述文件创建操作后,继续以下步骤。 2、创建服务云函数在上面创建网页的过程中,需要填写一个UrlScheme服务云函数。这个云函数主要用来调用微信服务端能力,获取对应的Scheme信息返回给调用前端。 我们在示例中填写的是 openMini 这个命名的云函数。 我们前往微信开发者工具,定位对应的云开发环境,创建一个云函数,名称叫做 openMini 。 在云函数目录中 index.js 文件替换输入以下代码: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { return cloud.openapi.urlscheme.generate({ jumpWxa: { path: '', // 打开小程序时访问路径,为空则会进入主页 query: '',// 可以使用 event 传入的数据制作特定参数,无需求则为空 }, isExpire: true, //是否到期失效,如果为true需要填写到期时间,默认false expire_time: Math.round(new Date().getTime()/1000) + 3600 //我们设置为当前时间3600秒后,也就是1小时后失效 //无需求可以去掉这两个参数(isExpire,expire_time) }) } 保存代码后,在 index.js 右键,选择增量更新文件即可更新成功。 接下来,我们需要开启云函数的未登录访问权限。进入小程序云开发控制台,转到设置-权限设置,找到下方未登录,选择上几步我们统一操作的那个云开发环境(注意:第一步配置的云开发环境和云函数所在的环境,还有此步操作的环境要一致),勾选打开未登录 [图片] 接下来,前往云函数控制台,点击云函数权限,安全规则最后的修改,在弹出框中按如下配置: [图片] 3、本地测试我们在本地浏览器打开第一步创建的 index.html ;唤出控制台,如果效果如下图则证明成功! 需要注意,此处本地打开需要时HTTP协议,建议使用live server等扩展打开。不要直接在资源管理器打开到浏览器,会有跨域的问题! [图片] 4、上传本地创建好的 index.html 至静态网站托管将本地创建好的 index.html 上传至静态网站托管,在这里静态托管需要是小程序本身的云开发环境里的静态托管。 如果你上传至其他静态托管或者是服务器,你仍然可以使用外部浏览器拉起小程序的能力,但会丧失在微信浏览器用开放标签拉起小程序的功能,也不会享受到云开发短信发送跳转链接的能力。 如果你的目标小程序底下有多个云开发环境,则不需要保证云函数和静态托管在一个环境中,无所谓。 比如你有A、B两个环境,A部署了上述的云函数,但是把 index.html 部署到B的环境静态托管中了,这个是没问题的,符合各项能力要求。只需要保证第一步 index.html 网页中的云开发环境配置是云函数所在环境即可。 部署成功后,你便可以访问静态托管的所在地址了,可以通过手机外部浏览器以及微信内部浏览器测试打开小程序的能力了。 5、短信发送云函数的配置在上面创建 openMini 云函数的环境中再来一个云函数,名字叫 sendsms 。 在此云函数 index.js 中配置如下代码: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { try { const config = { env: event.env, content: event.content ? event.content : '发布了短信跳转小程序的新能力', path: event.path, phoneNumberList: event.number } const result = await cloud.openapi.cloudbase.sendSms(config) return result } catch (err) { return err } } 保存代码后,在 index.js 右键,选择增量更新文件即可更新成功。 6、测试短信发送能力在小程序代码中,在 app.js 初始化云开发后,调用云函数,示例代码如下: App({ onLaunch: function () { wx.cloud.init({ env:"tcb-env", //短信云函数所在环境ID traceUser: true }) wx.cloud.callFunction({ name:'sendsms', data:{ "env": "tcb-env",//网页上传的静态托管的环境ID "path":"/index.html",//上传的网页相对根目录的地址,如果是根目录则为/index.html "number":[ "+8616599997777" //你要发送短信的目标手机,前面需要添加「+86」 ] },success(res){ console.log(res) } }) } }) 重新编译运行后,在控制台中看到如下输出,即为测试成功: [图片] 你会在发送的目标手机中收到短信,因为短信中包含「退订回复T」字段,可能会触发手机的自动拦截机制,需要手动在拦截短信中查看。 需要注意:你可以把短信云函数和URLScheme云函数分别放置在不同云开发环境中,但必须保证所放置的云开发环境属于你操作的小程序 另外,出于防止滥用考虑,短信发送的云调用能力需要真实小程序用户访问才可以生效,你不能使用云端测试、云开发JS-SDK以及其他非wx.cloud调用方式(微信侧WEB-SDK除外),会提示如下错误: [图片] 如果你想在其他处使用此能力,可以使用服务端API来做正常HTTP调用,具体访问官方文档 7、查看短信监控图表进入 云开发控制台 > 运营分析 > 监控图表 > 短信监控,即可查看短信监控曲线图、短信发送记录。 [图片] 三、总结短信跳转小程序核心是静态网站中配置的可跳转网页,外部浏览器通过URL Scheme 来实现的,这个方式不适用于微信浏览器,需要使用开放标签才可以URL Scheme的生成是云调用能力,需要是目标小程序的云开发环境的云函数中使用才可以。并且生成的URL Scheme只能是自己小程序的打开链接,不能是任意小程序(和开放标签的任意不一致)短信发送能力的体验是每个有免费配额的环境首月100条,如有超过额度的需求可前往开发者工具-云开发控制台-对应按量付费环境-资源包-短信资源包,进行购买。如当前资源包无法满足需求也可通过云开发 工单 提交申请[图片]短信发送也是云调用能力,需要真实小程序用户调用才可以正常触发,其他方式均报错返回参数错误,出于防止滥用考虑云函数和网页的放置可以不在同一个环境中,只需要保证所属小程序一致即可。(需要保证对应环境ID都能接通)如果你不需要短信能力,可以忽略最后两个步骤CMS配置渠道投放、数据统计可参考官方文档
2021-04-07 - 小程序转发到朋友圈示例及踩坑记录(含云开发环境配置)
偶小程序总算可以转发到朋友圈啦,撒花。。。 下面我们开始一步一步实现这个激动人心的功能,呵呵。 一、代码准备: 在页面js加入代码即可:参考文档(https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html#onShareTimeline) onShareTimeline:function(){ return { title:"哎呀呀", query:"from=timeline" } } 可能的坑:onShareTimeline生效的前提,需要有onShareAppMessage方法。 二、环境准备: 将本地环境基础库,修改为2.11.3或以上版本 [图片] 做完以上两部,分享到朋友圈功能就已经点亮了(暂时只支持安卓) [图片] 接下来是重头戏,朋友圈用户点击后进入的单页模式的权限处理。这个模式有较多限制,参见:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html 已DEMO为例,用户直接进入小程序,展示如下 [图片] 云开发环境返回的数据都是正常的。 如果是通过朋友圈进入,默认情况下,展示如下: [图片] 这个时候,云函数和数据库均未拿到数据。参见:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/identityless.html 我们就需要开启未登录模式及相关权限: 1.开启云环境的未登录访问权限 [图片] 2.修改对应的数据集合权限: [图片] 3.修改云函数权限(注意,这个地方改了,是所有的云函数均生效,请注意评估风险) [图片] 等待几分钟,权限生效后,从朋友圈进入,也就能获取数据了: [图片] 可能的坑: 权限放开之后的安全问题。
2020-08-07 - 微信 schema 跳转之非官方文档
微信“应该”是最近开放了 schema 跳转小程序 的能力,大大方便了短信、邮件、外部网页等唤起微信小程序。 schema 链接格式大体是这样:[代码]weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e[代码],但是这段文本在安卓端无法识别。小规模测试结果如下: [图片] 后来想到用一个正常能够识别的网页地址,内容是重定向到指定的 schema 链接。这就是擅长的领域了,query 参数上带上 schema 链接,location.href 一下不就行了。这里就不 show 代码了,能看到文章的你一定行。 但是,发现在部分安卓手机下(如小米)还是没反应,原来简单的 schema 跳转水这么深的,于是百度谷歌了一下,找到了下面两份关键材料: H5唤起APP进行分享的尝试 AlanZhang001/H5CallUpNative: H5端唤醒移动客户端程序 看源码也不多,总结下来,因不同系统和浏览器对 schema 规范的理解不同,还有一些商业因素,不同环境下面需要用不同的方式进行跳转,甚至有的环境你根本就跳不了。 时间紧,任务重。简单处理吧,不同方式都来一遍,谁好使就用谁。所以简单总结了下,能用的几种方式: location 跳转 a 链接跳转 iframe 跳转 以上三种方式,逐一试用,最后实在不行就不行吧,简单处理,看有没有大神补充的。 相关代码如下: location [代码]location.href = "weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e"; [代码] a 链接跳转 [代码]var aLink = document.createElement("a"); aLink.className = 'call_up_a_link'; aLink.href = "weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e"; aLink.style.cssText = "display:none;width:0px;height:0px;"; document.body.appendChild(aLink); aLink.click(); [代码] iframe [代码]var iframe = document.createElement('iframe'); iframe.className = 'call_up_iframe'; iframe.src = "weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e"; iframe.style.cssText = "display:none;width:0px;height:0px;"; document.body.appendChild(iframe); [代码] 以上代码均可从参考资料中找到出处,感谢 是直接一进来就执行,还是事件触发,都可以。或者是一开始进来就执行,失败了显示几个可选跳转按钮让用户手动触发跳转。 但是关键问题还有一个,如何判断是可以成功唤起了呢?上述 github 代码里提到了一个根据页面 hidden 状态,但不够精准,如果用户没有选择跳转到微信呢?这是另一个需要深究的问题。 出于时间考虑,先以业务交付优先,如果有朋友知道的也可以一起讨论下。 另行文时间短,以技术交流为主,若有瑕疵,欢迎指出。 附上 vue 版本源码:微信 schema 跳转 参考链接: 微信官方文档:urlscheme.generate H5唤起APP进行分享的尝试 AlanZhang001/H5CallUpNative: H5端唤醒移动客户端程序 安卓端,微信schema无法跳转微信小程序?
2021-01-04 - 从小程序分享行为谈页面路由
本文背景本文总结感悟来源于开发挑战答题小程序,在开发挑战答题小程序的时候有个分享复活的功能 本文内容在分享复活的功能开发时,我发现一个很有意思的现象,那就是每次分享之后,onShow这个页面路由钩子函数都被调用了一次,发现这个现象的具体过程是这样 在分享的时候,我有一次写库的操作,这个操作带了uuid,其中uuid是我在onShow里面通过云函数实现的,具体的逻辑代码如下所示 就是下面的onGetUUID,本来这个函数是在onShow里面调用,我在分享发现这个现象之后,把onGetUUID调用挪到onLoad里面了,因为我必须保证一次答题这个UUID是不变的,用来控制每次答题只有一次复活机会 但是按照之前的逻辑,每次分享,onGetUUI就调用一次,导致每次分享UUID都是变化的,就不能起到控制一次答题,只有一次复活的机会了。 f [图片] f 本文总结本文通过开发挑战答题小程序的过程,在分享行为触发时发现onShow会不断执行,这一点在本人之前的认知之外,通过本文记录,让我加深了onShow的触发时机,有利于我对目前分享功能的一些精细行为
2020-07-22 - 企业微信会话存档源码,会话存档开发有多难?会话存档开发流程,会话存档技术开发
很多开发的同学私信阅凛小编请教开发的过程,这里做一个统一的回应,需要的自取。 首先腾讯内部对于会话存档权限是有限制的开放的,这个也容易理解毕竟保存微信用户的聊天是个很敏感的事,如果不法分子用在一些灰产黑产甚至还产生舆论事件的话对腾讯是很大影响的,因此需要服务商提交资料、对客户做一个背书。关于申请流程参考文章《如何申请企业微信会话存档流程》 有了权限后就按照下图的指引做调研 [图片] 对接复杂点 1、SDK集成 获取会话存档内容需要集成腾讯提供的 sdk链接库来进行相关api的调用,目前腾讯只提供JAVA版本和C++版本(阅凛大数据使用java语言对接),虽腾讯有提供调用demo,但提供的demo仅仅只是程序调试版,将sdk嵌入到可运行的项目工程需要对sdk链接库有相关使用经验。 2、数据解密 腾讯为保证会话内容的安全性,会话内容在api接口传输中均使用密文传输,腾讯使用RSA指定模值和算法秘钥进行数据加密;需要针对此加密算法开发数据解密函数,再使用RSA对称性私钥结合返回的密文进行会话内容解密,对于和腾讯接触不多的同学这可能是个难点,毕竟我们从小程序、公众号开始和腾讯打了很多年交道,也参与了会话存档相关功能的内侧,对接起来就方便多了。 3、消息格式处理与消息数据存储 企业微信与微信非常相识,其官方提供的消息类型 如下图: [图片] 对接将要针对企业微信25种消息类型进行不同的处理,同时需要针对海量的会话数据内容进行合理设计和存储,并针对海量会话数据展示和使用提供高效的查询效率,这就是工作量的活了。 4、媒体文件处理 针对消息类型种包含媒体文件的类型,需要通过提供的sdkfileid属性值来调用企业微信提供的媒体流获取接口,获取到流数据再写入到对于的资源对象种进行存储,媒体文件可访问地址 5、变更回调 为提高操作性数据的实时性 如:客户删除企业成员、企业成员删除客户等业务,程序需要设置接收事件服务接口器供腾讯企业微信程序回调,回调数据格式均为xml,程序接收到企业微信回调后需要单独对xml格式数据进行处理解析 6、合规监控 此功能腾讯企业微信并未提供,阅凛会话存档通过市场调研为企业提供特有的会话内容监控功能,针对产生的敏感词,程序将进行实时的数据捕捉,并在系统中提供相应功能对此数据进行展示,供使用企业查阅 虽然腾讯提供了接口能力,但开发上无论是技术难点还是工作量都是有一定难度的。阅凛团队估算了下,需要产品1人、数据架构师1人、前端2人、服务端3人、ui1人、测试1人、运维1人总共10人的团队封闭开发约2个月的时间。如果想要源码的同学可以联系头像
2020-06-10 - 一套代码发布多个微信小程序的实践
之前接手了公司的一个微信小程序项目,上线了一段时间之后,需要以这个项目为基础再发布多个小程序。这些小程序的内容基本上都是一样的,只不过它们有不同的名称、主题、图标等等;或者,某几个小程序需要加一些定制化页面,功能等。本文主要记录下我从纯手工复制项目进化到使用命令行工具复制项目的实践过程。这个简单的命令行工具简单粗暴地叫做 quickcopy,文档在这里。 我是使用 Taro 2.2.13 开发小程序的,所以这个工具目前也是在这个环境下开发的。除了 Taro 插件功能 以外,2.x 都可以使用这个工具。 开发工具前 defineConstants 一开始,因为项目也不多,所以我就直接纯手工操作了。比如,我们已经有了一个小程序叫做 小程序A,现在我们需要以这个小程序为基础复制出一个新的小程序,并且在打包之后实现以下 3 个简单的需求: 设置 [代码]config.window.navigationBarTitleText[代码],在 navigation bar 显示各自的小程序名称; 设置 [代码]config.tabBar.selectedColor[代码],在 tabBar 选中时显示不同的颜色; 为新的小程序定制 [代码]config.tabBar[代码] 图标,小程序A 则继续使用原来的图标。 首先,我们使用全局常量来改造 app.jsx 中的 [代码]config[代码]: [代码]// app.jsx config = { tabBar: { // 改造前: selectedColor: '#000' selectedColor: __MAIN_COLOR, list: [ { // 改造前: 'assets/icons/tabbar-home-s.png' selectedIconPath: 'assets/' + __ICON_DIR + '/tabbar-home-s.png' } ] }, window: { // 改造前: navigaionBarTitleText: '小程序A' navigationBarTitleText: __APP_NAME } } [代码] 然后,在 config 目录下分别为这两个小程序创建 Taro 编译配置文件 build-configA.js 和 build-configB.js,写入 [代码]defineConstants[代码]: [代码]// build-configA.js module.exports = { defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#000'), __ICON_DIR: JSON.stringify('icons') } } // build-configB.js module.exports = { defineConstants: { __APP_NAME: JSON.stringify('小程序B'), __MAIN_COLOR: JSON.stringify('#111'), __ICON_DIR: JSON.stringify('icons-b') } } [代码] 最后,编译打包 小程序A 的时候,我需要在 config/index.js 的最后将 build-configA.js 与基础的编译配置 [代码]merge[代码]。当编译打包 小程序B 的时候也是一样。 [代码]module.exports = function(merge) { return merge({}, config, require('./dev'), require('./build-configA.js')) } [代码] 运行这两个小程序,我们就可以看到它们会显示各自的名称与主题色,小程序B 还会显示定制化的 tabBar 图标。 sass.resource 既然在上面的全局常量中我们已经定义了一个主题色 [代码]__MAIN_COLOR[代码],那么,我们肯定也需要为不同的小程序编写不同的主题样式。 首先,在 src/style/themes 目录下分别为两个小程序创建主题样式文件。然后在 build-configA.js 以及 build-configB.js 中进行全局注入: [代码]// build-configA.js sass: { resource: [ 'src/style/themes/themeA.scss', // build-configB.js 中写 src/style/themes/themeB.scss 'src/style/variable.scss', 'src/style/mixins.scss' ] } [代码] 全局注入后也就不需要在样式文件中一次次写 [代码]@import 'xxx.scss'[代码] 了。但在这里需要注意的是,必须完整的列出需要注入的 3 个文件。虽然像 variable.scss 和 mixins.scss 这种样式文件明显可以在所有项目共享,但如果只在 config/index.js 中注入,而在 build-configA.js 或者 build-configB.js 中只注入主题样式文件的话,是行不通的。 [代码]// build-configA.js sass: { resource: ['src/style/themes/themeA.scss'] } // config/index.js sass: { resource: [ 'src/style/variable.scss', 'src/style/mixins.scss' ], projectDirectory: path.resolve(__dirname, '..') } // 以上两个配置 `merge` 之后的结果是 sass: { resource: [ 'src/style/themes/themeA.scss', 'src/style/mixins.scss' ], projectDirectory: path.resolve(__dirname, '..') } [代码] 也就是说,对于数组来说,是按索引位置进行 [代码]merge[代码] 的。 到现在为止,我们实现了为不同的小程序配置不同的名称,icon 以及主题样式。但是本着能偷懒就偷懒的原则,我觉得这些步骤已经有点麻烦了,可以想象下,如果又有新的项目需要发布,我们需要手动做这些事情: 在 config 目录下创建项目的编译配置文件 config-project.js; 如果需要,为新项目建立定制化的 icons 目录; 为新项目创建主题样式文件,并在 config-project.js 中全局注入; 在 config-project.js 编写 [代码]defineConstants[代码],写入不同项目间有差异的常量,其他的常量则写入 config/index.js; 在 config/index.js 合并新项目的编译配置; 目前所有的项目都共享了根目录下的 project.config.json,所以在编译前需要修改 [代码]appid[代码]。 如果哪一天这些项目都需要进行更新,可以想象下: 首先修改 config/index.js 中需要 [代码]merge[代码] 的项目配置路径; 然后修改 project.config.json 中的 [代码]appid[代码]; 最后编译打包; 如此循环; 那么,上面这些步骤是不是可以交给程序来完成呢?为了尽可能偷懒,我就写了一个简单的命令行工具。它可以代替我们完成以下事情: 以 config/index.js 为模版,提取部分编译配置,创建并写入到新项目的 Taro 编译配置文件; 以根目录 project.config.json 为模版,创建新项目的小程序项目配置文件; 创建新项目的主题样式文件,并在编译配置全局注入; 在打包时寻找新项目有没有定制化图标,如果有,则替换。 开发工具后 假定我们已经有了一份现有项目 小程序A 的编译配置: [代码]// config/index.js const config = { projectName: 'projectA', outputRoot: 'dist', copy: { patterns: [ { from: 'src/components/wxs', to: 'dist/components/wxs' }, // ... ] }, sass: { resource: [ 'src/style/variable.scss', 'src/style/mixins.scss', // ... ], projectDirectory: path.resolve(__dirname, '..') }, defineConstants: { HOST: JSON.stringify('www.baidu.com'), APP_NAME: JSON.stringify('小程序A'), MAIN_COLOR: JSON.stringify('#999'), // ... } } [代码] 在这份编译配置里,指定了项目的输出目录是 dist,全局注入了 variable.scss 和 mixins.scss 文件,并指定了 3 个常量。由于 Taro 不会打包 wxs,所以在 [代码]copy.patterns[代码] 手动将 wxs 复制到了输出目录。 在复制项目之前,我们先对编译配置进行一点改造。在 [代码]defineConstants[代码] 中,我们找到那些不同项目间存在差异的常量,在这里就是 [代码]APP_NAME[代码] 和 [代码]MAIN_COLOR[代码],添加双下划线 [代码]__[代码] 作为开头,这样工具就知道这些常量是存在差异的,而剩余的常量在所有项目中都是一样的。然后在 variable.scss 中找到那些与主题有关的变量,这些变量随后需要写入项目各自的主题样式文件中。 对于已存在的项目 projectA,我们最好也进行一次复制操作。这样一来它就可以拥有独立的编译配置,而 config/index.js 不仅会作为一份基础的编译配置被所有项目共享,也会作为创建新项目独立编译配置时的一份模版。 复制项目 以分离已有的 projectA 项目为例(复制新项目也是类似的),在根目录执行: [代码]qc copy projectA wx123456789a [代码] 工具可以代替我们完成这些工作: 创建 Taro 编译配置文件,路径为 config/config-projectA/index.js; 以根目录 project.config.json 为模版创建微信小程序项目配置文件,路径为 config/config-prjectA/project.config.json; [代码]{ "miniprogramRoot": "dist-projectA/", "projectname": "projectA", "appid": "wx123456789a" } [代码] 其余的内容则会与根目录下的 project.config.json 保持一致; 以 src/style、src/styles 以及 src/css 为顺序查找是否存在这些样式目录。如果存在,则在对应目录下创建 themes/projectA.scss 主题样式文件;如果以上几个目录都不存在,则默认在 src/style 下创建。具体的样式则需要手动写入; 从 config/index.js 找到需要全局注入的样式文件,即 [代码]sass.resource[代码],与上一步创建的主题样式文件一同注入到 config/config-projectA/index.js: [代码]sass: { resource: [ 'src/style/themes/projectA.scss', 'src/style/variable.scss', 'src/style/mixins.scss', ] } [代码] 主题样式文件会放在第一位,以便 variable.scss 和 mixins.scss 可以依赖主题样式。 从 config/index.js 找到需要复制到输出目录的文件,即 [代码]copy.patterns[代码],修改 [代码]to[代码] 指定的路径; [代码]copy: { patterns: [ { from: 'src/components/wxs', to: 'dist-projectA/components/wxs' } ] } [代码] 从 config/index.js 中找到不同项目间具有差异的常量,即 [代码]defineConstants[代码] 中 [代码]__[代码] 开头的常量,并自动添加一个名为 [代码]__PROJECT[代码] 的新常量; [代码]defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#999'), __PROJECT: JSON.stringify('projectA') } [代码] 所以最终的 config/config-projectA/index.js 就像这样: [代码]module.exports = { projectName: 'projectA', outputRoot: 'dist-projectA', defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#999'), __PROJECT: JSON.stringify('prjectA') }, copy: { patterns: [ { from: 'src/components/wxs', to: 'dist-projectA/components/wxs' } ] }, sass: { resource: [ 'src/style/themes/projectA.scss', 'src/style/variable.scss', 'src/style/mixins.scss' ] } } [代码] 至于上文的 icon 问题,因为 Taro 提供了插件能力,所以我们不再需要像上文一样引入 [代码]__ICON_DIR[代码] 常量并改造 [代码]selectedIconPath[代码]。只需要在 config/index.js 的 [代码]plugins[代码] 中添加 [代码]quickcopy/plugin-copy-assets[代码] 即可。 举个例子,我们原本将 icon 放在 src/assets/icons 目录下,如果我们想为 projectA 指定定制化的 [代码]tabBar.list.selectedIconPath[代码],只需要新建一个名为 src/assets/icons-projectA 的目录,在这个目录下存放 projectA 定制化的 icon 即可。 当打包 projectA 的时候,这个插件会去 assets/icons-projectA 查找是否存在定制化的 icon,如果存在,则使用这个 icon,如果不存在,则使用 assets/icons 中默认的 icon。 其他的 icon 也是同样的道理。 编译前准备 当我们需要编译 projectA 的时候,在根目录执行: [代码]qc prep projectA [代码] 工具会做以下两件事情: 创建 config/build.export.js 文件,并将 config/config-projectA/index.js 导出; [代码]const buildConfig = require('./config-projectA/index') module.exports = buildConfig [代码] 将 config/config-projectA/project.config.json 复制到根目录。 我们只需要在 config/index.js 的最后 [代码]merge[代码] build.export.js,随后在根目录执行 Taro 编译指令。 如何添加定制化页面 也许在未来的有一天,我们接到一个需求,需要为 小程序A 添加一个定制化的页面。我们将这个页面路径添加到 app.jsx 的 [代码]config[代码],但又不希望其他小程序打包的时候把这个页面也打包进去。 一开始我使用的方法简单粗暴:在打包其他小程序的时候把这个页面路径注释起来,在打包 小程序A 的时候再把注释打开。 我们可以借助 babel-plugin-preval(在 Taro 文档中也有提到)以及上文的 [代码]__PROJECT[代码] 常量编写逻辑,来确定哪个项目需要打包定制化页面,哪些项目又不需要打包。 首先,把 [代码]config.pages[代码] 提取出来作为一个独立文件,比如: [代码]// pages.js module.exports = function(project) { const pages = [ 'pages/tabBar/home/index', 'pages/tabBar/profile/index' ] if (project == 'projectA') { pages.push('pages/special/index') } return pages } [代码] 然后改造 app.jsx: [代码]const project = __PROJECT class App extends Component { config = { // 这里使用了 project 而没有直接传入 __PROJECT 是因为我在测试的时候发现直接使用 __PROJECT 编译的时候会报错 pages: preval.require('./pages.js', project) } } [代码] 这样一来我们只需要修改 pages.js 就可以添加定制化页面,不仅避免被不需要的项目打包,也能清楚地看出哪些项目有定制化页面哪些没有。对于 [代码]subpackages[代码] 和 [代码]tabBar.list[代码] 也可以做同样的处理。 最后 这个工具到目前为止是根据公司的业务需求开发的,主要功能也并不多,还是有挺大的局限。我也还在探索如何更方便地打包为不同项目编写的定制化页面,所以这个工具还会继续更新下去。
2020-11-18 - 绑定在一个开发者帐号下,同个用户获取到的unionID不一样
只要绑在一个开发者帐号下,即使主体不一样,也允许获取到统一的unionID。绑定同一个微信开放平台帐号下,同一个用户的unionID如果不同的,原因只能是开发者搞混openid。openid要对应所属的AppID,才会相同。 举个例子: 1. 小程序AppID:wxc104eb635b8cxxxx ——帐号A, 公众号AppID:wx311a2a9a8e1dxxxx ——帐号B, 2.核实帐号A和帐号B 绑定同一个微信开放平台帐号是:xxxxxx@sina.com ,所以用一个用户的unionID相同, 3.而开发者所反馈的出现unionID不同,原因是:所提供的openid不属于帐号A,也不属于帐号B,而是属于帐号C或帐号D,而帐号C或帐号D并没有绑定在同一个微信开放平台帐号下,所以unionID不同
2019-09-16 - 微信小程序+webview+百度地图问题的终结答案
1、问题现象 [图片] 2、当前的反馈帖子 https://developers.weixin.qq.com/community/develop/doc/000eca6d5443c05f57ea3a84854400?highLine=webview%2520%2520%25E7%2599%25BE%25E5%25BA%25A6%25E5%259C%25B0%25E5%259B%25BE https://developers.weixin.qq.com/community/develop/doc/000a4c01754120c0f038259dc56800 https://developers.weixin.qq.com/community/develop/doc/000cc0bfab8258a6d3d70f4c751800 3、当前的结论 官方未回答,因为这是百度地图的功能。开发者也是迷茫为什么,该怎么办。 4、百度的开发文档和官方问答分析 http://lbsyun.baidu.com/index.php?title=wxjsapi http://lbsyun.baidu.com/index.php?title=FAQ/wxjsapi [图片] 5、结论: 小程序内web-view能否用百度地图,取决于你引用百度地图后是否有调用百度服务器。如果只是用来渲染地图类或本地交互类的是可以支持的,但是如果是打开网页直接调了百度地图的服务器,就会通不过微信服务器的校验。 如果需要再小程序内调百度地图的服务,只能用原生地图的开发模式,先注册百度地图-微信小程序类型的ak,并下载js文件后,给小程序配置服务器百度地图的request域名后,再调用。 综上所述:百度地图在小程序web-view模式,是无法调用百度地图服务。如果有此需求可以自己规避下,或者切换技术架构或实现方案。
2020-10-15 - Fiddler实现微信授权开发调试
一、下载、安装Fiddler https://link.jianshu.com/?t=https%3A%2F%2Fwww.telerik.com%2Ffiddler 二、微信授权调试 案发现场: 某天,一名正儿八经的开发"猿",在疯狂一顿Coding之后,他完成了微信授权登录功能的编码。下来他想先在本地调试一下,然后再部署到线上环境。于是在本地Run起了Project,假设微信回调的地址是:localhost:9002。这时,他就可以利用Fiddler进行代理测试,具体操作实现请参考以下两种方法。 PS: 请先自行登录微信公众平台进行相关配置。 Fiddler + 微信web开发者工具 打开微信web开发者工具,选择公众号网页开发: [图片] 修改Fiddler中的Hosts配置信息( Fiddler 的 Tools > HOSTS修改host配置) host修改参考https://blog.csdn.net/liguilicsdn/article/details/51286623 [图片] 完成以上配置,即可利用微信web开发者工具在PC本地进行微信授权调试,就这么简单。 Fiddler + 手机(需结合方法1的配置操作) 确保手机、电脑在同一个局域网,查看PC的ip地址 [图片] Fiddler代理配置 [图片] [图片] 手机代理信息配置 [图片] [图片] 完成以上配置,即可使用手机进行微信授权(可自行构造请求微信授权),微信回调后会走PC运行的项目接口,大概就这么简单。 三、推荐两个小工具 内网映射工具(第三种调试方法,具体请参考在线教程):NATAPP https://natapp.cn/ Hosts修改软件:SwitchHosts https://oldj.github.io/SwitchHosts/ 使用eclipse+fiddler+微信web开发者工具调试本地微信页面推荐文章 http://www.cnblogs.com/Gabriel-Wei/p/5981028.html
2019-03-06 - 关于业务域名相关总结
本文背景最近围绕 (如何把web做的一个H5应用,嵌入到小程序内) 这个话题做了一些调研,由于该H5应用用到了微信公众号sdk的不少功能函数 本文内容1、业务域名对多绑定多少个 [图片] 2、小程序里面可以在webview里面使用微信公众号的sdk吗 https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html [图片] 3、业务域名如何配置 这个问题之前总结过,具体请移步 如何配置业务域名? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/0002a64c9489884bf79a1466d51413 本文总结本文主要总结了最近几天调研的外部应用如果内嵌到小程序里面所面临的几个问题
2020-09-10 - 【转】微信小程序中实现瀑布流布局和无限加载
瀑布流布局是一种比较流行的页面布局方式,最典型的就是Pinterest.com,每个卡片的高度不都一样,形成一种参差不齐的美感。 在HTML5中,我们可以找到很多基于jQuery之类实现的瀑布流布局插件,轻松做出这样的布局形式。在微信小程序中,我们也可以做出这样的效果,不过由于小程序框架的一些特性,在实现思路上还是有一些差别的。 今天我们就来看一下如何在小程序中去实现这种瀑布流布局: [图片] 小程序瀑布流布局 我们要实现的是一个固定2列的布局,然后将图片数据动态加载进这两列中(而加载进来的图片,会根据图片实际的尺寸,来决定到底是放在左列还是右列中)。 [代码]/* 单个图片容器的样式 */.img_item { width: 48%; margin: 1%; display: inline-block; vertical-align: top; }[代码]我们知道,在HTML中,我们要动态加载图片的话,通常会使用new Image()创建一个图片对象,然后通过它来动态加载一个url指向的图片,并获取图片的实际尺寸等信息。而在小程序框架中,并没有提供相应的JS对象来处理图片加载。其实我们可以借助wxml中的[图片]组件来完成这样的功能,虽然有点绕,但还是能满足我们的功能要求的。 [代码]<view style="display:none"> <image wx:for="{{images}}" wx:key="id" id="{{item.id}}" src="{{item.pic}}" bindload="onImageLoad">image>view>[代码]我们可以在Page中通过数据绑定,来传递要加载的图片信息到wxml中,让[图片]组件去加载图片资源,然后当图片加载完成的时候,通过bindload指定的事件处理函数来做进一步处理。 我们来看一下Page文件中定义的onImageLoad函数。在其中,我们可以从传入的事件对象e上,获取到[图片]组件的丰富信息,包括通过它加载进来的图片的实际大小。然后我们将图片按照页面上实际需要显示的尺寸,计算出同比例缩放后的尺寸。接着,我们可以根据左右两列目前累积的内容高度,来决定把当前加载进来的图片放到哪一边。 [代码]let col1H = 0;let col2H = 0; Page({ data: { scrollH: 0, imgWidth: 0, loadingCount: 0, images: [], col1: [], col2: [] }, onLoad: function () { wx.getSystemInfo({ success: (res) => { let ww = res.windowWidth; let wh = res.windowHeight; let imgWidth = ww * 0.48; let scrollH = wh; this.setData({ scrollH: scrollH, imgWidth: imgWidth }); //加载首组图片 this.loadImages(); } }) }, onImageLoad: function (e) { let imageId = e.currentTarget.id; let oImgW = e.detail.width; //图片原始宽度 let oImgH = e.detail.height; //图片原始高度 let imgWidth = this.data.imgWidth; //图片设置的宽度 let scale = imgWidth / oImgW; //比例计算 let imgHeight = oImgH * scale; //自适应高度 let images = this.data.images; let imageObj = null; for (let i = 0; i < images.length; i++) { let img = images[i]; if (img.id === imageId) { imageObj = img; break; } } imageObj.height = imgHeight; let loadingCount = this.data.loadingCount - 1; let col1 = this.data.col1; let col2 = this.data.col2; //判断当前图片添加到左列还是右列 if (col1H <= col2H) { col1H += imgHeight; col1.push(imageObj); } else { col2H += imgHeight; col2.push(imageObj); } let data = { loadingCount: loadingCount, col1: col1, col2: col2 }; //当前这组图片已加载完毕,则清空图片临时加载区域的内容 if (!loadingCount) { data.images = []; } this.setData(data); }, loadImages: function () { let images = [ { pic: "../../images/1.png", height: 0 }, { pic: "../../images/2.png", height: 0 }, { pic: "../../images/3.png", height: 0 }, { pic: "../../images/4.png", height: 0 }, { pic: "../../images/5.png", height: 0 }, { pic: "../../images/6.png", height: 0 }, { pic: "../../images/7.png", height: 0 }, { pic: "../../images/8.png", height: 0 }, { pic: "../../images/9.png", height: 0 }, { pic: "../../images/10.png", height: 0 }, { pic: "../../images/11.png", height: 0 }, { pic: "../../images/12.png", height: 0 }, { pic: "../../images/13.png", height: 0 }, { pic: "../../images/14.png", height: 0 } ]; let baseId = "img-" + (+new Date()); for (let i = 0; i < images.length; i++) { images[i].id = baseId + "-" + i; } this.setData({ loadingCount: images.length, images: images }); } })[代码]这里是显示在两列图片的wxml代码,我们可以看到在scroll-view>组件上,我们通过使用bindscrolltolower设置了事件监听函数,当滚动到底部的时候,会触发loadImages去再加载下一组的图片数据,这样就形成了无限的加载:/scroll-view> [代码]<scroll-view scroll-y="true" style="height:{{scrollH}}px" bindscrolltolower="loadImages"> <view style="width:100%"> <view class="img_item"> <view wx:for="{{col1}}" wx:key="id"> <image src="{{item.pic}}" style="width:100%;height:{{item.height}}px">image> view> view> <view class="img_item"> <view wx:for="{{col2}}" wx:key="id"> <image src="{{item.pic}}" style="width:100%;height:{{item.height}}px">image> view> view> view>scroll-view>[代码]好了,挺简单的一个例子,如果你有更好的方法,不吝分享一下哦。 完整代码可以在我的Github下载:https://github.com/zarknight/wx-falls-layout 原作者:一斤代码(简书作者) 原文链接:http://www.jianshu.com/p/260f2623562d
2016-11-29 - 小程序简单两栏瀑布流效果
瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。视觉表现为参差不齐的多栏布局,即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次放入到高度最低的那一栏。 先上代码:https://developers.weixin.qq.com/s/Fgm5s1mz7Wdm 所谓简单,是指只考虑图片,图片之外的其他元素高度固定,不在考虑范围内。 说一下基本的实现思路: 1、加载列表数据 2、在一个隐藏的view中加载图片,通过image组件的bindload获取图片的实际宽高并存储 3、等所有图片加载完成后遍历列表,将图片插入到高度低的那一栏,同时更新该栏高度 我也考虑过在第二步bindload获取到宽高后就直接插入到栏位中,但是会出现小的图片先加载完先出现到页面中,虽然瀑布流不是普通的列表那样的排序,但是也不能小的图片在上面这样太乱顺序,所以就改成了获取宽高先存储,等所有图片加载完成后再往页面上渲染。 来看看实际的代码 不需要渲染到wxml中的数据,我放到了jsData中,主要是两栏的高度和是否在加载数据的标记。 tempPics是第一次加载的数据,临时存放,用于加载图片宽高 columns是两个栏位的实际展示数据 [代码]jsData: { columnsHeight: [0, 0], isLoading: false }, data: { columns: [ [], [] ], tempPics: [] } [代码] 1、加载列表数据 这一步没什么好说的,主要是触发方式,我的代码里是放在页面加载以及拉到页面底部时触发 [代码]onLoad: function() { this.loadData() }, onReachBottom: function() { this.loadData() } [代码] 加载后将列表数据存到tempPics中,用于页面加载获取宽高 2、在一个隐藏的view中加载图片,通过image组件的bindload获取图片的实际宽高并存储 [代码]<view class="hide"> <image wx:for="{{tempPics}}" src="{{item.pic}}" bindload="loadPic" binderror="loadPicError" data-index="{{index}}" /> </view> [代码] 主要是image组件的bindload来获取实际宽高,这里还增加了binderror,防止出现图片加载出错的时候卡死 [代码]loadPic: function(e) { var that = this, data = that.data, tempPics = data.tempPics, index = e.currentTarget.dataset.index if (tempPics[index]) { //以750为宽度算出相对应的高度 tempPics[index].height = e.detail.height * 750 / e.detail.width tempPics[index].isLoad = true } that.setData({ tempPics: tempPics }, function() { that.finLoadPic() }) } [代码] 获取到宽高后,以750为宽度计算出相对应的高度并存储,然后增加一个加载完成的标记。加载出错后就强制高度为750,这样展示的时候就是一个正方形。 单个图片加载完成并存储后调用finLoadPic方法来判断所有图片是否都加载完成。 遍历列表,只要有一个图片没有加载完成的标记,就判断为没有加载完成。 加载完成后进入下一步。 [代码]finLoadPic: function() { var that = this, data = that.data, tempPics = data.tempPics, length = tempPics.length, fin = true for (var i = 0; i < length; i++) { if (!tempPics[i].isLoad) { fin = false break } } if (fin) { wx.hideLoading() if (that.jsData.isLoading) { that.jsData.isLoading = false that.renderPage() } } } [代码] 3、等所有图片加载完成后遍历列表,将图片插入到高度低的那一栏,同时更新该栏高度 这里需要再便利一遍列表,根据当前栏位的高度情况,将图片插入到高度底的那一栏,同时把这一栏高度加上当前图片的高度(不是实际高度,是上一步以750为宽度算出来的高度) [代码]renderPage: function() { var that = this, data = that.data, columns = data.columns, tempPics = data.tempPics, length = tempPics.length, columnsHeight = that.jsData.columnsHeight, index = 0 for (var i = 0; i < length; i++) { index = columnsHeight[1] < columnsHeight[0] ? 1 : 0 columns[index].push(tempPics[i]) columnsHeight[index] += tempPics[i].height } that.setData({ columns: columns, tempPics: [] }) that.jsData.columnsHeight = columnsHeight } [代码] 在wxml中展示的时候image组件的mode要使用widthFix,同时wxss中图片的高度和宽度一样,这样加载出错的图片可以正方形展示 11月21日增加: 根据@杨泉的建议,也尝试了使用wx.getImageInfo来获取图片的宽高(具体代码可以参考评论区),代码也精简了很多。但是实际比较下来速度要比用image组件慢,初步推测原因是[代码]wx.getImageInfo[代码]会返回本地路径,多了写本地临时文件的时间 ps:用到瀑布流的地方,最好能后端直接返回图片的宽高,省去小程序端获取宽高的麻烦 再ps:我个人并不建议小程序端使用瀑布流
2020-01-14 - 小程序跳转数量限制总结
总结一下小程序的跳转能力和数量限制 表格 来源 目标 方案 数量限制 优点 缺点 小程序 小程序 通过原生Api跳转 10个 用户体验最好 仅限跳转10个小程序,且不可动态替换 官方已放开限制 小程序 小程序 通过预览Api,预览小程序码,长按识别跳转 无 无数量限制 操作复杂,需要先预览图片,然后用户长按识别 小程序 小程序 webview打开H5,在h5中长按识别小程序码 无 无数量限制 需要对接额外开发h5,但相比上一方案,可减少一步预览图片 小程序 h5 webview 20个业务域名 减少小程序页面的开发,直接对接已有的h5 需要配置业务域名,限制为20个 公众号h5 小程序 小程序卡片 10个相同主体,3个不同主体 用户体验最好 需要关联公众号,且仅支持微信文章h5,无法动态进入 公众号h5 小程序 长按识别小程序码 无 无限制,可动态生成二维码 需要用户自己长按图片,用户体验较差 app 小程序 微信sdk 50个相同主体、5个不同主体 就这一个方案,没得选 需要在开放平台上关联app和小程序 小程序 app 小程序原生Api 无 就这一个方案,没得选 只能从小程序返回app,不能主动打开app 小程序 公众号 原生关注公众号组件 1 必须是扫码进入小程序的场景,才会出现公众号关注组件,且需要关联公众号和小程序 小程序 公众号 小程序客服消息发送公众号二维码,用户自己长按识别 无 操作复杂,用户体验差 小程序 公众号 通过预览Api,预览公众号二维码,长按识别跳转 无 无数量限制 操作复杂,需要先预览图片,然后用户长按识别 小程序 公众号 webview打开H5,在h5中长按识别公众号二维码 无 无数量限制 需要对接额外开发h5,但相比上一方案,可减少一步预览图片 思维导图 [图片]
2020-06-24 - 微信上传图片被压缩终极解决方案
最近一直在对前期项目进行重构,遇到了之前一个悬而未解的问题,梳理下,寻找可能存在的解决方案 大家都知道微信上传图片被压缩了,但是这种情况是否能解决呢? 1 [图片] 2微信上传图片主要用到以下几个api [图片] 3 目前项目上传方案用到上述的①②两个接口,通过①选择图片,然后通过②获取图片的base64,其中在选择图片时,采用的尺寸模式为压缩。 由于该方案在减少传输和存储压力的同时,极大降低了图片的质量,导致在后续识别过程中,造成非常大的困扰。 同时,由于该方案在压缩模式选择这块,即使选择了原图,部分机型也会存在压缩的情况,并且没有一个明确的清晰的压缩策略,有时候这种压缩比非常大,同样会导致上传的身份证照片带的细节信息丢失,最后的图片甚至人眼不可识别 针对这个问题目前可供参考的解决方案是: 利用上述①③两个接口,在选择图片的时候,将图片上传到微信服务器,即通过微信的uploadImage上传到微信服务器,拿到服务器返回的文件serverId,然后通过素材管理,临时素材管理接口,根据serverId将图片下载到自己服务器,这种方案的优势在于,图片的压缩策略完全是由我们来掌控的,不管具体采用哪个压缩比,都是可以通过代码来控制。 下面简单分析下图片上传用到的几个api,传递参数以及输出相应 ①chooseImage拍照或从手机相册中选图接口 [图片] {"localIds":["wxLocalResource://6110441863775331"],"sourceType":"album","errMsg":"chooseImage:ok"} [图片] ②uploadImage上传图片接口 [图片] {"localId":"wxLocalResource://6110448596555452","serverId":"uNMAdM7ElbVX2m6bqfh77pMGD8t4u8TebDdcjOJpKidsWMKY3F0RHbQPFQp76ACB","errMsg":"uploadImage:ok"} [图片] 备注:上传图片有效期3天,可用微信多媒体接口下载图片到自己的服务器,此处获得的 serverId 即 media_id。 后端从微信服务器下拉图片 https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_temporary_materials.html 属于素材管理里面的获取临时素材接口 [图片] 关于access_token如何生成,具体可以参考下面链接的文档 https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html 可在下面网址进行测试 https://mp.weixin.qq.com/debug/cgi-bin/apiinfo?t=index&type=%E5%9F%BA%E7%A1%80%E6%94%AF%E6%8C%81&form=%E5%A4%9A%E5%AA%92%E4%BD%93%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E6%8E%A5%E5%8F%A3%20/media/upload [图片] 2 [图片] 具体参考文档 https://developers.weixin.qq.com/community/develop/doc/00088493fb47182c6e27b681b54c00 目前该方案已上生产,经得起实践的检验。
2020-05-22 - 给列表添加曝光统计【电商商品列表曝光统计】
场景: 电商平台商品推荐算法,是基于用户浏览商品进行推荐,介绍一下如何获取用户浏览列表的统计方法。 思路: 监听页面滚动 ——>判断用户浏览停留时间——>记录当前可视窗口的商品——>上报统计数据。 解析: 首先在页面滚动事件里监听,当用户停下来浏览超过1秒,获取当前屏幕视口有哪些商品,把获取到的商品记录下来,当用户离开页面上报曝光数据。 效果: [图片] 示例: https://developers.weixin.qq.com/s/YT6bJ7mx7NgE
2020-03-25 - 大数据量swiper滑动优化
问题场景 事情是这样的,我做了一个在线答题小程序,有一个顺序练习模块,每次顺序练习,都要把整个题库过一遍,每个题库题目数量不一,有的几百,有的上千,为方便讨论,我们假定某个题库1000道题目, 具体答题模块是用swiper来实现的,当swiper的数组很大的时候,setData会有三四秒的延迟,我们都知道setData的效率,但是没想到这么厉害。 问题描述问题不是swiper本身,而是setData https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips.html 官方资料https://developers.weixin.qq.com/miniprogram/dev/component/swiper.html 功能演示 [视频] https://developers.weixin.qq.com/community/develop/article/doc/00040235334788f8651a168d951413 这个问题困扰了我几个周时间, 不敢梳头,因为白头发会掉了一地,不要问我为什么是白头发 不敢照镜子,因为黑眼圈更重了; 不敢出门走路,因为问题没有解决,抬不起头来。 [图片] 昨天我在群里发了一个有偿征求优化方案,小伙伴很积极,讨论了一个晚上,第二天就有朋友把优化的方案,递给我,今天中午验证有效,亲测。 [图片] 现在好了,问题得到圆满解决,心情都不一样了,原来处处都是美 [图片] 解决方案大家都能想到方案就是分页,虽然总数组长度为1000,但是每次渲染到swiper的可能只有3、5、7不等的小数组,这样通过setData传递到UI层的时候才不会卡。 但是分页的逻辑要我们自己来控制,怎么判断左滑动,怎么判断右滑动,滑动边界问题,很多细节,总之我为了这个问题花了一个周末没有解决, 就是在大方向明确的情况下,还是写不出来。 具体的实现方案晚点我整理下发出来,同时也希望想挑战的同学锻炼下。 在这里特别感谢群里的两位小伙伴 社区相关帖子 https://developers.weixin.qq.com/community/develop/article/doc/000ecafb3486f07000c92c3225c013 https://developers.weixin.qq.com/community/develop/doc/000e4c77da47208296f8b0b4c51800 感谢@~~娃娃 @~~ dinner
2020-03-25 - 小程序如何上架到企业微信中的第三方?
小程序需先上线再上架,小程序上架流程与H5应用上架流程一致。 企业微信上架流程可参考: https://work.weixin.qq.com/api/doc#90001/90146/90569 https://work.weixin.qq.com/api/doc#90001/90142/90595
2019-12-31 - 官方小程序推荐
小程序推荐 1、在平时 开发,我们经常会遇到内容安全监测的服务,推荐官方小程序 如需体验、接入安全能力,请扫一扫如下小程序二维码进行详细了解。 珊瑚内容安全助手 [图片] 占位 2、在测试小程序时,时常遇到手机兼容性问题,可以扫码获取手机具体信息 [图片] 具体扫码截图如下所示 [图片] [图片] [图片]
2020-11-20 - “小程序直播”支持服务商接入
各位微信开发者: “小程序直播” 功能正在公测中,目前已支持服务商接入。 服务商申请权限之后,可帮助商户快速实现小程序直播功能。 功能简介 “小程序直播” 是微信官方提供的商家经营工具。符合接入要求的商家,通过直播组件,可以在小程序中实现直播互动与商品销售的闭环。 商户准入要求 满足以下条件的电商平台、自营商家,即有机会被邀请到小程序直播公测中来: (同时满足以下1、2、3条件,加上4、5、6条件的其中之一即可。) 1. 属于小程序直播开放类目,具体见《微信小程序直播功能准入要求》 2. 主体下小程序近半年没有严重违规 3. 小程序近90天存在支付行为 4. 主体下公众号累计粉丝数大于100 5. 主体下小程序近7日dau大于100 6. 主体在微信生态内近一年广告投放实际消耗金额大于1w 服务商接入指引 具体接入指引请参考《【小程序直播】服务商接入指引》,以下为服务商接入步骤。 1. 权限申请 1) 在问卷《服务商“小程序直播”接入申请》填写相关信息并等待权限开通,发送申请后7个工作日内,可登陆微信开放平台查看第三方平台权限集并勾选 “小程序直播” 能力; 2) 开通后,即可登陆 “微信开放平台” (open.weixin.qq.com)勾选 “小程序直播” 第三方权限集并全网发布; 2.功能开发 小程序直播需要实现【直播组件】与【后台API】两个部分,其中组件部分需要在小程序中进行配置开发。 具体开发文档,请参考《小程序直播组件接入指引》。
2020-05-18 - Sentry 小程序 SDK
希望对你有所帮助和启发
2019-06-12 - 小程序加急审核流程上线
为优化小程序审核体验,配合各位开发者解决小程序的紧急迭代需求。平台上线了加急审核流程,开发者可根据自身业务情况进行审核加速申请。 1.加急申请入口 符合条件用户在审核提交页面【审核加急】,选择【加急】并填写【“加急类型”“ 加急说明”】情况后提交审核。 [图片] 2.加急次数说明 (1)非个人主体类型:每个自然年有3次申请加急机会 (2)个人主体类型:每个自然年有1次申请加急机会 注:①提审勾选加急后,如在审核前撤回,机会不被消耗;如加急审核单已被审核,无论审核结果通过/不通过,加急机会都将被消耗。请开发者谨慎合理使用加急机会; ②如勾选加急后,审核单被驳回。开发者在12小时内再次提交审核或者通过驳回站内信内的【前往反馈页面】提交反馈,可获得相对加急的审核队列。 3.加急审核时间段与审核时长 (1)非个人主体类型:00:00-23:59 (2)个人主体类型:9:00-22:00 审核时长:正常加急审核预计2小时内完成。请开发者结合审核工作时间及加急单等待时长综合评估提审加急单的时间。 注意:如遇节假日如春节假期前等加急提审队列拥挤,或小程序代码包含复杂逻辑等特殊情况,将无法保证加急审核在2小时内完成。 4. 以下情形的代码提审单暂不支持加急审核 选择国内主体的以下类目或选择海外主体后首次提交代码审核,需报属地网信部门复核,预计审核时长7天左右,暂不支持加急审核。 [图片] 加急审核机会是根据平台审核资源调配,配合开发者遇重大提审节点快速审核迭代的体验优化。每个小程序的加急额度是有限的,请提交前自行检查,确保加速版本的小程序符合法律法规和平台规则,避免浪费有效加急机会。同时,开发者也可以通过小程序评测达标来获取更多的加急机会。 加急审核机制上线后,我们会根据开发者的使用额度情况及审核资源情况等,对目前加速审核机制进行动态调整与优化。
2022-07-26 - 企业微信基础库的版本
企业微信基础库的版本,当前是2.4.0,请问有升级计划吗? 预计什么时候能够升级,升到什么版本?
2020-01-07 - 开发工具模拟器对`env(safe-area-inset-bottom)`的支持
代码 ``` .app-tabbar{ padding-bottom: calc(constant(safe-area-inset-bottom) / 2); padding-bottom: calc(env(safe-area-inset-bottom) / 2); } ``` - 当前 Bug 的表现(可附上截图) [图片] 没有任何底间距 但在iOS真机上是OK的,安卓真机不行! [图片] - 预期表现 模拟器和真机效果一样
2019-02-27 - wx.requestSubscribeMessage 小程序模版消息升级为订阅消息,
还要从那不久前的炎炎夏日说起, 一位苦逼的前端小妹, 为了加模版消息,熬了好几个加班夜, 动了几十个页面, 修改了几百个按钮, 终于把模版消息都全面埋雷, 不留任何死角. 也就过了才 1 2 3 4 5个月吧, 订阅消息一出. 我们的前端小妹, 那脸色、那眼神、我至今找不到一个合适的词语来形容(主要是笔者词穷) 下面还是主要来说说订阅消息吧, 不然对不起读者. 升级第一步: 注意订阅消息是有最低版本库要求的 (这个主要是需要产品和客户同步, 不是所有人都能订阅哦) 注意:iOS客户端7.0.6版本、Android客户端7.0.7版本之后的一次性订阅/长期订阅才支持多个模板消息,iOS客户端7.0.5版本、Android客户端7.0.6版本之前的一次订阅只支持一个模板消息 升级第二步: 干就完了 官方 api 地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/subscribe-message/wx.requestSubscribeMessage.html wx.requestSubscribeMessage(Object object) [图片] 撸起袖子就是干 小手一抖, 代码全有 [图片] 屏幕一点, 效果立显 [图片] 友情提示: 这里 tmplIds 用的是订阅消息 id 哦, 不要搞错咯 这里必须是在手机上才能看到效果哦 每次弹框, 只能配置最多 3 个订阅消息哦 如果勾选了: “总是保持以上选择, 不再询问” , 真的就是“此生不复相见哦”, 删除了小程序也不管用哦 由于弹出的 3 个订阅消息, 是可以单独勾选, 就会出现如果某 1 个配置的已经“总是保持以上选择, 不再询问”. 那么弹框只会显示其他两个哦, 属于正常情况. 不要以为是哪里错了哈. 并且返回的结果还是 3 个(不要晕哈, 说的啰嗦, 其实不难理解) 小框框弹出来了, 返回也很顺利拿到了, 抿一口手边的枸杞菊花茶, 是不是很舒服? 上面说到了, “总是保持以上选择, 不再询问” , 就是“此生不复相见哦”, 那么如果之前手贱, 点了拒绝. 那如何才能重新订阅呢? 小生也是研究了的: 操作步骤: 右上角点点点, 》 设置 》订阅消息 效果一目了然 [图片] 前方高能: 设置里的订阅消息, 它此生的标签是 “总是保持以上选择, 不再询问”, 不管是你允许还是拒绝, 都不会在弹框里再看到 写在最后 提一个更苦逼的事情, 记得把之前为获取 formid 而写的代码, 统统删掉 写在最最后 给不给点赞? 不点我下一篇还问 未完待续… 以下是补充哦, 持续关注订阅消息 一: 一个模版, 在首页勾选了“总是保持以上选择, 不再询问”按钮. 在别的页面也将不会有弹框. 你懂的, 在产品角度这个是很重要、很关键的交互需求 二: 同意次数是可以累计的. 也就是说, 一个模版, 客户A点击了 10 次允许发送消息. 那我们就可以给他发 10 次模版消息提醒 三: 一次拒绝, 是不会清除之前同意所累计的次数的 这个是针对有网友说: “点一次同意, 再点击一次拒绝,是收不到消息的”. 实践证明: 点一次同意, 就能发一次消息, 后面点击拒绝, 不影响之前点击同意的 四: 有网友问: ”一个弹框有三个模版, 全都勾选并同意. 可以发几条消息? “ 很明显是三个模版每个可以发一次. 这个也是验证了的 干活!! 是不是满满的都是干活!
2019-12-09 - 小程序没有 DOM 接口,原因竟然是……?
拥有丰富的 Web 前端开发经验的工程师小赵今天刚刚来到新的部门,开始从事他之前没有接触过的微信小程序开发。在上手的第一天,他就向同办公室的小程序老手老李请教了自己的问题。 小赵:翻了一圈文档,小程序好像并不提供 DOM 接口?我还以为可以像之前一样用我喜欢的前端框架来做开发呢。老李,你说小程序为什么不给我们提供 DOM 接口呀。 老李:要提供 DOM 接口也没那么容易。你知道小程序的双线程模型吗?(小赵漏出了疑惑的表情)小程序是基于 Web 技术的,这你应该知道,但小程序和普通的移动端网页也不一样。你做了很多前端项目了,应该知道在浏览器里,UI 渲染和 JavaScript 逻辑都是在一个线程中执行的? 小赵:这我知道,在同一个线程中,UI 渲染和 JavaScript 逻辑交替执行,JavaScript 也可以通过 DOM 接口来对渲染进行控制。 老李:小程序使用的是一种两个线程并行执行的模式,叫做双线程模型。像我画的这样,两个线程合力完成小程序的渲染:一个线程专门负责渲染工作,我们一般称之为渲染层;而另外有一个线程执行我们的逻辑代码,我们一般叫做逻辑层。这两个线程同时运行,并通过微信客户端来交换数据。在小程序运行的时候,逻辑层执行我们编写的逻辑,将数据通过 setData 发送到渲染层;而渲染层解析我们的 WXML 和 WXSS,并结合数据渲染出页面。一方面,每个页面对应一个 WebView 渲染层,对于用户来说更加有页面的感觉,体验更好,而且也可以避免单个 WebView 的负担太重;另一方面,将小程序代码运行在独立的线程中的模式有更好的安全表现,允许有像 open-data 这样的组件可以在确保用户隐私的前提下让我们展示用户数据。 [图片] 小赵:怪不得所有和页面有关的改动都只能通过 setData 来完成。但是用两个线程来渲染我们平时用单线程来渲染的 Web 页面,会不会有些「浪费」?而且每一个页面有一个对应的渲染层,那页面变多的时候,岂不是会有很大的开销? 老李: 并不浪费,因为界面的渲染和后台的逻辑处理可以在同一时间运行了,这使得小程序整体的响应速度更快了。而在小程序的运行过程中,逻辑层需要常驻,但渲染层是可以回收的。实际上,当页面栈的层数比较高的时候,栈底页面的渲染层是会被慢慢回收的。 小赵: 原来如此。这么说的话,实际的 DOM 树是存在于渲染层的,逻辑层并不存在,所以逻辑层才没有任何的 DOM 接口,我明白了。但是……既然可以实现像 setData 这样的接口,为什么不能直接把 DOM 接口也代理到逻辑层呢?我觉得小程序可以做一个封装,让我们在逻辑层调用 DOM 接口,在渲染层调用接口后再把结果返回给我们呀。 老李:从理论上来说确实是可以的。但是线程之间的通信是需要时间的呀。将调用发送到渲染层,再将 DOM 调用结果发送回来,这中间由于线程通信发生的时间损耗可能会比这个接口本身需要的时间要多得多。如果以此为基础使用基于 DOM 接口的前端框架,大量的 DOM 调用可能会非常缓慢,让这个设计失去意义。 在实际测试中,如果每次 DOM 调用都进行一次线程通信,耗时大约是同等节点规模直接在渲染层调用的百倍以上;如果忽略通信需要的时间,一个实现良好的基于 DOM 代理的框架可以近似地看成一个动态模板的框架,而动态模板和静态模板相比要慢至少 50% 小赵:原来如此,线程通信的时间确实是我没有考虑到的问题。那现在的小程序框架中难道不存在这个问题吗? 老李: 在现在的小程序框架中,这个问题也是存在的,这也是现在的框架基于静态模板渲染的原因。静态模板可以在运行前就做好打包,直接注入到渲染层,省去线程传输的时间。在运行时,逻辑层只和渲染层进行最少的、必要的数据交换:也就是渲染用的数据,或者说 data 。另一方面,静态模板让两个线程都在启动时就拥有模板相关的所有数据,所以框架也充分利用了这一点,进行了很多优化。 小赵: 怪不得我在文档里发现很多和 setData 有关的性能提示,都提醒尽量减少设置不必要的数据,现在总算是知道为什么了。但是具体到实际开发里的时候,还是总觉得很难每次只设置需要的数据啊,像对象里或者数组里的数据怎么办呢? 老李: 如果只改变了对象里或者数组里的一部分数据,可以通过类似 array[2].message , a.b.c.d 这样的 数据路径 来进行「精准设置」。另外,现在自定义组件也支持 纯数据字段 了,只要在自定义组件的选项中设置好名为 pureDataPattern 的正则表达式, data 中匹配这个正则的字段将成为纯数据字段,例如,你可以用 /^_/ 来指定所有 开头的数据字段为纯数据字段。所有纯数据字段仅仅被记录在逻辑层的 this.data 中,而不会被发送到渲染层,也不参与任何界面渲染过程,节省了传输的时间,这样有助于提升页面更新性能。 小赵:小程序还有这样的功能,受教了。不过说来说去,我还是想在小程序里用我顺手的框架来开发,毕竟这样事半功倍嘛。我在网上搜索了一下,发现现在有很多支持用 Web 框架做小程序开发的框架,但好像都是将模板编译成 WXML,最终由小程序来做渲染,但这样的方法好像兼容性也不是很好。我在想,我们能不能在逻辑层仿造一套 DOM 接口,然后在运行时将 DOM 调用适配成小程序调用? 老李: 你的这个脑洞有一些意思。在逻辑层仿造一套 DOM 接口,直接维护一棵 DOM 树,这当然没问题。但是没有代理 DOM 接口,逻辑层的 DOM 树没法反映到渲染层,因为渲染层具体会出现什么样的组件,是运行时才能知道的,这不就没法生成静态模板了? 小赵:静态模板确实是没法生成了,但我看到小程序的框架支持自定义组件,我是不是可以做一个通用的自定义组件,让它根据传入的参数不同,变成不同的小程序内置组件。而且自定义组件还支持在自己的模板中引用自己,那么我只需要一个这个通用组件,然后从逻辑层用代码去控制当前组件应该渲染成什么内置组件,再根据它是否有子节点去递归引用自己进行渲染就可以了。你看这样可行吗? [图片] 老李: 这样的做法确实可行,而且微信官方已经按照这个思路推出小程序和 Web 端同构的解决方案 Kbone 了。Kbone 的原理就像你刚才说的那样,它提供一个 Webpack 插件,将项目编译成小程序项目;同时提供两个 npm 包,分别提供 DOM 接口模拟和你说的那个通用的自定义组件作为运行时依赖。要不你赶紧试试? 小赵:还有这么好的事,那我终于可以用我喜欢的框架开发小程序了!这么好的框架,为什么不直接内置到小程序的基础库里呀? 老李: 因为这样的功能完全可以用现在已有的基础库功能实现出来呀。Kbone 现在是 npm 包的形式,使得它的功能、问题修复可以随着自己的版本来发布,不需要依赖于基础库的更新和覆盖率,不是挺好的吗? 小赵: 好是好,但我担心的是代码包大小限制的问题。除了我们已经写好的业务逻辑之外,现在还得加上 Kbone,会不会装不下呀? 老李: 原来你是担心这个呀,放心,Kbone 现在已经可以在 扩展库 里一键搞定啦。扩展库是帮我们解决依赖的全新功能,只要在配置项中指定 Kbone 扩展库,就相当于引入了 Kbone 相关的最新版本的 npm 包,这样就不占用小程序的代码包体积了,快试试吧! 小赵:哇,那可太爽了,马上就搞起! 最后 如果你对 Kbone 感兴趣或者有相关问题需要咨询, 欢迎加入 Kbone 技术交流 QQ 群:926335938
2020-01-14 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - 小程序利用safe-area-inset-*兼容iPhoneX
分别创建屏幕上边框,右边框,下边框,左边框安全距离: safe-area-inset-top, safe-area-inset-right, safe-area-inset-bottom, safe-area-inset-left 使用: iOS 11 padding-top: constant(safe-area-inset-top); padding-right: constant(safe-area-inset-right); padding-bottom: constant(safe-area-inset-bottom); padding-left: constant(safe-area-inset-left); iOS 11.2 beta及其后 padding-top: env(safe-area-inset-top); padding-right: env(safe-area-inset-right); padding-bottom: env(safe-area-inset-bottom); padding-left: env(safe-area-inset-left); 兼容性写法: padding-top: 10px; padding-top: constant(safe-area-inset-top); padding-top: env(safe-area-inset-top); 与calc合用: padding-top: 10px; padding-top: calc(10px + constant(safe-area-inset-top)); padding-top: calc(10px + env(safe-area-inset-top)); 终!使用sass@mixin: @mixin x-padding-bottom($val:0px) { padding-bottom: $val; padding-bottom: calc(#{$val / 2} + constant(safe-area-inset-bottom)); /* no */ padding-bottom: calc(#{$val / 2} + env(safe-area-inset-bottom)); /* no */ } 注意!!! 1、默认值为0px,不是0,原因是calc不支持与0计算。 2、小程序单位为rpx,一般都会转换为rpx,但是calc不支持,所以不允许转换,保持px。 参考文档:苹果官方文档
2019-10-11 - 小程序取消橡皮筋回弹效果解决方案及坑总结
提到ios系统的橡皮筋效果,作为开发者是又爱又恨,有想要这个效果又有不想要的,无奈的是却没有一个简单的开关来设置这个效果是否开启。 最近在开发小程序时也遇到有关于ios橡皮筋回弹的问题,这里分两部分(取消橡皮筋回弹效果和因为这个效果遇到的坑)和大家分享一下。 取消IOS橡皮筋回弹效果的解决方案 1) 页面无滚动区域时,可通过页面json配置文件设置disableScroll:true禁止整个页面滚动,从而取消橡皮筋效果。 [代码]{ "disableScroll":true } [代码] 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo1 2) 页面有滚动区域,滚动区域通过view模拟实现,然后在页面json配置文件设置disableScroll:true禁止整个页面滚动,从而取消橡皮筋效果。 [代码]json文件配置 { "disableScroll":true } view元素模拟实现滚动样式 { height: calc(100vh - 120rpx); //高度必须是固定的值 overflow-y: auto; } [代码] 不足之处在于view元素模拟的滚动区域滚动时不够连贯,没有scroll-view那种原生丝滑般的感觉。 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo2 3) 页面有滚动区域,滚动区域使用scroll-view,这时通过disableScroll则无法实现,尝试设置一下scroll-view的scroll-y="{{false}}",上拉或下拉时居然不再触发橡皮筋的回弹啦,当然滚动区域也不能滚动。 小脑袋动一动,解决方法有啦! 通过设置一个变量scrollY动态控制滚动区域的滚动从而阻止回弹。 监听bindscrolltoupper\bindscrolltolower当scroll-view区域滚动到顶部或底部时候设置scrollY:false来关闭页面滚动,从而阻止回弹。 监听bindtouchstart\bindtouchmove 当用户反方向滑动的时候设置scrollY:true,再次开启页面滚动。 [代码]wxml滚动区域属性和事件处理,具体实现请点击测试代码链接 <scroll-view scroll-y="{{scrollY}}" class="list" upper-threshold="5" lower-threshold="5" bindscrolltoupper="bindscrolltoupper" bindscrolltolower="bindscrolltolower" bindtouchstart="touchstart" bindtouchmove="touchmove"> <view class="list-item" wx:for="{{list}}" wx:key="{{index}}">{{item}}</view> </scroll-view> [代码] 相对view模拟实现滚动区域,scroll-view滚动更丝滑,不过每次滚动到底部或顶部的时候,再反向滑动时由于再次开启scroll-view滚动会有操作卡顿的感觉,暂时没想到好的解决方法,有解决的大佬希望提供一下想法,一起学习下。 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo3 IOS橡皮筋效果遇到的坑 1) 操作左滑删除组件时上下移动,会触发橡皮筋效果导致页面抖动的问题 这个坑的严重程度看设计师的意愿了,反正我们团队目前是需要解决的,方案类似取消橡皮筋解决方案的第三种 在左滑的时候关闭scroll-view的滚动,取消时再次开启滚动 2) 如果页面顶部有置顶的横向滚动区域scroll-view,当页面滚动到底部时继续上拉会导致置顶头部消失,松开回弹后头部又会出现。 坑是社区里的朋友提出来的,我借了个iphone x 一预览,我嚓,还是真是个奇坑! 微信官方回复已复现正在解决中… 不想继续等下去的,暂时解决方法是 监听页面的滚动区域,当滚动到底部时设置顶部横向滚动scroll-view的scroll-x=false来解决。 写在最后 以上便是我在小程序开发中有关于ios橡皮筋回弹效果的分享,示例代码已上传github,可自行下载体验。 https://github.com/YuniorZen/minicode-debug/tree/master/minicode01 目前微信官方虽说已经着手解决(已两年)此类bug,但哪吒说 我命由我不由天,所以还是我们开发者多分享些解决方案自救来的快。 分享方案如有问题还望不吝指出,没有涉及到的坑也欢迎评论提出,一起学习和解决,后续也会基于此篇不断更新总结。
2021-01-14 - 阿里云服务无法校验业务域名的一种解决方式
发生错误场景: [图片] 解决方法:需要服务器名称指示不要打钩(见下图) [图片] 我是阿里云的服务器,目前我以这种方案解决这个问题。。大家可以参考试试
2019-07-04 - 腾讯云 微信小程序 即时通信IM demo
产品简介 即时通信(Instant Messaging,IM)基于QQ 底层 IM 能力开发,仅需植入 SDK 即可轻松集成聊天、会话、群组、资料管理能力,帮助您实现文字、图片、短语音、短视频等富媒体消息收发,全面满足通信需要。 应用场景 客服咨询 即时通信 IM 可满足商家与用户多场景沟通的需要,为客户提供专属客服服务,提升服务效率,通过与智能机器人结合,可有效降低人力成本,沉淀客户价值。 [图片] 直播弹幕 即时通信 IM 可支持弹幕、 送礼和点赞等多消息类型,轻松打造良好的直播聊天互动体验;提供弹幕内容审核能力,保证您的直播免受不雅信息干扰。 [图片] 网红带货 即时通信 IM 与商业直播相结合,通过提供点赞、询价、购物券等特定消息类型,帮助直播客户实现流量变现。 [图片] 教学白板 即时通信 IM 为可提供在线课堂,文本消息,画笔轨迹等能力,轻松实现教师学生沟通、画笔轨迹保存、大班课与小班课教学等教学场景。 [图片] 社交沟通 即时通信 IM 可实现单聊、群聊、弹幕等多种聊天模式,支持文字、图片、语音、短视频等多种消息类型,有效提升用户粘性与活跃度。 [图片] 企业办公 即时通信 IM 为企业客户提供覆盖桌面与移动端的完整解决方案,满足设备无缝切换的需求,提高企业内外沟通效率。 [图片] 智能设备 即时通信 IM 提供人与物、物与物协同通信,携手共进引领 5G 通信时代潮流。 [图片] 快速体验,IMSDK小程序demo运行 本 IM 小程序 demo 是基于 MpVue 框架进行开发的。[代码]一分钟跑通 demo[代码] 小节只是用于引导您打开编译后的文件进行快速预览,如果您想要进行二次开发,请看[代码]开发运行[代码]小节。 一分钟跑通demo 克隆仓库到本地 [代码]# 命令行执行 git clone https://github.com/tencentyun/TIMSDK.git # 进入小程序 Demo 项目 cd TIMSDK/WXMini [代码] 安装微信小程序 开发者工具。 使用微信开发者工具导入项目,请注意目录为 [代码]/dist/wx[代码],然后填入自己的小程序 AppID。 [图片] 配置 [代码]SDKAPPID[代码] 和 [代码]SECRETKEY[代码],获取方式参考:密钥获取方法 打开 [代码]/debug/GeneraterUserSig.js[代码] 文件 按图示填写相应配置后,保存文件 [图片] 本地配置如下图所示 勾选ES6转ES5选项 勾选不检验合法域名选项 基础库版本 > 2.1.1 [图片] 点击编译即可运行 [图片] 注意事项 合法域名 如果您要发布小程序,请将以下域名在【微信公众平台】>【开发】>【开发设置】>【服务器域名】中进行配置 进入微信公众平台,在小程序开发的服务器域名配置相关域名信息 添加到 request 合法域名: 域名 说明 是否必须 [代码]https://webim.tim.qq.com[代码] Web IM 业务域名 必须 [代码]https://yun.tim.qq.com[代码] Web IM 业务域名 必须 [代码]https://pingtas.qq.com[代码] Web IM 统计域名 必须 添加到 uploadFile 合法域名: 域名 说明 是否必须 [代码]https://cos.ap-shanghai.myqcloud.com[代码] 文件上传域名 必须 添加到 downloadFile 合法域名: 域名 说明 是否必须 [代码]https://cos.ap-shanghai.myqcloud.com[代码] 文件下载域名 必须 [图片] 开发运行 项目目录 [代码]├───sdk/ - 存放tim-wx.js,demo 中未使用,仅供自行集成 ├───build/ ├───config/ ├───dist/ │ └───wx/ - MpVue 项目编译后文件目录,使用小程序开发工具导入此文件夹 ├───src/ │ ├───components/ - 组件 │ ├───pages/ - 页面 │ ├───store/ - Vuex 目录 │ ├───stylus/ - 全局主题色样式,可以修改全局颜色 │ ├───utils/ - 方法 │ ├───app.json │ ├───App.vue │ └───main.js ├───static/ - 静态依赖资源 │ ├───debug/ - 包含 userSig 验证登录方法 │ ├───images/ - 图片 │ └───iview/ - 使用的 iview 组件 ├───_doc/ ├───.babelrc ├───.editorconfig ├───.eslintignore ├───.eslintrc.js ├───.postcssrc.js ├───index.html ├───package-lock.json ├───package.json ├───project.config.json └───README.md [代码] 准备工作 获取到您应用的 [代码]SDKAPPID[代码] 和 [代码]SECRETKEY[代码],方式参考:密钥获取方法 安装微信小程序 开发者工具 安装 nodejs 环境 ( Version > 8 ) ,选择合适您安装环境的安装包 安装后,在命令行输入[代码]node --version[代码] ,如果 > 8 即可 启动流程 克隆仓库到本地 [代码]# 命令行执行 git clone https://github.com/tencentyun/TIMSDK.git # 进入 Demo 项目 cd TIMSDK/WXMini [代码] 将[代码]project.config.json[代码]文件中的[代码]appid[代码]修改为自己微信小程序的[代码]appid[代码] [图片] 配置 [代码]SDKAPPID[代码] 和 [代码]SECRETKEY[代码],获取方式参考:密钥获取方法 打开 [代码]/static/debug/GeneraterUserSig.js[代码] 文件 按图示填写相应配置后,保存文件 [图片] 安装依赖并启动 [代码]# 安装demo构建和运行所需依赖 npm install # 构建并生成最终可在小程序开发工具内使用的代码 npm run start [代码] 使用 [代码]npm install[代码] 命令,如果有些依赖包无法成功安装 您可以试着切换源, 例如: [代码]npm config set registry http://r.cnpmjs.org/[代码] 然后再执行 [代码]npm install[代码] 使用微信开发者工具导入项目,目录为[代码]/dist/wx[代码] [图片] 本地配置如下图所示 勾选ES6转ES5选项 勾选不检验合法域名选项 基础库版本 > 2.1.1 [图片] 点击开发工具的编译即可预览该项目 [图片] 注意事项 合法域名 如果您要发布小程序,请将以下域名在【微信公众平台】>【开发】>【开发设置】>【服务器域名】中进行配置 进入微信公众平台,在小程序开发的服务器域名配置相关域名信息 添加到 request 合法域名: 域名 说明 是否必须 [代码]https://webim.tim.qq.com[代码] Web IM 业务域名 必须 [代码]https://yun.tim.qq.com[代码] Web IM 业务域名 必须 [代码]https://pingtas.qq.com[代码] Web IM 统计域名 必须 添加到 uploadFile 合法域名: 域名 说明 是否必须 [代码]https://cos.ap-shanghai.myqcloud.com[代码] 文件上传域名 必须 添加到 downloadFile 合法域名: 域名 说明 是否必须 [代码]https://cos.ap-shanghai.myqcloud.com[代码] 文件下载域名 必须 [图片] 项目截图 [图片] 备注 页面结构 目录 /src/pages 页面 简介 login/ 登录页 index/ 首页,对话列表 chat/ 聊天对话页 & 群信息/用户信息 contact/ 通讯录 own/ 个人信息 create/ 创建群聊 members/ 群成员 profile/ 修改个人信息 groups/ 群列表 groupDetail/ 群详细页 system/ 系统通知页 blacklist/ 黑名单页 detail/ 个人信息&群信息 friend/ 发起会话 mention/ @选择页 注意事项 1. 避免在前端进行签名计算 本 Demo 为了用户体验的便利,将 [代码]userSig[代码] 签发放到前端执行。若直接部署上线,会面临 [代码]SECRETKEY[代码] 泄露的风险。正确的 [代码]userSig[代码] 签发方式是将 [代码]userSig[代码] 的计算代码集成到您的服务端,并提供相应接口。在需要 [代码]userSig[代码] 时,发起请求获取动态 [代码]userSig[代码]。更多详情请参见 服务端生成 UserSig。 2. 如果无法访问github或者访问速度过慢 下载zip包 解压后,进入 TIMSDK/WXMini目录,即可查看demo代码。
2019-09-16 - 微信小程序三种授权登录的方式
经过一段时间对微信小程序的研发后 总结出以下三种授权登录的方式,我给他们命名为‘一次性授权’‘永久授权’‘不授权’ 1.一次性授权 常规写法,需要获取用户公开信息(头像,昵称等)时,判断调取授权登录接口,但是此方法如果不经处理的话 用户如果拒绝授权或者删除该微信小程序后 需要重新调取并获取用户公开信息(头像,昵称等),此方法用户体验较差,不建议使用; 2.永久授权 在不必要使用用户公开信息(头像,昵称等)时,不调取授权登录接口,只有在必要的时候再去判断调取授权登录接口并把获取到的用户公开信息存入数据库,这样在每次登录时直接先运行指定函数从数据库索取需要的用户公开信息(头像,昵称等)即可,此方法在删除小程序后不用再次去授权登录(因为在用户第一次授权登录时已经把用户的公开信息存入数据库了以后直接向数据库索取即可),建议使用; 3.不授权 不需要授权登录获取用户公开信息(头像,昵称等),使用wx.login获取用户code并传入后台,后台可以通过用户的code值向微信要一个值(具体需要问后台,我只是个小前端,后台的东西不是很懂,只是知道一些逻辑而且也已经成功实现)然后通过这个用code换取的值就可以识别到指定用户,如果需要的话,前端要显示的头像、昵称等这些信息可以使用自定义可编辑的功能,当然,也可以通过<open-data type=“userAvatarUrl”></open-data><open-data type=“userNickName”></open-data>小程序提供的这个组件显示用户的头像及昵称(不过这个组件只有显示功能),用户如果想直接使用自己的头像昵称,也可以自行授权(比如添加个引导按钮什么之类的),建议使用; [图片][图片] 文中使用的微信自带接口、组件及函数: <open-data type=“userAvatarUrl”></open-data> <open-data type=“userNickName”></open-data> wx.login({ success(res){ console.log(res.code) } }) 微信授权登录 以上三种方式可以灵活运用,也可以把需要的结合到一起,并不冲突; 当然,大佬很多,我也只是个小前端而已,第一次发表技术方面的帖子,希望互相学习,互相指导,如有说的不对的地方还望大佬们及时指出!!! 谢谢
2019-04-18 - 筛选分类,阻止底层页面穿透滚动
采用movable-area,movable-view,catchtouchmove实现筛选时,阻止底层页面穿透滚动,根据之前的项目需求,做了相应的简化: movable-area,movable-view实现弹窗内筛选项的滚动 catchtouchmove阻止页面滚动 https://developers.weixin.qq.com/s/xUPdx7mX75b8
2019-09-04 - downloadFile下载图片的时候 返回的图片类型为unknown
- 当前 Bug 的表现(可附上截图) 这个图片是nodejs的puppeteer进行网页截图获得的文件流。ios上是正常的,安卓的为unknown。 图片路径如下: https://mp.51polestar.com/goodteacher_api/toImg?height=1206&url=https%3A%2F%2Fmp.51polestar.com%2FbjxH5%2Fjq_h5%2Fposter.html%3Fname%3DChangeZ%26student_id%3D456835087%26code%3D-1%26task_id%3D24928%26subject_name%3D%25E8%25AF%25AD%25E6%2596%2587%26sum_score%3D100%26knowledge_num%3D5%26stem_num%3D1%26depth%3D2%26task_title%3D%25E5%2591%259C%25E5%2591%259C%26headImg%3Dhttps%253A%252F%252Fwx.qlogo.cn%252Fmmopen%252Fvi_32%252F4VKQ8UlUu9pT06CPcTIJ5vDibORfVLJghggl1dxjgGy3GAW1ia14G8JDKTNCibnEaZpGaIIOTI3u8C4BFhw5NIldw%252F132%26token%3D9513ec217f6f495c90bffb01994081d0 - 预期表现 能返回正常路径 - 复现路径 - 提供一个最简复现 Demo
2019-01-21 - 使用BackgroundAudioManager背景音频实现一个音频播放器
说明 使用BackgroundAudioManager创建的实例,小程序切换到手机后台、小程序内页面间跳转,都不会影响音频的连续播放,可以很好的实现一个音频播放器。 BackgroundAudioManager是单实例,全局唯一,在任意页面任何位置调用wx.getBackgroundAudioManager()既可以获得。 效果 音频列表循环播放,支持上一首、下一首切换,实时进度展示,快进。 思路 将播放的音频列表放在app.globalData或本地做缓存,保证音频切换时找到对应列表。 将音频播放的实时状态放在app.globalData或本地做缓存,保证展示音频播放详情页的音频名称、实时进度等正确展示。 方法中BackgroundAudioManager.on*为监听事件,操作业务放在回调函数中处理。 BackgroundAudioManager的属性中,所有属性可以直接BackgroundAudioManager.获取值,非只读的属性可以通过BackgroundAudioManager. = ‘’ 方式赋值。 效果图 小程序界面 [图片] 手机后台,顶部下拉 [图片] 代码片段 详细代码请下载代码片段,可以直接运行demo。 https://developers.weixin.qq.com/s/VAmjRsmZ7090
2019-06-28 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: Android、IOS手机对于页面title的展示不一致,安卓title的显示不居中 页面的title只支持纯文本级别的样式控制,不能够做更丰富的title效果 左上角的事件无法监听、定制 路由导航单一,只能够返回上一页,深层级页面的返回不够友好 探索 小程序自定义导航栏已开放许久>>了解一下,相信不少小伙伴已使用过这个功能,同时不少小伙伴也会发现一些坑: 机型多如牛毛:自定义导航栏高度在不同机型始终无法达到视觉上的统一 调皮的胶囊按钮:导航栏元素(文字,图标等)怎么也对不齐那该死的胶囊按钮 各种尺寸的全面屏,奇怪的刘海屏,简直要抓狂 一探究竟 为了搞明白原理,我先去翻了官方文档,>>飞机,点过去是不是很惊喜,很意外,通篇大文尽然只有最下方的一张图片与这个问题有关,并且啥也看不清,汗汗汗… 我特意找了一张图片来 [图片] 分析上图,我得到如下信息: Android跟iOS有差异,表现在顶部到胶囊按钮之间的距离差了6pt 胶囊按钮高度为32pt, iOS和Android一致 动手分析 我们写一个状态栏,通过wx.getSystemInfoSync().statusBarHeight设置高度 Android: [图片] iOS:[图片] 可以看出,iOS胶囊按钮与状态栏之间距离为:4px, Android为8px,是不是所有手机都是这种情况呢? 答案是:苹果手机确实都是4px,安卓大部分都是7和8 也会有其他的情况(可以自己打印getSystemInfo验证)如何快速便捷算出这个高度,请接着往下看 如何计算 导航栏分为状态栏和标题栏,只要能算出每台手机的导航栏高度问题就迎刃而解 导航栏高度 = 胶囊按钮高度 + 状态栏到胶囊按钮间距 * 2 + 状态栏高度 注:由于胶囊按钮是原生组件,为表现一致,其单位在各种手机中都为px,所以我们自定义导航栏的单位都必需是px(切记不能用rpx),才能完美适配。 解决问题 现在我们明白了原理,可以利用胶囊按钮的位置信息和statusBarHeight高度动态计算导航栏的高度,贴一个实现此功能最重要的方法 [代码]let systemInfo = wx.getSystemInfoSync(); let rect = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null; //胶囊按钮位置信息 wx.getMenuButtonBoundingClientRect(); let navBarHeight = (function() { //导航栏高度 let gap = rect.top - systemInfo.statusBarHeight; //动态计算每台手机状态栏到胶囊按钮间距 return 2 * gap + rect.height; })(); [代码] gap信息就是不同的手机其状态栏到胶囊按钮间距,具体更多代码实现和使用demo请移步下方代码仓库,代码中还会有输入框文字跳动解决办法,安卓手机输入框文字飞出解决办法,左侧按钮边框太粗解决办法等等 胶囊信息报错和获取不到 问题就在于 getMenuButtonBoundingClientRect 这个方法,在某些机子和环境下会报错或者获取不到,对于此种情况完美可以模拟一个胶囊位置出来 [代码]try { rect = Taro.getMenuButtonBoundingClientRect ? Taro.getMenuButtonBoundingClientRect() : null; if (rect === null) { throw 'getMenuButtonBoundingClientRect error'; } //取值为0的情况 if (!rect.width) { throw 'getMenuButtonBoundingClientRect error'; } } catch (error) { let gap = ''; //胶囊按钮上下间距 使导航内容居中 let width = 96; //胶囊的宽度,android大部分96,ios为88 if (systemInfo.platform === 'android') { gap = 8; width = 96; } else if (systemInfo.platform === 'devtools') { if (ios) { gap = 5.5; //开发工具中ios手机 } else { gap = 7.5; //开发工具中android和其他手机 } } else { gap = 4; width = 88; } if (!systemInfo.statusBarHeight) { //开启wifi的情况下修复statusBarHeight值获取不到 systemInfo.statusBarHeight = systemInfo.screenHeight - systemInfo.windowHeight - 20; } rect = { //获取不到胶囊信息就自定义重置一个 bottom: systemInfo.statusBarHeight + gap + 32, height: 32, left: systemInfo.windowWidth - width - 10, right: systemInfo.windowWidth - 10, top: systemInfo.statusBarHeight + gap, width: width }; console.log('error', error); console.log('rect', rect); } [代码] 以上代码主要是借鉴了拼多多的默认值写法,android 机子中 gap 值大部分为 8,ios 都为 4,开发工具中 ios 为 5.5,android 为 7.5,这样处理之后自己模拟一个胶囊按钮的位置,这样在获取不到胶囊信息的情况下,可保证绝大多数机子完美显示导航头 吐槽 这么重要的问题,官方尽然没有提供解决方案…竟然提供了一张看不清的图片??? 网上有很多ios设置44,android设置48,还有根据不同的手机型号设置不同高度,通过长时间的开发和尝试,本人发现以上方案并不完美,并且bug很多 代码库 Taro组件gitHub地址详细用法请参考README 原生组件npm构建版本gitHub地址详细用法请参考README 原生组件简易版gitHub地址详细用法请参考README 由于本人精力有限,目前只计划发布维护好这2种组件,其他组件请自行修改代码,有问题请联系 备注 上方2种组件在最下方30多款手机测试情况表现良好 iPhone手机打电话和开热点导致导航栏样式错乱,问题已经解决啦,请去demo里测试,这里特别感谢moments网友提出的问题 本文章并无任何商业性质,如有侵权请联系本人修改或删除 文章少量部分内容是本人查询搜集而来 如有问题可以下方留言讨论,微信zhijunxh 比较 斗鱼: [图片] 虎牙: [图片] 微博: [图片] 酷狗: [图片] 知乎: [图片] [图片] 知乎是这里边做的最好的,但是我个人认为有几个可以优化的小问题 打电话或者开启热点导致样式错落,这也是大部门小程序的问题 导航栏下边距太小,看起来不舒服 搜索框距离2侧按钮组距离不对等 自定义返回和home按钮中的竖线颜色重了,并且感觉太粗 如果您看到了此篇文章,请赶快修改自己的代码,并运用在实践中吧 扫码体验我的小程序: [图片] 创作不易,如果对你有帮助,请移步Taro组件gitHub原生组件gitHub给个星星 star✨✨ 谢谢 测试信息 手机型号 胶囊位置信息 statusBarHeight 测试情况 iPhoneX 80 32 281 369 48 88 44 通过 iPhone8 plus 56 32 320 408 24 88 20 通过 iphone7 56 32 281 368 24 87 20 通过 iPhone6 plus 56 32 320 408 24 88 20 通过 iPhone6 56 32 281 368 24 87 20 通过 HUAWEI SLA-AL00 64 32 254 350 32 96 24 通过 HUAWEI VTR-AL00 64 32 254 350 32 96 24 通过 HUAWEI EVA-AL00 64 32 254 350 32 96 24 通过 HUAWEI EML-AL00 68 32 254 350 36 96 29 通过 HUAWEI VOG-AL00 65 32 254 350 33 96 25 通过 HUAWEI ATU-TL10 64 32 254 350 32 96 24 通过 HUAWEI SMARTISAN OS105 64 32 326 422 32 96 24 通过 XIAOMI MI6 59 28 265 352 31 87 23 通过 XIAOMI MI4LTE 60 32 254 350 28 96 20 通过 XIAOMI MIX3 74 32 287 383 42 96 35 通过 REDMI NOTE3 64 32 254 350 32 96 24 通过 REDMI NOTE4 64 32 254 350 32 96 24 通过 REDMI NOTE3 55 28 255 351 27 96 20 通过 REDMI 5plus 67 32 287 383 35 96 28 通过 MEIZU M571C 65 32 254 350 33 96 25 通过 MEIZU M6 NOTE 62 32 254 350 30 96 22 通过 MEIZU MX4 PRO 62 32 278 374 30 96 22 通过 OPPO A33 65 32 254 350 33 96 26 通过 OPPO R11 58 32 254 350 26 96 18 通过 VIVO Y55 64 32 254 350 32 96 24 通过 HONOR BLN-AL20 64 32 254 350 32 96 24 通过 HONOR NEM-AL10 59 28 265 352 31 87 24 通过 HONOR BND-AL10 64 32 254 350 32 96 24 通过 HONOR duk-al20 64 32 254 350 32 96 24 通过 SAMSUNG SM-G9550 64 32 305 401 32 96 24 通过 360 1801-A01 64 32 254 350 32 96 24 通过
2019-11-17 - 微信支付 success
支付成功之后,需要点击支付成功页面的“完成“按钮,才会触发success()函数,但是[图片] 有的用户在以下页面中,不会点击“完成“,而是直接退出,这样就不会触发success()函数,请问怎么解决这个问题
2019-01-08 - 小程序实现人脸识别动效
[图片] wxml [代码]<view class='voice'></view> [代码] css [代码].voice { display: block; width: 212rpx; height: 238rpx; margin: 0 auto; background: url(http://clients-80105.oss-cn-hangzhou.aliyuncs.com/%E8%90%8D%E8%90%8D%E4%BA%BA%E8%84%B8%E8%AF%86%E5%88%AB/voice.png) 0 0 no-repeat; background-size: 1272rpx 238rpx; animation: step 2s steps(6) infinite; } @keyframes step { 100% { background-position: -1272rpx 0; } } [代码]
2019-07-31 - 转发二级页面胶囊按钮菜单去掉返回首页选项
当我们转发一个二级页面时,右上角的胶囊按钮菜单里会有一个“返回首页”的选项,可以返回到小程序的首页,有时我们并不需要这个功能,或者想禁用此功能。 [图片] 但小程序并没有提供编辑此菜单的功能,只要转发的页面不是根目录下的,就会自动生成返回首页这一项,要怎么操作呢? 如果转发的页面是首页,自然就不会有这个选项,因此,我们可以把转发的二级页面修改为先转发首页再进行页面重定向的方法来实现。 首先,在转发的onShareAppMessage方法里把path改成首页,并把要重定向的二级页面及其参数封装好。 然后,在首页的onLoad事件里,把接收到的二级页面及其参数,用wx.reLaunch方法进行重定向。 现在,用户打开转发的二级页面,胶囊按钮菜单就不会再出现“返回首页”这一选项了。
2019-07-30 - 如何使用scroll-view制作左右滚动导航条效果
最新:2020/06/13。修改为scroll-view与swiper联动效果,新增下拉刷新以及上拉加载效果。。具体效果查看代码片段,以下文章内容和就不改了 刚刚在社区里看到 有老哥在问如何做滚动的导航栏。这里简单给他写了个代码片段,需要的大哥拿去随便改改,先看效果图: [图片] 代码如下: wxml [代码]<scroll-view class="scroll-wrapper" scroll-x scroll-with-animation="true" scroll-into-view="item{{currentTab < 4 ? 0 : currentTab - 3}}" > <view class="navigate-item" id="item{{index}}" wx:for="{{taskList}}" wx:key="{{index}}" data-index="{{index}}" bindtap="handleClick"> <view class="names {{currentTab === index ? 'active' : ''}}">{{item.name}}</view> <view class="currtline {{currentTab === index ? 'active' : ''}}"></view> </view> </scroll-view> [代码] wxss [代码].scroll-wrapper { white-space: nowrap; -webkit-overflow-scrolling: touch; background: #FFF; height: 90rpx; padding: 0 32rpx; box-sizing: border-box; } ::-webkit-scrollbar { width: 0; height: 0; color: transparent; } .navigate-item { display: inline-block; text-align: center; height: 90rpx; line-height: 90rpx; margin: 0 16rpx; } .names { font-size: 28rpx; color: #3c3c3c; } .names.active { color: #00cc88; font-weight: bold; font-size: 34rpx; } .currtline { margin: -8rpx auto 0 auto; width: 100rpx; height: 8rpx; border-radius: 4rpx; } .currtline.active { background: #47CD88; transition: all .3s; } [代码] JS [代码]const app = getApp() Page({ data: { currentTab: 0, taskList: [{ name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, ] }, onLoad() { }, handleClick(e) { let currentTab = e.currentTarget.dataset.index this.setData({ currentTab }) }, }) [代码] 最后奉上代码片段: https://developers.weixin.qq.com/s/nkyp64mN7fim
2020-06-13 - 小程序播放音频的坑
小程序在媒体播放这块其实挺大的,最近正好做一个音频项目,跟大分享下经验 全局声明audio实例 let audioObj = wx.createInnerAudioContext(); 播放时长为0 audioObj.duration 无论你是写在play按钮点事件,还是 onPlay监听事件,都返回0。这是个很郁闷的事,解决方法: 在play按钮事件中加入如下代码 setTimeout(function(){audioObj.duration } , 3000); 这不是一个完美的解决方案,有时候还是无法在3秒后获取 我用到另外一个事件onTimeUpdate 真机调试下ios无声音 这个坑有很大一部分原因是我自己对文档没有详细阅读,当iphone在静音模式下,默认音频是没有声音的,我在auddio初始化时加了如下代码: audioObj.obeyMuteSwitch = false; 看到许多帖子都说能解决问题,但是我的不知道为啥没有解决,后面在文档里找到了解决方法,其实也挺适合我的需求。我在页面app.js,onLaunch事件中加入wx.setInnerAudioOption({obeyMuteSwitch : false}); 也可以把如下代码写在页面onLoad中 这样可以解决静音模式播放音频问题 经过一天的折腾,音频这块小程序框架还有很大的优化空间 最近附送一个将播放时长转换成 00:00这种格式 timespanFormat : function(time){ var i = Math.floor(time / 60); if(i <= 9){i = “0”+ i;} // 04 var s = Math.round(time % 60) ; if(s <= 9 ){s = “0” + s;} // return i + “:” + s; return [i , s]; //两种模式,一种把分和秒拆开,有一个小bug,当i或s 大于9 的时候返回了的是整形,js弱类型语言这个可以忽略,解决方法也简单,在i和s输出时 ‘’+i ,’’ +s 这样自动转string类型 }
2019-07-12 - 小程序插件使用-腾讯视频插件
在使用插件前都得先去小程序开放平台添加插件到自己的小程序(注意添加后不是立刻能使用,需要等待审核,不过一般都会很快) 设置 --- 第三方服务 --- 插件管理 ---添加插件 --- 腾讯视频 腾讯视频插件的AppID: wxa75efa648b60994b 腾讯视频插件的版本号:1.1.1 具体怎样使用腾讯视频插件呢? 接入步骤如下: 1.在app.json文件加入插件引入配置 [代码] [代码] [代码] "plugins"[代码][代码]: {[代码] [代码] [代码][代码]"tencentvideo"[代码][代码]: {[代码] [代码] [代码][代码]"version"[代码][代码]: [代码][代码]"1.1.1"[代码][代码],[代码] [代码] [代码][代码]"provider"[代码][代码]: [代码][代码]"wxa75efa648b60994b"[代码] [代码] [代码][代码]}[代码] [代码] }[代码] 2.新建一个Page: video;会自动生成四个文件 video.js,video.json,video.wxml,video.wxss 3.我们在video.json文件里面加入如下配置: [代码] [代码] [代码] "usingComponents"[代码][代码]: {[代码] [代码] [代码][代码]"txv-video"[代码][代码]: [代码][代码]"plugin://tencentvideo/video"[代码] [代码] [代码][代码]}[代码] 4.在video.wxml 中引入组件,代码如下: [代码] [代码] [代码] <txv-video playerid=[代码][代码]"txv1"[代码] [代码]vid=[代码][代码]"h07290i9vt0"[代码][代码]>[代码] [代码] [代码] [代码] </txv-video>[代码] [代码] [代码] 注意:vid 这个值是动态配置的,腾讯视频每个视频都有的 [图片] 5.运行~视频就播放了 error示例: jsEnginScriptError Component is not found in path "plugin://wxa75efa648b60994b/txv-video" (using by "pages/video/video") Error: Component is not found in path "plugin://wxa75efa648b60994b/txv-video" (using by "pages/video/video") 解决: video.json文件里面是不是写成了plugin://tencentvideo/txv-video,这样是错误的,应该是如下配置(v1.1.1) [代码] [代码] [代码] "usingComponents"[代码][代码]: {[代码] [代码] [代码][代码]"txv-video"[代码][代码]: [代码][代码]"plugin://tencentvideo/video"[代码] [代码] [代码][代码]}[代码] 上面就是简单接入腾讯视频插件步骤! 另外官方还提供了插件 js api [代码]const TxvContext = requirePlugin("tencentvideo"); let txvContext = TxvContext.getTxvContext('txv1') // txv1即播放器组件的playerid值 txvContext.play(); // 播放 txvContext.pause(); // 暂停 txvContext.requestFullScreen(); // 进入全屏 txvContext.exitFullScreen(); // 退出全屏 txvContext.playbackRate(+e.currentTarget.dataset.rate); // 设置播放速率[代码]官方文档:https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wxa75efa648b60994b
2018-07-30 - 小程序多端框架全面测评:chameleon、Taro、uni-app、mpvue、WePY
作者:coldsnap 原文:小程序多端框架全面测评 Fundebug经授权转载,版权归原作者所有。 最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一些疑惑:这些框架都有什么优缺点?到底应该用哪个? 作为 Taro 开发团队一员,笔者想在本文尽量站在一个客观公正的角度去评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待,权当抛砖引玉。 那么,当我们在讨论多端框架时,我们在谈论什么: 多端 笔者以为,现在流行的多端框架可以大致分为三类: 1. 全包型 这类框架最大的特点就是从底层的渲染引擎、布局引擎,到中层的 DSL,再到上层的框架全部由自己开发,代表框架是 Qt 和 Flutter。这类框架优点非常明显:性能(的上限)高;各平台渲染结果一致。缺点也非常明显:需要完全重新学习 DSL(QML/Dart),以及难以适配中国特色的端:小程序。 这类框架是最原始也是最纯正的的多端开发框架,由于底层到上层每个环节都掌握在自己手里,也能最大可能地去保证开发和跨端体验一致。但它们的框架研发成本巨大,渲染引擎、布局引擎、DSL、上层框架每个部分都需要大量人力开发维护。 2. Web 技术型 这类框架把 Web 技术(JavaScript,CSS)带到移动开发中,自研布局引擎处理 CSS,使用 JavaScript 写业务逻辑,使用流行的前端框架作为 DSL,各端分别使用各自的原生组件渲染。代表框架是 React Native 和 Weex,这样做的优点有: 开发迅速 复用前端生态 易于学习上手,不管前端后端移动端,多多少少都会一点 JS、CSS 缺点有: 交互复杂时难以写出高性能的代码,这类框架的设计就必然导致 [代码]JS[代码] 和 [代码]Native[代码] 之间需要通信,类似于手势操作这样频繁地触发通信就很可能使得 UI 无法在 16ms 内及时绘制。React Native 有一些声明式的组件可以避免这个问题,但声明式的写法很难满足复杂交互的需求。 由于没有渲染引擎,使用各端的原生组件渲染,相同代码渲染的一致性没有第一种高。 3. JavaScript 编译型 这类框架就是我们这篇文章的主角们:[代码]Taro[代码]、[代码]WePY[代码] 、[代码]uni-app[代码] 、 [代码]mpvue[代码] 、 [代码]chameleon[代码],它们的原理也都大同小异:先以 JavaScript 作为基础选定一个 DSL 框架,以这个 DSL 框架为标准在各端分别编译为不同的代码,各端分别有一个运行时框架或兼容组件库保证代码正确运行。 这类框架最大优点和创造的最大原因就是小程序,因为第一第二种框架其实除了可以跨系统平台之外,也都能编译运行在浏览器中。(Qt 有 Qt for WebAssembly, Flutter 有 Hummingbird,React Native 有 [代码]react-native-web[代码], Weex 原生支持) 另外一个优点是在移动端一般会编译到 React Native/Weex,所以它们也都拥有 Web 技术型框架的优点。这看起来很美好,但实际上 React Native/Weex 的缺点编译型框架也无法避免。除此之外,编译型框架的抽象也不是免费的:当 bug 出现时,问题的根源可能出在运行时、编译时、组件库以及三者依赖的库等等各个方面。在 Taro 开源的过程中,我们就遇到过 Babel 的 bug,React Native 的 bug,JavaScript 引擎的 bug,当然也少不了 Taro 本身的 bug。相信其它原理相同的框架也无法避免这一问题。 但这并不意味着这类为了小程序而设计的多端框架就都不堪大用。首先现在各巨头超级 App 的小程序百花齐放,框架会为了抹平小程序做了许多工作,这些工作在大部分情况下是不需要开发者关心的。其次是许多业务类型并不需要复杂的逻辑和交互,没那么容易触发到框架底层依赖的 bug。 那么当你的业务适合选择编译型框架时,在笔者看来首先要考虑的就是选择 DSL 的起点。因为有多端需求业务通常都希望能快速开发,一个能够快速适应团队开发节奏的 DSL 就至关重要。不管是 React 还是 Vue(或者类 Vue)都有它们的优缺点,大家可以根据团队技术栈和偏好自行选择。 如果不管什么 DSL 都能接受,那就可以进入下一个环节: 生态 以下内容均以各框架现在(2019 年 3 月 11 日)已发布稳定版为标准进行讨论。 1. 开发工具 就开发工具而言 [代码]uni-app[代码] 应该是一骑绝尘,它的文档内容最为翔实丰富,还自带了 IDE 图形化开发工具,鼠标点点点就能编译测试发布。 其它的框架都是使用 CLI 命令行工具,但值得注意的是 [代码]chameleon[代码] 有独立的语法检查工具,[代码]Taro[代码] 则单独写了 ESLint 规则和规则集。 在语法支持方面,[代码]mpvue[代码]、[代码]uni-app[代码]、[代码]Taro[代码] 、[代码]WePY[代码] 均支持 TypeScript,四者也都能通过 [代码]typing[代码] 实现编辑器自动补全。除了 API 补全之外,得益于 TypeScript 对于 JSX 的良好支持,Taro 也能对组件进行自动补全。 CSS 方面,所有框架均支持 [代码]SASS[代码]、[代码]LESS[代码]、[代码]Stylus[代码],Taro 则多一个 [代码]CSS Modules[代码] 的支持。 所以这一轮比拼的结果应该是: uni-app > Taro > chameleon > WePY、mpvue [图片] 2. 多端支持度 只从支持端的数量来看,[代码]Taro[代码] 和 [代码]uni-app[代码] 以六端略微领先(移动端、H5、微信小程序、百度小程序、支付宝小程序、头条小程序),[代码]chameleon[代码] 少了头条小程序紧随其后。 但值得一提的是 [代码]chameleon[代码] 有一套自研多态协议,编写多端代码的体验会好许多,可以说是一个能戳到多端开发痛点的功能。[代码]uni-app[代码] 则有一套独立的条件编译语法,这套语法能同时作用于 [代码]js[代码]、样式和模板文件。[代码]Taro[代码] 可以在业务逻辑中根据环境变量使用条件编译,也可以直接使用条件编译文件(类似 React Native 的方式)。 在移动端方面,[代码]uni-app[代码] 基于 [代码]weex[代码] 定制了一套 [代码]nvue[代码] 方案 弥补 [代码]weex[代码] API 的不足;[代码]Taro[代码]则是暂时基于 [代码]expo[代码] 达到同样的效果;[代码]chameleon[代码] 在移动端则有一套 SDK 配合多端协议与原生语言通信。 H5 方面,[代码]chameleon[代码] 同样是由多态协议实现支持,[代码]uni-app[代码] 和 [代码]Taro[代码] 则是都在 H5 实现了一套兼容的组件库和 API。 [代码]mpvue[代码] 和 [代码]WePY[代码] 都提供了转换各端小程序的功能,但都没有 h5 和移动端的支持。 所以最后一轮对比的结果是: chameleon > Taro、uni-app > mpvue > WePY [图片] 3. 组件库/工具库/demo 作为开源时间最长的框架,[代码]WePY[代码] 不管从 Demo,组件库数量 ,工具库来看都占有一定优势。 [代码]uni-app[代码] 则有自己的插件市场和 UI 库,如果算上收费的框架和插件比起 [代码]WePy[代码] 也是完全不遑多让的。 [代码]Taro[代码] 也有官方维护的跨端 UI 库 [代码]taro-ui[代码] ,另外在状态管理工具上也有非常丰富的选择(Redux、MobX、dva),但 demo 的数量不如前两个。但 [代码]Taro[代码] 有一个转换微信小程序代码为 Taro 代码的工具,可以弥补这一问题。 而 [代码]mpvue[代码] 没有官方维护的 UI 库,[代码]chameleon[代码] 第三方的 demo 和工具库也还基本没有。 所以这轮的排序是: WePY > uni-app 、taro > mpvue > chameleon [图片] 4. 接入成本 接入成本有两个方面: 第一是框架接入原有微信小程序生态。由于目前微信小程序已呈一家独大之势,开源的组件和库(例如 [代码]wxparse[代码]、[代码]echart[代码]、[代码]zan-ui[代码] 等)多是基于原生微信小程序框架语法写成的。目前看来 [代码]uni-app[代码] 、[代码]Taro[代码]、[代码]mpvue[代码] 均有文档或 demo 在框架中直接使用原生小程序组件/库,[代码]WePY[代码] 由于运行机制的问题,很多情况需要小改一下目标库的源码,[代码]chameleon[代码] 则是提供了一个按步骤大改目标库源码的迁移方式。 第二是原有微信小程序项目部分接入框架重构。在这个方面 Taro 在京东购物小程序上进行了大胆的实践,具体可以查看文章《Taro 在京东购物小程序上的实践》。其它框架则没有提到相关内容。 而对于两种接入方式 Taro 都提供了 [代码]taro convert[代码] 功能,既可以将原有微信小程序项目转换为 Taro 多端代码,也可以将微信小程序生态的组件转换为 Taro 组件。 所以这轮的排序是: Taro > mpvue 、 uni-app > WePY > chameleon 流行度 从 GitHub 的 star 来看,[代码]mpvue[代码] 、[代码]Taro[代码]、[代码]WePY[代码] 的差距非常小。从 NPM 和 CNPM 的 CLI 工具下载量来看,是 Taro(3k/week)> mpvue (2k/w) > WePY (1k/w)。但发布时间也刚好反过来。笔者估计三家的流行程度和案例都差不太多。 [代码]uni-app[代码] 则号称有上万案例,但不像其它框架一样有一些大厂应用案例。另外从开发者的数量来看也是 [代码]uni-app[代码] 领先,它拥有 20+ 个 QQ 交流群(最大人数 2000)。 所以从流行程度来看应该是: uni-app > Taro、WePY、mpvue > chameleon [图片] 5. 开源建设 一个开源作品能走多远是由框架维护团队和第三方开发者共同决定的。虽然开源建设不能具体地量化,但依然是衡量一个框架/库生命力的非常重要的标准。 从第三方贡献者数量来看,[代码]Taro[代码] 在这一方面领先,并且 [代码]Taro[代码] 的一些核心包/功能(MobX、CSS Modules、alias)也是由第三方开发者贡献的。除此之外,腾讯开源的 [代码]omi[代码] 框架小程序部分也是基于 Taro 完成的。 [代码]WePY[代码] 在腾讯开源计划的加持下在这一方面也有不错的表现;[代码]mpvue[代码] 由于停滞开发了很久就比较落后了;可能是产品策略的原因,[代码]uni-app[代码] 在开源建设上并不热心,甚至有些部分代码都没有开源;[代码]chameleon[代码] 刚刚开源不久,但它的代码和测试用例都非常规范,以后或许会有不错的表现。 那么这一轮的对比结果是: Taro > WePY > mpvue > chameleon > uni-app 最后补一个总的生态对比图表: [图片] 未来 从各框架已经公布的规划来看: [代码]WePY[代码] 已经发布了 [代码]v2.0.alpha[代码] 版本,虽然没有公开的文档可以查阅到 [代码]2.0[代码] 版本有什么新功能/特性,但据其作者介绍,[代码]WePY 2.0[代码] 会放大招,是一个「对得起开发者」的版本。笔者也非常期待 2.0 正式发布后 [代码]WePY[代码] 的表现。 [代码]mpvue[代码] 已经发布了 [代码]2.0[代码] 的版本,主要是更新了其它端小程序的支持。但从代码提交, issue 的回复/解决率来看,[代码]mpvue[代码] 要想在未来有作为首先要打消社区对于 [代码]mpvue[代码]不管不顾不更新的质疑。 [代码]uni-app[代码] 已经在生态上建设得很好了,应该会在此基础之上继续稳步发展。如果 [代码]uni-app[代码] 能加强开源开放,再加强与大厂的合作,相信未来还能更上一层楼。 [代码]chameleon[代码] 的规划比较宏大,虽然是最后发的框架,但已经在规划或正在实现的功能有: 快应用和端拓展协议 通用组件库和垂直类组件库 面向研发的图形化开发工具 面向非研发的图形化页面搭建工具 如果 [代码]chameleon[代码] 把这些功能都做出来的话,再继续完善生态,争取更多第三方开发者,那么在未来 [代码]chameleon[代码] 将大有可为。 [代码]Taro[代码] 的未来也一样值得憧憬。Taro 即将要发布的 [代码]1.3[代码] 版本就会支持以下功能: 快应用支持 Taro Doctor,自动化检查项目配置和代码合法性 更多的 JSX 语法支持,1.3 之后限制生产力的语法只有 [代码]只能用 map 创造循环组件[代码] 一条 H5 打包体积大幅精简 同时 [代码]Taro[代码] 也正在对移动端进行大规模重构;开发图形化开发工具;开发组件/物料平台以及图形化页面搭建工具。 结语 那说了那么多,到底用哪个呢? 如果不介意尝鲜和学习 DSL 的话,完全可以尝试 [代码]WePY[代码] 2.0 和 [代码]chameleon[代码] ,一个是酝酿了很久的 2.0 全新升级,一个有专门针对多端开发的多态协议。 [代码]uni-app[代码] 和 [代码]Taro[代码] 相比起来就更像是「水桶型」框架,从工具、UI 库,开发体验、多端支持等各方面来看都没有明显的短板。而 [代码]mpvue[代码] 由于开发一度停滞,现在看来各个方面都不如在小程序端基于它的 [代码]uni-app[代码] 。 当然,Talk is cheap。如果对这个话题有更多兴趣的同学可以去 GitHub 另行研究,有空看代码,没空看提交: chameleon: https://github.com/didi/chameleon mpvue: https://github.com/Meituan-Dianping/mpvue Taro: https://github.com/NervJS/taro uni-app: https://github.com/dcloudio/uni-app WePY: https://github.com/Tencent/wepy (按字母顺序排序)
2019-06-18 - 如何实现一个6位数的密码输入框
背景: 因为公司业务调整需要做用户支付这一块 开发者需要在小程序上实现一个简单的6位数密码输入框 [图片] 首先想下如何实现该效果: 1.使用input覆盖在框上面,设置letter-spacing达到数字之间间距的效果,实现时发现在input组件上使用letter-spacing无效果 2.循环六个view模拟的框,光标使用动画模拟,一个隐藏的input,点击view框时触发input的Focus属性弹起键盘,同时模拟的光标展示出来,输入值后,input的value长度发生变化,设置光标位置以及模拟的密码小黑圆点 好了,废话不多数,咱们直接上手。 wxml [代码]<view class='container'> <!-- 模拟输入框 --> <view class='pay-box {{focusType ? "focus-border" : ""}}' bindtap="handleFocus" style='width: 604rpx;height: 98rpx'> <block wx:for="{{boxList}}" wx:key="{{index}}"> <view class='password-box {{index === 0 ? "b-l-n":""}}'> <view wx:if="{{(dataLength === item - 1)&& focusType}}" class="cursor"></view> <view wx:if="{{dataLength >= item}}" class="input-black-dot"></view> </view> </block> </view> <!-- 隐藏input框 --> <input value="{{input_value}}" focus="{{isFocus}}" maxlength="6" type="number" class='hidden-input' bindinput="handleSetData" bindfocus="handleUseFocus" bindblur="handleUseFocus" /> </view> [代码] wxss [代码]/* 第一个格子输入框 */ .container .b-l-n { border-left: none; } .pay-box { margin: 0 auto; display: flex; flex-direction: row; border-left: 1px solid #cfd4d3; } /* 支付密码框聚焦的时候 */ .focus-border { border-color: #0c8; } /* 单个格式样式(聚焦的时候) */ .password-box { flex: 1; border: 1px solid #0c8; margin-right: 10rpx; display: flex; align-items: center; justify-content: center; } /* 模拟光标 */ .cursor { width: 2rpx; height: 36rpx; background-color: #0c8; animation: focus 1.2s infinite; } /* 光标动画 */ @keyframes focus { from { opacity: 1; } to { opacity: 0; } } /* 模拟输入的password的黑点 */ .input-black-dot { width: 20rpx; height: 20rpx; background-color: #000; border-radius: 50%; } /* 输入框 */ .hidden-input { margin-top: 200rpx; position: relative; } [代码] JS [代码]Component({ data: { //输入框聚焦状态 isFocus: false, //输入框聚焦样式 是否自动获取焦点 focusType: true, valueData: '', //输入的值 dataLength: '', boxList: [1, 2, 3, 4, 5, 6] }, // 组件属性 properties: { }, // 组件方法 methods: { // 获得焦点时 handleUseFocus() { this.setData({ focusType: true }) }, // 失去焦点时 handleUseBlur() { this.setData({ focusType: false }) }, // 点击6个框聚焦 handleFocus() { this.setData({ isFocus: true }) }, // 获取输入框的值 handleSetData(e) { // 更新数据 this.setData({ dataLength: e.detail.value.length, valueData: e.detail.value }) // 当输入框的值等于6时(发起支付等...) if (e.detail.value.length === 6) { // 通知用户输入数字达到6位数可以发送接口校验密码是否正确 this.triggerEvent('initData', e.detail.value) } } } }) [代码] 实现方式很简单,有点小问题,还有一些后续准备做的优化点,等完善后上线后再来修改一波。 最后附上代码片段: https://developers.weixin.qq.com/s/8CtRqJmT7W8k
2020-07-06 - 初试小程序接入three.js
看着小程序下的canvas日渐完善,特别是2.7.0库下新增了WebGL,终于可以摆脱原来用wx.createCanvasContext创建的2d上下文(不知为何在使用魔改后three.js中的CanvasRenderer渲染画面就是很慢,捕获JavaScript Profiler看着就是慢在draw方法上)。 不过理想很丰满,现实很骨感,想要在小程序上用three.js依然要来个大改造。让我们开始吧 官方文档里提供了一段如何获取WebGL Context的代码: [代码]Page({[代码][代码] [代码][代码]onReady() {[代码][代码] [代码][代码]const query = wx.createSelectorQuery()[代码][代码] [代码][代码]query.select([代码][代码]'#myCanvas'[代码][代码]).node().exec((res) => {[代码][代码] [代码][代码]const canvas = res[0].node[代码][代码] [代码][代码]const gl = canvas.getContext([代码][代码]'webgl'[代码][代码])[代码][代码] [代码][代码]console.log(gl)[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码]})[代码]我们就从这里入手 首先先写个wxml: [代码]<[代码][代码]canvas[代码] [代码]type[代码][代码]=[代码][代码]"webgl"[代码] [代码]id[代码][代码]=[代码][代码]"webgl"[代码] [代码]width[代码][代码]=[代码][代码]"{{canvasWidth||(320*2)}}"[代码] [代码]height[代码][代码]=[代码][代码]"{{canvasHeight||(504*2)}}"[代码] [代码]style[代码][代码]=[代码][代码]'width:{{canvasStyleWidth||"320px"}};height:{{canvasStyleHeight||"504px"}};'[代码] [代码]bindtouchstart[代码][代码]=[代码][代码]'onTouchStart'[代码] [代码]bindtouchmove[代码][代码]=[代码][代码]'onTouchMove'[代码] [代码]bindtouchend[代码][代码]=[代码][代码]'onTouchEnd'[代码][代码]></[代码][代码]canvas[代码][代码]>[代码]其中width和height是设置画布大小的,style中的width和height是设置画布的实际渲染大小的 然后js: [代码]onReady:[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]var[代码] [代码]query = wx.createSelectorQuery().select([代码][代码]'#webgl'[代码][代码]).node().exec((res) => {[代码][代码] [代码][代码]var[代码] [代码]canvas = res[0].node;[代码][代码] [代码][代码]requestAnimationFrame = canvas.requestAnimationFrame;[代码][代码] [代码][代码]canvas.width = canvas._width;[代码][代码] [代码][代码]canvas.height = canvas._height;[代码][代码] [代码][代码]canvas.style = {};[代码][代码] [代码][代码]canvas.style.width = canvas.width;[代码][代码] [代码][代码]canvas.style.height = canvas.height;[代码][代码] [代码][代码]self.init(canvas);[代码][代码] [代码][代码]self.animate();[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码]先模拟dom构造一个canvas对象,然后传入init方法中,我们在这里创建场景、相机、渲染器等 [代码]init: [代码][代码]function[代码] [代码](canvas) {[代码][代码]...[代码][代码] [代码][代码]camera = [代码][代码]new[代码] [代码]THREE.PerspectiveCamera(20, canvas.width / canvas.height, 1, 10000);[代码][代码] [代码][代码]scene = [代码][代码]new[代码] [代码]THREE.Scene();[代码][代码]...[代码][代码] [代码][代码]renderer = [代码][代码]new[代码] [代码]THREE.WebGLRenderer({ canvas: canvas, antialias: [代码][代码]true[代码] [代码]});[代码][代码] [代码][代码]}[代码]这样一个最基础的三维场景就搭好了,然后继续执行animate方法,开始渲染场景 [代码]animate:[代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]requestAnimationFrame([代码][代码]this[代码][代码].animate);[代码][代码] [代码][代码]this[代码][代码].render();[代码][代码] [代码][代码]}[代码]接下来尝试跑一下three.js提供的例子 webgl_geometry_colors : [图片] 锯齿问题比较严重,暂时没找到解决办法,但总体来说还是可以的,至少场景渲染出来了 由于暂时没想到如何改造CanvasTexture,我把例子中的 [代码]var[代码] [代码]canvas = document.createElement( [代码][代码]'canvas'[代码] [代码]);[代码][代码]canvas.width = 128;[代码][代码]canvas.height = 128;[代码][代码]var[代码] [代码]context = canvas.getContext( [代码][代码]'2d'[代码] [代码]);[代码][代码]var[代码] [代码]gradient = context.createRadialGradient( canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2 );[代码][代码]gradient.addColorStop( 0.1, [代码][代码]'rgba(210,210,210,1)'[代码] [代码]);[代码][代码]gradient.addColorStop( 1, [代码][代码]'rgba(255,255,255,1)'[代码] [代码]);[代码][代码]context.fillStyle = gradient;[代码][代码]context.fillRect( 0, 0, canvas.width, canvas.height );[代码][代码]var[代码] [代码]shadowTexture = [代码][代码]new[代码] [代码]THREE.CanvasTexture( canvas );[代码]替换成 webgl_geometries 例子中的TextureLoader [代码]var[代码] [代码]shadowTexture = [代码][代码]new[代码] [代码]THREE.TextureLoader().load(canvas,[代码][代码]'../../textures/UV_Grid_Sm.jpg'[代码][代码]);[代码]可能有人会发现load方法中传入的参数多了一个canvas,因为小程序提供的api没法直接创建Image对象,仅有一个Canvas.createImage()方法可以创建Image对象。因此我们还需要改造一下TextureLoader中的load方法,先看一下原版中的load方法: [代码]Object.assign( TextureLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]var[代码] [代码]texture = [代码][代码]new[代码] [代码]Texture();[代码] [代码] [代码][代码]var[代码] [代码]loader = [代码][代码]new[代码] [代码]ImageLoader( [代码][代码]this[代码][代码].manager );[代码][代码] [代码][代码]loader.setCrossOrigin( [代码][代码]this[代码][代码].crossOrigin );[代码][代码] [代码][代码]loader.setPath( [代码][代码]this[代码][代码].path );[代码] [代码] [代码][代码]loader.load( url, [代码][代码]function[代码] [代码]( image ) {[代码]其中实际调用了ImageLoader来加载图片,在看看ImageLoader: [代码]Object.assign( ImageLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]if[代码] [代码]( url === undefined ) url = [代码][代码]''[代码][代码];[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].path !== undefined ) url = [代码][代码]this[代码][代码].path + url;[代码] [代码] [代码][代码]url = [代码][代码]this[代码][代码].manager.resolveURL( url );[代码] [代码] [代码][代码]var[代码] [代码]scope = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]cached = Cache.get( url );[代码] [代码] [代码][代码]if[代码] [代码]( cached !== undefined ) {[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]setTimeout( [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( cached );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}, 0 );[代码] [代码] [代码][代码]return[代码] [代码]cached;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]var[代码] [代码]image = document.createElementNS( [代码][代码]'http://www.w3.org/1999/xhtml'[代码][代码], [代码][代码]'img'[代码] [代码]);[代码] [代码] [代码][代码]function[代码] [代码]onImageLoad() {[代码] [代码] [代码][代码]image.removeEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.removeEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]Cache.add( url, [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]function[代码] [代码]onImageError( event ) {[代码] [代码] [代码][代码]image.removeEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.removeEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onError ) onError( event );[代码] [代码] [代码][代码]scope.manager.itemError( url );[代码][代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]image.addEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.addEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( url.substr( 0, 5 ) !== [代码][代码]'data:'[代码] [代码]) {[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].crossOrigin !== undefined ) image.crossOrigin = [代码][代码]this[代码][代码].crossOrigin;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]image.src = url;[代码] [代码] [代码][代码]return[代码] [代码]image;[代码] [代码] [代码][代码]},[代码]document.createElementNS这种东西肯定是没法存在的,没办法,把canvas传进来用createImage方法创建Image对象,改造后: [代码]Object.assign( ImageLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( canvas,url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]if[代码] [代码]( url === undefined ) url = [代码][代码]''[代码][代码];[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].path !== undefined ) url = [代码][代码]this[代码][代码].path + url;[代码] [代码] [代码][代码]url = [代码][代码]this[代码][代码].manager.resolveURL( url );[代码] [代码] [代码][代码]var[代码] [代码]scope = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]cached = Cache.get( url );[代码] [代码] [代码][代码]if[代码] [代码]( cached !== undefined ) {[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]setTimeout( [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( cached );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}, 0 );[代码] [代码] [代码][代码]return[代码] [代码]cached;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//var image = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'img' );[代码][代码] [代码][代码]console.log([代码][代码]this[代码][代码], canvas);[代码][代码] [代码][代码]var[代码] [代码]image = canvas.createImage();[代码] [代码] [代码][代码]function[代码] [代码]onImageLoad() {[代码] [代码] [代码][代码]//image.removeEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.removeEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = [代码][代码]function[代码] [代码]() { };[代码][代码] [代码][代码]image.onerror = [代码][代码]function[代码] [代码]() { };[代码] [代码] [代码][代码]Cache.add( url, [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]function[代码] [代码]onImageError( event ) {[代码] [代码] [代码][代码]//image.removeEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.removeEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = [代码][代码]function[代码] [代码]() { };[代码][代码] [代码][代码]image.onerror = [代码][代码]function[代码] [代码]() { };[代码] [代码] [代码][代码]if[代码] [代码]( onError ) onError( event );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码][代码] [代码][代码]scope.manager.itemError( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//image.addEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.addEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = onImageLoad;[代码][代码] [代码][代码]image.onerror = onImageError;[代码] [代码] [代码][代码]if[代码] [代码]( url.substr( 0, 5 ) !== [代码][代码]'data:'[代码] [代码]) {[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].crossOrigin !== undefined ) image.crossOrigin = [代码][代码]this[代码][代码].crossOrigin;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]image.src = url;[代码] [代码] [代码][代码]return[代码] [代码]image;[代码] [代码] [代码][代码]},[代码]然后TextureLoader的load方法也改一下传参顺序: [代码]Object.assign( TextureLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( canvas,url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]var[代码] [代码]texture = [代码][代码]new[代码] [代码]Texture();[代码] [代码] [代码][代码]var[代码] [代码]loader = [代码][代码]new[代码] [代码]ImageLoader( [代码][代码]this[代码][代码].manager );[代码][代码] [代码][代码]loader.setCrossOrigin( [代码][代码]this[代码][代码].crossOrigin );[代码][代码] [代码][代码]loader.setPath( [代码][代码]this[代码][代码].path );[代码] [代码] [代码][代码]loader.load( canvas,url, [代码][代码]function[代码] [代码]( image ) {[代码]OK! 这个例子代码我放在https://github.com/leo9960/xcx_threejs,大家可以接着研究一下。潜力还是比较大的,比如我拿它搞了个全景展示 [图片] ---------------------------------------------------------------------- 2019.5.26 新上传了全景展示的范例,基于panolens.js,欢迎围观
2019-05-26 - 【U计划】弹幕biubiu小程序开发经验分享
弹幕biubiu小程序开发经验分享 Hello,大家好~我们是来自清华大学软件学院大三的弹幕弹幕团队,我是团队的Leader&Developer。我们团队开发的小程序叫作“弹幕biubiu”,主要应用场景是各类晚会现场。你可能已经发现了,我们的团队名和小程序名是不一样的,这是因为我们在确定了团队名称之后,发现这个名字已经被其他的小程序占用了,所以我们只能将小程序换成另一个名字。这也提醒了大家,在开发小程序的时候,一定要先确认自己起的名字没有被使用哦~ 我们的小程序是从去年10月开始开发的,直至今年3月基本完成。之后在今年4月的清华大学软件学院学生节上,我们的小程序作为观众弹幕互动平台被使用,并取得了广泛好评。这一方面说明我们小程序的实用性,另一方面也说明了弹幕互动及其衍生方向依然有很大的发展空间。 [图片] 我们团队的开发选用了敏捷开发的方式,并采用Scrum框架(下图源自清华大学软件学院刘强老师的软件工程课件)。本文将介绍我们团队在开发过程中所做的一些主要工作,希望能够给大家一些启发与帮助。[图片] 1.立项 万事开头难,开发过程中最困难的地方,往往就是在最开始的地方。一个团队中不缺技术人员,而缺少设计人员,也就是“有思想的人”。而且好的想法一定是来源于生活的,如果不仔细观察生活,只是天马行空地构想,是无法获得好的项目主题的。 我们团队在计划开发一个小程序之后,就开始讨论主题。首先我们确定了我们小程序的大致方向。我们发现在每次举办院系学生节时,都需要科协同学用一天的时间去部署弹幕墙,这样效率较低,而且也常常出现弹幕墙宕机的情况,所以我们决定开发一个学生节小程序。之后我们团队通过头脑风暴,将自己设想为学生节举办方与学生节观众,讨论我们可能需要哪些功能,不需要哪些功能等等,从而将项目目标进一步细化。 在这里我们并不能只单纯地讨论,我们需要一个记录者,将所有人的想法记录下来并进行归整,之后再由每个成员进行修改完善,得到我们开发的第一份文档——产品规划文档(弹幕biubiu的产品规划文档)。如果大家对产品规划文档形式不太清楚的话,可以参照上述我们的文档。在文档中我们对于产品的定位、产品的特性以及产品的路线都有了一个明确的描述。 当我们有了产品规划文档之后,立项的过程还没有结束。因为这个文档只是根据自己团队的想法写成的,但是真正的用户会有成千甚至上万人,并不一定每个人都和团队内的成员想法相同。除此之外,市场上可能已经有一些类似的产品。所以我们必须要进行一个调查,明确其他可能的用户的需求以及现有市场上的产品提供的功能。这样的调查有助于我们跳出团队内固有的思维模式,催生出一些新的点子,同时也能避免无用功。在调查结束之后,我们获得了更多的用户需求。我们需要对需求进一步整理细化,写出用户故事(弹幕biubiu的需求获取与用户故事文档),并画出用户故事地图。 [图片] 到此为止,我们基本完成了所有立项工作。这时整个团队应当对自己的项目开发目标有了明确清晰的认知。 2.设计 当明确需求之后,我们就要开始设计工作,也就是进入到敏捷开发的迭代周期中了。设计主要分为两部分: 系统设计 原型设计 系统设计中最重要的工具是UML,即统一建模语言。你可能会用到其中的类图、活动图、用例图等。 2.1 系统设计 系统设计目的是确立技术开发过程中的总纲,这也是开发过程中极为重要的一步,整个开发过程都是围绕系统设计文档展开的。 我们团队开发的小程序使用的是MVC模式,即模型(model)-视图(view)-控制器(controller)。这种模式满足了高内聚、低耦合的程序结构,便于团队开发与管理。 系统设计要求我们首先对于整体系统结构有一个清晰的构想。我们的弹幕小程序的系统架构图如下所示,可以看到这里我们将系统清晰地分为了数层。 [图片] 之后我们就需要对系统的结构进行细化,主要包括两部分内容: 数据存储结构。也就是数据库的设计。我们用一个数据库来管理数据,那么我们需要构造哪些表,每个表中需要保存那些数据等等,都是我们需要考虑的问题。我们团队采用了Mysql+Redis两种数据库相结合的存储模式,保证了数据的读写效率。 前后端接口设计。接口设计可以方便团队前后端开发的分离,提高开发效率。我们团队采用的是Restful的API接口规范,大家可以自行查阅了解该规范的内容。 因为不同的小程序会有不同的系统,所以我这里的设计思路也仅供大家参考,主要的还是要开发团队自己思索并设计,得到最适合自己的系统结构。当然合适的系统架构不意味着在项目的最初就要将所有细节想得十分透彻。一个好的系统架构主要有两方面的特征:稳定的框架与可扩展性。稳定的框架保证了开发过程中无需对代码进行很大程度的调整重构;可扩展性保证了开发人员可以很轻松地将后续的内容添加进系统,而不会影响系统整体的特性。 2.2 原型设计 原型设计,也就是UI设计。我们团队使用的工具是墨刀。墨刀有着丰富的素材库,并且可以设计控件行为,方便团队成员理解交互过程。 [图片] 原型设计往往会是团队中讨论最激烈的环节,因为每个人的审美是不同的,更何况团队中基本都是理工生(sigh…)。我们在原型设计时也进行了多次讨论与修改,才最终确定其样式。 3.开发 3.1 团队管理 自组织团队是敏捷开发的基础,团队被授权自己管理工作进程,并决定如何完成工作。团队成员在开发的过程中需要各司其职,扮演好自己的角色。但是根据著名的“20%定律”,每个团队中总会有20%的成员是free rider,所以这就需要团队的领导者对团队进行良好管理。 在开发的初期,一个团队需要制定自己团队的开发章程,包括每周的开会时间、开会地点、每个成员负责开发的模块、团建安排等,并在之后的开发过程中严格按照章程的规定管理团队。 其次,团队在每次例会时,需要每名成员汇报之前任务的开发进度,对开发过程中遇到的问题进行讨论思考,并确定下一阶段的开发任务。每次的例会都需要指定一名成员记录会议内容,方便团队日后查看。 团队管理中,最主要的就是任务安排的部分。任务安排主要包括两点:明确分工与时间规划。 明确分工是要让大家清楚自己具体是要做什么,其重点在于“细化”。举个例子,如果你和我说,“你去做一个主办方管理网站”,我肯定一头雾水无从下手;但是如果你和我说:“你去实现一个主办方登录的功能,主办方输入用户名和密码就可以跳转到活动列表页面”,那我就可以很快地完成这一个任务。所以明确分工是团队成员,尤其是组织者,需要重点注意的。 时间规划则是让大家有一个紧迫感。做时间规划最好的方法是,组织者先定一个大概的时间,然后所有团队成员进行协商,定出每个人都满意或至少不反对的时间安排。因为团队成员都会有惰性,就算最好的团队也不例外,所以一个明确的时间规划可以让每个人有计划有安排地完成任务。从这个意义上来讲,时间规划也是调动成员热情的不错的方案。 3.2 代码管理与持续集成 我们团队使用git进行代码版本的控制管理,在开发过程中维护了三个代码库,分别对应于系统后端、微信小程序以及弹幕主墙应用程序。我们也利用Github上的Issues、Projects、Wiki等功能辅助我们进行开发。由于我们团队尚未开源,所以这里也不方便向大家展示代码库的具体细节。 此外,我们团队使用Travis CI辅助我们进行代码的持续集成与自动部署,感兴趣的话大家可以自主学习一下它的使用方法。 3.3 文档管理 通过上文我们也发现了,在开发过程中我们会写很多的文档,所以合理的文档管理也是开发中的重点问题。我们团队使用的在线文档工具是石墨文档。石墨文档有三个优点: 有清晰简洁的界面与丰富的功能 支持多成员在线编辑 可以很方便地导出为word文档与pdf文档 我们团队还维护了一个产品文档目录,这样每次要修改或查阅文档时,都有一个很便捷的入口。 [图片] 4.测试 测试主要有三部分:单元测试、功能测试与性能测试。 单元测试。单元测试,就是对软件中最小可测试单元进行检查和验证。不同软件的单元测试是不同的,比如我们在开发后端时,使用的是Python中的Django框架,这一框架是自带单元测试模块的,所以我们只需在test.py中实现所有测试样例即可。单元测试保证了软件最基本的正确性,最佳的模式是开发者在开发的过程中就将单元测试样例写好。(现在微信小程序还没有单元测试的模块,希望之后小程序团队可以在这方面给出接口。) 功能测试。功能测试就是要测试软件系统的各个功能能否正常执行。功能测试的辅助软件有很多,但最简单也最方便的就是人工手动测试,也就是开发者模拟用户的使用场景测试一遍自己的软件系统。 性能测试。软件性能也是评判一个软件好坏的重要依据。就以我们的弹幕小程序为例,虽然现在的学生节晚会只有三百多人,但是如果要拓展到所有晚会时,就不得不考虑高并发的情况。所以我们团队使用Jmeter对于发送弹幕的功能进行了性能测试,并在测试之后通过图片压缩等方式提高了我们小程序的性能。 除此之外,测试环节还包括安全性测试、易用性测试、兼容性测试等。测试过程中大家需要尤其注意的地方是:一定要将测试场景与测试样例想全面。越是严密的测试,软件系统也就相应越理想。 5.分析与维护 在开发与测试结束之后,小程序也就被正式投入使用了。因为用户行为是多种多样的,所以这个时候不出意外会出现一些奇奇怪怪的bug。作为开发者一定要给出一个用户反馈的途径,并且根据用户反馈的问题,制定下一个迭代周期的任务。这样循环往复,直至软件达到预期。下图为我们小程序为用户提供的反馈接口: [图片] 6.总结 以上就是我在开发过程中的一些经验与体会,希望能够给大家一些帮助与启示。弹幕biubiu小程序的开发,对于我来说是一个特别宝贵的经历。在这个过程中我学到了很多新的知识,接触到了很多新的事物,也发现了其他同学很多的优点。同时也很感谢刘强与刘璘两位老师对我们团队的支持与指导,在这里也推荐一下两位老师在学堂在线上的软件工程课程,如果大家感兴趣的话可以去了解学习一下,相信会给你们很大的帮助。 附言 如果大家对我们的小程序感兴趣的话,也可以使用一下呀~ 使用说明 主办方管理网站 应用程序下载链接(也可在主办方管理网站中下载) 小程序二维码 [图片] 大家有什么问题或者建议的话,也欢迎随时与我交流~ 我的Github地址是:https://github.com/JL-Cheng 我的邮箱是:chengjl16@163.com
2019-05-23 - 小程序性能和体验优化方法
[图片] 小程序应避免出现任何 JavaScript 异常 出现 JavaScript 异常可能导致小程序的交互无法进行下去,我们应当追求零异常,保证小程序的高鲁棒性和高可用性 小程序所有请求应响应正常 请求失败可能导致小程序的交互无法进行下去,应当保证所有请求都能成功 所有请求的耗时不应太久 请求的耗时太长会让用户一直等待甚至离开,应当优化好服务器处理时间、减小回包大小,让请求快速响应 避免短时间内发起太多的图片请求 短时间内发起太多图片请求会触发浏览器并行加载的限制,可能导致图片加载慢,用户一直处理等待。应该合理控制数量,可考虑使用雪碧图技术或在屏幕外的图片使用懒加载 避免短时间内发起太多的请求 短时间内发起太多请求会触发小程序并行请求数量的限制,同时太多请求也可能导致加载慢等问题,应合理控制请求数量,甚至做请求的合并等 避免 setData 的数据过大 setData工作原理 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。 由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间 常见的 setData 操作错误 频繁的去 setData Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层 染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时 每次 setData 都传递大量新数据 由setData的底层实现可知,数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程 后台态页面进行 setData 当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行 避免 setData 的调用过于频繁 setData接口的调用涉及逻辑层与渲染层间的线程通过,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用 避免将未绑定在 WXML 的变量传入 setData setData操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入setData会造成不必要的性能消耗 合理设置可点击元素的响应区域大小 我们应该合理地设置好可点击元素的响应区域大小,如果过小会导致用户很难点中,体验很差 避免渲染界面的耗时过长 渲染界面的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要校验下是否同时渲染的区域太大 避免执行脚本的耗时过长 执行脚本的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要确认并优化脚本的逻辑 对网络请求做必要的缓存以避免多余的请求 发起网络请求总会让用户等待,可能造成不好的体验,应尽量避免多余的请求,比如对同样的请求进行缓存 wxss 覆盖率较高,较少或没有引入未被使用的样式 按需引入 wxss 资源,如果小程序中存在大量未使用的样式,会增加小程序包体积大小,从而在一定程度上影响加载速度 文字颜色与背景色搭配较好,适宜的颜色对比度更方便用户阅读 文字颜色与背景色需要搭配得当,适宜的颜色对比度可以让用户更好地阅读,提升小程序的用户体验 所有资源请求都建议使用 HTTPS 使用 HTTPS,可以让你的小程序更加安全,而 HTTP 是明文传输的,存在可能被篡改内容的风险 不使用废弃接口 使用即将废弃或已废弃接口,可能导致小程序运行不正常。一般而言,接口不会立即去掉,但保险起见,建议不要使用,避免后续小程序突然运行异常 避免过大的 WXML 节点数目 建议一个页面使用少于 1000 个 WXML 节点,节点树深度少于 30 层,子节点数不大于 60 个。一个太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长 避免将不可能被访问到的页面打包在小程序包里 小程序的包大小会影响加载时间,应该尽量控制包体积大小,避免将不会被使用的文件打包进去 及时回收定时器 定时器是全局的,并不是跟页面绑定的,当页面因后退被销毁时,定时器应注意手动回收 避免使用 css ‘:active’ 伪类来实现点击态 使用 css ‘:active’ 伪类来实现点击态,很容易触发,并且滚动或滑动时点击态不会消失,体验较差 建议使用小程序内置组件的 ‘hover-*’ 属性来实现 滚动区域可开启惯性滚动以增强体验 惯性滚动会使滚动比较顺畅,在安卓下默认有惯性滚动,而在 iOS 下需要额外设置 [代码]-webkit-overflow-scrolling: touch[代码] 的样式
2019-03-15 - 微盟小程序性能优化实践(下)
在上一篇分享中,给大家分享了启动性能加载的相关实践,详情可戳右方链接:微盟小程序性能优化实践(上) 接下来和大家聊一聊首屏加载的体验建议和渲染性能优化。 二、首屏加载的体验建议 · 提前请求 异步数据请求不需要等待页面渲染完成。 · 利用缓存 利用storage API对异步请求数据进行缓存,二次渲染页面,再进行后台更新。 · 避免白屏 先展示页面骨架和基础内容。 三、渲染性能优化 · 每次 setData 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关 · setData 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互 · setData 是小程序开发使用最频繁,也是最容易引发性能问题的 · 在页面列表中使用懒加载+动态移除非可视区域范围内的内容,让dom小下去 · 耗时比较长的js做到异步,不要阻塞进程(js属于单线程) · 少使用scroll-view,这个组件对性能的影响太大,单纯的只是需要一块区域滚动,可以使用view+css的方式实现 · 在页面频繁滚动触发回调函数,会导致页面卡顿,这时必须和防抖动函数或者节流函数相结合做一些处理 · 页面中的图片可以使用懒加载的方式(添加lazy-load属性,只针对page与scroll-view下的image有效) · 页面跳转要做一下限制,如果页面快速点击会出现跳转多次的情况 避免不正当的使用setData · 使用data在方法间共享数据,可能增加setData传输的数据量。data 应该仅仅包含与页面渲染相关的数据 · 使用setData 传输大量的数据,通讯耗时与数据量成正比,导致页面更新延迟 可能造成页面更新开销增加。所以setData 仅传输页面需要的数据,使用setData 的特殊Key 实现局部更新 · 短时间内频繁调用setData (操作卡顿、交互延迟 阻塞通信、页面渲染延迟),对连续的setData 调用进行合并 · 后台进行页面setData (抢占前台页面的渲染资源) 例如 活动定时器 再页面切入后台时应该将关闭 避免不正当的使用onPageScroll · 只在必要的时候监听pageScroll 事件 · 避免在onPageScroll 中执行复杂的逻辑 · 避免在onPageScroll 中频繁调用setData · 避免频繁查询节点信息(SelectQuery) 部分场景建议使用节点布局相交状态 · 监听( IntersectionObserver) 替代 使用自定义组件 在需要频繁更新的场景下,自定义组件的更新只在组件内部进行,不受页面部分内容的复杂性的影响。 使用体验评分功能 在开发过程中使用体验评分可以测试出代码中一些需要优化的点,准备定位到影响性能的原因,很大程序提高页面的性能。
2018-09-25 - 微盟小程序性能优化实践(上)
微盟小程序性能优化要分享的内容分为三部分,启动性能加载、首屏加载的体验建议和渲染性能优化。 今天主要讲启动性能加载的性能优化实践,先看启动加载过程的流程: [图片] · 公共库注入 · 资源准备(基础UI创建,代码包下载) · 业务代码注入和渲染 · 渲染首屏 · 异步请求 优化方案 1、控制代码包大小 · 开启开发者工具中的 “ 上传代码时自动压缩 ” · 及时清理无用代码和资源文件 · 减少代码包中的图片等资源文件的大小和数量 · 将图片等资源文件放到CND中 · 提取公共样式 · 代码压缩,图片格式,压缩,或者外联 · 公共组件提取,代码复用 2、 分包加载 分包加载过程流程 [图片] 在开发小程序分包项目时,会有一个或者多个分包,其中没有分包小程序必须包含一个主包,即放置启动页面或者tabBar页面,以及一些分包都需要用到的公共资源脚本。 在小程序启动时,默认会下载主包并且启动主包内页面,如果用户打开分包内的页面,客户端会把分包下载下来,下载完之后再进行展示。 · 分包加载流程 [图片] 使用分包加载的优点: · 能够增加小程序更大的代码体积,开发更多的功能 · 对于用户,可以更快地打开小程序,同时不影响启动速度 使用分包加载有哪些限制: · 整个小程序所有分包不能超过8M · 单个主包/分包不能超过2M 3、 运行机制优化 · 代码中减少立即执行的代码数量 · 避免高开销和长时间阻塞代码 · 业务代码都写入页面的生命周期中 · 做好缓存策略 4、 数据管理优化 · 首屏请求数量尽量不能超过5个,超过的可以做接口合并(node层,服务端都可以处理) · 对多次提交的数据可以做合并处理 首屏加载的体验建议和渲染性能优化这两部分的内容,将在下次分享给大家。微盟小程序性能优化实践(下)
2018-09-25 - 解决正则的插件 非常好用
小程序正则插件 粘贴下面如下 /** * 表单验证 * * @param {Object} rules 验证字段的规则 * @param {Object} messages 验证字段的提示信息 * */ class wxValidate { constructor(rules = {}, messages = {}) { Object.assign(this, { rules, messages, }) this.__init() } /** * __init */ __init() { this.__initMethods() this.__initDefaults() this.__initData() } /** * 初始化数据 */ __initData() { this.form = {} this.errorList = [] } /** * 初始化默认提示信息 */ __initDefaults() { this.defaults = { messages: { required: '这是必填字段。', email: '请输入有效的电子邮件地址。', tel: '请输入11位的手机号码。', url: '请输入有效的网址。', date: '请输入有效的日期。', dateISO: '请输入有效的日期(ISO),例如:2009-06-23,1998/01/22。', number: '请输入有效的数字。', digits: '只能输入数字。', idcard: '请输入18位的有效身份证。', equalTo: this.formatTpl('输入值必须和 {0} 相同。'), contains: this.formatTpl('输入值必须包含 {0}。'), minlength: this.formatTpl('最少要输入 {0} 个字符。'), maxlength: this.formatTpl('最多可以输入 {0} 个字符。'), rangelength: this.formatTpl('请输入长度在 {0} 到 {1} 之间的字符。'), min: this.formatTpl('请输入不小于 {0} 的数值。'), max: this.formatTpl('请输入不大于 {0} 的数值。'), range: this.formatTpl('请输入范围在 {0} 到 {1} 之间的数值。'), } } } /** * 初始化默认验证方法 */ __initMethods() { const that = this that.methods = { /** * 验证必填元素 */ required(value, param) { if (!that.depend(param)) { return 'dependency-mismatch' } else if (typeof value === 'number') { value = value.toString() } else if (typeof value === 'boolean') { return !0 } return value.length > 0 }, /** * 验证电子邮箱格式 */ email(value) { return that.optional(value) || /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(value) }, /** * 验证手机格式 */ tel(value) { return that.optional(value) || /^1[34578]\d{9}$/.test(value) }, /** * 验证URL格式 */ url(value) { return that.optional(value) || /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(value) }, /** * 验证日期格式 */ date(value) { return that.optional(value) || !/Invalid|NaN/.test(new Date(value).toString()) }, /** * 验证ISO类型的日期格式 */ dateISO(value) { return that.optional(value) || /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value) }, /** * 验证十进制数字 */ number(value) { return that.optional(value) || /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value) }, /** * 验证整数 */ digits(value) { return that.optional(value) || /^\d+$/.test(value) }, /** * 验证身份证号码 */ idcard(value) { return that.optional(value) || /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(value) }, /** * 验证两个输入框的内容是否相同 */ equalTo(value, param) { return that.optional(value) || value === that.scope.detail.value[param] }, /** * 验证是否包含某个值 */ contains(value, param) { return that.optional(value) || value.indexOf(param) >= 0 }, /** * 验证最小长度 */ minlength(value, param) { return that.optional(value) || value.length >= param }, /** * 验证最大长度 */ maxlength(value, param) { return that.optional(value) || value.length <= param }, /** * 验证一个长度范围[min, max] */ rangelength(value, param) { return that.optional(value) || (value.length >= param[0] && value.length <= param[1]) }, /** * 验证最小值 */ min(value, param) { return that.optional(value) || value >= param }, /** * 验证最大值 */ max(value, param) { return that.optional(value) || value <= param }, /** * 验证一个值范围[min, max] */ range(value, param) { return that.optional(value) || (value >= param[0] && value <= param[1]) }, } } /** * 添加自定义验证方法 * @param {String} name 方法名 * @param {Function} method 函数体,接收两个参数(value, param),value表示元素的值,param表示参数 * @param {String} message 提示信息 */ addMethod(name, method, message) { this.methods[name] = method this.defaults.messages[name] = message !== undefined ? message : this.defaults.messages[name] } /** * 判断验证方法是否存在 */ isValidMethod(value) { let methods = [] for (let method in this.methods) { if (method && typeof this.methods[method] === 'function') { methods.push(method) } } return methods.indexOf(value) !== -1 } /** * 格式化提示信息模板 */ formatTpl(source, params) { const that = this if (arguments.length === 1) { return function () { let args = Array.from(arguments) args.unshift(source) return that.formatTpl.apply(this, args) } } if (params === undefined) { return source } if (arguments.length > 2 && params.constructor !== Array) { params = Array.from(arguments).slice(1) } if (params.constructor !== Array) { params = [params] } params.forEach(function (n, i) { source = source.replace(new RegExp("\\{" + i + "\\}", "g"), function () { return n }) }) return source } /** * 判断规则依赖是否存在 */ depend(param) { switch (typeof param) { case 'boolean': param = param break case 'string': param = !!param.length break case 'function': param = param() default: param = !0 } return param } /** * 判断输入值是否为空 */ optional(value) { return !this.methods.required(value) && 'dependency-mismatch' } /** * 获取自定义字段的提示信息 * @param {String} param 字段名 * @param {Object} rule 规则 */ customMessage(param, rule) { const params = this.messages[param] const isObject = typeof params === 'object' if (params && isObject) return params[rule.method] } /** * 获取某个指定字段的提示信息 * @param {String} param 字段名 * @param {Object} rule 规则 */ defaultMessage(param, rule) { let message = this.customMessage(param, rule) || this.defaults.messages[rule.method] let type = typeof message if (type === 'undefined') { message = `Warning: No message defined for ${rule.method}.` } else if (type === 'function') { message = message.call(this, rule.parameters) } return message } /** * 缓存错误信息 * @param {String} param 字段名 * @param {Object} rule 规则 * @param {String} value 元素的值 */ formatTplAndAdd(param, rule, value) { let msg = this.defaultMessage(param, rule) this.errorList.push({ param: param, msg: msg, value: value, }) } /** * 验证某个指定字段的规则 * @param {String} param 字段名 * @param {Object} rules 规则 * @param {Object} event 表单数据对象 */ checkParam(param, rules, event) { // 缓存表单数据对象 this.scope = event // 缓存字段对应的值 const data = event.detail.value const value = data[param] || '' // 遍历某个指定字段的所有规则,依次验证规则,否则缓存错误信息 for (let method in rules) { // 判断验证方法是否存在 if (this.isValidMethod(method)) { // 缓存规则的属性及值 const rule = { method: method, parameters: rules[method] } // 调用验证方法 const result = this.methods[method](value, rule.parameters) // 若result返回值为dependency-mismatch,则说明该字段的值为空或非必填字段 if (result === 'dependency-mismatch') { continue } this.setValue(param, method, result, value) // 判断是否通过验证,否则缓存错误信息,跳出循环 if (!result) { this.formatTplAndAdd(param, rule, value) break } } } } /** * 设置字段的默认验证值 * @param {String} param 字段名 */ setView(param) { this.form[param] = { $name: param, $valid: true, $invalid: false, $error: {}, $success: {}, $viewValue: ``, } } /** * 设置字段的验证值 * @param {String} param 字段名 * @param {String} method 字段的方法 * @param {Boolean} result 是否通过验证 * @param {String} value 字段的值 */ setValue(param, method, result, value) { const params = this.form[param] params.$valid = result params.$invalid = !result params.$error[method] = !result params.$success[method] = result params.$viewValue = value } /** * 验证所有字段的规则,返回验证是否通过 * @param {Object} event 表单数据对象 */ checkForm(event) { this.__initData() for (let param in this.rules) { this.setView(param) this.checkParam(param, this.rules[param], event) } return this.valid() } /** * 返回验证是否通过 */ valid() { return this.size() === 0 } /** * 返回错误信息的个数 */ size() { return this.errorList.length } /** * 返回所有错误信息 */ validationErrors() { return this.errorList } } export default wxValidate 引入到app.js import wxValidate from 'utils/wxValidate.js'; App({ wxValidate: (rules, messages) => new wxValidate(rules, messages), }) var app=getApp() js文件如下 onload({ // 正则 this.wxValidate = app.wxValidate( { name: { required: true, name: true }, post:{ required: true, post: true, }, phone:{ required: true, tel: true, }, site:{ required: true, site: true, }, mailbox:{ required: true, email:true }, companyid:{ required:true, companyid:true } } , { name: { required: "请输入名字" }, post: { required: "请输入职位" }, phone:{ required: "请输入正确电话" }, site: { required: "请输入地址" }, mailbox:{ required: "请输入邮箱" }, companyid:{ required:"请输入公司名字" } } ) }) formSubmit:function(e){ if (!this.wxValidate.checkForm(e)) { const error = this.wxValidate.errorList[0] var error_text = `${error.msg}`; that.setData({ error_text: error_text }) wx.showToast({ title: error_text, icon:"none" }) return false } wxml <form bindsubmit='formSubmit'> <view class='list'> <view class='name'> <view> <image src='../../images/tx.png'></image> </view> <view> 姓名: </view> <input placeholder='请输入姓名' name="name" value='{{user[0].name}}'></input> </view> <view class='name'> <view> <image src='../../images/company.png'></image> </view> <view> 公司: </view> <input placeholder='请输入公司名字' name="companyid" value='{{user[0].gs}}'></input> </view> <view class='name'> <view> <image src='../../images/position.png'></image> </view> <view> 职位: </view> <input placeholder='请输入职位' name="post" value='{{user[0].zhiwei}}'></input> </view> <view class='name'> <view> <image src='../../images/phone.png'></image> </view> <view> 电话: </view> <input placeholder='请输入电话' type='number' name="phone" value='{{user[0].tel}}'></input> </view> <view class='name'> <view> <image src='../../images/yx.png'></image> </view> <view> 邮箱: </view> <input placeholder='请输入邮箱' class='mailbox' name="mailbox" value='{{user[0].email}}'></input> </view> <view class='name'> <view> <image src='../../images/address.png'></image> </view> <view> 地址: </view> <input placeholder='请输入地址' class='site' name="site" value='{{user[0].address}}'></input> </view> </view> <button form-type='submit' class='sbmit'>保存</button> </form> </view> 这样就可以了 有不明白的可以来骚扰啊 不用谢 [图片]
2018-09-15