- 自定义菜单设置小程序相关问题
1、自定义菜单怎么设置“跳转小程序”? 跳转小程序:用户可将已发布且公众号已关联的小程序添加至一级菜单和二级菜单内,用户点击后即可跳转至小程序页面。 [图片] 温馨提示:备用网页是必填项,旧版微信客户端无法支持小程序,用户点击菜单时将会打开备用网页(认证的公众号可设置外链)。 2、设置菜单时提示“小程序未关联,菜单设置无效,请检查”? 目前平台规定自定义菜单设置跳转的小程序都需要进行关联。设置自定义菜单时,请确认之前菜单设置的小程序是否全部已关联,若菜单内容存在未关联的小程序,则会出现提示“小程序未关联,菜单设置无效,请检查”。 [图片] 小程序关联方法:【小程序】->【小程序管理】->【添加】-【关联小程序】,公众号管理员扫码确认后,请输入小程序AppID,发送邀请后,需小程序管理员微信号确认成功即可绑定。 [图片] 温馨提示: 1)公众号可关联同一主体的10个小程序,不同主体的10个小程序;1个小程序可关联最多500个公众号。 2)公众号关联小程序不要求已发布,但未发布的小程序不可展示在公众号资料页、图文消息、自定义菜单等场景(设置未发布小程序后路径显示空白)。 [图片]
2020-06-05 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: Android、IOS手机对于页面title的展示不一致,安卓title的显示不居中 页面的title只支持纯文本级别的样式控制,不能够做更丰富的title效果 左上角的事件无法监听、定制 路由导航单一,只能够返回上一页,深层级页面的返回不够友好 探索 小程序自定义导航栏已开放许久>>了解一下,相信不少小伙伴已使用过这个功能,同时不少小伙伴也会发现一些坑: 机型多如牛毛:自定义导航栏高度在不同机型始终无法达到视觉上的统一 调皮的胶囊按钮:导航栏元素(文字,图标等)怎么也对不齐那该死的胶囊按钮 各种尺寸的全面屏,奇怪的刘海屏,简直要抓狂 一探究竟 为了搞明白原理,我先去翻了官方文档,>>飞机,点过去是不是很惊喜,很意外,通篇大文尽然只有最下方的一张图片与这个问题有关,并且啥也看不清,汗汗汗… 我特意找了一张图片来 [图片] 分析上图,我得到如下信息: Android跟iOS有差异,表现在顶部到胶囊按钮之间的距离差了6pt 胶囊按钮高度为32pt, iOS和Android一致 动手分析 我们写一个状态栏,通过wx.getSystemInfoSync().statusBarHeight设置高度 Android: [图片] iOS:[图片] 可以看出,iOS胶囊按钮与状态栏之间距离为:4px, Android为8px,是不是所有手机都是这种情况呢? 答案是:苹果手机确实都是4px,安卓大部分都是7和8 也会有其他的情况(可以自己打印getSystemInfo验证)如何快速便捷算出这个高度,请接着往下看 如何计算 导航栏分为状态栏和标题栏,只要能算出每台手机的导航栏高度问题就迎刃而解 导航栏高度 = 胶囊按钮高度 + 状态栏到胶囊按钮间距 * 2 + 状态栏高度 注:由于胶囊按钮是原生组件,为表现一致,其单位在各种手机中都为px,所以我们自定义导航栏的单位都必需是px(切记不能用rpx),才能完美适配。 解决问题 现在我们明白了原理,可以利用胶囊按钮的位置信息和statusBarHeight高度动态计算导航栏的高度,贴一个实现此功能最重要的方法 [代码]let systemInfo = wx.getSystemInfoSync(); let rect = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null; //胶囊按钮位置信息 wx.getMenuButtonBoundingClientRect(); let navBarHeight = (function() { //导航栏高度 let gap = rect.top - systemInfo.statusBarHeight; //动态计算每台手机状态栏到胶囊按钮间距 return 2 * gap + rect.height; })(); [代码] gap信息就是不同的手机其状态栏到胶囊按钮间距,具体更多代码实现和使用demo请移步下方代码仓库,代码中还会有输入框文字跳动解决办法,安卓手机输入框文字飞出解决办法,左侧按钮边框太粗解决办法等等 胶囊信息报错和获取不到 问题就在于 getMenuButtonBoundingClientRect 这个方法,在某些机子和环境下会报错或者获取不到,对于此种情况完美可以模拟一个胶囊位置出来 [代码]try { rect = Taro.getMenuButtonBoundingClientRect ? Taro.getMenuButtonBoundingClientRect() : null; if (rect === null) { throw 'getMenuButtonBoundingClientRect error'; } //取值为0的情况 if (!rect.width) { throw 'getMenuButtonBoundingClientRect error'; } } catch (error) { let gap = ''; //胶囊按钮上下间距 使导航内容居中 let width = 96; //胶囊的宽度,android大部分96,ios为88 if (systemInfo.platform === 'android') { gap = 8; width = 96; } else if (systemInfo.platform === 'devtools') { if (ios) { gap = 5.5; //开发工具中ios手机 } else { gap = 7.5; //开发工具中android和其他手机 } } else { gap = 4; width = 88; } if (!systemInfo.statusBarHeight) { //开启wifi的情况下修复statusBarHeight值获取不到 systemInfo.statusBarHeight = systemInfo.screenHeight - systemInfo.windowHeight - 20; } rect = { //获取不到胶囊信息就自定义重置一个 bottom: systemInfo.statusBarHeight + gap + 32, height: 32, left: systemInfo.windowWidth - width - 10, right: systemInfo.windowWidth - 10, top: systemInfo.statusBarHeight + gap, width: width }; console.log('error', error); console.log('rect', rect); } [代码] 以上代码主要是借鉴了拼多多的默认值写法,android 机子中 gap 值大部分为 8,ios 都为 4,开发工具中 ios 为 5.5,android 为 7.5,这样处理之后自己模拟一个胶囊按钮的位置,这样在获取不到胶囊信息的情况下,可保证绝大多数机子完美显示导航头 吐槽 这么重要的问题,官方尽然没有提供解决方案…竟然提供了一张看不清的图片??? 网上有很多ios设置44,android设置48,还有根据不同的手机型号设置不同高度,通过长时间的开发和尝试,本人发现以上方案并不完美,并且bug很多 代码库 Taro组件gitHub地址详细用法请参考README 原生组件npm构建版本gitHub地址详细用法请参考README 原生组件简易版gitHub地址详细用法请参考README 由于本人精力有限,目前只计划发布维护好这2种组件,其他组件请自行修改代码,有问题请联系 备注 上方2种组件在最下方30多款手机测试情况表现良好 iPhone手机打电话和开热点导致导航栏样式错乱,问题已经解决啦,请去demo里测试,这里特别感谢moments网友提出的问题 本文章并无任何商业性质,如有侵权请联系本人修改或删除 文章少量部分内容是本人查询搜集而来 如有问题可以下方留言讨论,微信zhijunxh 比较 斗鱼: [图片] 虎牙: [图片] 微博: [图片] 酷狗: [图片] 知乎: [图片] [图片] 知乎是这里边做的最好的,但是我个人认为有几个可以优化的小问题 打电话或者开启热点导致样式错落,这也是大部门小程序的问题 导航栏下边距太小,看起来不舒服 搜索框距离2侧按钮组距离不对等 自定义返回和home按钮中的竖线颜色重了,并且感觉太粗 如果您看到了此篇文章,请赶快修改自己的代码,并运用在实践中吧 扫码体验我的小程序: [图片] 创作不易,如果对你有帮助,请移步Taro组件gitHub原生组件gitHub给个星星 star✨✨ 谢谢 测试信息 手机型号 胶囊位置信息 statusBarHeight 测试情况 iPhoneX 80 32 281 369 48 88 44 通过 iPhone8 plus 56 32 320 408 24 88 20 通过 iphone7 56 32 281 368 24 87 20 通过 iPhone6 plus 56 32 320 408 24 88 20 通过 iPhone6 56 32 281 368 24 87 20 通过 HUAWEI SLA-AL00 64 32 254 350 32 96 24 通过 HUAWEI VTR-AL00 64 32 254 350 32 96 24 通过 HUAWEI EVA-AL00 64 32 254 350 32 96 24 通过 HUAWEI EML-AL00 68 32 254 350 36 96 29 通过 HUAWEI VOG-AL00 65 32 254 350 33 96 25 通过 HUAWEI ATU-TL10 64 32 254 350 32 96 24 通过 HUAWEI SMARTISAN OS105 64 32 326 422 32 96 24 通过 XIAOMI MI6 59 28 265 352 31 87 23 通过 XIAOMI MI4LTE 60 32 254 350 28 96 20 通过 XIAOMI MIX3 74 32 287 383 42 96 35 通过 REDMI NOTE3 64 32 254 350 32 96 24 通过 REDMI NOTE4 64 32 254 350 32 96 24 通过 REDMI NOTE3 55 28 255 351 27 96 20 通过 REDMI 5plus 67 32 287 383 35 96 28 通过 MEIZU M571C 65 32 254 350 33 96 25 通过 MEIZU M6 NOTE 62 32 254 350 30 96 22 通过 MEIZU MX4 PRO 62 32 278 374 30 96 22 通过 OPPO A33 65 32 254 350 33 96 26 通过 OPPO R11 58 32 254 350 26 96 18 通过 VIVO Y55 64 32 254 350 32 96 24 通过 HONOR BLN-AL20 64 32 254 350 32 96 24 通过 HONOR NEM-AL10 59 28 265 352 31 87 24 通过 HONOR BND-AL10 64 32 254 350 32 96 24 通过 HONOR duk-al20 64 32 254 350 32 96 24 通过 SAMSUNG SM-G9550 64 32 305 401 32 96 24 通过 360 1801-A01 64 32 254 350 32 96 24 通过
2019-11-17 - 微信小程序swiper的自适应高度
小程序组件swiper需要指定固定高度,但在某些场景中我们需要动态设置swiper的高度以完整展示swiper中的内容,比如高度不同的图片,笔者最近项目中的日历组件(31号有时会多出一行)等等,如何使swiper组件自适应高度呢? 翻阅了一些网上的例子,一般的解决方法是通过设置style.height来解决 [代码]<swiper style="{{style}}" > <swiper-item></swiper-item> </swiper> [代码] [代码] Page({ data: { style: '' }, onReady(){ this.setData({style: 'height: 100px'}) } }) [代码] 问题:状态丢失 直接设置样式可以动态设置高度,但这样做的不好之处在于会重新渲染结构,导致之前设置的状态丢失,比如我们在日历中选中的日期 我们的需求是,1. 动态设置swiper高度,2. 不丢失之前的状态 一番折腾过后,发现这条路是个死胡同,不能解决问题。 解决: CSS变量 后来发现使用css变量也能够动态改变样式,抱着试一试的想法 模板 [代码]<view class="box" style="{{boxStyle}}"> <swiper class="container"> <swiper-item></swiper-item> </swiper> </view> [代码] 样式 [代码].box{ --box-height: 400px; --append-height: 0; width: 100vw; height: calc(var(--box-height) + var(--append-height)) } .container{ height: 100%; width: 100%; } [代码] js [代码]Page({ data: { boxStyle: '' }, onReady(){ if (...) { this.setData({boxStyle: '--append-height: 50px'}) } else { this.setData({boxStyle: '--append-height: 0'}) } } }) [代码] 上述设置,居然能够完美的实现项目需求,现在项目正在上线中,等待测试出bug,哈哈 欢迎关注github项目 关注下面的小程序查看最新的DEMO示例 [图片]
2020-06-24 - 微信小程序答题页——swiper渲染优化及swiper分页实现
前言 swiper的加载太多问题,网上资料好像没有一个特别明确的,就拿这个答题页,来讲讲我的解决方案 这里实现了如下功能和细节: 保证swiper-item的数量固定,加载大量数据时,大大优化渲染效率记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验示例动图 [图片] 截图 [图片] [图片] 问题原因 当swiper-item数量很多的时候,会出现性能问题 我实现了一个答题小程序,在一次性加载100个swipe-item的时候,低端手机页面渲染时间达到了2000多ms 也就是说在进入答题页的时候,会卡顿2秒多去加载这100个swiper-item 思考问题 那我们能不能让他先加载一部分,然后滑动以后再去改变item的数据,让swiper一直保持一定量的swiper-item? 注意到官方文档有这么两个属性可以利用,我们可以开启衔接滑动,然后再bindchange方法中去修改data [图片] 1、保证swiper-item的数量固定,加载大量数据时,优化渲染效率 假设我们请求到的数据的为list,实际渲染的数据为swiperList 我们现在给他就固定3个swiper-item,前后滑动的时候去替换数据 正向滑动的时候去替换滑动后的下一页数据,反向滑动的时候去替换滑动后的上一页数据 当我们知道了要替换的条件,我们便可以去替换数据了 但是我们应该考虑到临界值的问题,如果当前页是list第一项和最后一项该怎么办,向左向右滑是不是得禁止啊 这边是判断没数据会让它再弹回去 2、记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页 有很多时候,我们是从某一项直接进来的,比如说上次答题答到了第五题,我这次进来要直接做第六题 那么我们需要去初始化这个swiperList,让它当前页、上一页、下一页都有数据 3、答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验 从答题卡选择index,那就不仅仅是滑动上下页了,它可以跳转到任何页,所以也采用类似初始化swiperList的方法 swiper切换动画我这边是默认250ms,但是发现有时候从答题卡点击回来,你在答题卡点击的下一项不知道会从左还是从右滑过来 体验真的很差,一开始不知道怎么禁掉动画,其实在跳转到答题卡页的时候把duration设为0就可以了 然后在答题卡页的unload方法中恢复 关键点: 在固定3个swiper-item的同时,要保证我们可以有办法来替代微信自带swiper的current属性和change方法 swiper-limited-load使用方法及说明: 将components中的swiper-limited-load复制到您的项目中在需要的页面引用此组件,并且创建自己的自定义组件item-view在初始化数据时,为你的list的每一项指定index属性具体可以参照项目目录start-swiper-limited-load中的用法说明:其它属性和swiper无异,你们可以自己单独添加你们需要的属性总结 一开始很头疼,为什么微信小程序提供的这个swiper,没去考虑这方面 然后在网上和社区找也没有一个特别好的解决方案。 后来想想,遇到需求就静下来解决吧。 项目地址:https://github.com/pengboboer/swiper-limited-load 如果错误,欢迎指出。 如有新的需求也可以提出来,如果有时间的话,我会帮你们完善。 如果能帮到你们,记得给一个star,谢谢。 ---补充 有很多朋友在评论区提到了分页的需求,抽时间写了一个分页的Demo和大家分享一下。 还是以答题为例,比如我们一共有500条数据,一页20条,可能需要如下功能,乍一看不就加了个分页,挺简单的,其实实现起来挺麻烦的,下面说一下思路和一些需要特别注意的点: 1、从其他页面跳转到答题页时,不光只能默认在第一题,可以是任意一题,比如第80题。 跳转到任意一题,那么需要我们根据index算出该数据在第几页,然后需要请求该页数据,最后显示对应的index。我的思路更注重用户体验,不可能是上滑或者下滑才开始去请求数据,一定是要用户滑动前提前请求好数据。所以起码要保证左右两侧在初始化那一刻都有数据。如果此题和它的上一题下一题都在同一页,那么我们只需要请求一页数据(第15题,那么只需请求第1页数据)。如果此题和它的上一题或者下一题不在同一页,那么我们可能需要请求两页数据。(第20题,那么需要请求第1页和第2页数据) 2、左滑、右滑没数据时,都可以加载新数据。直到滑到第一题或者最后一题。 如果我们初始化时是第24题,那么我们左滑到第21题时,就应该去请求第一页的数据。那么用户在看完21题时,再滑到20题,可能就根本不会感知到通过网络请求了数据。但是如果用户此刻滑动特别快:滑到21题时请求了网络,请求还没成功,就又向左滑了。那么我们需要限制用户的滑动,给用户一个提示:数据正在加载中。 3、从答题卡点击任意一题可以跳转到相应的题目,并且左右滑动显示正常数据 比如我们初始化是跳转到了第80题,不一会点击答题卡又要跳转到200题,一会又跳转到150题。各种无序操作,你也不知道用户要往哪里点。 一开始是想着维护一个主list,点到哪道题往list中添加这道题所在的当页的数据,但是还得判断这一页或者左滑右滑请求新一页的数据得往list的哪个位置添加。这来回来去乱七八糟的判断就很麻烦了,很容易出bug。而且list长度太长了以后insert的性能也不好。 后来就去想,要不答题卡点击任意一题都清空旧的list,然后请求新的数据,左右滑动没数据了再请求新的数据呗。但是这样很浪费资源,并且用户体验也不好,用户已经从第1题答到第200题了,这时用户从答题卡选择了一个25题,还得重新请求网络。而且200道题的数据都没了,那再选个26题,再重新请求网络?网络有延时不说,还浪费资源。 最后转念一想,这时候就需要弄一个缓存了。所以最终的解决方法就出来了:我们维护一个map,在网络请求成功后,在map中保存对应页的数据,同时我们维护一个主list来显示对应的题目。当我们在答题卡选择某一题目,就清空list,然后判断map中有没有该页的数据,如果有就直接拿来,没有就再去网络请求。这个处理方式,写法相对来说简单,不需要乱七八糟的判断,也不浪费资源,用户体验也很不错。 总结 以上就是一些思路和要注意的地方。这个Demo断断续续花了好几天时间写出来的。可能我说的比较啰嗦比较细,只是想让需要用到这个分页Demo的同学能理解我是如何实现的。 如果觉得能帮到你,记得给一个star,谢谢。同时如果这个demo有bug或者你们有新想法,欢迎提出来。
2021-01-07 - 极致的scroll-view的下拉刷新扩展组件
不敢说是最好的,但是感觉也应该是性能和体验比较极致的下拉刷新扩展了,老规矩,代码片段放最后了~ 2020.2.22 修复了小程序基础库v2.10.2带来的不能滚动的问题,最新代码片段见scroll-view-extends 原理 其实原理很简单,和普通H5以及市面上有的下拉刷新没有特别大的区别,都是基于[代码]touch[代码]手势检测事件来实现下拉刷新的。[代码]touchstart[代码]的时候记录当前触摸点,[代码]touchmove[代码]的时候开始计算移动方向和移动距离, [代码]touchend[代码]的时候计算是否要进行下拉刷新操作。如图所示: [图片] 实现方法 调研了一些实现方法,目前大部分都是通过js计算,然后setData来改变元素的[代码]transform[代码]值实现下拉刷新。考虑到性能问题,此处使用了[代码]wxs[代码]的响应式能力来实现整个计算逻辑,不用通过逻辑层和视图层通信,直接在视图层进行渲染。具体文档请参考wxs响应事件。 这里在[代码]list[代码]组件(由[代码]scroll-view[代码]组成)下抽出了一个[代码]scroll.wxs[代码]作为响应事件的事件处理函数集合,源码基本上就在[代码]scroll.wxs[代码]和[代码]list[代码]组件。 [代码]scroll.wxs[代码]定义了如下变量和函数: [代码]var moveStartPosition = 0 //开始位置 var moveDistance = 0 //移动距离 var moveRefreshDistance = 60 //达到刷新的阈值 var moveMaxDistance = 100 //最大可滑动距离 var isRefreshMaxDown = false //是否达到了最大距离, 用来判断是否要震动提示 var loading = false //是否正在loading ... ... module.exports = { touchStart: touchStart, //手指开始触摸事件 touchMove: touchMove, //手指移动事件 touchEnd: touchEnd, //手指离开屏幕事件 loadingTypeChange: loadingTypeChange, //请求状态变化监听,监听刷新请求开始和请求完成 triggerRefresh: triggerRefresh //主动触发刷新操作,比如点击页面上一个按钮,重新刷新list,这就需要用到这个方法 } [代码] [代码]touchStart[代码]和[代码]touchMove[代码]就不用说了,代码注释都很明白,普通的监听移动和处理逻辑。 [代码]touchEnd[代码]主要是判断移动距离是否达到了阈值,然后根据结果,调用监听实例的[代码]callMethod[代码]方法触发[代码]refreshStart[代码]或者[代码]refreshCancel[代码]方法,这两个方法都是写到[代码]list[代码]组件里面的,用来触发刷新方法或者取消刷新。 [代码]loadingTypeChange[代码]方法主要是监听刷新是否完成,以此来触发动画效果。 [代码]triggerRefresh[代码]通过监听主动触发的变量来处理。如果需要主动触发刷新,则调用[代码]list[代码]组件内部的[代码]forceRefresh[代码]方法,具体使用示例在[代码]index/index/js[代码]的[代码]onLoad[代码]函数有: [代码]this.selectComponent('.list').forceRefresh()[代码] [代码]scroll.wxs[代码]里面还有一个未导出的方法,叫[代码]drawTransitionY[代码],这个方法主要是因为[代码]ios12[代码]对于[代码]transition[代码]动画效果支持的不好,所以自己写了个Y轴方向的动画([代码]linear[代码]线性的),大佬们可以自己往上添加各种[代码]ease-in-out[代码]效果。 里面具体的实现可以查看代码注释哦~ 使用 好了,前面讲了实现的原理和方法,那么在代码里面,应该怎么直接使用呢?如下代码所示: [代码]<!-- 使用示例 --> <list class="list" refresh-loading="{{refreshLoading}}" loading="{{loading}}" bindrefresh="initList" bindloadmore="loadmore"> <!-- your code --> </list> [代码] [代码]refresh-loading[代码]属性用来通过外部loading态来控制刷新动画的开始结束,因为每当变化[代码]refresh-loading[代码]的值时,会将变化同步到组件内的[代码]showRefresh[代码]属性,[代码]wxs[代码]通过监听[代码]showRefresh[代码]来处理动画逻辑。 [代码]loading[代码]属性是上拉加载更多的时候触发的loading态展示,跟刷新无关 [代码]bindrefresh[代码]是刷新触发时绑定的函数,下拉刷新动画成功开始后触发这个函数 [代码]bindloadmore[代码]透传[代码]scroll-view[代码]的加载更多方法 当然,源码里面也包含了一个[代码]list-item[代码]组件,这个跟本文没太大关系,是用来做瀑布流长列表内容太多时的内存不足问题解决方案的,具体请看解决小程序渲染复杂长列表,内存不足问题 干货 最后,上代码片段, 小程序代码片段 github地址
2020-02-22 - 熟练使用微信开发工具的代码块功能提升编码效率,降低误码率
1. 写在前面 自从上次介绍了宇宙第一强集成IDE的微信开发者工具安装插件等隐藏功能后,很多开发者社区哥们老铁感兴趣,一些哥们还私我问我是否支持代码块功能。答案是:支持!而且很完美的支持(此处必须有感恩,感恩微信团队每天的辛苦开发,作为同开发过小编辑器的人知道其中的苦与乐,同时呼吁各位猿少些喷喷喷的负能量,多些宣传和赞美的正能量!)。代码块是小编非常非常常用的一个功能,基本从04年开始建站和开发过程用过的所有IDE中都必备的功能。熟练使用微信开发工具的代码块功能提升编码效率,降低高频代码段的误写率。 2. 什么是代码块 可能有些刚接触开发的哥们不了解什么是「代码块」,这里我仅仅说说我个人对「代码块」功能的理解:代码块,英文名字:Code snippets,代码块的作用就是把比较长的一段代码在编码时只需要输入简写后的几个字母,比如把高频的代码console.log();简写成「clg」,再比如把小程序的页面.js里面的onLoad(),onShow等常用代码简写成「page_init」几个字母,最早提出这个概念和想法的人我也不知道是谁,总之得感谢他。如果说现在很多的编程语言有「语法糖」的说法,那么我个人觉得「代码块」可以称为「代码糖/编码糖」!熟练使用编码糖一定能让你尝到编码中的甜味。他的优点非常明显:极速,0误差输入高频代码段。个人觉得无论你现在处在编码的哪个级别都应该熟练使用Code snippets来提升工作效率。 3. 如何使用 3.1 打开开发者工具的编辑器扩展目录 [图片] 3.2 创建相关文件夹 返回上一级目录到User目下(里面有Workspaces文件夹),创建/进入snippets,此目录mac下完整路径应为:"~/Library/Application Support/微信开发者工具/【当前开发者工具特征码】/Default/Editor/User/snippets" [图片] 3.3 新建/编辑代码块json文件(如上图) [图片] 格式如上面,1,2,3是我比较常用的代码糖,生效后输入图1里面的clg回车就是console.log()(可以用这个来检测你代码糖功能是否生效);并且光标自动定位到()里,如下图: [图片][图片] 3.4 附带自用all.code-snippets 下面附上我常用的粗陋的代码糖块文件all.code-snippets(可以直接复制使用,你可以自行自己添加删除。里面有自己写的java框架常用的代码块,php常用和html常用以及nginx比较常用的,个人习惯不一样建议全部干掉重来,只需按里面的格式来写就可以了,格式如下: 字段名 意义 备注 prefix 简写后的字符串 必填 body 原字符串,可以用转义符 必填 description 备注 可控 [代码]{ // Example: // "Print to console": { // "scope": "javascript,typescript", // "prefix": "log", // "body": [ // "console.log('$0');", // "$2" // ], // "description": "Log output to console" // } //java begin "valueOf": { "prefix": "val", "body": "valueOf($0)", }, "parseInt": { "prefix": "par", "body": "Integer.parseInt($0);", }, "parseLong": { "prefix": "parl", "body": "Long.parseLong($0);", }, "equ": { "prefix": "equ", "body": "equals(\"$0\")", }, "isnull": { "prefix": "isnull", "body": "Tools.myIsNull($0)", }, "setattr": { "prefix": "setattr", "body": "request.setAttribute(\"$0\",$2)", }, "getattr": { "prefix": "getattr", "body": "request.getAttribute(\"$0\")", }, "getparam": { "prefix": "getparam", "body": "request.getParameter(\"$0\")", }, "getpost": { "prefix": "getpost", "body": "TtMap postUrl = Tools.getUrlParam();\r\nTtMap post = Tools.getpostmap(request, true);// 过滤参数,过滤mysql的注入,url参数注入\r\n" }, "tcf": { "prefix": "tcf", "body": "try{\r\n}catch(Exception e){\r\n\tTools.logError(e.getMessage());\r\n}finally{\r\n\t$0.closeConn();\r\n}", }, "777": { "prefix": "777", "body": "Runtime.getRuntime().exec(\"chmod 777 -R \" + strFileFullPath);" }, "go-1": { "prefix": "go-1", "body": "javascript:history.go(-1);" }, "dbctrl": { "prefix": "!dbctrl", "body": "DbCtrl dbCtrl = new DbCtrl(\"$0\");\r\ntry{\r\n\tTtList list = dbCtrl.lists(\"\", \"\");\r\n\tTtMap info = dbCtrl.info($2);\r\n}catch(Exception e){\r\n\tTools.logError(e.getMessage());\r\n\tif (Config.DEBUGMODE) {\r\n\t\te.printStackTrace();\r\n\t}\r\n}finally{\r\n\tdbCtrl.closeConn();\r\n}", "description": "TT-DbCtrl" }, "dbtools": { "prefix": "dbtools", "body": "DbTools dbTools = new DbTools();\r\ntry{\r\n\tTtList list = dbTools.reclist(\"$0\");\r\n\tTtMap info = dbTools.recinfo(\"$2\");\r\n}catch(Exception e){\r\n\tTools.logError(e.getMessage());\r\n}finally{\r\n\tdbTools.closeConn();\r\n}" }, "!recinfo": { "prefix": "rrecinfo", "body": "Tools.recinfo(\"$0\")", "description": "TT-DbTools-recinfo" }, "!reclist": { "prefix": "rreclist", "body": "Tools.reclist(\"$0\")", "description": "TT-DbTools-reclist" }, "ss": { "prefix": "ss", "body": "String[] sS = new String[]{}; ", "description": "new String[]" }, "mss": { "prefix": "mss", "body": "TtMap info = new HashMap<>();", "description": "new Map<String,String>" }, "lss": { "prefix": "lss", "body": "TtList lmss = new ArrayList<>();", "description": "new TtList " }, "<%": { "prefix": "!s", "body": "<%\r\n\t$0\r\n%>", "description": "TT-JSP<%自动完成" }, "<%=": { "prefix": "!ss", "body": "<%=$0%>", "description": "TT-JSP<%=自动完成" }, "jsp_include": { "prefix": "!sss", "body": "<jsp:include page=\"<%=$0%>\">\r\n\t<jsp:param name=\"$3\" value=\"$2\"/>\r\n</jsp:include>" }, "jsp_include2": { "prefix": "<jsp", "body": "<jsp:include page=\"<%=$0%>\">\r\n\t<jsp:param name=\"$3\" value=\"$2\"/>\r\n</jsp:include>" }, "!jsp_requery": { "prefix": "!req", "body": "request.getParameter(\"$0\")", }, "!jsp_requery_getAttribute": { "prefix": "!req2", "body": "request.getAttribute(\"$0\")", }, "!": { "prefix": "!ssss", "body": "<%@page import=\"com.tt.tool\"%>\r\n<%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\"%>\r\n<%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%>\r\n<%@ taglib prefix=\"fmt\" uri=\"http://java.sun.com/jsp/jstl/fmt\"%>\r\n<%@ taglib uri=\"http://java.sun.com/jsp/jstl/functions\" prefix=\"fn\"%>\r\n<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\">\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\r\n <title>$0</title>\r\n</head>\r\n<body>\r\n\t$2\r\n</body>\r\n</html>", "description": "jsp-header" }, "div": { "prefix": "<div", "body": "<div id=\"$0\" name=\"$0\">\r\n\t\r\n</div>", }, "p": { "prefix": "<p", "body": "<p id=\"\" name=\"\"></p>", }, "a": { "prefix": "<a", "body": "<a href=\"$0\" id=\"\" name=\"\">$2</a>", }, "head": { "prefix": "<head", "body": "<head>\r\n\t$0\r\n</head>", }, "img": { "prefix": "<img", "body": "<img id=\"\" name=\"\" src=\"$0\">" }, "ul": { "prefix": "<ul", "body": "<ul id=\"\" name=\"\">\r\n\t\r\n</ul>" }, "li": { "prefix": "<li", "body": "<li id=\"\" name=\"\">\r\n\t\r\n</li>" }, "select": { "prefix": "<select", "body": "<select id=\"\" name=\"\">\r\n\t\r\n</select>" }, "option": { "prefix": "<option>", "body": "<option value=\"\">$0</option>" }, "input": { "prefix": "<input", "body": "<input type=\"$0\" id=\"$2\" name=\"$2\" value=\"$3\">" }, "form": { "prefix": "<form", "body": "<form id=\"info_form\" action=\"$${posturl}\" class=\"form-horizontal\" method=\"post\" enctype=\"multipart/form-data\">\r\n\t$2\r\n</form>" }, "!i": { "prefix": "!i", "body": "<%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\"%>\r\n<%@ page import=\"com.tt.tool.Tools\" %>\r\n<%@ page import=\"com.tt.data.TtMap\" %>\r\n<%@ page import=\"com.tt.data.TtList\" %>\r\n<%@ page import=\"com.tt.tool.JspTools\" %>\r\n<%@ page import=\"java.util.*\" %>\r\n<%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%>\r\n<%@ taglib prefix=\"fmt\" uri=\"http://java.sun.com/jsp/jstl/fmt\"%>\r\n<%@ taglib uri=\"http://java.sun.com/jsp/jstl/functions\" prefix=\"fn\"%>\r\n<%@ taglib prefix=\"Tools\" uri=\"/tld/manager\" %>\r\n", }, "loginfo": { "prefix": "loginfo", "body": "Tools.logInfo($0)", "description": "loginfo" }, "logerror": { "prefix": "logerror", "body": "Tools.logError($0)", "description": "loginfo" }, //javascript "!go-1": { "prefix": "!go-1", "body": "javascript:history.go(-1);", }, "!tag": { "prefix": "!tag", "body": "<%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%>\r\n<%@ taglib prefix=\"fmt\" uri=\"http://java.sun.com/jsp/jstl/fmt\"%>\r\n<%@ taglib uri=\"http://java.sun.com/jsp/jstl/functions\" prefix=\"fn\"%>\r\n<%@ taglib prefix=\"Tools\" uri=\"/tld/manager\" %>", }, "$$": { "prefix": "$$", "body": "$(function(){$0\r\n});", "description": "TT-jquery自动运行" }, "$(": { "prefix": "$f", "body": [ "$(function(){", "});" ], "description": "TT-jquery自动运行" }, "timer": { "prefix": "timer", "body": "inv = setInterval(\"$0showtimes();\",1000);", "description": "TT-JS每隔几秒自动运行", }, "!posturl": { "prefix": "!posturl", "body": "<%=Tools.urlKill(\"id$0\")%>", }, "!ifdebug": { "prefix": "ifd", "body": [ "if (Config.DEBUGMODE) {", "\te.printStackTrace();", "}" ], }, "reload":{ "prefix": "reload", "body": "location.reload(true);" }, //PHP "<?php": { "prefix": "php", "body": "<?php $0 ?>", "description": "Global-PHP插入标签" }, "nav":{ "prefix": "nav", "body": "<navigator url=\"car_yy/car_yy_ppuser/car_yy_ppuser\">", "description": "老子的微信小程序的跳转", }, "bind":{ "prefix": "bind", "body": "bindtap=\"$1\"", "description": "老子的bindtap" }, "wx_ra":{ "prefix": "wepy_ra", "body":"let result = await util.wxRequest(httpSet);", "description": "微信小程序阻塞等待请求" }, "wx_r":{ "prefix": "wepy_r", "body":["util.wxRequest(httpSet).then(function(result){\r\n\tif(result.result==\"success\"){$1\r\n\t}\r\n});"], "description": "微信小程序阻塞等待请求" }, "httpset":{ "prefix": "wepy_hs", "body": "let params= {};\r\nlet httpSet={\r\n\turl:'$1',\r\n\tparams:params,\r\n\tmethod:'GET',\r\n\tisShowLoading: false,\r\n}", "description": "httpSet" }, "wepy_global":{ "prefix": "wepy_global", "body": "wepy.$$instance.globalData.$1", }, "wepy_castfail":{ "prefix": "wepy_castfail", "body": "this.$$broadcast('alertHeaderWarning', '$1');" }, "wepy_castOK":{ "prefix": "wepy_castok", "body": "this.$$broadcast('alertHeader', '$1');" }, "wepy_castfail_height":{ "prefix": "wepy_castfailh", "body": "this.$$broadcast('alertHeaderWarningHeight', '$1',60);" }, "wepy_castOK_height":{ "prefix": "wepy_castokh", "body": "this.$$broadcast('alertHeaderHeight', '$1',60);" }, "clg": { "prefix": "clg", "body": "console.log($0);" }, "wepy_tt":{ "prefix": "wepy_tt", "body": "let that = this;" }, "wepy_currtarget":{ "prefix": "wepy_currt", "body": " e.currentTarget.dataset.$1", }, "wepy_gourl":{ "prefix":"wepy_gourl", "body":"@tap=\"goUrl\" data-url=\"$1\" data-memberflag=\"$2\"", }, "wepy_goback":{ "prefix": "wepy_goback", "body":"wepy.navigateBack();" }, "wepy_urlencode":{ "prefix": "wepy_urlencode", "body": "encodeURIComponent($1);" }, "wepy_urldecode":{ "prefix": "wepy_urldecode", "body": "decodeURIComponent($1);" }, "wepy_noimg":{ "prefix": "wepy_noimg", "body": "/images/imgload.png", }, "wepy_errmsg":{ "prefix": "wepy_errmsg", "body": "(result && result.errorMsg) ? result.errorMsg :\"删除失败!\"", }, "wepy_sets":{ "prefix": "wepy_sets", "body": "wepy.setStorageSync($1,$2);" }, "wepy_gets":{ "prefix": "wepy_gets", "body": "wepy.getStorageSync($1);" } } [代码] 4. 相关链接 持续更新:收藏整理官方隐藏的小程序功能/参数/方法/API 动手打造更强更好用的微信开发者工具-编辑器扩展篇 点击wxml里面的绑定事件/变量名/样式名直接跳转到对应文件代码 小程序编码时变量名中文转英文变量名工具,各种驼峰取名 微信开发者工具编辑器支持「书签」功能,快速跳转到指定文件指定行和列 愉快的编写和调试Java:体验新版开发者工具的编辑器扩展功能 5. 其他未公布的隐藏功能 当你觉得有用的时候,就点赞和收藏或者分享,觉得没用的话就投诉,不管点赞还是投诉后的码农写代码永无BUG ,CP设计的产品人见人爱,BOSS每年收入翻番! ↓↓↓↓↓↓__________________________________________________________________________投诉的话点…↓↓↓
2020-05-17 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - 实战丨如何制作一个完整的外卖小程序(已开源)
最近微信小店开放了,赶着微信全面开放之前,把自己的小程序开源出来给大家使用~ 小程序效果 [图片] [图片] [图片] 开发心得 如何在项目中集成云开发 一开始项目并非基于云开发而开发的,目前考虑用云开发,因此,需要在项目中开启云开发的相关选项。 首先,在小程序文件夹中建立 [代码]cloud[代码] 文件夹,并在package文件中配置,建立用户登录的云函数并上传到微信小程序云中。相关的操作可以参考官方文档。 我在项目目录中添加了 [代码]cloud[代码] 和 [代码]miniprogram[代码] 两个目录,并在 [代码]project.config.json[代码] 文件夹进行配置 [代码]{ "miniprogramRoot": "./miniprogram" "cloudfunctionRoot": "./cloud/" } [代码] 开通云开发 配置完成后,可以点击控制台中的「云开发」来开通云开发。 [图片] 在云开发的界面中配置,并开通云开发。 [图片] 开通数据库集合 云开发不会自动创建数据库集合,因此,你需要手动创建集合。分别创建 店铺表Seller、分类表Category、商品表Food、订单表Order、地址表Address、用户表*_User*。 [图片] 数据操作 有了数据库的表后,就可以在代码中对数据进行操作了。 下方是我进行目录操作的代码。 [代码]const db = wx.cloud.database() const { showModal } = require('../../utils/utils') Page({ onLoad: function(options) { // 管理员认证 getApp().auth() if (options.objectId) { // 缓存数据 this.setData({ isEdit: true, objectId: options.objectId }) // 请求待编辑的分类对象 db.collection('Category') .doc(options.objectId) .get() .then(res => { // 获取分类信息 this.setData({ category: res.data }) }) } }, add: function(e) { var form = e.detail.value if (form.title == '') { wx.showModal({ title: '请填写分类名称', showCancel: false }) return } form.priority = Number.parseInt(form.priority) // 添加或者修改分类 // 修改模式 if (this.data.isEdit) { const category = this.data.category db.collection('Category') .doc(category._id) .update({ data: form }) .then(res => { console.log(res) showModal() }) } else { db.collection('Category') .add({ data: form }) .then(res => { console.log(res) showModal() }) } }, showModal() { // 操作成功提示并返回上一页 wx.showModal({ title: this.data.isEdit ? '修改成功' : '添加成功', showCancel: false, success: () => { wx.navigateBack() } }) }, delete: function() { // 确认删除对话框 wx.showModal({ title: '确认删除', success: res => { if (res.confirm) { const category = this.data.category db.collection('Category') .doc(category._id) .remove() .then(res => { console.log(res) wx.showToast({ title: '删除成功' }) wx.navigateBack() }) } } }) } }) [代码] 联表查询 在使用数据库时,难免要进行联表查询,云开发支持在云函数侧进行联表查询,你可以参考我的代码,来实现联表查询的功能。 [代码]const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() // 云函数入口函数 exports.main = async (event, context) => { const result = await db.collection('Food') .aggregate() .lookup({ from: 'Category', localField: 'category', foreignField: '_id', as: 'categories' }) .end() // .orderBy('priority', 'asc') // .get() console.log(result) return result.list } [代码] 文件上传 在小程序的操作中,难免会遇到需要进行图片上传的场景。在进行图片上传时,云开发提供了方便的云存储供我们查询数据。 在获取到文件的本地路径后,调用 [代码]wx.cloud.uploadFile[代码] 即可上传文件。 [代码]chooseImage() { wx.chooseImage({ count: 1, // 默认9 sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有 sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 success: res => { const tempFilePaths = res.tempFilePaths const file = tempFilePaths[0] const name = utils.random_filename(file) //上传的图片的别名,建议可以用日期命名 console.log(name) wx.cloud.uploadFile({ cloudPath: name, filePath: file, // 文件路径 }).then(res => { console.log(res) const fileId = res.fileID // 将文件id保存到数据库表中 db.collection('Seller').doc(this.data.seller._id) .update({ data: { logo_url: fileId } }).then(() => { wx.showToast({ title: '上传成功' }) // 渲染本地头像 this.setData({ new_logo: fileId }) }, err => { console.log(err) wx.showToast({ title: '上传失败' }) }) }) } }) } [代码] 微信支付逻辑的实现 作为一个商城,难免会有微信支付相关逻辑的实现。在这种情况下,可以借助云开发提供的微信支付云调用功能实现快速的 API 调用和接口的实现。 绑定商户 在使用云开发提供的微信支付时,需要先执行微信支付的绑定,在云开发控制台添加相应的商户号 [图片] 添加后微信会发来通知 [图片] 根据提示,开通账号即可。 [图片] 如果不绑定,将报“受理关系不存在”的错误 [图片] 函数代码调用 配置完成后,只需要在云函数中调用微信支付的接口,就可以实现相关调用的能力 [代码]const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { console.log('请求中') console.log(cloud.getWXContext().ENV) let { orderId, amount, body } = event const wxContext = cloud.getWXContext() const res = await cloud.cloudPay.unifiedOrder({ body: body, outTradeNo: orderId, spbillCreateIp: '127.0.0.1', subMchId: '1447716902', totalFee: amount, envId: 'dinner-cloud', functionName: 'pay_cb' }) return res.payment } [代码] 这里 [代码]functionName: 'pay_cb'[代码]指的就是支付成功后,微信支付那侧给我的回调信息,后面我们就用它来更新我们的订单状态 小程序端代码调用 调用云函数后,会获得微信支付所需要的各种参数, [图片] 这个时候,就可以在小程序端调用微信支付接口,进行支付,相关代码可以参考 [代码]const { result: payData } = res wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, signType: 'MD5', paySign: payData.paySign, success: res => { console.log('支付成功', res) wx.showModal({ title: '支付成功', showCancel: false, success: () => { // 跳转订单详情页 wx.navigateTo({ url: '/order/detail/detail?objectId=' + order._id }) } }) }, ... [代码] 微信支付回调处理 微信统一下单里一个pay_cb回调函数,它是一个云函数,后续微信支付的支付信息将会发送在这个函数中,相应的,我们需要编写处理的方法 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ // API 调用都保持和云函数当前所在环境一致 env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() // 云函数入口函数 exports.main = async (event, context) => { console.log('支付回调') console.log(event) console.log(cloud.getWXContext().ENV) const orderId = event.outTradeNo const resultCode = event.resultCode if (resultCode === 'SUCCESS') { const res = await db .collection('Order') .doc(orderId) .update({ data: { status: 1 } }) console.log(res) return { errcode: 0 } } } [代码] 总结 云开发体验下来,优点自不必多说,微信登录与支付原生支持,调用与调试都很方便,特别是不用启本地服务开发,真的好用; 这个小程序的源码我已经开源了,你可以访问社区官网 获取源码,自行使用~ 作者:黄秀杰,16年开始从事小程序开发与技术布道,同名个人公众号「黄秀杰」。 云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 产品文档:https://cloud.tencent.com/product/tcb 技术文档:https://cloudbase.net 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2020-07-29 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 纯CSS实现圆环型进度条
以下内容来自于去年的一次案例,随着微信小程序的不断改版,部分条件可能已不再适用,请谨慎参考。内容比较短,主要都在代码片段里。 案例 某个项目中需要用到如下图这样的一个圆环行的进度条。 [图片] 一开始的想法是使用canvas来实现,但是canvas是原生组件,层级最高(当时的情况),实际使用时不方便使用。所以决定尝试用纯CSS来实现这一效果。 实现原理 先上代码:https://developers.weixin.qq.com/s/gjmxwUmm76dG 这里主要用到的是CSS中的clip属性,将一个正方形裁剪后只显示右侧一半,但是仍然以正方形中心为圆心来旋转,来实现需要的角度。 [图片] [代码]clip: rect(0rpx, 46rpx, 92rpx, 0rpx); [代码] 这样最上面那个进度条就可以由以下三部分叠加,在最上面再叠加一个小一号的白色圆形,最外层加上圆角后就可以实现。(下图中红线示例了最外层的圆角以及最上层叠加的白色圆形位置) [图片] 叠加效果 [图片] 用到蓝色圆环小于180度的情况下,需要把背景色和前景色对调。
2019-12-26 - 如何使用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