├── .babelrc ├── .gitignore ├── README.md ├── docs-src ├── develop │ ├── index.md │ └── 目录结构.md ├── static │ ├── images │ │ ├── fancyQrCode.jpg │ │ ├── qrCode │ │ │ ├── demo-customEntry.jpg │ │ │ ├── demo-dialog.jpg │ │ │ ├── demo-navigate.jpg │ │ │ ├── demo-noConcurrent.jpg │ │ │ ├── demo-operationGuide.jpg │ │ │ ├── demo-qrTest.jpg │ │ │ ├── demo-routeParams.jpg │ │ │ ├── demo-toast.jpg │ │ │ └── docs-index.png │ │ ├── toast │ │ │ ├── long.png │ │ │ ├── mid1.png │ │ │ ├── mid2.png │ │ │ └── short.png │ │ └── zzQrCode.jpg │ └── ppt │ │ └── fancy-mini_login.pptx └── tutorials │ ├── 0-getStarted.md │ ├── 1-demoProject.md │ ├── 2.1-login.md │ ├── 2.2-navigate.md │ ├── 2.3-request.md │ ├── 2.4-cookie.md │ ├── 2.5-canvasKit.md │ ├── 2.6-wxPromise.md │ ├── 2.7-routeParams.md │ ├── 2.8-eventHub.md │ ├── 2.9-customEntry.md │ ├── 2.a-qrTest.md │ ├── 2.b-rewardedVideoPlayer.md │ ├── 3.1-adaptiveToast.md │ ├── 3.2-textArea.md │ ├── 4.1-dialogCommon.md │ ├── 4.2-operationGuide.md │ ├── 5.1-noConcurrent.md │ ├── 5.2-errSafe.md │ ├── 5.3-compatible.md │ ├── 5.3.1-typeCheck.md │ ├── 5.4-wepyKit.md │ ├── 5.5-operationKit.md │ ├── 5.6-handleStr.md │ ├── 5.7-countDowner.md │ ├── structure.json │ └── 说明.txt ├── docs ├── AdaptiveToast.html ├── AdaptiveToast.js.html ├── BaseAuth.html ├── BaseLogin.html ├── BasePlugin.html ├── CloudFuncPlugin.html ├── Cookie.html ├── Cookie.js.html ├── CookiePlugin.html ├── EventHub.html ├── EventHub.js.html ├── FailRecoverPlugin.html ├── FormPlugin.html ├── History.html ├── InstantPlugin.html ├── LoginPlugin.html ├── Navigator.html ├── Requester.html ├── RewardedVideoPlayer.html ├── RewardedVideoPlayer.js.html ├── RouteParams.html ├── WechatAuth.html ├── canvasKit.js.html ├── countdowner.js.html ├── debugKit.js.html ├── decorator_compatible.js.html ├── decorator_errSafe.js.html ├── decorator_noConcurrent.js.html ├── decorator_typeCheck.js.html ├── decorators.js.html ├── globalEvents.js.html ├── handleStr.js.html ├── index.html ├── login_BaseLogin.js.html ├── login_auth_BaseAuth.js.html ├── login_auth_WechatAuth.js.html ├── module-canvasKit.html ├── module-compatible.html ├── module-countDowner.html ├── module-decorators.html ├── module-errSafe.html ├── module-globalEvents.html ├── module-handleStr.html ├── module-lib_canvasKit.html ├── module-lib_decorator_compatible.html ├── module-lib_decorator_errSafe.html ├── module-lib_decorator_noConcurrent.html ├── module-lib_decorators.html ├── module-lib_globalEvents.html ├── module-lib_operationKit.html ├── module-lib_uniAppKit.html ├── module-lib_wepyKit.html ├── module-lib_wxPromise.html ├── module-noConcurrent.html ├── module-operationKit.html ├── module-typeCheck.html ├── module-uniAppKit.html ├── module-wepyKit.html ├── module-wxPromise.html ├── navigate_History.js.html ├── navigate_Navigator.js.html ├── operationKit.js.html ├── request_Requester.js.html ├── request_plugin_BasePlugin.js.html ├── request_plugin_CloudFuncPlugin.js.html ├── request_plugin_CookiePlugin.js.html ├── request_plugin_FailRecoverPlugin.js.html ├── request_plugin_FormPlugin.js.html ├── request_plugin_InstantPlugin.js.html ├── request_plugin_LoginPlugin.js.html ├── routeParams.js.html ├── scripts │ ├── collapse.js │ ├── linenumber.js │ ├── nav.js │ ├── polyfill.js │ ├── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js │ └── search.js ├── static │ ├── images │ │ ├── fancyQrCode.jpg │ │ ├── qrCode │ │ │ ├── demo-customEntry.jpg │ │ │ ├── demo-dialog.jpg │ │ │ ├── demo-navigate.jpg │ │ │ ├── demo-noConcurrent.jpg │ │ │ ├── demo-operationGuide.jpg │ │ │ ├── demo-qrTest.jpg │ │ │ ├── demo-routeParams.jpg │ │ │ ├── demo-toast.jpg │ │ │ └── docs-index.png │ │ ├── toast │ │ │ ├── long.png │ │ │ ├── mid1.png │ │ │ ├── mid2.png │ │ │ └── short.png │ │ └── zzQrCode.jpg │ └── ppt │ │ └── fancy-mini_login.pptx ├── styles │ ├── jsdoc.css │ └── prettify.css ├── tutorial-0-getStarted.html ├── tutorial-1-demoProject.html ├── tutorial-2.1-login.html ├── tutorial-2.2-navigate.html ├── tutorial-2.3-request.html ├── tutorial-2.4-cookie.html ├── tutorial-2.5-canvasKit.html ├── tutorial-2.6-wxPromise.html ├── tutorial-2.7-routeParams.html ├── tutorial-2.8-eventHub.html ├── tutorial-2.9-customEntry.html ├── tutorial-2.a-qrTest.html ├── tutorial-2.b-rewardedVideoPlayer.html ├── tutorial-3.1-adaptiveToast.html ├── tutorial-3.2-textArea.html ├── tutorial-4.1-dialogCommon.html ├── tutorial-4.2-operationGuide.html ├── tutorial-5.1-noConcurrent.html ├── tutorial-5.2-errSafe.html ├── tutorial-5.3-compatible.html ├── tutorial-5.3.1-typeCheck.html ├── tutorial-5.4-wepyKit.html ├── tutorial-5.5-operationKit.html ├── tutorial-5.6-handleStr.html ├── tutorial-5.7-countDowner.html ├── uniAppKit.js.html ├── wepyKit.js.html └── wxPromise.js.html ├── jsdoc.conf.json ├── package-lock.json ├── package.json ├── scripts ├── clean.js ├── compile.js ├── doc.js ├── publish.js └── util.js └── src ├── components-uniApp └── DialogCommon.vue ├── components-wepy ├── CustomEntry.wpy ├── DialogCommon.wpy ├── QrCode.wpy ├── TextAreaEle.wpy └── operationGuide │ ├── OperationGuideModal.wpy │ └── operationGuide.js ├── lib-style ├── border.less ├── common.less ├── compatible.less └── values.less └── lib ├── AdaptiveToast.js ├── Cookie.js ├── EventHub.js ├── RewardedVideoPlayer.js ├── canvasKit.js ├── countdowner.js ├── debugKit.js ├── decorator ├── compatible.js ├── errSafe.js ├── mergingStep.js ├── noConcurrent.js └── typeCheck.js ├── decorators.js ├── globalEvents.js ├── handleStr.js ├── login ├── BaseLogin.js └── auth │ ├── BaseAuth.js │ └── WechatAuth.js ├── navigate ├── History.js └── Navigator.js ├── operationKit.js ├── request ├── Requester.js └── plugin │ ├── BasePlugin.js │ ├── CloudFuncPlugin.js │ ├── CookiePlugin.js │ ├── FailRecoverPlugin.js │ ├── FormPlugin.js │ ├── InstantPlugin.js │ └── LoginPlugin.js ├── routeParams.js ├── uniAppKit.js ├── wepyKit.js └── wxPromise.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | sourceMap: false, 3 | presets: [ 4 | 'env' 5 | ], 6 | plugins: [ 7 | 'transform-class-properties', 8 | 'transform-decorators-legacy', 9 | 'transform-object-rest-spread', 10 | 'transform-export-extensions', 11 | ] 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | */.DS_Store 3 | */*/.DS_Store 4 | */*/*/.DS_Store 5 | 6 | node_modules/ 7 | dist/ 8 | npm-debug.log 9 | selenium-debug.log 10 | test/unit/coverage 11 | test/e2e/reports 12 | ### Node template 13 | # Logs 14 | *.log 15 | npm-debug.log* 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (http://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules 42 | jspm_packages 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | ### JetBrains template 50 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 51 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 52 | 53 | # User-specific stuff: 54 | .idea/ 55 | 56 | ## File-based project format: 57 | *.iws 58 | 59 | ## Plugin-specific files: 60 | 61 | # IntelliJ 62 | /out/ 63 | 64 | # mpeltonen/sbt-idea plugin 65 | .idea_modules/ 66 | 67 | # JIRA plugin 68 | atlassian-ide-plugin.xml 69 | 70 | # Crashlytics plugin (for Android Studio and IntelliJ) 71 | com_crashlytics_export_strings.xml 72 | crashlytics.properties 73 | crashlytics-build.properties 74 | fabric.properties 75 | .wepycache 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fancy-mini 2 | 小程序代码库:小程序能力搭建/增强、疑难杂症参考处理、组件库、工具库。 3 | 4 | ## 功能 5 | ### 小程序能力搭建/增强 6 | - 健壮高效的登录机制 7 | - 交互能力:登录行为不用阻断用户操作流程、支持不同场景展示不同登录界面、支持不触发登录的同时悄悄进行个性化定制、支持用户访问要求登录态的业务时自动触发登录流程 8 | - 开发维护:登录态过期自动重新登录、并发调用自动合并、流程细节对业务方透明、支持多方复用、支持自定义逻辑扩展 9 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.1-login.html) 10 | 11 | - 无限层级的路由机制 12 | - 问题:小程序原生页面存在层级限制,最多只能同时打开10层页面,超过10层时便会无法打开新页面 13 | - 方案:自行维护完整历史记录,超出层级限制后在最后一层进行模拟导航 14 | - 效果:无限层级,即使超过10层依然可以正常打开正常返回 [体验](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.2-navigate.html#demo) 15 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.2-navigate.html) 16 | 17 | - 灵活易扩展的请求管理 18 | - 功能:发起数据请求 19 | - 特点:可以方便地在请求前后添加/移除各种扩展逻辑,如:设置默认表单类型、植入cookie逻辑、植入登录态检查逻辑、植入网络异常处理逻辑、植入云函数处理逻辑等 20 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.3-request.html) 21 | 22 | - cookie机制 23 | - 问题:很多时候,后端现有接口是先前对接M页/APP开发的,可能会使用cookie进行参数获取/传递;但小程序不支持cookie,导致后端接口复用/多端兼容成本增高。 24 | - 方案:利用前端存储,自行模拟&管理cookie;封装接口调用过程,植入cookie逻辑。 25 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.4-cookie.html) 26 | 27 | - canvas工具集 28 | - 功能:封装了一些常用的canvas操作,提高小程序canvas易用性,包括:图片居中裁剪、圆形头像、border-radius、多行文本、字符串过长截断/添加省略号等 29 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.5-canvasKit.html) 30 | 31 | - wx Promise化 32 | - 问题:目前小程序API均以回调形式提供,当逻辑较为复杂时会造成回调函数层层嵌套,影响代码可读性和逻辑清晰性,且不利于并发时序控制 33 | - 方案:将小程序API统一改造成Promise形式使用 34 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.6-wxPromise.html) 35 | 36 | - 跨页面传参 37 | - 场景1:后一页面向前一页面传参,如:发布页,点击选择分类->选择分类页,选定分类->自动返回发布页,此时发布页如何获取选择结果 38 | - 场景2:前一页面向后一页面传递大量数据,如:手机估价页,点击卖掉换钱->发布页,此时发布页如何获取用户在估价页填写的大量表单数据 39 | - 方案特点:无需借用后端接口,无需污染前端storage,纯内存操作性能较好;逻辑独立、通用,对页面代码无侵入 40 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.7-routeParams.html) 41 | 42 | - 跨页面事件 43 | - 功能:支持进行跨页面的事件通信 44 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.8-eventHub.html) 45 | 46 | - 入口构造工具 47 | - 用途1:支持PM&运营人员自助生成投放链接,避免频繁向FE沟通索要,节约沟通成本和打断成本 48 | - 用途2:支持FE&QA自助开发/测试没有线上入口的新页面,避免开发测试时引入作为临时入口的脏代码 49 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.9-customEntry.html) 50 | 51 | - 二维码测试工具 52 | - 用途1:支持扫码进入开发版/体验版小程序,便于QA在上线前充分测试二维码相关功能 53 | - 用途2:支持查看二维码编码参数,便于检查投放的二维码是否正确 54 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.a-qrTest.html) 55 | 56 | - 激励视频播放器 57 | - 功能:封装激励视频的加载、播放时序,使时序细节对外透明,便于调用;Promise化封装。 58 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.b-rewardedVideoPlayer.html) 59 | 60 | ### 小程序疑难杂症参考处理 61 | - toast截断问题 62 | - 问题:原生toast内容超过7个汉字时会被截断无法展示完整,自定义toast又无法覆盖textarea、video等层级最高的原生组件 63 | - 方案:根据内容长度自动选择合适的原生提示:带图标的原生toast、不带图标的原生toast、系统弹窗 64 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-3.1-adaptiveToast.html) 65 | 66 | - textarea遮盖浮层问题 67 | - 问题:textarea为原生组件层级最高,会遮盖价格填写蒙层、红包选择蒙层、绑定手机号提示框等各种普通浮层元素;特别是页面交互较复杂、浮层元素较多、出现时机较不确定时,难以有效规避。 68 | - 方案:封装一个自定义TextAreaEle组件,使其在处于编辑状态时使用原生textarea组件,处于非编辑状态时自动改用普通<view>元素展现内容 69 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-3.2-textArea.html) 70 | 71 | ### 小程序组件库 72 | - 通用弹窗 73 | - 功能:通用对话框,支持样式配置(单个/多个按钮、横版/竖版、带/不带关闭图标、带/不带顶部图标、自定义内联样式等)、按钮监听、按钮分享、按钮获取手机号、按钮异步交互结果统一返回等 74 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-4.1-dialogCommon.html) 75 | 76 | - 新手引导 77 | - 功能:新手引导、新功能操作引导 78 | - 特点: 79 | - 就地高亮:引导蒙层中高亮元素即为页面中实际元素 80 | - 就地交互:高亮区域可直接进行点击等交互 81 | - 依次引导:展示引导蒙层->响应用户点击->等待交互完毕->展示下一个引导蒙层->... 82 | - 公共逻辑抽离:公共逻辑统一封装,高亮元素只需进行少量配置,不必关注引导细节 83 | - [体验](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-4.2-operationGuide.html#demo) 84 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-4.2-operationGuide.html) 85 | 86 | ### 实用工具库 87 | - 免并发修饰器 88 | - 免并发 @noConcurrent 89 | 功能:在上一次操作结果返回之前,不响应重复操作 90 | 场景示例:用户连续多次点击同一个提交按钮,只响应一次,而不是同时提交多份表单 [体验](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.1-noConcurrent.html#demo) 91 | [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.1-noConcurrent.html) 92 | 93 | - 步骤并合 @mergingStep 94 | 功能:步骤并合,避免公共步骤重复执行 95 | 场景示例: 96 | 页面内同时发生如下三个请求: 登录-发送接口A、登录-发送接口B、登录-发送接口C 97 | 未使用本修饰器时,网络时序:登录,登录,登录 - 接口A,接口B,接口C, 登录请求将会被发送三次 98 | 使用本修饰器时,网络时序:登录 - 接口A,接口B,接口C,登录请求只会被发送一次 [体验](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.1-noConcurrent.html#demo) 99 | [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.1-noConcurrent.html) 100 | 101 | - 单通道执行 @singleAisle 102 | 功能: 使得并发调用逐个顺序执行 103 | 场景示例: 104 | 页面中多处同时调用弹窗函数 105 | 未使用本修饰器时,执行时序:弹窗1、弹窗2、弹窗3同时展现,用户同时看到多个弹窗堆在一起and/or弹窗相互覆盖 106 | 使用本修饰器时,执行时序:弹窗1展现、等待交互、用户关闭 => 弹窗2展现、等待交互、用户关闭 => 弹窗3展现、等待交互、用户关闭,弹窗函数依次顺序执行 [体验](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.1-noConcurrent.html#demo) 107 | [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.1-noConcurrent.html) 108 | 109 | - 多函数互斥 @makeMutex 110 | 功能: 多函数互斥免并发 111 | 场景示例: 跳转相关函数navigateTo、navigateToMiniProgram、reLaunch等相互之间免并发 112 | [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.1-noConcurrent.html) 113 | 114 | - 异常捕获修饰器 115 | - 异常捕获 @errSafe 116 | 功能:兼容函数异常 117 | 场景示例:页面获取数据后交由各子函数进行解析,子函数数据解析异常予以自动捕获,避免局部数据问题导致整个页面瘫痪 118 | [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.2-errSafe.html) 119 | 120 | - 异常提示 @withErrToast 121 | 功能:兼容异常,响应交互 122 | 场景示例: 页面操作响应过程,若出现异常自动予以捕获并toast提示,避免交互无响应 123 | [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.2-errSafe.html) 124 | 125 | - 快捷兼容修饰器 126 | - 兼容wx格式回调 @supportWXCallback 127 | 功能:使async/promise形式的函数自动支持success、fail、complete回调 128 | 场景示例:将回调形式的api改写为promise形式后,兼容旧代码 129 | [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.3-compatible.html) 130 | 131 | - 参数类型检查修饰器 132 | - 功能:检查方法参数类型 @typeCheck 133 | - 场景示例:调用函数时传递的参数会通 typeCheck 进行类型检查,避免不合规的参数致使代码报错 134 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.3.1-typCheck.html) 135 | 136 | - wepyKit 137 | - 功能:wepy1.x框架工具集,与wepy1.x框架耦合度较高的功能在此模块中提供,如:注册全局this属性、注册全局页面钩子等 138 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.4-wepyKit.html) 139 | 140 | - operationKit 141 | - 功能:各种杂七杂八的通用基础操作 142 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.5-operationKit.html) 143 | 144 | - handleStr 145 | - 功能:GBK字符串常用处理函数 146 | - 场景示例:计算GBK字符串长度、剪切GBK字符串 147 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.6-handleStr.html) 148 | 149 | - countdowner 150 | - 功能:倒计时模块,封装常用倒计时:暂停、重启等基本方法以及监听函数 151 | - 场景示例:页面需要倒计时展示模块时使用 152 | - [详情](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-5.7-countDowner.html) 153 | 154 | ## 演示 155 | - 小程序名称:fancyDemos 156 | - 访问: 157 | ![二维码](https://zhuanzhuanfe.github.io/fancy-mini/static/images/fancyQrCode.jpg) 158 | - 源码:[fancy-mini-demos](https://github.com/zhuanzhuanfe/fancy-mini-demos) (示例逐步补全中) 159 | 160 | 161 | ## 使用 162 | - [setup](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-0-getStarted.html) 163 | - [各功能使用说明](https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.1-login.html) 164 | - [各功能api查询](https://zhuanzhuanfe.github.io/fancy-mini/BaseLogin.html) 165 | 166 | ## 开发说明 167 | [README.md](./docs-src/develop/index.md) -------------------------------------------------------------------------------- /docs-src/develop/index.md: -------------------------------------------------------------------------------- 1 | ## 开发说明 2 | 3 | ### 目录结构 4 | 详见[目录结构](目录结构.md) 5 | 6 | ### 环境 7 | 1. node 大于v10.0.0 8 | 9 | ### 编译运行 10 | ```bash 11 | # 开发模式 12 | npm run dev # 编译并输出至2.x目录 13 | npm run dev -- -t 1.x # 编译并输出至1.x目录 14 | npm run dev -- -t 1.x,2.x # 编译并输出至1.x目录和2.x目录 15 | 16 | # 生产模式 17 | npm run build # 编译并输出至2.x目录 18 | npm run build -- -t 1.x # 编译并输出至1.x目录 19 | npm run build -- -t 1.x,2.x # 编译并输出至1.x目录和2.x目录 20 | ``` 21 | 22 | ### 生成文档 23 | ```bash 24 | npm run doc 25 | ``` 26 | 27 | ### 发布 28 | 1. 修改package.json中版本号 29 | ```js 30 | //package.json 31 | { 32 | //... 33 | "version-1.x": "1.0.17", //要发布1.x版本,修改此处 34 | "version-2.x": "2.0.0", //要发布2.x版本,修改此处 35 | //... 36 | } 37 | ``` 38 | 2. 发布 39 | ```bash 40 | # 发布 41 | npm run pub # 发布1.x版本和2.x版本 42 | npm run pub -- -t 1.x # 发布1.x版本 43 | npm run pub -- -t 2.x # 发布2.x版本 44 | ``` -------------------------------------------------------------------------------- /docs-src/develop/目录结构.md: -------------------------------------------------------------------------------- 1 | ## 目录结构 2 | 3 | - docs 文档输出目录(自动生成),jsdoc输出目录,文档网站根目录 4 | - docs-src 文档源目录(手动添加) 5 | - tutorials 使用相关文档,配合jsdoc使用,供代码库使用者阅读 6 | - 说明.txt 教程文档注意事项 7 | - static 图片等静态资源 8 | - develop 开发相关文档,供代码库开发者阅读 9 | - scripts 编译脚本 10 | - src 源码 11 | - lib js模块 12 | - lib-style css模块 13 | - components-wepy wepy框架组件 14 | - components-uniApp uni-app框架组件 15 | - dist 代码输出目录 16 | - 1.x 1.x版本发版目录 17 | - lib js模块,会编译成es5 18 | - lib-style css模块 19 | - components wepy框架组件,对应src/components-wepy 20 | - 2.x 2.x版本发版目录 21 | - lib js模块,保留es6形式 22 | - lib-style css模块 23 | - components uni-app框架组件,对应src/components-uniApp -------------------------------------------------------------------------------- /docs-src/static/images/fancyQrCode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/fancyQrCode.jpg -------------------------------------------------------------------------------- /docs-src/static/images/qrCode/demo-customEntry.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/qrCode/demo-customEntry.jpg -------------------------------------------------------------------------------- /docs-src/static/images/qrCode/demo-dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/qrCode/demo-dialog.jpg -------------------------------------------------------------------------------- /docs-src/static/images/qrCode/demo-navigate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/qrCode/demo-navigate.jpg -------------------------------------------------------------------------------- /docs-src/static/images/qrCode/demo-noConcurrent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/qrCode/demo-noConcurrent.jpg -------------------------------------------------------------------------------- /docs-src/static/images/qrCode/demo-operationGuide.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/qrCode/demo-operationGuide.jpg -------------------------------------------------------------------------------- /docs-src/static/images/qrCode/demo-qrTest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/qrCode/demo-qrTest.jpg -------------------------------------------------------------------------------- /docs-src/static/images/qrCode/demo-routeParams.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/qrCode/demo-routeParams.jpg -------------------------------------------------------------------------------- /docs-src/static/images/qrCode/demo-toast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/qrCode/demo-toast.jpg -------------------------------------------------------------------------------- /docs-src/static/images/qrCode/docs-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/qrCode/docs-index.png -------------------------------------------------------------------------------- /docs-src/static/images/toast/long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/toast/long.png -------------------------------------------------------------------------------- /docs-src/static/images/toast/mid1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/toast/mid1.png -------------------------------------------------------------------------------- /docs-src/static/images/toast/mid2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/toast/mid2.png -------------------------------------------------------------------------------- /docs-src/static/images/toast/short.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/toast/short.png -------------------------------------------------------------------------------- /docs-src/static/images/zzQrCode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/images/zzQrCode.jpg -------------------------------------------------------------------------------- /docs-src/static/ppt/fancy-mini_login.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/fancy-mini/6ef58f01e6d22c52f48b1afe54b635838284d01e/docs-src/static/ppt/fancy-mini_login.pptx -------------------------------------------------------------------------------- /docs-src/tutorials/0-getStarted.md: -------------------------------------------------------------------------------- 1 | ### 概述 2 | - 关于框架 3 | fancy-mini大部分功能与框架无关,支持各种小程序框架使用。 4 | 少部分功能与框架耦合,耦合部分集中抽离成了一个工具文件,以配置的方式使用;不同框架实现相应函数即可使用相应功能。 5 | 6 | - 关于代码包大小 7 | 为避免代码包膨胀,各模块没有合并导出,而是作为单独的文件独立引用。 8 | 这样,对于支持引用分析的框架,如uni-app、wepy等,只有项目中实际有引用的模块才会被打进小程序代码包中,无任何引用的部分不会被打进小程序代码包中;对于不支持引用分析的项目,如有必要也可以手动按需取用。 9 | 10 | - 关于版本 11 | - fancy-mini\@2.x (建议) 12 | - 直接导出es6源码,编译过程和项目统一,避免编译冲突/冗余 13 | - 包含丰富的jsdoc注释,便于IDE提示补全 14 | - 需要项目进行配置,使得fancy-mini目录参与编译过程,配置方式详见下文 15 | - fancy-mini\@1.x 16 | - 导出编译成es5的代码 17 | - 不需要参与项目编译 18 | 19 | ### 使用 - uni-app 框架 20 | - 安装 21 | ```bash 22 | npm install --save fancy-mini@2.x 23 | ``` 24 | - 使fancy-mini目录参与项目编译 25 | - 找到或创建 项目根目录/vue.config.js 26 | - 增加如下配置: 27 | ```js 28 | module.exports = { 29 | //默认情况下 babel-loader 会忽略所有 node_modules 中的文件。 30 | //如果你想要通过 Babel 显式转译一个依赖,可以在这个选项中列出来。 31 | transpileDependencies: ['fancy-mini'] 32 | } 33 | ``` 34 | - 相关文档 35 | - [uni-app自定义编译配置](https://uniapp.dcloud.io/collocation/vue-config?id=collocation) 36 | - [vue配置说明](https://cli.vuejs.org/zh/config/#transpiledependencies) 37 | - 使用js模块 38 | 直接根据路径引入即可使用,e.g.: 39 | 40 | ```js 41 | import {wxPromise} from 'fancy-mini/lib/wxPromise'; //引入需要的模块 42 | 43 | //使用该模块 44 | wxPromise.getSystemInfo().then(sysInfo=>{ 45 | console.log('sysInfo:', sysInfo); 46 | }); 47 | ``` 48 | [wepyKit模块](./module-wepyKit.html)除外,如果功能模块使用说明中用到了这个模块相关函数作为配置项,需替换成[uniAppKit模块](./module-uniAppKit.html)中对应函数或自行实现相应函数。 49 | 50 | - 使用ui组件 51 | 1. 安装相关依赖 52 | ```bash 53 | npm install --save-dev vue-property-decorator # 支持以class的形式书写vue语法 54 | npm install --save-dev less less-loader # 支持以less的形式书写css语法 55 | ``` 56 | 2. 根据路径引入并使用组件 57 | ```html 58 | 73 | 78 | ``` 79 | 3. 附加说明 80 | 文档中的组件有些尚未改写成uni-app版,如果文档中提到了某组件,而[源码-uniApp组件](https://github.com/zhuanzhuanfe/fancy-mini/tree/master/src/components-uniApp)中无对应文件, 81 | 请等待迁移完成后,更新fancy-mini版本,再行使用;或从[源码-wepy组件](https://github.com/zhuanzhuanfe/fancy-mini/tree/master/src/components-wepy)中下载源码自行改造成uni-app组件。 82 | 83 | ### 使用 - wepy 1.x 框架 84 | - 安装 85 | ```bash 86 | npm install --save fancy-mini@1.x 87 | ``` 88 | - 使用js模块 89 | 直接根据路径引入即可使用,e.g.: 90 | 91 | ```js 92 | import {wxPromise} from 'fancy-mini/lib/wxPromise'; //引入需要的模块 93 | 94 | //使用该模块 95 | wxPromise.getSystemInfo().then(sysInfo=>{ 96 | console.log('sysInfo:', sysInfo); 97 | }); 98 | ``` 99 | [uniAppKit模块](./module-uniAppKit.html) 除外,如果功能模块使用说明中用到了这个模块相关函数作为配置项,需替换成[wepyKit模块](./module-wepyKit.html)中对应函数或自行实现相应函数。 100 | 101 | - 使用ui组件 102 | - wepy 1.7.0以上 && Mac环境 103 | 直接根据路径引入即可,e.g.: 104 | ```html 105 | 120 | 125 | ``` 126 | - wepy 1.7.0以下 || Windows环境 127 | 暂不支持直接引用,请复制粘贴到项目目录中使用 128 | 原因: 129 | issue:[https://github.com/Tencent/wepy/issues/1035](https://github.com/Tencent/wepy/issues/1035) 130 | issue:[https://github.com/Tencent/wepy/issues/851](https://github.com/Tencent/wepy/issues/851) 131 | 132 | - 附加说明 133 | 后续新增组件不再提供wepy版,如果文档中提到了某组件,但[源码-wepy组件](https://github.com/zhuanzhuanfe/fancy-mini/tree/master/src/components-wepy)中无对应文件, 134 | 请从[源码-uniApp组件](https://github.com/zhuanzhuanfe/fancy-mini/tree/master/src/components-uniApp)中下载源码自行改写成wepy组件。 135 | 136 | - demo项目 137 | [fancy-mini-demos](https://github.com/zhuanzhuanfe/fancy-mini-demos) 138 | 139 | ### 使用 - 其它框架 140 | - 安装 141 | ```bash 142 | # 根据需要选择合适的安装版本 143 | npm install --save fancy-mini@2.x #直接使用es6源码(建议,注释齐全便于IDE提示补全,编译过程和项目统一避免冲突/冗余) 144 | # npm install --save fancy-mini@1.x #使用编译成es5的代码 145 | ``` 146 | - [fancy-mini\@2.x]使fancy-mini参与项目编译 147 | - 类vue框架 148 | - 找到vue.config.js或其等价文件 149 | - 加入如下配置: 150 | 151 | ```js 152 | module.exports = { 153 | //默认情况下 babel-loader 会忽略所有 node_modules 中的文件。 154 | //如果你想要通过 Babel 显式转译一个依赖,可以在这个选项中列出来。 155 | transpileDependencies: ['fancy-mini'] 156 | } 157 | ``` 158 | - 详见[vue配置说明](https://cli.vuejs.org/zh/config/#transpiledependencies) 159 | - 其它框架 160 | - 找到配置不编译node_modules目录的地方 161 | - 修改该配置,将fancy-mini排除在外 162 | 163 | ```js 164 | //e.g. 原配置 165 | { 166 | exclude: /node_modules/, 167 | loader: 'babel', 168 | } 169 | // 修改为 170 | { 171 | exclude: /node_modules\/(?!(fancy-mini)\/).*/, 172 | loader: 'babel', 173 | } 174 | ``` 175 | - 使用js模块 176 | 直接根据路径引入即可,e.g.: 177 | ```js 178 | import {wxPromise} from 'fancy-mini/lib/wxPromise'; //引入需要的模块 179 | 180 | //使用该模块 181 | wxPromise.getSystemInfo().then(sysInfo=>{ 182 | console.log('sysInfo:', sysInfo); 183 | }); 184 | ``` 185 | [uniAppKit模块](./module-uniAppKit.html)、[wepyKit模块](./module-wepyKit.html) 除外,这两个模块与对应框架耦合,如果功能模块使用说明中用到了这两个模块相关函数作为配置项,需自行实现相应函数并予以替换。 186 | 187 | 188 | - 使用ui组件 189 | 本代码库组件部分暂未直接支持其它框架,如有需要,请下载源码自行改造。 190 | -------------------------------------------------------------------------------- /docs-src/tutorials/1-demoProject.md: -------------------------------------------------------------------------------- 1 | - 小程序名称:fancyDemos 2 | - 访问: 3 | ![](./static/images/fancyQrCode.jpg) 4 | - 源码:[fancy-mini-demos](https://github.com/zhuanzhuanfe/fancy-mini-demos) (逐步补全中) -------------------------------------------------------------------------------- /docs-src/tutorials/2.2-navigate.md: -------------------------------------------------------------------------------- 1 | ### 背景 2 | - 小程序原生页面存在层级限制,最多只能同时打开5层,超过5层时便会无法打开新页面(注:后来原生层级限制放宽至了10层,代码中已作更新,文档仍以5层为例) 3 | 4 | - 业务流程很容易一不小心就超过5层,如:首页-列表页-商品详情-下单页-订单详情页-私信页-... 5 | - 访问回路很容易导致超过5层,如:首页-列表页-商品详情-查看更多-商品详情-查看更多-商品详情-... 6 | - 为避免层级限制导致的无法打开问题和层级限制带来的交互路径限制,提出此路由方案 7 | 8 | ### 策略 9 | - 修改小程序默认导航行为,自行维护完整历史记录 10 | - 页面层级小于等于5时,导航行为与原生导航行为一致 11 | - 请求打开第6层及以上时,逻辑层级记录完整历史,实际层级每次都是直接将第5层替换为目标页面 12 | - 返回时,逻辑层级相应回退;若回退后逻辑层级大于等于5,则实际层级将第5层替换为回退后目标页面,否则实际层级回退到相应页面 13 | - demo: 14 | 15 | ```txt 16 | 逻辑层级 1 - 2 - 3 - 4 - 5 17 | 实际层级 1 - 2 - 3 - 4 - 5 18 | 19 | 打开 20 | 21 | 逻辑层级 1 - 2 - 3 - 4 - 5 - 6 22 | 实际层级 1 - 2 - 3 - 4 - 6 23 | 24 | 打开,打开,打开 25 | 26 | 逻辑层级 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 27 | 实际层级 1 - 2 - 3 - 4 - 9 28 | 29 | 返回 30 | 31 | 逻辑层级 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 32 | 实际层级 1 - 2 - 3 - 4 - 8 33 | 34 | 返回,返回,返回 35 | 36 | 逻辑层级 1 - 2 - 3 - 4 - 5 37 | 实际层级 1 - 2 - 3 - 4 - 5 38 | 39 | 返回 40 | 41 | 逻辑层级 1 - 2 - 3 - 4 42 | 实际层级 1 - 2 - 3 - 4 43 | ``` 44 | 45 | 46 | 47 | ### 补充策略:空白中转 48 | - 从第6层及以上,如第9层返回时,理论上应直接将实际第5层直接替换为逻辑第8层页面,但由于系统返回行为无法取消,所以实际过程为:返回实际第4层-新开逻辑第8层 49 | - 这一中转过程会使返回时实际第4层一闪而过,影响用户体验 50 | - 为此,引入空白页中转:打开实际第5层时,将实际第4层替换为空白页;直到返回逻辑第4层时才将空白页替换回原始页面。 51 | - demo: 52 | 53 | ```txt 54 | 逻辑层级 1 - 2 - 3 - 4 55 | 实际层级 1 - 2 - 3 - 4 56 | 57 | 打开 58 | 59 | 逻辑层级 1 - 2 - 3 - 4 - 5 60 | 实际层级 1 - 2 - 3 - 空白页 - 5 61 | 62 | 打开,打开,打开 63 | 64 | 逻辑层级 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 65 | 实际层级 1 - 2 - 3 - 空白页 - 9 66 | 67 | 返回 68 | 69 | 逻辑层级 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 70 | 实际层级 1 - 2 - 3 - 空白页 - 8 71 | 72 | 返回,返回,返回 73 | 74 | 逻辑层级 1 - 2 - 3 - 4 - 5 75 | 实际层级 1 - 2 - 3 - 空白页 - 5 76 | 77 | 返回 78 | 79 | 逻辑层级 1 - 2 - 3 - 4 80 | 实际层级 1 - 2 - 3 - 4 81 | ``` 82 | 83 | ### 附加功能:实例覆盖自动恢复 84 | - 问题:wepy框架存在单实例问题,同一路径页面被打开两次时,其数据会相互影响,如:详情页A - 详情页B - 返回A,点击查看大图 - B的图片(而不是A的图片) 85 | 详见issue:[两级页面为同一路由时,后者数据覆盖前者](https://github.com/Tencent/wepy/issues/322) 86 | - 策略:返回时,若判断目标页面数据已被覆盖,则自动予以恢复 87 | - 引入:可配,参见“安装”小节 88 | 89 | ### 附加功能: 免并发 90 | - 问题:用户连续快速点击多个/多次按钮时,会一次性打开多个窗口,一则造成层级膨胀,二则影响浏览体验 91 | - 策略:第一次点击造成的跳转完成之前无视后续点击产生的跳转请求 92 | - 引入:默认支持 93 | 94 | ### 附加功能:数据预先加载 95 | - 问题:小程序的page1跳转到page2,到page2的onLoad是存在一个300ms ~ 400ms的延时的,在page2的onLoad中开始获取数据会浪费这个延时 96 | - 策略:在 page1 中预先拿取数据,然后在 page2 中直接使用数据;wepy框架对此有良好的实现,参见[WePY 在小程序性能调优上做出的探究](https://segmentfault.com/a/1190000008975448?winzoom=1) 97 | - 引入:可配,参见“安装”小节 98 | 99 | ### 实现细节 100 | 详见[无限层级路由方案](https://github.com/zhuanzhuanfe/articles/blob/master/wupenghe/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E6%97%A0%E9%99%90%E5%B1%82%E7%BA%A7%E8%B7%AF%E7%94%B1%E6%96%B9%E6%A1%88.md) 101 | 102 | 103 | 104 | ### 体验 105 | 106 | 107 | 扫描以上二维码,或微信-发现-小程序-搜索: 108 | - fancyDemos(直观功能演示,源码:[fancy-mini-demos](https://github.com/zhuanzhuanfe/fancy-mini-demos),二维码见上) 109 | - 转转二手交易网(复杂场景实际应用案例,在微信中有固定入口:微信-我-支付-转转二手) 110 | - 转转欢乐送 111 | - 天天步数换 112 | - …… 113 | 114 | ### 使用 - setup 115 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 116 | 1. 新建空白页面 pages/curtain/curtain (供[空白中转策略](#blankRedirect)使用) 117 | 2. 小程序入口文件中: 118 | ```js 119 | //项目入口文件(app.js/app.wpy/app.vue/main.js/……,因框架而异) 120 | import './lib/appPlugin'; //负责各种小程序级公共模块的引入和配置 121 | ``` 122 | 3. appPlugin.js 123 | ```js 124 | //appPlugin.js,负责各种小程序级公共模块的引入和配置 125 | import Navigator from 'fancy-mini/lib/navigate/Navigator'; 126 | import {customWxPromisify} from 'fancy-mini/lib/wxPromise'; 127 | import {registerToThis, registerPageHook, pageRestoreHandler, NavRefine} from 'fancy-mini/lib/wepyKit'; 128 | 129 | //无限层级路由方案 130 | Navigator.config({ 131 | enableCurtain: true, //是否开启空白中转策略 132 | curtainPage: '/pages/curtain/curtain', //空白中转页 133 | 134 | enableTaintedRestore: true, //是否开启实例覆盖自动恢复策略 135 | 136 | /** 137 | * 自定义页面数据恢复函数,用于 138 | * 1. wepy实例覆盖问题,存在两级同路由页面时,前者数据会被后者覆盖,返回时需予以恢复 139 | * 2. 层级过深时,新开页面会替换前一页面,导致前一页面数据丢失,返回时需予以恢复 140 | */ 141 | pageRestoreHandler: pageRestoreHandler, 142 | 143 | MAX_LEVEL: 10, //最多同时打开的页面层数 144 | 145 | //oriNavOverrides: NavRefine, //自定义覆盖部分/全部底层跳转api,如:此处底层可改用wepy定义的路由模块,便于使用wepy提供的prefetch等功能 146 | }); 147 | registerPageHook('onUnload', Navigator.onPageUnload); 148 | 149 | //wx接口Promise化 150 | let {wxPromise, wxResolve} = (function () { 151 | let overrides = { //路由相关api改为由Navigator接管 152 | navigateTo: Navigator.navigateTo, 153 | redirectTo: Navigator.redirectTo, 154 | navigateBack: Navigator.navigateBack, 155 | reLaunch: Navigator.reLaunch, 156 | switchTab: Navigator.switchTab, 157 | }; 158 | 159 | return { 160 | wxPromise: customWxPromisify({overrides, dealFail: false}), 161 | wxResolve: customWxPromisify({overrides, dealFail: true}), 162 | } 163 | }()); 164 | registerToThis("$wxPromise", wxPromise); //将改造后的wx模块注册到组件/页面this对象上,方便组件/页面文件使用 165 | registerToThis("$wxResolve", wxResolve); 166 | 167 | export { //导出改造后的wx模块,供独立js文件使用 168 | wxPromise, 169 | wxResolve, 170 | } 171 | ``` 172 | 173 | ### 使用 - 维护 174 | - 所有页面不直接调用wx.navigateTo、wx.redirectTo、wx.navigateBack、wx.switchTab、wx.reLaunch,统一改用this.$wxPromise.navigateTo等对应接口 175 | ```js 176 | wx.navigateTo({ url: '/pages/index/index'});//错误,无法确保路由过程全部由自定义模块接管 177 | this.$wxPromise.navigateTo({ url: '/pages/index/index'}); //正确 178 | ``` 179 | 180 | - 所有页面跳转由this.$wxPromise相应接口触发,而不使用<navigator>元素 181 | - [wepy框架] 所有页面若有自定义onUnload函数,需在函数中执行父类相应钩子: super.onUnload && super.onUnload() (wepyKit中监听页面钩子功能registerPageHook目前需要页面这样手动配合,uniAppKit无此约束) 182 | ```js 183 | //wepy框架额外约束 184 | export default class extends wepy.page { 185 | //正确,页面中无onUnload函数,会直接执行父类上的统一钩子 186 | } 187 | 188 | export default class extends wepy.page { 189 | onUnload(){//错误,页面中自定义onUnload函数覆盖了统一钩子,影响路由模块监听返回行为 190 | 191 | } 192 | } 193 | 194 | export default class extends wepy.page { 195 | onUnload(){//正确,页面自定义onUnload函数中调用了统一钩子 196 | super.onUnload && super.onUnload() 197 | } 198 | } 199 | ``` 200 | 201 | 202 | ### 注意事项 203 | - 要求所有页面遵循上一小节中的代码规范 204 | - 确保路由过程完全由自定义模块接管 205 | - 少量未遵循会影响返回逻辑正确性,但一般不会造成页面功能问题 206 | - 可使用eslint插件强制规范: [eslint-plugin-fancy-mini](https://www.npmjs.com/package/eslint-plugin-fancy-mini) 207 | 208 | ### api查询 209 | - [路由模块 Navigator](./Navigator.html) 210 | - [历史栈 History](./History.html) 211 | -------------------------------------------------------------------------------- /docs-src/tutorials/2.3-request.md: -------------------------------------------------------------------------------- 1 | ### 功能 2 | - 发起网络请求 3 | - 基础功能、参数、用法同小程序原生网络api 4 | - Promise化 5 | - 调用结果改以Promise形式返回,便于时序管理 6 | - 兼容success、fail、complete回调 7 | - 灵活组装 8 | - 可以方便地在请求前后添加/移除各种扩展逻辑 9 | - 设置默认表单类型 10 | - 植入登录态相关逻辑 11 | - 植入cookie相关逻辑 12 | - 网络异常监听&自动恢复 13 | - 云函数http化 14 | - …… 15 | 16 | ### 使用 17 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 18 | 1. 引入&配置 19 | ```js 20 | //项目入口文件(app.js/app.wpy/app.vue/main.js/……,因框架而异) 21 | import './lib/appPlugin'; //负责各种小程序级公共模块的引入和配置 22 | ``` 23 | ```js 24 | //appPlugin.js, 负责各种小程序级公共模块的引入和配置 25 | import Requester from "fancy-mini/lib/request/Requester"; 26 | import {registerToThis} from 'fancy-mini/lib/wepyKit'; 27 | 28 | //实例创建 29 | const requester = new Requester(); //请求管理器 30 | 31 | //请求管理器配置 32 | requester.config({ 33 | //使用的底层网络api 34 | underlayRequest: wx.request, 35 | 36 | //以插件的形式添加/移除各种扩展逻辑 37 | plugins: [ 38 | //各种插件,详见下文 39 | ] 40 | }); 41 | 42 | //将请求模块相关功能注册到this上,方便页面/组件直接使用 43 | const propMapThis2Requester = { //命名映射,key为this属性名,value为requester属性名, '*this'表示requester自身 44 | '$requester': '*this', // this.$requester 对应 requester 45 | '$http': 'request', // this.$http() 对应 requester.request() 46 | //... 47 | }; 48 | 49 | for (let [thisProp, requesterProp] of Object.entries(propMapThis2Requester)) { 50 | let requesterTarget = requesterProp === '*this' ? requester : requester.makeAssignableMethod(requesterProp); 51 | registerToThis(thisProp, requesterTarget); 52 | } 53 | 54 | //导出部分模块,便于独立js文件使用 55 | export { 56 | requester, 57 | } 58 | 59 | ``` 60 | 61 | 2. 小程序各处使用 62 | ```js 63 | //独立js文件,手动引入appPlugin模块并使用相关功能 64 | import {requester} from '../../lib/appPlugin'; 65 | 66 | async test(){ 67 | let dataRes = await requester.request({ 68 | url: 'https://xxx', 69 | data: { 70 | } 71 | }); 72 | 73 | console.log('dataRes:', dataRes); //请求结果,即为后端接口返回内容 e.g.后端返回"{respCode:0}",则dataRes={respCode: 0} 74 | } 75 | ``` 76 | 77 | ```js 78 | //页面/组件,可以直接通过this使用相关功能 79 | async test(){ 80 | let dataRes = await this.$http({ 81 | url: 'https://xxx', 82 | data: { 83 | 84 | } 85 | }); 86 | 87 | console.log('dataRes:', dataRes); //请求结果,即为后端接口返回内容 e.g.后端返回"{respCode:0}",则dataRes={respCode: 0} 88 | } 89 | ``` 90 | 91 | ### 扩展逻辑-概述 92 | - 说明 93 | - 各种扩展逻辑以插件的形式在requester上配置,可以根据需要添加/移除 94 | - 插件对象应为{@link BasePlugin}子类实例 95 | - 注意插件顺序,配置顺序即为执行顺序 96 | - 配置示例 97 | ```js 98 | import FormPlugin from 'fancy-mini/lib/request/plugin/FormPlugin'; 99 | import LoginPlugin from 'fancy-mini/lib/request/plugin/LoginPlugin'; 100 | import CookiePlugin from 'fancy-mini/lib/request/plugin/CookiePlugin'; 101 | import FailRecoverPlugin from 'fancy-mini/lib/request/plugin/FailRecoverPlugin'; 102 | import CloudFuncPlugin from 'fancy-mini/lib/request/plugin/CloudFuncPlugin'; 103 | 104 | requester.config({ 105 | //... 106 | 107 | //以插件的形式添加/移除各种扩展逻辑 108 | plugins: [ 109 | //表单插件,修改表单默认处理方式 110 | new FormPlugin({ 111 | defaultContentType: 'application/x-www-form-urlencoded' //指定默认表单类型 112 | }), 113 | //登录插件,自动检查、获取、更新登录态 114 | new LoginPlugin({ 115 | loginCenter, 116 | apiAuthFailChecker 117 | }), 118 | //cookie插件,自动读取&写入cookie 119 | new CookiePlugin({ 120 | cookie 121 | }), 122 | //网络异常处理插件,监听&处理网络异常 123 | new FailRecoverPlugin({ 124 | requestFailRecoverer 125 | }), 126 | //云函数插件,以http接口的形式使用云函数 127 | new CloudFuncPlugin({ 128 | fakeDomain: 'cloud.function' 129 | fakeRootPath: '/' 130 | }), 131 | ] 132 | }) 133 | ``` 134 | ### 扩展逻辑-表单插件 135 | - 功能 136 | 修改表单默认处理行为 137 | 1. 将请求头部中的content-type默认值改为构造函数中指定的defaultContentType 138 | 139 | - 使用 140 | ```js 141 | import FormPlugin from 'fancy-mini/lib/request/plugin/FormPlugin'; 142 | 143 | requester.config({ 144 | //... 145 | 146 | //以插件的形式添加/移除各种扩展逻辑 147 | plugins: [ 148 | //表单插件,修改表单默认处理方式 149 | new FormPlugin({ 150 | defaultContentType: 'application/x-www-form-urlencoded' //指定默认表单类型 151 | }) 152 | ] 153 | }) 154 | ``` 155 | 156 | ### 扩展逻辑-登录插件 157 | - 功能 158 | 在请求前后植入登录态检查和处理逻辑 159 | 1. 在requester上注册一个`requestWithLogin`方法,用于调用需要登录态的接口 160 | 2. 请求发出前,若未登录,则先触发登录,然后再发送接口请求 161 | 3. 请求返回后,若判断后端登录态已失效,则自动重新登录重新发送接口请求,并以重新请求的结果作为本次调用结果返回 162 | - 使用 163 | 参见{@tutorial 2.1-login} 164 | 165 | ### 扩展逻辑-cookie插件 166 | - 功能 167 | 在请求前后植入cookie相关逻辑 168 | 1. 请求发出前,在请求头部中注入当前环境cookie信息 169 | 2. 请求返回后,接收返回结果头部中的cookie信息,并写入当前环境cookie中 170 | - 使用 171 | 参见{@tutorial 2.4-cookie} 172 | 173 | ### 扩展逻辑-网络异常处理插件 174 | - 功能 175 | 监听网络异常情况,并进行恢复处理 176 | - 使用 177 | ```js 178 | import FailRecoverPlugin from 'fancy-mini/lib/request/plugin/FailRecoverPlugin'; 179 | 180 | requester.config({ 181 | //... 182 | 183 | //以插件的形式添加/移除各种扩展逻辑 184 | plugins: [ 185 | //网络异常处理插件,监听&处理网络异常 186 | new FailRecoverPlugin({ 187 | requestFailRecoverer({res, options, resolve, reject}){ 188 | //展示网络异常界面,提示用户“点击屏幕任意位置重试” 189 | //点击重试 190 | //重试成功,resolve(重试后的请求结果) 191 | //发生异常,reject(异常详情) 192 | } 193 | }), 194 | ] 195 | }) 196 | ``` 197 | 198 | ### 扩展逻辑-云函数插件 199 | - 功能 200 | 将云函数封装成http接口形式使用,便于: 201 | 1. 使用requester提供的各种逻辑扩展能力 202 | 2. 后续在云函数和后端服务器之间进行各种业务的相互迁移 203 | - 使用 204 | 1. 配置 205 | ```js 206 | //appPlugin.js 207 | import CloudFuncPlugin from 'fancy-mini/lib/request/plugin/CloudFuncPlugin'; 208 | 209 | requester.config({ 210 | //... 211 | 212 | //以插件的形式添加/移除各种扩展逻辑 213 | plugins: [ 214 | //云函数插件,以http接口的形式使用云函数 215 | new CloudFuncPlugin({ 216 | fakeDomain: 'fancy.com', //虚拟域名 217 | fakeRootPath: '/demos/cloud/' //虚拟路径 218 | }), 219 | ] 220 | }) 221 | ``` 222 | 2. 调用 223 | ```js 224 | //则调用指定虚拟域名虚拟路径下的接口 225 | let res = await requester.request({ 226 | url: 'https://fancy.com/demos/cloud/xxx?a=1&b=2' 227 | }); 228 | //等价于调用对应云函数 229 | let res = await wx.cloud.callFunction({ 230 | name: 'xxx', 231 | data: { 232 | a: "1", 233 | b: "2", 234 | } 235 | }) 236 | ``` 237 | 3. 云函数约定 238 | ```js 239 | exports.main = async (event, context) => { 240 | //云函数格式约定: 241 | 242 | let {a, b} = event; //调用方传入的参数可以通过event获取 243 | a = Number(a); //参数类型统一为string 244 | b = Number(b); 245 | 246 | //此外,还会额外拼入一些http相关参数 247 | let {reqHeader} = event; //http请求header信息 248 | console.log(reqHeader.cookie); //header中的cookie字段会解析成对象格式,形如:{uid: 'xxx'} 249 | 250 | //处理结果正常返回 251 | let result = { sum: a+b }; 252 | 253 | //此外,有一些保留字段可以用于设置http相关参数 254 | result.resStatusCode = 200; //设置http状态码 255 | result.resHeader = {}; //设置http返回结果中的header信息 256 | result.resHeader['Set-Cookie'] = [ //同一header有多条记录时,以数组形式设置 257 | 'uid=xxx;expires=111', 258 | 'sessionKey=yyy;expires=222' 259 | ] 260 | 261 | return result; 262 | } 263 | ``` 264 | 265 | ### 自定义扩展逻辑 266 | - 功能 267 | 在请求前后添加各种自定义逻辑。 268 | 269 | - 使用 270 | 1. 编写插件 271 | ```js 272 | import BasePlugin from 'fancy-mini/lib/request/plugin/BasePlugin'; 273 | 274 | //插件需继承插件基类BasePlugin 275 | class DIYPlugin extends BasePlugin { 276 | //各种自定义逻辑实现 277 | //可重写节点参见{@link BasePlugin}文档,实现示例可参考 api查询 小节中各插件源码 278 | } 279 | 280 | export default DIYPlugin; 281 | ``` 282 | 2. 使用插件 283 | ```js 284 | //appPlugin.js 285 | import DIYPlugin from './request/plugin/DIYPlugin'; 286 | 287 | requester.config({ 288 | //... 289 | 290 | //以插件的形式添加/移除各种扩展逻辑 291 | plugins: [ 292 | //自定义插件 293 | new DIYPlugin({ 294 | //自定义配置... 295 | }), 296 | ] 297 | }) 298 | ``` 299 | 300 | ### api查询 301 | - [请求管理器 Requester](./Requester.html) 302 | - [插件基类 BasePlugin](./BasePlugin.html) 303 | - [表单插件 FormPlugin](./FormPlugin.html) 304 | - [登录插件 LoginPlugin](./LoginPlugin.html) 305 | - [cookie插件 CookiePlugin](./CookiePlugin.html) 306 | - [网络异常处理插件 FailRecoverPlugin](./FailRecoverPlugin.html) 307 | - [云函数插件 CloudFuncPlugin](./CloudFuncPlugin.html) 308 | - [快捷插件 InstantPlugin](./InstantPlugin.html) -------------------------------------------------------------------------------- /docs-src/tutorials/2.4-cookie.md: -------------------------------------------------------------------------------- 1 | ### 功能 2 | - 问题 3 | - 小程序不支持cookie 4 | - 后端现有接口很多是先前对接M页/APP开发的,可能会使用cookie进行参数获取/传递,小程序无法直接复用 5 | - 后端新接口很多时候需要同时供M页和小程序两端使用,一端能用cookie一端不能用cookie,需要进行各种兼容处理 6 | 7 | - 方案 8 | - 抹平小程序和M页在cookie方面的差异,使得小程序调用后端接口时可以像M页一样使用cookie传参 9 | - 利用前端storage,自行模拟&管理cookie 10 | - 封装接口调用过程,自动植入cookie逻辑 11 | 12 | - 效果 13 | - 前端,可以像M页一样,通过cookie管理跨页面公共基础数据 14 | - 后端,可以正常使用cookie传参,不需要为小程序单独适配 15 | 16 | ### 使用 17 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 18 | 1. 引入&配置 19 | ```js 20 | //项目入口文件(app.js/app.wpy/app.vue/main.js/……,因框架而异) 21 | import './lib/appPlugin'; //负责各种小程序级公共模块的引入和配置 22 | ``` 23 | ```js 24 | //appPlugin.js, 负责各种小程序级公共模块的引入和配置 25 | import Cookie from 'fancy-mini/lib/Cookie'; 26 | import CookiePlugin from 'fancy-mini/lib/request/plugin/CookiePlugin'; 27 | import Requester from "fancy-mini/lib/request/Requester"; 28 | 29 | //实例创建 30 | const requester = new Requester(); //请求管理器 31 | const cookie = new Cookie(); //cookie管理器 32 | 33 | //请求管理器配置 34 | requester.config({ 35 | plugins: [ 36 | //cookie插件,在请求前后自动加入cookie相关逻辑 37 | new CookiePlugin({ 38 | cookie, 39 | }), 40 | ] 41 | }); 42 | 43 | export { 44 | requester, 45 | cookie, 46 | } 47 | ``` 48 | 49 | 2. 前端读写cookie 50 | ```js 51 | //页面/组件 52 | import {cookie} from '../../lib/appPlugin'; 53 | 54 | //写入cookie 55 | cookie.set('lon', '120'); 56 | 57 | //读取cookie 58 | cookie.get('lon'); //'120' 59 | 60 | //更多用法详见 api查询 小节 61 | ``` 62 | 3. 前端发送请求 63 | ```js 64 | //页面/组件 65 | import {requester} from '../../lib/appPlugin'; 66 | 67 | //前端发送接口请求时,要使用步骤1中封装了cookie逻辑的requester触发 68 | let dataRes = await requester.request({ 69 | url: 'https://xxx', 70 | data: { 71 | } 72 | }); 73 | 74 | console.log('dataRes:', dataRes); 75 | ``` 76 | 4. 后端读写cookie 77 | ```js 78 | //后端使用标准http协议读写cookie,处理方式跟对接M页时保持一致 79 | //读取cookie:请求内容-头部-读取'cookie'字段 80 | //写入cookie:返回结果-头部-设置'Set-Cookie'字段 81 | ``` 82 | 83 | ### 相关 84 | - {@tutorial 2.3-request} 85 | 86 | ### api查询 87 | - [cookie管理器 Cookie](./Cookie.html) 88 | - [请求管理-cookie插件 CookiePlugin](./CookiePlugin.html) 89 | - [请求管理器 Requester](./Requester.html) -------------------------------------------------------------------------------- /docs-src/tutorials/2.5-canvasKit.md: -------------------------------------------------------------------------------- 1 | ### 功能 2 | 封装了一些常用的canvas操作,提高小程序canvas易用性,包括:图片居中裁剪、圆形头像、border-radius、多行文本、字符串过长截断/添加省略号等。 3 | 4 | ### 使用 5 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 6 | 1. 引入&使用 7 | ```js 8 | //引入模块 9 | import canvasKit from 'fancy-mini/lib/canvasKit'; 10 | 11 | //使用模块 12 | canvasKit.fillText(ctx, {text, x, y, fontSize, color, lineHeight, textAlign}); //绘制文本,支持\n换行 13 | 14 | //模块支持的方法列表及参数说明,详见 api查询 小节 15 | ``` 16 | 17 | ### api查询 18 | - [canvasKit](./module-canvasKit.html) -------------------------------------------------------------------------------- /docs-src/tutorials/2.6-wxPromise.md: -------------------------------------------------------------------------------- 1 | ## 背景 2 | 目前小程序API均以回调形式提供,当逻辑较为复杂时会造成回调函数层层嵌套,影响代码可读性和思维清晰性,因而将其转为Promise形式使用。 3 | 4 | ## 基础用法 5 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 6 | 1. 直接引入使用 7 | ```js 8 | import {wxPromise, wxResolve} from 'fancy-mini/lib/wxPromise'; 9 | 10 | function func0() { 11 | wxPromise.getSystemInfo().then(res=>{ 12 | console.log('sysInfo:',res); 13 | }) 14 | } 15 | 16 | async function func1(){ 17 | let sysInfoRes = await wxPromise.getSystemInfo(); //调用wx.getSystemInfo,并在success回调中resolve, fail回调中reject 18 | console.log('sysInfo:', sysInfoRes); 19 | } 20 | 21 | async function func2(){ 22 | let sysInfoRes = await wxResolve.getSystemInfo(); //调用wx.getSystemInfo,并在success、fail回调中resolve,并在res中添加succeeded字段标记成功/失败 23 | if (!sysInfoRes.succeeded) //处理失败情形 24 | console.log('get system info failed'); 25 | console.log('sysInfo:', sysInfoRes); 26 | } 27 | ``` 28 | 29 | ## 自定义二次封装 30 | ```js 31 | import {customWxPromisify} from 'fancy-mini/lib/wxPromise'; 32 | import Navigator from 'fancy-mini/lib/navigate/Navigator'; 33 | 34 | let overrides = { //覆盖部分API,引入自定义逻辑,以优化性能/实现特定需求 35 | navigateTo: Navigator.navigateTo, 36 | redirectTo: Navigator.redirectTo, 37 | navigateBack: Navigator.navigateBack, 38 | }; 39 | 40 | let wxPromise = customWxPromisify({ 41 | overrides, 42 | dealFail: false, //不处理失败情形,成功时resolve,失败时reject 43 | }); 44 | 45 | let wxResolve = customWxPromisify({ 46 | overrides, 47 | dealFail: true, //处理失败情形,成功失败均resolve,并在返回结果中额外标记res.succeeded=true/false 48 | }); 49 | 50 | export{ 51 | wxPromise, 52 | wxResolve 53 | } 54 | 55 | ``` 56 | 57 | ### api查询 58 | - [wxPromise](./module-wxPromise.html) -------------------------------------------------------------------------------- /docs-src/tutorials/2.7-routeParams.md: -------------------------------------------------------------------------------- 1 | ### 功能 2 | - 后一页面向前一页面传参 3 | - e.g.发布页,点击选择分类->选择分类页,选定分类->自动返回发布页,此时发布页如何获取选择结果 4 | - 前一页面向后一页面传递大量数据 5 | - e.g.手机估价页,点击卖掉换钱->发布页,此时发布页如何获取用户在估价页填写的大量表单数据 6 | 7 | ### 特点 8 | - 无需借用后端接口,无需污染前端storage,纯内存操作性能较好 9 | - 逻辑独立、通用,对页面代码无侵入 10 | 11 | ### 效果 12 | ![](./static/images/qrCode/demo-routeParams.jpg) 13 | 14 | ### 使用 15 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 16 | 1. 后一页面向前一页面传参 17 | ```js 18 | //后一页面,e.g.发布页,点击选择分类->选择分类页,选定分类->自动返回发布页,中的选择分类页 19 | import routeParams from 'fancy-mini/lib/routeParams'; 20 | 21 | //用户选定分类 22 | onCate(cate){ 23 | //设置返回参数 24 | routeParams.setBackFromData({ 25 | cate, 26 | }); 27 | 28 | //自动返回发布页 29 | wx.navigateBack(); 30 | } 31 | ``` 32 | ```js 33 | //前一页面,e.g.发布页,点击选择分类->选择分类页,选定分类->自动返回发布页,中的发布页 34 | import routeParams from 'fancy-mini/lib/routeParams'; 35 | 36 | onShow(){ 37 | this.rcvCrossPageFieldsBackward(); //接收 返回时,后一页面传递回来的数据 38 | } 39 | 40 | //接收 返回时,后一页面传递回来的数据 41 | rcvCrossPageFieldsBackward(){ 42 | let route = routeParams.getBackFromRoute(); //从哪个页面返回的 43 | let data = routeParams.getBackFromData(); //该页面传了什么数据过来 44 | if (!data) //若用户直接返回,而不是操作完毕后返回,则不作处理 45 | return; 46 | 47 | switch (route){ //从哪个页面返回的 48 | case "pages/post/cates": //选择分类页 49 | let cate = data.cate; //获取数据 50 | //... 51 | break; 52 | default: 53 | } 54 | } 55 | ``` 56 | 2. 前一页面向后一页面传参 57 | ```js 58 | //前一页面,e.g.手机估价页,点击卖掉换钱->发布页 中的手机估价页 59 | import routeParams from 'fancy-mini/lib/routeParams'; 60 | 61 | //点击卖掉换钱 62 | onSell(){ 63 | //设置传参数据 64 | routeParams.setOpenFromData({ //前一页面向后一页面传参 65 | brand: 'demo', 66 | //... 67 | }); 68 | 69 | //跳转到发布页 70 | wx.navigateTo({ 71 | url: '/pages/post/post' 72 | }); 73 | }, 74 | ``` 75 | ```js 76 | //后一页面,e.g.手机估价页,点击卖掉换钱->发布页 中的发布页 77 | import routeParams from 'fancy-mini/lib/routeParams'; 78 | 79 | onLoad(){ 80 | this.rcvCrossPageFieldsForward(); //接收 打开时,前一页面额外传递过来的数据 81 | } 82 | 83 | //接收 打开时,前一页面额外传递过来的数据 84 | rcvCrossPageFieldsForward(){ 85 | let route = routeParams.getOpenFromRoute(); //从哪个页面打开的 86 | let data = routeParams.getOpenFromData(); //该页面额外传了什么数据过来 87 | if (!data) //没有额外传参,不作处理 88 | return; 89 | 90 | switch (route){ //从哪个页面打开的 91 | case "pages/phoneEval/phoneEval": //手机估价页 92 | console.log('data:', data); //获取数据 {brand: 'demo', /*...*/} 93 | //... 94 | break; 95 | default: 96 | } 97 | } 98 | ``` 99 | 3. tips 100 | - api断句 101 | ```js 102 | getBackFromRoute() => get | back-from | route 获取 返回处的 路由 103 | getBackFromData() => get | back-from | data 获取 返回处的 数据 104 | setBackFromData() => set | back-from | data 设置 返回处的 数据 105 | getOpenFromRoute() => get | open-from | route 获取 打开处的 路由 106 | getOpenFromData() => get | open-from | data 获取 打开处的 数据 107 | setOpenFromData() => set | open-from | data 设置 打开处的 数据 108 | ``` 109 | - 层级校验 110 | 为避免数据管理混乱,本传参模块只用于相邻页面传参,不支持跨多级页面使用 e.g. A->B->C,C设置参数,A获取参数,会失败,因为A和C不是相邻页面 111 | 112 | ### api查询 113 | - [RouteParams](./RouteParams.html) -------------------------------------------------------------------------------- /docs-src/tutorials/2.8-eventHub.md: -------------------------------------------------------------------------------- 1 | ### 使用 2 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 3 | 1. 待补充 4 | 5 | ### api查询 6 | - [EventHub](./EventHub.html) -------------------------------------------------------------------------------- /docs-src/tutorials/2.9-customEntry.md: -------------------------------------------------------------------------------- 1 | ### 功能 2 | 详见 [小程序入口构造工具&二维码测试工具](https://github.com/zhuanzhuanfe/articles/blob/master/wupenghe/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%85%A5%E5%8F%A3%E6%9E%84%E9%80%A0%E5%B7%A5%E5%85%B7%26%E4%BA%8C%E7%BB%B4%E7%A0%81%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7.md) 3 | 4 | ### 效果 5 | - ![](./static/images/qrCode/demo-customEntry.jpg) 6 | 7 | ### 使用 8 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 9 | 1. 待补充 10 | 2. [示例源码](https://github.com/zhuanzhuanfe/fancy-mini-demos/blob/master/src/pages/tools/customEntry.wpy) 11 | 12 | ### api查询 13 | - [组件源码](https://github.com/zhuanzhuanfe/fancy-mini/tree/master/src/components-wepy/CustomEntry.wpy) 14 | -------------------------------------------------------------------------------- /docs-src/tutorials/2.a-qrTest.md: -------------------------------------------------------------------------------- 1 | ### 功能 2 | 详见 [小程序入口构造工具&二维码测试工具](https://github.com/zhuanzhuanfe/articles/blob/master/wupenghe/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%85%A5%E5%8F%A3%E6%9E%84%E9%80%A0%E5%B7%A5%E5%85%B7%26%E4%BA%8C%E7%BB%B4%E7%A0%81%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7.md) 3 | 4 | ### 效果 5 | - ![](./static/images/qrCode/demo-qrTest.jpg) 6 | 7 | ### 使用 8 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 9 | 1. 待补充 10 | 2. [示例源码](https://github.com/zhuanzhuanfe/fancy-mini-demos/blob/master/src/pages/tools/qrCode.wpy) 11 | 12 | ### api查询 13 | - [组件源码](https://github.com/zhuanzhuanfe/fancy-mini/tree/master/src/components-wepy/QrCode.wpy) -------------------------------------------------------------------------------- /docs-src/tutorials/2.b-rewardedVideoPlayer.md: -------------------------------------------------------------------------------- 1 | ### 使用 2 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 3 | 1. 初始化: 4 | ``` 5 | this.rewardedVideoPlayer = new RewardedVideoPlayer({adUnitId: '广告位id'})js 6 | ``` 7 | 2. 监听页面onShow: 8 | ```js 9 | onShow(){ this.rewardedVideoPlayer.handlePageChange() }; 10 | ``` 11 | 3. 播放视频: 12 | ```js 13 | let playRes = await this.rewardedVideoPlayer.play(); 14 | if (playRes.code !== 0) { //播放异常(微信版本过低/视频加载失败/其它异常情况) 15 | wx.showToast({ title: playRes.errMsg }) 16 | } else if (!playRes.isEnded) { //用户提前关闭视频 17 | ; 18 | } else { //正常完整观看 19 | ; 20 | } 21 | ``` 22 | 23 | ### api查询 24 | - [RewardedVideoPlayer](./RewardedVideoPlayer.html) -------------------------------------------------------------------------------- /docs-src/tutorials/3.1-adaptiveToast.md: -------------------------------------------------------------------------------- 1 | ## 背景 2 | 1. 原生toast存在长度限制,超过7个汉字会被截断; 3 | 2. 自定义toast组件 无法覆盖textarea、canvas、video、map等层级最高的原生组件;且需要每个页面反复引入,使用较为繁琐。 4 | 5 | ## 功能 6 | 1. 不受长度限制、不受层级约束的原生toast 7 | 2. 支持Promise 8 | 9 | ## 原理 10 | 1. 文案简洁时,使用带图标的原生toast 11 | 2. 文案较多时,自动改用不带图标的原生toast 12 | 3. 文案巨长时,自动改用系统弹窗 13 | 尤其适用于后端返回的不定长报错信息/提示文案 14 | 15 | ## 效果 16 | ![](./static/images/qrCode/demo-toast.jpg) 17 | 18 | ## 使用 19 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 20 | 1. app.js: 21 | ```js 22 | import './appPlugin' 23 | ``` 24 | 2. appPlugin.js: 25 | ```js 26 | import {registerToThis} from 'fancy-mini/lib/wepyKit'; 27 | import AdaptiveToast from 'fancy-mini/lib/AdaptiveToast'; 28 | 29 | //长度自适应的原生toast 30 | let toast = (new AdaptiveToast({ 31 | icons: { 32 | success: '/images/tipsucc.png', 33 | fail: '/images/tipfail.png' 34 | } 35 | })).toast; 36 | registerToThis('$toast', toast); 37 | 38 | export { //导出部分api,方便lib文件使用 39 | toast 40 | } 41 | ``` 42 | 43 | 3. 页面/组件: 44 | ```js 45 | //普通使用示例 46 | this.$toast({ 47 | title: '一二三四五六七', 48 | type: 'fail', 49 | }); 50 | 51 | //promise使用示例 52 | async onSubmit(){ 53 | //.... 54 | await this.$toast({ 55 | title: resp.errMsg, 56 | type: 'fail', 57 | }); 58 | console.log('toast over'); 59 | //.... 60 | } 61 | ``` 62 | 63 | ### api查询 64 | - {@link AdaptiveToast} -------------------------------------------------------------------------------- /docs-src/tutorials/3.2-textArea.md: -------------------------------------------------------------------------------- 1 | ### 使用 2 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 3 | 1. 待补充 4 | 5 | ### api查询 6 | - [组件源码](https://github.com/zhuanzhuanfe/fancy-mini/blob/master/src/components-wepy/TextAreaEle.wpy) -------------------------------------------------------------------------------- /docs-src/tutorials/4.1-dialogCommon.md: -------------------------------------------------------------------------------- 1 | ### 功能 2 | 3 | ### 效果 4 | ![](./static/images/qrCode/demo-dialog.jpg) 5 | 6 | ### 使用 7 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 8 | 1. 待补充 9 | 10 | ### api查询 11 | - [组件源码](https://github.com/zhuanzhuanfe/fancy-mini/blob/master/src/components-uniApp/DialogCommon.vue) -------------------------------------------------------------------------------- /docs-src/tutorials/4.2-operationGuide.md: -------------------------------------------------------------------------------- 1 | ### 功能 2 | 3 | 4 | 5 | ### 效果 6 | ![](./static/images/qrCode/demo-operationGuide.jpg) 7 | 8 | ### 使用 9 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 10 | 1. 待补充 11 | 2. [示例](https://github.com/zhuanzhuanfe/fancy-mini-demos/blob/master/src/pages/operationGuide/operationGuide.wpy) 12 | 13 | 14 | ### api查询 15 | - [组件源码](https://github.com/zhuanzhuanfe/fancy-mini/blob/master/src/components-wepy/operationGuide/OperationGuideModal.wpy) 16 | 17 | -------------------------------------------------------------------------------- /docs-src/tutorials/5.1-noConcurrent.md: -------------------------------------------------------------------------------- 1 | ### 功能 2 | 3 | ### 效果 4 | ![](./static/images/qrCode/demo-noConcurrent.jpg) 5 | 6 | ### 使用 7 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 8 | 1. 待补充 9 | 10 | ### api查询 11 | - [noConcurrent](./module-noConcurrent.html) -------------------------------------------------------------------------------- /docs-src/tutorials/5.2-errSafe.md: -------------------------------------------------------------------------------- 1 | ### 使用 2 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 3 | 1. 待补充 4 | 5 | ### api查询 6 | - [errSafe](./module-errSafe.html) -------------------------------------------------------------------------------- /docs-src/tutorials/5.3-compatible.md: -------------------------------------------------------------------------------- 1 | ### 使用 2 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 3 | 1. 待补充 4 | 5 | ### api查询 6 | - [compatible](./module-compatible.html) -------------------------------------------------------------------------------- /docs-src/tutorials/5.3.1-typeCheck.md: -------------------------------------------------------------------------------- 1 | ### 使用 2 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 3 | 1. 待补充 4 | 5 | ### api查询 6 | - [typeCheck](./module-typeCheck.html) -------------------------------------------------------------------------------- /docs-src/tutorials/5.4-wepyKit.md: -------------------------------------------------------------------------------- 1 | ### 使用 2 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 3 | 1. 待补充 4 | 5 | ### api查询 6 | - [wepyKit](./module-wepyKit.html) -------------------------------------------------------------------------------- /docs-src/tutorials/5.5-operationKit.md: -------------------------------------------------------------------------------- 1 | ### 使用 2 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 3 | 1. 待补充 4 | 5 | ### api查询 6 | - [operationKit](./module-operationKit.html) -------------------------------------------------------------------------------- /docs-src/tutorials/5.6-handleStr.md: -------------------------------------------------------------------------------- 1 | ### 使用 2 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 3 | 1. 待补充 4 | 5 | ### api查询 6 | - [handleStr](./module-handleStr.html) -------------------------------------------------------------------------------- /docs-src/tutorials/5.7-countDowner.md: -------------------------------------------------------------------------------- 1 | ### 使用 2 | 0. [fancy-mini setup](./tutorial-0-getStarted.html) 3 | 1. 待补充 4 | 5 | ### api查询 6 | - [countDowner](./module-countDowner.html) -------------------------------------------------------------------------------- /docs-src/tutorials/structure.json: -------------------------------------------------------------------------------- 1 | { 2 | "0-getStarted" : { 3 | "title" : "setup" 4 | }, 5 | "1-demoProject" : { 6 | "title" : "demo" 7 | }, 8 | "2.1-login" : { 9 | "title" : "[基础能力] 健壮高效的登录机制" 10 | }, 11 | "2.2-navigate" : { 12 | "title" : "[基础能力] 无限层级的路由机制" 13 | }, 14 | "2.3-request" : { 15 | "title" : "[基础能力] 灵活易扩展的请求管理" 16 | }, 17 | "2.4-cookie" : { 18 | "title" : "[基础能力] cookie机制" 19 | }, 20 | "2.5-canvasKit" : { 21 | "title" : "[基础能力] canvas工具集" 22 | }, 23 | "2.6-wxPromise" : { 24 | "title" : "[基础能力] 微信接口Promise化" 25 | }, 26 | "2.7-routeParams" : { 27 | "title" : "[基础能力] 跨页面传参" 28 | }, 29 | "2.8-eventHub" : { 30 | "title" : "[基础能力] 跨页面事件" 31 | }, 32 | "2.9-customEntry" : { 33 | "title" : "[基础能力] 入口构造工具" 34 | }, 35 | "2.a-qrTest" : { 36 | "title" : "[基础能力] 二维码测试工具" 37 | }, 38 | "2.b-rewardedVideoPlayer" : { 39 | "title" : "[基础能力] 激励视频播放器" 40 | }, 41 | 42 | "3.1-adaptiveToast" : { 43 | "title" : "[疑难杂症] toast截断问题" 44 | }, 45 | "3.2-textArea" : { 46 | "title" : "[疑难杂症] textarea遮盖浮层问题" 47 | }, 48 | 49 | "4.1-dialogCommon" : { 50 | "title" : "[组件库] 通用弹窗" 51 | }, 52 | "4.2-operationGuide" : { 53 | "title" : "[组件库] 新手引导" 54 | }, 55 | 56 | "5.1-noConcurrent" : { 57 | "title" : "[工具库] 免并发修饰器" 58 | }, 59 | "5.2-errSafe" : { 60 | "title" : "[工具库] 异常捕获修饰器" 61 | }, 62 | "5.3-compatible" : { 63 | "title" : "[工具库] 快捷兼容修饰器" 64 | }, 65 | "5.3.1-typeCheck" : { 66 | "title" : "[工具库] 类型检查修饰器" 67 | }, 68 | "5.4-wepyKit" : { 69 | "title" : "[工具库] wepy框架工具集" 70 | }, 71 | "5.5-operationKit" : { 72 | "title" : "[工具库] 基础操作工具集" 73 | }, 74 | "5.6-handleStr" : { 75 | "title" : "[工具库] GBK字符串处理工具集" 76 | }, 77 | "5.7-countDowner" : { 78 | "title" : "[工具库] 倒计时模块" 79 | } 80 | } -------------------------------------------------------------------------------- /docs-src/tutorials/说明.txt: -------------------------------------------------------------------------------- 1 | 关于文件命名 2 | 由于jsdoc的教程排序功能目前不太好使,见issue:https://github.com/jsdoc/jsdoc/issues/1028, 3 | 暂通过在文件名前加序号的方式进行排序 e.g. "0-"、"1-" 4 | 5 | 由于文件名会影响链接引用地址,因而序号一旦确定就不宜再修改,若需要在两个现有文档之间插入文档, 6 | 应延长序号,而不是重命名现有文档 e.g."2.1-","2.2-"中插入新文档,可命名为"2.1.1-" 7 | 8 | 关于引用静态资源 9 | 静态资源放在/docs-src/static/目录下,通过'./static/'的相对路径进行引用 10 | 编译时,教程文件生成的html和static目录都会被输出至/docs/目录,二者会是平级关系 11 | 12 | 关于文档左侧导航 13 | 需要在structure.json中补充对文件的说明,若不写js-doc会根据文件名自动生成导航名称 14 | 15 | 关于引用其它教程/文档 16 | 通过'./tutorial-0-getStarted.html'形式的相对路径引用其它教程 17 | 通过'./BaseLogin.html'形式的相对路径引用api文档 18 | todo:查下是否可以像jsdoc代码注释一样通过@tutorial、@link等形式引用 -------------------------------------------------------------------------------- /docs/scripts/collapse.js: -------------------------------------------------------------------------------- 1 | function hideAllButCurrent(){ 2 | //by default all submenut items are hidden 3 | //but we need to rehide them for search 4 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(parent) { 5 | parent.style.display = "none"; 6 | }); 7 | 8 | //only current page (if it exists) should be opened 9 | var file = window.location.pathname.split("/").pop().replace(/\.html/, ''); 10 | document.querySelectorAll("nav > ul > li > a").forEach(function(parent) { 11 | var href = parent.attributes.href.value.replace(/\.html/, ''); 12 | if (file === href) { 13 | parent.parentNode.querySelectorAll("ul li").forEach(function(elem) { 14 | elem.style.display = "block"; 15 | }); 16 | } 17 | }); 18 | } 19 | 20 | hideAllButCurrent(); -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/nav.js: -------------------------------------------------------------------------------- 1 | function scrollToNavItem() { 2 | var path = window.location.href.split('/').pop().replace(/\.html/, ''); 3 | document.querySelectorAll('nav a').forEach(function(link) { 4 | var href = link.attributes.href.value.replace(/\.html/, ''); 5 | if (path === href) { 6 | link.scrollIntoView({block: 'center'}); 7 | return; 8 | } 9 | }) 10 | } 11 | 12 | scrollToNavItem(); 13 | -------------------------------------------------------------------------------- /docs/scripts/polyfill.js: -------------------------------------------------------------------------------- 1 | //IE Fix, src: https://www.reddit.com/r/programminghorror/comments/6abmcr/nodelist_lacks_foreach_in_internet_explorer/ 2 | if (typeof(NodeList.prototype.forEach)!==typeof(alert)){ 3 | NodeList.prototype.forEach=Array.prototype.forEach; 4 | } -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/scripts/search.js: -------------------------------------------------------------------------------- 1 | 2 | var searchAttr = 'data-search-mode'; 3 | function contains(a,m){ 4 | return (a.textContent || a.innerText || "").toUpperCase().indexOf(m) !== -1; 5 | }; 6 | 7 | //on search 8 | document.getElementById("nav-search").addEventListener("keyup", function(event) { 9 | var search = this.value.toUpperCase(); 10 | 11 | if (!search) { 12 | //no search, show all results 13 | document.documentElement.removeAttribute(searchAttr); 14 | 15 | document.querySelectorAll("nav > ul > li:not(.level-hide)").forEach(function(elem) { 16 | elem.style.display = "block"; 17 | }); 18 | 19 | if (typeof hideAllButCurrent === "function"){ 20 | //let's do what ever collapse wants to do 21 | hideAllButCurrent(); 22 | } else { 23 | //menu by default should be opened 24 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 25 | elem.style.display = "block"; 26 | }); 27 | } 28 | } else { 29 | //we are searching 30 | document.documentElement.setAttribute(searchAttr, ''); 31 | 32 | //show all parents 33 | document.querySelectorAll("nav > ul > li").forEach(function(elem) { 34 | elem.style.display = "block"; 35 | }); 36 | //hide all results 37 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 38 | elem.style.display = "none"; 39 | }); 40 | //show results matching filter 41 | document.querySelectorAll("nav > ul > li > ul a").forEach(function(elem) { 42 | if (!contains(elem.parentNode, search)) { 43 | return; 44 | } 45 | elem.parentNode.style.display = "block"; 46 | }); 47 | //hide parents without children 48 | document.querySelectorAll("nav > ul > li").forEach(function(parent) { 49 | var countSearchA = 0; 50 | parent.querySelectorAll("a").forEach(function(elem) { 51 | if (contains(elem, search)) { 52 | countSearchA++; 53 | } 54 | }); 55 | 56 | var countUl = 0; 57 | var countUlVisible = 0; 58 | parent.querySelectorAll("ul").forEach(function(ulP) { 59 | // count all elements that match the search 60 | if (contains(ulP, search)) { 61 | countUl++; 62 | } 63 | 64 | // count all visible elements 65 | var children = ulP.children 66 | for (i=0; icompilers[ver]())); 20 | }()); 21 | 22 | 23 | //配置选项 24 | function configOptions(){ 25 | program.option('-t, --target ', '目标版本,多个以逗号分隔,e.g.:-t 1.x | -t 2.x | -t 1.x,2.x', '2.x'); 26 | } 27 | 28 | //解析选项 29 | function parseOptions(){ 30 | program.parse(process.argv); 31 | 32 | return { 33 | target: program.target, 34 | } 35 | } 36 | 37 | //1.x版本编译 38 | async function compileV1() { 39 | let mode = process.env.NODE_ENV; //'production' | 'develop' 40 | const dist = 'dist/1.x'; //输出目录 41 | let watch = mode==='develop'; //是否需要监听修改 42 | 43 | //创建输出目录 44 | await mkdir({dist}); 45 | 46 | //编译lib目录 47 | let modeParam = mode === 'production' ? 48 | '--compact true --minified --no-comments' : 49 | '--watch --compact false --no-minified'; 50 | let libJob = execCmd(`npx --package babel-cli babel -d ${dist}/lib src/lib/ --copy-files ${modeParam}`); 51 | 52 | //编译lib-style目录 53 | let styleJob = copyFiles({src: 'src/lib-style', dist: `${dist}/lib-style`, watch}); 54 | 55 | //编译components目录 56 | let compJob = copyFiles({src: 'src/components-wepy', dist: `${dist}/components`, watch}); 57 | 58 | //生成package.json 59 | let packageJob = createPackageJson({ver: '1.x', dist: `${dist}/package.json`}); 60 | 61 | //生成readme 62 | let readmeJob = copyFiles({src: 'README.md', dist: `${dist}/README.md`, watch}); 63 | 64 | await Promise.all([libJob, styleJob, compJob, packageJob, readmeJob]); 65 | } 66 | 67 | //2.x版本编译 68 | async function compileV2() { 69 | let mode = process.env.NODE_ENV; //'production' | 'develop' 70 | const dist = 'dist/2.x'; //输出目录 71 | let watch = mode==='develop'; //是否需要监听修改 72 | 73 | //创建输出目录 74 | await mkdir({dist}); 75 | 76 | //编译lib目录 77 | let libJob = copyFiles({src: 'src/lib', dist: `${dist}/lib`, watch}); 78 | 79 | //编译lib-style目录 80 | let styleJob = copyFiles({src: 'src/lib-style', dist: `${dist}/lib-style`, watch}); 81 | 82 | //编译components目录 83 | let compJob = copyFiles({src: 'src/components-uniApp', dist: `${dist}/components`, watch}); 84 | 85 | //生成package.json 86 | let packageJob = createPackageJson({ver: '2.x', dist: `${dist}/package.json`}); 87 | 88 | //生成readme 89 | let readmeJob = copyFiles({src: 'README.md', dist: `${dist}/README.md`, watch}); 90 | 91 | await Promise.all([libJob, styleJob, compJob, packageJob, readmeJob]); 92 | } 93 | 94 | /** 95 | * 创建package.json文件 96 | * @param {string} ver 版本:1.x | 2.x 97 | * @param {string} dist 输出目录 98 | */ 99 | async function createPackageJson({ver, dist}) { 100 | console.log('[创建] package.json'); 101 | 102 | dist = path.normalize(dist); 103 | 104 | //读取根目录下的package.json作为模板 105 | let baseJson = await fsPromises.readFile('package.json', {encoding: 'utf8'}); 106 | let packageObj = JSON.parse(baseJson); 107 | 108 | //修改其中的version相关字段 109 | packageObj.version = packageObj[`version-${ver}`]; //取目标版本作为version 110 | for (let prop in packageObj) { //删除多余版本配置 111 | if (prop.indexOf('version-') === 0) 112 | delete packageObj[prop]; 113 | } 114 | 115 | //生成目标版本对应的package.json 116 | await fsPromises.writeFile(dist, JSON.stringify(packageObj, null, 2), {encoding: 'utf8'}); 117 | } -------------------------------------------------------------------------------- /scripts/doc.js: -------------------------------------------------------------------------------- 1 | const {execCmd, copyFiles} = require('./util'); 2 | 3 | (async function main(){ 4 | console.log('正在生成文档'); 5 | await execCmd(`npx jsdoc -c jsdoc.conf.json`); 6 | await copyFiles({ 7 | src: 'docs-src/static', 8 | dist: 'docs/static', 9 | watch: false, 10 | printLog: false, 11 | }); 12 | console.log('文档生成完毕'); 13 | }()); 14 | -------------------------------------------------------------------------------- /scripts/publish.js: -------------------------------------------------------------------------------- 1 | const program = require('commander'); 2 | const fsPromises = require('fs').promises; 3 | const path = require('path'); 4 | const { execCmd, copyFiles } = require('./util'); 5 | 6 | (async function main(){ 7 | //参数处理 8 | configOptions(); //配置选项 9 | const options = parseOptions(); //解析选项 10 | 11 | //编译 12 | await execCmd(`npm run clean`); //清理 13 | await execCmd(`npm run build -- -t ${options.target}`); //构建 14 | await execCmd(`npm run doc`); //生成文档 15 | 16 | //发布 17 | let versions = options.target.split(','); 18 | for (let ver of versions) { 19 | await execCmd(`npm publish`, {cwd: `dist/${ver}`}); 20 | } 21 | }()); 22 | 23 | 24 | //配置选项 25 | function configOptions(){ 26 | program.option('-t, --target ', '目标版本,多个以逗号分隔,e.g.:-t 1.x | -t 2.x | -t 2.x,1.x', '2.x,1.x'); 27 | } 28 | 29 | //解析选项 30 | function parseOptions(){ 31 | program.parse(process.argv); 32 | 33 | return { 34 | target: program.target, 35 | } 36 | } -------------------------------------------------------------------------------- /scripts/util.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const fs = require('fs'); 3 | const fsPromises = require('fs').promises; 4 | const path = require('path'); 5 | 6 | /** 7 | * 执行命令行 8 | * @param {string} cmd 命令 9 | * @param {object} [options] 选项,同https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback 10 | */ 11 | async function execCmd(cmd, options={}) { 12 | //使得命令行以utf8格式输出内容(windows环境需单独设置,mac环境默认就是utf8) 13 | const setExecEncoding = process.platform === "win32" ? '@chcp 65001 >nul & cmd /d/s/c' : ''; 14 | 15 | const child = exec(`${setExecEncoding} ${cmd}`, {encoding: 'utf8', ...options}); 16 | 17 | child.stdout.on('data', (chunk) => { 18 | process.stdout.write(chunk); 19 | }); 20 | 21 | child.stderr.on('data', (chunk) => { 22 | process.stderr.write(chunk); 23 | }); 24 | 25 | return new Promise(resolve=>{ 26 | child.on('close', resolve); 27 | }); 28 | } 29 | 30 | /** 31 | * 复制文件/目录 32 | * @param {string} src 源位置 33 | * @param {string} dist 目标位置 34 | * @param {boolean} [watch=false] 是否监听后续修改 35 | * @param {boolean} [printLog=true] 是否打印日志 36 | */ 37 | async function copyFiles({src, dist, watch=false, printLog=true}){ 38 | src = path.normalize(src); 39 | dist = path.normalize(dist); 40 | 41 | await _copyFiles({src, dist, printLog}); 42 | 43 | let watcher = null; 44 | if (watch) { 45 | watcher = fs.watch(src, {recursive: true}); 46 | watcher.on('change', (eventType, filename)=>{ 47 | let srcFile = path.join(src, filename); 48 | let distFile = path.join(dist, filename); 49 | console.log(`[修改] ${srcFile} => ${distFile}`); 50 | fsPromises.copyFile(srcFile, distFile); 51 | }); 52 | } 53 | 54 | return { 55 | watcher, 56 | } 57 | } 58 | 59 | /** 60 | * 复制文件/目录 61 | * @ignore 62 | * @param {string} src 源位置 63 | * @param {string} dist 目标位置 64 | * @param {boolean} printLog 是否打印日志 65 | */ 66 | async function _copyFiles({src, dist, printLog}){ 67 | let srcStat = await fsPromises.stat(src); 68 | 69 | if (srcStat.isFile()) { 70 | printLog && console.log(`[拷贝] ${src} => ${dist}`); 71 | await fsPromises.copyFile(src, dist); 72 | return; 73 | } 74 | 75 | if (!srcStat.isDirectory()) { 76 | return; 77 | } 78 | 79 | if (!fs.existsSync(dist)) { 80 | await fsPromises.mkdir(dist, {recursive:true}); 81 | } 82 | 83 | let children = await fsPromises.readdir(src); 84 | for (let child of children) { 85 | await _copyFiles({ 86 | src: path.join(src, child), 87 | dist: path.join(dist, child), 88 | printLog, 89 | }); 90 | } 91 | } 92 | 93 | /** 94 | * 创建目录 95 | * @param {string} dist 目标路径 96 | */ 97 | async function mkdir({dist}){ 98 | dist = path.normalize(dist); 99 | return await fsPromises.mkdir(dist, {recursive:true}); 100 | } 101 | 102 | module.exports = { 103 | execCmd, 104 | copyFiles, 105 | mkdir, 106 | } -------------------------------------------------------------------------------- /src/components-uniApp/DialogCommon.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 200 | 201 | 321 | -------------------------------------------------------------------------------- /src/components-wepy/CustomEntry.wpy: -------------------------------------------------------------------------------- 1 | 11 | 42 | 43 | 224 | 225 | 228 | 296 | -------------------------------------------------------------------------------- /src/components-wepy/QrCode.wpy: -------------------------------------------------------------------------------- 1 | 32 | 33 | 207 | 208 | 211 | 268 | -------------------------------------------------------------------------------- /src/components-wepy/TextAreaEle.wpy: -------------------------------------------------------------------------------- 1 | 56 | 80 | 81 | 199 | 200 | 212 | 225 | -------------------------------------------------------------------------------- /src/components-wepy/operationGuide/operationGuide.js: -------------------------------------------------------------------------------- 1 | let guideStatus = { 2 | eleId: '', 3 | onActionStart: null, 4 | onActionFinish: null, 5 | }; 6 | 7 | 8 | function operationGuideAction({eleId, eleReg}){ 9 | return function (target, name, descriptor) { 10 | let oriFunc = descriptor.value; 11 | descriptor.value = async function (...args) { 12 | let guiding = (eleId===guideStatus.eleId) || (eleReg && eleReg.test(guideStatus.eleId)); //当前是否正在进行该处理函数对应的新手引导 13 | guiding && guideStatus.onActionStart && guideStatus.onActionStart(); 14 | let res = await oriFunc.apply(this, args); 15 | guiding && guideStatus.onActionFinish && guideStatus.onActionFinish(); 16 | return res; 17 | } 18 | } 19 | } 20 | 21 | export { 22 | guideStatus, 23 | operationGuideAction, 24 | } 25 | -------------------------------------------------------------------------------- /src/lib-style/border.less: -------------------------------------------------------------------------------- 1 | /** 2 | *生成0.5px的细线 3 | *使用示例: 4 | * .line { 5 | position: relative; //position应为relative、absolute或fixed 6 | .border-bottom(solid; #f1f1f1); //底部0.5px细线,会占用before伪元素并重置border属性 7 | .border(solid; #f1f1f1; 5px); //0.5px的圆角边框,会占用before伪元素并重置border属性 8 | } 9 | */ 10 | .beforeMixin(@type, @color, @radius){ 11 | content: ''; 12 | position: absolute; 13 | width: 200%; 14 | height: 200%; 15 | top: 0; 16 | left: 0; 17 | transform: scale(0.5); 18 | transform-origin: 0 0; 19 | box-sizing: border-box; 20 | pointer-events: none; 21 | border-radius: @radius*2px; 22 | } 23 | 24 | .parentMixin(){ 25 | // position: relative; 26 | border: none!important; 27 | } 28 | 29 | .border(@type: solid, @color: black, @radius: 0) { 30 | .parentMixin(); 31 | &:before{ 32 | .beforeMixin(@type, @color, @radius); 33 | border: 1px @type @color; 34 | } 35 | } 36 | .border-top(@type: solid, @color: black, @radius: 0) { 37 | .parentMixin(); 38 | &:before{ 39 | .beforeMixin(@type, @color, @radius); 40 | border-top: 1px @type @color; 41 | } 42 | } 43 | .border-left(@type: solid, @color: black, @radius: 0) { 44 | .parentMixin(); 45 | &:before{ 46 | .beforeMixin(@type, @color, @radius); 47 | border-left: 1px @type @color; 48 | } 49 | } 50 | .border-bottom(@type: solid, @color: black, @radius: 0) { 51 | .parentMixin(); 52 | &:before{ 53 | .beforeMixin(@type, @color, @radius); 54 | border-bottom: 1px @type @color; 55 | } 56 | } 57 | .border-right(@type: solid, @color: black, @radius: 0) { 58 | .parentMixin(); 59 | &:before{ 60 | .beforeMixin(@type, @color, @radius); 61 | border-right: 1px @type @color; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/lib-style/common.less: -------------------------------------------------------------------------------- 1 | @import "border"; 2 | @import "values"; 3 | @import "compatible"; 4 | 5 | //吸顶 6 | .sticky(@top: 0; @zIdx: @zIdx-sticky){ 7 | & when (@top = 0){ 8 | position: relative; //兼容不支持sticky的环境,避免影响absolute子元素定位 9 | } 10 | & when not (@top = 0){ 11 | position: static; //避免元素位置偏移;不支持sticky、top不为0、absolute子元素定位依赖 的情况不便兼容,尽量规避 (试了下@supports,wxss目前不支持) 12 | } 13 | 14 | position: -webkit-sticky; 15 | position: sticky; 16 | top: @top; 17 | z-index: @zIdx; 18 | } 19 | 20 | //绝对定位时占据父元素全部空间 21 | .takeFullSpace(){ 22 | top:0; 23 | left: 0; 24 | width: 100%; 25 | height: 100%; 26 | } 27 | 28 | //单行文本,过长时省略号截断 29 | .ellipsisLine(){ 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | white-space: nowrap; 33 | } 34 | 35 | //多行文本,过长时省略号截断 36 | .ellipsisLines(@lines){ 37 | overflow: hidden; 38 | text-overflow:ellipsis; 39 | display: -webkit-box; 40 | -webkit-box-orient: vertical; 41 | -webkit-line-clamp: @lines; 42 | } 43 | 44 | //清除元素默认样式(场景示例:目前页内转发只能使用button组件,但样式需自定义,故使用前需对button默认样式进行清理) 45 | .clear(){ 46 | position:static; 47 | display:block; 48 | margin: 0; 49 | padding: 0; 50 | border: none; 51 | box-sizing:content-box; 52 | font-size:18px; 53 | text-align:center; 54 | text-decoration:none; 55 | line-height:1; 56 | border-radius:0; 57 | -webkit-tap-highlight-color:transparent; 58 | overflow:hidden; 59 | color:#000000; 60 | background-color:transparent; 61 | &::before, &::after { 62 | content: ''; 63 | margin: 0; 64 | padding: 0; 65 | border: 0; 66 | width: 0; 67 | height: 0; 68 | display: none; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib-style/compatible.less: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断机型是否为iPhone X,便于结合media query为iPhone X单独设置样式, e.g. 3 | .demo { 4 | bottom: 0; //普通机型下吸底 5 | @media @iPhoneX { //iPhone X下,底部额外留出小黑条的空间 6 | bottom: 45rpx; 7 | } 8 | } 9 | */ 10 | @iPhoneX: ~"only screen and (device-width : 375px) and (device-height : 812px) and (-webkit-device-pixel-ratio : 3), only screen and (device-width : 414px) and (device-height : 896px) and (-webkit-device-pixel-ratio : 3), only screen and (device-width : 414px) and (device-height : 896px) and (-webkit-device-pixel-ratio : 2)"; 11 | 12 | /** 13 | * 底部按钮兼容 iPhone X 14 | * @baseValue 普通机型下设置的bottom值 15 | * @safeExtend iPhone X下额外预留空间 16 | * e.g. 17 | .demo { 18 | .safeBottom(); //普通机型下生效样式:bottom: 0; iPhone X下生效样式: bottom: 45rpx; 19 | .safeBottom(10rpx); //普通机型下生效样式:bottom: 10rpx; iPhone X下生效样式: bottom: 55rpx; 20 | .safeBottom(10rpx; 30rpx); //普通机型下生效样式:bottom: 10rpx; iPhone X下生效样式: bottom: 40rpx; 21 | } 22 | */ 23 | .safeBottom(@baseValue:0; @safeExtend:45rpx;){ 24 | bottom: @baseValue; //普通机型 25 | @media @iPhoneX { //iPhone X下,底部额外留出小黑条的空间 26 | bottom: @baseValue + @safeExtend; 27 | } 28 | } 29 | 30 | //底部按钮兼容 iPhone X,参数及用法同.safeBottom,作用于padding-bottom 31 | .safePaddingBottom(@baseValue:0; @safeExtend:45rpx;){ 32 | padding-bottom: @baseValue; //普通手机 33 | @media @iPhoneX { 34 | padding-bottom: @baseValue + @safeExtend; 35 | } 36 | } 37 | 38 | //底部按钮兼容 iPhone X,参数及用法同.safeBottom,作用于margin-bottom 39 | .safeMarginBottom(@baseValue:0; @safeExtend:45rpx;){ 40 | margin-bottom: @baseValue; //普通手机 41 | @media @iPhoneX { 42 | margin-bottom: @baseValue + @safeExtend; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib-style/values.less: -------------------------------------------------------------------------------- 1 | //z-index标准值 2 | @zIdx-sticky: 100; //吸顶元素 3 | @zIdx-action: 100; //吸底按钮 4 | @zIdx-loading: 1000; //页面loading 5 | @zIdx-dialog: 1800; //对话框 6 | -------------------------------------------------------------------------------- /src/lib/AdaptiveToast.js: -------------------------------------------------------------------------------- 1 | import {delay, deepAssign} from './operationKit'; 2 | 3 | /** 4 | * 自适应的toast 5 | * 处理原生toast截断问题,详见{@tutorial 3.1-adaptiveToast} 6 | */ 7 | class AdaptiveToast { 8 | _options = { 9 | icons: { 10 | success: '/images/toast/success.png', 11 | fail: '/images/toast/fail.png' 12 | }, 13 | defaultOpts: { 14 | title: '', 15 | type: 'fail', 16 | duration: 2000, 17 | }, 18 | installProps: { 19 | '$toast': 'toast' 20 | } 21 | }; 22 | 23 | /** 24 | * 构造函数 25 | * @param {object} [options] 配置参数 26 | * @param {Object.} [options.icons={success: '/images/toast/success.png',fail: '/images/toast/fail.png'}] 图标映射表,key为调用方指定的toast场景类型,value为对应的图标路径 27 | * @param {AdaptiveToast~ToastOptions} [options.defaultOpts={title: '',type: 'fail',duration: 2000}] toast的默认选项 28 | */ 29 | constructor(options){ 30 | deepAssign(this._options, options); 31 | } 32 | 33 | /** 34 | * @ignore 35 | */ 36 | get installProps(){ 37 | return this._options.installProps; 38 | } 39 | 40 | /** 41 | * 自适应的toast,会自动根据文案长度选择合适的提示方式 42 | * @function 43 | * @async 44 | * @param {AdaptiveToast~ToastOptions} options toast参数 45 | */ 46 | toast = async (options)=>{ 47 | options = Object.assign({}, this._options.defaultOpts, options); 48 | 49 | let len = options.title.length; 50 | if (len <= 7) //文案简洁,使用带图标的toast 51 | return this.sysToastIcon(options); 52 | else if (len <= 20) //文案较长,使用长文本toast 53 | return this.sysToastText(options); 54 | else //文案巨长,改用弹窗 55 | return this.sysToastModal(options); 56 | } 57 | 58 | /** 59 | * 文案较少时使用的toast,带图标,最多只能展示7个汉字 60 | * @function 61 | * @async 62 | * @param {AdaptiveToast~ToastOptions} options toast参数 63 | */ 64 | sysToastIcon = async (options)=>{ 65 | wx.showToast({ 66 | title: options.title, 67 | image: this._options.icons[options.type] || options.type, 68 | duration: options.duration, 69 | success : options.success, 70 | fail:options.fail, 71 | complete:options.complete 72 | }); 73 | await delay(options.duration); 74 | } 75 | 76 | /** 77 | * 文案中等长度时使用的toast,不带图标,最多展示两行 78 | * @function 79 | * @async 80 | * @param {AdaptiveToast~ToastOptions} options toast参数 81 | */ 82 | sysToastText = async (options)=>{ 83 | if (!wx.setTabBarItem) //不带图标的toast从基础库1.9.0开始支持;wx.canIUse('showToast.object.icon.none')不好使,暂借用其它API来判断版本 84 | return this.sysToastModal(options); 85 | 86 | let title = options.title; 87 | /*if (!title.includes('\n')) { //折成字数相等的两行 (安卓机下有时第一行会变成'...'不能正常展示,且与内容编码无关,纯英文字符串亦可复现;原因不明,暂去掉自动换行逻辑) 88 | let mid = Math.ceil(title.length/2); 89 | title = title.substring(0, mid)+'\n'+title.substring(mid); 90 | }*/ 91 | 92 | wx.showToast({ 93 | title, 94 | icon: 'none', 95 | duration: options.duration, 96 | success : options.success, 97 | fail:options.fail, 98 | complete:options.complete 99 | }); 100 | await delay(options.duration); 101 | } 102 | 103 | /** 104 | * 文案巨长时使用的toast,自动改用系统弹窗 105 | * @function 106 | * @async 107 | * @param {AdaptiveToast~ToastOptions} options toast参数 108 | */ 109 | sysToastModal = async (options)=>{ 110 | return new Promise((resolve, reject)=>{ 111 | wx.showModal({ 112 | title: '提示', 113 | content: options.title, 114 | showCancel: false, 115 | confirmText: '知道了', 116 | success : options.success, 117 | fail:options.fail, 118 | complete: (...args)=>{ 119 | options.complete && options.complete(...args); 120 | resolve(); 121 | } 122 | }); 123 | }) 124 | } 125 | } 126 | 127 | /** 128 | * @typedef {object} AdaptiveToast~ToastOptions toast参数 129 | * @property {string} title toast文案 130 | * @property {string} [type] toast场景类型 131 | * @property {number} [duration] toast时长,单位:ms 132 | */ 133 | 134 | export default AdaptiveToast; 135 | -------------------------------------------------------------------------------- /src/lib/Cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cookie管理器 3 | * 利用前端存储,模拟实现web中的cookie逻辑,详见{@tutorial 2.4-cookie} 4 | * 注:目前仅支持基础的取值赋值操作,domain、path、expires等各种配置选项暂未支持,会予以忽略 5 | */ 6 | class Cookie { 7 | _cookieStorage = ''; //cookie相关信息存储到storage时使用的key 8 | _cookieStr = ''; //当前cookie列表,格式:'key1=value1;key2=value2' 9 | 10 | /** 11 | * 构造函数 12 | * @param {string} [cookieStorageName='__cookie'] cookie相关信息存储到storage时使用的key 13 | */ 14 | constructor({cookieStorageName='__cookie'}={}){ 15 | this._cookieStorage = cookieStorageName; 16 | } 17 | 18 | /** 19 | * 读取指定cookie 20 | * key未传时,返回全部cookie 21 | * @param {string} [key] 要读取的key 22 | * @param {object} [options] 配置选项(暂未支持) 23 | * @return {string | object} cookie中key对应的value | 未传key时,返回全部key-value组成的对象 24 | * @example 25 | * //假设当前环境所有cookie为:a=1;b=2;c=3 26 | * cookie.get('a'); //返回值:'1', cookie中存在指定key时,会返回其对应值 27 | * cookie.get('nonExist'); //返回值:'', cookie中不存在指定key时,会返回空串 28 | * cookie.get(); //返回值:{a:'1', b:'2', c:'3'},未传key时,会返回全部key-value组成的对象 29 | */ 30 | get(key, options){ 31 | let cookieStr = this.getCookie(); 32 | let cookieObj = Cookie.cookieStrToObj(cookieStr); 33 | return key === undefined ? cookieObj : (cookieObj[key] || ''); 34 | } 35 | 36 | /** 37 | * 写入指定cookie 38 | * @param {string} key 要写入的key 39 | * @param {string} value 要写入的value 40 | * @param {object} [options] 配置选项(暂未支持) 41 | */ 42 | set(key, value, options){ 43 | this.setCookie(`${key}=${value};`); 44 | } 45 | 46 | /** 47 | * 获取当前可访问的cookie字符串 48 | * @return {string} cookie字符串,形如:'key1=value1;key2=value2'(类似web中读取document.cookie) 49 | */ 50 | getCookie(){ 51 | // 优先尝试从内存中读取,尽量减少访问storage的开销 52 | if(this._cookieStr) 53 | return this._cookieStr; 54 | 55 | this._cookieStr = wx.getStorageSync(this._cookieStorage); 56 | return this._cookieStr; 57 | } 58 | 59 | /** 60 | * 写入cookie 61 | * @param {string} setStr 写入指令,格式形如:'key1=value1; path=/;'(类似web中document.cookie赋值) 62 | */ 63 | setCookie(setStr){ 64 | //参数处理 65 | setStr = setStr.trim(); 66 | 67 | //字段配置 68 | let setKey = ''; //要赋值的key 69 | let setValue = ''; //要赋值的value 70 | let configOptions = {}; //配置项:domain、path、expires等 71 | 72 | //字段解析 73 | let fieldStrArr = setStr.split(/\s*;\s*/); 74 | for (let [fieldIdx, fieldStr] of fieldStrArr.entries()) { 75 | let sepIdx = fieldStr.indexOf('='); 76 | let name = fieldStr.substring(0, sepIdx); 77 | let value = fieldStr.substring(sepIdx+1); 78 | 79 | if (fieldIdx === 0) { //第一个选项,认为是要赋值的key 80 | setKey = name; 81 | setValue = value; 82 | } else { //其它选项,认为是配置项 83 | configOptions[name.toLowerCase()] = value; 84 | } 85 | } 86 | 87 | //字段检查 88 | if (!setKey) { 89 | console.error('[setCookie] bad param, no key found:', setStr); 90 | return; 91 | } 92 | 93 | //更新cookie(配置项暂予忽略) 94 | this._cookieStr = Cookie.mergeCookieStr(this._cookieStr, `${setKey}=${setValue};`); 95 | wx.setStorage({ 96 | key: this._cookieStorage, 97 | data: this._cookieStr, 98 | }); 99 | } 100 | 101 | /** 102 | * 将'key1=value1;key2=value2'形式的cookie字符串转为{key1: value1, key2: value2}的对象形式 103 | * @param {string} cookieStr 104 | * @return {Object} cookieObj 105 | */ 106 | static cookieStrToObj(cookieStr){ 107 | let fieldStrArr = cookieStr.split(/\s*;\s*/).filter(fieldStr=>!!fieldStr); 108 | let cookieObj = {}; 109 | 110 | for (let fieldStr of fieldStrArr) { 111 | // 注意不要直接用匹配split('='), ppu等含=的不规则cookie会出错 112 | let index = fieldStr.indexOf('='); 113 | let key = fieldStr.substring(0, index); 114 | let value = fieldStr.substring(index+1); 115 | 116 | cookieObj[key] = value; 117 | } 118 | 119 | return cookieObj; 120 | } 121 | 122 | /** 123 | * 将{key1: value1, key2: value2}的对象形式键值对转为'key1=value1;key2=value2'形式的cookie字符串 124 | * @param {Object} cookieObj 125 | * @return {string} cookieStr 126 | */ 127 | static cookieObjToStr(cookieObj){ 128 | let cookieStr = ''; 129 | for (let key in cookieObj) 130 | cookieStr += `${key}=${cookieObj[key]};`; 131 | return cookieStr; 132 | } 133 | 134 | /** 135 | * 将'key1=value1;key2=value2'形式的cookie字符串合并,key相同时后面的覆盖前面的 136 | * @param {...string} cookieStrs 待合并的cookie字符串 137 | * @return {string} 合并后的cookie字符串 138 | */ 139 | static mergeCookieStr(...cookieStrs) { 140 | let cookieObjs = cookieStrs.filter(cookieStr=>!!cookieStr).map(Cookie.cookieStrToObj); 141 | 142 | let cookieObj = Object.assign( 143 | {}, 144 | ...cookieObjs, 145 | ); 146 | 147 | return Cookie.cookieObjToStr(cookieObj); 148 | } 149 | } 150 | 151 | export default Cookie; 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/lib/EventHub.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 事件中心,用于跨组件/跨页面事件通信,详见 {@tutorial 2.8-eventHub} 3 | */ 4 | class EventHub { 5 | _validEvents = []; //事件列表 6 | _listeners = []; //监听列表 7 | 8 | /** 9 | * 构造函数 10 | * @param {Array} validEvents 配置的事件列表 11 | */ 12 | constructor({validEvents}){ 13 | if (!Array.isArray(validEvents)) { 14 | console.error('[EventHub] bad param, validEvents shall be Array'); 15 | return; 16 | } 17 | 18 | this._validEvents = validEvents; 19 | } 20 | 21 | /** 22 | * 监听指定事件 23 | * @param {string} eventType 事件类型 24 | * @param {function} handler 监听函数 25 | * @param {string} [persistType='once'] 持续策略:once-触发一次后自动移除监听 | always-每次都触发 26 | */ 27 | subscribe({eventType, handler, persistType='once'}){ 28 | if (!(this._validEvents.includes(eventType))) { 29 | console.error('[EventHub] subscribe,试图监听无效事件:', eventType, '有效事件列表:', this._validEvents); 30 | return; 31 | } 32 | 33 | this._listeners.push({ 34 | eventType, 35 | handler, 36 | triggerCount: 0, //触发了几次 37 | limitCount: persistType==='once' ? 1 : 0, //最多触发几次,0表示不限 38 | }); 39 | } 40 | 41 | /** 42 | * 触发指定事件 43 | * @param {string} eventType 事件类型 44 | * @param {*} data 传递给监听函数的数据 45 | */ 46 | notify({eventType, data}){ 47 | if (!(this._validEvents.includes(eventType))) { 48 | console.error('[EventHub] notify,试图触发无效事件:', eventType, '有效事件列表:', this._validEvents); 49 | return; 50 | } 51 | 52 | //监听回调 53 | for (let listener of this._listeners) { 54 | if (listener.eventType !== eventType) 55 | continue; 56 | 57 | try { 58 | listener.handler(data); 59 | } catch (e) { 60 | console.error( 61 | '[EventHub] caught err when exec handler', 62 | 'err:', e, 63 | 'eventType:', eventType, 64 | 'handler:', listener.handler 65 | ); 66 | } 67 | 68 | ++ listener.triggerCount; 69 | } 70 | 71 | 72 | //移除达到回调上限的监听函数 73 | this._listeners = this._listeners.filter(listener=>!(listener.limitCount>0 && listener.limitCount<=listener.triggerCount)); 74 | } 75 | 76 | /** 77 | * 取消监听 78 | * @param {string} eventType 事件类型 79 | * @param {function} handler 监听函数 80 | */ 81 | unsubscribe({eventType, handler}){ 82 | this._listeners = this._listeners.filter(listener=>!(listener.eventType===eventType && listener.handler===handler)); 83 | } 84 | } 85 | 86 | export default EventHub; -------------------------------------------------------------------------------- /src/lib/RewardedVideoPlayer.js: -------------------------------------------------------------------------------- 1 | import {singleAisle, errSafe} from './decorators'; 2 | import {ctxDependConsole as debugConsole} from './debugKit'; 3 | 4 | /** 5 | * 激励视频播放器,封装激励视频加载、播放时序,详见{@tutorial 2.b-rewardedVideoPlayer} 6 | */ 7 | class RewardedVideoPlayer { 8 | _adUnitId = ''; //广告位id 9 | 10 | _rewardedVideo = null; //原生视频实例 11 | _loadStateValue = 'notStart'; //视频加载状态:notStart-未开始 | notSupport-版本过低不支持 | loading-加载中 | failed-加载失败 | loaded-加载成功 12 | _playStateValue = 'idle'; //视频播放状态:idle-空闲中 | playing-播放中 | aborted-中途退出 | ended-播放完毕 13 | 14 | _loadStateChangeListeners = []; //加载状态监听列表 15 | 16 | /** 17 | * 构造函数 18 | * @param {string} adUnitId 广告位id 19 | */ 20 | constructor({adUnitId}){ 21 | this._adUnitId = adUnitId; 22 | this._init(); 23 | } 24 | 25 | _init(){ 26 | if (!wx.createRewardedVideoAd) { 27 | this._rewardedVideo = null; 28 | this._loadState = 'notSupport'; 29 | this._playState = 'idle'; 30 | return; 31 | } 32 | 33 | this._rewardedVideo = wx.createRewardedVideoAd({ 34 | adUnitId: this._adUnitId 35 | }); 36 | 37 | this._rewardedVideo.onError((e)=>{ 38 | console.error('[rewardedVideoPlayer] error:', e); 39 | this._loadState==='loading' && (this._loadState = 'failed'); 40 | this._playState==='playing' && (this._playState = 'failed'); 41 | }); 42 | 43 | this._loadState = 'notStart'; 44 | this._playState = 'idle'; 45 | } 46 | 47 | /** 48 | * 切换页面时原生视频实例失效,故每次onShow需重新初始化 49 | * @param {boolean} [preload=true] 是否需要预加载视频:true-开始预加载 | false-不进行预加载(后续可手动调用load()决定加载时机) 50 | */ 51 | handlePageChange({preload=true}={}){ 52 | debugConsole.log('[rewardedVideoPlayer] enter handlePageChange'); 53 | if (this._playState === 'playing') //点击广告链接跳转其它小程序返回造成的onShow 54 | return; 55 | 56 | debugConsole.log('[rewardedVideoPlayer] re init'); 57 | this._init(); 58 | preload && this.load(); 59 | } 60 | 61 | /** 62 | * 开始加载视频 63 | * @param {boolean} [reset=false] 是否需要重置:true-强制重新加载 | false-可复用已有视频 64 | */ 65 | @singleAisle 66 | @errSafe 67 | async load({reset=false}={}){ 68 | debugConsole.log('[rewardedVideoPlayer] enter load'); 69 | if (!this._rewardedVideo) 70 | return {succeeded: false}; 71 | 72 | if (this._loadState==='loaded' && !reset) 73 | return {succeeded: true}; 74 | 75 | this._loadState = 'loading'; 76 | 77 | debugConsole.log('[rewardedVideoPlayer] begin load'); 78 | let loadRes = await new Promise((resolve,reject)=>{ 79 | this._rewardedVideo.load().then(()=>{ 80 | resolve({succeeded: true}); 81 | }).catch((e)=>{ 82 | console.error('[rewardedVideoPlayer] load failed, exception:', e); 83 | resolve({succeeded: false}); 84 | }); 85 | }); 86 | 87 | debugConsole.log('[rewardedVideoPlayer] finish load, res:', loadRes); 88 | this._loadState = loadRes.succeeded ? 'loaded' : 'failed'; 89 | return {succeeded: loadRes.succeeded}; 90 | } 91 | 92 | /** 93 | * 开始播放视频 94 | * @return {object} 播放结果 { 95 | code: 0, //是否正常:0-正常播放,其它-播放异常(微信版本过低/视频加载失败/其它异常情况) 96 | errMsg: '', //异常提示信息 97 | isEnded: true, //(正常时)是否观看完整 98 | * } 99 | */ 100 | @errSafe 101 | async play(){ 102 | debugConsole.log('[rewardedVideoPlayer] enter play'); 103 | //低版本兼容 104 | if (!this._rewardedVideo) 105 | return {code: -1, errMsg: '您的微信版本较低,不支持此功能', isEnded: false}; 106 | 107 | //加载视频 108 | if (this._loadState !== 'loaded'){ 109 | wx.showLoading({ 110 | title: '视频加载中' 111 | }); 112 | 113 | await this.load(); 114 | 115 | wx.hideLoading(); 116 | 117 | if (this._loadState !== 'loaded') 118 | return {code: -2, errMsg: '没有更多视频了', isEnded: false}; 119 | } 120 | 121 | //播放视频 122 | debugConsole.log('[rewardedVideoPlayer] begin play'); 123 | this._playState = 'playing'; 124 | let playRes = await new Promise((resolve, reject)=>{ 125 | let closeHandler = (status)=>{ 126 | let isEnded = (status && status.isEnded || status === undefined); 127 | this._rewardedVideo.offClose(closeHandler); 128 | resolve({code: 0, isEnded, errMsg: 'ok'}); 129 | }; 130 | this._rewardedVideo.onClose(closeHandler); 131 | this._rewardedVideo.show().catch((e)=>{ 132 | console.error('[rewardedVideoPlayer] play failed, exception:', e); 133 | resolve({code: -3, errMsg: '播放异常', isEnded: false}); 134 | }); 135 | }); 136 | 137 | debugConsole.log('[rewardedVideoPlayer] finish play, res:', playRes); 138 | //结果处理 139 | this._playState = playRes.code===0&&playRes.isEnded ? 'ended' : 'aborted'; 140 | this.load({reset: true}); //开始后台加载下一个视频(不管播没播完,都需要重新加载) 141 | 142 | return playRes; 143 | } 144 | 145 | /** 146 | * 监听加载状态变化,用于展示/隐藏入口等 147 | * @param {Function} handler 监听函数 148 | * @example 149 | * rewardedVideoPlayer.onLoadStateChange(({state})=>{ 150 | * console.log('state', state); //加载状态,详见{@link RewardedVideoPlayer#loadState} 151 | * }) 152 | */ 153 | onLoadStateChange(handler){ 154 | if (typeof handler !== 'function') 155 | return; 156 | 157 | handler({state: this._loadState}); 158 | this._loadStateChangeListeners.push(handler); 159 | } 160 | 161 | /** 162 | * 获取当前加载状态:notStart-未开始 | notSupport-版本过低不支持 | loading-加载中 | failed-加载失败 | loaded-加载成功 163 | */ 164 | get loadState(){ //供外部调用,只读 165 | return this._loadStateValue; 166 | } 167 | get _loadState(){ //内部使用,可读可写 168 | return this._loadStateValue; 169 | } 170 | set _loadState(newState){ //内部使用,可读可写 171 | if (this._loadState === newState) 172 | return; 173 | 174 | this._loadStateValue = newState; 175 | this._loadStateChangeListeners.forEach(listener=>listener({state: newState})); 176 | } 177 | get _playState(){ 178 | return this._playStateValue; 179 | } 180 | set _playState(newState){ 181 | this._playStateValue = newState; 182 | } 183 | } 184 | 185 | export default RewardedVideoPlayer; -------------------------------------------------------------------------------- /src/lib/canvasKit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * canvas工具集 3 | * @module canvasKit 4 | */ 5 | 6 | export default { 7 | /** 8 | * 绘制图片,保持宽高比居中裁剪,短边完全展示,长边居中截取 9 | * 说明: 10 | * 1.应先绘制图片,后填充图片周边内容,否则图片周边长边方向的现有内容会被擦除 11 | * 2.在开发者工具上图片多余部分无法被清除,但在真机上正常 12 | * 3.早期小程序canvas不支持clip,所以采用先绘制再擦除的方式实现,导致绘制顺序比较受限,后续考虑改用clip方式实现,待优化 13 | * @param ctx wx.createCanvasContext返回的canvas绘图上下文 14 | * @param {string} picFile 图片临时文件路径 15 | * @param {object} picInfo wx.getImageInfo返回的图片原始信息 16 | * @param {number} x 左上角横坐标 17 | * @param {number} y 左上角纵坐标 18 | * @param {number} w 宽度 19 | * @param {number} h 高度 20 | * @param {string} [bgColor="#ffffff"] 背景色,裁剪后多余部分用背景色擦除 21 | * 22 | */ 23 | aspectFill({ctx, picFile, picInfo, x, y, w, h, bgColor="#ffffff"}){ 24 | let aspect = picInfo.width / picInfo.height; //图片宽高比 25 | let [dx, dy, dw, dh] = [0, 0, 0, 0]; //整张图片绘制位置 26 | let extras = []; //需擦除的多余区域 27 | if (aspect < w/h) { 28 | dw = w; 29 | dh = dw/aspect; 30 | dx = x; 31 | dy = y - (dh-h)/2; 32 | extras = [[dx-1, dy-1, dw+2, (dh-h)/2+1], [dx-1, dy+(dh-h)/2+h, dw+2, (dh-h)/2+1]]; //为避免残余半像素的细线,擦除方向多加1px 33 | } else { 34 | dh = h; 35 | dw = dh*aspect; 36 | dx = x - (dw-w)/2; 37 | dy = y; 38 | extras = [[dx-1, dy-1, (dw-w)/2+1, dh+2], [dx+(dw-w)/2+w, dy-1, (dw-w)/2+1, dh+2]];//为避免残余半像素的细线,擦除方向多加1px 39 | } 40 | ctx.drawImage(picFile, dx, dy, dw, dh); //保持宽高比,缩放至指定区域后,绘制整张图片 41 | ctx.save(); 42 | ctx.setFillStyle(bgColor); 43 | for (let extra of extras) { //擦除整张图片中多余区域 44 | let [ex, ey, ew, eh] = extra; 45 | if (ex+ew <= 0 || ey+eh<=0) 46 | continue; 47 | if (ex < 0) { 48 | ew -= Math.abs(ex); 49 | ex = 0; 50 | } 51 | if (ey < 0) { 52 | eh -= Math.abs(ey); 53 | ey = 0; 54 | } 55 | ctx.fillRect(ex, ey, ew, eh); 56 | } 57 | ctx.restore(); 58 | }, 59 | 60 | /** 61 | * 绘制图片,保持图片纵横比,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。 62 | * @param ctx wx.createCanvasContext返回的canvas绘图上下文 63 | * @param {string} picFile 图片临时文件路径 64 | * @param {number} x 左上角横坐标 65 | * @param {number} y 左上角纵坐标 66 | * @param {number} w 宽度 67 | * @param {number} h 高度 68 | * @param {string} bgColor 背景色,裁剪后多余部分用背景色填充 69 | * 70 | */ 71 | aspectFit({ctx, picFile, x, y, w, h, bgColor}){ 72 | return this._getImageInfo(picFile) 73 | .then((picInfo)=>{ 74 | let aspect = picInfo.width / picInfo.height; //图片宽高比 75 | let [dx, dy, dw, dh] = [0, 0, 0, 0]; //整张图片绘制位置 76 | if (aspect < w/h) { 77 | dh = h; 78 | dw = dh*aspect; 79 | dx = x - (dw-w)/2; 80 | dy = y; 81 | } else { 82 | dw = w; 83 | dh = dw/aspect; 84 | dx = x; 85 | dy = y - (dh-h)/2; 86 | } 87 | 88 | if(bgColor){ 89 | ctx.save(); 90 | ctx.setFillStyle(bgColor); 91 | ctx.fillRect(x,y,w,h); 92 | ctx.restore(); 93 | } 94 | ctx.drawImage(picFile, dx, dy, dw, dh); 95 | }) 96 | }, 97 | 98 | /** 99 | * 将方形区域切成圆形,场景示例:将头像切成圆形展示 100 | * 说明: 101 | * 1.方形区域四角会被填充成指定的背景色,只保留中央圆形区域不变 102 | * 2.早期小程序canvas不支持clip,所以采用先绘制再擦除的方式实现圆形头像,只能在纯色背景上使用,后续考虑改用clip方式实现,待优化 103 | * @param ctx wx.createCanvasContext返回的canvas绘图上下文 104 | * @param {number} x 左上角横坐标 105 | * @param {number} y 左上角纵坐标 106 | * @param {number} w 宽度/高度/圆的直径 107 | * @param {string} [bgColor="#ffffff"] 背景色,擦除部分以背景色填充 108 | */ 109 | rounded({ctx, x, y, w, bgColor="#ffffff"}){ 110 | ctx.save(); 111 | ctx.translate(x, y); 112 | ctx.beginPath(); 113 | ctx.moveTo(w, w/2); 114 | ctx.arc(w/2,w/2,w/2,0,2*Math.PI, false); 115 | ctx.lineTo(w, 0); 116 | ctx.lineTo(0, 0); 117 | ctx.lineTo(0, w); 118 | ctx.lineTo(w, w); 119 | ctx.closePath(); 120 | ctx.setFillStyle(bgColor); 121 | ctx.fill(); 122 | ctx.restore(); 123 | }, 124 | 125 | /** 126 | * 将矩形切成圆角矩形 127 | * 说明: 128 | * 1.方形区域四角会被填充成指定的背景色,只保留中央圆角矩形区域不变 129 | * 2.早期小程序canvas不支持clip,所以采用先绘制再擦除的方式实现圆角矩形,只能在纯色背景上使用,现推荐改用 canvasKit.createBorderRadiusPath + ctx.clip 生成圆角矩形/图片/边框式实现,待优化 130 | * @param ctx wx.createCanvasContext返回的canvas绘图上下文 131 | * @param {number} x 矩形左上角横坐标 132 | * @param {number} y 矩形左上角纵坐标 133 | * @param {number} w 矩形宽度 134 | * @param {number} h 矩形高度 135 | * @param {number} radius 圆角半径 136 | * @param {string} [bgColor="#ffffff"] 背景色,擦除部分以背景色填充 137 | */ 138 | borderRadius({ctx, x, y, w, h, radius, bgColor="#ffffff"}){ 139 | ctx.save(); 140 | ctx.translate(x, y); 141 | 142 | ctx.setFillStyle(bgColor); 143 | 144 | //擦除左上角多余部分 145 | ctx.beginPath(); 146 | ctx.moveTo(0, 0+radius); 147 | ctx.quadraticCurveTo(0, 0, 0+radius, 0); 148 | ctx.lineTo(0, 0); 149 | ctx.closePath(); 150 | ctx.fill(); 151 | 152 | //擦除右上角多余部分 153 | ctx.beginPath(); 154 | ctx.moveTo(w-radius, 0); 155 | ctx.quadraticCurveTo(w, 0, w, radius); 156 | ctx.lineTo(w, 0); 157 | ctx.closePath(); 158 | ctx.fill(); 159 | 160 | //擦除右下角角多余部分 161 | ctx.beginPath(); 162 | ctx.moveTo(w-radius, h); 163 | ctx.quadraticCurveTo(w, h, w, h-radius); 164 | ctx.lineTo(w, h); 165 | ctx.closePath(); 166 | ctx.fill(); 167 | 168 | //擦除左下角多余部分 169 | ctx.beginPath(); 170 | ctx.moveTo(0, h-radius); 171 | ctx.quadraticCurveTo(0, h, 0+radius, h); 172 | ctx.lineTo(0, h); 173 | ctx.closePath(); 174 | ctx.fill(); 175 | 176 | ctx.restore(); 177 | }, 178 | 179 | /** 180 | * 生成圆角边框路径,后续可使用该路径绘制圆角矩形、圆角图片、圆角边框等 181 | * @param ctx wx.createCanvasContext返回的canvas绘图上下文 182 | * @param {number} x 矩形左上角横坐标 183 | * @param {number} y 矩形左上角纵坐标 184 | * @param {number} w 矩形宽度 185 | * @param {number} h 矩形高度 186 | * @param {number} radius 圆角半径 187 | */ 188 | createBorderRadiusPath({ctx, x, y, w, h, radius}){ 189 | ctx.beginPath(); 190 | ctx.moveTo(x, y+radius); 191 | ctx.quadraticCurveTo(x, y, x+radius, y); //左上角弧线 192 | 193 | ctx.lineTo(x+w-radius, y); //顶部水平线 194 | ctx.quadraticCurveTo(x+w, y, x+w, y+radius); //右上角弧线 195 | 196 | ctx.lineTo(x+w, y+h-radius); //右侧竖线 197 | ctx.quadraticCurveTo(x+w, y+h, x+w-radius, y+h); //右下角弧线 198 | 199 | ctx.lineTo(x+radius, y+h); //底部水平线 200 | ctx.quadraticCurveTo(x, y+h, x, y+h-radius); //左下角弧线 201 | ctx.closePath(); //左侧竖线 202 | }, 203 | 204 | /** 205 | * 绘制文本,支持\n换行 206 | * @param ctx wx.createCanvasContext返回的canvas绘图上下文 207 | * @param {string} text 文本内容,支持\n换行 208 | * @param {number} x 文本区域(含行高)左上角横坐标;居中对齐时,改取中点横坐标 209 | * @param {number} y 文本区域(含行高)左上角纵坐标 210 | * @param {number} fontSize 字号,单位:px 211 | * @param {string} color 颜色 212 | * @param {number} lineHeight 行高 213 | * @param {string} textAlign 水平对齐方式,支持'left'、'center',其它值没试过 214 | */ 215 | fillText(ctx, {text, x, y, fontSize, color, lineHeight, textAlign}){ 216 | ctx.save(); 217 | 218 | lineHeight = lineHeight || fontSize; 219 | fontSize && ctx.setFontSize(fontSize); 220 | color && ctx.setFillStyle(color); 221 | textAlign && ctx.setTextAlign(textAlign); 222 | 223 | let lines = text.split('\n'); 224 | for (let line of lines) { 225 | ctx.fillText(line, x, y+lineHeight-(lineHeight-fontSize)/2); 226 | y += lineHeight; 227 | } 228 | 229 | ctx.restore(); 230 | }, 231 | 232 | /** 233 | * 字符串过长截断,1个字母长度计为1,1个汉字长度计为2 234 | * 更新: 235 | * 1. 早期小程序canvas不支持测量文本实际尺寸,所以采用手动粗略计算的方式实现过长处理 236 | * 2. 后来小程序canvas提供了measureText接口,支持测量文本实际尺寸信息,本方法待优化 237 | * @param {string} str 原字符串 238 | * @param {number} len 最大长度 239 | * @param {boolean} ellipsis 过长时截断后是否加'...' 240 | * @return {string} 截断后字符串 241 | */ 242 | ellipsisStr(str, len, ellipsis=true) { 243 | var str_length = 0; 244 | var str_len = 0; 245 | var str_cut = new String(); 246 | str_len = str.length; 247 | for (var i = 0; i < str_len; i++) { 248 | let a = str.charAt(i); 249 | str_length++; 250 | if (escape(a).length > 4) { 251 | //中文字符的长度经编码之后大于4 252 | str_length++; 253 | } 254 | str_cut = str_cut.concat(a); 255 | if (str_length >= len) { 256 | str_cut = str_cut.concat(ellipsis&&(str_length>len || i+1 4) { 280 | //中文字符的长度经编码之后大于4 281 | str_length++; 282 | } 283 | } 284 | return str_length; 285 | }, 286 | 287 | _getImageInfo(picFile) { 288 | return new Promise((resolve,reject)=>{ 289 | wx.getImageInfo({ 290 | src: picFile, 291 | success: res =>{ 292 | resolve(res) 293 | }, 294 | fail: res=>{ 295 | reject(res) 296 | } 297 | }) 298 | }) 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/lib/countdowner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 倒计时模块 3 | * @module countDowner 4 | * @param {Object} obj - 必填项,以对象字面量形式传参 5 | * @param {Number} obj.countFromInMs - 倒计时开始时的剩余时间,单位ms 6 | * @param {Function} obj.onTimeout - 倒计时结束时执行的回调函数 7 | * @param {Function} obj.onTimeChange - 每隔interval时间触发一次的回调函数,参数 8 | * @param {Number} obj.interval - 每间隔interval毫秒计算一次剩余时间,计算结果通过onTimeChange函数*入参传给调用方 9 | * @example: 10 | * import Countdowner from 'fancy-mini/lib/countdowner' 11 | * var countdowner = new Countdowner({ 12 | * countFromInMs: 1000*60*60*24, // 倒计时一小时 13 | * onTimeChange: (res)=>{ 14 | * // 100ms执行一次,输出格式:[几天,几时,几分,几秒,几个(interval毫秒)] 15 | * console.log(res.currentTimeArr); // [00, 23, 59, 59, 9] 16 | * console.log(res.currentTimeArrWithoutDay); // [35, 23, 23, 23] 17 | * }, 18 | * onTimeout: ()=>{ 19 | * }, 20 | * interval: 100 // 100ms输出一次当前时间 21 | * }); 22 | * // 进阶用法: 23 | * //1、初始化完成后可通过实例绑定/解绑多个事件处理函数 24 | * countdowner.on('timechange/timeout', fn); 25 | * countdowner.off('timechange/timeout', fn); 26 | * //2、暂停/重启,countdowner.pause()/countdowner.restart(); 27 | */ 28 | export default class Countdowner { 29 | constructor({countFromInMs, onTimeChange, onTimeout, interval=1000}){ 30 | if(onTimeout !== undefined)this.on('timeout', onTimeout); 31 | if(onTimeChange !== undefined)this.on('timechange', onTimeChange); 32 | if(countFromInMs !== undefined){ 33 | countFromInMs = Number(countFromInMs); 34 | this.remainMs = this.countFromInMs = countFromInMs; 35 | this.deadline = Date.now() + countFromInMs; 36 | this.interval = interval; 37 | clearTimeout(this.timer); 38 | Countdowner.ticktock.call(this); 39 | } 40 | } 41 | 42 | timer = null; 43 | deadline = undefined; 44 | interval = 100; 45 | remainMs = undefined; 46 | isPause = false; 47 | pauseTime = undefined; 48 | eventsHandler = { 49 | timeout: [], 50 | timechange: [] 51 | }; 52 | 53 | /** 54 | * 事件监听函数 55 | * @param {string} eventName 事件名(timeout、 timechange) 56 | * @param {function} fn 回调函数 57 | */ 58 | on(eventName, fn){ 59 | if(eventName != 'timeout' && eventName != 'timechange'){ 60 | console.error('仅支持timeout和timechange事件'); 61 | return; 62 | } 63 | this.eventsHandler[eventName].push(fn); 64 | } 65 | 66 | /** 67 | * 取消事件监听 68 | * @param {string} eventName 事件名(timeout、 timechange) 69 | * @param {function} fn 回调函数 70 | */ 71 | off(eventName, fn){ 72 | if(eventName != 'timeout' && eventName != 'timechange'){ 73 | console.error('仅支持timeout和timechange事件'); 74 | return; 75 | } 76 | let index = this.eventsHandler[eventName].indexOf(fn); 77 | if(index > -1)this.eventsHandler[eventName].splice(index, 1); 78 | } 79 | 80 | /** 81 | * 暂停 82 | * @param {Object} Object.strict 是否从暂停时间开始计算剩余时间(默认为true) 83 | */ 84 | pause({ strict = true }){ 85 | this.pauseTime = Date.now(); 86 | this.strictPause = strict; 87 | this.isPause = true; 88 | } 89 | 90 | /** 91 | * 重启 92 | */ 93 | restart(){ 94 | this.isPause = false; 95 | Countdowner.ticktock.call(this); 96 | } 97 | 98 | static onTimeout(){ 99 | this.eventsHandler.timeout.forEach(fn => { 100 | typeof fn === 'function' && fn(); 101 | }) 102 | } 103 | 104 | static onTimeChange(timeSnapshot){ 105 | this.eventsHandler.timechange.forEach(fn => { 106 | typeof fn === 'function' && fn(timeSnapshot); 107 | }) 108 | } 109 | 110 | static async ticktock(){ 111 | if (this.remainMs<=0) { 112 | this.countFromInMs>0 && Countdowner.onTimeout.call(this); 113 | return; 114 | } 115 | await new Promise((resolve, reject)=>{ 116 | this.timer = setTimeout(resolve, this.interval); 117 | }); 118 | 119 | // 如果使用了暂停功能,从暂停时间开始计算剩余时间 120 | let limit = Date.now(); 121 | if(this.pauseTime && this.strictPause){ 122 | limit = this.pauseTime; 123 | this.pauseTime += this.interval; 124 | } 125 | this.remainMs = Math.max(0, this.deadline - limit); 126 | 127 | let timeSnapshot = [ 128 | this.remainMs / DAY, 129 | this.remainMs % DAY / HOUR, 130 | this.remainMs % HOUR / MINUTE, 131 | this.remainMs % MINUTE / SECOND, 132 | this.remainMs % SECOND / this.interval 133 | ].map(Math.floor).map((num,idx,arr)=>(padStart(num, 2, '0'))); 134 | 135 | let timeSnapshotWithoutDay = [ 136 | this.remainMs / HOUR, 137 | this.remainMs % HOUR / MINUTE, 138 | this.remainMs % MINUTE / SECOND, 139 | this.remainMs % SECOND / this.interval 140 | ].map(Math.floor).map((num,idx,arr)=>(padStart(num, 2, '0'))); 141 | 142 | Countdowner.onTimeChange.call(this, { 143 | currentTimeArr: timeSnapshot, 144 | currentTimeArrWithoutDay: timeSnapshotWithoutDay 145 | }); 146 | if(!this.isPause)Countdowner.ticktock.call(this); 147 | } 148 | } 149 | 150 | const SECOND = 1000; 151 | const MINUTE = 60 * SECOND; 152 | const HOUR = 60 * MINUTE; 153 | const DAY = 24 * HOUR; 154 | 155 | function padStart(str, minLen, leadChar) { 156 | str = String(str); 157 | while (str.length < minLen) 158 | str = leadChar+str; 159 | return str; 160 | } -------------------------------------------------------------------------------- /src/lib/debugKit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * @ignore 4 | */ 5 | 6 | const debug = false; //调试开关, todo:根据运行模式/其它条件自动判断是否开启调试 7 | 8 | /** 9 | * 上下文相关的控制台:开启调试模式时,功能同系统console;关闭调试模式时,忽略所有console调用。调试模式开关由本模块自动获取/统一指定,对调用方透明。 10 | * 使用示例: 11 | * import {ctxDependConsole as console} from '../../lib/debugKit' 12 | * console.log('ha ha ha'); //开启调试模式时,打印'ha ha ha';关闭调试模式时,自动无视此行代码 13 | */ 14 | export const ctxDependConsole = (function () { 15 | let ctxDependConsole = {}; 16 | for (let p in console) { 17 | if (typeof console[p] !== "function") { 18 | ctxDependConsole[p] = console[p]; 19 | continue; 20 | } 21 | ctxDependConsole[p] = debug ? console[p] : function () {}; 22 | } 23 | return ctxDependConsole; 24 | })(); 25 | -------------------------------------------------------------------------------- /src/lib/decorator/compatible.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 修饰器,实现各种兼容用法 3 | * @module compatible 4 | */ 5 | 6 | /** 7 | * 提供微信api形式的回调 8 | * 主要适用场景:将微信api改写为promise形式后,兼容旧代码 9 | * 被修饰函数应该返回一个promise,成功时resolve,失败时reject,或返回{succeeded: true/false, ...}格式,通过succeeded字段标识成功失败 10 | * 修饰后的函数会支持arguments[0]中传入success、fail、complete属性,并根据promise结果进行回调 11 | * @param target 12 | * @param funcName 13 | * @param descriptor 14 | * 15 | * @example 16 | * class Demo { 17 | * \@supportWXCallback //自动支持success、fail、complete回调 18 | * async getSystemInfo(){ 19 | * let sysInfo = {};//自定义getSystemInfo实现,比如加入一些缓存策略,添加一些额外字段等 20 | * return { 21 | * succeeded: true, //标示应该触发success回调还是fail回调 22 | * ...sysInfo, //返回成功/失败对应数据 23 | * } 24 | * } 25 | * 26 | * test(){ 27 | * this.getSystemInfo().then(sysInfo=>{ 28 | * //正常以Promise形式使用 29 | * }); 30 | * 31 | * this.getSystemInfo({ 32 | * success(sysInfo){ 33 | * //同时,自动兼容回调形式使用 34 | * } 35 | * }) 36 | * } 37 | * } 38 | */ 39 | export function supportWXCallback(target, funcName, descriptor) { 40 | let oriFunc = descriptor.value; 41 | descriptor.value = function (...args) { 42 | //获取回调函数 43 | let options = args[0] || {}; 44 | let {success, fail, complete} = options; 45 | 46 | //清除回调,避免函数体和修饰器重复处理 47 | delete options.success; 48 | delete options.fail; 49 | delete options.complete; 50 | 51 | //获取执行结果 52 | let fetchRes = oriFunc.apply(this, args); 53 | 54 | //格式检查 55 | if (!(fetchRes instanceof Promise)) { 56 | console.error('[supportWXCallback] 被修饰函数返回结果应为Promise,函数:', funcName, '返回值:', fetchRes); 57 | return fetchRes; 58 | } 59 | 60 | //触发回调 61 | fetchRes.then((...results)=>{ 62 | //恢复回调配置,尽量减小对入参原始对象的影响 63 | options.success = success; 64 | options.fail = fail; 65 | options.complete = complete; 66 | 67 | //判断应该按成功回调还是按失败回调 68 | let succeeded = results[0] && typeof results[0].succeeded === "boolean" ? results[0].succeeded : true; 69 | 70 | //回调 71 | if (succeeded) { 72 | success && success(...results); 73 | } else { 74 | fail && fail(...results); 75 | } 76 | 77 | complete && complete(...results); 78 | }); 79 | fetchRes.catch((e)=>{ 80 | //恢复回调配置,尽量减小对入参原始对象的影响 81 | options.success = success; 82 | options.fail = fail; 83 | options.complete = complete; 84 | 85 | //回调 86 | let res = { 87 | errMsg: (e instanceof Error) ? e.message : 88 | (e && e.errMsg) ? e.errMsg : 89 | (typeof e ==="string") ? e : 90 | 'fail' 91 | }; 92 | fail && fail(res); 93 | complete && complete(res); 94 | }); 95 | 96 | //保留promise调用形式 97 | return fetchRes; 98 | } 99 | } -------------------------------------------------------------------------------- /src/lib/decorator/errSafe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 修饰器,实现各种错误处理 3 | * @module errSafe 4 | */ 5 | 6 | /** 7 | * 捕获async函数中的异常,并进行错误提示 8 | * 函数正常结束时应 return 'ok',return其它文案时将toast指定文案,无返回值或产生异常时将toast默认文案 9 | * @param {string} defaultMsg 默认文案 10 | * @param {number} [duration] 可选,toast持续时长 11 | * 12 | * @example 13 | * class Demo { 14 | * //领取奖励 15 | * \@withErrToast({defaultMsg: '服务异常'}) //领取过程出现异常时,自动捕获异常并toast提示“服务异常”,避免交互无响应 16 | * async acquireReward(){ 17 | * //各种处理.... 18 | * // return acquireRes.errMsg; //使用接口返回的异常信息作为提示文案 19 | * 20 | * return 'ok'; //标示函数正常结束 21 | * } 22 | * } 23 | */ 24 | export function withErrToast({defaultMsg, duration=2000}) { 25 | return function (target, funcName, descriptor) { 26 | let oriFunc = descriptor.value; 27 | descriptor.value = async function () { 28 | let errMsg = ''; 29 | let res = ''; 30 | try { 31 | res = await oriFunc.apply(this, arguments); 32 | if (res != 'ok') 33 | errMsg = typeof res === 'string' && !/^\s*$/.test(res) ? res : defaultMsg; 34 | } catch (e) { 35 | errMsg = defaultMsg; 36 | console.error('caught err with func:',funcName, e.message, e);//真机下不支持打印错误栈,导致e打印出来是个空对象;故先单独打印一次e.message 37 | } 38 | 39 | if (errMsg) { 40 | this.$toast({ 41 | title: errMsg, 42 | type: 'fail', 43 | duration: duration, 44 | }); 45 | } 46 | return res; 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * 捕获函数异常,避免阻断主流程 53 | * 支持同步函数和async函数 54 | * @example 55 | * class Demo { 56 | * //解析banner数据 57 | * \@errSafe //若解析过程出现异常,予以捕获,避免局部数据异常导致整个页面白屏 58 | * parseDataBanner(){ 59 | * 60 | * } 61 | * } 62 | */ 63 | export function errSafe(target, funcName, descriptor) { 64 | let oriFunc = descriptor.value; 65 | descriptor.value = function () { 66 | try { 67 | let res = oriFunc.apply(this, arguments); 68 | 69 | if (res instanceof Promise) { 70 | res.catch((e)=>{ 71 | console.error('[errSafe decorator] caught err with func:',funcName, e.message, e); 72 | }); 73 | } 74 | 75 | return res; 76 | } catch (e) { 77 | console.error('[errSafe decorator] caught err with func:',funcName, e.message, e); //真机下不支持打印错误栈,导致e打印出来是个空对象;故先单独打印一次e.message 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/lib/decorator/mergingStep.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * @ignore 4 | */ 5 | 6 | /** 7 | * 步骤并合修饰器,避免公共步骤并发进行 8 | * 该功能已在免并发修饰器中统一抽象,本文件仅作 兼容旧代码 使用 9 | */ 10 | export {mergingStep} from './noConcurrent'; 11 | -------------------------------------------------------------------------------- /src/lib/decorator/noConcurrent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 修饰器,实现各种免并发处理 3 | * @module noConcurrent 4 | */ 5 | 6 | /** 7 | * 免并发修饰器,在上一次操作结果返回之前,不响应重复操作 8 | * @function 9 | * @example 10 | * class Demo { 11 | * //提交表单 12 | * \@noConcurrent //表单提交期间,无视后续点击,避免用户连续多次点击同一个提交按钮,造成同时提交多份表单 13 | * async onSubmit(){ 14 | * //... 15 | * } 16 | * } 17 | */ 18 | export const noConcurrent = makeNoConcurrent({mode: 'discard'}); 19 | 20 | /** 21 | * 步骤并合修饰器,避免公共步骤重复并发执行 22 | * 将公共步骤单例化:若步骤未在进行,则发起该步骤;若步骤正在进行,则监听并使用其执行结果,而不是重新发起该步骤 23 | * @function 24 | * @example 25 | * class Demo { 26 | * \@mergingStep //该函数可并合执行,短时间内连续多次调用可以共享执行结果 27 | * async login(){ 28 | * //... 29 | * } 30 | * 31 | * test(){ 32 | * //页面内同时发生如下三个请求: 登录-发送接口A、登录-发送接口B、登录-发送接口C 33 | * 34 | * //未使用本修饰器时,网络时序:登录,登录,登录 - 接口A,接口B,接口C, 登录请求将会被发送三次 35 | * //使用本修饰器时,网络时序:登录 - 接口A,接口B,接口C,登录请求只会被发送一次 36 | * } 37 | * } 38 | */ 39 | export const mergingStep = makeNoConcurrent({mode: 'merge'}); 40 | 41 | /** 42 | * 单通道修饰器,使得并发调用逐个顺序执行 43 | * @function 44 | * @example 45 | * class Demo { 46 | * //展示弹窗 47 | * \@singleAisle //并发调用时需依次执行 48 | * async popDialog({msg}){ 49 | * //展示弹窗 50 | * //await 等待弹窗交互 51 | * //关闭弹窗,return 52 | * } 53 | * 54 | * test(){ 55 | * //页面中多处同时调用弹窗函数 56 | * this.popDialog({msg: '提示a'}); 57 | * this.popDialog({msg: '提示b'}); 58 | * this.popDialog({msg: '提示c'}); 59 | * 60 | * //未使用本修饰器时,执行效果:多个弹窗同时展现相互覆盖,用户只看到了“提示c” 61 | * //使用本修饰器时,执行效果:展示“提示a”->用户关闭->展示“提示b”->用户关闭->展示“提示c” 62 | * } 63 | * } 64 | */ 65 | export const singleAisle = makeNoConcurrent({mode: 'wait'}); 66 | 67 | /** 68 | * 免并发修饰器模板 69 | * @param {string} mode 互斥模式: 70 | * discard - 丢弃模式,无视后续并发操作,场景示例:用户连续快速多次点击同一按钮,只执行一次监听函数,无视后续并发点击; 71 | * merge - 合并模式,共享执行结果,场景示例:页面中多处同时触发登录过程,只执行一次登录流程,后续并发请求直接共享该次登录流程执行结果; 72 | * wait - 等待模式,依次顺序执行,场景示例:页面中多处同时调用弹窗函数,一次只展示一个弹窗,用户关闭后再展示第二个,依次顺序展示 73 | * @param {*} discardRes (丢弃模式)被丢弃时函数返回结果 74 | * 75 | * @example 76 | * class Demo { 77 | * \@makeNoConcurrent({ //免并发处理 78 | * mode: 'discard', //并发调用时,无视后续调用 79 | * discardRes: { //被无视时返回的指定错误信息 80 | * succeeded: false, 81 | * errMsg: 'discarded: invoke too frequently' 82 | * } 83 | * }) 84 | * async func(){ 85 | * 86 | * } 87 | * } 88 | */ 89 | export function makeNoConcurrent({mode, discardRes}) { 90 | return _noConcurrentTplt.bind(null, {mutexStore:'_noConCurrentLocks', mode, discardRes}); 91 | } 92 | 93 | /** 94 | * 多函数免并发,具有相同互斥标识的函数不会并发执行 95 | * @param {Object} namespace 互斥函数间共享的一个全局变量,用于存储并发信息 96 | * @param {string} mutexId 互斥标识,具有相同标识的函数不会并发执行 97 | * @param {string} mode 互斥模式: 98 | * discard - 丢弃模式(默认),无视后续并发操作,场景示例:用户连续快速多次点击同一按钮,只执行一次监听函数,无视后续并发点击; 99 | * merge - 合并模式,共享执行结果,场景示例:页面中多处同时触发登录过程,只执行一次登录流程,后续并发请求直接共享该次登录流程执行结果; 100 | * wait - 等待模式,依次顺序执行,场景示例:页面中多处同时调用弹窗函数,一次只展示一个弹窗,用户关闭后再展示第二个,依次顺序展示 101 | * @param {*} discardRes (丢弃模式)被丢弃时函数返回结果 102 | * 103 | * @example 104 | * import {makeMutex} from 'fancy-mini/lib/decorators'; 105 | * 106 | * let globalStore = {}; 107 | * 108 | * class Navigator { 109 | * \@makeMutex({namespace:globalStore, mutexId:'navigate'}) //避免跳转相关函数并发执行 110 | * static async navigateTo(route){...} 111 | * 112 | * \@makeMutex({namespace:globalStore, mutexId:'navigate'}) //避免跳转相关函数并发执行 113 | * static async navigateToMiniProgram(route){...} 114 | * } 115 | */ 116 | export function makeMutex({namespace, mutexId, mode, discardRes}) { 117 | if (typeof namespace !== "object") { 118 | console.error('[makeNoConcurrent] bad parameters, namespace shall be a global object shared by all mutex funcs, got:', namespace); 119 | return function () {} 120 | } 121 | 122 | return _noConcurrentTplt.bind(null, {namespace, mutexStore:'_noConCurrentLocksNS', mutexId, mode, discardRes}); 123 | } 124 | 125 | 126 | /** 127 | * 免并发修饰器通用模板 128 | * @param {Object} namespace 互斥函数间共享的一个全局变量,用于存储并发信息,多函数互斥时需提供;单函数自身免并发无需提供,以本地私有变量实现 129 | * @param {string} mutexStore 在namespace中占据一个变量名用于状态存储 130 | * @param {string} mutexId 互斥标识,具有相同标识的函数不会并发执行,缺省值:函数名 131 | * @param {string} mode 互斥模式: 132 | * discard - 丢弃模式(默认),无视后续并发操作,场景示例:用户连续快速多次点击同一按钮,只执行一次监听函数,无视后续并发点击; 133 | * merge - 合并模式,共享执行结果,场景示例:页面中多处同时触发登录过程,只执行一次登录流程,各并发请求直接共享该次登录流程执行结果; 134 | * wait - 等待模式,依次顺序执行,场景示例:页面中多处同时调用弹窗函数,一次只展示一个弹窗,用户关闭后再展示第二个,依次顺序展示 135 | * @param {*} discardRes (丢弃模式)被丢弃时函数返回结果 136 | * @param target 137 | * @param funcName 138 | * @param descriptor 139 | * @private 140 | */ 141 | function _noConcurrentTplt({namespace={}, mutexStore='_noConCurrentLocks', mutexId, mode='discard', discardRes=undefined}, target, funcName, descriptor) { 142 | namespace[mutexStore] = namespace[mutexStore] || {}; 143 | mutexId = mutexId || funcName; 144 | 145 | namespace[mutexStore][mutexId] = namespace[mutexStore][mutexId] || { 146 | running: false, //是否有实例正在执行 147 | /* 148 | * 监听队列,当前函数实例执行完毕时调用 149 | * Array { 150 | * block: false, //是否需要继续保持免并发状态 151 | * handler: null, //处理函数,入参:刚结束的函数实例执行结果 152 | * } 153 | */ 154 | listeners: [], 155 | }; 156 | 157 | let oriFunc = descriptor.value; 158 | descriptor.value = async function () { 159 | let statusControl = namespace[mutexStore][mutexId]; 160 | //免并发处理 161 | if (statusControl.running) { //上一次操作尚未结束 162 | switch (mode) { 163 | case 'discard': //丢弃模式,无视本次调用 164 | return discardRes; 165 | case 'merge': //合并模式,直接使用上次操作结果作为本次调用结果返回 166 | let lastRes = await new Promise((resolve,reject)=>{ 167 | statusControl.listeners.push({ 168 | block: false, //无需继续免并发状态 169 | handler: resolve, 170 | }); 171 | }); 172 | return lastRes; 173 | case 'wait': //等待模式,等待上次操作结束后再开始本次操作 174 | await new Promise((resolve,reject)=>{ 175 | statusControl.listeners.push({ 176 | block: true, //继续保持免并发状态 177 | handler: resolve, 178 | }); 179 | }); 180 | break; 181 | default: 182 | console.error('[_noConcurrentTplt] unknown mode:', mode); 183 | return; 184 | } 185 | } 186 | 187 | //释放并发锁处理 188 | let handleRunFinish = async function (res) { 189 | while (statusControl.listeners.length > 0){ 190 | let listener = statusControl.listeners[0]; 191 | statusControl.listeners = statusControl.listeners.slice(1); 192 | 193 | listener.handler(res); 194 | if (listener.block) //阻塞性监听,则继续保持免并发状态;本轮处理结束,后续监听处理及互斥锁释放过程由监听函数接管 195 | return; 196 | //否则继续进行后续监听处理 197 | } 198 | 199 | statusControl.running = false; //监听函数全部处理完毕,则释放并发锁 200 | return; 201 | }; 202 | 203 | //操作开始 204 | statusControl.running = true; 205 | let res = oriFunc.apply(this, arguments); 206 | 207 | if (res instanceof Promise) 208 | res.then(()=>{ 209 | handleRunFinish(res); 210 | }).catch((e)=> { 211 | console.error(funcName, e); 212 | handleRunFinish(res); 213 | }); 214 | else { 215 | console.error('noConcurrent decorator shall be used with async function, yet got sync usage:', funcName); 216 | handleRunFinish(res); 217 | } 218 | 219 | return res; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/lib/decorator/typeCheck.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 检测工具 3 | * @module typeCheck 4 | */ 5 | const _toString = Object.prototype.toString; 6 | // 检测是否为纯粹的对象 7 | const _isPlainObject = function (obj) { 8 | return _toString.call(obj) === '[object Object]' 9 | } 10 | // 检测是否为正则 11 | const _isRegExp = function (v) { 12 | return _toString.call(v) === '[object RegExp]' 13 | } 14 | 15 | /** 16 | * @description 类型检测函数 17 | * 用于检测类型action 18 | * @param {Array} checked 被检测数组 19 | * @param {Array} checker 检测数组 20 | * @return {Boolean} 是否通过检测 21 | * @private 22 | */ 23 | const _check = function (checked,checker) { 24 | check: 25 | for(let i = 0; i < checked.length; i++) { 26 | if(/(any)/ig.test(checker[i])) 27 | continue check; 28 | if(_isPlainObject(checked[i]) && /(object)/ig.test(checker[i])) 29 | continue check; 30 | if(_isRegExp(checked[i]) && /(regexp)/ig.test(checker[i])) 31 | continue check; 32 | if(Array.isArray(checked[i]) && /(array)/ig.test(checker[i])) 33 | continue check; 34 | let type = typeof checked[i]; 35 | let checkReg = new RegExp(type,'ig') 36 | if(!checkReg.test(checker[i])) { 37 | console.error(checked[i] + 'is not a ' + checker[i]); 38 | return false; 39 | } 40 | } 41 | return true; 42 | } 43 | /** 44 | * 检测类型 45 | * 1.用于校检函数参数的类型,如果类型错误,会打印错误并不再执行该函数; 46 | * 2.类型检测忽略大小写,如string和String都可以识别为字符串类型; 47 | * 3.增加any类型,表示任何类型均可检测通过; 48 | * 4.可检测多个类型,如 "number array",两者均可检测通过。正则检测忽略连接符; 49 | * @param {Array} args 参数类型 50 | * @example 51 | * import { typeCheck } from 'fancy-mini/lib/decorators' 52 | * 53 | * \@typeCheck('array', 'object', 'string|number') 54 | * function multiple(list, item, maxNum) { 55 | * if (item.selected || selectedNum < maxNum) { 56 | * // do something 57 | * } else { 58 | * //... 59 | * } 60 | * } 61 | * 62 | * multiple(['paramValues'], { value: 3 }, 10}) 63 | */ 64 | export function typeCheck() { 65 | const checker = Array.prototype.slice.apply(arguments); 66 | return function (target, funcName, descriptor) { 67 | let oriFunc = descriptor.value; 68 | descriptor.value = function () { 69 | let checked = Array.prototype.slice.apply(arguments); 70 | let result = undefined; 71 | if(_check(checked,checker)){ 72 | result = oriFunc.call(this,...arguments); 73 | } 74 | return result; 75 | } 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/lib/decorators.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 集中导出各修饰器,便于使用修饰器较多时快捷引用 3 | * @module decorators 4 | */ 5 | 6 | export * from './decorator/noConcurrent'; 7 | export * from './decorator/errSafe'; 8 | export * from './decorator/compatible'; 9 | export * from './decorator/typeCheck'; 10 | -------------------------------------------------------------------------------- /src/lib/globalEvents.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局事件 3 | * 对小程序全局事件进行集中定义和管理 4 | * @module globalEvents 5 | */ 6 | 7 | import EventHub from './EventHub'; 8 | 9 | /** 10 | * 登录授权相关事件 11 | * | eventType | 语义 | 参数 | 12 | * | --- | --- | --- | 13 | * | userAuthFinish | 登录授权交互结束 | 交互结果,类型:{@link BaseLogin~UserAuthRes} | 14 | * @type {EventHub} 15 | */ 16 | const authEvents = new EventHub({ 17 | validEvents: [ 18 | 'userAuthFinish', //授权过程结束,授权入口页面触发 19 | ] 20 | }); 21 | 22 | export { 23 | authEvents 24 | } -------------------------------------------------------------------------------- /src/lib/handleStr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GBK字符串处理工具 3 | * @module handleStr 4 | */ 5 | 6 | /** 7 | * GBK长度计算函数 8 | * @param {string} str 源字符 9 | * @return {number} 字符串长度 10 | */ 11 | export function strLength(str) { 12 | if(!str) return 0 13 | var realLength = 0, len = str.length, charCode = -1; 14 | for (var i = 0; i < len; i++) { 15 | charCode = str.charCodeAt(i); 16 | if (charCode >= 0 && charCode <= 128) realLength += 1; 17 | else realLength += 2; 18 | } 19 | return realLength; 20 | } 21 | 22 | /** 23 | * GBK字符剪切函数 24 | * @param {string} str 源字符 25 | * @param {number} len 截取字符的GBK编码长度 26 | * @return {string} 截取的字符 27 | */ 28 | export function cutstr(str, len) { 29 | var str_length = 0; 30 | var str_len = 0; 31 | var str_cut = new String(); 32 | str_len = str.length; 33 | for (var i = 0; i < str_len; i++) { 34 | let a = str.charAt(i); 35 | str_length++; 36 | if (escape(a).length > 4) { 37 | //中文字符的长度经编码之后大于4 38 | str_length++; 39 | } 40 | str_cut = str_cut.concat(a); 41 | if (str_length >= len) { 42 | // str_cut = str_cut.concat("..."); 43 | return str_cut; 44 | } 45 | } 46 | //如果给定字符串小于指定长度,则返回源字符串; 47 | if (str_length < len) { 48 | return str; 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/lib/login/auth/BaseAuth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 鉴权模块基类 3 | * 负责根据用户提供的信息,完成校验过程,并返回对应的登录数据 4 | */ 5 | class BaseAuth { 6 | /** 7 | * 静默登录 8 | * 可以在用户无感知的情况下后台悄悄完成的登录过程 9 | * @async 10 | * @param {Object} loginOptions 登录函数调用参数,参见{@link BaseLogin#login} 11 | * @param {Object} configOptions 登录模块配置参数,参见{@link BaseLogin#config} 12 | * @return {BaseAuth~LoginRes} 13 | */ 14 | silentLogin({loginOptions, configOptions}){ 15 | return { 16 | succeeded: false, //是否成功 17 | errMsg: '该授权方式未实现静默登录', //详细错误信息,调试用 18 | toastMsg: '该授权方式未实现静默登录', //(若有)错误信息话术,展示给用户 19 | userInfo: {}, //(成功时)用户信息 20 | expireTime: -1, //(成功时)过期时间,绝对毫秒数,-1表示长期有效 21 | anonymousInfo: null, //(不管成功失败)匿名信息,登录成功前使用的临时标识,成功后继续关联 22 | } 23 | } 24 | 25 | /** 26 | * 进行授权登录之前的准备工作 27 | * 时序:beforeAuthLogin -> 用户交互,同意授权 -> authLogin 28 | * @async 29 | * @param {Object} loginOptions 登录函数调用参数,参见{@link BaseLogin#login} 30 | * @param {Object} configOptions 登录模块配置参数,参见{@link BaseLogin#config} 31 | * @return {*} 需要传递给authLogin的数据 32 | */ 33 | beforeAuthLogin({loginOptions, configOptions}){}; 34 | 35 | /** 36 | * 授权登录 37 | * 需要用户配合点击授权按钮/输入表单等才能完成的登录过程 38 | * @async 39 | * @param {Object} loginOptions 登录函数调用参数,参见{@link BaseLogin#login} 40 | * @param {Object} configOptions 登录模块配置参数,参见{@link BaseLogin#config} 41 | * @param {*} [beforeRes] beforeAuthLogin钩子执行结果 42 | * @param {Object} authData 登录界面交互结果 43 | * @return {BaseAuth~LoginRes} 44 | */ 45 | authLogin({loginOptions, configOptions, beforeRes, authData}){ 46 | return { 47 | succeeded: false, //是否成功 48 | errMsg: '该授权方式未实现授权登录', //详细错误信息,调试用 49 | toastMsg: '该授权方式未实现授权登录', //(若有)错误信息话术,展示给用户 50 | userInfo: {}, //(成功时)用户信息 51 | expireTime: -1, //(成功时)过期时间,绝对毫秒数,-1表示长期有效 52 | anonymousInfo: null, //(不管成功失败)匿名信息,登录成功前使用的临时标识,成功后继续关联 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * @typedef {Object} BaseAuth~LoginRes 鉴权模块登录结果 59 | * @property {boolean} succeeded 是否成功 60 | * @property {string} errMsg 详细错误信息,调试用 61 | * @property {string} [toastMsg] 错误信息话术,向用户提示用 62 | * @property {Object} userInfo (成功时)用户信息 63 | * @property {number} expireTime (成功时)过期时间,绝对毫秒数,-1表示长期有效 64 | * @property {Object} [anonymousInfo] (不管成功失败)匿名信息,登录成功前使用的临时标识,成功后继续关联 65 | */ 66 | 67 | export default BaseAuth; -------------------------------------------------------------------------------- /src/lib/login/auth/WechatAuth.js: -------------------------------------------------------------------------------- 1 | import BaseAuth from './BaseAuth'; 2 | import {wxPromise, wxResolve} from '../../wxPromise'; 3 | 4 | /** 5 | * 微信登录鉴权模块 6 | * 使用微信登录时,负责根据微信提供的信息,完成校验过程,并返回对应的登录数据 7 | * @extends BaseAuth 8 | */ 9 | class WechatAuth extends BaseAuth{ 10 | async silentLogin({loginOptions, configOptions}){ 11 | let wxLoginRes = await this.wxLogin(); 12 | return this.loginByWxSilent({wxLoginRes, loginOptions, configOptions}); 13 | } 14 | 15 | async beforeAuthLogin({loginOptions, configOptions}){ 16 | let wxLoginRes = await this.wxLogin(); 17 | return {wxLoginRes}; 18 | } 19 | 20 | async authLogin({loginOptions, configOptions, beforeRes, authData}){ 21 | return this.loginByWxAuth({ 22 | wxLoginRes: beforeRes.wxLoginRes, 23 | authData, 24 | loginOptions, 25 | configOptions, 26 | }); 27 | } 28 | 29 | /** 30 | * 微信登录:调用微信相关API,获取微信登录态 31 | * @return {WechatAuth~WxLoginRes} wx.login执行结果 32 | */ 33 | async wxLogin(){ 34 | return await wxResolve.login(); 35 | } 36 | 37 | /** 38 | * 微信静默登录 39 | * 根据wxLoginRes.code调后端接口解密获得用户openid,根据openid查询用户表 40 | * 若为老用户,则能成功从数据库中找到匹配项,从而悄悄完成登录过程 41 | * 若为新用户,则静默登录失败 42 | * @param {WechatAuth~WxLoginRes} wxLoginRes wx.login执行结果 43 | * @param loginOptions 登录函数调用参数,参见{@link BaseLogin#login} 44 | * @param configOptions 登录模块配置参数,参见{@link BaseLogin#config} 45 | * @return {BaseAuth~LoginRes} 46 | */ 47 | async loginByWxSilent({wxLoginRes, loginOptions, configOptions}){ 48 | //根据wxLoginRes.code调后端接口获得用户信息 49 | return { 50 | succeeded: false, 51 | errMsg: '请覆盖loginByWxSilent函数完成查询用户信息功能', 52 | toastMsg: '请覆盖loginByWxSilent函数完成查询用户信息功能', 53 | userInfo: {}, 54 | expireTime: -1, 55 | anonymousInfo: null, 56 | } 57 | } 58 | 59 | /** 60 | * 微信授权登录 61 | * 根据用户同意授权后从微信处拿到的信息,完成登录过程 62 | * @param {WechatAuth~WxLoginRes} wxLoginRes wx.login执行结果 63 | * @param {Object} authData 登录界面交互结果,格式同wx.getUserInfo返回结果 64 | * @param loginOptions 登录函数调用参数,参见{@link BaseLogin#login} 65 | * @param configOptions 登录模块配置参数,参见{@link BaseLogin#config} 66 | * @return {BaseAuth~LoginRes} 67 | */ 68 | async loginByWxAuth({wxLoginRes, authData, loginOptions, configOptions}){ 69 | //根据wxLoginRes.code和authData调后端接口获得用户信息 70 | return { 71 | succeeded: false, 72 | errMsg: '请覆盖loginByWxAuth函数完成注册/查询用户信息功能', 73 | toastMsg: '请覆盖loginByWxAuth函数完成查询用户信息功能', 74 | userInfo: {}, 75 | expireTime: -1, 76 | anonymousInfo: null, 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * @typedef {Object} WechatAuth~WxLoginRes wx.login执行结果 83 | * @property {boolean} succeeded 是否成功 84 | * @property {string} code wx.login接口返回的code 85 | */ 86 | 87 | export default WechatAuth; -------------------------------------------------------------------------------- /src/lib/navigate/History.js: -------------------------------------------------------------------------------- 1 | import {deepClone} from '../operationKit'; 2 | 3 | /** 4 | * 历史记录 5 | * 由于小程序只支持最多10级页面,但需求上希望维护更长的历史栈,故自行维护完整历史记录 6 | */ 7 | class History { 8 | _routes = []; //历史栈 9 | _correctLevel = 8; //自行维护的逻辑历史栈与系统实际历史栈的前若干项应当始终保持一致 10 | 11 | /** 12 | * 构造函数 13 | * @param {Array} routes 初始路由栈 14 | */ 15 | constructor({routes=[]}){ 16 | this._routes = routes.slice(0); 17 | } 18 | 19 | /** 20 | * 配置 21 | * @param {number} correctLevel 自行维护的逻辑历史栈与系统实际历史栈的前多少项应当始终保持一致,用于校正代码疏漏和系统交互造成的逻辑历史栈失真 22 | */ 23 | config({correctLevel=8}={}){ 24 | this._correctLevel = correctLevel; 25 | } 26 | 27 | /** 28 | * wepy框架存在单实例问题,同一路径页面被打开两次时,其数据会相互影响,,如:详情页A - 详情页B - 返回A,点击查看大图 - B的图片(而不是A的图片) 29 | * 故需检查历史页面实例是否已被覆盖,若已被覆盖,则返回时需手动刷新 30 | * @private 31 | */ 32 | _checkTainted(){ 33 | for (let i=0; i isSamePage(fullUrl(page.route||page.__route__, page.options), this._routes[idx].url)); 88 | if (!remainCorrect) { 89 | this._routes = curPages.map(page=>Object.assign(resetRoute({}), {url: fullUrl(page.route||page.__route__, page.options)})); 90 | this._checkTainted(); 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * 保存页面数据 97 | * @param {number} idx 路由栈下标 98 | * @param wxPage 对应的原生页面实例 99 | */ 100 | savePage(idx, wxPage){ 101 | this._routes[idx].wxPage = deepClone(wxPage); 102 | } 103 | 104 | /** 105 | * 获取路由 106 | * @param {number} idx 路由栈下标 107 | * @return {History~Route} 对应的页面 108 | */ 109 | getRoute(idx){ 110 | if (!(idx>=0 && idx} 139 | */ 140 | get routes(){ 141 | this.doCorrection(); 142 | return this._routes.map((val, idx)=>this.getRoute(idx)); 143 | } 144 | 145 | /** 146 | * 自行维护的逻辑历史栈与系统实际历史栈应当始终保持一致的层数 147 | * @return {number} 148 | */ 149 | get correctLevel(){ 150 | return this._correctLevel; 151 | } 152 | } 153 | 154 | /** 155 | * 将路径和参数拼成完整url 156 | * @ignore 157 | * @param path 路径 158 | * @param options 参数 159 | * @return {string} url 160 | */ 161 | function fullUrl(path='', options={}) { 162 | let url = path[0]==='/' ? path : '/'+path; 163 | let params = []; 164 | for (let name in options) { 165 | params.push(name+'='+options[name]); 166 | } 167 | url += params.length > 0 ? '?' : ''; 168 | url += params.join('&'); 169 | return url; 170 | } 171 | 172 | /** 173 | * 判断两个url是否为同一个页面的实例 174 | * @ignore 175 | * @param url1 176 | * @param url2 177 | * @return {boolean} 178 | */ 179 | function isSamePage(url1='', url2='') { 180 | return url1.split('?')[0] === url2.split('?')[0]; 181 | } 182 | 183 | /** 184 | * 重置路由对象 185 | * @ignore 186 | * @param route 187 | */ 188 | function resetRoute(route={}) { 189 | route.url = ''; 190 | route.tainted = false; 191 | return route; 192 | } 193 | 194 | /** 195 | * @typedef {object} History~Route 路由对象 196 | * @property {string} url 页面完整路径,e.g. '/pages/index/index?param1=1' 197 | * @property {boolean} [tainted] 实例数据是否已被污染,详见[实例覆盖自动恢复功能]{@link https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.2-navigate.html#taintedRestore} 198 | * @property [wxPage] 对应的微信原生页面实例,恢复页面数据时使用,使用场景:1.[实例覆盖自动恢复功能]{@link https://zhuanzhuanfe.github.io/fancy-mini/tutorial-2.2-navigate.html#taintedRestore} 2.层级过深时,新开页面会替换前一页面,导致前一页面数据丢失,返回时需予以恢复 199 | */ 200 | 201 | export default History; -------------------------------------------------------------------------------- /src/lib/request/plugin/BasePlugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 请求管理-插件基类 3 | * 用于在网络请求前后添加自定义扩展逻辑,详见{@tutorial 2.3-request} 4 | */ 5 | class BasePlugin { 6 | /** 7 | * 插件名称,主要用于打印日志和调试,便于追溯操作源 8 | * @type {string} 9 | */ 10 | pluginName = ''; 11 | /** 12 | * 请求管理器 13 | * @type {Requester} 14 | */ 15 | requester = null; 16 | 17 | /** 18 | * 构造函数 19 | * @param {string} pluginName 插件名称,主要用于打印日志和调试,便于追溯操作源 20 | */ 21 | constructor({pluginName}){ 22 | this.pluginName = pluginName || this.pluginName; 23 | 24 | if (!this.pluginName) { 25 | console.warn('[requester plugin] 建议为插件设置一个pluginName,便于出现问题时排查追溯:',this); 26 | } 27 | } 28 | 29 | /** 30 | * 钩子函数,插件被挂载到requester对象上时触发 31 | * @param {Requester} requester 被挂载到的requester对象 32 | */ 33 | mount({requester}){ 34 | this.requester = requester; 35 | } 36 | 37 | /** 38 | * 钩子函数,发请求之前调用,同步 39 | * 不会等待异步操作返回,如需等待异步逻辑,请改用{@link BasePlugin#beforeRequestAsync} 40 | * @param {Requester~ReqOptions} reqOptions 请求参数 41 | * @param {object} thisIssuer 发起接口请求的this对象 42 | * @return {undefined | Requester~BeforeRequestRes} 期望的后续处理,undefined表示继续执行默认流程 43 | */ 44 | beforeRequest({reqOptions, thisIssuer}){}; 45 | 46 | /** 47 | * 钩子函数,发请求之前调用,异步 48 | * 会等待async函数resolve,若无异步逻辑,建议使用{@link BasePlugin#beforeRequest} 49 | * @async 50 | * @param {Requester~ReqOptions} reqOptions 请求参数 51 | * @param {object} thisIssuer 发起接口请求的this对象 52 | * @return {undefined | Requester~BeforeRequestRes} 期望的后续处理,undefined表示继续执行默认流程 53 | */ 54 | beforeRequestAsync({reqOptions, thisIssuer}){}; 55 | 56 | /** 57 | * 钩子函数,请求返回之后调用,同步 58 | * 不会等待异步操作返回,如需等待异步逻辑,请改用{@link BasePlugin#afterRequestAsync} 59 | * @param {Requester~ReqOptions} reqOptions 请求参数 60 | * @param {object} thisIssuer 发起接口请求的this对象 61 | * @param {Requester~ReqRes} reqRes 请求结果 62 | * @return {undefined | Requester~AfterRequestRes} 期望的后续处理,undefined表示继续执行默认流程 63 | */ 64 | afterRequest({reqOptions, thisIssuer, reqRes}){}; 65 | 66 | /** 67 | * 钩子函数,请求返回之后调用,异步 68 | * 会等待async函数resolve,若无异步逻辑,建议使用{@link BasePlugin#afterRequest} 69 | * @async 70 | * @param {Requester~ReqOptions} reqOptions 请求参数 71 | * @param {object} thisIssuer 发起接口请求的this对象 72 | * @param {Requester~ReqRes} reqRes 请求结果 73 | * @return {undefined | Requester~AfterRequestRes} 期望的后续处理,undefined表示继续执行默认流程 74 | */ 75 | afterRequestAsync({reqOptions, thisIssuer, reqRes}){}; 76 | } 77 | 78 | export default BasePlugin; -------------------------------------------------------------------------------- /src/lib/request/plugin/CloudFuncPlugin.js: -------------------------------------------------------------------------------- 1 | import BasePlugin from './BasePlugin'; 2 | import Cookie from '../../Cookie'; 3 | 4 | /** 5 | * 请求管理-云函数插件 6 | * 将云函数封装成http接口形式使用,便于: 7 | * 1. 使用requester提供的各种逻辑扩展能力 8 | * 2. 后续在云函数和后端服务器之间进行各种业务的相互迁移 9 | * 10 | * 详见{@tutorial 2.3-request} 11 | * @extends BasePlugin 12 | */ 13 | class CloudFuncPlugin extends BasePlugin{ 14 | _fakeDomain = ''; 15 | _fakeRootPath = ''; 16 | 17 | /** 18 | * 构造函数 19 | * @param {string} [pluginName='CloudFuncPlugin'] 插件名称 20 | * @param {string} [fakeDomain='cloud.function'] 虚拟域名 21 | * @param {string} [fakeRootPath='/'] 虚拟根路径 22 | * @example 使用默认配置 23 | * let requester = new Requester({ 24 | * plugins: [ 25 | * new CloudFuncPlugin() //使用默认配置 26 | * ] 27 | * }); 28 | * 29 | * //则调用接口 30 | * let res = await requester.request({ 31 | * url: 'https://cloud.function/xxx?a=1&b=2' 32 | * }); 33 | * //等价于调用云函数 34 | * let res = await wx.cloud.callFunction({ 35 | * name: 'xxx', 36 | * data: { 37 | * a: "1", 38 | * b: "2", 39 | * } 40 | * }) 41 | * 42 | * @example 自定义虚拟域名和虚拟路径 43 | * let requester = new Requester({ 44 | * plugins: [ 45 | * new CloudFuncPlugin({ //自定义虚拟域名和虚拟路径 46 | * fakeDomain: 'fancy.com', 47 | * fakeRootPath: '/demos/cloud/' 48 | * }) 49 | * ] 50 | * }); 51 | * 52 | * //则调用指定虚拟域名虚拟路径下的接口 53 | * let res = await requester.request({ 54 | * url: 'https://fancy.com/demos/cloud/xxx?a=1&b=2' 55 | * }); 56 | * //等价于调用对应云函数 57 | * let res = await wx.cloud.callFunction({ 58 | * name: 'xxx', 59 | * data: { 60 | * a: "1", 61 | * b: "2", 62 | * } 63 | * }) 64 | * 65 | * @example 云函数实现 66 | * exports.main = async (event, context) => { 67 | //云函数格式约定: 68 | 69 | let {a, b} = event; //调用方传入的参数可以通过event获取 70 | a = Number(a); //参数类型统一为string 71 | b = Number(b); 72 | 73 | //此外,还会额外拼入一些http相关参数 74 | let {reqHeader} = event; //http请求header信息 75 | console.log(reqHeader.cookie); //header中的cookie字段会解析成对象格式,形如:{uid: 'xxx'} 76 | 77 | //处理结果正常返回 78 | let result = { sum: a+b }; 79 | 80 | //此外,有一些保留字段可以用于设置http相关参数 81 | result.resStatusCode = 200; //设置http状态码 82 | result.resHeader = {}; //设置http返回结果中的header信息 83 | result.resHeader['Set-Cookie'] = [ //同一header有多条记录时,以数组形式设置 84 | 'uid=xxx;expires=111', 85 | 'sessionKey=yyy;expires=222' 86 | ] 87 | 88 | return result; 89 | } 90 | 91 | */ 92 | constructor({pluginName='CloudFuncPlugin', fakeDomain='cloud.function', fakeRootPath='/'}={}){ 93 | super({ 94 | pluginName 95 | }); 96 | 97 | //参数处理 98 | //在路径前后补充斜杠 99 | fakeRootPath = /\/$/.test(fakeRootPath) ? fakeRootPath : fakeRootPath+'/'; 100 | fakeRootPath = /^\//.test(fakeRootPath) ? fakeRootPath : '/'+fakeRootPath; 101 | 102 | //参数配置 103 | this._fakeDomain = fakeDomain; 104 | this._fakeRootPath = fakeRootPath; 105 | } 106 | 107 | async beforeRequestAsync({reqOptions}){ 108 | //将http请求解析成云函数调用 109 | let {hit, funcName, funcParams} = this._parseReq({reqOptions}); 110 | if (!hit) //不是云函数调用,不作处理 111 | return; 112 | 113 | //调用云函数 114 | let cloudRes = await this._execCloudFunc({funcName, funcParams}); 115 | 116 | //将云函数返回结果解析成http请求结果 117 | let reqRes = this._parseRes({cloudRes}); 118 | 119 | //返回结果 120 | return { 121 | action: 'feed', 122 | feedRes: reqRes, 123 | } 124 | } 125 | 126 | /** 127 | * 解析请求,将http请求解析成云函数调用 128 | * @param {Requester~ReqOptions} reqOptions 129 | * @return {{hit: boolean, funcName: string, funcParams: object}} 解析结果,格式形如:{ 130 | * hit: true, //是否为云函数调用 131 | * funcName: 'xxx', //云函数函数名 132 | * funcParams: { //云函数入参 133 | * a: "1", 134 | * b: "2", 135 | * reqHeader: {} 136 | * } 137 | * } 138 | * @protected 139 | */ 140 | _parseReq({reqOptions}){ 141 | //判断是否云函数调用 142 | if (reqOptions.url.indexOf(`https://${this._fakeDomain}${this._fakeRootPath}`) !== 0) 143 | return {hit: false}; 144 | 145 | //参数解析 146 | let [path, queryStr=''] = reqOptions.url.split('?'); 147 | let queryObj = {}; 148 | queryStr.split('&').forEach(paramStr=>{ 149 | let [name, value] = paramStr.split('='); 150 | if (name && value!==undefined) 151 | queryObj[name] = value; 152 | }); 153 | 154 | let funcName = path.substring(`https://${this._fakeDomain}${this._fakeRootPath}`.length); 155 | let funcParams = Object.assign(queryObj, reqOptions.data); 156 | 157 | //参数处理 158 | 159 | // 调用网络请求时,不管参数原本是什么类型,传到后端时都会被转为string 160 | // 因而,在使用网络请求时调用方很可能不会特别关注实际类型,也不会主动做类型转换 161 | // 故,此处也统一进行类型规整,避免产生类型歧义 162 | for (let name in funcParams) 163 | funcParams[name] = String(funcParams[name]); 164 | 165 | //header解析 166 | let reqHeader = Object.assign({}, reqOptions.header); 167 | reqHeader.cookie = Cookie.cookieStrToObj(reqHeader.cookie); 168 | 169 | //header写入 170 | funcParams.reqHeader = reqHeader; 171 | 172 | //返回结果 173 | return { 174 | hit: true, 175 | funcName, 176 | funcParams, 177 | } 178 | } 179 | 180 | /** 181 | * 执行云函数 182 | * @param {string} funcName 函数名 183 | * @param {object} funcParams 函数入参 184 | * @return {{succeeded: boolean, result: object}} 云函数执行结果 185 | * @protected 186 | */ 187 | async _execCloudFunc({funcName, funcParams}){ 188 | try { 189 | let cloudRes = await wx.cloud.callFunction({ 190 | name: funcName, 191 | data: funcParams 192 | }); 193 | 194 | return { 195 | succeeded: true, 196 | ...cloudRes, 197 | }; 198 | } catch (e) { 199 | console.error('[CloudFuncPlugin] failed to exec cloud func:',funcName, 'err:', e); 200 | return { 201 | succeeded: false, 202 | errMsg: 'failed to exec cloud func:'+funcName 203 | } 204 | } 205 | } 206 | 207 | /** 208 | * 解析返回结果,将云函数返回结果解析成http请求结果 209 | * @param {{succeeded: boolean, result: object}} cloudRes 云函数执行结果 210 | * @return {Requester~ReqRes} 对应的http请求结果 211 | * @protected 212 | */ 213 | _parseRes({cloudRes}){ 214 | //云函数执行失败 215 | if (!cloudRes.succeeded) { 216 | return { 217 | succeeded: false, 218 | errMsg: cloudRes.errMsg, 219 | } 220 | } 221 | 222 | //云函数执行成功 223 | 224 | //解析返回结果 225 | let result = cloudRes.result; 226 | 227 | //约定的保留字段,用于进行http相关设置 228 | let httpFieldMap = { //key: http字段 value:对应的保留字段 229 | statusCode: 'resStatusCode', //状态码 230 | header: 'resHeader', //头部 231 | }; 232 | 233 | //提取保留字段,并从结果中删除 234 | let httpField = {}; 235 | for (let [httpName, cloudName] of Object.entries(httpFieldMap)) { 236 | httpField[httpName] = result[cloudName]; 237 | delete result[cloudName]; 238 | } 239 | 240 | //http字段处理 241 | //状态码 242 | httpField.statusCode = httpField.statusCode ? Number(httpField.statusCode) : 200; 243 | 244 | //头部 245 | httpField.header = httpField.header || {}; 246 | for (let name in httpField.header) 247 | httpField.header[name.toLowerCase()] = httpField.header[name]; 248 | 249 | //cookie(返回头部中有'set-cookie'时,wx.request会额外返回一个cookies字段,此处予以相同处理) 250 | let cookies = Array.isArray(httpField.header['set-cookie']) ? httpField.header['set-cookie'] : [httpField.header['set-cookie']]; 251 | cookies = cookies.filter(setStr=>!!setStr); 252 | 253 | //返回结果 254 | return { 255 | succeeded: true, 256 | errMsg: 'ok', 257 | data: result, 258 | statusCode: httpField.statusCode, 259 | header: httpField.header, 260 | cookies, 261 | }; 262 | } 263 | } 264 | 265 | export default CloudFuncPlugin; -------------------------------------------------------------------------------- /src/lib/request/plugin/CookiePlugin.js: -------------------------------------------------------------------------------- 1 | import BasePlugin from './BasePlugin'; 2 | import Cookie from '../../Cookie'; 3 | 4 | /** 5 | * 请求管理-cookie插件 6 | * 在请求前后植入cookie逻辑,详见{@tutorial 2.4-cookie} 7 | * @extends BasePlugin 8 | */ 9 | class CookiePlugin extends BasePlugin{ 10 | /** 11 | * cookie管理器 12 | * @type {Cookie} 13 | */ 14 | cookie = null; 15 | 16 | /** 17 | * 构造函数 18 | * @param {string} [pluginName='CookiePlugin'] 插件名称,主要用于打印日志和调试,便于追溯操作源 19 | * @param {Cookie} cookie cookie管理器 20 | */ 21 | constructor({pluginName, cookie}){ 22 | super({ 23 | pluginName: pluginName || 'CookiePlugin', 24 | }); 25 | this.cookie = cookie; 26 | } 27 | 28 | /** 29 | * 在请求头中注入cookie信息 30 | * @param {Requester~ReqOptions} reqOptions 31 | * @param {Requester} requester 32 | */ 33 | beforeRequest({reqOptions, requester}){ 34 | if(!reqOptions.header) 35 | reqOptions.header = {}; 36 | 37 | reqOptions.header.cookie = Cookie.mergeCookieStr(this.cookie.getCookie(), reqOptions.header.cookie); 38 | } 39 | 40 | /** 41 | * 接收返回结果头部中的cookie信息 42 | * @param {Requester~ReqOptions} reqOptions 43 | * @param {Requester~ReqRes} reqRes 44 | */ 45 | afterRequest({reqOptions, reqRes}){ 46 | //请求失败,不作处理 47 | if (!reqRes.succeeded) 48 | return; 49 | 50 | //请求结果中没有设置头部信息,不作处理 51 | if (!reqRes.cookies) //返回头部中有'Set-Cookie'时,wx.request会返回一个cookies字段 52 | return; 53 | 54 | //处理结果头部中的cookie信息 55 | for (let setStr of reqRes.cookies) 56 | this.cookie.setCookie(setStr); 57 | } 58 | } 59 | 60 | export default CookiePlugin; -------------------------------------------------------------------------------- /src/lib/request/plugin/FailRecoverPlugin.js: -------------------------------------------------------------------------------- 1 | import BasePlugin from './BasePlugin'; 2 | 3 | /** 4 | * 请求管理-网络异常处理插件 5 | * 在请求前后植入网络异常处理逻辑,详见{@tutorial 2.3-request} 6 | * @extends BasePlugin 7 | */ 8 | class FailRecoverPlugin extends BasePlugin{ 9 | requestFailRecoverer = null; 10 | 11 | /** 12 | * 构造函数 13 | * @param {string} [pluginName='FailRecoverPlugin'] 插件名称,主要用于打印日志和调试,便于追溯操作源 14 | * @param {FailRecoverPlugin~RequestFailRecoverer} requestFailRecoverer 网络异常处理函数 15 | */ 16 | constructor({pluginName, requestFailRecoverer}){ 17 | super({ 18 | pluginName: pluginName || 'FailRecoverPlugin' 19 | }); 20 | this.requestFailRecoverer = requestFailRecoverer; 21 | } 22 | 23 | /** 24 | * 在接口请求返回后检查并植入网络异常处理逻辑 25 | * @param {Requester~ReqOptions} reqOptions 请求参数 26 | * @param {object} thisIssuer 发起接口请求的this对象 27 | * @param {Requester~ReqRes} reqRes 请求结果 28 | * @return {undefined | Requester~AfterRequestRes} 期望的后续处理,undefined表示继续执行默认流程 29 | */ 30 | async afterRequestAsync({reqOptions, reqRes, thisIssuer}){ 31 | //未配置处理函数,默认不处理 32 | if (!this.requestFailRecoverer) 33 | return; 34 | 35 | //网络正常,无需处理 36 | if (reqRes.succeeded) 37 | return; 38 | 39 | //网络异常处理机制 40 | let overrideRes = await new Promise((resolve, reject)=>{ 41 | this.requestFailRecoverer.call(thisIssuer, { 42 | res: reqRes, 43 | options: reqOptions, 44 | resolve, 45 | reject, 46 | }); 47 | }); 48 | 49 | return { 50 | action: 'override', 51 | overrideRes 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * @typedef {function} FailRecoverPlugin~RequestFailRecoverer 网络异常处理函数 58 | * @param {Requester~ReqRes} res 接口请求结果 59 | * @param {Requester~ReqOptions} options 接口请求参数 60 | * @param {function} resolve 重试成功时回调重试结果 61 | * @param {function} reject 重试失败时回调失败原因 62 | * @example 63 | * function requestFailRecoverer({res, options, resolve, reject}){ 64 | * //展示网络异常界面,提示用户“点击屏幕任意位置重试” 65 | * //点击重试 66 | * //重试成功,resolve(重试后的请求结果) 67 | * //发生异常,reject(异常详情) 68 | * 69 | * console.log('this:', this); //另,调用时this对象会设置为:发起请求的this对象 70 | * } 71 | */ 72 | 73 | export default FailRecoverPlugin; -------------------------------------------------------------------------------- /src/lib/request/plugin/FormPlugin.js: -------------------------------------------------------------------------------- 1 | import BasePlugin from './BasePlugin'; 2 | 3 | /** 4 | * 请求管理-表单插件 5 | * 在请求前后植入表单处理逻辑,详见{@tutorial 2.3-request} 6 | * @extends BasePlugin 7 | */ 8 | class FormPlugin extends BasePlugin{ 9 | defaultContentType = ''; 10 | 11 | /** 12 | * 构造函数 13 | * @param {string} [pluginName='FormPlugin'] 插件名称 14 | * @param {string} [defaultContentType='application/x-www-form-urlencoded'] 默认表单类型 15 | */ 16 | constructor({pluginName, defaultContentType}={}){ 17 | super({ 18 | pluginName: pluginName || 'FormPlugin' 19 | }); 20 | this.defaultContentType = defaultContentType || 'application/x-www-form-urlencoded'; 21 | } 22 | 23 | /** 24 | * 在请求发起前植入表单处理逻辑: 25 | * 1. 将请求头部中的content-type默认值改为构造函数中指定的defaultContentType 26 | * 2. 将参数中的数组和对象转为json格式,避免被自动转为类似"[object Object]"的无语义字符串 27 | * @param reqOptions 28 | */ 29 | beforeRequest({reqOptions}){ 30 | //设置默认content-type 31 | reqOptions.header = reqOptions.header || {}; 32 | reqOptions.header['content-type'] = reqOptions.header['content-type'] || this.defaultContentType; 33 | 34 | //将参数中的数组和对象转为json格式,避免被自动转为类似"[object Object]"的无语义字符串 35 | if (reqOptions.header['content-type'].toLowerCase()==='application/x-www-form-urlencoded'){ 36 | reqOptions.data = reqOptions.data || {}; 37 | for (let name in reqOptions.data) { 38 | if (typeof reqOptions.data[name] === "object") 39 | reqOptions.data[name] = JSON.stringify(reqOptions.data[name]); 40 | } 41 | } 42 | } 43 | } 44 | 45 | export default FormPlugin; -------------------------------------------------------------------------------- /src/lib/request/plugin/InstantPlugin.js: -------------------------------------------------------------------------------- 1 | import BasePlugin from "./BasePlugin"; 2 | 3 | /** 4 | * 请求管理-快捷插件 5 | * 在请求前后植入指定处理逻辑,详见{@tutorial 2.3-request} 6 | * @extends BasePlugin 7 | */ 8 | class InstantPlugin extends BasePlugin{ 9 | hooks = {}; 10 | 11 | /** 12 | * 构造函数 13 | * 直接传入钩子函数,快速实现定制逻辑,而不用每次单独写一个子类 14 | * 适用场景:逻辑特别轻巧、逻辑为第三方传入等 15 | * @param {object} options 16 | * @param {string} options.pluginName 插件名称,主要用于打印日志和调试,便于追溯操作源 17 | * @param {function} [options.beforeRequest] 钩子函数,详见{@link BasePlugin#beforeRequest} 18 | * @param {function} [options.beforeRequestAsync] 钩子函数,详见{@link BasePlugin#beforeRequestAsync} 19 | * @param {function} [options.afterRequest] 钩子函数,详见{@link BasePlugin#afterRequest} 20 | * @param {function} [options.afterRequestAsync] 钩子函数,详见{@link BasePlugin#afterRequestAsync} 21 | */ 22 | constructor(options){ 23 | super({ 24 | pluginName: options.pluginName 25 | }); 26 | this.hooks = { 27 | beforeRequest: options.beforeRequest, 28 | beforeRequestAsync: options.beforeRequestAsync, 29 | afterRequest: options.afterRequest, 30 | afterRequestAsync: options.afterRequestAsync, 31 | }; 32 | } 33 | 34 | beforeRequest(...args){ 35 | return this.hooks.beforeRequest && this.hooks.beforeRequest.apply(this, args); 36 | } 37 | async beforeRequestAsync(...args){ 38 | return this.hooks.beforeRequestAsync && this.hooks.beforeRequestAsync.apply(this, args); 39 | } 40 | afterRequest(...args){ 41 | return this.hooks.afterRequest && this.hooks.afterRequest.apply(this, args); 42 | } 43 | async afterRequestAsync(...args){ 44 | return this.hooks.afterRequestAsync && this.hooks.afterRequestAsync.apply(this, args); 45 | } 46 | } 47 | 48 | export default InstantPlugin; -------------------------------------------------------------------------------- /src/lib/request/plugin/LoginPlugin.js: -------------------------------------------------------------------------------- 1 | import BasePlugin from "./BasePlugin"; 2 | import {makeAssignableMethod} from '../../operationKit'; 3 | 4 | /** 5 | * 请求管理-登录插件 6 | * 在请求前后植入登录态检查和处理逻辑,详见{@tutorial 2.1-login} 7 | * @extends BasePlugin 8 | */ 9 | class LoginPlugin extends BasePlugin{ 10 | /** 11 | * 登录中心 12 | * @ignore 13 | * @type {BaseLogin} 14 | */ 15 | loginCenter = null; 16 | /** 17 | * 登录态失效校验函数 18 | * @ignore 19 | * @type {LoginPlugin~ApiAuthFailChecker} 20 | */ 21 | apiAuthFailChecker = null; 22 | 23 | /** 24 | * 构造函数 25 | * @param {string} [pluginName='LoginPlugin'] 插件名称 26 | * @param {BaseLogin} loginCenter 登录中心 27 | * @param {LoginPlugin~ApiAuthFailChecker} apiAuthFailChecker 登录态失效校验函数 28 | */ 29 | constructor({pluginName, loginCenter, apiAuthFailChecker}){ 30 | super({ 31 | pluginName: pluginName || 'LoginPlugin' 32 | }); 33 | this.loginCenter = loginCenter; 34 | this.apiAuthFailChecker = apiAuthFailChecker; 35 | } 36 | 37 | mount(...args){ 38 | super.mount(...args); 39 | this.requester.registerToThis({ 40 | methodName: 'requestWithLogin', 41 | methodFunc: makeAssignableMethod({ 42 | instance: this, 43 | method: 'requestWithLogin', 44 | rcvThis: { 45 | argIdx: 1, 46 | argProp: 'thisIssuer' 47 | } 48 | }), 49 | }); 50 | } 51 | 52 | /** 53 | * 需要登录态的http请求,会在请求前后自动加入登录态相关逻辑: 54 | * 1. 请求发出前,若未登录,则先触发登录,然后再发送接口请求 55 | * 2. 请求返回后,若判断后端登录态已失效,则自动重新登录重新发送接口请求,并以重新请求的结果作为本次调用结果返回 56 | * 57 | * 本函数会注册到requester对象上,可以直接通过requester.requestWithLogin()调用 58 | * @param {Requester~ReqOptions} reqOptions 请求参数 59 | * @param {object} [reqOptions.loginOpts] 额外附增字段:登录参数,格式同{@link BaseLogin#login} 60 | * @param {Requester~ManageOptions} [manageOptions] 管理参数 61 | * @return {*|Requester~ReqRes} 请求结果,格式同{@link Requester#request} 62 | * 63 | * @example 64 | * let fetchData = requester.requestWithLogin({ 65 | * url: '', //正常设置url、data、method等各种请求选项 66 | * loginOpts: { //额外定义一个保留字段loginOpts,用于指定登录参数 67 | * mode: 'silent' 68 | * } 69 | * }); 70 | */ 71 | async requestWithLogin(reqOptions, manageOptions={}){ 72 | reqOptions.needLogin = true; 73 | return this.requester.request(reqOptions, manageOptions); 74 | } 75 | 76 | async beforeRequestAsync({reqOptions, thisIssuer}){ 77 | //检查是否需要登录态 78 | if (!reqOptions.needLogin) 79 | return; 80 | 81 | //获取登录态 82 | let loginRes = await this.loginCenter.login(reqOptions.loginOpts, {thisIssuer}); 83 | 84 | //判断是否需要取消接口调用 85 | return (loginRes.code===0 || loginRes.code===-200) ? {action: 'continue'} : {action: 'cancel', errMsg: '登录失败'}; 86 | } 87 | 88 | async afterRequestAsync({reqOptions, reqRes, thisIssuer}){ 89 | //检查是否需要登录态 90 | if (!reqOptions.needLogin) 91 | return; 92 | 93 | //网络异常,不作处理 94 | if (!reqRes.succeeded) 95 | return; 96 | 97 | //判断后端登录态是否失效 98 | let isAuthFail = this.apiAuthFailChecker(reqRes.data, reqOptions); 99 | 100 | //未失效,正常返回请求结果 101 | if (!isAuthFail) 102 | return; 103 | 104 | //已失效,清除前端登录态 105 | this.loginCenter.clearLogin(); 106 | 107 | //重新登录,重新发送接口请求,以重新请求的结果作为本次调用结果返回 108 | return { 109 | action: 'retry', 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * @typedef {function} LoginPlugin~ApiAuthFailChecker 登录态失效校验函数,根据接口返回内容判断后端登录态是否失效 116 | * @param {*} resData 后端接口返回内容 117 | * @param {Requester~ReqOptions} reqOptions 请求参数 118 | * @return {boolean} 后端登录态是否失效 119 | * @example 120 | * function apiAuthFailChecker(resData, reqOptions){ 121 | * return ( 122 | * resData.respMsg.includes('请登录') || //后端登录态失效通用判断条件 123 | * (reqOptions.url.includes('/bizA/') && resData.respCode===-1) || //业务线A后端接口登录态失效 124 | * (reqOptions.url.includes('/bizB/') && resData.respCode===-2) //业务线B后端接口登录态失效 125 | * ); 126 | * } 127 | */ 128 | 129 | export default LoginPlugin; -------------------------------------------------------------------------------- /src/lib/routeParams.js: -------------------------------------------------------------------------------- 1 | import {deepEqual} from './operationKit'; 2 | /** 3 | * 用于页面间传递参数,详见{@tutorial 2.7-routeParams} 4 | */ 5 | class RouteParams 6 | { 7 | _backFromRoute = ''; //后一页面(backFrom)路由 8 | _backFromData = ''; //后一页面传递给前一页面的数据 9 | _backFromHistoryStack = []; //后一页面路由栈,用于校验层级关系,避免backFrom数据未及时清理,对其它页面造成持续干扰 10 | 11 | _openFromRoute = ''; //前一页面(openFrom)路由 12 | _openFromData = ''; //前一页面传递给后一页面的数据 13 | _openFromHistoryStack = []; //前一页面路由栈,用于校验层级关系,避免openFrom数据未及时清理,对其它页面造成持续干扰 14 | 15 | static _getHistorySnapShot(history=getCurrentPages()){ 16 | return history.map(page=>page.route||page.__route__); 17 | } 18 | 19 | /** 20 | * 后一页面向前一页面传递数据 21 | * @param {*} data 数据内容 22 | */ 23 | setBackFromData(data){ 24 | let history = getCurrentPages(); 25 | let curPage = history[history.length-1]; 26 | this._backFromRoute = curPage.route || curPage.__route__; 27 | this._backFromData = data; 28 | this._backFromHistoryStack = RouteParams._getHistorySnapShot(history); 29 | } 30 | 31 | /** 32 | * 返回前一页面时,获取后一页面的页面路径 33 | * @return {string} 后一页面页面路径 34 | */ 35 | getBackFromRoute(){ 36 | let curStack = RouteParams._getHistorySnapShot(); 37 | if (!(curStack.length===this._backFromHistoryStack.length-1 && deepEqual(curStack, this._backFromHistoryStack.slice(0,-1)))) //路由栈不匹配,说明此为n久前其它页面遗留数据,应予以清理 38 | this.clearBackFrom(); 39 | 40 | return this._backFromRoute; 41 | } 42 | /** 43 | * 返回前一页面时,获取后一页面传递过来的数据 44 | * @return {*} 后一页面传递过来的数据 45 | */ 46 | getBackFromData(){ 47 | let curStack = RouteParams._getHistorySnapShot(); 48 | if (!(curStack.length===this._backFromHistoryStack.length-1 && deepEqual(curStack, this._backFromHistoryStack.slice(0,-1)))) //路由栈不匹配,说明此为n久前其它页面遗留数据,应予以清理 49 | this.clearBackFrom(); 50 | 51 | return this._backFromData; 52 | } 53 | /** 54 | * 清除后一页面向前一页面的传递内容 55 | */ 56 | clearBackFrom(){ 57 | this._backFromRoute = ''; 58 | this._backFromData = ''; 59 | } 60 | 61 | /** 62 | * 前一页面向后一页面传递数据 63 | * @param {*} data 数据内容 64 | */ 65 | setOpenFromData(data){ 66 | let history = getCurrentPages(); 67 | let curPage = history[history.length-1]; 68 | this._openFromRoute = curPage.route || curPage.__route__; 69 | this._openFromData = data; 70 | this._openFromHistoryStack = RouteParams._getHistorySnapShot(history); 71 | } 72 | /** 73 | * 进到后一页面时,获取前一页面的页面路径 74 | * @return {string} 前一页面页面路径 75 | */ 76 | getOpenFromRoute(){ 77 | let curStack = RouteParams._getHistorySnapShot(); 78 | let hisMatch = (curStack.length===this._openFromHistoryStack.length+1 && deepEqual(curStack.slice(0,-1), this._openFromHistoryStack)) || //navigateTo 79 | (curStack.length===this._openFromHistoryStack.length && deepEqual(curStack.slice(0,-1), this._openFromHistoryStack.slice(0,-1))); //redirectTo 80 | if (!hisMatch) //路由栈不匹配,说明此为n久前其它页面遗留数据,应予以清理 81 | this.clearOpenFrom(); 82 | 83 | return this._openFromRoute; 84 | } 85 | /** 86 | * 进到后一页面时,获取前一页面传递过来的数据 87 | * @return {*} 前一页面传递过来的数据 88 | */ 89 | getOpenFromData(){ 90 | let curStack = RouteParams._getHistorySnapShot(); 91 | let hisMatch = (curStack.length===this._openFromHistoryStack.length+1 && deepEqual(curStack.slice(0,-1), this._openFromHistoryStack)) || //navigateTo 92 | (curStack.length===this._openFromHistoryStack.length && deepEqual(curStack.slice(0,-1), this._openFromHistoryStack.slice(0,-1))); //redirectTo 93 | if (!hisMatch) //路由栈不匹配,说明此为n久前其它页面遗留数据,应予以清理 94 | this.clearOpenFrom(); 95 | 96 | return this._openFromData; 97 | } 98 | /** 99 | * 清除前一页面向后一页面的传递内容 100 | */ 101 | clearOpenFrom(){ 102 | this._openFromRoute = ''; 103 | this._openFromData = ''; 104 | } 105 | } 106 | 107 | export default new RouteParams(); 108 | -------------------------------------------------------------------------------- /src/lib/uniAppKit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * uni-app框架工具集,与uni-app框架耦合的内容在此处实现 3 | * @module uniAppKit 4 | */ 5 | 6 | import Vue from 'vue'; 7 | 8 | /** 9 | * 注册全局this属性 10 | * @param {Object|string} lib|name 11 | * @param {Object|*} propMap|value 12 | @example 13 | //通过键值对的方式一次注册一个属性 14 | registerToThis('$navigateTo', wx.navigateTo); 15 | registerToThis('$redirectTo', wx.redirectTo); 16 | //则所有页面&组件可以以 this.$navigateTo 的形式调用 wx.navigateTo 17 | 18 | //通过 库-映射表 的方式一次注册多个属性 19 | registerToThis(wx, { 20 | '$navigateTo': 'navigateTo', 21 | '$redirectTo': 'redirectTo' 22 | }); 23 | //则所有页面&组件可以以 this.$navigateTo 的形式调用 wx.navigateTo 24 | */ 25 | export function registerToThis(lib, propMap) { 26 | if (typeof lib === "string") { 27 | let [name, value] = [lib, propMap]; 28 | lib = { 29 | [name]: value 30 | }; 31 | propMap = { 32 | [name]: name 33 | }; 34 | } 35 | 36 | if (!((typeof lib==="object"||typeof lib==="function") && typeof propMap==="object")) { 37 | console.error('[registerToThis failed] bad params:', arguments); 38 | return; 39 | } 40 | 41 | for (let prop in propMap) 42 | Vue.prototype[prop] = propMap[prop]==='*this' ? lib : lib[propMap[prop]]; 43 | } 44 | 45 | /** 46 | * 注册全局页面钩子 47 | * @param {string} hook 页面生命周期钩子名称 48 | * @param {Function} handler 处理函数 49 | */ 50 | export function registerPageHook(hook, handler) { 51 | if (typeof handler !== "function") { 52 | console.error('[registerPageHook] bad params:', hook, handler); 53 | return; 54 | } 55 | 56 | Vue.mixin({ 57 | [hook]: function (...args) { 58 | if (this.mpType !== 'page') //非页面顶级组件,不作处理 59 | return; 60 | 61 | handler.apply(this, args); 62 | } 63 | }); 64 | } 65 | 66 | /** 67 | * 获取当前页面对应的uni-app框架实例 68 | * @return uniPage 69 | */ 70 | export function getCurUniPage() { 71 | let curPages = getCurrentPages(); 72 | let curPage = curPages[curPages.length-1]; 73 | return curPage && curPage.$vm; 74 | } 75 | 76 | /** 77 | * 页面数据恢复函数,用于 78 | * 1. [无关]wepy实例覆盖问题,存在两级同路由页面时,前者数据会被后者覆盖,返回时需予以恢复,详见bug:[两级页面为同一路由时,后者数据覆盖前者](https://github.com/Tencent/wepy/issues/322) 79 | * 2. 无限层级路由策略中,层级过深时,新开页面会替换前一页面,导致前一页面数据丢失,返回时需予以恢复 80 | * 81 | * @param {History~Route} route 页面路由对象 82 | * @param {string} context 数据丢失场景: tainted - 实例覆盖问题导致的数据丢失 | unloaded - 层级问题导致的数据丢失 83 | * @return {{succeeded: boolean}} 处理结果,格式形如:{succeeded: true} 84 | */ 85 | export function pageRestoreHandler({route, context}) { 86 | if (context === 'tainted') //uni-app框架不存在实例覆盖问题,无需处理 87 | return {succeeded: true}; 88 | 89 | //恢复页面数据 90 | restoreVmTree({ 91 | targetVm: getCurUniPage(), 92 | sourceVm: route.wxPage.$vm, 93 | }); 94 | return {succeeded: true}; 95 | } 96 | 97 | /** 98 | * 恢复组件树中各组件的数据 99 | * @ignore 100 | * @param {VueComponent} targetVm 待恢复的组件树根实例 101 | * @param {VueComponent} sourceVm 作为数据源的组件树根实例 102 | */ 103 | function restoreVmTree({targetVm, sourceVm}){ 104 | //恢复根实例自身数据 105 | Object.assign(targetVm, sourceVm.$data || sourceVm._data); 106 | 107 | //恢复各子组件及其后代组件的数据 108 | for (let i=0; i{ 95 | return callbackSdk[key](Object.assign({}, options, { 96 | success(res){ 97 | Object.assign(res, {succeeded: true}); 98 | options.success && options.success(res); 99 | resolve(res); 100 | }, 101 | fail(res){ 102 | Object.assign(res, {succeeded: false}); 103 | options.fail && options.fail(res); 104 | dealFail ? resolve(res) : reject(res); 105 | }, 106 | })); 107 | }); 108 | } 109 | }catch(e) { 110 | console.error('[error]promisify出错: ',e); 111 | continue; 112 | } 113 | } 114 | return promiseSdk; 115 | } 116 | --------------------------------------------------------------------------------