├── .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 | 
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 |
74 |
75 |
76 |
77 |
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 |
121 |
122 |
123 |
124 |
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 | 
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 | 
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 | - 
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 | - 
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 | 
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 | 
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 | 
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 | 
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{options.title || ''}}
10 | {{options.content || ''}}
11 |
12 |
16 | {{button.text}}
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
200 |
201 |
321 |
--------------------------------------------------------------------------------
/src/components-wepy/CustomEntry.wpy:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 | url:
16 |
17 | 路由表
18 |
19 |
20 |
21 |
22 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | tips:
39 | {{tips}}
40 |
41 |
42 |
43 |
224 |
225 |
228 |
296 |
--------------------------------------------------------------------------------
/src/components-wepy/QrCode.wpy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{info.title}}
14 | {{info.content}}
15 |
16 |
17 | 复制
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{tip.title}}
25 | {{tip.content}}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
207 |
208 |
211 |
268 |
--------------------------------------------------------------------------------
/src/components-wepy/TextAreaEle.wpy:
--------------------------------------------------------------------------------
1 |
56 |
57 |
58 |
59 |
71 |
72 | {{value || placeholder}}
75 |
76 |
77 |
78 |
79 |
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