- 微信小程序中安全区域计算和适配
前言 自从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 - 一张表解决云存储的七大痛点
就是这张表: 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 - 如何更优雅的使用IconFont你应该知道
微信小程序iconfont(也适用于网页和其他平台的小程序) 在微信小程序等使用iconfont进阶版,17年的时候就发过一次在微信小程序里使用iconfont的文章 去看看 时隔这么久也掌握了一些这方面的心得,稍微整理了一下。 用微信开发工具好还是用成熟的IDE好? 刚开始的时候我也是用开发工具写代码,因为刚接触的时候并没有什么特殊的需求,规规矩矩的按照开发文档来,到后面觉得效率太低了,于是改用了sublime text webstorm来做小程序开发,开发工具仅仅用来创建项目和预览。久而久之就已经完全弃用开发工具写代码了,并不是说开发工具不行而是说结合起来能给更高效的开发项目。 用webstorm来开发小程序 为什么选择ws而不用vscode,原因就是我用习惯了不想在去更换了。 讲重点: 项目目录结构 安装Less来编写css 利用ws编辑器来自动编译Less生成wxss 快速将iconfont引入项目 简单的一个例子 项目目录结构 [代码]iconfont -app 小程序项目目录 --pages 页面目录 ---index ----index.wxml ----index.less ----index.ts ----index.json ----index.js (自动编译) ----index.wxss (自动编译) --styles 样式目录 ---global.less 全局公共样式 ---global.wxss (自动斌阿姨) ---iconfont.less ---iconfont.wxss (自动编译) -app.ts -app.js (自动编译) -app.less -app.wxss (自动编译) app.json project.config.json 项目配置文件 sitemap.json 搜索引擎相关配置 -node_modules [代码] 安装Less 首先确保自己电脑是否安装了node.js,关于怎么安装node.js请自行谷歌或者百度。 全局或者单项目安装less [代码]npm install -g less [代码] [代码]webstorm[代码]配置[代码]less[代码]自动编译 打开ws设置找到[代码]File Watchers[代码] [图片] 添加一个less类型的监视器 [图片] 修改默认配置,按下图设置 [图片] 可以根据自己的习惯来设置,没有规定一定是这样,这套设置是当我less文件有改动我保存时会自动编译输出到wxss文件,这样的好处就是不会有改动就编译,而是保存的时候需要编译了才编译。 新建[代码]iconfont[代码]项目 打开阿里巴巴的图标库点我 新建一个项目,然后添加或者上传一些icon图标,生成Font Class的css链接,然后在styles目录下的[代码]iconfont.less[代码]文件里引入刚刚创建的链接,[代码]iconfont.wxss[代码]就会自动生成对应的代码,这样就不用每次icon有改变就得去打开创建的链接然后复制粘贴到iconfont.wxss了。 [图片] [图片] [图片] [图片] 举个栗子 [代码]// index.less文件中引入global.less跟iconfont.wxss // 至于为什么global引入的是less而iconfont引入的却是wxss // 因为less是css的预处理语言,所以最终还是会被打包编译成css // 所以我们global要引入的并不是要打包编译后的css代码 // 而iconfont因为我们在less里面引入了外链后被打包编译了需要的css @import (reference) "../../styles/global"; @import (css) "../../styles/iconfont.wxss"; [代码] [代码]// 在view里面直接使用icon <view class="icon iconaixin"></view> // 循序遍历所有的icon <view class="container"> <view wx:for="{{ iconList }}" wx:key="item" data-index="{{ index }}" class="item {{ index === iconIndex ? 'active' : '' }}" bind:tap="click"> <view class="icon icon{{ item.font_class }}"></view> <view class="name">{{ item.name }}</view> </view> </view> [代码] 效果图 [图片] 大致使用步骤就是,通过ws创建文件的监视器,自动编译打包,大大的提高了开发效率。 我less中使用rpx的时候用的是unit(10, rpx),当然你也可以直接用10rpx,但是IDE可能会识别不了,编译后或者格式化后会出现10 rpx这种情况,这样开发工具肯定就会报错了,解决办法就是在创建一个监视器,保存打包的时候,把10 rpx替换成10rpx即可,js部分是用ts编写,如果不懂可以直接看编译后的js,差别不会很大!如果也想ts修改代码,请执行[代码]npm install[代码]把相关依赖包下载就行了。 [图片] over 如果有疑问或者更好建议欢迎找我交流! 微信小程序代码片段 github
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 - 文件上传杂谈
一、写在前面文件上传是前端很常见的一类场景。图片、视频和文档等等都属于文件范畴,每个文件则是通过 File.Type 进行更细的划分。本文将针对文件上传的一些通用维度场景做简单的剖析和尝试,抛砖引玉,希望共同学习,共同成长。 本文案例里使用的组件来源于组件库 zent@7.4.4二、常见的上传场景及实现上传的形式或场景各式各样,除了业务级别的封装外,常遇到的通用场景有如下: 重复上传上传预览拖拽上传上传裁剪上传进度可视化文件压缩上传前置校验切片上传上传加密暂停&断网续传 ...我们抽取部分场景进行实现: 2.1 上传前置校验在文件上传前,经常会需要对文件格式进行校验,我们需要在文件上传/展示预览图前提示用户图片是否完成校验。 常用的格式校验:文件类型、文件大小、上传的尺寸 我们先看看和文件相关的两个对象的定义:[代码]Blob[代码] 和 [代码]File[代码] /** A file-like object of immutable, raw data.Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system. */ interface Blob { readonly size: number; readonly type: string; arrayBuffer(): Promise; slice(start?: number, end?: number, contentType?: string): Blob; stream(): ReadableStream; text(): Promise; } /** Provides information about files and allows JavaScript in a web page to access their content. */ interface File extends Blob { readonly lastModified: number; readonly name: string; } 通过定义我们知道, [代码]Blob[代码]是一个不可变、存储文件原数据的一个类文件,但其并非是JS的原生数据,而 [代码]File[代码]继承于 [代码]Blob[代码],使得 [代码]Blob[代码]信息扩展为用户操作系统可支持的文件,并使得页面里可以使用 [代码]Javascript[代码]访问其文件信息。 除了继承与原有的 size 和 type 属性, [代码]File[代码]对象还额外返回 lastModified (返回文件最后修改日期)和 name (文件名)属性。 以下是某个文件的 [代码]File[代码]实例信息 { lastModified: 1581424451211 lastModifiedDate: Tue Feb 11 2020 20:34:11 GMT+0800 (中国标准时间) name: "计算机网络.pdf" size: 70809807 type: "application/pdf" webkitRelativePath: "" } 通过上面信息,我们可以很轻松地校验文件类型和文件大小。具体的实现我们接着看下去。 2.1.1 限制文件上传类型1.使用 input 自带属性 [代码]accept[代码] Mime 类型列表 属性描述值例子accept 期望文件类型 image/* , audio/* , video/* ... image/jpeg ... [图片] 图1 Input限制上传类型 2.使用文件后缀或 MIME-TYPE // ... const acceptTypes = ['image/png', 'image/jpeg']; const picSlipt = name.split('.'); // 切割文件名后缀 const picSuffix = `image/${picSlipt[picSlipt.length - 1]}`; // 直接使用解析的文件信息 const fileType = file.type; if (acceptTypes.includes(picSuffix) || acceptTypes.includes(fileType)) { console.log('通过文件类型校验!'); }; //... 3.使用二进制文件信息流读取 但我们知道直接更改文件后缀并不会改变文件类型的本质。比如以下我直接更改一张 png 图片后缀为 jpg,那么它就很有可能绕过了我们的规则 [代码]image/jpeg[代码](虽然想要绕过前端的规则校验有非常多的方法) [图片] 图2 通过更改png图片后缀绕过前端上传规则 但实际上它还是png图片,我们可以通过图像信息查询网站可以得出该图片信息实际如下: [图片] 图3 后缀和类型不一致 上传校验的绕过会给服务器带来很多潜在危险,因此我们可能需要通过更严格的类型校验:文件头信息进行格式鉴别👇 文件类型的信息一般是头文件里前8个字节 我们看一下上面那张图avatar.jpg,第一行头信息里有什么? [图片] 图4 文件的16进制信息 通过vscode的插件hexdump可以查看到该文件头部信息前8个字节为:89 50 4E 47 0D 0A 1A 0A。这其实是 png 图片的头部信息,前8个字节属于 png 图片的头标识,后4个字节为数据域长度,最后4个字节为 png 的 IHDR 标识,是图片宽高等数据流前的第一个数据块。通过前8个字节证明了即使图片后缀为 jpg,但文件类型仍然为 png 以下列举一些常见的文件格式前字节标识信息(可从网上查阅或用 vscode 读取) 文件类型规则hex(十六进制)png 前8个字节 89 50 4E 47 0D 0A 1A 0A jpg 前2个字节 FF D8 gif 前6个字节 47 49 46 38 39 61 bmp 前2个字节 42 4D ES6已经支持我们我们直接通过 [代码]ArrayBuffer[代码]对象存储文件的二进制数据,并通过 [代码]DataView[代码]去读取。 const reader = new FileReader(); reader.onload = function () { // 这里从0开始获取文件二进制数据的前8个字节 const dataView = new DataView(this.result, 0, 8); for (let i = 0; i < dataView.byteLength; i++) { // 读取 1 个字节,返回一个无符号的 8 位整数 bufferUint8Array.push(dataView.getUint8(i)) } } // 这里生成包含文件信息的二进制数据,但不允许直接读写 reader.readAsArrayBuffer(file); 也可以在生成ArrayBuffer时先对文件进行切割: // 1.生成对象 reader.readAsArrayBuffer(file.slice(0, 8))); // 2.提取头部信息 new DataView(this.result); 完整代码 // index.js const handleChange = async e => { const files = e.target.files; const isPNG = await checkType(files[0]); } // utils.js export const checkType = file => { return new Promise(resolve => { const reader = new FileReader(); reader.onload = function () { // PNG文件头标识(16进制) const PNG_HEADER_HEX = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; const dataView = new DataView(this.result); const bufferUint8Array = new Array(dataView.byteLength).fill('').map((_, index) => dataView.getUint8(index)) console.log(`文件: ${file.name} 的前8个字节十进制为, ${bufferUint8Array}`); // 用获取到的字节和图片头信息进行对比 const isPNG = PNG_HEADER_HEX.every((hex, index) => { return hex === bufferUint8Array[index]; }); resolve(isPNG); } reader.readAsArrayBuffer(file.slice(0, 8)); }) } 现在我们分别上传一张标准 png 图片、一张更改后缀为 jpg 的 png 图片和一张标准 jpg 判断其是否符合标准的 png 头部信息。 [图片] 图5 判断上传文件是否为png格式 可以看到: avatar.png: 十进制: [137, 80, 78, 71, 13, 10, 26, 10] 十六进制为:0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A avatar.jpg: 十进制: [137, 80, 78, 71, 13, 10, 26, 10] 十六进制为:0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A banner.jpg: 十进制: [255, 216, 255, 224, 0, 16, 74, 70] 十六进制: 0xFF, 0xD8, 0xFF, 0xE1, 0x00, 0x18, 0x45, 0x78 2.1.2 文件尺寸的校验因为文件的尺寸无法通过 [代码]File[代码]对象直接获得,我们可以使用以下方法 1.使用 [代码]Image[代码]获取上传图片尺寸 const reader = new FileReader(); const widthLimit = 100; const heightLimit = 100; console.log('限制图片的宽度 & 高度', `${widthLimit}px`, `${heightLimit}px`); reader.readAsDataURL(file); reader.onload = async function () { // 加载图片获取图片真实高度和上传 const src = reader.result; const image = new Image(); image.onload = await function () { const width = image.width; const height = image.height; console.log('上传图片的宽度 & 高度', `${width}px`, `${height}px`); if (Number(widthLimit) !== width || Number(heightLimit) !== height) { console.log(` %c x 校验不通过 ,请上传${widthLimit}*${heightLimit}的尺寸图片`, 'color: #ed6a0c'); resolve(false) } else { console.log('%c y 校验通过!', 'color: #2da641'); resolve(true); } } // 放置onload后 image.src = src; } 2.使用二进制文件信息流读取通过下面两种方式可以利用文件的头信息找到宽高: 通过找到图片信息的前置标志,然后再进行字节偏移直接进行字节偏移(仅适用于信息在固定位的格式,例如 png、gif 等。jpg 的前置标志没有固定的位置,只能通过第一种方式)文件类型前置标志读取方式png IHDR(13-16字节) 宽度:17-20字节(4 bytes) 高度: 21-24字节(4 bytes) gif GIF89(1-6字节) GIF尺寸是反着存储 宽度:第8字节+第7字节(2 bytes) 高度: 第10字节+第9字节(2 bytes) bmp - 宽度:18-21字节(4 bytes) 高度: 22-25字节(4 bytes) jpg SOF0、SOF1... 偏移3个字节后(n)开始计算 高度:(n, n+1)(2 bytes) 宽度:(n+2,n+3)(2 bytes) 完整代码 export const checkPxByHeader = file => { console.log('文件信息', file); const reader = new FileReader(); reader.onload = function () { const dataView = new DataView(this.result); isPNG(dataView); } // 如果是判断jpg图片需要遍历整个Buffer,不能切割 // png的前置标志固定在13-16字节 reader.readAsArrayBuffer(file.slice(0, 50)); } // png文件信息第一块数据表示 IHDR(49 48 44 52) const isPNG = dataView => { const IHDR_HEX = [0x49, 0x48, 0x44, 0x52]; // 方法一 查找数据块标志 new Array(dataView.byteLength - 4).fill('').map((_, index) => { const fourBytesArr = [index, index + 1, index + 2, index + 3].map(num => dataView.getUint8(num)); // 通过提取的4位无符号的8-bit整数与标准的PNG-IHDR16进制对比,判断是否遍历到了IHDR位置 const isTouchIHDR = fourBytesArr.every((hex, index) => { return hex === IHDR_HEX[index]; }); if (isTouchIHDR) { // 找到IHDR位置,偏移4个字节后获取4个字节的32位整数即可获取宽度 const width = dataView.getInt32(index + 4); const height = dataView.getInt32(index + 8); console.log('方法一获取 width', width); console.log('方法一获取 height', height); } if (!isTouchIHDR && index === dataView.byteLength - 4) { console.log('方法一获取 上传文件并非png'); } }) // 方法二 直接偏移字节 // 从第17个字节开始读取 const width = dataView.getInt32(16); const height = dataView.getInt32(20); console.log('方法二获取 width', width); console.log('方法二获取 height', height); } [图片] 图6 通过文件信息获取宽高 2.2 大文件上传之切片上传 接口超时、上传失败后又从零开始上传等是大文件上传经常要面临的问题,通过切片上传、断点续传等方式可以很好地解决以上痛点,改善交互体验。我们先来看一下基础的大文件上传最终效果: [图片] 图7 切片上传完整演示图 其实切片上传和单文件上传没有很大的区别,切片上传实际上就是一个个小切片的单文件上传。可以归纳成以下几步操作,我们一一实现: 获取上传文件信息。前端根据实际情况进行切片。如果是断点续传,则需要从已上传的切片数后面开始切割。(注:需要给每个切片的名字带上唯一标志,一般为索引值)上传切片至服务端。通过 ajax 的 [代码]ProgressEvent[代码]读取上传进度,前端展示。(注:ProgressEvent返回的是每个切片上传的进度,总进度应该是所有切片上传的进度)服务器接收切片。切片上传完毕后,前端发送请求通知服务器端合并切片,最后清除切片缓存。返回上传结果 & 文件路径。 2.2.1 获取上传文件信息通过调用 [代码]input[代码]的实例,打开选择文件弹窗并获取上传文件信息。 [图片] 图8 获取上传文件信息 完整代码 /** html */ 添加文件 /** constants */ export const uploadStatusMap = { 'pending': 0, 'uploading': 1, 'done': 2, 'pause': 3, 'error': 4, } export const uploadStatus = { 0: '未上传', 1: '上传中', 2: '已上传', 3: '暂停中', }; /** js */ const inputRef = React.useRef(null); const [fileList, setFileList] = React.useState([]); // 打开文件选择框 const handleAddFile = () => { const inputEv = inputRef.current; inputEv.click(); }; // 上传文件后回调 const handleFileSelect = async e => { const File = e.target.files[0]; // 存储文件相关信息 let filesToCurrent = { id: createUploadId(), fileName: File.name, fileType: File.type, fileSize: File.size, File, chunkCount, uploadSingleProgress: 0, currentChunk: 0, uploadStatus: uploadStatusMap.pending, }; // 表格里显示文件信息 setFileList([...fileList, filesToCurrent]); } 2.2.2 生成切片这里提供了2种生成切片数量的方式,可以根据具体情况选择: 1.根据默认的切片数量切割 好处: 限定了http请求的数量坏处: 文件过大时有可能导致每块切片大小依然很大,失去了切片的意义2.根据默认的切片大小切割 好处: 限定了切片的大小坏处: 切片数量过多容易造成http负担通过切片数量来计算每个切片大小。例如一张图片总大小为15M,切割成5份后每份切片大小为15 / 3 = 3M。因为合并切片的时候需要按切割顺序进行,所以需要记录当前切片的索引值,在上传切片时带上(本场景把 index 拼接到切片名字里)。 // index从1开始计算 `${File.name}-chunk-${fileChunkList.length + 1}` 切片索引值除了合并切片时使用外,在读取上传进度等地方也发挥了很大作用。[图片] 图9 前端生成切片信息 2.2.3 上传切片需要使用post方法结合multipart/form-data头才能将文件内容填充到body中。 const formData = new FormData(); formData.append('name', name); formData.append('file', file); axios({ method: 'post', data: formData, header: { 'Content-type': 'multipart/form-data', }, // ... }) 2.2.4 展示上传进度为了演示方便,本场景里暂时使用发送所有请求的方案,会并发无序执行所有请求,直到所有的请求响应完成再发送合并切片请求。所以需要记录每个切片上传的进度,并通过其占有的进度比计算出最终的文件上传进度。例如: 一个文件15M,分成5份切片,每个切片大小占比20%,发送上传请求一段时间,假设切片1返回进度是50%,其他4个切片返回进度均为25%,则文件总上传进度是多少? (0.5 * 0.2 + 0.25 * 0.8) * 100 = 30(%) [图片] 图10 每个切片返回上传的进度 完整代码 // index.js const uploadPromise = uploadChunkList.map(async ({ name, file }) => { return axios({ // ... // 记录上传进度 onUploadProgress: uploadInfo => { let chunkUploadInfo = {}; // 计算当前切片上传百分比 已上传数/总共需要上传数(这里计算的是每个切片的上传进度) const chunkProgress = Number((uploadInfo.loaded / uploadInfo.total)); console.log('当前上传切片序号:', index); console.log('当前上传切片进度', `${(chunkProgress * 100).toFixed(2)}%`); chunkUploadInfo[index] = chunkProgress; currentUploadItem.isSingle = false; /** * 总的上传百分比是由 切片上传进度 * 切片分数占比 * chunkUploadInfo的格式为{[index]: progress1, [index1]: progress2, ...} index为切片索引值 */ currentUploadItem.chunkUploadInfo = { ...currentUploadItem.chunkUploadInfo, [index]: chunkProgress, }; // 切片上传进度100%时,更新当前上传切片的索引值 if (chunkProgress === 1) { currentUploadItem.currentChunk = index + 1; } setFileList([...newFileList]); }, // ... }) } // utils.js 计算表格里展示的总进度 export const getSliceFileUpload = (chunkUploadInfo = {}) => { let progress = 0; // chunkUploadInfo数据格式为: {0: 0, 1: 0, [切片索引值]: [切片上传进度], ...} const chunkCountArr = Object.keys(chunkUploadInfo); chunkCountArr.forEach(chunkIdx => { progress += chunkUploadInfo[chunkIdx] * (100 / chunkCountArr.length) }) return progress; } 2.2.5 服务器端接收切片node层要接收解析二进制文件流。提取的实现比较麻烦,这边直接使用@koa/multer@1.0.2(版本不一样使用方式可能也不一样,具体可查看官方文档),当然还有其他非常多优秀的npm包可以选择formidable [代码]@koa/multer[代码]允许用户设定一个存放文件的位置。其实例对象提供了几种模式,为方便演示,本文案例统一使用 single。具体区别可以查看其定义。 /** 流存放位置 */ const chunksPath = path.join(__dirname, '../static/stream'); [代码]@koa/multer[代码]允许用户通过不同方法接收上传的文件 interface Instance { /** Accept a single file with the name fieldName. The single file will be stored in req.file. */ single(fieldName?: string): Koa.Middleware; /** Accept an array of files, all with the name fieldName. Optionally error out if more than maxCount files are uploaded. The array of files will be stored in req.files. */ array(fieldName: string, maxCount?: number): Koa.Middleware; /** Accept a mix of files, specified by fields. An object with arrays of files will be stored in req.files. */ fields(fields: Field[]): Koa.Middleware; /** Accepts all files that comes over the wire. An array of files will be stored in req.files. */ any(): Koa.Middleware; } // 实例 router.post('/upload-chunk', koaMulterUpload.single('file'), async (ctx) => { const file = ctx.req.file; }) [代码]@koa/multer[代码]会默认为接收到的文件生成如下信息: { fieldname: 'file', originalname: 'blob', encoding: '7bit', mimetype: 'application/octet-stream', destination: '/YourLocalPath/static/stream', filename: 'ff7cd26c15305dbfd9173be5f80f9770', path: '/YourLocalPath/static/stream/ff7cd26c15305dbfd9173be5f80f9770', size: 14161959 } 为了方便后续合并切片,将切片名重命名为特定的格式 /** * 重命名二进制流文件 * 注意路径需要对齐 */ // 从前端接收到的重命名格式,例如`${fileName}-chunk-${index}` const { name } = ctx.req.body; const file = ctx.req.file; const chunkName = `${chunksPath}/${name}`; fs.renameSync(file.path, chunkName); [图片] 图11 切片存放位置以及切片合并后生成的文件 完整代码 const koaMulter = require('koa-multer'); /** fs的封装模块 */ const fs = require('fs-extra'); /** 流存放位置 */ const chunksPath = path.join(__dirname, '../static/stream'); const koaMulterUpload = koaMulter({ dest: chunksPath }); router.post('/upload-chunk', koaMulterUpload.single('file'), async (ctx) => { /** * axios方法 * ctx.req.file 文件流信息 * ctx.req.body 请求参数 */ const { name } = ctx.req.body; const file = ctx.req.file; const chunkName = `${chunksPath}/${name}`; /** * 重命名切片文件名 * 注意路径需要对齐 */ fs.renameSync(file.path, chunkName); ctx.status = 200; ctx.res.end(`upload chunk: ${name} success!`); }); 2.2.6 服务器端合并切片node根据约定的切片名字格式去读取已存储的切片文件,合并之后清除切片文件。 完整代码 // node.js router.post('/merge-chunk', async (ctx) => { /** * axios.post方法 * ctx.request.body 请求参数 */ // 由前端告诉服务端生成切片数量 const { fileName = '未命名', chunkCount } = ctx.request.body || {}; // 1.创建存储文件,初始为空 const filePath = `${uploadFilePath}/${fileName}`; fs.writeFileSync(filePath, ''); console.log('chunkCount', chunkCount); // 2.读取所有chunk数据 // 3.开始写入数据 for (let idx = 1; idx <= chunkCount; idx++) { /** * 约定的chunk文件名格式: fileName + '-' + index */ const chunkFile = `${chunksPath}/${fileName}-chunk-${idx}`; fs.appendFileSync(filePath, fs.readFileSync(chunkFile)); } /** 删除chunk文件 */ fs.emptyDirSync(`${chunksPath}/${fileName}`); ctx.status = 200; ctx.res.end('successful'); }); 2.3 大文件上传之断点续传断点续传的核心是在已经上传切片数后面续传,为了更好地实现,我们对上面切片上传的逻辑做几个优化的点: 切片需要按顺序上传。即在上传切片1后再上传切片2,解决同时上传出现后面的切片比前面的切片先上传成功的情况,避免续传时重新切割切片无法找到起点。为方便找到上传文件已上传的切片,在切片完全上传更换名字的时候存放到特定文件夹里(案例里会以文件本名为存放 chunks 的文件名)。 [图片] 图12 断点续传切片存放位置 node返回切片信息后,只需要从已存在切片数+1位置进行切割。文章案例是会在切片完全上传后进行重命名,所以根据重命名后的切片数量判断重新切割位置能保证最后合成的文件信息无误。虽然会导致未完全上传的切片在续传的时候丢失(可能会出现上传进度86%,暂停重启后进度变为80%),本案例暂不考虑该情况。 针对以上优化的点做代码优化: 2.3.1 切片按顺序上传由于 [代码]map[代码]是js的同步方法,去掉 [代码]map[代码]和 [代码]axios.all[代码],使用 [代码]for...of[代码]代替, [代码]for...of[代码]是ES6推出的具有iterator(可迭代)特性的方法,受控于方法里的异步操作(await等),详细可查看for...of 循环 但 [代码]for...of[代码]无法拿到索引值,因为我们需要对原数组做处理,这里使用 [代码]Object.entries[代码],数组的索引值会被填入内容里转化成 [代码][a,b]=>[[index1,a],[index2,b]][代码]格式,注意获取的index类型为 [代码]string[代码]。 for (let [indexStr, { name, file, fileName }] of Object.entries(uploadChunkList)) { //... } [图片] 图13 按顺序上传切片 2.3.2 切片存放位置在切片上传成功后重命名至以该文件名为命名的文件夹里 // node/index.js const chunksContinuePath = `${chunksPath}/${fileName}`; if (!fs.existsSync(chunksContinuePath)) { await fs.mkdirs(chunksContinuePath); } const chunkName = `${chunksPath}/${fileName}/${name}`; fs.renameSync(file.path, chunkName); 2.3.3 获取已上传切片在上传切片前,客户端需要先从node里读取已上传切片数量,并依据此切割新的切片。通过文件名去读取存放文件夹下是否有对应的切片。 // client/index.js // 查找文件是否已经存在上传的切片信息 const existChunksList = await axios.get(`/chekck-file_chunk-upload?fileName=${File.name}`).then(({ data = [] }) => data); // 存在切片信息 if (existChunksList.length) { // 状态更改为暂停 filesToCurrent.uploadStatus = uploadStatusMap.pause; // 存储切片最后的索引值 filesToCurrent.currentChunk = existChunksList.length; // 读取到的切片上传进度都设置为100% filesToCurrent.chunkUploadInfo = { ...new Array(chunkCount).fill('').map((_, index) => index < existChunksList.length ? 1 : 0) }; }; // node/index.js router.get('/chekck-file_chunk-upload', async (ctx) => { const { query: { fileName }, } = ctx; // 切片读取位置和重命名的路径要一致 const chunksContinuePath = `${chunksPath}/${fileName}`; let uploadedChunksList = []; if (fs.existsSync(chunksContinuePath)) { uploadedChunksList = fs.readdirSync(chunksContinuePath); } ctx.body = uploadedChunksList }); 2.3.4 重新切割前端拿到已上传切片数量后只需要从索引值位置开始切割即可,再将剩余的切片上传完成进行合并即可。 const currentSize = chunkSize * currentChunk; // 计算剩余切片大小 for (let current = currentSize; current < File.size; current += chunkSize) { fileChunkList.push({ fileName: File.name, // 注意名字里的索引值应该是从已上传切片数量+1开始 name: `${File.name}-chunk-${currentChunk + fileChunkList.length + 1}`, // 使用Blob.slice方法来对文件进行分割。 file: File.slice(current, current + chunkSize), }); } 这边通过axios的 [代码]CancelToken[代码]简单模拟一下断点的操作 const axiosCancelToken = axios. CancelToken ; const axiosSourceCancel = axiosCancelToken.source(); // 暂停上传 const handleStopUpload = ({ id }) => { axiosSourceCancel.cancel( '中断上传' ); // ... }; 因为要模拟继续上传的场景,因此不能在原取消的请求上继续发起,我们需要发起新的请求实例,简单地做一下封装,并使用每次生成的CancelToken去做取消操作。 [图片] 图14 断点续传 三、结尾对于切片上传、断点续传等功能其实很多第三方CDN服务已经提供了成熟的方案,例如七牛云的文件上传。除了上传前置校验、断点续传等场景外,还有上传压缩、上传加密等场景各式各样。以及经常与其绑定一起的文件下载相关技术(例如有趣的HLS)都是非常值得去学习的。 感兴趣的商家,可点击 → 免费试用有赞店铺~ [图片]
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