- 微信小程序中安全区域计算和适配
前言 自从iphoneX问世之后,因为iphoneX、iphoneXR和后续全面屏手机设备,因为物理Home键被底部小黑条代替了,这时候很多前端小伙伴在开发的过程都会遇到 “全面屏”和“非全面屏”的兼容性问题,普遍问题就是底部按钮或者选项卡与底部黑线重叠 解释 根据官方解释: 安全区域指的是一个可视窗口范围,处于安全区域的内容不受圆角(corners)、齐刘海(sensor housing)、小黑条(Home Indicator)的影响。 具体区域如图展示 [图片] 适配方案 当前有效的解决方式有几种 使用已知底部小黑条高度34px/68rpx来适配 使用苹果官方推出的css函数env()、constant()适配 使用微信官方API,getSystemInfo()中的safeArea对象进行适配 使用已知底部小黑条高度34px/68rpx来适配 这种方式是根据实践得出,通过物理方式测出iPhone底部的小黑条(Home Indicator)高度是34px,实际在开发者工具选中真机获取到高度也是34px,所以直接根据该值,设置margin-bottom、padding-bottom、height也能实现。同时这样做要有一个前提,需要判断当前机型是需要适配安全区域的机型。 但是这种方案相对来说是不推荐使用的。比较是一个比较古老原始的方案 使用苹果官方推出的css函数env()、constant()适配 这种方案是苹果官方推荐使用env(),constant()来适配,开发者不需要管数值具体是多少。 env和constant是IOS11新增特性,有4个预定义变量: safe-area-inset-left:安全区域距离左边边界的距离 safe-area-inset-right:安全区域距离右边边界的距离 safe-area-inset-top:安全区域距离顶部边界的距离 safe-area-inset-bottom :安全距离底部边界的距离 具体用法如下: Tips: constant和env不能调换位置 [代码] padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/ padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/ [代码] 其实利用这个能解决大部分的适配场景了,但是有时候开发需要自定义头部信息,这时候就没办法使用css来解决了 使用微信官方API,getSystemInfo()中的safeArea对象进行适配 通过 wx.getSystemInfo获取到各种安全区域信息,解析出具体的设备类型,通过设备类型做宽高自适应,话不多说,直接上代码 代码实现 [代码] const res = wx.getSystemInfoSync() const result = { ...res, bottomSafeHeight: 0, isIphoneX: false, isMi: false, isIphone: false, isIpad: false, isIOS: false, isHeightPhone: false, } const modelmes = result.model const system = result.system // 判断设备型号 if (modelmes.search('iPhone X') != -1 || modelmes.search('iPhone 11') != -1) { result.isIphoneX = true; } if (modelmes.search('MI') != -1) { result.isMi = true; } if (modelmes.search('iPhone') != -1) { result.isIphone = true; } if (modelmes.search('iPad') > -1) { result.isIpad = true; } let screenWidth = result.screenWidth let screenHeight = result.screenHeight // 宽高比自适应 screenWidth = Math.min(screenWidth, screenHeight) screenHeight = Math.max(screenWidth, screenHeight) const ipadDiff = Math.abs(screenHeight / screenWidth - 1.33333) if (ipadDiff < 0.01) { result.isIpad = true } if (result.isIphone || system.indexOf('iOS') > -1) { result.isIOS = true } const myCanvasWidth = (640 / 375) * result.screenWidth const myCanvasHeight = (1000 / 667) * result.screenHeight const scale = myCanvasWidth / myCanvasHeight if (scale < 0.64) { result.isHeightPhone = true } result.navHeight = result.statusBarHeight + 46 result.pageWidth = result.windowWidth result.pageHeight = result.windowHeight - result.navHeight if (!result.isIOS) { result.bottomSafeHeight = 0 } const capsuleInfo = wx.getMenuButtonBoundingClientRect() // 胶囊热区 = 胶囊和状态栏之间的留白 * 2 (保持胶囊和状态栏上下留白一致) * 2(设计上为了更好看) + 胶囊高度 const navbarHeight = (capsuleInfo.top - result.statusBarHeight) * 4 + capsuleInfo.height // 写入胶囊数据 result.capsuleInfo = capsuleInfo; // 安全区域 const safeArea = result.safeArea // 可视区域高度 - 适配横竖屏场景 const screenHeight = Math.max(result.screenHeight, result.screenWidth) const height = Math.max(safeArea.height, safeArea.width) // 状态栏高度 const statusBarHeight = result.statusBarHeight // 获取底部安全区域高度(全面屏手机) if (safeArea && height && screenHeight) { result.bottomSafeHeight = screenHeight - height - statusBarHeight if (result.bottomSafeHeight < 0) { result.bottomSafeHeight = 0 } } // 设置header高度 result.headerHeight = statusBarHeight + navbarHeight // 导航栏高度 result.navbarHeight = navbarHeight [代码]
2022-11-04 - Skyline渲染框架的头像合成小程序实例-比比头像生成 附uniapp源码
[图片] 2023 年国庆节就要到了,有粉丝问到今年是否可以出一款国庆头像生成小程序。 于是想到用微信新渲染引擎 skyline的新 API:Snapshot截图能力来完成 文档:https://developers.weixin.qq.com/miniprogram/dev/api/skyline/Snapshot.html [图片] 完成后,发现真的非常丝滑,它的原理就是先用<snapshot id="target">...</snapshot>将需要截图的区域包括起来,然后定义一个 ID, 接下来在脚本里通过createSelectorQuery()获取。 this.createSelectorQuery().select("#target") .node().exec(res => { const node = res[0].node node.takeSnapshot({ // type: 'file' 且 format: 'png' 时,可直接导出成临时文件 type: 'arraybuffer', format: 'png', success: (res) => { const f = `${wx.env.USER_DATA_PATH}/hello.png` const fs = wx.getFileSystemManager(); fs.writeFileSync(f, res.data, 'binary') 。。 }, fail(res) { console.log("takeSnapshot fail:", res) } }) }) 比比小程序对选择器进行了封装,可搜索核心代码进行学习(全部代码见源码): getSelectorNodeInfo('#target').then((res) => { console.log('res', res) const node = res[0].node node.takeSnapshot({ type: 'arraybuffer', format: 'png', success: (res:any) => { const savePath = `${wx.env.USER_DATA_PATH}/hello.png` const fs = wx.getFileSystemManager(); fs.writeFileSync(savePath, res.data, 'binary'); //图片保存至本地 wx.showShareImageMenu({ //唤起分享图片的界面 path: savePath }) }, fail(res) { } }) }) 在此项目中也可以借鉴一下微信隐私弹窗的交互 [图片] [图片] 因为此项目仅用了1个小时完成,实在是没有太多的难点,主要是让大家对新的渲染引擎有一个全新的认识。 项目对skyline和webView都做了布局兼容,对于新手学习skyline是个不错的demo。 也可以看看我用 skyline搭建的第一个小程序: 比比轻壁纸:https://developers.weixin.qq.com/community/minihome/article/doc/0004ce2acf8020ac32af1ed5f51813 ----- 附上源码(uniapp+vue3+ts)和 demo: https://github.com/shiheme/skyline-wx-avatar https://gitee.com/h5gallery/skyline-wx-avatar [图片] 喜欢的给个star、点赞、留个评论,谢谢。 喜欢小程序开发的也可以加我微信或者关注我的公众号(github/gitee里扫码)一起学习成长。
2023-09-09 - 小程序新渲染引擎 Skyline 发布正式版
为了进一步提升小程序的渲染性能和体验,我们推出了一套新渲染引擎 Skyline,现在,跟随着基础库 3.0.0 发布 Skyline 正式版。 我们知道,小程序一直用 WebView 来渲染界面,因其有不错的兼容性和丰富的特性,且各大厂商也在不断优化 Web 的渲染性能,但 Web 体系相比于原生开发,在性能上仍然有较大差距,并且特性上发展缓慢,使得小程序很难做出类原生的体验。因此,我们开发了一套新渲染引擎 Skyline,旨在替代 WebView 作为小程序的渲染层,以提供更优秀的渲染性能和诸多增强特性,让小程序能达到原生的体验。 以下为你全方位介绍 Skyline 的特点。 提供更好的性能 在渲染流程上,WebView 因其需要向后兼容,积累了较多历史包袱,加之整体设计目标不同,使其渲染流水线更加冗长复杂,而 Skyline 则更为精简,同时只保留更现代的 CSS 特性。在此基础上,我们还进一步实现了很多优化点: 单线程版本组件框架。Skyline 下默认启用了新版本的组件框架 glass-easel,该版本适应了 Skyline 的单线程模型,使得建树流程的耗时有效降低(优化 30%-40%),同时 setData 调用也不再有通信开销。 组件下沉。我们将部分内置组件(如 scroll-view、swiper、picker-view 等)直接在底层实现,以追求更流畅的交互体验。此外,我们也将常用的内置组件(view、text、image)从 JS 下沉到原生实现,相当于原生 DOM 节点,有效降低了创建组件的开销(优化 30%)。 长列表按需渲染。长列表是一个常用的但又经常遇到性能瓶颈的场景,Skyline 对其做了一些优化,使 scroll-view 组件只渲染在屏节点(用法上有一定的约束),并且增加 lazy mount 机制优化首次渲染长列表的性能,后续我们也计划在组件框架层面进一步支持 scroll-view 的可回收机制,以更大程度降低创建节点的开销。 WXSS 预编译。同 WebView 传输 WXSS 文本不同,Skyline 在后台构建小程序代码包时会将 WXSS 预编译为二进制文件,在运行时直接读取二进制文件获得样式表结构,避免了运行时解析的开销(预编译较运行时解析快 5 倍以上)。 样式计算更快。Skyline 通过精简 WXSS 特性大幅简化了样式计算的流程。同时 Skyline 与小程序框架结合也更为紧密,例如: Skyline 结合组件系统实现了 WXSS 样式隔离、基于 wx:for 实现了节点样式共享(相比于 WebView 推测式样式共享更为精确、高效)。 降低内存占用。在 WebView 渲染模式下,一个小程序页面对应一个 WebView 实例,并且每个页面会重复注入一些公共资源。而 Skyline 只有 AppService 线程,且多个 Skyline 页面会运行在同一个渲染引擎实例下,因此页面占用内存能够降低很多,还能做到更细粒度的页面间资源共享(如全局样式、公共代码、缓存资源等)。总体上,由于 Skyline 在渲染流程上更加可控,我们能让小程序的特性尽可能融合进渲染流程中完成,还有很多在细节上的优化(比如对 rpx 的处理、image mode=widthFix 的处理等,都是融入渲染流程中,而避免在 JS 做太多额外的计算)就不再一一介绍。另外,我们也在持续优化中,Skyline 会是之后小程序性能优化的重点。 至于目前整体的性能情况,我们从已上线的小程序数据观测到(基础库 3.0.0 glass-easel 带来的优化暂未体现),启动耗时方面,即点击到完全渲染(LCP)的耗时,WebView 对比 Skyline 为 2492ms vs 2052ms,减少 17.6%;渲染阶段耗时方面,即框架建树到完全渲染(LCP)的耗时,WebView 对比 Skyline 为 626ms vs 312ms,减少 50%。 根除旧有架构的问题 在基于 Web 体系的架构下,小程序的部分基础体验会受限于 WebView 提供的能力(特别是 iOS WKWebView 限制更大一些),使得一些技术方案无法做得很完美,留下一些潜在的问题。 原生组件同层渲染更稳定。iOS 下原生组件同层渲染的原理先前有介绍过,本质上是在 WKWebView 黑盒下一种取巧的实现方式,并不能完美融合到 WKWebView 的渲染流程,因此很容易在一些特殊的样式发生变化后,同层渲染会失效,而在 Skyline 下可以很好地融合到渲染流程中,因此会更稳定。 无需页面恢复机制。iOS 下 WKWebView 会受系统的管理,当内存紧张时,系统就会将不在屏的 WKWebView 回收,会使得小程序除前台以外的页面丢失,虽然在页面返回时,我们对页面做了恢复,但页面的状态并不能 100% 还原,而在 Skyline 下则不再有该问题。 无页面栈层数限制。由于 WebView 的内存占用较大,页面层级最多有 10 层,而 Skyline 在内存方面更有优势,因此在连续 Skyline 页面跳转(复用同一引擎实例)的情况下,不再有该限制。 全新的交互动画体系 我们发现,要达到类原生的体验,渲染性能与交互动画缺一不可,渲染性能能让页面更快渲染出来,而交互动画能让浏览页面的体验更佳。但在 Web 体系下,难以做到像素级可控,交互动画衔接不顺畅,究其原因,在于缺失了一些重要的能力,为此,我们提供一套全新的交互动画能力。 Worklet 动画机制。在原来双线程的架构下,若要对界面元素做逐帧动画是需要频繁在逻辑层和渲染层之间通信的,这会带来较大的延迟,动画也就不会流畅。而 Worklet 动画正是为了解决这类问题而诞生的,其运行机制与 WXS 类似,但比 WXS 更靠近渲染流程而性能更好,而且支持的特性更多,可扩展性更强,这个是 Skyline 交互动画体系的基础。 手势系统。在原生的交互动画里,手势识别与协商是一个很重要的特性,而这块在 Web 体系是缺失的,因此 Skyline 下补全手势系统相关特性,包括常用手势的识别,如缩放、拖动、双击等,还有很重要的手势协商机制,在遇到手势冲突(常见于滚动容器下)时决定让哪个手势生效,以实现更顺畅的动画衔接。 自定义路由与共享元素。页面间的自定义转场动画,在原生应用里也是一个很常见的交互动画。在原来的小程序架构下,每个页面都是独立的 WebView 渲染,互相隔离,其跨页能力是基本不具备的。因此,Skyline 提供了一套自定义路由机制,能实现市面上大多数页面转场动画,同时也提供了共享元素机制,能很方便地做到同一元素在页面间飞跃的效果。此外,对内置组件的扩展也是重要一环,特别是 scroll-view 组件,我们优化了下拉刷新的体验,并且实现“下拉二楼”的交互,也添加很多控制能力,这都是些在 Web 下很难做到又非常重要的特性。总之,这套全新的交互动画体系是 Skyline 能实现类原生交互体验的关键。 释放更多高级能力 除了上面提到的交互动画能力外,Skyline 所能释放的能力还远不止于此,借助 Skyline 的特点,我们还提供以下新的组件 grid-view 瀑布流组件。瀑布流是一种常用的列表布局方式,得益于 Skyline 在布局过程中的可控性,我们直接在底层实现并提供出来,渲染性能要比 WebView 更优。 snapshot 截图组件。大多数小程序都会基于 canvas 实现自定义分享图的功能,但分享图的布局较复杂时,canvas 的方案实现成本会更大,而 Skyline 是具备对 WXML 子树截图的能力的,因此我们直接封装后开放出来,这样能复用更完善的 WXSS 能力,极大降低开发成本。除了新增的组件,还有不少是原有内置组件扩展的小特性,这里就不一一介绍,可 查看文档 或 更新日志。未来,我们还会持续在 Skyline 上开放更多高级功能,如全局跨页面组件,scroll-view 列表节点 builder 模块支持节点可回收等,更多可查看 文档特性状态 一栏,同时,也欢迎开发者在社区给我们提议。 至此,Skyline 的主要特点已基本介绍完毕,更完整的介绍、用法、迁移指引、注意点等等请查阅 文档。建议开发者现在就使用起来,尽早享受到 Skyline 带来的优化和丰富的特性,如果开发中遇到问题,可在开发者社区发贴反馈,我们也会邀请加入沟通交流群。
2023-07-19 - Skyline | 快速搞定复杂的分享海报
在小程序中生成海报是一种非常有效的推广方式 用户可以使用小程序的过程中生成小程序海报并分享给他人 通过海报的形式,用户可以直观地了解产品或服务的特点和优势 [图片] 常见绘制海报方式 目前,小程序海报有两种常见的实现方式: · canvas 绘制海报 · 服务端绘制海报 这两种方式各有千秋 canvas 绘制海报使用 canvas 绘制海报主要有以下几个步骤 1、创建 [代码]canvasContext[代码] 2、获取网络图片的本地路径 3、绘制图片、文字等到 [代码]canvas[代码] 4、调用 [代码]wx.canvasToTempFilePath[代码] 导出图片 尽管 canvas 绘制功能强大,但实际使用中,这些操作看似简单,但调试起来却比较麻烦 而且面对一些复杂的排版时,使用 canvas 绘制相较于使用 CSS 绘制来说困难许多 除此之外,canvas 的宽高有最大限制,超出限制则会绘制空白 服务端绘制 小程序也可以通过调用服务端接口,将需要生成海报的数据传递给服务端, 由服务端使用 Canvas API 等第三方库来生成图片。 然而,这种绘制方式需要走网络请求,如果量大会给服务器带来一定的成本压力。 此外,对于复杂排版的实现,使用 Canvas 绘制也有一定的难度。 尽管小程序海报虽然好用,但是当遇到要求比较高的设计稿需要还原海报时,对小程序开发者来说是一个十分让人头疼的问题 考虑到海报在小程序中使用的广泛性,我们把开发者的烦恼交给官方来处理~ 小程序官方推出了 [代码]snapshot[代码] 组件,可以直接将小程序 wxml 导出图片。 snapshot 生成海报 当使用 canvas 或 服务端绘制海报遇到复杂排版时,如 圆角、百分比、自定义字体 等等,实现比较困难。 但是使用 wxml 实现却很简单 👇 下面的例子我们使用 wxml 实现海报 <view class="snapshot-box"> <view class="poster-container"> <view class="poster-header"> <image /> ... </view> <view class="description"> ... </view> <view class="footer"> ... </view> </view> </view> [图片] 接着,我们就可以导出海报啦,使用非常简单: 1、用 [代码]snapshot[代码] 组件包裹海报的 wxml 2、调用 [代码]takeSnapshot[代码] 获取图片数据 3、调用 [代码]fs.writeFileSync[代码] 将海报数据写入本地文件 4、调用 [代码]wx.saveImageToPhotosAlbum[代码] 将海报保存到本地 <snapshot id="view"> <!-- 这里是要海报的 wxml --> </snapshot> <button bindtap="tap">保存海报</button> tap() { this.createSelectorQuery().select("#view") .node().exec(res => { const node = res[0].node // 保存海报 node.takeSnapshot({ type: 'arraybuffer', format: 'png', success: (res) => { const f = `${wx.env.USER_DATA_PATH}/hello.png` const fs = wx.getFileSystemManager(); // 将海报数据写入本地文件 fs.writeFileSync(f, res.data, 'binary') this.setData({ img: f }) // 把海报图片保存到本地 wx.saveImageToPhotosAlbum({ filePath: f }) } }) }) } 最后我们来看看使用 [代码]snapshot[代码] 组件生成海报的效果吧~ [图片] 除了普通尺寸分享海报之外,对于 canvas 无法搞定的超长海报,[代码]snapshot[代码] 后续也会支持超长海报的导出~ [图片] 你的小程序也有海报生成需求吗? 赶紧 mark 下这个 代码片段 来接入使用吧~
2023-09-06 - Skyline|探秘下拉二楼,打造更丰富的内容展示
下拉二楼是一种常见的交互设计,可以为应用中的内容展示提供更多的可能性。 通过下拉操作,开发者可以在二楼展示更丰富、更多样化的内容,从而增加用户的点击量和留存率,例如宣传视频、精选商品、走心故事等等。 在小程序中,下拉二楼一直是一种难以实现的交互设计,即使部分小程序实现了,但效果和性能都很差。 为了丰富小程序的内容展示,提高用户的使用体验,小程序官方近期推出了下拉二楼的能力,方便小程序开发者使用。 效果展示 让我们来看看小程序 scroll-view 实现下拉效果的效果~ [图片] 实现步骤 接下来,我们来看下如何使用 scroll-view 实现下拉二楼 1、配置下拉相关属性 scroll-view 新增了以下接口供开发者配置下拉二楼的能力,开发者可以根据业务需要配置相关的属性 属性 说明 refresher-two-level-enabled 开启下拉二级能力,配置开启需同时配置 refresher-two-level-triggered 设置打开/关闭二级 refresher-two-level-threshold 下拉二级阈值 refresher-two-level-close-threshold 滑动返回时关闭二级的阈值 refresher-two-level-scroll-enabled 处于二级状态时是否可滑动 refresher-ballistic-refresh-enabled 惯性滚动是否触发下拉刷新 refresher-two-level-pinned 即将打开二级时否定住 [代码]<scroll-view type="list" scroll-y // 开启下拉刷新(下拉二级必须开启下拉刷新) refresher-enabled="{{true}}" // 开启下拉二级能力 refresher-two-level-enabled="{{true}}" // 处于二级状态是否可滑动 refresher-two-level-scroll-enabled="{{true}}" > ... </scroll-view> [代码] 2、实现二楼内容 配置完下拉二楼属性之后,接着就是将我们的二楼实现在 scroll-view 中。 在 scroll-view 放置一个子节点,声明 slot=“refresher”,该节点中的内容即为下拉二楼的内容。 [代码]<scroll-view ... > <view slot="refresher"> 这里是二楼的内容 </view> </scroll-view> [代码] 3、根据下拉状态回调进行个性化处理 接着我们需要根据业务小程序自身的诉求,根据下拉状态的回调进行个性化的处理,例如:下来完成跳转页面等。 在 scroll-view 绑定 bind:refresherstatuschange 监听下拉状态,下拉状态有以下几种 属性 说明 Idle 空闲 CanRefresh 超过下拉刷新阈值 Refreshing 下拉刷新 Completed 下拉刷新完成 Failed 下拉刷新失败 CanTwoLevel 超过下拉二级阈值 TwoLevelOpening 开始打开二级 TwoLeveling 打开二级 TwoLevelClosing 开始关闭二级 [代码]<scroll-view bind:refresherstatuschange="onStatusChange" ... > <view slot="refresher"></view> ... </scroll-view> // .js onStatusChange(e) { const status: RefreshStatus = e.detail.status if (status === RefreshStatus.TwoLeveling) { const that = this // 当打开二级之后,跳转到新的页面 wx.navigateTo({ url: '../goods/index', events: { nextPageRouteDone: function(data) { // 新页面打开之后,关闭下拉二楼 that.scrollContext.closeTwoLevel({ duration: 1 }) } } }) } } [代码] 我们来演示一下松手立即跳转(图左)、完全打开二楼后跳转(图右) [图片] 丰富小程序展示内容和形式,欢迎大家使用小程序下拉二楼,为小程序的内容展示提供更多的可能性和创意发挥的空间。 通过下拉二楼,可以展示更丰富、更多样化的内容,也为小程序的发展带来了更多的机会和挑战~ 赶紧 mark 下这个 代码片段 来接入使用吧~
2023-08-03 - 「笔记」个人主体迁移到企业主体小程序踩坑
前言 作为开发者,相信很多人都是个人开发小程序,想着等以后做大了或者有公司后再迁移。 迁移 小程序的迁移过程并不复杂,根据后台提示进行操作,提交相关资料就可以了。 迁移中的注意事项 如果迁移的企业和原主体是同一个管理员的话可以选择不变更小程序管理员,申请函中填同一个人的手机号或者只填一个就可以,但是小程序后台填资料的时候一番操作后会告诉你目标主体和原主体不能使用同一个手机号码,必须使用不同的号码(大部分人都有多个手机号,所以这个也不是什么大问题),迁移过程中审核人员会分别拨打2个号码进行确认,而且是上一个号码拨打完立马就拨打下一个,所以如果是同一个人的话最好手机都在身边。 迁移成功后 收到迁移成功后可能以为就万事大吉了,但是你登录后台的时候会发现小程序的主体信息变更了,但是微信认证状态是[代码]未认证[代码],这时候我不知道其他人是什么想法,反正我看到第一时间可能是很多头羊驼在奔腾,如果需要认证的话还需要走一遍认证流程。 在迁移后未认证状态的小程序大部分功能使用,比如[代码]业务域名[代码]可以直接配置,目前不知道对实际审核会有什么影响。 迁移后可复用公众号资质认证 经过上面的误解,最后发现通过公众号后台的小程序管理里点开迁移过来的小程序详情可以[代码]复用公众号资质认证[代码]进行免费认证,切记不要在小程序后台里走认证流程,那个还是需要支付300。 [图片] 本文仅代表个人观点,希望大家不要花冤枉钱。
2020-06-11 - 关于申请小程序地理位置相关接口的规范
随着小程序生态的发展,越来越多小程序开发者会通过官方接口来给用户提供便捷的服务。如何在提供良好的体验时又能保障用户合法权益,如何正确的进行相关接口准入申请?本文将会从以下方面进行详细说明。 一、可通过相应接口准入申请的小程序 对象:自身已有地理位置相关使用场景或需地理位置相关场景完善服务内容的小程序 申请wx.getLocation接口参考案例1)含有交通服务类目,同时含有代驾服务、租车网点查询服务、查询附近车辆服务、城市共享交通服务等 [图片] [图片] 2)含有餐饮-点餐平台、餐饮-外卖平台类目、餐饮-餐饮服务场所/餐饮服务管理企业,并涉及实际送餐场景 [图片] [图片] 3)含有工具-信息查询、工具-办公、工具-设备管理类目,并涉及与地理位置相关的打卡服务业务,如智能门禁、智能穿戴设备等 [图片] 4)含有汽车服务-维修保养、汽车服务-汽车用品、汽车服务-汽车经销商/4S店、汽车服务-汽车厂商、汽车服务-汽车预售、汽车服务-二手车类目,涉及提供汽车售卖、维保洗美服务、查找附近的维修点/洗车网点等导航服务 [图片][图片] 5)含有电商平台/商家自营类目,涉及提供售卖商品线下发货、收货服务、线下商超导览、导航服务 [图片] 6)含有金融-银行、金融-非金融机构自营小额贷款/融资担保/商业保理类目,涉及银行小程序提供线下网点预约、基于地理位置取号并现场报到、附近网点导航等服 [图片] 7)含有电商平台/商家自营类目,涉及提供售卖商品非即刻交易线下发货、收货服务,比如线下跑腿收货、社区团购线下自提点收货等服务场景 [图片] [图片] 申请wx.onLocationChange接口参考案例1)含有交通服务类目,同时含有代驾服务、城市共享交通服务等 [图片] 2)含有生活服务类目,同时含有线下跑腿、开锁服务、其他上门作业等实际服务内容 [图片] 3)含有旅游-景区服务、旅游-住宿服务,涉及提供景区导航、导览服务、酒店导航服务 [图片] 二、无法通过相关接口准入申请的小程序 1)开发者因涉及营销活动,希望申请wx.getLocation接口便于帮助用户定位所在位置,但根据服务内容可知当前仅需获取用户所在城市/地区,无需通过wx.getLocation获取详细的经纬度定位,使用wx.getFuzzyLocation、wx.chooseLocation或wx.choosePoi接口实现上述场景 [图片] 2)开发者因涉及提供外卖平台服务,希望申请wx.onLocationchange接口监听用户实时地理位置运动轨迹,但根据服务内容可知当前外卖平台服务仅需要获取用户外卖收货地址,并不展示派送员实时位置,无需通过wx.onLocationchange获取用户的实时运动轨迹,使用wx.getFuzzyLocation、wx.chooseLocation或wx.choosePoi接口实现上述场景 [图片] 3) 开发者因涉及提供新闻资讯服务,希望申请wx.onLocationchange接口监听用户实时地理位置运动轨迹,但小程序内未含有相关使用场景,所以暂时不支持 [图片] 4)开发者因涉及线上商城发货服务,需用户提供收货地址,希望申请wx.getLocation接口获取用户当前详细的实时位置,如果需要获取用户的收货地址可以使用wx.chooseAdress接口一键导入。如果需要省去用户手动填写地址的流程,可以使用wx.chooseLocation或wx.choosePoi让用户自行选择当前地理位置,无需获取用户获取用户当前详细的实时位置。 [图片] 5)房地产、餐饮、商家自营等小程序,希望申请wx.getLocation接口获取用户当前实时位置信息,为用户展示附近、周边门店信息,提供推荐营销服务,且小程序内未提供线下门店导航服务,仅在小程序内为用户提供附近、周边门店信息展示服务,该场景不支持使用wx.getLocation、wx.onLocationchange这类高精度位置接口,建议开发者使用wx.getFuzzylocation、wx.chooseLocation或wx.choosePoi实现上述场景。 [图片] 三、接口准入申请的步骤 1)登录微信公众平台:https://mp.weixin.qq.com,进入小程序后台「首页」,左侧导航栏点击「开发管理」模块 [图片] 2)「开发管理」模块下「接口设置」 [图片] 3)找到需要申请的地理位置接口点击「去开通」进入接口申请页面 [图片] 4)进入接口申请页面后,在接口「申请原因」中详细描述申请接口在小程序内的使用场景,或选择性提供小程序的图片视频或网页辅助审核,最后点击「提交申请」即完成该接口申请 [图片] 5)接口申请审核结果可通过「接口设置」模块的接口状态,或「通知中心」站内信进行查看 [图片] [图片]
03-13 - 一张表解决云存储的七大痛点
就是这张表: Collection: material { _id, _openid, createTime, cat,//分类。比如衣服、帽子 tag,//标签。比如产品号等 fileID,//cloud云存储路径 url,//Cloud.getTempFileURL获取的http路径,云存储权限设置为公有读, type,//img, video, file size, name,//上传前文件名 ext,//文件后缀 } 说明: 1、用一张表保存所有云存储文件的信息; 2、文件上传后,将相关信息保存在集合中。 3、任何地方引用图片src,都是使用表中的url,而不是使用fileID, 解决了以下痛点: 痛点一、云存储里有哪些文件,有哪些垃圾文件? 痛点二、云存储某文件夹下有哪些文件?怎么删除云存储文件夹?不熟悉cloud base node sdk或者manage sdk的同学,一定搞不定这个痛点; 痛点三、图片太大,我想用腾讯云图像处理进行压缩裁剪?fileID不支持,只能用url; 痛点四、跨云环境访问图片,不支持fileID,只能用url; 痛点五、在前端引用url,但是删除图片做不到。即通过url,不知道fileID是什么,删除不了云存储文件; 痛点六、前端可以统一管理图片,素材库,而不是在某流程中上传文件后,完全不管理它; 痛点七、可对所有文件图片,分类、贴标签,按openid检索,按type检索,各种姿势检索。 可能还有其他好处,不多介绍。 总之,无论如何,你应该需要这样一张表。
2022-07-12 - 安全课堂|关于小程序AppSecret密钥泄露漏洞
为进一步提升小程序的安全性和用户体验,目前平台对提审的小程序均需进行安全检测,在检测过程中发现仍有许多小程序存在安全漏洞,其中涉及AppSecret密钥泄露漏洞,希望通过以下相关的漏洞介绍、案例分析和修复建议,开发者能更加了解如何对该漏洞进行防御。 一、漏洞介绍 AppSecret是小程序的唯一凭证密钥,也是获取小程序全局唯一后台接口调用凭证(access_token)的重要参数,需要开发者妥善保管至后台服务器中,并严格保密,不向任何第三方等透露。小程序若存在AppSecret密钥泄露漏洞的情况,会造成身份信息仿冒、敏感数据外泄等严重后果,开发者应及时发现该漏洞并快速修复相应问题。 二、漏洞案例 某小程序因为AppSecret泄露,导致攻击者可以通过调用API获取该小程序敏感数据,如接口调用凭证、用户信息、用户使用数据等,造成了极大的安全风险。 通过以下展示我们可以明晰该小程序敏感数据外泄的原因,测试者先对小程序网络请求进行抓包,发现请求响应中包含了appid和AppSecret敏感信息: [图片] 通过上述获取的appid和AppSecret敏感信息,可以利用接口获取到相应的access_token: [图片] [图片] 最后可以实现使用access_token调用该小程序所有后台接口的目的,后台服务端接口已涵盖数据、运维、消息等多方面场景能力。 [图片] 下面我们再具体举几个利用access_token调用小程序后台接口的例子: 1.获取小程序用户评论 [图片] 2.获取小程序用户访问数据 [图片] 3.冒用小程序身份给用户发送消息 [图片] [图片] AppSecret密钥泄露漏洞其他的危害包括但不限于:冒用小程序身份给用户发送客服消息/模板消息、获取小程序session_key(用于解密微信侧提供的用户敏感数据)、获取小程序运维信息、日志等敏感信息、更改小程序相关的配置等。 三、漏洞修复 若小程序存在相应的AppSecret密钥泄露漏洞问题,请开发者尽快根据以下修复指引进行调整,以便消除风险: 1.后端API接口请勿把AppSecret敏感信息返回给前端(包括前端请求或小程序代码内传输、记录AppSecret); 2.立即登录小程序管理后台,在【开发-开发管理-开发设置】中对AppSecret进行重置。由于Appsecret存在历史泄露且仍然有效,务必进行重置才可消除风险,以免被攻击者恶意利用,请尽快按指引进行修复; 3.对AppSecret进行重置后,请及时修改后台代码,以免无法使用微信API [图片] 其他常见问题 Q1: 小程序提审不通过,显示小程序AppSecret存在历史泄露且仍然有效,是否需要重置AppSecret? A1: 需要,请重置AppSecret后再提审,若审核通过,说明该问题已消除,若审核不通过,说明仍存在明文的AppSecret,需进一步排查并去除AppSecret字段及其对应值。 Q2: 重置小程序AppSecret会影响到线上小程序吗? A2: auth.getAccessToken需要使用AppSecret进行调用入参,重置AppSecret后,如果用新的AppSecret去获取access_token,那么旧的access_token会在5分钟内失效,如果未使用新的AppSecret,旧的access_token会在两小时内失效,故即使重置AppSecret,access_token仍有一定的缓冲期,可及时修改后台代码,不会对线上小程序造成影响。 相关文章 安全课堂|关于小程序session_key泄露漏洞安全课堂|关于小程序云AK/SK泄露漏洞 如有其他相关疑问,欢迎随时参与官方社区讨论。
2022-09-09 - 安全课堂|关于小程序session_key泄露漏洞
为进一步提升小程序的安全性和用户体验,目前平台对提审的小程序均需进行安全检测,在检测过程中发现仍有许多小程序存在安全漏洞,其中涉及session_key泄露漏洞,希望通过以下相关的漏洞介绍、案例分析和修复建议,开发者能更加了解如何对该漏洞进行防御。 一、漏洞介绍 为了保证数据安全,微信会对用户数据进行加密传输处理,所以小程序在获取微信侧提供的用户数据(如手机号)时,就需要进行相应的解密,这就会涉及到session_key,具体流程可参考开放数据校验与解密开发文档。 session_key指的是会话密钥,可以简单理解为微信开放数据AES加密的密钥,它是微信服务器给开发者服务器颁发的身份凭证,这个数据正常来说是不能通过任何方式泄露出去的。小程序若存在session_key泄露漏洞的情况,则代表微信侧传递的用户数据有被泄露、篡改等风险,开发者应及时发现该漏洞并快速修复相应问题。 [图片] 二、漏洞案例 某小程序因为session_key泄露,导致该小程序可以使用任意手机号进行登录,造成了极大的安全风险。 我们可以很明显地看到,下列请求中的session_key已经被泄露: [图片] 通过获取该session_key,我们可以结合iv解密出密文: [图片] 只需如下脚本即可进行解密,所以攻击者也可利用同样的信息去篡改用户数据,然后加密后返回给服务器,从而达到使用任意手机号进行登录的目的。 [图片] 三、漏洞修复 通过上述案例,我们了解到session_key泄露会对小程序造成的危害,而导致session_key泄露的原因则可能有以下两种: 1.通过auth.code2Session接口获取用户openid时,返回小程序的数据中包含了session_key字段,以泄露的url:/api/get_openid.php?code=xxxx为例,具体的表现如下图所示: [图片] 查看后端get_openid.php的源码,经排查发现$response 变量包含了session_key字段,开发者应去掉变量中的session_key字段,若需获取openid,应只提取该字段返回小程序即可。 [图片] 2.在解密开放数据时,使用了错误的方式,以获取手机号接口为例,通过事件回调获取微信服务器返回的加密数据(encryptedData和iv)后,将服务端中的session_key传送至小程序前端,直接在前端进行解密: [图片] 这种方式是绝对不可取的,正确的流程应该是将加密数据(encryptedData和iv)传至服务端后,结合服务端中的session_key进行解密获取手机号,然后返回给小程序。另外,目前平台已对获取手机号接口进行了安全升级,建议开发者使用新版本,以增强小程序的安全性。 若小程序存在相应的session_key泄露漏洞问题,请开发者尽快自查并修复漏洞: 请尽快在网络请求中,去除请求和响应中的session_key字段及其对应值,后续也不应该将session_key传到小程序客户端等服务器外的环境,以便消除风险。 其他常见问题 Q1: 如何进行相应的修复,是需要把session_key字段更换个名字就可以了吗? A1: 不是,更换字段名无法从根本上消除风险,session_key这个字段及对应值不应该传到小程序客户端等服务器外的环境,需去除请求和响应中的所有相关信息,才可对该漏洞问题进行修复。 Q2: 解密开放数据的正确方式是什么? A2: 以获取手机号接口为例,通过事件回调获取微信服务器返回的加密数据(encryptedData和iv),将加密数据传至服务端后,结合服务端中的session_key进行解密获取手机号,然后返回给小程序。而不应将服务端中的session_key传送至小程序前端,直接在前端进行解密。 相关文章 安全课堂|关于小程序AppSecret密钥泄露漏洞安全课堂|关于小程序云AK/SK泄露漏洞 如有其他相关疑问,欢迎随时参与官方社区讨论。
2022-09-09 - 微信小程序,支付,退款,查询订单(支付篇)
微信支付功能首先必须开启以下几项授权 1.登录微信公众平台>点击功能>点击微信支付 确保已经授权微信商户号 [图片] 2.打开微信小程序开发者功具>点击云开发>点击设置>点击其它设置 确保微信支付配置三项已全部授权才能正常使用退款功能,但不影响支付功能[图片] 微信支付功能建议使用云开发,这样就不需要签名。小程序密钥等等! 接下来就可以开始微信支付以及其他功能了! 微信支付功能 wxml: <button bindtap="Buy">立即购买</button> js: // 首先在page({})外层声明随机数 var random=Math.floor((Math.random() * 100000) + 1) var random2=Math.floor((Math.random() * 1000000) + 1) page({ data:{ //在data中赋值一个唯一的订单号 outTradeNo:random+"9527"+new Date().getTime()+random2,//随机生成的订单号 } // 购买套餐 Buy(e){ //调用云函数 wx.cloud.callFunction({ name: 'buy', //填写云函数名 data:{ //data里的数据用于向云函数后台提交数据 name:"商品名称", //提交商品名称 totalFee:"商品的金额"*100, // 提交商品的金额,因为单位是(分)所以要*100确保 金额准确性 outTradeNo, // 提交订单号,这个是自己生成的订单号,确保唯一性 } }).then(res=>{ //成功回调 console.log(res) const payment = res.result.payment //如果回调成功会抛出一些订单信息 var that = this wx.requestPayment({ //调用微信支付api接口 ...payment, //这里就是上面拿到的订单信息 注意:这个前面是三个点 success (res) { //程序走到这里就是支付成功了 console.log('支付成功', res) }, fail (err) { //如果取消支付会走这一步 console.error('支付失败', err) } }) }) }, onLoad(){ this.setData({ //当每次进入页面时可以重置data中的随机数,否则随机数会一直保持一个数,当然为了严谨也可以使用雪花算法确保订单信息的唯一 outTradeNo:random+"9527"+new Date().getTime()+random2,//随机生成的订单号 }) } }) 支付云函数代码const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV //当前云函数的环境id这个值是默认第一个当然自己也可以定义 }) exports.main = async (event, context) => { const res = await cloud.cloudPay.unifiedOrder({ //注意这里是微信支付的代码和普通使用云函数调用数据库不一样一定要写 "body" : event.name,//商品名称 "outTradeNo" : event.outTradeNo,//订单编号唯一的自己生成的支付订单号 "spbillCreateIp" : "127.0.0.1", //这里是默认地址一般可以不用动 "subMchId" : "商户号",//商户号 这个是授权支付配置的商户号 "totalFee" : event.totalFee,// 订单金额以分为单位,js中已经*100了就直接调用就可以了 "envId": "你的云函数环境id", //当前云函数的环境id "functionName": "buy" //当前云函数的名称 }) return res //向前端代码抛出订单信息 }
2022-03-09 - 微信小程序,支付,退款,查询订单(退款篇)
微信退款功能wxml: 订单退款 js: //退款 refund() { var tk = "T1999" + new Date().getTime() //这个是退款单号和支付的时候支付单号一样 的都是自己生成的一串数字 wx.cloud.callFunction({ //调用云函数 name: 'refund', //填写云函数名称 data: { //向云函数抛出数据 refund: tk ,//商户退款单号 trade: ”商户订单号“,//商户当时支付的订单号,也就是咱们支付功能给他随机生成的数字,填写在这里 total_fee,//商品的订单金额 refund_fee,//申请商品的退款金额 }, success: res => { //退款成功回调 console.log("获取退款参数成功", res) }, fail: res => { //退款失败回调 console.log("获取退款参数失败", res) }, }) }, 退款云函数代码// 云函数代码 //申请退款 const cloud = require('wx-server-sdk') cloud.init({ env: 'user-3g8pqkczf4cfb983' }) exports.main = async (event, context) => { const res = await cloud.cloudPay.refund({ //这个是退款的函数代码一定要写对 "out_refund_no" : event.refund,//商户退款单号自己随机生成的 "out_trade_no" : event.trade,//商户支付订单号,用户支付时候的订单号 "nonce_str" : ""+new Date().getTime(),//随机字符串这个不用管就这么写 "sub_mch_id" : "1559727331",//子商户号这个是授权支付配置的商户号 "total_fee" : event.total_fee,//商品订单的金额 "refund_fee":event.refund_fee,//申请商品退款的金额 }) return res }
2022-03-10 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - 小程序调试新方案——使用WeConsole监控console/network/api/component/storage
[图片] 一、背景与简介 在传统的 PC Web 前端开发中,浏览器为开发者提供了体验良好、功能丰富且强大的开发调试工具,比如常见的 Chrome devtools 等,这些调试工具极大的方便了开发者,它们普遍提供查看页面结构、监听网络请求、管理本地数据存储、debugger 代码、使用 Console 快速显示数据等功能。 但是在近几年兴起的微信小程序的前端开发中,却少有类似的体验和功能对标的开发调试工具出现。当然微信小程序的官方也提供了类似的工具,那就是 vConsole,但是相比 PC 端提供的工具来说确实无论是功能和体验都有所欠缺,所以我们开发了 weconsole 来提供更加全面的功能和更好的体验。 基于上述背景,我们想开发一款运行在微信小程序环境上,无论在用户体验还是功能等方面都能媲美 PC 端的前端开发调试工具,当然某些(如 debugger 代码等)受限于技术在当前时期无法实现的功能我们暂且忽略。 我们将这款工具命名为[代码]Weimob Console[代码],简写为[代码]WeConsole[代码]。 项目主页:https://github.com/weimobGroup/WeConsole 二、安装与使用 1、通过 npm 安装 [代码]npm i weconsole -S [代码] 2、普通方式安装 可将 npm 包下载到本地,然后将其中的[代码]dist/full[代码]文件夹拷贝至项目目录中; 3、引用 WeConsole 分为[代码]核心[代码]和[代码]组件[代码]两部分,使用时需要全部引用后方可使用,[代码]核心[代码]负责重写系统变量或方法,以达到全局监控的目的;[代码]组件[代码]负责将监控的数据显示出来。 在[代码]app.js[代码]文件中引用[代码]核心[代码]: [代码]// NPM方式引用 import 'weconsole/init'; // 普通方式引用 import 'xxx/weconsole/init'; [代码] 引入[代码]weconsole/init[代码]后,就是默认将 App、Page、Component、Api、Console 全部重写监控!如果想按需重写,可以使用如下方式进行: [代码]import { replace, restore, showWeConsole, hideWeConsole } from 'weconsole'; // scope可选值:App/Page/Component/Console/Api // 按需替换系统变量或函数以达到监控 replace(scope); // 可还原 restore(scope); // 通过show/hide方法控制显示入口图标 showWeConsole(); [代码] 如果没有显式调用过[代码]showWeConsole/hideWeConsole[代码]方法,组件第一次初始化时,会根据小程序是否[代码]开启调试模式[代码]来决定入口图标的显示性。 在需要的地方引用[代码]组件[代码],需要先将组件注册进[代码]app/page/component.json[代码]中: [代码]// NPM方式引用 "usingComponents": { "weconsole": "weconsole/components/main/index" } // 普通方式引用 "usingComponents": { "weconsole": "xxx/weconsole/components/main/index" } [代码] 然后在[代码]wxml[代码]中使用[代码]<weconsole>[代码]标签进行初始化: [代码]<!-- page/component.wxml --> <weconsole /> [代码] [代码]<weconsole>[代码]标签支持传入以下属性: [代码]properties: { // 组件全屏化后,距离窗口顶部距离 fullTop: String, // 刘海屏机型(如iphone12等)下组件全屏化后,距离窗口顶部距离 adapFullTop: String, } [代码] 4、建议 如果不想将 weconsole 放置在主包中,建议将组件放在分包内使用,利用小程序的 分包异步化 的特性,减少主包大小 三、功能 1、Console 界面如图 1 实时显示[代码]console.log/info/warn/error[代码]记录; [代码]Filter[代码]框输入关键字已进行记录筛选; 使用分类标签[代码]All, Mark, Log, Errors, Warnings...[代码]等进行记录分类显示,分类列表中[代码]All, Mark, Log, Errors, Warnings[代码]为固定项,其他可由配置项[代码]consoleCategoryGetter[代码]产生 点击[代码]🚫[代码]按钮清空记录(不会清除[代码]留存[代码]的记录) [代码]长按[代码]记录可弹出操作项(如图 2): [代码]复制[代码]:将记录数据执行复制操作,具体形式可使用配置项[代码]copyPolicy[代码]指定,未指定时,将使用[代码]JSON.stringify[代码]序列化数据,将其复制到剪切板 [代码]取消置顶/置顶显示[代码]:将记录取消置顶/置顶显示,最多可置顶三条(置顶无非是想快速找到重要的数据,当重要的数据过多时,就不宜用置顶了,可以使用[代码]标记[代码]功能,然后在使用筛选栏中的[代码]Mark[代码]分类进行筛选显示) [代码]取消留存/留存[代码]:留存是指将记录保留下来,使其不受清除,即点击[代码]🚫[代码]按钮不被清除 [代码]取消全部留存[代码]:取消所有留存的记录 [代码]取消标记/标记[代码]:标记就是将数据添加一个[代码]Mark[代码]的分类,可以通过筛选栏快速分类显示 [代码]取消全部标记[代码]:取消所有标记的记录 [图片] 图 1 [图片] 图 2 2、Api 界面如图 3 实时显示[代码]wx[代码]对象下的相关 api 执行记录 [代码]Filter[代码]框输入关键字已进行记录筛选 使用分类标签[代码]All, Mark, Cloud, xhr...[代码]等进行记录分类显示,分类列表由配置项[代码]apiCategoryList[代码]与[代码]apiCategoryGetter[代码]产生 点击[代码]🚫[代码]按钮清空记录(不会清除[代码]留存[代码]的记录) [代码]长按[代码]记录可弹出操作项(如图 4): [代码]复制[代码]:将记录数据执行复制操作,具体形式可使用配置项[代码]copyPolicy[代码]置顶,未指定时,将使用系统默认方式序列化数据(具体看实际效果),将其复制到剪切板 其他操作项含义与[代码]Console[代码]功能类似 点击条目可展示详情,如图 5 [图片] 图 3 [图片] 图 4 [图片] 图 5 3、Component 界面如图 6 树结构显示组件实例列表 根是[代码]App[代码] 二级固定为[代码]getCurrentPages[代码]返回的页面实例 三级及更深通过[代码]this.selectOwnerComponent()[代码]进行父实例定位,进而确定层级 点击节点名称(带有下划虚线),可显示组件实例详情,以 JSON 树的方式查看组件的所有数据,如图 7 [图片] 图 6 [图片] 图 7 4、Storage 界面如图 8 显示 Storage 记录 [代码]Filter[代码]框输入关键字已进行记录筛选 点击[代码]🚫[代码]按钮清空记录(不会清除[代码]留存[代码]的记录) [代码]长按[代码]操作项含义与[代码]Console[代码]功能类似 点击条目后,再点击[代码]❌[代码]按钮可将其删除 点击[代码]Filter[代码]框左侧的[代码]刷新[代码]按钮可刷新全部数据 点击条目显示详情,如图 9 [图片] 图 8 [图片] 图 9 5、其他 界面如图 10 默认显示 系统信息 可通过[代码]customActions[代码]配置项进行界面功能快速定制,也可通过[代码]addCustomAction/removeCustomAction[代码]添加/删除定制项目 几个简单的定制案例如下,效果如图 11: [代码]import { setUIRunConfig } from 'xxx/weconsole/index.js'; setUIRunConfig({ customActions: [ { id: 'test1', title: '显示文本', autoCase: 'show', cases: [ { id: 'show', button: '查看', showMode: WcCustomActionShowMode.text, handler(): string { return '测试文本'; } }, { id: 'show2', button: '查看2', showMode: WcCustomActionShowMode.text, handler(): string { return '测试文本2'; } } ] }, { id: 'test2', title: '显示JSON', autoCase: 'show', cases: [ { id: 'show', button: '查看', showMode: WcCustomActionShowMode.json, handler() { return wx; } } ] }, { id: 'test3', title: '显示表格', autoCase: 'show', cases: [ { id: 'show', button: '查看', showMode: WcCustomActionShowMode.grid, handler(): WcCustomActionGrid { return { cols: [ { title: 'Id', field: 'id', width: 30 }, { title: 'Name', field: 'name', width: 70 } ], data: [ { id: 1, name: 'Tom' }, { id: 2, name: 'Alice' } ] }; } } ] } ] }); [代码] [图片] 图 10 [图片] 图 10 四、API 通过以下方式使用 API [代码]import { showWeConsole, ... } from 'weconsole'; showWeConsole(); [代码] replace(scope:‘App’|‘Page’|‘Component’|‘Api’|‘Console’) 替换系统变量或函数以达到监控,底层控制全局仅替换一次 restore(scope:‘App’|‘Page’|‘Component’|‘Api’|‘Console’) 还原被替换的系统变量或函数,还原后界面将不在显示相关数据 showWeConsole() 显示[代码]WeConsole[代码]入口图标 hideWeConsole() 隐藏[代码]WeConsole[代码]入口图标 setUIConfig(config: Partial<MpUIConfig>) 设置[代码]WeConsole[代码]组件内的相关配置,可接受的配置项及含义如下: [代码]interface MpUIConfig { /**监控小程序API数据后,使用该选项进行该数据的分类值计算,计算后的结果显示在界面上 */ apiCategoryGetter?: MpProductCategoryMap | MpProductCategoryGetter; /**监控Console数据后,使用该选项进行该数据的分类值计算,计算后的结果显示在界面上 */ consoleCategoryGetter?: MpProductCategoryMap | MpProductCategoryGetter; /**API选项卡下显示的数据分类列表,all、mark、other 分类固定存在 */ apiCategoryList?: Array<string | MpNameValue<string>>; /**复制策略,传入复制数据,可通过数据的type字段判断数据哪种类型,比如api/console */ copyPolicy?: MpProductCopyPolicy; /**定制化列表 */ customActions?: WcCustomAction[]; } /**取数据的category字段值对应的prop */ interface MpProductCategoryMap { [prop: string]: string | MpProductCategoryGetter; } interface MpProductCategoryGetter { (product: Partial<MpProduct>): string | string[]; } interface MpProductCopyPolicy { (product: Partial<MpProduct>); } /**定制化 */ interface WcCustomAction { /**标识,需要保持唯一 */ id: string; /**标题 */ title: string; /**默认执行哪个case? */ autoCase?: string; /**该定制化有哪些情况 */ cases: WcCustomActionCase[]; } const enum WcCustomActionShowMode { /**显示JSON树 */ json = 'json', /**显示数据表格 */ grid = 'grid', /** 固定显示<weconsole-customer>组件,该组件需要在app.json中注册,同时需要支持传入data属性,属性值就是case handler执行后的结果 */ component = 'component', /**显示一段文本 */ text = 'text', /**什么都不做 */ none = 'none' } interface WcCustomActionCase { id: string; /**按钮文案 */ button?: string; /**执行逻辑 */ handler: Function; /**显示方式 */ showMode?: WcCustomActionShowMode; } interface WcCustomActionGrid { cols: DataGridCol[]; data: any; } [代码] addCustomAction(action: WcCustomAction) 添加一个定制化项目;当你添加的项目中需要显示你自己的组件时: 请将 case 的[代码]showMode[代码]值设置为[代码]component[代码] 在[代码]app.json[代码]中注册名称为[代码]weconsole-customer[代码]的组件 定制化项目的 case 被执行时,会将执行结果传递给[代码]weconsole-customer[代码]的[代码]data[代码]属性 开发者根据[代码]data[代码]属性中的数据自行判断内部显示逻辑 removeCustomAction(actionId: string) 根据 ID 删除一个定制化项目 getWcControlMpViewInstances():any[] 获取小程序内 weconsole 已经监控到的所有的 App/Page/Component 实例 log(type = “log”, …args) 因为 console 被重写,当你想使用最原始的 console 方法时,可以通过该方式,type 就是 console 的方法名 on/once/off/emit 提供一个事件总线功能,全局事件及相关函数定义如下: [代码]const enum WeConsoleEvents { /**UIConfig对象发生变化时 */ WcUIConfigChange = 'WcUIConfigChange', /**入口图标显示性发生变化时 */ WcVisableChange = 'WcVisableChange', /**CanvasContext准备好时,CanvasContext用于JSON树组件的界面文字宽度计算 */ WcCanvasContextReady = 'WcCanvasContextReady', /**CanvasContext销毁时 */ WcCanvasContextDestory = 'WcCanvasContextDestory', /**主组件的宽高发生变化时 */ WcMainComponentSizeChange = 'WcMainComponentSizeChange' } interface IEventEmitter<T = any> { on(type: string, handler: EventHandler<T>); once(type: string, handler: EventHandler<T>); off(type: string, handler?: EventHandler<T>); emit(type: string, data?: T); } [代码] 五、后续规划 优化包大小 单元测试 体验优化 定制化升级 基于网络通信的界面化 weconsole 标准化 支持 H5 支持其他小程序平台(支付宝/百度/字节跳动) 六、License WeConsole 使用 MIT 协议. 七、声明 生产环境请谨慎使用。
2021-07-14 - 微信小程序实现i18n国际化能力的解决方案
需求背景介绍 现在有大量的外国客户会使用到我们的产品,从而产生了需要有通过切换语种达到显示不同语种文案的页面,用来满足外国客户的浏览使用。而如今市面上有很多流行的第三方库,可以实现这种能力,如vue-i18n。而微信原生小程序的运行环境是将WebViewe与AppService分层处理,所以需要自研一套支持在微信原生小程序技术框架上运行的i18n库。 设计概要 [图片] 主要模块。 实现一个下载进程器。保证多个相同的CDN请求时,只发一条CDN的正常下载。 实现一个观察者,用于在异步数据响应成功后通知相关的页面或组件进行更新。 实现一个缓存器,用于将CDN上获取的语言包内容缓存起来,避免重复请求。 实现一个i18n类,主要用于实例后的对外api整理。 实现一个package类,主要用于集成下载进程器、观察者、缓存器及实例后的api整理。 WXS(微信脚本),这个模块主要是实现i18n.t方法及与AppService层的通信。 主要的功能 i18n库提供install方法,用于集成在RPRM或未来的其它平台上。install方法返回一个i18n实例,该实例内置以下属性: 方法 作用描述 参数 备注 data 所有缓存的模块语言包内容,按package名分包。如:[代码]wx.rprm.i18n.data.package1[代码] [代码]wx.rprm.i18n.data.package2[代码] 不涉及 只缓存当前语种的语言包内容。 getCurrentLang() 获取当前语种的函数。 无 swtich({lang}) 全量切换语种。 {lang:string} use(options} 预加载模块语种 {package:string,version:string/number} globalConfig(config) 通过路由划分,实现全量配置模块描述。 object 开发者希望使用的方式 [代码]//语法 i18n.t('key','default',options:object|array) //具名格式 //对应的语言包格式 ... spec:'规格:{num}个' ... //I18n.t的使用方式 ... {{i18n.t('spec',{num:'100'})}} ... //结果 //规格:100个 //列表格式 //对应的语言包格式 ... spec:'规格:第{0}百零{1}个' ... //I18n.t的使用方式 ... {{i18n.t('spec', ['一','三'])}} ... //结果 //规格:第一百零三个 [代码] WXS(微信脚本) 由于i18n库涉及的模块比较多,所以这里说明WXS(微信脚本)模块是如何实现t方法及与AppService层实现通信能力。 如何在WXML上使用i18n.t方法 WXML上无法直接使用方法,所以要借助微信脚本(WXS)完成。声明一个WXS文件,在里面实现t方法,然后在WXML中引入这个WXS文件,其module的值设置为i18n。参考代码: [代码]//WXS function t(参数){ //实现功能 } module.exports = {t} [代码] [代码]//WXML <wxs src='路径/名称.wxs' module='i18n'></wxs> <view>{{i18n.t(参数)}}</view> [代码] 如何在微信脚本(WXS)上得到语言包内容 微信脚本(WXS)是WebView里的一个脚本,想得到AppService里的数据,是需要实现数据通信的能力。借助setData触发微信脚本(WXS)的WxspropObserver。 [图片] 参考代码: [代码]<wxs src='路径/名称.wxs' module='i18n'></wxs> <view change:prop="{{i18n.change}}" prop="{{langs}}"></view> [代码] [代码]//WXS function t(参数){ //实现功能 } function change(langs){ //langs参数就是AppService层的setData传递的值 } module.exports = {t} [代码] 上述代码的基本思路是,在view元素上的属性值绑定AppService层中data中的langs。用setData触发langs更新到Webview层的view元素上,然后用WxsPropObserver监听prop是否发生变化,变化了就触发i18n脚本模块中的change方法。change方法触发后会得到prop的值,也就是langs的值。 t方法格式化 [代码]... /** * 格式化列表 * 场景二:”Hello,{0}{1} = "Hello,world!" * */ var formatterByList = function(text, list = []) { //遍历数组 //利用正则将花括号内容捕获匹配的内容替换成对应下标的数组值。 return text; }; /** * 具名格式化 * 场景一:"Hello,{msg}" = "Hello,world“ * */ var formatterByName = function(text, names = {}) { //利用正则分析出花括号里的值 //在names对象里找到花括号里值的内容 return text; }; ... [代码] 总结 上述主要是说明如何在WXML使用t方法以及将语言包内容同步到微信脚本上。实际的项目中需要考虑到将i18n功能模块化,异步数据获取监听及通知到微信脚本,性能、缓存等问题。
2021-05-25 - 『前端工程』—— 封装Vue第三方组件的三板斧
前言在封装第三方组件中,经常会遇到一个问题,如何通过封装的组件去使用第三方组件的Attributes(属性)、Events(自定义事件)、Methods(方法)、Slots(插槽)。 当然这个问题并不是难以解决,用普通方法解决难免陷入繁琐重复的工作中,而且封装的组件代码可读性也不高。 本专栏将介绍三种技巧来使用第三方组件的Attributes(属性)、Events(自定义事件)、Slots(插槽),至于使用第三方组件的Methods(方法)的技巧还待优化,所以称为三板斧哈。 一、使用第三方组件的属性 [图片] 封装一个elementUI的el-input输入框组件称为myInput,若要在myInput组件上添加一个[代码]disabled[代码]属性来禁用输入框,要如何实现呢?一般同学会这么做 //myInput.vue export default { props: { value: { type: String, default: '', }, disabled: { type: Boolean, default: false } }, computed: { inputVal: { get() { return this.value; }, set(val) { this.$emit('input', val); } } } } 过一段时间后又要在myInput组件上添加el-input组件的其它属性,el-input组件总共有27个多属性,那该怎么呢,难道一个个用prop传进去,这样不仅繁琐而且可读性差,可以用[代码]$attrs[代码]一步到位,先来看一下[代码]attrs[代码]的官方定义。 [代码]$attrs[代码]: 包含了父作用域中不作为 [代码]prop[代码] 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何[代码]prop[代码] 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 [代码]v-bind="$attrs"[代码] 传入内部组件 //myInput.vue 这还不够,还得把[代码]inheritAttrs[代码]选项设置为[代码]false[代码],为什么呢,来看一下[代码]inheritAttrs[代码]选项的官方定义就明白了。 默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 [代码]inheritAttrs[代码] 为 [代码]false[代码],这些默认行为将会被去掉。而通过 [代码]$attrs[代码] 可以让这些 attribute 生效,且可以通过 [代码]v-bind[代码] 显性的绑定到非根元素上。注意:这个选项不影响 class 和 style 绑定。 简单来说,把[代码]inheritAttrs[代码]设置为[代码]false[代码],避免给myInput组件设置的属性被添加到myInput组件的根元素div上。 //myInput.vue export default { inheritAttrs: false, props: { value: { type: String, default: '', }, }, computed: { inputVal: { get() { return this.value; }, set(val) { this.$emit('input', val); } } } } 这样设置后,在myInput组件上就可以直接使用el-input组件的属性,不管后续el-input组件再增加了多少个属性。 二、使用第三方组件的自定义事件 [图片] 若在myIpput组件上使用el-input组件上自定义的事件呢,可能你的第一反应是[代码]this.$emit[代码]。 //myInput.vue export default { inheritAttrs: false, props: { value: { type: String, default: '', }, }, computed: { inputVal: { get() { return this.value; }, set(val) { this.$emit('input', val); } } }, methods: { blur() { this.$emit('blur') } } } el-input组件有4个自定义事件,还不算多,假如遇到自定义事件更多的第三方组件,要怎么办,难道一个一个添加进去,这样会增加一堆非必要的methods,其实可以用[代码]$listeners[代码]一步到位,先来看一下[代码]$listeners[代码]的官方定义。 [代码]$listeners[代码]:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。 //myInput.vue 那么在myInput组件中给el-input组件添加上[代码]v-on="$listeners"[代码],就可以在myInput组件上使用el-input组件自定义的事件。 三、使用第三方组件的插槽[图片] 若在myIpput组件上使用el-input组件上定义的插槽呢?这个没有多少取巧的方法,第三方组件定义多少个插槽,在封装的时候都得用[代码]slot[代码]标签暴露出去。比如暴露el-input组件中的prefix插槽,代码如下所示: //myInput.vue 四、使用第三方组件的方法 [图片] 利用[代码]ref[代码]来实现,首先在myInput组件中的el-input组件上添加一个[代码]ref="elInput"[代码]属性, //myInput.vue 这里要注意父子组件的[代码]mounted[代码]的执行时机,因为一般el-input组件是全局引入的,相当同步引入组件,此时el-input组件的[代码]mounted[代码]会比myInput组件的[代码]mounted[代码]先执行,所以可以在myInput组件的[代码]mounted[代码]中把[代码]this.$refs.elInput[代码]赋值到myInput组件的[代码]this[代码]的一个属性上。 myInput组件如何使用el-input组件的方法分两种情况,跟myInput组件的引入有关系。 假如myInput组件是同步引入的 import myInput from './myInput.vue'; export default { data() { return { } }, components: { myInput, }, mounted() { //调用el-input组件的focus方法 this.$refs.myInput.elInput.focus(); } } 假如myInput组件是异步引入的 export default { data() { return { } }, components: { myInput: () => import('./myInput.vue') }, mounted() { //调用el-input组件的focus方法 setTimeout(() => { this.$refs.myInput.elInput.focus(); }) } } 作者:红尘炼心 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi
2021-03-27 - 2019-07-12
- IconFont的高阶用法,提高开发效率。
介绍 适用于各平台的小程序的icon自定义组件,结合iconfont使用,更加方便易扩展。关于iconfont的使用方法参考前面的文章《如何更优雅的使用IconFont你应该知道》 安装 通过npm安装 需要注意的是 package.json 和 node_modules 必须在 miniprogram 目录下 [代码]npm install miniprogram_icon [代码] 构建npm包 打开微信开发者工具,点击 工具 -> 构建 npm,并勾选 使用 npm 模块 选项,构建完成后,即可引入组件 [图片] 使用 引入组件 只需要在app.json或index.json中配置 Icon 对应的路径即可。如果你是通过下载源代码的方式使用 miniprogram_icon,请将路径修改为项目中 miniprogram_icon 所在的目录。 [代码]// 全局引入 // app.json "usingComponents": { "l-icon": "miniprogram_icon/icon/index" } // 单页面引入 // index.json "usingComponents": { "l-icon": "miniprogram_icon/icon/index" } [代码] 使用组件 引入组件后,可以在 wxml 中直接使用组件 [代码]<l-icon name="icon-zuanshi_o"></l-icon> [代码] 代码演示 这里有一份微信小程序的代码片段,下载开发工具打开即可预览,打开代码片段 基础用法 Icon的[代码]name[代码]属性传入图标名称 [代码]// 线性图标 <l-icon name="icon-sousuo_o" /> // 加粗图标 <l-icon name="icon-sousuo" /> [代码] 图片代替 Icon的[代码]src[代码]属性传入图片地址 [代码]// 线性图标 <l-icon src="/assets/icons/shouye_o.svg" /> // 加粗图标 <l-icon src="/assets/icons/shouye.svg" /> [代码] 徽标提示 设置[代码]dot[代码]属性后,会在图标右上角展示一个小红点。设置[代码]info[代码]属性后,会在图标右上角展示相应的徽标 [代码]// 圆点 <l-icon name="icon-tongzhizhongxin_o" dot /> // 数字 <l-icon name="icon-tongzhizhongxin_o" info="1" /> // 自定义数字 <l-icon name="icon-tongzhizhongxin_o" info="99" /> // 超长数字 <l-icon name="icon-tongzhizhongxin_o" info="99+" /> [代码] 图标颜色 设置[代码]color[代码]属性来控制图标颜色,设置[代码]dot-bg[代码]属性来控制圆点颜色 [代码]// 自定义颜色 <l-icon name="icon-zuanshi_o" color="blue" /> // 16进制颜色 <l-icon name="icon-zuanshi_o" color="#20bf64" /> // 圆点颜色 <l-icon name="icon-xiaoxi_o" color="#db524a" dot dot-bg="#fba929" /> // 数字背景色 <l-icon name="icon-xiaoxi_o" info="123" dot-bg="#fba929" /> [代码] 图标大小 设置[代码]size[代码]属性来控制图标大小 [代码]// 支持像素单位px/rpx/em <l-icon name="icon-yinliang_o" size="40rpx" /> <l-icon name="icon-yinliang_o" size="50rpx" /> <l-icon name="icon-yinliang_o" size="60rpx" /> <l-icon name="icon-yinliang_o" size="70rpx" /> [代码] 加载图标 Icon的[代码]loading[代码]属性可旋转图标 [代码]// 好像iconfont的图标有点问题,旋转的时候有点晃动,可用图片来代替 <l-icon name="icon-jiazai_dan_o" loading /> <l-icon name="icon-jiazai_shuang_o" loading /> [代码] API Props 参数 说明 类型 name 图标名称 string src 图片地址 string dot 是否显示图标右上角小红点 boolean dotBg 圆点颜色,或者文字提示背景色 string info 图标右上角文字提示 string/number color 图标颜色 string size 图标大小,如 20px,20rpx,默认单位为rpx string loading 是否使用加载属性 boolean Event 事件名 说明 参数 bind:click 点击图标时触发 - 其他 如何使用其他iconfont图标,可用找到[代码]miniprogram_npm[代码]目录下的[代码]miniprogram_icon[代码]目录找到[代码]icon/iconfont.wxss[代码],替换即可。注意[代码]index.wxss[代码]里的[代码]@import[代码]路径是否正确。
2020-07-26 - 【一】从零实现商城多规格sku
前言 在商城里产品的spu、sku展示是很重要的一部分,常见的商城一般没有sku的概念,会把一个多规格的sku拆分成多个spu从而让用户选择。这样是最简单的做法,但是需求是真的跟不上,一般boss都会要求做多规格的sku选择。下面分享一个多规格sku实现的思路以及过程。 效果图 [图片] [图片] 数据分析 要实现sku首先要知道是什么数据组合成的sku列表,下面大概说说我自己sku数据格式。 [代码]{ "code": "1@0-0#1-0#2-0", "specs": [ { "id": "0-0", "key": "颜色", "value": "白色" }, { "id": "1-0", "key": "图案", "value": "圆点" }, { "id": "2-0", "key": "尺码", "value": "XXL" } ] } [代码] [代码]code[代码] 表示一个sku,在当前sku_list数据中是唯一存在的,后续都得通过 [代码]code[代码] 来查找sku数据。 [代码]specs[代码] 表示sku的规格信息,[代码]id[代码] 也表示是当前specs里唯一的值,如果用关系型数据库,可能数据库设计的时候,这个id会是子表的id主键,通过id去关联查询对应的数据,我这里[代码]0-0[代码] 则用预先定义好的规格key的下标来表示,其实跟关系型数据库的主键id一样。只要是唯一不会冲突即可,语义化之后等同于 [代码]颜色: 白色[代码],仔细观察其实是有规矩可行的,id会跟code对应起来。 视图规格列表 上面是定义的接口数据,并不能直接在视图上渲染成多规格的样式,因为还需要将多个sku的数据进行转换才能得到视图所见的sku列表。 如何转换数据 接口数据遍历如下: 黑色 圆点 XXL 白色 条纹 S 红色 卡通 L 视图渲染所需数据如下: 黑色 白色 红色 圆点 条纹 卡通 XXL S L 对照两组数据,其实我们将数据一进行了旋转,从而得到了数据二,用数学名词表示即是 [代码]矩阵转置[代码],具体是怎样可以百度百科,点我查看。只要搜搜 [代码]js数组矩阵转置[代码] 等关键词则可以找到相关的代码。 转置计算 [代码]const rows = [ { name: '颜色', values: ['黑色', '白色', '红色', '粉色', '紫色'] }, { name: '图案', values: ['圆点', '条纹', '卡通'] }, { name: '尺码', values: ['XXL', 'XL', 'L', 'M', 'S'] }, ] const skus = [ '1@0-0#1-0#2-0', '1@0-1#1-0#2-0', '1@0-2#1-0#2-0', '1@0-3#1-0#2-0', '1@0-4#1-0#2-0', '1@0-0#1-1#2-0', '1@0-1#1-1#2-1', '1@0-2#1-1#2-2', '1@0-3#1-1#2-3', '1@0-4#1-1#2-0', '1@0-0#1-2#2-0', '1@0-1#1-2#2-0', '1@0-2#1-2#2-0', '1@0-3#1-2#2-0', '1@0-4#1-2#2-2', '1@0-0#1-0#2-0', '1@0-1#1-0#2-1', '1@0-2#1-0#2-1', '1@0-3#1-2#2-4', '1@0-2#1-1#2-4', '1@0-3#1-0#2-4', '1@0-4#1-0#2-3', '1@0-4#1-2#2-0', ] const sku_list = skus.map((v) => { const codes = v.split('@')[1].split('#') const specs = codes.map((c) => { const key = c.split('-')[0] const value = c.split('-')[1] return { id: c, key: rows[key].name, value: rows[key].values[value], } }) return { code: v, specs } }) const EStatus = { PENDING : 'pending', DISABLED : 'disabled', SELECTED : 'selected', } const specs = sku_list.map(v => v.specs) const _isRepeat = (list, c, cell) => { return list[c].cells.some((v) => v.id === cell.id) } const _transpose = (specs) => { const result = [] for (let c = 0; c < specs[0].length; c++) { result[c] = { key: '', cells: [] } for (let i = 0; i < specs.length; i++) { // 去重 const cell = specs[i][c] if (!_isRepeat(result, c, cell)) { result[c].key = cell.key result[c].cells.push({ id: cell.id, status: EStatus.PENDING, value: cell.value, }) } } } return result } const fences = _transpose(specs) console.log('数组转置') console.log(JSON.stringify(fences)) [代码] 复制代码运行查看结果 [代码][{"key":"颜色","cells":[{"id":"0-0","status":"pending","value":"黑色"},{"id":"0-1","status":"pending","value":"白色"},{"id":"0-2","status":"pending","value":"红色"},{"id":"0-3","status":"pending","value":"粉色"},{"id":"0-4","status":"pending","value":"紫色"}]},{"key":"图案","cells":[{"id":"1-0","status":"pending","value":"圆点"},{"id":"1-1","status":"pending","value":"条纹"},{"id":"1-2","status":"pending","value":"卡通"}]},{"key":"尺码","cells":[{"id":"2-0","status":"pending","value":"XXL"},{"id":"2-1","status":"pending","value":"XL"},{"id":"2-2","status":"pending","value":"L"},{"id":"2-3","status":"pending","value":"M"},{"id":"2-4","status":"pending","value":"S"}]}] [代码] 渲染视图 [代码]<view class="demo"> <view wx:for="{{ skus }}" wx:key="item" mark:y="{{ index }}" class="rows" > <view class="key">{{ item.key }}</view> <view class="columns"> <view wx:for="{{ item.cells }}" wx:key="item" mark:x="{{ index }}" mark:status="{{ item.status }}" class="cell {{ item.status }}" bind:tap="change" >{{ item.value }}</view> </view> </view> </view> [代码] 如何获取可视规格 当我们将所有的sku规格进行数据转换之后,还需要将sku的所有组合计算出来,通过拆分 [代码]code[代码] 可以得到sku组合的信息,通过 [代码]组合[代码] 算法得到所有的可视规格,即视图所有可以点的规格路径,具体百度百科了解,点我。 组合计算 [代码]const codes = sku_list.map(v => v.code) const _combination = (arr, symbol = '#') => { let result = [] let s = [] for (let i = 0; i < arr.length; i++) { s.push(arr[i]) for (let j = 0; j < result.length; j++) { s.push(result[j] + symbol + arr[i]) } result = [...s] } return result } const paths = [] codes.map(v => { paths.push(..._combination(v.split('@')[1].split('#'))) }) console.log('数组组合') console.log(JSON.stringify(paths)) [代码] 运算结果 [代码]["0-0","1-0","0-0#1-0","2-0","0-0#2-0","1-0#2-0","0-0#1-0#2-0","0-1","1-0","0-1#1-0","2-0","0-1#2-0","1-0#2-0","0-1#1-0#2-0","0-2","1-0","0-2#1-0","2-0","0-2#2-0","1-0#2-0","0-2#1-0#2-0","0-3","1-0","0-3#1-0","2-0","0-3#2-0","1-0#2-0","0-3#1-0#2-0","0-4","1-0","0-4#1-0","2-0","0-4#2-0","1-0#2-0","0-4#1-0#2-0","0-0","1-1","0-0#1-1","2-0","0-0#2-0","1-1#2-0","0-0#1-1#2-0","0-1","1-1","0-1#1-1","2-1","0-1#2-1","1-1#2-1","0-1#1-1#2-1","0-2","1-1","0-2#1-1","2-2","0-2#2-2","1-1#2-2","0-2#1-1#2-2","0-3","1-1","0-3#1-1","2-3","0-3#2-3","1-1#2-3","0-3#1-1#2-3","0-4","1-1","0-4#1-1","2-0","0-4#2-0","1-1#2-0","0-4#1-1#2-0","0-0","1-2","0-0#1-2","2-0","0-0#2-0","1-2#2-0","0-0#1-2#2-0","0-1","1-2","0-1#1-2","2-0","0-1#2-0","1-2#2-0","0-1#1-2#2-0","0-2","1-2","0-2#1-2","2-0","0-2#2-0","1-2#2-0","0-2#1-2#2-0","0-3","1-2","0-3#1-2","2-0","0-3#2-0","1-2#2-0","0-3#1-2#2-0","0-4","1-2","0-4#1-2","2-2","0-4#2-2","1-2#2-2","0-4#1-2#2-2","0-0","1-0","0-0#1-0","2-0","0-0#2-0","1-0#2-0","0-0#1-0#2-0","0-1","1-0","0-1#1-0","2-1","0-1#2-1","1-0#2-1","0-1#1-0#2-1","0-2","1-0","0-2#1-0","2-1","0-2#2-1","1-0#2-1","0-2#1-0#2-1","0-3","1-2","0-3#1-2","2-4","0-3#2-4","1-2#2-4","0-3#1-2#2-4","0-2","1-1","0-2#1-1","2-4","0-2#2-4","1-1#2-4","0-2#1-1#2-4","0-3","1-0","0-3#1-0","2-4","0-3#2-4","1-0#2-4","0-3#1-0#2-4","0-4","1-0","0-4#1-0","2-3","0-4#2-3","1-0#2-3","0-4#1-0#2-3","0-4","1-2","0-4#1-2","2-0","0-4#2-0","1-2#2-0","0-4#1-2#2-0"] [代码] 修改规格状态 当点击规格列表里任意一个时,点击的需要显示激活状态,无规格的需要显示禁用状态。改变自身的状态很容易,要改变其他规格的状态就有点复杂了,需要通过多次循环遍历计算当前点击的可视规格,将不存在可视规格里的规格全部修改成禁用状态,语言组织起来比较难以理解,过程即是通过行号、列号找到对应规格,然后通过组合计算可视规格,通过对比以后就知道该显示的状态是什么了。 修改事件 [代码]// index.js import { sku_list } from '../mocks/demo.mock' import Sku, { IFence } from './sku' Page({ data: { sku: {} as Sku, skus: [] as IFence[], }, onLoad() { const sku = new Sku(sku_list) this.data.sku = sku this.setData({ skus: sku.fences, }) }, change({ mark }) { const { sku } = this.data sku.change(mark) this.setData({ skus: sku.fences, }) }, }) [代码] [代码]const selected = [] const change = ({ x, y, status }) => { if (status === EStatus.DISABLED) return // 改变点击的cell _changeCurrentCellStatus(x, y, status) // 改变其他cell fences.forEach((v, y) => { v.cells.forEach((cell, x) => { _changeOtherCellStatus(cell, x, y) }) }) } [代码] 修改自身状态 [代码]const _setCellStatus = (x, y, status) => { fences[y].cells[x].status = status } const _changeCurrentCellStatus = (x, y, status) => { const cell = fences[y].cells[x] // 选择 if (status === EStatus.PENDING) { selected[y] = cell _setCellStatus(x, y, EStatus.SELECTED) } // 反选 else if (status === EStatus.SELECTED) { selected[y] = null _setCellStatus(x, y, EStatus.PENDING) } } [代码] 修改无规格状态 [代码]const _changeOtherCellStatus = (cell, x, y) => { const path = _generatePath(cell, y) if (!path) return // 判断是否存在 if (paths.includes(path)) { _setCellStatus(x, y, EStatus.PENDING) } else { _setCellStatus(x, y, EStatus.DISABLED) } } const _generatePath = (cell, y) => { const path = [] for (let index = 0; index < fences.length; index++) { if (index === y) { if (isSelected(y, cell)) { return } path.push(cell.id) } else { const cell = selected[index] if (cell) { path.push(selected.id) } } } return path.join('#') } const isSelected = (index, cell) => { const value = selected[index] if (!value) { return false } return value.id === cell.id } [代码] 模拟点击规格 [代码]change({x: 0, y: 0, status: EStatus.PENDING}) change({x: 0, y: 1, status: EStatus.PENDING}) change({x: 0, y: 2, status: EStatus.PENDING}) console.log('点击规格') console.log(selected) [代码] 已选择sku的信息 [代码][ { id: '0-0', status: 'selected', value: '黑色' }, { id: '1-0', status: 'selected', value: '圆点' }, { id: '2-0', status: 'selected', value: 'XXL' } ] [代码] 总结 这篇文章主要分享多规格数据的转换,以及通过 [代码]code[代码] 码来获取所有的可视规格,通过行列号获取当前点击的规格以及当前点击的可视规格,比较绕口。查看在线代码示例,直接运行查看结果,也可查看代码片段直接体验demo。后续将继续分享多规格sku的联动,价格、图片、库存等同步更新。由于代码片段包体积有限制,项目如果报ts错误,执行 [代码]npm i[代码] 或者 [代码]yarn add[代码],将小程序的声明依赖添加就行了。
2021-03-29 - 2021-01-12
- 使用云开发CMS能力实现简易商场
源码 点此领取 技术栈 云开发 CloudBase:云端一体化的 Serverless 后端服务解决方案。Taro:一套遵循 React 语法规范的 多端开发 解决方案开发工具 建议提前安装好 微信开发者工具Node LTS 版本VS Code 编辑器CloudBase VS Code 插件需求分析 只考虑基本的功能: 商品列表与下单:展示商品信息,创建订单订单列表:展示订单列表 资源准备 1. 在微信开发者工具中开通云开发,请选择按量付费 如果你的环境是预付费,请到设置中,将支付方式转换为按量付费 [图片] 2. 安装 CMS 系统 (1)更新到最新的 Nightly 版本工具,在工具顶部 Tab 栏中,点击「更多」-「内容管理」。 [图片] (2)点击开通,勾选同意协议后,点击确定。 [图片] (3)开通内容管理需要填写管理员账号,填写账号后,点击「确定」完成。 [图片] (4)开通拓展需要一定时间,请耐心等待。 (5)完成后,点击「更多」-「内容管理」,即可看到内容管理的入口和相关信息。点击访问地址,即可在弹出的窗口中进行内容管理的相关配置。 [图片] 3. 登录 CMS 系统,创建资源 CloudBase CMS 已经部署在当前环境下的静态网站托管中,访问地址的格式如下:云开发静态托管默认域名/部署路径,例如 https://envid.ap-shanghai.app.tcloudbase.com/tcb-cms/(结尾有 / 符号)。默认域名可以访问控制台查看。 打开 CloudBase CMS 后,你需要先登录,账号密码为安装时设置的管理员账号和密码。 在开始管理内容数据前,我们需要先创建一个项目。CloudBase CMS 使用项目划分不同类的内容,便于区分内容数据用途,进行权限管理。 首先,我们需要点击新建项目下方的创建新项目按钮,创建一个名为小商店,Id 为 shop 的项目。 [图片] 创建完项目后,点击项目卡片,进入项目的管理页面,我们会看到项目的欢迎页面。 [图片] 创建商品类型,管理商品信息 创建一个名称为商品的内容模型,数据库名为 goods,即将商品数据存储到 goods 数据集合中。如果新建内容的时候指定的集合不存在,CloudBase CMS 会自动新建集合。 [图片] 在创建完内容模型后,我们会得到一个空的内容模型。接下来,我们需要为商品添加商品名称,商品图片,价格,库存数量等字段。 为商品添加商品名称属性,因为商品名称通常是比较短的文字,所以我们可以选择单行字符串字段,点击右侧的单行字符串卡片,填写商品名称的字段信息。除了基本的名称,数据库字段名之外,我们还可以为此字段添加其他的限制,如最大长度,限制填写商品名称时的最大长度,创建商品时,是否必需填写商品等。 [图片] 类似的,我们可以创建数字类型的价格字段以及库存数量,图片类型的商品图片字段。在创建图片字段时,考虑到商品的图片可能有多张,我们可以打开允许多个内容按钮,表明可以上传多张图片。 [图片] 创建的 goods 数据库集合的结构如下: [图片] 同上,类似的创建一个名称为订单列表,数据库集合名为 order 的内容模型,来管理订单信息。创建的 order 数据库集合的结构如下: [图片] 添加一个商品 [图片] 创建项目 1、拉取模板 # 安装 taro cli 工具 npm install -g @tarojs/cli@2.2.7 # 拉取模板 git clone https://github.com/TencentCloudBase/cloudbase-minishop.git 使用微信开发者工具导入项目,进入 client 目录,安装依赖: npm i 项目目录 cloud/functions 包含写好的微信支付的两个云函数, pay 和接收支付消息推送的 pay-callback 云函数。使用时需使用微信开发者工具上传这两个云函数。 2、项目目录 . ├── client // 小程序源码 │ ├── config │ └── src │ ├── assets │ ├── components │ └── pages │ ├── index │ └── order-list └── cloud // 云开发相关源码 │ └── functions │ ├── pay │ └── pay-callback ├── cloudbaserc.json // 云开发配置 ├── project.config.json // 小程序配置 微信支付下单流程 1、小程序调用云函数,在云函数中调用统一下单接口,参数中带上接收异步支付结果的云函数名和其所在云环境 Id。 const cloud = require("wx-server-sdk"); const res = await cloud.cloudPay.unifiedOrder({ envId: '', subMchId: '', body: "商品名", totalFee: 100, outTradeNo: '订单号', spbillCreateIp: "127.0.0.1", functionName: "pay-callback" }); // 返回 res.payment 支付结果回调的云函数必须返回如下一个对象,否则会视为回调不成功,云函数会收到重复的支付回调。 { errcode: '', errmsg: '', } 2、统一下单接口返回的成功结果对象中有 payment 字段,该字段即是小程序端发起支付的接口(wx.requestPayment)所需的所有信息。 3、小程序端拿到云函数结果,调用 wx.requestPayemnt 发起支付 wx.requestPayment({ ...payment, success (res) { }, fail (res) { }tt })https://docs.cloudbase.net/ 4、支付完成后,在统一下单接口中配置的云函数将收到支付结果通知。 多端支持 - 跨平台 小程序Web 相关文献 云开发文档 云开发微信支付 支付接口
2021-09-10 - 我要在小程序上也使用 nprogress !
目标 在小程序上使用 nprogress 进度条 使用 1. 获取源码,请使用 版本号 1.02.1812180 以上的 IDE 打开代码片段 代码片段:https://developers.weixin.qq.com/s/emHiNdmh7qm2 github: https://github.com/angxuejian/moto.wxui/tree/main/UI/nprogress 2. 获取 components 文件下的 nprogress 文件夹 并放在自己项目中; 然后将 nprogress 组件 注册为全局组件或单独组件; [代码]// app.json 或 index.json "usingComponents": { "nprogress": "components/nprogress/nprogress" // 你的实际路径 }, [代码] 3. index.wxml页面中使用 [代码]// index.wxml <view> <nprogress id='nprogress' ></nprogress> </view> [代码] 4. index.js方法中使用; 通过 selectComponent 方法获取组件实例 [代码]// index.js onShow: function() { this.getLoadData() // 请求接口数据 } getLoadData: function() { this.selectComponent('#nprogress').start() // 开始加载进度, 但不会加载到 100% wx.request({ url: 'https:www.baidu.com', method: 'POST', data: {}, success: res => { this.selectComponent('#nprogress').done() // 完成加载, 加载到 100% // something... } }) } [代码] 5. 全部nprogress组件实例方法 start() 开始加载 [代码]this.selectComponent('#nprogress').start() [代码] done() 完成加载 [代码]this.selectComponent('#nprogress').done() [代码] setting(Object object) 更改配置 [代码]this.selectComponent('#nprogress').setting({ bColor: '#4EC520', ... }) [代码] 参数 Object object 属性 类型 默认值 必填 说明 bColor string ‘#4EC520’ 否 进度条的颜色; 支持 ‘#000’/ ‘black’/ ‘rgba(0,0,0,0)’/ ‘rgb(0,0,0)’ height number 2 否 进度条的高度; 单位 px duration number 2000 否 动画完成时间; 单位 ms timingFunction string ‘linear’ 否 动画效果 speed number 10 否 进度条的起始步长 mask boolran true 否 是否需要遮罩层; 透明遮罩 timingFunction的合法值均为微信官方文档中的 timingFunction合法值 缺点 因小程序的特性。如要使用,必须在每个页面中都要引入 nprogress 组件 调用时可封装一个全局调用, 详看代码片段中的 utils文件中 request.js 上图 [图片] 最后 第一次写, 各位看官下手轻点, 欢迎大家点评及提出问题😋😋
2020-11-29 - 哈哈,花了点时间,实现不用npm),实现imgSecCheck,又精简了程序包,这不美吗
先看效果 [图片] 不废话,上代码就完事了 云函数端: // 云函数入口文件 const cloud = require('wx-server-sdk') const request = require('request') //node.js原生方法,不用npm cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { try { const qs = await new Promise((resolve, reject) => { //encoding: null,这个是关键,否则返回字符串而不是二进制数组 request({method: 'GET',url:event.url,encoding: null}, (error, response, body) => { if (error) { reject(error) } else { console.log('body',body) resolve(body) } }) }) const result = await cloud.openapi.security.imgSecCheck({ media: { contentType: 'image/png', value: qs } }) if (result && result.errCode.toString() === '87014') { return { code: 500, msg: '内容含有违法违规内容', data: result } } else { return { code: 200, msg: '内容ok', data: result } } } catch (err) { // 错误处理 if (err.errCode.toString() === '87014') { return { code: 500, msg: '内容含有违法违规内容', data: err } } return { code: 502, msg: '调用imgSecCheck接口异常', data: err } } } 小程序端: wx.chooseImage({ count: imgMaxNum-imgList.length, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (res) => { console.log(res) const tempFiles = res.tempFiles; wx.showLoading({ title: '导入中', }) that.data.fabuing = true tempFiles.forEach( (items,id) => { console.log(items.size/1024) console.log(items) // 官方说云调用imgSecCheck 限1M,但我实测超过450KB就很容易报错 if (items && items.size <= 450*1024) { // 此步关键,图片先转buffer console.log(items.path) wx.getFileSystemManager().readFile({ filePath: items.path, success: res => { var buff = res.data console.log(buff) wx.cloud.callFunction({ name: 'imgSecCheck', data: { url:wx.cloud.CDN(buff) //将CDN 上传buffer,在向云函数传入零时url } }) .then(res => { console.log(res); let {errCode} = res.result.data; switch (errCode) { case 87014: //此时说明图片内容不过 var index = list.findIndex(cur=>cur.path==items.path) that.setData({ ['imgList['+index+'].size']: 0 }) break; case 0: break; default: break; } }) .catch(err => { console.error(err); }) } }) } if(id==tempFiles.length-1){ wx.hideLoading() that.data.fabuing = false var list = imgList.concat(tempFiles) that.setData({ imgList: list }) console.log(list) } }) } }) 代码是没问题,但坑的是开发者工具里,云函数怎么都获取不到图片二进制信息,真机就没问题
2020-11-07 - 如何在小程序中快速实现环形进度条
在小程序开发过程中经常涉及到一些图表类需求,其中环形进度条比较属于比较常见的需求 [图片] [中间的文字部分需要自己实现,因为每个项目不同,本工具只实现进度条] 上图中,一方面我们我们需要实现动态计算弧度的进度条,还需要在进度条上加上渐变效果,如果每次都需要自己手写,那需要很多重复劳动,所以决定为为小程序生态圈贡献一份小小的力量,下面来介绍一下整个工具的实现思路,喜欢的给个star咯 https://github.com/lucaszhu2zgf/mp-progress 环形进度条由灰色底圈+渐变不确定圆弧+双色纽扣组成,首先先把页面结构写好: .canvas{ position: absolute; top: 0; left: 0; width: 400rpx; height: 400rpx; } 因为进度条需要盖在文字上面,所以采用了绝对定位。接下来先把灰色底圈给画上: const context = wx.createContext(); // 打底灰色曲线 context.beginPath(); context.arc(this.convert_length(200), this.convert_length(200), r, 0, 2*Math.PI); context.setLineWidth(12); context.setStrokeStyle('#f0f0f0'); context.stroke(); wx.drawCanvas({ canvasId: 'progress', actions: context.getActions() }); 效果如下: [图片] 接下来就要画绿色的进度条,渐变暂时先不考虑 // 圆弧角度 const deg = ((remain/total).toFixed(2))*2*Math.PI; // 画渐变曲线 context.beginPath(); // 由于外层大小是400,所以圆弧圆心坐标是200,200 context.arc(this.convert_length(200), this.convert_length(200), r, 0, deg); context.setLineWidth(12); context.setStrokeStyle('#56B37F'); context.stroke(); // 辅助函数,用于转换小程序中的rpx convert_length(length) { return Math.round(wx.getSystemInfoSync().windowWidth * length / 750); } [图片] 似乎完成了一大部分,先自测看看不是满圆的情况是啥样子,比如现在剩余车位是120个 [图片] 因为圆弧函数arc默认的起点在3点钟方向,而设计想要的圆弧的起点从12点钟方向开始,现在这样是没法达到预期效果。是不是可以使用css让canvas自己旋转-90deg就好了呢?于是我在上面的canvas样式中新增以下规则: .canvas{ transform: rotate(-90deg); } 但是在真机上并不起作用,于是我把新增的样式放到包裹canvas的外层元素上,发现外层元素已经旋转,可是圆弧还是从3点钟方向开始的,唯一能解释这个现象的是官方说:小程序中的canvas使用的是原生组件,所以这样设置css并不能达到我们想要的效果 [图片] 所以必须要在canvas画图的时候把坐标原点移动到弧形圆心,并且在画布内旋转-90deg [图片] // 更换原点 context.translate(this.convert_length(200), this.convert_length(200)); // arc原点默认为3点钟方向,需要调整到12点 context.rotate(-90 * Math.PI / 180); // 需要注意的是,原点变换之后圆弧arc原点也变成了0,0 真机预览效果达成预期 [图片] 接下来添加环形渐变效果,但是canvas原本提供的渐变类型只有两种: 1、LinearGradient线性渐变 [图片] 2、CircularGradient圆形渐变 [图片] 两种渐变中离设计效果最近的是线性渐变,至于为什么能够形成似乎是随圆形弧度增加而颜色变深的效果也只是控制坐标开始和结束的坐标位置罢了 const grd = context.createLinearGradient(0, 0, 100, 90); grd.addColorStop(0, '#56B37F'); grd.addColorStop(1, '#c0e674'); // 画渐变曲线 context.beginPath(); context.arc(0, 0, r, 0, deg); context.setLineWidth(12); context.setStrokeStyle(grd); context.stroke(); 来看一下真机预览效果: [图片] 非常棒,最后就剩下跟随进度条的纽扣效果了 [图片] 根据三角函数,已知三角形夹角根据公式radian = 2*Math.PI/360*deg,再利用cos和sin函数可以x、y,从而计算出纽扣在各部分半圆的坐标 const mathDeg = ((remain/total).toFixed(2))*360; // 计算弧度 let radian = ''; // 圆圈半径 const r = +this.convert_length(170); // 三角函数cos=y/r,sin=x/r,分别得到小点的x、y坐标 let x = 0; let y = 0; if (mathDeg <= 90) { // 求弧度 radian = 2*Math.PI/360*mathDeg; x = Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 90 && mathDeg <= 180) { // 求弧度 radian = 2*Math.PI/360*(180 - mathDeg); x = -Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 180 && mathDeg <= 270) { // 求弧度 radian = 2*Math.PI/360*(mathDeg - 180); x = -Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } else{ // 求弧度 radian = 2*Math.PI/360*(360 - mathDeg); x = Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } [图片] 有了纽扣的圆形坐标,最后一步就是按照设计绘制样式了 // 画纽扣 context.beginPath(); context.arc(x, y, this.convert_length(24), 0, 2 * Math.PI); context.setFillStyle('#ffffff'); context.setShadow(0, 0, this.convert_length(10), 'rgba(86,179,127,0.5)'); context.fill(); // 画绿点 context.beginPath(); context.arc(x, y, this.convert_length(12), 0, 2 * Math.PI); context.setFillStyle('#56B37F'); context.fill(); 来看一下最终效果 [图片] 最后我重新review了整个代码逻辑,并且已经将代码开源到https://github.com/lucaszhu2zgf/mp-progress,欢迎大家使用
2020-05-27 - 小程序开发起步
学习 5 节课程,从 0 至 1 做第一个属于你的小程序,深入浅出了解小程序开发。本系列视频,由腾讯课堂 NEXT 学院、微信学堂联合出品。
2022-03-24 - 如何使用scroll-view制作左右滚动导航条效果
最新:2020/06/13。修改为scroll-view与swiper联动效果,新增下拉刷新以及上拉加载效果。。具体效果查看代码片段,以下文章内容和就不改了 刚刚在社区里看到 有老哥在问如何做滚动的导航栏。这里简单给他写了个代码片段,需要的大哥拿去随便改改,先看效果图: [图片] 代码如下: wxml [代码]<scroll-view class="scroll-wrapper" scroll-x scroll-with-animation="true" scroll-into-view="item{{currentTab < 4 ? 0 : currentTab - 3}}" > <view class="navigate-item" id="item{{index}}" wx:for="{{taskList}}" wx:key="{{index}}" data-index="{{index}}" bindtap="handleClick"> <view class="names {{currentTab === index ? 'active' : ''}}">{{item.name}}</view> <view class="currtline {{currentTab === index ? 'active' : ''}}"></view> </view> </scroll-view> [代码] wxss [代码].scroll-wrapper { white-space: nowrap; -webkit-overflow-scrolling: touch; background: #FFF; height: 90rpx; padding: 0 32rpx; box-sizing: border-box; } ::-webkit-scrollbar { width: 0; height: 0; color: transparent; } .navigate-item { display: inline-block; text-align: center; height: 90rpx; line-height: 90rpx; margin: 0 16rpx; } .names { font-size: 28rpx; color: #3c3c3c; } .names.active { color: #00cc88; font-weight: bold; font-size: 34rpx; } .currtline { margin: -8rpx auto 0 auto; width: 100rpx; height: 8rpx; border-radius: 4rpx; } .currtline.active { background: #47CD88; transition: all .3s; } [代码] JS [代码]const app = getApp() Page({ data: { currentTab: 0, taskList: [{ name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, ] }, onLoad() { }, handleClick(e) { let currentTab = e.currentTarget.dataset.index this.setData({ currentTab }) }, }) [代码] 最后奉上代码片段: https://developers.weixin.qq.com/s/nkyp64mN7fim
2020-06-13 - 转发二级页面胶囊按钮菜单去掉返回首页选项
当我们转发一个二级页面时,右上角的胶囊按钮菜单里会有一个“返回首页”的选项,可以返回到小程序的首页,有时我们并不需要这个功能,或者想禁用此功能。 [图片] 但小程序并没有提供编辑此菜单的功能,只要转发的页面不是根目录下的,就会自动生成返回首页这一项,要怎么操作呢? 如果转发的页面是首页,自然就不会有这个选项,因此,我们可以把转发的二级页面修改为先转发首页再进行页面重定向的方法来实现。 首先,在转发的onShareAppMessage方法里把path改成首页,并把要重定向的二级页面及其参数封装好。 然后,在首页的onLoad事件里,把接收到的二级页面及其参数,用wx.reLaunch方法进行重定向。 现在,用户打开转发的二级页面,胶囊按钮菜单就不会再出现“返回首页”这一选项了。
2019-07-30