- 隐私协议相关接口实际使用方式
隐私协议相关接口官方在回滚了多天之后终于滚回来并且提供了demo。 我这边实际研究demo后总结了实际的三种使用方法 首先要在设计到隐私协议相关功能的页面中增加隐私协议弹窗,wxml代码如下,wxss可以参考官方的demo [代码]<view wx:if="{{showPrivacy}}" class="privacy"> <view class="popup"> <view>隐私弹窗内容....</view> <view bindtap="openPrivacyAgreement">点击查看隐私协议</view> <button id="disagreeBtn" bindtap="disagreePrivacy">不同意</button> <button id="agreeBtn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="agreePrivacy">同意</button> </view> </view> [代码] 1、全局控制 页面加载时自动弹窗,同意后可以使用对应功能,不同意退出页面或者隐藏相关功能。 这种情况下,需要在onLoad里使用wx.getPrivacySetting获取隐私授权情况,没授权时弹出隐私弹窗。完整代码如下 [代码]Page({ data: { showPrivacy: false // 控制隐私弹窗是否展示 }, openPrivacyAgreement() { // 查看隐私协议 wx.openPrivacyContract() }, disagreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) // 这里是不同意隐私协议的后续操作,比如退出页面、隐藏相关功能等 }, agreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) // 这里是同意隐私协议的后续操作,比如展示被隐藏的相关功能 }, onLoad() { if (wx.getPrivacySetting) { wx.getPrivacySetting({ success: res => { if (res.needAuthorization) { // 打开隐私弹窗 this.setData({ showPrivacy: true }) } else { // 用户已经同意过隐私协议,直接执行同意隐私协议的后续操作,比如展示被隐藏的相关功能 } } }) } } }) [代码] 2、按需使用 在使用到隐私接口时弹出隐私弹窗,同时隐私接口的流程会被挂起,用户同意后会继续执行,不同意则中止执行。 这种情况下,需要在onLoad里定义好监听隐私接口时的授权事件,也就是wx.onNeedPrivacyAuthorization,然后在用户点击同意或者不同意后调用回调接口 resolve 进行上报,完整代码如下 [代码]Page({ data: { showPrivacy: false // 控制隐私弹窗是否展示 }, openPrivacyAgreement() { // 查看隐私协议 wx.openPrivacyContract() }, disagreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) // 上报用户不同意隐私协议,隐私接口操作会被自动中止 this.resolvePrivacyAuthorization({ buttonId: 'disagreeBtn', event: 'disagree' }) }, agreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) // 上报用户同意隐私协议,隐私接口操作会被自动继续执行 this.resolvePrivacyAuthorization({ buttonId: 'agreeBtn', event: 'agree' }) }, onLoad() { if (wx.onNeedPrivacyAuthorization) { wx.onNeedPrivacyAuthorization(resolve => { // 打开隐私弹窗 this.setData({ showPrivacy: true, }) // 定义上报方法 this.resolvePrivacyAuthorization = resolve }) } } }) [代码] 3、全局控制和按需使用结合使用 这种情况是上面两种方式的结合,页面加载时弹出隐私弹窗,不管用户同意还是不同意都不需要做其他操作。然后用户在用到隐私接口时,根据用户是否同意再按需决定是否再次弹窗。 这种情况下,需要在onLoad里使用wx.getPrivacySetting获取隐私授权情况,没授权时弹出隐私弹窗。同时定义好监听隐私接口时的授权事件,也就是wx.onNeedPrivacyAuthorization,然后在用户点击同意或者不同意后调用回调接口 resolve 进行上报,完整代码如下 [代码]let pageOnload = true // 是否页面加载时弹窗 Page({ data: { showPrivacy: false // 控制隐私弹窗是否展示 }, openPrivacyAgreement() { // 查看隐私协议 wx.openPrivacyContract() }, disagreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) if (!pageOnload) { // 上报用户不同意隐私协议,隐私接口操作会被自动中止 this.resolvePrivacyAuthorization({ buttonId: 'disagreeBtn', event: 'disagree' }) } else { pageOnload = false } }, agreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) if (!pageOnload) { // 上报用户同意隐私协议,隐私接口操作会被自动继续执行 this.resolvePrivacyAuthorization({ buttonId: 'agreeBtn', event: 'agree' }) } else { pageOnload = false } }, onLoad() { if (wx.getPrivacySetting) { wx.getPrivacySetting({ success: res => { if (res.needAuthorization) { // 打开隐私弹窗 this.setData({ showPrivacy: true }) } } }) } if (wx.onNeedPrivacyAuthorization) { wx.onNeedPrivacyAuthorization(resolve => { // 打开隐私弹窗 this.setData({ showPrivacy: true, }) // 定义上报方法 this.resolvePrivacyAuthorization = resolve }) } } }) [代码] 4、open-type按钮藕合使用 这个是目前最简单的方式,按照官方文档,在用open-type方式调用隐私功能的按钮的open-type里加上[代码]|agreePrivacyAuthorization[代码]就可以了,js部分代码基本不用做任何修改,示例如下 [代码]<button open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="handleGetPhoneNumber">同意隐私协议并授权手机号</button> [代码] 这种方式,不需要上报同意事件,不需要设计隐私协议弹窗,不需要“不同意”按钮。当然,我建议在现有页面上增加一个查看隐私协议的入口。
2023-09-08 - 如何重置企业/媒体/组织/个体户类型管理员微信号?
请您通过浏览器打开该网页https://mp.weixin.qq.com/acct/findacct?action=scan重置公众号绑定邮箱和管理员微信号, 在申请找回帐号的页面填写的对公账户信息和运营者信息可以和注册不一致,完成找回后,管理员微信号会同步更新; 第一步:打开网址https://mp.weixin.qq.com/acct/findacct?action=scan,填写资料扫码提交; 第二步:查询银行账单的备注码,并记录好备注码上的6位数字; 第三步:等候24小时,届时会通过“微信团队”下发消息到扫码的微信号提示填写备注码,将备注码填写上去,通过验证; 第四步:到公众平台官网使用新邮箱重新设置密码; 第五步:使用新邮箱+新密码登录公众平台帐号。 温馨提示: 1)必须有对公帐户。如没有对公帐户(包括个体户),请尽快去银行办理并确认该对公户,正常接收款项后再进行帐号找回; 2)在申请找回帐号过程中,扫码的个人微信号会自动默认成为帐号安全中心新的管理员微信号; 3)1分钱打款到帐时间为3个工作日内,若提前收到打款,请耐心等待24小时后“微信团队”下发通知输入验证信息即可。 步骤详细说明: 1)输入公众号名称或原始ID、微信号点击搜索标志; [图片] 2)填写资料验证:包括企业名称、营业执照注册号、对公账户信息、运营者身份证姓名和身份证号码、运营者身份证验证,新的登录邮箱; [图片] [图片] 3)登录新邮箱确认; [图片]
2019-11-21 - 微信小程序可以跳腾讯文档的链接吗?
微信小程序可以跳腾讯文档的链接吗?
2022-01-18 - 微信小程序如何配置银联云闪付支付
前言: 早在9月30号,微信派公众号就发布了腾讯微信支付与银联云闪付深化支付合作与互联互通的声明,原文地址 那么问题来了,微信小程序怎么配置支持云闪付支付呢? 简简单单就一步,就可以让小程序支持云闪付支付了 登录微信支付商户后台->「产品中心」->「开发配置」页面最底部找到「支付方式配置」,点击「开启」就可以了,无需开发,无需额外配置,只要用户手机安装了云闪付app,在小程序支付时,就可以选择云闪付付款。 [图片] 注意事项 1、当前只支持小程序使用云闪付付款,微信app需要更新到最新版 2、开通后默认商户号绑定的所有小程序均开启支持云闪付支付,如有部分小程序不想开通云闪付付款,可以指定小程序appid不开启云闪付支付 [图片] 3、支持服务商模式 4、配置成功后支持停用 5、原有接口无需改动 6、如用户使用云闪付付款,中途取消付款,是会返回在选择支付方式页面 7、支持云闪付优惠 以下为实际支付测试截图 [图片][图片] [图片] 配置了没有云闪付入口等常见问题请看下面地址 https://developers.weixin.qq.com/community/develop/article/doc/000ac04bca8558f9991df282651413
2021-12-29 - 微信小程序自动化框架minium实践
一、背景需求精选小程序发生了一次线上问题,测试阶段的小程序开发码测试ok,但是小程序正式码由于打包问题,"我的订单"页面文件打包失败,导致线上用户访问我的页面白屏。 当前并不能避免该打包问题,为了规避异常版本发布至线上,需要在预发、体验码发布、正式码发布等各阶段进行主流程回归。手动回归测试非常耗时,在发布前的各阶段,测试人员须重复执行大量测试用例,以确保本次上线功能OK且对其他功能无影响。 一遍又一遍执行相同的测试用例,不仅要花费更多的时间,而且还会降低整体测试效率,因此引入微信小程序自动化以解放重复人力。 二、调研1.Jest+小程序SDK优点: 小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的,小程序自动化 SDK 本身不提供测试框架。这意味着你可以将它与市面上流行的任意 Node.js 测试框架结合使用;jest 是facebook推出的一款测试框架,集成了 Mocha,chai,jsdom,覆盖率报告等开发者所需要的所有测试工具,是一款几乎零配置的测试框架;缺点: 语言仅支持JavaScript 编写;使用中遇到问题,网上相关资料比较少;2.minium框架优点: 微信小程序官方推出的小程序自动化框架,是为小程序专门开发的自动化框架, 提供了 Python 和 JavaScript 版本。支持一套脚本,iOS & Android & 模拟器,三端运行提供丰富的页面跳转方式,看不到也能去得到可以获取和设置小程序页面数据,让测试不止点点点支持往 AppSerive 注入代码片段可以使用 minium 来进行函数的 mock, 可以直接跳转到小程序某个页面并设置页面数据, 做针对性的全面测试缺点: 暂不支持H5页面的调试;暂不支持插件内wx接口调用;3.选型精选小程序主要是原生页面,minium和Jest均能满足需求。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中的方法 代码示例: #!/usr/bin/env python3 import minium class FirstTest(minium.MiniTest): def test_get_system_info(self): sys_info = self.mini.get_system_info() self.assertIn("SDKVersion", sys_info) 四、环境搭建安装minium-doc,这个是小程序安装和使用的文档介绍,或者不用自己本地安装直接访问官方文档安装python 3.8及以上安装微信开发者工具(我本机使用的版本是1.05.2103200),并打开安全模式: 设置 -> 安全设置 -> 服务端口: 打开在工具栏菜单中点击设置,选择项目设置,切换到“本地设置”,将调试基础库选择大于2.7.3的库; [图片] 下载minium安装包并安装,地址参考官网安装命令:pip3 install minium-latest.zip 或者python3 setup.py install 安装完成后,可执行以下命令查看版本:minitest -v 开启微信工具安全设置中的 CLI/HTTP (提供了命令行和HTTP两种调用方式)调用功能。在开发者工具的设置 -> 安全设置中开启服务端口。 [图片] 开启微信工具安全设置中的 CLI/HTTP (提供了命令行和HTTP两种调用方式)调用功能。在开发者工具的设置 -> 安全设置中开启服务端口。开启被测试项目的自动化端口号"path/to/cli" auto --project "path/to/project" --auto-port 9420 默认的命令行工具所在位置: macOS: <安装路径>/Contents/MacOS/cli Windows: <安装路径>/cli.bat 五、小程序脚本编写思路:使用Page Object 架构,使系统架构分层,每一个页面设计为一个Class,包含了页面需要测试的元素,测试用例只要关心测试的数据即可;1.目录结构[图片] cases/: 存放测试脚本和用例case/base/:页面公共方法case/pages/:页面对象模型outputs/:测试报告test/:测试脚本route.py:小程序页面操作的路径2.自动化脚本BasePage是页面基类,封装所有页面会用到的公用方法class BasePage: def __init__(self, mini): self.mini = mini def navigate_to_open(self, route): """以导航的方式跳转到指定页面,不允许跳转到 tabbar 页面,支持相对路径和绝对路径, 小程序中页面栈最多十层""" self.mini.app.navigate_to(route) def redirect_to_open(self, route): """关闭当前页面,重定向到应用内的某个页面,不允许跳转到 tabbar 页面""" self.mini.app.redirect_to(route) def switch_tab_open(self, route): """跳转到 tabBar 页面,会关闭其他所有非 tabBar 页面""" self.mini.app.switch_tab(route) @property def current_title(self) -> str: """获取当前页面 head title, 具体项目具体分析,以下代码仅用于演示""" return self.mini.page.get_element("XXXXXX").inner_text def current_path(self) -> str: """获取当前页面route""" return self.mini.page.path HomePage是要测试的精选首页页面from case.base.basepage import BasePage from case.base import route class HomePage(BasePage): """小程序首页公共方法""" locators = { "BASE_ELEMENT": "view", "BASE_BANNER": "首页banner元素选择器XXX" } # 首页点击官方补贴的"更多"按钮 subsidy_more_button = ("跳转页面的元素选择器XXX", "更多") """ 校验页面路径 """ def check_homepage_path(self): self.mini.assertEqual(self.current_path(), route.homepage_route) """ 校验页面的基本元素 """ def check_homepage_base_element(self): # 校验页面是否包含view元素 self.mini.assertTrue(self.mini.page.element_is_exists(HomePage.locators['BASE_ELEMENT'])) # 校验页面banner位置 self.mini.assertTrue(self.mini.page.element_is_exists(HomePage.locators['BASE_BANNER'])) """ 获取官方补贴,点击"更多"按钮跳转 """ def get_subsidy_element(self): self.mini.page.get_element(str(self.subsidy_more_button[0]), inner_text=str(self.subsidy_more_button[1])).click() BaseCase是测试用例基类,用于设置用例输出路径和清理工作,项目的测试用例都继承此类from pathlib import Path import minium class BaseCase(minium.MiniTest): """测试用例基类""" @classmethod def setUpClass(cls): super(BaseCase, cls).setUpClass() output_dir = Path(cls.CONFIG.outputs) if not output_dir.is_dir(): output_dir.mkdir() @classmethod def tearDownClass(cls): super(BaseCase, cls).tearDownClass() cls.app.go_home() def setUp(self): super(BaseCase, self).setUp() def tearDown(self): super(BaseCase, self).tearDown() 3.元素定位的方法minium 通过 WXSS 选择器来定位元素的,目前小程序仅支持以下的选择器: [图片] 参考例子: 假如要查找像上面这一个元素的话,他的选择器会像是下面这样: tageName + #id + .className view#main.page-section.page-section-gap tagName :类型选择器,标签名称,view、checkbox 等等,选择所有指定类型的最简单方式。id:ID 选择器,自定义给元素的唯一 ID,使用时前面跟着 # 号,这是选择单个元素的最有效的方式。className:类选择器,由一个点.以及类后面的类名组成,存在多个类的时候可以以点为间隔一直拼接下4.编写精选首页的测试用例被测试的有赞精选小程序首页如下图:[图片] HomePageTest # coding=utf-8 from case.base import loader from case.base.basecase import BaseCase from case.pages.homepage import HomePage """ 小程序首页测试 """ class HomePageTest(BaseCase): def __init__(self, methodName='runTest'): super(HomePageTest, self).__init__(methodName) self.homePage = HomePage(self) """ case1:测试首页的跳转路径是否正确,跳转路径要使用绝对路径,小程序默认进入就是首页,所以不用再切换进入的路径 """ def test_01_home_page_path(self): self.homePage.check_homepage_path() """ case2:页面的基本元素是否存在 """ def test_02_page_base_element(self): self.homePage.check_homepage_base_element() """ case3:检查首页的"官方补贴"模块存在 """ def test_03_live_sale(self): self.assertTexts(["官方补贴"], "view") self.assertTexts(["轻松赚回早餐钱"], "view") """ case4:从首页点击"更多"跳转到直播特卖页面,页面包含"推荐"模块 """ def test_04_open_live_sale(self): # 点击首页的"更多"按钮的元素 self.homePage.get_subsidy_element() self.page.wait_for(2) result = self.page.wait_for("页面元素选择器xxx") # 等待页面渲染完成 if result: category = self.page.data['categoryList'] self.assertEquals("美食", category[0]['title'], "接口返回值包含美食模块") self.assertEquals("美妆", category[1]['title'], "接口返回值包含美妆模块") self.page.wait_for(2) self.app.go_home() if __name__ == "__main__": loader.run(module="case.homepage_test", config="../config.json", generate_report=True) 5.编辑配置文件config.json{ "project_path": "XXXXX", "dev_tool_path": "/Applications/wechatwebdevtools.app/Contents/MacOS/cli", "debug_mode": "debug", "test_port": 9420, "platform": "ide", "app": "wx", "assert_capture": false, "request_timeout":60, "remote_connect_timeout": 300, "auto_relaunch": true } 6.minitest 命令行minium安装时执行的setup.py文件,指定了minitest命令运行的方法入口为:minium.framework.loader:main [图片] loader.py文件解释了运行的命令行的含义 [图片] -h, --help: 使用帮助。-v, --version: 查看 minium 的版本。-p PATH/--path PATH: 用例所在的文件夹,默认当前路径。-m MODULE_PATH, --module MODULE_PATH: 用例的包名或者文件名--case CASE_NAME: test_开头的用例名-s SUITE, --suite SUITE:测试计划文件-c CONFIG, --config CONFIG:配置文件名,配置项目参考配置文件-g, --generate: 生成网页测试报告--module_search_path [SYS_PATH_LIST [SYS_PATH_LIST ...]]: 添加 module 的搜索路径-a, --accounts: 查看开发者工具当前登录的多账号, 需要通过 9420 端口,以自动化模式打开开发者工具--mode RUN_MODE: 选择以parallel(并行)或者fork(复刻)的方式运行用例7.suite测试计划文件{ "pkg_list": [ { "case_list": [ "test_*" ], "pkg": "case.*_test" } ] } suite.json的pkglist字段说明要执行用例的内容和顺序,pkglist 是一个数组,每个数组元素是一个匹配规则,会根据pkg去匹配包名,找到测试类,然后再根据case_list里面的规则去查找测试类的测试用例。可以根据需要编写匹配的粒度。注意匹配规则不是正则表达式,而是通配符。 8.命令行运行脚本minitest -m case.homepage_test --case test_07_open_live_sale -c config.json -g #运行执行class文件中的指定用例test_07_open_live_sale minitest -s suite.json -c config.json -g #按照suite配置去执行用例 9.生成测试报告生成报告之后,在对应的目录下面有index.html文件,但是我们不能直接用浏览器打开这个 文件,需要把这个目录放到一个静态服务器上 测试结果存储在outputs下,运行命令python3 -m http.server 12345 -d outputs然后在浏览器上访问http://localhost:12345即可查看报告 六、遇到的问题1.需要开启被测试小程序应用的自动化测试端口9420 [图片] 开启被测试工程的自动化端口 "path/to/cli" auto --project "path/to/project" --auto-port 9420 2.打开微信开发者工具超时 [图片] 微信开发者工具:设置-代理设置,关闭ide的代理 [图片] 3.连接开发者工具后报错 原因:可能是微信开发者工具和minium的版本不一致; 我测试使用ok的匹配版本为: Minium版本:1.0.5 开发者工具版本:1.05.2102010 python版本:3.8.8 4.出现以下报错,可能是登陆的开发者工具的账号,没有被测试小程序的开发者权限; [图片] 5.运行过程中,发现调用截图的方法比较耗时,但是在config文件设置了"assert_capture": false,配置没生效,仍然会去调用截图的方法; [图片] ps:猜测是一个bug,然后给微信社区留言了,最新版本1.0.6修复了这个问题 原因:是框架的minitest.py文件调用setup和TearDown方法的时候,没有判断配置文件"assert_capture": false这个条件 [图片] 可以修改minitest.py文件,增加配置文件的判断条件,修改如下: if self.test_config.assert_capture: self.capture("setup") 6.命令行执行的时候加了-p xxx参数,运行时报引入的包不存在 [图片] 原因:命令行运行时默认是当前路径,加-p xxx, 这样会导致脚本运行的PYTHONPATH变了(不是当前目录了),这样会导致包不存在 [图片] 解决方法: 命令行运行的时候,用-m 指定运行的包路径,不用-p把-p xxx用到的路径都加入到PYTHONPATH中七、参考资料 微信官方文档简书上Rethink的相关文章介绍
2022-06-23 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - 通过授权登录介绍小程序原生开发如何引入async/await、状态管理等工具
登陆和授权是小程序开发会遇到的第一个问题,这里把相关业务逻辑、工具代码抽取出来,展示我们如何引入的一些包使得原生微信小程序内也可以使用 async/await、fetch、localStorage、状态管理、GraphQL 等等特性,希望对大家有所帮助。 前端 目录结构 [代码]├── app.js ├── app.json ├── app.wxss ├── common │ └── api │ └── index.js ├── config.js ├── pages │ └── index │ ├── api │ │ └── index.js │ ├── img │ │ ├── btn.png │ │ └── bg.jpg │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss ├── project.config.json ├── store │ ├── action.js │ └── index.js ├── utils │ └── index.js └── vendor ├── event-emitter.js ├── fetch.js ├── fetchql.js ├── http.js ├── promisify.js ├── regenerator.js ├── storage.js └── store.js [代码] 业务代码 app.js [代码]import store from './store/index' const { loginInfo } = store.state App({ store, onLaunch() { // 打开小程序即登陆,无需用户授权可获得 openID if(!loginInfo) store.dispatch('login') }, }) [代码] store/index.js [代码]import Store from '../vendor/store' import localStorage from '../vendor/storage' import actions from './action' const loginInfo = localStorage.getItem('loginInfo') export default new Store({ state: { // 在全局状态中维护登陆信息 loginInfo, }, actions, }) [代码] store/action.js [代码]import regeneratorRuntime from '../vendor/regenerator'; import wx from '../vendor/promisify'; import localStorage from '../vendor/storage' import api from '../common/api/index'; export default { async login({ state }, payload) { const { code } = await wx.loginAsync(); const { authSetting } = await wx.getSettingAsync() // 如果用户曾授权,直接可以拿到 encryptedData const { encryptedData, iv } = authSetting['scope.userInfo'] ? await wx.getUserInfoAsync({ withCredentials: true }) : {}; // 如果用户未曾授权,也可以拿到 openID const { token, userInfo } = await api.login({ code, encryptedData, iv }); // 为接口统一配置 Token getApp().gql.requestObject.headers['Authorization'] = `Bearer ${token}`; // 本地缓存登陆信息 localStorage.setItem('loginInfo', { token, userInfo } ) return { loginInfo: { token, userInfo } } } } [代码] common/api/index.js [代码]import regeneratorRuntime from '../../vendor/regenerator.js' export default { /** * 登录接口 * 如果只有 code,只返回 token,如果有 encryptedData, iv,同时返回用户的昵称和头像 * @param {*} param0 */ async login({ code, encryptedData, iv }) { const query = `query login($code: String!, $encryptedData: String, $iv: String){ login(code:$code, encryptedData:$encryptedData, iv:$iv, appid:$appid){ token userInfo { nickName avatarUrl } } }` const { login: { token, userInfo } } = await getApp().query({ query, variables: { code, encryptedData, iv } }) return { token, userInfo } }, } [代码] pages/index/index.js [代码]import regeneratorRuntime from '../../vendor/regenerator.js' const app = getApp() Page({ data: {}, onLoad(options) { // 将用户登录信息注入到当前页面的 data 中,并且当数据在全局范围内被更新时,都会自动刷新本页面 app.store.mapState(['loginInfo'], this) }, async login({ detail: { errMsg } }) { if (errMsg === 'getUserInfo:fail auth deny') return app.store.dispatch('login') // 继续处理业务 }, }) [代码] pages/index/index.wxml [代码]<view class="container"> <form report-submit="true" bindsubmit="saveFormId"> <button form-type="submit" open-type="getUserInfo" bindgetuserinfo="login">登录</button> </form> </view> [代码] 工具代码 事件处理 vendor/event-emitter.js [代码]const id_Identifier = '__id__'; function randomId() { return Math.random().toString(36).substr(2, 16); } function findIndexById(id) { return this.findIndex(item => item[id_Identifier] === id); } export default class EventEmitter { constructor() { this.events = {} } /** * listen on a event * @param event * @param listener */ on(event, listener) { let { events } = this; let container = events[event] || []; let id = randomId(); let index; listener[id_Identifier] = id; container.push(listener); return () => { index = findIndexById.call(container, id); index >= 0 && container.splice(index, 1); } }; /** * remove all listen of an event * @param event */ off (event) { this.events[event] = []; }; /** * clear all event listen */ clear () { this.events = {}; }; /** * listen on a event once, if it been trigger, it will cancel the listner * @param event * @param listener */ once (event, listener) { let { events } = this; let container = events[event] || []; let id = randomId(); let index; let callback = () => { index = findIndexById.call(container, id); index >= 0 && container.splice(index, 1); listener.apply(this, arguments); }; callback[id_Identifier] = id; container.push(callback); }; /** * emit event */ emit () { const { events } = this; const argv = [].slice.call(arguments); const event = argv.shift(); ((events['*'] || []).concat(events[event] || [])).map(listener => self.emitting(event, argv, listener)); }; /** * define emitting * @param event * @param dataArray * @param listener */ emitting (event, dataArray, listener) { listener.apply(this, dataArray); }; } [代码] 封装 wx.request() 接口 vendor/http.js [代码]import EventEmitter from './event-emitter.js'; const DEFAULT_CONFIG = { maxConcurrent: 10, timeout: 0, header: {}, dataType: 'json' }; class Http extends EventEmitter { constructor(config = DEFAULT_CONFIG) { super(); this.config = config; this.ctx = wx; this.queue = []; this.runningTask = 0; this.maxConcurrent = DEFAULT_CONFIG.maxConcurrent; this.maxConcurrent = config.maxConcurrent; this.requestInterceptor = () => true; this.responseInterceptor = () => true; } create(config = DEFAULT_CONFIG) { return new Http(config); } next() { const queue = this.queue; if (!queue.length || this.runningTask >= this.maxConcurrent) return; const entity = queue.shift(); const config = entity.config; const { requestInterceptor, responseInterceptor } = this; if (requestInterceptor.call(this, config) !== true) { let response = { data: null, errMsg: `Request Interceptor: Request can\'t pass the Interceptor`, statusCode: 0, header: {} }; entity.reject(response); return; } this.emit('request', config); this.runningTask = this.runningTask + 1; let timer = null; let aborted = false; let finished = false; const callBack = { success: (res) => { if (aborted) return; finished = true; timer && clearTimeout(timer); entity.response = res; this.emit('success', config, res); responseInterceptor.call(this, config, res) !== true ? entity.reject(res) : entity.resolve(res); }, fail: (res) => { if (aborted) return; finished = true; timer && clearTimeout(timer); entity.response = res; this.emit('fail', config, res); responseInterceptor.call(this, config, res) !== true ? entity.reject(res) : entity.resolve(res); }, complete: () => { if (aborted) return; this.emit('complete', config, entity.response); this.next(); this.runningTask = this.runningTask - 1; } }; const requestConfig = Object.assign(config, callBack); const task = this.ctx.request(requestConfig); if (this.config.timeout > 0) { timer = setTimeout(() => { if (!finished) { aborted = true; task && task.abort(); this.next(); } }, this.config.timeout); } } request(method, url, data, header, dataType = 'json') { const config = { method, url, data, header: { ...header, ...this.config.header }, dataType: dataType || this.config.dataType }; return new Promise((resolve, reject) => { const entity = { config, resolve, reject, response: null }; this.queue.push(entity); this.next(); }); } head(url, data, header, dataType) { return this.request('HEAD', url, data, header, dataType); } options(url, data, header, dataType) { return this.request('OPTIONS', url, data, header, dataType); } get(url, data, header, dataType) { return this.request('GET', url, data, header, dataType); } post(url, data, header, dataType) { return this.request('POST', url, data, header, dataType); } put(url, data, header, dataType) { return this.request('PUT', url, data, header, dataType); } ['delete'](url, data, header, dataType) { return this.request('DELETE', url, data, header, dataType); } trace(url, data, header, dataType) { return this.request('TRACE', url, data, header, dataType); } connect(url, data, header, dataType) { return this.request('CONNECT', url, data, header, dataType); } setRequestInterceptor(interceptor) { this.requestInterceptor = interceptor; return this; } setResponseInterceptor(interceptor) { this.responseInterceptor = interceptor; return this; } clean() { this.queue = []; } } export default new Http(); [代码] 兼容 fetch 标准 vendor/fetch.js [代码]import http from './http'; const httpClient = http.create({ maxConcurrent: 10, timeout: 0, header: {}, dataType: 'json' }); function generateResponse(res) { let header = res.header || {}; let config = res.config || {}; return { ok: ((res.statusCode / 200) | 0) === 1, // 200-299 status: res.statusCode, statusText: res.errMsg, url: config.url, clone: () => generateResponse(res), text: () => Promise.resolve( typeof res.data === 'string' ? res.data : JSON.stringify(res.data) ), json: () => { if (typeof res.data === 'object') return Promise.resolve(res.data); let json = {}; try { json = JSON.parse(res.data); } catch (err) { console.error(err); } return json; }, blob: () => Promise.resolve(new Blob([res.data])), headers: { keys: () => Object.keys(header), entries: () => { let all = []; for (let key in header) { if (header.hasOwnProperty(key)) { all.push([key, header[key]]); } } return all; }, get: n => header[n.toLowerCase()], has: n => n.toLowerCase() in header } }; } export default (typeof fetch === 'function' ? fetch.bind() : function(url, options) { options = options || {}; return httpClient .request(options.method || 'get', url, options.body, options.headers) .then(res => Promise.resolve(generateResponse(res))) .catch(res => Promise.reject(generateResponse(res))); }); [代码] GraphQL客户端 vendor/fetchql.js [代码]import fetch from './fetch'; // https://github.com/gucheen/fetchql /** Class to realize fetch interceptors */ class FetchInterceptor { constructor() { this.interceptors = []; /* global fetch */ this.fetch = (...args) => this.interceptorWrapper(fetch, ...args); } /** * add new interceptors * @param {(Object|Object[])} interceptors */ addInterceptors(interceptors) { const removeIndex = []; if (Array.isArray(interceptors)) { interceptors.map((interceptor) => { removeIndex.push(this.interceptors.length); return this.interceptors.push(interceptor); }); } else if (interceptors instanceof Object) { removeIndex.push(this.interceptors.length); this.interceptors.push(interceptors); } this.updateInterceptors(); return () => this.removeInterceptors(removeIndex); } /** * remove interceptors by indexes * @param {number[]} indexes */ removeInterceptors(indexes) { if (Array.isArray(indexes)) { indexes.map(index => this.interceptors.splice(index, 1)); this.updateInterceptors(); } } /** * @private */ updateInterceptors() { this.reversedInterceptors = this.interceptors .reduce((array, interceptor) => [interceptor].concat(array), []); } /** * remove all interceptors */ clearInterceptors() { this.interceptors = []; this.updateInterceptors(); } /** * @private */ interceptorWrapper(fetch, ...args) { let promise = Promise.resolve(args); this.reversedInterceptors.forEach(({ request, requestError }) => { if (request || requestError) { promise = promise.then(() => request(...args), requestError); } }); promise = promise.then(() => fetch(...args)); this.reversedInterceptors.forEach(({ response, responseError }) => { if (response || responseError) { promise = promise.then(response, responseError); } }); return promise; } } /** * GraphQL client with fetch api. * @extends FetchInterceptor */ class FetchQL extends FetchInterceptor { /** * Create a FetchQL instance. * @param {Object} options * @param {String} options.url - the server address of GraphQL * @param {(Object|Object[])=} options.interceptors * @param {{}=} options.headers - request headers * @param {FetchQL~requestQueueChanged=} options.onStart - callback function of a new request queue * @param {FetchQL~requestQueueChanged=} options.onEnd - callback function of request queue finished * @param {Boolean=} options.omitEmptyVariables - remove null props(null or '') from the variables * @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api) */ constructor({ url, interceptors, headers, onStart, onEnd, omitEmptyVariables = false, requestOptions = {}, }) { super(); this.requestObject = Object.assign( {}, { method: 'POST', headers: Object.assign({}, { Accept: 'application/json', 'Content-Type': 'application/json', }, headers), credentials: 'same-origin', }, requestOptions, ); this.url = url; this.omitEmptyVariables = omitEmptyVariables; // marker for request queue this.requestQueueLength = 0; // using for caching enums' type this.EnumMap = {}; this.callbacks = { onStart, onEnd, }; this.addInterceptors(interceptors); } /** * operate a query * @param {Object} options * @param {String} options.operationName * @param {String} options.query * @param {Object=} options.variables * @param {Object=} options.opts - addition options(will not be passed to server) * @param {Boolean=} options.opts.omitEmptyVariables - remove null props(null or '') from the variables * @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api) * @returns {Promise} * @memberOf FetchQL */ query({ operationName, query, variables, opts = {}, requestOptions = {}, }) { const options = Object.assign({}, this.requestObject, requestOptions); let vars; if (this.omitEmptyVariables || opts.omitEmptyVariables) { vars = this.doOmitEmptyVariables(variables); } else { vars = variables; } const body = { operationName, query, variables: vars, }; options.body = JSON.stringify(body); this.onStart(); return this.fetch(this.url, options) .then((res) => { if (res.ok) { return res.json(); } // return an custom error stack if request error return { errors: [{ message: res.statusText, stack: res, }], }; }) .then(({ data, errors }) => ( new Promise((resolve, reject) => { this.onEnd(); // if data in response is 'null' if (!data) { return reject(errors || [{}]); } // if all properties of data is 'null' const allDataKeyEmpty = Object.keys(data).every(key => !data[key]); if (allDataKeyEmpty) { return reject(errors); } return resolve({ data, errors }); }) )); } /** * get current server address * @returns {String} * @memberOf FetchQL */ getUrl() { return this.url; } /** * setting a new server address * @param {String} url * @memberOf FetchQL */ setUrl(url) { this.url = url; } /** * get information of enum type * @param {String[]} EnumNameList - array of enums' name * @returns {Promise} * @memberOf FetchQL */ getEnumTypes(EnumNameList) { const fullData = {}; // check cache status const unCachedEnumList = EnumNameList.filter((element) => { if (this.EnumMap[element]) { // enum has been cached fullData[element] = this.EnumMap[element]; return false; } return true; }); // immediately return the data if all enums have been cached if (!unCachedEnumList.length) { return new Promise((resolve) => { resolve({ data: fullData }); }); } // build query string for uncached enums const EnumTypeQuery = unCachedEnumList.map(type => ( `${type}: __type(name: "${type}") { ...EnumFragment }` )); const query = ` query { ${EnumTypeQuery.join('\n')} } fragment EnumFragment on __Type { kind description enumValues { name description } }`; const options = Object.assign({}, this.requestObject); options.body = JSON.stringify({ query }); this.onStart(); return this.fetch(this.url, options) .then((res) => { if (res.ok) { return res.json(); } // return an custom error stack if request error return { errors: [{ message: res.statusText, stack: res, }], }; }) .then(({ data, errors }) => ( new Promise((resolve, reject) => { this.onEnd(); // if data in response is 'null' and have any errors if (!data) { return reject(errors || [{ message: 'Do not get any data.' }]); } // if all properties of data is 'null' const allDataKeyEmpty = Object.keys(data).every(key => !data[key]); if (allDataKeyEmpty && errors && errors.length) { return reject(errors); } // merge enums' data const passData = Object.assign(fullData, data); // cache new enums' data Object.keys(data).map((key) => { this.EnumMap[key] = data[key]; return key; }); return resolve({ data: passData, errors }); }) )); } /** * calling on a request starting * if the request belong to a new queue, call the 'onStart' method */ onStart() { this.requestQueueLength++; if (this.requestQueueLength > 1 || !this.callbacks.onStart) { return; } this.callbacks.onStart(this.requestQueueLength); } /** * calling on a request ending * if current queue finished, calling the 'onEnd' method */ onEnd() { this.requestQueueLength--; if (this.requestQueueLength || !this.callbacks.onEnd) { return; } this.callbacks.onEnd(this.requestQueueLength); } /** * Callback of requests queue changes.(e.g. new queue or queue finished) * @callback FetchQL~requestQueueChanged * @param {number} queueLength - length of current request queue */ /** * remove empty props(null or '') from object * @param {Object} input * @returns {Object} * @memberOf FetchQL * @private */ doOmitEmptyVariables(input) { const nonEmptyObj = {}; Object.keys(input).map(key => { const value = input[key]; if ((typeof value === 'string' && value.length === 0) || value === null || value === undefined) { return key; } else if (value instanceof Object) { nonEmptyObj[key] = this.doOmitEmptyVariables(value); } else { nonEmptyObj[key] = value; } return key; }); return nonEmptyObj; } } export default FetchQL; [代码] 将wx的异步接口封装成Promise vendor/promisify.js [代码]function promisify(wx) { let wxx = { ...wx }; for (let attr in wxx) { if (!wxx.hasOwnProperty(attr) || typeof wxx[attr] != 'function') continue; // skip over the sync method if (/sync$/i.test(attr)) continue; wxx[attr + 'Async'] = function asyncFunction(argv = {}) { return new Promise(function (resolve, reject) { wxx[attr].call(wxx, { ...argv, ...{ success: res => resolve(res), fail: err => reject(err) } }); }); }; } return wxx; } export default promisify(typeof wx === 'object' ? wx : {}); [代码] localstorage vendor/storage.js [代码]class Storage { constructor(wx) { this.wx = wx; } static get timestamp() { return new Date() / 1000; } static __isExpired(entity) { if (!entity) return true; return Storage.timestamp - (entity.timestamp + entity.expiration) >= 0; } static get __info() { let info = {}; try { info = this.wx.getStorageInfoSync() || info; } catch (err) { console.error(err); } return info; } setItem(key, value, expiration) { const entity = { timestamp: Storage.timestamp, expiration, key, value }; this.wx.setStorageSync(key, JSON.stringify(entity)); return this; } getItem(key) { let entity; try { entity = this.wx.getStorageSync(key); if (entity) { entity = JSON.parse(entity); } else { return null; } } catch (err) { console.error(err); return null; } // 没有设置过期时间, 则直接返回值 if (!entity.expiration) return entity.value; // 已过期 if (Storage.__isExpired(entity)) { this.remove(key); return null; } else { return entity.value; } } removeItem(key) { try { this.wx.removeStorageSync(key); } catch (err) { console.error(err); } return this; } clear() { try { this.wx.clearStorageSync(); } catch (err) { console.error(err); } return this; } get info() { let info = {}; try { info = this.wx.getStorageInfoSync(); } catch (err) { console.error(err); } return info || {}; } get length() { return (this.info.keys || []).length; } } export default new Storage(wx); [代码] 状态管理 vendor/store.js [代码]module.exports = class Store { constructor({ state, actions }) { this.state = state || {} this.actions = actions || {} this.ctxs = [] } // 派发action, 统一返回promise action可以直接返回state dispatch(type, payload) { const update = res => { if (typeof res !== 'object') return this.setState(res) this.ctxs.map(ctx => ctx.setData(res)) return res } if (typeof this.actions[type] !== 'function') return const res = this.actions[type](this, payload) return res.constructor.toString().match(/function\s*([^(]*)/)[1] === 'Promise' ? res.then(update) : new Promise(resolve => resolve(update(res))) } // 修改state的方法 setState(data) { this.state = { ...this.state, ...data } } // 根据keys获取state getState(keys) { return keys.reduce((acc, key) => ({ ...acc, ...{ [key]: this.state[key] } }), {}) } // 映射state到实例中,可在onload或onshow中调用 mapState(keys, ctx) { if (!ctx || typeof ctx.setData !== 'function') return ctx.setData(this.getState(keys)) this.ctxs.push(ctx) } } [代码] 兼容 async/await vendor/regenerator.js [代码]/** * Copyright (c) 2014-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var regeneratorRuntime = (function (exports) { "use strict"; var Op = Object.prototype; var hasOwn = Op.hasOwnProperty; var undefined; // More compressible than void 0. var $Symbol = typeof Symbol === "function" ? Symbol : {}; var iteratorSymbol = $Symbol.iterator || "@@iterator"; var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator"; var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; function wrap(innerFn, outerFn, self, tryLocsList) { // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator. var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator; var generator = Object.create(protoGenerator.prototype); var context = new Context(tryLocsList || []); // The ._invoke method unifies the implementations of the .next, // .throw, and .return methods. generator._invoke = makeInvokeMethod(innerFn, self, context); return generator; } exports.wrap = wrap; // Try/catch helper to minimize deoptimizations. Returns a completion // record like context.tryEntries[i].completion. This interface could // have been (and was previously) designed to take a closure to be // invoked without arguments, but in all the cases we care about we // already have an existing method we want to call, so there's no need // to create a new function object. We can even get away with assuming // the method takes exactly one argument, since that happens to be true // in every case, so we don't have to touch the arguments object. The // only additional allocation required is the completion record, which // has a stable shape and so hopefully should be cheap to allocate. function tryCatch(fn, obj, arg) { try { return { type: "normal", arg: fn.call(obj, arg) }; } catch (err) { return { type: "throw", arg: err }; } } var GenStateSuspendedStart = "suspendedStart"; var GenStateSuspendedYield = "suspendedYield"; var GenStateExecuting = "executing"; var GenStateCompleted = "completed"; // Returning this object from the innerFn has the same effect as // breaking out of the dispatch switch statement. var ContinueSentinel = {}; // Dummy constructor functions that we use as the .constructor and // .constructor.prototype properties for functions that return Generator // objects. For full spec compliance, you may wish to configure your // minifier not to mangle the names of these two functions. function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} // This is a polyfill for %IteratorPrototype% for environments that // don't natively support it. var IteratorPrototype = {}; IteratorPrototype[iteratorSymbol] = function () { return this; }; var getProto = Object.getPrototypeOf; var NativeIteratorPrototype = getProto && getProto(getProto(values([]))); if (NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) { // This environment has a native %IteratorPrototype%; use it instead // of the polyfill. IteratorPrototype = NativeIteratorPrototype; } var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype; GeneratorFunctionPrototype.constructor = GeneratorFunction; GeneratorFunctionPrototype[toStringTagSymbol] = GeneratorFunction.displayName = "GeneratorFunction"; // Helper for defining the .next, .throw, and .return methods of the // Iterator interface in terms of a single ._invoke method. function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function(method) { prototype[method] = function(arg) { return this._invoke(method, arg); }; }); } exports.isGeneratorFunction = function(genFun) { var ctor = typeof genFun === "function" && genFun.constructor; return ctor ? ctor === GeneratorFunction || // For the native GeneratorFunction constructor, the best we can // do is to check its .name property. (ctor.displayName || ctor.name) === "GeneratorFunction" : false; }; exports.mark = function(genFun) { if (Object.setPrototypeOf) { Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); } else { genFun.__proto__ = GeneratorFunctionPrototype; if (!(toStringTagSymbol in genFun)) { genFun[toStringTagSymbol] = "GeneratorFunction"; } } genFun.prototype = Object.create(Gp); return genFun; }; // Within the body of any async function, `await x` is transformed to // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test // `hasOwn.call(value, "__await")` to determine if the yielded value is // meant to be awaited. exports.awrap = function(arg) { return { __await: arg }; }; function AsyncIterator(generator) { function invoke(method, arg, resolve, reject) { var record = tryCatch(generator[method], generator, arg); if (record.type === "throw") { reject(record.arg); } else { var result = record.arg; var value = result.value; if (value && typeof value === "object" && hasOwn.call(value, "__await")) { return Promise.resolve(value.__await).then(function(value) { invoke("next", value, resolve, reject); }, function(err) { invoke("throw", err, resolve, reject); }); } return Promise.resolve(value).then(function(unwrapped) { // When a yielded Promise is resolved, its final value becomes // the .value of the Promise<{value,done}> result for the // current iteration. result.value = unwrapped; resolve(result); }, function(error) { // If a rejected Promise was yielded, throw the rejection back // into the async generator function so it can be handled there. return invoke("throw", error, resolve, reject); }); } } var previousPromise; function enqueue(method, arg) { function callInvokeWithMethodAndArg() { return new Promise(function(resolve, reject) { invoke(method, arg, resolve, reject); }); } return previousPromise = // If enqueue has been called before, then we want to wait until // all previous Promises have been resolved before calling invoke, // so that results are always delivered in the correct order. If // enqueue has not been called before, then it is important to // call invoke immediately, without waiting on a callback to fire, // so that the async generator function has the opportunity to do // any necessary setup in a predictable way. This predictability // is why the Promise constructor synchronously invokes its // executor callback, and why async functions synchronously // execute code before the first await. Since we implement simple // async functions in terms of async generators, it is especially // important to get this right, even though it requires care. previousPromise ? previousPromise.then( callInvokeWithMethodAndArg, // Avoid propagating failures to Promises returned by later // invocations of the iterator. callInvokeWithMethodAndArg ) : callInvokeWithMethodAndArg(); } // Define the unified helper method that is used to implement .next, // .throw, and .return (see defineIteratorMethods). this._invoke = enqueue; } defineIteratorMethods(AsyncIterator.prototype); AsyncIterator.prototype[asyncIteratorSymbol] = function () { return this; }; exports.AsyncIterator = AsyncIterator; // Note that simple async functions are implemented on top of // AsyncIterator objects; they just return a Promise for the value of // the final result produced by the iterator. exports.async = function(innerFn, outerFn, self, tryLocsList) { var iter = new AsyncIterator( wrap(innerFn, outerFn, self, tryLocsList) ); return exports.isGeneratorFunction(outerFn) ? iter // If outerFn is a generator, return the full iterator. : iter.next().then(function(result) { return result.done ? result.value : iter.next(); }); }; function makeInvokeMethod(innerFn, self, context) { var state = GenStateSuspendedStart; return function invoke(method, arg) { if (state === GenStateExecuting) { throw new Error("Generator is already running"); } if (state === GenStateCompleted) { if (method === "throw") { throw arg; } // Be forgiving, per 25.3.3.3.3 of the spec: // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume return doneResult(); } context.method = method; context.arg = arg; while (true) { var delegate = context.delegate; if (delegate) { var delegateResult = maybeInvokeDelegate(delegate, context); if (delegateResult) { if (delegateResult === ContinueSentinel) continue; return delegateResult; } } if (context.method === "next") { // Setting context._sent for legacy support of Babel's // function.sent implementation. context.sent = context._sent = context.arg; } else if (context.method === "throw") { if (state === GenStateSuspendedStart) { state = GenStateCompleted; throw context.arg; } context.dispatchException(context.arg); } else if (context.method === "return") { context.abrupt("return", context.arg); } state = GenStateExecuting; var record = tryCatch(innerFn, self, context); if (record.type === "normal") { // If an exception is thrown from innerFn, we leave state === // GenStateExecuting and loop back for another invocation. state = context.done ? GenStateCompleted : GenStateSuspendedYield; if (record.arg === ContinueSentinel) { continue; } return { value: record.arg, done: context.done }; } else if (record.type === "throw") { state = GenStateCompleted; // Dispatch the exception by looping back around to the // context.dispatchException(context.arg) call above. context.method = "throw"; context.arg = record.arg; } } }; } // Call delegate.iterator[context.method](context.arg) and handle the // result, either by returning a { value, done } result from the // delegate iterator, or by modifying context.method and context.arg, // setting context.delegate to null, and returning the ContinueSentinel. function maybeInvokeDelegate(delegate, context) { var method = delegate.iterator[context.method]; if (method === undefined) { // A .throw or .return when the delegate iterator has no .throw // method always terminates the yield* loop. context.delegate = null; if (context.method === "throw") { // Note: ["return"] must be used for ES3 parsing compatibility. if (delegate.iterator["return"]) { // If the delegate iterator has a return method, give it a // chance to clean up. context.method = "return"; context.arg = undefined; maybeInvokeDelegate(delegate, context); if (context.method === "throw") { // If maybeInvokeDelegate(context) changed context.method from // "return" to "throw", let that override the TypeError below. return ContinueSentinel; } } context.method = "throw"; context.arg = new TypeError( "The iterator does not provide a 'throw' method"); } return ContinueSentinel; } var record = tryCatch(method, delegate.iterator, context.arg); if (record.type === "throw") { context.method = "throw"; context.arg = record.arg; context.delegate = null; return ContinueSentinel; } var info = record.arg; if (! info) { context.method = "throw"; context.arg = new TypeError("iterator result is not an object"); context.delegate = null; return ContinueSentinel; } if (info.done) { // Assign the result of the finished delegate to the temporary // variable specified by delegate.resultName (see delegateYield). context[delegate.resultName] = info.value; // Resume execution at the desired location (see delegateYield). context.next = delegate.nextLoc; // If context.method was "throw" but the delegate handled the // exception, let the outer generator proceed normally. If // context.method was "next", forget context.arg since it has been // "consumed" by the delegate iterator. If context.method was // "return", allow the original .return call to continue in the // outer generator. if (context.method !== "return") { context.method = "next"; context.arg = undefined; } } else { // Re-yield the result returned by the delegate method. return info; } // The delegate iterator is finished, so forget it and continue with // the outer generator. context.delegate = null; return ContinueSentinel; } // Define Generator.prototype.{next,throw,return} in terms of the // unified ._invoke helper method. defineIteratorMethods(Gp); Gp[toStringTagSymbol] = "Generator"; // A Generator should always return itself as the iterator object when the // @@iterator function is called on it. Some browsers' implementations of the // iterator prototype chain incorrectly implement this, causing the Generator // object to not be returned from this call. This ensures that doesn't happen. // See https://github.com/facebook/regenerator/issues/274 for more details. Gp[iteratorSymbol] = function() { return this; }; Gp.toString = function() { return "[object Generator]"; }; function pushTryEntry(locs) { var entry = { tryLoc: locs[0] }; if (1 in locs) { entry.catchLoc = locs[1]; } if (2 in locs) { entry.finallyLoc = locs[2]; entry.afterLoc = locs[3]; } this.tryEntries.push(entry); } function resetTryEntry(entry) { var record = entry.completion || {}; record.type = "normal"; delete record.arg; entry.completion = record; } function Context(tryLocsList) { // The root entry object (effectively a try statement without a catch // or a finally block) gives us a place to store values thrown from // locations where there is no enclosing try statement. this.tryEntries = [{ tryLoc: "root" }]; tryLocsList.forEach(pushTryEntry, this); this.reset(true); } exports.keys = function(object) { var keys = []; for (var key in object) { keys.push(key); } keys.reverse(); // Rather than returning an object with a next method, we keep // things simple and return the next function itself. return function next() { while (keys.length) { var key = keys.pop(); if (key in object) { next.value = key; next.done = false; return next; } } // To avoid creating an additional object, we just hang the .value // and .done properties off the next function object itself. This // also ensures that the minifier will not anonymize the function. next.done = true; return next; }; }; function values(iterable) { if (iterable) { var iteratorMethod = iterable[iteratorSymbol]; if (iteratorMethod) { return iteratorMethod.call(iterable); } if (typeof iterable.next === "function") { return iterable; } if (!isNaN(iterable.length)) { var i = -1, next = function next() { while (++i < iterable.length) { if (hasOwn.call(iterable, i)) { next.value = iterable[i]; next.done = false; return next; } } next.value = undefined; next.done = true; return next; }; return next.next = next; } } // Return an iterator with no values. return { next: doneResult }; } exports.values = values; function doneResult() { return { value: undefined, done: true }; } Context.prototype = { constructor: Context, reset: function(skipTempReset) { this.prev = 0; this.next = 0; // Resetting context._sent for legacy support of Babel's // function.sent implementation. this.sent = this._sent = undefined; this.done = false; this.delegate = null; this.method = "next"; this.arg = undefined; this.tryEntries.forEach(resetTryEntry); if (!skipTempReset) { for (var name in this) { // Not sure about the optimal order of these conditions: if (name.charAt(0) === "t" && hasOwn.call(this, name) && !isNaN(+name.slice(1))) { this[name] = undefined; } } } }, stop: function() { this.done = true; var rootEntry = this.tryEntries[0]; var rootRecord = rootEntry.completion; if (rootRecord.type === "throw") { throw rootRecord.arg; } return this.rval; }, dispatchException: function(exception) { if (this.done) { throw exception; } var context = this; function handle(loc, caught) { record.type = "throw"; record.arg = exception; context.next = loc; if (caught) { // If the dispatched exception was caught by a catch block, // then let that catch block handle the exception normally. context.method = "next"; context.arg = undefined; } return !! caught; } for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; var record = entry.completion; if (entry.tryLoc === "root") { // Exception thrown outside of any try block that could handle // it, so set the completion value of the entire function to // throw the exception. return handle("end"); } if (entry.tryLoc <= this.prev) { var hasCatch = hasOwn.call(entry, "catchLoc"); var hasFinally = hasOwn.call(entry, "finallyLoc"); if (hasCatch && hasFinally) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } else if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else if (hasCatch) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } } else if (hasFinally) { if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else { throw new Error("try statement without catch or finally"); } } } }, abrupt: function(type, arg) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { var finallyEntry = entry; break; } } if (finallyEntry && (type === "break" || type === "continue") && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc) { // Ignore the finally entry if control is not jumping to a // location outside the try/catch block. finallyEntry = null; } var record = finallyEntry ? finallyEntry.completion : {}; record.type = type; record.arg = arg; if (finallyEntry) { this.method = "next"; this.next = finallyEntry.finallyLoc; return ContinueSentinel; } return this.complete(record); }, complete: function(record, afterLoc) { if (record.type === "throw") { throw record.arg; } if (record.type === "break" || record.type === "continue") { this.next = record.arg; } else if (record.type === "return") { this.rval = this.arg = record.arg; this.method = "return"; this.next = "end"; } else if (record.type === "normal" && afterLoc) { this.next = afterLoc; } return ContinueSentinel; }, finish: function(finallyLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.finallyLoc === finallyLoc) { this.complete(entry.completion, entry.afterLoc); resetTryEntry(entry); return ContinueSentinel; } } }, "catch": function(tryLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc === tryLoc) { var record = entry.completion; if (record.type === "throw") { var thrown = record.arg; resetTryEntry(entry); } return thrown; } } // The context.catch method must only be called with a location // argument that corresponds to a known catch block. throw new Error("illegal catch attempt"); }, delegateYield: function(iterable, resultName, nextLoc) { this.delegate = { iterator: values(iterable), resultName: resultName, nextLoc: nextLoc }; if (this.method === "next") { // Deliberately forget the last sent value so that we don't // accidentally pass it on to the delegate. this.arg = undefined; } return ContinueSentinel; } }; // Regardless of whether this script is executing as a CommonJS module // or not, return the runtime object so that we can declare the variable // regeneratorRuntime in the outer scope, which allows this module to be // injected easily by `bin/regenerator --include-runtime script.js`. return exports; }( // If this script is executing as a CommonJS module, use module.exports // as the regeneratorRuntime namespace. Otherwise create a new empty // object. Either way, the resulting object will be used to initialize // the regeneratorRuntime variable at the top of this file. typeof module === "object" ? module.exports : {} )); [代码] 后端 [代码]const typeDefs = gql` # schema 下面是根类型,约定是 RootQuery 和 RootMutation schema { query: Query } # 定义具体的 Query 的结构 type Query { # 登陆接口 login(code: String!, encryptedData: String, iv: String): Login } type Login { token: String! userInfo: UserInfo } type UserInfo { nickName: String gender: String avatarUrl: String } `; const resolvers = { Query: { async login(parent, { code, encryptedData, iv }) { const { sessionKey, openId, unionId } = await wxService.code2Session(code); const userInfo = encryptedData && iv ? wxService.decryptData(sessionKey, encryptedData, iv) : { openId, unionId }; if (userInfo.nickName) { userService.createOrUpdateWxUser(userInfo); } const token = await userService.generateJwtToken(userInfo); return { token, userInfo }; }, }, }; [代码]
2019-04-21 - 中秋节打动人的朋友圈文案和微信图文排版样式来了!
中秋节打动人的朋友圈文案和微信图文排版样式来了! 八月十五中秋节朋友圈文案: 1、回家,对中国人来说从来不是新闻,但你每次的回家,对家人来说都是重大事件。 家,是总有人等你回来的地方。 2、亲爱的月老,麻烦您敬业一点,这么亮的月光,你还是找不到我的另一半吗? 3、回不去的时光,回得去的家。 4、俗话说,有缘千里来相会;俗话又说,冤家路窄。 5、有的人,过节就要回家玩;有的人,想回家回不去;也有的人,就喜欢节日出去浪。 有的人中秋都赏月亮吃月饼合家团聚,有的却喜欢统统反着来,以自己的方式过节。 6、这个好的日子,一定要和喜欢的人在一起。 7、谢谢大家的中秋礼物 因为没有收到 所以就不一一感谢了。 8、月亮是我抛的硬币,两面都是祝福你。 9、长大后更明白,安乐团圆的意思。 10、月亮代表太多人的心,让它歇歇吧。 [图片] 11、亲爱的,中秋节到了,我为你定做了一个月饼。主料:我爱你!配料:思念。辅料:煎熬加寂寞少许!生产日期:从见到你那天起!保存日期:无限期!标准号:1314。 12、抱歉,月亮今天忙着营业,所以中秋快乐我来对你说。 13、今年中秋不收礼,收礼只收人民币。 14、你看那月亮有多圆,好像你的脸盘。 15、根据我对中秋作业的推断,我已经无法再直视国庆了。 16、如果恰好有人在中秋节这一天生日,那可真好,全国人民为你庆生。 17、别匆忙,记得抬头看看月亮,和星光。 18、如果中秋节那天没人陪你,那就给自己一个爱的抱抱吧。 19、今天的月亮好像不够圆呀,是不是因为你没在我身边? 20、中秋之际,我谨代表党中央、国务院、以及拉登兄和基地全体勇士、布什、普京、布莱尔,向你并通过你向全家人致以最真挚的问候:中秋快乐! [图片] 21、愿你也能自带光芒,和中秋的月亮不分上下。 22、这个节日是浪漫的,和月亮谈个恋爱吧。 23、愿你过的每一天都象十五的月亮一样成功圆满。 24、 中秋一个人,终究一个人。 25、中秋过完,再放假就过年了,珍惜年前最后的狂欢吧。 26、 就让我一个人安静的过完中秋吧,不用人陪我也可以过的很好。 27、今年中秋,想对月许下三个愿望:一愿美梦好似月儿圆,二愿日子更比月饼甜,三愿美貌犹如月中仙。 28、你看那圆圆的月亮,是生活馈赠给我们的光。 29、和十五的月亮一样圆的,还有我的脸。 30、团圆是月饼里最好的馅儿。 [图片] 31、让我们红尘作伴,看月亮白白胖胖。 32、中秋是一个月亮的孤独,一群月饼的狂欢。 33、真想和你坐在屋顶,吃着月饼,看看月光。 34、没有到不了的远方,没有回不去的家乡。 35、中秋节减肥是对月饼的不尊重。 36、快中秋节了可以教我做月饼吗?我有点笨,做什么都容易露馅,喜欢你也是。 37、月亮在慢慢变圆,事情也会慢慢变好。 38、愿世间所有的团圆,都不必跋山涉水相见。 39、不能一起吃月饼,但我们拥有同一个月亮。 40、愿世间所有的团圆,都不必跋山涉水相见。 八月十五中秋节微信图文排版样式: 期待回家的同时,还得完成手里的工作,还有中秋节的公众号推文也没有写呢,那正好,小编这里刚好整理了一些中秋节排版的简约动图样式,分享给你哦~~ (1)样式ID:17198 [图片] (2)样式ID:45818 [图片] (3)样式ID:45784 [图片] (4)样式ID:28693 [图片] (5)样式ID:45792 [图片] (6)样式ID:45768 [图片] (7)样式ID:45785 [图片] (8)样式ID:8707 [图片] (9)样式ID:45787 [图片] (10)样式ID:45788 [图片] 以上这些中秋的动图样式素材怎么使用呢?打开网页搜索【速排小蚂蚁编辑器】,然后点击左侧导航栏的【单样式】,在输入框输入样式ID进行搜索,就会出现相对应的素材啦!直接点击使用就可以啦~~~ [图片] 如果想要获取更多素材可以选择【节日】标签,点击【中秋节】就可以啦~~ [图片]
2021-09-06 - CC校友登记小程序(云开发)
项目介绍 但是由于年代久远,学校又经历了多次合并发展,有些校友毕业后流动等原因,目前还有很多校友的信息校友分会没有掌握,这极大地影响了校友分会向更多的校友提供母校发展的最新信息并提供更为贴心的服务, 有鉴于此,校友会决定开展校友信息登记工作。这次信息登记。 各位校友所登记的信息也将仅用于校友相关事宜,不会被用于商业用途,亦不用担心信息外泄等。 同时,恳请各位向同班、同届和认识的其他校友广为推介这次校友信息登记活动,让更多的校友加入校友会这个大家庭。 功能说明 [图片] 特色特点 简约:不臃肿,主打内容极简,功能简洁直击痛点 安全:保护校友的信息安全,隐私内容仅后台管理员后可见。 方便:上传自己的个人信息,方便在需要时取得联系。小程序无需下载APP随用随走。 技术运用 项目使用微信小程序平台进行开发。 使用腾讯云开发技术,免费资源配额, 无需域名和服务器即可搭建。 小程序本身的即用即走,适合小工具的使用场景,也适合程序的开发。 项目效果截图 [图片] [图片] [图片] [图片] [图片] [图片] 项目后台截图 [图片] [图片] [图片] 部署教程: 1 源码导入微信开发者工具 [图片] 2 开通云开发环境 参考微信官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html 在使用云开发能力之前,需要先开通云开发。 在开发者工具的工具栏左侧,点击 “云开发” 按钮即可打开云控制台,根据提示开通云开发,并且创建一个新的云开发环境。 [图片] 每个环境相互隔离,拥有唯一的环境 ID(拷贝此ID,后面配置用到),包含独立的数据库实例、存储空间、云函数配置等资源; 3 云函数及配置 本项目使用到了一个云函数reg_cloud [图片] 在云函数cloudfunctions文件夹下选择云函数reg_cloud , 右键选择在终端中打开,然后执行 npm install –product [图片] [图片] 打开cloudfunctions/reg_cloud/comm/ccmini_config.js文件,配置环境ID和后台管理员手机号码 [图片] 4 客户端配置 打开miniprogram/app.js文件,配置环境ID [图片] 5 云函数配置 在微信开发者工具-》云开发-》云函数-》对指定的函数添加环境变量 [服务端时间时区TZ] =>Asia/Shanghai [函数内存] =>128M [函数超时时间] => 20秒 [图片] 6 设置图片域名信任关系 进入小程序 开发管理=》开发设置=》服务器域名 =》downloadFile合法域名 添加2个域名: 1)你的云存储域名,格式类似:https://1234-test-pi5po-1250248.tcb.qcloud.la 2)微信头像域名:https://thirdwx.qlogo.cn [图片] 7 上传云函数&指定云环境ID [图片] 至此完全部署配置完毕。 gitee源码地址 https://gitee.com/minzonetech/ccreg/ 在线演示: [图片] 如有疑问,欢迎骚扰联系我鸭: 俺的微信: cclinux0730 https://gitee.com/minzonetech/ccreg
2021-08-17 - 如何写一个自己的脚手架 - 一键初始化项目
如何写一个自己的脚手架 - 一键初始化项目 介绍 脚手架的作用:为减少重复性工作而做的重复性工作 即为了开发中的:编译 es6,js 模块化,压缩代码,热更新等功能,我们使用[代码]webpack[代码]等打包工具,但是又带来了新的问题:初始化工程的麻烦,复杂的[代码]webpack[代码]配置,以及各种配置文件,所以就有了一键生成项目,0 配置开发的脚手架 本文项目代码地址 本文以我司的脚手架工具 简化之后为基础 本系列分 3 篇,详细介绍如何实现一个脚手架: 一键初始化项目 0 配置开发环境与打包 一键上传服务器 首先说一下个人的开发习惯 在写功能前我会先把调用方式写出了,然后一步一步的从使用者的角度写,现将基础功能写好后,慢慢完善 例如一键初始化项目功能 我期望的就是 在命令行执行输入 [代码]my-cli create text-project[代码],回车后直接创建项目并生成模板,还会把依赖都下载好 我们下面就从命令行开始入手 创建项目 [代码]my-cli[代码],执行 [代码]npm init -y[代码]快速初始化 bin [代码]my-cli[代码]: 在 [代码]package.json[代码] 中加入: [代码]{ "bin": { "my-cli": "bin.js" } } [代码] [代码]bin.js[代码]: [代码]#!/usr/bin/env node console.log(process.argv); [代码] [代码]#!/usr/bin/env node[代码],这一行是必须加的,就是让系统动态的去[代码]PATH[代码]目录中查找[代码]node[代码]来执行你的脚本文件。 命令行执行 [代码]npm link[代码] ,创建软链接至全局,这样我们就可以全局使用[代码]my-cli[代码]命令了,在开发 [代码]npm[代码] 包的前期都会使用[代码]link[代码]方式在其他项目中测试来开发,后期再发布到[代码]npm[代码]上 命令行执行 [代码]my-cli 1 2 3[代码] 输出:[代码][ '/usr/local/bin/node', '/usr/local/bin/my-cli', '1', '2', '3' ][代码] 这样我们就可以获取到用户的输入参数 例如[代码]my-cli create test-project[代码] 我们就可以通过数组第 [2] 位判断命令类型[代码]create[代码],通过第 [3] 位拿到项目名称[代码]test-project[代码] commander [代码]node[代码]的命令行解析最常用的就是[代码]commander[代码]库,来简化复杂[代码]cli[代码]参数操作 (我们现在的参数简单可以不使用[代码]commander[代码],直接用[代码]process.argv[3][代码]获取名称,但是为了之后会复杂的命令行,这里也先使用[代码]commander[代码]) [代码]#!/usr/bin/env node const program = require("commander"); const version = require("./package.json").version; program.version(version, "-v, --version"); program .command("create <app-name>") .description("使用 my-cli 创建一个新的项目") .option("-d --dir <dir>", "创建目录") .action((name, command) => { const create = require("./create/index"); create(name, command); }); program.parse(process.argv); [代码] [代码]commander[代码] 解析完成后会触发[代码]action[代码]回调方法 命令行执行:[代码]my-cli -v[代码] 输出:[代码]1.0.0[代码] 命令行执行: [代码]my-cli create test-project[代码] 输出:[代码]test-project[代码] 创建项目 拿到了用户传入的名称,就可以用这么名字创建项目 我们的代码尽量保持[代码]bin.js[代码]整洁,不将接下来的代码写在[代码]bin.js[代码]里,创建[代码]create[代码]文件夹,创建[代码]index.js[代码]文件 [代码]create/index.js[代码]中: [代码]const path = require("path"); const mkdirp = require("mkdirp"); module.exports = function(name) { mkdirp(path.join(process.cwd(), name), function(err) { if (err) console.error("创建失败"); else console.log("创建成功"); }); }; [代码] [代码]process.cwd()[代码]获取工作区目录,和用户传入项目名称拼接起来 (创建文件夹我们使用[代码]mkdirp[代码]包,可以避免我们一级一级的创建目录) 修改[代码]bin.js[代码]的[代码]action[代码]方法: [代码]// bin.js .action(name => { const create = require("./create") create(name) }); [代码] 命令行执行: [代码]my-cli create test-project[代码] 输出:[代码]创建成功[代码] 并在命令行所在目录创建了一个[代码]test-project[代码]文件夹 模板 首先需要先列出我们的模板包含哪些文件 一个最基础版的[代码]vue[代码]项目模板: [代码]|- src |- main.js |- App.vue |- components |- HelloWorld.vue |- index.html |- package.json [代码] 这些文件就不一一介绍了 我们需要的就是生成这些文件,并写入到目录中去 模板的写法后很多种,下面是我的写法: 模板目录: [代码]|- generator |- index-html.js |- package-json.js |- main.js |- App-vue.js |- HelloWorld-vue.js [代码] [代码]generator/index-html.js[代码] 模板示例: [代码]module.exports = function(name) { const template = ` { "name": "${name}", "version": "1.0.0", "description": "", "main": "index.js", "scripts": {}, "devDependencies": { }, "author": "", "license": "ISC", "dependencies": { "vue": "^2.6.10" } } `; return { template, dir: "", name: "package.json" }; }; [代码] [代码]dir[代码]就是目录,例如[代码]main.js[代码]的[代码]dir[代码]就是[代码]src[代码] [代码]create/index.js[代码]在[代码]mkdirp[代码]中新增: [代码]const path = require("path"); const mkdirp = require("mkdirp"); const fs = require("fs"); module.exports = function(name) { const projectDir = path.join(process.cwd(), name); mkdirp(projectDir, function(err) { if (err) console.error("创建失败"); else { console.log(`创建${name}文件夹成功`); const { template, dir, name: fileName } = require("../generator/package")(name); fs.writeFile(path.join(projectDir, dir, fileName), template.trim(), function(err) { if (err) console.error(`创建${fileName}文件失败`); else { console.log(`创建${fileName}文件成功`); } }); } }); }; [代码] 这里只写了一个模板的创建,我们可以用[代码]readdir[代码]来获取目录下所有文件来遍历执行 下载依赖 我们平常下载[代码]npm[代码]包都是使用命令行 [代码]npm install / yarn install[代码] 这时就需要用到 [代码]node[代码] 的 [代码]child_process.spawn[代码] api 来调用系统命令 因为考虑到跨平台兼容处理,所以使用 cross-spawn 库,来帮我们兼容的操作命令 我们创建[代码]utils[代码]文件夹,创建[代码]install.js[代码] [代码]utils/install.js[代码]: [代码]const spawn = require("cross-spawn"); module.exports = function install(options) { const cwd = options.cwd || process.cwd(); return new Promise((resolve, reject) => { const command = options.isYarn ? "yarn" : "npm"; const args = ["install", "--save", "--save-exact", "--loglevel", "error"]; const child = spawn(command, args, { cwd, stdio: ["pipe", process.stdout, process.stderr] }); child.once("close", code => { if (code !== 0) { reject({ command: `${command} ${args.join(" ")}` }); return; } resolve(); }); child.once("error", reject); }); }; [代码] 然后我们就可以在创建完模板后调用[代码]install[代码]方法下载依赖 [代码]install({ cwd: projectDir }); [代码] 要知道工作区为我们项目的目录 至此,解析 cli,创建目录,创建模板,下载依赖一套流程已经完成 基本功能都跑通之后下面就是要填充剩余代码和优化 优化 当代码写的多了之后,我们看上面[代码]create[代码]方法内的回调嵌套回调会非常难受 [代码]node 7[代码]已经支持[代码]async,await[代码],所以我们将上面代码改成[代码]Promise[代码] 在[代码]utils[代码]目录下创建,[代码]promisify.js[代码]: [代码]module.exports = function promisify(fn) { return function(...args) { return new Promise(function(resolve, reject) { fn(...args, function(err, ...res) { if (err) return reject(err); if (res.length === 1) return resolve(res[0]); resolve(res); }); }); }; }; [代码] 这个方法帮我们把回调形式的[代码]Function[代码]改成[代码]Promise[代码] 在[代码]utils[代码]目录下创建,[代码]fs.js[代码]: [代码]const fs = require(fs); const promisify = require("./promisify"); const mkdirp = require("mkdirp"); exports.writeFile = promisify(fs.writeFile); exports.readdir = promisify(fs.readdir); exports.mkdirp = promisify(mkdirp); [代码] 将[代码]fs[代码]和[代码]mkdirp[代码]方法改造成[代码]promise[代码] 改造后的[代码]create.js[代码]: [代码]const path = require("path"); const fs = require("../utils/fs-promise"); const install = require("../utils/install"); module.exports = async function(name) { const projectDir = path.join(process.cwd(), name); await fs.mkdirp(projectDir); console.log(`创建${name}文件夹成功`); const { template, dir, name: fileName } = require("../generator/package")(name); await fs.writeFile(path.join(projectDir, dir, fileName), template.trim()); console.log(`创建${fileName}文件成功`); install({ cwd: projectDir }); }; [代码] 结语 关于进一步优化: 更多功能与健壮 例如指定目录创建项目,目录不存在等情况 [代码]chalk[代码]和[代码]ora[代码]优化[代码]log[代码],给用户更好的反馈 通过[代码]inquirer[代码]问询用户得到更多的选择:模板[代码]vue-router[代码],[代码]vuex[代码]等更多初始化模板功能,[代码]eslint[代码] 更多的功能: 内置 webpack 配置 一键发布服务器 其实要学会善用第三方库,你会发现我们上面的每个模块都有第三方库的身影,我们只是将这些功能组装起来,再结合我们的想法进一步封装 虽然有[代码]vue-cli[代码],[代码]create-react-app[代码]这些已有的脚手架,但是我们还是可能在某些情况下需要自己实现脚手架部分功能,根据公司的业务来封装,减少重复性工作,或者了解一下内部原理
2019-09-26 - 【优化】利用函数防抖和函数节流提高小程序性能
大家好,上次给大家分享了swiper仿tab的小技巧: https://developers.weixin.qq.com/community/develop/article/doc/000040a5dc4518005d2842fdf51c13 [代码]今天给大家分享两个有用的函数,《函数防抖和函数节流》 函数防抖和函数节流是都优化高频率执行js代码的一种手段,因为是js实现的,所以在小程序里也是适用的。 [代码] 首先先来理解一下两者的概念和区别: [代码] 函数防抖(debounce)是指事件在一定时间内事件只执行一次,如果在这段时间又触发了事件,则重新开始计时,打个很简单的比喻,比如在打王者荣耀时,一定要连续干掉五个人才能触发hetai kill '五连绝世'效果,如果中途被打断就得重新开始连续干五个人了。 函数节流(throttle)是指限制某段时间内事件只能执行一次,比如说我要求自己一天只能打一局王者荣耀。 这里也有个可视化工具可以让大家看一下三者的区别,分别是正常情况下,用了函数防抖和函数节流的情况下:http://demo.nimius.net/debounce_throttle/ [代码] 适用场景 函数防抖 搜索框搜索联想。只需用户最后一次输入完,再发送请求 手机号、邮箱验证输入检测 窗口resize。只需窗口调整完成后,计算窗口大小。防止重复渲染 高频点击提交,表单重复提交 函数节流 滚动加载,加载更多或滚到底部监听 搜索联想功能 实现原理 [代码] 函数防抖 [代码] [代码]const _.debounce = (func, wait) => { let timer; return () => { clearTimeout(timer); timer = setTimeout(func, wait); }; }; [代码] [代码] 函数节流 [代码] [代码]const throttle = (func, wait) => { let last = 0; return () => { const current_time = +new Date(); if (current_time - last > wait) { func.apply(this, arguments); last = +new Date(); } }; }; [代码] [代码] 上面两个方法都是比较常见的,算是简化版的函数 [代码] lodash中的 Debounce 、Throttle [代码] lodash中已经帮我们封装好了这两个函数了,我们可以把它引入到小程序项目了,不用全部引入,只需要引入debounce.js和throttle.js就行了,链接:https://github.com/lodash/lodash 使用方法可以看这个代码片段,具体的用法可以看上面github的文档,有很详细的介绍:https://developers.weixin.qq.com/s/vjutZpmL7A51[代码]
2019-02-22 - 抽奖小程序抽奖算法的实现,附赠算法库
简易思路如下: 1、获取参与抽奖的人员列表,组合为数组 2、使用数组乱序算法,将抽奖列表大乱 3、根据一定规则取出打乱后的数组中的抽奖人员,这些就是中奖人员了 乱序算法思路具体如下,使用Fisher–Yates 洗牌算法: Fisher–Yates 算法由Fisher和Yates这两个人的发明的,一开始只是用来人工混排(真实荷官,现场发牌……)一组数字序列,原始算法的步骤非常容易理解。 写下从 1 到 N 的数字(一副扑克)取一个从 1 到剩下的数字(包括这个数字)的随机数 k——(从中间抽一沓,)从低位开始,得到第 k 个数字(这个数字还没有被取出),把它写在独立的一个列表的最前面一位重复第 2 步,直到所有的数字都被取出第 3 步写出的这个序列,现在就是原始数字的随机排列说通俗点就是: 一副扑克从中间随便取一沓(取的越少,越容易实现乱序,算法中只取一个)把取出的牌放在旁边,堆成一堆重复2、3步骤,直到手里没牌第三步开始另外堆起来的牌,就足够乱了算法库地址如下,需要自取: https://www.npmjs.com/package/lodash
2020-12-13 - 如何使用云开发实现发送邮件功能
新手:因为是刚接触云开发,所以有说的不对的地方,大佬帮忙指正。 背景:摆脱每周五晚写周报的烦恼。 目标:本次小目标就先实现小程序云函数发送邮件功能。 终极目标,写一个todolist小程序,每周五汇总本周工作,下周工作,使用云函数定时器触发,定时发送邮件。 [图片] 首先给大家介绍一个发邮件的第三方模块,nodemailer Nodemailer Github地址 Nodemailer官方文档 找到官方文档最下面,有一串示例代码。这次我们要用到的就是它了。 使用nodemailer我们需要使用到的传输方式:SMTP 文档如下:https://nodemailer.com/smtp/ 这里有介绍如何使用,示例代码我就不给大家复制过来了。 何为SMTP? 这里给大家百度了一发: SMTP是一种提供可靠且有效的电子邮件传输的协议。SMTP是建立在FTP文件传输服务上的一种邮件服务,主要用于系统之间的邮件信息传递,并提供有关来信的通知。SMTP独立于特定的传输子系统,且只需要可靠有序的数据流信道支持,SMTP的重要特性之一是其能跨越网络传输邮件,即“SMTP邮件中继”。使用SMTP,可实现相同网络处理进程之间的邮件传输,也可通过中继器或网关实现某处理进程与其他网络之间的邮件传输。 [图片] 怎么开启SMTP服务? 自己搭建邮件服务器是非常麻烦的,我们可以借助于QQ邮箱、Gmail、163个人邮件系统或企业邮件系统开启SMTP服务,SMTP也就是简单邮件传输协议,通过它可以控制邮件的中转方式,帮助计算机在发送或中转信件时找到下一个目的地,也就是发送邮件。 不同的邮件系统有着不同的smtp发送邮件服务器,端口号也会有所不同。 我这边以QQ邮箱为示例操作下。 在QQ邮箱的设置>账户 [图片] [图片] 我们可以看到IMAP/SMTP的开启和关闭,这里我们先开启,然后你会得到一串授权码,保存下这个授权码,我们后续会用到。QQ邮箱的发送邮件服务器:smtp.qq.com,使用SSL,端口号465或587。 [图片] 使用云函数发送邮件 首先我们创建一个云函数,比如就叫sendMail 吧。在该云函数的package.json里添加如下代码。右键点击sendMail云函数,在终端中打开,或者本地终端cd到该云函数文件夹内也可。输入npm install 安装nodemailer最新版依赖。 [代码] "dependencies": { "wx-server-sdk": "~2.1.2", "nodemailer": "latest" } [代码] 然后在index.js中添加如下代码: [代码]const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event) => { const nodemailer = require("nodemailer"); let transporter = nodemailer.createTransport({ host: "smtp.qq.com", //SMTP服务器地址 port: 465, //端口号,通常为465,587,25,不同的邮件客户端端口号可能不一样 secure: true, //如果端口是465,就为true;如果是587、25,就填false auth: { user: "8662054@qq.com", //这是我的邮箱账号,改为你们的即可。 pass: "myukmlybjotjbghh" //这里是我们上面说要用到的授权码,不是QQ邮箱的密码 } }); let postMsg = { from: event.from, //发件邮箱 to:event.to, //收件人 subject: event.subject, text: event.text, html: event.html // text 和 html只能同时存在一个。 }; let res = await transporter.sendMail(postMsg); return res; } [代码] 右键部署上传云函数。 到这里我们的发送邮件云函数就完成啦。 [图片] 如何调用发送邮件云函数? 调用方式也很简单: [代码]<input type="text" placeholder="请输入收件人邮件地址" bindinput="handleToAddress"/> <input type="text" placeholder="请输入邮件主题" bindinput="handleSubject"/> <textarea placeholder="请输入邮件内容" auto-height bindinput="handleTextarea" /> <button bindtap="handleClick">发送邮件</button> [代码] [图片] 这里因为我们绑定了 发件人是我的固定邮箱,所以没有增加发送人的输入框。 textarea 我有尝试使用editor组件生成富文本进行操作,最后发现弄出来样式太龊,就干掉了。 [代码]对了差点忘了: 邮件是支持: html:富文本 cc: 支持 抄送 bcc: 支持 密送 attachments: 支持多种附件形式,可以是String, Buffer或Stream 目前我这边用不到,就不展示了,有需要的可查看官方文档,或者评论给你示例代码。 [代码] 至于我们调用的地方: 首先拿到输入的值,然后调用云函数发送。 [代码]Page({ data: { }, handleToAddress(e) { this.setData({ toAddress: e.detail.value }) }, handleSubject(e) { this.setData({ toSubject: e.detail.value }) }, handleTextarea(e) { this.setData({ toTextArea: e.detail.value }) }, handleClick() { wx.showLoading({ title: '发送中' }) const {toAddress, toSubject, toTextArea} = this.data wx.cloud.callFunction({ name: 'sendMail', data: { from: '8662054@qq.com', to: toAddress, subject: toSubject, text: toTextArea // html: '<p><b>你好:</b><img src=""></p>' +'<p>欢迎欢迎<br/></p>' }, success: res => { wx.hideLoading() console.log(res) wx.showToast({ title: '发送成功' }) }, fail: err =>{ wx.hideLoading() console.log(err) } }) } }) [代码] 实现效果图 [图片] [图片] [图片] 以上就完成了使用云开发实现发送邮件功能。 有不懂的欢迎评论留言。 老规矩结尾的代码片段 没了 😂 。 代码仓库地址:https://github.com/minchangyong/wx-cloud-demo 最后感谢李东bbsky 大佬指导。
2020-07-09 - 【小程序技巧】学习腾讯QQ的UI设计,实现一个模态框组件
前言 本文是笔者分享的第一篇文章,大佬们嘴下留情😆。写这个模态框组件,刚好是看到了腾讯QQ的一些UI设计挺舒服的,以后也会分享如何实现一个一样的页面设计。 1.1 腾讯QQ的模态框 [图片] 1.2 我实现的模态框 [图片] [图片] 2.1 参数说明 属性 类型 默认值 说明 isShow Boolean false 是否显示模态框 mask Boolean true 是否显示遮罩 title String “温馨提示” 模态框的标题 content String “” 模态框的内容 showCancel Boolean true 是否显示取消按钮 cancelText String “取消” 取消按钮的文字,最多 6 个字符 cancelColor String #232323 取消按钮的文字颜色,必须是 16 进制格式的颜色字符串 confirmText String “确定” 确认按钮的文字,最多 6 个字符 confirmColor String #232323 确认按钮的文字颜色,必须是 16 进制格式的颜色字符串 2.2 事件回调 Object res.detail [代码]// page.wxml <custom-modal bind:action="showModal" /> // page.js showModal (res) { if (res.detail.confirm) { // doing something after click confirm } else if (res.detail.cancel) { // doing something after click cancel } } // res.detail { cancel: Boolean, confirm: Boolean, errMsg: "showModal:ok" } [代码] 最后附上代码片段,不足之处还请各位大佬不吝赐教 😊😊
2022-01-25 - JavaScript 中哪一种循环最快呢?
[图片]原文地址:Which type of loop is fastest in JavaScript?原文作者:kushsavani译者:霜羽 Hoarfroster校对者:Chorer、HumanBeing、用户1924214047173 了解哪一种 [代码]for[代码] 循环或迭代器适合我们的需求,防止我们犯下一些影响应用性能的低级错误。 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi [图片] JavaScript 是 Web 开发领域的“常青树”。无论是 JavaScript 框架(如 Node.js、React、Angular、Vue 等),还是原生 JavaScript,都拥有非常庞大的粉丝基础。我们来谈谈现代 JavaScript 吧。循环一直是大多数编程语言的重要组成部分,而现代 JavaScript 为我们提供了许多迭代或循环值的方法。 但问题在于,我们是否真的知道哪种循环或迭代最适合我们的需求。[代码]for[代码] 循环有很多变形,例如 [代码]for[代码]、[代码]for[代码](倒序)、[代码]for…of[代码]、[代码]forEach[代码]、[代码]for…in[代码]、[代码]for…await[代码]。本文将围绕这些展开讨论。 究竟哪一种循环更快?答案其实是: [代码]for[代码](倒序) 最让我感到惊讶的事情是,当我在本地计算机上进行测试之后,我不得不接受 [代码]for[代码](倒序)是所有 [代码]for[代码] 循环中最快的这一事实。下面我会举个对一个包含超过一百万项元素的数组执行一次循环遍历的例子。 声明:[代码]console.time()[代码] 结果的准确度在很大程度上取决于我们运行测试的系统配置。你可以在此处对准确度作进一步了解。 const million = 1000000; const arr = Array(million); // 注:这是稀疏数组,应该为其指定内容,否则不同方式的循环对其的处理方式会不同: // const arr = [...Array(million)] console.time('⏳'); for (let i = arr.length; i > 0; i--) {} // for(倒序) :- 1.5ms for (let i = 0; i < arr.length; i++) {} // for :- 1.6ms arr.forEach(v => v) // foreach :- 2.1ms for (const v of arr) {} // for...of :- 11.7ms console.timeEnd('⏳'); 造成这样结果的原因很简单,在代码中,正序和倒序的 [代码]for[代码] 循环几乎花费一样的时间,仅仅相差了 0.1 毫秒。原因是,[代码]for[代码](倒序)只需要计算一次起始变量 [代码]let i = arr.length[代码],而在正序的 [代码]for[代码] 循环中,它在每次变量增加后都会检查条件 [代码]i<arr.length[代码]。这个细微的差别不是很重要,你可以忽略它。(译者注:在数据量小或对时间不敏感的代码上,我们大可忽略它,但是根据译者的测试,当数据量扩大,例如十亿,千亿等的数量级,差距就显著提升,我们就需要考虑时间对应用程序性能的影响了。) 而 [代码]forEach[代码] 是 [代码]Array[代码] 原型的一个方法,与普通的 [代码]for[代码] 循环相比,[代码]forEach[代码] 和 [代码]for…of[代码] 需要花费更多的时间进行数组迭代。(译者注:但值得注意的是,[代码]for…of[代码] 和 [代码]forEach[代码] 都从对象中获取了数据,而原型并没有,因此没有可比性。) 循环的类型,以及我们应该在何处使用它们1. For 循环(正序和倒序)我想,也许大家都应该对这个基础循环非常熟悉了。我们可以在任何我们需要的地方使用 [代码]for[代码] 循环,按照核定的次数运行一段代码。最基础的 [代码]for[代码] 循环运行最迅速的,那我们每一次都应该使用它,对吗?并不然,性能不仅仅只是唯一尺度,代码可读性往往更加重要,就让我们选择适合我们应用程序的变形即可。 2. [代码]forEach[代码]这个方法需要接受一个回调函数作为输入参数,遍历数组的每一个元素,并执行我们的回调函数(以元素本身和它的索引(可选参数)作为参数赋予给回调函数)。[代码]forEach[代码] 还允许在回调函数中使用一个可选参数 [代码]this[代码]。 const things = ['have', 'fun', 'coding']; const callbackFun = (item, idex) => { console.log(`${item} - ${index}`); } things.foreach(callbackFun); /* 输出 have - 0 fun - 1 coding - 2 */ 需要注意的是,如果我们要使用 [代码]forEach[代码],我们不能使用 JavaScript 的短路运算符,即不能在每一次循环中跳过或结束循环。 3. [代码]for…of[代码][代码]for…of[代码] 是在 ES6(ECMAScript 6)中实现标准化的。它会对一个可迭代的对象(例如 [代码]array[代码]、[代码]map[代码]、[代码]set[代码]、[代码]string[代码] 等)创建一个循环,并且有一个突出的优点,即优秀的可读性。 const arr = [3, 5, 7]; const str = 'hello'; for (let i of arr) { console.log(i); // 输出 3, 5, 7 } for (let i of str) { console.log(i); // 输出 'h', 'e', 'l', 'l', 'o' } 需要注意的是,请不要在生成器中使用 [代码]for……of[代码],即便 [代码]for……of[代码] 循环提前终止。在退出循环后,生成器被关闭,并尝试再次迭代,不会产生任何进一步的结果。 4. [代码]for[代码] [代码]in[代码][代码]for…in[代码] 会在对象的所有可枚举属性上迭代指定的变量。对于每个不同的属性,[代码]for…in[代码] 语句除返回数字索引外,还将返回用户定义的属性的名称。 因此,在遍历数组时最好使用带有数字索引的传统 [代码]for[代码] 循环。 因为 [代码]for…in[代码] 语句还会迭代除数组元素之外的用户定义属性,就算我们修改了数组对象(例如添加自定义属性或方法),依然如此。 const details = {firstName: 'john', lastName: 'Doe'}; let fullName = ''; for (let i in details) { fullName += details[i] + ' '; // fullName: john doe } [代码]for…of[代码] 和 [代码]for…in[代码][代码]for…of[代码] 和 [代码]for…in[代码] 之间的主要区别是它们迭代的内容。[代码]for…in[代码] 循环遍历对象的属性,而 [代码]for…of[代码] 循环遍历可迭代对象的值。 let arr= [4, 5, 6]; for (let i in arr) { console.log(i); // '0', '1', '2' } for (let i of arr) { console.log(i); // '4', '5', '6' } [图片] 结论[代码]for[代码] 最快,但可读性比较差[代码]foreach[代码] 比较快,能够控制内容[代码]for...of[代码] 比较慢,但香[代码]for...in[代码] 比较慢,没那么方便最后,给你一条明智的建议 —— 优先考虑可读性。尤其是当我们开发复杂的结构程序时,更需要这样做。当然,我们也应该专注于性能。尽量避免增添不必要的、多余的花哨代码,因为这有时可能对你的程序性能造成严重影响。祝你编码愉快。 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi
2021-03-23 - 如何使用小程序的AR能力?
目前官方提供小程序基础能力,开发者可以接入第三方引擎,完成小程序AR效果开发。 微信相关的基础能力供参考: -摄像头组件(用户授权下,可获取视频帧数据): https://developers.weixin.qq.com/miniprogram/dev/api/media/camera/CameraContext.html -WebGL画布: https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html -完整示例代码: https://developers.weixin.qq.com/s/ElC24AmF729W -传感器: https://developers.weixin.qq.com/miniprogram/dev/api/device/compass/wx.onCompassChange.html#accuracy%20%E5%9C%A8%20iOS/Android%20%E7%9A%84%E5%B7%AE%E5%BC%82 -陀螺仪: https://developers.weixin.qq.com/miniprogram/dev/api/device/gyroscope/wx.startGyroscope.html https://developers.weixin.qq.com/miniprogram/dev/api/wx.onGyroscopeChange.html 欢迎开发者/开发平台进驻微信服务平台,成为服务商、提供插件,供更多服务商和品牌合作伙伴了解、联系你们。具体请参考:https://developers.weixin.qq.com/community/develop/doc/000e2ed8e44e98b1916884aa351008 开发者也可以多关注官方微信号"微信公开课"等解读:https://mp.weixin.qq.com/s/4_p2bAcp3OkTQizceFvMkQ
2019-10-09 - 微信小程序AR——商品引流
微信小程序AR [图片] 第一步、AR商品识别 扫一扫产品,扫一扫LOGO,扫一扫海报,扫一扫门店 基于AR图片识别技术,自己上传识别图,自定义识别操作 [图片] 第二步、交互式三维展示 360度产品展示、合拍分享到社交媒体 基于PBR物理渲染引擎,动画,支持手势缩放/拖拉/旋转操作 [图片] 第三步、虚拟试戴、虚拟化妆 逼真试戴体验、引流到在线商城 适合web/小程序的轻量深度神经网络、快速加载,对接微商城/微信小商店 「PLAY2XR眼镜试戴」 [图片] 「PLAY2XR口红试色」 [图片] 「PLAY2XR戒指试戴」 [图片] 扫码体验 [图片]
2020-10-20 - 小程序被封,申诉没过后就没有申诉入口了?
小程序被封,说我们涉嫌利诱其他用户参与、转发等等,违反的规则是5.3网赚行为。 主要是我们小程序都没有涉及这些,5.3里的规则更没有违反,有点冤枉。 申诉原因里面已经描述的很清楚了,申诉没通过,然后就没有申诉入口了。麻烦给看看,感谢。 appid:wx5b592676c1b297da
2021-03-02 - 如何实现一个简单的http请求的封装
好久没发文章了,最近浏览社区看到比较多的请求封装,以及还有在使用原始请求的童鞋。为了减少代码,提升观赏性,我也水一篇吧,希望对大家有所帮助。 默认请求方式,大家每次都这样一些写相同的代码,会不会觉得烦,反正我是觉得头大 😂 [代码]wx.request({ url: 'test.php', //仅为示例,并非真实的接口地址 data: { x: '', y: '' }, header: { 'content-type': 'application/json' // 默认值 }, success (res) { console.log(res.data) } }) [代码] 来,进入正题吧,把这块代码封装下。 首先新建个request文件夹,内含request.js 代码如下: [代码]/** * 网络请求封装 */ import config from '../config/config.js' import util from '../util/util.js' // 获取接口地址 const _getPath = path => (config.DOMAIN + path) // 封装接口公共参数 const _getParams = (data = {}) => { const timestamp = Date.now() //时间戳 const deviceId = Math.random() //随机数 const version = data.version || config.version //当前版本号,自定或者取小程序的都行 const appKey = data.appKey || config.appKey //某个小程序或者客户端的字段区分 //加密下,防止其他人随意刷接口,加密目前采用的md5,后端进行校验,这段里面的参数你们自定,别让其他人知道就行,我这里就是举个例子 const sign = data.sign || util.md5(config.appKey + timestamp + deviceId) return Object.assign({}, { timestamp, sign, deviceId, version, appKey }, data) } // 修改接口默认content-type请求头 const _getHeader = (headers = {}) => { return Object.assign({ 'content-type': `application/x-www-form-urlencoded` }, headers) } // 存储登录态失效的跳转 const _handleCode = (res) => { const {statusCode} = res const {msg, code} = res.data // code为 4004 时一般表示storage里存储的token失效或者未登录 if (statusCode === 200 && (code === 4004)) { wx.navigateTo({ url: '/pages/login/login' }) } return true } /** * get 请求, post 请求 * @param {String} path 请求url,必须 * @param {Object} params 请求参数,可选 * @param {String} method 请求方式 默认为 POST * @param {Object} option 可选配置,如设置请求头 { headers:{} } * * option = { * headers: {} // 请求头 * } * */ export const postAjax = (path, params) => { const url = _getPath(path) const data = _getParams(params) //如果某个参数值为undefined,则删掉该字段,不传给后端 for (let e in data) { if (data[e] === 'undefined') { delete data[e] } } // 处理请求头,加上最近比较流行的jwtToken(具体的自己百度去) const header = util.extend( true, { "content-type": "application/x-www-form-urlencoded", 'Authorization': wx.getStorageSync('jwtToken') ? `Bearer ${wx.getStorageSync('jwtToken')}` : '', }, header ); const method = 'POST' return new Promise((resolve, reject) => { wx.request({ url, method, data, header, success: (res) => { const result = _handleCode(res) result && resolve(res.data) }, fail: function (res) { reject(res.data) } }); }) } [代码] 那么如何调用呢? [代码]//把request的 postAjax注册到getApp()下,调用时: const app = getApp() let postData = { //这里填写请求参数,基础参数里的appKey等参数可在这里覆盖传入。 } app.postAjax(url, postData).then((res) => { if (res.success) { //这里处理请求成功逻辑。 } else { //wx.showToast大家觉得麻烦也可以写到util.js里,调用时:util.toast(msg) 即可。 wx.showToast({ title: res.msg || '服务器错误,请稍后重试', icon: "none" }) } }).catch(err => { //这里根据自己场景看是否封装到request.js里 console.log(err) }) [代码] config.js 主要是处理正式环境、预发环境、测试环境、开发环境的配置 [代码]//发版须修改version, env const env = { dev: { DOMAIN: 'https://dev-api.weixin.com' }, test: { DOMAIN: 'https://test-api.weixin.com', }, pro: { DOMAIN: 'https://api.qtshe.com' } } module.exports = { ...env.pro } [代码] 以上就是简单的一个request的封装,包含登录态失效统一跳转、包含公共参数的统一封装。 老规矩,最后放代码片段,util里内置了md5方法以及深拷贝方法,具体的我也不啰嗦,大家自行查看即可~ https://developers.weixin.qq.com/s/gbPSLOmd7Aft
2020-04-03