个人案例
- createInnerAudioContext播放音频没有声音
- 当前 Bug 的表现(可附上截图) 无声音 - 预期表现 能正常播放音频 - 复现路径 播放不了 - 提供一个最简复现 Demo 判断版本 大于1.6.0使用以下方法播放音频 [代码]const innerAudioContext = wx.createInnerAudioContext()[代码][代码]innerAudioContext.src = [代码][代码]'../../sound/tap.wav'[代码] [代码]// 本地音频地址[代码][代码]innerAudioContext.play()[代码] 开发工具的基础库版本我尝试调到了最新,也是不能播放声音,无任何报错日志!
2019-03-05 - 安卓手机wx.createInnerAudioContext()无法播放音频
手机播放走的是onError方法报的是unknow format 安卓手机无法播放,我的是荣耀手机
2019-05-31 - 数字藏品(NFT)小程序需要什么资质?
数字藏品(NFT)小程序需要什么资质?有懂的吗 麻烦提示下 谢谢
2022-03-03 - 小程序插屏广告,多个页面都得弹,我应该每个页面都申请一个广告id还是共用一个呢?
[图片] 小程序插屏广告,多个页面都得弹,我应该每个页面都申请一个广告id还是共用一个呢? 比如我现在有 5个小程序页面,每个页面都想弹出一个 插屏广告, 我应该申请一个广告位id,还是每个页面都申请一个独立的广告位id。 这个一个id对应多个界面和每个界面独立的id,有什么区别,在收益 单价上 会不会有影响。
03-06 - Coolui Scroll v3.2.3 新增下拉刷新二楼组件
前言 前些日子我收到了一个邮件,内容是这个样子的: [图片] 随即我就打开了他所说的麦当劳小程序,这不就是下拉二楼的效果么。。。复不复杂先不说,这不妥妥的挑衅么~ 好像我写不出来似的。有求必应,必须安排。何况我也有计划加这个功能。 尝试一: 说干就干,先在社区搜搜看看有没有人搞过这个。不过只搜到了下面这个帖子: [图片] 心说我都写小程序了还要自己写结构写动画么,应该不用吧~,而且我的组件库是基于scroll-view封装的。里面新提供了一些属性和方法还挺不错的。比如: ScrollViewContext.scrollTo(Object object) 这个方法不是可以设置滚动到指定位置还有动画么 scrollEnabled 这个设置不是能控制scroll-view什么时候开始滚动么 然后在scroll-view里设置两个一屏高度的view。然后将scroll-view默认先滚动上去一屏。然后下拉的时候在拉下来,是这个思路。然后就开始写起来。 经过努力搞定了。而且在我的三星手机上效果还不错~ 然后我就拿着我的demo开开心心的去找我的朋友(测试小白鼠),用他高贵的苹果体验一下。等他发过来录屏的视频我傻眼了。。。这怎么在ios下往下拖拽没反应?首先我想到了scrollEnabled,我在touchstart的时候开启,touchend的时候设置关闭。是不是他的问题?注释掉之后果然好了。。。能拖动了,但是这往下滑二楼没有自动往下滚啊。这。。。scrollTo也不好使。看一圈文档确定我写的没问题,enhanced啥的我都开启了。没辙文档也不能全信,安卓和开发者工具好使,ios真机不行~ 尝试二: 既然科技不行,那就上狠活被~ 新的渲染引擎Skyline看着不错。而且demo里手势协商那个页面不就是我要的效果么。。。改改就能实现了。这些个手势组件,动画方法想想都激动。 不过凡是留个心眼~我先用ios试试,万一不行呢。。。朋友的ios15.6.1运行官方示例没问题。我掏出我的iphone 6 ios12,小程序正常,点开Skyline的页面直接闪退了。。。果断放弃了 最后的实现: 算了还是踏踏实实写动画吧~ 还是那个思路一个view(wapper) 里套两个一屏高度view(floor、page)。往下拖拽的时候设置view的translateY 移动。利用小程序的界面的animation动画会比自己写的css效果流畅一些。 先设置外层的wapper默认移动上去一屏高度。 touchmove计算手拖动的距离, 实时改写translateY 松手时判断。当前移动的距离,如果小于屏幕的六分之一则自动回弹,如果大于六分之一小于三分之一,则页面移动到六分之一的位置开启正在刷新状态。如果大于三分之一,直接移动一屏距离将二楼展现出来。 过程写差不多了,而且也提供了插槽和一些搭配的refresh组件。 这还不够~ 我又打开了手机里的一些常见的app去看他们的效果,大受启发~ 我发现有的二楼是从中间展开的,有的是从顶部展开的,有的还带有缩放的效果。思考之后也加了进去。。 1. 基础使用 搭配 second-floor-refresh 组件实现文字提示,同时也提供了插槽位置支持您自定义,自定义 refresh 组件请参考 second-floor-refresh 内部的写法 [代码]setText[代码] 是关键方法 [代码]<second-floor bind:refresh="onRefresh" bind:secondShow="onSecondShow" bind:secondBack="onSecondBack" model:threshold="{{val}}" class="my-second-floor" > <view slot="second-floor"> 二楼区域 </view> <!-- 刷新组件 --> <second-floor-refresh slot="second-floor-refresh"></second-floor-refresh> <!-- 刷新组件 --> <view>页面主内容</view> </second-floor> [代码] [代码]Page({ data: { val: 0, // 拖拽的进度值 }, onRefresh() { // 下拉刷新时执行 }, onSecondShow() { // 下拉二楼展开之后执行 }, onSecondBack() { // 下拉二楼关闭之后执行 }, }); [代码] 2. 可搭配 page-container 很多 app 的下拉二楼效果是下拉之后打开一个新页,搭配 page-container, 在 onSecondShow 方法中控制 page-container 的 show 可以实现,然后利用组件的 back 事件进行关闭 [图片] [代码]<second-floor bind:refresh="onRefresh" bind:secondShow="onSecondShow" bind:secondBack="onSecondBack" model:threshold="{{val}}" class="my-second-floor" > <view slot="second-floor"> 二楼区域 </view> <!-- 刷新组件 --> <second-floor-refresh slot="second-floor-refresh"></second-floor-refresh> <!-- 刷新组件 --> <view>页面主内容</view> </second-floor> <page-container show="{{show}}" round="{{round}}" overlay="{{overlay}}" duration="{{duration}}" position="{{position}}" close-on-slide-down="{{false}}" bindbeforeenter="onBeforeEnter" bindenter="onEnter" bindafterenter="onAfterEnter" bindbeforeleave="onBeforeLeave" bindleave="onLeave" bindafterleave="onAfterLeave" bindclickoverlay="onClickOverlay" custom-style="{{customStyle}}" overlay-style="{{overlayStyle}}" > <view class="detail-page"> <button type="primary" bindtap="exit">退出</button> </view> </page-container> [代码] [代码]Page({ data: { val: 0, // 拖拽的进度值 show: false, duration: 300, position: "right", round: false, overlay: true, customStyle: "", overlayStyle: "", }, onRefresh() { // 下拉刷新时执行 }, onSecondShow() { // 下拉二楼展开之后执行 setTimeout(() => { this.setData({ show: true, }); }, 500); }, onSecondBack() { // 下拉二楼关闭之后执行 }, exit() { const secondFloor = this.selectComponent(".my-second-floor"); secondFloor.back().then(() => { this.setData({ show: false }); }); }, }); [代码] 3. 可设置下拉二楼的位置 可设置下拉二楼的位置 top、center、bottom。即展开的时候先展示的是二楼的哪个部位 [图片] [图片] [图片] [代码]<!-- top --> <second-floor top></second-floor> <!-- center --> <second-floor center></second-floor> <!-- bottom --> <second-floor bottom></second-floor> [代码] 4. 可设置下拉二楼是否开启缩放动画 就如同微信首页下拉出来小程序列表一样,二楼展开会有一个缩放的效果 [图片] [图片] [图片] [代码]<!-- top scale --> <second-floor top scale></second-floor> <!-- center scale--> <second-floor center scale></second-floor> <!-- bottom scale--> <second-floor bottom scale></second-floor> [代码] second-floor 配置 参数 说明 类型 默认值 版本 top 二楼初始位置 Boolean false 3.2.3 center 二楼初始位置 Boolean false 3.2.3 bottom 二楼初始位置 Boolean true 3.2.3 scale 二楼是否开启缩放动画 Boolean false 3.2.3 插槽 名称 说明 可用组件 second-floor 二楼插槽区域 - second-floor-refresh 下拉刷新插槽位置 [代码]second-floor-refresh[代码] methods 方法 名称 用法 说明 版本 settriggered 先获取组件实例:<br/>[代码]const secondFloor = this.selectComponent('.my-second-floor');[代码], 然后调用方法:<br/>[代码]secondFloor.settriggered()[代码] 关闭刷新的方法,在 onRefresh 中,数据刷新之后执行,返回 Promise 3.2.3 back 先获取组件实例:<br/>[代码]const secondFloor = this.selectComponent('.my-second-floor');[代码], 然后调用方法:<br/>[代码]secondFloor.back()[代码] 关闭二楼的方法,会触发 onSecondBack, 返回 Promise 3.2.3 events 事件 名称 用法 说明 版本 refresh [代码]bind:refresh[代码] 刷新时执行,可执行请求数据,然后执行 [代码]settriggered[代码] 关闭刷新 3.2.3 secondShow [代码]bind:secondShow[代码] 二楼打开之后执行 3.2.3 secondBack [代码]bind:secondBack[代码] 二楼关闭之后执行 3.2.3 second-floor-refresh 配置 参数 说明 类型 默认值 版本 refreshConfig second-floor-refresh 组件的设置,详见refreshConfig Object [代码]{ downText: "下拉刷新", loadingText: "正在加载", backText: "返回首页", tipText: "松开刷新", moreText: "继续下拉有惊喜~", color: "#ffffff" }[代码] 3.2.3 refreshConfig 参数 说明 类型 默认值 版本 downText 开始下拉时的文字 Boolean false 3.2.3 loadingText 正在加载时的文字 Boolean false 3.2.3 backText 二楼加载成功之后返回按钮的文字 Boolean false 3.2.3 tipText 松开刷新时的提示文字 Boolean false 3.2.3 moreText 继续下拉的提示文字 Boolean false 3.2.3 color 文字颜色 Boolean false 3.2.3 示例 demo 请微信扫码打开小程序查看 [图片] 详细文档 gitee文档入口 github文档入口
2022-11-29 - 商家接入微信支付后,如何对用户开通连续包月的扣款服务?
如题,申请开通连续包月功能需要达成什么条件?如何申请?在微信商户后台没有看到相关内容
2024-05-21 - 微信支付分扣款失败微信垫付资金么?
微信支付分扣款失败微信垫付资金么?
2024-09-26 - “微信医保支付”功能介绍与接入指引
产品简介:基于微信进行医保移动支付结算的功能。 优势:无需线下排队,医保移动支付。 接入方式:需开发。 关键词:进阶功能,便捷就医。 01 功能介绍 用户通过微信绑定个人社保卡,便可将微信号与个人医保账户关联,在就医挂号&门诊缴费等环节,进行便捷的医保、自费或医保+自费混合支付。 微信医保支付流程交互: 1)绑卡 [图片] 用户通过微信城市服务或当地人社公众号/小程序,进行实人实名等信息校验,将个人微信关联个人社保卡。 2)支付 [图片] 绑卡用户到已接入微信医保支付的医院就医,通过服务号/小程序挂号,可选择微信医保支付,支付诊费/药费(具体使用规定以当地医保政策为准)。 02 业务架构说明 微信医保支付的业务的整体架构,可以类比微信支付,涉及的最基本的模块有三方:社保卡绑卡类比银行卡绑卡,人社局类比银行,医院类比商户。 业务操作流程说明如下: 微信医保支付架构: [图片] 1、用户使用微信绑定个人社保卡; 2、微信与人社系统同步记录用户微信与社保账户的关联关系; 3、医院通过服务商接入微信医保支付打通移动医保支付通路; 4、绑卡用户通过公众号/小程序就诊后通过微信进行移动医保支付; 5、医院上传用户处方单至人社系统,医保结算成功后人社将费用结算信息同步至微信及医院侧,由医院通过服务通路通知用户。 03 接入指引 1) 微信医保支付接入条件 所在城市已上线微信电子社保卡服务,目前已开通的城市:深圳、成都、铜川、郑州、厦门、嘉兴、宁波、开封、武汉,延安、白城、攀枝花,长沙、哈尔滨、沈阳、潍坊、苏州、葫芦岛、西安、宝鸡、咸阳、榆林、渭南、安康、商洛、永康、合肥,常熟、青岛、广州、邯郸、台州、南昌、通化、石家庄、长春、大庆、济南、桂阳县、安吉县、太仓县、东莞、无锡、枣庄、威海等。 医院/药店已开通微信公众号或小程序,且已开通微信支付。 已向当地人社局或医保监管机构申请,确认本医院/药店可申请接入。 2 )申请方式 联系当地人社局或医保监管机构申请确认后,通过以下方式申请: 国家公立医疗机构:可通过医院公众号后台线上申请医保权限,具体操作链接指引https://mp.weixin.qq.com/s/TnaBUREMR8ikZ4efetEfQg 私立医疗机构或药店:通过邮件方式申请开通医保权限,须联系腾讯工作人员对接指引。 以上案例素材,整理自试点小程序。
2020-01-10 - 共享单车的押金算虚拟支付吗?
共享单车的押金算虚拟支付吗? [图片]
2021-08-15 - 购买押金是否属于虚拟支付?
https://mp.weixin.qq.com/s/CvawEWKvkIcIGOtwjsqZjg 针对虚拟支付会有一系列的相关的限制,如果有一个出租物品的微信小程序,对出租的物品需要购买押金及租金才可出租, 押金属于虚拟支付吗? 租金属于虚拟支付吗?
2024-08-14 - 小程序渲染引擎Skyline小试牛刀--快书
今年年初,在官方文档上看到小程序团队要推出一款性能逼近原生的渲染引擎Skyline,就一直在关注。刚好最近打算做一款新的阅读小程序,作为一名独立开发者,对于性能和用户体验的追求是永无止境的,于是我决定用纯Skyline打造这款小程序。 当然,这个项目里面所用到的skyline特性只是冰山一角,并非全部,更多酷炫的特性请前往官方文档查阅。 接下来,我会结合快书小程序,从以下几个方面,逐条阐述关于skyline特性(快书项目中所用到的)的理解与应用: 效果演示。如何开启Skyline。新版组件swiper。新版组件scroll-view。全新组件snapshot。增强特性worklet动画。增强特性手势系统。增强特性自定义路由。增强特性共享元素动画。希望对于刚接触Skyline,或者想要了解Skyline的同学有所帮助。当然,如有错误或遗漏,欢迎在评论区批评指正,不胜感激。 一、效果演示 [图片] 二、如何开启Skyline 开启Skyline的方式非常简单,只需要在app.json文件中,加入以下配置即可(这里是全局Skyline,若只打算指定页面开启,则在指定页面的json文件中配置即可): "renderer": "skyline", "lazyCodeLoading": "requiredComponents", "rendererOptions": { "skyline": { "defaultDisplayBlock": true, } }, "componentFramework": "glass-easel", 三、新版组件-Swiper 旧版的Swiper基于webview的,在性能上有所局限,特别是当swiper-item的数量动态不断增加的情况下。当然,也可以自己想办法去优化,比如做懒加载和缓存,但相对来说比较麻烦。而Skyline版本的Swiper性能会大幅度提升,首先渲染引擎本身的性能提升了,另外官方也做了缓存的功能,只需要通过定义cache-extent的值,就能轻松定义缓存区域大小,例如值为 1 则表示提前渲染上下各一屏区域。 [图片] 用法上,和webview版本没有太大区别(这里就不放代码了),只需注意不要使用某些webview独有的特性即可。 四、新版组件-Scroll-view 同样,旧版的scroll-view也基于webview的,滚动元素过多的时候会有明显卡顿,当然也是可以通过虚拟Dom的方式自行优化。然而,Skyline版本的scroll-view官方已经实现了只会渲染在屏节点的特性,大大提升了滚动的流畅度,真正做到了开箱即用。 用法上,有以下几个点要注意的。 指定type属性,有2个可选值,分别为:list和custom,对应的是列表模式和自定义模式。如是普通列表,list即可,如果是稍微复杂的列表,比如常见的瀑布流表现形式(类似小红书那样),则可使用custom。只有直接子节点才能根据是否在屏来按需渲染。即你不能把你的列表项,都放在同一个父级view中,而是应该直接放在scroll-view组件下。 // 错误的方式: <scroll-view type="list" scroll-y> <view> <view class="item" wx:for="{{dataList}}" wx:key="id"></view> </view> </scroll-view> // 正确的方式 <scroll-view type="list" scroll-y> <view class="item" wx:for="{{dataList}}" wx:key="id"></view> </scroll-view> // 正确的方式 <scroll-view type="custom" scroll-y> <list-view> <view class="item" wx:for="{{dataList}}" wx:key="id"></view> <list-view> </scroll-view> 另外,上面提到了瀑布流的问题,实现方式也很简单,官方提供了一个叫做grid-view的组件,只需定义它的type="masonry"即可,但若是在webview下,除了性能不理想以外,还会有一些小BUG,比如我在社区提的这个问题:grid-view masonry 在webview模式下经常会出现大块区域的空白。在Skyline下,就不会出现此类问题。 [图片] <scroll-view type="custom" scroll-y> <grid-view type="masonry" main-axis-gap="15" cross-axis-gap="15"> <view wx:for="{{dataList}}" wx:key="id"></view> </grid-view> </scroll-view> 五、全新组件Snapshot 我们常常会有分享精美海报的需求,但由于海报上的内容是动态,仅仅使用一张图片分享达不到我们的目的。在以往,我们可能会使用到wxml-to-canvas,通过绘制 canvas ,导出图片。现在,在Skyline下(基础库3.0.0以上),实现此需求就非常简单。只需要将我们要分享的内容包裹在snapshot组件下就行。 [图片] // wxml: <snapshot id="target"> <view>content</view> </snapshot> // page: Page({ onReady() { this.createSelectorQuery() .select("#target") .node() .exec(res => { const node = res[0].node node.takeSnapshot({ type: 'arraybuffer', format: 'png', success: (res) => { fs.writeFileSync(savePath,res.data,'binary'); //图片保存至本地 wx.showShareImageMenu({ //唤起分享图片的界面 path:savePath }) }, fail(res) {} }) } }) 六、增强特性-worklet动画 worklet动画相比传统的方式,流畅度提升了不少,但如何使用呢?常见的普通动画无非是对于页面元素的平移,缩放,旋转等变换。那么,要让一个元素动起来,只需要做以下2件事: 将页面元素的样式与某个变量进行绑定,变量值的变化会自动触发样式的更新。实时动态地改变这个变量。结合快书的例子(下拉时,让页面缩小,松手后,页面弹回),来看一下具体的实现步骤。 [图片] 首先,如何绑定样式与参数呢?通过官方提供的一个applyAnimatedStyle函数: // Wxml: <view id="#box">content</box> // Page: this.scale = shared(1); //这里是定义一个共享变量,即可在UI线程和JS线程间同步的变量。 this.applyAnimatedStyle(`#box`, () => { 'worklet'; // 声明这是一个worklet函数 return { transform: `scale(${this.scale.value})`, }; }); // 1、这里使用共享变量是为了让后续改变这个变量时,worklet的函数能捕获到。 // 2、#box你要动起来的元素 // 3、当this.scale.value变化时,会自动触发函数体的执行,从而改变#box的样式 第二步,下拉时,根据下拉的偏移量,改变这个scale的值。 this.scale.value = (evt.deltaY / 100) * 0.15; // 这里的evt.deltaY是下拉时的位置偏移量,然后根据偏移量按比例计算缩放的值 // 如何获取这个下拉偏移量?下一小节讲手势系统时会讲到 第三步,松手时,复原scale的值。 this.scale.value = timing(1, { duration: 300, easing: Easing.ease }); // timing函数表示:在300毫秒内,scale.value会逐渐变成1 // easing: Easing.ease 表示缓动的方式,具体可参考https://easings.net // 如何知道已经松手了?下一小节讲手势系统时会讲到 更多动画参考请查阅官方文档。 七、增强特性-手势系统 还是上面那个例子,我们只说了下拉时根据下拉的偏移量改变scale的值,那如何得到下拉的偏移量呢?这里就涉及到了手势系统。下面讲讲如何让一个元素能响应拖动,缩放等手势。 说回上一小结的例子,我们只需要讲#box元素包裹在手势组件vertical-drag-gesture-handler即可。更多示例可查阅官方文档 // wxml: <vertical-drag-gesture-handler worklet:ongesture="handlePan"> <view id="box"></view> <vertical-drag-gesture-handler> // page: handlePan(evt) { 'worklet' if (evt.state === GestureState.ACTIVE) { // 拖拽时 if (evt.deltaY > 0) { // 下拉 this.scale.value = Math.max(this.scale.value - (evt.deltaY / 100) * 0.15, 0.85); } else { // 上拉 this.scale.value = Math.min(this.scale.value - (evt.deltaY / 100) * 0.15, 1); } } else if (evt.state === GestureState.END || evt.state === GestureState.CANCELLED) { // 拖拽结束或取消 this.scale.value = timing(1, { duration: 300, easing: Easing.ease }); } }, 然而,当手势组件与scroll-view等可以滚动的组件嵌套时,会出现冲突的问题。比如,同上一小节的示例,为了让文章内容过长时可以滚动,我们需要将文章的内容放在scroll-view中。当scroll-view已经滚动到顶部,再继续下拉的话,应当触发手势组件的拖拽事件,即缩放页面。相反,则继续滚动scroll-view。 [图片] // wxml: <vertical-drag-gesture-handler tag="pan" worklet:ongesture="handlePan" shouldResponseOnMove="shouldPanResponse" simultaneousHandlers="{{['scroll']}}"> <vertical-drag-gesture-handler tag="scroll" native-view="scroll-view" shouldResponseOnMove="shouldScrollResponse" simultaneousHandlers="{{['pan']}}"> <scroll-view type="list" scroll-y bindscroll="handleContentScroll">文章内容</scroll-view> </vertical-drag-gesture-handler> </vertical-drag-gesture-handler> // page: // 处理scroll-view的滚动事件,获取scrollTop的值 handleContentScroll(evt) { 'worklet' this.scrollTop.value = evt.detail.scrollTop; }, // return false 则表示scroll-view不再响应滚动事件 shouldScrollResponse(evt) { 'worklet'; const { deltaY } = evt if (this.scrollTop.value <= 0 && deltaY > 0) { //scroll-view已经滚动到顶部,继续下拉时 this.pan.value = true; return false; } if (this.scale.value < 1 && deltaY < 0) { //#box已经被缩放,继续上拉时 this.pan.value = true; return false; } this.pan.value = false; return true; }, shouldPanResponse() { 'worklet' return this.pan.value; // true表示响应手势组件的拖拽事件,false则不响应 }, 八、增强特性-自定义路由 以往在webview中,路由的的过渡动画仅支持从右到左,较为单调。在skyline之后,我们可以自定义路由的过渡动画了,比如常见的淡入淡出,从底部弹起等。比如以下这个例子,从首页点击图片,会跳转到分享的页面,这里就是用自定义路由实现的淡入效果。 [图片] 自定义路由的使用相比前几个特性稍微复杂一点,这里官方讲的更为具体和清晰,可查阅官方文档。唯一要注意的一点是,只有连续的skyline页面跳转时,才会有效果。 九、增强特性-共享元素动画 还是上面的例子,当从首页点击图片跳转到分享页面时,图片看起来像是从首页飞到了分享页,这里便是使用到了共享元素动画。我同时也做Flutter的开发,所以这里看起来非常类似Flutter的hero动画或者叫飞行动画。 使用方式也类似于Flutter。将2个页面的相似组件都用share-element组件包括起来,并且使用相同的key即可。再配合自定义路由,可使得飞行动画看起来非常的丝滑。 // A页面: <share-element key="唯一key"> <image src="imagePath" mode="aspectFill" /> </share-element> // B页面: <share-element key="唯一key"> <image src="imagePath" mode="aspectFill" /> </share-element> // 有几个要注意的地方 // 1、两个个页面的share-element组件必须使用相同的key。 // 2、key是唯一的,即同一个页面中,不能出现重复的key。 // 3、image不要写死宽高,应百分比100%,具体宽高数值写在share-element组件上。 有一个常见的问题,A页面是一个列表,B页面是详情页,列表中的数据都是通过接口从后台返回的,由于共享元素的key又不能重复,那么这个key怎么定义?一般后台返回的数据都会有一个唯一标识,假设为ID,我们可以用这个ID当作Key。 但是,另一个问题来了,如果数据是后台接口返回的,然后通过setData的方式响应到页面,那么很有可能B页面的首帧获取不到这个Key,因为这时候接口请求可能还未完成,那么动画也是不会生效的。针对这种情况,官方也提供了一种方式: 共享元素动画需保证下一个页面首帧即创建好 [代码]share-element[代码] 节点,并设置了 key,用于计算目标位置。如果是通过 [代码]setData[代码] 设置的,可能会错过首帧。针对这种情况,可以 使用 Component 构造器构造下一个页面,只要在组件 [代码]attached[代码] 生命周期前(含)通过 [代码]setData[代码] 设置上去,就会在首帧渲染 十、总结 Skyline还有一些其他有趣的特性,大家感兴趣的话可以查阅官方文档。总的来说,相比起webview,skyline对性能的提升是显而易见的,并且,一些在webview很难实现的效果,在skyline的基础上,也能轻易实现,开箱即用。目前,skyline还在不断地迭代中,还有许多的新特性还在评估和开发中,相信之后的版本会更完善更好用。 最后,大家多多使用快书呀,球球了~ [图片]
2023-09-01 - worklet.timing toValue 类型问题?
SharedValue worklet.shared(any initialValue)参数 any initialValue(任何类型包括数组)初始值,可通过 [代码].value[代码] 属性进行读取和修改。类型可以是 [代码]number | string | bool | null | undefined | Object | [代码][代码]Array [代码][代码]| Function[代码]。 但是AnimationObject worklet.timing(number toValue, Object options, function callback) 但timing 只支持number ? 如果用shared 创建了一个数组,如何用timing 去改变这个值? 官方例子都是用的number ,没有其他类型的说明。const { shared, sequence, timing, spring } = wx.worklet const offset = shared(0) offset.value = sequence(timing(100), spring(0)) 上面是官方例子,如要改成 数组呢怎么办? const offset = shared([0,100]) offset.value = sequence(timing(???????), spring(0)) 针对CSS不光有数值上的调整,字符串也需要,display:flex background-color: cornflowerblue ,这种怎么办?
2024-05-04 - 普通page method中执行wx.worklet.runOnUI不会在ui线程执行?
按钮触发执行普通js函数,在js函数中调用wx.worklet.runOnUI(this.method.bind(this))()。但是在this.method中console.log答应发现并不是在UI线程执行的。是为什么?
2023-12-18 - 微信:把元宇宙装进小程序
[图片] 作为月活13.09亿的国民级应用,微信的每次小升级都很容易形成现象级。2023开年,微信放大招,试图把元宇宙装进小程序。 不久前,微信官方在开放社区贴出了“XR-FRAME”开发指南,这是一套为小程序定制的XR(扩展现实)/3D应用解决方案。简单来说,该方案上线后将从底层赋予小程序扩展现实和3D能力,让未来小程序的人机交互方式由2D向更立体化的3D转变。 之前,XR-FRAME还处于测试阶段,根据官方发布的Demo看,该组件可以更好地呈现3D效果,并提供AR换脸、AR游戏等体验。据透露,微信率先将XR小程序的试水场景瞄准了电商领域,落地AR 试穿试戴、AR 家装等不同类型案例。 如今,框架 XR-FRAME 发布正式版,曾进行了一系列的更新,且一些功能还在开发中。 [代码]xr-frame[代码]在基础库[代码]v2.32.0[代码]开始基本稳定。 [图片] 限制: 最低要求客户端iOS8.0.29、安卓8.0.30及以上,推荐稳定版在iOS8.0.36、安卓8.0.35及以上。 基础库最低2.27.1及以上,推荐2.32.0及以上。 开发工具需要最新版本,建议Nightly版本。 小程序全局同一时刻只能存在一个[代码]xr-frame[代码]组件,否则可能会发生异常。 同一个[代码]xr-frame[代码]组件只能存在一个[代码]xr-scene[代码],并且必须为顶层。 目前不支持和小程序传统标签比如[代码]<view>[代码]混写。 目前不支持[代码]wxml[代码]自动补全,真机调试需要特别注意,见真机调试文档。 同时未来还会追加更多的能力,在未来的规划中,我们还会着重致力于: XR-FRAME内置特色的UI组件,让开发者可以在XR-FRAME组件中写UI,来实现一套酷炫的UI系统。 AR/VR能力持续增强,支持眼睛设备。 交互手段进一步强化,物理碰撞、触发等功能(已完成,待发布)。 工具能力强化,包括标签属性自动补全等。 从战略角度,把元宇宙装进微信小程序,腾讯居安思危。经济学家朱嘉明预测,未来人们社交的基本形态将在元宇宙中进行,倘若这一切真的发生,微信等传统的社交模式将被颠覆。打败微信的可能并非“抖音”社交,而是元宇宙应用。 小程序要3D化 还记得在微信风靡一时的小游戏“跳一跳”吗?玩家通过长按屏幕让小人蓄力跳跃到前方的盒子上得分,小人不慎掉落则游戏结束。这款游戏在2017年12月登陆微信小程序,引来了无数玩家比拼较量。几个月前,消除闯关游戏“羊了个羊”也以微信小程序为载体火爆全网。 自2017年1月微信小程序上线以来,多款现象级应用在其中诞生。依托庞大的微信用户群,小程序为开发者们提供了一片新的创作土壤。6年来,小程序的平台能力和底层框架也在不断升级。2023年刚开年,小程序曝出大动作——正在内测XR框架。 XR即扩展现实,是VR(虚拟现实)、AR(增强现实)、MR(混合现实)的合称,可为受众带来真实与虚拟结合、人机交互的环境。不久前,微信官方在微信开放社区贴出了“XR-FRAME”开发指南,根据描述,这是一套小程序官方提供的XR/3D应用解决方案,基于混合方案实现,性能逼近原生、效果好、易用、强扩展、渐进式、遵循小程序开发标准。 现在这一底层框架还处于测试阶段,小程序官方在开放社区贴出了详细的教程,指导开发者们如何从头构建一个XR小程序。 比起当前小程序采用的Canvas(画布)组件,xr-frame带来了更多能力。据介绍,其提供xml(可扩展标记语言)的方式来描述3D场景,并集成了AR、物理、动画、粒子、后处理等等系统,上手简单。同时,内置完整的PBR(基于物理的渲染)效果、环境光照、阴影,可以快速通过全景图生成环境数据。此外还有渲染性能逼近原生、扩展性强等优势。 [图片] XR小程序开发效果示例 简单理解,xr-frame的上线将从底层赋予小程序扩展现实和3D能力,将让未来小程序的人机交互方式由2D向更立体化的3D转变。 根据官方发布的Demo来看,获得xr-frame支持后,虚拟3D 人、3D物、3D场景都可以在小程序里更好地呈现,此外AR换脸、扫描平面获得AR游戏、扫描特定图片获得AR交互等功能也可实现。这意味着,以后的微信小程序将有更多具有交互性的应用出现。 按照测试节奏,XR小程序功能很可能在中国农历新年后全量上线,业界普遍将其视为下一个微信大版本升级的核心功能。 让元宇宙发生在微信上? 微信内测XR小程序,恰处于元宇宙浪潮激荡之时。这个动作释放了一个信号,微信小程序将一定程度担起腾讯布局元宇宙的使命。 虽然诸如Meta、百度等互联网巨头都开发了元宇宙全景应用,但体验和人气都十分有限。目前与元宇宙相关的应用场景主要体现在人机交互游戏、沉浸式电商购物、虚拟人直播、虚拟会议等方面。没有自建元宇宙平台,腾讯试水元宇宙,选了个轻巧做法,借助微信流量入口,用小程序先接入虚拟现实场景。 据透露,目前微信率先将XR小程序的试水场景瞄准了电商,并与多个不同品类品牌小程序合作,落地 AR 试穿试戴、AR 家装等不同类型案例。 现阶段,XR技术最普遍应用于电商领域。在传统的电商购物体验中,消费者只能依赖商家展示的图片/视频进行购物决策,最终购买后往往会出现买家秀与卖家秀差距过大的情况。而在引入AR技术后,传统电商平台的商品展示模式逐渐被颠覆,尤其在服装试穿、珠宝穿戴、美妆试用等特定场景下,AR能够帮助用户做出更正确的决策。 尽管微信一直不是电商的主战场,但诸如NIKE、Adidas、香奈儿等消费品牌都上线了相关的微信小程序,当XR功能上线后,这些品牌可以更好地展示商品,或许会带动微信电商生态的增长。 另外,可以想象的是,未来微信小程序中也将诞生更多3D建模以及现实交互感更强的小游戏,也许,下一款爆款小游戏将是截然不同的3D形态。 事实上,作为微信生态重要的组成部分,小程序也进入了发展瓶颈。为了实现无需下载、用完即走的效果,微信小程序在性能上做出巨大的牺牲,同样的应用在微信小程序上的表现往往不如APP。这也导致,小程序没有如人们预想一样成为颠覆性的应用,相反,某些功能性较强的应用如果通过小程序使用,显得鸡肋。 XR功能的上线能否为小程序打开增长空间尚未可知。有开发者担忧地表示,xr-frame能否达到预期还需要谨慎乐观。毕竟小程序的设计初衷就是要求快速,这里的“快”指的是加载以及渲染,所以导致微信小程序一直是以webview渲染为主、原生渲染为辅的混合渲染方式,也使得小程序的开发主要是以前端技术为主。然而xr-frame的3D化开发则需要完全不同的技术栈,对于渲染的开销也成倍提升,这就要求开发者要有更为优秀的优化水平。此外,在优质3D建模动辄占用较大内存的情况下,小程序提供的 XR 体验是否会有折扣也要打个问号。 当然,微信作为月活13.09亿的“国民级应用”,流量优势显著,只要有优异的小程序出现,一定不缺用户。这也会吸引大量的品牌方、开发者在上面进行尝试,所以xr-frame的上线将会带动小程序开发生态的繁荣。 从战略角度而言,把元宇宙装进小程序也是腾讯在社交领域的进一步探索,海外的社交巨头Meta已经开始了。 经济学家、横琴数链数字金融研究院学术与技术委员会主席朱嘉明曾认为,Facebook之所以改名为Meta,是因为Facebook本来就是元宇宙,只不过在此前的时代,数字技术发展还有局限,社交体验只能以图文、视频的方式呈现。他预测,未来人们社交的基本形态将在元宇宙中进行,打破时空、地理界限,带来更沉浸式的体验。 倘若这一切真的发生,微信、QQ等传统的社交应用模式将被颠覆,打败微信的可能并非“抖音”这样的视频社交应用,而是元宇宙。 扫码体验 [图片] 你认为XR能否催生现象级小程序? 注:“现象级小程序”是指在短时间内突然爆红而被众所周知和使用
2023-06-13 - 运用小程序Skyline技术构建无缝用户体验 —— 同程旅行酒店最佳实践分享
[图片] 动效衔接设计与小程序渲染框架 1、什么是动效衔接设计? 随着互联网技术和设计理念的不断发展,动效设计成为现代 UI 设计中不可或缺的一部分。其中,动效衔接是非常重要的一环。动效衔接设计是指通过巧妙的动效设计,将不同的 UI 元素在动画过程中自然、流畅地衔接起来,从而增强用户的交互体验和视觉感受。在实际应用中,动效衔接设计主要应用于界面转场、信息提示、状态变化等方面,通过顺畅的衔接,降低用户因白屏等待而产生的焦虑。 2、动效衔接设计的意义 (1)极大提高用户体验,让用户感受到界面的流畅和自然,从而增加用户对产品的好感度。 (2)降低用户的操作认知成本,帮助用户更好地理解执行操作后所带来的结果,从而减少用户对产品的困惑。 (3)强化视觉层,让用户更好地区分不同的信息和元素,从而增强视觉层次感。 (4)增加界面的美感度,让界面更加生动有趣,从而提升整体的美感和设计价值。 (5)提升品牌的认知度,让产品更加具有特色和独特性,从而提高品牌的认知度和市场竞争力。 3、什么是小程序渲染框架Skyline? 为了进一步优化小程序性能,小程序在原 webview 渲染引擎之外最新推出 小程序渲染框架Skyline,其使用更精简高效的渲染管线,并拥有诸多增强特性,让它拥有更接近原生渲染的性能体验。新的增强特性有 worklet 动画系统、手势系统、自定义路由、共享元素动画,而且许多常用的组件如 scroll-view、swiper 都有了更高性能的实现。 [图片] [图片] 实践理念和场景拆解 1、动效衔接设计的核心原则 简单而清晰的动效设计,需要遵守以下几个原则: (1)一致性:动效衔接应该与整体设计风格保持一致。包括颜色、字体、动画速度等方面。 (2)可预测性:用户能够感知动画元素的变化关联性,从而增加用户对产品的掌控感和对界面的理解。 (3)反馈性:动效衔接与用户操作相响应,从而帮助用户理解他们的操作所带来的结果。 (4)视觉层次:动效衔接遵循视觉层次原则,让用户区分页面中的上下关系以及三维物理世界的关系层次,给用户清晰的层级区分感知,提高用户体验。 (5)自然性:动效衔接符合物理规律,例如重力、加速度等,从而增强动画的真实感和用户体验。 2、理念孵化与使用场景拆解 以提炼的动态感受为出发点,理性的层面给予了我们大致的产品体验感知,为我们动效理念的建成提供了框架。对此我们将继续从感性层面出发,找寻可传递真实感受的运动现象并加以组合提炼。 本次 同程旅行小程序 以酒店预订链路中核心的相册页面进行应用场景,在用户操作图片的过程中运用小程序渲染框架承接。 (1)将整个过程进行了拆解,首先为了退出时行动的路径更加清晰,做了一个响应设计,当界面向右滑动退出的过程中,相册图片进行缩小,在缩放过程中,会根据位移距离控制缩放的比例,同时蒙层的透明度以及毛玻璃效果也跟随手指移动变化,和相册列表页在视觉上呈现 XY 轴以及上下层级的空间关系,在缩小到一定的比例时,触发震动效果,松手退出到相册列表页。 (2)在交互结束时,图片退回相册列表页原始位置,在返回路径的过程中,根据交互结束时的定位点,来判断运动的方向和距离,计算运动加速度,以及模拟运动加速度带来的惯性回弹的方向和角度变化,加强与模拟真实物理世界的运动定律,和视觉上的动态感知。 结合自然世界的运动规律来看,把页面进入的元素比作是行驶的汽车,用户当作是正在斑马线上行驶的人,将马路作为页面空间。若汽车采用的是缓入运动(加速)的话,马路上的行人则看到的是一辆不断加速向他行驶过来的车辆。因为担心车辆高速的逼近导致刹车不及时的情况,行人便会本能的作出躲闪的反应。其实页面也是一个道理,进入的元素使用加速运动出现过冲的运动感知会让用户体验时产生不适。 [图片] 小程序渲染框架技术开发实践过程剖析 1、开发自定义路由实现此交互,需要 自定义路由动画,因为小程序渲染框架的页面支持自定义跳转动画。当使用自定义路由后,页面跳转时指定路由类型,就会触发自定义路由动画,而不再是默认的从右往左的动画,此处的实现可以使得页面跳转时,没有默认的路由动画,页面将直接以透明的方式渲染在屏幕上,由开发者自己控制页面内元素的动画展示方式,具体实现如下: (1)在图片查看页面配置文件 index.json 中声明 { "backgroundColor": "#00000000", "backgroundColorContent": "#00000000", // 设置客户端页面背景为透明 "navigationStyle": "custom", "renderer": "skyline", // skyline渲染引擎 "disableScroll": true, "usingComponents": { } } (2)在 wxss 中,设置图片查看页面的 page 节点为透明背景 page { background: transparent; } (3)在 js 中,使用 wx.router.addRouteBuilder(routeType, fn) 来声明自定义路由动画 wx.router.addRouteBuilder('myCustomRoute', function (params) { const handlePrimaryAnimation = () => { 'worklet'; return { // 可在此处,根据 params.primaryAnimation.value 的值,来设置页面的动画效果 backgroundColor: `rgba(0,0,0,${ params.primaryAnimation.value })` }; }; return { opaque: false, handlePrimaryAnimation, barrierColor: '', barrierDismissible: false, transitionDuration: 320, reverseTransitionDuration: 250, canTransitionTo: true, canTransitionFrom: false }; }) (4)在图片列表页面中,使用 x.navigateTo 来跳转页面,并且设置 routeType 为 myCustomRoute wx.navigateTo({ url: '/pages/skyline-image-viewer/index?index=0', routeType: 'myCustomRoute' }) [图片] 需配置页面的渲染引擎为 Skyline,并且在跳转时使用 routeType 就可以实现让页面在跳转时没有默认的路由动画。 2、共享元素穿越在连续的页面跳转时,页面间 key 相同的 share-element 节点将产生飞跃特效,还可自定义插值方式和动画曲线,通常作用于图片。为保证动画效果,前后页面的 share-element 子节点结构应该尽量保持一致 <share-element key="share-key"> <view> you code here </view> <!-- 需要注意,share-element 内要求只有一个根节点 --> </share-element> [图片] 这时,界面的表现像上面视频一样,是一个连续的动画状态,这完全是由 share-element 来控制的,share-element 的动画原理如下图所示: [图片] 3、接入手势组件,实现图片放大、缩小、平移在图片查看页面有如下结构: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <share-element key="{{ shareKey }}" class="current-item"> <image src="{{ src }}"/> </share-element> </scale-gesture-handle> 这里,我们使用小程序渲染框架提供的 手势组件 <scale-gesture-handle>,来实现图片的放大、缩小、平移等手势交互。 注意,所有声明为 worklet 指令的方法它们运行在UI线程,不要在方法中修改普通的变量,因为跨线程的关系,只能修改使用 wx.worklet.shared 声明的变量。 const GestureState = { POSSIBLE: 0, // 此时手势未识别 BEGIN: 1, // 手势已识别 ACTIVE: 2, // 连续手势活跃状态 END: 3, // 手势终止 CANCELLED: 4 // 手势取消 }; Component({ attached() { this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.sharScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.sharScale.value})` } }); // 页面所需的数据,需要在 attached 事件里初始化完毕,使其可以参与首帧渲染 this.setData({ src: '...', shareKey: '...' }); }, methods: { // 当手势组件识别到手势时,触发此回调 onScaleGestureHandle(e) { 'worklet'; const { state } = e; // 在worklet函数里,不要使用 const {} = this 对this解构 const shareX = this.shareX; const shareY = this.shareY; const sharScale = this.sharScale; if (state === GestureState.BEGIN) { // 手势已经识别,此时,可以获取到手势的初始值 } else if (state === GestureState.ACTIVE) { // 手势活跃状态,此时,可以获取到手势的变化值,如平移的距离、缩放的比例等 // 将当前变化的值,设置到 `shared` 变量,就可以改变元素的样式,类似于vue3的数据驱动 shareX.value += e.focalDeltaX; shareY.value += e.focalDeltaY; sharScale.value = e.scale; } else if (state === GestureState.END || state === GestureState.CANCELLED) { // 手势终止或取消,此时,可以获取到手势的最终值 } } } }) [图片] 4、手势协商(解决手势冲突) 上面的 demo 简单演示如何使用手势组件来做图片交互,但是在图片查看页面中,我们还有其他的手势交互,如图片的左右滑动切换等,一般我们会使用 <swiper> 组件来实现,但是 <swiper>组件的内部实现和 <scale-gesture-handle> 组件,都会监听手势事件,手势组件的事件不支持冒泡的,就会导致下面结构横时: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </scale-gesture-handle> 使用手势横向滑动时,会优先触发 swiper 的横向切换事件,而无法触发 <scale-gesture-handle> 的手势事件了,这在图片放大时的图片横向移动产生了冲突。此时就需要使用手势协商来解决手势冲突。 什么是手势协商? 手势协商指的是:当页面同时有多个手势交互时,需通过一定的约定来决定哪些手势事件应该被执行,哪些需要被忽略。 小程序渲染框架解决手势冲突的方式,主要是通过手势组件的 tag、simultaneous-handlers、native-view 和 should-response-on-move 来实现 tag:手势组件的标识,用于区分不同的手势组件simultaneous-handlers:手势组件的协商者,表示需要同时触发事件的手势组件的标识should-response-on-move:参与手势时间的派发过程,返回 false时,表示该手势时间不会继续派发native-view:用当前手势组件来代理原生组件内部的手势事件,如<swiper>组件内部的手势事件<swiper> 的内部也是使用了 <horizontal-drag-gesture-handler>手势组件,但是我们不能直接在<swiper>上设置tag来使其参与手势协商,需要用相同的手势组件通过native-view=swiper将其内部的事件代理出来,使其可以参与协商<!-- <scale-gesture-handle> 缩放手势 --> <!-- <horizontal-drag-gesture-handler> 横向拖动手势 --> <!-- 通过 simultaneous-handlers=tag 来声明多个手势应该同时触发 --> <scale-gesture-handle tag="scale" simultaneous-handlers="{{['swiper']}}" worklet:ongesture="onScaleGestureHandle"> <!-- 此处使用 native-view=swiper 代理内部的手势组件 --> <!-- 通过 should-response-on-move=fn 来参与`事件派发`过程,决定手势的事件是否应该派发 --> <horizontal-drag-gesture-handler tag="swiper" native-view="swiper" simultaneous-handlers="{{['scale']}}" worklet:should-response-on-move="shouldResponseOnMove"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </horizontal-drag-gesture-handler> </scale-gesture-handle> const GuestureMode = { INIT: 0, SCALE: 1, SWIPE: 2, MOVE: 3 // ... }; Component({ attached() { this.GuestureModeShared = wx.worklet.shared(GuestureMode.INIT); this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.shareScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.shareScale.value})` } }); // ... }, methods: { onScaleGestureHandle(e) { 'worklet'; const { state } = e; if (state === GestureState.BEGIN) { this.GuestureModeShared.value = GuestureMode.INIT; } else if (state === GestureState.ACTIVE) { if(this.GuestureModeShared.value === GuestureMode.INIT) { this.gestureBefore(e); // 手势类型未知时,判断手势类型 } else { this.gestureHandle(e); // 手势类型已知时,处理手势事件 } } else if (state === GestureState.END || state === GestureState.CANCELLED) { this.GuestureModeShared.value = GuestureMode.INIT; } }, // 判断手势类型 gestureBefore(e) { 'worklet'; const { focalDeltaX, focalDeltaY, scale } = e; if (Math.abs(focalDeltaX) > Math.abs(focalDeltaY)) { this.GuestureModeShared.value = GuestureMode.SWIPE; } else if (scale > 1) { this.GuestureModeShared.value = GuestureMode.SCALE; } else { this.GuestureModeShared.value = GuestureMode.MOVE; } }, // 处理手势事件 gestureHandle(e) { 'worklet'; if (this.GuestureModeShared.value === GuestureMode.SCALE) { this.shareScale.value = e.scale; } else if (this.GuestureModeShared.value === GuestureMode.SWIPE) { // swiper 切换模式时,这里什么都不用做 } else if (this.GuestureModeShared.value === GuestureMode.MOVE) { this.shareX.value += e.focalDeltaX; this.shareY.value += e.focalDeltaY; } }, // 用于判断手势事件是否应该派发 shouldResponseOnMove(e) { 'worklet'; return this.GuestureModeShared.value === GuestureMode.SWIPE; // 当模式为SWIPE时,才响应手势事件 } } }) [图片] 通过上面的代码,我们实现了手势协商,当用户在图片上进行滑动的操作时,总是会触发 <scale-gesture-handler> 的手势事件,通过对图片当前状态的判断来决定应该触发哪种手势,我们通过此种协商让 <horizontal-drag-gesture-handle> 手势在合适的时机触发,以此避免手势冲突。 5、使用小程序渲染框架时需要注意的一些地方作为一款新的渲染优化方式,开发者使用小程序渲染框架需要注意以下内容,以保证渲染的效果和性能。 (1)自定义路由时首帧渲染&首帧性能优化 小程序渲染框架的首帧渲染对共享元素动画非常重要,若共享元素节点的key 错过首帧设置的话,可能会丢失飞跃动画,所以在使用小程序渲染框架时,共享元素的 key 应该尽量在 attached 中或之前设置到页面,并且在首帧渲染时,应尽可能的减少 UI 层的渲染工作 如下: 1)所需要的数据应尽可能使用提前计算好,避免构建页面时等待太久影响响应速度 2)首次设置的数据应该尽可能的少,避免首次渲染时,页面上的元素过多,导致首帧渲染时间过长,导致动画卡顿(如:不要同时初始化太多的 <swiper-item>) 3)确保首帧渲染时,共享元素的 key 正确的设置,避免在首帧渲染时,由于找不到对应的共享元素,导致动画丢失,看不到飞跃动画 4)由于手势事件触发频繁,应尽量避免大量需要的计算的逻辑高频执行,容易导致机器发烫,或者导致动画卡顿 **worklet 函数的使用** worklet 函数的使用有一些限制,主要是由于它是在 UI 线程执行的,所以 worklet 函数中的 this 并非是页面的 this 实例, 里面所使用到的变量也是通过特殊的 babel 插件转换到UI线程的,需要与逻辑层共用的变量都需要用 wx.worklet.shared 将它声明成共享变量,在 UI 线程调用逻辑层的函数需要使用 wx.worklet.runOnJS (2)与 web 规范的差异 虽然小程序渲染框架尽可能的与 web 规范保持一致,但是由底层渲染引擎的限制,还是有一些差异,如: 1)display: flex 的默认朝向是 column,而不是 row,这需要开发者注意,官方后续会支持 block 布局方式 2)暂不支持 css 伪元素,如 ::after、::before,官方正在支持中 3)position 仅支持 absolute、relative,不支持 sticky,实现滚动吸附的效果需用 sticky-* 组件来配合 scroll-view 实现 ** <share-element> 在非小程序渲染框架运行环境里的表现是什么** 在非小程序渲染框架的运行环境内,<share-element> 组件会被视为一个 <view> 组件,需要做好布局的兼容 6、何时使用小程序渲染框架开发时,请确保小程序开发者工具版本是 最新版 nightly,sdk 版本在 2.30.2+,具体限制可参考 文档。 这些新特性的引入,使得小程序渲染框架在小程序开发中的优势更加明显,开发者可以更加便捷地实现各种复杂的交互效果,并且达到接近原生APP的体验。 [图片] 未来展望 1、个性化产品形态:将会根据不同的用户需求和场景,设计出更加符合用户喜好和习惯的动效衔接,进行组件化调用。 2、更加自然和真实的动效衔接:动效衔接将会更加贴近自然规律和真实物理效应,从而增强动画的真实感和用户体验。 3、更加智能化和自适应的动效衔接:动效衔接将会根据用户的操作行为和使用习惯,自适应调整动画效果,从而提高用户体验和产品效果。 4、扩大产品、设计与开发的协作效应:设计对动效的把控、产品对用户的洞察以及开发对新技术的应用,才可以发挥最大化的协作效应。 附1:本文作者 同程旅行研发工程师 同程旅行体验设计师 同程旅行产品经理 附2:代码片段 相册小程序代码片段(请使用 PC 端浏览器打开):https://developers.weixin.qq.com/s/E979jCmP7oHG 附3:UE标注 [图片] 附4:AB 实验效果 AB 实验显著win0.23% [图片]
2023-04-28 - JavaScript物理引擎之Matter.js与Box2d性能对比
前言在挑选JavaScript 2D物理引擎的时候,不外乎两种主流的选择:第一种是老牌的Box2D,最开始的版本是C++实现的,后来有了很多种实现,比如flash版本和js版本,具体可看:https://stackoverflow.com/que...;第二种是新潮的matter-js,matter-js比较轻量,API和文档都比较有友好。 本文简单对两个引擎的性能在不同平台上进行对比,其中Box2D采用的是TypeScript实现的版本:https://github.com/flyover/bo..., 作者仍然在更新,matter-js采用的是0.14.2版本(感觉作者已经更新不动这个库了:),大半年都不怎么活跃了)。 测试案例在屏幕随机位置重复创建相同的矩形刚体,使之自由落体到底部,计算不同刚体数量下,全部刚体落地后每一帧的物理计算平均耗时。下面是测试中的一些截图: 影响性能的因素机器本身的配置; JIT:苹果端微信小游戏没有JIT,性能会受到一些影响; 刚体的随机性:刚体在随机位置生成的过程中,如果与其他刚体重叠,物理引擎需要更多的性能消耗来修正重叠,因此,每次运行测试用例数据上都不可避免会有波动。 引擎本身的设计:比如matter-js没有圆形的定义,创建圆形刚体本质上是创建25边形,而Box2d天然就设计了圆形刚体,所以对于圆形刚体,两个引擎会存在不小的差异。 数据采集因为是测试物理引擎的性能,这里不考虑FPS,只采集物理引擎更新每一帧的时间,因为除开物理引擎,渲染引擎(PixiJS)也会带来性能消耗。 [代码][代码][代码]// Box2d数据打点[代码] [代码]let positionIterations = 3;[代码] [代码]let velocityIterations = 8;[代码] [代码]let timeStep = 1 / 60;[代码] [代码]// 数据打点函数细节略[代码] [代码]Performance.startPoint('box2dUpdateCost'); world.Step(timeStep, velocityIterations, positionIterations); Performance.endPoint('box2dUpdateCost');[代码][代码]// matter-js数据打点[代码] [代码]Performance.startPoint('matterUpdateCost');[代码] [代码]matter.Engine.update(this.engine, 1e3 / this.fps); Performance.endPoint('matterUpdateCost');[代码][代码]// 计算平均耗时[代码] [代码]function calAverage(list, key) { [代码] [代码] let sum = list.reduce((total, curr) => curr[key] + total, 0); [代码] [代码] console.log(sum / list.length)[代码] [代码]}[代码] [代码]// 所有数据会收集到一个数组里面[代码] [代码]let data = Performance.print();[代码] [代码]//calAverage(data, 'matterUpdateCost');[代码] [代码]calAverage(data, 'box2dUpdateCost');[代码] Box2D数据机型10个刚体20个刚体50个刚体100个刚体200个刚体300个刚体MacBook Pro 20150.2ms0.4ms~0.5ms0.6ms~0.8ms1.3ms~1.6ms4.6ms~5.6ms7ms~8msiPhone7 Plus微信小游戏3.3ms~3.5ms4.5ms~5.5ms7.5ms~8.5ms13ms~14ms33ms60ms+OPPO R11 Plus微信小游戏1.5ms~2.5ms1.8ms~3ms3.6ms6ms~8ms9ms~12ms17ms~19msmatter-js数据机型10个刚体20个刚体50个刚体100个刚体200个刚体300个刚体MacBook Pro 20150.5ms~0.6ms0.6ms~1ms2ms~3ms3.5ms~4ms6ms~8ms12ms~13msiPhone7 Plus微信小游戏2.3ms~2.8ms3.0ms~3.5ms6.0ms~6.5ms11.5ms~12ms26ms~28ms45msOPPO R11 Plus微信小游戏1.5ms~2.5ms2.5ms5~6ms8ms12ms~14ms30ms结论在PC端,Box2d全面战胜了matter-js,在苹果的微信小游戏端,因为没有JIT,Box2d性能反而不如matter-js,而回到安卓的微信小游戏端,因为有JIT,Box2d同样是可以战胜matter-js的。 关于圆形刚体上面提到了两个引擎对于圆形刚体的设计,因为matter-js没有正统的圆形,我大胆猜测圆形刚体的性能Box2D会大大高于matter-js! 特意去翻了下各自的源码,首先我们来看看matter-js的: [代码]Bodies.circle = function(x, y, radius, options, maxSides) {[代码][代码] options = options || {}; [代码] [代码] var circle = { [代码] [代码] label: 'Circle Body', [代码] [代码] circleRadius: radius[代码] [代码] }; // approximate circles with polygons until true circles implemented in SAT[代码][代码] maxSides = maxSides || 25; [代码] [代码] var sides = Math.ceil(Math.max(10, Math.min(maxSides, radius))); // optimisation: always use even number of sides (half the number of unique axes)[代码] [代码] if (sides % 2 === 1) sides += 1; return Bodies.polygon(x, y, sides, radius, Common.extend({}, circle, options)); };[代码]从上面的代码可得,matter-js将25边形当成圆,这里在进行碰撞检测的时候,会比纯圆有更多的计算量,不知道matter-js作者是出于什么目的这样设计。 再来看看Box2D版本的实现: [代码]class b2CircleShape extends b2Shape {[代码][代码] constructor(radius = 0) { [代码] [代码] super(exports.b2ShapeType.e_circleShape, radius); [代码] [代码] this.m_p = new b2Vec2();[代码] [代码] }[代码] [代码] Set(position, radius = this.m_radius) { [代码] [代码] this.m_p.Copy(position); [代码] [代码] this.m_radius = radius; [代码] [代码] return this;[代码] [代码] } }[代码]与matter-js相比,Box2D的圆与多边形是独立的。 多说无益,我们对比下100个刚体状态下,两个引擎的数据对比,为了凸显差距,我们选择Box2D打不过matter-js的苹果端微信小游戏平台查看数据: 引擎耗时Box2D8msmatter-js25ms我们可以得出一个有意思的结论:同样是100个刚体,矩形刚体的耗时是13ms~14ms,而圆形刚体的耗时下降到了8ms,这对于一些弹球类的游戏无疑是福音,据我的观察,100个圆形刚体在苹果端微信小游戏下面丝毫不会卡顿。而matter-js的耗时从11.5ms~12ms上升到了25ms,显然就是在越多边形碰撞检测需要的计算量越大!
2019-05-22 - 使用 Skyline Worklet 动画实现下拉页面放大头图
效果 [图片] 思路 监听滚动事件,映射滚动距离至头图的放大比例。 实现 <!-- index.wxml --> <scroll-view scroll-y class="scrollView" type="list" worklet:onscrollupdate="scrollViewOnScroll" > <image src="https://wx2.sinaimg.cn/large/007GYgpfly1hnfihmysbmj32yp281u0y.jpg" mode="aspectFill" class="headerImage" /> </scroll-view> /** index.ts */ const { shared } = wx.worklet /** 头图的放大高度 */ const headerImageHeight = shared(0) Component({ lifetimes: { attached() { this.applyAnimatedStyleToHeaderImage() } }, methods: { /** 绑定由 worklet 驱动的样式到头图 */ applyAnimatedStyleToHeaderImage() { this.applyAnimatedStyle( '.headerImage', () => { 'worklet' const scale = (240 + headerImageHeight.value) / 240 return {transform: `scale(${Math.max(1, scale)})`} }, {immediate: false} ) }, /** <scroll-view/> 的滚动回调 */ scrollViewOnScroll(event: any) { 'worklet' headerImageHeight.value -= event?.detail?.deltaY } } }) /* index.wxss */ .scrollView { width: 100vw; height: 100vh; } .headerImage { width: 100%; position: fixed; top: 0; left: 0; pointer-events: none; }
2024-07-07 - onShow与onLoad的一些理解和实践
基本介绍onShow、onLoad与onReady都是小程序页面生命周期函数。 onLoad 在页面加载时调用,仅一次; onShow页面显示/切入前台时触发,两个生命周期非阻塞式调用。 onReady 是页面初始化数据已经完成后调用的,并不意味着onLoad和onShow执行完毕。 调用顺序是onLoad > onShow > onReady 根据对应的执行机制,我们预期有三种执行的逻辑 A. 页面每次出现都会执行 从其他页面返回手机锁屏唤醒,重新看到小程序页面把当前小程序页面重写切换到前台(多任务)B. 页面加载后只需执行一次(页面第一次载入) C. 只在页面非第一次执行时才执行(A情况的子集,页面非第一次展示时) 需求与问题逻辑1: 因为onLoad和onShow是非阻塞执行的,当我们有一个这样的需求:页面载入执行A方法,页面展示执行B、C、D方法时,A需要在BCD之前执行,此时把A放在onLoad中,BCD放在onShow中就无法实现需求 逻辑2: 还有一种需求是:页面第一次执行A,非第一次执行R-A,这里onLoad和onShow并没有非第一次的逻辑,需要手动判断。 一种实践方法下面是纯粹使用onShow代替onLoad,完成所有逻辑的示例,保证了业务逻辑的执行顺序可控。 options获取使用其他方式代替。 为了保持onShow中逻辑的清晰性,尽量使用EventChannel去替代原本onShow+globalData的逻辑。 data:{ first: true }, async onShow(){ //代替onLoad中的options的获取 const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const options = currentPage.options; this.funD() // C2 页面每次都调用的逻辑 if(this.data.first){ this.data.first = false; await this.funA(); //A 仅在页面初次调用的逻辑(按需是否阻塞调用) }else{ await this.funB(); //B 仅在页面非初次时调用的逻辑 } await this.funC(); //C1 页面每次都调用的逻辑 } 另外一种使用实践data:{ first: true } onShow(){ this.funD() //页面每次都调用的逻辑(仅非阻塞) if(!this.data.first){ this.funC() //仅在页面非初次时调用的逻辑 } await this.funE() //页面每次都调用的逻辑(可阻塞,可非阻塞) }, onLoad(){ //仅在页面初次调用的逻辑 this.funA(); await this.funB(); } onReady(){ this.data.first = false; } 如有错误,恳请指出。
2022-09-23 - 如何使用painter实现一个海报编辑工具——以taro为例
文章开始前先做个简单的声明:这篇文章主要面向刚了解到 painter 的开发者,文中使用的框架、实现的方式只作为一种参考,并不一定是最佳实践。而使用 painter 能够做的扩展不止下文提到的这些能力,只是以下样例更为方便理解。欢迎各位酌情阅读。 自动态模版功能发布后,陆续有开发者开始尝试使用动态模版能力,我们也收集到了大家反馈的一些问题。这一系列文章的主要内容是从头开始实现一个简单的、基于 painter 动态模版能力的海报编辑工具。希望能通过这一过程,让大家了解为什么我们推出了动态模版能力,以及如何快速上手。同时在文中,也会统一回答一下关于动态模版使用的一些问题。文章中实现的编辑器代码,可以在https://github.com/Kujiale-Mobile/Taro-Painter-Demo/tree/2.x获取 先期准备 本次我们使用 2.2.15 版本的 taro 创建一个空项目 [代码]$ taro init [代码] painter 组件是使用了 mina-painter 包(https://www.npmjs.com/package/mina-painter),这是我们封装的 taro 风格组件,供 taro 1.x/2.x 版本使用,支持 base64 图片与 canvas2d 模式。 [代码]$ yarn add mina-painter [代码] 创建空页面,并引入 painter 组件。 [代码]// pages/index/index.tsx import Painter from 'mina-painter'; ... render() { return ( ... <Painter customStyle={`margin-top:5vh;`} customActionStyle={customActionStyle} dancePalette={danceTemplate} palette={outputTemplate} action={action} clearActionBox={clearActionBox} onImgOK={this.handleImgOk} onDidShow={this.handleDidShow} onTouchEnd={this.handleTouchEnd} onViewClicked={this.handleViewClick} onViewUpdate={this.handleViewUpdate} /> ... ) } [代码] 写一个简单的海报模版,包含 painter 内各种 view 类型。 [代码]// palette/index.ts const template = { width: '750rpx', height: '1334rpx', background: '#FFFFFF', views: [ { id: 'rect_10', type: 'rect', css: { scalable: true, color: '#F5F2EC', height: '348rpx', width: '750rpx', bottom: '0rpx', left: '0rpx', minWidth: '80rpx', minHeight: '80rpx', }, }, { id: 'rect_9', type: 'rect', css: { scalable: true, color: '#CBBD9F', height: '646rpx', width: '388rpx', left: '0rpx', top: '456rpx', minWidth: '80rpx', minHeight: '80rpx', }, }, { id: 'rect_8', type: 'rect', css: { scalable: true, color: '#EBE5D7', height: '160rpx', width: '360rpx', top: '222rpx', right: '0rpx', minWidth: '80rpx', minHeight: '80rpx', }, }, { id: 'qrcode', type: 'image', url: 'https://qhstaticssl.kujiale.com/newt/100082/image/jpeg/1623053391518/3EF9BB7ABE024959EB2A0E81078B40FA.jpeg', css: { width: '202rpx', height: '202rpx', bottom: '76rpx', right: '40rpx', borderRadius: '8rpx', borderColor: '#FFFFFF', borderWidth: '4rpx', }, }, { id: 'worker_type', type: 'text', text: '门店店长', css: { scalable: true, deletable: true, left: '156rpx', bottom: '76rpx', fontSize: '24rpx', color: '#656c75', lineHeight: '34rpx', }, }, { id: 'worker_name', type: 'text', text: 'tester', css: { scalable: true, deletable: true, fontSize: '30rpx', fontWeight: 'bold', color: '#333', left: '156rpx', bottom: '114rpx', width: '280rpx', lineHeight: '42rpx', maxLines: 1, }, }, { id: 'avatar', type: 'image', url: 'https://qhstaticssl.kujiale.com/newt/100082/image/png/1623053110600/BDA064C5ECDCB7DD50DEB466C70E2EB0.png', css: { width: '80rpx', height: '80rpx', borderRadius: '40rpx', left: '52rpx', bottom: '76rpx', }, }, { type: 'image', id: 'image-main', url: 'https://qhstaticssl.kujiale.com/newt/100082/image/jpeg/1623053489433/54EE335A9C385A3D99D8664CB9135F84.jpg', css: { width: '672rpx', height: '672rpx', mode: 'aspectFill', right: '0rpx', top: '314rpx', scalable: true, minWidth: '120rpx', }, }, { type: 'rect', css: { width: '666rpx', height: '2rpx', top: '144rpx', right: '0rpx', color: '#EBEFF5', }, }, { id: 'name', type: 'text', text: '一个蒙着红色布的——球?', css: { scalable: true, deletable: true, fontSize: '32rpx', color: '#383c42', maxLines: 1, width: '480rpx', left: '76rpx', top: '74rpx', lineHeight: '44rpx', }, }, { id: 'product', type: 'text', text: '¥9999', css: { scalable: true, deletable: true, fontSize: '80rpx', lineHeight: '90rpx', fontWeight: 'bold', color: '#383C42', textAlign: 'center', left: '76rpx', top: '170rpx', }, }, ], } [代码] 以上种种准备好之后,我们就能得到这样的一个页面: [图片] 这个页面有最基础的点选、拖动能力,通过配置 view.css 的 scalable 和 deletable 属性,可以使用 painter 内置提供的缩放功能。 怎么样,是不是已经功能很完备,好像可以满足需求了啊~好,今天的分享就到此为止(并不是) [图片] 接下来,我们主要会为 text 、image 提供一些能力拓展,并实现基本的撤销、恢复功能。能力拓展的方式是相似的,相信在看完文章后,你就可以熟练地为任意 view 类型拓展能力了。 通过刷新整个 palette 方式进行的操作 [代码]// pages/index/index.tsx refreshPalette = (palette?: IPalette) => { this.setState({ dancePalette: palette || { ...this.currentPalette }, }); }; [代码] 最简单的刷新海报的方式就是直接刷新整个 palette 了,这种操作即便是不使用动态模版也一样可以用。这种方式开销大,速度相对慢,但是可以完全改变海报的结构 删除 View [图片] 虽然 painter 提供了自定义删除 icon 的方法,但是点击删除按钮,你会发现 view 并没有被删除。这是因为我们希望这种修改 palette 的操作能够让外部主动操作,而不是将删除操作也内置——那可能会导致你对自己写的海报模版失去掌控。要想实现删除逻辑非常简单,当用户点击删除按钮时,我们可以从 onTouchEnd 处监听到一个 type = ‘delete’ 的事件。 [图片] 从 palette 中找出对应的 view 并删除,然后更新 palette 就能完成删除操作了。 [代码]// pages/index/index.tsx this.currentPalette.views.splice(detail.index, 1); this.refreshPalette(); [代码] 修改背景 修改背景更为简单——直接改 palette 的 background 属性,然后刷新模版即可 [图片] [代码]// pages/index/index.tsx this.currentPalette.background = color; this.refreshPalette(); [代码] 添加新 View —— 以 text 为例 [图片][图片] 准备一个预先定义好样式的 text 类型的 view ,将输入内容填充后塞入模版的 views 中,最后刷新模版即可 [代码]// common/index.ts export function getBlankTextView(text?: string): IView { return { type: 'text', text: text || '', id: `text_${new Date().getTime()}${Math.ceil(Math.random() * 10)}`, css: { scalable: true, deletable: true, width: '384rpx', fontSize: '36rpx', color: '#000', textAlign: 'center', padding: '0 8rpx 8rpx 8rpx', top: '50%', left: '50%', align: 'center', verticalAlign: 'center', }, }; } // pages/index/index.tsx this.currentPalette.views.push(getBlankTextView(inputValue)); this.refreshPalette(); [代码] 通过刷新 action 方式进行的操作 [代码]// pages/index/index.tsx refreshSelectView = (view?: IView) => { this.setState({ action: { view: view || this.currentView }, }); }; [代码] painter 动态模版功能的一大改动就是增加了 action 属性。当我们向 action 传入一个 view ,painter 会去寻找与其匹配的 view 并刷新状态。通过这种方式,我们最小化了需要修改的内容,从而减少了 painter 所需要的渲染时间。 刷新选中view的样式——以 text 为例 通过监听 onViewClick 事件,我们能够获取当前点击的 view ,在确定当前 view 后,我们就可以通过修改改 view 的 css ,然后刷新 action 来修改样式了。具体表现如下: [图片][图片][图片][图片][图片][图片] [代码]// pages/index/index.tsx this.currentView.css = newCss; this.refreshSelectView(); [代码] 除了 text ,其他各类 view 也都可以做类似操作,比如修改 rect 的尺寸、修改图片链接、基于替换图片链接实现图片裁剪等等。这里只是抛砖引玉,欢迎大家向我们分享你做出了什么炫酷的功能。 同时使用上述两种方法实现撤销与恢复操作 上面介绍了两种刷新海报的方式,而接下来,我们实现一个简单的撤销与恢复功能。这个功能的核心没有什么特殊的,就是同时维持撤销栈和恢复栈两个栈,通过记录之前所做的操作,做反向操作。 [图片] [代码]// pages/index/index.tsx interface ITimeStackItem { view?: IView; palette?: IPalette; index?: number; type?: string; } pushToHistory = (item: ITimeStackItem) => { this.future.length = 0; while (this.history.length > 19) { this.history.shift(); } this.history.push(item); this.refreshTop(); }; handleTimeMachine = (type: 'revert' | 'recover') => { let popStack: ITimeStackItem[]; let pushStack: ITimeStackItem[]; if (type === 'revert') { popStack = this.history; pushStack = this.future; } else { pushStack = this.history; popStack = this.future; } const pre = popStack.pop(); if (!pre) { return; } if (pre.type === 'delete') { this.currentView = undefined; if (this.currentPalette.views[pre.index!] && this.currentPalette.views[pre.index!].id === pre.view!.id) { this.currentPalette.views.splice(pre.index!, 1); } else { this.currentPalette.views.splice(pre.index!, 0, pre.view!); } pushStack.push(pre); this.refreshPalette(); } else if (pre.palette) { pushStack.push({ palette: JSON.parse(JSON.stringify(this.currentPalette)), }); this.currentPalette = pre.palette; this.currentView = undefined; this.refreshPalette(); } else { for (let i = 0; i < this.currentPalette.views.length; i++) { if (this.currentPalette.views[i].id === pre.view!.id) { pushStack.push({ view: JSON.parse(JSON.stringify(this.currentPalette.views[i])), }); this.currentPalette.views[i] = pre.view!; this.currentView = this.currentPalette.views[i]; this.refreshSelectView(pre.view); break; } } } this.setState({ editState: this.currentView && this.currentView.type === 'text' ? EditState.TEXT : EditState.NORMAL, }); this.refreshTop(); }; [代码] 保存生成的海报 在操作动态模版时,是不会触发 onImgOk 的,因为动态模版的内容渲染在四个不同层级的 canvas 上,无法实时生成完善的海报图片,所以需要手动设置 palette 使用静态模版生成对应的海报 [代码]// pages/index/index.tsx this.setState({ palette: JSON.parse(JSON.stringify(this.currentPalette)), }); handleImgOk = path => { ... }; [代码] 总结 经过上述的一个流程,是不是对如何使用 painter 的动态模版有一些新的想法了呢?欢迎大家基于 painter 开发出更多有趣的功能并在评论区与我们分享。
2021-06-16 - 第一次录音结束并且播放后,在二次录音时出现卡顿的解决方法.
问题:第一次录音结束并且播放,在播放后,可能我们需要二次录音,回到二次录音时会卡顿。 解决: 1、播放时创建播放实例innerAudioContext = wx.createInnerAudioContext(); 2、在二次录音前的初始化时一定要摧毁前一次的播放实例 init:function(){//初始化 innerAudioContext.stop(); innerAudioContext.onStop(() => { innerAudioContext.destroy();//初始化一定要摧毁上一次的播放实例,否则下一次录音会有卡顿,在播放时在创建播放实例 }) soundArr=[];//清空上一次录音 ... },
2020-08-14 - InnerAudioContext.destroy()是否真实销毁?
我在页面的onHide和onUnload方法中都调用了 this.innerAudioContext.destroy(); 但是页面切换后,有时音乐会继续播放,再次进入有音乐的页面后,音乐会重叠播放。 此问题没有稳定复现,所以我很好奇,为什么页面都切换了,都destroy了,还会继续播放
2019-12-03 - 自定义组件没有被销毁,导致存在内存泄露的情况?
Demo 地址 问题描述 自定义组件实例没有被销毁,如果给组件的 properties 传递了大量数据(Demo 中是 500k),将快速占用内存。 复现步骤 1.打开 Demo,然后打开开发者工具的 Memory 面板,录取内存快照,关注构造函数:l。 [图片] 2.点击 Click 按钮 10 次(相当于渲染、隐藏自定义组件 foo 5 次),然后点击强制垃圾回收按钮,接着再次录取内存快照。 3.选中快照 Snapshot2,filter 设置为:Objects allocated between Snapshot1 and Snapshot2,并筛选构造函数:l。这时可以观察到内存占用大幅上升,其中新增的 5 个对象 l 引用了 32% 的内存。 [图片] 4.展开其中一个 l 对象,可以观察到其 __methodCaller 属性引用的是一个自定义组件实例,从而判断 l 对象跟自定义组件有关。再观察 l 对象中内存占比较大的字段分别是 __vtObj 与 __innerData,它们都分别引用着父组件传入的 json 属性(500k 大数据)。( l 对象的 Retainers 是一些循环引用或内部代码,很难再往下追查) [图片] 5.右键点击 l 对象,选择 Store as a global variable,把 l 对象放到控制台进行观察,发现 __vtObj 与 __innerData 属性引用的是独立的 json 对象。 [图片] 6.综上,推测自定义组件实例没有被销毁,如果给自定义组件的 properties 传递了大型数据,会导致内存泄露问题更明显。 环境信息 基础库:2.32.2复现环境:微信开发者工具(真机调试不支持堆栈 snapshot)
2023-06-26 - wxml2canvas-2d:简单易用的小程序海报、战绩等分享图片生成方案
Github 地址:https://github.com/ChrisChan13/wxml2canvas-2d 介绍 当前,众多小程序的多处场景都需要能够生成分享图便于用户进行二次传播,从而提升小程序的传播率以及加强品牌效应。 比较简单的分享图,如寥寥几行文字和一张小程序码,可以通过微信的 Canvas API 绘制。旧版 Canvas API 绘制过程繁琐,且每次绘制都需要调用 draw 方法,一不小心代码就写了上百行。 新版 Canvas API 基本与 Web Canvas 对齐,使得开发效率提高、性能得到优化。虽然免去了很多繁琐操作,但面对拥有元素众多、结构复杂的分享图片,依然解决不了代码冗长的问题。 目前开源的一些小程序图片生成方案,有的年久失修、有的依然使用旧版 Canvas API、有的使用方式不够简便,于是便有了开发 wxml2canvas-2d 的想法。 wxml2canvas-2d 的图片生成方式简单直观:首先在 wxml 页面上编写元素结构,其次在 wxss 中编写元素样式,最后调用 wxml2canvas-2d 的相关方法即可生成所需的分享图片。 wxml2canvas-2d 会通过 class 类名查询元素节点的 computedStyle 和节点属性,从而将元素节点绘制到画布上。这样做的好处是在编写 wxml 结构以及样式时,可以直观的看见样式的变化,方便调整。当然也有坏处,这个用来生成图片的“wxml 模板”,必须存在于页面上。若需要隐藏这个“模板”,只可用定位将其移至屏幕外,不可以使用 wx:if 或 hidden 隐藏。 wxml2canvas-2d 已经支持大部分常用的 CSS 属性,等你来测~ 示例 克隆此 Github 仓库,运行 [代码]npm i & npm run dev[代码],将 miniprogram_dev 文件夹导入微信开发者工具 效果预览 小程序内容: [图片] 生成的图片: [图片] 安装 npm 使用 npm 构建前,请先阅读微信官方的 npm 支持 [代码]# 通过 npm 安装 npm i wxml2canvas-2d -S --production [代码] 构建 npm 包 打开微信开发者工具,点击 工具 -> 构建 npm,并勾选 使用 npm 模块 选项,构建完成后,即可引入组件。 使用 在页面配置中引入 [代码]wxml2canvas-2d[代码] ; [代码]{ "usingComponents": { "wxml2canvas": "wxml2canvas-2d" } } [代码] 在页面中编写 wxml 结构,将要生成画布内容的根节点用名为 [代码]wxml2canvas-container[代码] 的样式类名称标记,将该根节点内部需要生成画布内容的节点用名为 [代码]wxml2canvas-item[代码] 的样式类名称标记(文字类节点需在对应节点声明 [代码]data-text[代码] 属性,并传入文字内容)。上述两个样式类名称可以自定义,只需将对应名称传入 [代码]wxml2canvas[代码] 组件的对应属性参数即可; [代码]<!-- pages/index/index.wxml --> <view class="wxml2canvas-container box"> <view class="wxml2canvas-item title" data-text="测试标题">测试标题</view> <image class="wxml2canvas-item image" src="/your-image-path.png" /> <view class="wxml2canvas-item content" data-text="测试内容,长文本。。">测试内容,长文本。。</view> </view> <button catchtap="generateSharingCard">生成画布内容</button> <wxml2canvas id="wxml2canvas" /> [代码] 补充各个节点样式; [代码]/* pages/index/index.wxss */ .box { /* 根节点(容器)的样式 */ } .title { /* 标题的样式 */ } .image { /* 图片的样式 */ } .content { /* 内容的样式 */ } [代码] 依据 wxml 结构以及 css 样式,生成画布内容,并将生成结果导出。 [代码]// pages/index/index.js Page({ async generateSharingCard() { const canvas = this.selectComponent('#wxml2canvas'); await canvas.draw(); const filePath = await canvas.toTempFilePath(); wx.previewImage({ urls: [filePath], }); }, }); [代码] 更多内容及文档 点击此处 前往查看!如果有好的建议或者想法,也欢迎提交 Issue 或 PR~
2024-11-21 - 微信支付报错:201商户订单号重复
发起微信支付但未支付,调用关闭订单接口后,查询订单也是关闭状态, [图片] 再次使用之前的商户订单号发起支付报错:201 商户订单号重复[图片]
2024-04-22 - “商户订单号重复”这个问题该怎么解决呢?
近期在开发微信支付中碰到一个问题:下单后发起支付统一下单,然后取消支付,后台修改了支付价格,再发起支付就会出现“201 商户订单号重复”;通过关闭订单再发起支付也不行,请教一下遇到需要修改价格的情况下怎么发起支付呢?
2020-11-09 - Skyline 渲染引擎常见问题
Skyline 一定需要应用到整个小程序吗? 不需要,Skyline 支持按页面粒度开启,建议开发者逐个页面适配 在 Skyline 模式下,为什么使用真机调试会显示空白并且工具报错? 目前 Skyline 模式下暂不支持真机调试,建议使用真机预览完成调试,平台在尽快支持真机调试能力。 在 Skyline 模式下,为什么微信开发者工具热重载无响应? Skyline 模式暂不支持热重载,建议先关闭热重载,重新编译来预览渲染结果。后续平台将支持热重载能力。 开启 Skyline 后布局错乱 大多是由于没有全局滚动而导致挤压,以及 flex-direction 默认为 column 造成。前者只需要加上 scroll-view,后者可以在声明了display:flex 但又没指定 flex-direction的地方显示指定flex-direction:row。推荐开发者开启默认 Block 布局。 切换 Skyline后,为什么顶部原生导航栏消失? 不支持原生导航栏,需自行实现,或使用 weui 组件库 伪类及伪元素部分支持 对于伪类,目前只支持常用的 :first-child 和 :last-child 。其它伪类可通过按需添加 class 替代,如 :active 则手动给点击状态下的节点加个.active class 对于伪元素,目前只支持 ::before 和:after。其它伪元素建议用真实 WXML 节点实现。 全局固定元素失效 因不支持 fixed 导致,但由于没有全局滚动,在页面根节点下使用 absolute 即可达到 fixed 的效果,倘若封装原因无法移至页面根节点,可使用 root-portal 组件包裹 切换 Skyline 后,为什么 position: absolute 相对坐标不准确? 在 Skyline 模式下,所有节点默认是 relative,可能导致 absolute 相对坐标不准。建议开发者修改节点 position 或者修改相对坐标。 多段文本无法内联 因不支持 inline 布局导致,需改成 flex 布局实现,或者使用 text 组件包裹多段文本,而不是用 view 组件包裹,也可以使用 span 组件包裹 text 和 image 混合内联。如 、<span><image /></span>,<span><view style="width: 50px;"/></span> 多行文本的省略样式失效 在单行文本省略的基础上,通过 text 组件的 max-lines 属性设置最长行数,即 <text max-lines="{{2}}"></text> z-index 表现异常 这是由于 Skyline 不支持 web 标准的层叠上下文所致,只有在同层级的节点之前应用 z-index才有效,可根据实际情况调整取值 weui 扩展库无法使用 平台正在支持扩展库,预计近期上线。建议开发者使用 npm 安装 weui 组件库 后,将 node_ modules/weui-miniprogram 下的miniprogram_ dist 替换为 链接 中的 miniprogram_dist,然后在微信开发中工具中构建 npm 即可。 不支持组件 animate 动画接口 暂不支持组件 animate 动画接口。如需实现相关效果,可使用 worklet 动画机制 实现 svg 渲染不正确 Skyline 上的 SVG 不支持 <style> 选择器匹配,可自行转成内联的方式;不支持 rgba 格式,可使用 fill-opacity 替代;建议用 SVGO 在线工具优化 scroll-view 横向滚动不生效 横向滚动需打开 enable-flex 以兼容 WebView,同时 scroll-view 添加样式 display: flex; flex-direction: row;,scroll-view 子节点添加样式 flex-shrink: 0; icon-font 图标不显示 最新版本已支持伪元素,低版本可参考 代码片段 实现图标
2023-10-18 - 小程序- SaUi 之添加城市选择
趁着最近有时间,又搞了个经常会用到的城市选择器起来啦~~ 以下是tabs的页面图片 [图片] 主要实现了: tab的切换,这里需要注意一个问题,当你滚动一个tab页到某个位置的时候 再切换tab,另一tab的scroll也会定位在那里。所以在点击时,我作了处理。 [代码] scroll-into-view [代码] 滚动时,经过指定的父位置时 会有fixed。这里用到了新的样式类型 [代码] position: sticky; [代码] 3.点击右边的菜单,给出了提示框以及左边可以快速,准确的定位到相对的位置,需要注意的事scroll-into-view对应的是id [代码] scroll-into-view [代码] 4.点击右边菜单时,应该给上对应的选中类才是,这里漏了… 以上是携程所有的功能。同时也想添加新的功能,就是滚动左边的列表,同时切换右边的菜单,这里是目前还差的,待更新一版…
2019-09-10 - 微信小程序深度合成-AI问答类目获取指引(AI小程序必备)
前言只要AI相关的小程序没有深度合成类目提交审核都会被拒绝,涉及到AI问答,AI绘画,AI换脸都需要补充类目才能提交,那么如何准备深度合成AI问答类目所需材料,才能通过【深度合成-AI问答】类目审核? [图片] 方案选择资质时选第二个方案: [图片] 2.1、使用第三方技术:同时提供: ① 技术主体的《互联网信息服务算法备案》(算法类型为“生成合成类(深度合成)”)或《互联网信息服务算法备案》(算法类型为“生成合成类”)在审批中的系统截图及 ②小程序主体与技术主体的合作协议(协议需含【算法名称】或【应用产品】或【备案编号】相关内容) 材料需要两个:1.大模型算法备案截图 2.合作协议截图 首先进入微信服务市场选择「接口和插件」 [图片] https://fuwu.weixin.qq.com/ 然后找到大模型服务任意选择一个大模型服务即可 [图片] 进去大模型服务选择一个套餐进行购买操作 [图片] 购买成后会生成一个订单截图,订单截图中包含了算法备案截图分别提交即可申请AI问答类目。 算法备案截图如下: [图片] 合作订单截图如下: [图片] 分别按顺序上传到这两处资质文件即可(1.算法备案2.合作订单) [图片]
2024-10-27 - 微信云开发支付签名错误,请情况?
说一下没有子商户,只有商户号,就是用的以前的支付接口,这是云开发没有后端代码那个notify_url不写还不行, 不知道哪里有问题, 签名在微信工具里验证通过,调用下单接口就报签名错误,希望会的人答疑解惑一下,谢谢啦 //2 openid 就是支付用户的识别号 const mch_id = 'xxx '; // 商户号 const key = 'xxxxx'; // 商户密钥 const cloud = require('wx-server-sdk') const rp = require('request-promise') const crypto = require('crypto') cloud.init() function getSign(args) { let sa = [] for (let k in args) sa.push(k + '=' + args[k]) sa.push('key=' + key) console.log(sa.join('&')) return crypto.createHash('md5').update(sa.join('&'), 'utf8').digest('hex').toUpperCase() } function getXml(args) { let sa = [] for (let k in args) sa.push('<' + k + '>' + args[k] + '</' + k + '>') sa.push('<sign>' + getSign(args) + '</sign>') let axml = '<xml>' + sa.join('') + '</xml>' console.log("最后签名:",axml) return axml } function getNonceStr(){ var chars = ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']; var nums=""; for(var i=0;i<32;i++){ var id = parseInt(Math.random()*61); nums+=chars[id]; } nums= nums.toLowerCase() return nums ; } exports.main = async (event, context) => { const wxContext = cloud.getWXContext() const appId = appid = wxContext.APPID console.log("appid是:", appid, appId) console.log("key是:", key) const openid = wxContext.OPENID // const attach = 'attach' const body = event.msg; const total_fee = event.totalFee; const notify_url = "http://127.0.0.1" const spbill_create_ip = "127.0.0.1" const fee_type = "CNY" // const nonceStr = nonce_str = Math.random().toString(36).substr(2, 15) const nonceStr = nonce_str = getNonceStr() const timeStamp = parseInt(Date.now() / 1000) + '' const out_trade_no = event.outTradeNo; const trade_type = "JSAPI" const sign_type = "MD5" const xmlArgs = { appid, // attach, body, fee_type, mch_id, nonce_str, notify_url, openid, out_trade_no, sign_type, spbill_create_ip, total_fee, trade_type, } let xml = (await rp({ url: "https://api.mch.weixin.qq.com/pay/unifiedorder", method: 'POST', body: getXml(xmlArgs) })).toString("utf-8") console.log("签名是:",xml) if (xml.indexOf('prepay_id') < 0) return xml let prepay_id = xml.split("<prepay_id><![CDATA[")[1].split("]]></prepay_id>")[0] let payArgs = { appId, nonceStr, package: ('prepay_id=' + prepay_id), signType: 'MD5', timeStamp } return { ...payArgs, paySign: getSign(payArgs) } }
2024-10-23 - 记录一次云开发数据库查询的简单优化
云开发数据库联表查询,最开始表现优异,随着数据量的增加,某条语句的查询时间居然超过了2秒。后面着手优化查询到100毫秒以内。 优化主要事项: 1、将条件(match)、排序(sort)、分页(skip、limit)移到联表之前,先查出部分结果后再联表操作。 2、取消模糊查询连接后的子表字段。 优化后的语句如下: db.collection("form_answers") .aggregate() .match({ _openid: "xxxx" }) .sort({ createTime: -1 }) .skip(0) .limit(20) .lookup({ from: 'forms', let: { formId: '$formId' }, pipeline: $.pipeline() .match(_.expr($.and([ $.eq(['$_id', '$$formId']), ]))) .project({ _id: 0, name: 1, unionid: 1 }) .done(), as: 'formList', }) .replaceRoot({ newRoot: $.mergeObjects([{ formName: $.arrayElemAt(['$formList.name', 0]), formUnionid: $.arrayElemAt(['$formList.unionid', 0]) }, '$$ROOT']) }) .project({ formList: 0 }) .end()
2024-06-23 - 省钱有道之 减少云函数调用次数
由于云函数有一项计费规则是按调用次数计费,在小程序访问量比较小的情况下还比较无所谓,但当体量上来之后不得不考虑控制一下对公共接口的调用次数从而减少一些不必要的开销。比如获取用户信息接口、获取配置信息接口 这里分享一个我自己几个小程序用到的方法,公共接口的调用都放在app.js,然后提供函数供其他页面调用。同时由于异步问题,有可能页面加载完接口还未返回,因此还需能够注册回调函数,在接口返回数据后回调给调用页面 代码示例: app.js App({ onLaunch: async function (options) { //判断是否需要更新小程序 updateCheck.check(); await api.wxCloudInit(); //获取用户信息 this._getUserInfo().catch(res => { console.warn("获取用户信息失败,准备重试"); this._getUserInfo().then(); }); }, /** * 获取用户信息 * @param callback * @param refresh 等于true时表示重新查询用户信息,同时也会更新会员状态 */ getUserInfo: function (callback, refresh) { if (!refresh) { const userInfo = this.globalData.userInfo; if (!userInfo.ready) { if (typeof callback == 'function') { this.callbackFunctions.userInfoReadyCallback.push(callback); } if (!this.userInfoReadyCallback) { this.userInfoReadyCallback = res => { console.log("获取用户信息完毕,开始回调", res); const callbacks = this.callbackFunctions.userInfoReadyCallback; while (callbacks.length) { const callback = callbacks.pop(); typeof callback == 'function' && callback(res); } /*callbacks.forEach(callback => { typeof callback == 'function' && callback(res); })*/ } console.log("注册userInfoReadyCallback成功"); } else { console.log("已经注册了userInfoReadyCallback,不再重复注册"); } } else { typeof callback == 'function' && callback(userInfo); } } else { console.log("准备更新用户信息") this._getUserInfo().then(userInfo => { typeof callback == 'function' && callback(userInfo); }); } }, /** * 执行云函数,获取用户信息 * @returns {Promise<unknown>} * @private */ _getUserInfo: function () { return new Promise((resolve, reject) => { api.callCloudUserCenterFunction("UserInfoHandler/getUserInfo", {}, res => { console.log("获取用户数据完毕:", res.result); const result = res.result; if (result.success) { const data = result.data; this.globalData.userInfo = data; // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 // 所以此处加入 callback 以防止这种情况 if (this.userInfoReadyCallback) { this.userInfoReadyCallback(data); } resolve(data); } else { console.error("没有获取到用户信息"); reject("没有获取到用户信息"); } }, e => { console.error("获取用户信息失败", e); reject("获取用户信息失败"); }); }); }, /** * 异步事件回调函数列表 * 增加这个列表是为了避免不同地方同时调用,互相覆盖回调函数 */ callbackFunctions: { //用户信息异步回调 userInfoReadyCallback: [], }, globalData: { userInfo: { confirm: false//用来标记用户信息查询动作是否已经结束,等于true时,userInfo才可信 }, } }) 某page.js app.getUserInfo(res => { const isVip = res.isVip; if (isVip) { console.log("已开通会员", res); } });
2024-02-07 - 微信小程序的Text组件,设置user-select之后,怎么样才能当一行放不下的时候不整体换行?
微信小程序的Text组件,设置user-select之后,当一行放不下这个text文本的时候,会整体换行。有没有什么办法让这是user-select的text组件按照原来的样式正常换行(填充完当前行之后剩下部分的再换行展示) <scroll-view class="scroll-area" type="list" scroll-y style="padding: 0px 20px; box-sizing: border-box;"> <view style="display: inline;"> <text >欢迎使用代码片段,</text> <text >:可在控制台查看代码片段的说明和文档</text> </view> <view style="margin-top: 50px;"> <text user-select>欢迎使用代码片段,</text> <text user-select>:可在控制台查看代码片段的说明和文档</text> </view> </scroll-view> [图片]
2024-10-21 - 就医服务类目卫健委批文究竟是什么?
小程序就医服务类目需要的卫健委批发就是医疗机构执业许可证吗?
2022-04-05 - 强烈投诉,小程序服务类目被打回10次,一直被要求补充的资料卫健委说从不提供这种内容,为什么还不通过?
审核类型:服务类目审核“医疗服务>互联网医院” 提审时间:自9月20号上传第一版,到10月14号,过程中上传不少于10次,快要一个月过去了,依然没有审核通过。 面临问题: 申请服务类目为“医疗服务>互联网医院”,我们按照要求上传了相关资质。我们小程序主题是A,互联网医院牌照的主体是B。针对合作医院B的《医疗执业机构许可证》和A与B《合作协议》均没有什么异议。 但是,微信审核时,要求“《医疗主管部门许可文件》(含“互联网诊疗“相关内容)”和“《省级互联网医疗服务监管平台对接情况证明》”这两项资料, 需要上传任何一项资料。 在跟浙江省卫健委的领导沟通后,对方回复“我们已经发给你们了《医疗执业机构许可证》,还需要什么《医疗主管部门许可文件》,我们没有这样的内容可以提供。” 所以我们选择,上传省监管平台下载的《省级互联网医疗服务监管平台对接情况证明》,同时附了省监管平台及浙里办等官方平台查到的B互联网医院,以及公开可核实的官网链接,并加盖了我司公章。但是微信平台要求加盖政府相关部门单位的公章,可是浙江省卫健委的领导回复“我们没有这样的盖章流程,盖不了。” 想要问问小程序服务类目的审核人员: 1、你们要的《医疗主管部门许可文件》或者《省级互联网医疗服务监管平台对接情况证明》,具体是个什么样子,能否给个示例,我们也好发给浙江省卫健委的领导参考? 2、我们已经证明了B有有效期内的《医疗执业机构许可证》和A与B有有效期内的《合作协议》,上传另外两项资料的合理性是什么? 我们上传的资料你们真的看了么?你们的一次审核,关系到我们挣个团队一个季度的努力,但是你们并不会管我们死活。再审核不通过,我们整个产品部都要被开了。。。。客服联系不到,审核又不合理,真的。。。。
2024-10-14 - 小程序pdf、word、excel、ppt等文件页数与预览(第5篇)
一、实现思路: 通过调用第三方服务,将word、excel、ppt等文件通过转码,转成pdf文件方式,然后进行页码获取以及预览。 二、常用第三方转换平台: 阿里云文档处理:https://help.aliyun.com/zh/oss/user-guide/overview-65?spm=a2c4g.11186623.0.0.31927361ms2sPH 腾讯云文档处理:https://cloud.tencent.com/document/product/460/47495 wps开发平台:https://solution.wps.cn/docs/convert/principle.html 三、相关文章 在小程序里初步获取pdf页数在小程序里预览pdf文件小程序与h5接近实时的双向通信小程序页面通过webview获取pdf页数(改为:小程序页面实时计算pdf页数)小程序pdf、word、excel、ppt等文件页数与预览 备注:做过不少文档相关的服务,有需要可私信沟通。
2024-10-12 - 微信jsapi,一码多付
微信jsapi支付问题,想实现一码多付,就是客户只看到一个二维码120元,内部下两个订单,分别给商户a和商户b,可以实现吗?
2024-10-12 - 🎆我们开源啦 | 基于Skyline开发的组件库🚀
我们开源啦,希望可以给大家的开发之旅带来一些灵感。我后溪的小程序也都会基于这个组件库开发,并且会保持组件库的更新与维护。 我是第一次进行开源,肯定会有错漏,欢迎大家指正,我会以最快的时间响应修改。 Skyline UI 组件库 前言 Skyline 是微信小程序推出的一个类原生的渲染引擎,其使用更精简高效的渲染管线,性能比 WebView 更优异,并且带来诸多增强特性,如 Worklet 动画、手势系统、自定义路由、共享元素等。 使用这个组件库的前提是:通过微信小程序原生+skyline框架开发,所以目前我们不保证兼容webview框架(也就是电脑端与低版本的微信),但后续会进行系统性的兼容。 使用 Skyline UI前,请确保你已经学习过微信官方的 微信小程序开发文档 和 Skyline 渲染引擎文档 。 背景 随着Skyline 渲染引擎 1.1.0 版本发布,我们所运营的小程序也平稳的渡过了阵痛期,团队使用Skyline也越来得心应手,所以接下来,团队的开发重心全面偏向Skyline渲染框架,考虑有大量的UI交互重复,我们决定基于Skyline开发了这个UI组件库。 但团队力量有限,这个新生的组件可能有很多的不尽如人意,所以希望能以开源的方式吸引更多开发者使用Skyline框架,如果这个框架不适合你,也可以借鉴其思路。 Gitee Gitee仓库 在线预览 以下是目前两个使用该框架的小程序 SkylineUI组件库 [图片] NONZERO COFFEE [图片] 开始使用 UI库结构 Skyline UI组件库 依赖于以下四部分,具体使用参考以下的具体说明 utils工具库: 其中包含了UI库自定义的一个工具类SkyUtils,它包含了组件中所含的各种函数,非常重要。 各组件元素:sky-*(组件名) skywxss样式库:其中包含深浅色色彩、文字字体、布局等样式wxss 在小程序中引入 UI库 一、直接下载引入 点击下载组件包 将src下所有文件复制到您项目根目录下的components文件夹中,没有的话请自行新建。 二、npm引入 1.在小程序项目中,可以通过 npm 的方式引入 SkylineUI组件库 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 2.安装组件库 [代码]npm install jieyue-ui-com [代码] 3.npm 命令执行完后,需要在开发者工具的项目中点菜单栏中的 工具 - 构建 npm 两种引入方式的不同可能导致后续使用时,引用组件的路径不同,请注意区别 1.直接引入components文件夹内,引用地址通常是 ‘./components/‘ 2.npm引入,组件引用地址通常是’./miniprogram_npm/jieyue-ui-com/’ 如何使用 1.在app.js文件中初始化工具类,并且添加两个全局变量 [代码]// app.js App({ onLaunch() { ;(async ()=>{ // 全局注册工具类SkyUtils // 这里默认npm引用,地址为'./components/utils/skyUtils',如果是直接引用组件,地址可能是'./components/utils/skyUtils',后面不再说明 const SkyUtils = await import('./components/utils/skyUtils'); wx.SkyUtils = SkyUtils.default; // 初始化设备与系统数据 wx.SkyUtils.skyInit() // 小程序自动更新方法 wx.SkyUtils.versionUpdate() })() }, globalData: { sky_system:{}, sky_menu:{} }, }) [代码] 2.在app.wxss文件中引入样式文件 [代码]//wxss * _dark.wxss 是适配深色模式的色彩变量 @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor_dark.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfontline.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfont.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyother.wxss'; [代码] 3.page.json中引用组件 [代码]//page.json { "usingComponents": { "sky-text":"/miniprogram_npm/jieyue-ui-com/sky-text/sky-text" } } [代码] 4.页面中使用 [代码] // wxml <sky-text content="文本内容" max-lines="2" fade></sky-text> [代码] 5.其他组件具体使用请参考组件包中的redeme.md 适配深色模式 如果您在开发时,全部使用我们预设好的颜色变量,那么可以自动适配深色模式。 [代码].page{ background-color: var(--bg-l0); } [代码] [代码] <view style="background-color: var(--bg-l0)"></view> <view style="background-color: {{color}}"></view> [代码] [代码] Page({ data: { color: "var(--bg-l0)" } }) [代码]
2024-01-09 - 历时两年打造,完全基于skyline引擎的高性能图表工具【图表管家】小程序上线啦
基于微信最新的skyline引擎 微信最新的skyline渲染引擎提供了优异的性能支持,尤其是在页面部分渲染和长列表处理上,提供了底层支持。 基于echarts深度优化和适配 echarts是主流的图表框架,但是echarts团队的ec-canvas很久没有维护了,而skyline又是新发布的,究竟skyline和echarts能不能完美适配,似乎是一个非常大的疑问。经过我们长期的填坑和测试后,我们基于echarts官方组件完全重写,克服了在处理手势事件和其他需要高度自定义的场景的不足,实现了比较理想的适配 使用Taro+原生混合开发 原生小程序的开发方式和react hooks相比,开发效率低,样板代码多,组件的重渲染机制不够清晰,很多场景还必须使用wx.createSelectorQuery()。 更像是类似于angular的上一代组件框架。但是Taro 3的实现机制决定了是以牺牲性能换取对react的最大支持。因此,我们在非性能部分采用Taro+react hooks开发,在性能要求高的详情页面图表组件、以及表格组件,使用原生开发。基于glass-easel的最新特性,确保长表格的增删改查的高性能,当然,glass-easel仍有许多问题没有解决,我们也期待它的进一步优化跟进。 欢迎体验使用,技术交流 [图片][图片]
2024-02-26 - 如何隐藏微信小程序右上角的胶囊按钮呢?
我有一个需求,需要做到横屏时全屏展示,但是全屏展示时胶囊按钮隐藏不掉(微信开发者工具能够隐藏但是真机不会隐藏),我已经见到过有能横屏状态下不显示胶囊按钮的小程序,求问一下该怎么做
2024-04-07 - 原生微信小程序开发,使用 lottiejs-miniapp 实现 Lottie 动画的播放
在原生微信小程序开发中,使用 lottiejs-miniapp 实现 Lottie 动画的播放。 lottiejs-miniapp 基于 lottie-web ,当前使用的 lottie-web 版本号为: 5.8.1 “动效”微信小程序 演示: [图片] 打开微信开发工具: 我在这里新建了一个代码片段作为演示: (代码片段:https://developers.weixin.qq.com/s/Eo0K1emN7zwj) [图片] 1、在项目目录(我这里是在index目录)执行命令,初始化 npm 项目: npm init 2、安装 lottiejs-miniapp 组件: npm i lottiejs-miniapp 安装完 lottiejs-miniapp 组件后,我们可以发现在 index 目录下多出了一个 node_modules 文件夹,里面将包含 lottiejs-miniapp。 [图片] 3、构建 npm 这是很重要的一步,在微信开发者工具 -- 顶部菜单 -- 工具 中找到“构建 npm”功能,并点击。 [图片] [图片] 开发者工具提示“完成构建”即可。 构建npm完成后,会在项目中多出一个 miniprogram_npm 文件夹,如下: [图片] 4、下一步,我们开始进行动画的调用。 第一,打开 index.wxml 文件,我们需要在页面文件中 预置一个 <canvas> 组件: <canvas id="lottiejs-canvas" canvas-id="lottiejs-canvas" class="lottiejs-canvas" type="2d"></canvas> 其中,id 和 canvas-id 都命名为"lottiejs-canvas"。 [图片] 第二,打开 index.js 文件,先引入 lottiejs-miniapp import * as lottie from 'lottiejs-miniapp' [图片] 第三,在 index.js 文件,onReady() 中使用如下代码调用动画 wx.createSelectorQuery().select('#lottiejs-canvas').fields({node: true, size: true}).exec(res => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const dpr = wx.getSystemInfoSync().pixelRatio; canvas.width = res[0].width * dpr; canvas.height = res[0].height * dpr; ctx.scale(dpr, dpr); lottie.setup(canvas); lottie.loadAnimation({ loop: true, autoplay: true, //animationData: animationData, path: 'https://www.lottiejs.com/wp-content/uploads/2022/01/83351-taking-the-duggy-out.json', rendererSettings: { context: ctx, }, }); }); 大家主要替换 loadAnimation 中的 path 参数为自己Lottie动画json文件的http地址即可。 [图片] 我们使用了一个测试动效json: https://www.lottiejs.com/wp-content/uploads/2022/01/83351-taking-the-duggy-out.json 大家一定注意执行时要开启 不校验域名的 功能。 [图片] 第四,在 index.wxss 文件,可以对 <canvas> 组件添加样式,也可以在此为Lottie 动效添加背景颜色效果: .lottiejs-canvas{ width: 100%; height: 300px; background-color: rgb(255, 187, 0); } [图片] 到此,我们就可以预览动画效果了: [图片] 代码片段地址:https://developers.weixin.qq.com/s/Eo0K1emN7zwj 代码片段使用注意事项: 1、填写自己的小程序测试appid; 2、执行 npm install 安装依赖; 3、执行构建 npm 功能。
2022-01-11 - wx.miniapp.IAP实现 Apple 支付
使用微信Dount多端平台提供的wx.miniapp.IAP实现 Apple 支付的详细流程及注意事项 前言 微信Dount多端平台最近内测提供了可以将小程序转化为安卓和iOS的能力,想把之前做过的小程序转化为iOS,因为内容含有虚拟物品付费,所以就需要用到[代码]Apple 支付[代码],多端平台提供了wx.miniapp.IAP一整套接口,但是并没有详细地解释,让我一个没有做过iOS开发的前端人员很是苦恼。所以在我经历各种坑之后就有了这篇文章。 准备工作 首先你要有苹果后台账号创建自己的APP 创建完成之后在这个顶部的[代码]商务[代码]完成各种协议和收款账户的填写 [图片] 创建虚拟商品(链接讲的很详细) 创建自己的沙盒测试号必须是没有注册过苹果ID的邮箱 [图片] 退出测试设备的苹果商店账号 完成以上步骤就可以进行开发了 Apple 支付开发流程简述 [图片] 以下是微信Dount提供的接口和我自己的封装 一、添加交易队列观察者 API:[代码]wx.miniapp.IAP.addTransactionObserver[代码] 作用: 添加交易观察者以处理交易状态的更新,包括购买成功或失败等。 输入参数: [代码]ob[代码]:一个包含回调函数的对象,用于处理不同的交易事件。 [代码]updatedTransactions[代码]:处理交易状态更新。 [代码]restoreCompletedTransactionsFailedWithError[代码]:处理恢复购买时出现的错误。 [代码]paymentQueueRestoreCompletedTransactionsFinished[代码]:在恢复购买交易完成时调用。 [代码]shouldAddStorePayment[代码]:询问是否应该添加商店付款。 [代码]paymentQueueDidChangeStorefront[代码]:处理 App Store 店面变化。 [代码]didRevokeEntitlementsForProductIdentifiers[代码]:处理撤销某些产品的权限。 输出参数: 无直接输出参数,通过回调函数处理交易事件。 [代码]function initialize() { const ob = { updatedTransactions: (args) => { console.log('处理交易状态更新,例如购买成功或失败。:', args); args.transactions.forEach(item => handleTransaction(item)); }, restoreCompletedTransactionsFailedWithError: (args) => { console.log('处理恢复购买时出现的错误。:', args); }, paymentQueueRestoreCompletedTransactionsFinished: (args) => { console.log('在恢复购买交易完成时调用。', args); }, shouldAddStorePayment: (args) => { console.log('询问是否应该添加商店付款。', args); }, paymentQueueDidChangeStorefront: (args) => { console.log('处理 App Store 店面变化。', args); }, didRevokeEntitlementsForProductIdentifiers: (args) => { console.log('处理撤销某些产品的权限。', args); }, }; wx.miniapp.IAP.addTransactionObserver(ob); } [代码] 二、请求商品信息 API:[代码]wx.miniapp.IAP.requestSKProducts[代码] 作用: 请求指定商品的详细信息。 输入参数: [代码]productIdentifiers[代码]:商品标识符数组,指定需要请求信息的商品。 [代码]success[代码]:请求成功的回调函数,返回商品信息。 [代码]fail[代码]:请求失败的回调函数,返回错误信息。 输出参数: [代码]invalidProductIdentifiers[代码]:无效的商品标识符数组。 [代码]products[代码]:有效的商品信息数组。 [代码]function requestProduct(index, paySuccess, payFail) { const canMake = canMakePayments(); if (!canMake) { uni.showToast({ title: '没有支付环境', icon: 'none' }); payFail(); return; } gPaySuccess = paySuccess; gPayFail = payFail; wx.miniapp.IAP.requestSKProducts({ productIdentifiers: [],// 这里是用户要购买的商品ID success: (ret) => { console.log(ret.invalidProductIdentifiers, '无效商品标识'); console.log(ret.products, '商品标识'); if (ret.products.length > 0) { startPayment(null, ret.products); } else { startPayment('未查到商品', ret.products); uni.hideLoading(); } }, fail: (error) => { console.error(`获取商品信息失败: ${error}`); gPayFail(); uni.hideLoading(); } }); } [代码] 三、发起支付 API:[代码]wx.miniapp.IAP.addPaymentByProductIdentifiers[代码] 作用: 发起支付请求。 输入参数: [代码]productIdentifier[代码]:商品标识符,指定需要支付的商品。 [代码]success[代码]:支付请求成功的回调函数,返回支付请求的结果。 [代码]fail[代码]:支付请求失败的回调函数,返回错误信息。 输出参数: 无直接输出参数,通过回调函数处理支付请求的结果。 [代码]function startPayment(err, productIdentifier) { if (err) { uni.showToast({ title: err, icon: "none", }); gPayFail(); uni.hideLoading(); return; } wx.miniapp.IAP.addPaymentByProductIdentifiers({ productIdentifier: productIdentifier[0].productIdentifier, success: (args) => { console.log('拉起支付成功', args); uni.hideLoading(); }, fail: (args) => { console.error('拉起支付失败', args); gPayFail(); uni.hideLoading(); } }); } [代码] 四、处理交易 作用: 处理交易状态更新,包括成功、失败、恢复等。 输入参数: [代码]transaction[代码]:交易对象,包含交易状态、交易收据等信息。 输出参数: 无直接输出参数,通过交易状态处理不同的逻辑。 [代码]function handleTransaction(transaction) { if (transaction.transactionState === "SKPaymentTransactionStatePurchased" || transaction.transactionState === "SKPaymentTransactionStateRestored") { console.log(transaction.transactionReceipt, '订单收据'); finishTransaction(null, transaction); } else if (transaction.transactionState === "SKPaymentTransactionStateFailed") { finishTransaction('交易失败', transaction); gPayFail(); } else if (transaction.transactionState === "SKPaymentTransactionStateDeferred") { finishTransaction('等待外部操作', transaction); } } [代码] 五、结束交易 API:[代码]wx.miniapp.IAP.finishTransaction[代码] 作用: 完成交易,通知系统交易已经处理完毕。 输入参数: [代码]transactionIdentifier[代码]:交易标识符,指定需要结束的交易。 [代码]success[代码]:结束交易成功的回调函数,返回结果。 [代码]fail[代码]:结束交易失败的回调函数,返回错误信息。 输出参数: 无直接输出参数,通过回调函数处理结束交易的结果。 [代码]function finishTransaction(err, transaction) { if (err) { uni.showToast({ title: err, icon: "none", }); return; } wx.miniapp.IAP.finishTransaction({ transactionIdentifier: transaction.transactionIdentifier, success: (args) => { console.log('完成交易 success', args); gPaySuccess(); }, fail: (args) => { console.error('完成交易 fail', args); gPayFail(); } }); } [代码] 六、检查支付环境 API:[代码]wx.miniapp.IAP.canMakePayments[代码] 作用: 检查当前设备是否支持支付功能。 输入参数: 无 输出参数: 返回值:布尔值,表示设备是否支持支付功能。 [代码]function canMakePayments() { const canMake = wx.miniapp.IAP.canMakePayments(); console.log(canMake, "检查是否可以发起支付"); return canMake; } [代码] 七、获取交易列表 API:[代码]wx.miniapp.IAP.getTransactions[代码] 作用: 获取当前未完成的交易列表。 输入参数: [代码]success[代码]:获取交易列表成功的回调函数,返回交易列表。 [代码]fail[代码]:获取交易列表失败的回调函数,返回错误信息。 输出参数: [代码]transactions[代码]:未完成的交易列表。 [代码]function getTransactions(callback) { wx.miniapp.IAP.getTransactions({ success: (transactions) => { console.log('当前交易列表', transactions); callback(null, transactions); }, fail: (error) => { console.error('获取交易列表失败', error); callback('获取交易列表失败'); } }); } [代码] 八、恢复已完成的交易 API:[代码]wx.miniapp.IAP.restoreCompletedTransactions[代码] 作用: 恢复已完成的交易。 输入参数: [代码]success[代码]:恢复交易成功的回调函数,返回交易列表。 [代码]fail[代码]:恢复交易失败的回调函数,返回错误信息。 输出参数: [代码]transactions[代码]:恢复的交易列表。 [代码]function restoreTransactions(callback) { wx.miniapp.IAP.restoreCompletedTransactions({ success: (transactions) => { console.log('恢复的交易', transactions); callback(null, transactions); }, fail: (error) => { console.error('恢复交易失败', error); callback(error); } }); } [代码] 九、获取交易收据 URL API:[代码]wx.miniapp.IAP.getAppStoreReceiptURL[代码] 作用: 获取交易收据的 URL。 输入参数: [代码]success[代码]:获取收据 URL 成功的回调函数,返回收据 URL。 [代码]fail[代码]:获取收据 URL 失败的回调函数,返回错误信息。 输出参数: [代码]url[代码]:交易收据的 URL。 [代码]function getReceiptURL() { wx.miniapp.IAP.getAppStore ReceiptURL({ success: (url) => { console.log('交易收据 URL', url); getReceiptData(null, url); }, fail: (error) => { console.error('获取收据 URL 失败', error); getReceiptData('获取收据 URL 失败'); } }); } [代码] 十、获取交易收据数据 API:[代码]wx.miniapp.IAP.getAppStoreReceiptData[代码] 作用: 获取交易收据数据。 输入参数: [代码]success[代码]:获取收据数据成功的回调函数,返回收据数据。 [代码]fail[代码]:获取收据数据失败的回调函数,返回错误信息。 输出参数: [代码]data[代码]:交易收据数据。 [代码]function getReceiptData(err) { if (err) { uni.showToast({ title: err, icon: "none", }); return; } wx.miniapp.IAP.getAppStoreReceiptData({ success: (data) => { console.log('交易收据数据', data); // 调用后端接口传送交易数据 }, fail: (error) => { console.error('获取收据数据失败', error); } }); } [代码] 十一、刷新收据 API:[代码]wx.miniapp.IAP.requestSKReceiptRefreshRequest[代码] 作用: 发起请求刷新收据。 输入参数: [代码]success[代码]:刷新收据成功的回调函数,返回结果。 [代码]fail[代码]:刷新收据失败的回调函数,返回错误信息。 输出参数: [代码]args[代码]:刷新收据的结果。 [代码]function refreshReceipt(err, callback) { if (err) { uni.showToast({ title: err, icon: "none", }); return; } wx.miniapp.IAP.requestSKReceiptRefreshRequest({ success: (args) => { console.log('刷新收据成功', args); callback(null, args); }, fail: (error) => { console.error('刷新收据失败', error); callback(error); } }); } [代码] 十二、获取 App Store 信息 API:[代码]wx.miniapp.IAP.getStorefront[代码] 作用: 获取当前 App Store 店面的信息。 输入参数: [代码]success[代码]:获取店面信息成功的回调函数,返回店面信息。 [代码]fail[代码]:获取店面信息失败的回调函数,返回错误信息。 输出参数: [代码]info[代码]:App Store 店面信息。 [代码]function getStorefront(callback) { wx.miniapp.IAP.getStorefront({ success: (info) => { console.log('App Store 信息', info); callback(null, info); }, fail: (error) => { console.error('获取 App Store 信息失败', error); callback(error); } }); } [代码] 十三、移除交易观察者 API:[代码]wx.miniapp.IAP.removeTransactionObserver[代码] 作用: 移除交易观察者。 输入参数: [代码]success[代码]:移除交易观察者成功的回调函数,返回结果。 [代码]fail[代码]:移除交易观察者失败的回调函数,返回错误信息。 输出参数: [代码]args[代码]:移除交易观察者的结果。 [代码]function removeObserver() { wx.miniapp.IAP.removeTransactionObserver(ob); } [代码] 完整流程及注意事项 初始化:在应用启动时调用 [代码]initialize[代码] 函数添加交易队列观察者,以便处理交易状态的更新。 请求商品信息:用户选择商品后,调用 [代码]requestProduct[代码] 函数请求商品信息。首先检查设备是否支持支付([代码]canMakePayments[代码]),然后通过 [代码]wx.miniapp.IAP.requestSKProducts[代码] 请求商品信息。 发起支付:在成功获取商品信息后,调用 [代码]startPayment[代码] 函数发起支付请求,通过 [代码]wx.miniapp.IAP.addPaymentByProductIdentifiers[代码] 实现。 处理交易:交易状态更新会触发观察者的 [代码]updatedTransactions[代码] 回调函数,调用 [代码]handleTransaction[代码] 函数处理不同的交易状态。 结束交易:在处理完交易后,调用 [代码]finishTransaction[代码] 函数结束交易,通知系统交易已经处理完毕。 恢复交易:提供恢复已完成交易的功能,方便用户在重新安装应用或更换设备后恢复已购买的内容。 获取交易收据:在交易完成后,可以通过 [代码]getReceiptURL[代码] 和 [代码]getReceiptData[代码] 获取交易收据,并发送到后端进行验证。 注意事项 检查支付环境:确保设备可以进行支付([代码]canMakePayments[代码]),如果设备不支持支付,应提示用户并终止支付流程。 错误处理:每一步操作都需要进行错误处理,并且在错误发生时应向用户展示友好的提示信息。 安全性:获取订单收据后应及时发送到后端进行验证,确保支付的真实性。 交易状态处理:对于不同的交易状态,需要分别处理,确保在用户支付成功后能够正确提供商品或服务。 恢复交易:提供恢复已完成交易的功能,方便用户在重新安装应用或更换设备后恢复已购买的内容。 用户体验:在每一步操作(如请求商品信息、发起支付等)时应显示加载提示,确保用户了解当前正在进行的操作。 通过上述步骤和注意事项,可以完成苹果支付的流程,并确保在支付过程中为用户提供良好的体验。
2024-07-25 - 一个微信小程序是否支持多个公司主体的收款业务?
有两个不同的公司主体,我想做一个聚合小程序同时经营两个主体的业务,用两个商户号,这种可以吗?
2024-07-02 - AI智能体应用发布篇(公众号/小程序)
前言 上一篇《教你 3 分钟搭建 AI 助手(无需编码)》让大家快速搭建了微信云开发的AI智能体Web版和H5版。 如果想在微信生态中快速获取用户,那么公众号和小程序是必须要做的载体,所以这篇主要分享以下 3 点: AI智能体发布到公众号 小程序中集成AI智能体 AI深度合成类目如何申请 步骤 AI智能体发布到公众号 进入云模板 首先我们先进入云模板控制台,在这里再教大家一种快速进入云模版的方式,除了在《教你 3 分钟搭建 AI 助手(无需编码)》中提到的云开发控制台进入的方式之外。 还可以直接在微信开发者工具的代码目录区域右键呼出菜单然后选择「通过云模板或AI配置页面」菜单项。 注:非正式AppId,小游戏,游客态、代开发小程序,不是小程序开发者,看不到该菜单项。 [图片] 授权公众号 从「我的应用」列表进入「AI智能体应用」详情页点击「添加至多个平台」 [图片] 可以选择一个AI智能体进行「配置」支持 3 个平台 微信小程序客服 微信公众号(服务号) 微信公众号(订阅号) [图片] 配置方式非常方便只需要填写AppId即可 [图片] 前往微信公众平台“设置与开发” - “基本配置” - “公众号开发信息”,复制”开发者ID(AppID)”信息 [图片] 获取到AppID填写后点击「下一步」扫码授权即可,授权成功后 未授权 会变为 已授权 状态 [图片] 接下来来看下效果: [图片] 没有认证的公众号需要回复“继续”,已认证公众号无需回复“继续”可直接输出文案 小程序中集成AI智能体 集成应用 回到「添加至多个平台」面板选择「添加至小程序」 [图片] 根据操作指南下载好代码包解压 [图片] 复制到 miniprogram/ 目录下 [图片] 再将下载的 project.config.json 进行替换 [图片] 当以上两步都完成之后可以以下两种方式进行跳转: JS跳转代码 [代码]wx.navigateTo({url: "/$weda_root/packages/mIOXHS1t/pages/chat/index"}); [代码] WXML布局代码 [代码]<navigator url="/$weda_root/packages/mIOXHS1t/pages/chat/index">跳转至智能体</navigator> [代码] 在这里就相当于把整个AI智能体应用集成到小程序中了 [图片] 在这里需要注意,如果要发布小程序上线还需要在小程序管理后台更新域名配置 [图片] 集成API 如果想自定义界面,可以直接集成API即可 回到「AI智能体应用」详情页面切换到「接口展示」 [图片] 可直接复制代码在小程序端进行调用,以查看AI智能体列表接口为例 [代码] wx.cloud.callFunction({ name: 'cloudbase_module', data: { name: 'ai_bot_get_bot_list', data: { filter: { where: { }, }, select: { $master: true, // 常见的配置,返回主表字段 }, }, }, success: (res) => { console.log(res) }, }); [代码] [图片] 每个接口除了有示例代码,还有详细的参数说明: [图片] [图片] AI深度合成类目如何申请 想要上线AI相关的小程序,必须申请深度合成类目,所以这一步至关重要,回到「AI智能体应用」详情页切换到「AI算法备案资料」- 「获取AI算法合作协议」输入小程序主题即可 [图片] 截图证明,在这里需要注意截图一定要露出云模板字样,这样便于类目审核人员区分截图证明来源 [图片] 然后到小程序管理后台添加类目选择【深度合成 - AI问答】选择 2.2 使用第三方技术,上传截图证明即可 [图片] 通过以上方式类目已审核通过 [图片] 总结 本篇主要讲解了AI智能体应用的多平台发布,整体而言从创建到发布非常方便,不管你是公众号运营者还是小程序开发都可以拥有自己AI智能体应用,赶紧去试试吧~
2024-06-28 - 微信小程序 Editor组件在无内容的情况下,长按无法粘贴,会自动收起键盘,但没有失焦
微信小程序 Editor组件在无内容的情况下,长按无法粘贴,会自动收起键盘,但没有失焦
2023-07-11 - 小程序组件化开发
一、组件实现方式:template模板和component构造器 除了component,小程序中还有另一种组件化你的方式template模板 区别: 1、template主要是展示,方法则需要在调用的页面中定义。简单来说,如果只是展示,使用template就足够了 2、而component组件则有自己的业务逻辑,可以看做一个独立的page页面。如果涉及到的业务逻辑交互比较多,那就最好使用component组件了。 二、template模板 1、模板定义 建议单独创建template目录,在template目录中创建管理模板文件。 由于模板只有wxml、wxss文件,一个template的模板文件和样式文件只需要命名相同即可,方法则需要在调用的页面中定义 模板文件(wxml):用name区分多个模板 [代码]<template name="packModule"> <view class="packModule">packModule</view> </template> [代码] 模板文件(wxss):自定义模板的样式文件(略),实例中模块相关样式都统一集中在module.wxss中 2、页面引用:(如首页引用) index.wxml: [代码]<!--导入模板--> <import src="./modules/pack.wxml"/> <!--嵌入模板--> <view class="moduleWrap" wx:for="{{moduleInfoList}}" wx:for-item="moduleInfo" wx:key="index"> <!--自由容器模块 里面还有子模块--> <template is="packModule" data="{{moduleInfo}}" wx-if="{{moduleInfo.style == 5}}"></template> <view> [代码] index.wxss: [代码]@import "../../libs/templates/module.wxss"; [代码] 备注: 一个模板文件中可引用多个template,每个template均以name进行区分,页面调用的时候也是以name指向对应的template; template模板没有配置文件(.json)和业务逻辑文件(.js),所以template模板中的变量引用和业务逻辑事件都需要在引用页面的js文件中进行定义; 三、Component组件: [图片] 1. 组件创建: 新建component目录——创建子目录——新建Component(如示例组件:dialog组件) 示例dialog组件也由4个文件构成,与page文件类型相同,但是js文件和json文件与页面有所不同。 dialog.wxml: [代码]<view class="dialog" wx:if="{{ isShow }}"> <!-- 遮罩层 --> <view class="dialog_mask" catchtouchmove="_catchTouch"></view> <!-- 内容 --> <view class="container" catchtouchmove="_catchTouch"> <view class="title" wx:if="{{title}}">{{title}}</view> <view class="content" wx:if="{{content}}"> {{content}} </view> <view class="footer"> <view class="btn cancel_btn" wx:if="{{showCancelButton}}" bindtap="hide" style='background-color:{{globalColor}};'>{{cancelButton}}</view> <view class="btn comfirm_btn" bindtap="comfirm" style='background-color:{{globalColor}};'>{{confirmButton}}</view> </view> </view> </view> [代码] dialog.json: [代码]{ "component": true, "usingComponents": {} } [代码] dialog.wxss:(组件对应 wxss 文件的样式,只对组件wxml内的节点生效) [代码]/* components/dialog/dialog.wxss */ .dialog { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; position: fixed; top:0; z-index: 9999; } ... [代码] dialog.js: [代码]/* * dialog 模态对话框 * Props * 通过事件调用组件 * globalColor:全局色 * Event * show 模态对话框 @param {Object} 配置参数 * hide 模态对话框 * Example * 1、页面中引用dialog:(示例index页面) * 1-1:index.json中声明组件引用 * { * "usingComponents": { * "dialog": "../../components/dialog/dialog" * } * 1-2:index.wxml引用模板 * <dialog id='dialog' global-color="{{globalColor}}"></toast> * 1-3:index页面所引用js文件中,获取组件实例 * onReady: function () { * //获得组件 * this.dialog = this.selectComponent("#dialog"); * } * 2、根据业务条件进行调用: * 2-1、页面中调用: * this.dialog.show({ * title: "提交成功", * content: "描述信息", * cancelButton: "取 消", * showCancelButton:false,//是否显示取消按钮,可缺省,默认不显示 * confirmButton: '确 定', * callback: function () {}//确定按钮回调函数,可缺省,默认只关闭对话框 * }); * 2-1、页面组件中调用: * getCurrentPages()[getCurrentPages().length - 1].dialog.show(...);//传参同上 * 备注:不直接在组件ready中获取dialog组件,会产生多个组件实例,故直接调用页面已有组件实例方法 */ Component({ /** * 组件的属性列表 */ properties: { 'globalColor': String }, /** * 组件的初始数据 */ data: { isShow: false, title: '标题',// 弹窗标题 content: "", // 弹窗内容 cancelButton: '取 消', showCancelButton:false,//是否显示"取消"按钮 confirmButton: '确 定', callback: null //回调函数 }, /** * 组件的方法列表 */ methods: { //隐藏信息提示 hide() { this.setData({ isShow: !this.data.isShow }) }, // 阻止页面滚动 _catchTouch: function () { return; }, //展示信息提示 show(options) { this.setData({ isShow: !this.data.isShow, callback:null }); let _this = this; // 通过options参数配置 if (options) { this.setData(options); } }, // 确定回调 comfirm(){ this.hide(); this.data.callback && this.data.callback();//执行各dialog的回调逻辑 } } }) [代码] 2、页面引用: index.json:(需在json配置文件中进行配置开启使用组件) [代码]{ "usingComponents": { "dialog": "/components/dialog/dialog" } } [代码] index.wxml:(模板文件中引用) [代码]<!-- 自定义dialog组件 --> <dialog id='dialog' global-color="{{globalColor}}"></dialog> [代码] 四、注意点: 1、component组件中扩展自定义节点: 在组件模板中可以提供一个 <slot> 节点,用于承载组件引用时提供的子节点。 默认情况下,一个组件的wxml中只能有一个slot。需要使用多slot时,可以在组件js中声明启用,以不同的 name 来区分。 [代码]Component({ options: { multipleSlots: true // 在组件定义时的选项中启用多slot支持 } }) [代码] 2、component组件样式注意点: 组件对应 wxss 文件的样式,只对组件wxml内的节点生效。 2-1、组件和引用组件的页面不能使用id选择器(#a)、属性选择器([a])和标签名选择器,请改用class选择器。 2-2、组件和引用组件的页面中使用后代选择器(.a .b)在一些极端情况下会有非预期的表现,如遇,请避免使用。 2-3、子元素选择器(.a>.b)只能用于 view 组件与其子节点之间,用于其他组件可能导致非预期的情况。 2-4、继承样式,如 font 、 color ,会从组件外继承到组件内。 2-5、除继承样式外, app.wxss 中的样式、组件所在页面的的样式对自定义组件无效(除非更改组件样式隔离选项)。 [代码] #a { } /* 在组件中不能使用 */ [a] { } /* 在组件中不能使用 */ button { } /* 在组件中不能使用 */ .a > .b { } /* 除非 .a 是 view 组件节点,否则不一定会生效 */ [代码] 五、组件间通信与事件: 1、父组件(调用页面)向子组件传值通讯: 通过properties向自定义组件传递数据 2、子组件向父组件(调用页面)传值通讯: 1、监听事件 自定义组件可以触发任意的事件,引用组件的页面可以监听这些事件 [代码]<!-- 当自定义组件触发“myevent”事件时,调用“onMyEvent”方法 --> <component-tag-name bindmyevent="onMyEvent" name="{{name}}" /> Page({ data:{ name:"test" }, onMyEvent: function(e){ e.detail // 自定义组件触发事件时提供的detail对象 } }) [代码] 2、触发事件 自定义组件触发事件时,需要使用 triggerEvent 方法,指定事件名、detail对象和事件选项: [代码]<!-- 在自定义组件中 --> <view>{{name}}</view> <button bindtap="onTap">点击这个按钮将触发“myevent”事件</button> Component({ properties: { name: { type: String, value: '' } }, methods: { onTap: function(){ var myEventDetail = {} // detail对象,提供给事件监听函数 var myEventOption = {} // 触发事件的选项 this.triggerEvent('myevent', myEventDetail, myEventOption) } } }) [代码] 总结 自定义组件,可以理解为一个自定义的标签,页面的一个片段,可以分为template方式和component组件方式实现;如果是简单的内容展示,逻辑单一,使用template方式即可,但如果每一个组件都有自己的业务逻辑,各自独立,建议使用component组件方式实现,灵活性更高。 参考文献 官方文档
2019-06-20 - 小程序吸顶、网格、瀑布流布局都拿下
来看看新版 scroll-view 带来的新能力吧~ —————— 在之前的文章中,我们知道了新 scroll-view 可以让小程序的长列表做到丝滑滚动~ 也提到了新 scroll-view 提供了很多新能力 sticky、网格布局、瀑布流布局等,这一篇,我们就来看看这些新能力是怎么使用的~ 新 scroll-view 在原来列表模式(type="list")的基础上,新增了自定义模式(type="custom") 在自定义模式下,新增了以下新组件供开发者调用: list-view:列表布局容器sticky-section / sticky-header:吸顶布局容器grid-view:网格布局容器,可实现网格布局、瀑布流布局等 sticky布局sticky 布局即在应用中常见的吸顶布局,与 CSS 中的 position: sticky 实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。 常见的使用场景有:通讯录、账单列表、菜单列表等等。 与 position: sticky 不同的是,position: sticky 很难实现列表滚动需要的交错吸顶效果,而 sticky 组件则可以帮忙开发者轻松实现交错吸顶的效果。 sticky 的使用非常简单: 将 scroll-view 切换到 custom 模式采用 sticky-section 作为 scroll-view 的子元素sticky-header 放置吸顶内容list-view 放置列表内容<scroll-view type="custom"> <sticky-section wx:for="{{list}}"> <sticky-header> <view>{{item.name}}</view> </sticky-header> <list-view> <view>...</view> </list-view> </sticky-section> </scroll-view> 我们来看下采用 sticky 布局做出来的通讯录效果~ [视频] sticky 布局也可以通过给 sticky-section 配置 push-pinned-header 来声明吸顶元素重叠时是否继续上推 像下图输入框和标签列表这种类型,标签列表吸顶时还是希望保留输入框吸顶。 [视频] 网格布局网格布局即将列表切割成格子,每一行的高度固定,常见的视频列表、照片列表等通常都采用网格布局。 在此之前,实现网格布局需要开发者自行实现网格切割,再嵌入到 scroll-view 中。 新 scroll-view 直接提供了 grid-view 组件供开发者使用~ 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 aligned 做为直接子节点grid-view 中直接编写列表<scroll-view type="custom"> <grid-view type="aligned" cross-axis-count="3"> <view wx:for="{{list}}"> <image src="{{item.image_url}}" mode="aspectFit"></image> <view>...</view> </view> </grid-view> </scroll-view> 下面是使用网格布局实现的图片列表效果~ [视频] 瀑布流布局瀑布流布局与网格布局类似,不同的是瀑布流布局中每个格子的高度都可以是不一致的,所以在小程序中实现瀑布流布局就比较复杂了。 开发者需要通过计算格子高度,然后再进行瀑布流拼接,当滚动内容过多时还需要处理节点过多导致内存不足等问题。 grid-view 组件直接支持了瀑布流模式供开发者直接使用,grid-view 组件会根据子元素高度自动布局: 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 masonry 做为直接子节点grid-view 中直接编写列表<scroll-view type="custom"> <grid-view type="masonry" cross-axis-count="2"> <view wx:for="{{list}}"> <image src="{{item.image_url}}" mode="widthFix"></image> <view>...</view> </view> </grid-view> </scroll-view> 下面是使用瀑布流布局实现的图片列表效果~ [视频] 想要立即体验?现在通过微信开发者工具导入 代码片段,即可体验新版 scroll-view 组件能力~
2023-10-20 - 小程序使用scss,设置 page 全局自定义属性,在组件 wxss 中无效?
小程序使用scss, 使用 第三方的UI 组件(vant-weapp)。 想要通过变量去修改wxss样式如 下, height: var(--tabbar-height,50px); 然后在 app.scss 中设置 全局变量 page { --tabbar-height: 88px; } 样式没有生效。--tabbar-height 没有值,还是取得 50px。
2023-08-18 - 微信开发者工具支持scss了?
[图片] 在微信开发者工具的设置里看到了scss,这是能配置了?下面的内容看不太懂。求官方解答一下配置方案或者后续会有此项计划吗?
2020-11-25 - 小程序压测怎么做?快来试试 Donut X 小程序云测的解决方案吧
需求场景 很多小程序都会进行一些运营活动,这时候会有很多用户同时进来访问。当用户量比较大时,业务方可能会担心压力过大导致小程序功能异常。 这时开发同学/测试同学希望能够对小程序进行一次压力测试,比如可以模拟1000个用户同时打开小程序场景。 自然而然的,一个常见想法是直接用1000个手机,同时打开小程序,模拟用户并发情况。但是用纯UI自动化方式去压测,会遇到以下几个问题: 由于每台手机的性能不一,控制多台手机都是在同一个时刻去打开小程序基本无法做到 用1000台手机去做端到端的压测成本高。而且由于手机数量是有限的,压力瓶颈有明显上限 解决方案 微信推出的 Donut 微信安全网关 支持小程序压测能力,可以生成微信code,实现真实用户请求业务接口以及微信开放接口的全链路压测 微信安全网关的压测工具主要关注服务端请求响应情况,包括请求的正常响应、请求耗时等内容;在小程序 UI 相关的表现感知不强 这里可以将 小程序云测 的自动化/性能测试能力和微信安全网关结合起来。首先利用微信安全网关对后台服务器进行发压,例如并发用户量调整为1000。压力上来后,在云测正常执行对应的测试任务。这样相当于用几台手机模拟测试了“1000台手机同时打开小程序”的场景。 通过结合云测的自动化测试和性能测试能力(比如收集到的截图,录屏,体验评分,启动性能 ,网络请求,是否存在黑白屏JSError等异常情况),用户可以全面观察小程序在压力情况下的实际UI表现 实践案例 需求:小程序A希望对首页内容进行压测,观察100个用户同时打开首页的表现是否有异常。可以按照以下步骤进行: 1. Donut发压 用户在 Donut 平台创建安全网关后,前往「压测工具」页面(压测工具在内测期间时,需要联系技术支持开通),创建压测任务并调试后台压测请求链路,例如小程序A希望对首页进行压测,请求的链路为: 首先获取用户code 用微信code访问服务器后台,识别用户 识别用户身份后,生成首页内容数据 [图片] 调试成功后,可以按照实际需求去预约压测任务,如配置并发用户数为100,压测时长为30分钟 2. 云测跑测质检任务 压测任务启动后,建议立即启动云测任务(由于云测需要对真机进行初始化操作,一般需要2-5分钟后才会真正拉起小程序)。 执行云测的任务类型推荐使用小程序质检能力,因为质检会同时发起启动性能测试和自动化测试任务: 启动性能测试:观察压力情况下,小程序启动耗时是否会变慢 自动化测试:自动化任务可以根据业务实际需求,使用Monkey、录制回放、Minium的一种。主要目的是观察压力情况下,小程序是否会出现功能异常或性能问题(如Monkey测试可以检测JSError,黑白屏等异常) 这里小程序A使用Monkey作为自动化测试方案。一般情况下,首页内容是正常加载的,但是当服务器压力较大,网络返回较慢时,小程序A出现了JSError问题 [图片] 后续规划 目前用户需要手动在Donut和云测端分别操作去启动任务。 后续云测和Donut结合起来,可以让用户在Donut中,执行压测任务时可以选择同时启动云测任务,带来更好地压测体验。 关于压测有任何问题,欢迎在帮助页面,加入官方企微群,和云测小助手一起探讨
2024-05-30 - 微信小程序如何将doc, xls, ppt, pdf, docx, xlsx, pptx文件保存到本地
大家好,我是兔兔,兔兔答题开发者。 最近在做兔兔答题时,涉及到将文件保存到微信本地,这里的本地是指微信文件助手或者微信好友,是直接分享文件而不是做微信分享好友的形式。 在微信开放社区中,也有不少关于该话题的帖子。大家感兴趣的也可以去搜索一下。 [图片] 对于第一次做微信小程序,或者是没去了解过这块的,刚开始不知道如何着手,也不知道如何实现。当你发现其实是非常简单的,就几行代码就敲定了。在这里就不像其他的文章,还单独分享一下各种API,我就直接贴正确代码。需要注意的是,我这里使用的是uniapp开发,如果你是微信原生小程序开发,你直接使用微信原生的语法调用这两个函数即可。 [代码]let _that = this uni.downloadFile({ url: _that.url, success: function(res) { uni.openDocument({ filePath: res.tempFilePath, showMenu: _that.is_download == 1 ? true : false, success: function(res) { uni.previewImage({ urls: ['https://imgcdn.tutudati.com/20231001004615552606228.png'], }) } }) }, fail(res) { _that.$func.showToast(res.errMsg) } }) [代码] 需要注意的是$func.showToast()函数是我自己封装的组件。 通过上述代码,其实也不难看出来,就只调用了两个uniapp的函数就实现了功能。 第一个方法是[代码]uni.downloadFile()[代码],这个函数是将远程文件下载到本地,你会获取到一个临时文件地址[代码]tempFilePath[代码]。 第二个方法是[代码]uni.openDocument()[代码],这个函数是打开本地临时文件地址,这里的临时文件地址就是第一步中获取到的[代码]tempFilePath[代码],例如PDF文件,会直接进行预览显示。 关于第二个方法中,我添加了一个[代码]showMenu[代码]的配置项,这是一个非常重要的地方。如果你设置为false,当文件进行预览时,右上角是不会显示功能菜单,也就是说你没法把文件进行保存到本地。当你开启时,将是如下效果。 [图片] 右上角有三个点,当你点击三个点就会弹窗转发好友的选项,你直接点击转发好友就可以保存到文件助手或者你的微信好友了。 注意事项 这个功能看起来,体验性就不是很强。但也是目前为止,能够解决的方案。在使用该方式保存文件,你需要注意如下几个地方: 1、在微信小程管理后台,文件的域名要和文件下载域名保持一致,否则在调用[代码]uni.downloadFile()[代码]函数时就会提示,下载域名不是合法的域名。 2、在调用[代码]uni.openDocument()[代码]函数时,filePath一定是小程序内本地文件地址,你也可以通过其他的函数下载文件来获取本地文件地址,也可以使用文章中的这个函数。 3、打开的文件也是有限制的,目前根据uniapp官方文档来看,只支持doc, xls, ppt, pdf, docx, xlsx, pptx这几种文档类型。查看了一下微信小程序的官方文档,也是支持这几种格式。对于不在这几种格式的范围内,可能就需要通过其他的方式实现。例如通过文件链接,让用户打开浏览器预览;还有是直接通过webview来实现。 关于微信小程序如何将文件保存到本地的解决方案就算完成啦,希望这篇文章的分享对你有所帮助。
2024-05-19 - 长列表:按需渲染vs回收创建
在 Skyline 支持了长列表按需渲染之后,还是有很多开发者对于按需渲染表示疑惑: 开发者A:scroll-view 下拉不太流畅 开发者B:list-view 有什么作用呢? 开发者C:关于长列表的按需渲染功能,我们如何能检测到这个功能正确触发了呢? 关于以上几个问题,我们一一来解答: Q:scroll-view 下拉不太流畅? 当发现 scrll-view 下拉不够流畅时,可能是用法不对导致的不流畅。 根据 type 不同,按需渲染的用法也不同,建议按以下方式检查一下 type="list" : 根据直接子节点是否在屏来按需渲染type="custom" : 只渲染在屏节点,对于列表、网格、瀑布流等,子节点必须包裹在 list-view、grid-view 内部才会按需渲染。 Q:list-view 有什么作用呢? 对于 list-view、grid-view 等 *-view 组件,符合规定的写法则会按需渲染。 默认情况下,视口外节点不渲染。也可以根据业务需要,设置 scroll-view 的 cache-extent 指定视口外渲染区域的距离来优化滚动体验和加载速度。 [图片] 当然 cache-extent 越大也会提高内存占用且影响首屏速度,建议大家按需启用。 Q:关于长列表的按需渲染功能,我们如何能检测到这个功能正确触发了呢? 当使用按需渲染时,例如下面用的 type="list",其实直接子节点都是一开始就创建的,所以没有办法从开发者工具检查到这个功能正常触发。 [图片] 不过可以在真机上开启 “开发调试 - Debug Skyline - checkerboardRasterCacheImages” 调试 [图片] 当滚动 view 离开屏幕回来之后颜色变了,说明节点重新渲染了,以此来确认按需渲染功能正确触发 👇例如下图中第一个节点,一开始是紫色,当离开屏幕重新滚动回屏幕时,变成了黄色,证明按需渲染成功~ 注意:不是所有的组件都会形成 RasterCache,需要结构复杂一些才会; [图片] *-builder 组件 除了 *-view 组件,很多开发者应该也注意到了 *-builder 组件 list-view 对应 list-buildergrid-view 对应 grid-builder看文档描述的能力是一样的,但是为什么会分成两个组件呢? 因为目前 *-view 组件是按需渲染,节点还是会不断的创建,当长列表越来越长时,内存占用会越来越多。 于是我们新增了 *-builder 组件来支持 scroll-view 的可回收,可以更大程度降低创建节点的开销。 我们来看下效果,可以从开发者工具的 wxml 看到,当列表滚动时,list-builder 中渲染的 view 节点只有在屏的几个 [图片] 除了使用 wxml 板块查看之外,*-builder 组件还提供了监听事件,开发者可以监听列表创建和回收 binditembuild:列表项创建时触发,event.detail = {index},index 即被创建的列表项序号binditemdispose:列表项回收时触发,event.detail = {index},index 即被回收的列表项序号 使用场景既然 *-builder 组件拥有回收+创建能力,是不是可以不用 *-view 组件啦? 当然不是啦~~~ 回收+创建能力本身就是有开销的,所以也要根据业务场景按需使用哦 *-builder:对于长列表、无限滚动列表等,或者节点内存占用高的,每个时刻都确保不会有太多节点创建出来,使用 *-builder 可以节省内存*-view:对于短列表,或者内存占用不高的列表则比较适合使用 *-view 代码片段:https://developers.weixin.qq.com/s/rp07iKmW7UQS
2024-05-16 - 利用 CSS 解决 slot 显示默认值
起因 众所周知,小程序至今还未支持 slot 显示默认值(五年啦),但是业务中这个需求还是挺普遍的,故此分享下我是怎么实现该需求的。 构建一个场景 有一个列表单项组件,当名为 icon 的 slot 有内容传入时,显示该 slot,否则显示默认的 icon。 示例代码 需要将组件的 js 中的 multipleSlots 设置为 true。 [代码]<!-- ListItem.wxml --> <view class="list-item"> <view class="list-item__content"> <view class="list-item__left"> <view class="list-item__left-icon--slot"> <slot name="icon"></slot> </view> <view class="list-item__left-icon"></view> <view>{{title}}</view> </view> <view class="list-item__right"> <slot></slot> </view> </view> </view> [代码] [代码]// ListItem.scss .list-item { .list-item__content { .list-item__left { &-icon--slot { margin-right: 12rpx; &:empty { display: none; } &:not(:empty) + .list-item__left-icon { display: none; } } &-icon { width: 40rpx; height: 40rpx; margin-right: 12rpx; color: var(--color-text-disabled); background-color: currentColor; -webkit-mask-repeat: no-repeat; -webkit-mask-position: center; -webkit-mask-size: 100%; -webkit-mask-image: url('xxx.png') } } } } [代码] 总结 实现的原理解释: 当 slot 没有内容时,利用了 [代码]:empty[代码] 伪类选择器,隐藏 [代码].list-item__left-icon--slot[代码] 的元素。 当 slot 有内容时,利用了 [代码]:not[代码] 以及 [代码]+[代码] 选择器,使与 [代码].list-item__left-icon--slot[代码] 紧邻且在其之后的 [代码].list-item__left-icon[代码] 的元素隐藏 不过需要注意的一点是,官方文档中 wxss 支持的选择器很有限,但是实测是大部分支持的,目前个人尝试已知不可用的选择器有 [代码]*[代码]、[代码]~[代码] 以及属性选择器(还有一些复杂情况可能也不支持,需要大家自己尝试)。 希望这次分享对大家有一定的帮助吧。 参考 微信小程序开发文档(小程序框架 /视图层 /WXSS)
2024-04-02 - 自定义组件中scroll-view的scroll-into-view属性在子节点是slot时不生效
https://developers.weixin.qq.com/miniprogram/dev/component/scroll-view.html
2022-12-22 - scroll-view 中使用slot插槽 子节点设置id scroll-into-view无效?
scroll-view 中使用slot插槽插入内容 子节点设置id scroll-into-view无效 [图片][图片] [图片]
2023-06-19 - 微信小程序如何在分包中使用npm库
因为tencentcloud-webar这个库比较大,我就在分包中使用了,但是从分包中的miniprogram_npm目录去引入,无法使用,目录结构如下: [图片]
2023-05-17 - 隐私授权手机号授权提示invoke getPhoneNumber too frequently?
手机号快捷登录页面,增加隐私授权的监听,隐私授权弹窗按钮 点击拒绝后,再次点击手机号快捷登录提示“invoke getPhoneNumber too frequently”过于频繁地调用getPhoneNumber,问一下有遇到过这种情况么? [图片][图片]
2023-08-29 - 云开发环境共享的小程序,不能给用户发送订阅消息吗?
本人有两个小程序,小程序A是主程序,有自己的云开发数据库资源。小程序B没有单独申请云开发资源,只是使用了小程序A共享的环境资源。 在小程序A可以订阅消息,可以接收消息。 小程序B可以订阅消息,但是不能接收订阅消息。 错误提示: errCode: 40003 errMsg: "openapi.subscribeMessage.send:fail invalid openid rid: 656455ac-6cce5320-04c66132" 意思是说:openid错误。 小程序B调用小程序A的云函数获取openid: const wxContext = cloud.getWXContext(); let openid = wxContext.FROM_OPENID;//环境共享的小程序B获取openid的方法 本人测试过,小程序B使用 wxContext.OPENID 是获取不到openid的,只能通过 wxContext.FROM_OPENID 获取。 请问大神,是我哪里写错了?还是云开发环境共享的小程序B,不能给用户发送订阅消息?
2023-11-27 - 省钱有道之 云开发环境共享小结
#前言 最近为了节省一点小程序的运营成本,一些没啥流量的小程序如果每个月也要19块略微有些肉疼(主要还是穷),研究了一下云环境共享,在这里简单做一下总结。 [图片] 这里有官方的小程序环境共享文档需提前了解一下,具体共享步骤按官方文档操作即可。 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/resource-sharing/introduce.html #注意点 共享环境有几个注意点大致如下: 1、必须是相同主体 2、开通了云开发环境的小程序可以共享给同主体的小程序、公众号,被共享方无需开通云开发环境 3、一个云开发环境最多可以共享给10个小程序/公众号 4、共享后双发均可主动解除 5、按官方文档要求,资源方需有云函数cloudbase_auth,测试时发现没有这个云函数其实也能正常运行,可能我验证的场景还不够多 6、云能力初始化的方式不同,资源方按传统的云环境初始化方式即可,也就是 wx.cloud.init({ env: env.activeEnv, traceUser: true }); 而调用方的初始化方式有所不同 const cloud = new wx.cloud.Cloud({ //资源方AppID resourceAppid, //资源方环境ID resourceEnv, }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 const initRes = await cloud.init(); 后续调用资源方的云函数就用这个cloud就行了:cloud.callFunction({...}); 7、调用方有操作到云存储文件的api也需要用6步骤中的cloud 8、云存储fileId需要用cloud.getTempFileURL转换成临时/永久链接,否则在调用方无法展示 9、一些api的云调用方式也有变化,需指明具体的appid。比如A小程序授权给了B小程序,想给B小程序推送客服消息需要写成 await cloud.openapi({appid:B小程序appid}).customerServiceMessage.send({...}); 10、获取调用方的appid/openid/unionid也有所不同 // 跨账号调用时,由此拿到来源方小程序/公众号 AppID console.log(wxContext.FROM_APPID) // 跨账号调用时,由此拿到来源方小程序/公众号的用户 OpenID console.log(wxContext.FROM_OPENID) // 跨账号调用、且满足 unionid 获取条件时,由此拿到同主体下的用户 UnionID console.log(wxContext.FROM_UNIONID) #适配 基于以上注意点,开始进行适配,由于我是一套代码部署N个小程序,然后一个云环境共享给其他小程序,希望通过配置决定哪个小程序作为资源方,哪些作为调用方 首先是云开发环境的初始化: 1、env.js 环境配置: //云开发环境 const cloudBase = { //使用共享云环境资源,资源方=false,调用方=true useShareResource: false, //资源方AppID resourceAppid: "wx9d2xxxxxxxx0088", //资源方环境ID resourceEnv: "prod-9gxqvi3qb3c257ef", //云环境ID prod: "prod-9gxqvi3qb3c257ef" } 2、api.js 操作模块 const env = require('../env.js'); let cloud; /** * 初始化云能力 * @returns {Promise} */ const wxCloudInit = async function () { const {cloudBase} = env; if (!wx.cloud) { console.error('请使用 2.2.3 或以上的基础库以使用云能力') } else if (cloudBase.useShareResource) { const {resourceAppid, resourceEnv} = cloudBase; // 声明新的 cloud 实例 cloud = new wx.cloud.Cloud({ //资源方AppID resourceAppid, //资源方环境ID resourceEnv, }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 const initRes = await cloud.init(); console.log("初始化云能力完毕:", initRes, "资源方appid:", resourceAppid, "资源方环境ID:", resourceEnv); } else { wx.cloud.init({ env: env.activeEnv, traceUser: true }); console.log("初始化云能力完毕,当前环境:", env.activeEnv); cloud = wx.cloud; } this.cloud = cloud; } /** * 云函数调用 * @param name * @param data * @param success * @param fail * @param complete */ const callCloudFunction = function (name, data, success, fail, complete) { //执行云函数 cloud.callFunction({ // 云函数名称 name: name, // 传给云函数的参数 data: Object.assign({}, data, {env: env.activeEnv}) }).then(res => { typeof success == 'function' && success(res); }).catch(res => { typeof fail == 'function' && fail(res); }).then(res => { typeof complete == 'function' && complete(res); }); }; 3、在app.js中初始化云环境,后续有用到wx.cloud的都需要改成api.cloud const api = require('utils/api.js'); App({ onLaunch: async function (options) { await api.wxCloudInit(); } }); 其次是资源方的获取用户信息调整 每次都要判断wxContext.FROM_OPENID是否为空,不为空则是调用方的用户信息,为空则是资源方的用户信息,略微繁琐,干脆封装了一个npm包wx-server-inherit-sdk,改造了一下getWxContext函数,源码如下,引入这个包后也就可以不用引入官方的wx-server-sdk const cloud = require('wx-server-sdk'); // 保存原始getWXContext方法到另一个变量 const originalGetWXContext = cloud.getWXContext; cloud.getWXContext = function () { //调用原始getWXContext方法 const wxContext = originalGetWXContext.call(this); const {FROM_APPID, FROM_OPENID} = wxContext; //云开发环境共享时获取到的APPID会替换成源方APPID if (FROM_APPID) { Object.assign(wxContext, {APPID: FROM_APPID}); } //云开发环境共享时获取到的OPENID会替换成源方OPENID if (FROM_OPENID) { Object.assign(wxContext, {OPENID: FROM_OPENID}); } return wxContext; } module.exports = cloud; 到此也就大功告成。为了省钱也是够折腾的[哭笑]
2023-08-28 - 微信小程序如何配置银联云闪付支付
前言: 早在9月30号,微信派公众号就发布了腾讯微信支付与银联云闪付深化支付合作与互联互通的声明,原文地址 那么问题来了,微信小程序怎么配置支持云闪付支付呢? 简简单单就一步,就可以让小程序支持云闪付支付了 登录微信支付商户后台->「产品中心」->「开发配置」页面最底部找到「支付方式配置」,点击「开启」就可以了,无需开发,无需额外配置,只要用户手机安装了云闪付app,在小程序支付时,就可以选择云闪付付款。 [图片] 注意事项 1、当前只支持小程序使用云闪付付款,微信app需要更新到最新版 2、开通后默认商户号绑定的所有小程序均开启支持云闪付支付,如有部分小程序不想开通云闪付付款,可以指定小程序appid不开启云闪付支付 [图片] 3、支持服务商模式 4、配置成功后支持停用 5、原有接口无需改动 6、如用户使用云闪付付款,中途取消付款,是会返回在选择支付方式页面 7、支持云闪付优惠 以下为实际支付测试截图 [图片][图片] [图片] 配置了没有云闪付入口等常见问题请看下面地址 https://developers.weixin.qq.com/community/develop/article/doc/000ac04bca8558f9991df282651413
2021-12-29 - pc版小程序怎么监听鼠标右键,实现点击右键出菜单?
pc版小程序上线了,测试了一下,将腾讯文档小程序用pc版微信打开后,可以右键跳菜单,是怎么实现的呢? [图片]
2019-08-13 - 个人开发者把小程序发布到 App Store 5个步骤(保姆级教程)
用完即走,小程序已经成为连接用户与服务的重要桥梁,无论是购物、出行还是娱乐,小程序都以其便捷性和高效性赢得了用户的青睐。 那小程序是否有边界,能否拓展到 App? 微信开发者工具的最新升级,为这一问题提供了创新的解答。现在,开发者们可以轻松将已有的小程序代码构建为全功能的 App,而无需从零开始开发,这不仅节省了大量的人力和时间成本,更为小程序开发者们打开了通往 App Store 巨大流量的大门。 在这篇文章中,我们将深入探讨微信开发者工具支持小程序 App 化的五大步骤,分析其潜在价值,并通过实际案例来展示这一过程的效果。 背景:个人开发者,将小程序代码构建为 iOS App,以下为整体流程,适合 iOS 开发 / 上架零基础的同学。 [图片] 缘起 一个周末,突然奇想,我还没有搞过 App,要不搞搞玩玩😄 从 0 开始学还是很慢的,毕竟时间有限,好在了解过提示工程 [代码]《ChatGPT 进阶:提示工程入门 陈颢鹏 李子菡》[代码],问了一下助手 ChatGPT 几个常见的问题。 开发适用于 iPhone 的 App 的流程是什么 注册开发者账号 -> 下载 Xcode -> 学习 Swift -> 设计 + 编码 + 测试 -> TestFlight 内测 -> 准备上架 (准备) -> 提交审核 -> 应用上架 -> 应用更新和维护 很好,请给出可运行的应用实例,完成查询本机 IP 地址 我是一个新手,请问在 XCode 中如何运行你提供的代码 几轮对话后,大约用了 1 个小时,一个 iOS Demo 在模拟器上跑成功了,有点意思😄 不过功能有点简单,几年前用 Vue 写过一个还在跑的网站,同时我知道 ChatGPT 的编程能力,于是我丢了一个问题给他。 [代码]你是一个开发,会 Vue 、iOS 开发(使用 SwiftUI 框架 )两种语言,现在需要你根据 Vue 的代码重写为 iOS 代码,以下是 Vue 代码 [代码] [图片] 笔者是一个运维平台的产品,为了不忘记运维场景和技能,自己维护一个业务场景,开发语言:golang + Vue,部署架构:腾讯云 CLB+TKE+ 服务网格,运营系统:CLS+ 云函数 +Kafka+Doris+Flink+Hadoop+Streamsets。 结果惊奇的发现,真的可以执行,不服不行 👍 [图片] 1. 转折:把小程序代码构建为 iOS App 测试包(1 小时) 如果仅仅只是这样,那么这篇文章标题就该叫“GPT 如何将 Vue 改写为 App”。 万万没想到,过了 2 周从朋友那里获悉 微信开发者工具可以直接将小程序代码构建为 App,就像 Golang 一样,可以通过参数 [代码]GOARCH[代码] 控制构建的程序是在跑在 [代码]amd64[代码],还是 [代码]arm64[代码] CPU 架构上。 [图片] 现实就是这么巧,几年前使用 Vue 开发站点时,同时也开发了同款小程序。 有点意思,参照文档 构建你的第一个应用 花了 1 个小时,在我的 iPhone 上跑了 测试版 的 App. [图片] 此处应该给多端应用的产品和开发点个赞👍🏻 搞到这里,我其实进入了这款的第一个哎哈时刻,确实很爽,因为我不需要花心思用 GPT 来迁移 Vue 程序,直接用微信开发者工具构建为 App 即可,交互完全一致。 另外记录构建过程中遇到的两个问题 问题 1:小程序的图片在 App 中无法渲染 启用 Media SDK 即可 [图片] 问题 2:App 带有 Vconsole 入口 一开始以为在模拟器中才有,最后发现是一个配置,需要自己主动关闭。 [图片] 2. 构建正式包 谁不想在 App Store 能搜到自己的 App 呢,第二步,构建正式包。 2.1 准备苹果开发者账号 在 MacBook Air 或 iPhone 中安装 Apple Developer,然后注册苹果开发者账号 [图片] 一年 688 元会费 [图片] 正常情况下,交完会费后,第二天会收到一封欢迎加入 Apple Developer Program 的邮件,代表苹果开发者账号注册成功。 很遗憾,我注册时提示“未知错误,请再试一次” 找 Apple Developer 客服反馈,最后答复 [代码]由于一个或多个原因,您无法完成 Apple Developer Program 的注册。我们目前无法继续处理您的注册。[代码]。 好吧,估计是被风控命中了,于是找了家人的账号来注册,直接成功😄 2.2 生成 Bundle ID/ 证书 /Profile 生成 App 备案和构建正式包都需要的 Bundle ID/ 证书 /Profile。 生成 Bundle ID Bundle ID 是一个唯一的标识符,用来识别你的应用程序。它通常采用反向域名格式,例如 com.example.myapp。在开发和发布应用程序时,你需要在苹果的开发者账户中注册一个 Bundle ID,这样苹果的服务才能识别出你的应用程序。 参照 文档 生成 Bundle ID。 生成 证书 /Profile 证书(Certificates)用于建立开发者的身份,并确保应用是由已注册的开发者发布的。开发者需要从苹果开发者中心申请证书,用来对应用进行签名,这样 iOS 设备才会信任并运行这个应用程序。 配置文件(Provisioning Profiles)是一个包含证书、应用程序 ID、设备 ID 和其他信息的文件,它告诉 iOS 设备一个应用程序可以被安装和运行。配置文件将应用、开发者和设备联系起来,控制哪些设备可以安装和运行你的应用程序。 参照 文档 生成 iOS 证书和 Provisioning Profile。 [图片] 拓展资料:创建证书签名请求 问题:申请的 iPhone Distribution 证书不受信任 导入 Apple WWDRCA 证书 即可,可能原因:大致是分发的根证书没有导入你的 Mac 上。 更多资料详见 Apple PKI。 [图片] 2.3 备案(10 天 +) App 如果没有备案,在中国大陆将无法上架,这是苹果官方的说明。 中国工业和信息化部(MIIT)要求 App 必须具备有效的互联网信息服务提供者(ICP)备案号,了解更多 [图片] 其实备案比较简单,参照 App 备案 ,使用上一部分申请的 [代码]Bundle ID[代码]、证书(可查看 [代码]公钥[代码]、[代码]签名 MD5 值[代码])即可,不需要把 App 开发完,再来备案。 备案最长需要 20 个工作日,笔者用了 10 个工作日,在一个周五的下午收到了工信部发来的备案通过短信。 2.4 创建移动应用 移动应用是为了让 App 能用上微信的能力(比如分享到朋友圈或发送给朋友、微信登录 / 支付等),在移动应用中同时登记了 Bundle ID 和 Universal Links,这将会传递给下一步的多端框架,这是构建可正式包(采用苹果的分发证书)的必备条件。 先介绍一下 Universal Links。当用户使用 iPhone 手机访问你的网站,同时安装了 App 时,能在网站顶部快速跳转到 App。具体可以看下苹果官方的文档 Supporting associated domains 你需要有一个网站,未来要放 Universal Links 要用到的 [代码]apple-app-site-association[代码] 文件,不过对于我来说,这个功能好像用处不大,我更需要的是当用户用 iPhone 访问网站,引导他去 Apple Store 安装 App. 这里有一个关键信息,如果你不需要微信支付 / 微信登录 / 微信卡券的能力,不需要做开发者认证(开发者认证不能是个人主体) 访问 微信开放平台,创建移动应用,提交审核,几个小时就审核通过了。 [图片] 2.5 绑定多端框架 在 Donut 开发平台 中将 多端应用绑定上一步创建的移动应用,这样可以用到移动应用中登记的 Bundle ID 和 Universal Links,官方这么做比较合理,关键信息必须通过移动应用这关人工审核来起到一定的约束。 [图片] 绑定后,在多端应用中可以看到 Bundle ID 和 Universal Links 了。 [图片] 2.6 准备 App icon 等资料 App Icon 先用工具为你的 App 设计一个 1024px X 1024px 的图标,然后在 App Icon Generator 上生成 iPhone 所有规格的图标,之后在 [代码]project.miniapp.json[代码] 配置。 [图片] 启动图片 App 启动一般需要 2~4 秒,如果没有启动图片是白屏,用户会有点慌,不知道当前 App 是否正在启动,启动图片就是解决这个问题,同时在启动图片中传达 App 的价值主张。 我是直接用 Sketch 设计的,分辨率为 1290px x 2796px,这是兼容性最强的 6.7 寸(iPhone 15 Pro Max/15 Plus/14 Pro Max)手机的分辨率。 考虑到启动图片在不同机型上的兼容性,如果你用 Xcode 开发,苹果官方会推荐使用 Launch Screen Storyboard 隐私信息访问许可描述 小程序虽然没有用到摄像头、麦克风等权限,但多端的 SDK 中有(具体详见 Donut 官方文档 上架应用市场常见问题),所以得提前申明,不然把包通过 [代码]Transporter[代码] 上传后,会收到苹果发出的不合规邮件。 [图片] 以下是根据苹果官方打回的邮件中定义的隐私信息访问许可描述,应该是最基础的了,可以贴到你的 [代码]project.miniapp.json[代码] 文件中(用编辑器打开)。 [代码]{ "privateDescriptions": { "NSBluetoothPeripheralUsageDescription": "为了提供完整的功能,我们的应用程序需要访问蓝牙外设。这将用于与其他设备进行通信和数据交换。我们承诺保护用户隐私和数据安全。", "NSMicrophoneUsageDescription": "为了提供完整的功能,我们的应用程序需要访问麦克风。这将用于录制音频和进行语音交互。我们承诺保护用户隐私和数据安全。", "NSCalendarsUsageDescription": "为了提供完整的功能,我们的应用程序需要访问日历。这将用于提醒和日程管理。我们承诺保护用户隐私和数据安全。", "NSLocationAlwaysAndWhenInUseUsageDescription": "","NSBluetoothAlwaysUsageDescription":" 为了提供完整的功能,我们的应用程序需要始终访问蓝牙外设。这将用于与其他设备进行通信和数据交换。我们承诺保护用户隐私和数据安全。","NSPhotoLibraryUsageDescription":" 为了提供完整的功能,我们的应用程序需要始终访问相册。这将用于 IP 查询时显示 ISP 的图标。我们承诺保护用户隐私和数据安全。","NSCameraUsageDescription":" 为了提供完整的功能,我们的应用程序需要访问摄像头。这将用于录制视频。我们承诺保护用户隐私和数据安全。","NSLocationWhenInUseUsageDescription":" 为了提供完整的功能,我们的应用程序需要在使用时访问位置信息。这将用于提供定位服务和相关功能。我们承诺保护用户隐私和数据安全。" } } [代码] 2.7 构建正式版版本包 参照 打包生成 IPA 生成正式版的版本,注意使用分发证书。 [图片] 报错:file must be in miniprogram project 解决:把 mobileprovision 放在 miniprogram 目录下,因为 profile 不像 App icon 一样会自动上传到 miniprogram/ 目录下。 2.8 使用 Transporter 上传版本 参照 官方文档 上传正式版的 APK 包。 [图片] 遇到问题: Transporter,无法为 App “comxxxx.ipa” 创建临时 .itmsp 软件包。No suitable application records were found. Verify your bundle identifier ‘com.xxxx’ is correct and that you are signed into Xcode with an Apple ID that has access to the app in App Store Connect. [图片] 解决办法:去 App Store Connect 添加 App,绑定 [代码]Bundle id[代码],这样 Transporter 可以验证包在 App Store Connect 中已注册。 3. 使用 TestFlight 测试 在 App Store Connect 的 TestFlight 页面,可以选择内部、外部测试,外部测试版本需要 Apple 官方审核,把 公开链接发给朋友即可。 [图片] 在测试的同时,可以同步准备上架 App Store 的资料了。 4. 准备上架 Apple Store 审核资料 截屏 截屏是用来在 App Store 中显示你的 App 产品介绍页的,具体参照 截屏规范 [图片] 有 [代码]iPhone 15 Plus[代码] 和 [代码]iPhone 8 Plus[代码] 这两款机型就足够了,其他型号的手机能复用,分辨率应该是等比率缩放。 如果你像我一样,没有这两款手机,那用 iOS 模拟器。 Xcode -> 工具栏 Windows -> Devices and Simulators -> Create a new simulator -> Download more simulator runtimes [图片] 在微信开发者工具中运行这两款模拟器,利用模拟器自带截屏工具即可。 隐私政策 找一下常见 App 的隐私政策,在其产品介绍页中可以跳转过去。 如果你有网站就放在网站上,如果没有可以放在腾讯文档上。 [图片] 选择 App 供应的地区范围 哪些地区的用户可以下载你的 App。 [图片] 提交审核 一切准备好了后(包含备案),开启提交审核。 下午 5:35 提交审核,第二天早上 3:40 上架成功。✌🏻 [图片] 5. App Store 的数据 上架后刚好一周,看看最近一周的数据,还不错。 [图片] 这是评分数据 [图片] 6. 引流 二维码引流:草料二维码 通过草料二维码生成 App 的下载链接,放在网站上,引导用户跳转至 App。 Universal Links 参照 Apple 官网文章 Supporting associated domains 准备 Universal Links。 前面已经介绍了这个东东是干嘛的。 准备 [代码]apple-app-site-association[代码] 文件,放在网站的 [代码].well-known[代码] 目录下,完整路径为 [代码]/.well-known/apple-app-site-association[代码] 以下为示例,特别注意的是 [代码]appID[代码] 是由 [代码]团队 ID[代码] + [代码]Bundle ID[代码] 组成。 [代码]{ "applinks":{"apps":[], "details":[ { "appID":"<team_id>.<bundle_id>", "paths":["*"] } ] } } [代码] team_id 从 开发者账户 中获取 [图片] 顶部导航 当用户访问网站时,顶部引导用户跳转到 App 下载页。 等有空了搞搞。 7. 后记 小程序转 App,让个人或企业可以快速拥有 App,获取应用市场的流量,让开发者把精力放在业务逻辑上。 同时在开发小程序的过程中,发现开发者生态会散落在多个地方,比如 github,提供一些小程序模版、组件等能力,无法集中在一个地方比较方便的找到整个开发者生态的能力,和 VSCode 插件生态有点区别。 [图片] 先说 IDE 插件,比如我用 GPT4-Turbo 来写先代码或排查问题会在微信开发者工具和 Web 间跳转,操作流不太顺,如果能在微信开发者工具的插件入口中找到对应的 AI 代码助手,用起来应该很爽。 一旦平台的开放能力放出来,这些能力将源源不断的涌入到这个市场中,而不是作为平台方来集成这些能力,毕竟精力有限,同时还不一定做的最好,用插件可以让用户有更多的选择。 再说说 小程序组件,以大模型为例,目前市场有备案的大模型基座模型有好几家,在小程序开发过程中其实比较缺整体组件(UI + 背后的 API),有点像商场一样,平台方构建开放的能力,引导各个供应商提供开箱即用的能力,让用户可以快速上手,赶上这波大模型的技术趋势。 比如我自己在设计开放能力时的思考,平台专注骨架功能的开发,让开发者能参与到平台的建设中来,把生态盘活起来,最终提升大家研发运营的效率。 最后就是管理后端比较分散,比如 开放平台、donut、we 分析、云测、云托管,云开发,产品矩阵看不清,不容易知道整体的能力,缺少一个集中的控制台。 最后希望小程序越来越好 😄
2024-01-30 - 2023-12-12
- [填坑手册]小程序PC版来了,如何做PC端的兼容?!
[图片] 微信宣布小程序将可以在PC端微信打开后,智库君就接到要求,需要兼容PC端小程序,一开始以为官方已经做了完美适配,不需要改什么,但当本人下载内测版开始测试的时候,才发现或许坑还挺多的~~~ 下面分享下本人“搬砖填坑”的全过程: (以下都是PC端小程序特有的问题,手机端正常) 先说下使用流程 [图片] 微信开发者工具菜单栏点击 设置->通用设置,在自动预览部分勾选“启动 PC 端自动预览”。 使用自动预览功能,点击 预览->自动预览->编译并预览,成功的话将在微信 PC 版上自动拉起小程序。 [图片] PC版打开后就横屏问题 [图片] [代码]{ "pages": [], "resizable":false, //在这里设置false,使得小程序默认手机尺寸 "pageOrientation":"portrait", //这里默认设置即可 ... } [代码] PC版微信默认打开小程序是ipad版,这样就会出现各种形变,布局错乱,这个可以在app.json进行配置,静止自动旋转,默认手机竖屏样子打开。 页面找不到问题 [图片] 这个问题本人也找了很久,一直很纳闷IDE工具和手机打开看都没什么问题,用PC打开小程序就出现页面找不到的情况,大致报错是: [代码]page[pages/XXX/XXX] not found.May be caused by :1. Forgot to add page route in app.json.2. Invoking Page() in async task. [代码] 一般这种情况以往是 app.json没配,或者页面里面缺少page(),但这次诡异的地方是只有“PC版小程序”报这个错!后来分析问题发现是:目前PC版小程序不能直接支持ES6,必须转换成ES5,同时由于一些语法转化不够完善,特别是ES7中的await 和 async 导致转化二次报错,这里就需要打开 “增强编译” 配置。 [图片] 打开有CSS报错 [图片] 因为目前PC版小程序估计内核的机制问题,还只支持低版本的选择器,如果你直接写小程序的标签,它无法识别,比如 [代码].popCont navigator{ //navigator 标签是小程序里的,PC端无法支持 width: 560rpx; height: 300rpx; } .popCont image{ //image 标签是小程序里的,PC端无法支持 width: 560rpx; height: 300rpx; } [代码] 但这些写法,其实在手机小程序和IDE工具里是完全正常的,PC版需要做兼容,改成class选择器。 布局结构混乱 如果遇到这种情况,会检查一下是否使用屏幕尺寸(rpx)来计算布局,PC 上屏幕尺寸比窗口尺寸大,应该使用窗口尺寸来计算。 小程序如何判断是 PC 平台? 通过 getSystemInfo 官方接口(platform 是 windows) 通过 UA(PC UA 包含 MiniProgramEnv/Windows) 微信官方PC版小程序内测地址: https://dldir1.qq.com/weixin/Windows/WeChat2.7.0_beta.exe 最新官方IDE调试工具 https://developers.weixin.qq.com/miniprogram/dev/devtools/nightly.html 往期回顾: [打怪升级]小程序评论回复和发帖功能实战(二) [打怪升级]小程序评论回复和发贴组件实战(一) [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二) [填坑手册]小程序目录结构和component组件使用心得
2021-09-13 - [省钱小妙招]当业务起来,”云开发“费用起飞后如何省钱!
[图片] 很多小程序的早期开发者,为了快速起项目,会使用微信的云函数、云开发来作为后台的数据存储和交互,给我们提供非常多便利的同时,也需要关注一些问题: [图片] 假如这个项目火了!数据量一下子大上去后,套餐用完直接费用炸了~ 除了把后端迁移到xx云服务器外,想继续使用云开发的话,有什么好的控制成本的方案呢? 一、业务起来,云开发费用炸了 下面来看下我们遇到的问题: [图片] 1.1 超出套餐后,费用很贵 这里可以看到,套餐最高的级别是999的那个,我们其中一个小程序已经购买了最贵的套餐,现在就遇到这个问题,超出套餐的部分价格会变得很贵。 [图片] 1.2 云开发收费规则分析 [图片] 注意,这里要仔细研究下这个计费模式,你会发现核心是内存占用 云函数并发数:云函数的并发数量是指在任意指定时间对函数代码的执行数量。对于当前的 SCF 函数来说,每个发布的事件请求就会执行一次。因此,这些触发器发布的事件数(即请求量)会影响函数的并发数。 每秒请求量 x 函数执行时间(按秒) 例如,考虑一个处理存储事件的函数,假定函数平均用时0.2秒(即200毫秒),存储每秒发布300个请求至函数。这样将同时生产 300 * 0.2 = 60 个函数实例。 数据库同时连接数 :数据库请求并发数量,如同时有三十个数据库操作请求,则有二十个会同时执行,剩下十个返回超出并发错误;一次数据库请求(无论小程序端发起还是云函数端发起)将耗费一个连接;每个云环境分别有一个同时连接数限制、独立计数。 常驻云函数闲置量:计算公式: 闲置的常驻云函数数量 * 该云函数的配置内存 * 闲置时长 * 常驻云函数闲置量定价 假如数据库查询平均耗时 10ms,那么一个连接可以支持 100qps(1000ms/10ms=100),20个连接可以支持到 2000qps。 二、优化方案: 2.1 云函数操作优化 [图片] 一些需要增删改查的方案,尽可能放在一个云函数里实现,因为如果你修改后,再调用另外一个云函数查找结果,这样计算的时候,就算2个流量~ 2.2 云函数内存调整 如果你用的云函数没有非常复杂的功能,考虑到云函数费用计算的公式。 [图片] 可以把它的内存占用调整到最低档128MB,相比于默认的256MB,每次访问都能省一半内存,效果立竿见影,费用可以节省40%左右~ 2.3 数据静态化 如果你用到云数据库,这里就会有一个问题,你可以一个操作需要调用云函数的同时,还需要使用数据库的资源,一旦超过套餐费用可不低哦,所以你需要做的核心是 尽可能减少对云开发和运数据的使用,我们可以采用以下的优化方案: 如果你的数据是JSON或者不经修改的配置数据,可以使用云存储,这个费用低多了 如果你有一些静态页面,授权文件,或者静态数据,可以使用云主页 [图片] [图片] [图片] 三、官方费用计算网站 https://cloud.weixin.qq.com/cloudbase/price https://developers.weixin.qq.com/miniprogram/dev/wxcloud/billing/price.html 总结: 以上是针对你还是想 继续用微信云开发 的费用优化,对于早期开发项目的小伙伴,本人还是非常推荐使用的,毕竟开发成本是真的低,后期数据和流量上来了,再优化也来得及~
2023-11-30 - 一个新的小程序日历
两年前因为项目需求的原因,设计了一个日历组件,两年后小程序接口渐渐丰富起来,bug也积累起来,我,重构了。 1 新的交互设计 - 借鉴了MIUI的系统日历设计 [图片] 2 支持webview和skyline渲染,在skyline的加持下,更丝滑了 3 支持darkmode,可以跟随系统更改深浅模式 特意找了个夜晚时段,试了下 [视频] 4 支持扩展插件,自带农历插件,更多插件计划中 5 喜欢给个star~,github - https://github.com/lspriv/wx-calendar/tree/develop
2023-10-30 - 小程序海报绘制方案(原生,Uniapp,Taro)
背景 小程序海报绘制方案有很多,但是大多数都是基于canvas的,而且都是自己封装的,不够通用,不够灵活,不够简单,不够好用。 本方使用一个开源的小程序海报绘制,非常灵活,扩展性非常好,仅布局就能得到一张海报。 准备工作 安装依赖,也可以把源码下载到本地,源码地址。 [代码]npm install wxml2canvas [代码] 布局 无论哪种方案,布局都是一致的,需要注意一些暂未支持的属性: 变形:transform,但是节点元素使能读取此属性,但是库不支持,所以不要使用 圆角,border-radius,同上,不要使用,圆形图片有特定的属性去实现,除此之外无法实现其他类型的圆角 布局示例: 注意,除了uniapp,原生和Taro要使用原生组件的方式绘制canvas,因为Taro不支持data-xx的属性绑定方式,这一点很糟糕 [代码]<!-- 外层wrap用于fixed定位,使得整个布局离屏,离屏canvas暂未支持 --> <view class='wrap'> <!-- canvas id,一会 new 的时候需要 --> <canvas canvas-id="poster-canvas"></canvas> <view class="container"> <view data-type="text" data-text="测试文字绘制" class='text'>测试文字绘制</view> <image data-type="image" data-src="https://img.yzcdn.cn/vant/cat.jpeg" class='image'></image> <image data-type="radius-image" data-src="https://img.yzcdn.cn/vant/cat.jpeg" class='radius-image'></image> </view> </view> [代码] 原生小程序 [代码]import Wxml2Canvas from 'wxml2canvas' Component({ methods: { paint() { wx.showLoading({ title: '生成海报' }); // 创建绘制实例 const drawInstance = new Wxml2canvas({ // 组件的this指向,组件内使用必传 obj: this, // 画布宽高 width: 275, height: 441, // canvas-id element: 'poster-canvas', // 画布背景色 background: '#f0f0f0', // 成功回调 finish: (url) => { console.log('生成的海报url,开发者工具点击可预览', url); wx.hideLoading(); }, // 失败回调 error: (err) => { console.error(err); wx.hideLoading(); }, }); // 节点数据 const data = { list: [ { // 此方式固定 wxml type: 'wxml', class: '.text', // draw_canvas指定待绘制的元素 limit: '.container', // 限定绘制元素的范围,取指定元素与它的相对位置计算 } { // 此方式固定 wxml type: 'wxml', class: '.image', // draw_canvas指定待绘制的元素 limit: '.container', // 限定绘制元素的范围,取指定元素与它的相对位置计算 } { // 此方式固定 wxml type: 'wxml', class: '.radius-image', // draw_canvas指定待绘制的元素 limit: '.container', // 限定绘制元素的范围,取指定元素与它的相对位置计算 } ] } // 调用绘制方法 drawInstance.draw(data); } } }) [代码] Uniapp uniapp 主要讲Vue3的版本,因为涉及 this,需要获取 this 以及时机 [代码]import { getCurrentInstance} from 'vue'; // 调用时机 setup内,不能在其他时机 // @see https://github.com/dcloudio/uni-app/issues/3174 const instance = getCurrentInstance(); function paint() { uni.showLoading({ title: '生成海报' }); const drawInstance = new Wxml2Canvas({ width: 290, // 宽, 以iphone6为基准,传具体数值,其他机型自动适配 height: 430, // 高 element: 'poster-canvas', // canvas-id background: '#f0f0f0', obj: instance, finish(url: string) { console.log('生成的海报url,开发者工具点击可预览', url); uni.hideLoading(); }, error(err: Error) { console.error(err); uni.hideLoading(); }, }); // 节点数据 const data = { list: [ { // 此方式固定 wxml type: 'wxml', class: '.text', // draw_canvas指定待绘制的元素 } { // 此方式固定 wxml type: 'wxml', class: '.image', // draw_canvas指定待绘制的元素 } { // 此方式固定 wxml type: 'wxml', class: '.radius-image', // draw_canvas指定待绘制的元素 } ] } // 调用绘制方法 drawInstance.draw(data); } [代码] Taro Taro 比较特殊,框架层面的设计缺陷导致了 Taro 组件增加的 [代码]data-xx[代码] 属性在编译的时候是会清除的,因此Taro要使用这个库要用原生小程序的方式编写组件。 代码和原生的一样,只是要用原生的方式编写组件,然后在 Taro 中使用。 参考原生的代码,原生小程序js参考这 假设原生组件名为 [代码]draw-poster[代码],那么首先需要再Taro的页面中引入这个组件,然后在页面中使用这个组件,然后在组件中使用这个库。 [代码]export default { navigationBarTitleText: '', usingComponents: { 'draw-poster': '../../components/draw-poster/index', }, }; [代码] [代码] const draw = useCallback(() => { const { page } = Taro.getCurrentInstance(); // 拿到目标组件实例调用里面的方法 const instance = page!.selectComponent('#draw_poster'); // 调用原生组件绘制方法 instance.paint(); }, []); return <draw-poster id="draw_poster"/> [代码] 总结 对比原生的canvas绘制方案,布局的方式获取节点的方式都是一样的,只是绘制的时候不一样,原生的是直接绘制到canvas上,而这个库是先把布局转换成canvas,然后再绘制到canvas上,所以这个库的性能会比原生的差一些,但是这个库的优势在于布局的方式,不需要自己去计算位置,只需要布局,然后调用绘制方法就可以了,非常方便,而且扩展性非常好,可以自己扩展一些布局方式,比如说flex布局,grid布局等等,这些都是可以的,只需要在布局的时候把布局转换成canvas的布局就可以了,这个库的布局方式是参考的微信小程序的布局方式,所以布局的时候可以参考微信小程序的布局方式,这样就可以很方便的布局了。
2023-10-31 - 小程序web-worker实践
大家好,我,阿盟 Web Workers 提供了在后台线程中执行计算密集型任务的能力,有助于充分利用多核处理器的计算能力,微信小程序也是在基础库 v2.18.1 开始支持在插件内使用 worker,但与浏览器对比,还是有显著的一些差异,接下来我们就来介绍并实践操作一下 1. 跨域限制:在小程序中,Web Workers 通常不允许跨域加载脚本。这意味着 Web Workers 只能加载与主线程代码在同一域下或其子域下的脚本文件,而不像浏览器中的 Web Workers 可以加载来自不同域的脚本。这是为了维护小程序的安全性和防止跨域问题。 2. 本地资源限制:小程序的 Web Workers 受到资源下载的限制,例如限制了 HTTP 请求的能力,因此在 Web Workers 中进行网络请求可能受到限制。 3. WebSocket 限制:小程序中的 Web Workers 通常不支持直接创建 WebSocket 连接,这与浏览器中的 Web Workers 不同。WebSocket 连接通常需要在主线程中创建和管理。 4. IndexedDB 限制:小程序中的 Web Workers 通常不支持 IndexedDB 数据库的访问,这是因为小程序环境对存储访问有一些限制。 5. 并发限制:浏览器通常允许多个 Web Workers 同时运行,但具体的线程数限制取决于浏览器的实现。通常,现代浏览器支持多个 Web Workers,可能在不同核心上并行执行它们, 但在小程序中,Worker 最大并发数量限制为 1 个,创建下一个前请用 Worker.terminate() 结束当前 Worker // 示例代码 // 1. 配置 Worker 信息 { "workers": "workers" // 如采用分包模式 "isSubpackage": true } // 微信小程序中使用 Web Workers const worker = wx.createWorker('worker.js'); // 监听从 Web Worker 返回的消息 worker.onMessage(function (res) { console.log('Received message from Web Worker:', res); }); // 向 Web Worker 发送消息 worker.postMessage({ data: 'Hello from main thread' }); // 在 Web Worker 文件 worker.js 中定义 Worker 的逻辑 // worker.js onmessage = function (e) { console.log('Received message in Web Worker:', e.data); postMessage({ result: 'Hello from Web Worker' }); }; // 从基础库 v2.27.3 开始,如果 worker 代码配置为了分包,也就是配置了isSubPackage为true,则需要先通过 wx.preDownloadSubpackage 接口下载好 worker 代码,再初始化 Worker var task = wx.preDownloadSubpackage({ packageType: "workers", success(res) { console.log("load worker success", res) var worker = wx.createWorker("workers/request/index.js") // 创建 worker。 如果 worker 分包没下载完就调 createWorker 的话将报错 }, fail(res) { console.log("load worker fail", res) } }) task.onProgressUpdate(res => { console.log(res.progress) // 可通过 onProgressUpdate 接口监听下载进度 console.log(res.totalBytesWritten) console.log(res.totalBytesExpectedToWrite) })
2023-10-19 - 微信小程序深度合成类目如何通过?
前言 在8月15日当天我分享了一篇《微信小程序深度合成类目资质如何准备?》,发完文章后有很多朋友私信我,发现有很人和我遇到了同样的问题,这个问题我昨天已经解决了,接下来分享下具体流程和材料。 [图片] 准备材料 最开始需要《安全评估报告》现在已经不需要了,最新的只有自己的AI大模型算法备案或者合作的AI达模型算法备案+合作协议。 第一种方式 在《微信小程序深度合成类目资质如何准备?》这篇有人留言(我没有询问过待办机构,我用的是合作的方式)。 [图片] 不建议这种方式,费用虽然不多,但是时间太久了,等办下来可能产品竞争力就没了。 第二种方式 算法备案证明截图 找市面上已经有AI模型算法备案的公司合作,比如:通义千问(阿里巴巴)、文心一言(百度)、讯飞星火(科大讯飞)等,那么怎么知道公司有没有算法备案?直接在「互联网信息服务算法备案系统」通过公司名称查找。 如:百度 [图片] 我们可以看到这里面就可以查看算法为「生成合成类」的算法备案信息为「正常」状态。 合作协议 确定有算法备案后找相关产品的商户去谈合作,看具体条件和签合同流程(在这里我不做任何公司的推荐,大家根据自身业务需求去体验下已备案的产品,然后再做判断) 在这里要注意合同中要包含算法备案证明截图中的【算法名称】双方公司都需要盖公章。 最后 当然具体你要用什么方式根据自己的实际情况决定,我就是用的第二种方式提供这两个材料就申请通过,最后祝大家都申请通过,产品顺利上线!
2023-08-31 - 基于小程序请求接口 wx.request 封装的类 axios 请求
Introduction wx.request 的配置、axios 的调用方式 ----------------源码戳我--------------- ---------------demo 戳我-------------- feature 支持 wx.request 所有配置项 支持 axios 调用方式 支持 自定义 baseUrl 支持 自定义响应状态码对应 resolve 或 reject 状态 支持 对响应(resolve/reject)分别做统一的额外处理 支持 转换请求数据和响应数据 支持 请求缓存(内存或本地缓存),可设置缓存标记、过期时间 use app.js @onLaunch [代码] import axios form 'axios' axios.creat({ header: { content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }, baseUrl: 'https://api.baseurl.com', ... }); [代码] page.js [代码]axios .post("/url", { id: 123 }) .then((res) => { console.log(response); }) .catch((err) => { console.log(err); }); [代码] API [代码] axios(config) - 默认get axios(url[, config]) - 默认get axios.get(url[, config]) axios.post(url[, data[, config]]) axios.cache(url[, data[, config]]) - 缓存请求(内存) axios.cache.storage(url[, data[, config]]) - 缓存请求(内存 & local storage) axios.creat(config) - 初始化定制配置,覆盖默认配置 [代码] config 默认配置项说明 [代码]export default { // 请求接口地址 url: undefined, // 请求的参数 data: {}, // 请求的 header header: "application/json", // 超时时间,单位为毫秒 timeout: undefined, // HTTP 请求方法 method: "GET", // 返回的数据格式 dataType: "json", // 响应的数据类型 responseType: "text", // 开启 http2 enableHttp2: false, // 开启 quic enableQuic: false, // 开启 cache enableCache: false, /** 以上为wx.request的可配置项,参考 https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html */ /** 以下为wx.request没有的新增配置项 */ // {String} baseURL` 将自动加在 `url` 前面,可以通过设置一个 `baseURL` 便于传递相对 URL baseUrl: "", // {Function} (同axios的validateStatus)定义对于给定的HTTP 响应状态码是 resolve 或 reject promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 reject validateStatus: undefined, // {Function} 请求参数包裹(类似axios的transformRequest),通过它可统一补充请求参数需要的额外信息(appInfo/pageInfo/场景值...),需return data transformRequest: undefined, // {Function} resolve状态下响应数据包裹(类似axios的transformResponse),通过它可统一处理响应数据,需return res transformResponse: undefined, // {Function} resolve状态包裹,通过它可做接口resolve状态的统一处理 resolveWrap: undefined, // {Function} reject状态包裹,通过它可做接口reject状态的统一处理 rejectWrap: undefined, // {Boolean} _config.useCache 是否开启缓存 useCache: false, // {String} _config.cacheName 缓存唯一key值,默认使用url&data生成 cacheName: undefined, // {Boolean} _config.cacheStorage 是否开启本地缓存 cacheStorage: false, // {Any} _config.cacheLabel 缓存标志,请求前会对比该标志是否变化来决定是否使用缓存,可用useCache替代 cacheLabel: undefined, // {Number} _config.cacheExpireTime 缓存时长,计算缓存过期时间,单位-秒 cacheExpireTime: undefined, }; [代码] 实现 axios.js [代码]import Axios from "./axios.class.js"; // 创建axios实例 const axiosInstance = new Axios(); // 获取基础请求axios const { axios } = axiosInstance; // 将实例的方法bind到基础请求axios上,达到支持请求别名的目的 axios.creat = axiosInstance.creat.bind(axiosInstance); axios.get = axiosInstance.get.bind(axiosInstance); axios.post = axiosInstance.post.bind(axiosInstance); axios.cache = axiosInstance.cache.bind(axiosInstance); axios.cache.storage = axiosInstance.storage.bind(axiosInstance); [代码] Axios class 初始化 defaultConfig 默认配置,即 defaults.js axios.creat 用户配置覆盖默认配置 注意配置初始化后 mergeConfig 不能被污染,config 需通过参数传递 [代码]constructor(config = defaults) { this.defaultConfig = config; } creat(_config = {}) { this.defaultConfig = mergeConfig(this.defaultConfig, _config); } [代码] 请求别名 axios 兼容 axios(config) 或 axios(url[, config]); 别名都只是 config 合并,最终都通过 axios.requst()发起请求; [代码] axios($1 = {}, $2 = {}) { let config = $1; // 兼容axios(url[, config])方式 if (typeof $1 === 'string') { config = $2; config.url = $1; } return this.request(config); } post(url, data = {}, _config = {}) { const config = { ..._config, url, data, method: 'POST', }; return this.request(config); } [代码] 请求方法 _request 请求配置预处理 实现 baseUrl 实现 transformRequest(转换请求数据) [代码] _request(_config = {}) { let config = mergeConfig(this.defaultConfig, _config); const { baseUrl, url, header, data = {}, transformRequest } = config; const computedConfig = { header: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', ...header, }, ...(baseUrl && { url: combineUrl(url, baseUrl), }), ...(transformRequest && typeof transformRequest === 'function' && { data: transformRequest(data), }), }; config = mergeConfig(config, computedConfig); return wxRequest(config); } [代码] wx.request 发起请求、处理响应 实现 validateStatus(状态码映射 resolve) 实现 transformResponse(转换响应数据) 实现 resolveWrap、rejectWrap(响应状态处理) [代码]export default function wxRequest(config) { return new Promise((resolve, reject) => { wx.request({ ...config, success(res) { const { resolveWrap, rejectWrap, transformResponse, validateStatus, } = config; if ((validateStatus && validateStatus(res)) || ifSuccess(res)) { const _resolve = resolveWrap ? resolveWrap(res) : res; return resolve( transformResponse ? transformResponse(_resolve) : _resolve ); } return reject(rejectWrap ? rejectWrap(res) : res); }, fail(res) { const { rejectWrap } = config; reject(rejectWrap ? rejectWrap(res) : res); }, }); }); } [代码] 请求缓存的实现 默认使用内存缓存,可配置使用 localStorage 封装了 Storage 与 Buffer 类,与 Map 接口一致:get/set/delete 支持缓存标记&过期时间 缓存唯一 key 值,默认使用 url&data 生成,无需指定 [代码] import Buffer from '../utils/cache/Buffer'; import Storage from '../utils/cache/Storage'; import StorageMap from '../utils/cache/StorageMap'; /** * 请求缓存api,缓存于本地缓存中 */ storage(url, data = {}, _config = {}) { const config = { ..._config, url, data, method: 'POST', cacheStorage: true, }; return this._cache(config); } /** * 请求缓存 * @param {Object} _config 配置 * @param {Boolean} _config.useCache 是否开启缓存 * @param {String} _config.cacheName 缓存唯一key值,默认使用url&data生成 * @param {Boolean} _config.cacheStorage 是否开启本地缓存 * @param {Any} _config.cacheLabel 缓存标志,请求前会对比该标志是否变化来决定是否使用缓存,可用useCache替代 * @param {Number} _config.cacheExpireTime 缓存时长,计算缓存过期时间,单位-秒 */ _cache(_config) { const { url = '', data = {}, useCache = true, cacheName: _cacheName, cacheStorage, cacheLabel, cacheExpireTime, } = _config; const computedCacheName = _cacheName || `${url}#${JSON.stringify(data)}`; const cacheName = StorageMap.getCacheName(computedCacheName); // return buffer if (useCache && Buffer.has(cacheName, cacheLabel)) { return Buffer.get(cacheName); } // return storage if (useCache && cacheStorage) { if (Storage.has(cacheName, cacheLabel)) { const data = Storage.get(cacheName); // storage => buffer Buffer.set( cacheName, Promise.resolve(data), cacheExpireTime, cacheLabel ); return Promise.resolve(data); } } const curPromise = new Promise((resolve, reject) => { const handleFunc = (res) => { // do storage if (useCache && cacheStorage) { Storage.set(cacheName, res, cacheExpireTime, cacheLabel); } return res; }; this._request(_config) .then((res) => { resolve(handleFunc(res)); }) .catch(reject); }); // do buffer Buffer.set(cacheName, curPromise, cacheExpireTime, cacheLabel); return curPromise; } [代码]
2020-07-03 - 初步上手小程序笔记1:单独封装axios请求及测试获得文章数据
小程序自身有单独请求的api,并且已经解决了跨域问题,可以不需要重新配置一下。 这里贴下小程序的请求api代码 [图片] 新建了一个axios.js文件用于封装axios //封装axios请求 class Axios { post(url, data) { return this.request("POST", url, data) } get(url, data) { return this.request("GET", url, data) } put(url, data) { return this.request("PUT", url, data) } update(url, data) { return this.request("UPDATE", url, data) } detete(url, data) { return this.request("DELETE", url, data) } request(method, url, data) { return new Promise((resolve, reject) => { wx.request({ url: base_url+url, method:method, data:data, header: { "content-type": 'application/json'//默认值 }, success(res) { console.log(res.data);//打印返回的数据 resolve(res.data) }, fail(err) { reject(err) } }) }) } } module.exports = new Axios();//将其暴露出去 这里小程序的暴露方法和react用的export default有所不同,稍微记录一下。 然后新建文件夹,这里我命名为servies,专门用于存放发送请求的js文件,这里新建一个article.js文件,专门用于存放请求文章相关数据的请求 // 请求数据部分 const axios=require("../utils/axios")//引入axios module.exports={//将请求暴露出去 //获取首页文章信息 getHomeArticle(url,data){ return axios.get(url,{data:data}) } } 在pages下的index中的index.js引入 const {getHomeArticle} =require("../../servies/article.js"); 随后进行引用 onReady: function () { getHomeArticle(`/article/queryArticleListByNewTime2/FRONTEND/1/10/4`,{}); }, 测试的请求接口部分 [图片] 最后编译运行,在控制台可查看请求结果 [图片] 其实一开始封装完axios请求后测试请求数据时会出现请求地址不在request合法域名列表中,后来查看文档发现需要进行服务器域名的配置,但在小程序工具里找了好久没有发现服务器域名配置,就先采用了一个简单粗暴的方式: [图片] 这里在小程序开发工具点击详情部分,勾选如图所示选项 [图片] 但这并不是长久之计,之后还是需要去配置服务器域名,顺便想问下社区的大佬这个配置在哪里我好像一直都找不到😂
2021-11-12 - 小程序前后端交互使用JWT
前言 现在很多Web项目都是前后端分离的形式,现在浏览器的功能也是越来越强大,基本上大部分主流的浏览器都有调试模式,也有很多抓包工具,可以很轻松的看到前端请求的URL和发送的数据信息。如果不增加安全验证的话,这种形式的前后端交互时候是很不安全的。 相信很多开发小程序的开发者也不一定都是大神,能够精通前后端,作为小程序的初学者不少人也是根据官方的文档去学习开发的。我自己最开始接触小程序也是从wafer2开始的,那时候腾讯云提供的SDK包含PHP和Node.js,因为对于一直做前端的人来说,Node.js的学习成本比较低,只要会JS基本能看懂,也是从那时候才开始接触Node.js,所以本文主要是基于wafer2的服务端基于Koa2的后端来说(其实这个不重要,Node.js基本都差不多)。 什么是JWT? 根据维基百科的定义,JSON WEB Token,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。 为什么使用JWT? 首先,这不是一个必选方案。有时候我们的API是其它服务端和小程序公用的,那么就涉及到安全验证的问题了。 微信官方不鼓励小程序一打开就要求必须登陆的方式去获取用户信息,因此我们也不能去校验这个用户是否有权限访问这个接口,但是有的接口又不能让任何人随便去看或者被随意采集。 基于token(令牌)的用户认证 用户输入其登录信息 服务器验证信息是否正确,并返回已签名的token token储在客户端,例如存在local storage或cookie中 之后的HTTP请求都将token添加到请求头里 服务器解码JWT,并且如果令牌有效,则接受请求 一旦用户注销,令牌将在客户端被销毁,不需要与服务器进行交互一个关键是,令牌是无状态的。后端服务器不需要保存令牌或当前session的记录。 关于JWT的详细介绍网上有很多,这里也就不说了,下面介绍在Koa2框架里的添加方法。 安装依赖 [代码]npm install jsonwebtoken npm install koa-jwt [代码] app.js 引用 [代码]const jwtKoa = require('koa-jwt'); [代码] 设置不需要JWT验证的目录或者文件 [代码]const secret = '设置密钥'; app.use(jwtKoa({secret}).unless({ path: ['/','\/favicon.ico',/^\demo/] })) [代码] 数组中的路径不需要通过jwt验证。 授权 小程序 wx.request 发送网络请求的 referer header 不可设置。 其格式固定为 https://servicewechat.com/{appid}/{version}/page-frame.html,其中 {appid} 为小程序的 appid,{version} 为小程序的版本号,版本号为 0 表示为开发版、体验版以及审核版本,版本号为 devtools 表示为开发者工具,其余为正式版本。 那么我们就可以根据 ctx.header 里的 referer 进行初步的限制,比如指定的 appid 才能生成令牌。 我们在生成令牌的时候可以把简单的信息加入进去,如: [代码]const userToken = { referer: refererArray[2], appid: refererArray[3], version: refererArray[4], data: '此处可传入用户的信息' } [代码] 生成令牌: [代码]const jwt = require('jsonwebtoken'); const secret = '设置密钥'; jwt.sign(userToken, secret, {expiresIn: '2h'}); [代码] expiresIn:为令牌的有效期 这样简单的JWT令牌就生成好了,再通过接口返回给小程序端。 小程序前端如何使用JWT? 很简单,在header里加入下面属性即可。 [代码]authorization: 'Bearer 获取到的令牌' [代码] JWT优点 可扩展性好 应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而JWT不需要。 无状态 JWT不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外JWT的载荷中可以存储一些常用信息,用于交换信息,有效地使用JWT,可以降低服务器查询数据库的次数。 JWT缺点 安全性 由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。 性能 JWT太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用JWT的http请求比使用session的开销大得多。 一次性 无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT。 (1)无法废弃 通过上面JWT的验证机制可以看出来,一旦签发一个 JWT,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的JWT还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的JWT,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。 (2)续签 如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变JWT的有效时间,就要签发新的JWT。最简单的一种方式是每次请求刷新JWT,即每个http请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。另一种方法是在redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间。
2019-02-20 - 给大伙儿开发一个记账小程序 - 小记账本
项目前端小程序二维码: [图片] 简介: 记录个人,家庭等财务收支情况,可免费导出收支明细,与家人好友共享账本,让记账变得更简单 我的个人blog网站:https://www.zhooson.cn/ 里面其他全栈项目开源Github地址 1. 技术: 前端:uniapp(vue3) 后端:egg node:16.5.0 数据库:mysql 工具:HbuilerX filezilla pm2 Termius等 2. 整体项目结构 [图片] 3. uniapp 具体的开发文档:https://uniapp.dcloud.net.cn/ 技术选型:uniapp以前没有使用过,这次决定尝试一次。 使用感觉,感觉不咋好,也许我是了解的不够全面,我每次小程序开发工具添加新的编译模式,重新打包后就没有了,这一点软件默认设置不太友好。 uniapp仔细阅读文档即可,本文不做详细讲解。 4. egg 1. 初始化的项目的老掉牙的命令自行查看文档:https://www.eggjs.org/zh-CN/intro/quickstart 2. [代码]jwt[代码]使用 安装 [代码] yarn add egg-jwt [代码] 配置 [代码]// {app_root}/config/plugin.js exports.jwt = { enable: true, package: "egg-jwt" }; [代码] [代码]// {app_root}/config/config.default.js exports.jwt = { secret: "123456" }; [代码] 使用 [代码]// {app_root}/app/controller/user.js //签发 token 数据 ... let result = await service.user.query({ openId }); const token = app.jwt.sign( { nickname: result.openId, userId: result.id, exp: Math.floor(Date.now() / 1000) + 60 * 60, // 1h }, app.config.jwt.secret ); [代码] [代码]// {app_root}/app/router.js module.exports = (app) => { const { router, controller, jwt } = app; /** * 用户 */ router.post('/api/user/login', controller.user.login); router.post('/api/user/update', jwt, controller.user.update); router.get('/api/user/list', jwt, controller.user.list); } [代码] 3. 获取微信小程序用户openId(前端只需传递code) [代码]// {app_root}/app/service/tool.js 'use strict'; const Service = require('egg').Service; const axios = require('axios'); class ToolService extends Service { // wx 相关操作 async decodeWXByCode({ code }) { return new Promise((resolve, reject) => { const { ctx, app } = this; const { AppSecret, AppID } = app.config.wx; axios .get( `https://api.weixin.qq.com/sns/jscode2session?appid=${AppID}&secret=${AppSecret}&js_code=${code}&grant_type=authorization_code` ) .then((res) => { // console.log('decodeWXByCode', res.data); if (res.data.errcode === 40029) { resolve({ status: 201, message: '无效code' }); } else if (res.data.errcode === 40163) { resolve({ status: 201, message: 'code被使用' }); } else if (res.data.session_key && res.data.openid) { resolve({ status: 200, message: '获取成功', data: { openid: res.data.openid, }, }); } else { resolve({ status: 201, message: '未知错误' }); } }); }); } } module.exports = ToolService; [代码] 4. 查询首页数据service, 分级查询 [图片] [图片] [代码] async list({ openId, plus = 0, year, month, name_id }) { // console.log('2023-2-1', openId, plus, plus === 0, year, month, day); // let w = `where 1=1`; // let a = `where 1=1`; // if (openId) { // w += ` and b.openId = '${openId}'`; // a += ` and b.openId = '${openId}'`; // } // if (year) { // w += ` and year = ${year}`; // a += ` and year = ${year}`; // } // if (month) { // w += ` and month = ${month}`; // a += ` and month = ${month}`; // } // // if (day) { // // a += ` and day = ${day}`; // // } // if (+plus) { // w += ` and plus = ${plus}`; // } // 本月 支出 + 收入 = 总和 // const sumSql = `select sum(price) from book b ${a}`; // const MonthCount = await this.app.mysql.query(sumSql); // 本月 支出 const outSql = `select sum(price) from book where name_id = ${name_id} and year = ${year} and month = ${month} and plus = 1 and disabled = 0`; const MonthOutCount = await this.app.mysql.query(outSql); // 本月 收入 const inSql = `select sum(price) from book b where name_id = ${name_id} and year = ${year} and month = ${month} and plus = 2 and disabled = 0`; const MonthInCount = await this.app.mysql.query(inSql); // 当月 所有明细 // const sql = `select b.*, u.nickname, u.avatar, i.title icon_title from book b inner join user u on b.openId = u.openId inner join cate i on b.cate_id = i.id ${w} group day order by create_time desc`; const sql = `select distinct day, month, year from book where name_id = ${name_id} and year = ${year} and month = ${month} and disabled = 0 order by day desc`; let days = await this.app.mysql.query(sql); // const detailSql = `select * from book where openId = '${openId}' and year = ${year} and month = ${month}`; // const detailSql = `select b.*, u.nickname, u.avatar, i.title icon_title from book b inner join user u on b.openId = u.openId inner join cate i on b.cate_id = i.id where openId = '${openId}' and year = ${year} and month = ${month} order by create_time desc`; let n = '1 = 1 and disabled = 0'; if (+plus) { n += ` and b.plus = ${plus}`; } for (let val of days) { val.date = `${val.year}-${val.month}-${val.day}`; val.items = []; val.items = await this.app.mysql.query( `select b.*, u.nickname, u.avatar, c.title icon_title from book b inner join user u on b.openId = u.openId inner join cate c on b.cate_id = c.id where ${n} and b.name_id = ${name_id} and year = ${year} and month = ${month} and day = ${val.day} order by create_time desc` ); } } [代码] 我的sql语法不太完美,请大神提出宝贵意见。 5. 导出数据 excle表格, 可根据自己的需求导出想要的类目。 [图片] [代码] // 导出 async export() { const { ctx, service } = this; try { let query = ctx.request.query.code // 当前code需要解密,需要自己的制定自己的解密规则 console.log('search-query', query); query = JSON.parse(query); let list = await service.book.search(query); const bookDetail = await service.name.query({ id: query.name_id }); let xls = [[]]; xls[0] = ['方式', '金额', '创建人', '账本', '类别', '时间', '备注']; for (let index = 0; index < list.length; index++) { const element = list[index]; xls[index + 1] = [ element.plus === 1 ? '支出' : '收入', element.price, element.nickname, element.name_title, element.cate_title, element.year + '/' + element.month + '/' + element.day, element.remark, ]; } // console.log('xls', xls); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(xls), '账本'); const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }); const filename = encodeURIComponent( `${bookDetail.title}_${query.year}_${query.month}月账本` ); // 设置header关键代码 ctx.set('Content-Disposition', `attachment; filename="${filename}.xlsx"`); ctx.set('Content-Type', 'application/vnd.ms-excel'); ctx.status = 200; ctx.body = buf; // cb(ctx, 200, 200, '导出成功', list); } catch (err) { cb(ctx, 200, 422, '导出失败', JOSN.stringify(err)); } } [代码] excle: [图片] 6. 上传文件 [代码]用户头像和cover图[代码],可动态生成文件夹目录。 [代码]'use strict'; const fs = require('fs'); const path = require('path'); const mkdirp = require('mkdirp'); const { cb, formatDate } = require('../../utils'); // 生成新的文件名称 function getUploadFileExt(name) { let ext = name.split('.'); let last = formatDate(new Date(), 'YYYYMMDDhhmmssms'); return `${last}.${ext[ext.length - 1]}`; } const Controller = require('egg').Controller; class UploadController extends Controller { async file() { const { ctx } = this; try { // 1. 获取文件流 const file = ctx.request.files[0]; // console.log(33, file); // 2. 生成filename const name = getUploadFileExt(file.filename); // console.log('name', name); // 3. 获取bucket ps: demo 或者 demo/test 或者 demo/test/cd const { bucket = 'avatar' } = ctx.request.body; // 4. 生成文件夹 const dir = path.join(__dirname, `../public/images/${bucket}`); // console.log('dir', dir); await mkdirp(dir); // 5. 文件流读取/写入 const filePath = `${dir}/${name}`; let readStream = fs.createReadStream(file.filepath); var writeStream = fs.createWriteStream(filePath); readStream.pipe(writeStream); readStream.on('end', function () { fs.unlinkSync(file.filepath); }); cb(ctx, 200, 200, '上传成功', { url: `http://${ctx.request.header.host}/public/images/${bucket}/${name}`, }); } catch (err) { cb(ctx, 200, 500, '上传失败!', JSON.stringify(err)); } } } module.exports = UploadController; [代码] 7. 好友共享账本 [图片] [图片] 8. 具体的数据表设计展示如下 账本表 [图片] 用户表 [图片] 5. 博客 我的个人blog网站:https://www.zhooson.cn/ 有其他前后端项目代码已开源。
2023-03-16 - fail api scope is not declared in the privacy agreement
fail api scope is not declared in the privacy agreement,api 范围未在隐私协议中声明 建议大家更具公告,更新对应的隐私协议 https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=11691660367cfUvX&version=&lang=zh_CN&token= 登录mp后台,设置, [图片] [图片] [图片] 完善并提交信息, 注意:更新好隐私协议,要通过审核的,接口才能正常访问 正确处理隐私弹窗逻辑 https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html demo1: 演示使用 [代码]wx.getPrivacySetting[代码] 和 [代码]<button open-type="agreePrivacyAuthorization">[代码] 在首页处理隐私弹窗逻辑 https://developers.weixin.qq.com/s/gi71sGm67hK0 demo2: 演示使用 [代码]wx.onNeedPrivacyAuthorization[代码] 和 [代码]<button open-type="agreePrivacyAuthorization">[代码] 在多个页面处理隐私弹窗逻辑,同时演示了如何处理多个隐私接口同时调用。 https://developers.weixin.qq.com/s/hndZUOmA7gKn demo3: 演示 [代码]wx.onNeedPrivacyAuthorization[代码]、[代码]wx.requirePrivacyAuthorize[代码]、[代码]<button open-type="agreePrivacyAuthorization">[代码] 和 [代码]<input type="nickname">[代码] 组件如何结合使用 https://developers.weixin.qq.com/s/jX7xWGmA7UKa demo4: 演示使用 [代码]wx.onNeedPrivacyAuthorization[代码] 和 [代码]<button open-type="agreePrivacyAuthorization">[代码] 在多个 tabBar 页面处理隐私弹窗逻辑 https://developers.weixin.qq.com/s/g6BWZGmt7XK9 常见错误 [代码]{ "errMsg": "A:fail api scope is not declared in the privacy agreement", "errno": 112 }[代码] 使用到了 A 隐私接口,但是开发者未在[mp后台-设置-服务内容声明-用户隐私保护指引]中声明收集 A 接口对应的隐私类型。 [图片] 在审核提交时候,选择采集用户隐私 [图片] 在js上需要配合配置用户隐私授权弹窗 微信提供了wx.onNeedPrivacyAuthorization(function callback) 接口,意为用户触发了一个微信侧未记录过同意的隐私接口调用,开发者可通过响应该事件选择提示用户的时机。此外,微信还提供了 wx.requirePrivacyAuthorize(Object object) 接口,可用于模拟触发 onNeedPrivacyAuthorization 事件。 2023.08.22更新: 以下指南中涉及的 getPrivacySetting、onNeedPrivacyAuthorization、requirePrivacyAuthorize 等接口目前可以正常接入调试。调试说明: 在 2023年9月15日之前,在 app.json 中配置 [代码]__usePrivacyCheck__: true[代码] 后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。在 2023年9月15日之后,不论 app.json 中是否有配置 [代码]__usePrivacyCheck__[代码],隐私相关功能都会启用。接口用法可参考下方完整示例demo 2023.09.14更新: 隐私相关功能启用时间延期至 2023年10月17日。在 2023年10月17日之前,在 app.json 中配置 [代码]__usePrivacyCheck__: true[代码] 后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。在 2023年10月17日之后,不论 app.json 中是否有配置 [代码]__usePrivacyCheck__[代码],隐私相关功能都会启用。新增官方隐私授权弹窗功能,相关功能参考下方官方隐私弹窗功能说明。此功能目前仍在开发阶段,开发者目前可以先阅读本指南文档和接口文档进行理解,平台将会尽快正式上线相关能力,上线后会在本指南文档中进行说明。 小程序开发者可自行设计提示方式与触发时机,详细文档可查看隐私协议开发指南 。 仅有在指引中声明所处理的用户个人信息,才可以调用平台提供的对应接口或组件。若未声明,对应接口或组件将直接禁用。 [图片] (参考样例)
2023-09-15 - 云函数冷启动引起的Bug
1. 现象 最近对知识竞赛答题小程序进行了重构,设置了与服务器时间对齐功能。 通过云函数获取服务器时间,与本地时间差在5秒之间(考虑手机均实时校对时间,阈值设得相对较小) 开始按钮对时间差进行核对 出现的现象有: 用户表users记录多,成绩表scores记录少 成绩表scores中有些无使用记录 疑问:进入小程序后,为何不使用? 2. 排查 初步考虑是云函数冷启动导致。测试: 早起,删除小程序,清空本地数据 重新打开小程序,出现时间差超过阈值,阻止进入功能页,因此无成绩scores记录 [图片] 3. 原因及修复 首先明确,正常情况下,云服务器的时间与本地时间差在200毫秒以内,甚至更少。 原因:云函数冷启动,可能需要3-5秒的时间,而阈值设得太小。 修复:在调大阈值的同时,还发现另一个小bug,一并修复。 问题:能否实现定时刷新云函数,保证最小使用次数又能激活云函数。 欢迎体验云知识竞赛小程序:) [图片]
2023-09-09 - 小程序商品页性能优化开发实践
一、前言 小程序的性能又可以分为「启动性能」和「运行时性能」。「启动性能」让用户能够更快地打开并看到小程序的内容,「运行时性能」保障用户能够流畅地使用小程序的功能。除了本身的功能之外,良好性能带来的良好用户体验,也是小程序能够留住用户的关键。 二、优化性能诊断工具 诊断工具能让开发者更好地知道性能瓶颈在哪里,并且能让用户在优化后,更好地知道效果如何。微信官方提供的性能诊断工具有: 1. 代码依赖分析 该工具可以帮助开发者分析代码中的依赖关系,以便更好地优化代码的结构和性能。通过分析代码的依赖关系,开发者可以确定哪些代码模块是最重要的,以及哪些模块可能需要进行重构或优化。该工具可以帮助开发者分析代码中的依赖关系,以便更好地优化代码的结构和性能。通过分析代码的依赖关系,开发者可以确定哪些代码模块是最重要的,以及哪些模块可能需要进行重构或优化。 2. 性能报告 该工具可以为开发者提供有关应用程序性能的详细信息,包括加载时间、响应时间、资源使用情况等。这些信息可以帮助开发者确定应用程序的性能瓶颈,并采取相应的措施来提高应用程序的性能。 3. 代码质量扫描 该工具可以帮助开发者分析代码的质量,以便更好地管理和维护代码。通过分析代码的质量,开发者可以确定哪些代码模块需要进行重构或优化,以便更好地满足业务需求并提高代码的可维护性和可扩展性。 4.调试区的 Performance 面板 该工具可以帮助开发者分析应用程序的性能,包括 CPU、内存、网络和渲染等方面。通过 Performance 面板,开发者可以查看代码执行的时间线、函数调用栈等信息,以便更好地确定性能瓶颈并进行优化。 5.Memory 面板 该工具可以帮助开发者分析应用程序的内存使用情况。通过 Memory 面板,开发者可以查看内存使用情况的时间线、对象分配情况、垃圾收集情况等信息,以便更好地确定内存泄漏等问题并进行优化。 6.JavaScript Profiler 面板 该工具可以帮助开发者分析 JavaScript 代码的执行情况。通过 JavaScript Profiler 面板,开发者可以查看 JavaScript 代码的执行时间、函数调用次数、内存使用情况等信息,以便更好地确定 JavaScript 代码的性能瓶颈并进行优化。 7.体验评分(Audits)面板 该工具可以帮助开发者评估应用程序的用户体验。通过 Audits 面板,开发者可以查看应用程序在加载速度、响应速度、可访问性、SEO优化等方面的得分情况,以便更好地确定哪些方面需要进行优化以提高用户体验。 8.对于具体的业务代码,我们通过打时间戳的形式,数字化业务逻辑执行的时间,显性量化优化后的效果 开发者量化业务逻辑的执行时间,以便更好地确定业务逻辑的性能瓶颈并进行优化。通过在关键代码位置打时间戳并记录代码执行时间,开发者可以比较不同版本之间的执行时间差异,以便更好地确定优化效果。 三、启动性能优化 小程序的加载基本流程: [图片] 3.1 商品主图业务优化 行业内商详页面模板元素大致相同,在进入页面时,映入眼帘的肯定是商品主图。一般商户的主图照片都会十分高清,资源较大。对于性能来说是个较大消耗,如果简单压缩,就没办法满足高清的业务需求。基于以上背景,商详页对于主图照片有个特定的业务优化。 该方法说得简单点就是先加载清晰度差一点的商详照片,等非高清照片加载完成。在回调事件中。无缝替换成高清大图,用户无感知。 具体实现方案: [图片] a)技术实现方案 首先在图片中预加载 2 个元素。分别加载资源非高清大图: {picFilter(item || '//:0','md', pictureRatio) 以及高清大图 picFilter(item || '//:0','lg', pictureRatio) 利用 hasLoad 变量控制元素加载。加载不同清晰度的图片方法是 picFilter,lg 是高清大图,md 表示非高清大图。 在非高清图片加载完成后, 预加载高清图片。 loadImg(evt) { // 加载看不见的图片,预加载 this.setData({ isLoadBg: true, }); }, loadEvent() { // 预加载完成,setData this.setData({ hasLoad: true, }); }, b)优化效果 我们选取了一个高质量的图片进行了测试。发现图片在优化前从 120kb 到优化后的 9.6kb。资源加载大大减小了。 优化前: [图片] 优化后: [图片] 3.2按需注入 在 app 中加入全局配置。可以有效降低小程序的启动时间和运行内存。 { "lazyCodeLoading": "requiredComponents" } 3.3分包异步化 分包异步化是微信官方提供的分包加载的优化方案。目前来说多渠道小程序-支付宝也已支持,并且商品详情页也已经投入生产环境使用。分包异步化是将小程序的分包从页面粒度细化到组件甚至文件粒度。这使得本来只能放在主包内页面的部分插件、组件和代码逻辑可以剥离到分包中,并在运行时异步加载,从而进一步降低启动所需的包大小和代码量。 3.4跨分包的自定义组件引用优化 商详分包有引用大量的其他分包的自定义组件,例如商详模板的装修组件(@design-platform/wx-sdk/index),浏览未购组件(platform://ec/browseLogCollector),快速下单组件(ec_order/fastTrade),收银台组件。因为加载商详时,由于其他分包还未下载或注入,其他分包的组件处于不可用的状态。所以我们首先需要设置占位组件,渲染占位组件作为替代,分包下载完成再进行替换。核心原理与商详主图业务的优化大致相同。不过该方案是小程序官方自行提供的。 占位组件使用方式: { "usingComponents": { "pay-payment": "package://payment_cashier/pay-payment", "browse-log-collector": "platform://ec/browseLogCollector", "design-sdk": "@design-platform/wx-sdk/index" }, "componentPlaceholder": { "pay-payment": "view", "browse-log-collector": "view", "design-sdk": "view" } } 3.5非首屏组件懒加载 商详装修详情组件内嵌了装修 SDK,业务上有很多定制组件需要显示,包括商品描述。但这些组件应该属于非首屏组件。商详利用滚动加载的方式,将这些组件的渲染以及加载延迟。 技术上利用 isShowDesc 变量控制组件加载以及渲染。 onPageScroll({ scrollTop }) { this.setData({ isShowDesc: true }); } 该方案存在一种场景:首屏组件特别少,商品描述组件在首屏就无法展示出来了。所以 商品详情页加载首屏组件后需要判断商品描述组件的可视性。 const queryDom = wx.createSelectorQuery().in(this); const queryGoodsDes = queryDom.select('#detail'); if (queryGoodsDes) { queryGoodsDes .boundingClientRect((rect) => { if (!rect) return; if ((wx.rprm as any)?.systemInfo.windowHeight - rect.top > rect.height) { this.setData( { isShowDesc: true, }, () => this.emitGoodsDetail(), ); } }) .exec(); } 3.6文描图片的懒加载 上面虽然对于文件进行了懒加载,但是 商品详情页文描大部分的场景都是图片,并且文描又是商户设置的动态文本。所以文描组件基本都是一次性发大量图片请求的场景。 一次发送大量的 http 请求是非常耗时的。 针对此场景, 商品详情页利用元素可视性也进行了该场景的业务优化。 this.setData({ parseHtml }, () => { // 此处做滚动到图片位置的时候加载图片 parseHtml.images.forEach((item, index) => { const { obList } = this.data; const idx = item.index.replace(/(\d+)\.?/g, (s, $1) => `nodes[${$1}].`); if (!this.data.imgLoayLoad) { this.setData({ [`parseHtml.${idx}imgShow`]: true, [`parseHtml.${idx}imgLoayLoad`]: this.data.imgLoayLoad, }); return; } if (obList[index]) { obList[index].disconnect(); } obList[index] = this.createIntersectionObserver().relativeToViewport({ bottom: 750 }); obList[index].observe(`.wx-parse-img${item.imgIndex}`, (res) => { const idx = item.index.replace(/(\d+)\.?/g, (s, $1) => `nodes[${$1}].`); if (res.intersectionRatio > 0) { obList[index].disconnect(); this.setData({ [`parseHtml.${idx}imgShow`]: true, [`parseHtml.${idx}imgLoayLoad`]: this.data.imgLoayLoad, }); } }); }); }); 3.7依赖分析指标展示优化前后变化 商品业务繁重,我们在优化前对于商品依赖进行了分析,剔除重复引用,将很多商品相关业务,海报业务全部放进一个单独的分包。以及非商品提供的能力利用按需注入、分包异步化、分包异步化、跨分包的自定义组件引用优化等方案进行优化 四、运行性能优化 4.1商祥数据出参数优化 商祥的 spu 数据字段是比较多,为了拼团的两套购买逻辑,做了两套 spu 数据来维护,这样无行之中又使数据变大了,结合这种场景,我们把两套 spu 改为维护一套,拼团业务的单独购买,重新获取 spu 数据做业务逻辑。 [图片] 4.2setData 优化 setData 是小程序中使用最多,也是最容易引发性能问题的接口。 由于微信官方文档可知。小程序的逻辑层和视图层是两个独立的运行环境、分属不同的线程或进程,不能直接进行数据共享,需要进行数据的序列化、跨线程/进程的数据传输、数据的反序列化,因此数据传输过程是异步的、非实时的。 数据传输的耗时与数据量的大小正相关,如果对端线程处于繁忙状态,数据会在消息队列中等待。 因此在使用上遵循小程序的规范。 const BulletChatConfig = { // 消费弹幕配置 }; Component({ data: { originalData: [], }, bulletChatConfig:bulletChatConfig, pageLifetimes: { show() { this.animateStart(); }, } }); 页面或组件与渲染无关的数据。 最好挂在 this 下面。 控制 setData 的频率。对于连续的 setData 进行合并。 选择合适的 setData 范围。 setData 应只传发生变化的数据。 // before: ❌ 不要在setData中偷懒一次性传所有的data; this.setData({ singleSkuData: { ...skuData, selectedMap: this.data.selectedMap, }, }); // after this.setData({ ['singleSkuData.selectedMap']: this.data.selectedMap, }); 4.3选择高性能的动画实现方式 动画循环是前端的一个消耗。商详页也存在动画循环的组件。例如消费弹幕。秒杀定时器等。 起初 商品详情页消费弹幕的动画循环是利用定时器 setTimeOut 去实现的。 const that = this; const timer = setInterval(function () { if (dataIndex === data.length) { dataIndex = 0; } if (queueIndex === rowQueue.length) { queueIndex = 0; } tempData.push(that.getBulletChatItem(data[dataIndex], rowQueue[queueIndex], config.itemStyle)); if (tempData.length > 8) { tempData.splice(0, 1); } that.setData({ bulletChatList: tempData, }); dataIndex++; queueIndex++; }, config.intervalTime); that.setData({ timer, type: customType || DisplayType[display] || 'scrollup', bulletChatConfig: config }); 改成 createIntersectionObserver 的可视性来进行循环动画。 this.intersectionObserver = wx.createIntersectionObserver(this, { observeAll: true, thresholds: [0, 0.5, 1, 0] }); if (!this.intersectionObserver) return; this.intersectionObserver.relativeTo('.bullet-chat').observe('.bullet-chat-item-container', (res) => { if (res.intersectionRatio >= 0.5) { let { dataset: { id }, } = res; if (id === data.length - 1) { id = -1; } this.setData({ [`bulletChatList[${id + 1}].style`]: config.itemStyle, }); } if (res.intersectionRatio === 0 && res.intersectionRect.width === 0) { const { dataset: { id }, } = res; this.setData({ [`bulletChatList[${id}].style`]: 'left: 100%', }); } }) 五、运优化结果展示 我们利用打时间戳的形式,对于小程序的各种业务逻辑时间,以及渲染时间进行量化。显性得出优化前后的各种指标变化。 优化前: [图片] 优化后: [图片] 六、总结 以上就是商详页面的性能优化实践,分析小程序的启动过程。可知小程序优化可分成运行性能和启动性能优化。 启动性能优化利用商品主图优化,文描图片的懒加载,非首屏组件的懒加载,分包异步化,按需注入等方案实践。利用小程序的依赖分析可看出商祥分包的确肉眼可见地变小了,首屏组件也变快了。 运行时性能优化方案包括 商品详情页数据出参的优化,setData 的一些优化,选择高性能的动画实现方式的优化等。 我们利用打时间戳的方式。将 商品详情页分成初始化,请求,渲染等过程,分别计时。对比出优化前后的指标变化,由指标可知,商祥从加载到用户可交互的时间确实从 2.5s 优化到 0.9s 左右,优化了近 50%。 优化前后对比: [图片] 优化前 [图片] 优化后
2023-09-05 - 用纯 CSS 方式实现动态切换主题风格
一、前言 UI 组件库是现代 Web 应用程序开发中不可或缺的一部分。动态主题风格切换是一个非常重要的功能,它可以允许应用程序用户在不同的场景下选择自己喜欢的主题。这样的一个特性可以增加用户体验的个性化,并提高应用程序的可用性和易用性。 微盟移动端组件库 Titian 提供了动态主题切换的能力,并且延展了主题范围。 二、背景 在以前商户店铺的品牌视觉风格往往千篇一律,同时还因为需要逐个繁琐地配置界面中元素,导致的风格错乱等问题。针对上述的痛点,本次升级在确保商户品牌风格统一的前提下,基于品牌调性提炼了具有共性的视觉特征,分别为颜色、图标、圆角。并用这些特征组合为“通用”、“潮流”、“可爱”三套风格,能够让商家随心选择,让线上店铺更贴合自身的品牌调性,提高品牌识别度,维持 C 端用户的品牌心智。 [图片] 微盟移动端组件库 Titian 的主题风格包括三个维度的风格变化: 1、主题颜色的风格切换,这里主题色可以设置任意一种色值。 2、字体图标风格的切换,组件库中的所有图标包含三种风格,具体分为通用型,超流型和可爱型。 3、所有组件的圆角的切换,组件库中所有圆角也分为三种风格,风格的分类和字体图标分类一致,通用型的圆角即为设计稿上的圆角,超流型的圆角则是所有的组件圆角都变成直角,可爱型的圆角则为在设计稿上的圆角的基础上加上8个像素。 [图片] 我们要实现三种风格的切换是互相独立的,可以互相组合搭配。另外,图标风格的切换可以是全量一起切换,也可以是部分单独切换,而且需要运行时可以动态切换。 这些风格的切换都需要内置到组件库中,只需要给业务方提供一个变量来改变整体风格。 三、需求分析 对于主题色的风格配置,由于有些组件使用的的具有百分比透明度的主题色,所以采用 RGBA 色值更加方便。对于图标风格切换,从一种风格增加到三种风格,能不能尽量的不要增加代码体积,毕竟小程序对包体积有严格的要求。 对于圆角风格,有些需要将设计稿的圆角加8像素,有些需要变成直角,而有些又需要单独处理成大圆角。 这都该如何设计呢?这么多的风格切换,如何能尽量设计少的接口来让业务方写最少的代码,不去增加业务方的记忆负担呢?另外需要在运行时进行风格的切换,我决定使用 CSS 原生变量的方式。 CSS变量的好处包括: 代码重用:可以在多个元素中使用同一个变量,避免了重复编写样式代码的问题。 简化维护:当需要修改样式时,只需要修改变量的值,而不是每个元素的样式。 动态更新:CSS 变量可以通过 JavaScript 动态修改,使得样式在运行时可以动态变化。 提高可读性:通过使用有意义的变量名,可以使样式表更易于理解和维护。 CSS 变量可以提高代码的可维护性、可读性和灵活性。 四、技术方案和实施 [图片] 4.1 主题颜色切换方案 组件库内部定义三个 CSS 变量:--theme-r、--theme-g、--theme-b,这三个 CSS 变量也是对外暴露出去修改主题颜色的关键。组件内部的全局 less 文件使用这三个变量定义主题色,所有组件使用到主题色的地方都统一使用下面的 less 变量。 @theme-r: var(--theme-r, 250); @theme-g: var(--theme-g, 44); @theme-b: var(--theme-b, 25); @brand-color: rgb(@theme-r, @theme-g, @theme-b); @brand-color-fade-10: rgba(@theme-r, @theme-g, @theme-b, 0.1); @brand-color-fade-20: rgba(@theme-r, @theme-g, @theme-b, 0.2); @brand-color-fade-30: rgba(@theme-r, @theme-g, @theme-b, 0.3); @brand-color-fade-40: rgba(@theme-r, @theme-g, @theme-b, 0.4); @brand-color-fade-50: rgba(@theme-r, @theme-g, @theme-b, 0.5); @brand-color-fade-60: rgba(@theme-r, @theme-g, @theme-b, 0.6); @brand-color-fade-70: rgba(@theme-r, @theme-g, @theme-b, 0.7); @brand-color-fade-80: rgba(@theme-r, @theme-g, @theme-b, 0.8); @brand-color-fade-90: rgba(@theme-r, @theme-g, @theme-b, 0.9); @brand-color-fade-100: rgba(@theme-r, @theme-g, @theme-b, 1); [图片] 4.2 圆角风格切换方案 三种圆角是对应三种圆角数值,默认的圆角是设计稿的圆角,怎样变成直角和大8像素的圆角呢?我采用设计圆角加上增量圆角来达到最终圆角的目的。 针对于所有增量圆角,我们定义一个css变量:--base-radius-size,另外在全局less变量中定义圆角变量,我们所有使用到圆角的地方都使用less变量;默认的增量为0。 潮流型风格需要将圆角变成直角,那么只需将增量圆角设置为一个较大负值比如-999px,那么最终也会得到一个负数圆角,因为圆角不存在负值,所以负值圆角表现就是圆角为0的直角效果。 可爱型风格需要将设计稿圆角增加8像素。那么这个增量圆角就设置为8px;而对于那些特殊需求,要单独设置成大圆角即半圆形的圆角,那么只需给一个较大的圆角即可。但是为了做区分,所以这里新增了一个css变量:--capsule-radius-size,这个是专供特殊需求圆角使用,比如button和search的圆角,他们在可爱风格下会直接变成胶囊型圆角。那么这里就把--capsule-radius-size设置为999px即可。 下图就是全局 less 变量中定义的圆角,在组件中统一使用如下圆角。目前只罗列了4px、8px、12px、16px和大圆角。如果有更多圆角,可以新增多个圆角数值。业务方在使用时,设置通用风格,只需设置--base-radius-size:0px;--capsule-radius-size:0px;这也是默认风格。设置成潮流型,只需设置--base-radius-size:-999px;--capsule-radius-size:-999px;设置成可爱型,只需设置--base-radius-size:8px;--capsule-radius-size:999px; @radius-4: calc(var(--base-radius-size, 0px) + 4px); @radius-8: calc(var(--base-radius-size, 0px) + 8px); @radius-12: calc(var(--base-radius-size, 0px) + 12px); @radius-16: calc(var(--base-radius-size, 0px) + 16px); @radius-999: calc(var(--base-radius-size, 0px) + 999px); // 圆角 (按钮button、搜索search)采用如下圆角; // 可以自适应变成胶囊型 @special-radius-4: calc(var(--capsule-radius-size, 0px) + 4px); @special-radius-8: calc(var(--capsule-radius-size, 0px) + 8px); @special-radius-12: calc(var(,--capsule-radius-size 0px) + 12px); @special-radius-16: calc(var(--capsule-radius-size, 0px) + 16px); @special-radius-999: calc(var(--capsule-radius-size, 0px) + 999px); 到这里切换圆角的功能已经实现了,但是让业务方去记忆不同的圆角值对应三种风格,比如设置可爱风需要设置--base-radius-size:8px;--capsule-radius-size:999px; 这会增加业务方的记忆负担,能不能继续优化,让设置更简单易用呢?我又探索了在 CSS 中使用布尔运算,让业务方通过传入0、1、2,组件内自动计算出需要使用的圆角值。 利用calc + var实现纯css布尔运算 [图片] 三种风格的计算逻辑 [图片] 逻辑延展,适应更多风格 [图片] 这样的方式就大大简化了业务的使用负担,只需要根据接口返回的风格类型,将对应的0、1、2通过 CSS 变量传入组件库,就可使用不同的圆角风格,计算过程完全在组件内部。后续如果要调整规则,也只需要在组件中进行全局的修改即可。 4.3 图标风格切换方案 目前常见的图标风格切换方式,主要是图标名称的切换。假如原有 50 个通用型风格的图标,现在分别新增 50 个潮流型和可爱型图标,对应不同的图标名称,换图标名就达到了换风格的目的。我的方案简单概括就是换字体,不换图标名称;由于小程序中对包体积有严格控制,所以能不增加包体积则最好; [图片] [图片] 在字体图标平台创建三套字体图标库,分别为通用型,潮流型和可爱型字体库;并分别上传对应风格的图标;按照通用型图标库为基准,修改新增字体库里的图标名称和 Unicode 编码,做到三套字体库中图标名称和 Unicode 编码一一对应相同;如下图,同一个删除图标,在三种风格的字体库中,下图标记的地方代表 Unicode 编码和图标名称,在三个字体库中要设置成一样的。将三套字体图标引入到小程序项目中,由于图标名称和 Unicode 编码一致,所以只需要引入三套字体的定义内容,具体的图标伪元素定义内容基本一致,无需新增。 [图片] 上图是设置的关键,每个图标库中需对应设置成一样的值。 五、总结 主题色可以设置任意一种色值,图标可以三种风格互相切换,圆角也可以三种风格互相切换。这三中风格又可以互相搭配。微盟移动端组件库 Titian 采用 CSS 变量方式切换风格,其中主题色风格提供三个 CSS 变量:--theme-r、--theme-g、--theme-b 对应主题色的 RGBA 色值,字体图标提供一个 CSS 变量,--icon-family 来设置图标对应的字体库的名称,圆角风格提供两个 CSS 变量:--base-radius-size 和 --capsule-radius-size 来设置圆角的增量,后续又优化为使用 --s 来计算得到增量圆角。 通过以上几个简单的 CSS 变量,微盟移动端组件库 Titian 实现了,使用纯 CSS 方式,在运行时动态切换主题风格和自由搭配三种类型风格的能力,在小程序和 H5 中是完全通用的,体验完全一致。微盟移动端组件库 Titian 动态切换主题的能力,给使用方丰富的选择性,体现品牌调性的多样化。也兼顾了商家品牌个性化需求的灵活性,圆角与图标风格可以进行脱钩单独选择风格,一键配置,全店生效。以及在已定义的部分场景中也能与全局风格脱钩 (例如价格色与标签色),既有统一的品牌风格,又不失场景化的灵活表达。贴合用户心智,维持品牌认知。在赋能品牌的同时,开发者能够探索出无限可能。 目前微盟移动端组件库 Titian 已经完全开源,期待大家共同构建组件库生态,让Titian组件库更加易用好用。
2023-09-04 - 云调用能力—图像处理和OCR
云调用有些接口属于 AI 服务的范畴,比如借助于人工智能来进行智能裁剪、扫描条码/二维码、图片的高清化等图像处理和识别银行卡、营业执照、驾驶证、身份证、印刷体、驾驶证等 OCR,有了这些接口我们也能在小程序里使用人工智能了。接下来我们以小程序的条码/二维码识别和识别印刷体为例来介绍一下云调用。 13.3.1 图像处理使用开发者工具新建一个云函数,如 scancode,然后在 config.json 里添加 img.scanQRCode 云调用的权限,使用 npm install 安装依赖之后,上传并部署所有文件(此时也会更新权限)。 { "permissions": { "openapi": [ "img.scanQRCode" ] } } 然后再在 index.js 里输入以下代码,注意[代码]cloud.openapi.img.scanQRCode[代码]方法和[代码]img.scanQRCode[代码]权限的对应写法,不然会报 604100 的错误。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { try { const result = await cloud.openapi.img.scanQRCode({ imgUrl: "https://tcb-1251009918.cos.ap-guangzhou.myqcloud.com/demo/qrcodetest.png", //注意二维码必须是条码/二维码,不能是小程序码 }); return result; } catch (err) { console.log(err); return err; } }; 调用该云函数之后,返回的 result 对象里包含 result 对象,在 codeResults 的 data 里可以得到二维码里包含的内容。 codeResults: [{ data: "使用云开发来开发微信小程序可以免费。。。", pos: {leftTop: {…}, rightTop: {…}, rightBottom: {…}, leftBottom: {…}},typeName: "QR_CODE"}] errCode: 0 errMsg: "openapi.img.scanQRCode:ok" imgSize: {w: 260, h: 260} 13.3.2 OCR 人工智能识别使用开发者工具新建一个云函数,如 ocrprint,然后在 config.json 里添加 ocr.printedText 云调用的权限,使用 npm install 安装依赖之后,上传并部署所有文件(此时也会更新权限)。 { "permissions": { "openapi": [ "ocr.printedText" ] } } 调用该云函数之后,返回的 result 对象里包含 result 对象,在 codeResults 的 data 里可以得到二维码里包含的内容。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { try { const result = await cloud.openapi.ocr.printedText({ imgUrl: "https://tcb-1251009918.cos.ap-guangzhou.myqcloud.com/demo/ocrprint.png", }); console.log(result); return result; } catch (err) { console.log(err); return err; } }; 调用该云函数之后,返回的 result 对象里包含 result 对象,在的 items 里可以返回图片包含的文字内容。 items: Array(4) 0: {text: "JavaScript入门", pos: {…}} 1: {text: "JavaScript是目前世界上最流行的编程语言之一,它也是小程序开发最重要的基础语言。要做出一个功能复杂的小程序,除了需要掌握JavaScript的基本语", pos: {…}} 2: {text: "法,还要了解如何使用JavaScript来操作小程序(通过API接口)", pos: {…}} 3: {text: "过API接口)。", pos: {…}} 13.3.3 图像处理拓展能力图片是小程序非常重要的元素,尤其是旅游照片、社交图片、电商产品图片、相册类小程序、媒体图文等,图片的加载速度、清晰度、图片的交互、图片效果的处理以及图片加载的 CDN 消耗都是一个不得不需要去关注的问题。而云开发图像处理拓展能力结合云存储则可以非常有效的解决很多问题。 强烈建议所有有图片处理需求的用户都应该安装图像处理拓展能力,这个能力大大弥补和增强了云存储在图片处理能力,尤其是图片按照需求的规格进行缩放可以大大减少 CDN 的消耗以及图片的加载速度以及我们可以按照不同的业务场景使用快速缩略模板,而这一切的操作和云存储的结合都是非常实用且易用的。 1、图像处理能力介绍云开发图像处理能力结合的是腾讯云数据万象的图片解决方案,图像处理提供多种图像处理功能,包含智能裁剪、无损压缩、水印、格式转换等,图像处理拓展能力所包含的功能非常丰富,使用如下图片处理的费用是按量计费的,计费周期为月,10TB 以内免费,超出 10TB,按 0.025 元/GB 来计费,省事而便宜: 缩放:等比缩放、设定目标宽高缩放等多种方式;裁剪:普通裁剪、缩放裁剪、内切圆、人脸智能裁剪;旋转:普通旋转、自适应旋转;格式转换:jpg、bmp、gif、png、webp、yjpeg 格式转换,gif 格式优化,渐进显示功能;质量变换:针对 JPG 和 WEBP 图片进行质量变换;高斯模糊:对图片进行模糊处理;锐化:对图片进行锐化处理;图片水印:提供图片水印处理功能;文字水印:提供实时文字水印处理功能;获取图片基本信息:查询图片基本信息,包括格式、长、宽等;获取图片 EXIF:查询图片 EXIF 信息,如照片的拍摄参数、缩略图等;获取图片主色调:获取图片主色调信息;去除元信息:去除图片元信息,减小图像体积;快速缩略模板:快速实现图片格式转换、缩略、剪裁等功能,生成缩略图;管道操作符:对图片按顺序进行多种处理当我们在腾讯云云开发网页控制台(注意要使用微信公众号的方式登录)添加完图像处理的拓展能力之后,我们可以在腾讯云的数据万象存储桶里看到云开发的云存储,而关于图像处理能力的深入使用,也可以参考腾讯云数据万象的技术文档。在小程序云开发里使用图像处理能力的方法有三种: 图像地址的拼接,只需要在图片的下载地址 url 里拼接一些简单的参数(API 管道操作符),就能够使用到图像处理的能力,非常方便易用,这个不会把图片处理的结果存储到云存储,不会占用云存储的空间;在获取图片基本信息、获取图片 EXIF、获取图片主色调等方面非常方便;在前端(小程序端)做持久化图像处理,支持有结果图输出的处理操作,也就是我们可以把缩放、裁剪、格式转换、质量变换等处理之后的图片存储到云存储方便以后使用;在云函数端做持久化图像处理,支持有结果图输出的处理操作 01图像地址的拼接在了解图像处理能力之前,我们需要先了解一下云存储文件的 fileID、下载地址以及下载地址携带的权限参数 sign(图像处理能力的参数拼接就是基于下载地址的),如下图所示: [图片] 在安装了图像处理拓展能力的情况下,我们可以直接拿云存储的下载地址进行拼接,拼接之后的链接我们既可以在小程序里使用,也可以用于图床,比如原始图片下载地址为: https://786c-xly-xrlur-1300446086.tcb.qcloud.la/hehe.jpg?sign=b8ac757538940ead8eed4786449b4cd7&t=1591752049 而相关的图像处理能力的拼接案例如下,具体的操作可以看技术文档,实际的效果,可以复制粘贴链接到浏览器或小程序里体验(换成自己的地址),注意拼接方式就是在下载地址后面加了一个[代码]&imageMogr2/thumbnail/!20p[代码](注意这里由于已经有了一个 sign 参数,所以拼接时用的是[代码]$[代码],不能写成[代码]?[代码],否则不会生效),直接就可以啦,非常易用: //将图片等比例缩小到原来的20% https://786c-xly-xrlur-1300446086.tcb.qcloud.la/hehe.jpg?sign=b8ac757538940ead8eed4786449b4cd7&t=1591752049&imageMogr2/thumbnail/!20p 后面为了方便,我们将[代码]https://786c-xly-xrlur-1300446086.tcb.qcloud.la/hehe.jpg?sign=b8ac757538940ead8eed4786449b4cd7&t=1591752049[代码]简写为 download_url: //缩放宽度,高度不变,下面案例为宽度为原图50%,高度不变 download_url&imageMogr2/thumbnail/!50px //缩放高度,宽度不变,下面案例为高度为原图50%,宽度不变 download_url&imageMogr2/thumbnail/!x50p //指定目标图片的宽度(单位为px),高度等比压缩,注意下面的是x,不是px,p与x在拼接里代表着不同的意思 download_url&imageMogr2/thumbnail/640x //指定目标图片的高度(单位为px),宽度等比压缩: download_url&imageMogr2/thumbnail/x355 //限定缩略图的宽度和高度的最大值分别为 Width 和 Height,进行等比缩放 download_url&imageMogr2/thumbnail/640x355 //限定缩略图的宽度和高度的最小值分别为 Width 和 Height,进行等比缩放 download_url&imageMogr2/thumbnail/640x355r //忽略原图宽高比例,指定图片宽度为 Width,高度为 Height ,强行缩放图片,可能导致目标图片变形 download_url&imageMogr2/thumbnail/640x355! //等比缩放图片,缩放后的图像,总像素数量不超过 Area download_url&imageMogr2/thumbnail/150000@ //取半径为300,进行内切圆裁剪 download_url&imageMogr2/iradius/300 //取半径为100px,进行圆角裁剪 download_url&imageMogr2/rradius/100 //顺时针旋转90度 download_url&imageMogr2/rotate/90 //将jpg格式的原图片转换为 png 格式 download_url&imageMogr2/format/png //模糊半径取8,sigma 值取5,进行高斯模糊处理 download_url&imageMogr2/blur/8x5 //获取图片的基础信息,返回的是json格式,我们可以使用https请求来查看图片的format格式,width宽度、height高度,size大小,photo_rgb主色调 download_url&imageInfo 2、小程序端持久化图像处理当我们希望把缩放、裁剪、旋转、格式变换等图像处理的结果(也就是处理之后的图片)存储到云存储,这个就叫做持久化图像处理,在安装了图像处理能力之后,我们也可以在小程序端做图像处理。 当用户把原始图片上传到小程序端时,我们需要对该图片进行一定的处理,比如图片过大就对图片进行裁剪缩小;比如图片需要进行一定的高斯模糊、旋转等处理,这些虽然在图像处理之前,也是可以使用 js 来做的,但是小程序端图像处理的效果并没有那么好或者过于复杂,使用图像处理的拓展能力就非常实用了。在小程序端构建图像拓展依赖 首先在开发者工具小程序根目录(一般为 miniprogram),右键“在终端中打开”,然后在终端里输入以下代码,也就是在小程序端安装图像拓展依赖,安装完时,我们就可以在 miniprogram 文件夹下看到 node_modules: npm install --save @cloudbase/extension-ci-wxmp@latest 然后点击开发者工具工具栏里的工具-构建 npm,构建成功之后,就可以在 miniprogram 文件夹下看到 minprogram_npm 里有@cloubase 文件夹,里面有 extension-ci-wxmp,说明图像拓展依赖就构建完成。 在小程序端进行图像处理 使用开发者工具新建一个 imgprocess 的页面,然后在 imgprocess.wmxl 里输入如下代码,我们新建一个 button 按钮: 处理图片button> 然后再在 imgprocess.js 的 Page()函数的上面(外面)引入图像处理依赖,代码如下: const extCi = require("./../../miniprogram_npm/@cloudbase/extension-ci-wxmp"); 然后再在 imgprocess.js 的 Page()函数的里面写一个 imgprocess 的事件处理函数,点击 button 之后会先执行 readFile()函数,也就是获取图片上传到小程序临时文件的结果(是一个对象),然后再调用 imageProcess()函数,这个函数会对图片进行处理,图片会保存为[代码]tcbdemo.jpg[代码],而处理之后的图片会保存为 image_process 文件夹下的 tcbdemo.png,相当于保存了两张图片: async imgprocess(){ const readFile = async function() { let res = await new Promise(resolve=>{ wx.chooseImage({ success: function(res) { let filePath = res.tempFilePaths[0] let fm = wx.getFileSystemManager() fm.readFile({ filePath, success(res){ resolve(res) } }) } }) }) return res } let fileResult = await readFile(); //获取图像的临时文件上传结果 const fileContent = fileResult.data //获取上传到临时文件的图像,为Uint8Array或Buffer格式 async function imageProcess() { extCi.invoke({ action: "ImageProcess", cloudPath: "tcbdemo.jpg", // 图像在云存储中的路径,有点类似于wx.cloud.uploadFile接口里的cloudPath,上传的文件会保存为云存储根目录下的hehe.jpg operations: { rules: [ { fileid: "/image_process/tcbdemo.png", //将图片存储到云存储目录下的image_process文件夹里,也就是我们用image_process存储处理之后的图片 rule: "imageMogr2/format/png", // 处理样式参数,我们可以在这里写图片处理的参数拼接 } ] }, fileContent }).then(res => { console.log(res); }).catch(err => { console.log(err); }) } await imageProcess() } 可能你的开发者工具会报以下错误:[代码]https://786c-xly-xrlur-1300446086.pic.ap-shanghai.myqcloud.com 不在以下 request 合法域名列表中,请参考文档:https://developers.weixin.qq.com/miniprogram/dev/framework/ability/network.html[代码],这个要按照参考文档将链接加入到合法域名当中,不然不会生成图片;[代码]action[代码]是操作类型,它的值可以为:ImageProcess 图像处理,DetectType 图片安全审核(后面会介绍),WaterMark 图片忙水印、DetectLabel 图像标签等。[代码]operations[代码]是图像处理参数,尤其是 rule 和我们之前 url 的拼接是一致的,比如[代码]imageMogr2/blur/8x5[代码]、[代码]imageMogr2/rradius/100[代码]等参数仍然有效。上面函数里的 fileContent 不是必要的,也就是说我们可以不在小程序端上传图片,而是直接修改云存储里面已有的图片,并将图片处理后的照片保存,这种情况代码可以写成如下: async imgprocess(){ extCi.invoke({ action: "ImageProcess", cloudPath: "tcbdemo.jpg", // 会直接处理这张图片 operations: { rules: [ { fileid: "/image_process/tcbdemo.png", rule: "imageMogr2/format/png", // 处理样式参数,与下载时处理图像在url拼接的参数一致 } ] }, }).then(res => { console.log(res); }).catch(err => { console.log(err); }) } 3、云函数端持久化图像处理在云函数端的处理和小程序端的处理,使用的方法大体上是一致的,不过云函数的处理图片的场景和小程序端处理图片的场景会有所不同,小程序端主要用于当用于上传图片时就对图片进行处理,云函数则主要用于从第三方下载图片之后进行处理或者对云存储里面的图片进行处理(比如使用定时触发器对云存储里指定文件夹的图片进行处理)。不建议把图片传输到云函数端再来对图片进行处理。 使用开发者工具新建一个 imgprocess 的云函数,然后在 package.json 里添加 latest 最新版的[代码]@cloudbase/extension-ci[代码],并右键云函数目录选择在终端中打开输入命令 npm install 安装依赖: "dependencies": { "wx-server-sdk": "latest", "@cloudbase/extension-ci": "latest" } 然后再在 index.js 里输入以下代码,代码的具体含义可以参考小程序端的内容讲解: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const extCi = require("@cloudbase/extension-ci"); cloud.registerExtension(extCi); async function process() { try { const opts = { rules: [ { fileid: "/image_process/tcbdemo.jpeg", rule: "imageMogr2/format/png", }, ], }; const res = await app.invokeExtension("CloudInfinite", { action: "ImageProcess", cloudPath: "tcbdemo.jpg", fileContent, operations: opts, }); console.log(res); return res; } catch (err) { console.log(err); } }
2021-09-10 - 小程序pc端全屏(小程序页面横竖屏)的代码实现
1、在app.json文件中,与“window”同级别的地方增加配置 "resizable": true; 2、在app.json文件中,“window”模块中增加"pageOrientation":"landscape"。 这样配置后,就可以让小程序的页面呈现横屏状态,然后用户只需要点击右上角的全屏按钮就可以全屏了,赶紧去试试吧。 3、如果有的页面不想横屏显示的话,只需要在这个页面下的json文件中加上配置"pageOrientation":"portrait"即可。 这样配置后,只有页面json文件中配置了portrait的才会竖屏显示,其他的就都默认横屏显示了。 4、发现的问题:如果全局window设为了landscape,而某个页面,比如叫A页面中的json文件中单独设置了portrait(竖屏显示),假如你恰好在A页面加了激励式视频广告,那么你就会发现本来事竖屏显示的A页面,在点击观看激励式视频广告后返回来的时候就被强制显示为横屏了。 以上是我在项目中时间pc端全屏和小程序横竖屏显示配置时的总结和发现的问题,希望能给有需要的人带来帮助。
2023-04-25 - ios该如何确保小程序添加到桌面功能生效呢?
我注意到对于账号主体为个人开发者的小程序,点击右上角菜单进入后,选择添加到桌面,仅出现链接生成失败,请重试的提醒; 对于账号主体为公司的,似乎都会自动跳转safari浏览器引导用户添加到主屏幕界面,所以仅仅是账号主体的区别决定该功能是否生效的吗?
2023-04-12 - 根据经纬度坐标获取当前所在城市——自己开发国内城市逆地址解析接口
背景 最近各大地图商齐刷刷的开始对地图的一些接口收费,特别是对商业用户。我在一些论坛上看到有水友吐槽,自己的APP用到了逆地址解析接口来获取当前城市,现在都要面临既收费、又限制调用频率和次数的问题,于是萌生了做一个国内城市逆地址解析接口的想法。 具体想法 具体实现并不难,主要分以下几步: 1、获取国内省市的地理轮廓 2、使用geo库解析轮廓 3、根据用户输入的坐标,按照“国——省——市”的顺序,找出坐标落在哪个市级范围内 开始实现 开发环境 操作系统:ubuntu 18.04 python版本: 3.8 django版本:2.2.4 postgrsql版本:10.23 开发步骤 1、下载省市轮廓,下载地址http://www.geojson.cn/preview,我是用python脚本下载的,格式是geojson #!/user/bin/pyhton import os import urllib.request import json BASE_URL = 'https://geojson.cn/api/data' TAR_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data', 'geojson') def download_file(file_url, file_path): urllib.request.urlretrieve(file_url, file_path) if __name__ == '__main__': cina_url = '/'.join([BASE_URL, '100000.json']) cina_filename = os.path.join(TAR_DIR, '100000.json') download_file(cina_url, cina_filename) with open(cina_filename, 'r', encoding='UTF-8') as fd: cina_data = json.load(fd) prv_list = cina_data['features'] for index, prv_item in enumerate(prv_list): props = prv_item['properties'] if 'code' in props: print('index: %d, name: %s, code: %d' % (index, props['name'], props['code'])) try: download_file('/'.join([BASE_URL, '%d.json' % props['code']]), os.path.join(TAR_DIR, '%d.json' % props['code'])) except: print('[error]index: %d, name: %s, code: %d' % (index, props['name'], props['code'])) else: print('index: %d, props: %s' % (index, props)) 2、在django内构造省市区域的model from django.contrib.gis.db import models from common.base_model import BaseModel class AdArea(BaseModel): """行政区域 Args: BaseModel (_type_): _description_ """ code = models.IntegerField(unique=True, null=False, verbose_name='区域编码') name = models.CharField(max_length=256, verbose_name='名称') fullname = models.CharField(max_length=256, verbose_name='区域全名') center = models.PointField(verbose_name='地理中心') children_num = models.IntegerField(verbose_name='子区域个数') level = models.CharField(max_length=64, verbose_name='级别') bbox = models.PolygonField(verbose_name='区域矩形边框') parent_code = models.IntegerField(verbose_name='父区域编码') mpoly = models.MultiPolygonField(verbose_name='区域地理边界') mpoly2 = models.GeometryCollectionField(null=True, blank=True, verbose_name='区域地理边界2', help_text='mpoly无效几何的修正结果') def __str__(self): return self.name class Meta: indexes = [ models.Index(fields=["level"]) ] 3、将geojson数据导入到postgresql数据库 4、构造rest api用于逆地址解析 from django.shortcuts import render from django.contrib.gis.geos import Point from django.contrib.gis.geos.error import GEOSException from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response from .models import AdArea from .serializers import AdAreaSerializer def is_in_china_bbox(latitude, longitude): china_bbox = (73.502355, 17.98689826522479, 135.09567, 53.563269) if latitude < china_bbox[1] or latitude > china_bbox[3]: return False if longitude < china_bbox[0] or longitude > china_bbox[2]: return False return True def is_in_area(location, m): if not m.bbox: return True try: if not m.bbox.contains(location): return False if m.mpoly2 is not None: return m.mpoly2.contains(location) else: return m.mpoly.contains(location) except GEOSException: print('=================geo contains error==================') for poly in m.mpoly: for ring in poly: print(list(ring)) print('=================geo contains error==================') raise @api_view(['GET']) def reverse_city(request): """经纬度逆解析-获取当前所在城市 Args: http://localhost:8000/api/zzgeo/reverse_city/?longitude=120.592528&latitude=31.310623 Returns: { 'code': 320500, 'name': '苏州', 'level': 'city' } """ if request.method == 'GET': latitude = float(request.GET['latitude']) longitude = float(request.GET['longitude']) if not is_in_china_bbox(latitude, longitude): return Response(status=status.HTTP_404_NOT_FOUND) city_location = Point(longitude, latitude) province_list = AdArea.objects.filter(level='province') for province_item in province_list: if not is_in_area(city_location, province_item): continue city_list = AdArea.objects.filter(parent_code=province_item.code, level='city') if not city_list: return Response(AdAreaSerializer(province_item).data) for city_item in city_list: if not is_in_area(city_location, city_item): continue return Response(AdAreaSerializer(city_item).data) return Response(status=status.HTTP_404_NOT_FOUND) 接口展示 [图片] 总结 使用geodjango,将下载的省市geojson格式的轮廓导入到系统,构造rest api,在用户发起查询时,按照“国——省——市”的顺序,找出坐标落在哪个市级范围内。 本文为抛砖引玉,geo库有非常多,覆盖了几乎所有的编程语言,例如nodejs可以使用turfjs。 欢迎大家参与讨论。
2023-06-17 - 深入理解CSS字符转义行为
[图片] 深入理解CSS字符转义行为 深入理解CSS字符转义行为 前言 为什么要转义? CSS 转义 什么是合法[代码]css[代码]的表达式 左半部分 右半部分 练习 参考链接 前言 在日常的开发中,我们经常写css。比如常见的按钮: [代码]<button class="btn"></button>[代码],我们往往写出这样的样式 [代码].btn { display: inline-flex; cursor: pointer; user-select: none; /* ..more decl.. */ } [代码] 然而我们有时候也会见到这样的元素: [代码]<div class="2xl:text-base">Hello world</div>[代码] 与之对应生效的CSS样式为: [代码]@media (min-width: 1536px) { .\32xl\:text-base { font-size: 1rem; line-height: 1.5rem; } } [代码] 这时候就纳闷了,我明明写的是 [代码]2xl:text-base[代码] 啊?[代码]\:[代码]这个转义还好说,[代码]\3[代码] 这个又是哪来的呢?本篇文章就来从 [代码]W3C[代码] 的角度,对 [代码]css[代码]转义行为进行揭秘。 为什么要转义? 我们先把目光提升一些,其实 转义 ([代码]Escaping[代码])这个行为,在各个语言系统中都存在,小到正则表达式,[代码]html[代码],[代码]css[代码],大到 [代码]javascript[代码] 或者其他成熟的编程语言,都多少存在着这种行为。 那些需要转义的字符,往往是和语言中的特定关键字([代码]keywords/meta[代码])产生了冲突,所以被迫让位。 比如,正则表达式中的 [代码].[代码] 就是一个元字符,代表的是匹配任意单个除了换行符的字符。要想匹配 [代码].[代码] 就需要转义一下写成 [代码]\.[代码]。 [代码]html[代码] 中的 [代码]<[代码],[代码]>[代码] 需要写成 [代码]<[代码],[代码]>[代码],不然就会和 [代码]html[代码] 中的标签匹配方式([代码]<div></div>[代码])产生冲突。 而 [代码]javascript[代码] 中我们也经常写出这样的单/双引号字符串 [代码]'i\'m a "happy" fool'[代码] or [代码]"i'm a \"happy\" fool"[代码]。 同样 [代码]css[代码] 也是如此。 CSS 转义 首先让我们来看看 [代码]w3c[代码] [代码]css[代码] 转义的说明: https://www.w3.org/TR/css-syntax-3/#escaping Any Unicode code point can be included in an ident sequence or quoted string by escaping it. CSS escape sequences start with a backslash (\), and continue with: Any Unicode code point that is not a hex digits or a newline. The escape sequence is replaced by that code point. Or one to six hex digits, followed by an optional whitespace. The escape sequence is replaced by the Unicode code point whose value is given by the hexadecimal digits. This optional whitespace allow hexadecimal escape sequences to be followed by “real” hex digits. 从这段说明中,我们理解了转义行为具体的逻辑。大致如下图所示: [图片] [代码]\0[代码] 是一个非常特殊的字符,本篇文章不对它进行讨论,有兴趣可以自行搜索相应文档。 可以看到转义逻辑是很简单的,无非就是加 [代码]\[代码] 判断是否是16进制数字,然后进行判断走不同的分支罢了。比如: [代码]<div class="a:">a:</div> [代码] 我们既可以这么写, [代码].a\: { color: red; } [代码] 也可以这么写 [代码].a\3a { color: blue; } [代码] 这[代码]2[代码]个选择器,效果上是等价的,但是它们各自走了不同的转义分支。 什么是合法[代码]css[代码]的表达式 这里我们以最常使用的 [代码]<ident-token>[代码] 为例,我们写的那些具体的选择器的值就需要符合这样的规范,即: [图片] 这类流程图片,相信对正则熟悉的同学,一眼就看懂了。 左半部分 我们先重点看左半部分,可以看到表达式开头必须以 [代码]--[代码] 或 [代码]-[代码],或者 [代码]_[代码], [代码]a-z[代码],[代码]A-Z[代码],[代码]non-ASCII[代码] 开头。 这里解释一下什么是 [代码]non-ASCII[代码],本质上就是非[代码]ASCII[代码]字符,也就是 [代码]code point > 127[代码] 的字符。 接着让我们来看看熟悉可爱的 [代码]ASCII[代码] 表吧。 [图片] 经过对照之后,可以筛选出表达式第一个字母的 [代码]code point[代码] 需要满足的要求是: [代码]code === 45 || // - code === 95 || // _ (code >= 97 && code <= 122) || // a-z (code >=65 && code<=90) || // A-Z code > 127 || // non-ASCII (escape chars) // 或者转义字符 [代码] 所以根据这个规则,所有不在上述范围内的 [代码]ASCII[代码] 字符都需要转义,才能正确表达。注意上面的表达式是不包括数字的哟,所以数字开头的类名,在写 [代码]css[代码] 选择器的时候都要进行转义,不论正负值。比如 [代码]<div class="2">2</div> [代码] 要想写选择器作用在这些元素上,就需要这样写: [代码].\32 { color: red; } [代码] 所以你就了解为什么选择 [代码]class="2"[代码] 的这个 [代码]css[代码] 选择器是 [代码].\32[代码] 了,因为这本质上是一个 [代码]十六进制(hex)[代码] 的字符。[代码]\32[代码] 换算一下就是 [代码]3 * 16 + 2 = 50[代码],而 [代码]50[代码] 这个 [代码]code point[代码] 在 [代码]ASCII[代码] 表里对应的字符就是 [代码]2[代码] ! 让我们再来点进阶的例子: [代码]<div class="2b">2b</div> <div class="2g">2g</div> <div class="-2g">-2g</div> [代码] 对应匹配的 [代码]css[代码] 选择器为(注意注释): [代码]/* 补全6位,不需要跟空格*/ .\000032b { color: blue; } /* 没有补全6位,需要跟空格*/ .\32 b { color: red; } /* 没有补全6位,然而16进制表示的字符范围是 0-f, 而字符g已经超出这个范围,所以空格 可加可不加, 而上面的 .\32 b 必须加空格,不然会认为 \32b 是一个hex数字整体 */ .\32g { color: red; } /* 负数开头,即第一位是'-',第二位是数字的也需要转义 */ .-\32 g { color: red; } [代码] 右半部分 接下来我们来观察表达式的右半部分。 再定义完成前置部分之后,右侧不止可以接受 [代码]_[代码],[代码]a-z[代码],[代码]A-Z[代码],[代码]non-ASCII[代码],也可以接受 [代码]0-9[代码],[代码]-[代码] 这些字符了。用代码来表达则为: [代码]code === 45 || // - code === 95 || // _ (code >= 48 && code <= 57) || // 0-9 (code >= 97 && code <= 122) || // a-z (code >=65 && code<=90) || // A-Z code > 127 || // non-ASCII (escape chars) // 或者转义字符 [代码] 相比左半部分要宽泛一些。这里我给出一些示例: [代码]<div class="a:b">a:b</div> <div class="lg:[&:nth-child(3)]:hover:underline"></div> <div class="bg-[url('/img/hero-pattern.svg')]"> <!-- ... --> </div> <div class="text-[color:var(--my-var)]">...</div> <div class="before:content-['我爱中国\_icebreaker']"> <!-- ... --> </div> [代码] 与之对应的那些样式: [代码]/* 语法错误 : 字符是 ASCII 且不在合法范围内 需要转义为 \: */ .a:b{ color: red; } /* 合法表达式 */ @media (min-width: 1024px) { .lg\:\[\&\:nth-child\(3\)\]\:hover\:underline:hover:nth-child(3) { text-decoration-line: underline; } } .bg-\[url\(\'\/img\/hero-pattern\.svg\'\)\] { background-image: url(/img/hero-pattern.svg); } .text-\[color\:var\(--my-var\)\] { color: var(--my-var); } .before\:content-\[\'\6211\7231\4F60_\4E2D\56FD\\_icebreaker\'\]::before { content: '我爱你 中国_icebreaker'; } [代码] 练习 假如你已经理解了上述内容,可以试试为下方的元素添加对应的生效的样式: [代码]<div class="-">单个-是特殊情况哟</div> <div class="我❤️中国,你好,世界。">我❤️中国,你好,世界。</div> <div class="émotion">émotion</div> <div class="-3:2yo:ur[x'\ds]">-3:2yo:ur[x'\ds]</div> [代码] 参考链接 https://www.w3.org/TR/css-syntax-3/#escaping https://www.w3.org/TR/css-syntax-3/#ident-sequence
2023-06-12 - Gitter - 高颜值 GitHub 小程序客户端诞生记
0. 前言 嗯,可能一进来大部分人都会觉得,为什么还会有人重复造轮子,GitHub第三方客户端都已经烂大街啦。确实,一开始我自己也是这么觉得的,也问过自己是否真的有意义再去做这样一个项目。思考再三,以下原因也决定了我愿意去做一个让自己满意的GitHub第三方客户端。 对于时常关注GitHub Trending列表的笔者来说,迫切需要一个更简单的方式随时随地去跟随GitHub最新的技术潮流; 已有的一些GitHub小程序客户端颜值与功能并不能满足笔者的要求; 听说iOS开发没人要了,掌握一门新的开发技能,又何尝不可? 其实也没那么多原因,既然想做,那就去做,开心最重要。 1. Gitter [图片] GitHub:https://github.com/huangjianke/Gitter,可能是目前颜值最高的GitHub小程序客户端,欢迎star 数据来源:GitHub API v3 目前实现的功能有: 实时查看Trending 显示用户列表 仓库和用户的搜索 仓库:详情展示、README.md展示、Star/Unstar、Fork、Contributors展示、查看仓库文件内容 开发者:Follow/Unfollow、显示用户的followers/following Issue:查看issue列表、新增issue、新增issue评论 分享仓库、开发者 … Gitter的初衷并不是想把网页端所有功能照搬到小程序上,因为那样的体验并不会很友好,比如说,笔者自己也不想在手机上阅读代码,那将会是一件很痛苦的事。 在保证用户体验的前提下,让用户用更简单的方式得到自己想要的,这是一件有趣的事。 2. 探索篇 技术选型 第一次觉得,在茫茫前端的世界里,自己是那么渺小。 当决定去做这个项目的时候,就开始了马不停蹄的技术选型,但摆在自己面前的选择是那么的多,也不得不感慨,前端的世界,真的很精彩。 原生开发:抱着学习的心态,希望尝试下非原生开发体验; WePY:之前用这个框架已经开发过一个小程序,诗词墨客,同样是抱着学习的心态,尝试下其他框架; mpvue:用Vue的方式去开发小程序,个人觉得文档并不是很齐全,加上近期维护比较少,可能是趋于稳定了? Taro:用React的方式去开发小程序,Taro团队的小伙伴维护真的很勤快,也很耐心的解答大家疑问,文档也比较齐全,开发体验也很棒,还可以一键生成多端运行的代码(暂没尝试) 货比三家,经过一段时间的尝试及踩坑,综合自己目前的能力,最终确定了Gitter的技术选型: Taro + Taro UI + Redux + 云开发 Node.js 页面设计 其实,作为一名Coder,曾经一直想找个UI设计师妹子做老婆的(肯定有和我一样想法的Coder),多搭配啊。现在想想,code不是生活的全部,现在的我一样很幸福。 话回正题,没有设计师老婆页面设计怎么办?毕竟笔者想要的是一款高颜值的GitHub小程序。 嗯,不慌,默默的拿出了笔者沉寂已久的Photoshop和Sketch。不敢说自己的设计能力如何,Gitter的设计至少是能让笔者自己心情愉悦的,倘若哪位设计爱好者想对Gitter的设计进行改良,欢迎欢迎,十二分的欢迎! 3. 开发篇 Talk is cheap. Show me the code. 作为一篇技术性文章,怎可能少得了代码。 在这里主要写写几个踩坑点,作为一个前端小白,相信各位读者均是笔者的前辈,还望多多指教! Trending 进入开发阶段没多久,就遇到了第一个坑。GitHub居然没有提供Trending列表的API!!! 也没有过多的去想GitHub为什么不提供这个API,只想着怎么去尽快填好这个坑。一开始尝试使用Scrapy写一个爬虫对网页端的Trending列表信息进行定时爬取及存储供小程序端使用,但最终还是放弃了这个做法,因为笔者并没有服务器与已经备案好的域名,小程序的云开发也只支持Node.js的部署。 开源的力量还是强大,最终找到了github-trending-api,稍作修改,成功部署到小程序云开发后台,在此,感谢原作者的努力。 Trending列表云函数 [代码]// 云函数入口函数 exports.main = async (event, context) => { const { type, language, since } = event let res = null; let date = new Date() const cacheKey = `repositories::${language || 'nolang'}::${since || 'daily'}`; const cacheData = await db.collection('repositories').where({ cacheKey: cacheKey }).orderBy('cacheDate', 'desc').get() if (cacheData.data.length !== 0 && ((date.getTime() - cacheData.data[0].cacheDate) < 1800 * 1000)) { res = JSON.parse(cacheData.data[0].content) } else { res = await fetchRepositories({ language, since }); await db.collection('repositories').add({ data: { cacheDate: date.getTime(), cacheKey: cacheKey, content: JSON.stringify(res) } }) } return { data: res } } [代码] Markdown解析 嗯,这是一个大坑。 在做技术调研的时候,发现小程序端Markdown解析主要有以下方案: wxParse:作者最后一次提交已是两年前了,经过自己的尝试,也确实发现已经不适合如README.md的解析 wemark:一款很优秀的微信小程序Markdown渲染库,但经过笔者尝试之后,发现对README.md的解析并不完美 towxml:目前发现是微信小程序最完美的Markdown渲染库,已经能近乎完美的对README.md进行解析并展示 在Markdown解析这一块,最终采用的也是towxml,但发现在解析性能这一块,目前并不是很优秀,对一些比较大的数据解析也超出了小程序所能承受的范围,还好贴心的作者(sbfkcel)提供了服务端的支持,在此感谢作者的努力! Markdown解析云函数 [代码]const Towxml = require('towxml'); const towxml = new Towxml(); // 云函数入口函数 exports.main = async (event, context) => { const { func, type, content } = event let res if (func === 'parse') { if (type === 'markdown') { res = await towxml.toJson(content || '', 'markdown'); } else { res = await towxml.toJson(content || '', 'html'); } } return { data: res } } [代码] markdown.js组件 [代码]// 云函数解析markdown parseReadme() { const { md, base } = this.props let that = this wx.cloud.callFunction({ // 要调用的云函数名称 name: 'parse', // 传递给云函数的event参数 data: { func: 'parse', type: 'markdown', content: md, } }).then(res => { let data = res.result.data if (base && base.length > 0) { data = render.initData(data, {base: base, app: this.$scope}) } that.setState({ fail: false, data: data }) }).catch(err => { console.log('cloud', err) that.setState({ fail: true }) }) } [代码] [代码]// Markdown渲染 render() { const { data } = this.state return ( <View> { data ? ( <View> <import src='../towxml/entry.wxml' /> <template is='entry' data='{{...data}}' /> </View> ) : ( <View className='loading'> <AtActivityIndicator size={20} color='#2d8cf0' content='loading...' /> </View> ) } </View> ) } [代码] Redux 其实,笔者在该项目中,对Redux的使用并不多。一开始,笔者觉得所有的接口请求都应该通过Redux操作,后面才发现,并不是所有的操作都必须使用Redux,最后,在本项目中,只有获取个人信息的时候使用了Redux。 [代码]// 获取个人信息 export const getUserInfo = createApiAction(USERINFO, (params) => api.get('/user', params)) [代码] [代码]// action export function createApiAction(actionType, func = () => {}) { return ( params = {}, callback = { success: () => {}, failed: () => {} }, customActionType = actionType, ) => async (dispatch) => { try { dispatch({ type: `${customActionType }_request`, params }); const data = await func(params); dispatch({ type: customActionType, params, payload: data }); callback.success && callback.success({ payload: data }) return data } catch (e) { dispatch({ type: `${customActionType }_failure`, params, payload: e }) callback.failed && callback.failed({ payload: e }) } } } [代码] [代码]getUserInfo() { if (hasLogin()) { userAction.getUserInfo().then(()=>{ Taro.hideLoading() Taro.stopPullDownRefresh() }) } else { Taro.hideLoading() Taro.stopPullDownRefresh() } } const mapStateToProps = (state, ownProps) => { return { userInfo: state.user.userInfo } } export default connect(mapStateToProps)(Index) [代码] [代码]// reducers export default function user (state = INITIAL_STATE, action) { switch (action.type) { case USERINFO: return { ...state, userInfo: action.payload.data } default: return state } } [代码] 目前,笔者对Redux还是处于一知半解的状态,嗯,学习的路还很长。 有需要的同学可以前往开源仓库查看相应的完整源码,还请多多指教。 4. 结语篇 当Gitter第一个版本通过审核的时候,心情是很激动的,就像自己的孩子一样,看着他一点一点的长大,笔者也很享受这样一个项目从无到有的过程,在此,对那些帮助过笔者的人一并表示感谢。 当然,目前功能和体验上可能有些不大完善,也希望大家能提供一些宝贵的意见,Gitter走向完美的路上希望有你! 最后,希望Gitter小程序能对你有所帮助!
2019-02-21 - 云函数中实现耗时操作解决方案
起因 在实际开发业务中需要生成带图的表格,由于数据过多导致服务超时。当时我通过在 腾讯云控制台 设置的时间的函数超时600秒,没到时间就超时了。 异常信息如下: WAServiceMainContext.js:2 Error: cloud.callFunction:fail Error: errCode: -501002 resource server timeout | errMsg: ESOCKETTIMEDOUT 后来通过和官方人员沟通得知小程序基础库的 callFunction 接口的默认限制了云函数超时时间的设置为60秒的上限,无法通过腾讯云控制台修改突破限制。 解决方案 如果不是通过callFunction调用的云函数是可以突破限制的,最多可以设置900秒,所以我用了两个云函数来解决这个超时问题。 云函数A用于小程序调用。 云函数B执行耗时操作,设置超时时间为900秒。 小程序调用云函数A,云函数A不用await修饰符调用云函数B,(云函数内互相调用是稳定的)然后云函数A返回调用成功,小程序这边收到云函数A的返回值就知道任务正在执行了,在小程序A里面去数据库存储一条开始执行状态的数据,返回ID。 然后在云函数B执行耗时操作完成去修改数据库的数据状态。 最后在小程序端监听数据库具体ID数据的状态变化来对用户进行反馈。 总结 当然如果数据量超大的话 900秒也会被用完,优化代码是一方面,但是如果代码优化不了的情况下这个时候就需要与产品功能想一个更好的解决方案。 假如900秒最多导出5000条数据,那么超过5000条就可以让用户分页导出,这样的话又可以保证不超时又能满足用户的方案。
2022-09-21 - 云调用subscribeMessage.send出现如下问题如何解决?
一、云调用错误如下: Error: errCode: -501007 invalid parameters | errMsg: subscribeMessage.send:fail missing wxCloudApiToken 二、附源码: async function batchSend(event) { const { messages } = event console.info({event: JSON.stringify(event)}) console.info('处理订阅消息', messages.length) // 循环消息列表 const sendPromises = messages.map(async message => { let { touser, page, data, templateId } = message // 发送订阅消息 await cloud.openapi.subscribeMessage.send({ touser, templateId, page, data, }) }); await Promise.all(sendPromises) } 三、现象描述: 直接云端测试云函数5 次, 结果:失败、成功、失败、成功、成功。一旦出错后,会一直报上述错误。需要调用其他云调用成功一次,才可以恢复。恢复后又是间歇性失败。其他云调用,如“cloud.openapi.wxacode.getUnlimited”从不会失败。总结:这个问题已经追踪了两天了,仍然没有找到必现的规律,失败的概率很大,很容易复现。跟其他人说的miniprogram_statestring参数也无关,因为我一直没有传此参数,默认值为formal。
2021-03-25 - 跨小程序怎么互相发送订阅消息?
我现在有两个小程序,A小程序申请了云开发,B小程序没有申请,我把A的环境共享给了B,实现了环境共享。 要实现一个业务场景,我想用A小程序的云环境给B小程序的用户发送订阅消息,直接调用云函数的api已经失败,因为模板用的是B的,直接报错了。 想问一下这个场景怎么实现?
2021-11-15 - 岁寒之松柏:小程序skyline渲染引擎初尝试
小程序架构介绍 我们都知道小程序本质上是运行在安卓端,苹果端的混合APP,只是微信提供了一套JSBridge,方便用户对一些原生功能和微信相关的功能的进行调用。而微信为了安全和性能的需要,一改以往网络架构中的单线程架构,改为小程序的双线程架构。分别是AppServie 和 Webview 两个线程,我们在小程序中编写的JS代码就是运行在AppService线程的JSCore引擎(类似V8 引擎,一个Js解释器)中,而我们的Wxml和Wxss则会依赖WebView线程进行渲染。 [图片] 目前架构存在的问题 这样的架构虽然已经极大了提高了webview的渲染性能,但是依然会存在一些问题比如: 当页面节点数目过多,很容易发生卡顿 当我们新建一个页面,就要新建一个Webview进行渲染 页面之间共享资源,需要使用Native进行通信,就会消耗更多性能 当AppService(逻辑层)与Webview(视图层)通信也需要依赖Native 所以为了解决这些问题小程序推出Skyline渲染引擎 Skyline引擎介绍 在Skyline环境中,Skyline 会创建了一条渲染线程来负责 Layout, Composite 和 Paint 等渲染任务,并在 AppService 中划出一个独立的上下文,来运行之前 WebView 承担的 JS 逻辑、DOM 树创建等逻辑。说白了就是之前的样式计算是放到渲染线程来处理,现在把和样式相关的逻辑也放到AppService线程中处理,个人猜测这个渲染线程很有可能很有可能就是flutter,这样的架构就极大减少内存的消耗,和线程上通信时间的消耗。原本wxs中的逻辑,也可以移到Appservice线程中运行 [图片] 使用Skyline引擎的使用步骤 在app.json 文件添加 [代码]"lazyCodeLoading": "requiredComponents"[代码] 属性,这是因为Skyline 依赖按需注入的特性。 [代码] { "pages": [ "pages/index/index", "pages/logs/logs", "pages/test/test" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "Weixin", "navigationBarTextStyle": "black" }, "sitemapLocation": "sitemap.json", // 在 app.json 文件添加 "lazyCodeLoading": "requiredComponents" } [代码] 在全局或页面配置中声明为 Skyline 渲染,即 app.json 或 page.json 配上[代码]"renderer": "skyline"[代码] Skyline 不支持页面全局滚动,需在页面配置项加上 [代码]"disableScroll": true[代码],在需要滚动的区域使用scroll-view 实现 Skyline 不支持原生导航栏,需在页面配置项加上 [代码]"navigationStyle": "custom"[代码],并自行实现自定义导航栏 [代码] { "usingComponents": {}, // 在 app.json 文件添加或者page页面的json中添加 "disableScroll": true, "navigationStyle": "custom" //也可以放在App.json文件中 "renderer": "skyline" } [代码] 组件适配,参考 Skyline 基础组件支持与差异 WXSS 适配,参考 Skyline WXSS 样式支持与差异 在本地设置中勾选Skyline渲染调试,如果看不到这个选项框,看一下是否在app.json中配置了[代码]"renderer": "skyline"[代码] [图片] Skyline的 worklet 动画介绍 小程序采用双线程架构,渲染线程(UI 线程)和逻辑线程(JS 线程)分离。[代码]JS[代码] 线程不会影响 [代码]UI[代码] 线程的动画表现,如滚动效果。但引入的问题是,[代码]UI[代码] 线程的事件发生后,需跨线程传递到 [代码]JS[代码] 线程,进而触发开发者回调,当做交互动画(如拖动元素)时,这种异步性会带来较大的延迟和不稳定,[代码]worklet[代码] 动画正是为解决这类问题而诞生的,使得小程序可以做到类原生动画般的体验 worklet函数定义 [代码]function helloWorklet() { 'worklet'; //'worklet'声明该函数为work函数,可以在js线程和UI线程中调用 console.log('hello worklet'); } Page({ onLoad(options) { helloWorklet('hello') // print: hello wx.worklet.runOnUI(helloWorklet)() }, }) [代码] 在小程序控制台可以看到如下输出 [图片] 如果看见SkylineGlobal is not defined错误看看是否开启了Skyline渲染调试 [图片] worklet函数间的相互调用 [代码]function slave() { 'worklet'; return "I am slave" } function master() { 'worklet'; const value = slave() console.log(value); } [代码] 从 UI 线程调回到 JS 线程 [代码]const {runOnUI ,runOnJS} = wx.worklet function jsFun(message) { // 普通函数不需要声明为worklet console.log(message) } function uiFun() { 'worklet'; runOnJS(jsFun)('I am from UI') } [代码] 使用shared共享数据 由worklet函数捕获的静态变量,会在编译期间序列化后生成在UI线程的拷贝环境之中,这就导致我们在JS线程中后续更新了变量,但是在UI线程中时得不到最新的数值的。 [代码]const obj = { name: 'skyline'} function someWorklet() { 'worklet' console.log(obj.name) // 输出的仍旧是 skyline } obj.name = 'change name' wx.worklet.runOnUI(someWorklet)() [代码] 因此shyline使用shared来实现线程之间数据的共享 [代码]const { shared, runOnUI } = wx.worklet const offset = shared(0) function someWorklet() { 'worklet' console.log(offset.value) // 输出的是新值 1 } offset.value = 1 runOnUI(someWorklet)() [代码] 简单案例–实现探探的卡片功能 注意:编辑器版本:1.06.2303162 基础库版本:2.30.2 先看效果 [图片] 代码如下 <br> wxml 代码 [代码]<navigation-bar title="探探" /> <view class="page"> <block wx:for="{{containers}}" wx:key="*this"> <pan-gesture-handler data-id="container-{{index}}" onGestureEvent="handlePan"> <view id="container-{{index}}" class="container" style="z-index: {{zIdnexes[index]}};background-image: url({{partContentList[index]}});"> </view> </pan-gesture-handler> </block> </view> [代码] scss代码 [代码].page{ display: flex; justify-content: center; align-items: center; height: 100vh; width: 100vw; position: relative; .container{ height: 80vh; width: 95vw; background-color: burlywood; position: absolute; border-radius: 16rpx; display: flex; justify-content: center; align-items: center; background-size: cover; .image{ display: block; height: 1067rpx; width: 712rpx; margin: 0 0; } } } [代码] 核心逻辑 [代码]import { useAnimation, setAni, Animation, GestureState } from "./method" Page<{ pos: Animation }, any>({ /** * 页面的初始数据 */ data: { containers: [ "burlywood", "blue", "cyan", "black" ], zIdnexes:[], current:0, partContentList:[] }, /** * 生命周期函数--监听页面加载 */ onLoad() { this.initNode() // 当前node的下标 this.active = wx.worklet.shared(0) // 当前contentList的下标 this.current = wx.worklet.shared(0) this.zIndex = 100000 }, initNode() { // 用与保存shared值 this.Nodes = {} // 图片文件 this.contentList = [ "https://i.hexuexiao.cn/up/ca/63/4a/a32912fc26b8445797c8095ab74a63ca.jpg", "https://th.bing.com/th/id/OIP.kSrrRGx6nqOgWzbaEvVD9AHaNK?pid=ImgDet&rs=1", "https://img.zmtc.com/2019/0806/20190806061552744.jpg", "https://img.zmtc.com/2019/0806/20190806061000600.jpg", "https://img.ratoo.net/uploads/allimg/190523/7-1Z5231J058.jpg", "https://th.bing.com/th/id/R.47de9dfcc25d579d84850d4575d24a6a?rik=%2fGkmrewzIEY4Iw&riu=http%3a%2f%2fimg3.redocn.com%2ftupian%2f20150930%2fqizhimeinvlisheyingtu_5034226.jpg&ehk=rG9Ks2QRzj81mZl38gVGmWVAgCHVLWppoDezpfwdxjo%3d&risl=&pid=ImgRaw&r=0", "https://th.bing.com/th/id/R.95f8e6f6bd5b660ae3ad4f3e0d712276?rik=ELKcha%2bE5ryuiw&riu=http%3a%2f%2f222.186.12.239%3a10010%2fwlp_180123%2f003.jpg&ehk=mVN7AzIRR%2fmVPJYWrWOFbEiher3QWtwSdH%2f%2fe4lE7n8%3d&risl=&pid=ImgRaw&r=0" ] this.data.containers.forEach((_: string, index: number) => { if (index == 0) { this.Nodes[`#container-${index}`] = useAnimation(`#container-${index}`, { x: 0, y: 0 }, this) this.setData({ [`zIdnexes[${index}]`]:100000-index, [`partContentList[${index}]`]:this.contentList[index] }) } else { console.log("10123") this.Nodes[`#container-${index}`] = useAnimation(`#container-${index}`, { x: 0, y: 20, scale: 0.95 }, this) this.setData({ [`zIdnexes[${index}]`]:100000-index, [`partContentList[${index}]`]:this.contentList[index] }) } }); }, handlePan(evt: any) { "worklet"; console.log(evt) const now = this.Nodes[`#container-${this.active.value}`] as Animation const next = this.Nodes[`#container-${(this.active.value+1)%4}`] as Animation if (evt.state == GestureState.ACTIVE) { // 滑动激活状态 // 设置当前的滑动块 now.x.value += evt.deltaX now.y.value += evt.deltaY now.rotate.value = now.x.value * 10 / 360 // 设置下一个滑动块 let rate = Math.abs(now.x.value) / 150 rate = rate > 1 ? 1 : rate next.y.value = (20 - rate * 20) < 0 ? 0 : (20 - rate * 20) next.scale.value = 0.95 + rate * 0.05 } if (evt.state == GestureState.END) { // 滑动结束 if (Math.abs(now.x.value) < 150) { // 判断是否超过界限值 setAni(now.x, 0) setAni(now.y, 0) setAni(now.rotate, 0) } else if (now.x.value < 0) { // 判断判断左划还是右划 setAni(now.x, -2000) setAni(now.y, -2000) setAni(now.rotate, 0) // 通知js线程进行数据的更新 wx.worklet.runOnJS(this.toNext.bind(this))() } else if (now.x.value > 0) { setAni(now.x, 2000) setAni(now.y, -2000) setAni(now.rotate, 0) wx.worklet.runOnJS(this.toNext.bind(this))() } } }, // 将当前序号的跳转到下一个 toNext(){ const current = this.current.value+1 this.active.value = current%4 this.current.value = current this.setData({ current }) if(current-2>=0){ wx.worklet.runOnUI(this.toReset)((current-2)%4) this.setData({ [`zIdnexes[${(current-2)%4}]`]:99998-current, [`partContentList[${(current-2)%4}]`]:this.contentList[current+2] }) } }, // 将动画归位 toReset(index:number){ "worklet"; const reset = this.Nodes[`#container-${index}`] as Animation setAni(reset.x, 0,0) setAni(reset.y, 20,0) setAni(reset.rotate, 0,0) setAni(reset.scale, 0.95,0) } }) [代码] 参考 skyline worklet 动画
2023-03-20 - 如何在云开发中搭建 KV 缓存系统
云开发的数据库在实际使用中的性能是非常不错的。但涉及到一些比较重的计算量时,依然会让查询花费不少的时间(比如提取所有的数据计算排行),在这种情况下,就需要一种方式来优化数据查询,让原本对数据库压力比较大的数据查询消耗更少的数据库性能,降低数据查询耗时,优化数据体验。 一个简单易行的方式就是加入缓存,比如建设一套 KV 缓存系统,来完成数据的优化。 如何在云开发中实现一个简单的 Key - Value 数据库? 想要在云开发中实现一个简单的 Key-Value 数据库,十分简单, 你只需要打开云开发的数据库,在其中创建一个[代码]cache[代码]表,就可以开始编写代码来实现数据的缓存。 需要注意的是,如果你需要让缓存可以在移动端读取,则需要将数据表的权限设置为 所有用户可读,仅创建者可写。 云开发的数据库中,以 [代码]_id[代码] 作为数据主键,并默认为 [代码]_id[代码] 提供了索引,因此,我们只需要将缓存的 key 放在 [代码]_id[代码] 中,就可以借助数据库的索引机制,避免扫描全表来获取数据。同时,还可以借助云开发的 [代码]doc[代码] 方法来快速读取数据,简化代码。 具体的实现,你可以参考下方的代码。 设置缓存 [代码]async function setCache(key,value){ return await db.collection("cache").add({ data:{ _id:key, value:value } }) } [代码] 读取缓存 [代码]async function getCache(key){ let { data } = await db.collection("cache").doc(key).get(); return data.value; } [代码] 如何使用 当你需要设置缓存的时候,你只需要执行 [代码]setCache[代码] 方法,就可以将特定的数据设置到数据库中,并在需要的地方使用 [代码]getCache[代码] 方法来获取这些数据,提升你的数据库查询数据。 借助自建的简易缓存系统,可以快速的完成产品的耗时查询的优化,同时还可以借助官方提供的 API,降低学习的成本,你不再需要去啃厚厚的 Redis 使用教程了。 问题 在这篇文章中,我简单介绍了如何基于云开发开发出一个简单的缓存系统,那么你发现这个缓存系统的一些问题了么?如果你要优化这个系统,你会怎么做?
2020-10-02 - 小程序开发新能力解读
这个月小程序释放了什么新能力?又有哪些新规则?收藏课程,及时了解小程序开发动态,听官方为你解读新能力。
2023-01-17 - 做个优秀的小程序 - 体验评分
随着小程序的开发迭代,慢慢的我们会更加关注小程序的质量,今天来讲讲小程序的隐藏功能 -- 体验评分。 为什么需要体验评分 我们多做一点,就可以给用户更好的体验。(窃喜) 当然,做为开发者的我们,动动鼠标点一点就能帮助我们发现问题,是不是很愉快~~ 接下来我们来看看怎么使用体验评分? 怎么使用体验评分 体验评分的能力目前开放在【微信开发者工具 - 调试器 - Audits】 操作步骤:运行体验评分 - 一顿操作 - 获取体验报告 - 一顿优化。 (优化其实是一个圈,新代码加上之后也要继续关注哦~) [图片] 体验评分实践 我们用《小程序示例》来操作一波看看效果~ 01. 运行体验评分 使用开发者工具打开小程序,调试器区域切换到 Audits 面板,就一个“运行”按钮,点它。 [图片] 02.一顿操作 然后在工具上对小程序进行操作,比如:我点开了 “接口 - 媒体 - 音频 - 播放 ”。 [图片] 03.获取体验报告 操作完之后,点击“停止”,我们就可以获取到体验报告(简单~)。 [图片] 拿到报告之后,我们就可以看到总分 98,最佳实践 80。往下拉会有扣分的实际原因。 看第一条是 “发现正在使用废弃接口”,报告已经很清楚的告诉我们使用了废弃组件 audio,我们根据报告进行优化即可。 [图片] 04.一顿优化 按照报告优化完之后,我们可以继续进行体验评分功能确认优化是否完善。这是一个有用的圈圈⚪⚪⚪ 我们来讲几个优化过程中遇到的问题,咳咳咳 存在图片没有按原图宽高比显示 [图片] 在测试预览图片的时候,发现图片被挤了,体验评分告诉我们宽高比有问题,发现是 <image> 使用了默认的 mode (scaleToFill:缩放模式,不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素)。所以通过添加 mode="aspectFill" (缩放模式,保持纵横比缩放图片,只保证图片的短边能完全显示出来。)来解决宽高比的问题。 [图片] [图片] 发现固定底部的可点击组件可能不在iPhone X安全区域内 [图片] 这个问题我们用手机测试是正常的,但是体验评分给了提示,所以就来看看实现方式是不是有问题: 原有方式:通过接口监听systemInfo.model.indexOf('iPhone X') 给 view 添加专属 class 官方推荐:官方推荐的方式是用 wxss 来兼容,不一定只有 iPhone X 下面会有安全区域 [图片] 发现正在使用废弃接口 [图片] 这个问题对一些老旧代码来说很有用,比如示例很久之前写的 auto-focus,由于基本没有改动,所以代码就一直保持不变。使用体验评分的时候检测到了这个属性是废弃属性,所以我们更换了可用属性 focus 来解决问题。 [图片] 体验评分总结 使用体验评分进行小程序示例的优化,有以下优点: 可以发现代码中使用的废弃api,避免后续踩坑根据实际操作发现相关耗时久的情况,预先发现体验问题合理的视觉/交互检测,提前做好兼容资源使用检测,用合适的资源做好小程序当然,体验过程中也有不足: 开发者工具不支持预览的 组件 / API 暂不支持体验评分(听说官方已经在努力推进啦)一起体验评分 如果你也在做小程序优化,欢迎使用体验评分来优化哦~ 预祝大家都拿 100婚 !!! [图片] 体验评分文档传送门 如果你有疑问,请在下方评论区留言给binnie,㊗️大家都没有bug,✌️✌️✌️
2020-12-04 - 微信开发者工具使用 ESLint 插件的正确方法分享
看到很多帖子反馈开发者工具中的 ESLint 插件无法生效,自己测试也确实如此。 ESLint 插件错误提示怎么生效?小程序中如何使用eslint进行代码的实时检测 经过研究发现,最核心的一个点在于:要安装与小程序中的 ESLint 插件相匹配的 ESLint 版本。 解决过程如下: 1. 首先查看小程序中 ESLint 的插件版本为 v2.1.16,查看该插件的 版本日志 发现:最后一次正式版(v2.1.10)的发布日期是 2020 年 10 月 12 日。 [图片][图片] 2. 然后查看 ESLint 版本日志 中该日期附近的版本,比较接近是 2020 年 10 月 9 号发布的 v7.11.0 版本。 [图片] 3. 项目安装 ESLint v7.11.0 版本,并进行配置(查看 ESLint 教程,此处不赘述)。 npm i eslint@7.11.0 --save-dev [图片] 4. 重启工具,验证可用! [图片]
2023-02-16 - IOS scroll-view中的自定义组件fixed问题
这个是正常现象,因为 iOS 下加了 -webkit-overflow-scrolling: touch,这个会产生滚动惯性,体验更好,但会改变 fixed 的行为,建议不在 scroll-view 里有 fixed 元素
2020-04-23 - WebAudioContext 和 InnerAudioContext 区别?
WebAudioContext 和 InnerAudioContext 区别
2022-02-17 - wx.createInnerAudioContext()
[图片] [图片] 真机和体验版innerAudioContext.onError报错set audio src "wxfile://tmp_218fb3930ab36d39ac21d39e4dfc1121.mp3" fail: undefined is not an object (evaluating 't.duration')
2022-11-17 - 【手把手喂饭】Behavior教程:如何给每个页面混入统一的分享-share?
1、创建一个 js文件,我放在了个根目录下的 utils 里面 ,你也可以放在 behavior文件夹下(官方示例)取名为 share.js [图片] share.js 代码如下: let title = '你的默认分享主题' let imageUrl= '你的默认分享图片地址' let path = 'pages/index/index' // 默认放了个首页 module.exports = Behavior({ methods: { onShareAppMessage() { return { title, path, imageUrl } }, onShareTimeline() { return { title, path, imageUrl } } } }) 有没有很简单?我自己的 还加入了 默认的分享链接,还能知道是谁分享的 (大家可忽略) let userInfo = getApp().globalData.userInfo path = 'pages/index/index?share=' + userInfo._id 具体使用 ,在页面中引入 share.js,再配置一下就好 [图片] import share from "../../utils/share" Page({ // options: { // pureDataPattern: /^_/ // 指定所有 _ 开头的数据字段为纯数据字段 // }, behaviors: [share], // 混合 data: { 喂饭完毕~
2023-02-03 - 【小程序技巧】如何让长文本超过限定行数自动折叠,并且可以展开收起
这是去年在校做项目遇到的一个需求,文章沉在草稿箱里一直没写完,主要分享一下如何实现长文本的折叠展开。 长文本超过限定行数自动折叠,点击长文本或者按钮,实现展开收起效果。这类效果其实在平时的app中或者网站中很常见,举几个栗子: 微信朋友圈: [图片] 新浪微博: [图片] 分析需求 1、文本超长省略,主要是通过 line-clamp 实现: [代码].text-clamp2 { overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } [代码] 文本效果: [图片] 2、如何判断文本是否超出两行,显示「全文」「收起」按钮呢? [图片] 通过上图我们可以发现,当文本区域省略时,它的高度会相对变小,那么我们只需要获取到不省略和省略时的文本区域高度,进行比较就能知道是否超出了两行。 [图片] 思路解决了,怀着喜悦的心情翻看了一下文档:咦?为什么小程序没有像 js 那样操作 dom 节点的接口?那还怎么获取元素的尺寸高度!好在功夫不负有心人,终于在文档找到类 DOM 操作的 API「SelectQuery」。 实现需求 3、什么是 SelectQuery?如何去使用它? 从文档(传送门)描述来看 SelectQuery 是一个查询节点信息的对象,它可以选择匹配选择器的所有节点以及显示区域内的节点信息。既然它可以类似 jQuery 那样去匹配选择器,那么我们可以获取到需要的高度信息了。 [代码]// wxml <view class="contentInner1 text-clamp2">小程序是一种新的开放能力,开发者可以快速地开发一个小程序。小程序可以在微信内被便捷地获取和传播,同时具有出色的使用体验。</view> <view class="contentInner2">小程序是一种新的开放能力,开发者可以快速地开发一个小程序。小程序可以在微信内被便捷地获取和传播,同时具有出色的使用体验。</view> [代码] [代码]// js wx.createSelectorQuery().selectAll(".contentInner1, .contentInner2").boundingClientRect(res => { console.log(res) }).exec() [代码] 查询结果(文本区域省略时高度为 52px、不省略时为 104px,只要 res[0].height < res[1].height,此时就应该显示展开收起按钮 ) [图片] 4、逻辑设计上的优化 由于论坛帖子不只一个,我们得匹配对应的两个长文本节点,如果都给一个唯一的选择器,那么在页面中一次性查询这么多节点,很明显这不是最优的。 实际上我们可以将这封装成一个自定义组件,可供每个页面循环复用,在组件内我们只需要关注 单个 长文本的节点信息,不需要一次性获取当前页面的所有长文本节点,更重要的是:在组件内每个长文本的展开与收起状态都是独立的,也省去了在页面内定义字段去标识每个帖子的展开状态。 5、实现效果 [图片] [图片] 6、参数说明 属性 类型 默认值 说明 content String “示例文本” 长文本内容 maxline Number 1 最多展示行数[只允许 1-5 的正整数] position String “left” 展开收起按钮位置[可选值为 left right] foldable Boolean true 点击长文本是否展开收起 最后附上代码片段,有疑问欢迎在下方留言或者发社区私信(三连暗示) [图片]
2021-12-30 - Comi - 小程序 markdown 渲染和代码高亮解决方案
写在前面 Comi 读 ['kəʊmɪ],类似中文 科米,是腾讯 Omi 团队开发的小程序代码高亮和 markdown 渲染组件。有了这个组件加持,小程序技术社区可以开始搞起来了。 体验 [图片] 感谢【小程序•云开发】提供技术支持。 预览 [图片] Comi 基于下面的 5 个组件进行开发: prismjs wxParse remarkable html2json htmlparser 先看 Comi 使用,再分析原理。 使用 先拷贝 此目录 到你的项目。 js: [代码]const comi = require('../../comi/comi.js'); Page({ onLoad: function () { comi(`你要渲染的 md!`, this) } }) [代码] wxml: [代码]<include src="../../comi/comi.wxml" /> [代码] wxss: [代码]@import "../../comi/comi.wxss"; [代码] 简单把! 在 omip 中使用 先拷贝 此目录 到你的项目。 js: [代码]import { WeElement, define } from 'omi' import './index.css' import comi from '../../components/comi/comi' define('page-index', class extends WeElement { install() { comi(`你要渲染的 md`, this.$scope) } render() { return ( <view> <include src="../../components/comi/comi.wxml" /> </view> ) } }) [代码] WeElement 里的 this 并不是小程序里的 this,需要使用 [代码]this.$scope[代码] 访问小程序 Page或 Component 的 this。 css: [代码]@import '../../components/comi/comi.wxss'; [代码] 原理 在开发 Comi 之前,我们进行了预研,是否有必要造这个轮子。 代码高亮预研 wxParse 只是用标签包括代码,并未处理代码转成 WXML,所以渲染出的代码是没有颜色 老牌的 highlightjs 没有 WXML 对应的方案 老牌的 highlightjs 对 JSX 高亮支持太差 prismjs 是 react 官方使用的高亮插件,对 JSX 支持高亮很好 prismjs 支持几乎所有的语言,并且支持自定义扩展语言 prismjs 拥有 Line Highlight 插件(目前还未移植到 Comi) 综合上面信息,决定基于 prismjs 二次开发。 markdown 渲染预研 wxParse 老牌的渲染组件,支持 markdown wxParse 内置的 showdownjs 不满足代码高亮的格式需求(比如语言种类也会生成一个标签,当然可以通过 wxss 隐藏) 小程序基础库 1.4.0 开始支持 [代码]rich-text[代码] 组件展示富文本,但是格式需要转成 json 高性能 remarkable,Facebook 和 Docusaurus 都在使用,支持 md 语法修改和扩展 [代码]<rich-text nodes="{{nodes}}" bindtap="tap"></rich-text> [代码] [代码]Page({ data: { nodes: [{ name: 'div', attrs: { class: 'div_class', style: 'line-height: 60px; color: red;' }, children: [{ type: 'text', text: 'Hello World!' }] }] }, tap() { console.log('tap') } }) [代码] 综合上面信息,放弃 rich-text,决定基于 wxParse + remarkable 二次开发,移除 showdownjs。Comi 需要 remarkable 的高性能和灵活性。markdown 会持久化存在 db, 在小程序内运行时转换成 wxml,所以对性能还是有一定要求。 劫持 prismjs tokens [代码]tokens: function(text, grammar, language) { var env = { code: text, grammar: grammar, language: language }; _.hooks.run('before-tokenize', env); env.tokens = _.tokenize(env.code, env.grammar); _.hooks.run('after-tokenize', env); for (var i = 0, len = env.tokens.length; i < len; i++) { var v = env.tokens[i] if (Object.prototype.toString.call(v.content) === '[object Array]') { v.deep = true this._walkContent(v.content) } } return env.tokens }, [代码] [图片] 这段代码增加 tokens 方法到 prismjs 中,原库自带的 prism.highlight 的会把 tokens 转成 html,因为我们的目标的 wxml,所以这里提前把 tokens 作为方法返回值。当然还做了一件事,就是扩展了 token item 的 deep 属性来决定是否需要继续向下遍历生成 wxml。 原始的 jsx: [代码]render() { const { tks } = this.data return ( <view class='pre language-jsx'> <view class='code'> {tks.map(tk => { return tk.deep ? <text class={'token ' + tk.type}>{ tk.content.map(stk => { return stk.deep ? stk.content.map(sstk => { return <text class={'token ' + sstk.type}>{sstk.content || sstk}</text> }) : <text class={'token ' + stk.type}>{stk.content || stk}</text> })}</text> : <text class={'token ' + tk.type}>{tk.content || tk}</text> })} </view> </view> ) } [代码] jsx 编译出生成的 wxml,把这段 wxml 嵌入到 wxparse 里: [代码]<!-- 千万 不要格式化下面的 wxml,不然 text 嵌套 text 导致换行全部出来了 --> <template name="wxParseCode"> <view class="pre language-jsx"> <view class="code"> <block wx:for="{{item.tks}}" wx:for-item="tk"> <block wx:if="{{tk.deep}}"><text class="{{'token ' + tk.type}}"><block wx:for="{{tk.content}}" wx:for-item="stk"><block wx:if="{{stk.deep}}"><text class="{{'token ' + sstk.type}}" wx:for="{{stk.content}}" wx:for-item="sstk">{{sstk.content || sstk}}</text> </block> <block wx:else><text class="{{'token ' + stk.type}}">{{stk.content || stk}}</text> </block> </block> </text> </block> <block wx:else><text class="{{'token ' + tk.type}}">{{tk.content || tk}}</text> </block> </block> </view> </view> </template> [代码] 这段 wxml 不能进行格式化美化,不然多出许多换行符,因为 text 嵌套 text 会保留换行符!! 修改 wxparse 里的分支逻辑: [代码]<block wx:elif="{{item.tagType == 'block'}}"> <view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}"> <block wx:if="{{item.tag == 'pre'}}"> <template is="wxParseCode" data="{{item}}" /> </block> <block wx:elif="{{item.tag != 'pre'}}" > <block wx:for="{{item.nodes}}" wx:for-item="item" wx:key=""> <template is="wxParse1" data="{{item}}" /> </block> </block> </view> </block> [代码] 当 [代码]item.tag[代码] 为 [代码]pre[代码] 的时候使用 wxParseCode 模板,数据传入 item。item 的数据从哪里来? 先修改 md 渲染器为 Remarkable: [代码]} else if (type == 'md' || type == 'markdown') { var converter = new Remarkable() var html = converter.render(data) transData = HtmlToJson.html2json(html, bindName); } [代码] 使用上面的 prism.tokens 计算出代码片段的 tokens,用于 wxparse 的模板渲染: [代码]function transPre(transData) { transData.nodes.forEach((node, index) => { if (node.tag == 'pre') { var lan = 'markup' if (node.nodes[0].classStr) { lan = node.nodes[0].classStr.split(' ')[0].replace('language-', '') } var tks = prism.tokens(node.nodes[0].nodes[0].text, prism.languages[lan], lan) transData.nodes[index].tks = tks } }) } [代码] language- 支持多少种呢?目前 comi 默认支持: markup css clike javascript bash json typescript jsx tsx 默认使用的主题 css 是 okaidia。如果 comi 默认的配置不支持你的需求,你可以: 进 https://prismjs.com/download.html 这里自行下载 劫持 prismjs tokens 拷贝进你下载的 prismjs 里 把 prismjs 拷贝替换掉 comi 自带的 prismjs 精简 comi 使用流程 WXML 提供两种文件引用方式 import 和 include。和 import 不同,include 可以将目标文件除了 template 和 wxs 外的整个代码引入,相当于是拷贝到 include 位置,如: [代码]<!-- index.wxml --> <include src="header.wxml" /> <view>body</view> <include src="footer.wxml" /> [代码] [代码]<!-- header.wxml --> <view>header</view> [代码] [代码]<!-- footer.wxml --> <view>footer</view> [代码] comi 利用了 import 和 include 特性简化使用流程: comi.wxml [代码]<import src="./wxParse.wxml"/> <template is="wxParse" data="{{wxParseData:article.nodes}}"/> [代码] comi.js [代码]var WxParse = require('./wxParse.js'); module.exports = function comi(md, scope) { WxParse.wxParse('article', 'md', md, scope, 5); } [代码] comi.wxss [代码]@import './wxParse.wxss'; @import './prism.wxss'; [代码] 使用时,只需要 : import [代码]comi.js[代码] include [代码]comi.wxml[代码] import [代码]comi.wxss[代码] 另外,在 omip 使用 comi 时候发现不会拷贝 include 的文件到 dist,发现 taro/omip 的正则没有去匹配 include 文件,所以,把: [代码]exports.REG_WXML_IMPORT = /<[import](.*)?src=(?:(?:'([^']*)')|(?:"([^"]*)"))/gi [代码] 改成: [代码]exports.REG_WXML_IMPORT = /<[import|inculde](.*)?src=(?:(?:'([^']*)')|(?:"([^"]*)"))/gi [代码] 搞定。 开始使用吧 Github Powered by Omi Team
2019-04-09 - 云托管部署时,提示已经在机器上了,但其实是新建版本的,有知道是怎么回事的吗?
查看部署日志,发现有一句:Container image "ccr.ccs.tencentyun.com/tcb-......:smile-score-app-007-20220214104808" already present on machine; Back-off restarting failed container; Readiness probe failed: dial tcp 10.0.64.15:8080: connect: connection refused; ] 现在这个版本的状态是 状态 端口异常
2022-02-14 - 云托管部署失败?
环境id:prod-1gre0jx010022bea;服务名称:express-bb96;版本:express-bb96-006;状态:构建失败
2022-04-25 - 微信云托管docker部署mvn运行失败?
FROM maven:3.8.4-openjdk-17-slim as maven # 指定构建过程中的工作目录 WORKDIR /app # 将src目录下所有文件,拷贝到工作目录中src目录下 COPY src /app/src # 将pom.xml文件,拷贝到工作目录下 COPY pom.xml /app # 执行代码编译命令 RUN mvn -f /app/pom.xml clean package [2021-12-22 16:58:01] Step 8/14 : RUN mvn -f /app/pom.xml clean package [2021-12-22 16:58:01] ---> Running in 977f1c20df2e [2021-12-22 16:58:01] ls: cannot access '/usr/bin/mvn': Operation not permitted [2021-12-22 16:58:01] Error: Could not find or load main class org.codehaus.plexus.classworlds.launcher.Launcher [2021-12-22 16:58:01] Caused by: java.lang.ClassNotFoundException: org.codehaus.plexus.classworlds.launcher.Launcher [2021-12-22 16:58:02] The command '/bin/sh -c mvn -f /app/pom.xml clean package' returned a non-zero code: 1 script returned exit code 1
2021-12-22 - 视频号的视频尺寸?
视频号标准尺寸多大合适? 或者是有哪几个尺寸呢?
2020-10-30 - 避免分包加载引起的页面重复打开问题
你有经历过在小程序里进行页面跳转时页面被重复打开的情况吗? 如果有的话您可以继续往下看。 随着我们业务的拓展,功能的增加小程序包体变大,我们不得不使用小程序分包来解决首屏加载问题。 在解决问题的时候,必将会引起另一些问题,比如: 小程序启动之后,用户通过按钮打开未访问过的分包路由页面,跳转时由于分包加载数据会耗时,界面上的元素仍然可以响应用户的操作,如果用户不小心或由于焦急重复点击按钮后,按钮的事件处理函数就会随着点击重复执行,页面会被重复打开好几次。 但一旦页面被加载过,跳转是非常快的,就很难出现重复点击的情况。 例如在下面路由配置中 { "subPackages": [ { "root": "pages/index/", "pages": [ "index" ] }, { "root": "pages/post/", "pages": [ "details", "list", ] } ] } 我们给首页的Page里添加一个点击事件来跳转到我们的文章页面 handleArticleTap(e){ const pid = e.currentTarget.dataset.pid; consturl =`pages/post/details?pid=${pid}; wx.navigateTo({url}); } 我们打开页面 pages/index/index 来访问小程序的首页,首页上存在一些推荐文章的链接按钮,用户可通过这些按钮来查看我们的文章。 在实际情况中,我们偶尔会观察到诡异的现象:我们的文章页面被重复打开了。 用户首次通过点击跳转按钮来跳转到pages/post/details?pid=1 时,pages/post/details 页面未被小程序框架加载,页面会处于假死状态,在Android上可以明显的看到系统显示【模块加载中】 这样的提示。 重复打开页面会给用户带来不必要的麻烦。 解决这个问题,那就是上锁 wx.navigateTo 给我们提供了一个success接口,我们的处理函数可以稍作修改就解决这个问题? handleArticleTap(e){ if (this.handleArticleTapLock){ return; } this.handleArticleTapLock=true; const pid = e.currentTarget.dataset.pid; const url =`pages/post/details?pid=${pid}; wx.navigateTo({ url, success()=>{ this.handleArticleTapLock=false; } }); } 文档上并没有描述success的触发时机,然后我就亲手试了一下,发现果然不行哦。 页面还没被打开navigateTo就success了!! 那我们就试试通过Page的onShow生命周期上锁: onShow(){ this.handleArticleTapLock=false; }, handleArticleTap(e){ if (this.handleArticleTapLock){ return; } this.handleArticleTapLock=true; const pid = e.currentTarget.dataset.pid; const url =`pages/post/details?pid=${pid}`; wx.navigateTo({ url }); } 这下只能点一下了,除非页面退回来,点再多下都不管用。
2020-01-07 - InnerAudioContext 调用destroy方法后内存为什么没有释放?
按顺序播放多个歌曲,打开调试后,发现内存一直上涨,并不会降下来。 每次播放下一首歌曲前,都会调用destroy销毁上一首歌曲。
2022-12-02 - 小程序引入了一个自定义组件,想问下小程序页怎么调用组件内的方法?
如题
2020-05-07 - 自定义组件如何获取外部文件?
[图片]获取不到fileurl
2022-05-30 - 自定义组件中如何调用带形参的外部js方法?
如图,打算求消耗的时间占总时间的百分比,将其用到progress上。打算用一个外部js方法来实现,但是在传参的时候报了bug,查了一些资料还是没能解决这个问题,想知道应该怎么修改,或者如果不能这么写的话,应该怎么写来实现这个功能?谢谢 [图片] [图片] [图片] [图片]
2020-04-09 - slider能不能在指定位置上加一个点?
用slider做音乐播放器,然后音乐有免费时长要在滑动条上加一个断点,怎么能根据音频时长在滑动条上加一个点呢?
2022-05-18 - Skyline|小程序吸顶、网格、瀑布流布局都拿下~
在之前的文章中,我们知道了新 scroll-view 可以让小程序的长列表做到丝滑滚动~ 也提到了新 scroll-view 提供了很多新能力 sticky、网格布局、瀑布流布局等,这一篇,我们就来看看这些新能力是怎么使用的~ 新 scroll-view 在原来列表模式(type="list")的基础上,新增了自定义模式(type="custom") 在自定义模式下,新增了以下新组件供开发者调用 list-view:列表布局容器sticky-section / sticky-header:吸顶布局容器grid-view:网格布局容器,可实现网格布局、瀑布流布局等sticky布局sticky 布局即在应用中常见的吸顶布局,与 CSS 中的 position: sticky 实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。 常见的使用场景有:通讯录、账单列表、菜单列表等等。 与 position: sticky 不同的是,position: sticky 很难实现列表滚动需要的交错吸顶效果,而 sticky 组件则可以帮忙开发者轻松实现交错吸顶的效果。 sticky 的使用非常简单: 将 scroll-view 切换到 custom 模式采用 sticky-section 作为 scroll-view 的子元素sticky-header 放置吸顶内容list-view 放置列表内容 {{item.name}} ... 我们来看下采用 sticky 布局做出来的通讯录效果~ [视频] sticky 布局也可以通过给 sticky-section 配置 push-pinned-header 来声明吸顶元素重叠时是否继续上推 像下图输入框和标签列表这种类型,标签列表吸顶时还是希望保留输入框吸顶。 [视频] 网格布局网格布局即将列表切割成格子,每一行的高度固定,常见的视频列表、照片列表等通常都采用网格布局。 在此之前,实现网格布局需要开发者自行实现网格切割,再嵌入到 scroll-view 中。 新 scroll-view 直接提供了 grid-view 组件供开发者使用~ 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 aligned 做为直接子节点grid-view 中直接编写列表 ... 下面是使用网格布局实现的图片列表效果~ [视频] 瀑布流布局瀑布流布局与网格布局类似,不同的是瀑布流布局中每个格子的高度都可以是不一致的,所以在小程序中实现瀑布流布局就比较复杂了。 开发者需要通过计算格子高度,然后再进行瀑布流拼接,当滚动内容过多时还需要处理节点过多导致内存不足等问题。 grid-view 组件直接支持了瀑布流模式供开发者直接使用,grid-view 组件会根据子元素高度自动布局: 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 masonry 做为直接子节点grid-view 中直接编写列表 ... 下面是使用瀑布流布局实现的图片列表效果~ [视频] 想要立即体验?现在通过微信开发者工具导入 代码片段,即可体验新版 scroll-view 组件能力~
2023-08-03 - 视频号的推荐机制是不是有问题
视频号短视频不喜欢可以长按视频,点不感兴趣,然后会减少播放此类视频;不喜欢作者,可以点头像,不看对方动态,结果倒好,不点还好,越点越多。我真的没搞明白,目前我点一个,就给我推荐三个。我只能说,算你牛逼。本人不是针对任何人,就是不喜欢看一些跟自己无关的信息而已,结果你这么玩我,点到我怀疑人人生,一个号三只羊,35个号105个羊了,数羊羊呢?这种推荐机制是不是得好好处理下? 目前,我发现,当天是真的越点不看推荐越多,隔了几天后,好像是好多了,但是,你这也不合理啊。太影响体验了。 [图片]
2023-01-01 - wxs使用正则表达式
**.xws中不能直接new RegExp()或者 var reg = / / 字面量声明正则表达式 通过内置方法 getRegExp() 这是将手机号分割为 3-4-4的格式的案例 function splitPhone(mobile) { if (mobile.length !== 11) return mobile; var mobileReg = getRegExp("(?=(\d{4})+$)", "g"); return mobile.replace(mobileReg, "-"); }
2022-12-26 - 怎么在小程序里面实现点按翻译
首发于公众号 iKeepLearn [图片] #0 English Podcast (原 BBC English Podcast)一直没有怎么维护。最近刚好有时间,就登录后台看看用户反馈。有没有什么值得更新的。其中呼声最高的就是希望加入翻译功能。所以就先实现这一点吧。 #1 确定需求后,就先搜一下其他小程序,看看他们实现的效果。中国日报社和微信研究院联合推出的每日英文电台就刚好有这个功能。试了一下效果不错。所以这个需求点是可实现的。 那怎么实现呢?毕竟小程序里面不像网页和 APP 那样支持长按选择文字并弹出自定义菜单项,官方支持选择文字的就 text 组件。但没有相关的 api 去获取选定的内容? 在搜索了一圈后也没有找到很好的实现,那就只好自己来实现。 #2 思考了一下,只有用常见的支持绑定点击事件的 view 了。思路是先把英文句子按单词分割,然后每一个单词绑定相应的事件。这样就能获取点按的单词了,再调用接口去获取翻译内容。 所以数据结构按这样设计 [图片] 思路确定后,相关编码就很简单了。 wxml文件和wxss文件 [图片] #3 在找翻译接口的时候,有谷歌,必应、百度和有道作为备选。最后思考了一下还是选了微信官方的接口。这样可以避免关键字检测? 本来在微信服务市场找到的英中翻译接口是最适合需求的,因为不需要服务器调用可以直接在小程序客户端里使用。只是按文档接入后提示出错了。只好选择多语言翻译接口。 [图片] #4 最后可以扫码下方的小程序体验点按翻译 [图片]
2022-12-19 - 微信支付分账和退款时的踩坑记录
先添加分账收款方: 1.可以是appId对应的个人openid 2.或者其他商户mchId 分账比例默认30%,由发起分账方申请或者设置。 申请更高分账比例,可以参考帖子进行申请 https://developers.weixin.qq.com/community/develop/article/doc/00042e3a5b4d78f5f06bcdfb951c13 分账的订单支付后资金进入冻结状态 注意:实际可分配的冻结资金需要减掉微信手续费(默认0.6%) 总分账金额不能大于订单金额的30% 微信端对于分账订单退款的处理: 1.订单未分账,直接从冻结金额中退款 2.订单部分分账 若退款金额小于剩余冻结金额,直接从剩余冻结中退款 若退款金额大于剩余冻结金额,需要先解冻剩余金额,并从商户余额中退款。 3.订单分账已完成,直接从商户余额中退款。 先发生退款后分账的订单,需要注意 1.总分账金额不能大于订单金额的30% 2.可分账金额=(订单金额-已退款金额)*(1-0.6%);减掉微信手续费
2022-12-27 - Skyline|长列表也可以丝滑~
[图片] [图片] 对于长列表出现的白屏、卡顿、界面跳动等问题,小程序提供了新 scroll-view 来解决这一系列问题。我们先来看看效果~ 快速滚动效果对比我们通过一组长列表来展示新旧 scroll-view 在快速滚动下的效果对比。 当长列表快速滚动时,旧 scroll-view 容易出现白屏的情况,新 scroll-view 则不会出现白屏。 左:旧 scroll-view、右:新 scroll-view [视频] 在安卓机器快速滚动过程中,旧 scroll-view 反应卡顿,容易出现手指离开操作时,滚动动画还在进行。 而新 scroll-view 则可以保持界面滚动效果跟随手指,停止滚动则缓慢结束动画效果。 左:旧 scroll-view、右:新 scroll-view ,测试机型:Xiaomi MIX 3 [视频] 反向滚动效果对比在对话等场景下,反向滚动是常见的功能,旧 scroll-view 并没有提供反向滚动的能力,我们来看看旧 scroll-view 下是怎么完成反向滚动的~ 在对话数据在加载的时候,对话列表需要在更新完列表数据之后,再使用 scroll-into-view 或者 scroll-top 来保持当前滚动位置,因为设置滚动位置会有延迟,所以容易出现 界面跳动 的情况。 // .js // scroll-view 滚动到顶部时触发 bindscrolltoupper() { // 先更新列表数据 this.setData({ recycleList: getnewList() }, () => { // 更新完数据后再设置滚动位置 this.setData({ scrollintoview: scrollintoview }) }) } 为了解决界面跳动的问题,社区上也有通过翻转的方法来解决,将 scroll-view 与 scroll-view 的子元素进行翻转。 // .wxss .reserve { transform: rotateX(180deg); } // .wxml 然而进行翻转之后,会遇到手指滚动方向与列表滚动方向相反、scroll-into-view 属性无效等问题。 为了帮开发者们解决反向滚动类列表的一系列问题,新 scroll-view 直接提供了 reverse 属性支持反向滚动的能力,滚动效果更加顺滑。 左:旧 scroll-view、右:新 scroll-view(图片加载期间,GIF 渲染较慢) [视频] 怎么接入新 scroll-view ?新的 scroll-view 使用起来很简单,主要有以下两个步骤: 修改小程序配置scroll-view 增加 type="list"// app.json // "renderer": "skyline" 开启之后所有页面会变成自定义导航,可参考 https://developers.weixin.qq.com/s/Y5Y8rrm37qEY 实现自定义导航 // 也可在 page.json 中配置 "renderer": "skyline" 逐个页面开启 { ... "lazyCodeLoading": "requiredComponents", "renderer": "skyline" } // page.json { ... "disableScroll": true, "navigationStyle": "custom" } // page.wxml ... // 反向滚动 新的 scroll-view 从安卓 8.0.28 / iOS 8.0.30 开始支持,如需兼容低版本需要进行兼容处理。 wx.getSkylineInfo({ success(res) { if (res.isSupported) { // 使用新版 scroll-view } else { // 使用旧版 scroll-view } } }) 如需体验长列表效果,可在微信开发者工具导入该代码片段即可体验:https://developers.weixin.qq.com/s/Y5Y8rrm37qEY 更多接入详情请参考文档 怎么做到的?大家肯定好奇为什么新 scroll-view 可以解决这个头疼的问题呢? 我们来对比一下新旧 scroll-view 有什么区别就可以知道答案啦~ 旧 scroll-view 逻辑层与渲染层的通信需要通过 JSBridge 进行转换,需要一定的时间开销渲染采用异步分块光栅化,当渲染赶不上滚动的速度,来不及渲染则会出现白屏渲染大量节点内存占用高,需要开发者自行优化只渲染在屏节点,开发成本高新 scroll-view 逻辑层与渲染层的通信无需通过 JSBridge 进行转换,减少了大量通信时间开销渲染采用同步光栅化,滚动与渲染在同一线程,不会出现白屏针对长列表进行优化,只渲染在屏节点,内存占用低,减少了一些渲染耗时,且开发接入成本低[图片] 除此之外,新 scroll-view 后续将提供 type="custom" 支持 sticky 吸顶效果、网格布局、瀑布流布局等能力便于开发者接入使用~
2023-08-03 - 云开发支付流程闭环
云开发支付流程闭环 extends 微信小程序–使用云开发完成支付闭环 在上述文章中,我们对支付结果的处理更多依赖于小程序端的操作 订单号存储在小程序端 支付结果采用小程序端定时触发器轮询 现在我对该流程进行了优化处理 1.流程介绍 [图片] 2.小程序端 请求统一下单云函数 调用支付接口 侦听器获取支付结果 [代码]// pages/index/details.js const app = getApp(); const db = wx.cloud.database(); var watcher = null Page({ /** * 页面的初始数据 */ data: { }, //付费解锁 payUnlock() { var that = this; this.setData({ vis: true }) //用户ID 即为OPENID let userid = this.data.selfcard._id; wx.cloud.callFunction({ name: 'userpay', data: { fee: 1, paydata: { userid } }, success: res => { console.log(res) //统一下单云函数中需要返回侦听器 需要的记录id that.payWatcher(res.result.docid); that.setData({ vis: false }) //根据统一下单参数 请求支付接口 const payment = res.result.payment wx.requestPayment({ ...payment, success(ans) { console.log(ans) }, fail(ans) { that.setData({ errMsg: '调用支付失败' }) } }) } }) }, payWatcher(docid){ var that = this; //为用户支付记录表设置侦听器,侦听docid信息的变动 this.watcher = db.collection('USERPAYLOG').doc(docid).watch({ onChange: async function (snapshot) { //只打印变动的信息 // console.log(snapshot) if (snapshot.docChanges.length != 0) { console.log(snapshot.docChanges) let paydoc = snapshot.docChanges[0].doc; //侦听到支付成功 if(paydoc.paystatus == 1){ that.setData({ succMsg:'支付成功', locked:false, bottom:0 }) } // await that.watcher.close(); } }, onError: function (err) { console.error('the watch closed because of error', err) } }) }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, /** * 生命周期函数--监听页面卸载 */ onUnload: function () { try { this.watcher.close(); } catch (error) { console.log('暂未启动支付侦听器') } } }) [代码] 3.云函数端 [代码]userpay[代码] 云调用统一下单【CloudPay.unifiedOrder】 数据库中存入订单记录并设置为未支付状态 需要配置商户(云开发控制台) [图片] [代码]const cloud = require('wx-server-sdk') //需要在此处修改你的云环境ID cloud.init({ env: '' }) const db = cloud.database(); const _ = db.command; // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() var openid = event.openid || wxContext.OPENID //获取统一下单金额 var fee = parseInt(event.fee); let paydata = event.paydata; //生成订单号 let tradeno = GetTradeNo(); //调用统一下单接口 const res = await cloud.cloudPay.unifiedOrder({ //填写你的商户主体信息 例如 xx商贸 "body": "", "outTradeNo": tradeno, "spbillCreateIp": "127.0.0.1", //填写你的商户ID -- 可在云开发控制台中绑定获得(上图所示) "subMchId": "", "totalFee": fee, //填写你的云环境ID "envId": "", //填写你的回调函数名称 "functionName": "userpaynotify" }) console.log(res) res.outTradeNo = tradeno res.totalFee = fee //支付状态 0 为未支付 paydata.tradeno = tradeno paydata.paystatus = 0 paydata.totalfee = fee paydata.openid = openid paydata.paytime = TimeCode() //统一下单shuju paydata.uniOrder = res //拦截处理 为保持数据库字段一致性 if (res.returnCode == 'SUCCESS' && res.resultCode == 'SUCCESS') { //在云数据库中写入未支付的订单信息 let tdata = await db.collection('USERPAYLOG').add({ data: paydata }) console.log(tdata) //将该记录ID携带返回给小程序端 res.docid = tdata._id; return res; }else{ return res; } } function GetTradeNo() { var outTradeNo = ""; //订单号 for (var i = 0; i < 6; i++) //6位随机数,用以加在时间戳后面。 { outTradeNo += Math.floor(Math.random() * 10); } outTradeNo = "COP" + new Date().getTime() + outTradeNo; //时间戳,用来生成订单号。 return outTradeNo; } function TimeCode() { var date = new Date(); var year = date.getFullYear() var month = date.getMonth() + 1 var day = date.getDate() var hour = date.getHours() var minute = date.getMinutes() var second = date.getSeconds() return [year, month, day].map(formatNumber).join('-') + ' ' + [hour, minute, second].map(formatNumber).join(':') } function formatNumber(n) { n = n.toString() return n[1] ? n : '0' + n } [代码] 支付成功后触发云环境中该回调函数 回调函数携带的请求信息请在参考文档中查看 [代码]userpaynotify[代码] 修改数据库中订单状态 返回给回调请求SUCCESS数据【Cloud.paymentCallback】 订单在支付成功时会触发该回调函数 该回调函数必须有返回值,且必须是固定格式 根据回调函数携带的订单号,修改对应订单号的订单状态,并且返回对应格式的返回信息 字段名 变量名 必填 类型 描述 错误码 errcode 是 Number 0 错误信息 errmsg 是 String [代码]const cloud = require('wx-server-sdk') //填写你的云环境ID cloud.init({ env: '' }) const db = cloud.database(); const _ = db.command; // 云函数入口函数 exports.main = async (event, context) => { console.log('支付成功回调函数触发') console.log(event) let tradeno = event.outTradeNo; try { //修改数据库中订单状态 为已支付 db.collection('USERPAYLOG').where({ tradeno:tradeno }).update({ data:{ paystatus:1 } }) } catch (error) { return { errmsg: 'SERVER_ERROR', errcode: -1 } } return { errmsg: 'SUCCESS', errcode: 0 } } [代码] 参考文档 云开发文档 Cloud.CloudPay | 微信开放文档 (qq.com) 回调函数请求携带参数 [代码]{ appid: '', bankType: 'OTHERS', cashFee: 1, feeType: 'CNY', isSubscribe: 'N', mchId: '', nonceStr: '', openid: '', outTradeNo: '', resultCode: 'SUCCESS', returnCode: 'SUCCESS', subAppid: '', subIsSubscribe: 'N', subMchId: '', subOpenid: '', timeEnd: '', totalFee: 1, tradeType: 'JSAPI', transactionId: '', userInfo: { appId: '', openId: '' } } [代码]
2021-06-02 - 节流与防抖分享
常用函数 2个 不废话 直接上 代码: /** * 节流原理:在一定时间内,只能触发一次 * * @param {Function} func 要执行的回调函数 * @param {Number} wait 延时的时间 * @param {Boolean} immediate 是否立即执行 * @return null */ let timer, flag; function throttle(func, wait = 500, immediate = true) { if (immediate) { if (!flag) { flag = true; // 如果是立即执行,则在wait毫秒内开始时执行 typeof func === 'function' && func(); timer = setTimeout(() => { flag = false; }, wait); } } else { if (!flag) { flag = true // 如果是非立即执行,则在wait毫秒内的结束处执行 timer = setTimeout(() => { flag = false typeof func === 'function' && func(); }, wait); } } }; /** * 防抖原理:一定时间内,只有最后一次操作,再过wait毫秒后才执行函数 * * @param {Function} func 要执行的回调函数 * @param {Number} wait 延时的时间 * @param {Boolean} immediate 是否立即执行 * @return null */ let timeout = null; function debounce(func, wait = 500, immediate = false) { // 清除定时器 if (timeout !== null) clearTimeout(timeout); // 立即执行,此类情况一般用不到 if (immediate) { var callNow = !timeout; timeout = setTimeout(function() { timeout = null; }, wait); if (callNow) typeof func === 'function' && func(); } else { // 设置定时器,当最后一次操作后,timeout不会再被清除,所以在延时wait毫秒后执行func回调方法 timeout = setTimeout(function() { typeof func === 'function' && func(); }, wait); } } 常用函数 2个 不废话 直接上 代码: /** * 节流原理:在一定时间内,只能触发一次 * * @param {Function} func 要执行的回调函数 * @param {Number} wait 延时的时间 * @param {Boolean} immediate 是否立即执行 * @return null */ let timer, flag; function throttle(func, wait = 500, immediate = true) { if (immediate) { if (!flag) { flag = true; // 如果是立即执行,则在wait毫秒内开始时执行 typeof func === 'function' && func(); timer = setTimeout(() => { flag = false; }, wait); } } else { if (!flag) { flag = true // 如果是非立即执行,则在wait毫秒内的结束处执行 timer = setTimeout(() => { flag = false typeof func === 'function' && func(); }, wait); } } }; /** * 防抖原理:一定时间内,只有最后一次操作,再过wait毫秒后才执行函数 * * @param {Function} func 要执行的回调函数 * @param {Number} wait 延时的时间 * @param {Boolean} immediate 是否立即执行 * @return null */ let timeout = null; function debounce(func, wait = 500, immediate = false) { // 清除定时器 if (timeout !== null) clearTimeout(timeout); // 立即执行,此类情况一般用不到 if (immediate) { var callNow = !timeout; timeout = setTimeout(function() { timeout = null; }, wait); if (callNow) typeof func === 'function' && func(); } else { // 设置定时器,当最后一次操作后,timeout不会再被清除,所以在延时wait毫秒后执行func回调方法 timeout = setTimeout(function() { typeof func === 'function' && func(); }, wait); } }
2022-12-04 - 【实战记录】关于实现聊天室语音播放问题的一次曲线救国的记录
需求背景: 该项目为互联网医院小程序,其中重要的功能为在线问诊,需要建立医患聊天的功能,故本项目集成了融云sdk即时通讯单聊功能。 问题: 接了融云的sdk后,发文字,图片及自定义消息都是那么的丝滑,但在上线阶段,发现微信小程序真机中部分语音播放不了;通过分析定位,发现由于 wx.createInnerAudioContext(),部分语音在ios真机中会报INNERERRCODE:-11800, ERRMSG:这项操作无法完成。通过和融云技术一顿沟通,也在查找社区文档后发现,早在2021年3月份,有人也提了这个问题,也没有找到官方的相关反馈,并且我自己提了bug(https://developers.weixin.qq.com/community/develop/doc/0004224a4c09c0553ddd1c09657800),也没有下文了。没办法,项目上线迫在眉睫,只能换其他技术替代方案了 解决方案: 为兼容wx.createInnerAudioContext()报错,在onError时,保存需要通过uni.getBackgroundAudioManager()来播放的音频列表 this.audio = uni.createInnerAudioContext(); this.audio.src = this.src; this.audio.onPlay(() => { this.audio.isPlaying = true; this.animate = true; this.timer = setInterval(() => { this.animate = false setTimeout(() => { this.animate = true }, 50) }, 1250); this.audio.onStop(() => { this.audio.isPlaying = false; this.animate = false; this.timer && clearInterval(this.timer) }) this.audio.onEnded(() => { this.audio.isPlaying = false; this.animate = false; this.timer && clearInterval(this.timer) }) }) this.audio.onError((err) => { this.$bgAudioList.push({ messageUId: this.messageUId, animate: false, }) // 所有需要通过背景音乐播放的存入全局 this.useBackgroundAudioManager = true; }) this.$audioList.push(this.audio)//所有实例加入全局变量 效果如下: [图片]
2022-05-06 - 关于 wx.createInnerAudioContext安卓MP3文件不能播放的解决问题
好多人都遇到了 wx.createInnerAudioContext 这个api在开发者工具上或者是iphone上可以播放的MP3文件但是在安卓上报错不能播放的问题 主要是MP3还分各种格式 具体的可以看这个文档 https://blog.csdn.net/datamining2005/article/details/78954367 大家可以按照文中所述 去对比一下 一个安卓能播放的MP3和不能播放的有什么区别 我这边遇到的是文字转语音的需求碰到这个问题 eg: 我们之前是用科大讯飞的接口转化的 MP3 安卓上就不能播放 IOS没问题 之后用百度的就可以了 希望可以帮到你们 第一次写文章 格式什么的不重要 看内容
2019-08-15 - 苹果手机的音频播放无声,报错啦,基础库更新前是正常的,怎么整?
苹果手机的音频播放无声,报错啦: {message: "undefined is not an object (evaluating 'r.failCallbacks')", line: 1, column: 840045, sourceURL: "https://lib/WAServiceMainContext.js", stack: "@https://lib/WAServiceMainContext.js:1:840045"} 此前是正常的,帮忙看一下是哪里出错了呀? 查看了一下公告:小程序基础库 2.28.0 更新(2小时前) 更新 API 音频接口优化一优化,就有事,真是难受啊。安卓手机能正常播放。
2022-11-28 - 开通虚拟支付的资质不全,但是我们已经上线好几个月,并且资质是齐全的
appid:wxc0c12c04e8fadd14 我们的小游戏今天收到封禁通知,提醒开通虚拟支付的资质不全,但是我们已经上线好几个月,并且资质是齐全的,上传资质申诉以后还是被驳回,麻烦看一下具体缺的是什么资质 [图片]
2022-11-24 - 云开发数据库如何在数组中添加一个元素?
在已知name,subjectName的情况下,如何在相对应的text中添加一个元素url4? [图片] 目前想通过在云函数中遍历找到对应的下标,用push方法将元素加入到对应数组后整体更新subject,代码如下: 但是在测试中语句并没有执行,newSubject并没有更新旧的subject const facultys = db.collection('facultys') // 云函数入口函数 exports.main = async (event, context) => { return await facultys .where({ name: event.xueyuan // "subject.subjectName": event.xueke }).get({ success: function(res){ var newSubject=res.result.data[0].subject;//获取subject //console.log(newSubject.length); for(var i=0;i<newSubject.length;i++){//遍历subject获取对应学科名位置 if(newSubject[i].subjectName===event.xueke) { newSubject[i].text.push(event.shijuan);//在text中添加试卷 //用newSubject更新老subject facultys.where({ name:event.xueyuan }).update({ data:{ subject:newSubject }, success:res=>{ console.log("更新成功",res); }, fail:err=>{ console.log("更新失败",err) } }); break; } } },//end success fail: console.error }) }
2020-05-05 - 微信支付拉去支付的时候,可能网络原因再次点击支付会重复调起支付 最终成了支付两笔怎么解决?
节流防抖也用过了 loading 加载也用过了
2021-07-08 - 如何实现动态生成好友分享图
本文章采用官方最新的 Canvas.createImage() 来实现下动态生成好友分享图,可以拿来即用。展示效果如下(其中蓝框中文案和红框头像为插入的文本、图像。背景图也支持动态更换): [图片] 小程序demo案例:https://developers.weixin.qq.com/s/nJtr4QmL7RD3 一、市面案例缺陷:翻阅了目前市面上的小程序动态生成好友分享图,大部分还是使用已废弃的『wx.createSelectorQuer』接口来实现。目前小程序已无法很好支持。 二、主要有以下几个关键点需要注意下: 做好友分享图要考虑5:4的比例使用 wx.getImageInfo 一定要考虑图片失败的场景,然后采用兜底图片。相同的逻辑在complete中执行。要区分 wx.createImage ,这个是小游戏用来创建图片对象的。小程序要用Canvas.createImage()。也不是使用 new Image!!!使用的时候一定要在 canvas 类型中注明 type 是 2d 的 canvas[图片] 三、优化知识点: 如何用 async in image loading:https://stackoverflow.com/questions/46399223/async-await-in-image-loading ----- 采用await img.decode() 或者 img.onload = () => resolve() 如何隐藏Canvas:https://developers.weixin.qq.com/community/develop/doc/1aadfacdd9f38584881e0c50db2bcda1 ----- position:fixed;left:100%;
2023-06-19 - 微信小程序高级指南-基于miniprogram-template模版开发小程序
介绍 miniprogram-template是一个快速开发小程序的解决方案,它基于 vant-weapp 实现。它使用了小程序目前支持的最新配置和 api,内置了 eslist + prettier 代码规范,husky + lint-staged Git 提交代码规范验证,提供了丰富的功能组件,它可以帮助你快速搭建企业级小程序产品原型,希望本项目都能帮助你敏捷开发企业需求。 建议 本项目的定位是小程序开发模版,适合当基础模板来进行二次开发,公共组件指在各种类型的小程序中都会使用到,后续会持续迭代,欢迎提 issues。 使用案例 官方示例 Fabrique 精品店 番茄博客园 [图片] [图片] [图片] 预览 扫描下方小程序二维码,体验小程序模版示例: [图片] 功能 [代码]- tabBar放置在主包中, 其他页面放置到对应的分包中 - 多环境发布 - dev test pre prod - 组件 - 断网 - iconfot字体图标 - 图片 - 导航栏 - 富文本 - 全局配置 - eslist + prettier 代码规范 - husky + lint-staged git提交代码规范验证 - 支持scss语法[ less 语法需更改配置 ] - 初始化获取已包含 networkType、isConnected、systemInfo - npm 脚本设置环境变量,读取多种环境信息,基于 miniprogram-ci 实现自动化上传代码 - 工具类在 utils 文件夹中 - 路由表包含所有页面涉及交互跳转统一读取路由表路径,需个人维护 - 配置文件在 config 文件夹中 - 数据请求在 api 文件夹中 - 小程序发布后提示更新 [代码] 前序准备 你需要在本地安装 node 和 git。本项目技术栈基于 ES2015+、vant-weapp和dayjs,提前了解和学习这些知识会对使用本项目有很大的帮助。 目录结构 本项目已经为你生成了一个完整的开发框架,提供了涵盖小程序开发的各类封装和规范,下面是整个项目的目录结构。 [代码]├── README.md ├── api │ ├── content-service.js │ └── user-service.js ├── app.js ├── app.json ├── app.scss ├── assets │ ├── images │ └── styles ├── components │ ├── custom-broken-network │ ├── custom-iconfont │ ├── custom-image │ └── custom-nav-bar ├── config │ ├── development.js │ ├── env.js │ ├── index.js │ ├── local.js │ ├── preview.js │ ├── production.js │ └── test.js ├── miniprogram-ci.js ├── miniprogram_npm │ ├── @vant │ └── dayjs ├── package.json ├── packageA │ └── logs ├── pages │ ├── home ├── private.wx2f3fed2106f72ceb.key ├── project.config.json ├── project.private.config.json ├── sitemap.json ├── switch-env.js ├── utils │ ├── request.js │ ├── router.js │ ├── util.js │ └── wxs.wxs └── yarn.lock [代码] 安装 [代码]# 克隆项目 git clone https://github.com/zhihuifanqiechaodan/miniprogram-template.git # 进入项目目录 cd miniprogram-template # 安装依赖 yarn install # 小程序编辑器-工具-构建 # 编译刷新 [代码] TIP 强烈建议使用 yarn 安装依赖,避免使用 npm 或者 cnpm 安装,可能会有各种诡异的 bug。 完成上述安装 构建 编译后即可看到小程序内容,当你看到下面的页面说明你操作成功了。 [图片] 接下来你可以修改代码进行业务开发了,本项目内建了常用公共组件、全局路由管理等等各种实用的功能来辅助开发,你可以通过查看已有的工具类和封装方法来使用。 建议 使用前建议将目前项目中已有的配置和文件夹工具类先行查看一番,方便后续使用,其次小程序路由和跳转都进行了封装,方便统一管理,后续需要自行维护。 公共组件 关于公共组件的介绍和使用请查看对应组件文件夹下的 README.md 其它 基于miniprogram-template模版开发上线的小程序已有多个,可参考 Fabrique 精品店 / 番茄博客园等。 对于一些小程序开发中常遇到的问题和解决方案欢迎讨论。 欢迎您提供宝贵的意见和建议,也欢迎提 issues 增加和修改功能或组件,另外如果可以的话请给个 start,感谢~ 更新日志 v1.0.3(20221116) 新增 custom-video 公共组件, 封装微信小程序原生 video 标签,单例模式,解决视频播放黑屏,多视频播放混音,视频列表存在多个视频同时播放,自定义 UI 样式等等,目前支持属性配置,如需扩展其他原生功能可直接修改组件添加属性。 components 文件夹下的公共组件统一增加 README.md 说明文件。 新增 custom-video 演示页面。 custom-image 公共组件优化。 v1.0.2(20221028) 新增 custom-rich-text 公共组件,基于 mp-html封装,目前支持识别富文本以及 markdown 格式内容如需其他插件功能,可查看 mp-html 文档,通过配置打包后将生成的 mp-weixin 文件夹放置到 components 文件件中覆盖原有的 mp-weixin 文件夹 v1.0.1 (20221020) 新增 custom-image 公共组件,属性同 van-image,图片裁剪模式同原生小程序 image 组件的 mode 属性。 新增 custom-iconfont 公共组件,支持设置大小,颜色,图标(需要在/assets/styles/iconfont.scss 文件中提前引入使用的 iconfont),支持接收组件外部样式 external-iconfont。
2022-11-16 - innerAudioContext.onPlay() 执行了三次
[图片] 代码里只调用了一次innerAudioContext.play();结果innerAudioContext.onPlay() 和 innerAudioContext.onEnded() 都执行了三次。 是bug?
2017-12-14 - 微信小程序中安全区域计算和适配
前言 自从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 - 云开发 聚合阶段 match无法根据时间查询
如图,同样根据时间查询,where里面可以执行,match内执行报错;仅服务端,客户端无此BUG,SDK版本如下。 另外,请提供事务能力,否则有很多业务场景操作没法回滚 [图片] [图片] [图片]
2019-11-05 - 小程序实用npm包推荐
虽然都说不要重复造轮子, 但还是屡见不鲜, 下面给大家推荐几款实用的小程序npm包, 欢迎各位同志评论区继续补充. 1.mobx-miniprogram, mobx-miniprogram-bindings, 小程序状态管理, 类似于vuex; mobx-miniprogram用于创建状态数据, mobx-miniprogram-bindings用于对页面或组件绑定状态数据;使用文档: https://github.com/wechat-miniprogram/mobx; import { observable, action } from 'mobx-miniprogram'; export const store = observable({ // 数据字段 numA: 1, numB: 2, // 计算属性 get sum() { return this.numA + this.numB; }, // actions update: action(function () { this.numA++; }), }); // 页面使用 import{ createStoreBindings }from'mobx-miniprogram-bindings' import{ store }from'./store' Page({ data:{ someData:'...' }, onLoad(){ // 绑定 this.storeBindings = createStoreBindings(this,{ store, fields:['numA','numB','sum'], actions:['update'], }) }, onUnload(){ this.storeBindings.destroyStoreBindings() }, }) 2.dayjs, 时间处理工具, 包含时间解析, 时间格式化, 时间比较等常用功能, 最重要的尺寸较小, 非常适合移动端来使用, 使用文档: https://dayjs.fenxianglu.cn/category/; dayjs().format('YYYY-MM-DD HH:mm:ss'); // 2022-10-27 13:50:12 dayjs().add(7, 'day') // 7天后 dayjs().isBefore(dayjs('2011-01-01')) // 是否在2011-01-01之前 3.mp-html, 富文本解析利器, 小程序提供的rich-text组件虽然可以解析富文本, 但存在若干缺陷: 1. 文字无法选择; 2. 链接无法跳转; 3.图片无法预览和自适应尺寸等, 使用mp-html可以很好解决上述问题, 使用文档: https://www.npmjs.com/package/mp-html; 1.安装npm npm i mp-html 2.在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "mp-html" } } 在需要使用页面的 wxml 文件中添加
2022-10-27 - 共享云环境时代来了,解决fileID带来不兼容问题。
云开发收费了,不管你怎么选择,只要你还继续使用云开发,共享云环境的课题就不可避免。 我们知道,共享云环境下,fileID是无法使用的,怎么兼容,一个最简的方法如下: <wxs module="wxs"> module.exports = { getUrl: function (link) { if (link) { } else return '' if (link.substring(0, 5) == 'cloud') { } else return link var arr = link.split('/') arr[0] = 'https:' arr[2] = arr[2].split('.')[1] + '.tcb.qcloud.la' return arr.join('/') } } </wxs> <image src="{{wxs.getUrl(link)}}"></image> 可见:只要将原项目所有的fileID换成wxs.getUrl(link) 其他代码可以一分不动,也不需要用到wx.cloud.getTempFileURL 可以将wxs.getUrl放在lib.wxs里,任何wxml引用即可。
2022-10-28 - 小程序支付报错:requestPayment:fail 系统错误,错误码:-6
已知,上周五小程序支付是OK的。 今天发现了这个问题: requestPayment:fail 系统错误,错误码:-6, [20211025 15:16:03][wxa25e5dfbc035026c]。 这个报错必现,能调起支付,但支付回调直接进入fail ; 支付参数没变。
2021-10-25 - JavaScript小技巧【建议收藏】
在JavaScript世界里,有些操作会让你无法理解,但是却无比优雅! 有时候读取变量属性时,他可能不是Ojbect。这个这个你就要判断这个变量是否为对象,如果是在如引用 [代码]var obj; if(obj instanceof Object){ console.log(obj.a); }else{ console.log('对象不存在'); } [代码] ES6中有简便写法,可以帮我们简短代码,从而更加明确 [代码]var obj; console.log(obj?.a || '对象不存在'); [代码] 1,2,3,4,5,6…10,11,12 小于10的前面补0 其实我们用slice函数可以巧妙解决这个问题 slice(start,end); start:必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。 end :可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。 [代码]var list=[1,2,3,4,5,6,7,8,9,10,11,12,13]; list=list.map(ele=>('0' + ele).slice(-2)); console.log(list); [代码] [图片] 打印可视化console.table() [代码]var obj = { name: 'Jack' }; console.table(obj); obj.name= 'Rose'; console.table(obj); [代码] [图片] 获取数组中的最后的元素 数组方法slice()可以接受负整数,如果提供它,它将从数组的末尾开始截取数值,而不是开头。 [代码]let array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; console.log(array.slice(-1)); // Result: [9] console.log(array.slice(-2)); // Result: [8, 9] console.log(array.slice(-3)); // Result: [7, 8, 9] [代码] es6模板字符串 [代码]let name = "Charlse" let place = "India"; let isPrime = bit =>{ return (bit === 'P'? 'Prime' : 'Nom-Prime') } let messageConcat = `Mr.name' is form ${place} .He is a ${isPrime('P')} member`; [代码] H5语音合成 在网页端实现将指定的文字进行语音合成并进行播报。 使用HTML5的Speech Synthesis API就能实现简单的语音合成效果。 [代码]<input id="btn1" type="button" value="点我" onclick="test();" /> <script> function test() { const sos = `阿尤!不错哦`; const synth = window.speechSynthesis; let msg = new SpeechSynthesisUtterance(sos); synth.speak(msg) } </script> [代码] 然后点击按钮,就会触发test方法的执行实现语音合成 这里推荐使用Chrome浏览器,因为HTML5的支持度不同 数字取整 [代码]let floatNum = 100.5; let intNum = ~~floatNum; console.log(intNum); // 100 [代码] [图片] 字符串转数字 [代码]let str="10000"; let num = +str; console.log(num); // 10000 [代码] 交换对象键值 [代码]let obj = { key1: "value1", key2: "value2" }; let revert = {}; Object.entries(obj).forEach(([key, value]) => revert[value] = key); console.log(revert); [代码] [图片] 数组去重 [代码]let arrNum = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 ]; let arrString = [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" ]; let arrMixed = [ 1, "1", "2", true, false, false, 1, 2, "2" ]; arrNum = Array.from(new Set(arrNum)); arrString = [...new Set(arrString)]; arrMixed = [...new Set(arrMixed)]; console.log(arrNum); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] console.log(arrString); // ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] console.log(arrMixed); // [1, "1", "2", true, false, 2] [代码] 数组转化为对象 [代码]const arr = [1,2,3] const obj = {...arr} console.log(obj) [代码] 合并对象 [代码]const obj1 = { a: 1 } const obj2 = { b: 2 } const combinObj = { ...obj1, ...obj2 } console.log(combinObj) [代码] 也就是通过展开操作符(spread operator)来实现。 获取数组中的最后一项 [代码]let arr = [0, 1, 2, 3, 4, 5]; const last = arr.slice(-1)[0]; console.log(last); [代码] 一次性函数,适用于初始化的一些操作 [代码]var sca = function() { console.log('msg')//永远只会执行一次 sca = function() { console.log('foo') } } sca() // msg sca() // foo sca() [代码] 提高工作效率,减少代码量,降低键盘磨损程度 我希望你喜欢它并学到了一些新东西 感谢你的阅读,编程快乐!
2022-10-25 - 调用单次分账,多个商户,如果部分商户分账失败,如何处理,还能再发起分账吗
调用单次分账,多个商户,如果部分商户分账失败,比如是分账两个商户的,有一个成功一个失败,那如何 处理结果,再分账吗,因为单次分账已经解冻了。
2020-12-11 - 微信支付的订单分账后能否退款?
微信分账后,是否还可以退款?我需要的逻辑如下: 商户号收到10元,分账给服务商1元。然后客户申请退款,不进行分账回退。商户号直接退10元给客户。 有这个退款的接口吗?不分账回退,这个服务商的钱到了就不回退的。
2022-09-27 - 代开发小程序的第三方,该怎么调用物流助手的接口?
目前,只能先由小程序在后台绑定物流账号,第三方平台获知其biz_id并得到其授权后调用业务api。后续,将支持通过api绑定物流账号。
2019-12-30 - subscribeMessage.send errCode: -504002报错?
[图片]Error: cloud.callFunction:fail Error: errCode: -504002 functions execute fail | errMsg: TypeError: Do not know how to serialize a BigInt 调用订阅消息后测试消息可以发送成功,但是返回值报错Do not know how to serialize a BigInt,无法判断是否发送成功。 2022-05-01 解决方法:云函数的返回值不要返回result,直接返回result.errCode,就不会报错。原因是直接返回result被序列化导致报错(其实就是bug),所以返回result.errCode足以用于判断!
2022-05-01 - 【拎包哥】新版云开发获取手机号
在获取手机号时,新版的code(2022/03/28)取代了旧版的cloudID,整体代码变得更加简洁,可读。 下面,让我们跟着文档来学习吧。 1. 云开发配置 微信文档:getPhoneNumber云函数 [图片] 在config.json配置权限,并使用openapi接口的函数 [代码]exports.main = async (event) => { // 云函数/index.js return await cloud.openapi.phonenumber.getPhoneNumber({ code: event.code }) } -------- // 云函数/config.json { "permissions": { "openapi": [ "phonenumber.getPhoneNumber" ] } } [代码] 2. 获取code并使用云函数 微信文档:获取手机号 通过设置了open-type的<button>获取code,然后使用云函数解密得到手机号。 [代码]<button open-type="getPhoneNumber" bindgetphonenumber='phoneClk'>手机号码</button> --------- phoneClk(e) { const code = e.detail.code wx.cloud.callFunction({ name: '云函数', data: { code } }).then(res => { let phoneNumber = res.result.phoneInfo.phoneNumber }) } [代码] 总结 云开发获取手机号需要一定的前置知识,而且微信文档又属实有点抽象了嗷。来沈阳大街我头套把你薅一地,指定没你好果汁吃嗷。 新手需要了解 微信开放能力 <button> open-type 云函数使用及配置 openapi
2022-03-31 - 第三方服务平台,怎么申请通用的订阅消息模板?
每个帐号的行业类目不尽相同,仅可根据小程序类目,调用行业匹配的业务订阅需求模板消息。
2020-04-23 - 云开发“分账功能”踩坑记
云开发的分账功能还在公测中,需要去申请开通。如果等了几天还没开通,可以在社区提问,会有官方人员来跟进。 我等了 5 天才开通。 开始之前,首先要搞清楚一个概念:云开发就是一个服务商,所以开发者不需要注册成为支付服务商,就可以使用服务商分账。 以下是我开发过程中遇到的一些坑。可能你会遇到其他坑,记得把调用接口的结果打印出来,看看报的是什么错。 1、文档不全,需要传 profit_sharing 或者 profitSharing 云开发统一下单接口的文档缺少 profit_sharing 参数,需要结合原支付接口文档来看,在分账对接步骤里也有说明。 如果没传这个参数,会报错:“非分账订单不支持分账”。 经过测试发现,这个参数是必需的,可以是 profit_sharing,也可以是 profitSharing,两者都兼容。由于其他参数都是驼峰写法,所以我用了 profitSharing . 2、不需要每次分账都“添加分账接收方” 添加分账接收方,只需要调一次接口即可,添加成功后,不需要每次分账都添加接收方。 有个小提醒,receiver 这个参数不是 JSON,而是JSON 序列化后的字符串,记得用 JSON.stringify 处理。序列化前是对象,不是数组,如果有多个接收方就调用多次接口。 (注意区分一下,在分账接口中,分账接收方的参数是 receivers,不是 receiver。不管单次分账还是多次分账,receivers 参数都是序列化后的字符串,序列化前是对象数组,不是对象) 这里还有个 bug,根据通知指引,我并没有找到这个入口:“商户平台 - 交易中心 - 管理分账接收方”。后面发了个提问帖,找到了这个暗门。登录商户平台,然后访问这个地址:https://pay.weixin.qq.com/index.php/xphp/ccmn_sharing/split_relation_manage 这可能也不是 bug,我猜估计是担心商户会有意或者无意的删除分账接收方。 3、sub_appid 是必填 文档有误,这个参数是必填。 4、PERSONAL_OPENID 和 PERSONAL_SUB_OPENID receivers 的 type 参数,PERSONAL_OPENID 和 PERSONAL_SUB_OPENID,刚开始会有点迷糊。我填的是 PERSONAL_SUB_OPENID,account 填小程序数据库里的用户 openid . 5、支付成功后,需要延时 1 分钟处理 支付成功后如果立即处理分账会报错:“订单处理中,暂时无法分账,请稍后再试”。 分账产品介绍的文档是这么写:“在交易完成后,准实时(建议1分钟后)或30天内调分账接口。” 我单独用了一个云函数来处理分账,setTimeout 设置 59 秒后调用分账接口。 在开发者工具里,进入该云函数的配置设置,把超时时间设置为 60 秒,否则会返回超时。 支付成功后调用该云函数。 2021-9-23 更新: 不能用 setTimeout 处理分账,这样云函数会一直占用内存,超成资源浪费(而且是巨大的浪费)。应改成定时触发,例如每小时触发一次,把已支付的并且支付时间已经过了一分钟的订单找出来,调用分账接口。
2021-09-23 - 「笔记」字节跳动小程序如何接入腾讯云CloudBase?
前言 最近在把微信小程序迁移至字节跳动小程序,由于服务端使用了腾讯云 CloudBase,网上搜索了一遍,文章千篇一律,都是复制腾讯云官方1年以前的适配器文档,在经过和腾讯云官方技术人员沟通后终于成功解决问题。 安装 npm i @cloudbase/js-sdk -S npm i @maoyan/cloudbase-adapter-tt_mp -S 使用 由于字节跳动小程序没有提供getAccountInfoSync()接口,无法通过接口获取appId 所以需要将appId设置到字节跳动小程序app对象上。 [代码]App({ onLaunch(options) { this.appId = appId } }) [代码] 腾讯云 CloudBase 安全配置 由于字节跳动小程序使用云开发不享受微信生态下的免鉴权,要在终端应用(如APP、小程序等)中使用云开发的身份验证服务,需要将授权的应用加入白名单,并在SDK使用时传入分配的凭证信息。 腾讯云 CloudBase 登陆授权 为了增加安全性,建议开启匿名登陆。启动匿名登录后,用户将不需要登录即可访问应用。如果有更严格的安全要求,可以自行开启其它身份验证方式。 完整代码 [代码]import tcb from '@cloudbase/js-sdk'; import { adapter } from '@maoyan/cloudbase-adapter-tt_mp'; let app; App({ onLaunch(options) { // appId必须设置 this.appId = "字节跳动小程序的appid"; tcb.useAdapters(adapter); // 腾讯云共享环境初始化 app = tcb.init({ env: '云环境id', appSign: '应用标识', // 需要设置成字节跳动小程序的appid appSecret: { appAccessKeyId: '版本', appAccessKey: '凭证' } }) // 匿名登陆 const auth = app.auth() const loginState = auth.anonymousAuthProvider().signIn() let data = await app.callFunction({ name: "云函数名", data: "参数" }); console.log(data) } }) [代码]
2022-03-03 - 关于多次分账失败问题:分帐方与原请求不一致?
调用多次分账接口的时候,第一次分账成功 ,第二次换金额或者换分账接收方发时候,会分账失败,提示信息:分帐方与原请求不一致。反反复复看文档,看不出什么名堂。 最后,看到一个参数:order_id(微信分账单号),这是你第一次分账成功之后,返回的一个ID。官方解释是:微信分账单号,微信系统返回的唯一标识。 没错,就是 <out_order_no>P20150806125346</out_order_no>这个破玩意,就是他,就是他,就是他。。。 在你第二次分账的时候: 你一定要用order_id来替换你原先的out_order_no。 你一定要用order_id来替换你原先的out_order_no。 你一定要用order_id来替换你原先的out_order_no。 重要的事情要说三遍。
2019-04-19 - 编译前预处理config文件,切换环境
背景: 小程序项目有“开发版,QA版,QAP版,正式版”多个环境,每个环境都与多个服务器有交互。 其中部分服务器访问需要basic认证。 因为一些特殊原因,basic密匙现在阶段只能写在前端。但是又规定git上的代码中不能有密匙明文。 以上问题导致切换环境时,HOST URL和其他常量能够自动切换,但是HOST URL对应的basic认证密匙,必须手动填写。开发过程如果频繁切换环境的话会非常繁琐。版本发布时,也需要手动补全basic密匙,风险较高。 解决方案: 发现开发者工具有自定义处理命令的功能,在编译前可以对文件预处理。 基于这个功能尝试了在每次编译前,使用脚本修改config.js文件。而脚本存储在本地不传git,规避了密匙上传的问题。 [图片] 脚本思路很简单,就是使用fs读取config.js,然后通过正则匹配replace相应变量,重写config.js env.js: [代码]const path = require("path"); const fs = require("fs"); const ENV = "dev" const BASIC_AUTH = { dev: "**********", qa: "**********", prod: "**********", qap: "**********", } // 配置文件地址 let pathName = path.join(__dirname, "config.js") // 读取文件 格式为utf-8 fs.readFile(pathName, { encoding: "utf-8" }, function (err, data) { // 正则替换变量 data = data.replace(new RegExp("BASIC_AUTH:.*'"), `BASIC_AUTH: '${BASIC_AUTH[ENV]}'`) fs.writeFile(pathName, data, function (err) { console.log(err); }) }) [代码] 总结: 其他环境变量的切换也可以用相同方式完成。至此,每次环境的切换只需要修改env.js中 ENV的值,点击开发者工具编译按钮,就可以完成。 [图片] 考虑过一些其他思路,比如用云函数从云数据库中拿到密匙,保存在全局变量中,但是考虑到这种方式需要修改线上代码,且可能存在一些异步的问题,所以没有使用。如果有更好的解决方案,欢迎回帖~~
2022-07-28 - 微信小程序,使用云函数实现发布短信
使用云函数发送短信验证码前端代码:wxml: <!-- 手机号 --> <view class="kaidian-ziliao-content flex"> <view class="ziliao-content-name xing">手机号码</view> <input type="text" placeholder="请输入手机号码" placeholder-style="font-size:26rpx;" value="{{mobile}}" style="font-size: 26rpx;" bindinput="mobile" /> </view> <!-- 验证码 --> <view class="kaidian-ziliao-content flex"> <view class="ziliao-content-name xing">验 证 码</view> <view class="ziliao-content-right flex"> <input type="text" placeholder="请输入验证码" value="{{codeInput}}" placeholder-style="font-size:26rpx;" bindinput="codeInput" value="{{codeInput}}" style="font-size: 26rpx; width: 240rpx;" /> <view class="fasongCode" bindtap="{{!maxTimeOnly ? 'mobileCode' : ''}}" style="font-size: {{maxTimeOnly ? '22rpx' : '24rpx'}};">{{maxTimeMsg}}</view> </view> </view> js:点击验证码按钮 // 手机号码验证 var code = 'aaa' //生成验证码,用来发送短信验证,这个code一定要放到全局,否则发送到手机上就是undefined page({ data:{ codeInput:'',//手机验证码 maxTime:60,//60秒倒计时 maxTimeOnly:false, //控制倒计时 maxTimeMsg:'发送验证码', } mobileCode(){ var that = this let mobileNum = /^1[34578]\d{9}$/ //验证电话号码 // let mobileNum = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/ var {mobile} = that.data //获取到input输入的手机号码 if(mobileNum.test(mobile)){ //手机号码格式验证 wx.showToast({ icon:'none', title: '验证码已发送,请注意查收!', }) // 60秒后重新获取验证码 var inter = setInterval(function() { //开启1秒执行一次定时器:注意需要为定时器命名否则无法关闭定时器 that.setData({ maxTime: that.data.maxTime - 1, //一秒执行一次的秒数-1 maxTimeMsg:that.data.maxTime+'s后重新发送', //点击按钮后需要动态改变为此内容 maxTimeOnly:true , //这里是为页面做了个三元运算符,为真的话按钮就无法绑定这个函数,为假则可以绑定此函数 }); if (that.data.maxTime < 0) { //在这里做判断如果倒计时为0 clearInterval(inter) //则关闭定时器 that.setData({ //将数据重新刷新 maxTimeMsg:'发送验证码', maxTime: 60, maxTimeOnly:false }); } }, 1000); //1000为1秒钟执行一次 code = that.generateMixed(6) //调用方法执行6位随机数,想向用户发几位验证码就写几,这个方法我会放在下面 console.log('发送短信验证码:',code) //执行完上面的代码后就拿到了我们随机出来的验证码 wx.cloud.callFunction({ //连接云函数 name:'mobile', //云函数名称 data:{ //传数据 mobile, //手机号码 code, //我们随机出来的验证码,传递给云函数,让云函数给用户发短信 } }).then(res=>{ console.log(res) }) }else{ wx.showToast({ icon:'error', title: '手机号码有误', }) } }, }) js:获取随机验证码 //获取随机验证码,n代表几位 generateMixed(n) { let chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; let res = ""; for (var i = 0; i < n; i++) { var id = Math.ceil(Math.random() * 10); //10为chars数组个数,数组里也可以写字母,只要这边个数写对就没问题 res += chars[id]; } return res; }, 云函数代码const cloud = require('wx-server-sdk') cloud.init({ env: 'flower-7gagi8mzf6917ccb' //替换自己的云环境id }) exports.main = async (event, context) => { var {mobile,code} = event try { const result = await cloud.openapi.cloudbase.sendSms({ env: 'flower-7gagi8mzf6917ccb', //替换自己的云环境id content: '验证码为'+code, phoneNumberList: [ "+86"+mobile ] }) return result } catch (err) { return err } } tips:值得一提的是如果只是操作了上面的流程,云函数还是会报错,此时我们需要,打开我们的云开发》点击静态网站,把这个打开后什么也不用操作 [图片] 关于腾讯的短信费用,这边腾讯为我们赠送了100条的短信提供开发者测试使用 [图片] [图片] 当然如果100条免费的用完之后我们可以点击设置》往下滑滑倒底部,可以选择购买 [图片]
2022-04-02 - 用Proxy实现页面和组件公共状态动态更新(类Vuex)
支持vuex中的mapActions,mapMutations辅助函数,也可以在bindStore时添加到页面中,页面卸载时最好调佣 removeStore.remove() 来删除不需要更新的页面对象 页面中使用 [图片] 或者 [图片] 组件中使用 [图片] 也可以这样 [图片] //初始化state值 let state = { } //更新state方法 let mutations = { } //公共方法 let actions = { } let stateProxy; const Store = (options = {}) => { if(options.state) state = options.state if(options.state) mutations = options.mutations if(options.state) actions = options.actions //创建Proxy stateProxy = new Proxy(state, { get(target, property) { return target[property] }, set(target, property, value) { pageArrays.forEach(item => { const newData = {} //判断该页面是否需要更新property if (item.arr.includes(property)) { newData[property] = value } item.self.setData(newData) }) return Reflect.set(target, property, value) } }) return { state, commit: commitFun, dispatch: dispatchFun, } } //以下为主要代码(原理) // Actions commit方法 export const commitFun = (str, params) => { return mutations[str](stateProxy, params) } // Actions dispatch方法 export const dispatchFun = (str, params) => { return actions[str]({ state, commit: commitFun, dispatch: dispatchFun }, params) } //存放页面对象数组 const pageArrays = [] //创建数组索引 let id = 1; //删除卸载的页面和组件 class RemovePageItem { ids = null; constructor(ids) { this.ids = ids } remove() { pageArrays.some((item, index, arr) => { if (item.id === this.ids) { arr.splice(index, 1) return true } }) } } //绑定页面并初始化所需state export const bindStore = (self, useState = [], useMutations = [], useActions = []) => { id++ const newData = {} useState.forEach(item => { newData[item] = (state[item] || state[item] === 0) ? state[item] : '' }) useMutations.forEach(item => { self[item] = (params) => mutations[item](stateProxy, params) }) useActions.forEach(item => { self[item] = (params) => actions[item]({ state, commit: commitFun, dispatch: dispatchFun }, params) }) self.setData(newData) pageArrays.push({ self, id, arr: useState }) return new RemovePageItem(id) } //绑定更新state方法 export const mapMutations = (arr = []) => { const List = {} arr.forEach(item => { List[item] = (params) => mutations[item](stateProxy, params) }) return List } export const mapActions = (arr = []) => { const List = {} arr.forEach(item => { List[item] = params => actions[item]({ state, commit: commitFun, dispatch: dispatchFun }, params) }) return List } const _default = { ...Store() } export default _default
2022-06-14 - 简单且实用的购物车实现方案
本方案主要特点: 购物车cart数据保存在storage,不保存在云数据库。 好处及原因: 1、简单; 2、体验流畅。 在用户点击“加入购物车”后,不需要与后台数据库同步数据,完全没有卡顿现象,体验很好; 3、可扩展。 将来如果需要上传用户购物车数据到后台时,可以任何地方加入相关代码,将cart数据同步到数据库去,不需要改变现有的代码流程; 以下是实现流程及代码: 一、当用户点击“+1”或“-1”,在购物车中加货或减货; 1、购物车的增减功能; 2、tab页中小红点; 3、onShow实现多个页面的数据同步,比如在首页+1了,在分类页、购物车页会同步显示+1; onUpdateCart: function (e) { let product = e.currentTarget.dataset.item//读取传入的产品数据 let action = e.currentTarget.dataset.action//取值为'reduce'和'increase' let cart = lib.updateCart(product, this.data.cart, action)//计算新的cart表 this.setData({ cart })//刷新wxml上的cart渲染 wx.setStorageSync('cart', cart)//将新cart保存到缓存 lib.badgeCart(cart)//更新tab页中cart页的小红点:显示/消除小红点,或显示购物车的商品数值 }, onShow: function () { let cart = wx.getStorageSync('cart') || []//读取缓存中的cart表。 lib.badgeCart(cart)//更新tab页中cart页的小红点:显示/消除小红点,或显示购物车的商品数值 this.setData({ cart }) }, 二、用户点击“去结算”时; 1、购物车数据与后台数据实时同步。 onSettle: async function (cart) { //此处按其他业务逻辑过滤cart let ids = cart.map(v => v._id)//获取cart中所有产品的_id组 //从库里读取这组产品的最新数据 let res = await db.collection('product').aggregate() .match({ _id: _.in(ids), ...{ //other query } }) .group({ _id: null, stocks: app.$.push('$$ROOT') //一次性超限拉取所有产品列表 }) .end() let stocks = res.list[0].stocks//获取库中最新的产品列表 //将cart里产品与后台读取的数据进行对比,裁剪库存以及相关数据。包括缺货,库存不足,价格变动,等最新数据 lib.cartFilter(stocks, cart) wx.navigateTo({//跳转到结算页,计算订单总额、运费等 url: '../settle/settle', }) }, 三、用户点击“生成订单” 1、实现购物车中删除订单所含的产品, 2、后台数据库减库存 //清除缓存中的cart列表 cleanCart: function (order) { let dcart = order.items//订单中的商品表 let scart = wx.getStorageSync('cart')//缓存中的商品表 dcart.forEach(v => { let index = scart.findIndex(u => v._id == u._id) if (index > -1) { scart.splice(index, 1) } }) wx.setStorageSync('cart', scart) }, onCreateOrder:function(){ //生成待支付订单,同时后台减库存 }, 四、去支付。
2022-04-27 - 小程序页面通信、数据刷新、event bus 解决方案之 iny-bus 2.0 来了
背景介绍 在很久之前,我在写小程序的时候需要遇到一些问题,为了解决这些问题,便有了 [代码]iny-bus[代码] 这个库,当时主要解决了那些问题呢,让我们回顾一下并介绍一下 2.0的新功能 存在问题 [图片] 在各种小程序中,我们经常会遇到这种情况 有一个 列表,点击列表中的一项进入详情,详情有个按钮,删除了这一项,这个时候当用户返回到列表页时, 发现列表中的这一项依然存在,这种情况,就是一个 [代码]bug[代码],也就是数据不同步问题,这个时候测试小姐姐 肯定会找你,让你解决,这个时候,你也许会很快速的解决,但过一会儿,测试小姐姐又来找你说,我打开了 四五个页面更改了用户状态,但我一层一层返回到首页,发现有好几个页面数据没有刷新,也是一个 [代码]bug[代码], 这个时候你就犯愁了,怎么解决,常规方法有下面几种 解决方法 将所有请求放到 生命周期 [代码]onShow[代码] 中,只要我们页面重新显示,就会重新请求,数据也会刷新 通过用 [代码]getCurrentPages[代码] 获取页面栈,然后找到对应的 页面实例,调用实例方法,去刷新数据 通过设置一个全局变量,例如 [代码]App.globalData.xxx[代码],通过改变这个变量的值,然后在对应 [代码]onShow[代码] 中检查,如果值已改变,刷新数据 在打开详情页时,使用 [代码]redirectTo[代码] 而不是 [代码]navigateTo[代码],这样在打开新的页面时,会销毁当前页面, 返回时就不会回到这个里面,自然也不会有数据不同步问题 存在的问题 假如我们将 所有 请求放到 [代码]onShow[代码] 生命周期中,自然能解决所有数据刷新问题,但是 [代码]onShow[代码] 这个生命周期,有两个问题 第一: 它其实是在 [代码]onLoad[代码] 后面执行的,也就是说,假如请求耗时相同,从它发起请求到页面渲染, 会比 [代码]onLoad[代码] 慢 第二:那就是页面隐藏、调用微信分享、锁频等等都会触发执行,请求放置于 [代码]onShow[代码] 中就会造成大量不需要的请求,造成服务器压力,多余的资源浪费、也会造成用户体验不好的问题 通过 [代码]getCurrentPages[代码] 获取页面栈,然后找到对应的页面实例,调用实例方法,去刷新数据,这也不失为一个办法,但是还是有限制 第一: [代码]getCurrentPages[代码] 无法获取 [代码]tabBar[代码] 页面,而[代码]tabBar[代码] 页面是需要刷新和通信的重灾区 第二: 当需要通信的页面有两个、三个、多个呢,这里去使用 [代码]getCurrentPages[代码] 就会比较困难、繁琐 通过设置全局变量的方法,当需要使用的地方比较少时,可以接受,当使用的地方多的时候,维护起来就会很困难,代码过于臃肿,也会有很多问题 使用 redirectTo 而不是navigateTo,从用来体验来说,很糟糕,并且只存在一个页面,对于tab 页面,它也无能为力,不推荐使用 最佳实践 在 Vue 中, 可以通过 new Vue() 来实现一个 event bus作为事件总线,来达到事件通知的功能,在各大 框架中,也有自身的事件机制实现,那么我们完全可以通过同样的方法,实现一个事件中心,来管理我们的事件, 同时,解决我们的问题。iny-bus 就是这样一个及其轻量的事件库,使用 typescript 编写,100% 测试覆 盖率,能运行 js 的环境,就能使用 安装 方式一. 通过 npm 安装 小程序已经支持使用 npm 安装第三方包,详见 npm 支持 [代码] # npm npm i iny-bus -save # yarn yarn add iny-bus --production [代码] 方式二. 下载代码 直接通过 git 下载 iny-bus 源代码,并将[代码]dist[代码]目录 中的 index.js 拷贝到自己的项目中 [代码] git clone https://github.com/landluck/iny-bus.git [代码] 使用 使用内置方法(2.0 推荐) [代码] // App、Page、Component 使用方法一致,完全不需要手动去添加监听和移除监听了 import bus from 'iny-bus' // bus.app bus.page bus.component const page = bus.page({ busEvents: { // 简单使用 postMessage(msg) { this.setData({ msg }) }, // 一次性事件 postMessageOnce: { handler (msg) { this.setData({ msg }) }, once: true } }, onLoad() { bus.emit('postMessage', 'hello bus') bus.emit('postMessageOnce', 'hello bus once') } }) Page(page) [代码] 在生命周期中使用(1.0使用 和 2.0完全兼容) [代码] // 小程序 import bus from 'iny-bus' // 添加事件监听 // 在 onLoad 中注册, 避免在 onShow 中使用 onLoad () { this.eventId = bus.on('事件名', (a, b, c, d) => { // 支持多参数 console.log(a, b, c, d) this.setData({ a, b, c } }) } // 移除事件监听,该函数有两个参数,第二个事件id不传,会移除整个事件监听,传入ID,会移除该页面的事件监听,避免多余资源浪费, 在添加事件监听后,页面卸载(onUnload)时建议移除 onUnload () { bus.remove('事件名', this.eventId) } // 派发事件,触发事件监听处更新视图 // 支持多参传递 onClick () { bus.emit('事件名', a, b, c) } // 清空所有事件监听 onClear () { bus.clear() } [代码] 借助 iny-bus 小程序webview和原生通信 小程序 [代码]webview[代码] 和原生页面怎么通信,我相信这是一部分同学遇到的一个很不好处理的问题,那么如何借助 [代码]iny-bus[代码] 来实现 小程序 [代码]webview[代码] 和原生页面通信呢 webview 和原生通信要借助 web-view 组件的 bindmessage 方法 [图片] 要提前在小程序中内置好 [代码]iny-bus[代码] 的事件监听 要在 [代码]H5[代码] 中使用 [代码]wx.miniProgram.postMessage[代码] [图片] 4. 参考以下代码 [代码] // 小程序 webview // web-view 组件的 bindmessage 方法 onMessage ({ detail: { data } }) { for (let i = 0; i < data.length; i++) { const event = data[i] // 分享事件 if (event.type === 'share') { } // 刷新事件 if (event.type === 'refresh' && event.name) { // 派发事件,传递参数 bus.emit(event.name, event.data) } // 其它事件 } } // H5 wx.miniProgram.postMessage( { type: 'refresh', name: 'refreshData', data: { a: 1 } } ) [代码] 最后 iny-bus 的核心代码,非常少,但是能解决我们在小程序中遇到的大量 通信 和 数据刷新问题,是采用 各大平台小程序 原生开发时,页面通信的不二之选,同时,100% 的测试覆盖率,确保了 iny-bus 在使用中的稳定性和安全性,当然,每个库都是从简单走向复杂,功能慢慢完善,如果 大家在使用或者源码中发现了bug或者可以优化的点,欢迎大家提 pr 或者直接联系我 最后,如果 iny-bus 给你提供了帮助或者让你有任何收获,请给 作者 点个赞,感谢大家 点赞 代码地址
2020-03-07 - 一个公众号(服务号),多个小程序,云开发,绑定通知,扫描登录解决方案
1、小程序云开发,新建一个接收公众号的云函数mp // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ // API 调用都保持和云函数当前所在环境一致 env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { console.log(event) return { ToUserName: event.FromUserName, FromUserName: event.ToUserName, CreateTime: Date.parse(new Date())/1000, MsgType: 'text', Content: '收到!' } } 云托管接收消息推送 2、账号绑定:公众号打开云开发-》更多-》共享环境-》消息推送-》绑定推送事件,小程序也一样。 [图片] 3、根据event.MsgType的值:text,event等(跟公众号消息类型一致),判断消息类型做相应处理。 下面是通过公众号关注绑定小程序所属用户,可以用于通知消息,扫描登录等功能开发 switch (event.MsgType) { case 'text': obj.Content = `Hi~,我们将竭诚为您服务!` return obj break case 'event': if (event.Event == 'subscribe') { if (wxContext.FROM_UNIONID) { await db.collection('ws_user').where({ unionid: wxContext.FROM_UNIONID }).update({ data:{ mpOpenid: wxContext.FROM_OPENID, updateTime: Date.now() } }) } obj.Content = '已关注!' return obj } if (event.Event == 'unsubscribe') { await db.collection('ws_user').where({ unionid: wxContext.FROM_UNIONID }).update({ data:{ mpOpenid: '', updateTime: Date.now() } }) } break }
2022-09-15 - 云函数突破二十个云函数限制 实现一键切换环境
我新建一个名为 router 的云函数 使用tcb-router 实现 一个函数 多个操作 实现环境 秒切换 从此告别了 一个个函数的修改上传 。模块共用 安装 tcb-router npm install tcb-router // 云函数入口文件 npm install --production const cloud = require('wx-server-sdk'); // npm install tcb-router const TcbRouter = require('tcb-router'); const util = require('util.js'); //环境设置 只需要修改这里就可以实现环境切换 cloud.init({ env: 'xxxxx' }) const db = cloud.database(); const _ = db.command; // 云函数入口函数 exports.main = async (event, context) => { const app = new TcbRouter({ event }); /** * 测试 */ app.router('test', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { const test = require('test/index.js'); ctx.body = test.main(event, context, db, _, util); }); /** * 登录日志 */ app.router('logList', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { const logList = require('logList/index.js'); ctx.body = logList.main(event, context, db, _, util); }); /** * 登录 */ app.router('login', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { const login = require('login/index.js'); ctx.body = login.main(event, context, db, _, util); }); /** * 用户列表 */ app.router('userList', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { const userList = require('userList/index.js'); ctx.body = userList.main(event, context, db, _, util); }); return app.serve(); } 在 router 目录 新建test目录 test目录下 新建文件 index.js 这里只演示调用测试的 参考使用 文件目录 router/test/index.js module.exports = { main: async (event, context, db, _, util) => { const count = await db.collection('xxxx').count(); const list= wait db.collection('xxx').get(); return { event, context,count,list}; }} 小程序端调用 wx.cloud.callFunction({ name: 'router', data: { $url:'test' }, success: res => { console.log('test调用成功', res); }, fail: err => { console.log('test调用失败', err); } })
2018-11-13 - 云储存文件如何批量下载到本地
[图片] 云储存文件上传下载文档地址,https://docs.cloudbase.net/cli-v1/storage 首先确保你已经操作了以上 3 步~~~ 然后你可以选择在桌面新建一个 downloads 文件夹,然后选择打开桌面的命令提示符, [图片] //通过 --mode 指定你的云环境id tcb --mode '你的云环境id' [图片] 环境部署成功后,下载云储存文件到本地(下载 dianXiaoTwo云储存文件 到 本地downloads文件 中) // tcb storage download '云环境文件路径' '本地文件路径' --dir tcb storage download dianXiaoTwo downloads --dir [图片] [图片]
2022-10-13 - 微信开放平台 平台型服务商只能添加30个开发小程序?
就是我目前的义务是,我有要为很多客户提供小程序服务,一个客户就有一个小程序,类似凡科的服务,每次我升级版本,或者修改bug,都需要一个一个小程序的去打包上传,所以我想通过微信开放平台,弄一个开放服务,让客户的小程序授权平台,然后就可以统一管理代码上线下线等,但是现在遇到一个问题,就是平台型服务商最多只能添加30个开发小程序,但是我的小程序不止30个,我这种情况有没有其他的解决方案,望平台关注,万分感谢,十万火急。
2021-02-12 - 小程序页面(Page)扩展,为所有页面添加公共的生命周期、事件处理等函数
背景 在小程序的原生开发中,页面中经常会用到一些公共方法,例如在页面onLoad中验证权限、所有页面都需要onShareAppMessage设置分享等 假设我们在编码时每个页面都写一遍,显然不是一个高级程序员会干的事情,太Low了。如果我们定义一个公共文件,导出这些公共方法,每个页面都引入,然后再生命周期或者事件处理函数中调用,虽然看起来很方便,但不够优雅,达不到我们最终的目的(偷懒)。 下面给大家介绍一种相对比较优雅的实现方式,扩展Page来实现以上的操作。 Page(页面) 需要传入的是一个 [代码]object[代码] 类型的参数,那么我们重载一个 [代码]Page[代码] 函数,将这个 [代码]object[代码] 参数拦截改掉就可以了,下面直接上代码。 实现 1、在根目录新建一个 [代码]page-extend.js[代码] 文件,公共的逻辑都写在这里面 [代码]/** * * Page扩展函数 * * @param {*} Page 原生Page */ const pageExtend = Page => { return object => { // 导出原生Page传入的object参数中的生命周期函数 // 由于命名冲突,所以将onLoad生命周期函数命名成了onLoaded const { onLoaded } = object // 公共的onLoad生命周期函数 object.onLoad = function (options) { // 在onLoad中执行的代码 ... // 执行onLoaded生命周期函数 if (typeof onLoaded === 'function') { onLoaded.call(this, options) } } // 公共的onShareAppMessage事件处理函数 object.onShareAppMessage = () => { return { title: '分享标题', imageUrl: '分享封面' } } return Page(object) } } // 获取原生Page const originalPage = Page // 定义一个新的Page,将原生Page传入Page扩展函数 Page = pageExtend(originalPage) [代码] 2、在 [代码]app.js[代码] 中引入 [代码]page-extend.js[代码] 文件 [代码]require('./page-extend') App({ // 其他代码 ... }) [代码] 代码片段 https://developers.weixin.qq.com/s/Cyx8iGmV7Ldp 本文内容及评论未经允许,禁止任何形式的转载与复制(代码可在程序中使用)
2019-12-24 - 微信小程序头像昵称实战篇
2022-08-25 api文档地址: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html 目前的api变更后,得到的地址为 临时地址, 这个是文档没有说明的, 最佳实践,是需要把得到的地址上传到自己的服务器,然后用服务器返回的地址作为 真实头像的永久地址. 核心点说明: //获取到api返回的新地址路径 onChooseAvatar(e) { this.avatarUrl = e.detail.avatarUrl console.log('e.detail', e.detail) // this.updateUserInfo(); this.uploadFile(); }, /* 上传 头像 转 话格式*/ uploadFile(){ uni.uploadFile({ url: config.webUrl + '/upload/uploadImages',//后台接口 filePath: this.avatarUrl,// 上传图片 url name:'image', // formData: this.formData, header: { 'content-type': 'multipart/form-data', 'token': uni.getStorageSync('token') }, // header 值 success: res => { let obj = JSON.parse(res.data) console.log('obj', obj) if (obj.code == 1) { let imgUrl = obj.data.full_path; this.userImg = imgUrl; this.updateUserInfo(); } else { uni.showToast({ icon: 'none', title: '图片太大,请重新选择!' }); } }, fail: e => { this.$toast('上传失败') } }); }, 这里需要注意, wx.uploadFile 返回的是字符串类型,需要前端自己处理一下数据结构: [图片] 完整代码如下: import config from "@/common/config.js"; import {debounce} from '@/utils/debounce.js' export default { data() { return { defaultAvatarUrl: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0', avatarUrl: '', nick_name: '', userImg: '', } }, onLoad() { let userInfo = uni.getStorageSync('userInfo') || {}; let { nick_name,img_url } = {...userInfo}; this.userImg = img_url; this.nick_name = nick_name; }, methods: { onChooseAvatar(e) { this.avatarUrl = e.detail.avatarUrl console.log('e.detail', e.detail) // this.updateUserInfo(); this.uploadFile(); }, inputWord: debounce(function(e){ this.nick_name = e.detail.value console.log('this.nick_name.length',this.nick_name.length) let str = this.nick_name.trim(); if(str.length==0){ this.$toast('请输入合法的昵称') return } if((/[^/a-zA-Z0-9\u4E00-\u9FA5]/g).test(str)){ this.$toast('请输入中英文和数字') return } this.updateUserInfo() }, 1500), /* 上传 头像 转 话格式*/ uploadFile(){ uni.uploadFile({ url: config.webUrl + '/upload/uploadImages',//后台接口 filePath: this.avatarUrl,// 上传图片 url name:'image', // formData: this.formData, header: { 'content-type': 'multipart/form-data', 'token': uni.getStorageSync('token') }, // header 值 success: res => { let obj = JSON.parse(res.data) console.log('obj', obj) if (obj.code == 1) { let imgUrl = obj.data.full_path; this.userImg = imgUrl; this.updateUserInfo(); } else { uni.showToast({ icon: 'none', title: '图片太大,请重新选择!' }); } }, fail: e => { this.$toast('上传失败') } }); }, updateUserInfo(){ let self = this; uni.showLoading({}); let params = { img_url: this.userImg, nick_name: this.nick_name.trim(), } self.$http.post('updateUserInfo', params).then(res => { uni.hideLoading() if (res.data.code == 1) { self.$toast('修改成功!') }else { self.$toast(res.data.msg) } }) }, } } 请一键三连,争取升个级,谢谢各位道友! 补充一下,如果api不生效注意切换一下版本库: 我本地用的2.26.1 [图片] 实际效果图: [图片] [图片]
2022-11-24 - 微信小程序怎样在访问数据库的WHERE语句里判断某个属性包含一个字符串?
[图片]
2022-03-02 - 微信小程序教你使用eventbus一步一步构建全局跨页面通信系统
微信小程序提供了页面通信通信解决方案EventChannel,但实际使用中需要在wx.navigateTo跳转页面时使用,且需要在跳转前声明监听的事件,如下图 [图片] 这是一种页面间的通信,但是局限性过于明显,仅可以在跳转间的页面之间建立通信,A跳转B可以建立通信关系,A不跳转G就不可以建立通信关系,在实际开发中如果某个注册页面的信息想做回显,我们可以使用重新请求、放到storage中、glabalData、eventbus全局通信等,但是肯定不能用navigateTo建立eventbus信道进行传值,从交互层面是完全不可接收的。 这时我们就需要一个全局的eventbus来进行通信了,下面讲解一下微信小程序如何搭建全局eventbus进行通信。 注:eventbus传值,如果没有对引用类型进行深拷贝,那么会将引用传过去导致错误。 首先,我们需要开发页面扩展功能,我们知道每一个页面都是一个Page({}),传入的是一个对象,对象中包含双向绑定的数据、生命周期函数、自定义函数,这里我们需要增加页面的生命周期函数。 原理可以参考这篇文章: 小程序页面(Page)扩展 其中我们需要这5个文件: [图片] 其中config.js是小程序全局构造函数App、Page扩展规则配置项,eventBus是eventbus的实现,index是将eventbus扩展的页面上,然后再app.js中引入index文件即可,pageExtends即页面扩展方法,public是初始化eventbus的方法。 使用方法: A页面声明: [图片] B页面触发: [图片] 以下为源码 config.js源码: [代码]/* * @Author: 徐强国 * @Date: 2022-08-15 15:43:32 * @Description: Page公共方法扩展 */ const EventBus = require('./eventBus') let eventBus // 初始化页面的eventbus,事件用法参照dom2事件 export const initEventBus = (pageObj) => { // let eventBus = new EventBus(); if (!eventBus) { eventBus = new EventBus(); } else { } pageObj['$on'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.on(...argu) } pageObj['$off'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.off(...argu) } pageObj['$emit'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.emit(...argu) } // 创建页面声明的自定义事件 let events = pageObj['events']; if (Array.isArray(events)) { events.forEach((event, index) => { if (typeof event === 'string') { eventBus.createEvent(event) } else { console.error(`==请传入String类型的事件名称== index:${index}`, events) } }) } else if (typeof events !== 'undefined') { console.error('==events字段已被占用,用于声明当前页面需要创建的自定义事件,值为字符串数组== events:', events) } } module.exports = { onLoad(options) { this.$initPage() }, $initPage() { if (!this.$on) { initEventBus(this) } }, } [代码] eventBus.js源码 [代码]/** * @authors 徐强国 * @date 2022-8-8 * eventBus,订阅/发布 * */ // 是否是字符串 function isString(str) { return typeof str === 'string' } // 是否是函数 function isFunction(fn) { return typeof fn === 'function' } // 消息中心 class MessageHub { constructor() { this.pubDictionary = {} } // 创建发布者 createEvent(name, isGlobal) { if (!isString(name)) { console.error(`==请传入创建事件的名称 name==`) return false } let _pub = this.pubDictionary[name] if (_pub) { if (!isGlobal) { console.warn(`==${name} 事件已存在==`) } return _pub } else { let pub = new Publish(name, this) this.pubDictionary[name] = pub return pub } } removeEvent(name) { if (!isString(name)) { console.error(`==请传入删除事件的名称 name==`) return false } delete this.pubDictionary[name] } on(name, callback, mark) { if (!isString(name)) { console.error(`==请传入监听事件的名称 name==`) return false } console.log('ononoonon这里的区文体', this.pubDictionary, callback, mark) if (!isFunction(callback)) { console.error(`==请传入监听事件的回调函数 callback==`) return false } let pub = this.pubDictionary[name] if (pub) { let watcher = new Watcher(pub.dep, callback, mark) pub.dep.addSub(watcher) } else { console.error(`==尚未创建 ${name} 事件==`) } } off(name, callback) { if (!isString(name)) { console.error(`==请传入监听事件的名称 name==`) return false } if (!isFunction(callback)) { console.error(`==请传入监听事件的回调函数 callback==`) return false } let pub = this.pubDictionary[name] pub.dep.removeSub(callback) } emit(name, val) { if (!isString(name)) { console.error(`==请传入触发事件的名称 name==`) return false } console.log('这里的区文体emit', this.pubDictionary) let pub = this.pubDictionary[name] if (pub) { pub.refresh(val) } else { console.warn(`==${name} 事件不存在==`) } } clearEvent() { this.pubDictionary = {} } } // 发布者 class Publish { constructor(name, messageHub) { this.name = name this.messageHub = messageHub this.dep = new Dep(this) } refresh(val) { this.dep.notify(val) } } // 订阅者 class Watcher { constructor(dep, run, mark) { this.dep = dep this.run = run this.mark = mark || '' } update() { let val = this.dep.value let run = this.run run(val) } } // 依赖收集 class Dep { constructor(pub) { this.pub = pub this.subs = [] } addSub(sub) { this.subs.push(sub) } removeSub(run) { let sub = this.subs.filter(item => item.run === run)[0] remove(this.subs, sub) } notify(val) { this.value = val let subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } function remove(arr, el) { for (let i = 0; i < arr.length; i++) { if (arr[i] === el) { arr.splice(i, 1) return true } } return false } module.exports = MessageHub [代码] pageExtends.js源码 [代码]/** * @authors 徐强国 * @date 2022-08-15 * 小程序全局构造函数App、Page扩展 */ const { appLiftTimes, pageLiftTimes } = require('./config'); // 判断是否是App的生命周期及原始方法 function isAppLiftTimes (name, fn) { if (typeof fn === 'function') { return appLiftTimes.indexOf(name) > -1 } return false } // 判断是否是Page的生命周期及原始方法 function isPageLiftTimes(name, fn) { if (typeof fn === 'function') { return pageLiftTimes.indexOf(name) > -1 } return false } // 函数混入 function rewriteFn(context, name, fn) { if (context[name]) { let originFn = context[name]; context[name] = function (e) { let argu = Array.prototype.slice.call(arguments); fn.apply(this, argu); return originFn.apply(this, argu) } } else { context[name] = fn } } // 是否是对象 function isObject(obj) { return obj !== null && typeof obj === 'object' } // 重写App const originApp = App; const appExtendsList = []; App = function (obj) { // app拓展方法 appExtendsList.forEach(item => { rewriteFn(obj, item.key, item.value) }) return originApp(obj) } const appExtends = function (key, value) { if (isAppLiftTimes(key, value)) { appExtendsList.push({ key, value }) } else { console.error('==*App暂不支持非生命周期的扩展*==', key) } } // 重写Page const originPage = Page; const pageExtendsList = []; Page = function (obj) { let illegalKeys = Object.keys(obj).filter(key => /^\$+/.test(key)); if (illegalKeys.length) { // throw new Error(`Page中自定义属性禁止以 \$ 开头, ${illegalKeys.join(', ')}`) console.error(`Page中自定义属性禁止以 \$ 开头, ${illegalKeys.join(', ')}`) } // 页面拓展方法 pageExtendsList.forEach(item => { // 非生命周期属性只能拓展一次 if (isPageLiftTimes(item.key, item.value)) { rewriteFn(obj, item.key, item.value) } else { if (typeof obj[item.key] === 'undefined') { obj[item.key] = item.value; } else { console.error(`Page中已拓展 ${item.key} 属性`, obj[item.key]) } } }) return originPage(obj) } const pageExtends = function (key, value) { // Page拓展属性,非生命周期的属性必须以 $ 开头 if (/^\$+/.test(key) || isPageLiftTimes(key, value)) { if (isPageLiftTimes(key, value) || !pageExtendsList.filter(item => item.key === key).length) { pageExtendsList.push({ key, value }) } else { console.warn(`==*Page中已扩展 ${key} 属性*==`) } } else { console.warn(`==*Page中拓展属性必须以 \$ 开头*==`, `\n key: ${key}`) } } const AppPlus = { appExtends: function (mixinObj, value) { if (typeof mixinObj === 'string') { appExtends(mixinObj, value) } else if (isObject(mixinObj)) { Object.keys(mixinObj).forEach(key => { appExtends(key, mixinObj[key]) }) } else { console.warn('==*请传入 对象 或者 key, value*==') } }, pageExtends: function (mixinObj, value) { if (typeof mixinObj === 'string') { pageExtends(mixinObj, value) } else if (isObject(mixinObj)) { Object.keys(mixinObj).forEach(key => { pageExtends(key, mixinObj[key]) }) } else { console.warn('==*请传入 对象 或者 key, value*==') } } } module.exports = AppPlus [代码] public.js源码 [代码]/* * @Author: 徐强国 * @Date: 2022-08-15 15:43:32 * @Description: Page公共方法扩展 */ const EventBus = require('./eventBus') let eventBus // 初始化页面的eventbus,事件用法参照dom2事件 export const initEventBus = (pageObj) => { // let eventBus = new EventBus(); if (!eventBus) { eventBus = new EventBus(); } else { } pageObj['$on'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.on(...argu) } pageObj['$off'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.off(...argu) } pageObj['$emit'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.emit(...argu) } // 创建页面声明的自定义事件 let events = pageObj['events']; if (Array.isArray(events)) { events.forEach((event, index) => { if (typeof event === 'string') { eventBus.createEvent(event) } else { console.error(`==请传入String类型的事件名称== index:${index}`, events) } }) } else if (typeof events !== 'undefined') { console.error('==events字段已被占用,用于声明当前页面需要创建的自定义事件,值为字符串数组== events:', events) } } module.exports = { onLoad(options) { this.$initPage() }, $initPage() { if (!this.$on) { initEventBus(this) } }, } [代码] index.js源码 [代码]/* * @Author: 徐强国 * @Date: 2022-08-15 15:18:12 * @Description: 小程序提供扩展App、Page扩展入口 * * * AppPlus提供拓展App及Page的接口,校验自定义属性命名 * @param appExtends * @parm pageExtends * * 传入一个对象,此对象的属性及方法将混入App或者Page实例中 * 生命周期函数将与自定义的声明周期混合,且先执行, * 其他属性只能以$开头,且不可覆盖、混入,应避免名称重复 */ const AppPlus = require('./pageExtends') const Public = require('./public') AppPlus.pageExtends(Public) [代码] 代码下载:https://github.com/TobinXu/MiniProgramEventBus/tree/main
2022-08-19 - rich-text组件与editor组件的配合使用,用rich-text组件渲染显示editor组件内容
很多开发者在使用rich-text组件与editor组件会遇到这样的问题:希望通过rich-text组件按editor组件编辑时呈现的样式渲染出来。但直接用rich-text组件渲染的话,有些样式明显不一样。尤其是有序列表、无序列表、check-list等。 那是因为官方rich-text组件与editor组件是相互独立的。也就是说直接用rich-text组件渲染editor组件获取的html,可能跟想要的结果有差距 这篇文章主要就是告你开发者,怎么消除这些差距。 只需要做到两点就能渲染出想要的结果 1、节点结构 要想得到想要的结果,节点结构必须是下面这样的结构 [代码]<view class="ql-container"> <view class="ql-editor"> <rich-text nodes="{{html}}"></rich-text> </view> </view> [代码] 2、给 .ql-container 做样式调整 [代码].ql-container { display: block; position: relative; box-sizing: border-box; -webkit-user-select: text; user-select: text; outline: none; overflow: hidden; width: 100%; height: auto; min-height: 50px !important; } [代码]
2021-08-14 - 小程序如何动态的设置页面是否禁止滚动?
如题,想要在某个操作时禁止页面滚动,操作完成后页面恢复滚动
2021-11-18 - 通过自定义collection的安全规则,允许管理员去更新其他人的记录, 实现不了
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/security-rules.html 自定义了一个云数据库的collection安全规则,以实现让管理员可以更新其他人创建的记录。 { "read": true, "create": true, "delete": "auth.openid == doc._openid", "update": "auth.openid == doc._openid || auth.openid in get(`database.resident.${doc.residentId}`).manager" } 小程序端的语句 [图片] 这里提示更新记录为0,这个也是可以想通,是where 条件没有匹配上,但是我看文档里是可以实现的,请问是我哪里弄错了么? [图片]
2022-08-13 - 如何动态修改page{}样式?
请问如何动态修改*.wxss里面page{}样式呢?wxml里面也不可以改变page样式,只能改变某组件的样式,假如改变整个页面的背景,如何在js动态?
2017-12-06 - 求一个:微信小程序rich-text组件可识别的,后台富文本编辑器?
项目中使用的是百度富文本编辑器,但是想要实现需求:官方发布图(注:图片添加点击事件可放大查看)文(注:添加商品及物品链接进行跳转)中添加事件及链接时,小程序的rich-text组件中展示该图文链接点击无效
2021-11-22 - 小程序富文本解析
wxParse 微信小程序富文本解析 原因 由于原作者仓库 wxParse 不再维护,我们项目中商品信息展示又是以wxParse这个用做富文本解析的; 于是乎,决定采用 递归Component 的方式对其进行重构一番; 原项目使用的 [代码]template[代码] 模板的方式渲染节点,存在以下问题: 节点渲染支持到12层,超出会原样输出 [代码]html[代码] 代码; 每一层级的循环模板都重复了一遍所有的可解析标签,代码十分臃肿。 [代码]li[代码]标签不支持 [代码]ol[代码] 有序列表渲染(统一采用的是 [代码]ul[代码] 无序列表),[代码]a[代码]标签无法实现跳转,也无法获取点击事件回调等等; 节点渲染没有绑定 [代码]key[代码] 值,一是在开发工具看到一堆的 [代码]warning[代码]信息(看着十分难受),二是节点频繁删除添加,无法比较[代码]key值[代码],造成 [代码]dom[代码] 节点频繁操作。 功能标签 目前该项目已经可以支持以下标签的渲染: audio标签(可自行更换组件样式,暂时采用微信公众号文章的[代码]audio[代码]音乐播放器的样式处理) ul标签 ol标签 li标签 a标签 img标签 video标签 br标签 button标签 h1, h2, h3, h4标签 文本节点 其余块级标签 其余行级标签 支持 npm包 引入 [代码]npm install --save wx-minicomponent [代码] 使用 原生组件使用方法 克隆 项目 代码,把 components目录 拷贝到你的小程序根目录下面; 在你的 page页面 对应的 [代码]json[代码] 文件引入 [代码]wxParse[代码] 组件 [代码]{ "usingComponents": { "wxParse": "/components/wxParse/wxParse" } } [代码] 组件调用 [代码]<wxParse nodes="{{ htmlText }}" /> [代码] npm组件使用方法 安装组件 [代码]npm install --save wx-minicomponent [代码] 小程序开发工具的 [代码]工具[代码] 栏找到 [代码]构建npm[代码],点击构建; 在页面的 json 配置文件中添加 [代码]wxParse[代码] 自定义组件的配置 [代码]{ "usingComponents": { "wxParse": "/miniprogram_npm/wx-minicomponent/wxParse" } } [代码] [代码]wxml[代码] 文件中引用 wxParse [代码]<wxParse nodes="{{ htmlText }}" /> [代码] 提示:详细步骤可以参考小程序的npm使用文档 补充组件:代码高亮展示组件使用 在 [代码]page[代码]的 [代码]json[代码] 文件里面引入 [代码]highLight[代码] 组件 原生引入: [代码]{ "usingComponents": { "highLight": "/components/highLight/highLight" } } [代码] npm组件引入: [代码]{ "usingComponents": { "highLight": "/miniprogram_npm/wx-minicomponent/highLight" } } [代码] 组件调用 [代码]<highLight codeText="{{codeText}}" /> [代码] 参数文档 wxParse:富文本解析组件 参数 说明 类型 例子 nodes 富文本字符 String “<div>test</div>” language 语言 String 可选:“html” | “markdown” (“md”) 受信任的节点 节点 例子 audio <audio title=“我是标题” desc=“我是小标题” src=“https://media.lycheer.net/lecture/25840237/5026279_1509614610000.mp3?0.1” /> a <a href=“www.baidu.com”>跳转到百度</a> p div span li ul ol img button h1 h2 h3 h4 … highLight:代码高亮解析组件 参数 说明 类型 例子 codeText 原始高亮代码字符 String “var num = 10;” language 代码语言类型 String 可选值:“javascript/typescript/css/xml/sql/markdown” 提示:如果是html语言,language的值为xml wxAudio:仿微信公众号文章音频播放组件 参数 说明 类型 例子 title 标题 String “test” desc 副标题 String “sub test” src 音频地址 String 示例展示 富文本解析 html文本解析实例 [图片] markdown文本解析实例 [图片] 代码高亮 [图片] 更新历史 2020-5-31 迁移utils目录到wxParse目录下; 富文本增加markdown文本解析支持; 2020-5-21: 富文本组件image标签添加loading过渡态,优化图片加载体验 2020-5-17: 完善组件参数文档,增加wxParse对audio标签标题,副标题的解析功能 2020-5-13: 增加css,html,ts,sql,markdown代码高亮提示支持 2020-5-6: 增加图片预览功能 项目地址 项目地址:https://github.com/csonchen/wxParse
2020-06-01 - editor 的toolbar定位底部,ios键盘弹起页面滑动的时候toolbar也移动怎么解决?
editor 富文本编辑器的 toolbar定位固定在底部,当获取焦点键盘弹起的时候,安卓手机是没有问题, toolbar一直固定在键盘的上面,但是ios中,页面滑动,toolbar也跟着页面移动,之前是好的,现在突然不能用了,怎么解决?[图片]
2021-09-28 - editor将页面内容上推后,自定义标题栏高度变化
[图片][图片][图片][图片] 页面含有editor控件,标题栏是自定义标题栏,初始化页面标题栏高度正常,但是点击editor 弹出后,标题栏高度数值没有变,但是,实际高度发生变化,实际高度发生降低,同时,此容器的子代高度同样是88 ,但是高度要比此容器高一些,将此容器高度单位变成 em,vh ,vw,都不行,都会发生实际高度减小的问题,同时有配置disableScrolltrue 安卓端无问题
2020-01-15 - 微信小程序怎么隐藏页面的默认滚动条?
//去除滚动条 ::-webkit-scrollbar { width: 0; height: 0; color: transparent; display: none; } 没有效果呢, 竖着还是有滚动条
2021-12-07 - 【原创】关于多input连续输入问题的解决办法
[图片] 开干! 1、wxml <view data-place="0" class="input{{curFocus === 0 ? ' on':''}}" bindtap="openKeyBoard">{{passwd[0]}}</view> <view data-place="1" class="input{{curFocus === 1 ? ' on':''}}" bindtap="openKeyBoard">{{passwd[1]}}</view> <view data-place="2" class="input{{curFocus === 2 ? ' on':''}}" bindtap="openKeyBoard">{{passwd[2]}}</view> <view data-place="3" class="input{{curFocus === 3 ? ' on':''}}" bindtap="openKeyBoard">{{passwd[3]}}</view> <input type="number" model:value="{{inputValue}}" focus="{{focus}}" class="input-original" hold-keyboard="{{true}}" confirm-hold="{{true}}" maxlength="4" bindinput="bindKeyInput" /> 2、less 闪烁代替光标 .on { animation: glow 1s linear infinite; } @keyframes glow { 50% { border-color: #ff6666; } 100% { border-color: #eee; } } 3、js openKeyBoard(e: any) { const { place } = e?.currentTarget?.dataset; this.data.passwd[place] = ''; this.setData({ curFocus: parseInt(place), inputValue: '', focus: true, assign: true, passwd: this.data.passwd }); }, bindKeyInput: function (e) { const { value } = e?.detail; let tmp = value.split(''); let curFocus = tmp.length; let inputValue = ''; if (this.data.assign) { this.data.passwd[this.data.curFocus] = value.substr(-1); inputValue = this.data.passwd.join(''); curFocus = this.data.curFocus; console.log('修改指定位置'); } else { inputValue = value; this.data.passwd = value.split(''); } this.setData({ inputValue: inputValue, assign: false, passwd: this.data.passwd, curFocus: curFocus }) if (inputValue.length === 4) { this.setData({ list: [], loading: true }); wx.hideKeyboard(); this.loadList(true); } },
2022-08-11 - 复赛结束后微信群内参赛选手讨论内容值得学习的部分总结
一. 为何有这篇文档 记录原因:2022年8月5日13:00比赛提交结束后,各队伍均在微信群中进行了高参考价值的赛题相关方法讨论,现予以总结以备后期学习,同时也分享给初赛至今努力比赛的大家作为参考。 整理结构:将以信息流的结构对提出和回答问题的相关情况进行整理。 备注信息:各位大佬的讨论可能会因为上下文信息不能完整呈现,建议参考的时候自己检索具体知识点。记录的顺序是以讨论的时间顺序为准。大佬排名不分先后。另外各位大佬在讨论细节上存在的不同意见,可能存在不完整的展示。 受限于记录者水平不足,括号内的内容一般为文档撰写者的理解或者猜测。可能存在记录错误,恳请阅读者谅解。 二. 这篇文档具体内容 关于比赛后处理方案? 陪跑:我们的后处理的思路是把少类别的概率强行放大,这样初赛复赛都有比较稳定的提升(3k)左右.具体做法是 1/value_count,每个类别得到一个放大的概率值,大类是1,小类>1, 具体的值设置是通过初赛的线下五折调出来的参数。 mixup 是否有效? 辉:初赛尝试了mixup,复赛并没有用上,模型抖的厉害。 陪跑:我这里尝试了很多次mixup,但是没啥用。 伪标签是否有效? 陪跑:有效果,但取决于生成的标签的精度。在损失函数上的选择,可以考虑使用kd loss,也可以使用交叉熵。 郭大:这题的关键其实还是伪标签,提升是2个百分点,0.720–0.741.伪标签不是为了利用数据,而是为了利用大模型(估计是为了使用clip large)。在训练伪标签上,是否使用soft label或者使用hard label,区别并不大。如果soft 和hard 分别都用,融合还能提升分数。本赛题受限于规则上进行限时,所以需要伪标签,不限时的话,都不需要伪标签(猜测是直接用多个large融合)。 chizhu:伪标签在我们这里提升是2个百。0.689–0.710,限于挥霍的stacking细节,可能也影响了模型效果。具体使用了8帧的单折。我们采用了两阶段训练小模型,先用100万无标签数据训练第一阶段,然后第二阶段用9万真实标签修正,(可不可以描述的更细致一点呢?)。 挥霍的人生:受限于我们组的打伪标签的base模型精度太低问题,导致我们后期运用伪标签提升未达到预期(主要是上班太忙,没时间做题才是真相)。 UA:伪标签在本题中可以理解为蒸馏的思路,伪标签可以套娃,训练出好的模型,再去标注,再训练,再标注,形成一个循环过程。评估的标注是测试集上无法再次上分为止。当出现大量的unlabel数据的时候,就可以考虑使用伪标签,例如本赛题,特征均出现对齐的情况,唯一的区别是缺失label。如果使用kl loss做软标签,就可以看做是知识蒸馏。 Lawliet:我们一开始用的swin-base上分,后来看到大家都在传clip,就产生了敏锐的洞察力。 虚着点和气:关于chizhu描述先用100万无标签数据训练第一阶段,然后第二阶段用9万真实标签修正这部分,完全可以边打标,边修正。 本赛题限时的原因? ★★★★★官方解答: 复赛限制QPS的原因–>主要是更贴近实际工业应用场景,虽然内容理解不需要做到实时性,但机器是有很高成本的,大家可以考虑一下如果每天有数百万数千万的视频发表,而且有峰值低谷,需要多少台机器?如果QPS低,那么就需要更多的机器。实际上,工业应用要求的QPS会比这里高很多,inference的GPU也最多用T4。 特征提取的backbone用谁好? 各队伍都使用各种类型的视频特征预训练模型作为提取的主要方法,例如swin, tiny, clip, vit等,但从本赛题来看,最有效的方法仍然是clip。具体可以参考的代码地址如下: openai/CLIP 具体采用的预训练模型文件之一参考如下: ```shell {“ViT-B/32”: “https://openaipublic.azureedge.net/clip/models/40d365715913c9da98579312b702a82c18be219cc2a73407c4526f58eba950af/ViT-B-32.pt”} Lawliet, 陪跑:几乎所有的论文都是用clip的,这一点可以作为选择backbone有更好效果的一种指导方向。 高等数学:初赛我们就在使用clip,由于初赛已经明确了视频特征提取结果,因此是在文本上用clip提取的文本信息。(这一点的使用方法没有理解到)。 lawliet: 2022年7月29日clip开始上线了中文clip模型。 伪标签使用后是否会导致训练结果leak? UA:伪标签确实会存在leak,这时候验证集可能存在无法作为线上预期分数的参考的问题。 Tibur:为了评估自己的模型效果,当出现leak的时候,可以通过提交线上来查看是否自己的整体流程是有效的。 clip的学习率要设置成多少? 陪跑:bert的1/10作为参考,例如bert的学习率为1e-4,那么clip的学习率就是1e-5. 在这里需要配置分层学习率。 clip是否要冻结不参与微调训练? Tibur: 需要冻结,这里应该是指只需要做推理得到特征。 陈佳烁(两面包夹芝士): 可以一起参与训练,提升是0.7个百分点。在学习率设置上是5e-6.具体的学习率设置如下参考: [代码]shell -- other_learning_rate 5e-4 -- bert_learning_rate 5e-5 -- clip_learning_rate 5e-6[代码] 哪些预训练任务是有效的? Tibur: ita, itm 在我这边是最有效的。这是通过初赛消融得到的结论,复赛没时间。我的双流就是图片过backbone, 文本过bert,然后两个做cross_attentiony以后直接输出. 陪跑:我这边clip模型最重要,双流里,算text vision的相似度可以明显上分。我们的双流就是UniVL(3个transformer 编码器) + LOUPE.在双流里用UniVL,具体操作上使用clip替代了itm任务.在双流里,是文本过tfs,图片embedding过tfs,合并起来继续tfs.相当于是三个独立的tfs.这样的模型比单流要慢一点。我使用ALBEF的模型,如果用中间分层的话会出现过拟合,而且会预训练的时候没法在微调上产生效果,暂时未排查到原因。在设计MLM任务的时候,可以考虑使用ngram mask,也比普通的MLM任务有明显提升。 一只大海龟:ALBEF在这个场景下预训练不上分。 融合阶段使用logits融合好还是使用prob? prob在此处应该是指对logits做softmax后的结果。 陪跑:使用prob融合出现掉分。考虑单流和双流模型如果差异大的话,Logits不一定在一个向量空间内。融合方法上就是直接加权。 陈佳烁(两面包夹芝士):初赛上融合logits不如prob,复赛默认使用prob。 Tibur:我们对两种方法都做了尝试,但是没有区别。我的理解是应该融prob。 UA: 如果模型中存在很多的Norm,不同模型的预测结果的值域应该不会差太远。 数据上特别是asr ocr存在很多脏数据,是否可以做特征工程清洗呢?(玄学,清洗更可能的是掉分) Lawliet:初赛做了清洗,掉分。 Tibur:清洗文本有用,我洗了涨了3k。但是预训练后反而没用了。 还有哪些数据EDA存在很高的价值呢? 陪跑:top1的预测准确率acc是0.8左右,但是top5的hit就有0.95,这表明一般ground truth就在top5内。所以对小类乘以权重,变大一点,就可以让top2的到top1了。初赛上涨了3k,复赛更多上分5-6k。具体代码如下:[代码]from collections import Counter a = pd.DataFrame(Counter(train_data["category_id"].map(CATEGORY_ID_TO_LV2ID)), index=[0]).T.sort_index() a[0] = (10 / a[0] + 1) [代码] swa 应该在全量数据训练中如何参与做提升? 陪跑:我们swa了top5个。开着ema训练,最后再手动swa.(代码也非常简单,就是把字典里的参数的weight和bias提取出来加权)。 2021QQ浏览器比赛由于和本场比赛非常类似,第一名开源方案,在本场比赛中的预训练是否仍然有重要参考价值呢? Tilbur(队伍名:一拳超人):我们的单流参考了他们的方案,提升非常大,微调加上预训练可以直接到0.72。 虚着点和气:在具体参考和使用上,应该还是有一定区别,照搬过来应该效果不会很理想(应该是表达知识应该活学活用)。 复赛阶段非常讲究推理效率,有哪些方法可以提升推理效率呢?onnx,tensorRT,EFFT,half如何组合运用? 郭大:个人建议,如果不是真正到了瓶颈,由于TensorRT相对而言工程上比较复杂,在有限的比赛时间内,可以先不考虑调试成功这个,微调阶段仍然使用float32执行训练,简单的在推理阶段使用half()做推理就能加速很多。(当使用了half以后,TensorRT的效率二次提升效果并不太大)。在抽取视频帧的使用上,基于base的模型half以后可以支持28帧,追加上TensorRT可以使用32帧全量推理,而如果仅仅使用float32推理只能支持8帧。在half以后,实际上推理的结果基本上和float32区别不大,结果基本一致。(这个可以使用训练数据划分一折做验证,个人估计郭大这个在比赛期间分享的点应该是让很多团队受益了)。 UA:如果比赛使用的图像侧是Swin Transformer,可以参考使用EFFT做加速,可能比TensorRT更快。
2022-08-10 - 网络请求优化之使用本地缓存
[视频] 你好,我是李艺。 上节课我们主要学习了有关setData调用相关的优化技巧,这节课我们学习网络请求相关的优化技巧。 首先我们看一下问题,针对网络请求的优化主要有以下三个方面: 一、减少不必要的网络请求,使用本地缓存的数据代替从后端接口拉取的数据 二、优化网络请求参数,提高网络请求的通讯效率 三、优化网络请求的并发数,让优先级高的请求先执行 其中第二项在第6.8讲我们已经讲过了,这节课我们重点看一三两项的优化,下面看项目实践 。 首先看实践一:在本地缓存数据。 在首页的JS文件里边有加载小程序导航数据的代码,我们可以在这里尝试使用本地缓存技术,如我们屏幕上看到的截图,首先从本地缓存中尝试取出缓存数据,如果取到了就先用上,然后向后端发起网络请求,拿到最新的导航数据以后再调用setData重新设置一下数据,并把本地数据也刷新一遍,避免本地缓存过时,运行以后如我们屏幕上看到的,在调试区的Storage面板里边可以看到本地缓存的导航数据,但是这个实现方案是有瑕疵的,有什么瑕疵,先从本地缓存,再从后端接口请求。这是一个顺序的并发过程,实际上这个过程还可以再优化一下,改用并发复合命令,让两个异步操作同时并行。或者我们更简单一些,改成两个异步函数,同时开始执行也可以,优化就不做演示了。留给你自己实践一下。 下面我们进行实践一的代码演示。 首先我们看一下我们最终实现的一个源码,找到主页的JS文件,在这个地方,我们有一个从主页中,从后端加载导航列表这样的一段代码。这段代码它的主要的一个作用,在这个地方。从这个地方开始,它的主要的一个作用就是从接口去拉取导航数据。拉取完成以后,然后我们再去设置我们的data数据对象里面的navs,这样的一个列表,同时设置完以后,我们还需要将我们本地缓存里边的这个navs这个列表然后进行一个更新。由于缓存里面它存储的是字符串,所以这个地方我们要拿json方法,进行序列化,更新一下。 在前面我们是先向本地,通过getStorage这个方法,然后去取了一下本地缓存的数据。当然这个地方有可能会取不到,所以我们会首先做一个判断。如果能取到的话,我们就将它进行设置,同时在设置之前我们还需要拿JSON.parse,然后进行一个解析。因为我们取到的数据它是一个字符串的数据,这就是我们主要的代码。首先我们将这部分代码给它拷贝一下,来到我们的小程序项目里面,找到我们首页的js文件,然后在这个里面我们先搜索一下。搜索navs 在这个位置,这是我们现有的代码,它一上就是从后端进行下载,我们添加一个从本地获取数据这样的一个代码,同时在这个地方这个代码已经有了,所以我们不需要再添加了。本质上在之前这个代码其实它是不需要的,因为我们如果前边没有消费代码的话,我们在这个地方去设置本地数据,它本质上它也是无用的。现在这个代码我们已经设置完了,单击编译按钮,然后进行测试,注意看我们的导航区。当然我们编译模式现在可以改一下了,改成我们的普通编译模式,注意看一下我们的调试区,这里面有一个已取到缓存的导航数据,这个代码是在这一行打印的。然后在下面还有一个也取到了后端的导航数据,这行代码是在这个地方打印的。也就是说我们这个代码它首先会从本地,然后取到缓存的数据,然后并且马上启用,同时它接着又向后端发起接口的请求,然后再获取数据,同时将本地的缓存的数据然后进行一个刷新。下面再看在我们这个调试区它有一个面板,一我们本地的这个Storage面板,这个里面navs这就是我们本地缓存的数据,这个代码演示我们就到这里。 下面我们看实践二。 打破网络请求的10个并发限制,并按优先级排序。由wx.request接口发出的网络请求,有最大10个的并发限制。为了破除这个限制,同时让高优先级的网络请求操作先执行,我们可以进一步改造我们的request工具函数,改造以后这个函数的代码如我们屏幕上显示的。首先我们引入了一个自带优先级的异步队列,叫做priority-async-queue。这个模块需要使用yarn或者npm安装,安装指令如屏幕上所示。安装以后在工具菜单栏,别忘记选择构建npm进行模块代码的构建。在使用自定义的request方法的时候,针对重要的网络请求只需要添加一个值等于urgent的一个priority参数即可,如我们屏幕上显示的这样。感觉是不是很简单。调用方法及其他参数都不需要修改,这是接口迭代进化中的向后兼容性,可以最大程度的保证旧代码在项目迭代中的一个持续使用,运行效果与之前一样。在网络环境顺畅的情况下基本上是无感知的。 下面我们看代码演示。 查看package.json这个文件,在这个文件里面我们可以看到多了一个模块的引用,这个模块叫做priority-async-queue,我们将这个给它复制一下,模块名给它复制一下,后面的版本是2.1.1,如果为了保证这个版本一致,稍后我们在安装的时候还可以将这个版本号给它加上。拷贝以后我们需要打开一个本地的终端窗口,终端窗口我们可以在VSCode里面操作,当然也可以在我们的微信小程序里面也是可以的。在我们miniprogram上面选择内建终端打开,选择以后这个地方我们使用yarn add,然后将我们刚才那个名字给它拷贝一下,还可以附带我们的版本号。添加,很快它就已经装上了。装上以后我们还可以顺带看一下这个目录下的package.json文件,确认一下 这个地方已经有这个模块了,这是第一步。第二步就是改造我们的request方法,目前我们request方法它是不支持优先级的,我们需要对它进行一个改造,打开我们已经修改好的代码,首先在最上面有一个对我们这个模块新安装模块的一个引入,同时下面有一个queue对象的创建,这个数字代表是我们最大允许的一个并发数字默认等于10。当然我们也可以传一个其他的数字都是可以的,然后将这个代码放在文件的最上面,再往下这个地方有一个关于优先级的定义,包括这个地方,它有一个导出,这个代码我们都需要拷贝过来,然后放在这里这些参数,还有这些参数其实都不需要修改,然后这个地方有一个关于默认的优先级的设置,如果它没有优先级的话我们就给它一个normal的这样的优先级。再往下在这个里边重点的代码在这个地方有一个queue.addTask,同时将priority优先级也给它传进去,后面是一个箭头函数,这个箭头函数它代表的是一个匿名的一个闭包。我们可以将这个代码给它拷贝一下,然后放在这个里面可以对比一下我们上下,它这个代码的一个区别。其实等于是我们将原来的代码,也就是这个代码放在了它的里边,同时将用addTask方法对它进行了一个封装。封装以后我们原来这个代码它是作为,然后由闭包函数然后封装一下,然后作为第二个参数,然后传进来的,是这样的一个改造方式,这样就可以了。 现在我们需要对我们原有的代码做一个改造。我们目前有一个是拉取首页数据的这样的一个代码我们修改一下它的优先级,在这个文件里面retrieve_home_data.js,然后这个里边,有对request的方法的一个调用,在这个地方,有一个调用,然后我们要在原来的这个位置,原来它是有一个url参数,我们再加另外的一个参数,就是priority,然后它的值我们让它等于urgent,等于这个,确认一下priority。这是一个priority,让它等一个urgent代表是最高的一个优先级,这样修改就可以了。修改完成以后我们单击编译按钮,然后进行测试,这个地方出现了一个错误。在调试区,大意是说module,然后这个module is not defined没有定义。这个模块没有定义,为啥没有定义,为什么没有定义?因为我们在本地刚才安装了第三方的模块以后我们没有选择,我们没有在这个菜单里面选择工具构建npm。这一步很重要,只有构建以后,我们新添加的模块它才会从我们的目录下面小程序这个目录下面有一个是node modules,从这个目录下面然后再转到我们的这个npm,就转到这个miniprogram_npm 转到这个目录下,转到这个目录下以后,然后我们才可以去加载和使用priority-async-queue这样的一个模块。现在这个目录下它已经有了,说明我们现在可以访问了,我们再次单击编译进行测试。现在代码错误已经不存在了,然后我们再看数据的表现,数据仍然可以加载,也没有问题,这个代码演示就到这里。 下面我们看一下小结,关于本地缓存接口,前面我们已经介绍过了,它们都是同步接口,即使像wx.setStorage、wx.getStorage这样不以Sync结尾的接口,由于某种历史原因,它们也是同步接口。那么至少目前是这样的,以后可能会修改,所以在使用这些接口的时候,我们一定要特别注意使用的时机,最好在Page.onReady周期函数中,或者是在之后的时机使用本地缓存接口,使用本地缓存。另外一个特别需要注意的点是一定要时刻铭记,本地缓存的数据是不可靠的,本地数据有可能因为各种原因缺失或者损坏。使用本地缓存的数据,但不能依赖本地数据。当获取本地缓存数据失败的时候一定要有后端接口可以顶上,或者是其他的方式可以顶上。对于小程序里边的wx.request接口可以管控起来,不仅因为网络请求在低版本的基础库版本中有最大10个并发限制,还处于优先级排序的需求,以及有可能存在的页面访问的权限控制要求。在实际的项目开发里面,某些后端接口是一定要用户实现鉴权以后才允许访问的。这类统一的鉴权访问控制就适合在request工具函数中统一实现,这部分内容不属于优化内容,但是对项目来说也十分必要,如果需要拓展的话都可以在request.js文件的基础之上然后进行修改。 点击查看相关文档: 数据缓存 /wx.setStoragepriority-async-queue这节课就讲到这里,上面的网址是本课涉及的文档地址。 这节课我们主要学习了如何使用本地缓存数据,即如何使用优先级队列优化网络请求。下节课我们学习图片优化技巧。 最后我们看一下思考题。这里有个问题请你思考一下,webp是Google在2010年推出的一种新的图片格式,它使用更优的图片压缩算法在相同的图片质量下能让图片保持更小的图片体积,Youtube的视频缩略图采用webp格式以后网络加载速度提升了10%左右,Google的Chrome网上应用商店采用webp的格式以后每天大概节省了几TB的一个带宽,现在小程序中的image组件也开始支持使用webp格式的图片了,但一般我们在团队开发中使用的图片多半是png,或者是jpg这样的格式,那么有什么办法可以快速将这些图片转化为webp格式,并且在小程序项目里边使用。下节课我们就一起来深入探讨一下这个问题。
2022-07-15 - 使用recycle-view做长列表如何做上拉加载?
使用recycle-view做长列表如何做上拉加载?
2020-05-08 - 复杂场景无法自动化测试?Minium 测试试一试!
录制回放文章 提到录制回放支持输入、文本查找、断言等自动化测试基础操作,无需编写代码,用例生成效率高,但是该操作对于部分复杂的业务场景具有局限性。如果用户希望适用复杂的业务场景,自主制定测试场景,自定义测试(Minium)方案就非常适用了。 自定义测试 Minium 能力介绍 小程序测试框架 Minium 是微信测试团队为小程序开发或测试同学提供的一套测试接口,它实现了 miniprogram-automator 中小程序自动化的所有能力,例如直接触发小程序页面元素、设置页面数据、向 AppService 注入代码片段、Mock / Hook wx 对象的接口等支持并封装所有的原生操作,屏蔽 iOS / Android 底层差异,实现了一套脚本在三端同时运行。用户写好的 Minium 脚本,可以在本地执行,也可以直接上传到微信小程序云测服务执行,无需准备和维护真机环境。 自定义测试 Minium 流程介绍 一、 编写用例编写小程序自动化测试脚本,常见操作包括: 基本操作,例如如页面跳转,元素定位及相关操作处理小程序 API处理小程序的原生控件,例如处理授权弹窗支持数据驱动测试1、基本操作例如定位小程序页面元素、操作元素、跳转页面等。例如以下用例 class FirstTest(minium.MiniTest): def test_network(self): # 页面跳转 self.app.navigate_to("/packageAPI/pages/get-network-type/get-network-type") # 元素定位 ele = self.page.get_element("button", inner_text="获取手机网络状态") # 元素点击 ele.click() # 打印元素文本 self.logger.info(self.page.get_element("/page/view/view[2]/view/view[1]/text").inner_text) 2、处理小程序 APIMinium 框架提供处理小程序开放 API 方法,根据需求选择相应方法,例如 mock_wx_method() — mock 小程序 API 的调用hook_wx_method() — hook 小程序 API 的调用call_wx_method() — 调用小程序的 API...更多接口方法参考 Minium 接口。以调用小程序 API 获取回调信息为例: class FirstTest(minium.MiniTest): def test_call_wx_method(self): """ 调用小程序API,获取回调对象 :return: """ sys_info = self.app.call_wx_method("getSystemInfo").get("result", {}).get("result") self.assertIsInstance(sys_info, dict, "is dict") self.assertTrue(True if sys_info else False, "not empty") 3、处理小程序原生控件Minium 提供了针对小程序内涉及原生控件 (授权弹窗、弹窗、地图、分享小程序等) 的操作封装。 注意:部分封装的接口暂不支持 IDE 平台调用。若跑测平台是 IDE,则需要在 config.json 中配置 mock_native_modal 配置项,后通过 mock 的方式实现 处理模态弹窗用例示例如下 class FirstTest(minium.MiniTest): def test_native(self): self.mini.clear_auth() self.app.redirect_to("/pages/testnative/testnative") called = threading.Semaphore(0) callback_args = None def callback(args): nonlocal callback_args called.release() callback_args = args # hook showModal方法,获取回调后执行callback self.app.hook_wx_method("showModal", callback=callback) self.page.get_element("#testModal").tap() time.sleep(2) # 点击弹窗 确定 self.native.handle_modal("确定") is_called = called.acquire(timeout=10) # 释放hook showModal方法 self.app.release_hook_wx_method("showModal") self.assertTrue(is_called, "callback called") self.assertDictContainsSubset( {"errMsg": "showModal:ok", "cancel": False, "confirm": True}, callback_args[0]) 跑测平台 IDE,[代码]config.json[代码] 配置 [代码]mock_native_modal[代码] 示例如下 "mock_native_modal": { "showModal": { "title": "test modal", "content": "modal content" }, } 4、数据驱动自动化测试往往需多组数据测试,若采用录制回放测试,则需录制多个用例,灵活性不足,所以若需测试同一个用例不同组测试数据,可使用数据驱动(DDT)模式,实现测试数据与测试脚本的分离,通过 DDT 将测试数据加载到脚本中。数据驱动(DDT)有以下优点: 灵活配置测试数据与功能代码分开易维护以集成数据驱动测试 (基于 DDT 封装) 为例: @minium.ddt_class class BaseTest(minium.MiniTest): @minium.ddt_case([], ["1", "2"]) def test_evaluate_sync(self, args): """ 向 app Service 层注入代码 同步返回结果 :param args: :return: """ # 参数 args: [] args: ["1", "2"] result = self.app.evaluate( "function(...args){return `test evaluate: ${args}`}", args, sync=True ) self.assertEqual( result.get("result", {}).get("result"), "test evaluate: {}".format(",".join(args)) ) 此外,DDT 模式还支持给具体的 test data 命名,自定义命名会体现在测试方法名中。数据驱动详情可参考 测试流程控制 & 数据驱动测试。 二、 执行用例开发者编写完 Python 用例脚本后,可本地调试,也可在云测上测试。 1、本地执行开发者将编写好的用例进行本地调试,minitest 命令加载用例,初始化环境,开启自动化能力,进行环境检查,后执行用例。该方法需 IDE 依赖,支持 USB 真机调试。在初始化环境过程中遇到常见问题如下: 开发者工具没有自动打开,建议先排查微信开发者工具自动化能力,进行 环境检查配置真机环境但无法拉起真机上的小程序,建议排查是否使用真机调试 2.0,如果是,切换使用真机调试 1.0报错 traceback 中有出现 _miniClassSetUp 的调用,确认微信开发者工具选用的基础库是否为最新版本。路径如下:开发者工具项目窗口右上角 -> 详情 -> 本地设置 -> 调试基础库为了保证同一套代码在 IDE、Android、iOS 上运行,Minium 环境组成比较复杂,所以测试用例的运行依赖于配置文件,支持配置运行平台、IDE 监听端口号、连接手机的参数、账号信息、自动处理授权弹窗等等。具体操作可参考 项目配置。 执行完用例后,云测平台生成日志文件,提供本地测试报告,包括截图、运行日志、错误日志。具体实践可参考 示例。 [图片] 2、云测服务执行开发者可以将本地调试好的用例上传至云测平台,新建测试计划 -> 新建 Minium 任务,可选择多平台真机,且支持多平台同时运行,无需用户部署和维护真机环境。 测试结束后,云测服务提供详细的测试报告,包括运行截图、日志信息、网络请求分析、性能分析等。当用例执行失败时,云测服务会提供错误日志及错误行代码,方便用户排查错误原因。具体操作可参考云测官方文档 自定义测试。 [图片] 对比[图片] *详情点击查看 虚拟账号使用流程、打通 devops 流程。 最佳实践分享对于第三方小程序服务商测试团队来说,多个小程序开发管理工作导致更复杂的测试情况。以明源云测试团队为例,他们需要管理多个小程序,并且每个小程序的页面数量较多。传统的手工测试显然无法覆盖业务需求。利用 Minium 自动化测试,明源云测试同学通过以下操作实现快速的自动化测试: 使用了 Minium 框架编写自定义测试用例,执行超过 90 个用例;并且在编写用例时采用了 Page Object 模式(简称 PO 模式),将测试用例和页面元素定位、元素、元素操作等分离,提升用例复用性,降低维护成本;在执行用例的具体过程中,打通云测服务和内部的 devops 流程,利用云测第三方接口,定时触发或者自动触发自动化任务,然后利用查询任务接口,再将测试结果同步到内部的用例管理平台。如果有问题提单给程序修复,实现整个流程闭环。[图片] [图片] 总结通过自定义测试(Minium),小程序测试团队能够自主定制测试场景,并且结合云测服务,获取详细的测试报告,实现较高的灵活度。 小程序云测服务提供 3 项服务,每项服务具备不同的特点,适用于不同的场景,助力开发者提升测试效率。 [图片] 如有更多小程序云测服务的相关问题,可点击 微信小程序云测服务专区 发帖反馈,技术专员将为大家解答及进行深度交流。
2022-08-05 - 如何创建高效数据库索引
在创建索引上,建议每个生产环境查询都应有索引支持,并且尽可能使用组合索引,同时注意组合索引升降序,并利用覆盖索引高效查询。此外,还有大数据量下应该避免使用低区分度操作符等8个实践建议,一起通过视频了解一下吧。 [视频]
2021-09-22 - 微信小程序自定义watch属性
微信小程序没有提供类似vue中watch类似的监听属性,如果我们想用,就得自己写一个,直接上代码。 注意:vue中的watch监听属性滥用会造成性能问题,这里自定义的watch也是一样的,要适度使用。 watch.js const observe = (obj, key, watchFun, deep, page) => { let val = obj[key]; if (val != null && typeof val === "object" && deep) { Object.keys(val).forEach((item) => { observe(val, item, watchFun, deep, page); }); } Object.defineProperty(obj, key, { configurable: true, enumerable: true, set: (value) => { watchFun.call(page, value, val); val = value; if (deep) { observe(obj, key, watchFun, deep, page); } }, get: () => { return val; } }); } const setWatcher = (page) => { let data = page.data; let watch = page.watch; Object.keys(watch).forEach((item) => { let targetData = data; let keys = item.split("."); for (let i = 0; i < keys.length - 1; i++) { targetData = targetData[keys[i]]; } let targetKey = keys[keys.length - 1]; let watchFun = watch[item].handler || watch[item]; let deep = watch[item].deep; observe(targetData, targetKey, watchFun, deep, page); }); } module.exports = { setWatcher }; 使用示例: // 引入自定义监听属性 import watch from '../../core/js/watch';//这里的路劲取决于你自己的watch.js文件的存放路径 Page({ data: { name: "测试watch" }, onLoad() { watch.setWatcher(this); let that = this; setTimeout(function () { that.data.name = "测试watch变化了" }, 5000) }, watch: { name: function(newVal, oldVal) { //这里的this和onLoad里的this指向一样 console.log(newVal, oldVal); } } });
2022-08-03 - 使用串发命令模式延迟同步请求(上)
[视频] 你好,我是李艺。 上节课我们主要学习了如何将前端计算工作后移,这节课我们学习如何使用数据缓存。 下面我们看一下问题,一般情况下wx API以Sync结尾的接口是同步接口,例如像wx.getSystemInfoSync还有wx.setStorageSync,这些接口都属于同步接口,反之不以Sync结尾的接口都是异步接口,由于历史原因,有些接口虽然名称上它由Sync结尾,但实际上却仍然是同步接口,例如像那个wx.getSystemInfo还有这个wx.getStorage以及wx.setStorage这三个接口在开发里面还经常用到,而且经常是在App.onLaunch还有Page.onLoad,这些周期函数里面用到的,这对启动性能其实它是十分有影响的,小程序的启动流程是不能有任何人为的同步代码阻塞主线程的,如果需要拉取这些系统信息的话,在优化的时候我们可以由分接口代替,我们看一下都有哪些分接口。 例如第一个getSystemSetting接口是获取设备信息,还有像getAppAuthorizeSetting接口是获取微信APP授权信息 授权设置信息,再往下是getDeviceInfo是获取设备基础信息,第四个是getWindowInfo是获取窗口信息,最后一个是getAppBaseInfo是获取微信APP的一个基础信息,这里的每一个分接口它返回的信息都不一样,需要什么信息我们就调用什么样的信息的一个分接口就可以了。 还有在小程序启动过程中,我们尽量先使用默认参数在启动完成以后,也就是在Page.onReady这个事件派发以后再进行相关接口的一个调用,对于缓存后端接口数据的本地缓存存取代码,如果没有必要的话也要尽量放在启动流程完成以后,也就是Page.onReady这个事件派发以后再去调用,这里有一点我们需要注意,就是小程序现在它有一个接口叫做wx.getSystemInfoAsync,注意这个里面多了一个A ,这是一个不多见的以Async结尾的这样一个接口,它是一个异步接口,可以异步拉取这个系统信息,不阻塞主线程,但是这个接口需要一定的微信客户端版本支持,如果不在受支持的客户端上面使用、调用的时候,它会自动地又用原来的同步接口进行代替,这是我们要注意的,下面我们看项目实践。 首先看实践一,创建SystemInfoManager模块使用串发命令模式延迟同步请求。 目前在我们的app.js文件里面,在它的App.onLaunch这个周期函数里面有对wx.getSystemInfo接口的调用,这个代码是同步的,它会阻塞我们小程序启动流程的主线程的一个执行,我们必须将它进行改写,可以在globalData里面先定义默认参数,在启动的时候拿这些默认参数先给程序使用,然后在Page.onReady这个事件派发以后再拉取实际需要的这些数据。 下面在改造过程中我们将创建一个SystemInfoManager模块,用这个模块专门用于处理系统信息的一个拉取,目前我们需要的系统信息是比较有限的,可以先实现一些基础的代码,后续如果还需要其他的一些系统信息可以再逐步进行扩展,接下来就是使用串发复合指令对象延迟执行系统信息的一个拉取,这部分代码我们要放在app.js文件里面,这个代码量可能会稍微有一点点多,但是这个代码的逻辑还是比较清晰的。我们先在globalData上面设置默认的全局信息,然后再创建串发的复合命令对象 创建完成以后将它进行执行,那么我们什么时候让串发命令对象开始执行,可以在首页的JS文件里面,它的onReady周期函数里面设置命令的一个启动,这样关于系统信息的拉取操作,它其实就是在首屏渲染完成以后才开始执行的,这样一种设置其实不影响我们小程序整体的启动,串发复合命令的对象它有一个特点,就是前面的子命令完成以后后面的子命令它才会开始执行,我们利用这个特点就可以延迟这系统信息的一个拉取了。 在这里有一个问题请你思考一下,为什么我们要把拉取系统信息的主要代码要写在app.js文件里面的onLaunch周期函数里面?因为globalData它是在这里定义的这段代码的一个主要作用,为了拉取 往globalData对象里面存储的这些信息,那么将这些代码放在这里是最合适的了,面向对象模拟真实世界的事物关系,它使得我们软件设计有规可依,但是面向对象它有一个缺陷就是容易将相互联系在一起的代码人为地给它隔离开,这种情况下 我们利用这种串发的复合命令对象就可以巧妙地弥补缺陷,下面我们看代码演示。 首先打开我们微信小游戏这个项目,在开始改造之前我们先看一下我们目前的项目在执行的时候,它这个信息是如何获取的,重启一下我们这个项目,注意看一下我们调试区,这个地方有一个打印 已取到系统消息,这个信息我们是在哪里打印的呢,是在app.js里面对不对,我们打开代码看一眼,现在开发者工具因为我同时开了录屏的原因,它启动以及运行变得非常的一个卡顿,其实如果把录屏关了以后,它运行效果还是可以的,我们看一下这个代码里面,这个地方有个打印已取到系统消息了,这是我们在调用getSystemInfo接口以后,就是我们拿到这个系统消息以后打印的一个信息,然后注意一下我们这个信息打印,它其实是在哪里,在index onready之前,它其实在它的前面打印的,也就是说我们的首页渲染还没有完成,这个系统消息已经拉取了对不对,当然这个代码它是同步的,它会阻塞我们整体的一个流程的进行,所以我们需要将它进行优化。 优化的第一步,首先我们要创建一个SystemInfoManager,这样的一个管理器模块,管理器模块我们要放在我们的library manager放在这个下面,这个下面还没有,但是我们可以去我们的最终源码里面看一下,因为那个地方肯定有已经写好的代码对不对,6.5.1找到miniprogram,然后library manager,这个文件就是我们需要的文件 将它拷贝一下放到我们目前的目录下面,library然后manager放在下面。 现在我们看一下我们这个代码主要是做了什么事情,首先是引入它,因为我们接下来有相关接口的调用需要用到这个工具方法,这是一个类 SystemInfoManager,然后在下面我们有个导出,其实直接导出的是它的一个实例,它实例化的一个实例,本身这个模块在我们程序里面可以说是单例的,在主线程里面它是唯一的,在这个里面有一个很重要的方法就是retrieveSystemInfo,一上来我们会判断一下是不是可以调用这样的一个接口,这个接口,先前我们提到了它其实是一个什么样的接口,一个特殊的异步接口对不对,getSystemInfoAsync,很少有接口这个里面是加A的,一般都是加Sync后面的后缀,看它能不能用,如果能用我们就用它,如果不可以我们就用后面getSystemInfo进行调用,进行调用完以后拿到这个结果,这个地方拿到结果以后,在这个地方我们不能直接用这样的一种方式,就是wx.getSystemInfoSync等于它 这样是不可以的,这个代码我们是想重写接口的一个实现,让这个接口直接返回,我们已经取到的信息不需要重复地去拉取了,当然这样不可以的话可以变通一种方式,用Object的里面的一个defineProperty,用这样的一个接口,这个是JS的方法,JS对象的一个方法用于我们在一个对象上,定义它的属性,我们用这个,然后同时将后面的参数给它置为true,把它value写成一个箭头函数让它返回,这样就可以了,这个方法执行完成以后,我们这个信息就拉取到了,拉取到了以后我们再调用这个方法,就是getSystemInfoSync 再调用它的话其实它已经不是一个同步接口了,它其实就相当于调箭头函数,然后直接把已经拿到的本地的信息给它取出来,再取出它的statusBarHeight就是状态栏的高度,这个高度我们在代码里面会用到,还有屏幕的分辨率也会用到,还有这个信息稍后也会用到,这就是它的简单的一个实现,管理器代码然后完成以后,接下来我们要去调用它。 首先我们要看一下app.js,app.js里面我们有哪些改造,在这个地方有一个按新方式拉取系统信息,这是我们的主要的一个代码,这个代码稍微有一点点长对吧,这个代码给它拷贝一下到这个地方拉取新的信息,这是我们原来代码 原来这个代码里面干了什么事情,拿到这个信息以后,我们设置了这个信息,还有是又调用它 拿到custom又设置这些信息对吧,还有这个信息,它设置了一系列的一个信息,这些信息可能是在接下来这个程序运行的时候需要用到的一些信息,将这个先给它注掉,然后将我们新代码给它放在这个地方来,看一下新代码。 首先一上来我们先设置这些,这个是我们默认配置,我们当前以什么样的一个屏幕大小进行测试的时候,我们就设置什么样的一个默认的信息,把这个信息给它设置上 设置完以后,我们这个程序就可以用这些默认的信息了,再往下是我们引入了三个模块,其中就包括我们新创建的 system_info_manager,引用它的实例,再往下这个地方我们有个ClosureCommand,ClosureCommand是我们的一个闭包指令对象,在这个里面,我们首先会这个地方注意,这有一个await,因为我们这个地方是async 这个地方是await,然后去调用它的retrieveSystemInfo,因为这个调用它会占用一些时间,所以它是同步的,会占用一些时间,取到以后会打印这些信息,已取到系统信息,在取到完成以后,我们这个地方看一下,这有一个statusBarHeight的一个获取,我们可以调这个接口拿到,还有custom 调用它里面的方法拿到,然后这个信息 就是这些信息包括这个信息其实就是我们原来的这些信息,它就是稍微修改一下用新的方式去设置了,这就是它的一个修改,我们现在不需要了把它删掉,这是在app.js里面所要做的一些修改。 接下来我们还要看在我们主页里面还有一个修改,主页里面还有修改,在index目录下面要找到我们的onReady周期函数,在这个地方允许异步拉取系统信息了,将这个给它拷贝一下到我们项目里面来 这里,放在这个地方 onReady完毕了,我们调用了asyncRetrieveSystemInfo,这个对象它的getCommand取到它的第一个子命令,然后markComplete标记它的完成,然后后续的子命令才可以执行,因为这个对象它本身是一个串发复合命令。 我们再确认一下我们这个里边,在app.js里面,我们来看一下它这个命令,其实在这个地方复制的 看到没有,等于它对不对,然后这个里面它的这个地方有一个new ClosureCommand,它虽然没有传任何代码,但是它也占据了一席之地,然后它的第二个子命令才是cmd,才是我们前面的这些,第一个子命令它不完成的话,第二个子命令它就不会执行,也就是我们上面这些代码就不会执行,然后它里面的同步调用它就不会占用时间,这样一来对我们启动 整体的小程序的启动它就没有影响了,而在我们主页里面,在这个页面首页 首屏渲染完成以后,把第一个指令然后设置完成了,设置完成以后,它这个复合对象就开始执行,第二个指令就开始执行,然后拉取系统信息代码也开始执行,这样的话就不影响了,代码改完了。 接下来我们打开我们微信开发者工具单击编译,看一下它的实际运行效果,注意看一下已取到了系统信息,看到没有,这有一条打印信息,现在信息的打印它已经在index onready,index onload,这两个信息打印在它的后面了 也就是说它现在要做的工作其实已经不再影响,我们小程序的一个正常启动了,代码演示就到这里。
2022-07-14 - [拎包哥] 批量下载云开发云存储的文件到pc端
官方教程 注:第2步和第3部的代码是反过来的。 [图片] 我的步骤: 0.按照官方教程安装tcb脚手架。 1.在桌面新建文件夹hellowWorld,新建cloudbaserc.json,json里的内容为: [图片] { "envId":"你的云开发环境的id" } 2.在cmd输入命令行。 e.g. 下载云存储里的qrCode文件夹到PC端文件夹(桌面的helloWorld文件夹)。 注:--dir在这里的意思是声明前面的 . 是一个文件夹,不需要另外修改。 [图片] tcb storage download qrCode . --dir
2021-09-28 - recycle-view 商品长列表应用
recycle-view 在商品长列表中的应用 参考官方文档 WXML: [代码]<recycle-view class="list" batch="{{batchSetRecycleData}}" id="recycleId" bindscrolltolower="bindscrolltolower" scroll-y="true"> <recycle-item class="record" wx:for="{{recycleList}}" wx:key="id"> <view class="record_image" style="background:url( {{item.images[0] }}&x-oss-process=image/resize,s_320 )"></view> <view class="record_view">{{index}} . {{item.title}}</view> </recycle-item> <view slot="after">加载中...</view> </recycle-view> [代码] JS: [代码]const app = getApp() const createRecycleContext = require('miniprogram-recycle-view'); Page({ pageNum:1,//页码 listobj: Object,//RecycleContext对象 postflg:true,//是否可以加载列表,用户误触控制 windowWidth:0,//系统页面可视宽度 data: {}, onReady: function () { var than = this; //获取系统参数 wx.getSystemInfo({ success: function(res) { than.windowWidth = res.windowWidth; //创建RecycleContext对象来管理 recycle-view 定义的的数据 than.listobj = createRecycleContext({ id: 'recycleId', dataKey: 'recycleList', page: than, itemSize: than.itemSizeFunc, }) than.getlist();//请求接口 }, }) }, //设置item宽高信息,样式所设必须与之相同 itemSizeFunc: function (item, idx) { var than = this; return { width: than.windowWidth * 0.47, height: than.windowWidth * 0.61 } }, //滚动到底部监听,分页加载 bindscrolltolower(e) { console.log('滚动到底部----'); if(this.postflg){ this.postflg = false;//请求完成前不再更改页码请求接口 this.pageNum++; this.getlist(); } }, //数据请求 getlist(){ var than = this; wx.request({ url: 'https://w.taopaitang.com/api/discover?page=1&pagenum=10', data:{ page: than.pageNum, pagenum:10, }, method: 'get', success(res){ console.log('数据请求成功----' + than.pageNum +'---',res); if(res.data.message){ //append RecycleContext 对象提供的方法:在当前的长列表数据上追加list数据 than.listobj.append(res.data.data.items); than.postflg = true; } } }) } }) [代码] 全部代码 -------> 代码片段 <--------- 我想把此列表改成瀑布流展示,求路过的大神指点下!!!!!!
2019-10-07 - 小程序端会话场景下长列表实现
1 前言 腾讯云医小程序中有医生和患者聊天的场景,在处理该场景的列表过程中遇到两个问题: 一是下拉加载历史消息时需要在容器顶部进行衔接导致的界面抖动问题;二是大量的会话内容导致的长列表问题。 问题一:插入历史消息带来的抖动问题是因为在已有dom的前面插入dom。如果能够在已有dom的后面插入新增dom并且在视觉上看起来是在顶部插入的则可以解决该问题。前端开发中聊天场景的体验优化一文中给出的方案是[代码]transform:rotate(180deg);[代码]。另外[代码]flex-direction:reverse[代码]也是可以做到的。 由于会话场景的一些其他特点如列表初始化时定位在底部(新消息在底部),本文的实现采用了[代码]transform:rotateX(180deg)[代码]方式处理进行处理。由于只需要在垂直方向进行翻转,所以在实现时使用rotateX代替了rotate。 下面简易demo说明该样式应用后的效果 [代码] .container { height: 100px; overflow: auto; } .item { width: 100px; border: 1px solid black; text-align: center; } /*关键*/ .x_reverse { transform: rotateX(180deg); } [代码] [代码]<div class="container x_reverse"> <div id="item-1" class="x_reverse item">数据项-1</div> ... <div id="item-9" class="x_reverse item">数据项-9</div> </div> [代码] 添加[代码].x-reverse[代码]样式前后的初始状态对比 翻转前 [图片] 翻转后 [图片] 问题二: 长列表问题。 我们先在h5端看下大量的dom会有哪些问题,如下demo验证 [代码]<button id="button">button</button><br> <ul id="container"></ul> [代码] [代码]document.getElementById('button').addEventListener('click',function(){ let now = Date.now(); const total = 10000; let ul = document.getElementById('container'); for (let i = 0; i < total; i++) { let li = document.createElement('li'); li.innerText = Math.random() ul.appendChild(li); } }) [代码] 在chrome的开发者工具performance栏下记录点击button后的运行过程,可以看到包含脚本运行在内的整个运行过程中 Rendering部分占用时间最多(包含[代码]Recalculate Style[代码]、[代码]Layout[代码]、[代码]Update Layer Tree[代码])。当列表项数越多并且列表项结构越复杂的时候,会在Recalculate Style和Layout阶段消耗大量的时间,所以有必要减少列表项的同时渲染。 [图片] 小程序的架构决定着小程序端该问题相较于h5端更为突出。在微信小程序官方文档 -> 指南 -> 性能与体验部分提到一些点如:setData数据大小、WXML节点数等原因都会影响到小程序的性能。以及图片资源的主要性能问题在于大图片和长列表图片上,这两种情况都有可能导致 iOS 客户端内存占用上升,从而触发系统回收小程序页面。显然在长列表场景下如果一次性将所有的数据全部加载出来就会有WXML节点过多,setData数据量过大的问题、图片资源过度等问题。 这些问题不仅仅是列表在初始化的时候存在,如在插入新数据(unshift)需要将整个数组进行传递,以及更新列表项数据时diff时间也会增大。 微信小程序官方提供了recycle-view组件来解决等高列表项的情况。但是对于会话场景下消息的高度是不等的,因此我们得自己实现一套符合这种特性的长列表组件。 2 接入前后对比 2.1 视频效果对比 对比腾讯云医小程序会话接入长列表组件前后的效果,优化前滚动过程中有卡顿的感觉,并且在发送消息的时候,消息输入框进入到列表中的延迟能够比较明显的感受到,优化后滚动较丝滑,并且发送消息没有明显的延迟。 接入前:https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john/after-chat.mp4" 接入后:https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john/before-chat.mp4" 对比腾讯云医小程序->群发助手下的患者列表初始化和选中时接入长列表组件前后的对比 接入前 :https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john/group-send-before.mp4 接入后:https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john/group-send-after.mp4" 2.2 数据对比 这里对比下群发助手接入前后的setData(发起到回到)时间的对比 初始化用时对比 [图片] 选中item用时对比 [图片] 上面两张图的横坐标是数据条数,纵坐标是setData时间,可以看到无论是初始化还是选中操作二者的轨迹都是相似的 明显的看到接入前,setData的时间随着数据量的增大越来越大,接入后则没有这个问题。显然,接入后通信的数据量,diff时间,浏览器渲染时间都会较少。 3 基础实现 关于长列表实现的基本思路是只渲染可视区域及其附近的几屏数据,但是由于小程序端和h5端架构的差异导致二者在具体实现上存在差异。 3.1 如何模拟滚动条? h5实现长列表的常规思路 [代码]<div id="list-container"> <div id="list-phantom"></div> <div id="list"> <!-- item-1 --> <!-- ... --> <!-- item-n --> </div> </div> [代码] #list-container 滚动容器 通过引入#list-phantom来占位,高度为列表项高度之和用于撑开容器形成滚动条 #list用来装载列表数据 当有新的列表项添加后,则更新#list-phantom高度从而达到模拟滚动条的目的,然后通过监听#list-container的scroll事件在其回调中根据scrollTop来计算出现在可视区域的内容。 浏览器是多进程多线程架构,浏览器中打开一个tab页时可以认为是打开一个渲染进程,渲染进程中包含了GUI渲染线程(包括了html、css解析,dom树构建等工作)和js引擎线程等等。我们知道GUI渲染线程和JS引擎线程是互斥的,js引擎发起界面更新到渲染完成是同步的。 而小程序架构的通信是异步的,比如逻辑层setData发起通信到渲染层,通信过程中渲染层依然在执行的。如果按照h5的思路去计算,逻辑层计算的结果到达渲染层后就已经不是正确的结果了即界面中的数据和滚动条的位置是对不上的。 为了保证滚动条的位置和数据项所在位置是正确对应,起初的想法是列表项消失后通过一个同等高度的div元素进行代替,这样做带来的问题是依然会产生大量的dom元素。进一步的想法是通过对列表数据进行分组并且每个分组在界面中会存在一个真实的dom(称为分组dom)来包裹该分组内的所有列表项,并且认为每个分组算是一屏数据,当每个分组从界面中消失时,分组dom不会删除,只会删除内部的列表项,并且将消失的列表项高度之和赋值给该分组dom。这样解决了滚动条高度的问题,并且不需要计算具体哪些列表项数据需要被加载出来,只需要知道加载哪(些)个分组即可。 分组的想法既简化了计算又保证了数组项和滚动条的位置是正确对应的。 高度的获取和赋值是在wxs里面做的,由于wxs是在渲染层执行的,相比在逻辑层减少了通信的成本。 下面给出简易(伪)代码来描述这段过程 视图层 [代码]<scroll-view clearingids="{{clearingGroupIds}}" renderingids="{{renderedGroupIds}}" change:clearingids="{{module.clearingHandle}}" change:renderingids="{{module.renderingHandle}}" class="list-wrapper x_reverse"> <!-- 分组dom --> <view class="piece-container" wx:for="groups" wx:for-item="group" id="piece-container-{{group.id}}"> <view class="x_reverse" wx:for="group.data"> {{item.content}} </view> </view> </scroll-view> [代码] 逻辑层 [代码]// 分组数据结构(二维数组) groups:[ { id: 1, // 分组id data:[{ content:{a:'a'} },...] } ,...], // 当前需要渲染的分组 renderingids:[], // 需要移除的分组 clearingGroupIds:[] [代码] wxs 更新分组dom高度,用法参考官方文档WXS响应事件 [代码]module.exports = { clearingHandle:function(clearingGroupIds, oldV, ownerInstance){ clearingGroupIds.forEach(function(groupId){ // 1. 根据 groupId 找到对应的分组dom // 2. 获取分组dom高度 // 3. 设置分组dom样式:height }) }, renderingHandle: function (renderingGroupIds, oldGroup, ownerInstance) { renderingGroupIds.forEach(function(groupId){ // 1. 根据 groupId 找到对应的分组dom // 2. 移除height样式 }) } } [代码] 3.2 如何知道渲染哪些数据 当有新的数据需要渲染到列表中时,首先是对数据进行分组,然后通过小程序提供的IntersectionObserver能力对分组dom进行监听,在其回调中判断该dom是否进入scroll-vew从而来更新正在渲染的分组和需要移除的分组。 [代码] // 滚动容器domId const containerId = '#scroll-container-xxx' // 创建监听 _createObserver(groupIds = []) { groupIds.forEach(groupId => { const observer = wx.createIntersectionObserver(this).relativeTo(containerId); observer.observe(domId, this._observerCallback); }) } // 监听回调 _observerCallback(result) { // 1. 根据result拿到domId然后解析拿到groupId(domId包含了groupId信息 // 2. 判断当前分组是否在视口内,如果不在视口内直接返回 // 3. 如果分组在视口内,则计算需要渲染的分组ids和需要移除的分组ids // 4. 通信至视图层,渲染目标分组数据和移除失效的分组数据( // 4.1 移除的优先级不高,不应该阻塞渲染目标分组,因此可以通过debounce/throttle处理) // 4.2 短时间内多次setData会导致通信通道阻塞,比如可以将setData放在队列中处理,一个一个来(中间可能有些失效则可以跳过 } [代码] 总结:基于2.1和2.2已经可以完成基本的雏形,另外有些其他的点需要优化 4 优化 4.1 unshift带来的问题 在小程序中通常将列表数据存储到数组中,由于小程序setData的数据量越小越好,更新数组时通常不会将整个数组对象进行setData,而只是更新数组对象的某个属性,如下: [代码]// 在数组尾部插入数据时 小程序支持下面方式 this.setData({ [array[array.length]]: newObj }) // 更新数组中某项的属性时 this.setData({ [array[0].a]: 'a' }) [代码] 如果要向数组顶部插入数据,做不到只传递新增的数据 [代码]array.unshift({}) this.setData({array}) // => 缺点是 逻辑层到渲染层会传递整个数组对象 [代码] 本文的背景是要解决会话场景下的长列表问题,对于会话即存在插入历史消息的场景,又存在插入新消息的场景,相当于我们数组两端都需要有插入数据的能力。需要对数据进行push/unshift操作。但是前面提到unshift效果不好。因此本文通过两个数组,一个数组存放历史消息,一个数组存放新消息,并在dom结构上也增加了对应的结构。 dom结构如下 [代码]<scroll-view class="x_reverse"> <view class="next-item-wrapper"> <!--多了一层--> <view class="x_reverse"> <!--新消息区域--> <view wx:for=“new-groups” wx:for-item="group"> <view wx:for="group.data"> {{item.content}} </view> </view> </view> </view> <view class="history-item-wrapper"> <!--历史消息区域--> <view wx:for=“his-groups” wx:for-item="group"> <view class="x_reverse" wx:for="group.data"> {{item.content}} </view> </view> </view> </scroll-view> [代码] 区域定义: 历史消息区域:初始化的消息以及插入的历史消息 新消息区域:列表初始化完成之后新来的消息 制定了如下规则 分组id越大表示分组的消息越久远,分组id越小表示分组的消息越新 历史分组id从1开始递增,新消息区域分组id从0开始递减 新消息区域自身未做任何的翻转,就像正常的列表一样,有新的消息或者新的分组push就行 历史消息区域的分组受到翻转的影响,在历史消息分组中push新的消息或者新的分组表现为插入历史消息 其原理如下图 [图片] 与上面dom结构对应的数据结构如下 [代码]class fuse { constructor() { // 存储历史消息 this.histGroups = []; // groupId >= 1 // 存储新消息 this.newGroups = []; // groupId <= 0 } // 插入新消息 push(listGroups){ this.newGroups.push(...listGroups) } // 插入历史消息 unshift(listGroups){ this.histGroups.push(...listGroups) } } [代码] 4.2 白屏问题 4.2.1 白屏现象的解释 滚动过程中长列表组件会进行setData操作以更新视口区域的数据,在快速滚动的情况下,假设此时逻辑层的计算结果是需要渲染第3屏幕的数据,但是由于从逻辑层通信到视图层是需要时间,这段时间中第三屏的界面可能已经滚动到视口外,此时的渲染是无效的,用户看到的可能已经是第8屏的数据,但是这个时间点第8屏幕的数据并没有渲染,这就会导致白屏现象的出现。 如果我们能根据屏幕滚动的速率和通信的时间去预测下一帧哪一屏出现在视口区域,那么就可以避免白屏问题。显然这是个难题,因为你不知道用户什么时候会调整滚动的速度,并且setData的时间也受限于很多因素。因此小程序架构下长列表组件带来的白屏问题是无解的。但可以通过预加载上下几屏的数据等一些其他优化方案降低白屏出现的几率以及给出一些骨架效果来缓解用户的焦虑。 4.2.2 骨架效果模拟 由于WXML节点过多也会影响长列表性能,因此否定了渲染真实dom来实现骨架,目前是通过图片作为背景通过在垂直方向平铺的方式来模拟骨架效果。 这种方式对于列表项是等高的场景是完美的解决方案,对于列表项非等高的场景可能会看到背景有被’截断‘情况。不过实际体验来看在快速滚动的情况下,这种’截断‘被看到的概率是偏低的,从实际效果来看是可以接受的。 等高列表项(患者列表 ):https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john//video-1.mp4 非等高列表项(会话 ):https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john//video-2.mp4 4.3 图片高度异步确定带来的麻烦 加载图片资源需要经过网络,属于异步加载,因此img标签的高度的确定也是异步的。假设一种场景,当前分组中的图片资源尚未加载完成,由于滚动的发生需要将该分组中的列表项移除,显然这个时候给分组dom设置的高度是不准确的,当下一次重新渲染该分组时,图片重新加载到完成后,该分组的高度会发生生变化,此时会发生界面的跳动,该如何处理呢? 通过添加滚动锚定特性处理。滚动锚定是指当前视区上面的内容突然出现的时候,浏览器自动改变滚动高度,让视区窗口区域内容固定,就像滚动效果被锚定一样。因此通过设置滚动锚定特性可以解决界面跳动的问题 也可以通过动画的过渡效果来缓解跳动现象,这依赖于height相关的样式属性,因此需要给分组dom设置相关的样式值。 可以显示的给分组dom设置height样式:比如可以在图片加载完成后通知长列表组件去更新分组dom的高度,当高度设置了css3过渡动画,就会以动画形式展开。 也可以通过给分组dom设置min-height/max-height代替height,并给min-height/max-height设置css3动画。上面使用height方式存在一个问题,分组的高度只有在增高的前提下才会被感知,没有降低的可能性;而通过min-height/max-height组合(min-height:0,max-height:height + 1000px),分组高度的增加和降低都会被感知到 本文的实现是:滚动锚定 + min-height/max-height 下面是更新min-height/max-height的核心代码,通过监听 renderingids & clearingids属性的变化,在change回到中处理相关逻辑。 [代码]<scroll-view clearingids="{{clearingGroupIds}}" renderingids="{{renderedGroupIds}}" change:clearingids="{{chat.clearingHandle}}" change:renderingids="{{chat.renderingHandle}}" /> [代码] wxs [代码]// 分组消失时 设置mix-height/max-height = 实际高度 clearingHandle: function (clearingGroupIds, oldV, ownerInstance) { clearingGroupIds.forEach(function (groupId) { // 获取分组dom var pieceContainer = ownerInstance.selectComponent('#piece-container-' + groupId) var res = pieceContainer.getComputedStyle(['height']) pieceContainer.setStyle({ 'min-height': res.height, 'max-height': res.height }) }) // 分组重新渲染时 // min-height设置为0,实际的高度由分组中的列表项撑开 renderingHandle: function(renderingGroupIds, oldV, ownerInstance) { renderingGroupIds.forEach(function (groupId) { // 获取分组dom var pieceContainer = ownerInstance.selectComponent('#piece-container-' + groupId) var res = pieceContainer.getComputedStyle(['height']) // 高度大于一瓶 足够视口区域的内容发挥了 var maxHeight = parseInt(res.height) + 1000 + 'px' pieceContainer.setStyle({ 'min-height': '0' }) pieceContainer.setStyle({ 'max-height': maxHeight }) }) } [代码] 事实上最完美的方式是在上传图片的时候记录图片的宽高比例等信息,在渲染时计算好img标签高度,而不是依赖图片的加载结果,这样可以保证img标签高度是同步确定的。退一步的做法是可以在图片第一次加载完成后缓存宽高,再次渲染的时候显示的设置img标签宽高。 5 其他 5.1 由于翻转带来的其他副作用 ios下transform:rotate会导致z-index无效 Safari 3D transform变换z-index层级渲染异常的研究–张鑫旭。在Safari浏览器下,此Safari浏览器包括iOS的Safari,iPhone上的微信浏览器,以及Mac OS X系统的Safari浏览器,当我们使用3D transform变换的时候,如果祖先元素没有overflow:hidden/scroll/auto等限制,则会直接忽略自身和其他元素的z-index层叠顺序设置,而直接使用真实世界的3D视角进行渲染。 scroll-into-view无效问题 该问题在另一篇文章中说到过并且给出了解决方案。 小程序scroll-view翻转后 scroll-into-view的替代方案 5.2 根据groupNums计算待渲染/移除的分组id 本文实现的长列表组件提供了groupNums属性,该属性用来指定每个分组包含多个列表项。上文说到我们在IntersectionObserver监听的回调中来计算需要渲染的下一屏分组id。 如果长列表组件不存在删除元素的操作,那么假设当前进入视口的分组id是x,并且总是额外显示上一屏和下一屏的分组。那么当x是边缘分组时,目标分组就是[x,x+1] 或 [x-1,x];当x不是边缘分组的情况,目标分组是[x-1, x, x+1] 由于本文实现的长列表组件提供了删除中间列表项的方法,假设x,x-1,x+1这三个分组都被删除只剩下1一个列表项,那么按照上述计算方式计算返回的分组渲染出来后实际上可能还不够一屏。这个时候我们需要利用groupNums这个指标进行计算,比如当分组在中间时,得确保有3 * groupNums个列表项被渲染出来。 5.3 scroll-view底部回弹区域setData时跳动问题 问题:滑动页面到底部,使其出现橡皮筋效果,处于橡皮筋效果时SetData数据,会使页面跳动一下,处于橡皮筋效果时SetData会使页面跳动闪屏 解决方案:关闭橡皮筋效果即可 示例代码: [代码]<scroll-view enhanced="{{true}}" bounces="{{false}}" /> [代码] 5.4 一条消息的布局 问题:当滚动区域只有少数列表项,这些列表项高度之和小于滚动容器高度时,由于对滚动容器应用了翻转样式,此时列表项会布局在底部(应该在顶部) 解决方案:通过包裹在一个div内,应用如下样式解决 示例代码: [代码]<scroll-view class="x_reverse"> <view class="all-container"> <view class="next-item-wrapper">...</view> <view class="history-item-wrapper">...</view> </view> </scroll-view> [代码] [代码].all-container { display: flex; flex-direction: column; justify-content: flex-end; height: auto; min-height: 100%; } [代码] 5.5 自动弹出加载更多组件 问题:以加载历史消息为例,当消息滚动到顶部下拉开始加载历史消息时,如果只是设置showLoadMore为true,视觉上会看不到loadmore组件(原因是scroll-view设置了滚动锚定),需要再次向下拉一次,才能把该组件拉入到视区内。显然这样的体验不够好,如果拉到顶部开始加载历史消息时,该组件自动出现在用户的视觉内效果会好些。 示例代码(old): [代码]<scroll-view class="x_reverse"> <view class="all-container"> <view class="next-item-wrapper">...</view> <view class="history-item-wrapper">...</view> </view> <view class="x_reverse"> <load-more wx-if={{showLoadMore}}/> </view> </scroll-view> [代码] 解决方案:通过两个变量loadingDone&loading来维护该组件,loading为true时显示上面的组件,loadingDone为true时显示内部的组件 示例代码(new): [代码]<block> <!--正在加载,显示这里--> <load-more wx-if={{loading}}/> <scroll-view class="x_reverse"> <view class="all-container"> <view class="next-item-wrapper">...</view> <view class="history-item-wrapper">...</view> </view> <view class="x_reverse"> <!--没有更多数据了,显示这里--> <load-more wx-if={{loadingDone}}/> </view> </scroll-view> </block> [代码] 5.6 计算reccordIndex 在不删除中间列表项的情况下,传递的recordIndex是准确的,通过数学关系在wxs中实时进行计算 [代码]<list-item recordIndex="{{chat.calculateIndex(group, groupNums, index, renderedHistorySum)}}" /> [代码] wxs [代码]// index 当前列表项在当前分组的索引 // groupNums 单个分组列表项数 // renderedHistoryGroups是历史区域的列表项数 // group 用于获取groupId calculateIndex: function (group, groupNums, index, renderedHistorySum) { if (group.id > 0) { // 历史区域 return renderedHistorySum - ((group.id - 1) * groupNums + index) - 1 } return renderedHistorySum + (-group.id) * groupNums + index } [代码] [代码]observers: { 'renderedHistoryGroups.**'() { let renderedHistorySum = 0; const { renderedHistoryGroups, groupNums } = this.data; if (renderedHistoryGroups.length) { const { data: endGroupData } = getEndElement(renderedHistoryGroups); renderedHistorySum = (renderedHistoryGroups.length - 1) * groupNums + endGroupData.length; } this._setDataWrapper({ renderedHistorySum }); }, }, [代码] 5.7 抽象节点 列表项组件是通过抽象节点注入给长列表组件的 6 总结 下面是基于文中所述实现的目录,所有逻辑层代码放在behavior中以共享,normal-scroll针对普通场景的长列表,而chat-scroll针对会话场景的长列表。 [图片]
2022-02-17 - 微信小程序数字累加动画
推荐一下别人写的一个动画 NumberAnimate.js //Created by wangyy on 2016/12/26. 'use strict'; class NumberAnimate { constructor(opt) { let def = { from:50,//开始时的数字 speed:2000,// 总时间 refreshTime:100,// 刷新一次的时间 decimals:2,// 小数点后的位数,小数做四舍五入 onUpdate:function(){}, // 更新时回调函数 onComplete:function(){} // 完成时回调函数 } this.tempValue = 0;//累加变量值 this.opt = Object.assign(def,opt);//assign传入配置参数 this.loopCount = 0;//循环次数计数 this.loops = Math.ceil(this.opt.speed/this.opt.refreshTime);//数字累加次数 this.increment = (this.opt.from/this.loops);//每次累加的值 this.interval = null;//计时器对象 this.init(); } init(){ this.interval = setInterval(()=>{this.updateTimer()},this.opt.refreshTime); } updateTimer(){ this.loopCount++; this.tempValue = this.formatFloat(this.tempValue,this.increment).toFixed(this.opt.decimals); if(this.loopCount >= this.loops){ clearInterval(this.interval); this.tempValue = this.opt.from; this.opt.onComplete(); } this.opt.onUpdate(); } //解决0.1+0.2不等于0.3的小数累加精度问题 formatFloat(num1, num2) { let baseNum, baseNum1, baseNum2; try { baseNum1 = num1.toString().split(".")[1].length; } catch (e) { baseNum1 = 0; } try { baseNum2 = num2.toString().split(".")[1].length; } catch (e) { baseNum2 = 0; } baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); return (num1 * baseNum + num2 * baseNum) / baseNum; }; } export default NumberAnimate; 使用: import NumberAnimate from "../utils/NumberAnimate";//引入NumberAnimate.js 请根据自己的实际路径来 let n1 = new NumberAnimate({ from:100,//开始时的数字 speed:1000,//总时间 refreshTime:100,//新一次的时间 decimals:0,//小数点后的位数 onUpdate:()=>{//更新回调函数 }, onComplete:()=>{//完成回调函数 } });
2022-07-21 - 微信小程序压缩图片,也可以转换图片格式
wxml: <canvas class="canvas" style="height:{{windowHeight}}px;width:{{windowWidth}}px;" canvas-id='attendCanvasId'></canvas> wxss: .canvas{ position: fixed; top:-9999999999999999999px;left:0; z-index: -1; background: #fff; } js: let that = this; var windowWidth = 750; //图片压缩的宽度 var quality=0.9;//图片的质量,目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。 wx.getImageInfo({ src: tempFilePaths, success(imgres) { var imgwidth = imgres.width;//图片实际宽度 var imgheight = imgres.height;//图片实际高度 if (imgwidth > windowWidth) {//判断图片实际宽度是否大于要压缩的宽度,这个判断也可以不要,根据实际需求来 that.setData({ windowWidth: windowWidth,//图片压缩宽度 windowHeight: (windowWidth * imgheight) / imgwidth//计算图片压缩之后的高度,与图片原比例一致 }) // 放到对应的wxml页面 const ctx = wx.createCanvasContext('attendCanvasId');//canvas id ctx.drawImage(tempFilePaths, 0, 0, windowWidth, (windowWidth * imgheight) / imgwidth); ctx.draw(false, function () { wx.canvasToTempFilePath({ canvasId: 'attendCanvasId', fileType: imgres.type == 'png' ? 'png' : 'jpg', //目标文件的类型,这里可以根据实际情况来, quality: quality, //图片的质量,目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。 success(s){ console.log("压缩之后的图片", s); } }); }); } } })
2022-07-20 - 分享朋友圈是参数这样填写为什么不对?
=============================== 这个是上一个页面进入参数 wx.navigateTo({ url: '/pages/product/goods/goods?serialNumber=' + serialNumber + '&status=2&isMaterial=1', }) ========================== 这个是当面页面的分享朋友圈功能 onShareTimeline: function () { const { serialNumber } = this.data.options; console.log("分享朋友圈"); const { imgList } = this.data; return { title: this.data.remark, query: 'serialNumber=' + serialNumber + '&status=2&isMaterial=1', imageUrl: imgList[0] } }, 这样发布到朋友圈以后,其他人打开这个链接就是个空壳,所有的数据都没有了
2022-04-23 - 单页模式禁止分享的方法
直接上代码 [代码]let page ={ ... onShareTimeline:function(){ 正常情况的分享操作 } ... }; //判定当前是否 单页模式, 单页模式移除 onShareTimeline 即可 let option = wx.getLaunchOptionsSync(); if(option.scene==1154) { page.onShareTimeline = null } Page( page ); [代码]
2022-04-27 - 小程序自定义头部导航栏
示例图: [图片][图片] wxml: <!-- 顶部自定义导航样式 --> <!-- 样式1、2 黑色胶囊 白色胶囊--> <view class='nav' style="height:{{navH}}px;{{background? background:''}}" wx:if="{{styles==1 || styles==2}}"> <!-- 页面标题 --> <view class='nav-title' style="height:{{navTitle}}px;"> <view class="nav-back2" style="width:{{menuW}}px;height:{{menuH}}px;" wx:if="{{!isTab}}"> <text class="icons icons-zuojiantou" style="color:{{styles==1? '#000':'#fff'}};" data-type="back" catchtap="goback"></text> <text class="nav-shu" style="background:{{styles==1? '#2F2F2F':'#fff'}};"></text> <view data-type="index" catchtap="goback"><image mode="widthFix" src="img/{{styles==1? 'index1':'index2'}}.png"></image></view> </view> <text style="color:{{styles==1? '#000':'#fff'}};width:{{titleWidth}}px;" class="line1">{{title}}</text> </view> </view> <!-- 样式3、4 只有返回按钮或回到主页 黑色 白色--> <view class='nav' style="height:{{navH}}px;{{background? background:''}}" wx:if="{{styles==3 || styles==4}}"> <view class='nav-title' style="height:{{navTitle}}px;"> <view class="nav-back2" style="border:0;width:{{menuW}}px;height:{{menuH}}px;" wx:if="{{!isTab}}"> <text class="icons icons-zuojiantou icons2" style="color:{{styles==3? '#333':'#fff'}};" data-type="back" catchtap="goback" wx:if="{{getCurrentPages.length>1}}"></text> <image wx:else style="width:56rpx;height:56rpx;" mode="widthFix" src="img/{{styles==3? 'indexs':'index'}}.png" data-type="index" catchtap="goback"></image> </view> <text style="color:{{styles==3? '#333':'#fff'}};width:{{titleWidth}}px;" class="line1">{{title}}</text> </view> </view> <view style="height:{{navH}}px;" wx:if="{{bot}}"></view> wxss: @font-face { font-family: "icons"; /* Project id 3500499 */ src:url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAKcAAsAAAAABlQAAAJRAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACCcApMXwE2AiQDCAsGAAQgBYRnBzQbnwXInpo8beAx5Q1RfAAXGAghgvr9yp733wGAQtSA+jjALuqESlgCGX2rQ8KnIlR2XQCF/YM390zOBZltBK9jFyyjEXU9SxNqazrWPo+KUF2yljHV9H9t/rvDhZRh/BIK47GGBkQYO+3zfz/5F9AHviD0a5UU1ZT4gDYfyD7osi2KcGc+9Bksr/xq9k2+RKDZCgftz7eHyuhcSVEZI5zeIK+O89ioSkeHZJlmG4V6dGUWb9WRnqXPeBN/Pv5Yi0jqCk7d0d1ZWPl9X3HHFEoUXz1droWel1CHAguATJz1pw7URRuPNbsaxmhjFnxfKcvQKQ5tE+qvcypbwSgVnkn0pUfNJnisgboBg5MeQHTn38rTfm0+Ty+e94bPJ334+vbYuen/WrmoVGeTvOr1pTZTC9ZWbaugfFwfrBMz/uLdX97pWP/Xz6D264oXB8rHDZiHxn24gh9iuAkSswmTcpuou5Puc2ib3Sglx9ftnWruOXEuNOqZ4GkwkqPQaIzK3BzqtFhDvUYbaDbPr2/RY1AXuQlTVoLQqSJp9wmFThuVuQd1+n2o1+lHs8vRtluLqdiEUQmGEB/QB1RozySx2h2aMlPcFD8hX2IaBWs51ewCC+Q55sxX4y2ioWHKIcjzMMsIBqYEA7E6kbOvbd32FitRXplgSBEoBKIH0AuQgg7JUv6+HWSUMgqD1CnFLmKqHliKAaiFvoA6HuSa2ZXhWQgNNBjJgaCHoUyGgKF9WAIFhKWbkDjzaZfUoHpre2P+a/ugmWNJ4S4qNN8rKYlVIVRWKgA=') format('woff2'),url('iconfont.woff?t=1656989362799') format('woff'),url('iconfont.ttf?t=1656989362799') format('truetype'); } .icons { font-family: "icons" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icons-zuojiantou:before { content: "\e630"; } .flex1{ flex:1; } /* 字体显示一行 */ .line1{ text-overflow: ellipsis; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; } /* 自定义头部 */ .nav{ width: 100%; overflow: hidden; position: fixed; top:0;left:0; z-index: 50; background-repeat: no-repeat; background-size:100% auto; transition: all 0.3s; background-position:top left; } .nav-title{ width: 100%; position: absolute; bottom: 0; left: 0; z-index: 10; color:#fff; font-size: 32rpx; font-weight: 500; padding-left:0; display: flex; align-items: center; justify-content: center; text-align: center; } .nav .back{ width: 22px; height: 22px; position: absolute; bottom: 0; left: 0; padding: 10px 15px; } .nav-back{ width:76rpx; position: absolute; bottom: 0; left: 0; text-align: left; padding-left: 20rpx; z-index: 11; display: flex; align-items: center; } .nav-back .iconfont{ font-size: 32rpx; color: #fff; } .nav-back image{ width:56rpx; height:56rpx; border-radius: 50%; display: block; } .nav-back2{ border-radius: 32rpx; position: absolute; left:14rpx; top:0;bottom:0; margin: auto; z-index:10; display: flex; flex-wrap: nowrap; align-items: center; border:1px solid #EDEDED; } .nav-back2>.icons{ font-size:30rpx; flex:1; display: flex; align-items: center; justify-content: center; height:100%; line-height: normal; } .nav-shu{ width: 1px; height: 60%; background: #2F2F2F; display: block; } .nav-back2>view{ flex:1; height:100%; display: flex; align-items: center; justify-content: center; } .nav-back2>view>image{ width:30rpx; height:32rpx; display: block; vertical-align: unset; margin:0; } .nav-title2>view>image{ width:56rpx; height:56rpx; } .itemcenter{ display: flex; flex-wrap: nowrap; align-items: center; justify-content: center; } .nav-back2 .icons2{ justify-content: left;flex:unset;width:56rpx;height:56rpx; } js: const systemInfo = wx.getSystemInfoSync(); // 获取系统信息 const menuButtonInfo = wx.getMenuButtonBoundingClientRect();// 胶囊按钮位置信息 const getTopInfo={ statusBarHeight:systemInfo.statusBarHeight,//状态栏高度 navBarHeight: systemInfo.statusBarHeight + menuButtonInfo.height + (menuButtonInfo.top - systemInfo.statusBarHeight) * 2, // 导航栏高度 menuWidth: menuButtonInfo.width, // 胶囊宽度 menuHeight: menuButtonInfo.height, // 胶囊高度 menuRight: systemInfo.screenWidth - menuButtonInfo.right, // 胶囊距右方间距(方保持左、右间距一致) menuTop: menuButtonInfo.top, //胶囊距离顶部的距离 }; Component({ properties: { // 是否是tab页 false不是 true是 isTab:{ type:Boolean, value:false, }, // 导航栏样式 1:黑色胶囊 2:白色胶囊 3黑色 4白色 styles:{ type:String, value:"1" }, // 占位块是否显示 默认显示 bot:{ type:Boolean, value:true, }, // 页面标题 title:{ type:String, value:"页面标题" }, // 导航栏背景色,支持所有css样式,也可以是背景图,渐变色 background:{ type:String, value:"background:#fff;" } }, data: { navH: getTopInfo.navBarHeight, //导航栏高度 navTitle: getTopInfo.navBarHeight - getTopInfo.statusBarHeight, //导航栏标题高度 titleWidth: systemInfo.screenWidth - (getTopInfo.menuWidth + getTopInfo.menuRight * 2) * 2, //导航栏标题宽度 menuW: getTopInfo.menuWidth,//胶囊宽度 menuH: getTopInfo.menuHeight,//胶囊高度 menuRight:getTopInfo.menuRight, getCurrentPages:getCurrentPages(),//判断是否有上级页面 }, methods: { // 返回上一页 goback(e) { // 返回上一级 if (e.currentTarget.dataset.type == 'back') { wx.navigateBack({ delta: 1, }) } else { // 回首页 wx.switchTab({ url: '/pages/index/index', }) } }, } }) json: { "component": true, "usingComponents": {} }
2022-07-18 - wx:for循环遍历数据过多小程序直接卡白屏
[图片] 该模块只能循环遍历98次,当数据多的时候直接卡白屏啥也不显示了,有没有大佬知道怎么解决
2022-07-17 - 小程序分享朋友圈的那个单页模式的页面是否可以配置禁止再分享呢?
目前单页模式有很多限制比如不能调用login,导致我们分享逻辑有点问题。 请问是否可以考虑下 分享朋友圈点击进入的那个 单页模式 可以配置禁止分享。
2020-07-21 - 有关查询某一天的详情情况的相关思路以及实现方法
首先,要清楚某一天的日期如何获取:当today的数据为null时,则表示是今天,其实这是重点,这就可以推算出你想要查询哪一天的数据。 onLoad: function(options) { var that = this; //获取明天 that.getDateStr(null,1) }, getDateStr: function(today, addDayCount) { var date; if(today) { date = new Date(today); }else{ date = new Date(); } date.setDate(date.getDate() + addDayCount);//获取AddDayCount天后的日期 var y = date.getFullYear(); var m = date.getMonth() + 1;//获取当前月份的日期 var d = date.getDate(); if(m < 10){ m = '0' + m; }; if(d < 10) { d = '0' + d; }; console.log( y + "-" + m + "-" + d) return y + "-" + m + "-" + d; }, 第二:你要想查询,在新建数据的时候,必须要保存当时完成数据的时间点,然后用 date=new Date(today).getdate(),来实现某一天的查询 其中today表示当时存的时间点,自己摸索实现,留个记录,也希望能够帮到各位网友吧,有不同的见解的朋友希望留言讨论,谢谢
2022-07-14 - 一张表解决云存储的七大痛点
就是这张表: 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 - 通过WXS实现回弹的平滑滚动容器
前言 最近在愉快的开发微信小程序的时候碰到了一个体验需求,需要在 Android 侧的滚动也需要带回弹效果,类似于在 Web 端可以使用的 better-scroll,查阅微信小程序内置组件 [代码]scroll-view[代码] 无法满足这种场景,没办法,需求得做呀,只能自己动手撸了! 在微信小程序中,我们可以通过 WXS响应事件 来替代逻辑层处理从而有效的提高交互流畅度,其中使用到的 WXS语法 也是非常类似我们非常熟悉 JavaScript,不过很多的 JavaScript 高级语法在 WXS 模块中不能使用,具体可以点击链接进入微信小程序提供的文档。 思路 以横向滚动为例,内容的宽度大于容器的宽度时可以发生滚动,如图 [图片] 接着通过监听三个触摸事件[代码]touchstart[代码]、[代码]touchmove[代码]、[代码]touchend[代码]来实时的改变 content 的 CSS translate,从而从视觉上达到滚动的目的。 WXS 示例 我们先从一个简单的 WXS 使用示例来了解回顾一下使用方式,WXS 的模块系统类似 CommomJS 规范,使用每个模块内置的 [代码]module[代码] 对象中的 [代码]exports[代码] 属性进行变量、函数导出: [代码]// helper.wxs module.exports = { // 注意 WXS 模块中不支持函数简写 touchstart: function touchstart() { console.log('touchstart called') } } [代码] [代码]<!-- index.wmxl --> <!-- module 为模块名,可按规范任意取名 --> <wxs src="./helper.wxs" module="helper" /> <!-- 与普通的逻辑层事件不同,这里需要加上 {{}} --> <view bind:touchstart="{{ helper.touchstart }}">view</view> [代码] 这样就给 [代码]view[代码] 绑定了一个 [代码]touchstart[代码] 事件,在事件触发后,会在控制台打印出字符串 "touchstart called" 好了,现在正式进入滚动容器的逻辑实现 开工 新建 [代码]scroll.wxml[代码] 文件,准备符合上图中结构的 WXML 内容来构造出一个正确的可以滚动条件 [代码]<!-- scroll.wxml --> <!-- 即图中的 container --> <view class="container" style="width: 100vw;"> <!-- 即图中的 content --> <view class="content" style="display: inline-block; white-space: nowrap;"> <view wx:for="{{ 10 }}" wx:key="index" style="width: 200rpx; height: 300rpx; border: 1px solid; display: inline-block;">{{ item }}</view> </view> </view> [代码] 新建 [代码]scroll.wxs[代码] 文件,里边用于存放我们实现滚动的所有逻辑 接下来进行初始化操作,首先需要获取到 container 和 content 组件实例,在上一节 “WXS 示例” 中我们知道可以通过在组件中触发一个事件来调用 WXS 模块中的方法,但有没有什么方式可以不用等到用户来触发事件就可以执行吗? 通过阅读 WXS 响应事件 文档,可以了解到,另外一种调用 WXS 模块方法就是可以通过 [代码]change:[prop][代码] 监听某一个组件的 Prop 的改变来执行 WXS 模块中指定的方法,且这个方法会立即执行一次,如下面一个示例 [代码]// helper.wxs module.exports = { setup: function setup() { console.log('setup') } } [代码] [代码]<!-- index.wxml --> <wxs src="./helper.wxs" module="helper"></wxs> <!-- 例如我们指定一个 prop 为 prop1,值为 {{ prop1Data }} --> <!-- 通过 change:prop1 语法对这个 prop 的变化进行监听 --> <view prop1="{{ prop1Data }}" change:prop1="{{ helper.setup }}"></view> [代码] [代码]// index.js Page({ data: { prop1Data: {} } }) [代码] 上面示例中,在页面初始化或 [代码]prop1Data[代码] 发生改变时(准确来说是在逻辑层对 [代码]prop1Data[代码] 调用了 [代码]setData[代码] 方法后,即使 [代码]prop1Data[代码] 的内容不变化),都会调用 [代码]hepler.wxs[代码] 模块中的 setup 方法。 现在我们可以通过 [代码]change:prop[代码] 会立即执行一次的特点,来对我们的滚动逻辑进行一次初始化操作 [代码]// scroll.wxs var exports = module.exports // 页面实例 var ownerInstance // container BoundingClientRect var containerRect // content 实例,通过此实例设置 CSS 属性 var slidingContainerInstance // content BoundingClientRect var slidingContainerRect // X方向的最小、最大滚动距离。如 -200 至 0(手势往右时,元素左移,translateX 为负值) var minTranslateX var maxTranslateX = 0 /** * @param newValue 最新的属性值 * @param oldValue 旧的属性值 * @param ownerInstance 页面所在的实例 * @param instance 触发事件的组件实例 */ exports.setup = function setup(newValue, oldValue, _ownerInstance, instance) { ownerInstance = _ownerInstance containerRect = instance.getBoundingClientRect() slidingContainerInstance = ownerInstance.selectComponent('.content') slidingContainerRect = slidingContainerInstance.getBoundingClientRect() minTranslateX = (slidingContainerRect.width - containerRect.width) * -1 } [代码] [代码]<!-- scroll.wxml --> <wxs src="./scroll.wxs" module="scroll" /> <!-- 因本案例只利用 change:[prop] 首次执行的机制,传递的给 _ 的参数是个对象字面量 --> <view class="container" style="width: 100vw;" _="{{ { k: '' } }}" change:_="{{ scroll.setup }}" bind:touchstart="{{ scroll.touchstart }}" bind:touchmove="{{ scroll.touchmove }}" bind:touchend="{{ scroll.touchend }}" > <view class="content" style="display: inline-block; white-space: nowrap;"> <view wx:for="{{ 10 }}" wx:key="index" style="width: 200rpx; height: 300rpx; border: 1px solid; display: inline-block;">{{ item }}</view> </view> </view> [代码] 完成基本的跟随手指移动 [代码]// scroll.wxs var exports = module.exports // 页面实例 var ownerInstance // container BoundingClientRect var containerRect // content 实例,通过此实例设置 CSS 属性 var slidingContainerInstance // content BoundingClientRect var slidingContainerRect // X方向的最小、最大滚动距离。如 -200 至 0(手势往右时,元素左移,translateX 为负值) var minTranslateX var maxTranslateX = 0 /** * @param newValue 最新的属性值 * @param oldValue 旧的属性值 * @param ownerInstance 页面所在的实例 * @param instance 触发事件的组件实例 */ exports.setup = function setup(newValue, oldValue, _ownerInstance, instance) { ownerInstance = _ownerInstance containerRect = instance.getBoundingClientRect() slidingContainerInstance = ownerInstance.selectComponent('.content') slidingContainerRect = slidingContainerInstance.getBoundingClientRect() minTranslateX = (slidingContainerRect.width - containerRect.width) * -1 } // 实时记录 content 位置 var pos = { x: 0 } // 记录每次触摸事件开始时,content 的位置,后续的移动都是基于此值增加或减少 var startPos = { x: 0 } // 记录触摸开始时,手指的位置,后续需要通过比较此值来计算出移动量 var startTouch = { clientX: 0 } function setTranslate(pos0) { slidingContainerInstance.setStyle({ transform: 'translateX(' + pos0.x + 'px)' }) pos.x = pos0.x } exports.touchstart = function touchstart(event) { startTouch.clientX = event.changedTouches[0].clientX startPos.x = pos.x } exports.touchmove = function touchmove(event) { var deltaX = event.changedTouches[0].clientX - startTouch.clientX var x = startPos.x + deltaX setTranslate({ x: x }) } exports.touchend = function touchend() {} [代码] 效果图: [图片] 处理松手后移动超出的情况,需要对其归位: 添加 clamp 工具方法 [代码]// 给出最小、最大、当前值,返回一个在最下-最大范围之间的结果 // 如: -100, 0, -101 => -100 function clamp(min, max, val) { return Math.max(min, Math.min(max, val)) } [代码] 在 touchend 事件中,添加位置校验的逻辑 [代码]// scroll.wxs exports.touchend = function touchend() { setTranslate({ x: clamp(minTranslateX, maxTranslateX, pos.x) }) } [代码] 看看效果: [图片] 回去是能回去了,有点生硬~ 加上松手回弹动画 其中动画可以使用两种实现方式 CSS Transition:在松手后,给 content 元素设置一个 [代码]transition[代码],然后调整 [代码]translateX[代码] 值归位 JS 帧动画:在松手后,利用动画函数不断调整 [代码]translateX[代码] 来进行归位 两种方式通过给相同的动画函数可以达到一样的体验,但 CSS Transition 在我的理解中不太好处理中止的情况,如在动画过程中,又有了新的触摸事件,这里就会产生抖动或未预期到的结果,但 JS 动画可以很简单的应对 因此后续的动画部分打算采用 JS 动画实现,先准备一些动画函数 [代码]// scroll.wxs // 下面内容通过 better-scroll 借鉴 ~ // 可以理解为入参是一个 [0, 1] 的值,返回也是一个 [0, 1] 的值,用来表示进度 var timings = { v1: function (t) { return 1 + --t * t * t * t * t }, v2: function(t) { return t * (2 - t) }, v3: function(t) { return 1 - --t * t * t * t } } [代码] 定义 [代码]moveFromTo[代码] 方法来实现从一个点通过指定的动画函数运动到另一点 [代码]// scroll.wxs /** * @param fromX 起始点xx * @param toX 目标点 x * @param duration 持续时长 * @param timing 动画函数 */ function moveFromTo(fromX, toX, duration, timing) { if (duration === 0) { setTranslate({ x: fromX }) } else { var startTime = Date.now() var disX = toX - fromX var rAFHandler = function rAFHandler() { var progressX = timing(clamp(0, 1, (Date.now() - startTime) / duration)) setTranslate({ x: disX * progressX + fromX }) if (progressX < 1) { ownerInstance.requestAnimationFrame(rAFHandler) } } ownerInstance.requestAnimationFrame(rAFHandler) } } [代码] 调整 touchend 事件处理逻辑,添加归位的动画效果 [代码]// scroll.wxs exports.touchend = function touchend() { moveFromTo( pos.x, clamp(minTranslateX, maxTranslateX, pos.x), 800, timings.v1 ) } [代码] 看看效果: [图片] 看起来达到了目的,再优化一下,在滑动超出边界后,需要给一些阻力,不能滑的“太简单了” 给超边界的滚动加阻力 [代码]// scroll.wxs exports.touchmove = function touchmove(event) { var deltaX = event.changedTouches[0].clientX - startTouch.clientX var x = startPos.x + deltaX // 阻尼因子 var damping = 0.3 if (x > maxTranslateX) { // 手指右滑导致元素左侧超出,超出部分添加阻尼行为 x = maxTranslateX + damping * (x - maxTranslateX) } else if (x < minTranslateX) { // 手指左滑导致元素右侧超出,超出部分添加阻尼行为 x = minTranslateX + damping * (x - minTranslateX) } setTranslate({ x: x }) } [代码] 瞅瞅: [图片] 效果达到了,手指都划出屏幕了,才移动了这么一点距离 到现在已经完成了一个带回弹效果的滚动容器,但还没有做到“平滑”,即在滑动一段距离松手后,需要给 content 一些“惯性”来继续移动一些距离,体验起来就不会那么生硬 加滑动惯性 在这之前,还有一些准备工作需要做 [代码]// scroll.wxs // 记录触摸开始的时间戳 + var startTimeStamp = 0 // 增加动画完成回调 + function moveFromTo(fromX, toX, duration, timing, onComplete) { if (duration === 0) { setTranslate({ x: fromX }) + ownerInstance.requestAnimationFrame(function() { + onComplete && onComplete() + }) } else { var startTime = Date.now() var disX = toX - fromX var rAFHandler = function rAFHandler() { var progressX = timing(clamp(0, 1, (Date.now() - startTime) / duration)) setTranslate({ x: disX * progressX + fromX }) if (progressX < 1) { ownerInstance.requestAnimationFrame(rAFHandler) + } else { + onComplete && onComplete() + } } ownerInstance.requestAnimationFrame(rAFHandler) } } exports.touchstart = function touchstart(event) { startTouch.clientX = event.changedTouches[0].clientX startPos.x = pos.x + startTimeStamp = event.timeStamp } [代码] 因为是在松手后加动量,所以继续处理 touchend [代码]// scroll.wxs exports.touchend = function touchend(event) { // 记录这一轮触摸动作持续的时间 var eventDuration = event.timeStamp - startTimeStamp var finalPos = { x: pos.x } var duration = 0 var timing = timings.v1 var deceleration = 0.0015 // 计算动量,以下计算方式“借鉴”于 better-scroll,有知道使用什么公式的朋友告知以下~ var calculateMomentum = function calculateMomentum(start, end) { var distance = Math.abs(start - end) var speed = distance / eventDuration var dir = end - start > 0 ? 1 : -1 var duration = Math.min(1800, (speed * 2) / deceleration) var delta = Math.pow(speed, 2) / deceleration * dir return { duration: duration, delta: delta } } // 此次滑动目的地还在边界中,可以进行动量动画 if (finalPos.x === clamp(minTranslateX, maxTranslateX, finalPos.x)) { var result = calculateMomentum(startPos.x, pos.x) duration = result.duration finalPos.x += result.delta // 加上动量后,超出了边界,加速运动到目的地,然后触发回弹效果 if (finalPos.x > maxTranslateX || finalPos.x < minTranslateX) { duration = 400 timing = timings.v2 var beyondDis = containerRect.width / 6 if (finalPos.x > maxTranslateX) { finalPos.x = maxTranslateX + beyondDis } else { finalPos.x = minTranslateX + beyondDis * -1 } } } moveFromTo(pos.x, finalPos.x, duration, timing, function () { // 若动量动画导致超出了边界,需要进行位置修正,也就是回弹动画 var correctedPos = { x: clamp(minTranslateX, maxTranslateX, pos.x) } if (correctedPos.x !== pos.x) { moveFromTo( pos.x, correctedPos.x, 800, timings.v1 ) } }) } [代码] 继续看看效果: [图片] 有了有了 只是现在的滚动容器还很“脆弱”,在进行动量动画、回弹动画时,如果手指继续开始一轮新的触摸,就会出现问题,也就是最开始我们在选择 CSS 过渡和 JS 动画考虑到的问题 解决连续触摸滑动问题 在 [代码]moveFromTo[代码] 方法中,添加强制中止的逻辑 [代码]// scroll.wxs + var effect = null function moveFromTo(fromX, toX, duration, timing, onComplete) { + var aborted = false if (duration === 0) { setTranslate({ x: fromX }) ownerInstance.requestAnimationFrame(function () { onComplete && onComplete() }) } else { var startTime = Date.now() var disX = toX - fromX var rAFHandler = function rAFHandler() { + if (aborted) return var progressX = timing(clamp(0, 1, (Date.now() - startTime) / duration)) setTranslate({ x: disX * progressX + fromX }) if (progressX < 1) { ownerInstance.requestAnimationFrame(rAFHandler) } else { onComplete && onComplete() } } ownerInstance.requestAnimationFrame(rAFHandler) } + if (effect) effect() + effect = function abort() { + if (!aborted) aborted = true + } } exports.touchstart = function touchstart(event) { startTouch.clientX = event.changedTouches[0].clientX startPos.x = pos.x startTimeStamp = event.timeStamp + if (effect) { + effect() + effect = null + } } [代码] 体验一下: [图片] 这样一个带回弹的平滑滚动容器就处理的可以使用啦,有问题的地方欢迎大家指出讨论 结尾 完整源码托管在 Github 中:weapp-scroll 其中功能、逻辑更为完善,并同时支持横向、竖向方向的滚动,适合在 Android、PC 场景的使用(毕竟 IOS 侧可以直接使用微信内置组件 [代码]scroll-view[代码]~)。若有帮到希望可以给个星星~ 完~
2023-07-07 - 【技巧】swiper仿tab切换
大家好,上次给大家分享了swiper多图片的解决方案:https://developers.weixin.qq.com/community/develop/doc/000068ff25ccf0bae4e76eab156c04 今天再给大家分享一个关于swiper的小技巧,利用swiper仿tab切换。 相信大家在app或浏览器上阅读新闻时,比如今日头条,会有这样一个场景,左右滑动的时候可以切换不同栏目,体验非常好,但是小程序好像没有提供相关组件,如果想实现这种效果该怎么做呢今天就给大家介绍一下在小程序里是怎么实现的。 首先先看下效果 [图片] 实现原理很简单,利用小程序swiper再配合scroll-view就能实现,不过这里面有几点需要注意一下: 1.scroll-view一定要给一个高度,不然会有问题; 2.切换的时候只显示当前的swiper-item里的内容,其它swiper-item里的内容可以先隐藏掉,这是因为如果你的swiper-item里的图片太多的话可能会造成页面回收,因为新闻列表大多是图文列表,而tab经常是不止两个的,可能是7、8个或更多,如果每个tab都显示的话到时上拉加载页面会非常庞大,所以这里我建议不用显示的内容先隐藏,记住是swiper-item里的内容不是swiper-item,到时切换回来时再重新渲染,如果你要保存滚动的位置还要做其它的一些处理,这里就不仔细讲解了; 3.这里适用的是整个页面都是tab切换的,如果只是在页面的某处实现tab切换,还要考虑高度的问题,加载数据的时候根据数据个数长度来计算高度,每次加载数据都要计算高度,切换到不同的tab也是,这部分比较麻烦,因为要计算,不过并不难,只要 计算正确的话是没有问题的; 大概就是这样,基本实现思路,大家可以根据这个思路去拓展,在上面加上自己的功能,over! 代码片段:https://developers.weixin.qq.com/s/89OO1smX736d 系甘先,得闲饮茶
2019-02-26 - 【好文】小程序动态换肤解决方案 - 本地篇
小程序动态换肤方案 – 本地篇 需求说明 在开发小程序的时候,尤其是开发第三方小程序,我们作为开发者,只需要开发一套模板,客户的小程序对我们进行授权管理,我们需要将这套模板应用到对方的小程序上,然后进行发版审核即可; 但是个别客户的小程序需要做 [代码]定制化配色方案[代码],也就是说,不同的小程序个体需要对页面的元素(比如:按钮,字体等)进行不同的配色设置,接下来我们来讨论一下怎么实现它。 方案和问题 一般来说,有两种解决方案可以解决小程序动态换肤的需求: 小程序内置几种主题样式,通过更换类名来实现动态改变小程序页面的元素色值; 后端接口返回色值字段,前端通过 [代码]内联[代码] 方式对页面元素进行色值设置。 当然了,每种方案都有一些问题,问题如下: 方案1较为死板,每次更改主题样式都需要发版小程序,如果主题样式变动不大,可以考虑这种; 方案2对于前端的改动很大,[代码]内联[代码] 也就是通过 [代码]style[代码] 的方式内嵌到[代码]wxml[代码] 代码中,代码的阅读性会变差,但是可以解决主题样式变动不用发版小程序的问题。 ps:我一直在尝试如何在小程序里面,通过js动态修改stylus的变量问题,这样就可以解决上面说的问题了,后期如果实现了,一定周知各位 本文先重点描述第一种方案的实现,文章末尾会贴上我的 [代码]github项目[代码] 地址,方便大家尝试。 前期准备 本文采用的是 [代码]gulp[代码] + [代码]stylus[代码] 引入预编译语言来处理样式文件,大家需要全局安装一下 [代码]gulp[代码],然后安装两个 [代码]gulp[代码] 的插件 [代码]gulp-stylus[代码](stylus文件转化为css文件) [代码]gulp-rename[代码](css文件重命名为wxss文件)。 gulp 这里简单贴一下gulpfile文件的配置,比较简单,其实就是借助 [代码]gulp-stylus[代码] 插件将 [代码].styl[代码] 结尾的文件转化为 [代码].css[代码] 文件,然后引入 [代码]gulp-rename[代码] 插件对文件重命名为 [代码].wxss[代码] 文件; 再创建一个任务对 [代码].styl[代码] 监听修改,配置文件如下所示: [代码]var gulp = require('gulp'); var stylus = require('gulp-stylus'); var rename = require('gulp-rename'); function stylusTask() { return gulp.src('./styl/*.styl') .pipe(stylus()) .pipe(rename(function(path) { path.extname = '.wxss' })) .pipe(gulp.dest('./wxss')) } function autosTask() { gulp.watch('./styl/*.styl', stylusTask) } exports.default = gulp.series(gulp.parallel(stylusTask, autosTask)) [代码] stylus 这里会分为两个文件,一个是主题样式变量定义文件,一个是页面皮肤样式文件,依次如下所示: 主题样式变量设置 [代码]// theme1 theme1-main = rgb(254, 71, 60) theme1-sub = rgb(255, 184, 0) // theme2 theme2-main = rgb(255, 158, 0) theme2-sub = rgb(69, 69, 69) // theme3 theme3-main = rgb(215, 183, 130) theme3-sub = rgb(207, 197, 174) [代码] 页面皮肤样式 [代码]@import './define.styl' // 拼接主色值 joinMainName(num) theme + num + -main // 拼接辅色值 joinSubName(num) theme + num + -sub // 遍历输出改变色值的元素类名 for num in (1..3) .theme{num} .font-vi color joinMainName(num) .main-btn background joinMainName(num) .sub-btn background joinSubName(num) [代码] 输出: [代码].theme1 .font-vi { color: #fe473c; } .theme1 .main-btn { background: #fe473c; } .theme1 .sub-btn { background: #ffb800; } .theme2 .font-vi { color: #ff9e00; } .theme2 .main-btn { background: #ff9e00; } .theme2 .sub-btn { background: #454545; } .theme3 .font-vi { color: #d7b782; } .theme3 .main-btn { background: #d7b782; } .theme3 .sub-btn { background: #cfc5ae; } [代码] 代码我写上了注释,我还是简单说明一下上面的代码:我首先定义一个主题文件 [代码]define.styl[代码] 用来存储色值变量,然后会再定义一个皮肤文件 [代码]vi.styl[代码] ,这里其实就是不同 [代码]主题类名[代码] 下需要改变色值的元素的属性定义,元素的色值需要用到 [代码]define.styl[代码] 预先定义好的变量,是不是很简单,哈哈哈。 具体使用 但是在具体页面中需要怎么使用呢,接下来我们来讲解一下 页面的 [代码]wxss[代码] 文件导入编译后的 [代码]vi.wxss[代码]文件 [代码]@import '/wxss/vi.wxss'; [代码] 页面的 [代码]wxml[代码] 文件需要编写需要改变色值的元素,并且引入变量 [代码]theme[代码] [代码]<view class="intro {{ theme }}"> <view class="font mb10">正常字体</view> <view class="font font-vi mb10">vi色字体</view> <view class="btn main-btn mb10">主色按钮</view> <view class="btn sub-btn">辅色按钮</view> </view> [代码] 页面 [代码]js[代码] 文件动态改变 [代码]theme[代码]变量值 [代码] data: { theme: '' }, handleChange(e) { const { theme } = e.target.dataset this.setData({ theme }) } [代码] 效果预览 [图片] 项目地址 项目地址:https://github.com/csonchen/wxSkin 这是本文案例的项目地址,为了方便大家浏览项目,我把编译后的wxss文件也一并上传了,大家打开就能预览,如果觉得好,希望大家都去点下star哈,谢谢大家。。。
2020-04-23 - 重写计算属性和watch数据监控器
前言 一直在用官方插件 miniprogram-computed(当前版本 4.3.8). 细读源码发现一些性能问题,这才有了重写的念头。在这个做个记录贴,欢迎讨论。 计算属性的源码分析 初始化时机 源码截图 [图片] 官方在组件 attached 周期会对配置中 computed 字段做初始化。 在首屏渲染时,有计算属性的组件都会运行一次 attached 周期,项目中不乏有大量复用的组件或计算属性较多的组件。这显然对首屏渲染速度不是很友好。 解决思路:在beforeCreated周期做初始化,每个组件的计算属性的初始化值只需计算一次,不必担心复用带来的性能问题。 计算属性更新器(computedUpdaters) 源码截取 [代码]if (computedDef) { observersItems.push({ fields: "**", observer(this: BehaviorExtend) { if (!this._computedWatchInfo) return; const computedWatchInfo = this._computedWatchInfo[computedWatchDefId]; if (!computedWatchInfo) return; let changed: boolean; do { changed = computedWatchInfo.computedUpdaters.some((func) => func.call(this) ); } while (changed); }, }); } [代码] 官方初始化计算属性时会在 observers 字段内中添加’**’ 字段,在其中循环调用 computedUpdaters 函数,每次会拿出所有缓存中所有计算属性的关联字段,循环对比缓存值和当前实例的新值。来判断是否需要重新初始化对应的计算字段,需要的话, setData对应计算属性新值(不会立即运行,会被收集), 把新的关联和值替换旧的缓存,进入下一次do while循环,直到没有关联的计算属性需要更新后,setData此次do while收集的所有更新的计算字段。这会再一次触发 observers’**’ 整体循环。 <br> 性能问题。 计算属性更新后,会再此触发 observers '**'进行一次无意义的 计算属性更新器运行(新旧值检测)。 observers监控很敏感 即使数据没有改变,也会触发计算属性更新器运行。 即使this.setData 无关计算属性的字段,也会触发计算属性更新器运行。 若计算属性依赖properties字段且字段类型为对象,那么由于小程序的组件由内而外挂载数据,后代组件会可能接受n多次的 null 或者 空对象{},这都会引起计算属性 计算属性更新器运行毫无意义。 <br> 解决思路:劫持 setData,获取到当前 setData 的配置对象,若有字段关联了 计算属性 则更新对应的计算属性(A),若有计算属性B依赖A,再更新B… , 所有计算属性更新验证完毕后,把劫持的的setData配置对象加入需要更新的计算属性字段 一起做一次setData。properties 字段在初始化计算属性时(beforeCreated周期中),为被计算属性关联的字段加入 observer 函数,针对对象可设置当传入null和空对象直接返回,否则比对新旧值,不同的话收集,全部 关联的properties 都收集完后 统一触发 计算属性更新。 <br> 关联的路径 源码 [代码]const wrapData = ( data: unknown, relatedPathValues: Array<IRelatedPathValue>, basePath: Array<string>, ) => { if (typeof data !== "object" || data === null) return data; const handler = { get(obj: unknown, key: string) { if (key === "__rawObject__") return obj; let keyWrapper = null; const keyPath = basePath.concat(key); const value = obj[key]; relatedPathValues.push({ path: keyPath, value, }); keyWrapper = wrapData(value, relatedPathValues, keyPath); return keyWrapper; }, }; try { return new Proxy(data, handler); } catch (e) { return new ProxyPolyfill(data, handler); } }; [代码] 示例 A [代码]Component({ data: { productInfo: { id: "001", selectedCount: 0, discount: 9, originalPrice: 10, }, }, computed: { realPrice(data) { return ( (data.productInfo.originalPrice * data.productInfo.discount) / 10 ); }, // ... }, // ... }); [代码] 示例 A 中,官方生成的 realPrice 缓存依赖相关路径为: [图片] 图中可以看出 0 和 2 重复,且这两项不是真正的关联依赖。会导致 50%的性能浪费(二段对象依赖如示例),如果是三段对象依赖会浪费 2/3 的性能…,而且会导致一些情况发生,比如更新的是 [代码]productInfo.selectedCount[代码] 有可能会匹配上这个计算属性导致这个缓存重做,而实际上是没有意义的,浪费更多性能。 解决办法: [代码]const handler = { get(obj: unknown, key: string) { if (key === "__rawObject__") return obj; let keyWrapper = null; const keyPath = basePath.concat(key); const value = obj[key]; // 去除关联的上一个路径 只要最后一个路径 if (basePath.length !== 0) { relatedPathValues.pop(); } relatedPathValues.push({ path: keyPath, value, }); keyWrapper = wrapData(value, relatedPathValues, keyPath); return keyWrapper; }, }; [代码] [图片] 很遗憾的时,使用proxy劫持get函数得到的关联路径是不准确的。因为无法对一些方法返回字段做proxy代理。如下 [代码]data:{ bool:false, list:[1,2,3,4,5] }, computed:{ listOther(){ const bool = this.data.bool const list = this.data.list.slice() // 得到的list 是无法被prxoy代理的 if(bool){ list.splice(2) ///无法获取依赖 return list }else{ return list[4] //无法获取依赖 } } } [代码] 即使传入克隆的this.data(为了减少一些方法的使用) 也无法保证获取到正确的关联字段。看了其他正则收集依赖等思路都有问题存在。如果您更好收集路径的办法,请留言告诉我。 所以官方和重写的计算属性当前都存在无法避免的性能浪费。特别是计算属性依赖数组时,很有可能做无意义的触发计算属性更新。很遗憾。 watch 监控器源码分析 初始化 官方watch 在 created 周期对配置中 watch 字段做了初始化, 如下图: [图片] 主要是生成第一次监控字段的值,缓存起来用于后续比对。 之后会在把每个字段加入到 observers 字段下 触发 当 observers 对应字段触发时,watch 劫持函数通过对比当前值和旧值(缓存中)是否相等(===)或者严格相等(深度比较)来决定是否触发 watch 对应的函数。触发情况下,会对缓存值更新。 需要注意的时避免在 watch 函数中使用 setData 触发可能引起自身 watch 字段变换的值。会循环触发,监控函数递归,内存泄露。 已知的不足。 对 properties 对象类型字段监控时,如果传入的是异步数据,那么在子组件 attach 阶段获取到的数据为"null",一样会触发 watch 的监控。 示例 C [代码]// 页面 wxml <product-item attach productInfo="{{productInfo}}" />; // 页面 js Component({ methods: { onLoad() { console.log("onLoad"); // 模拟异步获取数据 setTimeout(() => { console.log("异步数据获取成功"); this.setData({ productInfo: { id: "001", name: "可乐", selectedCount: 0, originalPrice: 10, discount: 5, }, }); }, 1000); }, }, }); // product-item js Component({ properties: { productInfo: Object, // type productInfo = {id:string;name:string;selectedCount:number;originalPrice:number;discount:number} }, computed: { realPrice(data) { return data.productInfo?.originalPrice || 0 * data.productInfo?.discount || 0 / 10; }, selectedCount(data) { return data.productInfo?.selectedCount || 0; }, }, lifetimes: { attach() { console.log(`attach时productInfo的值为${this.data.productInfo}`); }, }, watch: { productInfo() { // 在created初始化时缓存val为null,在attach时因为页面异步数据未到达,productInfo为undefined强转为null,监控触发。 }, "productInfo.selectedCount"() { // 避免这么写。 // 报错 TypeError: Cannot read property 'selectedCount' of null }, "realPrice,selectedCount"() { console.log("realPrice或selectedCount发生改变"); }, }, }); [代码] 或许你会说使用"**"啊,那么如果对象有默认值的情况呢?同样会触发导致 watch 下的"productInfo.selectedCount"字段报错。是由于强转带来的后果(最讨厌的黄字提醒)。从根本上来说是生命周期顺序引起的。回顾下组件加载顺序 [代码]beforeCreate --> created-->attach -->attached[代码] 其中 attach 周期时 即获取父组件properties的传值可以触发 observers 字段,且 setData 数据是有效的。 解决思路:watch 劫持函数监控到值为 null 时不触发 watch 函数 更好的办法时 不要对properties中对象字段的子字段做watch处理。 由于监控器生效在 created 之后(attach 周期就可以触发),而计算属性生效在 attached 周期。如果 watch 字段监控了计算属性,那么在 attached 周期后,watch 会得到计算属性的’无意义’的触发。有人提了 issue #58 官方也做了"修复"。 官方在 computed 初始化的时候,给计算属性关联字段做了个 mark("_triggerFromComputedAttached"=true)。 [图片] observers 在监控到 comupted 字段改变时,会判断是否为第一次触发(_triggerFromComputedAttached===true),是的话不许触发 watch 缓存更新和调用 watch 字段函数,把 mark 字段变为 false。 [图片] 但忽略一个问题 如上面 示例 C 中 watch 字段 "realPrice,selectedCount"不会 1 秒后被触发,但显然他们的值改变了。原因就在 mark 的判断上。因为在第一次 mark 判断时没有对所有字段的 mark 做 false 处理。导致触发时,因为 watch 字段上后面的字段还存在 mark 为 true 的情况。导致整个字段跳过。示例 C 中之所以不触发是因为 selectedCount 字段的 mark 还存在,被误认为第一次触发。而如果只是单独监控一个字段。那么都会被触发。 解决思路:在 watch 判断计算属性是否为第一次触发时,把整个 watch 字段关联的 mark 都设置为 false。而不是设置第一个后就跳出。 重写后的功能实现 解决上面已知问题。 computed 和 watch 都在 beforeCreate(主要的) 和 created(辅助的) 周期完成。不涉及 attached 周期,提高效率。 computed 改用 this获取data,取消参数传值。主要考虑是 ts 类型可以实现彼此调用提示。当前官方通过参数传值,无法获取其他计算字段的类型提示(ts 的泛型机制问题),this 只提供 data 字段,不喜欢可以自己改为全 this 字段。 watch 若监控的是对象或对象子属性时,若新值为null或空对象,不报错,不触发watch, 增加偶数位参数为前一参数的旧值。去除’**’,新旧值比较为JSON.stringify。只支持单字段触发(多字段不常用,且可替代,主要为了ts类型性能考虑) 对响应式数据的支持(非官方,新方案) <br> 示例 D [代码] import { observable, runInAction } from "mobx"; //基于mobx最新版本 const counter = observable({ count: 1, }); setInterval(() => { runInAction(() => { store.age ++ }); }, 1000); Component({ properties:{ productInfo:Object //type:{id:string;name:string;selectedCount:number;originalPrice:number;discount:number} }, data:{ // 传入响应式数据(新的响应式数据方案) responsiveCounter:()=>counter.count } computed:{ realPrice(){ return this.data.productInfo?.originalPrice || 0 * this.data.productInfo?.discount || 0 / 10 }, selectedCount(){ return this.data.productInfo?.selectedCount }, count(){ //支持计算响应式数据 return this.data.responsiveCounter + 1 } }, watch:{ //productInfo为null 或 {} 不报错 不触发 "productInfo"(newVal,oldVal){ newVal; oldVal; }, //productInfo为null 或 {} 不报错 不触发 "productInfo.originalPrice"(newVal,oldVal){ newVal; oldVal; } //对计算属性watch "realPrice"(realPriceNewVal,realPriceOldVal){ //... } } }) [代码] 代码片段 这个计算属性和watch在不断的迭代中 最新behavior查看源码 欢迎留言讨论,指错,如果此文对你有帮助,请点赞支持。
2022-07-31 - 这个报错怎么处理????
真机上出现: Some selectors are not allowed in component wxss, including tag name selectors, ID selectors, and attribute selectors。 这是什么错误????怎么处理????没感觉出啥错误!!!!
2019-04-03 - base64图片上传云托管对象存储的方法
H5端使用canvas生成的图片获取到的是图片的base64字符串,想要把该图传到对象存储那么就要将base64想办法转为图片file文件对象。 给大家分享一个base64转file文件对象的方法: //参数1为 dataurl为base64 //参数2为 name为自定义名称 const base64ToFile = (dataurl,name) => { const arr = dataurl.split(','); const mime = arr[0].match(/:(.*?);/)[1]; const bstr = atob(arr[1]); let n = bstr.length; const u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } const suffix = mime.split('/'); return new File([u8arr], name, { type: mime }); };
2022-06-27 - 实现一个虚拟滚动的React-Hook
前言 网页的日常开发中,渲染长列表的场景非常常见。比如旅游网站需要完全展示出全国的城市列表,或者购物网站的商品列表。 长列表的数量一般在几百条范围内不会出现意外的效果,浏览器本身足以支撑.可一旦数量级达到上千,页面渲染过程会出现明显的卡顿。数量突破上万甚至十几万时,网页可能直接崩溃了。 为了解决长列表造成的渲染压力,业界出现了相应的应对技术,即长列表的[代码]虚拟滚动[代码]。 [代码]虚拟滚动[代码]的本质,不管页面如何滑动,[代码]HTML 文档[代码]只渲染当前屏幕视口展现出来的少量[代码]Dom[代码]元素。 假设长列表有[代码]10[代码]万条数据,,对用户而言,他永远只会看到屏幕展现出的那十几条数据。因此页面滑动时,通过监听滚动事件快速切换视口的数据,就能高度模拟滚动效果。 参考下图加深理解: [图片] [代码]虚拟滚动[代码]最终只需要渲染少量的[代码]Dom[代码]元素就能模拟出相似的滚动效果,这让前端工程师开发几万甚至十几万条的长列表都成为了可能。 下图是手机上实测滑动一张涵盖全球所有城市的长列表页面. [图片] 虚拟滚动实现步骤 实现「虚拟列表」可以简单理解为就是在列表发生滚动时,改变「可视区域」内的渲染元素。大概的文字逻辑步骤如下: 根据单个元素高度计算出滚动容器的可滚动高度,并撑开滚动容器; 根据可视区域计算总挂载元素数量; 根据可视区域和总挂载元素数量计算头挂载元素(初始为 0)和尾挂载元素; 当发生滚动时,根据滚动差值和滚动方向,重新计算头挂载元素和尾挂载元素。 核心的实现步骤: [图片] 数据项不定高 还可以稍微做点小拓展,将item高度设定为一个配置项,可以设定为一个方法,改方法以每一个数据项的值和索引index作为参数。 按照这种方式,我们的hook能支持的自定义化会更加强大一些,我们在计算展示List片段的时候,在需要用到item高度的时候都需要针对性的做一些改变,高度不一定是个固定数值,可能是一个方法。 虚拟滚动和React Hook [代码]Hook[代码]是 React 16.8 中增加的新功能,可以让我们更好地复用React状态逻辑代码,我们可以使用React提供给我们的hook将虚拟滚动抽取为一个hook,方便我们随时复用这块功能。 Hook整合虚拟滚动 利用 [代码]useEffect[代码]监听容器元素,当容器元素渲染完毕,注册[代码]Scroll[代码]事件监听,当Scroll事件触发时候,根据滚动距离计算需要展示的List片段 按照虚拟滚动原理,截取需要展示的List并重置页面state,触发页面重新渲染 hook将需要展示的List片段作为状态返回,方便Hook与组件进行交互 总结 通过借助Hook和我们对于虚拟滚动功能的抽象,我们后续就可以很方便的给我们组件复用这块相对复杂的功能。 [代码]const [list, scrollTo] = useVirtualList(originalList, { containerTarget: containerRef, wrapperTarget: wrapperRef, itemHeight: 60, overscan: 10, }); [代码] 存在的不足 虚拟功能虽然很强大,但是仍然存在它的不足,列表项的高度必须相对固定,如果每个列表项渲染的高度完全未知,那虚拟滚动功能就无法使用。 链接 源代码地址 demo地址
2022-06-27 - 小程序云开发查询数据库查询慢?该如何优化。
小程序云开发查询数据库好慢呀,有一个列表页需要查询大量数据,将查询到的数据进行了拼接。获取所有的,将近要3秒。有什么优化的方法吗?小白的代码很烂,勿喷。 [图片]
2022-06-26 - 小程序中如何修改 svg 图片的颜色
已知小程序 <image /> 支持 svg 图片渲染;但是在开发场景中,如果我们需要修改 svg 图片原有的颜色,往往需要去修改 svg 文件本身。这样修改既不优雅,也不利于 svg 图片的复用。有没有一种方法可以更加优雅地去修改 svg 图片的颜色呢? 一、可行性探讨 svg 源码修改 既然要在原 svg 文件的基础上修改颜色,让我们先看一下 svg 源码是如何的,下面是一个三角形的 svg 源码: [图片] 我们可以发现在源码中,<path> 中的 fill 属性便是我们需要修改的颜色。如果我们能读取源码,修改对应属性,便能修改 svg 图片的颜色了;但开发过程中,我们的 svg 源文件往往是网络资源,并不能直接修改,有没有一种方法可以将 svg 源码直接在小程序进行渲染呢? svg 源码渲染 小程序原生虽然不支持 <svg> 渲染的,但我们可以通过 background-image 样式属性对 URL 资源进行加载;我们只需要将修改后 svg 源码进行 URL 编码,即可将我们想要的效果渲染出来。 方案总结 1、读取 svg 文件 2、匹配 Hex 字段并进行修改 3、将修改后的 svg 数据进行 URL 编码 4、将 URL 数据通过 background-image 样式属性进行渲染 二、技术实现 调用方式:组件 为方便调用,将其封装自定义组件,组件命名 svg,承接 svg 渲染能力,后续可在此基础上丰富 svg 的能力。 入参:src <string>,colors <string[]> 通过 src 参数传递 svg 图片链接; svg 图片可能包括多个元素,这个时候就需要我们对不同的元素定义各自的颜色。 默认属性 由于 svg 自身不存在尺寸,我们可以将其宽高同时设置成 100%,这样就可以通过承载其的父元素决定 <svg /> 的渲染尺寸。 三、<svg /> 组件代码 代码片段,点击进入 四、更多功能扩展 自定义 svg 渲染 前面我们修改 svg 图片是通过修改其 fill 属性实现的;更进一步我们完全可以不依赖外部资源,自定义 svg 节点,按照 svg 的规范进行图片的绘制,感兴趣可以尝试尝试。
2022-06-22 - wx.getUpdateManager如何在真机进行测试
在编辑器上可以正常使用。也能够监听到Wx.getUpdateManager()的onCheckForUpdate方法的值 [图片] [图片] False以及HasUpdate的都可以监听的到 。 测试过程中在编辑器上可以模拟出有新版本的更新以及重启小程序 可是 我想问一下 如果我用真机预览测试 怎样去模拟除这样的更新 上边的编译模式选中了下次编译时模拟更新,可是点击确定以后编辑器自动重新编译。而此时再去扫预览二维码或者说远程调试的二维码。不会模拟出有新版本的 。。 希望管理 能给解答一下这个问题 .
2018-04-18 - 小程序app.onLaunch与page.onLoad异步问题的最佳实践
场景: 在小程序中大家应该都有这样的场景,在onLaunch里用wx.login静默登录拿到code,再用code去发送请求获取token、用户信息等,整个过程都是异步的,然后我们在业务页面里onLoad去用的时候异步请求还没回来,导致没拿到想要的数据,以往要么监听是否拿到,要么自己封装一套回调,总之都挺麻烦,每个页面都要写一堆无关当前页面的逻辑。 直接上终极解决方案,公司内部已接入两年很稳定: 1.可完美解决异步问题 2.不污染原生生命周期,与onLoad等钩子共存 3.使用方便 4.可灵活定制异步钩子 5.采用监听模式实现,接入无需修改以前相关逻辑 6.支持各种小程序和vue架构 。。。 //为了简洁明了的展示使用场景,以下有部分是伪代码,请勿直接粘贴使用,具体使用代码看Github文档 //app.js //globalData提出来声明 let globalData = { // 是否已拿到token token: '', // 用户信息 userInfo: { userId: '', head: '' } } //注册自定义钩子 import CustomHook from 'spa-custom-hooks'; CustomHook.install({ 'Login':{ name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } }, globalData) // 正常走初始化逻辑 App({ globalData, onLaunch() { //发起异步登录拿token login((token)=>{ this.globalData.token = token //使用token拿用户信息 getUser((user)=>{ this.globalData.user = user }) }) } }) //关键点来了 //Page.js,业务页面使用 Page({ onLoadLogin() { //拿到token啦,可以使用token发起请求了 const token = getApp().globalData.token }, onLoadUser() { //拿到用户信息啦 const userInfo = getApp().globalData.userInfo }, onReadyUser() { //页面初次渲染完毕 && 拿到用户信息,可以把头像渲染在canvas上面啦 const userInfo = getApp().globalData.userInfo // 获取canvas上下文 const ctx = getCanvasContext2d() ctx.drawImage(userInfo.head,0,0,100,100) }, onShowUser() { //页面每次显示 && 拿到用户信息,我要在页面每次显示的时候根据userInfo走不同的逻辑 const userInfo = getApp().globalData.userInfo switch(userInfo.sex){ case 0: // 走女生逻辑 break case 1: // 走男生逻辑 break } } }) 具体文档和Demo见↓ Github:https://github.com/1977474741/spa-custom-hooks 祝大家用的愉快,记得star哦
2023-04-23 - 小代码大作用,云函数openapi
以下云函数openapi的代码极简,但是作用很多,包括: (代码直接复制可用) 1、支持所有云调用;是所有哦。 2、支持大图片安全检查 3、支持环境共享的云调用。 云函数代码如下: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const opt = {} exports.main = async event => { const wxc = cloud.getWXContext() opt.appid = wxc.FROM_APPID || wxc.APPID//获取环境或共享环境的访问端小程序appid if (event.action == 'security.imgSecCheck') return await imgSecCheck(event)//大图片安全检查 if (event.action == 'xxx') return await xxx(event)//其他特殊处理 return await cloud.openapi(opt)[event.action](event.body || {}) } async function imgSecCheck(event) { let res = await cloud.downloadFile({ fileID: event.fileID, }) return await cloud.openapi(opt).security.imgSecCheck({ media: { contentType: "image/png", value: res.fileContent } }) } 小程序端的调用代码示例: 1、获取小程序码 app.cloud.callFunction({ //app.cloud是小程序当前环境的cloud,在app.js中初始化,可能是wx.cloud,也可能是共享环境的cloud name: 'openapi', data: { action: 'wxacode.getUnlimited', body: { scene, width: 280 }, } }) 2、发送订阅消息 app.cloud.callFunction({ name: 'openapi', data: { action: 'subscribeMessage.send', body: { "touser": openid, "page": 'pages/index/index?orderId=' + order._id, data, "templateId": tid, "miniprogramState": 'trial' } } }) 3、获取小程序直播房间列表 app.cloud.callFunction({ name: 'openapi', data: { action: 'liveBroadcast.getLiveInfo', body: { start: 0, limit: 100 } } })
2022-06-14 - 小程序状态管理方案
详细的文档 [图片] [图片] redux 小程序适配方案。在小程序开发中使用 [代码]redux[代码] 管理全局状态。 尽管小程序入门门槛非常之低,但是在项目不停的迭代过程中,不可避免的项目代码复杂度也会越来越高,从前我们可以将跨页面数据管理在Storage或者SessionStorage中,利用一定的代码规范来管理不同页面不同开发者的数据,但随着时间的推移这种方式会造成代码、数据过于分散,且容易出错覆盖。再者每个页面间都需要手动的去 [代码]storage[代码] 读取数据,略显繁琐。 当项目开始变得复杂,我们想要统一的管理起状态数据,自动的同步、分发数据到需要的页面、组件([代码]Reactive[代码])。 安装 [代码]# 使用npm安装 npm i -S @wxa/redux # 使用yarn安装 yarn add @wxa/redux [代码] 基本用法 挂载插件 在[代码]app.js[代码]/[代码]app.wxa[代码]中挂载插件 [代码]// app.js or app.wxa import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers} from '@wxa/redux' import promiseMiddleware from 'redux-promise'; // 注册插件 wxa.use(wxaRedux, { reducers: combineReducers(...your reducer), middlewares: [promiseMiddleware] }) @App export default class Main {}; [代码] 注册完 redux 插件之后,将会自动的调用 [代码]redux.createStore[代码] 创建一个用于存储全局状态数据 [代码]store[代码],并且插件会在自动的挂载 [代码]store[代码] 到 App、Component、Page 实例中 [代码]$store[代码] 。 通过 [代码]this.$store.getState()[代码]可以获得所有全局状态。 通过 [代码]this.$store.dispatch()[代码]可以提交一个状态修改的 action。 更详细的 store api 获取全局状态 在页面/组件类中定义 [代码]mapState[代码] 对象,指定关联的全局状态(在[代码]react[代码]中叫[代码]connect[代码])。 [代码]import {Page} from '@wxa/core'; @Page export default class Index { mapState = { todolist$ : (state)=>state.todo, userInfo$ : (state)=>state.userInfo } add() { // dispatch change state. // todo list will auto add one. this.$store.dispatch({type: 'Add_todo_list', payload: 'coding today'}); } } [代码] 然后再[代码]template[代码]中就可以直接使用映射的数据了。 [代码]<view>{{userInfo$.name}}</view> <view wx:for="{{todolist$}}">{{key+1}}{{item}}</view> [代码] 得益于 [代码]@wxa/core[代码] 的 diff方法,redux在同步数据的时候只会增量的修改数据,而不是全量覆盖 😁 在任意位置获取全局状态数据 编写一些通用的基础函数提供给页面调用的时候,可能会需要从 [代码]store[代码] 中读取相应数据做处理。 例如在我们需要在所有请求的 postdata 中统一的加上用户的基本信息,可以这么实现: [代码]// 任意 api.js import {fetch} from '@wxa/core'; import {getStore} from '@wxa/redux'; export default const customFetch = (...args) => { let {idNo, name} = getStore().getState().UserModel; // 每个请求自动添加用户 args[1] = { idNo, name ...args[1], }; return fetch(...args); } [代码] 个性化页面数据 有时我们可能需要临时改写一下数据用于展示,实现类似 [代码]vue[代码] [代码]computed[代码] 的效果,此时我们可以相应的改造 mapState。 [代码]export default class A { mapState = { userInfo$(state){ let model = state.UserModel; // 自动掩码用户的身份证、姓名 // diff 数据并自动调用 setData this.$diff({ idNoCover: model.idNo.replace(/([\d]{4})(\d{10})([\dxX]{4})/, '$1***$3') }) return model } } } [代码] 分包用法 当小程序应用开始使用分包技术的时候,redux 方案也需要相应的做出优化,分包有以下特点: 引用原则 packageA 无法 require packageB JS 文件,但可以 require app、自己 package 内的 JS 文件 packageA 无法 import packageB 的 template,但可以 require app、自己 package 内的 template packageA 无法使用 packageB 的资源,但可以使用 app、自己 package 内的资源 即当分包 A 定义了自己业务逻辑的数据 model 之后,且该 model 无法被其他分包复用,则我们完全可以把对应 model 放到分包的页面中,懒加载对应 [代码]redux.reducer[代码],以此减少主包体积。 为了做到懒加载对应的 reducer,我们需要在改造一下我们的代码。 挂载插件 在[代码]app.js[代码]/[代码]app.wxa[代码]中,改造 [代码]reducer[代码] 的注册方式。 [代码]// app.js or app.wxa import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers} from '@wxa/redux' import promiseMiddleware from 'redux-promise'; // 注册插件 wxa.use(wxaRedux, { // reducers: combineReducers(...your reducer), reducers: { UserModel: userReducer, AppModel: appReducer, ...your reducer }, middlewares: [promiseMiddleware] }) @App export default class Main {}; [代码] 动态添加分包 Reducer 假设我们在分包 A 中定义了专门用于订单处理的 [代码]reducer[代码],分包入口页面为 [代码]subpages/A/pages/board[代码]。 在分包页面被使用之前我们需要动态的注册一个新的 [代码]reducer[代码]。 [代码]// subpages/A/pages/board import {reducerRegistry} from '@wxa/redux'; import AOrderModel from '/subpages/A/models/order.model.js'; // 注册对应的数据 model reducerRegistry.register('AOrderModel', AOrderModel); [代码] 注册完毕之后,后续所有分包 A 的页面都可以正常的使用 [代码]mapState[代码] 中映射页面需要使用的状态数据。 调试 Redux [代码]@wxa/redux[代码] 提供了小程序 [代码]redux-remote-devtools[代码] 的适配代码。稍微改造一下我们的挂载插件部分的代码即可使用: [代码]import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers, applyMiddleware} from '@wxa/redux' import { composeWithDevTools } from '@wxa/redux/libs/remote-redux-devtools.js'; import promiseMiddleware from 'redux-promise'; const composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 }); // 注册插件 wxa.use(wxaRedux, { // reducers: combineReducers(...your reducer), reducers: { UserModel: userReducer, AppModel: appReducer, ...your reducer }, middlewares: composeEnhancers(applyMiddleware(promiseMiddleware)) }) [代码] 打开开发者工具不校验合法域名开关,就可以正常使用 [代码]redux-devtools[代码] 了。 由于 [代码]devtools[代码] 仅用于开发阶段,我们可以利用 [代码]wxa[代码] 提供的依赖分析能力,按需引入。 改写上续配置如下: [代码]import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers, applyMiddleware} from '@wxa/redux' import promiseMiddleware from 'redux-promise'; let composeEnhancers = (m) => m; if (process.env.NODE_ENV === 'production') { let composeWithDevTools = require('@wxa/redux/libs/remote-redux-devtools.js').composeWithDevTools; composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 }); } // 注册插件 wxa.use(wxaRedux, { // reducers: combineReducers(...your reducer), reducers: { UserModel: userReducer, AppModel: appReducer, ...your reducer }, middlewares: composeEnhancers(applyMiddleware(promiseMiddleware)) }) [代码] 如上配置,当 [代码]process.env.NODE_ENV[代码] 设置为生产环境的时候,[代码]@wxa/redux/libs/remote-redux-devtools.js[代码] 将不会被打包进 [代码]dist[代码] 持久化数据 某些场景,为了用户体验,我们需要将对应数据缓存下来,方便下次用户可以直接看到对应页面,此时我们需要将 [代码]store[代码] 的数据缓存下来,这里我们使用 [代码]redux-persist[代码] 用于持久化数据。 示例如下: [代码]import {wxa, App} from '@wxa/core'; import wxaRedux from '@wxa/redux'; import wxPersistStorage from '@wxa/redux/libs/wx.storage.min.js'; import {persistStore, persistReducer} from 'redux-persist'; import orderModel from './order.model.js'; let persistOrderModel = persistReducer({ key: 'orderModel', storage: wxPersistStorage, timeout: null, // 超时时间,设置为 null }, orderModel); wxa.use(wxaRedux, { reducers: { orderModel: persistOrderModel } }) @App export default class { onLaunch() { // 冷启动开始就加载缓存数据 persistStore(this.$store, {}, ()=>this.$storeReady=true); } } [代码] 实时日志 我们可以结合小程序实时日志和 [代码]redux-logger[代码] 一起使用。 [代码]import {wxa, App} from '@wxa/core'; import {createLogger} from 'redux-logger'; import wxaRedux from '@wxa/redux'; let log = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : console; let logger = createLogger({ logger: log }); wxa.use(wxaRedux, { reducers: {...your reducers}, middlewares: [logger] }); [代码] 配置完毕之后,项目中所有的 [代码]Action[代码] 日志都将上报到微信的实时日志后台,开发者可以登录 mp.weixin.qq.com 查看用户所有操作记录。 配置 reducers 类型: Function [代码]combineReducers(...reducer)[代码]的返回 Object [代码]reducer[代码] 列表,用于动态注册场景 middlewares 类型: Array redux 中间件列表 Function [代码]applyMiddleware(...middlewares)[代码]的返回 initialState 类型: any reducer 初始状态,参考 [代码]redux 文档[代码] debug 类型: Boolean [代码]false[代码] 是否打印插件日志 技术细节 [代码]wxa/redux[代码]根据不同的实例类型有不同的任务,在App层,我们需要创建一个[代码]store[代码]并挂载到app中,在[代码]Page[代码]和[代码]Component[代码]层,我们做了更多细节处理。 App Level 创建[代码]store[代码],应用redux的中间件,挂载[代码]store[代码]到App实例。 Page Level 在不同的生命周期函数,有不同的处理。 [代码]onLoad[代码] 根据[代码]mapState[代码]订阅[代码]store[代码]的数据,同时挂载一个[代码]unsubscribe[代码]方法到实例。 [代码]onShow[代码] 标记页面实例[代码]$$isCurrentPage[代码]为[代码]true[代码], 同时做一次状态同步。因为有可能状态在其他页面做了改变。 [代码]onHide[代码] 重置[代码]$$isCurrentPage[代码],这样子页面数据就不会自动刷新了。 [代码]onUnload[代码] 调用[代码]$unsubscribe[代码]取消订阅状态 Component Level 针对组件生命周期做一些单独处理 [代码]created[代码] 挂载[代码]store[代码] [代码]attached[代码] 订阅状态,并同步状态到组件。 [代码]detached[代码] 取消订阅
2020-07-05 - 小程序自动销毁后的使用体验优化
2023年12月24日更新 经过测试发现,官方提供的onSaveExitState有一些问题,不太符合官方文档提出的预期,大家暂时慎重使用onSaveExitState功能,用其他方案吧。 ======================================================================== 假设用户在小程序内进行一个答题的活动,或者进行一个测试,这个活动或测试的时间比较长,大概需要10分钟的时间。当用户答题进行到一半的时候,来了一个重要的电话,电话打了十几分钟,回来之后想着继续进行操作,发现小程序是重新打开的状态。之前答题答了5分钟,白费了。这样,用户需要重新进行答题。 问题场景分析 用户离开小程序时间太久(官方说30分钟以上,但测试十几分钟分钟以上)或者手机内存不够用的时候,小程序会被销毁,也就是完全终止运行了。此时用户再想进入小程序进行之前的操作,只能重新操作一遍。 解决方案 以本场景为例,如果用户正在答题,在用户退出小程序的时候,将当前页面的答题进度数据进行一个保存,当用户再重新进入小程序的时候,检查是否有答题进行一半的数据。如果有,自动跳转到答题的页面,并且在onload中恢复退出之前状态的数据,让用户继续进行答题的操作。 微信小程序有一个非常好用的回调函数onSaveExitState。 退出状态onSaveExitState 每当小程序可能被销毁之前,页面回调函数 [代码]onSaveExitState[代码] 会被调用。如果想保留页面中的状态,可以在这个回调函数中“保存”一些数据,下次启动时可以通过 [代码]exitState[代码] 获得这些已保存数据。 代码示例: { "restartStrategy": "homePageAndLatestPage" } Page({ onLoad: function() { var prevExitState = this.exitState // 尝试获得上一次退出前 onSaveExitState 保存的数据 if (prevExitState !== undefined) { // 如果是根据 restartStrategy 配置进行的冷启动,就可以获取到 prevExitState.myDataField === 'myData' } }, onSaveExitState: function() { var exitState = { myDataField: 'myData' } // 需要保存的数据 return { data: exitState, expireTimeStamp: Date.now() + 24 * 60 * 60 * 1000 // 超时时刻 } } }) onSaveExitState 返回值可以包含两项: 字段名 类型 含义 data Any 需要保存的数据(只能是 JSON 兼容的数据) expireTimeStamp Number 超时时刻,在这个时刻后,保存的数据保证一定被丢弃,默认为 (当前时刻 + 1 天) 一个更完整的示例:在开发者工具中预览效果 注意事项如果超过 [代码]expireTimeStamp[代码] ,保存的数据将被丢弃,且冷启动时不遵循 [代码]restartStrategy[代码] 的配置,而是直接从首页冷启动。[代码]expireTimeStamp[代码] 有可能被自动提前,如微信客户端需要清理数据的时候。在小程序存活期间, [代码]onSaveExitState[代码] 可能会被多次调用,此时以最后一次的调用结果作为最终结果。在某些特殊情况下(如微信客户端直接被系统杀死),这个方法将不会被调用,下次冷启动也不遵循 [代码]restartStrategy[代码] 的配置,而是直接从首页冷启动。
2023-12-24 - 为小程序添加生命周期(beforeCreate和attach)
示例 1 index 页面 [代码]// index.json { "usingComponents": { "parent":"/component/parent/parent" } } // index.wxml <parent /> // index.js 这里使用Component来取代Page构造器生成页面实例。 Component({ lifetimes: { created() { console.log("index --> created"); }, attached() { console.log("index --> attached"); }, }, methods: { onLoad() { console.log("index --> onLoad"); }, }, }); [代码] parent 组件 [代码]//parent.json { "usingComponents": { "son": "/components/son/son" } } //parent.wxml <son /> //parent.js Component({ lifetimes: { created() { console.log("parent --> created"); }, attached() { console.log("parent --> attached"); }, }, }); [代码] son 组件 [代码]//son.json { "usingComponents": {} } //son.wxml <test>son</test> //son.js Component({ lifetimes: { created() { console.log("son --> created"); }, attached() { console.log("son --> attached"); }, }, }); [代码] 编译后 控制台打印如下 son --> created parent --> created index --> created index --> attached parent --> attached son --> attached index–>onLoad 小结: 组件建立是由内而外的。页面所有组件都挂载到页面组件后,才由外向内触发每个子组件的 attached 周期。最后触发页面的(最外层组件)onLoad 周期。 值得注意的是每个组件 attached 周期触发时,this.data 数据 可能被上级组件传入多次,导致获取不到首次挂载时的 this.data 对象。对调试等一些情况是不友好。 beforeCreate 和 attach 示例 1 展示了小程序给我们提供的生命周期触发顺序。但缺少一些生命周期。或许是小程序认为那些不重要,我们用不到吧。让我们来补充一下,这对调试、开发插件等情形很有帮助 增加 attach 周期 (可用作复杂组件调试场景,相比与 attached 只是触发时机不一样,this.data.fields 是首次挂载数据时的实例数据.而 attached 触发时,由于上级组件传入数据导致实例data中的数据已不是当初的样子了,在应对复杂组件应用的调试时不是很好用) 分别在示例 1 增加 如下代码 [代码]<!-- index.wxml 增加 attach 传值--> <parent attach /> <!-- parent.wxml 增加 attach 传值--> <son attach /> [代码] [代码]// parent.js Component({ //新增 properties: { attach: Boolean, }, observers: { attach() { console.log("parent --> attach"); }, }, //...省略之前示例1中的代码 }); // son.js Component({ //新增 properties: { attach: Boolean, }, observers: { attach() { console.log("son --> attach"); }, }, //...省略之前示例1中的代码 }); [代码] 控制台打印如下 son --> created son --> attach parent --> created parent --> attach index --> created index --> attached parent --> attached son --> attached 至此我们有了 attach 生命周期 利用这个周期,我们可以看到组件挂载时 this 上的所有信息,在某写情形下对代码优化,调试带来方便。 稍后我们将它和 beforeCreate 一起封装起来。 增加 beforeCreate 周期 (触发时机在实例数据建立之前 this 指向当前配置) 因为 created 周期无法修改实例数据、attached 周期又触发太晚、配置阶段又不能调用函数 。 某些情形下(全局注入等)还是需要 beforeCreate 周期的。 办法是 可以为单个组件添加 或 劫持 Component 全局添加如下 behavior [代码]const beforeCreate = Behavior({ definitionFilter(opt) { opt.lifetimes?.beforeCreate?.call(opt); delete opt.lifetimes?.beforeCreate; //删除 避免冲突吧。 }, }); // 全局注入beforeCreate const originalComponent = Component; Component = function (options) { options.behaviors ||= [].push(beforeCreate); return originalComponent(options); }; [代码] 编译后 控制台打印如下 parent --> beforeCreate son --> beforeCreate son --> created parent --> created index --> created index --> attached parent --> attached son --> attached index --> onLoad 合并 attach 和 beforeCreate [代码]// beforeCreateAndAttach.js export const beforeCreateAndAttach = Behavior({ definitionFilter(opt) { //attach 注意这里没有对properties和observers字段中判断是否已经有了attach字段 (opt.properties ||= {}).attach = Boolean; (opt.observers ||= {}).attach = function () { delete this.data.attach; // 没什么用了 opt.lifetimes?.attach?.call(this); }; // beforeCreate opt.lifetimes?.beforeCreate?.call(opt); delete opt.lifetimes?.beforeCreate; // 没什么用了 }, }); //全局注入 const originalComponent = Component; Component = function (options) { (options.behaviors ||= []).push(beforeCreateAndAttach); return originalComponent(options); }; [代码] [代码]// app.js import "./beforeCreateAndAttach"; App({}); [代码] index --> beforeCreate parent --> beforeCreate son --> beforeCreate son --> created son --> attach parent --> created parent --> attach index --> created index --> attached parent --> attached son --> attached index --> onLoad 总结: 利用 behavior 中的 definitionFilter,可在组件实例创建前修改原配置。模拟 beforeCreate 生命周期。 利用 properties 传 boolean 值,通过 observers 监控传值字段 达到获取组件挂载时第一时间的 this 实例数据,模拟 attach 生命周期。 已知不足是:需要在要调用 attach 生命周期的组件上加入 attach(可自定义)字段, 有可能导致数据字段冲突,但此周期多用于调试,仅当需要调试组件,查看挂载时数据时,手动添加也没什么问题。 水平有限,如有错误之处,请留言指教。 代码片段
2022-06-03 - 【技巧】swiper仿tab切换
大家好,上次给大家分享了swiper多图片的解决方案:https://developers.weixin.qq.com/community/develop/article/doc/0008aa4fdbce08405c288f37951813 今天再给大家分享一个关于swiper的小技巧,利用swiper仿tab切换。 相信大家在app或浏览器上阅读新闻时,比如今日头条,会有这样一个场景,左右滑动的时候可以切换不同栏目,体验非常好,但是小程序好像没有提供相关组件,如果想实现这种效果该怎么做呢今天就给大家介绍一下在小程序里是怎么实现的。 首先先看下效果 [图片] 实现原理很简单,利用小程序swiper再配合scroll-view就能实现,不过这里面有几点需要注意一下: 1.scroll-view一定要给一个高度,不然会有问题; 2.切换的时候只显示当前的swiper-item里的内容,其它swiper-item里的内容可以先隐藏掉,这是因为如果你的swiper-item里的图片太多的话可能会造成页面回收,因为新闻列表大多是图文列表,而tab经常是不止两个的,可能是7、8个或更多,如果每个tab都显示的话到时上拉加载页面会非常庞大,所以这里我建议不用显示的内容先隐藏,记住是swiper-item里的内容不是swiper-item,到时切换回来时再重新渲染,如果你要保存滚动的位置还要做其它的一些处理,这里就不仔细讲解了; 3.这里适用的是整个页面都是tab切换的,如果只是在页面的某处实现tab切换,还要考虑高度的问题,加载数据的时候根据数据个数长度来计算高度,每次加载数据都要计算高度,切换到不同的tab也是,这部分比较麻烦,因为要计算,不过并不难,只要 计算正确的话是没有问题的; 大概就是这样,基本实现思路,大家可以根据这个思路去拓展,在上面加上自己的功能,over! 代码片段:https://developers.weixin.qq.com/s/89OO1smX736d
2019-02-26 - Aggregate.geoNear使用注意事项
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/aggregate/Aggregate.geoNear.html 官方文档表述和例子都不够详尽,自己踩了一天的坑,总结一下分享给大家: 1、参数near应该传入一个什么对象? 文档表明需要传入GeoPoint对象 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/geo/Geo.Point.html 该文档中指出可以用json对象等效使用,如:{ type: 'Point', coordinates: [113, 23] },在做数据插入时确实work,但是在这里只有传入db构造函数才work,如:db.Geo.Point(113, 23) 2、maxDistance 使用时需要以米为单位,再除以地球半径,即:6378137。如限定10公里范围内:maxDistance: 100000/637813 3、distanceMultiplier 基本上用不到这个参数,输出的结果distance以米为单位。 4、其他小问题,比如它只能处在aggregate第一阶段、必须建立地理位置索引,不必细说。
2022-06-02 - 【讨论】关于收回getUserProfile,使用button的开放能力chooseAvatar替换获取头像的思考
背景: 应微信官方通知(https://developers.weixin.qq.com/community/develop/doc/00022c683e8a80b29bed2142b56c01?blockType=1),即将收回getUserProfile,并且官方推荐通过使用button的开放能力chooseAvatar来获取头像(https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html),最近项目也在做相应的调整。 问题描述: 由于原有项目中,由于原先获取的微信头像都为正方形,且项目中显示头像的地方都为圆形,如下图1所示。现替换button的开放能力chooseAvata后,如下图2所示,选择”从相册选择“或者”拍照“时,得到的图片为长方形,这样获取到的图片,在项目中对应的头像显示或宽高比失调、或图片显示效果差(即设置image组建mode属性保持宽高比不变时,自动截取的部分图片不是用户想要的那个部分)。 图1:[图片] 图:2 [图片] 需求分析: 基于以上的场景,查阅了图片相关的api,找到了wx.editImage,效果如下图1所示;倘若wx.editImage支持自定义裁剪比例就好了,遗憾的是wx.editImage的入参只有:src,success,fail,complete这四个,如下图2所示;GG,黔驴技穷了,总不能跟产品经理说,微信小程序相关api不支持,不予处理。一个好的api工程师是需要严格要求自己的。 图1:[图片] 图2:[图片] 小结: 作为一个严谨的程序员,希望官方大大能拉我一把,快帮帮孩子吧(wx.editImage,能不能加一些配置项啥的),也欢迎大家一起帮忙分析下,有没有啥其他的解决方案,欢迎留言哦~
2022-05-28 - 微信小程序运行性能注意点
小程序的运行时性能直接决定了用户在使用小程序功能时的体验。如果运行时性能出现问题,很容易出现页面滚动卡顿、响应延迟等问题,影响用户使用。如果内存占用过高,还会出现黑屏、闪退等问题。 1.控制WXML节点数量和层级 建议一个页面 WXML 节点数量应少于 1000 个,节点树深度少于 30 层,子节点数不大于 60 个。太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长,影响体验。 2.避免滥用image组件的 widthFix/heightFix模式,控制图片资源的大小。 这种模式会在图片加载完成后,动态改变图片的高度或宽度。图片高度或宽度的动态改变,可能会引起页面内大范围的布局重排,导致页面发生抖动,并造成卡顿。 3.合理使用的setData setData 应只用来进行渲染相关的数据更新。用 setData 的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。 不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用;与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下。对于列表来说,可以利用setData进行列表局部刷新。 4.避免不当的使用onPageScroll 每一次事件监听都是一次视图到逻辑的通信过程,所以只在必要的时候监听pageSrcoll。避免在 scroll 事件监听函数中执行复杂逻辑。 5.合理的利用缓存 利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新,这不仅优化了性能,在无网环境下,用户也能很顺畅的使用到关键服务。 6.采用独立分包技术 提升体验最直接的方法是控制小程序包的大小。目前很多小程序主包+子包的方式,这对用户停留时间比较短的场景中,体验不是很好,且浪费了部分流量。 可以采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源。 7.使用自定义组件 自定义组件的更新只在组件内部进行,不受页面其他不能分内容的影响。各个组件也将具有各自独立的逻辑空间。每个组件都分别拥有自己的独立的数据、setData调用。
2022-05-28 - 小程序的同层渲染
背景 小程序的原生组件无法被覆盖,例如video 、ive-player 组件上无法覆盖原生组件 原因:由于 微信小程序的架构wxml设计 wxml 微信小程序的webview 层渲染是依托于 html 的。那么想view、span 这些都是被微信小程序通过wcc工具编译wxml为js,得到 Virtual DOM 结构,例如 [代码] "tag": "wx-page", "children": [ { "tag": "wx-view", 」 ] } [代码] 最后在被parse 为html,那像一些原生的组件则是依托微信的能力实现,那原生组件在创建的时候,会先用 基础组件占位,然后在上层使用,原生组件。 原生组件的层级是最高的,所以页面中的其他组件无论设置 z-index 为多少,都无法盖在原生组件上 cover-view 与 cover-image: 为了解决原生组件层级最高的限制。小程序专门提供了 cover-view 和 cover-image 组件,可以覆盖在部分原生组件上面。 限制: 无法覆盖textarea、input。 不支持设置单边的border、background-image、shadow、overflow: visible等。 cover-view 支持 overflow: scroll,但不支持动态更新 overflow。 cover-view和cover-image的aria-role仅可设置为 button,读屏模式下才可以点击,并朗读出“按钮”;为空时可以聚焦,但不可点击。 cover-view和cover-image的子节点如果溢出父节点,容易出现布局错误。 自定义组件嵌套 cover-view 时,自定义组件的 slot 及其父节点暂不支持通过 wx:if 控制显隐,否则会导致 cover-view 不显示。 同层渲染 微信官方给出解决方案, 在 WebView 所渲染的页面中,与其他 HTML 控件在同一层级。具体原生组件https://developers.weixin.qq.com/miniprogram/dev/component/native-component.html#原生组件的使用限制
2022-05-21 - 小程序销毁的时机
小程序会被销毁的三大场景: 1 当钱小程序进入后台后,如果很长时间-目前是 30 分钟-后没有再次进入,小程序会被销毁。 2 当小程序占用系统资源过高,会被系统销毁或被微信客户端主动回收。 3 在 iOS 上,当微信客户端在一定时间间隔内连续收到系统内存告警时,会根据一定的策略,主动销毁小程序,并提示用户 (运行内存不足,请重新打开该小程序)。 如果小程序中有过多占用内存的场景,建议使用 wx.onMemoryWarning 监听内存告警事件,进行必要的内存清理。
2022-05-21 - 借助小程序云开发实现小程序支付功能(含源码)
我们在做小程序支付相关的开发时,总会遇到这些难题。小程序调用微信支付时,必须要有自己的服务器,有自己的备案域名,有自己的后台开发。这就导致我们做小程序支付时的成本很大。本节就来教大家如何使用小程序云开发实现小程序支付功能的开发。不用搭建自己的服务器,不用有自己的备案域名。只需要简简单单的使用小程序云开发。 老规矩先看效果图: [图片] 本节知识点 1,云开发的部署和使用 2,支付相关的云函数开发 3,商品列表 4,订单列表 5,微信支付与支付成功回调 支付成功给用户发送推送消息的功能会在后面讲解。 下面就来教大家如何借助云开发使用小程序支付功能。 支付所需要用到的配置信息 1,小程序appid 2,云开发环境id 3,微信商户号 4,商户密匙 一,准备工作 1,已经申请小程序,获取小程序 AppID 和 Secret 在小程序管理后台中,【设置】 →【开发设置】 下可以获取微信小程序 AppID 和 Secret。 [图片] 2,微信支付商户号,获取商户号和商户密钥在微信支付商户管理平台中,【账户中心】→【商户信息】 下可以获取微信支付商户号。 [图片] 在【账户中心】 ‒> 【API安全】 下可以设置商户密钥。 [图片] 这里特殊说明下,个人小程序是没有办法使用微信支付的。所以如果想使用微信支付功能,必须是非个人账号(当然个人可以办个体户工商执照来注册非个人小程序账号) 3,微信开发者 IDE https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 4,开通小程序云开发功能:https://edu.csdn.net/course/play/9604/204526 二,商品列表的实现 效果图如下,由于本节重点是支付的实现,所以这里只简单贴出关键代码。 [图片] wxml布局如下: [代码]<view class="container"> <view class="good-item" wx:for="{{goods}}" wx:key="*this" ontap="getDetail" data-goodid="{{item._id}}"> <view class="good-image"> <image src="{{pic}}"></image> </view> <view class="good-detail"> <view class="title">商品: {{item.name}}</view> <view class="content">价格: {{item.price / 100}} 元 </view> <button class="button" type="primary" bindtap="makeOrder" data-goodid="{{item._id}}" >下单</button> </view> </view> </view> [代码] 我们所需要做的就是借助云开发获取云数据库里的商品信息,然后展示到商品列表,关于云开发获取商品列表并展示本节不做讲解(感兴趣的同学可以翻看我的历史博客,有写过的) 也有视频讲解: https://edu.csdn.net/course/detail/9604 [图片] 三,支付云函数的创建 首先看下我们支付云函数都包含那些内容 [图片] 简单先讲解下每个的用处 config下的index.js是做支付配置用的,主要配置支付相关的账号信息 lib是用的第三方的支付库,这里不做讲解。 重点讲解的是云函数入口 index.js 下面就来教大家如何去配置 1,配置config下的index.js, 这一步所需要做的就是把小程序appid,云开发环境ID,商户id,商户密匙。填进去。 [图片] 2,配置入口云函数 [图片] 详细代码如下,代码里注释很清除了,这里不再做单独讲解: [代码]const cloud = require('wx-server-sdk') cloud.init() const app = require('tcb-admin-node'); const pay = require('./lib/pay'); const { mpAppId, KEY } = require('./config/index'); const { WXPayConstants, WXPayUtil } = require('wx-js-utils'); const Res = require('./lib/res'); const ip = require('ip'); /** * * @param {obj} event * @param {string} event.type 功能类型 * @param {} userInfo.openId 用户的openid */ exports.main = async function(event, context) { const { type, data, userInfo } = event; const wxContext = cloud.getWXContext() const openid = userInfo.openId; app.init(); const db = app.database(); const goodCollection = db.collection('goods'); const orderCollection = db.collection('order'); // 订单文档的status 0 未支付 1 已支付 2 已关闭 switch (type) { // [在此处放置 unifiedorder 的相关代码] case 'unifiedorder': { // 查询该商品 ID 是否存在于数据库中,并将数据提取出来 const goodId = data.goodId let goods = await goodCollection.doc(goodId).get(); if (!goods.data.length) { return new Res({ code: 1, message: '找不到商品' }); } // 在云函数中提取数据,包括名称、价格才更合理安全, // 因为从端里传过来的商品数据都是不可靠的 let good = goods.data[0]; // 拼凑微信支付统一下单的参数 const curTime = Date.now(); const tradeNo = `${goodId}-${curTime}`; const body = good.name; const spbill_create_ip = ip.address() || '127.0.0.1'; // 云函数暂不支付 http 触发器,因此这里回调 notify_url 可以先随便填。 const notify_url = 'http://www.qq.com'; //'127.0.0.1'; const total_fee = good.price; const time_stamp = '' + Math.ceil(Date.now() / 1000); const out_trade_no = `${tradeNo}`; const sign_type = WXPayConstants.SIGN_TYPE_MD5; let orderParam = { body, spbill_create_ip, notify_url, out_trade_no, total_fee, openid, trade_type: 'JSAPI', timeStamp: time_stamp, }; // 调用 wx-js-utils 中的统一下单方法 const { return_code, ...restData } = await pay.unifiedOrder(orderParam); let order_id = null; if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { const { prepay_id, nonce_str } = restData; // 微信小程序支付要单独进地签名,并返回给小程序端 const sign = WXPayUtil.generateSignature({ appId: mpAppId, nonceStr: nonce_str, package: `prepay_id=${prepay_id}`, signType: 'MD5', timeStamp: time_stamp }, KEY); let orderData = { out_trade_no, time_stamp, nonce_str, sign, sign_type, body, total_fee, prepay_id, sign, status: 0, // 订单文档的status 0 未支付 1 已支付 2 已关闭 _openid: openid, }; let order = await orderCollection.add(orderData); order_id = order.id; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { out_trade_no, time_stamp, order_id, ...restData } }); } // [在此处放置 payorder 的相关代码] case 'payorder': { // 从端里出来相关的订单相信 const { out_trade_no, prepay_id, body, total_fee } = data; // 到微信支付侧查询是否存在该订单,并查询订单状态,看看是否已经支付成功了。 const { return_code, ...restData } = await pay.orderQuery({ out_trade_no }); // 若订单存在并支付成功,则开始处理支付 if (restData.trade_state === 'SUCCESS') { let result = await orderCollection .where({ out_trade_no }) .update({ status: 1, trade_state: restData.trade_state, trade_state_desc: restData.trade_state_desc }); let curDate = new Date(); let time = `${curDate.getFullYear()}-${curDate.getMonth() + 1}-${curDate.getDate()} ${curDate.getHours()}:${curDate.getMinutes()}:${curDate.getSeconds()}`; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } case 'orderquery': { const { transaction_id, out_trade_no } = data; // 查询订单 const { data: dbData } = await orderCollection .where({ out_trade_no }) .get(); const { return_code, ...restData } = await pay.orderQuery({ transaction_id, out_trade_no }); return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { ...restData, ...dbData[0] } }); } case 'closeorder': { // 关闭订单 const { out_trade_no } = data; const { return_code, ...restData } = await pay.closeOrder({ out_trade_no }); if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { await orderCollection .where({ out_trade_no }) .update({ status: 2, trade_state: 'CLOSED', trade_state_desc: '订单已关闭' }); } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } } } [代码] 其实我们支付的关键功能都在上面这些代码里面了。 [图片] 再来看下,支付的相关流程截图 [图片] 上图就涉及到了我们的订单列表,支付状态,支付成功后的回调。 今天就先讲到这里,后面会继续给大家讲解支付的其他功能。比如支付成功后的消息推送,也是可以借助云开发实现的。 由于源码里涉及到一些私密信息,这里就不单独贴出源码下载链接了,大家感兴趣的话,可以私信我,或者在底部留言。单独找我要源码也行(微信2501902696) 视频讲解地址:https://edu.csdn.net/course/detail/24770
2019-06-11 - 小程序云开发获取并保存用户IP属地
现在各大平台发表文章、评论等内容都显示出了用户的IP属地,现在来探讨一下小程序使用云开发怎么获取并保存用户IP属地。 1、获取到用户ip,这里演示使用云函数获取。 2、使用腾讯位置服务的WebService API的IP定位接口,获取归属地。 响应示例: { "status": 0, "message": "Success", "result": { "ip": "111.206.145.41", "location": { "lat": 39.90469, "lng": 116.40717 }, "ad_info": { "nation": "中国", "province": "北京市", "city": "北京市", "district": "", "adcode": 110000 } } } 演示代码: // 云函数入口文件 const cloud = require('wx-server-sdk') const axios = require('axios') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); var ip = wxContext.CLIENTIP ? wxContext.CLIENTIP : wxContext.CLIENTIPV6; if (ip) { const res = await axios.get("https://apis.map.qq.com/ws/location/v1/ip", { params: { ip: ip, key: "xxx" // 使用腾讯WebService API:https://lbs.qq.com/service/webService/webServiceGuide/webServiceIp } }); return res; } return null; }
2022-05-11 - 怎么在微信开发者工具中模拟小程序卸载来测试onUnload呢?
如题
2020-01-17 - 小程序客服消息云开发注意
用云开发处理小程序客服消息推送,有两点需要注意。 1、人工接入后的客户,不会收到自动回复 云开发可以针对不同类型的消息进行自动回复。 例如对 text 类型的消息设置了自动回复,那客户如果发送其他类型的消息(例如 image 类型),这个消息仍然会被推送到微信服务器。 此时如果人工接入了这个客户,那他后面发送 text 类型的消息,也不会触发自动回复了。 这个坑我花了近一个小时才爬出来。 那么怎样才能对已接入的客户启用自动回复?可以设置成“客服离线”。接下去该客户发送消息不会立即触发自动回复,需要发送两条,第二条开始才会触发自动回复。 2、user_enter_tempsession 触发条件 当客户进入客服对话时会触发这个事件,但有个前提条件:只有客户主动发送过消息,下次该客户再次进入客服对话时才会触发这个事件。 客户每发送一次消息,客服在 48 小时内可以回复 5 条。 基于这个限制,自动回复应当引导客户主动发送消息,以便在该客户下次进入聊天时可以立即收到欢迎语句。
2022-04-13 - 小程序横屏兼容处理
背景 在h5开发中可用的css适配单位有 em/百分比/rem/vw/vh/vmin/vmax,小程序提供了 rpx: 可以根据屏幕宽度进行自适应,规定屏幕宽为750rpx。 日常小程序开发中,一般设计图尺寸为 750 * 1334 px,则在小程序中一般 1px 直接写为 1rpx ,当小程序为竖屏([代码]"pageOrientation": "portrait"[代码] 默认为竖屏)时,根据 rpx 可以直接还原UI图,但是当小程序为横屏([代码]"pageOrientation": "landscape"[代码])时,根据 rpx 适配就明显比较大,不符合UI图,因为 rpx 是根据屏幕宽度适配的。 这个时候就需要一种既能适配不同屏幕大小,又能以设计图为准,快速布局的方式。通过了解 em/百分比/rem/vw/vh/vmin/vmax 这几种方式,明显 vmin 更符合,vmin 是vw和vh中比较 小 的值。 vw: Viewport宽度, 1vw 等于viewport宽度的 1%。 vh: Viewport高度, 1vh 等于viewport高的的 1%。 所以 100 vmin = 750px。 [代码].wxss[代码] 文件处理 当设置某个元素的宽度为 100px 时,根据 [代码]100px / 750px = x / 100vmin[代码] ,则对应的 vmin 值为 100vmin / 7.5 ,当单位为 rpx 时, vmin 值为 100vmin / 7.5 ,即 [代码]100px = 100vmin / 7.5[代码] 或者 [代码]100rpx = 100vmin / 7.5[代码] ,但是每次都写 [代码]vmin / 7.5[代码] 又有点麻烦,所以就写了个小工具 rpx2vmin ,支持将 rpx/px 转译为 vmin,这样布局的时候依然写 rpx/px ,最后再转译一下就可以了。 将需要转译的 [代码].wxss[代码] 文件复制粘贴到 [代码]input[代码] 文件下,在项目目录下执行如下命令行 ,会在 [代码]ouput[代码] 目录下生成对应的文件名称,需要提前安装 nodejs。 [代码]# 安装依赖 npm install # 将 rpx 转译为 vmin npm run rpx2vmin # 将 px 转译为 vmin npm run px2vmin [代码] 主要处理的如下: [代码]font-size: 12rpx; height: 60rpx; padding: 12rpx 16rpx; border-left: 2rpx dashed #5DA5FF; width: calc(100vw - 50rpx - 80rpx); [代码] 转移为 [代码]font-size: calc(12vmin / 7.5); height: calc(60vmin / 7.5); padding: calc(12vmin / 7.5) calc(16vmin / 7.5); border-left: calc(2vmin / 7.5) dashed #5DA5FF; width: calc(100vw - 50vmin / 7.5 - 80vmin / 7.5); [代码] 或者是: [代码]font-size: 12px; height: 60px; padding: 12px 16px; border-left: 2px dashed #5DA5FF; width: calc(100vw - 50px - 80px); [代码] 转移为 [代码]font-size: calc(12vmin / 7.5); height: calc(60vmin / 7.5); padding: calc(12vmin / 7.5) calc(16vmin / 7.5); border-left: calc(2vmin / 7.5) dashed #5DA5FF; width: calc(100vw - 50vmin / 7.5 - 80vmin / 7.5); [代码] js 中的处理 某些时候我们可能需要通过 js 计算设置,这个时候可以通过 wx.getSystemInfo() 得到 [代码]windowWidth[代码](可使用窗口宽度,单位px) 和 [代码]windowHeight[代码](可使用窗口高度,单位px) , [代码]100vmin = Math.min(windowWidth, windowHeight)[代码], [代码]1px = Math.min(windowWidth, windowHeight) / 750[代码] ,其中750为布局的时候可视窗口的最小宽度,其他尺寸乘以比例即可得到对应的 px 值或者 rpx 值。
2022-04-12 - 微信小程序可视化电影选座组件
推荐一款可视化电影选座组件,具体使用方法请看原文链接:https://juejin.cn/post/6996913047725932575 [图片] gitee地址:https://gitee.com/jensmith/source-coding 原文地址:https://juejin.cn/post/6996913047725932575
2022-04-12 - 云开发微信支付配置添加商户号后,绑定状态为“待模板消息确认”如何处理?
近期发现不少同学不知道如何确认云开发微信支付的授权,这里写一下解决方法: 云开发微信支付配置商户号方式:在云控制台 -> 设置 -> 全局设置->添加商户号->输入要绑定的商户号 [图片] 添加商户号后要进行商户号绑定确认,在全局设置界面显示待模板消息确认后,这时绑定了微信支付的商户号“超级管理员的微信”会收到一条授权确认的模板消息,点击模板消息会弹出服务商助手小程序,确认授权之后就可以在云开发控制台看到绑定状态为“已绑定”,而 JS API 权限也会显示“已授权”。(此项步骤需要关注过“微信支付商家助手”公众号),如未关注“微信支付商家助手”公众号,可以看下面方法进行手动绑定,退款权限下同。 jsapi 和 api 退款权限授权:登录微信支付商户平台-产品中心-我的授权产品中进行确认授权完成授权后才可以调用微信支付相关接口能力。如果你在你的产品中心看不到我的授权产品,可以点击链接:授权产品 [图片]
2021-01-14 - 云开发日期型字段的比较
不要直接对比字符串,应该将时间字段转换成字符串类型再进行对比。 比如: db.collection("rideRecords") .aggregate() .match({ 'record.subLineId': 'l1', creationDate: _.gte('2022-01-30 00:00:00').and(_.lte('2022-01-30 23:59:59')), 'record.people._id': "381d149061ac0a5a00921a680d1281fe" }) .lookup({ from: "sublineRecords", localField: "_id", foreignField: "rideRecords", as: "sublineRecords" }) .lookup({ from: "driverRecords", localField: "sublineRecords.driverRecordID", foreignField: "_id", as: "driverRecords" }) .end() 上面语句,执行时返回空。 改成: db.collection("rideRecords") .aggregate() .addFields({ formatDate: $.dateToString({ date:'$creationDate', format:'%Y-%m-%d %H:%M:%S', timezone:'Asia/Shanghai' }) }) .match({ 'record.subLineId': 's4', formatDate:_.gte('2022-01-30 00:00:00').and(_.lte('2022-01-30 23:59:59')), }) .end() 关键点:把日期型通过格式转化:dateToString,转成字符类型再做比较 dateToString 相关文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/command/aggregate/AggregateCommand.dateToString.html 参考文档:https://www.jianshu.com/p/8da04042ffdd
2022-01-30 - 小程序云函数和云数据库中的时区必坑笔记
云函数 云函数中默认的时区是UTC +0 参考:注意事项 & FAQ 然而里面有个错误,导致我调试了好久才发现问题 设置云函数时区的两种方式: 在控制台设置: 环境变量 TZ=Asia/Shanghai *注意:TZ大小写敏感,官方文档里写的是错误的! 在代码中设置 process.env.TZ=“Asia/Shanghai” *注意:TZ大小写敏感 云数据库 聚合指令$.dateToString如果不指定时区,默认是UTC +0。所以使用这个指令格式化日期字符串时一定要加上时区属性。 参考:MongoDB参考手册 [代码]$.dateToString({ date: '$closeBookingTime', format: '%Y-%m-%d %H:%M', timezone: 'Asia/Shanghai', }), [代码]
2020-07-16 - 这个库能轻松解决99%的异步和逻辑加载时机问题(异步篇)
[图片] 你是否纠结过底层业务逻辑(登陆、获取用户信息等)到底是放app.js的onLaunch还是page的onLoad里比较好,或者因为异步问题被迫放在了onload,我们来分析一下优劣 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 分析 onLaunch处理 优点:底层业务逻辑集中并且只需写一次,比较好维护 缺点:目前没有一个理想的方案来解决onLaunch和onLoad的异步问题,包括注册回调、重写onLoad、请求拦截等。 onLoad处理 优点:因为不涉及跨页面通知,因此异步逻辑比较好处理 缺点:每个页面都得写一次底层业务逻辑,非常繁琐,而且既然是公用的底层业务逻辑,分散在每个页面的onLoad里,好像也不大对劲。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 抉择 按照高内聚低耦合的原则,那逻辑和数据放onLaunch里肯定的,不应该和普通page逻辑耦合在一起,通用的数据和逻辑应该在入口去处理,执行一次到处使用,就像vue的main.js一样,会注册一些技术层的基础设施(路由、状态管理等插件),那业务层的基础设施不就是token、用户信息、所在位置等逻辑吗? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 想象中的最佳实践 那我们的目标就是如何满足两者的优点,避免两者的缺点,做到真正的“高内聚低耦合” 1.保持底层业务逻辑写在入口app.js,避免耦合page里的逻辑 2.能在任何page里第一时间拿到globalData数据 3.使用方便,做到在业务开发中无感知,不需要写额外的调用、通知等代码 4.无任何副作用,不会影响其他功能,比如重写阻塞onLoad 5.灵活可配,适用以后此类任何业务 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 梦想成真先看一段代码 ⬇️ // page.js export default { name: 'Home', onLoadLogin(){ //登录成功(拿到token) && 页面初始化完成 //Tips:适用于某页面发送的请求依赖token的场景 }, onLoadUser(){ //页面初始化完成 && 获取用户信息完成 //Tips:适用于页面初始化时需要用到用户信息去做判断再走页面逻辑的场景 }, onReadyUser(){ //dom渲染完成 && 获取用户信息完成 //Tips:适用于首次进入页面需要在canvas上渲染头像的类似场景 }, onReadyShow(){ //小程序内页面渲染完成 && 页面显示 //Tips:适用于需要获取小程序组件或者dom,并且每次页面显示都会执行的场景 }, } 应该懂什么意思了吧?是不是你理想中的样子,使用起来跟没有似的 ⬆️ 这段示例代码满足了上面的第2、3、4条目标 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 再来看一段 ⬇️ // app.js // 配置自定义钩子,所有钩子都可以随意组合搭配使用,执行机制类似于Promise.all(但不是用Promise实现的) CustomHook.install({ 'Login':{ // 自定义钩子名称、必须大写字母开头 name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ // 自定义钩子名称 name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } //依赖globalData中数据 }, globalData) 怎么样,是不是很棒,依赖globalData,名字可配,连触发规则都可配,而且还附加了可随意组合的功能(意外还解决了页面内逻辑执行时机问题,在下篇讲) ⬆️ 这段示例代码满足了上面的第1、5条目标。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 是不是跃跃欲试了,那就赶紧试试,好用回来告诉我! ⬇️(公司内部已接入两年了很稳定) GitHub:https://github.com/1977474741/spa-custom-hooks [图片]
2023-07-07 - 节流和防抖
小程序防抖的使用[图片] 2:加入以下代码: /*函数节流*/ function throttle(fn, interval) { var enterTime = 0;//触发的时间 var gapTime = interval || 300 ;//间隔时间,如果interval不传,则默认300ms return function() { var context = this; var backTime = new Date();//第一次函数return即触发的时间 if (backTime - enterTime > gapTime) { fn.call(context,arguments); enterTime = backTime;//赋值给第一次触发的时间,这样就保存了第二次触发的时间 } }; } /*函数防抖*/ function debounce(fn, interval) { var timer; var gapTime = interval || 1000;//间隔时间,如果interval不传,则默认1000ms return function() { clearTimeout(timer); var context = this; var args = arguments;//保存此处的arguments,因为setTimeout是全局的,arguments不是防抖函数需要的。 timer = setTimeout(function() { fn.call(context,args); }, gapTime); }; } export default { throttle, debounce }; 3:js层面进行调用,要切记e[0],否者会一直报错 import tool from "../../utils/tool.js"; formSubmit:tool.debounce(function(e){ // 获取商品名称 var title=e[0].detail.value.title; // 商品价格 var price=e[0].detail.value.price; // 商品类型 var type=e[0].detail.value.type; // 商品属性 var info=e[0].detail.value.info; let that=this var priceTF = /^\d+(\.\d{1,2})?$/ // 验证非空 if (e[0].detail.value.title === "") { wx.showToast({ title: '请输入商品名称', icon: "none", duration: 1000, mask: true, }) } else if (e[0].detail.value.title.length > 60) { wx.showToast({ title: '商品名称不得大于60字', icon: "none", duration: 1000, mask: true, }) } else if (e[0].detail.value.title.length === "") { wx.showToast({ title: '请输入商品价格', icon: "none", duration: 1000, mask: true, }) } else if (!priceTF.test(e[0].detail.value.price)) { wx.showToast({ title: '商品价格精确到两位', icon: "none", duration: 1000, mask: true, }) } else if (e[0].detail.value.info === "") { wx.showToast({ title: '请输入商品信息', icon: "none", duration: 1000, mask: true, }) } else if (e[0].detail.value.point === "") { wx.showToast({ title: '请输入商品卖点', icon: "none", duration: 1000, mask: true, }) } else if (that.data.typeInd === -1) { wx.showToast({ title: '请选择商品类型', icon: "none", duration: 1000, mask: true, }) } else if (that.data.detail.length === 0) { wx.showToast({ title: '请选择图片', icon: "none", duration: 1000, mask: true, }) } // 发送Ajax请求,进行入库 wx.request({ url: 'http://www.yan.com/api/xcx/getData', data: { title:title, price :price, type:type, info:info, }, header: { 'content-type': 'application/json' // 默认值 }, method:'POST', success (res) { // 提示发布成功 if(res.data.code==200){ wx.showToast({ title: res.data.meg, }) wx.switchTab({ url: '/pages/good_index/good_index', }) } } }) }),
2022-03-15 - 分包异步化 分包难题不用怕
在小程序开发过程中,你是否对分包问题感到困扰? 多业务的分包难以划分 分包体积膨胀 下载并注入无用代码 插件无法实现分包处理 …… 为解决上述问题,微信团队提供【分包异步化】新能力,实现跨分包组件、跨分包方法,成功解决分包难、分包不合理等问题。 • • 分包异步化原理 • • 原有的分包隔离机制导致各分包之间无法引用自定义组件或逻辑代码,因此导致分包难等一系列问题。分包异步化能力打通不同分包的引用关系,解决小程序代码包合理化的问题,支持跨分包组件、跨分包方法。 [图片] • • 跨分包组件 • • 当使用其他分包组件时,代码包需要增加占位组件 (component placeholder),实现页面高效配置。例如页面展示时,分包 (subpackageB) 仍未下载,进行以下操作实现跨分包组件: 1. 使用组件 <simple-list> 代替 <list>,使用 <view> 代替 <card>,完成页面渲染 2. 完成渲染后,开始下载和注入分包 3. 完成分包下载和注入后,将占位组件替换成真正的组件 // subPackageA/pages/index.json { "usingComponents": { "button": "../../commonPackage/components/button", "list": "../../subPackageB/components/full-list", "simple-list": "../components/simple-list" }, "componentPlaceholder": { "button": "view", "list": "simple-list" } } • • 跨分包方法 • • 在小程序开发过程中,通过require回调函数或requireAsync异步调用2种方法,分包异步化能够引用其他分包的逻辑代码。具体操作如下: // subPackageA/index.js // 使用回调函数风格的调用 require('../subPackageB/utils.js', utils => { console.log(utils.whoami) // Wechat MiniProgram }) // 或者使用 Promise 风格的调用 require.async('../commonPackage/index.js').then(pkg => { pkg.getPackageName() // 'common' }) • • 兼容性要求 • • 分包异步化能力要求基础库版本 2.17.3 及以上(正式发布需在 mp 设置最低版本基础库 2.17.3)。平台能力兼容安卓微信、iOS 微信、1.05.2104272 及以上版本的微信开发者工具。更低版本的基础库兼容工作预计在一个月后完成。 • • 总结 • • 实现分包异步化能力后,主包的「公有」性质被削弱,「前置」性质显得更重要(优先于所有分包注入运行且默认注入运行)。开发者可以根据自身业务诉求,结合分包异步化,进行小程序调优,实现更快的启动速度、按需下载和注入代码包、合理处理公有组件等效果。
2021-09-16 - 小程序自定义导航栏完整适配方案
写这篇博客的背景 临近节日,产品想给小程序首页头部设置图片背景,这个只能自定义导航栏来实现 [图片] 当然除了自定义背景图,还可以放置其他组件,按钮、搜索框等 实践部分设备状态栏、胶囊、间距的高度(仅供参考)(单位px) 状态栏 胶囊 上下间距 整个导航栏高度 iPhone 5 20 32 4 60 iPhone 6/7/8 20 32 4 60 iPhone 6/7/8 Plus 20 32 4 60 iPhone X/XR/XS 44 32 4 84 小米6 (非刘海) 24 29 7 67 华为 nova 7 Pro(刘海) 42 29 7 85 后续遇到其他设备再补充 @[toc] 步骤 1.隐藏小程序自带的导航栏 小程序配置 [代码]// 1.全局配置 // app.json { ... "navigationStyle": "custom" ... } // 2.页面配置 // pages.json { ... "navigationStyle": "custom" ... } [代码] 每一个小程序页面也可以使用 .json 文件来对本页面的窗口表现进行配置。页面中配置项在当前页面会覆盖 app.json 的 window 中相同的配置项。 [图片] 2.编写自定义导航栏 导航栏的组成部分(主要是状态栏和标题栏) 1.状态栏(时间和电量显示那一栏) + 2.状态栏和标题栏之间的间距 + 3.标题栏(小程序胶囊按钮那一栏) + 4.标题栏和正文区域之间的间距 刘海屏 [图片] 非刘海屏 [图片] 计算各部分的高度 获取系统信息api获取状态栏高度 [代码]// 状态栏高度 wx.getSystemInfoSync().statusBarHeight; [代码] 获取胶囊按钮信息api获取胶囊按钮的宽高和位置 [代码]/** * 获取微信小程序菜单栏(胶囊)信息 * 菜单按键宽度:width * 菜单按键高度:height * 菜单按键上边界坐标:top * 菜单按键右边界坐标:right * 菜单按键下边界坐标:bottom * 菜单按键左边界坐标:left */ wx.getMenuButtonBoundingClientRect(); > 重点: 此api返回的是胶囊按钮在页面中的的上下左右坐标的绝对位置 > 注意:在模拟器使用时记得把视图百分比调为100%,否则可能会导致获取数据不准确 [代码] 因为整个小程序的导航栏高度是不变的,我们可以把高度信息放在全局,方便使用。一般会在小程序的app.js(如果使用的uni-app,就是App.vue) 的 onLaunch生命周期进行获取和计算。 [代码]//app.js App({ onLaunch() { this.calcNavBarInfo() }, globalData: { //全局数据管理 navBarHeight: 0, // 导航栏高度 menuBottom: 0, // 胶囊距底部间距(顶部间距也是这个) menuHeight: 0, // 胶囊高度 }, /** * @description 计算导航栏信息 */ calcNavBarInfo () { // 获取系统信息 const systemInfo = wx.getSystemInfoSync(); // 胶囊按钮位置信息 const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); // 导航栏高度 = 状态栏到胶囊的间距(胶囊上坐标位置-状态栏高度) * 2 + 胶囊高度 + 状态栏高度 this.globalData.navBarHeight = (menuButtonInfo.top - systemInfo.statusBarHeight) * 2 + menuButtonInfo.height + systemInfo.statusBarHeight; // 状态栏和菜单按钮(标题栏)之间的间距 // 等同于菜单按钮(标题栏)到正文之间的间距(胶囊上坐标位置-状态栏高度) this.globalData.menuBottom = menuButtonInfo.top - systemInfo.statusBarHeight; // 菜单按钮栏(标题栏)的高度 this.globalData.menuHeight = menuButtonInfo.height; } }) [代码] 到此各个部分的元素高度都已拿到, 而且是根据不同设备的屏幕信息动态设置,无论是刘海屏还是非刘海屏,安卓还是ios,样式皆可统一。 3.如何使用 [代码] <view class="nav" style="height:{{navBarHeight}}px; background: url();"> <!-- 胶囊区域 --> <view class="capsule-box" style="height:{{menuHeight}}px; min-height:{{menuHeight}}px; line-height:{{menuHeight}}px; bottom:{{menuBottom}}px;"> <view class="nav-handle"> <view class="back"> <!-- 返回按钮 --> <image src=""></image> </view> <view class="home"> <!-- 首页按钮 --> <image src=""></image> </view> </view> <view class="nav-title">导航标题</view> </view> </view> // js Page({ data: { navBarHeight: getApp().globalData.navBarHeight, menuBottom: getApp().globalData.menuBotton, menuHeight: getApp().globalData.menuHeight } }) // style // 导航栏 .nav { position: relative; } // 胶囊栏 .capsule-box { position: absolute; display: flex; align-items: center; } // 标题文字 .nav-title { height: 100%; width: 50%; margin: 0 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } [代码] 最近使用wx.getMenuButtonBoundingClientRect(),在ios端偶尔值全都是0,导致无法正确自定义导航栏高度 想了一下,ios端的所有设备胶囊按钮信息都是一样的,android端各个品牌都或多或少的有差距,根据这个规律作如下改造: 我这边后期用的uni-app ,所以api都换成了uni [代码]uni.getSystemInfo({ success: res => { let menuButtonInfo = {} if (res.platform === 'ios') { // ios设备的胶囊按钮都是固定的 menuButtonInfo = { width: 87, height: 32, left: res.screenWidth - 7 - 87, right: res.screenWidth - 7, top: res.statusBarHeight + 4, bottom: res.statusBarHeight + 4 + 32 } } else { // 安卓通过api获取 menuButtonInfo = uni.getMenuButtonBoundingClientRect() } console.log('获取胶囊信息:', menuButtonInfo); // 导航栏高度 = 状态栏到胶囊的间距(胶囊距上未知-状态栏高度)* 2 + 胶囊高度 + 状态栏高度 this.$options.globalData.navHeight = (menuButtonInfo.top - res.statusBarHeight) * 2 + menuButtonInfo.height + res.statusBarHeight; console.log('navHeight:', this.$options.globalData.navHeight); // 按钮上下边距高度 this.$options.globalData.menuBottom = menuButtonInfo.top - res.statusBarHeight; // 导航栏右边到屏幕边缘的距离 this.$options.globalData.menuRight = res.screenWidth - menuButtonInfo.right; // 导航栏高度 this.$options.globalData.menuHeight = menuButtonInfo.height; }, fail(err) {} }) [代码]
2022-03-04 - 自定义导航栏高度精确适配方案
之前自己做小程序,凡是自定义导航栏的地方,我都是 状态栏高度用 getSystemInfo 中的 statusBarHeight导航栏高度固定 44 px 不过最近发现在几款安卓较新的机型上,状态栏高度都OK,但导航栏的高度要么大了要么小了。于是我又重新研究了一下,尝试归纳目前(截至发文 2021-06-08)小程序的导航栏高度的规律,总结如下。 小程序自定义导航栏高度精确适配方案 iOS 端 状态栏高度用 getSystemInfo 中的 statusBarHeight导航栏高度固定 44 px导航栏高度 44 px 的表现效果,与默认导航栏("navigationStyle": "default")一致,但这种情况下胶囊不是上下居中的,各位需根据具体场景来自行选择方案。如果想要胶囊精确地上下居中,可使用下方 Android 端的方案Android 端 状态栏高度用 getSystemInfo 中的 statusBarHeight导航栏高度根据胶囊位置与高度计算,计算方式如下let sysInfo = wx.getSystemInfoSync(); let menuInfo = wx.getMenuButtonBoundingClientRect(); let navigationBarHeight = (menuInfo.top - sysInfo.statusBarHeight) * 2 + menuInfo.height; 按照胶囊位置计算出的导航栏高度,与默认导航栏("navigationStyle": "default")一致,且这种情况下胶囊是上下居中的 以上的方案经过多款 iOS 设备与 Android 设备的测试验证,但我们能用于测试的设备有限,得出的结论并不 100% 适用于所有设备。若有补充欢迎在评论区发言。
2021-06-08 - 小程序消息推送,订阅消息的实现,借助云开发云函数实现定时推送订阅消息功能
我在云开发基础课程里给大家讲过小程序消息推送功能的实现,等下会给大家回顾下。但是有时候我们如果想实现定时推送的功能该怎么做呢 一,普通订阅消息的发送 我们先来看下订阅消息的官方简介。 [图片] 接下来我们就来借助云开发,来快速实现小程序消息推送的功能。 1-1,获取模板 ID 这一步和我们之前的模板消息推送是一样的,也是先添加模板,然后拿到模板id [图片] 首先是开通订阅消息功能,很简单,如下图 [图片] [图片] 由于长期性订阅消息,目前仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。仅就线下公共服务这一点,长期性订阅消息就和大部分开发者无缘了。 所以我们这里只能以使用一次性订阅消息为例。 [图片] 如上图,我们从公共模板库里选择一个一次性订阅的模板。然后编辑模板如下图 [图片] 下图就是我们添加好的模板,下图的模板id就是我们需要的。 [图片] 1-2,请求用户授权 我们做订阅消息授权时,只能是用户点击或者支付完成后才可以调起来授权弹窗,官方是这么要求的: [图片] 我们这里用到了wx.requestSubscribeMessage这个方法,来获取用户的授权。 1,编写index.wxml代码 [图片] 2,编写index.js代码,实现点击获取授权 [图片] 这一步tmplIds里的一串字符,就是我们自己添加的模板id [图片] 3,点击按钮运行效果如下 开发者工具模拟器上点击授权弹窗是这样的: [图片] 手机上的授权弹窗是这样的: [图片] 可以看到,这里显示的就是我们添加的 ‘上课提醒’的模板。 细心的同学可以看到, 真机上多了一个 ‘总是保持以上选择,不再询问’ 其实,你自己仔细多品一些。也能明白,我们正常订阅消息授权时,用户允许的话,你只能推送一次消息。也就是用户允许一次,我们就可以推送一条消息给用户,并且这个允许不存在过期。所以我们可以让用户尽量多的点击允许,这样我们就可以尽量多的给用户发送消息了。 这里用户允许后,我们就可以给用户推送消息了,接下来我们来借助云开发的云函数来实现消息推送功能。 1-3,获取用户的opneid 先来看官方爸爸是怎么说的。 [图片] 可以看出官方提供了两种方式,我们这里使用云调用。说白了就是在云函数里调用推送功能。 推送所需参数 [图片] 可以看到我这里用来openapi功能,并且需要用到用户的opneid,关于openid的获取,我之前有写过文章,也录过视频的。文章的话,大家去翻下我历史的文章,视频的话,点击这个即可:《借助云函数获取用户openid》 这里的openid的获取我就不再详细讲解了,把对应云函数的代码给大家贴出来。 [图片] 在使用云开发时,有几点需要注意的 1,需要在project.config.json里创建云函数目录如下图 [图片] 2,需要在app.js里初始化云开发环境 [图片] 至于云开发的环境id从哪里拿,我视频里也讲过很多遍了,直接去看我视频或者翻看我历史文章即可。 《零基础入门云开发视频》 1-4,用云函数实现消息推送 我们只需要创建一个云函数如下,然后填入用户的openid,要跳转的小程序页面链接,模板内容,模板id即可。通常这些数据都应该传进来,简单起见,我就把这里的模板内容写成固定的。 [图片] 注意:我在编写上面的代码时,推送内容的key必须和小程序模板里的key保持一致,否则就会报如下错误。 [图片] 然后看下调用这个云函数的地方 [图片] 如果用户没有授权,我们推送会报如下错误 [图片] 如果用户授权过,我们就可以成功推送了,推送后的打印日志如下 [图片] 还记得我们真机上的授权吗,如果用户只是点击了允许,没有选择一直允许,那我我们在推送成功一次后,如果再次推送,就需要用户重新授权。否则,还是会报这个错误的 [图片] 所以我们用户点击一次允许,我们就可以推送一次消息,比如,我点击了4次允许那么我就可以成功的推送4次 [图片] 效果图 [图片] 可以看到,我们成功的收到 上课提醒的模板消息,点击进去,就是我们具体的推送内容 [图片] 其实我这是连续收到了4条消息,因为我点击了4次允许推送,所以就可以成功的推送4次。 到这里我们就完整的实现模板消息推送功能了,下面我把主要代码贴给大家,大家也可以私信我获取完整源码。 index.wxml [代码]<button bindtap="shouquan" type='primary'>获取订阅消息授权</button> <button bindtap="getOpenid">获取用户的openid并推送消息</button> [代码] index.js [代码]//编程小石头wechat:2501902696 Page({ //获取授权的点击事件 shouquan() { wx.requestSubscribeMessage({ tmplIds: ['CFeSWarQLMPyPjwmiy6AV4eB-IZcipu48V8bFLkBzTU'], //这里填入我们生成的模板id success(res) { console.log('授权成功', res) }, fail(res) { console.log('授权失败', res) } }) }, //获取用户的openid getOpenid() { wx.cloud.callFunction({ name: "getopenid" }).then(res => { let openid = res.result.openid console.log("获取openid成功", openid) this.send(openid) }).catch(res => { console.log("获取openid失败", res) }) }, //发送模板消息到指定用户,推送之前要先获取用户的openid send(openid) { wx.cloud.callFunction({ name: "sendMsg", data: { openid: openid } }).then(res => { console.log("推送消息成功", res) }).catch(res => { console.log("推送消息失败", res) }) } }) [代码] 推送对应的云函数 [代码]//编程小石头wechat:2501902696 const cloud = require('wx-server-sdk') cloud.init() exports.main = async(event, context) => { try { const result = await cloud.openapi.subscribeMessage.send({ touser: event.openid, //要推送给那个用户 page: 'pages/index/index', //要跳转到那个小程序页面 data: {//推送的内容 thing1: { value: '小程序入门课程' }, thing6: { value: '杭州浙江大学' }, thing7: { value: '第一章第一节' } }, templateId: 'CFeSWarQLMPyPjwmiy6AV4eB-IZcipu48V8bFLkBzTU' //模板id }) console.log(result) return result } catch (err) { console.log(err) return err } } [代码] 后面我会分享更多小程序相关的知识出来,请持续关注。 注意:授权一次,只能发送一条消息。 二,定时发送消息 我们上面用户授权和发送消息都需要手动点击才可以实现发送。但是有时候我们需要定时提醒用户,比如做的闹钟小程序,要定时提醒用户,该怎么做呢,接下来我们就来实现定时发送消息的功能。 注意 当然了这里还是要先授权才可以发送消息的,同样也是授权一次可以发送一条消息,所以这里要尽量先多授权几次 2-1,什么是定时触发器 我们实现定时发送的功能就是要用到云函数里的定时触发器,官方介绍如下。 [图片] 大家有时间可以自己去仔细读下 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/triggers.html [图片] 官方已经教我们怎么写定时触发器了 2-2,定时触发器时间设置规则 建议大家仔细去读下官方文档。 [图片] 下面是官方给出的一些示例 [图片] 我这里就取用每隔5秒通过该定时触发器调用下我们的云函数,实现订阅消息的发送。 2-3,添加定时触发器 添加步骤如下图,我们需要新建一个云函数timer [图片] 我们要在timer云函数里调用我们的fasong云函数来实现发送功能 [图片] 然后在timer文件夹下新建一个config.json文件 [图片] [图片] 然后给config.json做如下配置 [图片] 注意json里不能有注释,配置好的触发器如下 [图片] 2-4,部署定时触发器 添加好以后,记得部署触发器 [图片] 2-5,定时发送效果 首先看定时触发器是不是每隔5秒执行了一次 [图片] 然后看手机是否接到了消息 [图片] 可以看出我们手机上每隔5秒也接到了消息。这里还是要记得多授权才可以多接消息。 当然了,我们不可能这样每隔5秒给客户发条消息,这样骚扰到客户,很容易被封的,所以可以停止触发器 2-6,停止触发器 [图片] 到这里我们的定时发送消息功能也实现了,当然了我们要发给指定用户,就要先去获取用户openid,并且得让用户多授权。
2022-02-28 - 关于云开发的反爬、反抓、反刷、反反翻译的一些思考
最近接了个项目,甲方对于反爬、反抓包、反刷流量的要求令人发指,对我有限的代码保安知识真是一个巨大的挑战; 以下是一些经验总结: 一、关于反刷 本项目有大量的图片,放在云存储里,甲方怕有黑方用户不停地刷流量,造成套餐爆掉,所以特别要求,实际情况也确实发生了,仅不到100用户的时候,存储读取次数,一天就有10多万次,免费套餐被爆。采取措施如下: 1、关闭的有官方的页面收藏sitemap { "desc": "", "rules": [{ "action": "disallow", "page": "*" }] } 2、限制用户访问次数,比如限时超过1000次打开详情页就禁止,结果该方案被黑方破解数次,前后经历了多次方案: 将次数限制写在缓存里(删除小程序清缓存被破解); 将限时按手机时间来获取(修改手机时间被破解); 限时按服务器时间,次数写在云表里,才算解决; 二、关于反爬反抓 甲方有一些保密数据,由一些保密参数组成,通过运算得到一个价格再公布出去,一开始以为是云开发,读数据库和云函数都是保密的,结果惨遭泄密。 方案一:通过aggregate聚合,计算参数聚合出最终价格,悲伤的是,集合里的数字保存的字符串,不支持聚合的计算操作,云开发还没有任何办法批量将字符串形式的数字转成number,除非删库重建,一开始就保存成number。惨痛的教训,今后慎重,该是number的就number,别看前端可以随意,数据库里必须区分; 方案二:写一个页面,管理员输入参数,然后生成价格,应用到每一个doc里,结果数据量太大,批量修改集合所有数据的某字段值,目前用云开发完全没法实现。 最终方案:通过云函数来获取doc列表,在云函数里读取参数表,计算后生成价格,应用到每一条列表,然后再返回给小程序端; 结论:云函数是运行在微信服务器上的,还是安全的。 关于图片反爬反抓,结论:无解; 三、关于反翻译 官方的事,我们毫无办法。 设置里,什么混淆、什么代码保护等都勾上了,还是挡不住; 更多内容: [图片]
2020-10-20 - 小程序云函数能被抓包吗?
小程序一些请求用到的了rsa加密 ,现在私钥是写在代码中,然后发现还是会泄露, 如果说吧私钥写在云函数中,通过云函数前端获取私钥然后加密请求接口,这么做是否还有被抓包的可能 云函数返回的结果能被抓包吗?
2022-03-01 - 云开发分账功能,提示"没有分账权限"
调用云开发分账功能,提示"没有分账权限",如何才能获得云开发分账权限?商户号 160648900
2022-03-01 - 解决微信小程序input组件不能隐藏问题
在开发过程 中发现,input作为原生组件无法隐藏,用hidden={{!show}} 替换wx:if={{show}}即可 <view hidden="{{!changeQ}}" catchtouchmove="stopmove"> <view class='toast-box'> <view class='toastbg'></view> <view class='showToast'> <view class='toast-title'> <text>修改第{{quest0.id+1}}个问题</text> <text>{{quest0.type==1?"单选":"多选"}}</text> <!-- <checkbox-group bindchange="checkboxChange"> <checkbox value="2">多选</checkbox> </checkbox-group> --> </view> <view class='toast-main'> <view class="inputbox"> <text class="input-title">问题:</text> <input class='toast-input' focus="false" bindinput='getUserInput' value="{{changeQ?quest0.question:' '}}" data-quest="quest" auto-focus="false"></input> </view>
2022-02-26 - 微信小程序开发中获取用户进入小程序的场景
// 微信场景值 scene.js 文件 var scene={ "1000":"其他", "1001":"发现栏小程序主入口,「最近使用」列表,「我的小程序」列表", "1005":"微信首页顶部搜索框的搜索结果页", "1006":"发现栏小程序主入口搜索框的搜索结果页", "1007":"单人聊天会话中的小程序消息卡片", "1008":"群聊会话中的小程序消息卡片", "1010":"收藏夹", "1011":"扫描二维码", "1012":"长按图片识别二维码", "1013":"扫描手机相册中选取的二维码", "1014":"小程序订阅消息", "1017":"前往小程序体验版的入口页", "1019":"微信钱包(支付入口)", "1020":"公众号 profile 页相关小程序列表", "1022":"聊天顶部置顶小程序入口", "1023":"安卓系统桌面图标", "1024":"小程序 profile 页", "1025":"扫描一维码", "1026":"发现栏小程序主入口,「附近的小程序」列表", "1027":"微信首页顶部搜索框搜索结果页「使用过的小程序」列表", "1028":"我的卡包", "1029":"小程序中的卡券详情页", "1030":"自动化测试下打开小程序", "1031":"长按图片识别一维码", "1032":"扫描手机相册中选取的一维码", "1034":"微信支付完成页", "1035":"公众号自定义菜单", "1036":"App 分享消息卡片", "1037":"小程序打开小程序", "1038":"从另一个小程序返回", "1039":"摇电视", "1042":"添加好友搜索框的搜索结果页", "1043":"公众号模板消息", "1044":"带 shareTicket 的小程序消息卡片", "1045":"朋友圈广告", "1046":"朋友圈广告详情页", "1047":"扫描小程序码", "1048":"长按图片识别小程序码", "1049":"扫描手机相册中选取的小程序码", "1052":"卡券的适用门店列表", "1053":"搜一搜的结果页", "1054":"顶部搜索框小程序快捷入口", "1056":"聊天顶部音乐播放器右上角菜单", "1057":"钱包中的银行卡详情页", "1058":"公众号文章", "1059":"体验版小程序绑定邀请页", "1060":"微信支付完成页", "1064":"微信首页连Wi-Fi状态栏", "1065":"URL scheme", "1067":"公众号文章广告", "1068":"附近小程序列表广告", "1069":"移动应用通过openSDK进入微信,打开小程序", "1071":"钱包中的银行卡列表页", "1072":"二维码收款页面", "1073":"客服消息列表下发的小程序消息卡片", "1074":"公众号会话下发的小程序消息卡片", "1077":"摇周边", "1078":"微信连Wi-Fi成功提示页", "1079":"微信游戏中心", "1081":"客服消息下发的文字链", "1082":"公众号会话下发的文字链", "1084":"朋友圈广告原生页", "1088":"会话中查看系统消息,打开小程序", "1089":"微信聊天主界面下拉,「最近使用」栏,「我的小程序」栏", "1090":"长按小程序右上角菜单唤出最近使用历史", "1091":"公众号文章商品卡片", "1092":"城市服务入口", "1095":"小程序广告组件", "1096":"聊天记录,打开小程序", "1097":"微信支付签约原生页,打开小程序", "1099":"页面内嵌插件", "1100":"红包封面详情页打开小程序", "1101":"远程调试热更新(开发者工具中,预览 -> 自动预览 -> 编译并预览)", "1102":"公众号 profile 页服务预览", "1103":"发现栏小程序主入口,「我的小程序」列表", "1104":"微信聊天主界面下拉,「我的小程序」栏", "1106":"聊天主界面下拉,从顶部搜索结果页,打开小程序", "1107":"订阅消息,打开小程序", "1113":"安卓手机负一屏,打开小程序(三星)", "1114":"安卓手机侧边栏,打开小程序(三星)", "1119":"【企业微信】工作台内打开小程序", "1120":"【企业微信】个人资料页内打开小程序", "1121":"【企业微信】聊天加号附件框内打开小程序", "1124":"扫“一物一码”打开小程序", "1125":"长按图片识别“一物一码”", "1126":"扫描手机相册中选取的“一物一码”", "1129":"微信爬虫访问", "1131":"浮窗", "1133":"硬件设备打开小程序", "1135":"小程序profile页相关小程序列表,打开小程序", "1144":"公众号文章 - 视频贴片", "1145":"发现栏 - 发现小程序", "1146":"地理位置信息打开出行类小程序", "1148":"卡包-交通卡,打开小程序", "1150":"扫一扫商品条码结果页打开小程序", "1151":"发现栏 - 我的订单", "1152":"订阅号视频打开小程序", "1153":"“识物”结果页打开小程序", "1154":"朋友圈内打开“单页模式”", "1155":"“单页模式”打开小程序", "1157":"服务号会话页打开小程序", "1158":"群工具打开小程序", "1160":"群待办", "1167":"H5 通过开放标签打开小程序", "1168":"移动应用直接运行小程序", "1169":"发现栏小程序主入口,各个生活服务入口(例如快递服务、出行服务等)", "1171":"微信运动记录(仅安卓)", "1173":"聊天素材用小程序打开", "1175":"视频号主页商店入口", "1176":"视频号直播间主播打开小程序", "1177":"视频号直播商品", "1178":"在电脑打开手机上打开的小程序", "1179":"#话题页打开小程序", "1181":"网站应用打开PC小程序", "1183":"PC微信 - 小程序面板 - 发现小程序 - 搜索", "1185":"群公告", "1186":"收藏 - 笔记", "1187":"浮窗", "1189":"表情雨广告", "1191":"视频号活动", "1192":"企业微信联系人profile页", "1194":"URL Link", "1195":"视频号主页商品tab", "1197":"视频号主播从直播间返回小游戏", "1198":"视频号开播界面打开小游戏", "1203":"微信小程序压测工具的请求" } module.exports={ scene } 然后写一个函数去返回场景值的描述,放到app.js中 // 判断小程序入口 pdScene(val){ var wxscene=require('scene.js').scene; var str=""; for(var i in wxscene){ if(i==val){ console.log(wxscene[i]) str=wxscene[i]; } } return str; }, 在需要存储用户进入场景值的地方写 console.log(app.pdScene(wx.getLaunchOptionsSync().scene)); 微信官方场景值文档https://developers.weixin.qq.com/miniprogram/dev/reference/scene-list.html
2022-02-22 - 目前为止项目中用到的一些校验
// 判断是否是json字符串 isJSON(str) { if (typeof str == 'string') { try { var obj=JSON.parse(str); if(typeof obj == 'object' && obj ){ return true; }else{ return false; } } catch(e) { return false; } } }, //验证手机号 checkPhone(phone){ var res = /^1[3456789]\d{9}$/; return res.test(phone);//返回true:手机号正确 false:手机号错误 }, // 国内座机 checkTel(phone){ var res = /\d{3}-\d{8}|\d{4}-\d{7}/; return res.test(phone);//返回true:正确 false:错误 }, // 验证邮箱(支持中文邮箱) checkEmail(email) { var res =/^[A-Za-z0-9\u4e00-\u9fa5]+([-_.][A-Za-z\d]+)*@([A-Za-z\d]+[-.])+[A-Za-z\d]{2,5}$/; return res.test(email);//返回true:邮箱正确 false:邮箱错误 }, // 验证中国大陆身份证号 checkIdcard(idcard) { var res = /(^\d{8}(0\d|10|11|12)([0-2]\d|30|31)\d{3}$)|(^\d{6}(18|19|20)\d{2}(0\d|10|11|12)([0-2]\d|30|31)\d{3}(\d|X|x)$)/; return res.test(idcard);//返回true:正确 false:错误 }, // 验证中国大陆一代身份证号 checkIdcard1(idcard){ var res =/^\d{8}(0\d|10|11|12)([0-2]\d|30|31)\d{3}$/; return res.test(idcard);//返回true:正确 false:错误 }, // 验证中国大陆二代身份证号 checkIdcard2(idcard){ var res = /^\d{6}(18|19|20)\d{2}(0\d|10|11|12)([0-2]\d|30|31)\d{3}(\d|X|x)$/; return res.test(idcard);//返回true:身份证正确 false:身份证错误 }, // 验证统一社会信用代码和组织机构代码 CheckSocialCreditCode(code){ var res = /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/; return res.test(code);//返回true:正确 false:错误 }, // 验证是否是图片链接 isImageUrl(str){ var res=/^https?:\/\/.*?(?:gif|png|jpg|jpeg|webp|svg|psd|bmp|tif)$/i; return res.test(str);//返回true:正确 false:错误 }, // 验证是否是视频链接 isVideoUrl(str){ var res=/^https?:\/\/.*?(?:swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|mp4)$/i; return res.test(str);//返回true:正确 false:错误 }, // 是否是base64 isBase64(str){ var res=/^\s*data:(?:[a-z]+\/[a-z0-9-+.]+(?:;[a-z-]+=[a-z0-9-]+)?)?(?:;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*?)\s*$/i; return res.test(str);//返回true:正确 false:错误 }, // 验证银行卡号 isBankCard(str){ var res=/^[1-9]\d{9,29}$/; return res.test(str);//返回true:正确 false:错误 }, // 中文姓名 isChineseName(str){ var res=/^(?:[\u4e00-\u9fa5·]{2,16})$/; return res.test(str);//返回true:正确 false:错误 }, // 英文姓名 isEnglishName(str){ var res=/(^[a-zA-Z]{1}[a-zA-Z\s]{0,20}[a-zA-Z]{1}$)/; return res.test(str);//返回true:正确 false:错误 }, // 是否是新能源车牌号 isNewCarCard(str){ var res=/[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-HJ-NP-Z]{1}(([0-9]{5}[DF])|([DF][A-HJ-NP-Z0-9][0-9]{4}))$/; return res.test(str);//返回true:正确 false:错误 }, // 非新能源车牌号 isOldCarCard(str){ var res=/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-HJ-NP-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/; return res.test(str);//返回true:正确 false:错误 }, // 车牌号(新能源+非新能源) isCarCard(str){ var res=/^(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-HJ-NP-Z]{1}(?:(?:[0-9]{5}[DF])|(?:[DF](?:[A-HJ-NP-Z0-9])[0-9]{4})))|(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9 挂学警港澳]{1})$/; return res.test(str);//返回true:正确 false:错误 }, // 护照 (包含香港、澳门) checkPassport(str){ var res=/(^[EeKkGgDdSsPpHh]\d{8}$)|(^(([Ee][a-fA-F])|([DdSsPp][Ee])|([Kk][Jj])|([Mm][Aa])|(1[45]))\d{7}$)/; return res.test(str);//返回true:正确 false:错误 }, // 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线组合) checkAccount(str){ var res=/^[a-zA-Z][a-zA-Z0-9_]{4,15}$/; return res.test(str);//返回true:正确 false:错误 }, // 纯中文汉字 checkChineseWords(str){ var res=/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/; return res.test(str);//返回true:正确 false:错误 }, // 纯英文字母 checkEnglistWords(str){ var res=/^[a-zA-Z]+$/; return res.test(str);//返回true:正确 false:错误 }, // 是否是小数 isDecimal(str){ var res=/^\d+\.\d+$/; return res.test(str);//返回true:正确 false:错误 }, // 纯数字 isNumber(str){ var res=/^\d{1,}$/; return res.test(str);//返回true:正确 false:错误 }, // qq号 isQQNumber(str){ var res=/^[1-9][0-9]{4,10}$/; return res.test(str);//返回true:正确 false:错误 }, // 微信号 6至20位,以字母开头,字母,数字,减号,下划线 checkWxCode(str){ var res=/^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/; return res.test(str);//返回true:正确 false:错误 }, // 中国邮政编码 checkChinaPostalCode(str){ var res=/^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\d{4}$/; return res.test(str);//返回true:正确 false:错误 }, // 判断字符串中是否含有表情 isEmojiCharacter(substring) { // if(isEmojiCharacter(str)){console.log('不能含有表情')} for (var i = 0; i < substring.length; i++) { var hs = substring.charCodeAt(i); if (0xd800 <= hs && hs <= 0xdbff) { if (substring.length > 1) { var ls = substring.charCodeAt(i + 1); var uc = ((hs - 0xd800) * 0x400) + (ls - 0xdc00) + 0x10000; if (0x1d000 <= uc && uc <= 0x1f77f) { return true; } } } else if (substring.length > 1) { var ls = substring.charCodeAt(i + 1); if (ls == 0x20e3) { return true; } } else { if (0x2100 <= hs && hs <= 0x27ff) { return true; } else if (0x2B05 <= hs && hs <= 0x2b07) { return true; } else if (0x2934 <= hs && hs <= 0x2935) { return true; } else if (0x3297 <= hs && hs <= 0x3299) { return true; } else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030 || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b || hs == 0x2b50) { return true; } } } }, // 判断字符串中是否含有特殊字符 isTeShuString(substring){ var reg = /[~#^$@%&!?%*]/gi; return reg.test(substring); }, 不足之处忘大家指正,非常感谢
2022-02-22 - 微信小程序获取某一个用户授权的封装写法
参考文章 --https://developers.weixin.qq.com/community/develop/article/doc/00002c39258858bc052d066905f413 个人基于上一篇文章的逻辑和踩坑总结了现版本的获取某一个用户授权的封装写法 这是我发现的坑。。。 [图片] 用async和await 调用wx.showModal 在调用wx.openSetting无法跳转权限设置页面 提示 errMsg: "openSetting:fail can only be invoked by user TAP gesture." 后来改成promise.then写法就可以了,完整代码如下,有些地方写的不好,欢迎指教 /** * 校验微信权限 * @param {string} auth 需要授权的api * @returns {boolean} 是否校验成功 */ async function checkWXAPIoAuthority(auth){ const AuthorityMap = new Map([ ['bluetooth', '蓝牙'], ['userLocation', '用户地理位置'], ['userLocationBackground', '后台用户位置'], ['record', '麦克风'], ['camera', '摄像头'], ['writePhotosAlbum', '访问相册'], ['addPhoneContact', '访问联系人'], ['addPhoneCalendar', '日历'], ['werun', '运动步数'] ]) if(!AuthorityMap.has(auth)) return false try { let { authSetting } = await wx.getSetting() if(authSetting['scope.' + auth]){ return true }else{ let resAuthorize = {} // 这里之所以有个if语句判断 authSetting是否存在该权限字段 // 是因为 报错信息:{errMsg: "authorize:fail 系统错误,错误码:-12006,auth deny"} // --https://developers.weixin.qq.com/community/develop/doc/0004cae5a34490ac227b55f7251c00?highline=12006 // 用户首次授权用wx.authorize,非首次授权(用户拒绝了或手动取消后)就用wx.openSetting if(!authSetting.hasOwnProperty('scope.' + auth)) { try{ resAuthorize = await wx.authorize({ scope: 'scope.' + auth }) }catch(authorizeFail){ resAuthorize = authorizeFail } } if(resAuthorize?.errMsg && resAuthorize.errMsg.indexOf('ok') !== -1){ return true }else{ // let operate = await wx.showModal({title: '提示', content: `需要您授权获取${AuthorityMap.get(auth)}权限`}) // if (operate.confirm) { // console.log('打开setting') // 打开setting // let setting = await wx.openSetting() // console.log('setting==>', setting) // undefined // return true // } else { // return false // } return new Promise((resolve, reject) => { wx.showModal({ title: '提示', content: `需要您授权获取${AuthorityMap.get(auth)}权限`, success: (operate) =>{ if(operate.confirm){ wx.openSetting({ success:({authSetting})=>{ console.log('openSetting', authSetting) if(authSetting && authSetting['scope.' + auth]){ resolve(true) } else { resolve(false) } }, fail:()=>{ resolve(false) } }) } else{ resolve(false) } }, fail:()=>{ resolve(false) } }) }) } } }catch(e){ return false } } 应用: if(!await checkWXAPIoAuthority('bluetooth')) return 考虑到以后I18国际化,AuthorityMap 还要再做修改
2022-02-18 - DAPP中的NFT是什么|DAPP开发|NFT项目开发
DAPP中的NFT是什么|DAPP开发|NFT项目开发 [代码]什么是NFT?为什么一件纯数字作品可以拍卖近7000万美元?NFT对我有什么好处?相信很多人对这些问题都充满了疑惑。 下面我们将从几个方面来回答这些问题。 NFT是什么? NFT的全称是Non-Fungible Tokens,它在中文中通常被翻译为“不可替代代币””。简而言之,nft 是区块链的一个条目,这是一种去中心化的数字簿记技术,类似于比特币之类的加密货币。 因为NFT有不可替代性,这意味着它可以用来代表一些独特的东西,比如博物馆里的蒙娜丽莎原画,或者一块土地的所有权。 虽然比特币(BTC)和以太币(ETH)等主流加密资产也记录在区块链中,但NFT与它们的区别在于,任何NFT代币都是不可替代、不可分割的。 当您购买NFT令牌时,这意味着您可以访问其不可擦除的所有权记录和实际资产。 NFT是数字世界中的一种“独特”资产,可以在现实世界中买卖和使用来代表商品,但它以一种无形的方式存在。 目前大部分都是数字艺术品或交换卡。有些是虚拟商品,有些是以普通格式打包的,如JPEG和PDF。只有少数NFT代币是真实所有权的数字记录。 为什么这幅纯数码画这么贵? 3月11日,一幅代表艺术家比普尔创作的由5000幅较小图像组成的数字绘画所有权的NFT(非同质令牌)在佳士得以6900多万美元的价格售出,震惊了艺术界。这也意味着基于区块链的加密资产NFT开启了数字收藏领域的新时代。 毕竟,三年前,整个NFT市场的价值还不到4200万美元。现在一幅纯数码画卖到了6934万美元。 虽然这听起来很可笑:花了这么多钱,难道只能证明我是这幅画的实际主人吗?但这确实是NFT狂热的一个关键点。人们收藏NFT作品的一个原因是,这种收藏行为可以帮助他们展示自己在数字领域的地位。 虽然不可能预测目前的市场规模,但肯定会进一步扩大。未来的增长是不可避免的。但这可能比投资股票等更为主流的行业风险更大。数字货币市场在很大程度上是风险与回报的混合体,可能会出现巨大的价格波动。一个新字段必须经过不同的循环,才能确定其真实值。 [代码] https://img.kxnrl.com/ugc/E4C3B43FE9E9EAA00DC657891AE55543BC0CBB8E!source [代码]NFT对我有什么好处? 现在,艺术家、音乐家、有影响力的人和体育特许经营商正在使用NFT将过去廉价或免费的数字产品货币化。 如果你是一个创造者,那么你可以创造或“铸造”NFT,这意味着你对你的工作拥有所有权。 由于NFT的独特性和流通性,如果有人侵权或抄袭,你可以用这个NFT来证明你的所有权。也可以卖掉,在交易中心赚多少就赚多少。避免从平台上惹太多麻烦。 传统的艺术作品,如绘画和照片,是有价值的,因为他们是独特的。但是数字文件很容易被无限复制。通过 nft,艺术品可以被“标记” ,创建一个可以买卖的数字所有权证书。[代码]
2021-08-10 - page-container 影响页面正常回退
影响基础库版本:>= 2.14.0 在 pageA 注册一个 <page-container>,属性 show 为 false, 当从 pageA 跳转到 pageB 之前,将属性 show 设为 true,然后跳转到 pageB, 期望:从 pageB 能正常操作返回 pageA。 实际:无论右滑,还是左上角的回退,都失效,无法返回 pageA。
2021-03-20 - 小程序左上角的返回按钮 怎么监听?
需求监听该行为 返回到指定的页 或 调用事件
2019-09-09 - 健身房预约小程序平台开发笔记
介绍以健身场馆预约为核心功能,提供线上健身课程预约的小程序平台 [图片] 特点预约管理:开始/截止时间/人数/审核规则可灵活设置自定义客户预约填写的数据项预约凭证:线下到场后校验/核销/二维码自助签到详尽的数据:掌控全局/细致洞察/数据导出及时到位的提醒:赴约提醒/手机日历提醒仅需一台手机:便可发布及管理预约平台数据库 [图片] 架构 [图片] 截图 [图片] [图片] [图片] [图片] [图片] [图片]
2022-02-11 - 小程序 rich-text 无法渲染 table?
// 节点Json数据如下: [ { "name" : "table", "attrs" : { "width" : "812" }, "children" : [ { "name" : "tbody", "children" : [ { "name" : "tr", "attrs" : { "height" : "43", "class" : "firstRow" }, "children" : [ { "name" : "td", "attrs" : { "width" : "41", "height" : "43" }, "children" : [ { "name" : "p", "attrs" : { "class" : "vsbcontent_start" }, "children" : [ { "type" : "text", "text" : "序号" } ] } ] }, { "name" : "td", "attrs" : { "width" : "419" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "工 作 内 容" } ] } ] }, { "name" : "td", "attrs" : { "width" : "93" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "起止周数" } ] } ] }, { "name" : "td", "attrs" : { "width" : "83" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "时间" } ] } ] }, { "name" : "td", "attrs" : { "width" : "176" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "负 责 单 位" } ] } ] } ] }, { "name" : "tr", "attrs" : { "height" : "43" }, "children" : [ { "name" : "td", "attrs" : { "width" : "41", "height" : "43" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "1" } ] } ] }, { "name" : "td", "attrs" : { "width" : "419" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "2019~2020学年第二学期第二轮选课" } ] } ] }, { "name" : "td", "attrs" : { "width" : "93" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "第18-19周" } ] } ] }, { "name" : "td", "attrs" : { "width" : "83", "rowspan" : 5 }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "元月" } ] } ] }, { "name" : "td", "attrs" : { "width" : "176" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "教务科" } ] } ] } ] }, { "name" : "tr", "attrs" : { "height" : "43" }, "children" : [ { "name" : "td", "attrs" : { "width" : "41", "height" : "43" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "2" } ] } ] }, { "name" : "td", "attrs" : { "width" : "419" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "期末考试" } ] } ] }, { "name" : "td", "attrs" : { "width" : "93" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "第18-19周" } ] } ] }, { "name" : "td", "attrs" : { "width" : "176" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "考试中心、各学院" } ] } ] } ] }, { "name" : "tr", "attrs" : { "height" : "43" }, "children" : [ { "name" : "td", "attrs" : { "width" : "41", "height" : "43" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "3" } ] } ] }, { "name" : "td", "attrs" : { "width" : "419" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "教学督导工作总结" } ] } ] }, { "name" : "td", "attrs" : { "width" : "93" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "第19周" } ] } ] }, { "name" : "td", "attrs" : { "width" : "176" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "质量评估中心" } ] } ] } ] }, { "name" : "tr", "attrs" : { "height" : "43" }, "children" : [ { "name" : "td", "attrs" : { "width" : "41", "height" : "43" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "4" } ] } ] }, { "name" : "td", "attrs" : { "width" : "419" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "学籍异动汇总发文" } ] } ] }, { "name" : "td", "attrs" : { "width" : "93" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "第20周" } ] } ] }, { "name" : "td", "attrs" : { "width" : "176" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "学籍管理科" } ] } ] } ] }, { "name" : "tr", "attrs" : { "height" : "43" }, "children" : [ { "name" : "td", "attrs" : { "width" : "41", "height" : "43" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "5" } ] } ] }, { "name" : "td", "attrs" : { "width" : "419" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "年终教学工作总结" } ] } ] }, { "name" : "td", "attrs" : { "width" : "93" }, "children" : [ { "name" : "p", "children" : [ { "type" : "text", "text" : "第21周" } ] } ] }, { "name" : "td", "attrs" : { "width" : "176" }, "children" : [ { "name" : "p", "attrs" : { "class" : "vsbcontent_end" }, "children" : [ { "type" : "text", "text" : "教务处、各学院" } ] } ] } ] } ] } ] } ]
2021-11-28 - 自定义组件创建顺序和 "lazyCodeLoading"引发的问题?
错误示例 https://developers.weixin.qq.com/s/vOdyHymR7atQ 1.上述示例 在工具中编译或者真机调试会报错 Component is not found in path "components/comp2/comp2" (using by "components/comp1/comp1") 但是预览正常 2.原示例 去除app.json中的配置"lazyCodeLoading" : "requiredComponents",工具和真机调试报错依旧, 预览白屏。 3.原示例 删除comp1文件夹,再在回收站中还原此文件夹后,工具编译正常,预览正常,真机调试控制台正常,屏幕一直中间转圆圈 说下自己的理解。 配置的lazyCodeLoading在工具编译(我是windows环境)时无效,只在真机使用小程序时有效. 工具编译的时,因为某些因素,组件有预注入行为(形容),并与组件创建时间相关,如果子组件创建在父组件后,会造成报错,找不到子组件。但由于设置了lazyCodeLoading,在真机启动小程序时(预览),组件注入行为有变,所以正常显示。(把父组件删除再还原的操作会使得子组件创建时间早于父组件,工具就不报错了) 需求产生是因为,开发的第三方插件(JS,TS开发)想利用自定义组件的返回值为载体承载组件类型,在父组件中写子组件properties时有更严格的类型检查,搭配behaviors可以实现更细粒度开发模式。插件已经开发完毕(实现ts类型的严格检查很费事),但由于现有错误,不得不下架npm。希望管理开发人员看到,给个官方思路,如果没有想法改变,看来就得放弃维护此插件了(心疼)。其实就是让lazyCodeLoading模式在工具端编译时生效。以下为一些插件相关截图。 [图片] [图片] [图片][图片] [图片]
2021-09-24 - 多环境配置,同事可自主切换方案(多测试环境)
1)实现效果: 正式版:一定使用生产数据体验版和开发版:同事可自主切换数据环境支持多个测试数据环境2)实现思路: 环境信息都在一个配置文件里使用wx.getAccountInfoSync()判断当前如果是正式版,则读取生产数据。如果是体验版或开发版,通过缓存获取上次选中的环境,如果无环境,则取生产数据环境。提供一个位置,供同事切换环境,并保存到缓存,然后退出小程序。重新打开小程序,则切换完成。3)代码片段: https://developers.weixin.qq.com/s/ImBsBzmH7fwc
2022-01-25 - 微信小程序前端JS实现XML和JSON的格式互转
最近开发小程序微信支付的时候,后端返回了XML格式的数据,我需要吧XML转JSON才可以使用数据,话不多说直接看代码 首先下载需要的JS文件(我是放在CSDN上面的):https://download.csdn.net/download/m0_46156566/19550071[图片] JS相关代码,我的是小程序,其他开发同理引入调用就行了 const X2JS = require('../../utils/x2js/we-x2js'); let x2js = new X2JS(); let xmlStr = '' let myJson = x2js.xml2js(xmlStr); console.log(myJson) => {xml:{appId:wx123456,nonceStr:123456789}} 至于JSON转化为XML同理,把x2js.xml2js(xmlStr);换成x2js.js2xml(xmlStr);就可以了,如果还有不懂的可以在下方留言一起讨论
2021-06-10 - 新富文本组件
mp-html小程序富文本组件 news欢迎加入 QQ 交流群:699734691示例小程序添加获取组件包功能[图片] 功能介绍 支持在多个平台使用 支持丰富的标签(包括 table、video、svg 等) 支持丰富的事件效果(自动预览图片、链接处理等) 支持锚点跳转、长按复制等丰富功能 支持大部分 html 实体 丰富的插件(关键词搜索、内容编辑等) 效率高、容错性强且轻量化使用方法1. npm 方式 在项目根目录下执行 npm install mp-html 开发者工具中勾选 使用 npm 模块 并点击 工具 - 构建 npm 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "mp-html" } } 在需要使用页面的 wxml 文件中添加 <mp-html content="{{html}}" /> 在需要使用页面的 js 文件中添加 Page({ onLoad() { this.setData({ html: 'Hello World!' }) } }) 2. 源码方式 将源码中的代码包(dist/mp-weixin)拷贝到 components 目录下,更名为 mp-html 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "/components/mp-html/index" } } 后续步骤同上 获取github 链接:https://github.com/jin-yufeng/mp-html npm 链接:https://www.npmjs.com/package/mp-html 文档链接:https://jin-yufeng.gitee.io/mp-html
2022-03-04 - 富文本editor怎么实现首行缩进?
可以通过 this.editCtx.format('textIndent', '2em') 的方式实现
2019-09-16 - 微信小程序开发之富文本编辑器
微信小程序开发之富文本编辑器 一年多去了,还有这么多人关注这个编辑器,那就索性把这个组件放上去,各位直接引用吧!如果您感觉很好用,很实用,也请大家给点一个赞!前言:富文本在Web开发上的地位大家可想而知,很多地方都需要用到富文本编辑器,比如开发类似新闻管理小程序、商品简介等。微信小程序在基础库2.7.0之后上线了一个editor富文本编辑器组件,这个组件是本次要讲的内容。组件相关的内容大家可以去看官方文档的内容,这里我们就不进行讲解。而我们要做的就是将官方的富文本组件进行二次开发达到一个好用而又实用的地步:https://developers.weixin.qq.com/miniprogram/dev/component/editor.html 先看效果图(以下只是一个基础的实用): [图片] 代码方案: 1.引入组件(组件的下载地址链接:https://pan.baidu.com/s/15D3ejvs30BZPwn94RgyNmw 提取码:hg66) 2、在你需要的使用的页面的JSON文件中引入该组件,引入方法如下: "usingComponents": { "hg-editor":"../../../components/hg-editor/hg-editor(根据自己的放置位置修改,其中/hg-editor/hg-editor是固定的)" } 3、在wxml文件中使用,使用案例如下,可选参数有四个 参数详解: [图片] showTabBar :是否显示工具栏(默认为true,显示,如果改为false则为不显示)placeholder:文本框提示文字,默认为“请输入相关内容”name:是编辑器的name属性,默认为空uploadImageURL:图片的上传地址,默认为空使用属性案例测试: bind:input可以获得用户输入的内容: onInputtingDesc: function (e) { let html = e.detail.html; //相关的html代码 let originText = e.detail.text; //text,不含有任何的html标签 this.setData({ ['topic.text']: html, ['topic.originText']: originText }); } 使用案例: [图片][图片][图片] 您的想法有多大,组件拓展的无限可能就有多大,欢迎各位留言,欢迎各位使用! 好用,就来收藏一下,更新不易,点个赞!
2022-04-22 - 小程序图片裁剪插件 image-cropper
之前的插件类目没有了导致搜不到了,重新发个文章。 image-cropper 一款高性能的小程序图片裁剪插件,支持旋转。 [图片] 优势 [代码]1.功能强大。[代码] [代码]2.性能超高超流畅,大图毫无卡顿感。[代码] [代码]3.组件化,使用简单。[代码] [代码]4.点击中间窗口实时查看裁剪结果。[代码] ㅤ 初始准备 1.json文件中添加image-cropper [代码] "usingComponents": { "image-cropper": "../image-cropper/image-cropper" }, "navigationBarTitleText": "裁剪图片", "disableScroll": true [代码] 2.wxml文件 [代码]<image-cropper id="image-cropper" limit_move="{{true}}" disable_rotate="{{true}}" width="{{width}}" height="{{height}}" imgSrc="{{src}}" bindload="cropperload" bindimageload="loadimage" bindtapcut="clickcut"></image-cropper> [代码] 3.简单示例 [代码] Page({ data: { src:'', width:250,//宽度 height: 250,//高度 }, onLoad: function (options) { //获取到image-cropper实例 this.cropper = this.selectComponent("#image-cropper"); //开始裁剪 this.setData({ src:"https://raw.githubusercontent.com/1977474741/image-cropper/dev/image/code.jpg", }); wx.showLoading({ title: '加载中' }) }, cropperload(e){ console.log("cropper初始化完成"); }, loadimage(e){ console.log("图片加载完成",e.detail); wx.hideLoading(); //重置图片角度、缩放、位置 this.cropper.imgReset(); }, clickcut(e) { console.log(e.detail); //点击裁剪框阅览图片 wx.previewImage({ current: e.detail.url, // 当前显示图片的http链接 urls: [e.detail.url] // 需要预览的图片http链接列表 }) }, }) [代码] 参数说明 属性 类型 缺省值 取值 描述 必填 imgSrc String 无 无限制 图片地址(如果是网络图片需配置安全域名) 否 disable_rotate Boolean false true/false 禁止用户旋转(为false时建议同时设置limit_move为false) 否 limit_move Boolean false true/false 限制图片移动范围(裁剪框始终在图片内)(为true时建议同时设置disable_rotate为true) 否 width Number 200 超过屏幕宽度自动转为屏幕宽度 裁剪框宽度 否 height Number 200 超过屏幕高度自动转为屏幕高度 裁剪框高度 否 max_width Number 300 裁剪框最大宽度 裁剪框最大宽度 否 max_height Number 300 裁剪框最大高度 裁剪框最大高度 否 min_width Number 100 裁剪框最小宽度 裁剪框最小宽度 否 min_height Number 100 裁剪框最小高度 裁剪框最小高度 否 disable_width Boolean false true/false 锁定裁剪框宽度 否 disable_height Boolean false true/false 锁定裁剪框高度 否 disable_ratio Boolean false true/false 锁定裁剪框比例 否 export_scale Number 3 无限制 输出图片的比例(相对于裁剪框尺寸) 否 quality Number 1 0-1 生成的图片质量 否 cut_top Number 居中 始终在屏幕内 裁剪框上边距 否 cut_left Number 居中 始终在屏幕内 裁剪框左边距 否 [代码]img_width[代码] Number 宽高都不设置,最小边填满裁剪框 支持%(不加单位为px)(只设置宽度,高度自适应) 图片宽度 否 [代码]img_height[代码] Number 宽高都不设置,最小边填满裁剪框 支持%(不加单位为px)(只设置高度,宽度自适应) 图片高度 否 scale Number 1 无限制 图片的缩放比 否 angle Number 0 (limit_move=true时angle=n*90) 图片的旋转角度 否 min_scale Number 0.5 无限制 图片的最小缩放比 否 max_scale Number 2 无限制 图片的最大缩放比 否 bindload Function null 函数名称 cropper初始化完成 否 bindimageload Function null 函数名称 图片加载完成,返回值Object{width,height,path,type等} 否 bindtapcut Function null 函数名称 点击中间裁剪框,返回值Object{src,width,height} 否 函数说明 函数名 参数 返回值 描述 参数必填 upload 无 无 调起wx上传图片接口并开始剪裁 否 pushImg src 无 放入图片开始裁剪 是 getImg Function(回调函数) [代码]Object{url,width,height}[代码] 裁剪并获取图片(图片尺寸 = 图片宽高 * export_scale) 是 setCutXY X、Y 无 设置裁剪框位置 是 setCutSize width、height 无 设置裁剪框大小 是 setCutCenter 无 无 设置裁剪框居中 否 setScale scale 无 设置图片缩放比例(不受min_scale、max_scale影响) 是 setAngle deg 无 设置图片旋转角度(带过渡效果) 是 setTransform {x,y,angle,scale,cutX,cutY} 无 图片在原有基础上的变化(scale受min_scale、max_scale影响) 根据需要传参 imgReset 无 无 重置图片的角度、缩放、位置(可以在onloadImage回调里使用) 否 GitHub https://github.com/wx-plugin/image-cropper/tree/master 如果有什么好的建议欢迎提issues或者提pr
2021-12-15 - 小程序的表单的组件化封装及使用
表单一直是类web项目中开发的难点,表单涉及UI,交互,校验,接口,回填等各种坑点,在项目中,我们遵循下列原则设计表单组件 配置化表单 统一的表单结构 丰富的API,简化出错,提示等操作 支持任一表单元素之间的联动 原生微信所有表单组件支持 示例代码 [代码]https://github.com/webkixi/aotoo-xquery => pages/form [代码] 表单组件配置说明 表单由配置文件生成,表单属性构成大致如下图 [图片] 文本表单使用 以文本类表单展开说明 wxml [代码]<ui-form wx:if="{{formConfig}}" dataSource="{{formConfig}}" /> [代码] js [代码]const Pager = require('../../components/aotoo/core/index') const config = [ { title: '文本框表单区域', desc: '说明信息', input: [ { id: 'aaa', type: 'text', title: '文本', placeholder: '数字输入键盘', error: '错误信息', desc: '说明信息' bindblur: 'onbindblur', bindinput: 'onbindinput', bindfocus: 'onbindfocus', bindconfirm: 'onbindconfirm', bindkeyboardheightchange: 'onbindkeyboardheightchange', }, ] }, { title: '数字表单区域', input: [ {id: 'ccc', type: 'number', title: '整数型', placeholder: '数字输入键盘', bindblur: 'onBlur'}, {id: 'ddd', type: 'idcard', title: '身份证', placeholder: '身份证输入键盘', bindblur: 'onBlur'}, {id: 'eee', type: 'password', title: '密码串', maxlength: 30, placeholder: '隐藏的密码串', bindblur: 'onBlur'} ] }, { title: 'TEXTAREA', input: [ {id: 'aaa', type: 'textarea', title: '文本域', placeholder: '输入文字', bindblur: 'onBlur'}, ] }, ] const mthSet = { onbindblur(e) { console.log('=====text', e.detail.value); }, onbindinput(e) { console.log('=====text', e); }, onbindfocus(e) { console.log('=====text', e); }, onbindconfirm(e) { console.log('=====text', e); }, onbindkeyboardheightchange(e) { console.log('=====text', e); }, } Pager({ data: { formConfig: { $$id: 'myForm', formStyle: 'width: 90vw;', data: config, methods: mthSet }, } }) [代码] 示例如下图所示 [图片] 更多说明 请移步 https://juejin.im/post/5eba58aaf265da7b9d50d7dd
2020-05-15 - 小程序云开发如何获取集合里所有字段的名称?
如何获取云数据库某个集合里所有字段的名称
2021-03-04 - 如何主动改变小程序的场景值?
做了一个扫码二维码签到的功能,onshow判断场景值是否通过指定的二维码进入小程序,然后给出一个提示。 问题:扫码成功进入小程序后给出扫码结果提示。 用户切出小程序在打开,或者按电源关闭屏幕再次打开又会执行一次onshow,显示之前提示的信息。 请问怎么才能在提示过后修改场景值,api文档没找到方法。
2019-08-29 - 云开发云函数中使用Redis的最佳实践,包括五种常用数据结构和分布式全局锁
Redis因其拥有丰富的数据结构、基于单线程模型可以实现简易的分布式锁、单分片5w+ ops的超强性能等等特点,成为了大家处理高并发问题的最常用的缓存中间件。 那么云开发能不能使用Redis呢?答案是肯定的。 下面我介绍下云开发中Redis使用的最佳实践: 第一步、购买Redis,安装Redis扩展 参见官方文档:https://developers.weixin.qq.com/community/develop/article/doc/000a4446518488b6002c9fa3651813 吐槽一下,写这篇文章的原因之一就是上面的官方文档中的示例代码是在不堪入目,希望这篇文章能让小伙伴少踩些坑。 第二步、创建并部署测试云函数,配置云函数的网络环境 [图片] 第三步、编写代码 cache.js const Redis = require('ioredis') const redis = new Redis({ port: 6379, host: '1.1.1.1', family: 4, password: 'password', db: 0 }) exports.redis = redis /** * 加redis全局锁 * @param {锁的key} lockKey * @param {锁的值} lockValue * @param {持续时间,单位s} duration */ exports.lock = async function(lockKey, lockValue, duration) { const lockSuccess = await redis.set(lockKey, lockValue, 'EX', duration, 'NX') if (lockSuccess) { return true } else { return false } } /** * 解redis全局锁 * @param {锁的key} lockKey * @param {锁的值} lockValue */ exports.unlock = async function (lockKey, lockValue) { const existValue = await redis.get(lockKey) if (existValue == lockValue) { await redis.del(lockKey) } } 上面是操作redis的工具方法,可以打包放到云函数的层管理中,方便其他云函数引用。层管理使用方式参见官方文档:https://cloud.tencent.com/document/product/876/50940 index.js const cloud = require("wx-server-sdk") const cache = require('/opt/utils/cache.js') // 使用到了云函数的层管理 cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) global.cloud = cloud global.db = cloud.database() global._ = db.command global.$ = _.aggregate exports.main = async (event, context) => { context.callbackWaitsForEmptyEventLoop = false const wxContext = cloud.getWXContext() let appId = wxContext.APPID if (wxContext.FROM_APPID) { appId = wxContext.FROM_APPID } let unionId = wxContext.UNIONID if (wxContext.FROM_UNIONID) { unionId = wxContext.FROM_UNIONID } let openId = wxContext.OPENID if (wxContext.FROM_OPENID) { openId = wxContext.FROM_OPENID } // redis五种常用数据结构 // 字符串 await cache.redis.set('hello', 'world') // 无过期时间 await cache.redis.set('hello', 'world', 'EX', 60) // 过期时间60s let stringValue = await cache.redis.get('hello') console.log('string: ', stringValue) // hash await cache.redis.hset('hash', 'hello', 'world') let hashValue = await cache.redis.hget('hash', 'hello') console.log('hash: ', hashValue) // list await cache.redis.lpush('list', 'hello', 'world') let listList = await cache.redis.lrange('list', 0, -1) // 读取队列所有元素 await cache.redis.ltrim('list', 1, 0) // 清空队列 console.log('listList: ', listList) // set await cache.redis.sadd('set', 'hello', 'world') let setExist = await cache.redis.sismember('set', 'hello') // 检查元素是否在集合中 console.log('set: ', setExist) // zset await cache.redis.zadd('zset', 1, 'hello', 2, 'world') let zsetList = await cache.redis.zrange('zset', 0, -1, 'WITHSCORES') console.log('zsetList: ', zsetList) // redis实现分布式全局锁 // 加全局锁,锁的过期时间应根据实际业务调整 const createOrderLock = `createOrderLock:${unionId}` const ts = Date.now() if (!(await cache.lock(createOrderLock, ts, 3))) { return { code: 4, msg: '操作太频繁了' } } // 这边写全局互斥的业务逻辑代码 // 比如创建订单,一个用户同时只能并发创建一个订单 // 解全局锁 await cache.unlock(createOrderLock, ts) return { code: 0, data: {} } } 上面是测试云函数的入口文件,演示了redis五种常用数据结构和redis全局锁的使用方法。 最后还有个小tips,所有引用到cache.js的云函数需要安装ioredis的依赖,进入云函数目录,使用如下命令: npm install ioredis
2021-06-17 - [开盖即食]利用“云函数”生成小程序码和将buffer流转化图片
[图片] 为什么要用云函数来做这个? 前端一条龙不求人(后端) 利用官方资源来快速解决问题,白piao不香吗? 之前也有不少人分享过云函数生成小程序码的方法,这里我为大家总结下不同方法的区别和优缺点~ [图片] 开盖食用思路: 小程序端请求 --> 云函数API小程序 --> 返回图片的buffer --> 把buffer转化成图片 1、云函数部分 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { try { const result = await cloud.openapi.wxacode.get({ path: event.url, //传入需要配置的url,但是新版本不能传参param width: 300 }) console.log(result) return result } catch (err) { console.log(err) return err } } [代码] 新建一个云函数名字为getWxacode,示例是openapi.wxacode.get的方法,这个参数可以直接跳转小程序内部对应的page。 注意: 与 wxacode.createQRCode 总共生成的码数量限制为 100,000,如果你需要大量生成码,请谨慎调用。且新版本只能传path,不能传参param~ 2、页面部分 [代码]Page({ data: { imgUrl: "" //图片地址 }, getWxacode() { wx.cloud.init(); let self = this; wx.showLoading({ title: '请求云函数' }) // 调用云函数 获取 内容 wx.cloud.callFunction({ name: 'getWxacode', data: { url: "pages/home/index" }, success: res => { console.log('云函数调用成功', res); let bufferImg = "data:image/png;base64," + wx.arrayBufferToBase64(res.result.buffer); self.setData({ //imgUrl: res.result.fileID imgUrl: bufferImg }); wx.hideLoading(); }, fail: err => { console.error('云函数调用失败', err) } }) } }) [代码] [图片] 可以获得一个返回值,里面有个图片的buffer,转化buffer即可展示图片内容。 3、转化buffer流成图片的三种方法 3.1 直接将buffer转化Base64 [代码]console.log('云函数调用成功返回值:', res); let bufferImg = "data:image/png;base64," + wx.arrayBufferToBase64(res.result.buffer); self.setData({ imgUrl: bufferImg }); [代码] 这里用到的方法是 wx.arrayBufferToBase64(buffer)转化,加好base64头,即可食用~ 优点: 方便简单 确定: 阅后即焚,无法保存,个别场景可能需要缓存或者拼接canvas海报 3.2 在云函数直接将Buffer上传到云存储 [代码]await cloud.uploadFile({ cloudPath: 'test/' + event.userInfo.openId + '.jpg', //这里如果可以重复就用openId,如果不可能重复就用 fileContent: result.buffer, //处理buffer 二进制数据 success: res => { // 文件地址 console.log(res.fileID) }, fail: err =>{ console.log(err) } }) [代码] [图片] 将生成的小程序码上传到自带的云存储上,可以长期永久保存 优点: 长期保存,合适只要生成一次反复使用的场景 缺点: 生成量大的话,比较占用有限云存储资源 3.3 将图片转化保存在手机本地 [代码]let { buffer } = res.result; const wxFile = wx.getFileSystemManager(); const filePath = wx.env.USER_DATA_PATH + '/test.jpg'; //把图片写在本地 wxFile.writeFile({ filePath, encoding: "binary", data: buffer, success: res => { console.log(res); //writeFile:ok self.setData({ imgUrl: filePath }); } }) [代码] 这里用到的是wx.getFileSystemManager()的方法,将图片buffer转化后保存一个本地地址~ 优点: 生成实体地址,有时候图片太大,base64会出现一些诡异的BUG 缺点: 耗时 4、最终方案 经过综合考虑,这里使用的是不限次数的 openapi.wxacode.getUnlimited方法。 云函数部分: [代码]const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { try { const result = await cloud.openapi.wxacode.getUnlimited({ scene: event.scene }); console.log(result) return await cloud.uploadFile({ cloudPath: 'test/' + event.userInfo.openId + '.jpg', fileContent: result.buffer, //处理buffer 二进制数据 success: res => { // 文件地址 console.log(res.fileID) }, fail: console.error }) } catch (err) { console.log(err) return err } } [代码] 页面代码: [代码]Page({ data: { imgUrl: "" //图片地址 }, getWxacode() { wx.cloud.init(); let self = this; wx.showLoading({ title: '请求云函数' }) // 调用云函数 获取 内容 wx.cloud.callFunction({ name: 'getWxacode', data: { scene: "goTo:pages/home/index" }, success: res => { console.log('云函数调用成功', res); self.setData({ imgUrl: res.result.fileID }); wx.hideLoading(); }, fail: err => { console.error('云函数调用失败', err) } }) }, }) [代码] 5、注意事项 5.1 生成码一共有三种方法: openapi.wxacode.get 小程序码,可以直接生成path,但不能传参,有次数限制 openapi.wxacode.createQRCode 二维码,有次数限制 openapi.wxacode.getUnlimited 特定scene传参,无次数限制 (推荐使用) 1和2方法累积10w次限制 5.2 代码报错,跑不了等 [图片] 记得初始化 wx.cloud.init(); [图片] 记得部署云函数~ ctrl+s 是没用的~~~ [图片] 记得先本地调试好,再上传~~~ 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.get.html 看完觉得有帮助记得点个赞哦~ 你的赞是我继续分享的最大动力!^-^
2021-09-13 - svg简单图标在小程序中的使用分享
项目中经常会使用不同颜色的小图标,需要在项目中放置不同颜色的图标图片,有时候涉及图标大小和压缩问题。抽空研究了下svg代码在小程序中如何使用,整理了一个基础组件,以后项目中遇到使用频率高的小图标,用它就比较方便了。项目代码在git平台公开,可克隆下载。 svg代码在小程序中展示: 使用background-image(url('data:image/svg+xml,svg转换后代码'))进行小图标的展示; svg代码符号转换成十六进制的ascii码: “<”替换成“%3C”,“>”替换成“%3E”; fill=color更新图标颜色,color支持英文单词和16进制的颜色码,16进制颜色码“#”替换成“%23”; svg代码通过iconfont平台复制 [图片] js代码: class SVGCON { constructor() { } svgXml(n, c) { let name = n; let data = ''; let casualData = this[name](); let newArray = []; let color = 'black'; let newFill = ''; // 颜色转换 if (c && c.indexOf('#') >= 0) { color = c.replace('#', '%23'); } else if (c) { color = c; } newFill = "fill=" + "'" + color + "'"; // 更新颜色,加入fill=color(svg去掉fill=color相关代码) // 查找svg中的path数量 newArray = casualData.split('>'); casualData = ''; for (let i = 0; i < newArray.length; i++) { if (i == newArray.length - 1) { } else { newArray[i] = newArray[i] + ' ' + newFill + ">"; } casualData = casualData + newArray[i]; } // 转换成svg+xml data = casualData; data = data.replace('<', '%3C'); data = data.replace('>', '%3E'); data = 'data:image/svg+xml,' + data; //双引号展示不出来,需要转换成单引号 data = data.replace(/\"/g, "'"); return data; } arrowRight() { //向右箭头 return '复制的svg代码' } loading() { //加载中 return '复制的svg代码' } setting() { //设置 return '' } } export { SVGCON }; wxml组件代码: <view class="m-icon mini-class" style='background-image:url("{{backgroundImage}}")'></view> 开发全局基础组件 创建存放全局组件的目录components,开发完成组件之后在app.js配置全局组件 [图片] 页面直接使用组件 <mini-icon mini-class="icon" icon="arrow-top" color="{{color}}" /> 微信扫码预览效果: [图片] 代码托管于微信开发者-代码管理: https://git.weixin.qq.com/yukiyuki/yuki_svg.git 以上是关于svg代码在小程序中作为基础组件的使用的内容。有表达或者总结的不对的地方,请多指教,谢谢!
2021-02-19 - [填坑手册]小程序Canvas生成海报(一)--完整流程
[图片] 海报生成示例 最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。 [图片] 原型图 这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。 [图片] 海报生成流程 [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 下面分享下主要的代码内容和“填坑现场”: 一、添加字体 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html [代码]canvasContext.font = value //示例 ctx.font = `normal bold 20px sans-serif`//设置字体大小,默认10 ctx.setTextAlign('left'); ctx.setTextBaseline("top"); ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)//绘制文本 [代码] 符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif 文字过长在canvas下换行问题处理(最多两行,超过“…”代替) [代码]ctx.setTextAlign('left'); ctx.setFillStyle('#000');//文字颜色:默认黑色 ctx.font = `normal bold 18px sans-serif`//设置字体大小,默认10 let canvasTitleArray = canvasTitle.split(""); let firstTitle = ""; //第一行字 let secondTitle = ""; //第二行字 for (let i = 0; i < canvasTitleArray.length; i++) { let element = canvasTitleArray[i]; let firstWidth = ctx.measureText(firstTitle).width; //console.log(ctx.measureText(firstTitle).width); if (firstWidth > 260) { let secondWidth = ctx.measureText(secondTitle).width; //第二行字数超过,变为... if (secondWidth > 260) { secondTitle += "..."; break; } else { secondTitle += element; } } else { firstTitle += element; } } //第一行文字 ctx.fillText(firstTitle, 20, 278, 280)//绘制文本 //第二行问题 if (secondTitle) { ctx.fillText(secondTitle, 20, 300, 280)//绘制文本 } [代码] 通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。 (一行字允许宽度为280时,判断需要写小点,比如260) 二、获取临时地址并设置图片 [代码]let mainImg = "https://demo.com/url.jpg"; wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { //处理图片纵横比例过大或者过小的问题!!! let h = res.height; let w = res.width; let setHeight = 280, //默认源图截取的区域 setWidth = 220; //默认源图截取的区域 if (w / h > 1.5) { setHeight = h; setWidth = parseInt(280 / 220 * h); } else if (w / h < 1) { setWidth = w; setHeight = parseInt(220 / 280 * w); } else { setHeight = h; setWidth = w; }; console.log(setWidth, setHeight) ctx.drawImage(res.path, 0, 0, setWidth, setHeight, 20, 50, 280, 220); ctx.draw(true); }, fail: function (res) { //失败回调 } }); [代码] 在开发过程中如果封面图无法按照约定的比例(280x220)给到: 那么我们就需要处理默认封面图过大或者过小的问题,大致思路是:代码中通过比较纵横比(280/220=1.27)正比例放大或者缩小原图,然后从左上切割,竟可能保证过高的图是宽度100%,过宽的图是高度100%。 在canvas中draw图片,必须是一个(相对)本地路径,我们可以通过将图片保存在本地后生成的临时路径。 微信官方提供两个API: wx.downloadFile(OBJECT)和wx.getImageInfo(OBJECT)。都需先配置download域名才能生效。 三、裁切“圆形”头像画图 [代码]ctx.save(); //保存画图板 ctx.beginPath()//开始创建一个路径 ctx.arc(35, 25, 15, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.closePath(); ctx.drawImage(headImageLocal, 20, 10, 30, 30); ctx.draw(true); ctx.restore()//恢复之前保存的绘图上下文 [代码] 使用图形上下文的不带参数的clip()方法来实现Canvas的图像裁剪功能。该方法使用路径来对Canvas话不设置一个裁剪区域。因此,必须先创建好路径。创建完整后,调用clip()方法来设置裁剪区域。 需要注意的是裁剪是对画布进行的,裁切后的画布不能恢复到原来的大小,也就是说画布是越切越小的,要想保证最后仍然能在canvas最初定义的大小下绘图需要注意save()和restore()。画布是先裁切完了再进行绘图。并不一定非要是图片,路径也可以放进去~ 小程序 canvas 裁切BUG [代码]ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); //第一个填充矩形 wx.downloadFile({ url: headUri, success(res) { ctx.beginPath() ctx.arc(50, 50, 25, 0, 2 * Math.PI) ctx.clip() ctx.drawImage(res.tempFilePath, 25, 25); //第二个填充图片 ctx.draw() ctx.restore() ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); ctx.draw(true) ctx.restore() } }) [代码] clip裁切这个功能,如果有超过一张图片/背景叠加,则裁切效果失效。 错误参考:http://html51.com/info-38753-1/ 四、将canvas导出成虚拟地址 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'customCanvas', success: (res) => { console.log(res.tempFilePath) //为canvas的虚拟地址 } }) res: { errMsg: "canvasToTempFilePath:ok", tempFilePath: "http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr….cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg" } [代码] 这里需要把canvas里面的内容,导出成一个临时地址才能保存在相册,比如: http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr5UfJVR4k.cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg 五、询问并获取访问手机本地相册权限 [代码]wx.getSetting({ success(res) { console.log(res) if (!res.authSetting['scope.writePhotosAlbum']) { //判断权限 wx.authorize({ //获取权限 scope: 'scope.writePhotosAlbum', success() { console.log('授权成功') //转化路径 self.saveImg(); } }) } else { self.saveImg(); } } }) [代码] 判断是否有访问相册的权限,如果没有,则请求权限。 六、保存到用户手机本地相册 [代码]wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '保存到系统相册成功', icon: 'success', duration: 2000 }) }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取权限成功,给出再次点击图片保存到相册的提示。') } else { console.log('获取权限失败,给出不给权限就无法正常使用的提示') } } }) } else { wx.showToast({ title: '保存失败', icon: 'none' }); } }, complete(res) { console.log(res); } }) [代码] 保存到本地需要一定的时间,需要加一个loading的状态。 七、关于组件中引用canvas [代码]let ctx = wx.createCanvasContext('posterCanvas',this); //需要加this [代码] 在components中canvas无法选中的问题: 在components自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 <canvas> ,如果省略则不在任何自定义组件内查找。
2021-09-13 - 我个人总结的小程序字体解决方案
最近在用到小程序的字体,用起是真的有一丝烦躁,各种莫名其妙的bug。下面做一些总结,免得大家犯同样的错误,同样也希望官方能出一个详细的字体解决说明。 问题原因: 首先使用字体无非就是用font-family,在网页上是有很多很好的探讨的文章。但是在小程序中,我们并不知道font-family支持哪些字体,据我这一周的查找,官方也没有详细的说明。 因此如果我们想使用某种字体,最好的方法只有通过font-face外部引入字体。 但是小程序是不支持引入本地的字体文件,即font-face中的src属性的值不能是本地路径。否则会报do-not-use-local-path错: [图片] 因此我所知道的最终的解决方案就有两种 1.引入网络路径 2.通过base64编码引入 ①引入网络路径的方法是将你的字体文件放在服务器中,然后通过网络路径引入,需要注意跨域的问题(可能因为我把字体文件放在腾讯云的对象存储中了,因此没有专门处理跨域也没有问题,但是不排除你可能会遇到跨域问题)。使用这种方法时要注意字体文件不能太大,具体限制我也不清楚。这里我只测试过3M的中文字体,会报ERR_CACHE_MISS[图片],这个情况下,虽然在开发工具上显示的字体是正确的,但是预览到真机上就直接表现为字体文件没有加载的效果。(另外,即使你的字体文件很小,也会在 清除缓存后第一次编译 报这个错,但这种情况下预览到真机上是没有问题的) ②第二种方式就是通过base64编码引入。使用这种方法时需要注意一点,很多转base64编码的工具(例如我这里使用的font-min)会自动帮你兼容ie,生成的代码像这样:[图片] 在网页上这样用是没有问题的,还兼容其他浏览器。 但是在小程序中是不行的,必须把多余的部分全部删除,只留base64那一行,不然在开发工具上显示直接没有加载字体的效果,像这样: [图片] 另外据说base64编码不能太大,否则也会出错;这里由于我的需求只是需要几个固定文字,因此生成的base64很小,没有遇到这个问题。 以上就是我在处理小程序字体时遇到的问题总结,希望大家遇到时能少走弯路; 总结的不对的地方,欢迎大家讨论指正。
2018-05-08 - 开发者不骗开发者,你跟我说这只要100块?
导语2021腾讯游戏年度发布会在线上举行。今年,发布会以“超级数字场景”战略理念为核心,传递对游戏认知、产业边界的建设性思考,并通过60余款游戏产品与内容集中发布,展现腾讯游戏为玩家带来的丰富体验与多元价值。 本次发布会再次选择了云开发 CloudBase 作为技术选型之一,以极低的成本实现了实时弹幕系统,并保障稳定运行,为游戏爱好者带来了优质互动体验。下文将重点介绍项目组使用云开发实现弹幕功能的全过程。 “各部门注意,前方高能!” 一、业务背景2021腾讯游戏年度发布会开发了专属小程序,包含直播、抽奖、观看回放等功能,其中所有的弹幕功能均基于云开发的实时数据推送实现。 [图片] 在进行弹幕功能的技术选型前,开发同学梳理了业务场景: 弹幕实时互动允许少量的弹幕丢失仅发布会直播当晚使用敏感信息/关键字过滤在综合考虑成本、稳定性、与小程序适配性等多个方面后,项目最终选择了云开发的实时数据推送功能,早在去年的发布会里,项目组就使用了云开发的实时数据推送来实现直播节目单进度提醒等功能,在此基础上,把弹幕也统一搬上云开发。 二、技术实践开发思路一开始想直接把全部用户的弹幕集合直接监听,但官方限制单次监听数据不能大于5000条,且监听数据条越多初始化性能越差,超出上限会抛错并停止监听。最后设计为:用户弹幕插入集合a,监听数据集合b,使用云函数的定时器定期合并弹幕,并更新到对应的正在监听的数据记录上(如图)。 [图片] 这样保证了用户监听的数据记录为恒定数量,这里采用10条记录(循环数组)汇总弹幕数据,每秒更新当前时间戳的所有弹幕到 index = timestamp%10 的数据记录上,同时弹幕刷新频率固定为1s,减轻前端由于数据频繁改动而不断 callback/ 渲染的性能消耗。 代码演示用户发送弹幕部分代码: exports.main = async (event, context) => { // ...省略部分鉴权/黑名单/校验内容安全逻辑 let time = Math.ceil(new Date().getTime() / 1000); // 插入弹幕 let res = await db.collection('danmu_all').add({ data: { openid, content, time, }, }); return {err: 0, msg: 'ok'}; }; 弹幕合并处理: exports.main = async (event, context) => { // ....省略一部分非关键代码 // 只取其中100条弹幕,可动态调整 let time = Math.ceil(new Date().getTime() / 1000) - 1; const result = await db .collection('danmu_all') .where({time}).limit(100).get(); let msg = []; for (let i of result.data) { msg.push({ openid: i.openid, content: i.content, }); } // 更新循环数组的对应位置 db .collection('watch_collection') .where({index: time % 10}) .update({ data: {msg,time}, }); return msg; } 前端处理消息通知,注意不要重复 watch。其中如果打开了云开发的匿名登录,那 H5 端的页面同样可以使用同步弹幕功能: this.watcher = db.collection('watch_collection').watch({ onChange: function(snapshot) { for (let i of snapshot.docChanges) { // 忽略非更新的信息 if (!i.doc || i.queueType !== 'update') { continue; } switch (i.doc.type) { // ...省略其他类型的消息处理 case 'danmu': // 弹幕渲染 livePage.showServerBarrage(i.doc.msg); break; } } }, }); 至此,整个弹幕的核心功能已经完全实现。 二次优化跑了一段时间后发现偶现丢弃几秒内的弹幕,后面查看执行日志,发现即使配置定时器为每秒执行一次,实际生产中也不是严格每秒执行一次,有时候会跳过1-3秒去执行,这里另外使用了 redis 去标记当前处理的进度,即使有跳过的秒数,也能往前回溯未处理的时间进行补录。其中云函数使用 redis 的教程可以查看官方云函数使用 redis 教程。 [图片] 用户发送弹幕部分代码添加标记代码: exports.main = async (event, context) => { // ...省略部分鉴权跟校验内容安全代码 // ...省略插入代码 // 标记合并任务 await redisClient.zadd('danmu_task', time, time+'') }; 弹幕合并处理,注意:要 redis5.0 以上的才支持 zpopmin 命令,如需购买,需要选对版本。 exports.main = async (event, context) => { //当前秒 let time = Math.ceil(new Date().getTime() / 1000) - 1; while (true) { // 弹出最小的任务 let minTask = await redisClient.zpopmin('danmu_task'); // 当前无任务 if (minTask.length <= 0) { return; } // 当前秒的任务,往回塞,并结束 if (parseInt(minTask[0]) > time) { redisClient.zadd('danmu_task', minTask[1], minTask[0]); return; } // 执行合并任务 await danmuMerge(time); } }; 安全逻辑上也做了一定的策略,如本地先渲染发送的弹幕,客户端收到弹幕推送时,判断 openid 为自己时候不渲染,这样即使用户的弹幕被过滤掉也能在本地展现,保留一定的用户体验。 另外,单个云函数的实例上限是1000,如果确定当晚流量比较大,可以考虑用多个云函数分摊流量。 管理后台的实现同时,利用 watch 功能可以做到管理后台同步实时刷新客户端的弹幕,达到管理的目的,同一份代码前端和管理端都能复用: [图片] 节选部分管理后台代码: methods: { stop() { this.watcher.close(); }, }, beforeDestroy() { this.watcher.close(); }, mounted() { this.app = this.$store.state.app; this.db = this.app.database(); let that = this; this.watcher = this.db.collection('danmu_merge').watch({ onChange(snapshot) { for (let d of snapshot.docChanges) { for (let v of d.msg) { that.danmu.unshift(v); } } if (that.danmu.length > 500) { that.danmu = that.danmu.slice(0, 499); } }, }); 集合的读权限设置在实时数据推送里同样生效,如果权限是设置为仅可读用户自己的数据,则监听的时候无法监听到非用户自己创建的数。 Tips当时没注意到 watch 对数据库权限限制的问题,数据库权限默认为仅创建者可读写,循环数组第一次初始化是开发过程中在客户端创建,默认添加了当前用户的openid,导致其他用户无法读取到 merge 的数据,解决方法:删除 openid 字段或设置权限为全部人可读。 集合的读权限设置在实时数据推送里同样生效,如果权限是设置为仅可读用户自己的数据,则监听的时候无法监听到非用户自己创建的数。 三、项目成果与价值基于云开发的云函数、实时数据推送、云数据库等能力,项目全程平稳运行,即便在发布会当晚流量峰值的时候,弹幕的写入运行稳定。在监听方面(读),watch 的性能能够稳定支持百万级同时在线。 [图片] 最终,2名研发仅用2天就完成了弹幕系统的开发和调试。而在费用方面,支撑整个项目弹幕系统运行的总费用仅为100元左右,主要集中在数据库读写和云函数调用(目前监听数据库实时数据功能处于免费阶段,不会计算到数据库读取费用上),抛去其他模块的费用,实际弹幕模块可能仅消耗了小几十块钱,费用大大低于预期,相对比传统即时通讯等方案节省超过数十倍。 总体上,项目采用云开发,具备以下优势: 自带弹性扩缩容,可以抗住瞬时高并发流量,保障直播顺利进行;费用便宜,只收取云函数调用和数据库读写费用,实时数据推送免费使用,非常适合项目;安全稳定,项目的访问都基于云开发自带的微信私有链路实现,保证安全性;自由度高,能够契合其他开发框架和服务。
2021-11-24 - 绘制分享海报要注意的点
业务上经常存在一种场景就是需动态的绘制出一张漂亮的海报,并且能支持到把海报分享给朋友或者朋友圈的操作。今天就简单来聊聊怎么实现动态绘制海报并保存分享图片的功能。 首先,选用Qrcode插件来实现动态生成二维码,由于小程序的限制,这边实现直接通过把qrcode源码放到项目里,用自定义utils的方式来引入。 接下来就实现绘制方法: Tips: 微信小程序canvas有两套API,大家按照自己的需求使用,本文使用的是老的接口; <!-- 因为小程序同层渲染和原生组件的坑,用wxss设置样式使视窗不可见会导致绘制不出来,但是用内联样式可以实现 --> <canvas wx:if="{{!posterImage}}" class="poster-canvas" id="poster" canvas-id="poster" style="width:{{standardWidth}}px;height:{{standardHeight}}px;position:fixed;left:2000px;top:0;" /> <!-- 仅仅用做于绘制qrcode载体,不需要显示 --> <canvas wx:if="{{!posterImage}}" id="qrcode" canvas-id="qrcode" class="qrcode-canvas" style="width:{{qrCodeWidth}}px;height:{{qrCodeHeight}}px;position:fixed;left:2000px;top:0;" /> 用两个元素的原因是因为绘制过程中经常会出现位置偏移的问题,这边通过占位元素和定位来实现位置固定; const { ctx } = await this.getCtx('poster') // 获取canvas元素 getCtx (selector) { return new Promise((resolve) => { const ctx = wx.createCanvasContext(selector, this) const { standardWidth, standardHeight } = this.properties resolve({ ctx, width: standardWidth, height: standardHeight, }) }) } // 绘制二维码 drawQrCode (canvasNodeRef) { const { ctx } = canvasNodeRef const { qrCode } = this.data.jsonData const { standardRate, shareLink } = this.data return new Promise(async (resolve, reject) => { QrCode.api.draw(shareLink, 'qrcode', qrCode.width, qrCode.height, this) // Qrcode.api.draw的坑,接口流程已经完成,但是实际上并没有绘制完成(视窗不可见),所以通过一些延迟来确保大部分都已经绘制完成 await this.delay() wx.canvasToTempFilePath({ canvasId: 'qrcode', success: async (res) => { ctx.drawImage( res.tempFilePath, qrCode.x * standardRate, qrCode.y * standardRate, qrCode.width * standardRate, qrCode.height * standardRate ) resolve(res.tempFilePath) }, fail: (err) => { reject(err) } }, this) }) } 绘制的过程中需要注意处理一下绘制的图片资源 getTransImg (imgUrl) { const newUrl = imgUrl.replace('http://', 'https://') return new Promise((resolve) => { this.downLoadPicSource(newUrl).then(async (res) => { resolve(res.tempFilePath) }).catch(e => { console.log('资源下载失败: ', e.message, newUrl) }) }) } downLoadPicSource (url) { const newUrl = url.replace('http://', 'https://') return downloadSource({ url: newUrl }) } 这时候几本就把二维码绘制完成了,如果需要绘制其他元素可以自己按需添加,以下举例几种: // 绘制背景 const bgUrl = await this.getTransImg(pictureUrl) ctx.drawImage(bgUrl, 0, 0, standardWidth, standardHeight) // 绘制logo/头像 const { ctx } = canvasNodeRef const { logo } = this.data.jsonData const { standardRate } = this.data const logoUrl = await this.getTransImg(this.properties.logoUrl) ctx.drawImage( logoUrl, logo.x * standardRate, logo.y * standardRate, logo.width * standardRate, logo.height * standardRate, ) 绘制完成之后需要保存海报图,需要注意保存图片失真的问题,需要根据当前系统的dpx做一些处理 ctx.draw(true, () => { // 绘制完成 - 把canvas转化为图片 const dpr = wx.getSystemInfoSync().pixelRatio const picW = this.properties.standardWidth const picH = this.properties.standardHeight wx.canvasToTempFilePath({ canvasId: 'poster', width: picW, height: picH, destWidth: picW * dpr, destHeight: picH * dpr, success: (res) => { this.setData({ inDrawing: false, posterImage: res.tempFilePath, }) this.triggerEvent('drawEnd', res.tempFilePath) }, fail: (e) => { this.drawError('生成海报失败', e) }, this) } 绘制完成之后,只要把海报图回显到页面中即可。接下来就只要实现长按分享或者保存就可以实现了 // 判断分享图片功能是否可用 if (!wx.canIUse('showShareImageMenu')) { // 把图片保存到相册 wx.saveImageToPhotosAlbum({ filePath: this.data.posterImage, success: (evt) => { console.log('保存成功: ', evt) wx.showToast({ title: '保存成功' }) }, fail: (err) => { console.log('保存失败了: ', err) wx.showToast({ title: '保存失败', icon: 'none' }) } }) return } // 分享海报 wx.showShareImageMenu({ path: this.data.posterImage, success: (res) => { console.log('分享成功', res) wx.showToast({ title: '操作成功', }) }, fail: (err) => { console.log('分享失败', err) // 取消的情况不弹窗提醒 if (err.errMsg.indexOf('cancel') !== -1) { return } wx.showToast({ title: `分享失败:${err.errMsg}`, icon: 'none' }) } }) 大功告成!
2021-12-30 - 通过云开发调用数据预拉取
通过云开发调用数据预拉取 其实就是调用云函数的过程,本人使用时间不长,也欢迎各位大佬们分享一下在使用过程中遇到的问题呀~ 向云函数数据预拉取使用流程 1. 创建云函数 创建云函数,并在开发者工具中上传部署云函数。 2. 配置数据下载函数 登录小程序 MP 管理后台,在左侧边栏进入开发->开发管理 -> 开发设置 -> 数据预拉取,点击开启,选择数据来源“云开发”,选择环境ID,选择第一步中创建的云函数,会直接调用该函数下的[代码]index.js[代码]入口文件。 [图片] 3. 设置TOKEN 第一次启动小程序时,调用 wx.setBackgroundFetchToken() 设置一个 TOKEN 字符串,可以跟用户态相关,会在后续微信客户端向云函数请求时带上,便于给后者校验请求合法性。 示例: [代码]App({ onLaunch() { wx.setBackgroundFetchToken({ token: 'xxx' }) } }) [代码] 4. 微信客户端提前拉取数据 当用户打开小程序时,微信服务器将向云函数发送请求,客户端请求参数如下,数据获取到后会将整个云函数返回的字符串缓存到本地。 客户端请求示例: [代码] "event":{ "clientipv4":"xxx", "clientipv6":"xxx", "path":"pages/index/index", "query":"", "scene":1001, "token":"xxx", "userInfo":{ "appId":"wx1e174f3...", "openId":"oCTCE4ns8r..." } } [代码] 客户端向云函数传递的event参数: 参数 类型 必填 说明 appid String 是 小程序标识。 openId String 是 普通用户的标识,对当前公众号唯一。 token String 否 前面设置的 TOKEN。 path String 否 打开小程序的路径。 query String 否 打开小程序的query。 scene Number 否 打开小程序的场景值。 5. 读取数据及调试 读取数据及调试同向开发者服务器预拉取数据,调试时注意先确保云函数全部上传更新后再调试。 在云函数中读取数据 在调用云函数时传入的数据在event中进行调用 云函数示例: [代码]// 云函数入口函数 exports.main = async (event, context) => { let token = event.token let query = event.query return { myToken: "myToken:"+token, myQuery: "myQuery:"+query } } [代码]
2021-12-30 - 小程序跨账号调用共享云函数和图片的方法
最近在做小程序开发,需要两个小程序使用一个云服务,我想可通过以下两种方式使用使得方的资源,特别是图片链接交换。请大佬指正。 首先要保证资源方已授权使用方可以共享其资源,并已存在cloudbase_auth云函数,此部分内容可参考官方文档。 一、跨账号调用云函数:(可调用资源方所有云函数) 1.调用方新建函数:可放在工具类中 //运行资源方云函数 通用函数 export async function runCloud(runName, data) { let c1 = new wx.cloud.Cloud({ appid: 'xxxxxxxx', // 资源方 AppID resourceAppid: 'xxxxxxxx', // 资源方环境 ID resourceEnv: 'xxxxxxxx', }) await c1.init() return await c1.callFunction({ name: runName, data: data, }) } 2.假如资源方有如下云函数:login(得到临时图片链接) const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { console.log("正在调用云函数:[得到临时图片链接]:", event) const fileList = event.fileList const result = await cloud.getTempFileURL({ fileList: fileList, }) return result.fileList } 3.调用方通过如下方式调用: runtCloud('login', { fileList: tempgoods._goodsPicUrl }).then(res => { console.log("临时链接-成功", res); for (let i = 0; i < res.result.length; i++) { console.log(res.result[i].tempFfileIDleURL) this.data.files.push(res.result[i].tempFileURL) } this.setData({ files: this.data.files }) }) .catch(err => { console.log("临时链接-失败", err); }) 二、调用方使用本地函数使用资源方的图片方法: 1.调用方新建函数:可放在工具类中 export async function getUrl(fileLis) { let c1 = new wx.cloud.Cloud({ appid: 'xxxxxx', // 资源方 AppID resourceAppid: 'xxxxxx', // 资源方环境 ID resourceEnv: 'xxxxxx', }) await c1.init() return await c1.getTempFileURL({ fileList: fileLis }) } 2.调用方通过如下方式使用: getUrl(tempgoods._goodsPicUrl).then(res => { // get temp file URL console.log(res.fileList) }).catch(error => { // handle error })
2021-05-24 - 小程序开发笔记#1 封装一个页面栈工具类:思路分析(一)
为啥想要写一个页面栈工具类? 关于官方API 微信官方文档中提供了有关路由的5个API,对应实现不同的页面切换需求,分别为: wx.switchTab(Object object) wx.navigateTo(Object object) wx.navigateBack(Object object) wx.reLaunch(Object object) wx.redirectTo(Object object) 改进思路(主要还是打代码的习惯) 1)5个API可以看作页面栈的进出栈操作 tabBar之间的跳转 保留当前页面,跳转到应用内的某个页面(但是不能跳到 tabbar 页面)——push(进栈) 关闭当前页面,返回上一页面或多级页面——pop(出栈) 关闭所有页面,打开到应用内的某个页面——清空栈后push一个页面 关闭当前页面,跳转到应用内的某个页面(但是不能跳 tabbar 页面)——先pop后push一个页面 不妨尝试封装一个页面栈工具类,通过更简洁的进出栈操作实现页面切换 2)要实现页面间跳转时的数据传输,一般采用把data放入url中,如: [代码]wx:navigateTo({ url: '../pageTest/pageTest?id=123&name='bao'' }) [代码] 在目标页面pageTest的onload方法中可以这样获取数据: [代码]onLoad: function (res) { this.setData({ id:res.id, name:res.name }) console.log(this.data.id); console.log(this.data.name) }, [代码] 可以看到,对于大量数据的传输时,url会非常长,处理起来比较不方便,我们可以尝试封装一些方法实现从要传输数据对象到url的转换 NavigateUtilAPI push(page页面地址,data需要传递的数据) pop(delta返回层数) switch(page页面地址,data需要传递的数据):进行tabBar间的切换 change(page页面地址,data需要传递的数据):改变当前页面 goto(page页面地址,data需要传递的数据):强制跳转 (……更加细化的接口,如将data嵌入url,当前页面获取等 后记 下一节开始写代码实现!希望能对刚开始学小程序的朋友有所帮助! 还是小白,如果有错误或更好的想法,欢迎各位大佬一同交流、指出哇~
2021-11-13 - 云开发私人实时聊天室
云开发私人实时聊天室说明 在最开始开发小程序时,本人和团队成员实现小程序的聊天室时遇到一些困难,查阅了一些资料,有些讲得太泛,有些讲的太难,在一个阶段克服了这个困难后,收获了很多,对整个流程也熟悉了很多,在这里记录自己的一个思路,希望也能对开发新手有帮助。 项目基本配置 1.项目创建及云开发配置: 官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/quick-start/miniprogram.html PS:注意云函数目录是否为此样式:[图片] 若是普通目录样式记得在project.config.json中配置加入: [图片] 2.添加包colorui,用于样式使用,并在app.wxss中导入改包 [图片][图片] 3.在pages下新建文件夹index和新建page:index [图片] 聊天室静态页面 最终呈现的效果: 自己: [图片] 对方: [图片] 1. wxml 整体结构: [图片] 整一个页面说白了就是由一个scroll-view和一个回复框组成,scroll-view中由消息数组构成,消息的内容可以自己定义(时间,头像,消息内容等等) 具体源码: <!-- scroll-view来实现页面拖动 --> <scroll-view id='page' scroll-into-view="{{toView}}" upper-threshold="100" scroll-y="true" enable-back-to-top="true" class="message-list"> <!-- 每一条消息 --> <view class="cu-chat" wx:for="{{3}}" wx:key="index" id="row_{{index}}"> <!-- 自己发出的消息 --> <block wx:if="{{false}}"> <block wx:if="{{true}}"> <view class="datetime" style="width:100%">2021-11-16 18:10</view> </block> <view class="cu-item self" style="width: 750rpx; height: 120rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx"> <view class="main"> <view class="content bg-green shadow" style="position: relative; left: 0rpx; top: 22rpx;border-radius: 10rpx"> <text style="font-size:33rpx">这是一条消息</text> </view> </view> <view class="cu-avatar radius center" style="background-image: url({{useravatar}}); width: 71rpx; height: 71rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx" bindtap="go_myinfo"></view> </view> </block> <!-- 对方发出的消息 --> <block wx:else> <block wx:if="{{true}}"> <view class="datetime" style="width:100%">2021-11-16 19:10</view> </block> <view class="cu-item" style="width: 750rpx; height: 120rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx"> <view class="cu-avatar radius center" style="background-image: url({{match_avatar}}); width: 71rpx; height: 71rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx"> </view> <view class="main"> <view class="content bg-white shadow" style="position: relative; left: 0rpx; top: 22rpx;border-radius: 10rpx"> <text style="font-size:33rpx">这是对面的一条消息</text> </view> </view> </view> </block> </view> </scroll-view> <!-- 回复框 --> <view class="reply cu-bar"> <!-- 输入框 --> <view class="opration-area"> <input type="text" bindinput="getContent" value="{{textInputValue}}" maxlength="300" cursor-spacing="10" style="width: 544rpx; height: 64rpx; display: block; box-sizing: border-box;"></input> </view> <!-- 发送按钮 --> <button class="cu-btn bg-green shadow" bindtap='sendMsg' style="width: 150rpx; height: 64rpx; display: flex; box-sizing: border-box; left: -22rpx; top: 0rpx; position: relative">发送</button> </view> 2. wxss一些样式的配置,具体就不详细叙述了,见源码: /*消息窗口*/ .message-list { margin-bottom: 54px; } /*文本输入或语音录入*/ .reply .opration-area { flex: 1; padding: 8px; } /*回复文本框*/ .reply input { background: rgb(252, 252, 252); height: 36px; border: 1px solid rgb(221, 221, 221); border-radius: 6px; padding-left: 3px; } /*回复框*/ .reply { display: flex; flex-direction: row; justify-content: flex-start; align-items: center; position: fixed; bottom: 0; width: 100%; height: 108rpx; border-top: 1px solid rgb(215, 215, 215); background: rgb(245, 245, 245); } /*日期*/ .datetime { font-size: 10px; padding: 10px 0; color: #999; text-align: center; } 到此,静态的页面就已经做好啦,现在主要的难题也是数据部分,下面将先讲述数据库chatroom的设计及解释,最后进行js的代码编写。 数据库创建及设计1.数据库表创建:在编辑器打开云开发控制台,点击数据库,再点击集合名称右边加号,创建一个集合名称为chatroom的表。 [图片][图片] [图片] [图片] 2.chatroom设计具体页面如图: [图片] 其中, _id为记录创建时自动创建的标识属性,即主键 _openid和match_openid代表了自身和对方 records为一个对象数组,每个对象的属性分别是: msgText:消息属性(此案例中只有text属性,即文本,可自扩展为图片、音频等) openid:发送人的标识 sendTime:消息创建时间 sendTimeTS:消息创建时的时间戳(用于做时间比较,判断时间显示) showTime:消息是否显示时间 textContent:具体文本内容 其中, records:array类型, records中的记录:object类型 records中的sendTimeTS:number类型 records中的showTime:boolean类型 其余全为string类型 PS: 1、openid和match_openid可标识一个聊天室,是唯一不变的; 2、用户本身的openid是有可能在记录中的match_openid位置上的,谁发起了这个聊天室,openid这个位置就是那个发起用户的openid,所以在开发中,想要获取自己和所有其他人的聊天室,要查每条记录中的openid或者match_openid与自身openid是否匹配。 3.权限设置因为该表中的记录,非记录创建者也可以进行读写,这里的权限记得设置,不然会出问题: [图片] [图片] [图片] 具体功能实现(JS写法)1.先配置Page.data:6个属性,如有需要可自行扩展 [图片] chats存储数据库表中的records的所有信息; textInputValue是输入框内容。 2.绑定数据库表onChange函数: [图片] [图片] 这里的onChange输出e是这样的: [图片] type=init,获取了数据库表中该记录的所有内容,在这里将js中的chats进行赋值即可; 另外,当该记录内容变化时,type是update类型 3.wxml修改,wx-for将chats显示,以及一些判断和内容显示的设置: [图片] 到此,显示效果就有啦 [图片] 接下来,就是信息的添加了,下面将显示如何添加新信息到数据库 4.发送信息 先获取输入框内容: [图片] [图片] 发送函数: 增加一条信息,就是在records数组中加一条记录,所以在函数内部要对新纪录的属性进行一些赋值和判断等。 对showTime的处理: [图片] 消息空白处理: [图片] 对消息内的所有属性进行一个打包处理: [图片] 存储记录,并滑动页面: [图片] 最后,清空消息框内容 [图片] 发送一条消息,最终效果如图: [图片][图片] js源码const app = getApp() const db = wx.cloud.database() const _ = db.command const chatroomCollection = db.collection("chatroom") var util = require('../../utils/util.js'); Page({ data: { //这里的openid和match_openid应该是在上一级页面传进来的属性,这里由于只有聊天室所以暂时设置为一些固定值,用于测试 openid:'', match_openid:'', //这里的avatar是头像,具体传参方式自己设定,这里暂时设置为固定值,用于测试 useravatar:'', match_avatar:'', chats:[], textInputValue:'' }, onReady() { var that = this //查询openid和match_openid所标识的唯一聊天室 chatroomCollection.where({ _openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid)), match_openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid)) }) //绑定onChange,直观而言即表中该记录发生变动时,调用该函数 .watch({ onChange: this.onChange.bind(this), onError(err) { console.log(err) } }) }, //数据库表onchange绑定函数 onChange(e) { let that = this //type="init"的情况:初始化聊天窗口信息 if (e.type == "init") { that.initchats(e.docs[0].records) } //type="update"的情况:records中增加了一条记录 else { //在chats数组中增加该新消息 let i = that.data.chats.length const new_chats = [...that.data.chats] if (e.docs.length) new_chats.push(e.docs[0].records[i]) this.setData({ chats: new_chats }) } }, initchats(records) { this.setData({ chats: records }) //跳转到页面底部 this.goBottom() }, //获取输入文本 getContent(e) { this.data.textInputValue = e.detail.value }, sendMsg(){ let that = this //show代表了数据库表中的showTime属性,是否显示消息时间 var show = false //无记录时,true if (this.data.chats.length == 0) show = true //判断上下两条消息的时间差决定是否显示时间,这里设置了2分钟:120000毫秒,可自行修改 else { if (Date.now() - this.data.chats[this.data.chats.length - 1].sendTimeTS > 120000) show = true } const _ = db.command //消息空白处理 if (!that.data.textInputValue) { wx.showToast({ title: '不能发送空白信息', icon: 'none', }) return } //消息内容赋值 const doc = { openid: that.data.openid, msgText: "text", textContent: that.data.textInputValue, sendTime: util.formatTime(new Date()), sendTimeTS: Date.now(), showTime: show, } //添加数据库表中该记录的records数组,并跳转页面到底部 chatroomCollection.where({ _openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid)), match_openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid)) }) .update({ data: { records: _.push(doc) } }) .then(res => { that.goBottom() }) //消息设空 that.setData({ textInputValue: "" }) }, goBottom() { wx.createSelectorQuery().select('#page').boundingClientRect(function (rect) { if (rect) { // 使页面滚动到底部 wx.pageScrollTo({ scrollTop: rect.height + 4 }) } }).exec() }, }) 其中,util.js内容如下: const formatTime = date => { const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() const hour = date.getHours() const minute = date.getMinutes() const second = date.getSeconds() return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}` } const formatNumber = n => { n = n.toString() return n[1] ? n : `0${n}` } module.exports = { formatTime } 一个简单的demo就完成了,大家有什么问题欢迎随时q我。 -----------完结撒花----------
2021-11-18 - scroll-view组件中使用bindscrolltoupper上滑加载历史内容
dom结构 [代码]<scroll-view scroll-y bindscrolltoupper="upper" scroll-into-view="{{toView}}" > <view id="green" >1</view> <view id="red" >2</view> <view id="yellow">3</view> <view id="blue" >4</view> </scroll-view> [代码] 可能出现的问题: 使用[代码]scroll-view[代码]中的[代码]bindscrolltoupper[代码]事件监听上拉事件,然后在[代码]scroll-view[代码]里面内容的上面追加内容,滚动条会直接滚动到对顶端,而不是停留在当前的视图窗口(类似微信消息界面上拉会加载以前的消息记录,加载完成后,界面不会自动滚动到最上面) 解决思路 该问题的核心在于需要加载历史数据后,滚动条停留在当前的视图内容,而不是滚动到顶部,关键在于要使用[代码]scroll-into-view[代码]属性,将滚动条滚动进入指定的视图,那么每个[代码]view[代码]都要有一个唯一的id 坑:新添加的view,使用新的id,已经渲染出来的view,它的id不要变更 伪代码 [代码]upper(){ // 给每个新的view指定一个唯一的新id this.list = this.list.map(x => { x.id = XXX }) // 把this.list set 进入页面中 // 滚动进入指定视图 this.toView = toView } [代码]
2019-09-27 - scroll-view组件的scroll-into-view是否可以逆向操作?
在做这样一个项目的时候,右边是一个scroll-view组件,给每一个标题盒子设置了不同的ID。在点击左边nav的选项的时候,利用scroll-into-view属性给他赋值相应的ID,右边可以滚动到相应的盒子,这是可以实现的。但是,在不点击左边选项的情况下,手动滚动右边的商品,在滚动到相应ID盒子的时候是否能监听到滚到哪个ID呢?我在@scroll里面没有找到,有是有ID字段,但是是空的值,我想请问一下是我姿势不对吗?还是说只能通过商品数量*高度去算滚到哪了?[图片]
2021-03-04 - 如何使用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 - scroll-view滚动导航自动伪居中实现
注:每个选项卡固定宽度 官方文档中通过scroll-into-view可以控制scroll-view定点滚动 [代码]<[代码][代码]scroll-view[代码] [代码]class[代码][代码]=[代码][代码]"scroll-nav"[代码] [代码]scroll-x[代码][代码]=[代码][代码]"{{true}}"[代码] [代码]scroll-with-animation[代码][代码]=[代码][代码]"true"[代码] [代码]scroll-into-view[代码][代码]=[代码][代码]"item{{currentTab}}"[代码] [代码]scroll-left[代码][代码]=[代码][代码]"{{scrollLeftSys}}"[代码] [代码]bindscroll[代码][代码]=[代码][代码]"scrollNav"[代码][代码]>[代码] [代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"scroll-nav-item {{currentTab == index ? 'scroll-nav-item-active' : ''}}"[代码] [代码]id[代码][代码]=[代码][代码]"item{{index}}"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{[1,2,3,4,5,6,7,8,9]}}"[代码] [代码]data-index[代码][代码]=[代码][代码]"{{index}}"[代码] [代码]bindtap[代码][代码]=[代码][代码]"clickNav"[代码][代码]>标题{{item}}</[代码][代码]view[代码][代码]>[代码][代码]</[代码][代码]scroll-view[代码][代码]>[代码] [代码]clickNav(e){[代码][代码] [代码][代码]this[代码][代码].setData({[代码][代码] [代码][代码]currentTab: e.currentTarget.dataset.index[代码][代码] [代码][代码]})[代码][代码]}[代码] 效果如下 [图片] 缺点就是从后往前点的时候,到第3个就没法继续点了,需要主动滑动scroll-view才能点到前面几个选项卡。 实现伪居中: 让点击前四个的时候scroll-view不滚动,即currentTab保持为0 在点击第5个的时候,屏幕内最左测是第2个,相差3个,后面的选项卡依此类推都减3 [代码]<[代码][代码]scroll-view[代码] [代码]class[代码][代码]=[代码][代码]"scroll-nav"[代码] [代码]scroll-x[代码][代码]=[代码][代码]"{{true}}"[代码] [代码]scroll-with-animation[代码][代码]=[代码][代码]"true"[代码] [代码]scroll-into-view[代码][代码]=[代码][代码]"item{{currentTab < 4 ? 0 : currentTab - 3}}"[代码] [代码]scroll-left[代码][代码]=[代码][代码]"{{scrollLeftSys}}"[代码] [代码]bindscroll[代码][代码]=[代码][代码]"scrollNav"[代码][代码]>[代码] [图片]
2018-01-01 - 小程序读取excel表格数据,并存储到云数据库
最近一直比较忙,答应大家的小程序解析excel一直没有写出来,今天终于忙里偷闲,有机会把这篇文章写出来给大家了。 老规矩先看效果图 [图片] 效果其实很简单,就是把excel里的数据解析出来,然后存到云数据库里。说起来很简单。但是真的做起来的时候,发现其中要用到的东西还是很多的。不信。。。。 那来看下流程图 流程图 [图片] 通过流程图,我看看到我们这里使用了云函数,云存储,云数据库。 流程图主要实现下面几个步骤 1,使用wx.chooseMessageFile选择要解析的excel表格 2,通过wx.cloud.uploadFile上传excel文件到云存储 3,云存储返回一个fileid 给我们 4,定义一个excel云函数 5,把第3步返回的fileid传递给excel云函数 6,在excel云函数里解析excel,并把数据添加到云数据库。 可以看到最神秘,最重要的就是我们的excel云函数。 所以我们先把前5步实现了,后面重点讲解下我们的excel云函数。 一,选择并上传excel表格文件到云存储 这里我们使用到了云开发,使用云开发必须要先注册一个小程序,并给自己的小程序开通云开发功能。这个知识点我讲过很多遍了,还不知道怎么开通并使用云开发的同学,去翻下我前面的文章,或者看下我录的讲解视频《5小时入门小程序云开发》 1,先定义我们的页面 页面很简单,就是一个按钮如下图,点击按钮时调用chooseExcel方法,选择excel [图片] 对应的wxml代码如下 [图片] 2,编写文件选择和文件上传方法 [图片] 上图的chooseExcel就是我们的excel文件选择方法。 uploadExcel就是我们的文件上传方法,上传成功以后会返回一个fildID。我们把fildID传递给我们的jiexi方法,jiexi方法如下 3 把fildID传递给云函数 [图片] 二,解下来就是定义我们的云函数了。 1,首先我们要新建云函数 [图片] 如果你还不知道如何新建云函数,可以翻看下我之前写的文章,也可以看我录的视频《5小时入门小程序云开发》 如下图所示的excel就是我们创建的云函数 [图片] 2,安装node-xlsx依赖库 [图片] 如上图所示,右键excel,然后点击在终端中打开。 打开终端后, 输入 npm install node-xlsx 安装依赖。可以看到下图安装中的进度条 [图片] 这一步需要你电脑上安装过node.js并配置npm命令。 3,安装node-xlsx依赖库完成 [图片] 三,编写云函数 我把完整的代码贴出来给大家 [代码]const cloud = require('wx-server-sdk') cloud.init() var xlsx = require('node-xlsx'); const db = cloud.database() exports.main = async(event, context) => { let { fileID } = event //1,通过fileID下载云存储里的excel文件 const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent const tasks = [] //用来存储所有的添加数据操作 //2,解析excel文件里的数据 var sheets = xlsx.parse(buffer); //获取到所有sheets sheets.forEach(function(sheet) { console.log(sheet['name']); for (var rowId in sheet['data']) { console.log(rowId); var row = sheet['data'][rowId]; //第几行数据 if (rowId > 0 && row) { //第一行是表格标题,所有我们要从第2行开始读 //3,把解析到的数据存到excelList数据表里 const promise = db.collection('users') .add({ data: { name: row[0], //姓名 age: row[1], //年龄 address: row[2], //地址 wechat: row[3] //wechat } }) tasks.push(promise) } } }); // 等待所有数据添加完成 let result = await Promise.all(tasks).then(res => { return res }).catch(function(err) { return err }) return result } [代码] 上面代码里注释的很清楚了,我这里就不在啰嗦了。 有几点注意的给大家说下 1,要先创建数据表 [图片] 2,有时候如果老是解析失败,可能是有的电脑需要在云函数里也要初始化云开发环境 [图片] 四,解析并上传成功 如我的表格里有下面三条数据 [图片] 点击上传按钮,并选择我们的表格文件 [图片] 上传成功的返回如下,可以看出我们添加了3条数据到数据库 [图片] 添加成功效果图如下 [图片] 到这里我们就完整的实现了小程序上传excel数据到数据库的功能了。 再来带大家看下流程图 [图片] 如果你有遇到问题,可以在底部留言,我看到后会及时解答。后面我会写更多小程序云开发实战的文章出来。也会录制本节的视频出来,敬请关注。
2019-11-12 - V3统一下单二次签名nodejs云函数简单代码
得到prepay_id后,小程序支付二次签名: function getPayment(prepay_id){ let wxContext = cloud.getWXContext() let appid = wxContext.APPID let package = 'prepay_id=' + prepay_id let timestamp = parseInt(Date.now() / 1000) + '' let nonce_str = Math.random().toString(36).substr(2, 15) return { appId: appid, nonceStr: nonce_str, package, signType: 'RSA', timeStamp: timestamp, paySign: getSign([appid, timestamp, nonce_str, package], key) } } function getSign(parts = [], key, str = '') { parts.forEach(v => str += v + '\n') return crypto.createSign('RSA-SHA256').update(str).sign(key, 'base64') }
2021-12-17 - 几句代码理解async/await的用法
运行一下几句代码,立马就能理解async/await怎么使用了: testAsync: async function () { let res = await wx.showModal({ content: 'step1?' }) if (res.confirm) { } else return await this.step1() res = await wx.showModal({ content: 'step2?' }) if (res.confirm) { } else return await this.step2() console.log('end') }, step1: async function () { await wx.showModal({ content: 'this is step1' }) console.log('end of step1') }, step2: async function () { let res = await wx.showActionSheet({ itemList: ['A', 'B'], }).catch(err => console.log('err:', err))//catch可以防止阻断 if (res.tapIndex == 0) { console.log('A') } if (res.tapIndex == 1) { console.log('B') } console.log('end of step2') },
2021-12-16 - 通过腾讯云地图服务,从坐标中获得所在城市。
开通腾讯位置服务: 1、进入小程序mp后台,开发--开发者工具--腾讯地图服务--开通; 2、进入腾讯位置服务网站,控制台--key管理;申请一个key,把key设置里,能勾的都勾上。 云函数代码: const rp = require('request-promise') exports.main = async (event, context) => { var options = { uri: `https://apis.map.qq.com/ws/geocoder/v1/`, qs: { location: `${event.lat},${event.lng}`,//传入经纬度 key: 'VRSBZ-PYDWJ-*****-KGV5E-LQ4U6-JVBL6' }, json: true }; return await rp(options) }
2020-07-26 - 小程序动态消息设置没效果,updateShareMenu接口回调告知成功设置?
[图片] 调用wx.updateShareMenu如上,activityId:1043_czP87nWYfItlDfPk-7OzV9n_gIxXgSnXAQHHQDnvl9iz_icmIH0DdYnqzVKJYAOioe87dWac3mKpjpaZ 用开发者工具打码,iphone x测试,转发时没有动态消息,打印成功的回调,提示更新成功 [图片]
2020-01-10 - updateShareMenu设置转发动态消息,没有效果
- 当前 Bug 的表现(可附上截图) [图片] 没有出现“成员正在加入,当前1/3人”提示信息。 - 预期表现 - 复现路径 - 提供一个最简复现 Demo /转发 onShareAppMessage: function () { wx.updateShareMenu({ withShareTicket: true, isUpdatableMessage: true, activityId: '1006_wX5j8g8X5QSQ+Lzr6L91vUbbcmnFPMYUnzGvYgerVc6C0Y3jB_PjMIdkhe8~', // 活动 ID targetState: 0, templateInfo: { parameterList: [{ name: 'member_count', value: '1' }, { name: 'room_limit', value: '3' }] } }); return { title: '分享', path: 'pages/index/index' , } } })
2019-05-06 - 如何实现长期订阅推送的功能
如何实现长期订阅推送的功能 很多小程序开发者限于资质问题,很难申请长期订阅,但是对于一些场景,比如腾讯待办这种,需要根据日期推送信息的,没有长期订阅肯定是不可以的 那么如何实现长期订阅呢 我总计了目前小程序中几个不错的实现方式 1、引导关注公众号,利用公众号的模板消息 如何实现长期订阅推送的功能 [图片] 2、在消息内引导用户主动订阅一次性订阅消息,比如下面这种 [图片] ~ 在冯老师的某小程序也存在同样的场景 [图片] ~ [图片] ~ 确实不可否认, 冯老师的这个小程序,充分利用了一次性订阅完成了长期订阅的目的,各方面细节都是小程序里面的佼佼者。
2021-12-04 - 2019-09-20
- 组件slot动态数据,默认slot实现方案
该文根据之前的一问答整理而来。 目前,小程序的组件和模板相较于之前已经完善了很多,对于组件slot占位只能使用静态数据,无法支持动态数据,确实有些鸡肋。 在此,提供一种间接处理的方案,提供给大家参考。 网上有一种方式,slot的name设置为动态的,比如index1,index2,index3这种,额外的另外一边也对应提供index1,index2,index3对应的片段。这种方式,是一种思路,不算真正的支持动态数据,灵活性也不高,而且对于列表这种有N个的话,估计够呛。 最好的是提供类似这样的写法: <!--slot占位--> <slot name="slotOne" data="{{item}}"></slot> <!--实际的slot内容--> <view slot="slotOne">{{item.name}}</view> data为动态传递的数据,但是很可惜,官方并不支持。 本来打算断点调试跟踪下,万一有呢,只是官方没说明而已^_~。代码压缩处理了的,看着头疼,观察了几个地方,应该是没有提供,而且根据官方的实现,因为要处理组件和视图的隔离,估计还不好实现(如果容易实现的话,估计也不用等到现在,之前的帖子是2018年问的),主要跟他实现机制有一定关系。 能否自己实现呢?思考了下这个问题,恰好自己有这样的需求。 小程序本身也是一个编译运行的过程,对于前端开发而已,引入编译处理也十分常见,再加上想到自己很久之前就这么玩过,那时候小程序的组件和模板相关的还不如现在这样完善,自己就通过编译的方式实现过组件的处理。 templage可以传递数据,可以利用这一点。于是,就有了下面这样大概的构想。 ========================================== 比如component-a组件中,提供动态slot模板,只需要占位处理如下: <slot-template name="slotOne" data="{{item}}"></slot-template> data即为动态传递的数据,name是对应slot可以有多个的情况。这里自定义了一个slot-template的标签,注意这里不是组件,只是一个占位编译替换而已。 最终会编译为两部分: <!--实际的模板内容--> <template name="pageA:slotOne" data="{{item}}" >模板内容{{item.name}}</template> <template name="pageB:slotOne" data="{{item}}" >模板内容{{item.name}}</template> <!--使用对应的模板--> <template is="{{ slotName }}"></template> 因为我们的组件不会在一个地方使用,比如这里的pageA和pageB,可以约定为对应页面的目录。 页面如何使用呢? <!--pageA.wxml--> <component-a slot-name="pageA"></component-a> <!--pageB.wxml--> <component-a slot-name="pageB"></component-a> slot-name属性,用以区分,是哪个页面使用,就可以对应到具体的模板内容,模板内容也动态注入了变量。 到这里,还缺一个环节,模板内容呢?也就是编译替换到组件的内容在哪里? 按照小程序的约定,使用组件前,都需要先配置,对应有一个pageA.json的文件,大概内容如下: { "usingComponents": { "component-a":"../../components/component-a/index" } } 对此,我们也在该文件上添加一个字段,来表示组件占位模板与页面实际模板内容之间的的关系,如下: { "usingComponents": { "component-a":"../../components/component-a/index" }, "slotTemplates":{ "component-a:slotOne":"./slot/slot-one.wxml", "component-a:slotTwo":"./slot/slot-two.wxml" } } 这里的slotTemplates即为扩展添加的字段,component-a为组件名字,slotOne和slotTwo为组件内的slot占位,slot/slot-one.wxml和slot/slot-two.wxml为页面实际的模板内容。 当然,由于slotTemplates字段是自定义的,开发工具会得到提示,可以忽略,或者编译处理的时候移除都是可以的。 上述方案,已经简单验证了下,通过gulp插件的方式。当然,从效率上讲,还不如直接在组件内提供A、B、C几种模板,通过一个参数类型之类的控制使用哪个模板。整个过程实现原理,其实就是这样的,只是把这一机制隐藏到编译之后了而已。更多的是从架构,实现机制上考虑,而非解决业务问题。 上述方案,有个不足的地方就是,因为小程序组件和页面之间有隔离,这种隔离机制是小程序内部处理的。使得替换的模板内容必须要在组件内,这样的话,就会出现一个问题,如果有N个页面在使用该组件的话,编译替换后的模板内容那里就会有N个,虽然写的时候,只写了一条<slot-template/>语句而已。如果小程序支持,全局模板的话,就没这么麻烦了。 ========================================== 补充内容: 上面说,slot-template属于占位编译替换的标签,并非自定义组件,如果是自定义的组件呢???将其调整为自定义组件之后,再稍微处理下编译处理过程,可以实现我之前想要的默认slot的效果。 <slot-template name="slotA" data="{{item}}"> <view>默认slot区域</view> </slot-template> 其中,slot-template中间的就是默认的slot。 我们只需要将slot-template当做一个组件,提供data和name两个properties,内容中定义一个slot占位即可。大概如下: <!--components/slot-template/index.json--> properties:{ name:{ type:String, value:'' }, data:{ type:null } } <!--components/slot-template/index.wxml--> <view class="slot-default-template"> <slot></slot> </view> 不配置slotTemplates的时候,就是普通的自定义组件的是否方法而已。配置了slotTemplates,就会编译处理为template支持动态数据的方式。 一切OVER! ========================================== 吐槽下,微信开发者这个编辑器,难用得要死。
2021-11-30 - 单页面多个跳动的倒计时器
最近写了一个活动列表,列表展示中有一个活动倒计时,开始挺迷茫的,一点思路都没有,在社区也发了帖子求帮助,(在这里感谢热心的家人们提出宝贵的意见),最后还算是成功计算出来了 在这里分享一下,大伙能用到的话就当提供了一个帮助(不喜勿喷) html <text>{{gTime[0].day}}</text>天<text>{{gTime[0].hou}}</text>:<text>{{gTime[0].min}}</text>:<text>{{gTime[0].sec}}</text> <!-- 这里我只渲染第一个结果 --> js data() { return { timer: null, //让方法无限循环 gTime: [], //空数组 bTime: {day: '',hou: '',min: '',sec: ''} //每一次计算的倒计时结果整合 } }, onLoad(option) { this.timer = setInterval(()=>{ this.add() //倒计时 },1000); }, methods: { add(){ let nowtime = new Date(), //获取当前时间 jstime,lefttime,lefda,lefth,leftm,lefts for (var i = 0; i < this.endtime.length; i++) { this.bTime = {day: '',hou: '',min: '',sec: ''} jstime = new Date(this.endtime[i]); //定义结束时间 this.endtime是已经拿到的一个装有无数个结束时间的数组 lefttime = (jstime.getTime() - nowtime.getTime())/1000 //距离结束时间的毫秒数 if (lefttime <= 0) lefttime = 0 //判断一下活动时候到期 lefda = parseInt(lefttime/(24*60*60)) //计算天 lefth = parseInt(lefttime/(60*60)%24) //计算小时数 leftm = parseInt(lefttime/60)%60 //计算分钟数 lefts = parseInt(lefttime%60) //计算秒数 //为了美观考虑,如果是单位数就在其前面加一个 0 if (lefda < 10) this.bTime.day = '0' + lefda; //返回时 else this.bTime.day = lefda; if (lefth < 10) this.bTime.hou = '0' + lefth; //返回时 else this.bTime.hou = lefth; if (leftm < 10) this.bTime.min = '0' + leftm; //返回分 else this.bTime.min = leftm; if (lefts < 10) this.bTime.sec = '0' + lefts; //返回秒 else this.bTime.sec = lefts; this.gTime[i] = this.bTime //把每一次计算的结果付给空数组 } } } 以上就是这次的分享,有可以改进的地方,大伙自由发挥(不喜勿喷)
2021-12-01 - 一种优雅的小程序授权方案分享
一种优雅的小程序授权方案分享 ~ [图片] ~ ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ 其实在很久之前的一次产品调研中,我体验过这个交互,但是当时体验的太多了,没有做好记录,回头再找已经找不到了,昨天突然又翻到了这个小程序,十分惊喜 还是原先的设计 这个授权的交互设计,是我见过的所有授权中最棒的一个, 把用户信息授权和手机授权做到了一起,同时展示了授权的进度 目前我还有一个疑惑就是 如果只做了用户授权,而没有手机授权,下次进来会是什么交互呢,这个值得继续体验下
2021-12-01 - Coolui Scroll v3基于小程序原生组件scroll-view的上拉加载下拉刷新
coolui-scroller [图片] [图片] [图片] [图片] 前言 初衷 本来写这个组件的初衷,是在我写了一个小程序之后,发现小程序如果要实现下拉刷新、上拉加载有两种方式: 页面级的:利用页面 Page 里提供的方法。下拉虽说是那个东西但是它只有下拉三个点的动画效果而且只能显示在头部就很尴尬。很多时候一个列表的头部往往会有一些组件比如搜索、分类导航等等。所以往往列表都是局部的非页面级的。这时候下拉时动画出现在最顶部就显得很突兀。 [代码]Page({ onPullDownRefresh: function () { // 监听用户下拉刷新事件。 }, onReachBottom: function () { // 监听用户上拉触底事件。 }, onPageScroll: function () { // 监听用户滑动页面事件。 }, }); [代码] 组件级的:利用 scroll-view。 但是当你打开 scroll-view 官方文档时,映入眼帘的是一列列的参数属性方法。要完全弄懂里面的内容,恐怕你得上手写写,挨个试试里面的参数和方法才行。而对于下拉刷新这个效果文档上有个简易的 demo 可寻。上拉加载也只有 bindscrolltolower 这么个方法和 lower-threshold 阈值。所以要实现起来完全还得靠自己。 所以在写项目的最后我把页面的列表下拉刷新,上拉加载进行了初步的封装。单独拿出来方便之后重复使用。所以有了起初的 1.0 版。 发展 V2.0 scroll-view 组件初期并没有那么多配置,所以 1.0 实现的效果很有限。 后来随着官方 scroll-view 组件的不断的更新。增加了很多新的属性和事件使得下拉可以自定义起来。虽然也有很多地方不尽人意,但是可玩度还是有很多的。所以又升级了 2.0 版增加了很多下拉的自定义动画效果和上拉加载的效果。 2.0 版组件还是围绕着 scroll-view,写法上只有一个封装好的 scroller 组件。内置了一个基础的下拉效果。提供下拉的插槽位置。并给出了几个有趣的下拉效果 demo(如:天猫效果、京东小人效果)让下拉又有了更多的可能性;配置上也考虑了很多增加了列表为空时的设置上拉加载的配置。整个配置就是一个 Obj,细化到文字、背景。 V2.0 配置: [代码]// data 中配置 scroll: { // 设置分页信息 pagination: { page: 1, totalPage: 10, limit: 10, length: 100 }, // 设置数据为空时的图片 empty: { img: 'http://coolui.coolwl.cn/assets/mescroll-empty.png' }, // 设置下拉刷新 refresh: { type: 'default', style: 'black', background: "#000" }, // 设置上拉加载 loadmore: { type: 'default', icon: 'http://upload-images.jianshu.io/upload_images/5726812-95bd7570a25bd4ee.gif', background: '#f2f2f2', // backgroundImage: 'http://coolui.coolwl.cn/assets/bg.jpg', title: { show: true, text: '加载中', color: "#999", shadow: 5 } } }, [代码] 之后由于疫情及个人原因这个组件搁置了一阵子。当我再次打开它时便有了重构的想法。 V3.0 3.0 版打算把之前各个部分的插槽进行细化及拆分。并新增空列表插槽及组件、初次进入程序时的手势提示组件、顶部插槽及顶部插槽可用的组件(如:搜索组件、分类组件、下拉筛选组件)。 除了组件的变化外,核心列表准备加入长列表处理,解决数据量大时列表会出现的问题(如:setData 加载大数据的耗时高、列表渲染出来的 Dom 结构多、占用的内存高,造成页面被系统回收的概率变大等)。起初想以官方给出的 recycle-view 组件进行扩展。但是使用中,遇到很多坑及不方便之处。最让我接受不了的是需要设置 itemSize 这个方法。当我在不确定列表元素宽高的时候就很难设置。后来经过大量的思考和查资料及尝试。决定采用知乎上 daisy 提出的长列表解决方案。 该组件还在开发中各组件陆续上线~ v3.0 版 基于小程序原生组件 scroll-view 的扩展与封装,实现简单的上拉加载下拉刷新 扩展下拉刷新动画,有灵感的朋友可以丰富更多下拉动画 上传至 npm 包可安装下载并 npm 构建 修改参数配置使组件使用更便捷 增加加载插槽可以自定义加载更多样式 增加多组件配合使列表功能更丰富 开发进度 调整为虚拟长列表模式 支持多组件搭配,使插件更灵活 新增组件 coolui-scroller-item(列表项组件) 新增组件 coolui-scroller-page(长列表分页组件) 新增组件 coolui-scroller-empty(空列表组件) 新增组件 coolui-scroller-handtip(手势提示组件) 新增组件 coolui-scroller-loadmore(加载更多组件) 新增组件 coolui-scroller-nav(分类导航组件) 新增组件 coolui-scroller-refresh(下拉刷新组件) 新增组件 coolui-scroller-parallax(下拉刷新视差位移组件) 新增组件 coolui-scroller-search(搜索组件) 新增组件 coolui-scroller-sort(分类筛选及排序组件) 示例 demo 请微信扫码打开小程序查看 [图片] 示例代码: https://github.com/wzs28150/coolui-scroller/tree/demo 请 clone 下载到本地使用微信开发者工具查看 [代码]git clone -b demo https://github.com/wzs28150/coolui-scroller.git [代码] 安装 npm 安装 [代码]npm i coolui-scroller --production [代码] 引入 1.调用组件 在[代码]app.json[代码]或[代码]index.json[代码]中引入组件 [代码]"usingComponents": { "scroller": "coolui-scroller/scroller/index" } [代码] 2.页面结构 [代码]<scroller class="my-scroller"> </scroller> [代码] 3.配置 在 js 的 data 中进行配置参数设置,v3.0 版将功能细化到各个组件中具体配置详见(组件) 4.组件 根据自己的业务场景选用组件,也可以在对应的插槽中自定义 详细文档 gitee文档入口 github文档入口
2021-11-30 - Fisher-Yates Shuffle洗牌算法
在程序开发的过程中,我们可能经常会遇到数组乱序的需求,比如从题库中随机抽取题目、歌曲随机播放、或者随机展示一些用户信息这些,我们往往需要把原始数据进行打乱,然后利用打乱后的结果进行展示。 提到打乱顺序,我们往往首先想到的是利用数组的sort,通过传入返回值随机的比较函数,简洁的将数组顺序打乱。 function shuffle(arr) { return arr.sort(()=> Math.random() > 0.5); } 但是由于V8引擎中sort的实现方式,此方法并不能真正的将数组打乱,或者说打的不够乱,所以我们今天分享Fisher–Yates Shuffle算法,今天我们经常说起的Fisher-Yates Shuffle算法其实经过Knuth 和 Durstenfeld改良过后的算法,比起Fisher等人原始的算法,在原始数组上进行交互,可以省去额外O(n)的空间。 具体的思路是将指针指向数组的最后一个元素,从前面的元素中随机取出一个元素进行置换,完成后,指针向前移动,依次完成置换。此算法的javascirpt如下,供大家参考。 function shuffle(array) { var m = array.length, t, i; while (m) { i = Math.floor(Math.random() * m--); t = array[m]; array[m] = array[i]; array[i] = t; } return array; } 参考文档:https://bost.ocks.org/mike/shuffle/
2021-11-29 - [拆弹时刻]分包大作战,主包调用分包资源问题和若干报错处理
[图片] 由于一个项目主包要爆了,但是项目的地址二维码已经出去,不能修改,所以必须将一些JS库移到分包中,在这过程中踩了不少坑。 一些错误提示误导性很强,所以我将遇到的都一一罗列出来。 1、显示component组件引入错误,其实是配置问题 错误提示:VM7771 WAService.js:2 Component is not found in path “packageA/ec-canvas/ec-canvas” (using by “pages/smallTools/token”)(env: Windows,mp,1.05.2109262; lib: 2.14.1) [图片] 解决方法: [代码]"usingComponents": { "ec-canvas": "../../packageA/ec-canvas/ec-canvas", "mp-msg": "weui-miniprogram/msg/msg" }, "componentPlaceholder": { "ec-canvas":"view" } [代码] JSON配置中没有写:<code>componentPlaceholder</code>,这种常常出现在Components引用时候,因为调用异步,所以需要占位! 2、because they are in diffrent subPackages错误提示 这种错误,一般有两种可能性。 (1)引用路径错误,不支持import直接引用 Error: should not require …/…/packageA/ec-canvas/echarts in pages/smallTools/token.js without a callback, because they are in diffrent subPackages [图片] [代码]//比如: import * as echarts from '../../packageA/ec-canvas/echarts'; [代码] (2)基础库版本太低,比如2.17.3版本以上 VM10490 WAService.js:2 Please do not register multiple Pages in packageA/smallTools/token.js(env: Windows,mp,1.05.2109262; lib: 2.14.1) [图片] [代码]let echarts; require.async('../../packageA/ec-canvas/echarts.js').then(pkg => { echarts = pkg; console.log(pkg); }) [代码] 出现不支持文档中的 <code> require.async </code>,主要原因是基础库版本过低。 3、主包/分包A调用其他分包B的资源,必须写在function中 VM10490 WAService.js:2 Please do not register multiple Pages in packageA/smallTools/token.js(env: Windows,mp,1.05.2109262; lib: 2.14.1) [图片] 一般可能是引入方法没有写在function中,而直接头部引用 [代码]//错误位置 require('../subPackageB/utils.js', utils => { console.log(utils.whoami) // Wechat MiniProgram }) page({ onLoad(){ //正确位置 } }) [代码] 4、主包引用分包JS正确的写法(示例) 官方的几个方法都可以,但要注意一些细节,这里展示一个echart图表的实战demo。 [代码]//配置JSON: { "navigationBarBackgroundColor": "#1757c4", "navigationBarTextStyle": "white", "enablePullDownRefresh": true, "usingComponents": { "ec-canvas": "../../packageA/ec-canvas/ec-canvas", "mp-msg": "weui-miniprogram/msg/msg" }, //这个一定要写 "componentPlaceholder": { "ec-canvas":"view" } }; //JS: let echarts; page({ async onLoad(){ //注意:要写在方法里面 require('../../packageA/ec-canvas/echarts.js', pkg => { echarts = pkg; }); //同步方法 require.async('../commonPackage/index.js').then(pkg => { echarts = pkg; // 'common' }) } }) [代码] 单个JS的引用注意点比较多,其他component直接调用即可。 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic.html (和你一样,我用的时候,压根没一个个字仔细看哈~) 如有疑问请留言~ 觉得有用,请点个赞哦,让我继续分享更有动力~
2022-02-08 - 【教程】关于uniapp中对axios的封装
对于axios请求做的其他操作,例如拦截、过滤等等····· 下图中baseUri自己定义所需的baseURL即可 [代码]import axios from "axios"; import {baseUri} from "./api.js" const service = axios.create({ baseURL: baseUri, timeout: 600000, }); axios.defaults.retry = 4; axios.defaults.retryDelay = 4000; axios.defaults.withCredentials = true; function startLoading() { //使用Element loading-start 方法 console.log(baseUri); uni.showLoading({ title: "加载中..." }); } function endLoading() { //使用Element loading-close 方法 uni.hideLoading(); } let needLoadingRequestCount = 0 export function showFullScreenLoading() { if (needLoadingRequestCount === 0) { startLoading() } needLoadingRequestCount++ } export function tryHideFullScreenLoading() { if (needLoadingRequestCount <= 0) return needLoadingRequestCount--; if (needLoadingRequestCount === 0) { endLoading() } } service.interceptors.request.use((config) => { showFullScreenLoading(); return config; }, error => { tryHideFullScreenLoading(); return Promise.reject(error) }); // axios 请求处理超时处理 service.interceptors.response.use( function(response) { tryHideFullScreenLoading(); return response; } ); export default service; [代码]
2021-11-27 - 小程序开通云闪付支付后常见问题Q&A
看完记得点赞收藏好评一键三连 Q1:小程序如何开通云闪付支付功能? A1:超级管理员扫码登录商户平台「点我访问」,点击「产品中心」->「开发配置」->「支付方式配置」->「开通“云闪付付款”功能」 小提示:支付方式配置在页面最底部 [图片] Q2:开通“云闪付付款”功能后小程序需要做开发对接吗? A2:不需要,开通“云闪付付款”功能后,商户号绑定的小程序默认就支持云闪付付款了,无需做任何开发对接,原有系统无需调整。 Q3:用户使用云闪付付款,商户收款手续费是多少? A3:与商户号原费率保持一致,举例:用户在商户A小程序使用云闪付支付100元,商户A当前费率为0.6%,则应收手续费应收为1000.6%=0.6元。 Q4:用户使用云闪付付款,商户收款资金在什么时间结算到商家银行卡? A4:用户使用云闪付付款实时到微信支付商户号,根据商户号原结算周期结算。举例:用户在商户A小程序使用云闪付支付100元,商户A当前结算周期为T+1并开通自动提现功能,则在用户付款时间后一个工作日自动提现到商户银行卡。 Q5:用户使用云闪付付款的订单应如何进行查询? A5:1.在商户后台通过交易账单中的「付款银行」字段来检索对应的订单 扫码登录商户平台-交易中心-交易账单-下载交易账单,「付款银行字段」取值为:UPQUICKPASS_CREDIT和UPQUICKPASS_DEBIT的,即为用户使用云闪付付款的交易 [图片] [图片] 2.微信端可以通过商家助手小程序查看,访问「微信支付商家助手小程序」,点击「收款记录」,付款人展示为“云闪付用户”的,即为用户使用云闪付进行付款的交易。[图片] Q6:小程序“云闪付付款”功能是否支持服务商模式? A5:支持 Q7:某个小程序可以单独关闭用户使用云闪付付款功能吗? A7:可以,商户可以在商户平台指定小程序关闭该功能,并查看已关闭的小程序列表。超级管理员扫码登录商户平台「点我访问」,点击「产品中心」->「开发配置」->「支付方式配置」->「新增关闭云闪付APPID」,添加成功后该小程序用户将不再支持云闪付付款 小提示:支付方式配置在页面最底部 [图片] Q8:用户在小程序使用云闪付付款是否支持云闪付优惠? A8:支持云闪付通用优惠或全场优惠 Q9:用户使用云闪付付款后支付结果通知没有「attach」字段返回是什么原因? A9:已知问题,已排期修复 Q10 :商户后台开通了“云闪付付款”功能,为什么小程序支付时没有云闪付付款功能入口呢? A:1、手机需要安装云闪付APP 2、云闪付付款选项需要调起微信支付后才会让用户选择,当小程序自身功能有多项支付选择时,需要选择「微信支付」后才可会有云闪付付款选择[图片] [图片] Q11:用户在小程序使用“云闪付”付款后,服务商还有技术服务费吗? A:满足基础技术服务费和行业政策要求的,保持不变 Q12:商户后台开通配置云闪付后,支付时没有云闪付付款选择如何排查? A: 1.商户在后台关闭了此功能; 2.用户手机有安装云闪付app; 3.如属于以下场景,也不会展示云闪付:境外交易、指定身份支付、未成年支付、支付中签约; 4.商户号为新开通商户或近期无交易,要有稳定的流水后才会开启入口; 5.开通了“自助清关”产品不会展示云闪付; 6.电商收付通托管模式的商户不会展示云闪付; 7.小微商户(注:商业版小微灰度内测阶段)不会展示云闪付。 如有更多疑问可以跟帖回复,也可以拨打95017进行咨询
2021-12-31 - 表格table示例
目的 实现允许编辑的【课程表】。 效果图[图片][图片] wxml代码<view class="mainPage"> <view class="hearderView"> <van-icon class="arrow" bindtap="goHome" name="notes-o" size="20" /> <text class="tipText" style="font-size: 12px;width: 85%;">滑动查看完整课表,长按可以编辑保存。</text> </view> <scroll-view scroll-x="true" scroll-y="true" style="height: {{validHeight-50}}px;"> <view class="table"> <view class="tr h"> <view class="th h1"></view> <view class="th" wx:for="{{[0,1,2,3,4,5,6]}}" wx:key="weekIndex" wx:for-item="weekItem">{{m1.getWeekday(weekItem)}}</view> </view> <block wx:for="{{kebiao.detail}}" wx:key="index" wx:for-item="item"> <view class="tr"> <view class="td c1"> <text>第</text><text>{{item.no+1}}</text><text>节</text> </view> <view class="td c" wx:for="{{item.ke}}" wx:key="index2" wx:for-item="item2" bindlongpress="longpress" data-no="{{item.no}}" data-day="{{index}}"> <text>{{item2.name}}</text> <text>{{item2.time}}</text> <text>{{item2.classroom}}</text> <text>{{item2.teacher}}</text> </view> </view> </block> </view> </scroll-view> <view class="botView" style="width: 100%;height: {{botHeight}}px;"></view> </view> <van-popup show="{{ show }}" position="bottom" custom-style="height: 230px;" bind:close="onClose"> <view class="popView"> <view class="popHeader"> <van-icon class="arrow" bindtap="onClose" name="close" size="20" color="Red" /> <text class="tipText">{{m1.getWeekday(currentKe.day)}} 第{{currentKe.no+1}}节</text> <van-icon class="arrow" bindtap="confirmEdit" name="passed" size="20" color="#1989fa" /> </view> <view class="kebiaoLine"> <text class="label"><text class="kebiaoNo" space="ensp">{{item.no}} </text>课程名</text> <input bindinput="inputText" id="name" value="{{currentKe.ke.name}}"></input> </view> <view class="kebiaoLine"> <text class="label">时间</text> <input bindinput="inputText" id="time" value="{{currentKe.ke.time}}"></input> </view> <view class="kebiaoLine"> <text class="label">教室</text> <input bindinput="inputText" id="classroom" value="{{currentKe.ke.classroom}}"></input> </view> <view class="kebiaoLine"> <text class="label">老师</text> <input bindinput="inputText" id="teacher" value="{{currentKe.ke.teacher}}"></input> </view> </view> </van-popup> <wxs module="m1"> function getWeekday(index) { switch (index) { case 0: return "周一" case 1: return "周二" case 2: return "周三" case 3: return "周四" case 4: return "周五" case 5: return "周六" case 6: return "周日" default: return "" } } module.exports.getWeekday = getWeekday </wxs> js代码data: { kebiao: { totalWeek: 16, currentWeek: 11, currentKe:{day:-1,no:-1,ke:{name:'',time:'',classroom:'',teacher:''}}, detail: [ { no:0, ke:[{name:"计算机组成与结构",time:"1-16周",classroom:"信工楼E536",teacher:"徐苏"}, {name:"",time:"",classroom:"",teacher:""}, {name:"计算机组成与结构",time:"1-16周",classroom:"信工楼E536",teacher:"徐苏"}, {name:"",time:"",classroom:"",teacher:""}, {name:"嵌入式系统",time:"1-16单周",classroom:"信工楼E536",teacher:"周聪"}, {name:"",time:"",classroom:"",teacher:""}, {name:"党课",time:"1-13单周",classroom:"党校B302",teacher:"张三"}] }, }, }, 使用Vant组件 van-popup、van-icon 体验小程序 主页-->小玩艺-->课表日历 [图片]
2021-11-25 - 小程序实现高性能虚拟列表优化+节流+分页请求(固定高度)
场景引入 为什么需要用到高性能虚拟列表+节流+分页请求的优化? 当有场景需求为需要将大量数据(10000条)呈现在一页上,我们不断下拉访问,页面中有大量的数据列表的时候,用户会不会有不好的体验?会不会出现滚动不流畅而卡顿的情况?会不会因卡顿而出现短暂的白屏现象(数据渲染不成功)? 通过微信开发者工具自带的调试器->Network页面,我们可以观察到当有长列表时如果不使用高性能虚拟列表+节流+分页请求的优化,会出现以下问题: FPS:每秒帧数,图表上的红色块表示长时间帧,很可能会出现卡顿。 CPU:CPU消耗占用,实体图越多消耗越高。 NET:网络请求效率低,一次性请求10000条的渲染效率远远低于分1000次,每次请求10条数据 内存:滑动该列表时明显能看到内存消耗大。 总结:如果需要将大量数据(10000条)呈现在一页上,可以通过高性能虚拟列表+节流+按需请求分页数据并追加显示。 优化的具体实现可拆分为以下需求(将一个大问题拆分为一个个小问题并逐个去解决): 不把长列表数据一次性全部直接显示在页面上。 截取长列表一部分数据用来填充屏幕容器区域。 长列表数据不可视部分使用使用空白占位填充。 监听滚动事件根据滚动位置动态改变可视列表。 监听滚动事件根据滚动位置动态改变空白填充。 分页从服务器请求数据,将一次性请求所有数据变为滚动到底部才再次向服务器发送获取数据的请求 开始实战 此次实例我设定每个元素的固定高度为210rpx [图片] (1)首先计算屏幕内的容积最大容量(即屏幕一次性可以容纳多少个高度为210rpx的元素) 调用wx.createSelectorQuery()的api [图片] 上图是id为scrollContainer的组件,滚动时触发函数handleScroll() [代码]// 计算容器的最大容积,onReady中触发,即初次渲染时触发 getContainSize() { wx.createSelectorQuery().select('#scrollContainer').boundingClientRect(function (rect) { rect.id // 节点的ID rect.dataset // 节点的dataset rect.left // 节点的左边界坐标 rect.right // 节点的右边界坐标 rect.top // 节点的上边界坐标 rect.bottom // 节点的下边界坐标 rect.width // 节点的宽度 rect.height // 节点的高度 }).exec((option) => { // console.log(~~(option[0].height / this.data.oneHeight) + 2); this.data.containSize = ~~(option[0].height / this.data.oneHeight) + 2; }) }, [代码] 调用api后返回的option中的height就是小程序页面的视口高度,除以oneHeight(210rpx)就是能容纳的个数,用[代码]~~[代码]来对结果进行向下取整(实际能容纳的应+1),由于在滚动时会出现上一个元素在上边界还没完全消失,第四个元素就从下边界进入视口了,因此最大容纳量应再+1。即实际容纳量应该如图最后一行代码所示+2. [图片] (2)监听滚动事件动态截取数据 监听用户滚动、滑动事件,根据滚动位置,动态计算当前可视区域起始数据的索引位置 startIndex,再根据containsize,计算结束数据的索引位置 endIndex,最后根据 startIndex与endIndex截取长列表所有数据a11Datalist 中需显示的数据列表 showDatalist。 PS:下列代码中函数handleScroll()最下面的[代码]this.setDataStartIndex(data);[代码]才是滚动时真正进行的滚动事件动态截取数据,上面那些代码用途在文章后面部分再详细介绍(节流)。 [代码]// 定义滚动行为事件方法 handleScroll(data) { if (this.data.isScrollStatus) { this.data.isScrollStatus = false; // 节流,设置一个定时器,1秒以后,才允许进行下一次scroll滚动行为 let mytimer = setTimeout(() => { this.data.isScrollStatus = true; clearTimeout(mytimer); }, 17) this.setDataStartIndex(data); } }, [代码] [代码]// 执行数据设置的相关任务, 滚动事件的具体行为 setDataStartIndex(data) { // console.log("scroll active") this.data.startIndex = ~~(data.detail.scrollTop / this.data.oneHeight); // 通过scrollTop滑动后距离顶部的高度除以每个元素的高度,即可知道目前到第几个元素了 this.setData({ showDataList: this.data.allDatalist.slice(this.data.startIndex, this.data.endIndex) }) // 动态截取实际拥有10000条数据的数组中下标为startIndex到endIndex的数组出来呈现在前端页面上 // 容器最后一个元素的索引 if (this.data.startIndex + this.data.containSize <= this.data.allDatalist.length - 1) { this.setData({ endIndex: (!this.data.allDatalist[this.data.endIndex]) ? this.data.allDatalist.length - 1 : this.data.startIndex + this.data.containSize // 滚动到底部了吗,是的话那就将endIndex设置为9999,不然的话设置为startIndex+视口最大容量 }) } else { console.log("滚动到了底部"); this.data.pageNumNow++; // 例如一次性从数据库拿10条数据赋值到allDataList,如果滚动到底部(即allDataList所有数据都已经呈现了),那就再次向服务器发送请求获取数据库中的下10条数据 this.addMes(); // 该函数内就写你实际向数据库请求时的代码,请求成功后拼接到allDataList即可 console.log(this.data.allDatalist.length) } }, [代码] (3)使用计算属性动态设置上下空白占位 我们设置了根据容器滚动位移动态截取ShowDatalist 数据,现在我们滚动一下发现滚动2条列表数据后,就无法滚动了,这个原因是什么呢? 在容器滚动过程中,因为动态移除、添加数据节点丢失,进而强制清除了顶部列表元素DOM节点,导致滚动条定位向上移位一个列表元素高度,进而出现了死循环根据 startIndex和endIndex的位置,使用计算属性,动态的计算并设置,上下空白填充的高度样式blankFi11Sty1e,使用padding或者margin 进行空白占位都是可以的 PS:由于小程序没有computed,所以为了使用计算属性,得另外引入封装好computed的包,引入computed组件的教程我放在最后,我使用的是官方推荐的computed,且注意使用该插件时,不要加[代码]this -> this.data[代码],直接data即可 [代码]computed: { // 定义上空白高度 topBlankFill(data) { // console.log("change") return data.startIndex * data.oneHeight; }, // 定义下空白高度 BottomBlankFill(data) { return (data.allDatalist.length - data.endIndex) * data.oneHeight; }, // 定义一个 待显示的数组列表元素 showDataList(data) { // console.log(data.allDatalist.slice(data.startIndex, data.endIndex)) return data.allDatalist.slice(data.startIndex, data.endIndex) }, }, [代码] (4)下拉置底自动请求加载数据 下方代码中[代码]setDataStartIndex()[代码]函数末尾的if-else便是判断是否已经滚动到现有全部数据的allDataList数组是否已经滚动到底部,全部呈现完了。 如果是那就执行else部分,请求的页数pageNumNow+1,然后调用addMes()请求数据,然后将新请求到的数据进行拼接到allDataList上 [代码]// 执行数据设置的相关任务, 滚动事件的具体行为 setDataStartIndex(data) { // console.log("scroll active") this.data.startIndex = ~~(data.detail.scrollTop / this.data.oneHeight); this.setData({ showDataList: this.data.allDatalist.slice(this.data.startIndex, this.data.endIndex) }) // 容器最后一个元素的索引 if (this.data.startIndex + this.data.containSize <= this.data.allDatalist.length - 1) { this.setData({ endIndex: (!this.data.allDatalist[this.data.endIndex]) ? this.data.allDatalist.length - 1 : this.data.startIndex + this.data.containSize }) } else { console.log("滚动到了底部"); this.data.pageNumNow++; this.addMes(); console.log(this.data.allDatalist.length) } }, // 根据接口数据来给数组添加真实数据 addMes: function () { this.list() .then(res => { // console.log(res.result.data); // 将接口获取到得所有数据存储起来 this.data.allDatalist = this.data.allDatalist.concat(res.result.data); // 设置初始显示列表 this.setData({ showDataList: this.data.allDatalist.slice(0, 5) }) }) }, // 以下为获取数据 list: function () { let pageNum = this.data.pageNumNow; let pageSize = 20; // console.log('当前请求的页码为:' + pageNum); return new Promise((resolve, reject) => { wx.cloud.callFunction({ name: 'teacher', data: { action: 'list', pageNum, pageSize } }).then(res => { resolve(res) }).catch(err => { reject(err) }) }) }, [代码] 到此处,高性能虚拟列表+分页请求的优化已经搞定了,下面开始节流的优化 (5)滚动事件节流定时器优化 由于监听滚动事件触发对应函数方法的频率是极高的,因此页面节流优化是必须的。 方法:在data中声明一个属性scro11State用来记录滚动状态,只有scro11State值为true的时候才会具体执行 PS:下面代码中定时器设置为17ms的原因是,由于小程序没有web的requestAnimationFrame的api,无法作滚动事件节流请求动画帧优化,因此可以手动计算显示器的帧率,大概一帧在17ms,因此定时器设置为17ms [代码]// 定义滚动行为事件方法 handleScroll(data) { //节流部分代码 if (this.data.isScrollStatus) { this.data.isScrollStatus = false; // 节流,设置一个定时器,1秒以后,才允许进行下一次scroll滚动行为 let mytimer = setTimeout(() => { this.data.isScrollStatus = true; clearTimeout(mytimer); }, 17) //节流部分代码 this.setDataStartIndex(data); } }, [代码] 到这为止,节流也搞定啦,觉得从中学到了许多的小伙伴不妨点个赞。 小程序如何引入computed计算属性请参考我的这篇文章:https://developers.weixin.qq.com/community/develop/article/doc/000a4442bd44c84e740d6b6b051413 在后续我也会将高性能虚拟列表+节流的优化封装成一个插件,给小伙伴们直接使用,欢迎关注我以便及时获取到我文章的更新~ 在这里也推荐一篇防抖和节流的性能优化知识介绍的文章:https://segmentfault.com/a/1190000018428170 觉得有帮助的小伙伴欢迎点赞,有其他问题也欢迎在评论区提出
2021-11-11 - css中 伪类伪元素前面有没有空格的区别
css中 伪类伪元素前面有没有空格的区别 :hover和 :hover(前面多一个空格)的区别 :hover 表示所选择元素的整个元素 而 :hover(前面多一个空格)表示所选择的元素的子元素才起作用(本身不起作用) [代码]//html <div class="demo"> <span class="demo1">111</span> <span class="demo">222</span> </div> //css .demo { background-color: #fd7d8c; margin-left: 20px; } .demo:hover { background-color: #2911fc; } /*.demo :hover { background-color: #2911fc; } */ [代码] .demo:hover ** 正常情况下** [图片] 鼠标悬浮在主demo上(当子元素有相同名称时,不会被选择到) [图片] 鼠标悬浮在右上角子demo上 [图片] .demo :hover(前面多一个空格) 鼠标悬浮在主demo上(此时是主元素demo是没有悬浮效果的) [图片] 鼠标悬浮在子元素上(不一定要同名 只要是demo的子元素即可) [图片][图片] 总结 不只是hover 其它伪类和伪元素也是一样的,其实在css选择器中空格就代表是选择子元素,像平时用的 .class1 .class2,是选择到class1中的子元素class2。以此类推!
2021-11-24 - JavaScript-this
JavaScript-this 分享 最近又在this的指向中迷惑了,虽然之前学过一次,丝毫不影响再次掉坑,这次又捋了一遍,加深影响! 其实在javascript中this的指向主要有四种 默认绑定(全局对象 严格模式下为undefined) 隐式绑定(谁调用this指向谁) 显示绑定(call bind apply) new绑定 ! 而在箭头函数里面,这四种都不适合,因为ES6规定箭头函数是没有this的,所以它只能继承,继承外层作用域中的this,也就是要看箭头函数所定义使用的地方(宿主对象)。 网上找了好多this指向的资料,个人觉得这两个总结得很好,大家可以看一下,结合两篇文章,相信大家对this得指向问题会更加清晰。 js 中的this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解_toforu.com-CSDN博客 ①this指向四种:默认,隐式,显示call apply bind,new构造函数②优先级③new构造函数有无return④new做了什么⑤严格模式指向_lazylYYY的博客-CSDN博客 下面是阮一峰老师讲得this原理,大家也可以看一下! JavaScript 的 this 原理 - 阮一峰的网络日志 (ruanyifeng.com) Node中的this和浏览器中的this的区别 大家注意,虽然node和浏览器都是js的运行环境,但node中this和浏览器中的this还是稍有不同的 浏览器中的全局对象是window,node的全局对象是global 浏览器最外层的的this是windows,而node中最外层的this并不是全局对象global,而是module.export [代码]//在node环境下 console.log(this==global) //false console.log(this==module.exports) //true this.a=1; console.log(module.exports.a) //1 b=2; //不用var声明的变量 默认作为全局对象的属性 [代码] module.export是es6实现模块化的新特性,大家有兴趣可以了解一下。 vue中的this 在vue中的this,其实在vue的设计转化下,this一般指向vue实例 在Vue所有的生命周期钩子方法(created,mounted等等)和普通方法(method里面定义),this指向调用它的vue实例 其他情况可以参考js的this指向 作用域和原型的区别 当访问某个变量是,就是要通过作用域链去查找了,而访问某个对象的属性时,这个时候就时要通过原型链去找了了。 这其实也是有关变量和属性的区别,变量是属于作用域的范畴,而属性其实应该是属于原型的范畴 当我们定义一个变量是,比如 var a=1;其实a不是this里面的属性,a只是一个变量。 变量和属性一个很大的区别就是,属性可以删除,而变量不可以 [代码]var a=1; this.b=2; delete a; //false delete this.b ;//true [代码]
2021-11-22 - 最佳实践丨云开发CloudBase多环境管理实践
背景云开发 CloudBase 提供环境复制能力,方便开发者进行多环境下项目开发。 环境资源复制实践 (环境A -> 环境B)函数资源1、云函数代码从 A 环境对应函数拷贝,注意代码中写死的环境 ID A需手动修改为 B。 若函数中使用 Node SDK 且 使用当前环境,建议写法: const cloudbase = require("@cloudbase/node-sdk") const app = cloudbase.init({ env: cloudbase.SYMBOL_CURRENT_ENV // 自动选取当前环境 }) 2、函数属性配置如 内存,超时时间,环境变量,定时触发器,VPC,公网访问配置,函数对应云接入配置&鉴权,CLS 日志配置均拷贝,无需开发者操作。 3、函数层不会复制,需手动在 B 中新建层。 数据库资源数据库复制时仅在新环境中创建出同名的空集合,表数据需用户在控制台中手动导 (A环境库导出,导入到B环境库)。数据库安全规则,索引设置均拷贝,开发者无需操作。云存储资源云存储配置如权限配置,缓存配置均拷贝,无需用户操作具体的文件资源,需用户手动导(A 环境导出文件资源,导入到 B 环境)推荐实践: 安装 cloudbase cli 工具并登陆npm i -g @cloudbase/cli tcb login 下载 A 环境全部文件至本地# 下载全部文件 tcb storage download / localPath --dir -e A 上传本地文件至 B 环境tcb storage upload localPath -e B 多环境项目开发实践1. 开发环境,生产环境区分基于环境复制能力,可以快速搭建开发 dev 和生产 prod 两套环境(免去了重复建表,重复建函数的操作)。 参考文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/concepts/environment.html 操作实践 1、云函数端使用SDK 时采用取动态环境写法(类似函数资源复制),避免写死环境 ID。 小程序侧示例const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) 腾讯云侧示例const cloudbase = require("@cloudbase/node-sdk") const app = cloudbase.init({ env: cloudbase.SYMBOL_CURRENT_ENV // 自动选取当前环境 }) 2、客户端在开发及生产环境下,分别指定对应环境 ID 即可 wx.cloud.init({ // 此处请填入环境 ID, 环境 ID 可打开云控制台查看 env: config.ENV // dev or prod }) 更进一步,为免去人为操作带来的风险,可通过工程化的方式配置环境 ID,如 开发模式下配置 dev 环境 ID,生产模式下配置为 prod 环境 ID,具体实现此处不展开。2. 项目协同开发同环境下,多人协作开发时容易产生脏数据问题,各开发人员可基于项目初始环境复制出各自开发环境,各环境下调试开发互不影响。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite
2021-11-17 - 小程序云开发学习笔记3
做完了简单的增删改查操作之后,可以更深入地进行查找操作了,熟悉后端的同学都知道,后端80%是增删改查,而增删改查中80%又是查找,所以查找的排序和定量查找是基本功,现在就来实现一下小程序的按价格排序和条件筛选等功能。 首先,先写两个简陋的按钮表示排序吧: [图片] 然后就是绑定按钮点击事件orderByAsc()和orderByDesc(),在其中进行排序操作,排序代码如下: wx.cloud.database.collection("表名") .orderBy("排序所根据的字段", "asc") //第二个参数升序是asc,降序是desc .get() .then(res => { }) .catch(err => { }) 可以看到排序功能就是在原先的查询功能只上加入了orderBy(),这里需要注意的是,不同于mysql,sql server等数据库语言,微信中的orderBy()是放在查询操作get()前面的,orderBy()放到get()后面会报错。另外,排序的话一般是数字,如果你所要排序的字段是字符串型的数字,是没法进行排序的,需要将该字段类型从”string”变为”number”。 知道佮排序了,那么这么一来,初始无序查询需要一串代码,升降序查询有需要两串代码,难免让人感到冗余,所以,需要精简代码,用if进行条件判断,wxjs也支持语句的直接拼接,使得我们需要写的代码减少很多: [图片] 有时候数据没有现在演示地这么少,我们需要进行分页操作,而分页操作的前提就是获取指定条数的数据,这是我们可以在get()之前加limit(),limit括号中的参数表示返回的条数。limit 在小程序端默认及最大上限为 20,在云函数端默认及最大上限为 1000。如果需要数据的起始位置不是第一个,而是要返回地2-5个的数据,可以在limit()后面加上.skip(),skip中的参数表示从第几个开始读。如图:这就返回了第二个到第四个的数据: [图片] 最后,还有一种查询就是对数值的区间进行查询,比如查询价格为50-100的书,查询价格大于200的书,和msyql等传统数据库类似,微信小程序也是在原先的.where()里面添加操作,实现条件查询,不过,微信小程序“大于小于”不是用>和<,而是使用了特殊的符号,具体如下: [图片] 上图为单条件查询,如果要进行多条件查询,也要用and连接,示例如下: [图片] 但这样的话and带来的括号太多了,很容易出错,可以采用以下方法简洁表示与关系: [图片] 甚至直接省略and [图片] 其他逻辑运算法or,not,nor的用法与and类似,大家可以查询微信开发文档学习: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/Command.html#Command-and-expressions-any-Command
2021-11-12 - 云开发Aggregate.geoNear聚合查询没有skip?
Aggregate.geoNear(options: Object): AggregateAggregate聚合查询的参数没有skip,请问如何分页查询? [代码]db.collection('users') .aggregate() .skip(5) .end()[代码]是先查出多少条再剔除多少条,如果数据库记录数大于100我就无法拿到100之后的数据了(因为最大只能取100条记录);分页应该是先剔除前几条再查询后面的多少条(skip应该在limit前面) 已解决: result = await db.collection('tableName').aggregate() .geoNear({ distanceField: 'distance', // 输出的每个记录中 distance 即是与给定点的距离 spherical: true, near: db.Geo.Point(lng, lat), query: { id: id, }, //limit: 10 geoNear里面也有limit,我就是加了这个才没达到效果 }).skip(currentPage * 10).limit(10).end()
2019-12-17 - 云函数里调用aggregate.geoNear查询结果不正确?
db.collection('activity').aggregate() .geoNear({ distanceField: 'distance', // 输出的每个记录中 distance 即是与给定点的距离 spherical: true, near: db.Geo.Point(114.06058996826175, 22.550311425481723), maxDistance: 300 * 1000//300公里 // query: { // docType: 'geoNear', // }, // key: 'location', // 若只有 location 一个地理位置索引的字段,则不需填 // includeLocs: 'location', // 若只有 location 一个是地理位置,则不需填 }) .end() 以上同一份代码,在小程序端运行和云函数(版本:~2.3.0)中调用返回结果不一样,小程序端返回的距离为,distance: 6617.587567221755这个是正确的,而云函数返回结果为distance: 0.0010375484183725177 这个距离是错误的。为什么给定同样的点,查询同样的数据,两者返回结果不一样?
2020-09-09 - 小程序里附近的人功能实现,云开发数据库实现附近的人,按照位置远近排序,附近多少公里内的好友
文末有源码 最近好多同学问石头哥附近的人如何实现。今天呢,就借助这篇文章,给大家做一个系统的解答。 老规矩,先看效果图 [图片] 可以看到我们在地图上显示了附近的一些标记点。 接下来就教大家如何实现附近的位置。 一,创建数据 首先我们查询附近的人的时候,需要先有附近人的位置,也就是经纬度。这里我以几个城市的经纬度为例。 [图片] 大家可以自行百度查找你所需要的经纬度。 这里经纬度查到后,我们需要把这些位置信息存到数据库里。 1,注意存位置时必须是Point类型 [图片] 如上图所示,我们可以直接在云开发数据库里添加位置信息,类型是geopoint类型。 如我添加的北京的位置如下 [图片] 这里按照这样的类型,自己多添加几个城市的经纬度。当然现实开发中,应该是添加附近人的位置(经纬度) 2,批量添加(选看) 如果感觉一个添加比较麻烦的话,可以先添加一条,然后导出为json,自己在json里批量编辑。 [图片] 一定要注意_id不能重复,格式要保持一致。这样你批量编辑后,再把这个json重新导入到数据即可。 [图片] 批量导入不是本节的重点,就不在讲解。我这里默认你已经添加好位置信息了 3,修改数据表权限 我们这里要让所有人可以读取到数据,必须设置权限如下 [图片] 4,创建对应字段的索引(**重要) 我们如果想查找位置信息,必须设置存位置的对应字段对应的索引才可以。 如果不创建索引直接查询,会报如下错误。 [图片] 所以我们必须要先创建对应的索引。如下图所示添加索引 [图片] 然后做如下设置即可 [图片] 到这里准备工作就做完了。 二,查找附近的人 我们查找附近的人,肯定是想按照排序由近到远的显示附近的人在地图上,所以这里我们就要用到geoNear做聚合查询。 geonear查询有两种方式,建议大家用Aggregate.geoNear [图片] 详细介绍大家可以自己去看官方文档 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/aggregate/Aggregate.geoNear.html 用这个的主要好处是,我们可以拿到附近人离自己的距离 [图片] 这个距离在做附近的人时很重要的。既然可以直接拿到,能省很多事的。具体的代码后面给大家列出来,我们先继续往下学习 三,获取当前的位置 我们要做附近的人肯定要先获取自己的位置,获取自己的位置就用wx.getLocation即可,对应文档如下 https://developers.weixin.qq.com/miniprogram/dev/api/location/wx.getLocation.html 这个使用之前必须要在app.json里注册权限,如果不注册权限,就会报如下提示 [图片] 所以在app.json里写如下代码 [代码] "permission": { "scope.userLocation": { "desc": "获取位置所需要的权限" } } [代码] [图片] 四,获取附近人的经纬度 代码其实很简单,如下 [图片] 这样我们就可以按照由近到远的顺序获取附近的人了,结果如下 [图片] 石头哥是在杭州,可以看到几个城市里离杭州最近的是上海159公里的距离。 五,在地图上显示附近的人 既然位置都已经查询到了,我们就可以在地图上显示了,地图上显示用到了map组件的markers [图片] 对应的js代码如下 [图片] 下面我把完整的代码贴出来给到大家 wxml代码 [代码]<map markers="{{markers}}" show-location scale="4" style="height: 100vh;" /> [代码] js代码 [代码]Page({ data: { markers: [] }, onLoad() { wx.getLocation({ //1,获取自己的位置 type: 'wgs84', success: res => { const latitude = res.latitude const longitude = res.longitude console.log('当前在杭州的经纬度', res.longitude, res.latitude) //2,查找附近的人 let markers = [] const db = wx.cloud.database() const $ = db.command.aggregate db.collection('location').aggregate() .geoNear({ distanceField: 'juli', // 与给定点的距离 spherical: true, near: db.Geo.Point(longitude, latitude), //当前自己的位置 }).end() .then(res => { console.log('位置', res) res.list.forEach(item => { markers.push({ longitude: item.location.coordinates[0], latitude: item.location.coordinates[1] }) }) // 地图上的标记点 this.setData({ markers }) }) } }) } }) [代码] 好了,到这里就带大家完整的实现了地图上显示附近人的功能了。如果觉得石头哥文章还不错,欢迎关注点赞。
2021-08-24 - 小程序环境共享入门/跨账号环境调用
先上文档:官方文档 一般碰到问题比较多的是怎么起步, !important 为了方便讲清楚概念,我们称呼主账号(提供资源/函数的小程序)为【资源方】,要进行跨账号环境调用资源的账号为【调用方】 (需小程序公共库 2.13.0 或以上)(需 wx-server-sdk 版本大于或等于 2.3.0) 确保要共享资源的两个小程序在同个主体下面 例如同个公司、个人开发者等。 用微信开发者工具,打开【资源方】,点击头像右侧的云开发 点击云开发弹框右侧的设置 选择顶部tab栏目的拓展功能 点击环境共享的开通 点击添加共享,并输入【调用方】的appid,根据需求勾选权限并确认 用微信开发者工具,打开【调用方】 点击云开发确认可以看到【资源方】的云函数和云存储,如果这一步没看到共享的内容,返回确认前面的步骤 在【资源方】的云开发-设置-环境设置-环境名称处,创建开通按量计费(免费)的云开发环境。 配置部分非常繁琐,但是到这里就结束了。接下来是开发部分 【资源方】云函数部分改动的内容,入口 [代码]const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() console.log(event) console.log(wxContext) // 跨账号调用时,由此拿到来源方小程序/公众号 AppID console.log(wxContext.FROM_APPID) // 跨账号调用时,由此拿到来源方小程序/公众号的用户 OpenID console.log(wxContext.FROM_OPENID) // 跨账号调用、且满足 unionid 获取条件时,由此拿到同主体下的用户 UnionID console.log(wxContext.FROM_UNIONID) return { errCode: 0, errMsg: '', auth: JSON.stringify({ // 自定义安全规则 // 在前端访问资源方数据库、云函数等资源时,资源方可以通过 // 安全规则的 `auth.custom` 字段获取此对象的内容做校验, // 像这个示例就是资源方可以在安全规则中通过 `auth.custom.x` 获取 x: 1, }), } } [代码] 【调用方】云函数部分改动的内容 !important 这边前端建议对所有云函数入口做一个统一的路由,不要到处callFunction。 在统一路由那边,我一般是这样写在utils里,或者写到route.js里,这样你只要改一个地方,就可以修改整个小程序的所有请求 [代码]async function requestCloud(requestName, data) { wx.cloud.init({ traceUser: true }) let c1 = new wx.cloud.Cloud({ // 资源方 AppID resourceAppid: 'wx5d8d765e252720eb', // 资源方环境 ID resourceEnv: 'production-rjntq', }) await c1.init() return await c1.callFunction({ name: requestName, data: data }) } [代码] 如果是用promise的写法,就外面再包一层,举例,把上面的代码,加上下面的部分,塞到同一个新的function里, [代码]return new Promise(function (resolve, reject) { requestCloud(requestName, data).then(res => { if (res.result.code == 0) { resolve(res.result.data) return } else { hideLoading() wx.showToast({ title: res.result.msg, icon: 'none' }) reject(res.result) return } }).catch(err => { reject(err) return }) }) [代码] 纯前端的部分就结束了,这边遇到问题最多的是,没想到这玩意儿要初始化两次云函数,一次是自己的,一次是【资源方】的。 如果是【调用方】也有自己的后台,想要调用【资源方】的云函数, [代码]const cloud = require('wx-server-sdk') exports.main = async (event) => { // 声明新的 cloud 实例 var c1 = new cloud.Cloud({ // 资源方 AppID resourceAppid: 'wxe0e2656d74f0bff3', // 资源方环境 ID resourceEnv: 'test-f96b31', }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 await c1.init() // 完成后正常使用资源方的已授权的云资源 return c1.callFunction({ name: '函数名', data: {}, }) } [代码] 好了,你再编译一下自己的【调用方】代码,应该就ok了。 我现在卡在怎么以【调用方】身份,在wxml中例如Image src里直接访问【资源方】的图片资源。按理说云开发面板上是可以看到的,以及也授权了,应该是可以直接打开才对,但是渲染层就是打不开。有发现的朋友麻烦联系我微信:SH-Yushi。十分感谢。 已经通过后台批量替换cloud前缀为https格式解决。 现在还剩如何直接操作【资源方】云数据库。保底方案是通过云函数绕过去 update by 2020-11-05 有人私聊我怎么直接访问资源方图片。 我把我批量替换的方法写下面,供后人参考 [代码]function changeImageUrl(imageUrl) { if (this.isEmpty(imageUrl)) { return } let la = 'cloud://production-rjntq.' let ra = '1302413556/' let start = imageUrl.indexOf(la) if (start < 0) { return imageUrl } let end = imageUrl.indexOf(ra) - 1 let front = 'https://' + imageUrl.substring(la.length, end + ra.length) + '.tcb.qcloud.la' let back = imageUrl.substring(end + ra.length) return front + back } [代码] 记得去云存储里面看一眼自己的图片,右键查看详情,查看下载地址,你的那串数字应该和我的肯定是不同的,到我封的方法里把数字替换成自己的。 这边的问题是,虽然小程序开放了跨资源调用,也在文档里说可以跨资源访问图片,但是开发组告诉我们暂时不支持直接调用cloud://的path。因此想到转成https。好在他们的格式都是固定的,所以可以批量替换。 这边有个小坑,在动手操作之前还是仔细看自己的图片存储目录,因为每个人的 文件目录层级不同,因此前缀格式会有略微差别。但是思路是一致的。
2020-11-05 - 【跨账号共享环境】云开发cloudfunctions无法指定环境?
操作系统:macOS,微信开发工具:1.05.2105170 Stable,云开发控制台:v1.3.89 按云开发模版新建项目后,无法指定云开发环境,云开发环境是另一个小程序共享过来的(同一主题),试了很多办法都不行,在新建项目的时候,明明显示有绑定的云开发环境 [图片] 云函数本地调试,也显示未指定环境[图片] 但这app.js和云函数index.js内都指定了云开发环境 [图片] [图片]
2021-05-29 - 跨账号环境共享时,资源方环境中FROM_OPENID为什么是空的?
开发环境: 1、云开发、跨账号资源共享 2、基础库版本:2.14.1 问题描述: 有两个小程序云开发环境,分别是环境1和环境2,在环境1云函数中调用环境2的云函数时,在环境2的云函数中国cloud.getWXContext()返回对象中FROM_OPENID是空的,cloud.getWXContext(),的调用位置为环境2的云函数中(见最后一张图)。 相关源码: (云开发环境1)下图为环境1中云函数:cloud和cloud2分别是调用方小程序云开发环境和资源方云开发环境 [图片] (云开发环境1)下图为上图调用的函数, [图片] (云开发环境1)下图为上图调用的函数,里面实现调用云开发环境cloud2云函数的调用 [图片] (云开发环境2)下图为云开发环境2中被上图调用的云函数 [图片] (云开发环境2)下图为上图调用的函数,cloud.getWXContext()函数返回的FROM_OPENID值是空的 [图片]
2021-06-09 - 没有企业付款到零钱的功能
商户号开通了又3个多月了,请问要什么样的的资质才能开通? mch_id 1601486350
2020-11-09 - 分包 主包可以加载分包目录下的js文件吗?
请问 主包可以加载分包目录下的js文件吗?
2020-10-16 - 微信云托管如何实现一套代码对应多个环境
微信云托管 是由微信团队联合腾讯云推出的一站式后端云服务。对于应用开发采用前后端分离架构的场景,云托管可做到免运维免域名、免服务器管理、防 DDoS 攻击和境外加速等,从代码管理到 CI/CD 流水线部署发布,提供全链路、低成本、企业级的云原生解决方案。 PC 端访问 https://cloud.weixin.qq.com 即可立即开始使用微信云托管。 前言在项目开发时,通常具有多种环境,用来在开发的各个流程阶段进行作用,比如预发、生产。 环境的不同,对应的数据库等配置信息就有所不同,就必须要针对配置信息的变更,对应创建单独的代码仓库,流水线对应一个代码仓库,这样维护成本太大。 在这里主要是配置信息的变化,本篇主要介绍如何在项目代码中动态感知所处的微信云托管环境,进而给予正确的配置信息。 一、环境变量微信云托管在运行项目服务过程中,带入了一个环境变量,名称为 [代码]CBR_ENV_ID[代码] ,意义是当前运行服务所处的 [代码]微信云托管环境ID[代码] 所以在项目内部,可以通过获取 [代码]CBR_ENV_ID[代码] 变量,来感知当前项目在哪个环境运行。 二、划分环境微信云托管的环境可以建立多个,单个环境下可以有多个服务,和单独的数据库以及对象存储,在客观条件上做了隔离。 所以可以用 [代码]微信云托管环境[代码] 为维度进行隔离,创建项目开发的各个环境(预发、生产) 最终我们可以得到一个清单,样式如下: werun—id1 = '预发' werun—id2 = '生产' 其中 [代码]werun—id[代码] 为微信云托管的环境ID。 三、配置代码接下来将上述清单的配置写到项目代码中,以 [代码]nodejs[代码] 为例: 假设一开始我们的配置 [代码]config.js[代码] 是这样的: module.exports = { "text":"开发环境" } 为了简化在这里只有一个text,实际应用中,应该是数据库、存储、网络通信的配置信息,也就是需要根据环境区分的信息都要写到一块。 接下来,加上 [代码]CBR_ENV_ID[代码] 变量,来进行改造,代码如下: const config = { 'werun—id1': { text:'预发环境' }, 'werun—id2': { text:'生产环境' }, NO: { text: '本地环境' } } module.exports = function(env=null){ const key = env || process.env.CBR_ENV_ID if(config[key] != null){ return config[key] } else { return config.NO } } 以上代码,将各个环境的配置信息统一囊括,然后根据 [代码]CBR_ENV_ID[代码] 变量来进行分发,如果没有预设的环境配置,则返回本地测试配置。 以上就已经做完了项目的动态配置改造,接下来我们开始配置流水线。 四、配置流水线预发、生产环境在整体上应该保持一致,但在数据层面予以区分,所以两个环境在整体配置方面区别不大。 根据自己的业务需求,配置一个或若干个服务,选配数据库或对象存储等。 接下来我们重点来讲流水线的配置差异: [图片] 以上是新建流水线的信息框,两个环境的同一服务,应该配置相同的代码仓库、分支和目标目录。 在触发条件中,可以根据开发流程的不同稍有改变,合理选择是变更就触发还是定时触发;比如你每周三发布版本,那就按照计划选择定时触发就可以。 在发布策略中,因为生产环境要求稳定,所以不能直接全量发布,可以选择仅构建镜像,或者构建版本。 预发环境可以直接发全量发布,便于直接开始系统的体验测试,在测试通过后,就可以安排生产环境的灰度发布了。 五、环境流程建议每个团队都有自己的开发测试流程和环境,请根据自身情况合理理解本篇内容。 传统开发模式下的开发测试流程在转移到微信云托管时需要有些变通,以下是一些建议: 1、开发环境:团队开发人员产出代码自测的环境,一般可以在本地PC开一个Docker容器挂载项目代码开发。容器镜像保证团队统一性,将极大的减少因为环境问题导致的联调失败。 2、测试、联调、回归环境:需要项目整体性的测试体验,在这里分形态来建议,项目形态上有 单一服务型 和 微服务 两种形态: 单一服务型:不管项目有多大,就一个服务运行,更新需要全部替换;这种情况下不建议将其放在微信云托管环境中联调测试,直接在本地测试和联调开发的效率会更高一些。因为只有内部业务互相调用,不涉及其他的服务(API接口、数据库、对象存储除外,本身属于外联通性)。 微服务形态:项目被拆分成不同的模块,每个模块服务独立运行,共同作用,更新只需要替换变更的;这种情况下建议放在微信云托管中测试,也就是变更的模块在经过测试,表现稳定的情况下,将其部署到微信云托管中,和其他服务一起做整体测试。 3、预发、生产环境:微信云托管中开单独的环境,如果需要预发环境连接生产环境数据库,可以设置两个环境内网联通,然后内网连接对应的数据库。 现在体验微信云托管,立享1个月免费额度:https://cloud.weixin.qq.com/cloudrun
2021-10-11 - 小程序怎么取消已关联的商户号 然后再关联到一个新的商户号?
小程序取消已关联的商户号,怎么取消? 然后再关联到一个新的商户号,对现有小程序有什么影响,需要重新发布或者做迁移吗? 看帖子说 https://kf.qq.com/faq/18083032QvyY180830yAZVnM.html 这个解绑方式已经失效!
2021-10-11 - 微信小程序调用oss上传至阿里云
1.首先创建oss密钥 这里我新建了一个名字 命名为config.js var fileHost = "https://*****************/"; //你的阿里云地址最后面跟上一个/ 在你当前小程序的后台的uploadFile 合法域名也要配上这个域名 var config = { //aliyun OSS config uploadImageUrl: `${fileHost}`, // 默认存在根目录,可根据需求改 AccessKeySecret: '*******', // AccessKeySecret 去你的阿里云上控制台上找 OSSAccessKeyId: '********', // AccessKeyId 去你的阿里云上控制台上找 timeout: 87600 //这个是上传文件时Policy的失效时间 }; module.exports = config 2.创建上传配置文件 我这里命名为upload.js const env = require('config.js'); //配置文件,在这文件里配置你的OSS keyId和KeySecret,timeout:87600; // 以下算法在https://gitee.com/chenkuo1997/oss-wx.git 复制到同upload.js目录下即可 const base64 = require('base64.js');//Base64,hmac,sha1,crypto相关算法 require('hmac.js'); require('sha1.js'); const Crypto = require('crypto.js'); /* *上传文件到阿里云oss *@param - filePath :图片的本地资源路径 *@param - dir:表示要传到哪个目录下 *@param - successc:成功回调 *@param - failc:失败回调 */ const uploadFile = function (filePath, dir, successc, failc) { if (!filePath || filePath.length < 9) { wx.showModal({ title: '图片错误', content: '请重试', showCancel: false, }) return; } console.log('上传图片.....'); //图片名字 可以自行定义, 这里是采用当前的时间戳 + 150内的随机数来给图片命名的 console.log(dir) const aliyunFileKey = dir+ new Date().getTime() + Math.floor(Math.random() * 150) + '.png'; const aliyunServerURL = env.uploadImageUrl;//OSS地址,需要https const accessid = env.OSSAccessKeyId; const policyBase64 = getPolicyBase64(); const signature = getSignature(policyBase64);//获取签名 console.log(env) wx.uploadFile({ url: aliyunServerURL,//开发者服务器 url filePath: filePath,//要上传文件资源的路径 name: 'file',//必须填file formData: { 'key': aliyunFileKey, 'policy': policyBase64, 'OSSAccessKeyId': accessid, 'signature': signature, 'success_action_status': '200', }, success: function (res) { console.log(res) if (res.statusCode != 200) { failc(new Error('上传错误:' + JSON.stringify(res))) return; } successc(aliyunServerURL+aliyunFileKey); }, fail: function (err) { err.wxaddinfo = aliyunServerURL; failc(err); }, }) } const getPolicyBase64 = function () { let date = new Date(); date.setHours(date.getHours() + env.timeout); let srcT = date.toISOString(); const policyText = { "expiration": srcT, //设置该Policy的失效时间,超过这个失效时间之后,就没有办法通过这个policy上传文件了 "conditions": [ ["content-length-range", 0, 5 * 1024 * 1024] // 设置上传文件的大小限制,5mb ] }; const policyBase64 = base64.encode(JSON.stringify(policyText)); return policyBase64; } const getSignature = function (policyBase64) { const accesskey = env.AccessKeySecret; const bytes = Crypto.HMAC(Crypto.SHA1, policyBase64, accesskey, { asBytes: true }); const signature = Crypto.util.bytesToBase64(bytes); return signature; } module.exports = uploadFile; 3.小程序页面调用的时候 const uploadImage = require('../../utils/upload'); /** * 上传图片 */ uploadImage() { let that = this; let applyRefundImgList = that.data.applyRefundImgList wx.chooseImage({ count: 5 - applyRefundImgList.length, //处理图片上传数量 (可根据自身需求配置) success: function(res) { wx.showLoading({ title: '上传中', mask: true }) for (let index = 0; index < res.tempFilePaths.length; index++) { //applyrefund 则为bucket下的文件夹路径 可根据自身需求进行分类 uploadImage(res.tempFilePaths[index], `applyrefund/${shopUuid}/`, function(res) { wx.hideLoading() applyRefundImgList.push(res) that.setData({ applyRefundImgList }) }, function(res) { wx.hideLoading() } ) } } }) },
2021-07-06 - 小程序开发笔记:Array或Object类型数据库操作
数据库交互时数组写法:['ArrayName.'+[ArrayIndex]]:value 数据库交互时对象写法:['ObjectName.'+[ObjectIndex]]:value [图片] 获取值后使用时写法: [图片]
2021-10-03 - 微信支付普通分账、服务商分账申请高比例流程及材料(维护中,暂不对外,恢复时间待定)
普通分账申请高比例流程和资料 【务必按照邮件模板要求申请,附件名称规范、邮件主体带有申请表格】 流程:向微信支付运营邮件申请 注意:要如实描述场景。 别问行不行了,行不行看图不就知道了,不行的找找自己的原因,最后预祝大家申请成功。 [图片]
2024-07-15 - QQ小程序如何调用微信小程序云开发数据库?
QQ小程序的代码(备注:本人使用的是web sdk。云开发资源是在微信小程序开发工具里面开通的,但是腾讯云cloudbase里面可以进行查看和配置): 参考:https://docs.cloudbase.net/api-reference/webv2/adapter.html#di-2-bu-pei-zhi-an-quan-ying-yong-lai-yuan import cloudbase from "./utils/outer/cloudbase.full.1.4.1.js"; import adapter from './utils/outer/cloudbase-adapter-qq_mp.js'; App({ //已经参考https://docs.cloudbase.net/api-reference/webv2/adapter.html#di-2-bu-pei-zhi-an-quan-ying-yong-lai-yuan onLaunch: function () { cloudbase.useAdapters(adapter); cloudbase.init({ env: 'envid', appSign: 'appSign', appSecret: { appAccessKeyId: '1', appAccessKey: '666fd52' } }) const db = cloudbase.database(); db.collection("articles").where( {_id:"articleID"} ).get().then(res=>{ console.log(res) }); } }) 最后报错:{code: "DATABASE_PERMISSION_DENIED", message: "Permission denied", requestId: "13128a9ea14d1"}
2021-03-12 - 云开发web端调用SDK时报错?
云开发web端调用SDK,CDN 地址:https://res.wx.qq.com/open/js/cloudbase/1.0.0/cloud.js,cloud.init()初始化后使用数据库返回错误码:errCode: -501023 permission denied | errMsg: Unauthenticated access is denied 有大佬遇到过吗,求教 [图片] [图片]
2020-06-17 - QQ小程序如何调用微信支付?可不可以调用?
这里仅仅指在QQ小程序中直接拉起微信支付,当然场景是这样想的,目前没方案,希望神通广大的万能网友们能给个方案或者指点一下?
2020-12-22 - 单向链表是什么,前端如何实现(数据结构系列)
1. 什么是链表 链表类似于火车:有一个火车头,一堆车厢。火车头会通过节点连接车厢,车厢间也会使用节点依次串联起来。 [图片] 链表的数据结构 [图片] 2. 链表与数组对比 共同点 都是用于存储一系列元素 不同点 数组 优点: 编程语言基本都会内置数组结构,使用简单方便 用[代码][][代码]访问对应的元素,访问的时间复杂度为O(1) 缺点: 固定大小:大多数编程语言定义数组时,需要先声明大小。(js不需要,js底层会根据数组大小,自动伸缩管理容量) 连续空间:在开辟内存时,需要申请连续的空间 数组越往前[代码]增删[代码]元素时,时间复杂度越高。(js底层为保证连续性,需要移动增删位置之后的元素) 链表 优点: 申请空间时不需要事先确定大小 不需要申请连续的内存空间,内存利用率更高 越往前对链表增删,时间复杂度越低,增删开头时间复杂度为O(1) 缺点 访问任意位置元素时,需要从头开始遍历,时间复杂度为O(N) 3. 链表代码的实现 [代码]class Node { constructor(value) { this.value = value; this.next = null; } } class LinkedList { constructor() { this.count = 0; this.head = null; } // 链表尾部追加元素 append(value) { let newNode = new Node(value); if (this.head === null) { this.head = newNode; } else { let currentNode = this.getElementAt(this.count - 1); currentNode.next = newNode; } this.count++; } // 在指定位置插入元素,越界返回false,表示插入失败 insert(index, value) { if (index < 0 || index > this.count) { return false; } let newNode = new Node(value); if (this.head === null) { this.head = newNode; } else if (index === 0) { newNode.next = this.head; this.head = newNode; } else { let lastNode = this.getElementAt(index - 1); newNode.next = lastNode.next; lastNode.next = newNode; } this.count++; return true; } // 移除指定元素。若找不到该元素,则返回false,表示移除失败 remove(value) { let lastNode = null; let currentNode = this.head; while (currentNode) { if (currentNode.value === value) { if (lastNode === null) { this.head = this.head.next; } else { lastNode.next = currentNode.next; } this.count--; return true; } lastNode = currentNode; currentNode = currentNode.next; } return false; } // 移除指定位置上的元素。若找不到该位置,则返回false,表示移除失败 removeAt(index) { if (index < 0 || index > this.count - 1) { return false; } if (index === 0) { this.head = this.head.next; } else { let lastNode = this.getElementAt(index - 1); lastNode.next = lastNode.next.next; } this.count--; return true; } // 返回指定位置的节点,越界返回undefined getElementAt(index) { if (index < 0 || index >= this.count) { return undefined; } let currentNode = this.head; for (let i = 0; i < index; i++) { currentNode = currentNode.next; } return currentNode; } // 返回元素的索引值,若没改元素则返回-1 indexOf(value) { let currentNode = this.head; let index = 0; while (currentNode) { if (currentNode.value === value) { return index; } currentNode = currentNode.next; index++; } return -1; } // 返回链表元素个数 size() { return this.count; } // 返回当前是否为空链表 isEmpty() { return this.count === 0 } // 清空链表 clear() { this.count = 0; this.head = null; } // 输出链表每项元素 toString() { let arr = []; let currentNode = this.head; while (currentNode) { arr.push(currentNode.value); currentNode = currentNode.next; } return arr.join(",") } } [代码] 4. 线上代码 戳我查看,含测试代码 5. 应用场景 结合数组缺点、链表优点,可得出当频繁对数据增删的话,用链表效率更高;若是频繁读取数据,则数组更优。 队列数据结构,之前是基于数组,在出栈时效率没有基于链表的效率高,后面基于双向链表再实现队列。 6. 相关文章 上一篇:前端es6实现优先级队列 下一篇:双向链表(暂未更新) 相关篇:es6实现队列数据结构(基于数组)
2021-09-27 - 抽奖小程序抽奖算法的实现,附赠算法库
简易思路如下: 1、获取参与抽奖的人员列表,组合为数组 2、使用数组乱序算法,将抽奖列表大乱 3、根据一定规则取出打乱后的数组中的抽奖人员,这些就是中奖人员了 乱序算法思路具体如下,使用Fisher–Yates 洗牌算法: Fisher–Yates 算法由Fisher和Yates这两个人的发明的,一开始只是用来人工混排(真实荷官,现场发牌……)一组数字序列,原始算法的步骤非常容易理解。 写下从 1 到 N 的数字(一副扑克)取一个从 1 到剩下的数字(包括这个数字)的随机数 k——(从中间抽一沓,)从低位开始,得到第 k 个数字(这个数字还没有被取出),把它写在独立的一个列表的最前面一位重复第 2 步,直到所有的数字都被取出第 3 步写出的这个序列,现在就是原始数字的随机排列说通俗点就是: 一副扑克从中间随便取一沓(取的越少,越容易实现乱序,算法中只取一个)把取出的牌放在旁边,堆成一堆重复2、3步骤,直到手里没牌第三步开始另外堆起来的牌,就足够乱了算法库地址如下,需要自取: https://www.npmjs.com/package/lodash
2020-12-13 - 云函数端Aggregate聚合操作limit默认20条限制
云函数端 聚合 Aggregate 经过以下代码验证: [代码]// test_record 集合中 openid 为 test user openid 的实际上有超过20条数据的 db .collection('test_record') .aggregate() .match({ _openid: 'test user openid', }) .end(); [代码] 最后实际只返回了20条数据,所以说在云函数端如果某些接口返回确定以及肯定超过20条的话,还是老老实实加上 [代码].limit(100)[代码] ‘保命’吧。 改成如下代码即可超过默认20条的限制: [代码]db .collection('test_record') .aggregate() .match({ _openid: 'test user openid', }) .limit(100) // important .end(); [代码] 上限可以达到1万条数据 经过 stop eating 同学点拨,原来可以直接淦到10000 参考文档 Collection.limit(value: number): Collection [图片] 获取一个集合的数据 [图片]
2020-07-23 - 请教lookup pipeline如何查询二级数组?
表一: "_id":"2a0398605f1114*****d69a167ebf9ed" "alist":[[100,101],[200,201],[300,301]] 表二: "_id":"xxxxxxxxx" "name":"A" "aAid":100 ------------ "_id":"xxxxxxxxx" "name":"B" "aAid":101 -------------- "_id":"xxxxxxxxx" "name":"C" "aAid":200 依次 现在想查找出这样的结果 alist:[ { {aAid:100,name:"A"}, {aAid:101,name:"B"}, }{ {aAid:200,name:"C"}, {aAid:201,name:"C"}, } ... ] alist如果是一级数组,我知道直接用.match(_.expr($.in(['$aAid', '$$alist'])))这种方式就可以了,但是二级数组我就抓瞎了.
2021-09-22 - [开盖即食]小程序不同环境切换和自动判断的四个方案
[图片] 实际开发中,我们经常会遇到“开发版,体验版/QA版,正式版”多个环境的切换,它们对应的是不同的后端接口地址,开发中容易把dev环境的接口地址提交到git,这时候合并没注意的话,就容易出现事故。 总结的4个方案,避免此类事件的发生,做到自动化判断环境,而非手动修改~ 先说下核心逻辑:就是判断是否线上,其他环境就算配错,风险也没线上环境大 1、使用官方API getAccountInfoSync判断 [代码]const Hosts = { mock: 'http://192.168.1.996:007', dev: 'https://dev.qq.com', //开发环境 hidden: "https://hidden.qq.com", //预发布环境 prod: 'https://prod.qq.com', //线上环境 qa: 'https://qa.qq.com', }; const { envVersion } = wx.getAccountInfoSync().miniProgram; let baseUrl = ""; switch (envVersion) { case 'develop': baseUrl = `${Hosts.dev}`; break; case 'trial': baseUrl = `${Hosts.hidden}`; break; case 'release': baseUrl = `${Hosts.prod}`; break; default: baseUrl = `${Hosts.prod}`; break; } console.log(baseUrl) [代码] [图片] 可以看到IDE工具的返回值是 [代码]develop[代码],这样就可以根据API自动切换不同的场景了。 文档:https://developers.weixin.qq.com/miniprogram/dev/api/open-api/account-info/wx.getAccountInfoSync.html 2、通过本地特殊文件来判断 在本地添加一个大家约定好的文件,比如local.txt,如果调试的时候有这个文件,就是当前环境。 [代码]let _ENV = 'prod'; const fileManager = wx.getFileSystemManager(); try { fileManager.accessSync('/local.txt'); _ENV = 'dev'; } catch (e) { } if (_ENV === 'prod') { //这里其实只判断是否是线上 baseUrl = Hosts.prod; } [代码] 通过 .gitignore 忽略这个文件,这样就不会合并到master分支。 当然这种方法,其实不一定用判断环境,也可用来测试一些其他功能 3、采用CI机器人自动化切换环境发布 [代码]//写好脚本,在每次上传前或者预览前修改 host文件 const fs = require('fs'); fs.readFile('./host.js','utf8',function(err,data){ // 字符串转数组 let txt = "let baseUrl = Hosts.prod"; fs.writeFile('./host.js',txt,function(err){ console.log(arr); }) }) //上传脚本 const ci = require('miniprogram-ci') ;(async () => { const project = new ci.Project({ appid: 'wxsomeappid', type: 'miniProgram', projectPath: 'the/project/path', privateKeyPath: 'the/path/to/privatekey', ignores: ['node_modules/**/*'], }) const uploadResult = await ci.upload({ project, version: '1.1.1', desc: 'hello', setting: { es6: true, }, onProgressUpdate: console.log, }) console.log(uploadResult) })() [代码] 无论是上传发布,还是预览都可以通过node重新配置host,或者修改你想修改的文件。 官方miniprogram-CI文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/ci.html 4、本地配置中的自定义处理命令 [图片] 在这里配置node或者yarn的一些方法,也可以解决这个问题 在上传审核前,重新设置host为线上的后端地址,同时这个也可以用于一些多端开放切换和执行一些自定义的node策略。 总结 如果只是解决 “多人开发中切换本地环境导致误传git和发布后指向后端url错误"的话,用第一种方法即可。 其他方法除了解决此类问题,还可为解决预编译,第三方框架等问题提供一种思路。 如有疑问请留言~ 觉得有用,请点个赞哦,让我继续分享更有动力~
2021-09-10 - 使用对象存储,优雅的管理前端切图
背景: 众所周知,小程序主包&分包大小限制(2M),目前很多开发者都为之抓耳挠腮。 然而目前在现在的开发过程中,一张前端切图动辄十多 K, 几十 K,甚至有些 gif 图片上百 K 都是很常见的。 我们从包大小的角度思考,如果空间都拿来存储图片资源,属实是非常浪费的。 相信每个小程序开发者都对此有过思考,也看到有的项目已经用网络图片来解决此问题。但如果只是拷贝 url 拿过来用,使用起来会比较繁琐,维护起来比较麻烦,怎么更优雅的用,更优雅的维护是我们所追求的。 这里利用腾讯云提供的 对象存储服务(COS)& cos-node-js-sdk,讲一下我对于前端切图素材的解决方案。 思路: 在对象存储上,其实是跟电脑的硬盘一样,是有目录结构的。 如图我们以 ui-material 文件夹为根目录,所有的图片资源都放在这个目录里。 [图片] [图片] 而文件目录本身就是树形结构的,放一张图可能更容易理解 [图片] 所以就有了思考,想能不能把所有图片以 JSON 的方式存储 ? key=>文件名,value=>图片地址 比如我想访问 HP.jpg 这张图片,我希望是这样的访问 Images.HP 而访问 active/IBM.jpg 我希望是 Images.active.IBM 也就是说把文件的 path 以 json 的方式输出就是不是就 ok 了 ~ 下面使用 node 实现 ~ 安装依赖: npm install cos-nodejs-sdk-v5 path-parse ramda lodash --save -dev 1.连接 cos : // index.js const COS = require("cos-nodejs-sdk-v5"); const PathParse = require("path-parse"); const Ramda = require("ramda"); const Lodash = require("lodash"); const fs = require("fs"); /** 这里是一些对象存储的配置信息,可以在腾讯云控制台中查看 */ const COS_SECRETID = "xxxx填你自己的"; const COS_SECRETKEY = "xxxx填你自己的"; const COS_BUCKET = "log-1255751956"; const COS_REGION = "ap-hongkong"; const COS_ENCODING_TYPE = "url"; /** 访问地址 */ const COS_ACCESS_DOMAIN = "https://log-1255751956.cos.ap-hongkong.myqcloud.com"; /** UI素材资源目录 */ const UI_MATERIAL_PATH = "ui-material"; /** 获取 cos 实例 */ const cos = new COS({ SecretId: COS_SECRETID, SecretKey: COS_SECRETKEY, }); 2.获取 ui-material 文件夹下的文件列表,通过官方提供的 cos-node-js-sdk /** 获取 Bucket 信息 * 我们只获取 ui-material 这个文件夹下的文件 * 所以后面后面统一上传素材文件到这个目录下*/ cos.getBucket( { /**指定存储桶 */ Bucket: COS_BUCKET, /**指定地区 */ Region: COS_REGION, /**指定文件夹 */ Prefix: `${UI_MATERIAL_PATH}/`, /**指定编码方式 */ EncodingType: COS_ENCODING_TYPE, }, (err, data) => { if (!err) { const { Contents } = data; console.log('Contents==>',Contents); } else { console.log("getBuket err", err); } } ); // 输出结果: [ { Key: 'ui-material/', LastModified: '2021-09-09T06:32:28.000Z', ETag: '"d41d8cd98f00b204e9800998ecf8427e"', Size: '0', Owner: { ID: '1255751956', DisplayName: '1255751956' }, StorageClass: 'STANDARD' }, { Key: 'ui-material/HP.png', LastModified: '2021-09-09T15:43:49.000Z', ETag: '"4363f2b9df8a0ec2de805ae2938571fb"', Size: '10990', Owner: { ID: '1255751956', DisplayName: '1255751956' }, StorageClass: 'STANDARD' }, { Key: 'ui-material/active/', LastModified: '2021-09-09T09:24:18.000Z', ETag: '"d41d8cd98f00b204e9800998ecf8427e"', Size: '0', Owner: { ID: '1255751956', DisplayName: '1255751956' }, StorageClass: 'STANDARD' }, { Key: 'ui-material/active/IBM.png', LastModified: '2021-09-09T15:44:31.000Z', ETag: '"b5974b615efa779c702a15316490f464"', Size: '3202', Owner: { ID: '1255751956', DisplayName: '1255751956' }, StorageClass: 'STANDARD' }, { Key: 'ui-material/active/moduleA/', LastModified: '2021-09-09T15:46:04.000Z', ETag: '"d41d8cd98f00b204e9800998ecf8427e"', Size: '0', Owner: { ID: '1255751956', DisplayName: '1255751956' }, StorageClass: 'STANDARD' }, { Key: 'ui-material/active/moduleA/intel.png', LastModified: '2021-09-09T15:46:12.000Z', ETag: '"edec5fe92c7d52cb69a40890eaa6a113"', Size: '5316', Owner: { ID: '1255751956', DisplayName: '1255751956' }, StorageClass: 'STANDARD' }, { Key: 'ui-material/order.png', LastModified: '2021-09-09T07:18:30.000Z', ETag: '"dd5cee76f07bc77f1e3b8b5e65e130da"', Size: '3106', Owner: { ID: '1255751956', DisplayName: '1255751956' }, StorageClass: 'STANDARD' } ] 我们可以清晰的看见 List 中的 Key 其实就是我们所需要的文件路径,我们只需要去解析这个 Key 就 ok 了 3.获取图片路径: const Key = 'ui-material/active/moduleA/intel.png'; const url = `${COS_ACCESS_DOMAIN}/${Key}` // 结果=====> https://log-1255751956.cos.ap-hongkong.myqcloud.com/ui-material/active/moduleA/intel.png 4.获取图片对应的 objcet props: const parsePath = PathParse(Key); /** 解析 path 信息 **/ const props = [ ...parsePath.dir.split('/') ,parsePath.name ]; props.shift(); /** 删除数组第一个元素,因为 ui-material 是根目录 ===> [ 'active', 'moduleA', 'intel' ] **/ 5.利用 loadsh.set 为对象属性赋值 let obj = {}; Lodash.set(obj, props, url); console.log(obj) /** =====> { active: { moduleA: { intel: 'https://log-1255751956.cos.ap-hongkong.myqcloud.com/ui-material/active/moduleA/intel.png' } } } **/ 好了到这里我们以经可以顺利的生成文件的 JSON 了,没错它是一个树形结构。 我们只需要把这个 JSON 给写入 js 文件,后续直接从这个文件拿数据就行了。 6.写入数据到 js 文件 const outputStr = `export default ${Ramda.toString(json)};`; fs.writeFileSync("./assets/data.js", outputStr); 7.运行脚本 node index.js 会得到这样一个 js 文件 export default { HP: "https://log-1255751956.cos.ap-hongkong.myqcloud.com/ui-material/HP.png", active: { IBM: "https://log-1255751956.cos.ap-hongkong.myqcloud.com/ui-material/active/IBM.png", moduleA: { intel: "https://log-1255751956.cos.ap-hongkong.myqcloud.com/ui-material/active/moduleA/intel.png", }, }, order: "https://log-1255751956.cos.ap-hongkong.myqcloud.com/ui-material/order.png", }; 8.在小程序里使用 [图片] // assets/index.js import Images from './data'; export { Images } 在 Page 里使用 import { Images } from '../../assets/index'; Page({ data: { // 这种方式引用有没有很爽 intelIcon: Images.active.moduleA.intel } }); 可以结合 npm script 或者 gulp 每次在图片更新时,重新执行脚本,更新 data.js 9.完整代码 const COS = require("cos-nodejs-sdk-v5"); const PathParse = require("path-parse"); const Ramda = require("ramda"); const Lodash = require("lodash"); const fs = require("fs"); /** 这里是一些对象存储的配置信息,可以在腾讯云控制台中查看到 */ const COS_SECRETID = "xxxx填你自己的"; const COS_SECRETKEY = "xxxx填你自己的"; const COS_BUCKET = "log-1255751956"; const COS_REGION = "ap-hongkong"; const COS_ENCODING_TYPE = "url"; /** 访问地址 */ const COS_ACCESS_DOMAIN = "https://log-1255751956.cos.ap-hongkong.myqcloud.com"; /** UI素材资源目录 */ const UI_MATERIAL_PATH = "ui-material"; /** 获取 cos 实例 */ const cos = new COS({ SecretId: COS_SECRETID, SecretKey: COS_SECRETKEY, }); /** 获取 Bucket 信息 * 我们只获取 ui-material 这个文件夹下的文件 * 所以后面后面统一上传素材文件到这个目录下*/ cos.getBucket( { /**指定存储桶 */ Bucket: COS_BUCKET, /**指定地区 */ Region: COS_REGION, /**指定文件夹 */ Prefix: `${UI_MATERIAL_PATH}/`, /**指定编码方式 */ EncodingType: COS_ENCODING_TYPE, }, (err, data) => { if (!err) { const { Contents } = data; const effectiveFile = filterEffectiveFile(Contents); const jsonObj = genJson(effectiveFile); outJsFile(jsonObj); } else { console.log("getBuket err", err); } } ); /** 输出 js 文件*/ function outJsFile(json) { try { const outputStr = `export default ${Ramda.toString(json)};`; fs.writeFileSync("./assets/data.js", outputStr); } catch (e) { console.log("writeFile file fail", e); } } /** 过滤出有效的 file */ function filterEffectiveFile(Contents) { /**过滤出 size > 0 的file */ return Contents.filter((_) => _.Size > 0).map((_) => { const url = `${COS_ACCESS_DOMAIN}/${_.Key}`; const parsePath = PathParse(_.Key); const props = [...parsePath.dir.split("/"), parsePath.name]; props.shift(); return { url, props, }; }); } /** 生成 json 对象 */ function genJson(fileList) { let objects = {}; fileList.forEach((file) => { /** 核心方法 可以看 loadsh 文档中的 set 方法 */ Lodash.set(objects, file.props, file.url); }); return objects; }
2021-09-10 - 环形进度条
动画效果一直都喜欢用setInterval,看到了文档提供了requestAnimationFrame,所以尝试了一下,下面是代码(组件)。 wxml [代码]<view class="progress-container"> <canvas class="canvas" type="2d" id="posterCanvas"></canvas> <view wx:if="{{showText}}" class="num-text"> <view class="num">{{number}}</view> <view class="text">{{text}}</view> </view> </view> [代码] js [代码]Component({ properties: { showText: { type: Boolean, value: true, }, percent: { type: Number, value: 0, }, number: { type: Number, value: 0, }, text: { type: String, value: '打卡天数', }, }, data: {}, attached() { this.value = 0; this.init(); }, methods: { init() { const query = this.createSelectorQuery(); query .select('#posterCanvas') .fields({ node: true, size: true }) .exec((res) => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const system = wx.getSystemInfoSync(); const dpr = system.pixelRatio; const ratio = system.windowWidth / 750; canvas.width = res[0].width * dpr; canvas.height = res[0].height * dpr; ctx.scale(dpr, dpr); // 设置圆环的宽度 ctx.lineWidth = parseInt(10 * ratio); // 设置圆环端点的形状 ctx.lineCap = 'round'; this.drawBottomColor(ctx, ratio); if (this.data.percent !== 0) { const renderLoop = () => { if (this.value <= this.data.percent) { this.render(ctx, ratio); canvas.requestAnimationFrame(renderLoop); } }; canvas.requestAnimationFrame(renderLoop); } }); }, render(ctx, ratio) { ctx.clearRect(0, 0, parseInt(750 * ratio), parseInt(240 * ratio)); this.draw(ctx, ratio); }, draw(ctx, ratio) { // 画底色 this.drawBottomColor(ctx, ratio); // 画高亮色 this.drawActiveColor(ctx, ratio, this.value++); ctx.stroke(); }, // 底色条 drawBottomColor(ctx, ratio) { ctx.beginPath(); ctx.strokeStyle = '#F8F8F8'; ctx.arc( parseInt(90 * ratio), parseInt(90 * ratio), parseInt(85 * ratio), -0.5 * Math.PI, 1.5 * Math.PI, false ); ctx.stroke(); }, // 高亮进度条 drawActiveColor(ctx, ratio, percent) { ctx.beginPath(); ctx.strokeStyle = '#00CBA3'; // 设置圆环的颜色 ctx.arc( parseInt(90 * ratio), parseInt(90 * ratio), parseInt(85 * ratio), -0.5 * Math.PI, -0.5 * Math.PI + Number(percent / 100) * 2 * Math.PI, false ); }, }, }); [代码]
2021-03-05 - [开盖即食]基于canvas的“刮刮乐”刮奖组件
[图片] 工作中有时候会遇到一些关于“抽奖”的需求,这次以“刮刮乐项目”举例,分享一个实战抽奖功能。 本人对之前网上流传的一些H5刮刮乐JS插件版本进行了一些改造,使其能适用于实际项目,并且支持小程序canvas 2D的新API,这里顺便提下2D API和实际H5 canvas中JS写法非常类似,只有少数不同。 [图片] 1、方法介绍: 1.1 刮刮乐JS组件 [代码]class Scratch { constructor(page, opts) { opts = opts || {}; this.page = page; this.canvasId = opts.canvasId || 'canvas'; this.width = opts.width || 300; this.height = opts.height || 300; this.bgImg = opts.bgImg || ''; //覆盖的图片 this.maskColor = opts.maskColor || '#edce94'; this.size = opts.size || 15, //this.r = this.size * 2; this.r = this.size; this.area = this.r * this.r; this.showPercent = opts.showPercent || 0.2; //刮开多少比例显示全部 this.rpx = wx.getSystemInfoSync().windowWidth / 750; //设备缩放比例 this.scale = opts.scale || 0.5; this.totalArea = this.width * this.height; this.startCallBack = opts.startCallBack || false; //第一次刮时触发刮奖效果 this.overCallBack = opts.overCallBack || false; //刮奖完触发 this.init(); } init() { let self = this; this.show = false; this.clearPoints = []; const query = wx.createSelectorQuery(); //console.log(this.canvasId); query.select(this.canvasId) .fields({ node: true, size: true }) .exec((res) => { //console.log(res); this.canvas = res[0].node; this.ctx = this.canvas.getContext('2d') this.canvas.width = res[0].width; this.canvas.height = res[0].height; //const dpr = wx.getSystemInfoSync().pixelRatio; //this.canvas.width = res[0].width * dpr; //this.canvas.height = res[0].height * dpr; self.drawMask(); self.bindTouch(); }) } async drawMask() { let self = this; if (self.bgImg) { //判断是否是网络图片 let imgObj = self.canvas.createImage(); if (self.bgImg.indexOf("http") > -1) { await wx.getImageInfo({ src: self.bgImg, //服务器返回的图片地址 success: function (res) { imgObj.src = res.path; //res.path是网络图片的本地地址 }, fail: function (res) { //失败回调 console.log(res); } }); } else { imgObj.src = self.bgImg; //res.path是网络图片的本地地址 } imgObj.onload = function (res) { self.ctx.drawImage(imgObj, 0, 0, self.width * self.rpx, self.height * self.rpx); //方法不执行 } imgObj.onerror = function (res) { console.log('onload失败') //实际执行了此方法 } } else { this.ctx.fillStyle = this.maskColor; this.ctx.fillRect(0, 0, self.width * self.rpx, self.height * self.rpx); } //this.ctx.draw(); } bindTouch() { this.page.touchStart = (e) => { this.eraser(e, true); } this.page.touchMove = (e) => { this.eraser(e, false); } this.page.touchEnd = (e) => { if (this.show) { //this.page.clearCanvas(); if (this.overCallBack) this.overCallBack(); this.ctx.clearRect(0, 0, this.width * this.rpx, this.height * this.rpx); //this.ctx.draw(); } } } eraser(e, bool) { let len = this.clearPoints.length; let count = 0; let x = e.touches[0].x, y = e.touches[0].y; let x1 = x - this.size; let y1 = y - this.size; if (bool) { this.clearPoints.push({ x1: x1, y1: y1, x2: x1 + this.r, y2: y1 + this.r }) } for (let item of this.clearPoints) { if (item.x1 > x || item.y1 > y || item.x2 < x || item.y2 < y) { count++; } else { break; } } if (len === count) { this.clearPoints.push({ x1: x1, y1: y1, x2: x1 + this.r, y2: y1 + this.r }); } //添加计算已清除的面积,达到标准值后,设置刮卡区域刮干净 let clearNum = parseFloat(this.r * this.r * len) / parseFloat(this.scale * this.totalArea); if (!this.show) { this.page.setData({ clearNum: parseFloat(this.r * this.r * len) / parseFloat(this.scale * this.totalArea) }) }; if (this.startCallBack) this.startCallBack(); //console.log(clearNum) if (clearNum > this.showPercent) { //if (len && this.r * this.r * len > this.scale * this.totalArea) { this.show = true; } this.clearArcFun(x, y, this.r, this.ctx); } clearArcFun(x, y, r, ctx) { let stepClear = 1; clearArc(x, y, r); function clearArc(x, y, radius) { let calcWidth = radius - stepClear; let calcHeight = Math.sqrt(radius * radius - calcWidth * calcWidth); let posX = x - calcWidth; let posY = y - calcHeight; let widthX = 2 * calcWidth; let heightY = 2 * calcHeight; if (stepClear <= radius) { ctx.clearRect(posX, posY, widthX, heightY); stepClear += 1; clearArc(x, y, radius); } } } } export default Scratch [代码] 1.2 JS 调用方法 [代码]new Scratch(self, { canvasId: '#coverCanvas', //对应的canvasId width: 600, height: 300, //maskColor:"", //封面颜色 showPercent: 0.3, //刮开多少比例显示全部,比如0.3为 30%面积 bgImg: "./cover.jpg", //封面图片 overCallBack: () => { //刮奖刮完回调函数 }, startCallBack: () => { //当用户触摸canvas板的时候触发回调 } }) [代码] 实际中还支持其他很多的配置项,比如缩放比例,刮开比例,放置区域等等,大家可以根据实际需求设置。 1.3 实际页面中的JS调用方法: [代码]//引入刮刮乐部分 import Scratch from './scratch.js'; const app = getApp() Page({ data: { firstTouch: 0, isOver: 0, }, onLoad() { let self = this; new Scratch(self, { canvasId: '#coverCanvas', width: 600, height: 300, //maskColor:"", //封面颜色 bgImg: "./cover.jpg", //封面图片 overCallBack: () => { this.setData({ isOver: "结束啦" }) //this.clearCanvas(); }, startCallBack: () => { this.setData({ firstTouch: "开始刮啦" }) //this.postScratchSubmit(); } }) }, //刮卡已刮干净 clearCanvas() { let self = this; console.log("over"); }, }) [代码] 1.4 HTML/CSS [代码]<-- html --> <view class="wrap"> <canvas class="cover_canvas" type="2d" disable-scroll="false" id='coverCanvas' bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd"></canvas> <image class="img" src="reward.jpg" mode="widthFix" /> </view> /* css */ .wrap { width: 600rpx; height: 300rpx; margin: 100rpx auto; border: 1px solid #000; position: relative; } .cover_canvas { width: 600rpx; height: 300rpx; z-index: 9; } .wrap .img { position: absolute; left: 0; top: 0; z-index: 1; width: 600rpx; height: 300rpx; } [代码] 这里注意 type=“2d” 写法,这里使用的是新的2D canvas。 2、注意事项 canvas一些效果不支持真机调试,直接预览就行了 如果刮奖结果是通过第一次触碰canvas触发的,这里的请求需要写一个同步方法 刮刮乐JS的配置会优先判断bgImg这个属性,再判断maskColor 需要反复刮奖,可以反复new 它。 3、代码片段 地址: https://developers.weixin.qq.com/s/RxiaHam574or 建议将IDE工具升级到 1.03.24以上,避免一些BUG [图片] 觉得有用,请点个赞,这是我继续分享的动力~
2021-02-18 - Request 请求封装思路
请求封装前言 通常在小程序项目中,微信已经提供了网络请求API,但是往往这点并不能满足我们,所以有了下面的封装 Requset请求往往我们有很多需求,此处简单罗列需求并代码实现 根据不同版本小程序 [代码]develop --> trial --> release[代码] 版本使用不同域名 request 拦截器,对当前请求发送前做相应处理 可控式请求 loading 动画,同时支持扩展自定义 loading 文字提示 根据后端返回请求响应结果做相应处理(列如 210 未登录,220 登录失效等需要与后端共同定义) 笔者使用的是 [代码]Taro[代码] 进行请求封装,解释步骤基本在代码中有描述,笔者没有对每个方法拆分详细讲述 笔者是根据之前分享的是Taro模板中的请求封装,拆分讲述思路,可供参考借鉴 前言废话太多下面上代码 开始 首先在项目 [代码]pages[代码] 平级创建 [代码]service[代码] 文件夹,然后分别建立3个文件分别是: [代码]interceptors.ts[代码] – 请求响应拦截器。 [代码]import Taro from '@tarojs/taro' import { HTTP_STATUS } from './status' // 笔者这里引入 mobx 是对 未登录,登陆失效 等做处理 import cartStroe from '../store/user' const customInterceptor = (chain:any) => { const requestParams = chain.requestParams return chain.proceed(requestParams).then((res:any) => { // 清除 loading if(requestParams.loading) Taro.hideLoading() switch(res.statusCode) { case HTTP_STATUS.SUCCESS: const result = res.data if(res.data.code === 200) { // 接口调通且无异常赋予success标识 result.success = true } else { // 请求接口错误提示,可通过参数中加入 openErrTips: false 关闭 if(requestParams.openErrTips && result.msg) Taro.showToast({ title: result.msg, icon: 'none' }) // 登录过期或未登录 需要与后端共同定义 if(result.code === 210 || result.code === 220) { // 跳转登陆 清空用户信息等 处理 cartStroe.setStatus(false) cartStroe.setUser({}) Taro.showToast({ title: (result.code === 210 ? '未登录,请先登陆' : '登录信息失效,请重新登陆' ), icon: 'none' }) Taro.navigateTo({ url: '/pages/login/index' }) return Promise.reject(result) } } return result case HTTP_STATUS.CREATED: return Promise.reject('请求成功并且服务器创建了新的资源') case HTTP_STATUS.ACCEPTED: return Promise.reject('接受请求但没创建资源') case HTTP_STATUS.CLIENT_ERROR: return Promise.reject('服务器不理解请求的语法') case HTTP_STATUS.AUTHENTICATE: return Promise.reject('请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应') case HTTP_STATUS.FORBIDDEN: return Promise.reject('服务器拒绝请求') case HTTP_STATUS.NOT_FOUND: return Promise.reject('服务器找不到请求的网页') case HTTP_STATUS.SERVER_ERROR: return Promise.reject('(服务器内部错误) 服务器遇到错误,无法完成请求') case HTTP_STATUS.BAD_GATEWAY: return Promise.reject('(错误网关) 服务器作为网关或代理,从上游服务器收到无效响应') case HTTP_STATUS.SERVICE_UNAVAILABLE: return Promise.reject('(服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。') case HTTP_STATUS.GATEWAY_TIMEOUT: return Promise.reject('(网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求') default: console.log('请开发者检查请求拦截未匹配到错误,返回statusCode :>> ', res.statusCode) break } }) } // Taro 提供了两个内置拦截器 // logInterceptor - 用于打印请求的相关信息 // timeoutInterceptor - 在请求超时时抛出错误。 const interceptors = [customInterceptor, Taro.interceptors.logInterceptor] export default interceptors [代码] [代码]status.ts[代码] – HTTP 通用响应状态码提示(方便开发者寻找请求错误源)。 [代码]export const HTTP_STATUS = { // 成功处理了请求,一般情况下都是返回此状态码 SUCCESS: 200, // 请求成功并且服务器创建了新的资源 CREATED: 201, // 接受请求但没创建资源 ACCEPTED: 202, // 服务器不理解请求的语法 CLIENT_ERROR: 400, // 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应 AUTHENTICATE: 401, // 服务器拒绝请求 FORBIDDEN: 403, // 服务器找不到请求的网页 NOT_FOUND: 404, // (服务器内部错误) 服务器遇到错误,无法完成请求 SERVER_ERROR: 500, // (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应 BAD_GATEWAY: 502, // (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。 SERVICE_UNAVAILABLE: 503, // (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求 GATEWAY_TIMEOUT: 504 } /** * 此处有替补实现方式 * 使用键值对直接通过状态码取值 * const HTTP_STATUS = { * '200': '请求服务器端成功', * '201': '' * } * if(HTTP_STATUS[res.statusCode]) * console.log(HTTP_STATUS[res.statusCode]) * else * console.log(`请开发者检查请求拦截未匹配到错误,返回statusCode :>> ${res.statusCode}`) */ [代码] [代码]index.ts[代码] – 封装请求导出供调用。 [代码]import Taro from '@tarojs/taro' import interceptors from './interceptors' interceptors.forEach(interceptorItem => Taro.addInterceptor(interceptorItem)) // 模块,命名空间,基础接口声明,此处不作解释 自行了解,结尾提供文档指引 declare namespace RequestProps { interface Method { 'GET', 'POST', 'PUT', 'DELETE' } interface Options { url: string, method: keyof Method, data: any, loading?: boolean, loadingTitle?: string, contentType?: string, openErrTips?: boolean } interface requestParams { url: string, method: keyof Method, data: any, header: any, loading?: boolean, loadingTitle?: string, contentType?: string, openErrTips?: boolean } } /** * 获取版本 retrun 对应环境域名 * develop: '开发版', trial: '体验版', release: '正式版' * 支持扩展 - 思路 可通过 process.env.NODE_ENV 判断当前打包是 生产模式或工厂模式 进而判断 适合多环境 dev -> beta -> uat -> pro * @returns 域名 */ const getVersion = () => { // @ts-ignore switch (__wxConfig.envVersion) { case 'develop': return 'http://develop.gavinpeng.club' case 'trial': return 'http://trial.gavinpeng.club' case 'release': return 'http://release.gavinpeng.club' default: return 'http://develop.gavinpeng.club' } } class Request { baseOptions(options: RequestProps.Options) { let { url, method, data } = options // 过滤 扩展属性 let { loading, loadingTitle, contentType, openErrTips, ...rest } = data if(loading) Taro.showLoading({ title: loadingTitle || '加载中...', mask: true }) const requestParams: RequestProps.requestParams = { url: getVersion() + url, method, data: rest, header: { // 支持自定义 contentType 'content-type': contentType || 'application/json', // Token 'Authorization': Taro.getStorageSync('token') // 此处支持扩展,可通过请求 data 参数中加入 header 对象,在上面过滤 语法糖 ...header 此处就不做过多解释,需要的自行添加了解 // ...header }, // 请求是否带 loading, 传递到 请求响应拦截器 清除 loading loading, openErrTips } return Taro.request(requestParams) } get(url:string, data:any) { return this.baseOptions({ url, method:'GET', data }) } post(url:string, data:any) { return this.baseOptions({ url, method:'POST', data }) } } export default new Request() [代码] 使用方式(笔者对接口模块区分接口管理) 在项目 [代码]pages[代码] 平级创建 [代码]api[代码] 文件夹(统一管理接口) 在 [代码]api[代码] 建立 [代码]index.ts[代码] (导出接口) [代码]import * as test from './test' const Api = { // 测试模块 ...test, // xxx 模块 } // 导出所有接口 export default Api [代码] 在 [代码]api[代码] 建立测试模块接口列表 [代码]test.ts[代码] [代码]/* * @Author: Gavin * @CreateTime: xxxx * @Describe: 测试模块相关接口 */ // 引入封装后的请求方法 import request from '@/service/index' /** * 测试 * @param params * @returns */ export const isTest = (url:string, data:any) => { return request.post(url, data) } [代码] 在 [代码]page[代码] 页面引入接口 [代码]import Api from '@/api/index'[代码] 并使用接口 [代码]Api.isTest({ id: 1, name: 'Gavin', // 扩展参数 - 是否需要 loading 非必传默认:false loading: true, // 扩展参数 - 是否自定义 loadingTitle 非必传默认:加载中...(注:没有开启 loading 此参数无效) loadingTitle: '自定义加载提示', // 扩展参数 - 是否自定义 contentType 非必传默认:application/json contentType: 'x-www-form-urlencoded', // 扩展参数 - 是否需要请求错误提示 openErrTips 非必传默认:false openErrTips: true, // 扩展参数 - 上面讲到的 自定义 header 需要的在封装中添加 // header: { } }).then((res:any) => { // 成功 }).catch((err:any) => { // 失败 }) [代码] 结尾 讲述过程中如有不对或错地方还请指出,欢迎各路大佬们指教 每一次分享是为了进步,在此过程中,讲述代码思路,亦可以很好的锻炼自身的表达能力 成长是生活的动力,加油吧[代码]新生代农民工[代码]们! TypeScript 中文文档指引 请求封装陈述源码地址
2021-09-07 - 小程序图片懒加载终极方案
效果图 既然来了,把妹子都给你。 [图片] 定义懒加载,前端人都知道的一种性能优化方式,简单的来说,只有当图片出现在浏览器的可视区域内时,才设置图片正真的路径,让图片显示出来。这就是图片懒加载。 实现原理监听页面的[代码]scroll[代码]事件,判读元素距离页面的[代码]top[代码]值是否是小于等于页面的可视高度 判断逻辑代码如下 [代码]element.getBoundingClientRect().top <= document.documentElement.clientHeight ? 显示 : 默认[代码] 我们知道小程序页面的脚本逻辑是在JsCore中运行,JsCore是一个没有窗口对象的环境,所以不能在脚本中使用window,也无法在脚本中操作组件。 所以关于图片懒加载就需要在数据上面做文章了。 页面页面上面只需要根据数据的某一个字段来判断是否显示图片就可以了,字段为Boolean类型,当为false的时候显示默认图片就行了。 代码大概长成这样 <view wx:for="{{list}}" class='item item-{{index}}' wx:key="{{index}}"> <image class="{{item.show ? 'active': ''}}" src="{{item.show ? item.src : item.def}}"></image> </view> 布局跟简单,[代码]view[代码]组件里面有个图片,并循环[代码]list[代码],有多少就展示多少 [代码]image[代码]组件的[代码]src[代码]字段通过每一项的[代码]show[代码]来进行绑定,[代码]active[代码]是加了个透明的过渡 样式 image{ transition: all .3s ease; opacity: 0; } .active{ opacity: 1; } 逻辑 本位主要讲解懒加载,所以把数据写死在页面上了 数据结构如下: [图片] 我们使用两种方式来实现懒加载,准备好没有,一起来快乐的撸码吧。 WXML节点信息 小程序支持调用createSelectQuery创建一个[代码]SelectorQuery[代码]实例,并使用[代码]select[代码]方法来选择节点,并通过[代码]boundingClientRect[代码]来获取节点信息。 wx.createSelectorQuery().select('.item').boundingClientRect((ret)=>{ console.log(ret) }).exec() 显示结果如下 [图片] 悄悄告诉你,小程序里面有个[代码]onPageScroll[代码]函数,是用来监听页面的滚动的。 还有个[代码]getSystemInfo[代码]函数,可以获取获取系统信息,里面包含屏幕的高度。 接下来,思路就透彻了吧。还是上面的逻辑, 扒拉扒拉直接写代码就行了,这里只写下主要的逻辑,完整代码请戳文末github showImg(){ let group = this.data.group let height = this.data.height // 页面的可视高度 wx.createSelectorQuery().selectAll('.item').boundingClientRect((ret) => { ret.forEach((item, index) => { if (item.top <= height) { 判断是否在显示范围内 group[index].show = true // 根据下标改变状态 } }) this.setData({ group }) }).exec() } onPageScroll(){ // 滚动事件 this.showImg() } 至此,我们完成了一个小程序版的图片懒加载,只是思维转变了下,其实并没有改变实现方式。我们来学些新的东西吧。 节点布局相交状态 节点相交状态是啥?它是一个新的API,叫做[代码]IntersectionObserver[代码], 本文只讲解简单的使用,了解更多请猛戳没错,就是点我 小程序里面给它的定义是节点布局交叉状态API可用于监听两个或多个组件节点在布局位置上的相交状态。这一组API常常可以用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见。 里面设计的概念主要有五个,分别为 参照节点:以某参照节点的布局区域作为参照区域,参照节点可以有多个,多个话参照区域取它们的布局区域的交集目标节点:监听的目标,只能是一个节点相交区域:目标节点与参照节点的相交区域相交比例:目标节点与参照节点的相交比例阈值:可以有多个,默认为[0], 可以理解为交叉比例,例如[0.2, 0.5]关于它的API有五个,依次如下 1、[代码]createIntersectionObserver([this], [options])[代码],见名知意,创建一个IntersectionObserver实例 2、[代码]intersectionObserver.relativeTo(selector, [margins])[代码], 指定节点作为参照区域,margins参数可以放大缩小参照区域,可以包含top、left、bottom、right四项 3、[代码]intersectionObserver.relativeToViewport([margin])[代码],指定页面显示区域为参照区域 4、[代码]intersectionObserver.observer(targetSelector, callback)[代码],参数为指定监听的节点和一个回调函数,目标元素的相交状态发生变化时就会触发此函数,callback函数包含一个result,下面再讲 5、[代码]intersectionObserver.disconnect()[代码] 停止监听,回调函数不会再触发 然后说下callback函数中的result,它包含的字段为 [图片] 我们主要使用[代码]intersectionRatio[代码]进行判断,当它大于0时说明是相交的也就是可见的。 先来波测试题,请说出下面的函数做了什么,并且log函数会执行几次 1、 wx.createIntersectionObserver().relativeToViewport().observer('.box', (result) => { console.log('监听box组件触发的函数') }) 2、 wx.createIntersectionObserver().relativeTo('.box').observer('.item', (result) => { console.log('监听item组件触发的函数') }) 3、 wx.createIntersectionObserver().relativeToViewport().observer('.box', (result) => { if(result.intersectionRatio > 0){ console.log('.box组件是可见的') } }) duang,揭晓答案。 第一个以当前页面的视窗监听了[代码].box[代码]组件,log会触发两次,一次是进入页面一次是离开页面 第二个以[代码].box[代码]节点的布局区域监听了[代码].item[代码]组件,log会触发两次,一次是进入页面一次是离开页面 第三个以当前页面的视窗监听了[代码].box[代码]组件,log只会在节点可见的时候触发 好了,题也做了,API你也掌握了,相信你已经可以使用[代码]IntersectionObserver[代码]来实现图片懒加载了吧,主要逻辑如下 let group = this.data.group // 获取图片数组数据 for (let i in this.data.group){ wx.createIntersectionObserver().relativeToViewport().observe('.item-'+ i, (ret) => { if (ret.intersectionRatio > 0){ group[i].show = true } this.setData({ group }) }) } 最后 至此,我们使用两种方式实现了小程序版本的图片懒加载,可以发现,使用[代码]IntersectionObserver[代码]来实现不要太酸爽
2020-05-12 - 请问小程序云函数能否设置动态定时触发?
现在的定时触发器只能固定在某些时间点自动触发,能不能在小程序端设置可修改的时间点,在某个时间点触发呢?谢谢
2020-11-17 - 华哥有约第二期:云开发“三大件”&环境共享
栏目介绍「华哥有约」是云开发Cloud Base官方出品的问答专栏,将由社区产品经理“华哥”分主题从不同维度解答云开发的热门门问题、剖析常见误区,帮助开发者更高效地使用云开发。 Q:云函数时区问题,怎么解决? 华哥:云函数中的时区为 UTC+0,不是UTC+8,可以通过语言的时间处理相关库或代码包(如 moment-timezone),识别 UTC 时间并转换为+8 区北京时间。 [图片] Q:云函数费用是按设置内存还是实际运行使用的内存计费? 华哥:云函数费用是按照函数配置内存和计费时长来计算费用的。 资源使用量 = 函数配置内存 X 运行计费时长。用户资源使用量,是由函数配置内存,乘以函数运行时的计费时长得出,其中配置内存转换为 GB 单位,计费时长由毫秒(ms)转换为秒(s)单位,因此,资源使用量的计算单位为 GBs(GB-秒)。计费时长最小粒度为100ms,不足100ms向上取整。例如,配置为 256MB 的函数,单次运行了 1760 ms,计费时长为 1800 ms,则单次运行的资源使用量为 (256/1024)*(1800/1000) = 0.45 GBs。针对函数的每次运行,均会计算资源使用量,并按月汇总求和,作为当月的资源使用量。 Q:跨账号环境共享,调用方上传文件资源至共享方后能否正常访问共享方的资源? 华哥:使用 new wx.cloud.Cloud 新建实例使用,再调用实例的 uploadFile 接口,上传资源成功后,另,B 目前无法通过 fileID 访问 A 的资源,可先使用 getTempFileURL 换临时链接的方式实现。 Q:跨账号环境共享,调用方无法在云文件目录右键选择环境且不可上传云函数? 华哥:目前共享的环境,不能在 cloudfunctions 上右键选择,另,因云函数权限很大,共享环境需要在资源方创建上传云函数,即创建空白函数可以在控制台进行,但是上传代码需要资源方在IDE上传。 Q:获取数据库集合数据Collection.get成功,但是返回空值? 华哥:读写数据库受权限控制限制,数据库数据权限默认是“仅创建者可读写”,如果业务需要所有用户可读,需开发者自行设置数据库数据权限为 “所有用户可读,仅创建者可读写” 。 Q:数据库无读写,为什么控制台资源使用情况会有数据库操作次数? 华哥:控制台对数据库菜单的操作,也会产生读次数。 Q:数据库可以创建多少个集合?单个集合大小限制? 华哥:预付费模式下,数据库集合个数取决于当前环境的配额方案,按量付费模式下可以创建800个集合;单个文档大小限制是16MB,但是不建议达到上限,最优解是越小越好,可拆分表格,有助于提升查询效率。 Q:数据库查询数据 limit 的使用? 华哥:limit 在小程序端默认及最大上限为 20,在云函数端默认及最大上限为 1000,取更多数据建议结合skip分页分批次获取。 const params = { // 从集合 data 中随便选点全部 _id:db.command.neq(null) } const MAX_LIMIT = 100; const total = (await db.collection('data').where(params).count()).total; const batchTimes = Math.ceil(total / MAX_LIMIT) const tasks = [] for (let i = 0; i < batchTimes; i++) { tasks.push(db.collection('data').where(params).skip(i * MAX_LIMIT).limit(MAX_LIMIT).get()) } const data = [] if (tasks.length != 0) { (await Promise.all(tasks)).map(item => {data = data.concat(item.data||[]) }) } return data Q:云开发数据库同时连接数是多少? 华哥:预付费模式下,数据库同时连接数取决于当前环境的配额方案,按量付费模式下,数据库的同时连接数是1000。 数据库同时连接数 :数据库请求并发数量,如同时有三十个数据库操作请求,则有二十个会同时执行,剩下十个返回超出并发错误;一次数据库请求(无论小程序端发起还是云函数端发起)将耗费一个连接;每个云环境分别有一个同时连接数限制、独立计数。假如数据库查询平均耗时 10ms,那么一个连接可以支持 100qps(1000ms/10ms=100),20个连接可以支持到 2000qps。 Q:如何下载云存储的文件夹? 华哥:可以直接使用 SDK 的 downloadDirectory 接口下载文件夹或者使用 CLI工具进行下载。 Q:存储缓存的配置规则? 华哥:存储配置设置了多条缓存策略时,相互之间会有重复,配置项列表底部的优先级会高于顶部优先级。 华哥有礼你还想看云开发的哪些问题合集?在评论区告诉我们!9月3日中午12:00,抽取2名幸运用户赠送精美礼品一份!
2021-08-31 - 让小程序支持代码高亮
对于编程技术类的小程序来说,在文章会有很多代码,那么代码高亮就是一个文章显得很出色的需求了。代码高亮功能的实现,主要是依靠小程序里对富文本内容的解析。对于富文本解析,微慕小程序专业版以前采用的开源的wxParse组件,但这个组件不支持代码高亮,且二次开发的难度较大。从微慕小程序专业版v3.8.0开始引入了mp-html组件,该组件提供对代码高亮显示的支持。 在小程序里通过mp-html实现代码高亮方式如下: 1.在小程序里引入mp-html将mp-html的源码中对应平台的代码包(dist/platform)拷贝到 components 目录下,更名为 mp-html 在需要使用页面的 json 文件中添加如下代码: { "usingComponents": { "mp-html": "/components/mp-html/index" } } JSON复制代码 2.在小程序里使用mp-html1.在需要使用页面的 wxml 文件中添加 <mp-html content="{{html}}"></mp-html> HTML复制代码 2.在需要使用页面的 js 文件中添加 Page({ onLoad () { this.setData({ html:'<div>Hello World!</div>' }) } }) JavaScript复制代码 3.在mp-html里引入代码高亮highlight插件在mp-html的源代码里tools/config.js 中的 plugins 中启用highlight插件,设置完成后,可通过项目提供的命令行工具生成新的组件包。 编辑 plugins/highlight/config.js ,可以选择是否需要以下功能: copyByLongPress 是否需要长按代码块时显示复制代码内容菜单 showLanguageName 是否在代码块右上角显示语言的名称 showLineNumber 是否在左侧显示行号 引入本插件后,html 中符合以下格式的 pre 将被高亮处理: <!-- pre 中内含一个 code,并在 pre 或 code 的 class 中设置 language --> <pre><code class="language-css"> p { color: red } </code></pre> HTML复制代码 本插件的高亮功能依赖于prismjs,默认配置中仅支持 html、css、c-like、javascript 变成语言,如果需要更多语言下需要去prismjs网站下载对应的 prism.min.js 和 prism.css 并替换 plugins/highlight/ 目录下的文件。 目前微慕专业版小程序里代码高亮支持的编程语言是TIOBE排名前20的编程语言,比如C 、Java、Python 、C++、C Sharp、PHP等。 4.在wordpress里文章页面支持代码高亮微慕小程序是通过wordpress的api构建的,因此如果在wordpress文章页面也同时支持代码高亮就完美了,做到这个其实比较简单,只要把mp-html目录下plugins/highlight/prism.min.js 和 prism.css 引入到wordpress的主题模板即可。 如果在wordpress的文章里代码高亮支持:显示行号,复制代码,显示语言,可以去prismjs下载相应的插件。 1.显示编程语言的prismjs插件:https://prismjs.com/plugins/show-language/ 2.显示行号的prismjs插件:https://prismjs.com/plugins/line-numbers/ 3.复制代码的prismjs插件:https://prismjs.com/plugins/copy-to-clipboard/ 下载上述插件后,引入到wordpress主题里,在code 便签里加入data-prismjs-copy 和data-prismjs-copy-success,就可以支持上述三个功能了。 示例代码如下: <pre><code class="language-html line-numbers" data-prismjs-copy="复制代码" data-prismjs-copy-success="代码已复制"> </code></pre>
2021-08-13 - 云开发数据库 对象数组的某个属性求和?
数据库数据 { "_id": 1, "name":"Tom", "list": [{"course":"eglish", "score":60},{"course":"math", "score":70} ] } { "_id": 2, "name":"Kat", "list": [{"course":"eglish", "score":100},{"course":"math", "score":70} ] } 要求输出数据结果 { "_id": 1, "name":"Tom", "total":130, "list": [{"course":"eglish", "score":60},{"course":"math", "score":70} ] } { "_id": 2, "name":"Kat", "total":170, "list": [{"course":"eglish", "score":100},{"course":"math", "score":70} ] } 怎么数组中对象的某个属性求和? reduce可以吗?
2020-12-26 - 小程序富文本解析利器mp-html
小程序富文本解析利器mp-html [图片] 微慕小程序是资讯、媒体类小程序,因为对富文本内容和媒体内容的显示有较高的需求。对于富文本解析,微慕小程序以前采用的开源的wxParse组件,不过wxParse组件存在很多的问题且已经停止维护支持,随着微慕小程序功能不断的增加和优化,wxParse组件已经无法适应,同时对wxParse二次开发优化的难度比较大,基于此微慕团队考虑寻找更合适的解析组件,经过朋友的推荐和我们的考察,最终选择开源组件:mp-html(https://jin-yufeng.gitee.io/mp-html),这个组件堪称小程序富文本解析利器。微慕团队对mp-html组件二次开发后可以与微慕小程序完美兼容,微慕小程序专业版v3.8.0加入了该组件。mp-html组件给富文本的内容提供了不少出色的功能。 全面支持html标签小程序大多数都是基于html标签来渲染和显示内容的,mp-html组件支持以下列表标签和属性,同时支持id、style、class、align、height、width 属性。几乎可以完美兼容html的标签内容,并保持web内容和小程序内容在显示上兼容性,页面渲染的性能很强。 组件对html标签支持的稳定性很好: 1.标签名中可以含有 : 等特殊字符(如 o:p) 2.标签名和属性名大小写不敏感 3.属性值可以不加引号、加单引号、加双引号,也可以却缺省(默认 true) 4.属性之间可以没有空格(通过引号划分)、有空格(可以多个)、有换行符 5.支持正常格式、CDATA 等多种形式的注释 同时,对于一些错误情况,程序也能够自动处理: 1.标签首尾不匹配 2.属性值中冒号不匹配 3.标签未闭合 自定义样式配置样式(css)是富文本中最重要的内容之一,组件提供多种样式设置的方法,可以进行灵活的自定义设置,让小程序端的文本显示更丰富。 1.行内样式 这是最常用的样式设置方法,直接将需要的样式放在对应标签的 style 属性中即可,这种方式仅作用于单个标签,优先级最高 2.tag-style 这是本组件独有的一种样式设置方式,可以给某一种标签名设置默认的样式,可以通过 tag-style 属性设置,具体用法见对应说明 3.外部样式 如果希望将某些样式固定的用于渲染,可以添加到 tools/config.js 的 externStyle 字段中,该方法仅支持 class 选择器(2.1.0 版本起支持标签名选择器),优先级最低。 需要调整优先级时,可以通过设置 !important 实现。 另外,通过引入 style 插件,还可以实现匹配 style 标签中样式的功能。 图片加载在富文本内容里图片显示非常重要,mp-html在图片显示上充分考虑小程序的特点,主要提供一下功能: 1。占位图 支持设置图片未加载完成时的占位图 loading-img 和加载出错时的占位图 error-img 2.懒加载 内容较长、图片较多时,开启懒加载有助于改善性能,需要时可通过 lazy-load 属性开启 3.自动预览 图片被点击时,将自动放大预览,如不需要,可通过 preview-img 属性关闭。还可以在 imgtap 事件中进行自定义处理 自动预览通过特定的处理,可以实现左右滑动查看所有图片、预览重复链接不错位等效果 4.预览高清图 同一张图片,可以给显示时和预览时设置不同的链接地址以达到最佳效果 设置方式 1:给 img 标签增加一个 original-src 即可 设置方式 2:通过 imgList 的 api 进行设置 5.长按弹出菜单 微信和百度平台支持图片长按时弹出菜单,可以进行保存、分享等操作,如不需要,可通过 show-img-menu 属性关闭 6.装饰图片处理 有时对于一些小的装饰性图片,可能不希望产生上述效果,此时可以给 img 标签设置 ignore 属性,将屏蔽预览、弹出菜单等操作,提升体验。 在链接内的、src 为 data url 且没有设置 original-src 的图片,默认为不可预览的小图片。 7.支持原大小显示 本组件通过合理转换,基本实现了和 html 中 img 的相同效果:没有设置宽度时按原大小显示;设置了宽度时按比例缩放;同时设置宽高时按设置的值显示。不必去考虑小程序中的 mode 等问。。 8.支持 svg 虽然小程序中不支持 svg 系列标签,本组件通过在解析过程中转为 data url 图片的方式实现了 svg 的显示。 表格和列表小程序中没有 table 标签,使得显示表格一直是一个难题,mp-html解决了这个问题,并支持独立横向滚动,支持含有合并单元格的表格,常用表格属性(border, cellspacing, cellpadding, align). 组件主要通过以下三种方式显示表格 显示方式适用情况说明rich-text 标签表格内部没有链接、图片等特殊标签效果最佳,几乎不需要进行转换table 布局表格内有特殊标签但没有使用合并单元格需要进行一定转换,将 table, tr, td 等标签转为对应的布局grid 布局表格内有特殊标签且使用了合并单元格需要进行复杂的转换将合并单元格用 grid 布局表现出来 对于列表支持也非常友好,完全兼容html里的列表。 1.支持多层嵌套 支持嵌套多层列表,对于无序列表,不同的层级会显示不同的黑点格式。 2.支持多种有序列表格式 通过设置 ol 标签的 type 属性,可以显示数字、字母、罗马数字等多种形式的标号。 3.支持不显示标号 支持通过设置 list-style:none 的方式不显示 li 标签开头的标号。 支持音频和视频对于音频和视频支持自动暂停、多源加载、自动添加控件。 1.自动暂停 在存在多个视频的情况下,同时播放可能会影响体验,本组件支持在播放一个视频的时候自动暂停其他所有视频,如不需要,可通过 pause-video 属性关闭 音频在引入 audio 插件后也可以实现此效果 2.多源加载 不同平台支持播放的格式不同,只设置一个 src 可能会出现兼容性问题导致无法播放,因此本组件支持像 html 中一样给 video 和 audio 设置多个 source,将按照顺序进行加载,直到可以播放,最大程度上避免无法播放 3.自动添加控件 对于既没有设置 controls 也没有设置 autoplay 的标签将自动把 controls 属性设置为 true,避免无法播放,影响体验。 支持多个平台的小程序支持小程序包括:微信小程序,qq小程序,百度小程序,支付宝小程序,头条小程序
2021-08-12 - 云开发http请求的两种写法
对于简单的GET表单请求 可以直接将参数封装在url中 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') var request = require('request') // 云函数入口函数 exports.main = async (event, context) => { //qz return new Promise((resolve, reject) => { request({ url: event.URL, method: "POST",//GET json: true, headers: { "content-type": "application/json", "token": event.token }, }, function (error, response, body) { if (!error && response.statusCode == 200) { try { resolve(body) } catch (e) { reject() } } }) }) } [代码] 对于POST请求 参数不好封装的 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') var request = require('request') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { //这里写普通话成绩查询方式 return new Promise((resolve, reject) => { request({ url: event.url, method: "POST", json: true, headers: { "content-type": "application/json", "token":event.token }, body: event.body }, function (error, response, body) { if (!error && response.statusCode == 200) { try { resolve(body) } catch (e) { reject() } } }) }) } [代码] body中填写需要的参数 body是json形式 [代码]{ xxx:xxx } [代码] 请求头可以根据自己的需要进行修改。
2019-05-28 - 云函数使用request-promise得到的结果在云函数使用为空值,但小程序是正常有值,为什么?
在云函数使用request-promise出现问题,resolve(body)可以正常的将数据返回给小程序,但如果直接在云函数使用body,他就是一个空值 eturn new Promise((resolve,reject)=>{ request({ url:url, method:method, json:true, form:data, header:header },function(error,response,body){ resolve(body) }) }) 上面没问题 下面有问题 return new Promise((resolve,reject)=>{ request({ url:url, method:method, json:true, form:data, header:header },function(error,response,body){ resolve(body.result) }) }) 在小程序端运行body.result有值,但在云函数这么运行没有值,这是为什么,卡了很久研究不出来,麻烦大佬告诉下
2020-08-10 - 小程序云开发 如何调用第三方接口
小程序云开发 如何调用第三方接口
2018-12-24 - 收纳控福音!从0到1用云开发制作物品管理小程序
先上我们最终使用云开发开发的小程序,小程序码如下: [图片] 小程序名字为家物馆,主要用来管理家中物品。涉及到用户账号系统,物品管理,分类及搜索等功能,使用了云开发的云函数,数据库,存储,CMS 内容管理等能力。 一、快速开始如果对云开发不熟悉的话,可以先按照官方文档,快速新建一个云开发的小程序用于参考。文档暂时不用看太多,小程序跑起来了就可以(云开发的文档较多,下文会对所涉及的内容会给出相应的文档链接)。 由于我们要开发的小程序涉及到数据库、云函数及云存储,接下来将按照这几个部分进行介绍。 二、数据库2.1 内容管理系统手写表结构实在是有点慢,而且一不小心还有可能犯错,于是借助了内容管理系统,这可以大大提高工作效率。详细的文档请移步CloudBase CMS。 [图片] 使用上面的内容管理系统,我们既可以方便管理内容模型(注意内容模型的名字不要随便改,不然内容那块会出问题,所以起名字要慎重),还可以创建内容。 2.2 云开发面板之数据库除了上面的内容管理系统,我们同样还可以在云开发面板的数据库中进行管理。如下图: [图片] 除此之外,高级操作还提供了一些数据库操作实例,如下图: [图片] 2.3 数据库增删改查这里先记住操作数据库三步走(具体的实例操作我们在云函数中继续): 1、选择哪个环境的数据库 const DB = wx.cloud.database({ env: 'test' // 哪个环境 }); 2、选择哪个集合 const users = DB.collection('users'); 3、对集合进行增删改查操作 const user = users.doc('_id'); 更详情的文档参考:数据库增删改查SDK 2.4 文档 ID在内容管理系统中,文档 ID 属于系统字段,目前只能自动生成不可自定义。但是有些情况下,我们还是想可以自定义文档 ID 的,如统一分类的数据。 所幸还有一条路,云开发面板的数据库中是支持自定义的,所以如果真需要自定义的文档 ID,可以直接在云开发面板的数据库中定义。不过云开发面板的数据库自定义 ID 的那个字段输入框,是有长度限制的。 [图片] 文档 ID 在查询单个数据记录时非常有用,如获取某个用户信息: // 以openid 为自定义的文档 ID // 如果找到则返回该用户信息 // 如果没有找到该用户信息,则表示该用户没有注册。 users.doc('openid').get().then((res) => { console.log(res.data) }).catch((e) => { console.log('未注册')}); 三、云函数3.1 实现第一个云函数首先我们对着我的第一个云函数文档,实现我们的第一个云函数。 这里面主要有一个库和两个 API 需要注意: 一个库文档:wx-server-sdk两个 API 文档:getWXContext (一定要注意不同的调用方式可能会返回不同的数据)、callFunction接下来,我们可以按照云函数的文档,一直看到本地调试。 [图片] 看完这些之后,我们就可以正式开始云函数开发了。下面以获取用户云函数为例。 3.2 云函数实战1、我们首先新建一个 user 云函数(在云函数根目录上右键,在右键菜单中,选择创建一个新的 Node.js 云函数,命名为 user)。 [图片] 2、安装依赖文件 右键 user 文件夹右键,选择在内建终端中打开,输入 [代码]<span>npm i</span>[代码] 命令,安装依赖文件。 [图片] 3、开启云函数本地调试 依赖文件安装完成后,同样右键 user 文件夹,选择开启云函数本地调试。 [图片] 打开的云函数本地调试面板如下,注意右边的那个勾选。 [图片] 4、编写云函数 整体代码大概如下(可根据 type 类型判断要请求的数据): [图片] 5、小程序端调用 先在 app.js 中完成云能力初始化,代码如下:(文档可参考:小程序端云能力初始化文档)。 App({ onLaunch() { if (!wx.cloud) { console.error('请使用 2.2.3 或以上的基础库以使用云能力'); } else { wx.cloud.init({ env: 'cloud1-xxx', traceUser: true, }); } }, }); 在需要的地方调用云函数 user,代码如下: wx.cloud.callFunction({ // 云函数名称 name: 'user', // 传给云函数的参数 data: { type: 'get' }, }) .then(res => { console.log(res.result) }) .catch(console.error) 6、上传并部署 调试开发完毕,就可以上传部署了,如下图: [图片] 3.3 云函数管理我们所有的云函数都可以通过云开发面板中进行管理,如下图: [图片] 四、云存储其实在快速开始里面,默认创建的小程序里面就有上传图片的一个云开发实例,对着里面的实例抄一遍,把一些信息打印出来看看就会用了。 具体文档参考: 文件存储 。 当然对于用户上传的图片来说,最好还得有个裁剪的功能,小程序裁剪图片的组件网上也有很多,找个合适自己即可。 对于存储的内容,我们同样可以通过云开发面板查看,如下图: [图片] 五、开放数据云开发还提供了一种新的方法去调用开放数据:开放数据校验与解密 。 下面我们以获取电话号码为例,具体实战下: 1、使用 [代码]button[代码] 组件,[代码]open-type[代码] 为 [代码]getPhoneNumber[代码] 2、在 [代码]getPhoneNumber[代码] 中拿到 [代码]cloudID[代码] [图片] 3、编写云函数 user,调用 getOpenData API,主要代码为: const { type, cloudID } = event; // 电话号码授权 if (type === 'getPhone') { const res = await cloud.getOpenData({ list: [cloudID], }); const resPhone = res.list[0].data.phoneNumber; return resPhone; } 4、小程序端调用云函数,这样就拿到了电话号码。 wx.cloud.callFunction({ // 云函数名称 name: 'user', // 传给云函数的参数 data: { type: 'getPhone', cloudID, // 这个是上面获取到的 cloudID }, }) .then(res => { console.log(res.result) }) .catch(console.error) 六、总结总体来说,小程序云开发圆了我们全栈的梦,一个人一把梭是快乐的,但是摸索的过程中其实也是有挑战的,云开发不断的发展优化,需要我们跟紧步伐~ 文章来源:腾讯IMWEB团队 产品介绍云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 技术交流群、最新资讯关注微信公众号【腾讯云开发CloudBase】
2021-08-06 - 自定义导航栏导致wx.pageScrollTo的selector指定元素位置出现偏差?
[图片] 点击右下角的按钮是跳转到item number:5才对,因为自定义导航栏后导致偏差一个导航栏高度的位移。 https://developers.weixin.qq.com/s/hg2E3amA7zgu 后续: wx.pageScrollTo官方已经支持selector和offsetTop搭配解决问题,详情看https://developers.weixin.qq.com/miniprogram/dev/api/ui/scroll/wx.pageScrollTo.html
2022-03-30 - 微信小程序路由实战
欢迎来到我博客阅读:BlueSun - 微信小程序路由实战 0. 目录 1. 前言 2. 智能路由跳转 — Navigator 模块 3. 虚拟路由策略 — Router 模块 4. 落地中转策略 — LandTransfer 模块 4.1. 对于要解决的第一个问题:统一的落地页 4.2. 对于第二个要解决的问题:短链参数 4.3. LandTransfer 模块设计 5. 更好的开发体验 5.1. Typescript + Router 5.2. 智能生成路由配置 5.3. 自定义组件跳转 6. 整体架构图 7. 最后的最后 1. 前言 在微信小程序由一个 [代码]App()[代码]实例,和众多[代码]Page()[代码]组成。而在小程序中所有页面的路由全部由框架进行管理,框架以栈的形式维护了所有页面,然后提供了以下 API 来进行路由之间的跳转: [代码]wx.navigateTo[代码] [代码]wx.redirectTo[代码] [代码]wx.navigateBack[代码] [代码]wx.switchTab[代码] [代码]wx.reLaunch[代码] 但是,对于一个企业应用,把这些问题留给了开发者: 原生 API 使用了 [代码]Callback[代码] 的函数实现形式,与我们现代普遍的 [代码]Promise[代码] 和 [代码]async/await[代码] 存在 gap。 基于小程序路由的设计,暴露给外部的是真实路由(如扫码,公众号链接等方式),对后续项目重构留下历史包袱。 小程序页面栈最多十层, 在超过十层后 [代码]wx.navigateTo[代码] 失效,需要开发者判断使用 [代码]wx.redirectTo[代码] 或其他API 小程序页面栈存在一种特殊的页面:Tab 页面,需要使用 [代码]wx.switchTab[代码] 才能跳转。需要开发者主动判断,不方便后期改动 Tab 页面属性。 额外的,对于小程序码,要使用无数量限制 API wxacode.getUnlimited ,存在参数长度限制32位以内。需要开发者自行解决。 而本文,期望能对这若干问题,逐个提供解决方案。 2. 智能路由跳转 — Navigator 模块 在这里我们一起解决: 原生 API 非 Promsie 页面栈突破十层时特殊处理 特殊页面 Tab 的跳转处理 我们的思路是,希望能设计一种逻辑,根据场景来自动判断使用哪个微信路由 API,然后对外只提供一个函数,例如: [代码]gotoPage('/pages/goods/index') [代码] 具体逻辑如下: 当跳转的路由为小程序 tab 页面时,则使用 [代码]wx.switchTab[代码]。 当页面栈达到 10 层之后,如果要跳转的页面在页面栈中,使用 [代码]wx.navigateBack({ delta: X })[代码] 出栈到目标页面。 当页面栈达到 10 层之后,目标页面不存在页面栈中,使用 [代码]wx.redirectTo[代码] 替换栈顶页面。 其他情况使用 [代码]wx.navigateTo[代码] 顺带的,我们把这个函数以 Promise 形式实现,以及支持参数作为 [代码]object[代码]传入,例如: [代码]gotoPage('/pages/goods/index', { name: 'jc' }).then(...).catch(...); [代码] 大部分场景下,只要使用[代码]gotoPage[代码]就能满足。 那肯定也会有特定的情况,需要显式的指定使用 [代码]navigateTo/switchTab/redirectTo/navigateBack[代码]的哪一个。 那么我们也按照类似的实现,满足相同模式的 API [代码]navigateTo('/pages/goods/index', { name: 'jc' }).then(...).catch(...); switchTab('/pages/goods/index', { name: 'jc' }).then(...).catch(...); redirectTo('/pages/goods/index', { name: 'jc' }).then(...).catch(...); navigateBack('/pages/goods/index', { name: 'jc' }).then(...).catch(...); [代码] 这些函数都可以内聚到同一个模块,我们称其为:Navigator [代码]const navigator = new Navigator(); navigator.gotoPage(...); navigator.navigateTo(...); navigator.switchTab(...); navigator.redirectTo(...); navigator.navigateBack(...); [代码] 模块设计: [图片] 3. 虚拟路由策略 — Router 模块 在这里,我们解决: 对外暴露了真实路由,导致历史包袱沉重的问题。 在许多应用开发中,我们经常需要把某种模式匹配到的所有路由,全都映射到同个页面中去。 例如,我们有一个 Goods 页面,对于所有 ID 各不相同的商品,都要使用这个页面来承载。 [图片] 那么在代码层面上,期望能实现这样的调用方式: [代码]// 创建路由实例 const router = new Router(); // 注册路由 router.register({ path: '/goods/:id', // 虚拟路由 route: '/pages/goods/index', // 真实路由 }); // 跳转到 /pages/goods/index,参数: onLoad(options) 的 options = { id: '123' } router.gotoPage('/goods/123'); // 跳转到 /pages/goods/index,参数: onLoad(options) 的 options = { id: '456' } router.gotoPage('/goods/456'); [代码] Class Router 的核心逻辑是完成: 路由的注册,完成「虚拟路径」和「真实路径」关系的存储。 满足「虚拟路径」到「真实路径」的转换,并且识别「动态路径参数」(dynamic segment)。 路由跳转。 对于「路由的注册」,我们在其内部存储一个 map 就能完成。 而对于「路径的转换」, [代码]vue-router[代码] 有类似的实现,通过其源码发现,内部是使用 path-to-regexp 作为路径匹配引擎,我们可以拿来用之。 然后对于「路由的跳转」,我们可以直接复用上面提到的 Navigator 模块,通过输入真实路径,来完成路由的跳转。 模块设计: [图片] 其中: RouteMatcher:提供动态路由参数匹配功能,内部使用 path-to-regexp 作为路径匹配引擎。 Route: 为每个路径创建路由器,存储每个路由的虚拟路径和真实路由的关系。 Router:整合内部各模块,对外提供统一且优雅的调用方式。 4. 落地中转策略 — LandTransfer 模块 在这里,我们解决: 小程序扫码、公众号链接等场景下的落地页统一。 小程序码,对于无限量API wxacode.getUnlimited ,突破参数32位长度限制。 4.1. 对于要解决的第一个问题:统一的落地页 我们把如:扫小程序码、公众号菜单、公众号文章等方式打开小程序某个页面的路径称为「外部路由」。 根据小程序的设计,暴露给外部的连接是真实的页面路径,如:[代码]/pages/home/index[代码],该设计在实践中存在的弊端:各个落地页分散,后期修改真实文件路径难度大。 在 「中长生命周期」 产品中,随着产品的迭代,我们难免会遇到项目的重构。如果分发出去的都是没经过处理的真实路径的话,我们重构时就会束手束脚,要做很多的兼容操作。因为你不知道,分发出去的小程序二维码, 有多少被打印到实体物料中。 那么,「虚拟路由」+「落地中转」 的策略就显得基本且重要了。 「虚拟路由」的功能,**Router **模块给我们提供了支持了,我们还需要对外提供一个统一的落地页面,让它来完成对内部路由的中转。 基本逻辑: 分发出去的真实路由,指向到唯一的落地页面,如:[代码]$LAND_PAGE: /pages/land-page/index[代码] 由这个落地页面,进行内部路由的重定向转发,通过接收 参数,如:[代码]path=/user&name=jc&age=18[代码] [图片] 在代码层面上,我们希望能实现这样的使用: [代码]// /pages/land-page/index.ts const landTransfer = new LandTransfer(landTransferOptions); Page({ onLoad(options) { landTransfer .run(options) .then(() => {...}) .catch(() => {...}); } }); [代码] 然后针对 TS,我们还可以使用装饰器版本,更加简便: [代码]import { landTransferDecorator } from 'wxapp-router'; Page({ @landTransferDecorator(landTransferOptions) onLoad(options) { // ... }, }); [代码] 4.2. 对于第二个要解决的问题:短链参数 微信小程序主要提供了两个接口去生成小程序码: wxacode.get: 获取小程序码,适用于需要的码数量较少的业务场景。通过该接口生成的小程序码,永久有效,数量限制为 100,000 个 wxacode.getUnlimited: 获取小程序码,适用于需要的码数量极多的业务场景。通过该接口生成的小程序码,永久有效,数量暂无限制。 第一种方式,[代码]wxacode.get[代码] 数量限制为 10w 个,虽然量很大了,绝大多数的小程序可能用不到这个量。 但如果我们运营的是一个中大型电商小程序的话,假如:1w 种商品 x 10 种商品规格,那就会超过这个数量。到时候再进行改造,就困难了。 所以,如果抱着是运营一个 「中长生命周期」 的产品的话,我们会使用第二种方式:[代码]wxacode.getUnlimited[代码] 不尽人意的是,虽然它没有数量限制,但是对参数会有 32 个字符的限制,显然是不够用的(一个 uuid 就 32 字符了)。 对于这种情况,我们可以使用「短链参数」的形式解决,由于wxacode.getUnlimited 会通过 [代码]scene[代码]字段作为 query 参数传递给小程序的,那么我们可以通过 [代码]scene[代码]参数来实现短链服务,这需要后端配合。 前后端交互如下: [图片] 当小程序需要生成小程序码的时候,请求后端提供的接口,例如:[代码]/api/encodeShortParams[代码] 后端把内容转换为 32 字符内的字符串,存储到数据库中。 后端通过 wxacode.getUnlimited 接口,以短链字符串作为 [代码]scene[代码]的值,以商定好的统一落地页 [代码]$LAND_PAGE[代码]作为 [代码]page[代码]值,生成小程序码。 当通过小程序码进入小程序,小程序获取到 [代码]scene[代码]参数,请求后端提供的接口,例如:[代码]/api/decodeShrotParams[代码] 小程序理解内容,跳转到目标页面中去。 而前端对于统一落地页的逻辑处理,我们只需要在第一个问题的基础上,增加一个转换短链参数内容的逻辑就行了: [图片] 代码层面上,我们我们只需要多定义转换短链参数的方式:[代码]convertScenePrams[代码] [代码]// in /pages/land-page/index.js import { landTransferDecorator } from 'wxapp-router'; const landTransferOptions = { // 此处接收 onLoad(options) 中的 options.scene convertSceneParams: (sceneParams) => { return API.convertScene({ sceneParams }).then((content) => { // 假如后端存的是 JSON 字符串,前端decode // 要求 content = { path: '/home', a: 1, b:2 } return JSON.parse(content); }); }, }; Page({ @landTransferDecorator(landTransferOptions) onLoad(options) { // ... }, }); [代码] 而其中的 [代码]API.convertScene[代码] 就对接服务端提供 HTTP 接口服务来完成。 4.3. LandTransfer 模块设计 [图片] 5. 更好的开发体验 5.1. Typescript + Router 对于小程序内部的路由跳转,我们除了指定一个字符串的路由,我们是否也可以通过链式调用,像调用函数那样去跳转页面呢?类似这样; [代码]routes.pages.user.go({ name: 'jc' }); [代码] 这样做的好处是: 更自然的调用方式。 能结合 TS,来做到类型提示和联想。 由于事先 [代码]wxapp-router[代码] 并不知道开发者需要注册的路由是什么样的,所以路由的 TS 声明文件,需要开发者来定义。 例如,我们在项目中维护一份路由文件: [代码]// config/routes.ts // 创建路由实例 const router = new Router(); const routesConfig = [{ path: '/user', route: '/pages/user/index', }, { path: '/goods', route: '/pages/goods/index', }]; type RoutesType { paegs: { user: Route<{name: string}>, goods: Route, } } // 注册路由 router.batchRegister(routesConfig); // 获取 routes const routes: RoutesType = router.getRoutes(); export default routes; [代码] 然后在别的地方使用它: [代码]import routes from './routes.ts'; routes.pages.user.go({ name: 'jc' }); [代码] 5.2. 智能生成路由配置 如果路由变多的时候,我们还需要对每个路由手动去编写 [代码]RoutesType[代码] 的话,就有点难受了。 在小程序中,我们把正式路由都配置到 [代码]app.json[代码] ,那么在遵循既定的项目结构情况下,我们可以通过自动构建,完成大部分工作,例如: 智能注册路由 智能识别页面入参声明 5.3. 自定义组件跳转 以上都是脚本层面的使用,小程序中还有 [代码]wxml[代码], 我们希望能在有个组件快速使用: [代码]<Router path="/pageA" query="{{pageAQuery}}"></Router> <Router path="/pageB" query="{{pageBQuery}}" type="redirectTo"></Router> <Router path="/pageC/katy"></Router> [代码] 那么,实现一个自定义组件,然后把 Router模块包装一下,问题就不大了。 示例代码: [代码]// components/router.wxml <view class="wxapp-router" bind:tap="gotoPage"> <slot /> </view> [代码] [代码]// components/router.ts Component({ properties: { path: String, type: { type: String, value: 'gotoPage' }, route: String, query: Object, delta: Number, setData: Object, }, methods: { gotoPage(event) { const router = getApp().router; const { path, route, type, query} = this.data; const toPath = route || path; if (['gotoPage', 'navigateTo', 'switchTab', 'redirectTo'].includes(type)) { (router as any)[type](toPath, query); } if (type === 'navigateBack') { const { delta, setData } = this.data; router.navigateBack({ delta }, { setData }) } } } }) [代码] 6. 整体架构图 最后,我们来整体回顾一下各模块的设计 [图片] Navigator:封装微信原生路由 API,提供智能跳转策略。 LandTransfer:提供落地页中转策略。 RouteMatcher:提供动态路由参数匹配功能。 Route: 为每个路径创建路由器。 Router:整合内部各模块,对外提供优雅的调用方式。 Logger:内部日志器。 Path-to-regexp: 开源社区的路由匹配引擎。 7. 最后的最后 鉴于写过很多的实战类的文章,会有不少同学想要到整体的示例代码,这次我就索性写了一个工具,Enjoy it! wxapp-router: 🛵 The router for Wechat Miniprogram
2021-03-31 - 小程序管理蓝牙设备开发指北
小程序管理蓝牙设备开发记录 前段时间接到一个管理蓝牙设备的需求,要求能搜索并连接指定设备,并读取设备的信息,然后发送指令给设备,让设备运行起来。 允许连接多台同类型的设备,并对设备做分开管理 期间,遇到不少的坑,在此记录下来,希望能对大家有所帮助,有欠缺的地方,还请大家帮忙指正一下,谢谢 话不多说,接下来就进入开发: 1. 初始化 考虑到需要在不同页面都要使用微信的蓝牙接口,并且还需要一些数据的互通,所以我建了一个单例,用于处理微信接口,和设备状态、信息管理 [代码] constructor(config = {}) { if (!manager.instance) { manager.instance = this; this.connectPool = []; this.cachePool = []; this.discoveryPool = []; this.timeout = 5000; this._timer = null; this.adpterStatus = {open: false} } Object.assign(manager.instance, config); if (!this.adpterStatus.open) { this.initBluetoothAdapter() } return manager.instance; } [代码] [代码]connectPool[代码] 设备连接池,用于存储正在连接的设备 <br> [代码]cachePool[代码] 设备缓存池,用于存储连接过的设备 <br> [代码]discoveryPool[代码] 设备发现池,用于存储扫描到的设备 <br> [代码]timeout[代码] 超时时间 <br> [代码]adpterStatus[代码] 蓝牙适配器状态 [代码]{open: '是否打开', available: '是否可用', discovering: '是否正在搜索设备'}[代码] 若适配器打开状态为[代码]false[代码]那么初始化适配器[代码]initBluetoothAdapter[代码]: [代码] /** * 为了方便处理微信的回调,建了一个公共的callBack方法 */ commonCall(success = ()=>{}, fail = ()=>{}, complete = ()=>{}) { return {success, fail, complete} } initBluetoothAdapter() { const that = this; /** * 监听适配器状态,开启监听之前先关闭监听,防止状态重复 * offBluetoothAdapterStateChange 关闭适配器状态监听 * onBluetoothAdapterStateChange 开启适配器状态监听 */ wx.offBluetoothAdapterStateChange(); wx.onBluetoothAdapterStateChange(res => { // 同步适配器状态,TODO做manager工具内的监听,可以参考下一步搜索状态的监听 3. 发现设备 中的 discoveryPoolDidUpdate 方法 Object.assign(that.adpterStatus, res) // TODO 若适配器重新可获取时,重新开启适配器 // 若适配器open = true,开始 -> 2. 设置监听 that.setListener() }); /** * 开启小程序蓝牙适配器,开启之前先关闭,防止状态重复 * closeBluetoothAdapter 关闭蓝牙适配器 * openBluetoothAdapter 开启蓝牙适配器 */ wx.closeBluetoothAdapter(); wx.openBluetoothAdapter(that.commonCall(success => { // 蓝牙适配器初始化成功 })); } [代码] 2. 设置监听 适配器初始化完成后,设置监听,统一处理数据和状态: [代码]onBluetoothDeviceFound[代码] 蓝牙搜索监听 [代码]onBLEConnectionStateChange[代码] 蓝牙设备连接状态监听,并在连接成功的时候 [代码]onBLECharacteristicValueChange[代码] 蓝牙设备特征值变化监听,用户小程序和蓝牙的交互 [代码] setListener() { wx.offBluetoothDeviceFound() wx.onBluetoothDeviceFound(res => { // 设备搜索监听,更新设备,详情请移步 -> 3. 发现设备 }) wx.offBLEConnectionStateChange() wx.onBLEConnectionStateChange(res => { /** * res = { * errorCode: 0 成功 * errorMsg: 错误信息 * connected: 0 断开连接,1 连接成功 * deviceId:连接设备的deivceId * } * * 设备连接状态更新,若连接成功,则开始针对设备进行数据监听,详情请移步 -> 设备交互 */ }) wx.offBLECharacteristicValueChange() wx.onBLECharacteristicValueChange(res => { // 设备特征值发生变化,更新设备数据,详情请移步 -> 设备交互 }) } [代码] 3. 发现设备 [代码] /** * services: 可以通过设备是否具备特定的服务UUID来筛选自己想要的设备 * sCall: 扫描方法调用成功 * fCall:扫描方法调用失败 */ discoveryBluetoothDevices(services, sCall, fCall) { const that = this; // 扫描前清空discoveryPool that.discoveryPool = []; const discoveryCall = that.commonCall(sCall, fCall); discoveryCall.services = services; wx.stopBluetoothDevicesDiscovery(that.commonCall(__=>__, __=>__, () => { wx.startBluetoothDevicesDiscovery(discoveryCall) })) } [代码] 若设备有新设备,则‘设置监听’中的[代码]onBluetoothDeviceFound[代码]会进行新设备上报 调用[代码]updateDevice[代码]方法进行设备过滤和保存 [代码] updateDevice(device) { if (!device) return; if (!device.name) return; if (!device.advertisData) return; const that = this; // 判断设备是否是新设备 if (that.discoveryPool.map(v => v.deviceId).indexOf(device.deviceId) === -1) { // ab2str 见下方备注 device.advertisData = ab2str(device.advertisData) that.discoveryPool.push(device) // 给单例添加提供给外部监听状态的接口,当设备有更新的时候,触发接口回调 that.discoveryPoolDidUpdate && that.discoveryPoolDidUpdate instanceof Function && that.discoveryPoolDidUpdate(that.discoveryPool) // 私有回调 -> 4.连接设备 that._discoveryPoolDidUpdate && that._discoveryPoolDidUpdate instanceof Function && that._discoveryPoolDidUpdate(device) that._timer && clearTimeout(that._timer) } // 超时时间,超时后若无新设备,则关闭Discovery方法 that._timer = setTimeout(() => { wx.stopBluetoothDevicesDiscovery() that._timer && clearTimeout(that._timer) }, that.timeout) } [代码] [代码]备注:[代码]设备广播数据是[代码]ArrayBuffer[代码]的形式,所以通过[代码]ab2str[代码]的方法进行转换,方法见下: [代码] ab2str(buffer) { return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''); } [代码] 4. 连接设备 小程序连接设备的接口[代码]wx.createBLEConnection()[代码]接受的值是[代码]deviceId[代码],作为设备的标识符。 但是这个设备Id在iOS和Android手机上,同一设备的值是不同的,所以如果我们要把deviceId保存给后台服务器,下次再拿到,不一定可以直接使用。 所以为了通用性,我们应该跟设备制造商统一一下,让设备广播出自己的特征码,亦或者直接通过[代码]服务UUID[代码]获取到设备的mac地址,用这些不变且唯一的字符串作为保存到后端的设备标识符。 如果以广播中的特征码为唯一标识符,搜索设备并向后台保存的过程中,无需跟设备进行连接操作,本文以这样的方式进行; 如果通过服务UUID获取到设备的mac,保存给后端,需要扫描到设备之后,连接设备,并通过获取设备mac地址的服务UUID,读取到设备的mac,保存到后台。 [代码] connectBluetoothDevice(indentify, sCall, fCall) { const that = this; const connectCall = that.commonCall(sCall, fCall); if (that.adpterStatus.discovering) { for (let i=0; i<that.discoveryPool.length; i++) { if (that.discoveryPool[i].advertisData == indentify) { connectCall.deviceId = that.discoveryPool[i].deviceId connectCall.timeout = that.timeout wx.createBLEConnection(connectCall) } } } else { that.discoveryPoolDidUpdate = null that.discoveryBluetoothDevices([], s => { // 扫描到设备之后,用广播数据进行比对,若一样,获取该设备的deviceId,并连接 that._discoveryPoolDidUpdate = res => { if (res.advertisData == indentify) { connectCall.deviceId = res.deviceId wx.createBLEConnection(connectCall) wx.stopBluetoothDevicesDiscovery() } else { // 检查是否已经停止扫描 console.log('_discoveryPoolDidUpdate', that.adpterStatus) fCall && fCall instanceof Function && fCall("device not found !") } } }) } } [代码] 5. 设备交互 设备交互有三种形式: [代码]read[代码] 程序读取设备的信息 [代码]write[代码] 向设备发指令 [代码]notify[代码] 订阅设备的上报 蓝牙设备出厂的时候,就设置了一些接口,并定义好访问它的服务ID和特征值ID以及访问方式,通过这些可以跟设备做到交互 用前端跟后端交互的方式理解,跟设备进行交互的时候,服务ID和特征值ID 就相当于我们访问接口的api接口,[代码]read[代码]相当于get接口,获取到数据,[代码]write[代码]相当于post接口,数据发送给后台,后台就对应数据逻辑做相应变更,[代码]notify[代码]相当于与服务器建立websocket连接,实时获取服务器发来的数据(单方向) 由于[代码]服务ID[代码]和[代码]特征值ID[代码]都是这样[代码]00002A23-0000-1000-8000-008BF9B054F3[代码]难以记住的串,所以我们建立一个[代码]服务适配器(services-adpter)[代码],它负责配置我们需要用到的服务,如下 [代码] export const serviceAdapter = [ { // 开始设备 serviceName: 'start', serviceUUID: '服务ID', characterUUID: '特征值ID', inFormatter: '入参格式化方法', outFormatter: '出参格式化方法', type: 'write' } ] [代码] 我们传给设备的数据需要转换[代码]二进制数据[代码]和[代码]异或[代码]操作,所以在这里进行配置入参格式化方法和出参格式化方法 接下来就是交互,在设备连接上之后,处理serviceAdapter中的type = read 和 type = notify的任务 处理serviceAdapter任务的顺序为 :处理read任务,对设备进行属性的初始化 -> 处理notify任务,对设备属性进行监听,并设置callBack -> write任务需要主动触发 [代码] // 处理read任务,对设备进行属性的初始化 const readServices = that.getServicesBy('read') readServices.forEach(rs => { console.log('will start read servce:', rs); const call = that.commonCall(success => { console.log('readBLECharacteristicValue success:', success) }) call.deviceId = device.deviceId call.serviceId = rs.serviceUUID call.characteristicId = rs.characterUUID wx.readBLECharacteristicValue(call) }) // 处理notify任务,对设备属性进行监听,并设置callBack const notifyServices = that.getServicesBy('notify') notifyServices.forEach(ns => { console.log('will start notify servce:', ns); const call = that.commonCall(success => { console.log('notifyBLECharacteristicValueChange success:', success) }) call.deviceId = device.deviceId call.serviceId = ns.serviceUUID call.characteristicId = ns.characterUUID wx.notifyBLECharacteristicValueChange(call) }) [代码] write方式,需要用户主动触发 [代码] beginService(indentify, serviceName, params, sCall, fCall) { const that = this for (let i=0; i<serviceAdapter.length; i++) { let adapter = serviceAdapter[i] if (adapter.serviceName == serviceName) { if (adapter.inFormatter && adapter.inFormatter instanceof Function) { const device = this.deviceBy(indentify, 'connect') const call = that.commonCall(sCall, fCall) call.deviceId = device.deviceId call.serviceId = adapter.serviceUUID call.characteristicId = adapter.characterUUID call.value = adapter.inFormatter(params) wx.writeBLECharacteristicValue(call) } return } } } [代码] indentify 设备的唯一标志符 <br> serviceName 需要访问的服务名称 <br> params 发送给设备的数据 <br> 发送数据给用户,并在监听中获取最新的设备信息和状态 自此我们就初步的完成了设备搜索到连接到交互的过程 6. TODOS 为了让工具更加完善,需要增加错误处理,异常抛出,错误重试等操作,这里就不在此赘述了
2021-07-30 - 云函数(nodejs)使用上传图片api获取MediaID
接口说明 适用对象:服务商/电商平台 请求URL:https://api.mch.weixin.qq.com/v3/merchant/media/upload 逻辑流程: 从云储存或小程序端获取图片二进制数据 使用crypto生成文件摘要 使用crypto生成签名和授权信息(需要先有文件名和文件摘要) 自定义分割字符串boundary和HTTP头Content-Type 创建传输body的头部和尾部,并与文件拼接 发送请求 1. 获取图片二进制数据 可以从云储存获取,或小程序端传参 小程序获取的是ArrayBuffer,在后续使用中需要转型成Buffer 场景1: 从云储存获取图片buffer 云函数代码 [代码]// [云储存文件id] 可以从小程序端传入 const fileRes = await cloud.downloadFile({fileID: '云储存文件id'}) const imgBuffer = fileRes.fileContent [代码] 场景2:从小程序端获取图片buffer 小程序代码 [代码]const fileSystemManager = wx.getFileSystemManager() const buffer = fileSystemManager.readFileSync('图片路径') wx.cloud.callFunction({ name: '云函数名称', data: { buffer, filename: '图片名.jpg' } }) [代码] 云函数代码 [代码]let { buffer, filename } = event const imgBuffer = Buffer.from(buffer) [代码] 2. 生成摘要、创建meta [代码]const hash = crypto.createHash('sha256'); hash.update(imgBuffer); let sha256 = hash.digest('hex') let meta = { filename, sha256 } [代码] 3. 生成签名、创建授权信息 [代码]let getAuthorization = async function(meta) { // 获取商户私钥、证书序列号、商户号 // (可以储存在云数据中,从云数据库获取) const priKey = '商户私钥' const serialNo = '商户证书序列号' const mchid= '商户号' // 生成随机序列 let nonceStr = Math.random().toString() // 获取当前时间戳(这里需要的是秒数不是毫秒,要除以1000) let timestamp = Math.floor(Date.now() / 1000) // 创建待签名文本 let signatureText = "POST\n" + "/v3/merchant/media/upload\n" + timestamp + "\n" + nonceStr + "\n" + JSON.stringify(meta)+"\n" // 生成签名 let sign = crypto.createSign('RSA-SHA256') sign.update(signatureText) let signature = sign.sign(priKey, 'base64') // 合成授权信息 let authorization = 'WECHATPAY2-SHA256-RSA2048' + ` mchid="${mchid}"` + `,nonce_str="${nonceStr}"` + `,timestamp="${timestamp}"` + `,serial_no="${serialNo}"` + `,signature="${signature}"` return authorization } [代码] 4. 自定义分割字符串boundary和HTTP头Content-Type [代码]// 自定义分割字符串(可以自行定义,和发送的内容不重复即可) let boundary = 'miwoo-boundary-' + Math.random() let contentType = 'multipart/form-data; boundary=' + boundary [代码] 5. 创建传输body的前半部分和尾部,并与文件拼接 [代码]// 创建body的前半部分并转换为buffer // 注意反引号`与单引号'的区别 // 分割符要在boundary前加-- let beginBuffer = Buffer.from( `--${boundary}\r\n` + 'Content-Disposition: form-data; name="meta";\r\n' + 'Content-Type: application/json\r\n' + '\r\n' + `${JSON.stringify(meta)}\r\n` + `--${boundary}\r\n` + `Content-Disposition: form-data; name="file"; filename="${filename}";\r\n` + 'Content-Type: image/jpg\r\n' + '\r\n' ) // 创建body尾部(第一个\r\n是接在imgBuffer后面的换行) // 结束分割符要在boundary两边加-- let endBuffer = Buffer.from(`\r\n--${boundary}--\r\n`) // 将imgBuffer加入头部与尾部,拼接成完整body(不能直接使用+号连接) let body = Buffer.concat([beginBuffer,imgBuffer,endBuffer]) [代码] 6. 发送请求 [代码]axios({ method: 'POST', url: 'https://api.mch.weixin.qq.com/v3/merchant/media/upload', headers: { 'Authorization': authorization, 'Content-Type': contentType }, data: body }) .then(res => { console.log(res.data) }) [代码] 完整流程 [代码]const cloud = require('wx-server-sdk') cloud.init() const crypto = require('crypto') //使用crypto生成文件摘要以及签名 const axios = require('axios') //使用axios发送请求(npm install axios) exports.main =async (event, context) => { // 1. 获取图片buffer const imgBuffer = '从云储存或小程序获取' // 获取文件名(请自行获取) const filename = '图片名.jpg' // 2. 生成文件摘要 const hash = crypto.createHash('sha256'); hash.update(imgBuffer); let sha256 = hash.digest('hex') let meta = { filename, sha256 } // 3. 获取签名(使用上面的签名函数) let authorization = await getAuthorization(meta) // 4. 自定义分割字符串和Content-Type let boundary = 'miwoo-boundary-' + Math.random() let contentType = 'multipart/form-data; boundary=' + boundary // 5. 创建(拼接)body let beginBuffer = Buffer.from( `--${boundary}\r\n` + 'Content-Disposition: form-data; name="meta";\r\n' + 'Content-Type: application/json\r\n' + '\r\n' + `${JSON.stringify(meta)}\r\n` + `--${boundary}\r\n` + `Content-Disposition: form-data; name="file"; filename="${filename}";\r\n` + 'Content-Type: image/jpg\r\n' + '\r\n' ) let endBuffer = Buffer.from(`\r\n--${boundary}--\r\n`) let body = Buffer.concat([beginBuffer,imgBuffer,endBuffer]) // 6. 发送请求 return await axios({ method: 'POST', url: 'https://api.mch.weixin.qq.com/v3/merchant/media/upload', headers: { 'Authorization': authorization, 'Content-Type': contentType }, data: body }) .then(res => { console.log(res.data) return res.data.media_id }) } [代码] 欢迎留言 本文为**电商平台(云开发)**的填坑之作,欢迎提出不足之处、分享云开发的经验和坑。
2021-06-02 - 小程序实现的列表上下拖拽排序
先来看看效果 快速拖拽排序测试演示视频地址:https://v.qq.com/x/page/r3207k4fxe1.html 完整拖拽排序效果演示视频地址:https://v.qq.com/x/page/y3207g6agur.html [图片] 采用技术:uni-app 接下来分析分析实现该效果所需要用到的标签 元素是通过拖拽进行排序的,此处采用的是官方出的 <movable-area> <movable-view> 两位标签大佬解决移动的问题 (主要是相信官方支持的动画会比自己搞更加丝滑一些)。支持拖拽到上下边界,检查可视区域的位置并自动进行滚动, 此处就需要我们的 <scroll-view> 标签大佬坐镇了。标签的选择搞定了,再来了解了解这些标签要用到的重点属性 movable-view 想要移动就必须作为 movable-area 的直接子元素,且 movable-area 必须设置 width,height 属性 (还有些提示可以查看文档)。movable-view 的 x, y 属性决定了 movable-view 再 movable-area 所处的位置 (是不是猜出了要搞些什么东东了)scroll-view 滚动到指定位置可以通过控制 scroll-top 的属性值来进行控制滚动 接下来就是怎么个实现思路,先来捋捋实现的步骤 列表该如何渲染如何控制拖拽元素的跟随如何使拖拽中的元素与相交互的元素进行位置调换如何判断拖拽元素至上下边界滚动屏幕如何使页面的滚动与拖拽时的滚动互不影响 描述完宏观的蓝图,接下来就是代码小细节,客官请随我来 一、解决列表渲染问题 /** * 上面说到 movable-view 可以通过 x,y 决定它的位置, 且 movable-area 需要设置 widht,height 属性 * 配置完这些属性 movable-view 就可以再 movable-area 愉快的拖拽玩耍了 * 思路: * 1. 通过列表的数量乘于显示列表项的高度得出最终可拖拽区域的总高度,赋值给 movable-area * 2. 扩展列表项一些字段,此处使用 y 保存当前项距离顶部位置, idx 保存当前项所在列表的下标 / // 伪代码 // js initList(list) { this.areaHeight = list.length * this.height; // aeraHieght 可拖拽区域总高度, height 为元素所需高度 this.internalList = list.map((item, idx) => { return { ...item, y: idx * this.height, // movable-view 当前项所处的高度 idx: idx, // 当前项所处于列表的下标,用于比较 animation: true, // 主要用于控制拖拽的元素要关闭动画, 其他的元素可以保留动画 } }) } // html 二、 如何控制拖拽元素的跟随 // 主要是通过监听 movable-view 的 touchstart touchmove touchend 三个事件完成拖拽动作的起始、移动、结束。 // methods { _dragStart(e){ // 取得触摸点距离行顶部距离 this.deviationY = (e.mp.touches[0].clientY - this.wrap.top) % this.height; this.internalList[idx].animation = false; // 关闭当前拖拽元素的动画属性 this.activeIdx = idx; // 保存当前拖拽元素的下标 }, _dragMove(e) { const activeItem = this.internalList[this.activeIdx]; if (!activeItem) return; // 实时取得触摸点的位置信息 const clientY = e.mp.touches[0].clientY; let touchY = clientY - this.wrap.top - this.deviationY + this.scrollTop; if (touchY <= 0 || touchY + this.height >= this.areaHeight) return; activeItem.y = touchY; // 拖拽元素的移动秘密就在于此 } } 三、如何使拖拽中的元素与相交互的元素进行位置调换 // 上述代码解决了当前拖拽元素的位置移动问题, 接下来就需要解决拖拽元素和上下元素交互的问题 // methods { __dragMove(e){ // ...同上代码一致 // 上下元素交互位置代码实现 for(let item of this.internalList) { if (item.idx !== activeItem.idx) { if (item.idx > activeItem.idx) { // 如果当前元素下标大于拖拽元素下标,则检查当前拖拽位置是否大于当前元素中心点 if (touchY > item.idx * this.height - this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; // 对调位置 item.y = item.idx * this.height; // 更新对调后的位置 break; // 退出循环 } } else { // 如果当前元素下标小于拖拽元素下标,则检查当前拖拽位置是否小于当前元素中心点 if (touchY < item.idx * this.height + this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; item.y = item.idx * this.height; break; } } } } } } 四、如何判断拖拽元素至上下边界滚动屏幕 // 将 movable-area 包裹在 scroll-view 标签中, 通过控制 scroll-top 的值来进行滚动 // 思路: 判断当前拖拽元素的位置信息与当前屏幕可视区域进行比较 // methods { _dragMove(e) { // ...同上代码 // 检查当前位置是否处于可视区域 if (activeItem.idx + 1 * this.height + this.height / 2 > this.scrollTop + this.wrap.top) { this.viewTop = this.scrollTop + this.height; // 往上滚动一个元素的高度 } else if (activeItem.idx * this.height - this.height / 2 < this.scrollTop ) { this.viewTop = this.scrollTop - this.height; // 往下滚动一个元素的高度 } } } 五、如何使页面的滚动与拖拽时的滚动互不影响 // 事实上我是通过一种取巧的方式, scroll-veiw 有一个 scroll-y 属性可以控制滚动方向 // 思路: // 1.不进行滚动的时候将 scroll-y 置为 true , 使用默认的滚动效果 // 2.当进入拖拽排序状态时则将 scroll0y 置为 false, 滚动通过拖拽代码比较计算滚动位置 完整代码: 主要小程序上的插槽不允许往外传值、所以自定义元素实现的方式相比于H5实现Vue的方式比较别扭。 因为有多个地方需要用到排序功能,所以边抽离了 js 部分进行混入。 // DargSortMixin.js 文件 export default { props: { list: { type: Array, default() { return []; }, }, sort: { type: Boolean, default: false, }, height: { type: Number, default: 66, }, }, data() { return { areaHeight: 0, // 区域总高度 internalList: [], // 列表 activeIdx: -1, // 移动中激活项 deviationY: 0, // 偏移量 // 包裹容器信息 wrap: { top: 0, height: 0, }, viewTop: 0, // 指定滚动高度 scrollTop: 0, // 容器实时滚动高度 scrollWithAnimation: false, canScroll: true, }; }, created() { // 组件使用选择器,需用使用this const query = this.createSelectorQuery(); query .select('#scroll-wrap') .boundingClientRect(rect => { if (rect) { this.wrap = { top: rect.top, height: rect.height, }; } }) .exec(); }, watch: { list: { handler(val) { this.initList(val); }, immediate: true, }, }, methods: { getList() { return this.internalList .sort((a, b) => { return a.idx - b.idx; }) .map(item => { let newItem = { ...item }; delete newItem.y; delete newItem.idx; delete newItem.animation; return newItem; }); }, initList(list) { this.areaHeight = list.length * this.height; this.internalList = list.map((item, idx) => { return { ...item, y: idx * this.height, idx, animation: true, }; }); }, _dragStart(e, idx) { // 取得触摸点距离行顶部距离 this.deviationY = (e.mp.touches[0].clientY - this.wrap.top) % this.height; this.internalList[idx].animation = false; // 关闭动画 this.activeIdx = idx; this.scrollWithAnimation = true; this.canScroll = false; }, _dragMove(e) { const activeItem = this.internalList[this.activeIdx]; if (!activeItem) return; // 保存触摸点位置和长按时中心一致 const clientY = e.mp.touches[0].clientY; let touchY = clientY - this.wrap.top - this.deviationY + this.scrollTop; if (touchY <= 0 || touchY + this.height >= this.areaHeight) return; activeItem.y = touchY; // 设置位置 // 检查元素和上下交互元素的位置 for (const item of this.internalList) { if (item.idx !== activeItem.idx) { if (item.idx > activeItem.idx) { if (touchY > item.idx * this.height - this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; // 对调位置 item.y = item.idx * this.height; // 更新位置 break; } } else { if (touchY < item.idx * this.height + this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; // 对调位置 item.y = item.idx * this.height; // 更新位置 break; } } } } // 检查当前位置是否处于可视区域 if ( (activeItem.idx + 1) * this.height + this.height / 2 > this.scrollTop + this.wrap.height ) { this.canScroll = true; activeItem.y = activeItem.idx * this.height; this.$nextTick(() => { this.viewTop = this.scrollTop + this.height; }); } else if (activeItem.idx * this.height - this.height / 2 < this.scrollTop) { this.canScroll = true; activeItem.y = activeItem.idx * this.height; this.$nextTick(() => { this.viewTop = this.scrollTop - this.height; }); } }, _dragEnd(e) { const activeItem = this.internalList[this.activeIdx]; if (!activeItem) return; activeItem.animation = true; activeItem.disabled = true; activeItem.y = activeItem.idx * this.height; this.activeIdx = -1; this.scrollWithAnimation = false; this.canScroll = true; }, _onScroll(e) { this.scrollTop = e.detail.scrollTop; }, }, }; // TheDragSortAreaList.vue 文件 import DragSortMixin from '@/mixins/DragSortMixin'; export default { name: 'TheDragSortTableList', mixins: [DragSortMixin], }; .active-item { z-index: 10; } .drag-item { background: $theme-color; color: $white !important; .count { color: $white !important; } }
2020-11-27 - 微信小程序车牌键盘实现
@TOC 前言 是真没想到小小的组件都有人剽窃。只能重发一遍了 各大网站文章链接还有源码地址都在下方评论区。 微信小程序中导航栏一般来说是默认的展示标题等等,可以做的样式改变仅仅能通过配置一些官方提供的属性来实现。除此之外小程序还提供了navigationStyle这个属性可以让用户去自定义的实现导航栏。下面直接奉上代码来说明实现沉浸式导航栏。 展示效果 [图片] 文件说明 涉及到的文件有app.json license-plate.js license-plate.wxml license-plate.wxss (这三个是封装的组件) input-license.js input-license.wxml input-license.wxss (这三个是调用组件的页面,同时也涉及组件中的数据传输,方便调用的页面拿到输入的数据) 此外有input-license.wxss中引入的app.wxss这个是我根据自己习惯写的一些布局命名方式就不贴在文章里了 文件代码 JSON文件 app.json 可以在全局的json里引入组件也可以在某个页面去单独引入,我这里是把组件引在了全局里 [代码]app.json[代码] [代码] "usingComponents": { "license-plate":"/component/license-plate/license-plate" }, [代码] 组件代码 license-plate.js [代码]// component/license-plate/license-plate.js Component({ /** * 组件的属性列表 */ properties: { }, /** * 组件的初始数据 */ data: { firstRow:[], secondRow:[], thirdRow:[], fourthRow:[], currentFocus:0, tabIndex:'0' //0-蓝牌,1-新能源 }, /** * 组件的方法列表 */ methods: { // 输入省份 inpuProvince:function(e){ var first=['1','2','3','4','5','6','7','8','9','0']; var second=['Q','W','E','R','T','Y','U','O','P']; var third=['A','S','D','F','G','H','J','K','L']; var fourth=['Z','X','C','V','B','N','M'] console.log(e) this.triggerEvent('inputProvince',e.currentTarget.dataset.name) this.setData({ currentFocus:1, firstRow:first, secondRow:second, thirdRow:third, fourthRow:fourth }) }, loadkeyboard:function(e,tab){ console.log(e) if(e==0){ console.log('aaa') this.setData({ currentFocus:0, firstRow:['苏','京','津','沪','翼','渝','黑','吉','辽'], secondRow:['晋','青','豫','皖','浙','闽','赣','湘','鄂'], thirdRow:['粤','琼','甘','陕','贵','云','川','蒙'], fourthRow:['新','藏','宁','桂','港','澳'] }) } else{ console.log('bbb') this.setData({ currentFocus:e, firstRow:['1','2','3','4','5','6','7','8','9','0'], secondRow:['Q','W','E','R','T','Y','U','O','P'], thirdRow:['A','S','D','F','G','H','J','K','L'], fourthRow:['Z','X','C','V','B','N','M'] }) } this.data.tabIndex=tab }, // 输入市 inputCity:function(e){ var first=['1','2','3','4','5','6','7','8','9','0']; var second=['Q','W','E','R','T','Y','U','O','P']; var third=['A','S','D','F','G','H','J','K','L']; var fourth=['Z','X','C','V','B','N','M'] console.log(e) this.triggerEvent('inputCity',e.currentTarget.dataset.name) this.setData({ currentFocus:2, firstRow:first, secondRow:second, thirdRow:third, fourthRow:fourth }) }, // 输入车牌 inputLicense:function(e){ if(e.currentTarget.dataset.name!='O'){ //蓝牌 if(this.data.tabIndex=='0'&&this.data.currentFocus!=7){ this.triggerEvent('inputLicense',e.currentTarget.dataset.name) this.setData({ currentFocus:this.data.currentFocus+1 }) } else if(this.data.tabIndex=='1'&&this.data.currentFocus!=8){ //新能源 this.triggerEvent('inputLicense',e.currentTarget.dataset.name) this.setData({ currentFocus:this.data.currentFocus+1 }) } else{ return; } } }, backSpace:function(){ if(this.data.currentFocus>2){ this.setData({ currentFocus:this.data.currentFocus-1 }) this.triggerEvent('backspace',this.data.currentFocus) } else if(this.data.currentFocus==2){ this.setData({ currentFocus:this.data.currentFocus-1 }) this.triggerEvent('backspace',this.data.currentFocus) } else if(this.data.currentFocus==1){ this.setData({ currentFocus:this.data.currentFocus-1, firstRow:['苏','京','津','沪','翼','渝','黑','吉','辽'], secondRow:['晋','青','豫','皖','浙','闽','赣','湘','鄂'], thirdRow:['粤','琼','甘','陕','贵','云','川','蒙'], fourthRow:['新','藏','宁','桂','港','澳'] }) this.triggerEvent('backspace',this.data.currentFocus) } else{ return; } }, closeKeyBoard:function(){ this.triggerEvent('closeKeyBoard') } } }) [代码] license-plate.wxml [代码]<!--component/license-plate/license-plate.wxml--> <view class="keyBoard flxc"> <view class="top-part flxr aic jcb"> <view class="font30 fontgrey" bindtap="closeKeyBoard">取消</view> <view class="font30 fontblue" bindtap="closeKeyBoard">确定</view> </view> <!-- 省份键盘 --> <view class="middle-part flxc aic" wx:if="{{currentFocus==0}}"> <view class="flxr"> <view wx:for="{{firstRow}}" class="key-class" data-name="{{item}}" bindtap="inpuProvince">{{item}}</view> </view> <view class="flxr mt10"> <view wx:for="{{secondRow}}" class="key-class" data-name="{{item}}" bindtap="inpuProvince">{{item}}</view> </view> <view class="flxr mt10"> <view wx:for="{{thirdRow}}" class="key-class" data-name="{{item}}" bindtap="inpuProvince">{{item}}</view> </view> <view class="flxr mt10"> <view wx:for="{{fourthRow}}" class="key-class" data-name="{{item}}" bindtap="inpuProvince">{{item}}</view> <view class="key-class flxc aic jcc" catchtap="backSpace"> <image src="/image/delete.png" class="backspace"></image> </view> </view> </view> <!-- 市区键盘 --> <view class="middle-part flxc aic" wx:if="{{currentFocus==1}}"> <view class="flxr"> <view wx:for="{{firstRow}}" class="key-class2" data-name="{{item}}">{{item}}</view> </view> <view class="flxr mt10"> <view wx:for="{{secondRow}}" class="key-class" data-name="{{item}}" catchtap="inputCity">{{item}}</view> </view> <view class="flxr mt10"> <view wx:for="{{thirdRow}}" class="key-class" data-name="{{item}}" catchtap="inputCity">{{item}}</view> </view> <view class="flxr mt10"> <view wx:for="{{fourthRow}}" class="key-class" data-name="{{item}}" catchtap="inputCity">{{item}}</view> <view class="key-class flxc aic jcc" catchtap="backSpace"> <image src="/image/delete.png" class="backspace"></image> </view> </view> </view> <!-- 车牌键盘 --> <view class="middle-part flxc aic" wx:if="{{currentFocus!=1&¤tFocus!=0}}"> <view class="flxr"> <view wx:for="{{firstRow}}" catchtap="inputLicense" class="key-class" data-name="{{item}}">{{item}}</view> </view> <view class="flxr mt10"> <view wx:for="{{secondRow}}" class="{{item=='O'?'key-class2':'key-class'}}" data-name="{{item}}" catchtap="inputLicense">{{item}}</view> </view> <view class="flxr mt10"> <view wx:for="{{thirdRow}}" class="key-class" data-name="{{item}}" catchtap="inputLicense">{{item}}</view> </view> <view class="flxr mt10"> <view wx:for="{{fourthRow}}" class="key-class" data-name="{{item}}" catchtap="inputLicense">{{item}}</view> <view class="key-class flxc aic jcc" catchtap="backSpace"> <image src="/image/delete.png" class="backspace"></image> </view> </view> </view> </view> [代码] license-plate.wxss [代码]/* component/license-plate/license-plate.wxss */ @import '/app.wxss'; .friendlyAlert{ height: 100%; width: 100%; position: absolute; } .keyBoard{ height: 616rpx; width: 100%; background: #E1E3E7; border-top-left-radius: 20rpx; border-top-right-radius: 20rpx; position: fixed; bottom: 0; z-index: 100 } .top-part{ height: 82rpx; width: 100%; padding: 0 24rpx; } .font30{ font-size: 30rpx; } .font36{ font-size: 36rpx; } .fontblue{ color: #3485F4; } .fontgrey{ color: #91959C; } .middle-part{ height: 454rpx; width: 100%; padding: 26rpx 10rpx; } .key-class{ height: 90rpx; width: 66rpx; border-radius: 8rpx; font-size: 36rpx; color: #333; line-height: 90rpx; text-align: center; box-shadow: 0 1rpx 1rpx rgba(0, 0, 0, 0.16); background: #fff; margin-right: 8rpx; } .key-class2{ height: 90rpx; width: 66rpx; border-radius: 8rpx; font-size: 36rpx; color: #CACACA; line-height: 90rpx; text-align: center; box-shadow: 0 1rpx 1rpx rgba(0, 0, 0, 0.16); background: #fff; margin-right: 8rpx; } .backspace{ height: 32rpx; width: 44rpx; } [代码] 页面代码 input-license.js [代码]// pages/component/input-license/input-license.js Page({ /** * 页面的初始数据 */ data: { tabIndex: '0', code: [{ value: '' }, { value: '' }, { value: '' }, { value: '' }, { value: '' }, { value: '' }, { value: '' }], currentFocus: 0, isFocus: false, showKeyBoard: false, license_color: '0', license_plate: '' }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, // 输入省份 inputProvince: function (e) { var temp = this.data.code; temp[0].value = e.detail; this.setData({ code: temp, currentFocus: 1 }) }, // 输入城市 inputCity: function (e) { var temp = this.data.code; temp[1].value = e.detail; this.setData({ code: temp, currentFocus: 2 }) }, //输入车牌 inputLicense: function (e) { var temp = this.data.code; var i = this.data.currentFocus temp[i].value = e.detail; this.setData({ code: temp, currentFocus: i + 1 }) }, // 退格 backspace: function (e) { var i = e.detail console.log(i) var temp = this.data.code; temp[i].value = ''; this.setData({ code: temp, currentFocus: i }) }, closeKeyBoard: function () { this.setData({ showKeyBoard: false, isFocus: false }) }, openKeyBoard: function () { this.setData({ showKeyBoard: true, isFocus: true }) this.keyboard = this.selectComponent("#keyboard"); this.keyboard.loadkeyboard(this.data.currentFocus, this.data.tabIndex) }, // 切换车牌 changeTab: function (e) { console.log(e) this.setData({ tabIndex: e.currentTarget.dataset.index, currentFocus: 0 }) if (e.currentTarget.dataset.index == '1') { this.setData({ code: [{ value: '' }, { value: '' }, { value: '' }, { value: '' }, { value: '' }, { value: '' }, { value: '' }, { value: '' }] }) this.data.license_color = '4' } else { this.setData({ code: [{ value: '' }, { value: '' }, { value: '' }, { value: '' }, { value: '' }, { value: '' }, { value: '' }] }) this.data.license_color = '0' } }, }) [代码] input-license.wxml [代码]<!--pages/component/input-license/input-license.wxml--> <nav-bar title="车牌键盘" whetherShow="1"></nav-bar> <view class="top-part" style="margin-top:235rpx"> <view class="title">选择车牌类型</view> <view class="chooseType flxr aic mt20"> <image wx:if="{{tabIndex=='1'}}" class="type-item" src="/image/lanpai2.png" bindtap="changeTab" data-index="0"></image> <image wx:if="{{tabIndex=='0'}}" class="type-item" src="/image/lanpai.png"></image> <image wx:if="{{tabIndex=='0'}}" class="type-item ml40" src="/image/lvpai2.png" bindtap="changeTab" data-index="1"></image> <image wx:if="{{tabIndex=='1'}}" class="type-item ml40" src="/image/lvpai.png"></image> </view> <view class="title mt20">请输入需要办理车辆的车牌号</view> <view class="flxr license mt20" bindtap="openKeyBoard"> <view wx:for="{{code}}" class="edit-text {{index==0?'':'ml10'}} {{tabIndex=='1'?'colorG':''}}" wx:for-index="index"> <view>{{item.value}}</view> <view wx:if="{{currentFocus==index&&isFocus}}" class="cursor"></view> </view> </view> </view> <view wx:if="{{showKeyBoard}}" class="friendlyAlert" catchtap="closeKeyBoard"></view> <license-plate id="keyboard" wx:if="{{showKeyBoard}}" bindcloseKeyBoard="closeKeyBoard" bindinputProvince="inputProvince" bindinputCity="inputCity" bindinputLicense="inputLicense" bindbackspace="backspace"></license-plate> [代码] input-license.wxss [代码].top-part{ width: 100%; height: 460rpx; background: #fff; border-radius: 12rpx; padding: 24rpx; } .middle-part{ width: 100%; height: 300rpx; background: #fff; border-radius: 12rpx; padding:0 32rpx; } .middle-part .middle-item{ height: 33%; width: 100%; padding: 29rpx 0; } .chooseType{ height: 80rpx; width: 100%; } .type-item{ height:80rpx; width: 200rpx; } .license{ height: 94rpx; width: 100%; } .edit-text{ height: 94rpx; width: 66rpx; position: relative; border: 1rpx solid #4E92EF; border-radius: 6rpx; line-height: 94rpx; text-align: center; font-size: 36rpx; } .cursor { width: 36rpx; height: 4rpx; background-color: #333333; animation: focus 1.2s infinite; position: absolute; left: 50%; margin-left: -18rpx; bottom: 14rpx; } .friendlyAlert{ height: 100%; width: 100%; position: absolute; top: 0; } .colorG{ border: 1rpx solid #5BCA92; } .tips{ color: #91959C; font-size: 22rpx; } [代码] 总结 下载代码链接——车牌组件 有不足之处还希望各位老哥们指出。感谢感谢 如果大家有什么比较实用的组件想法需要帮忙实现可以找我 PS:感谢释予老哥的切图
2021-07-28 - 小程序调试新方案——使用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 - 小程序云数据库日常操作脚本整理
介绍校友录小程序采用腾讯云开发技术,云开发提供了一个 JSON 数据库,顾名思义,数据库中的每条记录都是一个 JSON 格式的对象。一个数据库可以有多个集合(相当于关系型数据中的表),集合可看做一个 JSON 数组,数组中的每个对象就是一条记录,记录的格式是 JSON 对象。 其主要特性: 安全性:对于数据库而言,数据安全是第一位的;易用性:与小程序的特征类似,“开箱即用,用完即走”,简单上手,免运维;低成本:按量收费,精细化成本控制;高性能:Nosql,支持高并发读写;灵活性:无固定的数据库表模式([代码]no-schema[代码]),支持弹性伸缩;在本案例中,校友录小程序总共用到了16个集合,包含校友用户,校友相册,校友活动,校友互助,校友聚会,校友后台管理员,校友资讯,校友日志等 在云开发控制台有个高级操作,这里可以执行开发者输入的脚本,比如清空集合,根据某个条件删除集合内部分数据,查询集合等等 [图片] 常用脚本1、清空操作 清空校友用户集合 db.collection('t_user') .where({ _id: _.exists(true) }) .remove() 2、删除操作删除1990年入学的校友用户 db.collection('t_user') .where({ USER_ENROLL: 1990 }) .remove() 3、查询操作查询名字为“覃建平”的校友用户的所有数据 db.collection('t_user') .where({ USER_NAME: '覃建平' }) .get() 多条件,指定字段查询 db.collection('t_user') .where({ USER_INVITE_ID: '' }) .field({ USER_INVITE_ID:true }) .skip(0) .limit(10) .get() 4、去掉某个字段删除校友用户集合的USER_VIP_MONEY,USER_VIP_RETURN_MONEY,UER_VIP_LEAVE_MONEY字段 db.collection('t_user').where({_id:_.neq(1)}) .update({ data: { USER_VIP_MONEY:_.remove(), USER_VIP_RETURN_MONEY:_.remove(), USER_VIP_LEAVE_MONEY:_.remove(), } }) 5、更新某个字段或者新增某个字段更新校友用户集合的USER_VIP_MONEY字段,如果原来没有这个字段,则自动新增该字段且赋值 db.collection('t_user').where({_id:_.neq(1)}) .update({ data: { USER_VIP_MONEY: 1111, } }) 6、复杂的多条件查询对于校友用户集合按毕业年份,行业,学校,班级,专业,用户身份,最近来访,访问次数等多维度查询,排序 /** 取得用户分页列表 */ async getUserList(userId, { search, // 搜索条件 sortType, // 搜索菜单 sortVal, // 搜索菜单 orderBy, // 排序 whereEx, //附加查询条件 page, size, oldTotal = 0 }) { orderBy = orderBy || { USER_LOGIN_TIME: 'desc' }; let fields = FILEDS_USER_BASE; let where = {}; where.and = { USER_OPEN_SET: ['>', 0], USER_STATUS: [ ['>=', UserModel.STATUS.COMM], ['<=', UserModel.STATUS.VIP] ], _pid: this.getProjectId() //复杂的查询在此处标注PID }; if (util.isDefined(search) && search) { where.or = [{ USER_NAME: ['like', search] }, { USER_ITEM: ['like', search] }, { USER_COMPANY: ['like', search] }, { USER_TRADE: ['like', search] }, { USER_TRADE_EX: ['like', search] }, ]; } else if (sortType && util.isDefined(sortVal)) { let user = {}; // 搜索菜单 switch (sortType) { case 'companyDef': // 单位性质 where.and.USER_COMPANY_DEF = sortVal; break; case 'trade': // 行业 where.and.USER_TRADE = ['like', sortVal] break; case 'workStatus': //工作状态 where.and.USER_WORK_STATUS = sortVal; break; case 'same_enroll': //同级 user = await UserModel.getOne({ USER_MINI_OPENID: userId }); if (!user) break; where.and.USER_ENROLL = user.USER_ENROLL; break; case 'same_item': //同班 user = await UserModel.getOne({ USER_MINI_OPENID: userId }); if (!user) break; where.and.USER_ITEM = user.USER_ITEM; break; case 'same_trade': //同行 user = await UserModel.getOne({ USER_MINI_OPENID: userId }); if (!user) break; let trade = user.USER_TRADE; if (trade.includes('-')) trade = trade.split('-')[0]; where.and.USER_TRADE = ['like', trade]; break; case 'same_city': //同城 user = await UserModel.getOne({ USER_MINI_OPENID: userId }); if (!user) break; where.and.USER_CITY = user.USER_CITY; break; case 'enroll': //按入学年份分类 switch (sortVal) { case 1940: where.and.USER_ENROLL = ['<', 1950]; break; case 1950: where.and.USER_ENROLL = [ ['>=', 1950], ['<=', 1959] ]; break; case 1960: where.and.USER_ENROLL = [ ['>=', 1960], ['<=', 1969] ]; break; case 1970: where.and.USER_ENROLL = [ ['>=', 1970], ['<=', 1979] ]; break; case 1980: where.and.USER_ENROLL = [ ['>=', 1980], ['<=', 1989] ]; break; case 1990: where.and.USER_ENROLL = [ ['>=', 1990], ['<=', 1999] ]; break; case 2000: where.and.USER_ENROLL = [ ['>=', 2000], ['<=', 2009] ]; break; case 2010: where.and.USER_ENROLL = ['>=', 2010]; break; } break; case 'sort': // 排序 if (sortVal == 'new') { //最新 orderBy = { 'USER_LOGIN_TIME': 'desc' }; } if (sortVal == 'last') { //最近 orderBy = { 'USER_LOGIN_TIME': 'desc', 'USER_ADD_TIME': 'desc' }; } if (sortVal == 'enroll') { //入学 orderBy = { 'USER_ENROLL': 'asc', 'USER_LOGIN_TIME': 'desc' }; } if (sortVal == 'info') { orderBy = { 'USER_INFO_CNT': 'desc', 'USER_LOGIN_TIME': 'desc' }; } if (sortVal == 'album') { orderBy = { 'USER_ALBUM_CNT': 'desc', 'USER_LOGIN_TIME': 'desc' }; } if (sortVal == 'meet') { orderBy = { 'USER_MEET_CNT': 'desc', 'USER_LOGIN_TIME': 'desc' }; } if (sortVal == 'login_cnt') { orderBy = { 'USER_LOGIN_CNT': 'desc', 'USER_LOGIN_TIME': 'desc' }; } break; } } let result = await UserModel.getList(where, fields, orderBy, page, size, true, oldTotal, false); return result; } 查询结果 [图片] 交流vx: cclinux0730
2021-07-12 - 利用hash不刷新页面的特性,实现小程序向webView通讯
如果直接使用hash值传递参数,ios上会异常添加页面栈,点击小程序左上角会返回到上一次带hash的地址,参数传递多少次,页面栈就有多少个。以下代码解决了这个问题,废话不多说,直接贴代码。 //在webview中添加代码 let hash; // 监听hash变化 window.addEventListener("popstate", function() { // 有hash值时返回上一个页面,并储存hash。 // 因为返回了上一个页面,会立马进入else分支。 // 将存好的hash渲染到页面 if(location.hash){ hash = location.hash.split('#')[1]; history.back(); } else { //hash值最好是编码后的数据 document.getElementsByTagName('body')[0].innerHTML = '解码后参数:' + decodeURIComponent(hash); } }, false); wxml中引入web-view组件 <web-view src='{{src}}'></web-view> 小程序js代码 const src = 'http://127.0.0.1:8080/'; //替换成webview地址 Page({ data: { src: src, }, onLoad(){ let j = 0; // 每2秒传递一次值 setInterval(() => { let a = {test: j++}; let data = encodeURIComponent(JSON.stringify(a));//hash值最好经过编码 this.setData({src: `${src}#${data}`}) }, 2000); } })
2021-07-15 - margin-top失效(塌方)及解决办法
当两个空的块级元素嵌套时,如果内部的块设置有margin-top属性,而且没有下边解决方法所述的特征,那么内部块的margin-top属性会绑架父元素(即margin-top传递给父元素)。 解决方法: 1、设置父元素或者自身的display:inline-block;(建议) 2、设置父元素的border:1px aqua solid;(>0) 3、设置父元素的padding:1px;(>0) 4、给父元素设置overflow:hidden; 5、给父元素或者自身设置position:absolute; 6、设置父元素非空,填充一定的内容。 [图片] (解决前效果) [图片] (解决后效果)
2021-07-13 - 请问各位大佬,如果某个字段为对象数组,怎么更新这个数组中对象的某个值?
[图片] 比如我想让这个ViewFlag的值变为true
2021-02-05 - # 使用小程序云开发API更新数组中的单个数组元素
使用小程序云开发API更新数组中的单个数组元素 看了看mongoDB的更新数据方式,找到了解决办法,解决方法如下,亲测可用: 第一种方法:使用位置操作符$ [代码]代码,条件更新写在云函数中 [代码] [图片] [代码]test_api集合原始数据如下 [代码] [图片] [代码]在云函数中执行1中的代码,数组users中id为1001的用户添加了一个新的属性test [代码] [图片] [代码]原理分析 [代码] where条件是查找数组中id属性为1001的用户 update中的使用’users.$.test’: ‘test’ 注意里面的$符号,在mongoDB中,这个符号叫做位置操作符,代表数组的下标,如下引自《mongoDB实战》 [图片] 第二种方法:直接使用数组下标 云函数代码 [图片] test_api集合原始数据如下 [图片] 代码执行后 [图片] 相对于第一种方法,这种方法更加简单,只不过users.1.test这种写法有点颠覆js和java中的属性书写规则,让人感觉怪怪的,在mongoDB中,也支持点数字这种写法。 一个可能的疑惑 可不可以写作’users[1].test’:‘test’,测试结果如下: [图片] [图片] 可以看到’user[1]'无法被识别为数组的第二个元素,而是作为了属性名新增了一个属性,结论:必须写成”点数字“不能写成“中括号” 结论: 经过测试,使用这两种种方法可以更新数组中的一个元素。 方法一适合在不知道数组元素下标的情况下根据查询条件更新元素; 方法二适合在知道数组元素下标的情况下更新元素; 当然也存在既知道元素下标也可以通过属性查到的情况,想用哪个就看心情了-.- 但是暂未找到查询返回数组中的一个元素的方法,再探索探索吧 ——。——
2019-03-06 - 求助,云开发能一次请求在同一集合内使用多个条件每个条件各获取5条数据吗?
例如该集合内有100条数据,每条数据有个num值,如何通过一次请求获取5条num=5,5条num=3,5条num=10,5条num=13,5条unm=2的数据,然后一次性返回?
2020-08-31 - 微信小程序 -- 基于 movable-view 实现拖拽排序
微信小程序 – 基于 movable-view 实现拖拽排序 项目基于[代码]colorui[代码]样式组件 ColorUI组件库 (color-ui.com) 1.实现效果 [图片] 2. 设计思路 movable-view 绑定块移动事件的 块[代码]ID[代码] ,块移动的坐标 移动结束后触发[代码]moveEnd[代码]事件,根据[代码]Y[代码]坐标对对象数组进行排序 根据排序结果重置块位置 3.实现代码 代码已经进行了最简化处理 图中效果实现需引入[代码]colorui[代码]的[代码]main.wxss[代码]样式部分。 wxml [代码]<movable-area class="padding text-center bg-grey" style="width:100%;height:500px;" > <movable-view class="radius shadow bg-white" style="width:80%;height:80px;z-index:{{index==moveId?2:1}}" wx:for="{{tabList}}" wx:key="index" x="{{item.x}}" y="{{item.y}}" direction="all" bindchange="moveStatus" bindtouchend='moveEnd' data-moveid="{{index}}"> {{item.name}}</movable-view> </movable-area> [代码] js [代码]var compare = function (obj1, obj2) { var val1 = obj1.y; var val2 = obj2.y; if (val1 < val2) { return -1; } else if (val1 >= val2) { return 1; } else { return 0; } } Page({ /** * 页面的初始数据 */ data: { branchid:'', appdocid:'', tabList:[ { name:'十步杀一人' }, { name:'千里不留行' }, { name:'事了拂衣去' }, { name:'深藏身与名' } ], //移动的是哪个元素块 moveId:null, //最终停止的位置 endX:0, endY:0 }, initMove(){ let tabList = this.data.tabList; var tarr = [] tabList.forEach(function(ele,index){ let obj = ele obj.id = index obj.x = 30 obj.y = 100*index +20 tarr.push(obj) }) console.log(tarr) this.setData({ tabList:tarr }) }, moveEnd(e){ console.log(e) var that = this; that.setData({ ["tabList["+that.data.moveId+"].x"]:that.data.endX, ["tabList["+that.data.moveId+"].y"]:that.data.endY },()=>{ let tabList = this.data.tabList; tabList = tabList.sort(compare); that.setData({ tabList },()=>{ setTimeout(function(){ that.initMove(); },500) }) }) //计算位置 }, moveStatus(e){ // console.log(e) //移动的块ID var moveid = e.currentTarget.dataset.moveid; //最终坐标 let x = e.detail.x let y = e.detail.y this.setData({ moveId:moveid, endX:x, endY:y }) }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function () { this.initMove(); } }) [代码] 4.参考文档 movable-view | 微信开放文档 (qq.com)
2021-06-17 - 用movable组件写出简短的拖放/拖拽/拖动 排序,含详细的讲解【拎包哥】
「前言」 这应该是社区目前(2020/12/8)最简短的拖拽排序教程之一,助你快速上手哦。 拖放排序是前端中可以和订单规格选择等等比较的,知识点最密集的基础之一。 如果你有html的基础知识,你会发现微信小程序其实是集成度非常高的框架,和vue,react等响应式前端框架没有本质的区别,甚至集成度还更高。 所以在这里好好利用小程序自身的组件及其属性,就能快速写出简短的拖拽排序。 注:感谢@烟斗 留言帮助! ========================效果图============================= [图片] 微信小程序 ========================HTML篇============================= 只使用小程序提供的movable组件即可。它简化了拖放排序的条件 ,让我们只需要控制y值就可以确定组件的位置。拖放中的放动作有手指离开的动作,而movable组件没有这个属性,所以引用了touchend。注意z-index判断层级<movable-area class='ctr'> <block wx:for='{{arr}}' wx:key='x'> <movable-view bindchange='change' bindtouchend='end' y='{{item.y}}' class='item' direction='vertical' style="z-index:{{index==dragId?2:1}}"> {{item.name}} </movable-view> </block></movable-area>(ps. 由于微信社区难以理解的bug,这里的代码不能放在代码片段里) ========================CSS篇============================= 在这个CSS我只有item的height用到了px,因为y值的像素单位是px。在css尽量不要增加额外的height属性,否则这个组件就不精准了。.ctr{ width: 400rpx; height: 800rpx; border: 1rpx solid black; } .item{ width: 400rpx; height: 50px; /* 与后来确定y值的的 i * 50对应 */ border-bottom: 1rpx solid black; box-sizing: border-box; background:white; /* 让边框内嵌,否则会随着1rpx的叠加而让y值变得不精准 */ } =========================JS篇============================== 主要步骤 用y值来确定拖放动作中放的位置将源item放置在目标item前(这也是排序的本质)注意 拖拽的数组arr一开始就放在onLoad方法而不是data里,否则会因为data的提前渲染而产生缓慢的位移。movable-view一开始是重叠的,所以要根据下标来确定每个item的y值。bindchange对应的是拖行为,我们只需要在这个方法里获取我们在拖行为时产生的y值。拖动行为不会触发bindtap那么在touchend的时候就可以获得bindchange最后一个y值,并借此确定放行为的对应的下标。 Page({ onLoad() { var arr = [ {name: 'Mike'}, {name: 'Paul'}, {name: 'Peter'}, {name: 'Andy'}, {name: 'Larry'} ] for (var i in arr) { arr[i].y = i * 50 } // movable-view的y值单位是px console.log(arr) this.setData({ arr }) }, tap(){ // console.log('在拖拽时是否出发点击行为?') // 在拖拽时不触发点击行为 }, change(e) { this.y = e.detail.y var dragId = e.currentTarget.id // 默认item id,wx-for 分配给每个item的index,我在html里id={{index}},即用id变量记录分配后的index this.setData({ dragId }) }, end(e) { console.log('im 触摸结束') console.log(this.y) // this.y item下边线到movearea顶端的距离 var arr = this.data.arr var id = e.currentTarget.id var currentId = this.y / 50 // 移动时不断计算的id if (id > currentId) { var transferId = Math.ceil(currentId) } else { var transferId = Math.floor(currentId) } var save = arr[id] // 保存初始id arr.splice(id,1) arr.splice(transferId,0,save) // 精华 for (var i in arr) { arr[i].y = i * 50 } this.setData({ arr }) } }) ------------------------------------------进阶篇vue-cli------------------------------------------- vue-cli4 ========================HTML篇============================= 挖坑,在研究vue脚手架vue-cli4的拖拽排序,未完待续。
2021-01-17 - 如何提取对象数组字段中指定的数据?而不是返回所有的字段
[图片] [图片] 如图,请问我该如何只提取,且只返回student_list对象数组字段中的,下标为2的对象数据,即student_list[2]?我尝试的很多方式,发现都会返回student_list中的所有对象,而不是只有student_list[2]这个对象。
2021-04-10 - 云函数延迟执行(异步调用),除了addDelayedFunctionTask,还有什么解决方案吗?
云函数逻辑可以分为先执行A,再执行B。 我要达到的效果是在执行A之后立即返回结果给小程序端,减少用户等待时间,然后在后台默默执行B 自问自答:在Mr.Zhao大佬的指点下,经过一番调试,结论是云函数A中调用云函数B时,直接去掉await就是异步延迟执行,完全符合需求。 但是注意一下,nodejs8.9环境下这样运行不成功,nodejs10.15下可以。 另外可能因为平台对云函数计费的考虑,即使异步调用,云开发控制台日志显示的云函数A的运行时间显示的与同步执行时间相当,包含了云函数B的执行时间。但实际运行确实是异步调用,从小程序端监控云函数A的响应时间可以判别。所以即使采用异步调用,如果云函数B3秒超时,A也会3秒超时,设置云函数的超时时间要注意一下这个问题。 官方文档对nodejs8异步问题也有特别说明,后来才看到的,贴上来分享给大家: https://docs.cloudbase.net/cloud-function/deep-principle 目前使用addDelayedFunctionTask有几个问题: 1、必须最快在6秒之后执行,而我希望实现能立即执行,0秒。 2、目前还不支持自识别环境 DYNAMIC_CURRENT_ENV,这样容易造成发布事故。 3、该调用过程需要0.5秒左右耗时。
2021-11-12 - scrollIntoView对于 slot 的元素无效
[代码]<[代码][代码]scroll-view[代码] [代码]scroll-y[代码][代码]=[代码][代码]"{{true}}"[代码] [代码]scroll-with-animation[代码][代码]=[代码][代码]"{{true}}"[代码] [代码]style[代码][代码]=[代码][代码]"{{anonymousState__temp2}}"[代码] [代码]scroll-into-view[代码][代码]=[代码][代码]"{{scrollId}}"[代码] [代码]bindscroll[代码][代码]=[代码][代码]"anonymousFunc1"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]slot[代码][代码]></[代码][代码]slot[代码][代码]>[代码][代码]</[代码][代码]scroll-view[代码][代码]>[代码] [图片] 右侧没有任何滚动 [图片] 试过了,如果不用 slot 直接写是可以滚动的
2019-07-09 - 【改进版】如何从零实现上拉无限加载瀑布流组件
前言 之前分享过一篇瀑布流如何实现的文章,经过时间的证明,之前的做法并不好,性能上会有问题,所以还是不投机取巧了,老老实实的实现。 回顾: 通过grid-auto-rows的特性实现 item通过grid-row设置高度 js获取节点高度计算span的值 通过wxs设置css的变量实现修改样式 痛点: grid-auto-rows数值越大,span计算准确度越低。 谷歌浏览器、微信开发工具,如果界面高度超过[代码]1000 * grid-auto-rows[代码]的高度,那么后面的内容就不会显示了,谷歌解释说是为了不过渡消耗性能。 因为性能问题,超过1000的item就不会显示了,全会挤压在最下面,导致页面非常卡,开发工具能直接卡崩溃,手机上还没发现这个问题,之前也忽视了这个问题,后面调试的时候就非常恼火,开发工具跟真机上效果不一致。 为了保证span计算的准确度高,grid-auto-rows一般设置成1-10px,1px准确就等于view的高度,但是超过1000px就卡没了。 实现思路 通过selectAllComponents获取所有的子节点 通过getComputedStyle获取节点的高度 简单的排序算法计算节点位置 设置节点的样式 通过wxs的[代码]getState[代码]储存每屏节点渲染的数据 触发[代码]image[代码]组件的[代码]load[代码]事件重新计算并渲染节点位置 创建组件 需要开启抽象节点 [代码]// waterfall/index.json { "componentGenerics": { "selectable": true } } [代码] 利用wxs响应事件获取页面的节点 [代码]<view class="waterfall" views="{{ views.length }}" data-option="{{ {span} }}" change:views="{{ wxs.init }}" > <!-- 嵌套遍历views二维数组 --> <block wx:for="{{ views }}" wx:key="item" wx:for-index="i" > <selectable class="item view-{{ i }}" wx:for="{{ item }}" wx:key="item" value="{{ item }}" /> </block> </view> [代码] 创建item的x,y边距变量 [代码]--span[代码] [代码].waterfall { --span: 5px; width: 100%; position: relative; .item { width: calc(50% - var(--span)); position: absolute; } } [代码] 创建 [代码]index.wxs[代码],核心业务代码都写在这里 [代码]// 当views被setData的时候会被触发 module.export = { init: function(newValue, oldValue, ownerInstance, instance) { console.log(newValue, oldValue, ownerInstance, instance) } } [代码] 业务逻辑 步骤一:获取所有节点 [代码]function init(length, oldValue, ownerInstance, instance) { // 加个判断,避免views长度为0时,或者长度为发生变化时也会执行业务代码 // 只有当views被push新的内容才会执行下面的业务 if (!length || length === oldValue) return // index 其实就是views的长度减一,就等于当前的数组下标 var index = length - 1 var views = ownerInstance.selectAllComponents('.view-' + index) console.log(JSON.stringify(views)) } [代码] [图片] [图片] 步骤二:遍历views获取节点的高度 [代码]views.forEach(function(v, k){ var viewStyle = v.getComputedStyle(['width', 'height']) // 获取高度 var height = viewStyle.height console.log(viewStyle) // [WXS Runtime info] {"width":"182.5px", "height":"242px"} }) [代码] 步骤三:计算view的位置信息 [代码]var LH = 0 var RH = 0 views.forEach(function (v, k) { var viewStyle = v.getComputedStyle(['width', 'height']) // 格式化高度,将px去掉 var height = fixUnit(viewStyle.height) var style = {} if (LH <= RH) { style = { left: 0, top: LH + 'px' } LH += height } else if (RH < LH) { style = { right: 0, top: RH + 'px' } RH += height } // 设置view的样式 v.setStyle(style) }) [代码] 此时,页面的节点会根据position自动排列好 [图片] 步骤四:储存LH,RH到局部变量 [代码]function init(length, oldValue, ownerInstance, instance) { if (!length || length === oldValue) return // 获取局部变量 var state = ownerInstance.getState() // 获取当前节点的dataset var dataset = instance.getDataset() var index = length - 1 state.option = dataset.option state.page = length // 创建并生成记录左侧、右侧高度 // 用二维数组来记录 if (!state.heights) { state.heights = [[0, 0]] } // 记录初次渲染时间戳 if (!state.timeouts) { state.timeouts = [] } // 获取时间戳,并且加上3000毫秒,用于后面计算图片loaded完是否超时 state.timeouts[index] = getDate().getTime() + 3000 refreshViews(index, ownerInstance, state) } function refreshViews(index, ownerInstance, state) { var views = ownerInstance.selectAllComponents('.view-' + index) var span = state.option.span var LH = state.heights[index][0] // 左侧 var RH = state.heights[index][1] // 右侧 views.forEach(function (v, k) { var viewStyle = v.getComputedStyle(['width', 'height']) var height = fixUnit(viewStyle.height) var style = {} if (LH <= RH) { style = { left: 0, top: LH + 'px' } LH += height + span[0] } else if (RH < LH) { style = { right: 0, top: RH + 'px' } RH += height + span[0] } v.setStyle(style) // 保存LH, RH的值到state.heights // 当前的LH,RH其实就是下屏开始的坐标 state.heights[index + 1] = [LH, RH] console.log('渲染', index, k) }) } [代码] 步骤五:图片加载完重新计算位置 [代码]// waterfall/index.js Component({ properties: { views: Array, span: { type: Array, value: [10, 10], }, }, methods: { onLoaded({ detail: { width, height, pIndex, index } }) { this.setData({ [`views[${pIndex}][${index}].loaded`]: { width, height }, }) }, }, }) [代码] [代码]function loaded(value, oldValue, ownerInstance, instance) { if (!value.item.loaded || !oldValue) return // 获取局部变量 var state = ownerInstance.getState() // 判断加载是否超时,如果超时则不触发计算渲染事件 // 让该节点保持当前的位置及高度 var timeout = state.timeouts[value.pIndex] if (timeout < getDate().getTime()) { console.log('加载超时') return } var view = instance.selectComponent('.loaded-view') var viewWidth = view.getComputedStyle(['width']).width // 设置虚拟节点card组件里的loaded-view高度 view.setStyle({ height: computedHeight( viewWidth, value.item.loaded.width, value.item.loaded.height ) + 'px', }) // 加个函数防抖,因为图片加载快的情况下,会并发触发事件 // 尽可能的少触发计算,渲染事件,保证性能 ownerInstance.clearTimeout(timer) timer = ownerInstance.setTimeout(function () { // 渲染当前图片加载完后面的所有views // for循环处理当前图片所在的views,以及后面所有的views // 因为有些图片过大,可能会加载5s左右,但是用户如果上拉又加载了 // 一屏内容并且也通过计算渲染了,这时候上一屏又触发了计算渲染 // 那么可能位置信息就会发生变化,导致被遮挡,或者有空白,这时候只能 // 计算触发事件的图片以及后面的图片,保证位置信息是正确的 for (var i = value.pIndex; i < state.page; i++) { console.log('需要渲染', i) refreshViews(i, ownerInstance, state) } }, 300) } [代码] [图片] 优化 瀑布流最好后台会返回图片的尺寸信息,然后初次渲染的时候就计算好节点的长宽比例,这样就不用监听图片loaded事件了,瀑布流组件代码也不会频繁触发计算渲染,性能也好,方法也简单。 [代码]<image src="xxxx" style="{{ wxs.computed({width, height}) }}" /> [代码] [代码]// wxs function computed(option) { // 节点宽度自己去计算 var viewWidth = 375 / 2 var width = option.width var height = option.height return (viewWidth / width) * height + 'px } [代码] 完整代码 打开代码片段https://developers.weixin.qq.com/s/SO5q6UmF7doL,可直接运行。 https://github.com/liziwork/li-ui github 如果打不开,请切换到码云,gitee.com,代码同步更新的,觉得有用动动您的小手点个Star。 扫码查看更多组件 [图片]
2021-03-19 - 上传图片 保证图片顺序获取fileID 缩短耗时(for循环+async await +promise)
第三套方案(for循环中进行Promise异步操作) 之前上传图片发现for循环异步上传图片导致图片顺序混乱,修改成递归上传发现处理图片时间变长,经网友提醒async+await的方法,第三胎的方案出产了! for循环+async await+promise 1.封装uploadImg函数 处理请求,并return resolve(结果) [代码]//循环中如果后一个Promise的执行依赖与前一个Promise的执行结果(即必要等当前Promise执行完了再进行下次循环) //img 为待传图片形参 const uploadImg = function (img, i) { return new Promise((resolve, reject) => { //cloudPath为文件名 let cloudPath = "postImage/" + new Date().getTime() + img[i].match(/\.[^.]+?$/)[0]; //上传存储 wx.cloud.uploadFile({ cloudPath, filePath: img[i], success: (res) => { resolve(res.fileID); }, fail: (err) => { reject(err); }, }); }); }; [代码] 通过async+await将异步循环 变为 必要等当前Promise执行完再进行下次循环; Promise.all( 所有返回的promise )将数据return [代码] //img为待传图片形参 async function upload(img) { //fileIDs为多个Promise容器 let fileIDs = []; for (let i = 0, len = img.length; i < len; i++) { //await uploadImg函数(必须是Promise) fileIDs.push(await uploadImg(img, i)); } return Promise.all(fileIDs); } [代码] 因为upload()返回的是Promise对象,所以方便用then()获取值 [代码]//temp待处理的图片实参 upload(temp).then(res=>{ //也方便进行下一步待处理... }) [代码] 结果: [图片]
2021-05-13 - 微信开放文档|云开发|云函数|异步返回结果示例
异步返回结果经常,我们需要在云函数中处理一些异步操作,在异步操作完成后再返回结果给到调用方。此时我们可以通过在云函数中返回一个 [代码]Promise[代码] 的方法来完成。 一个最简的 [代码]setTimeout[代码] 示例: // index.js exports.main = async (event, context) => { return new Promise((resolve, reject) => { // 在 3 秒后返回结果给调用方(小程序 / 其他云函数) setTimeout(() => { resolve(event.a + event.b) }, 3000) }) } https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/async.html 这里设置3000会超时。
2020-11-02 - Promise.all的奇技淫巧
一个云开发多层嵌套下的异步操作,用promise.all 取出多层嵌套下的异步值。 或许对你有用,仅供参考。 var p1 = new Promise((rs, rj) => { test.where({ name: 'Anthony' }).watch({ onChange(e) { var data = e.docs[0] console.log(data.mInput) if (data.mInput == 'hi') { await test.where({ name: 'Anthony' }).update({ data: { school: 'Peking' } }).then(() => { rs('im insider') }) } }, onError(err) { console.log(err) } })}) p1.then(res=>{ console.log(res)}) // 打印了最内层的 im insidera
2021-01-14 - 云数据库开发中的事务处理这个例子感觉不对呀,太困惑了,能帮忙看看吗?
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/Database.startTransaction.html 如上面的链接提供的例子。这个里边给出的例子是从A用户转10元给B用户,如果用事务的的话,应该是在操作A-10完成,B+10失败的时候才需要进行事务的rollback呀?为啥例子中是查找A,B账号中某一个不存在就进行rollback? 而没有放在A-10,B+10这两个成功与否上?另外,如果try里边的操作如果发生异常(例如:恰巧是A-10操作完成了,而B+10操作抛异常),被catch 捕获到,那么这个时候不应该进行rollback 吗? const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database({ throwOnNotFound: false, }) const _ = db.command exports.main = async (event) => { try { const transaction = await db.startTransaction() const aaaRes = await transaction.collection('account').doc('aaa').get() const bbbRes = await transaction.collection('account').doc('bbb').get() if (aaaRes.data && bbbRes.data) { const updateAAARes = await transaction.collection('account').doc('aaa').update({ data: { amount: _.inc(-10) } }) const updateBBBRes = await transaction.collection('account').doc('bbb').update({ data: { amount: _.inc(10) } }) await transaction.commit() console.log(`transaction succeeded`) return { success: true, aaaAccount: aaaRes.data.amount - 10, } } else { await transaction.rollback() return { success: false, error: `rollback`, rollbackCode: -100, } } } catch (e) { console.error(`transaction error`, e) return { success: false, error: e } } }
2020-07-07 - 小程序组件中监听全局变量,方法中的this指向为undefined
这是app.js globalData: { userInfo: null, loginMode: true, }, //app 全局属性监听 watch: function (method) { var obj = this.globalData; Object.defineProperty(obj, "userInfo", { //这里的 data 对应 上面 globalData 中的 data configurable: true, enumerable: true, set: function (value) { //动态赋值,传递对象,为 globalData 中对应变量赋值 this._showPictureDetail = value.showPictureDetail; this._pictureTime = value.pictureTime; this._pictureAddress = value.pictureAddress; method(value); }, get: function () { //获取全局变量值,直接返回全部 return this.globalData; } }) } 这是组件监听 created() { getApp().watch(this.watchBack) }, watchBack: function (value) { console.log(this) // 这里的this打印出来是undefined this.setData({ loginMode: false }) },
2021-06-07 - 云函数 云 数据库可以并发 吗?
云函数 云 数据库可以并发 吗? ,比如云数据库有10张车票,100人同时预定( 都是用云函数访问数据库),云函数怎么处理这种并发情况,云函数可以实现吗? 云函数要加 互斥锁吗?
2019-08-26 - 云开发能否处理秒杀或者电商中超卖的问题?
- 需求的场景描述(希望解决的问题) 比如在秒杀的场景,怎么处理超卖的问题? 是否能给某次读写加锁,或者其他解决方案? - 希望提供的能力 云函数读写加锁或者其他能够保证读写过程中没有其他写入操作。
2019-01-28 - 云开发短信跳小程序(自定义开发版)教程
写在前面如果你想要自主开发,但没有云开发相关经验,可以采用演示视频来学习本教程: [视频] 一、能力介绍境内非个人主体的认证的小程序,开通静态网站后,可以免鉴权下发支持跳转到相应小程序的短信。短信中会包含支持在微信内或微信外打开的静态网站链接,用户打开页面后可一键跳转至你的小程序。 这个链接的网页在外部浏览器是通过 URL Scheme 的方式来拉起微信打开主体小程序的。 总之,短信跳转能力的实现分为两个步骤,「配置拉起网页」和「发送短信」。本教程将介绍如何执行操作完成短信跳转小程序的能力。 如果你想要无需写代码就能完成短信跳转小程序的能力,可以参照无代码版教程进行逐步实现。 二、操作指引1、网页创建首先我们需要构建一个基础的网页应用,在任何代码编辑器创建一个 html 文件,在教程这里命名为 index.html 在这个 html 文件中输入如下代码,并根据注释提示更换自己的信息: window.onload = function(){ window.web2weapp.init({ appId: 'wx999999', //替换为自己小程序的AppID gh_ID: 'gh_999999',//替换为自己小程序的原始ID env_ID: 'tcb-env',//替换小程序底下云开发环境ID function: { name:'openMini',//提供UrlScheme服务的云函数名称 data:{} //向这个云函数中传入的自定义参数 }, path: 'pages/index/index.html' //打开小程序时的路径 }) } 以上引入的 web2weapp.js 文件是教程封装的有关拉起微信小程序的极简应用,我们直接引用即可轻松使用。 如果你想进一步学习和修改其中的一些WEB展示信息,可以前往 github 获取源码并做修改。 有关于网页拉起小程序的更多信息可以访问官方文档 如果你只想体验短信跳转功能,在执行完上述文件创建操作后,继续以下步骤。 2、创建服务云函数在上面创建网页的过程中,需要填写一个UrlScheme服务云函数。这个云函数主要用来调用微信服务端能力,获取对应的Scheme信息返回给调用前端。 我们在示例中填写的是 openMini 这个命名的云函数。 我们前往微信开发者工具,定位对应的云开发环境,创建一个云函数,名称叫做 openMini 。 在云函数目录中 index.js 文件替换输入以下代码: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { return cloud.openapi.urlscheme.generate({ jumpWxa: { path: '', // 打开小程序时访问路径,为空则会进入主页 query: '',// 可以使用 event 传入的数据制作特定参数,无需求则为空 }, isExpire: true, //是否到期失效,如果为true需要填写到期时间,默认false expire_time: Math.round(new Date().getTime()/1000) + 3600 //我们设置为当前时间3600秒后,也就是1小时后失效 //无需求可以去掉这两个参数(isExpire,expire_time) }) } 保存代码后,在 index.js 右键,选择增量更新文件即可更新成功。 接下来,我们需要开启云函数的未登录访问权限。进入小程序云开发控制台,转到设置-权限设置,找到下方未登录,选择上几步我们统一操作的那个云开发环境(注意:第一步配置的云开发环境和云函数所在的环境,还有此步操作的环境要一致),勾选打开未登录 [图片] 接下来,前往云函数控制台,点击云函数权限,安全规则最后的修改,在弹出框中按如下配置: [图片] 3、本地测试我们在本地浏览器打开第一步创建的 index.html ;唤出控制台,如果效果如下图则证明成功! 需要注意,此处本地打开需要时HTTP协议,建议使用live server等扩展打开。不要直接在资源管理器打开到浏览器,会有跨域的问题! [图片] 4、上传本地创建好的 index.html 至静态网站托管将本地创建好的 index.html 上传至静态网站托管,在这里静态托管需要是小程序本身的云开发环境里的静态托管。 如果你上传至其他静态托管或者是服务器,你仍然可以使用外部浏览器拉起小程序的能力,但会丧失在微信浏览器用开放标签拉起小程序的功能,也不会享受到云开发短信发送跳转链接的能力。 如果你的目标小程序底下有多个云开发环境,则不需要保证云函数和静态托管在一个环境中,无所谓。 比如你有A、B两个环境,A部署了上述的云函数,但是把 index.html 部署到B的环境静态托管中了,这个是没问题的,符合各项能力要求。只需要保证第一步 index.html 网页中的云开发环境配置是云函数所在环境即可。 部署成功后,你便可以访问静态托管的所在地址了,可以通过手机外部浏览器以及微信内部浏览器测试打开小程序的能力了。 5、短信发送云函数的配置在上面创建 openMini 云函数的环境中再来一个云函数,名字叫 sendsms 。 在此云函数 index.js 中配置如下代码: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { try { const config = { env: event.env, content: event.content ? event.content : '发布了短信跳转小程序的新能力', path: event.path, phoneNumberList: event.number } const result = await cloud.openapi.cloudbase.sendSms(config) return result } catch (err) { return err } } 保存代码后,在 index.js 右键,选择增量更新文件即可更新成功。 6、测试短信发送能力在小程序代码中,在 app.js 初始化云开发后,调用云函数,示例代码如下: App({ onLaunch: function () { wx.cloud.init({ env:"tcb-env", //短信云函数所在环境ID traceUser: true }) wx.cloud.callFunction({ name:'sendsms', data:{ "env": "tcb-env",//网页上传的静态托管的环境ID "path":"/index.html",//上传的网页相对根目录的地址,如果是根目录则为/index.html "number":[ "+8616599997777" //你要发送短信的目标手机,前面需要添加「+86」 ] },success(res){ console.log(res) } }) } }) 重新编译运行后,在控制台中看到如下输出,即为测试成功: [图片] 你会在发送的目标手机中收到短信,因为短信中包含「退订回复T」字段,可能会触发手机的自动拦截机制,需要手动在拦截短信中查看。 需要注意:你可以把短信云函数和URLScheme云函数分别放置在不同云开发环境中,但必须保证所放置的云开发环境属于你操作的小程序 另外,出于防止滥用考虑,短信发送的云调用能力需要真实小程序用户访问才可以生效,你不能使用云端测试、云开发JS-SDK以及其他非wx.cloud调用方式(微信侧WEB-SDK除外),会提示如下错误: [图片] 如果你想在其他处使用此能力,可以使用服务端API来做正常HTTP调用,具体访问官方文档 7、查看短信监控图表进入 云开发控制台 > 运营分析 > 监控图表 > 短信监控,即可查看短信监控曲线图、短信发送记录。 [图片] 三、总结短信跳转小程序核心是静态网站中配置的可跳转网页,外部浏览器通过URL Scheme 来实现的,这个方式不适用于微信浏览器,需要使用开放标签才可以URL Scheme的生成是云调用能力,需要是目标小程序的云开发环境的云函数中使用才可以。并且生成的URL Scheme只能是自己小程序的打开链接,不能是任意小程序(和开放标签的任意不一致)短信发送能力的体验是每个有免费配额的环境首月100条,如有超过额度的需求可前往开发者工具-云开发控制台-对应按量付费环境-资源包-短信资源包,进行购买。如当前资源包无法满足需求也可通过云开发 工单 提交申请[图片]短信发送也是云调用能力,需要真实小程序用户调用才可以正常触发,其他方式均报错返回参数错误,出于防止滥用考虑云函数和网页的放置可以不在同一个环境中,只需要保证所属小程序一致即可。(需要保证对应环境ID都能接通)如果你不需要短信能力,可以忽略最后两个步骤CMS配置渠道投放、数据统计可参考官方文档
2021-04-07 - 云开发原子操作
- 需求的场景描述(希望解决的问题) 并发情况下,云开发修改一个类型为数组的属性(删除其中的一项或添加新的,可同时发生)。 目前请求可能落在不同的实例上?貌似无法加锁之类的方法解决? - 希望提供的能力 求相关方案,队列?
2019-06-17 - 分享使用tcb-router路由开发的云函数短信平台SDK
榛子应用市场 上篇文章我们分享了如何使用纯的云函数开发的榛子短信短信(http://smsow.zhenzikj.com)SDK,由于微信对于未付费云函数个数的限制,这种方法存在缺陷,经过改进,使用tcb-router作为路由,这样只需要整合到一个云函数中就行 下载sdk和demo: http://smsow.zhenzikj.com/sdkdownload/weixinmp_yun2.html 目前SDK中包含三个功能: send(发送短信)、balance(查询余额)、findSmsByMessageId(查询单条短信) SDK源码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const TcbRouter = require('tcb-router') const rq = require('request') const baseUrl = 'https://smsdeveloper.zhenzikj.com' cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const app = new TcbRouter({ event }); app.router('send', async (ctx) => { ctx.body = new Promise(resolve => { rq({ url: baseUrl + '/sms/send.html', method: "POST", json: true, form: { apiUrl: event.apiUrl, appId: event.appId, appSecret: event.appSecret, message: event.message, number: event.number, messageId: event.messageId, } }, function (error, response, body) { resolve({ body: body, error: error }) }); // setTimeout(() => { // resolve('male'); // }, 500); }); }); app.router('balance', async (ctx) => { ctx.body = new Promise(resolve => { rq({ url: baseUrl + '/sms/balance.html', method: "POST", json: true, form: { apiUrl: event.apiUrl, appId: event.appId, appSecret: event.appSecret } }, function (error, response, body) { resolve({ body: body, error: error }) }); }); }); app.router('findSmsByMessageId', async (ctx) => { ctx.body = new Promise(resolve => { rq({ url: baseUrl + '/sms/findSmsByMessageId.html', method: "POST", json: true, form: { apiUrl: event.apiUrl, appId: event.appId, appSecret: event.appSecret, messageId: event.messageId } }, function (error, response, body) { resolve({ body: body, error: error }) }); }); }); return app.serve(); } [代码] 如何使用SDK [代码]//index.js const app = getApp() Page({ data: { }, onLoad: function() { }, // 发送短信 send: function () { wx.cloud.callFunction({ name: 'zhenzisms', data: { $url: 'send', apiUrl: 'https://sms_developer.zhenzikj.com', appId: '你的appId', appSecret: '你的appSecret', message: '你的验证码为:3333', number: '15811111111', messageId: '' } }).then((res) => { console.log(res.result.body); }).catch((e) => { //console.log(e); }); }, // 查询余额 balance: function () { wx.cloud.callFunction({ name: 'zhenzisms', data: { $url: 'balance', apiUrl: 'https://sms_developer.zhenzikj.com', appId: '你的appId', appSecret: '你的appSecret' } }).then((res) => { console.log(res.result.body); }).catch((e) => { //console.log(e); }); }, // 查询单条信息 findSmsByMessageId: function () { wx.cloud.callFunction({ name: 'zhenzisms', data: { $url: 'findSmsByMessageId', apiUrl: 'https://sms_developer.zhenzikj.com', appId: '你的appId', appSecret: '你的appSecret', messageId: 'aaaabbbbba' } }).then((res) => { console.log(res.result.body); }).catch((e) => { //console.log(e); }); } }) [代码]
2019-04-01 - #小程序云开发挑战赛#-同学在哪儿-你说的都队
「同学在哪儿」是一个地图小程序,可以在地图上查看班级同学的毕业去向以及地域分布,多联(蹭)系(饭) 作品简介 [图片] 「同学在哪儿」是一个地图小程序,用户可以创建班级,同学通过填写自己的毕业去向,就可以加入班级,并查看其他的同学的毕业位置去向。小程序的初衷就是希望能够将同学们联系的更加紧密,等到一个陌生的城市,打开这个小程序,可以直接找到能够联(蹭)络(饭)的人。 目标用户 即将毕业、或者已经毕业的学生群体。 实现思路 产品设计 项目的灵感来源于自己的一个 idea,这是我来到初次来到杭州实习的一个感受。刚来的那几天,迫切地想知道有没有同学在杭州。因此有了这样的一个 idea。做一个分布地图,让我能直接在地图上知道大家目前所在的位置以及目前的状态,是在工作 or 在学习。 在网上查了相关信息,没有类似的产品,而知乎上有相关的提问需求,因此也坚定了我做这个产品的信念。 [图片] 这是最初的一个手稿: [图片] 之后学习了 Sketch,将这个手稿用 Skech 进行完善,这是小程序的设计稿:「同学在哪儿」小程序设计稿。 这篇文章介绍了我的一个产品设计的相关理念以及思路:一个 Weekend Project 引发的产品设计思考 产品开发 小程序使用 Taro 框架,利用小程序云开发存储数据并进行前后台交互。整个开发周期约 2 周,并于 8 月中旬上线,中间经历了几次小型的迭代。 整体的构架如下图所示: [图片] 前端 小程序的前端页面使用 Taro 框架进行开发,Taro 是一个开放式跨端跨框架解决方案,使用 React 的语法,可以编译出微信、QQ 等平台的小程序。 前端最主要做的就是页面的展示、模块的拆分,项目的大致目录如下: [代码]. ├── app.scss ├── app.tsx ├── assets ├── components ├── constants ├── index.html ├── pages ├── styles └── utils [代码] 其中利用组件化的思想,将一些模块拆分成可复用的组件,例如小程序的弹窗、标签、按钮、提示框等等。 [图片] 这里我并没有完全使用 Taro-UI,因为引入后会大大增加小程序的体积。大部分的 UI 都是纯手写,通过 SCSS 这类的预编译样式,利用变量、继承、mixin 等一些特性,我可以很方便的抽离出一套属于自己的样式。 [代码]// theme.scss $primary-color: #2F54EB; $hover-primary-color: #1d39c4; $second-color: #ADC6FF; $tag-bg-color: #D6E4FF; $primary-text-color: #303030; $second-text-color: #666666; $grey: #AAAAAA; $light-grey: #DDDDDD; $shadow-color: rgba(0,0,0,0.08); $radius: 12px; // mixin.scss @mixin flex($dir: row, $main: start, $cross: flex-start) { display: flex; flex-direction: $dir; justify-content: $main; align-items: $cross; } [代码] 小程序的页面和组件都是函数式的,比较轻量级,通过 Taro 提供的 hooks,可以达到与原生小程序一样的生命周期。 [代码]import './index.scss' interface IAvatarProps { image: string radius: number border?: number } function Avatar (props: IAvatarProps) { const { image, radius, border} = props useDidShow(() => { }) return ( <View className='avatar_container'> </View> ) } export default Avatar [代码] 云开发 云开发其实就是一套完 Serverless 服务,来降低开发者的成本。目前云开发和小程序结合,可以简化复杂的登录授权、消息订阅、支付等能力。 [图片] 「同学在哪儿」小程序的所有后台能力都是使用云开发来完成,云函数处理用户的请求逻辑、查询数据、内容合法检测等等,云存储存放用户上传的照片以及头像。 云函数中使用到了 tcb-router 这个轻量级类路由库,可以用于优化服务端函数处理逻辑。比如可以在一个云函数里面,分别对某一个数据库进行增删改查。 [代码]exports.main = async (event) => { const {OPENID} = cloud.getWXContext() const app = new TcbRouter({ event }) const collection = 'info' const { info } = event app.use(async (ctx, next) => { ctx.data = {} await next() }); // 添加一条记录 app.router('add', async (ctx, next) => { const now = Date.now() const data = await db.collection(collection).add({ data: {...info, openId: OPENID, createTime: now, updateTime: now} }) ctx.body = { data } }) // 修改个人信息 app.router('update', async (ctx, next) => { const now = Date.now() const data = await db.collection(collection).where({ openId: OPENID }).update({ data: { ...info, updateTime: now } }) ctx.body = { data } }) // 获取个人信息 app.router('get', async (ctx, next) => { const { data } = await db.collection(collection).where({ openId: OPENID }).get() ctx.body = { data } }) return app.serve() } [代码] 功能介绍 首页 [图片] 创建班级 首页可以直接创建班级,填写好相关的信息。如果取消勾选「允许被搜索」选项,用户只能通过分享进入。 信息准确无误后,跳转到创建成功的页面,这时候就可以将创建的班级分享出去。 [图片] 加入班级 将班级分享到班级群,同学输入正确口令即可加入。 [图片] 已加入的班级会在首页显示,长按可以退出班级。 [图片] 完善信息 在加入班级前,会跳转到完善信息的页面,需要同学填好毕业去向以及联系方式,方便其他同学可以及时联系。 [图片] 个人信息只能被同一班级的同学查看。如需修改信息,可在首页点击头像跳转到修改页。 查看分布地图 如果已经加入一个班级,可以在班级详情页面点击「查看分布地图」按钮,进入到地图页面。 点击具体的头像可以查看更多详细的信息,也可以一键联系对方。 [图片] 作品体验二维码 [图片] 视频介绍 「同学在哪儿」视频介绍 团队介绍 阿远:来自关山口男子职业技术学校,性别男,爱好音乐和足球,脑子里时不时会有些灵光一现的想法。
2020-09-17 - 【实战】微信小程序云开发中,使用TcbRouter路由模块在一个云函中解决多个业务逻辑
最近在做自己的小程序《看啥好呢》,这个小程序是使用云开发的方式开发的,功能特别简单,就是获取豆瓣、大麦网的数据展示,虽然功能简单,但还是记录下开发过程和一些技术点,大约会有两篇博文产出,这是第二篇。GitHub 地址 [图片] 在上一篇《实战:在小程序中获取用户所在城市信息》中,介绍了如何获取用户所在城市,这一篇就介绍一下小程序云函数开发的一些东西。 项目结构 小程序《看啥好呢》全部数据都来自豆瓣网和大麦网,整个项目结构如下 [图片] 电影、电视模块下的每个分类,只是改变豆瓣网同一个接口某个字段即可,本地好看模块是拿的大麦网的接口,而电影详情页是使用 Cherrio 实现豆瓣电影详情页网页解析拿到的数据。 项目目录结构 [图片] 项目开发 由于电影、电视列表模块用的都是同一个接口,只是某些参数不同,而详情页是解析网页方式,不是走的接口,所以处理逻辑与列表不相同,怎么样在一个云函数中处理不同的逻辑呢。 从上面的项目目录结构可以看出,我为整个项目只划分了两个云函数,分别是 damai 和 douban,在 damai 中处理来自大麦网的数据,douban 中处理来自豆瓣的数据。 Router 模块 在前端中,Router 可以处理不同的请求分支,于是在云函数中也可以使用 Router,下面使用了 tcb-router,它是一个基于 koa 风格的小程序·云开发云函数轻量级类路由库,主要用于优化服务端函数处理逻辑。 douban/index.js [代码]// 云函数入口文件 const cloud = require("wx-server-sdk"); const TcbRouter = require("tcb-router"); cloud.init(); // 云函数入口函数 exports.main = async (event, context) => { const app = new TcbRouter({ event }); /** 查询列表 */ app.router("list", async (ctx, next) => { const list = require("./list.js"); ctx.body = list.main(event, context); }); /** 查询详情 */ app.router("detail", async (ctx, next) => { const detail = require("./detail.js"); ctx.body = detail.main(event, context); }); return app.serve(); }; [代码] 云函数目录结构如下 [代码]/douban ----/node_modules ----index.js ----list.js ----detail.js ----package.json [代码] HTTP 请求 HTTP 请求方面,小程序云函数中常用的是 request-promise,它是一个 Promise 分格的 HTTP 请求库,使用它还必须安装它的依赖,两个包都要安装 [代码]npm install --save request npm install --save request-promise [代码] 下面看看电影列表是怎么处理的,douban/list.js [代码]const rp = require("request-promise"); exports.main = async (event, context) => { const type = event.type; const tag = encodeURI(event.tag); const limit = event.limit || 50; const start = event.start || 0; const options = { uri: `https://movie.douban.com/j/search_subjects?type=${type}&tag=${tag}&page_limit=${limit}&page_start=${start}`, headers: { Host: "movie.douban.com", Referer: "https://movie.douban.com/", }, json: true, }; return rp(options) .then((res) => res) .catch((err) => { console.log(err); }); }; [代码] 请求参数都放在 event 当中,在调用云函数的时候传递,下面是电影列表页面调用云函数的代码 [代码]let { id, type } = this.data; wx.cloud .callFunction({ name: "douban", data: { $url: "list", type, tag: id == "hot" ? "热门" : "最新", }, }) .then((res) => { const result = res.result; this.setData( { dataList: result.subjects, }, () => { wx.hideLoading(); } ); }) .catch((err) => { console.log(err); wx.showToast({ title: "出错了", icon: "none", }); wx.hideLoading(); }); [代码] 从调用云函数的 data 属性中的第一个参数 [代码]$url[代码] 是请求的路由,第二个参数开始即是请求需要的参数。 Cherrio 实现详情页解析 cheerio 是一个 jQuery Core 的子集,其实现了 jQuery Core 中浏览器无关的 DOM 操作 API,以下是一个简单的示例: [代码]var cheerio = require("cheerio"); // 通过 load 方法把 HTML 代码转换成一个 jQuery 对象 var $ = cheerio.load('<h2 class="title">Hello world</h2>'); // 可以使用与 jQuery 一样的语法来操作 $("h2.title").text("Hello there!"); $("h2").addClass("welcome"); console.log($.html()); // 将输出 <h2 class="title welcome">Hello there!</h2> [代码] 简单来说,cheerio 就是服务器端的 jQuery,去掉了 jQuery 的一些效果类和请求类等等功能后,仅保留核心对 dom 操作的部分,因此能够对 dom 进行和 jQuery 一样方便的操作。它是我们筛选数据的利器——把多余的 html 标签去掉,只留下我们想要的内容的重要工具。需要注意的是,cheerio 并不支持所有 jQuery 的查询语法,比如 [代码]$('a:first')[代码] 会报错 ,只能写成 [代码]$('a').first()[代码] ,在使用的时候需要注意。 下面是电影、电视的详情页处理逻辑 [代码]const rp = require("request-promise"); const cheerio = require("cheerio"); exports.main = async (event, context) => { const subjectId = event.id; const baseUrl = "https://movie.douban.com/j"; const options = { uri: `${baseUrl}/subject_abstract?subject_id=${subjectId}`, headers: { Host: "movie.douban.com", Referer: "https://movie.douban.com/", }, json: true, }; return rp(options) .then((res) => { return rp( `https://movie.douban.com/subject/${subjectId}/` ) .then((html) => { const $ = cheerio.load(html); const plot = $("#link-report") .find("span") .text(); //.replace(/\s/g, '') res.subject.plot = plot; return res; }) .catch((err) => { console.log(err); }); }) .catch((err) => { console.log(err); }); }; [代码] 完整源码已开源 GitHub,是一个很好的学习项目。
2020-12-23 - 如何提升你的云函数性能
在使用云开发一段时间后,你一定会遇见一个问题:虽然云函数非常的方便,但我的云函数似乎性能不够好,为什么我的云函数每次加载都需 2 ~ 3 秒种,时间太长了!。 这篇文章,就来告诉你,应该如何提升你的云函数性能。 如何了解云函数运行情况? 在了解如何优化云函数的运行情况之前, 我们需要先了解,如何查看当前的云函数运行情况,这样才能有个对比。 [图片] 打开小程序开发者工具,并打开你的项目 进入到你要调试的页面,打开调试器 调用云函数,并在调试器中切换到 Network 页面,找到你的请求。 点击你的请求,然后切换到 Timing 页面,查看具体的情况。 在这个页面中,你可以理解其中的 Waiting(TTFB) 是你发起请求到你接收到返回结果的第一个字节的时间,简单的来说,就是服务器计算结果需要花费的时间。而下方的 Content Download 则是下载内容所需的时间,你可以理解是表现出网络速度快慢的数据。 总结来说,就是如果 Waiting TTFB 的值比较大,你就去优化云函数性能。如果 Content Download 的数值毕竟大,你就需要优化网络情况 优化 Waiting TTFB 云函数的运行机制 Waiting TTFB 的优化是云函数性能端的优化,那么在优化之前,我们就需要先来了解一下云函数的运行机制,以便帮助我们了解应该如何去进行性能优化。 [图片] 在蕴含运行时,具体的顺序是这样的 用户发起请求,请求发送到云开发的后台 云开发后台的调度器将请求分发给下方的执行的 worker 、容器 容器创建环境、下载代码 执行代码 在这个过程中,发起请求到云开发、调度器调度速度、调度器传递信息到容器、函数调用等,都是可以优化的,但是我们在具体的使用过程中。这些大都需要由云开发的工作人员来完成,对于我们自己来说,只能去尽可能的优化容器内部到代码层面的东西。 接下来,我们可以看看更细致的调用逻辑。 [图片] 在云开发中,我们可以将调用分为三种类型: 冷启动:图中的红色阶段,需要重新创建容器、下载代码,耗时最长 温启动:图中的黄色阶段,需要下载代码,耗时较长 热启动:图中的蓝色阶段,不需要下载代码,耗时最短 我们可以看到,最快的,是热启动,函数不需要创建容器,不需要启动函数就可以完成执行,显然比要创建容器或要下载代码的温启动和冷启动速度更快。这样,我们就得到了优化云函数性能的第一个方法 1. 让你的云函数每次调用都走热启动 当我们可以让我们的云函数的每一次调用都走热启动,少了容器的创建和函数的部署,请求的速度理所当然的要比冷启动和温启动更快。 我们可以测试一下,我设置每秒调用一次云函数,看看 TTFB 的变化。 [代码]setInterval(()=>{wx.cloud.callFunction({name:'profile'})},1000) [代码] 函数内代码是默认创建的云函数代码。 则对应的执行效果如下 [图片] 可以看到,函数的执行时间从第一次的 1.2s 降低到了 200ms左右,性能提升了 80%,我们仅仅是简单的提升了函数的调用频次,就可以实现提升函数的调用性能,这就是热启动带给我们的价值。 实施方案 如果你需要足够高的性能,不妨借助云开发的定时器,定期唤起你的容器,从而为你的容器保活,确保你的函数时刻被热启动。 2. 缩小你的函数大小 在前面我们曾介绍过,云函数在启动过程中,会创建容器和下载代码。创建容器的过程对于开发者来说不可控,不过我们可以使用一些方法,缩小我们的代码,提升代码的下载速度,比如说,缩小我们的函数代码。 这里我们可以做个测试,这里我创建了两个函数,两个函数的代码完全一致,不同的是,在实验组的函数中,我加入了一个 temp 变量的声明,这个变量的值是一个非常长的字符串,从而使得两个函数的大小分别是 68K 和 4K。 接下来,我们看看二者的执行时间。 [图片] 我们会发现,几乎没有差距的代码,因为加入了变量声明的因素,在性能上会略慢几毫秒,后续随着容器的不断复用,函数的之间的差距也越来越小,几乎可以忽视。 实施方案 对于你的代码,要尽可能的精炼,减少无用的代码,减少代码下载所需时间。 3. 削减不需要的 Package 除了下载代码以外,还需要下载 Node 环境运行所需的依赖包,虽然云开发可能针对 Node Modules 已经做了缓存,但依然存在下载的时间差区别,这里我也做了一个实验。 空包:什么都没装,把 wx-server-sdk 都卸载掉了。 复杂包:装了 Mongoose、sequelize、sails 等依赖的包。 函数逻辑上也相差无几,都是返回 Event ,则结果如下 [图片] 我们发现,前三次可能是因为涉及到依赖包的下载问题,所以前三次的时长大小对比特别的明显,而从第四次开始,二者的区别就不大了,可能是因为依赖已经完成了缓存,所以可以直接使用缓存来完成函数的执行。 实施方案 你可以选择看看你的 package.json ,看看其中是否有你不需要的依赖,将其删除,仅保留有需要的依赖,可以有效提升你的代码执行速度。 优化 Content Download 如果你想要优化 Content Download ,核心需要优化的是两个点: 手机到服务端的节点的距离和速度 内容的大小 前者一般来说,你可以通过切换不同的网络环境来实现优化,比如从 3G 切换到 4G ,从 4G 升级到 5G,这些都可以提升你的手机到服务端节点之间的速度。 此外,还可以借助内容分发网络 CDN 能力来完成缩小你到服务端节点之间的距离,不过对于云函数来说,因为你不可控,无法控制,所以这一点不再谈。 这里补充一句,云开发的文件存储都是有 CDN 的,因此,你通过云存储下载的文件才会比别人更快。 后者则一般通过调整代码来完成,比如只返回必须的资源,对于不需要的内容,不再返回,或压缩返回。 总结 最后,我们回顾一下这篇文章中介绍的优化云函数的方法: 函数下载性能优化 保持函数容器的热启动,提升函数启动性能 缩小函数大小,提升代码下载速度 削减不必要的包,减少依赖大小 网络优化 使用更好的网络,比如 Wi-Fi 云函数中仅返回所需要的内容,减少下载时间。 以上这些方法,你都在你的函数中试过么?有没有其他的优化方法?欢迎你与我分享。
2019-12-08 - 微信小程序新能力:URL Scheme,可从短信跳转小程序
最近小程序上线了一个超级流量的新入口:URL Scheme。通过小程序页面的URL Scheme,可以在短信、邮件或微信外部的网页中打开小程序。 那么如何实现呢?官方文档已经写的很清楚啦,这里简单介绍一下。 首先,获取URL Scheme,通过服务端接口可以获取打开小程序任意页面的URL Scheme,支持生成到期失效和永久有效的URL Scheme。 [图片] 然后,通过短信群发平台将获取的URL Scheme + 营销文案发送到用户的手机上。 最后,用户收到短信后,直接点击URL Scheme唤起微信,跳转到对应小程序页面,就是这么简单。 除此之外,还可以通过邮件或外部浏览器打开跳转小程序。 由于部分操作系统仍不支持直接识别URL Scheme,因此直接将Scheme发送给用户可能存在无法打开小程序的情况。 为此,我们可以先准备一个H5页面,再从H5页面跳转到URL Scheme实现打开小程序。 [代码]location.href = 'weixin://dl/business/?ticket= *TICKET*' [代码] H5的示例代码我已经更新到Github,可以复用起来,基于官方的案例做了些改动,增加PC端打开时生成二维码方便手机扫码使用。 这次新能力的更新将使微信小程序不再局限于微信内部的流量,天花板被掀开啦。 而且短信和邮件营销的触达成本非常低,营销成本的压低也会催生出很多新的流量玩法,我们敬请期待吧。
2021-01-08 - 小程序内嵌二维码长按识别内测QA
小程序内嵌二维码长按识别内测QA Q1:支持识别的码类型与场景如何? A1:小程序内一直支持小程序码的长按识别,公众号二维码仅在小程序内嵌公众号文章场景下识别。 此次放开内测识别的码包括:微信个人码、企业微信个人码、普通群码与互通群码,支持的场景包括: 调用previewImage接口后,长按图片出现菜单:iOS 8.0.6&安卓8.0.3以上版本支持调用previewMedia接口后,长按图片出现菜单:iOS 8.0.6&安卓8.0.3以上版本支持<image>组件将 show-menu-by-longpress属性设置为true后,长按图片出现菜单:iOS 8.0.8&安卓8.0.7以上版本支持(未发布)<web-view>组件中长按图片出现菜单:iOS 8.0.6&安卓8.0.3以上版本支持 Q2:使用该能力时需要注意什么? A2:请勿使用利诱等方式诱导用户添加好友或者加入群聊,页面内容需要遵循小程序运营规范,若发现违反规范的行为将封禁识别能力。 Q3:为什么有些图片长按没有弹出菜单? A3:在小程序中<image>组件需要将 show-menu-by-longpress属性设置为true后才可以直接长按出现菜单。 同时<image>支持识别微信个人码、企微个人码、普通群码、互通群码的能力目前在iOS下存在问题需要客户端进行修复(预计8.0.8版本);安卓8.0.3版本未在此场景下支持,预计8.0.7版本完成支持。 Q4:为什么有些图片长按会出现菜单,也会出二维码的跳转入口,但是点击后不跳转? A4:此问题已知,是iOS的跳转出现了问题,将在8.0.8版本修复 Q5:为什么企业微信群码有时可以识别有时无法识别? A5:请确认是否为企业微信活码,企业微信活码不支持识别,暂无放开计划 Q6:为什么H5中的图片长按不出现菜单,反而出现一个系统的共享/添加到“照片”/拷贝菜单? A6:此处是iOS WebView的特性,可参考此链接进行禁用:https://developers.weixin.qq.com/community/develop/doc/000a20560c89a8f7555a0b16051400
2021-06-09 - wx.chooseImage permission denied 如何排查解决?
按照这个文档配置的 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html API 检查通过,调用没权限 华为Mate30 5G Android 10 EMUI 11 将其中一个安全域名下的的页面url 发送到微信,直接打开,每次必现 [图片] [图片] [图片] [图片]
2021-03-09 - 微信小程序使用自定义目录(文件路径)进行下载/保存 案例(fail permission denied 解决方案)
场景描述 最近项目中有一个需要把网络文件下载下来保存到本地,然后对下载的文件进行读取,待文件不再使用后把文件进行删除的需求。当然也类似的需求还有很多,比如把小程序中的临时图片/文件永久保存下来等等,都是对文件操作的典型场景。 常见问题 在以上场景的实现过程中可能会遇到各式各样的问题,以下是比较常见的几个: 不清楚文件应该保存到哪个目录下。 fail permission denied 文件权限问题。 使用同步函数不清楚怎么获取执行结果。 API提炼 提到文件操作我们会自然而然地想到了API中的FileSystemManager相关的API,我这里用到的函数有以下几个: 下载函数 wx.downloadFile(Object object) 异步函数: FileSystemManager.access(Object object) FileSystemManager.mkdir(Object object) 同步函数: FileSystemManager.accessSync(string path) FileSystemManager.mkdirSync(string dirPath, boolean recursive) 我对同样的业务逻辑分别分别尝试了异步和同步的两种不同的方案,下面我们用一个最简单的下载保存到本地的案例来切入正题。 案例实践 一:获取正确的文件目录路径 当然在保存文件之前我们先要解决一个小问题,那就是我们要保存到哪里?也就是我们自定义的目录。这里我们简单命名其为 [代码]//自定义缓存文件根路径 var rootPath = "......"; [代码] (当然你也可以命名成其他名字) 变量名字可以随便写,不过自定义路径可不能随便写,也不可以在下载的时候直接给一个path路径,否者就会抛出没有权限或找不到文件的异常。所以开发者并不是可以随意的决定自定义文件的路径,这里就不卖关子了,小程序API中有一个很容易被忽略的API, wx.env.USER_DATA_PATH是专门获取文件系统中的用户目录路径的常量值。 这就是我们在小程序中合法的可操作文件的根目录路径: [代码]rootPath = wx.env.USER_DATA_PATH; [代码] 好了到目前为止我们已经知道了我们该往里存储文件了。 定义一下我们下载文件的缓存目录 [代码]var cachePath = rootPath+"/cache"; [代码] 也就是说之后我们下载的文件都会保存到 /cache 目录下,我们在之后的代码中用到的目录路径均为此路径。 二:异步函数实现方案 我们先来用异步函数来实现一下下载保存的流程。 1 下载文件之前我们首先要判断当前目录是否存在,如果目录不存在我们就直接下载文件到该目录下就会抛出 fail permission denied [图片] 这是判断目录存在的代码 [代码] access() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); fm.access({ path: cachePath, success: function(res) { resolve(); }, fail: function(err) { resolve(err); } }); }); }, [代码] 2 如果目录真实存在那我们当然可以直接使用,如果目录不存在则需要开发者自己创建目录。 [代码] mkdir(){ return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); fm.mkdir({ dirPath: cachePath, recursive: true, success: function(res) { resolve(); }, fail: function(err) { resolve(err); } }); }); }, [代码] 代码执行完之后我可以验证一下目录是否如我们所愿被创建出来。 开发工具右上角的详情–>基本信息–>文件系统–>当前小程序文件系统根目录 [图片] 点击usr文件夹进入根目录 [图片] 执行完上面代码之后我们可以看一下 cache 文件夹确实已经存在了 [图片] 3 目录也存在了,万事具备只欠下载了 [代码] downloadFile() { let fileUrl = 'https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg'; wx.downloadFile({ url: fileUrl, filePath: cachePath + '/temp.png', success: function(res) { console.log('downloadFile success', res); }, fail: function(err) { console.log('downloadFile fail', err); } }); }, [代码] 那执行完下载的代码之后我们再来看看cache目录 [图片] 这就是我们刚刚下载的图片。 ok,异步的整个下载和文件创建流程就走完了。接下来我们来瞅瞅同步流程中有哪些需要我们注意的。 三: 同步方案 同步方案和异步方案的流程大体一致,都是先判断文件目录是否存在,若不存在则创建目录,存在则执行下载逻辑。 使用同步函数需要特别注意的是怎么去判断函数的执行结果,由于 FileSystemManager.accessSync(string path) FileSystemManager.mkdirSync(string dirPath, boolean recursive) 这两个同步函数没有直接给出任何的返回结果。那我们怎么知道目录是否存在、目录是否被创建成功了呢? 这里我们需要剑走偏锋一下,即利用同步函数抛出的异常来判断结果。 我们直接来看代码 [代码] accessSync() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); try { fm.accessSync(cachePath); resolve(); } catch (err) { resolve(err); } }); }, mkdirSync() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); try { fm.mkdirSync(cachePath, true); resolve(); } catch (err) { resolve(err); } }); }, [代码] 可以看到我们的代码中多了 try catch 的代码结构,因为同步方法中没有直接返回给我们可用的信息,那我们可以认为同步函数正常执行完的结果为true或success,而进入 catch 后则结果为false或fail亦或根据具体异常具体处理。我们利用同步函数来走一遍下载保存的流程。 [代码] // 同步函数流程 this.accessSync().then(function (err) { if (err) { return that.mkdirSync(); } }).then(function (err) { if (!err) { that.downloadFile() } }); [代码] 可以看到我们利用同步函数下载的图片。 [图片] 总结时刻 我们分别使用异步和同步函数完成了目录的创建和文件的下载等流程。在这个过程中我们特别需要注意几点: 操作文件的根目录是以 wx.env.USER_DATA_PATH 开头的。 使用自定义目录时一定主要不可直接使用,需要增加 判断目录存在、创建目录 两个步骤。 使用同步函数时的执行结果是通过抓取同步函数抛出的异常来进行判断的。在没有给出直接结果的时候要学会利用异常信息来达到目的。 在创建目录时如果该目录存在同样会抛出异常(fail file already exists),这时按照success逻辑继续往下执行即可。 异步方案中介绍了目录查看的步骤和方法,可自行验证。其中usr目录是开发者自定义目录根节点,tmp目录是小程序默认的缓存根节点。 结尾 叙述若有不对或不严谨之处还请不吝指正。
2019-10-31 - easyEcharts 3.0版本新增雷达图,旧(折线,柱状,饼图,地图)数据treeTopo
pc端实例:http://jstopo.top 网站本人微信号:jays611 easyEcharts又称简易echarts(本人针对uniapp 的canvas纯JS源码绘制, 如出现bug可以及时在论坛或联系微信,会及时修改。更新时间:2021-05-31 16:29 性能优越!不依赖任何JS包,让使用者可以自行查看分析原理,修改源码!) [图片] [图片] [图片] 0.new 雷达图 参数实例: [代码]let grid = { x: dom.width/2,//占总canvas标签元素的宽度一半 y: dom.height/2,//占总canvas元素的高度一半 R: dom.height/2-5,//半径 splitNumber: 5,//分几段圆 textColor: "#565656",//文字颜色 arcColor: "#838383",//圆的线条颜色 areaColor: "rgba(251,180,167,0.8)",//中间多边形区域颜色 }, arrText = [//数据文字 {name: "2星",part: 0.8}, {name: "3星",part: 0.56}, {name: "4星",part: 0.72}, {name: "5星",part: 0.66}, {name: "6星",part: 0.87} ]; [代码] 1. line版块(折线图) 参数实例: [代码]let grid = { top: (12 * elem.height) / 100, //canvas标签的高度的12%(相对总高的百分比) bottom: ((100 - 18) * elem.height) / 100, //canvas图形距离底部的百分比 18% left: (12 * elem.width) / 100, //距离左侧的百分比(12%总宽度) right: ((100 - 8) * elem.width) / 100 //距离右侧百分比(8%总宽度) }, lineColor = "#999", //x,y轴线颜色 fillColor = "#333", //x,y轴number颜色 yAxis = { textSize: 10, //刻度数字fontSize maxNumber: 80, //分段的最大值 splitNumber: 5, //分成几段 splitLen: 5, //轴左侧的小横线 -| marginSplit: 5 //刻度文字与 “-|”的距离 }, lineWidth = 1, xAxis = { textSize: 10, //刻度数字fontSize maxNumber: 50, splitNumber: 5, splitLen: 5, marginSplit: 5 }, dotStyle = [{ color: "#fff", arcR: 4, //半径 dash: 0 //是否线条虚线 0实线 1以上虚线 }, { color: "#fff", arcR: 4, //半径 dash: 0 //是否线条虚线 0实线 1以上虚线 }], lineStyle = [{ color: "#4caf50", lineDotType: "wave", //两圆点的连接线 line直线,wave二次贝塞尔曲线 width: 2, //连线的width dash: 3 //是否线条虚线 0实线 1以上虚线 }, { color: "#03a9f4", lineDotType: "wave", //两圆点的连接线 line直线,wave二次贝塞尔曲线 width: 2, //连线的width dash: 0 //是否线条虚线 0实线 1以上虚线 }], dataJSON = [//月份数据,字符类型 { x: ["1月", "3月", "5月", "9月", "10月", "12月"],//设置字符类型,非Number y: [69, 28, 70, 65, 76, 65] }, { x: ["1月", "3月", "5月", "9月","10月","12月"],//非Number类型数据复制第一条x的数据 y: [9, 18, 70, 75, 56, 35] } ]; 二者数据类型只能选一种 dataJSON = [ //数据data number类型数据 { x: this.sortFn([10, 30, 35, 39, 45, 28]),//sortFn是封装的排序方法 y: [69, 28, 70, 65, 76, 65] }, { x: this.sortFn([5, 23, 45, 39, 50, 28]), y: [9, 18, 70, 75, 56, 35] } ]; [代码] 2. bar版块(柱状图 2d, 伪3d) 伪3d版参数实例: [代码]let grid = { top: (10 * elem.height) / 100, //canvas标签的高度的12%(相对总高的百分比) bottom: ((100 - 18) * elem.height) / 100, left: (12 * elem.width) / 100, //距离左侧的百分比(12%总宽度) right: ((100 - 8) * elem.width) / 100 }, lineColor = "#999", //x,y轴线颜色 fillColor = "#333", //x,y轴number颜色 lineWidth = 1, yAxis = { textSize: 10, //刻度数字fontSize maxNumber: 80, //分段的最大值 splitNumber: 5, //分成几段 splitLen: 5, //轴左侧的小横线 -| marginSplit: 5 //刻度文字与 “-|”的距离 }, xAxis = { textSize: 10, //刻度数字fontSize maxNumber: 50, splitNumber: 5, splitLen: 5, marginSplit: 3 }, barMargin = 5, //柱子之间间隔 barStyle = [{ faceStyle: [{ //up face fillColor: "#4ed837", strokeColor: "#ccc" }, { //down face fillColor: "#3f51b5", strokeColor: "#ccc" }], fillColor: "#3f51b5", strokeColor: "#ccc", color: "#3f51b5", lineWidth: 1, barWidth: 18, //连线的width dash: 3 //是否线条虚线 0实线 1以上虚线 }, { faceStyle: [{// fillColor: "#4ed837", strokeColor: "#ccc" }, { fillColor: "#03a9f4", strokeColor: "#ccc" }], fillColor: "#03a9f4", strokeColor: "#ccc", color: "#03a9f4", lineWidth: 1,// 使用于ctx.setLineWidth(1) barWidth: 18, //bar柱子的width dash: 0 //是否线条虚线 0实线 1以上虚线 }], xData = ["楚国之汉国争霸", "秦国", "韩国", "魏国", "赵国"], dataJSON = [ //数据对象 { y: [69, 28, 70, 65, 76] }, { y: [9, 18, 50, 75, 56] }]; [代码] 3. Pie饼图,环形图 参数实例: [代码]let radius = { outside: {//外侧圆 x: (50 * elem.width) / 100,//elem是canvas的dom,50/100是总width的1/2,百分比50% y: (50 * elem.height) / 100,//50%的元素高作为圆心的y坐标 r: (43 * elem.height) / 100//43%元素的高作为半径 }, inside: {//内侧圆 r: 50 / 100//占外圈圆的百分比 } }, colorRadius = ["#999", "#34ED56", "#555"], //设置扇形1,2,3颜色 arcWidth = 1, //圆边框线宽 radiusOutStyle = { //环外侧圆样式 fillColor: "#666",//形状颜色对应setFillColor strokeColor: "#999"//线条颜色对应setStrokeColor }, radiusInStyle = { //环内侧圆样式 fillColor: "#f8f8f8", strokeColor: "#999" }, dataJSON = [30, 80, 180]; //colorRadius数据json对应数量 [代码] 4.map地图 2021-4-26 参数实例: [代码]import map from "@/static/map.js" mapCenter:{//map整个地图区域中心点位置 默认江西省的板块中心点 经纬度 lng: 116,//经度 lat: 27.25//纬度 }, colorStyleMap:{//样式参数 map地图 2021-4-26 strokeColor:{ default: "#8f8f8f",//默认样式 isTouch: "#258429",//touch选中样式 }, textColor: "#333",//文字样式 fillColor:{ default: "#efefef", isTouch: "#4CD964", } },//地图的样式 map地图组件参数 2021-4-26 max: 28,//缩放的倍数 map地图组件参数 2021-4-26 pointArr: [],//所有地图坐标数组,map地图组件参数 2021-4-26 colorMapIndex: null,//map地图组件参数 2021-4-26 mapIndex: 0,//map地图组件参数 2021-4-26 geoJsonData: map,//高德地图的api数据(下载到js本地/static/map.js)map地图组件参数 2021-4-26 [代码] 其余水球,水柱,环,参数详情查看源码中函数function实例。更新了lineDraw折线x轴字符类型渲染方法。 这个是topo的跳转路由/pages/topo/topo
2021-06-04 - app.onLaunch与page.onLoad异步问题
问题:相信很多人都遇到过这个问题,通常我们会在应用启动app.onLaunch() 去发起静默登录,同时我们需要在加载页面的时候,去调用一个需要登录态的后端 API 。由于两者都是异步,往往page.onload()调用API的时候,app.onLaunch() 内调用的静态登录过程还没有完成,从而导致请求失败。 解决方案:1. 通过回调函数// on app.js App({ onLaunch() { login() // 把hasLogin设置为 true .then(() => { this.globalData.hasLogin = true; if (this.checkLoginReadyCallback) { this.checkLoginReadyCallback(); } }) // 把hasLogin设置为 false .catch(() => { this.globalData.hasLogin = false; }); }, }); // on page.js Page({ onLoad() { if (getApp().globalData.hasLogin) { // 登录已完成 fn() // do something } else { getApp().checkLoginReadyCallback = () => { fn() } } }, }); ⚠️注意:这个方法有一定的缺陷(如果启动页中有多个组件需要判断登录情况,就会产生多个异步回调,过程冗余),不建议采用。 2. 通过Object.defineProperty监听globalData中的hasLogin值 // on app.js App({ onLaunch() { login() // 把hasLogin设置为 true .then(() => { this.globalData.hasLogin = true; }) // 把hasLogin设置为 false .catch(() => { this.globalData.hasLogin = false; }); }, // 监听hasLogin属性 watch: function (fn) { var obj = this.globalData Object.defineProperty(obj, 'hasLogin', { configurable: true, enumerable: true, set: function (value) { this._hasLogin = value; fn(value); }, get: function () { return this._hasLogin } }) }, }); // on page.js Page({ onLoad() { if (getApp().globalData.hasLogin) { // 登录已完成 fn() // do something } else { getApp().watch(() => fn()) } }, }); 3. 通过beautywe的状态机插件(项目中使用该方法) // on app.js import { BtApp } from '@beautywe/core/index.js'; import status from '@beautywe/plugin-status/index.js'; import event from '@beautywe/plugin-event/index.js'; const app = new BtApp({ onLaunch() { // 发起静默登录调用 login() // 把状态机设置为 success .then(() => this.status.get('login').success()) // 把状态机设置为 fail .catch(() => this.status.get('login').fail()); }, }); // status 插件依赖于 beautywe-plugin-event app.use(event()); // 使用 status 插件 app.use(status({ statuses: [ 'login' ], })); // 使用原生的 App 方法 App(app); // on page.js Page({ onLoad() { // must 里面会进行状态的判断,例如登录中就等待,登录成功就直接返回,登录失败抛出等。 getApp().status.get('login').must().then(() => { // 进行一些需要登录态的操作... }) }, }); 具体实现 具体实现可以参考我的商城小程序项目 项目体验地址:体验 代码:代码
2021-05-20 - 微信小程序转发动态消息后点击提醒,当动态消息结束后,没有提醒。
[图片] 如上图所示 结束后什么消息也没有通知。
2020-06-05 - updateShareMenu设置为动态消息,但是不显示提醒按钮
转发,只出现“成员正在加入,当前1/3人”,但是不显示提醒按钮???出现这句话是不是说明调用及配置都是对的,为什么不出现提醒按钮呢 [图片]
2018-11-06 - wx.getShareInfo?
通过 wxgetShareInfo shareTicket 换取群标识 但是现在 在开发版可以获取shareTicket 体验版之前可以 从昨天下午在群聊中获取不到shareTicket打开场景值为1008 请问这是为什么呢 写了个demo 但是无法复现 期望 发送够群聊打开有shareTicket 场景值为1044 官方大大能给个回复吗???
2019-12-31 - 微信小程序全局mixin 全局stroe 全局状态管理 全局公共组件 方案 mp-mixin js库 劫持生命周期
前言 由于微信小程序没有顶层组件与原生mixin方案,在做一些全局公共组件时十分棘手,也没有办法跨组件维持状态 所以笔者将我平时用到的一个方案封装成了一个js库 mp-mixin , 分享给大家,希望能够有帮助,以下为github地址与文档 该库原理大致就是劫持Page构造器和组件setData等api来实现状态注入与跨组件状态同步 当然,对于使用uni-app taro等框架的,可以忽略这个问题,不存在这个问题哈,这些框架支持全局组件或mixin mp-mixin <h3>🚀 微信小程序 mixin 和 store 方案</h3> 1. 特性 支持 mixin data、methods、生命周期及Page事件 支持不同Page 使用 store 共用状态 支持全局 mixin 和 store typescript编写 支持QQ小程序 以及其他api和微信小程序相似的小程序 2. 快速使用 2.1 npm 安装 [代码]npm i mp-mixin [代码] [代码]import 'mp-mixin'; [代码] 2.2 cdn 点击下载 cdn 文件,复制到您的小程序项目中,然后 import 这个文件就可以 cdn地址: https://cdn.jsdelivr.net/npm/mp-mixin/mp-mixin.min.js 2.3 快速使用 2.3.1 mixin 对象 mixin 是一个对象,数据结构如下 [代码]const mixin = { data: {}, // 可选 methods: {}, // 可选 store: wx.creteStore({}), // 可选 当全局注入时,store可以是一个json, 否则 必须是 store对象 // 以下为Page生命周期或事件 onLoad(){ }, onShareAppMessage(){ } } [代码] 2.3.2 全局mixin 全局mixin, 推荐在 app.js 中引入 [代码]import 'mp-mixin'; wx.mixin(mixin); // mixin 对象 见 2.3.1 [代码] 2.3.3 Page mixin 也可以在Page构造中按需引入 mixin [代码]Page({ mixin: mixin, // mixin 对象 见 2.3.1 // ... }) [代码] 说明 如有相同的键值对,优先级为 组件 > 局部mixin > 全局mixin data 优先级 高于 store mixin 中的 data 会被深克隆分别注入对应的Page中的data,使用setData互不影响 mixin 中的 store也会被注入Page中的data,区别是如果不同Page引入的是同一个,则一个页面setData会影响其他页面的 状态,且UI会更新 3 api 引入 mp-mixin 之后,mp-mixin 会将一下三个 api 挂载到 wx 对象上 [代码]wx.mixin wx.createStore wx.initGlobalStore [代码] [代码]wx.initGlobalStore[代码] 等价于在 [代码]wx.mixin[代码] 方法中加入 store属性 [代码]wx.initGlobalStore({ // state }) wx.mixin({ store: { // state } }) [代码] 您也可以主动引入来使用上述三个API [代码]import {globalMixin, createStore, initGlobalStore} from 'mp-mixin' // ... [代码] 您可以通过 [代码]injectStaff[代码] 方法手动注入到任何对象上 [代码]import {injectStaff} from 'mp-mixin' injectStaff(anyObject); [代码] 4. 类型声明 type.d.ts index.d.ts
2021-05-17 - 升级2.0!可延迟显示的小程序loading组件
一、介绍 可控制延迟显示的微信小程序 loading 组件,默认请求超过0.5s才显示loading动画;支持 slot 自定义 loading 内容。 在项目中,若网络良好的情况下,每次请求都显示loading动画,会导致页面短时间内频繁闪现loading动画,用户体验不佳。本组件可自定义loading组件显示延时,只有当请求超过设置的时间未完成时,才显示loading动画,减少loading动画出现的次数。 注:2.0版本简化了使用流程及API,与1.x版本不兼容。 点击查看 demo 二、使用 安装 [代码]npm i wx-delay-loading[代码] 组件初始化:在 app.js 的 onLaunch 中执行组件初始化方法,挂载全局对象 DLoading [代码]// app.js import DelayLoading from 'wx-delay-loading/lib/index' App({ onLaunch: function () { // 初始化组件,挂载全局对象 DLoading DelayLoading.init() } }) [代码] 在使用组件的页面或组件的配置 json 内,引入组件 注:微信小程序组件名不允许使用 wx 做前缀 [代码]// page.json "usingComponents": { // 微信小程序组件名不允许使用wx做前缀 "delay-loading": "wx-delay-loading/index" } [代码] 在页面 wxml 中使用,设置 id 属性为 loading,否则 DLoading 静态方法会找不到组件。 注:若 delay-loading 组件存在父组件,需要同时把父组件和 delay-loading 组件的 id 设为 loading [代码]// page.wxml // 不使用 slot <delay-loading id="loading" /> // 使用 slot 自定义内容 <delay-loading id="loading" customLoading="{{true}}"> <view class="container"> <image class="logo" src="/static/image/logo.png" mode="widthFix" /> <view class="text">加载中...</view> </view> </delay-loading> [代码] 请求开始时(例如 wx.request),调用全局对象 DLoading 的静态方法 setReqDelay(delaytime),delaytime 默认为超过500毫秒请求未结束则显示 loading 组件;delaytime 为0时,每次请求都会显示组件。<br/> 请求结束时,调用静态方法 endReq(),会检查正在进行的请求数,若为0,则隐藏 loading 组件。 [代码]// page.js Page({ // 仅为示例 exampleRequest () { // 请求开始 DLoading.setReqDelay(300) // 请求超过0.3秒没完成,显示 loading 组件 wx.request({ url: 'https://example.com/getData', complete () { // 请求完成 DLoading.endReq() } }) }, }) [代码] 三、进阶:在统一封装请求 request.js 内使用 项目开发中,通常会针对请求和响应进行统一处理,封装成一个 request.js 使用。 [代码]// request.js const request = (options) => { return new Promise ((resolve, reject) => { // 请求开始前调用设置延时 DLoading.setReqDelay() wx.request({ ...options, success (res) { // 请求成功后的各种处理操作... resolve(res.data) }, fail (err) { // 请求失败后的各种处理操作... reject(err) }, complete () { // 请求完成 DLoading.endReq() } }) }) } export default request [代码] [代码]// page.js import request from request.js Page({ // 仅为示例 exampleRequest () { // 使用封装后的request request({ url: 'https://example.com/getData' }).then(res => { // 对返回数据的处理... }) }, }) [代码] 四、调试:模拟低网速情况 通常在网络环境良好的情况下,请求都会很快完成,不会超过0.5s。可通过微信开发者工具-调试器-Network,把网络设置 Online,更改为 Slow 3G,或者使用 Custom 自定义网络速度。 五、文档 组件 options 参数 说明 类型 默认值 customLoading 是否使用 slot 插槽自定义 loading 内容 boolean false id 组件标识 string 需手动设置为 loading <br/> 对象 methods 方法名 说明 参数 参数类型 init 初始化组件,挂载全局对象 DLoading - - setReqDelay 标记请求开始并设置延迟显示的时间 延迟的时间,单位毫秒 number endReq 检测正在进行的请求数,若清零则隐藏 loading 组件 - - <br/> 六、示例 点击查看 demo
2021-05-17 - 通过授权登录介绍小程序原生开发如何引入async/await、状态管理等工具
登陆和授权是小程序开发会遇到的第一个问题,这里把相关业务逻辑、工具代码抽取出来,展示我们如何引入的一些包使得原生微信小程序内也可以使用 async/await、fetch、localStorage、状态管理、GraphQL 等等特性,希望对大家有所帮助。 前端 目录结构 [代码]├── app.js ├── app.json ├── app.wxss ├── common │ └── api │ └── index.js ├── config.js ├── pages │ └── index │ ├── api │ │ └── index.js │ ├── img │ │ ├── btn.png │ │ └── bg.jpg │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss ├── project.config.json ├── store │ ├── action.js │ └── index.js ├── utils │ └── index.js └── vendor ├── event-emitter.js ├── fetch.js ├── fetchql.js ├── http.js ├── promisify.js ├── regenerator.js ├── storage.js └── store.js [代码] 业务代码 app.js [代码]import store from './store/index' const { loginInfo } = store.state App({ store, onLaunch() { // 打开小程序即登陆,无需用户授权可获得 openID if(!loginInfo) store.dispatch('login') }, }) [代码] store/index.js [代码]import Store from '../vendor/store' import localStorage from '../vendor/storage' import actions from './action' const loginInfo = localStorage.getItem('loginInfo') export default new Store({ state: { // 在全局状态中维护登陆信息 loginInfo, }, actions, }) [代码] store/action.js [代码]import regeneratorRuntime from '../vendor/regenerator'; import wx from '../vendor/promisify'; import localStorage from '../vendor/storage' import api from '../common/api/index'; export default { async login({ state }, payload) { const { code } = await wx.loginAsync(); const { authSetting } = await wx.getSettingAsync() // 如果用户曾授权,直接可以拿到 encryptedData const { encryptedData, iv } = authSetting['scope.userInfo'] ? await wx.getUserInfoAsync({ withCredentials: true }) : {}; // 如果用户未曾授权,也可以拿到 openID const { token, userInfo } = await api.login({ code, encryptedData, iv }); // 为接口统一配置 Token getApp().gql.requestObject.headers['Authorization'] = `Bearer ${token}`; // 本地缓存登陆信息 localStorage.setItem('loginInfo', { token, userInfo } ) return { loginInfo: { token, userInfo } } } } [代码] common/api/index.js [代码]import regeneratorRuntime from '../../vendor/regenerator.js' export default { /** * 登录接口 * 如果只有 code,只返回 token,如果有 encryptedData, iv,同时返回用户的昵称和头像 * @param {*} param0 */ async login({ code, encryptedData, iv }) { const query = `query login($code: String!, $encryptedData: String, $iv: String){ login(code:$code, encryptedData:$encryptedData, iv:$iv, appid:$appid){ token userInfo { nickName avatarUrl } } }` const { login: { token, userInfo } } = await getApp().query({ query, variables: { code, encryptedData, iv } }) return { token, userInfo } }, } [代码] pages/index/index.js [代码]import regeneratorRuntime from '../../vendor/regenerator.js' const app = getApp() Page({ data: {}, onLoad(options) { // 将用户登录信息注入到当前页面的 data 中,并且当数据在全局范围内被更新时,都会自动刷新本页面 app.store.mapState(['loginInfo'], this) }, async login({ detail: { errMsg } }) { if (errMsg === 'getUserInfo:fail auth deny') return app.store.dispatch('login') // 继续处理业务 }, }) [代码] pages/index/index.wxml [代码]<view class="container"> <form report-submit="true" bindsubmit="saveFormId"> <button form-type="submit" open-type="getUserInfo" bindgetuserinfo="login">登录</button> </form> </view> [代码] 工具代码 事件处理 vendor/event-emitter.js [代码]const id_Identifier = '__id__'; function randomId() { return Math.random().toString(36).substr(2, 16); } function findIndexById(id) { return this.findIndex(item => item[id_Identifier] === id); } export default class EventEmitter { constructor() { this.events = {} } /** * listen on a event * @param event * @param listener */ on(event, listener) { let { events } = this; let container = events[event] || []; let id = randomId(); let index; listener[id_Identifier] = id; container.push(listener); return () => { index = findIndexById.call(container, id); index >= 0 && container.splice(index, 1); } }; /** * remove all listen of an event * @param event */ off (event) { this.events[event] = []; }; /** * clear all event listen */ clear () { this.events = {}; }; /** * listen on a event once, if it been trigger, it will cancel the listner * @param event * @param listener */ once (event, listener) { let { events } = this; let container = events[event] || []; let id = randomId(); let index; let callback = () => { index = findIndexById.call(container, id); index >= 0 && container.splice(index, 1); listener.apply(this, arguments); }; callback[id_Identifier] = id; container.push(callback); }; /** * emit event */ emit () { const { events } = this; const argv = [].slice.call(arguments); const event = argv.shift(); ((events['*'] || []).concat(events[event] || [])).map(listener => self.emitting(event, argv, listener)); }; /** * define emitting * @param event * @param dataArray * @param listener */ emitting (event, dataArray, listener) { listener.apply(this, dataArray); }; } [代码] 封装 wx.request() 接口 vendor/http.js [代码]import EventEmitter from './event-emitter.js'; const DEFAULT_CONFIG = { maxConcurrent: 10, timeout: 0, header: {}, dataType: 'json' }; class Http extends EventEmitter { constructor(config = DEFAULT_CONFIG) { super(); this.config = config; this.ctx = wx; this.queue = []; this.runningTask = 0; this.maxConcurrent = DEFAULT_CONFIG.maxConcurrent; this.maxConcurrent = config.maxConcurrent; this.requestInterceptor = () => true; this.responseInterceptor = () => true; } create(config = DEFAULT_CONFIG) { return new Http(config); } next() { const queue = this.queue; if (!queue.length || this.runningTask >= this.maxConcurrent) return; const entity = queue.shift(); const config = entity.config; const { requestInterceptor, responseInterceptor } = this; if (requestInterceptor.call(this, config) !== true) { let response = { data: null, errMsg: `Request Interceptor: Request can\'t pass the Interceptor`, statusCode: 0, header: {} }; entity.reject(response); return; } this.emit('request', config); this.runningTask = this.runningTask + 1; let timer = null; let aborted = false; let finished = false; const callBack = { success: (res) => { if (aborted) return; finished = true; timer && clearTimeout(timer); entity.response = res; this.emit('success', config, res); responseInterceptor.call(this, config, res) !== true ? entity.reject(res) : entity.resolve(res); }, fail: (res) => { if (aborted) return; finished = true; timer && clearTimeout(timer); entity.response = res; this.emit('fail', config, res); responseInterceptor.call(this, config, res) !== true ? entity.reject(res) : entity.resolve(res); }, complete: () => { if (aborted) return; this.emit('complete', config, entity.response); this.next(); this.runningTask = this.runningTask - 1; } }; const requestConfig = Object.assign(config, callBack); const task = this.ctx.request(requestConfig); if (this.config.timeout > 0) { timer = setTimeout(() => { if (!finished) { aborted = true; task && task.abort(); this.next(); } }, this.config.timeout); } } request(method, url, data, header, dataType = 'json') { const config = { method, url, data, header: { ...header, ...this.config.header }, dataType: dataType || this.config.dataType }; return new Promise((resolve, reject) => { const entity = { config, resolve, reject, response: null }; this.queue.push(entity); this.next(); }); } head(url, data, header, dataType) { return this.request('HEAD', url, data, header, dataType); } options(url, data, header, dataType) { return this.request('OPTIONS', url, data, header, dataType); } get(url, data, header, dataType) { return this.request('GET', url, data, header, dataType); } post(url, data, header, dataType) { return this.request('POST', url, data, header, dataType); } put(url, data, header, dataType) { return this.request('PUT', url, data, header, dataType); } ['delete'](url, data, header, dataType) { return this.request('DELETE', url, data, header, dataType); } trace(url, data, header, dataType) { return this.request('TRACE', url, data, header, dataType); } connect(url, data, header, dataType) { return this.request('CONNECT', url, data, header, dataType); } setRequestInterceptor(interceptor) { this.requestInterceptor = interceptor; return this; } setResponseInterceptor(interceptor) { this.responseInterceptor = interceptor; return this; } clean() { this.queue = []; } } export default new Http(); [代码] 兼容 fetch 标准 vendor/fetch.js [代码]import http from './http'; const httpClient = http.create({ maxConcurrent: 10, timeout: 0, header: {}, dataType: 'json' }); function generateResponse(res) { let header = res.header || {}; let config = res.config || {}; return { ok: ((res.statusCode / 200) | 0) === 1, // 200-299 status: res.statusCode, statusText: res.errMsg, url: config.url, clone: () => generateResponse(res), text: () => Promise.resolve( typeof res.data === 'string' ? res.data : JSON.stringify(res.data) ), json: () => { if (typeof res.data === 'object') return Promise.resolve(res.data); let json = {}; try { json = JSON.parse(res.data); } catch (err) { console.error(err); } return json; }, blob: () => Promise.resolve(new Blob([res.data])), headers: { keys: () => Object.keys(header), entries: () => { let all = []; for (let key in header) { if (header.hasOwnProperty(key)) { all.push([key, header[key]]); } } return all; }, get: n => header[n.toLowerCase()], has: n => n.toLowerCase() in header } }; } export default (typeof fetch === 'function' ? fetch.bind() : function(url, options) { options = options || {}; return httpClient .request(options.method || 'get', url, options.body, options.headers) .then(res => Promise.resolve(generateResponse(res))) .catch(res => Promise.reject(generateResponse(res))); }); [代码] GraphQL客户端 vendor/fetchql.js [代码]import fetch from './fetch'; // https://github.com/gucheen/fetchql /** Class to realize fetch interceptors */ class FetchInterceptor { constructor() { this.interceptors = []; /* global fetch */ this.fetch = (...args) => this.interceptorWrapper(fetch, ...args); } /** * add new interceptors * @param {(Object|Object[])} interceptors */ addInterceptors(interceptors) { const removeIndex = []; if (Array.isArray(interceptors)) { interceptors.map((interceptor) => { removeIndex.push(this.interceptors.length); return this.interceptors.push(interceptor); }); } else if (interceptors instanceof Object) { removeIndex.push(this.interceptors.length); this.interceptors.push(interceptors); } this.updateInterceptors(); return () => this.removeInterceptors(removeIndex); } /** * remove interceptors by indexes * @param {number[]} indexes */ removeInterceptors(indexes) { if (Array.isArray(indexes)) { indexes.map(index => this.interceptors.splice(index, 1)); this.updateInterceptors(); } } /** * @private */ updateInterceptors() { this.reversedInterceptors = this.interceptors .reduce((array, interceptor) => [interceptor].concat(array), []); } /** * remove all interceptors */ clearInterceptors() { this.interceptors = []; this.updateInterceptors(); } /** * @private */ interceptorWrapper(fetch, ...args) { let promise = Promise.resolve(args); this.reversedInterceptors.forEach(({ request, requestError }) => { if (request || requestError) { promise = promise.then(() => request(...args), requestError); } }); promise = promise.then(() => fetch(...args)); this.reversedInterceptors.forEach(({ response, responseError }) => { if (response || responseError) { promise = promise.then(response, responseError); } }); return promise; } } /** * GraphQL client with fetch api. * @extends FetchInterceptor */ class FetchQL extends FetchInterceptor { /** * Create a FetchQL instance. * @param {Object} options * @param {String} options.url - the server address of GraphQL * @param {(Object|Object[])=} options.interceptors * @param {{}=} options.headers - request headers * @param {FetchQL~requestQueueChanged=} options.onStart - callback function of a new request queue * @param {FetchQL~requestQueueChanged=} options.onEnd - callback function of request queue finished * @param {Boolean=} options.omitEmptyVariables - remove null props(null or '') from the variables * @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api) */ constructor({ url, interceptors, headers, onStart, onEnd, omitEmptyVariables = false, requestOptions = {}, }) { super(); this.requestObject = Object.assign( {}, { method: 'POST', headers: Object.assign({}, { Accept: 'application/json', 'Content-Type': 'application/json', }, headers), credentials: 'same-origin', }, requestOptions, ); this.url = url; this.omitEmptyVariables = omitEmptyVariables; // marker for request queue this.requestQueueLength = 0; // using for caching enums' type this.EnumMap = {}; this.callbacks = { onStart, onEnd, }; this.addInterceptors(interceptors); } /** * operate a query * @param {Object} options * @param {String} options.operationName * @param {String} options.query * @param {Object=} options.variables * @param {Object=} options.opts - addition options(will not be passed to server) * @param {Boolean=} options.opts.omitEmptyVariables - remove null props(null or '') from the variables * @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api) * @returns {Promise} * @memberOf FetchQL */ query({ operationName, query, variables, opts = {}, requestOptions = {}, }) { const options = Object.assign({}, this.requestObject, requestOptions); let vars; if (this.omitEmptyVariables || opts.omitEmptyVariables) { vars = this.doOmitEmptyVariables(variables); } else { vars = variables; } const body = { operationName, query, variables: vars, }; options.body = JSON.stringify(body); this.onStart(); return this.fetch(this.url, options) .then((res) => { if (res.ok) { return res.json(); } // return an custom error stack if request error return { errors: [{ message: res.statusText, stack: res, }], }; }) .then(({ data, errors }) => ( new Promise((resolve, reject) => { this.onEnd(); // if data in response is 'null' if (!data) { return reject(errors || [{}]); } // if all properties of data is 'null' const allDataKeyEmpty = Object.keys(data).every(key => !data[key]); if (allDataKeyEmpty) { return reject(errors); } return resolve({ data, errors }); }) )); } /** * get current server address * @returns {String} * @memberOf FetchQL */ getUrl() { return this.url; } /** * setting a new server address * @param {String} url * @memberOf FetchQL */ setUrl(url) { this.url = url; } /** * get information of enum type * @param {String[]} EnumNameList - array of enums' name * @returns {Promise} * @memberOf FetchQL */ getEnumTypes(EnumNameList) { const fullData = {}; // check cache status const unCachedEnumList = EnumNameList.filter((element) => { if (this.EnumMap[element]) { // enum has been cached fullData[element] = this.EnumMap[element]; return false; } return true; }); // immediately return the data if all enums have been cached if (!unCachedEnumList.length) { return new Promise((resolve) => { resolve({ data: fullData }); }); } // build query string for uncached enums const EnumTypeQuery = unCachedEnumList.map(type => ( `${type}: __type(name: "${type}") { ...EnumFragment }` )); const query = ` query { ${EnumTypeQuery.join('\n')} } fragment EnumFragment on __Type { kind description enumValues { name description } }`; const options = Object.assign({}, this.requestObject); options.body = JSON.stringify({ query }); this.onStart(); return this.fetch(this.url, options) .then((res) => { if (res.ok) { return res.json(); } // return an custom error stack if request error return { errors: [{ message: res.statusText, stack: res, }], }; }) .then(({ data, errors }) => ( new Promise((resolve, reject) => { this.onEnd(); // if data in response is 'null' and have any errors if (!data) { return reject(errors || [{ message: 'Do not get any data.' }]); } // if all properties of data is 'null' const allDataKeyEmpty = Object.keys(data).every(key => !data[key]); if (allDataKeyEmpty && errors && errors.length) { return reject(errors); } // merge enums' data const passData = Object.assign(fullData, data); // cache new enums' data Object.keys(data).map((key) => { this.EnumMap[key] = data[key]; return key; }); return resolve({ data: passData, errors }); }) )); } /** * calling on a request starting * if the request belong to a new queue, call the 'onStart' method */ onStart() { this.requestQueueLength++; if (this.requestQueueLength > 1 || !this.callbacks.onStart) { return; } this.callbacks.onStart(this.requestQueueLength); } /** * calling on a request ending * if current queue finished, calling the 'onEnd' method */ onEnd() { this.requestQueueLength--; if (this.requestQueueLength || !this.callbacks.onEnd) { return; } this.callbacks.onEnd(this.requestQueueLength); } /** * Callback of requests queue changes.(e.g. new queue or queue finished) * @callback FetchQL~requestQueueChanged * @param {number} queueLength - length of current request queue */ /** * remove empty props(null or '') from object * @param {Object} input * @returns {Object} * @memberOf FetchQL * @private */ doOmitEmptyVariables(input) { const nonEmptyObj = {}; Object.keys(input).map(key => { const value = input[key]; if ((typeof value === 'string' && value.length === 0) || value === null || value === undefined) { return key; } else if (value instanceof Object) { nonEmptyObj[key] = this.doOmitEmptyVariables(value); } else { nonEmptyObj[key] = value; } return key; }); return nonEmptyObj; } } export default FetchQL; [代码] 将wx的异步接口封装成Promise vendor/promisify.js [代码]function promisify(wx) { let wxx = { ...wx }; for (let attr in wxx) { if (!wxx.hasOwnProperty(attr) || typeof wxx[attr] != 'function') continue; // skip over the sync method if (/sync$/i.test(attr)) continue; wxx[attr + 'Async'] = function asyncFunction(argv = {}) { return new Promise(function (resolve, reject) { wxx[attr].call(wxx, { ...argv, ...{ success: res => resolve(res), fail: err => reject(err) } }); }); }; } return wxx; } export default promisify(typeof wx === 'object' ? wx : {}); [代码] localstorage vendor/storage.js [代码]class Storage { constructor(wx) { this.wx = wx; } static get timestamp() { return new Date() / 1000; } static __isExpired(entity) { if (!entity) return true; return Storage.timestamp - (entity.timestamp + entity.expiration) >= 0; } static get __info() { let info = {}; try { info = this.wx.getStorageInfoSync() || info; } catch (err) { console.error(err); } return info; } setItem(key, value, expiration) { const entity = { timestamp: Storage.timestamp, expiration, key, value }; this.wx.setStorageSync(key, JSON.stringify(entity)); return this; } getItem(key) { let entity; try { entity = this.wx.getStorageSync(key); if (entity) { entity = JSON.parse(entity); } else { return null; } } catch (err) { console.error(err); return null; } // 没有设置过期时间, 则直接返回值 if (!entity.expiration) return entity.value; // 已过期 if (Storage.__isExpired(entity)) { this.remove(key); return null; } else { return entity.value; } } removeItem(key) { try { this.wx.removeStorageSync(key); } catch (err) { console.error(err); } return this; } clear() { try { this.wx.clearStorageSync(); } catch (err) { console.error(err); } return this; } get info() { let info = {}; try { info = this.wx.getStorageInfoSync(); } catch (err) { console.error(err); } return info || {}; } get length() { return (this.info.keys || []).length; } } export default new Storage(wx); [代码] 状态管理 vendor/store.js [代码]module.exports = class Store { constructor({ state, actions }) { this.state = state || {} this.actions = actions || {} this.ctxs = [] } // 派发action, 统一返回promise action可以直接返回state dispatch(type, payload) { const update = res => { if (typeof res !== 'object') return this.setState(res) this.ctxs.map(ctx => ctx.setData(res)) return res } if (typeof this.actions[type] !== 'function') return const res = this.actions[type](this, payload) return res.constructor.toString().match(/function\s*([^(]*)/)[1] === 'Promise' ? res.then(update) : new Promise(resolve => resolve(update(res))) } // 修改state的方法 setState(data) { this.state = { ...this.state, ...data } } // 根据keys获取state getState(keys) { return keys.reduce((acc, key) => ({ ...acc, ...{ [key]: this.state[key] } }), {}) } // 映射state到实例中,可在onload或onshow中调用 mapState(keys, ctx) { if (!ctx || typeof ctx.setData !== 'function') return ctx.setData(this.getState(keys)) this.ctxs.push(ctx) } } [代码] 兼容 async/await vendor/regenerator.js [代码]/** * Copyright (c) 2014-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var regeneratorRuntime = (function (exports) { "use strict"; var Op = Object.prototype; var hasOwn = Op.hasOwnProperty; var undefined; // More compressible than void 0. var $Symbol = typeof Symbol === "function" ? Symbol : {}; var iteratorSymbol = $Symbol.iterator || "@@iterator"; var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator"; var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; function wrap(innerFn, outerFn, self, tryLocsList) { // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator. var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator; var generator = Object.create(protoGenerator.prototype); var context = new Context(tryLocsList || []); // The ._invoke method unifies the implementations of the .next, // .throw, and .return methods. generator._invoke = makeInvokeMethod(innerFn, self, context); return generator; } exports.wrap = wrap; // Try/catch helper to minimize deoptimizations. Returns a completion // record like context.tryEntries[i].completion. This interface could // have been (and was previously) designed to take a closure to be // invoked without arguments, but in all the cases we care about we // already have an existing method we want to call, so there's no need // to create a new function object. We can even get away with assuming // the method takes exactly one argument, since that happens to be true // in every case, so we don't have to touch the arguments object. The // only additional allocation required is the completion record, which // has a stable shape and so hopefully should be cheap to allocate. function tryCatch(fn, obj, arg) { try { return { type: "normal", arg: fn.call(obj, arg) }; } catch (err) { return { type: "throw", arg: err }; } } var GenStateSuspendedStart = "suspendedStart"; var GenStateSuspendedYield = "suspendedYield"; var GenStateExecuting = "executing"; var GenStateCompleted = "completed"; // Returning this object from the innerFn has the same effect as // breaking out of the dispatch switch statement. var ContinueSentinel = {}; // Dummy constructor functions that we use as the .constructor and // .constructor.prototype properties for functions that return Generator // objects. For full spec compliance, you may wish to configure your // minifier not to mangle the names of these two functions. function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} // This is a polyfill for %IteratorPrototype% for environments that // don't natively support it. var IteratorPrototype = {}; IteratorPrototype[iteratorSymbol] = function () { return this; }; var getProto = Object.getPrototypeOf; var NativeIteratorPrototype = getProto && getProto(getProto(values([]))); if (NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) { // This environment has a native %IteratorPrototype%; use it instead // of the polyfill. IteratorPrototype = NativeIteratorPrototype; } var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype; GeneratorFunctionPrototype.constructor = GeneratorFunction; GeneratorFunctionPrototype[toStringTagSymbol] = GeneratorFunction.displayName = "GeneratorFunction"; // Helper for defining the .next, .throw, and .return methods of the // Iterator interface in terms of a single ._invoke method. function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function(method) { prototype[method] = function(arg) { return this._invoke(method, arg); }; }); } exports.isGeneratorFunction = function(genFun) { var ctor = typeof genFun === "function" && genFun.constructor; return ctor ? ctor === GeneratorFunction || // For the native GeneratorFunction constructor, the best we can // do is to check its .name property. (ctor.displayName || ctor.name) === "GeneratorFunction" : false; }; exports.mark = function(genFun) { if (Object.setPrototypeOf) { Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); } else { genFun.__proto__ = GeneratorFunctionPrototype; if (!(toStringTagSymbol in genFun)) { genFun[toStringTagSymbol] = "GeneratorFunction"; } } genFun.prototype = Object.create(Gp); return genFun; }; // Within the body of any async function, `await x` is transformed to // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test // `hasOwn.call(value, "__await")` to determine if the yielded value is // meant to be awaited. exports.awrap = function(arg) { return { __await: arg }; }; function AsyncIterator(generator) { function invoke(method, arg, resolve, reject) { var record = tryCatch(generator[method], generator, arg); if (record.type === "throw") { reject(record.arg); } else { var result = record.arg; var value = result.value; if (value && typeof value === "object" && hasOwn.call(value, "__await")) { return Promise.resolve(value.__await).then(function(value) { invoke("next", value, resolve, reject); }, function(err) { invoke("throw", err, resolve, reject); }); } return Promise.resolve(value).then(function(unwrapped) { // When a yielded Promise is resolved, its final value becomes // the .value of the Promise<{value,done}> result for the // current iteration. result.value = unwrapped; resolve(result); }, function(error) { // If a rejected Promise was yielded, throw the rejection back // into the async generator function so it can be handled there. return invoke("throw", error, resolve, reject); }); } } var previousPromise; function enqueue(method, arg) { function callInvokeWithMethodAndArg() { return new Promise(function(resolve, reject) { invoke(method, arg, resolve, reject); }); } return previousPromise = // If enqueue has been called before, then we want to wait until // all previous Promises have been resolved before calling invoke, // so that results are always delivered in the correct order. If // enqueue has not been called before, then it is important to // call invoke immediately, without waiting on a callback to fire, // so that the async generator function has the opportunity to do // any necessary setup in a predictable way. This predictability // is why the Promise constructor synchronously invokes its // executor callback, and why async functions synchronously // execute code before the first await. Since we implement simple // async functions in terms of async generators, it is especially // important to get this right, even though it requires care. previousPromise ? previousPromise.then( callInvokeWithMethodAndArg, // Avoid propagating failures to Promises returned by later // invocations of the iterator. callInvokeWithMethodAndArg ) : callInvokeWithMethodAndArg(); } // Define the unified helper method that is used to implement .next, // .throw, and .return (see defineIteratorMethods). this._invoke = enqueue; } defineIteratorMethods(AsyncIterator.prototype); AsyncIterator.prototype[asyncIteratorSymbol] = function () { return this; }; exports.AsyncIterator = AsyncIterator; // Note that simple async functions are implemented on top of // AsyncIterator objects; they just return a Promise for the value of // the final result produced by the iterator. exports.async = function(innerFn, outerFn, self, tryLocsList) { var iter = new AsyncIterator( wrap(innerFn, outerFn, self, tryLocsList) ); return exports.isGeneratorFunction(outerFn) ? iter // If outerFn is a generator, return the full iterator. : iter.next().then(function(result) { return result.done ? result.value : iter.next(); }); }; function makeInvokeMethod(innerFn, self, context) { var state = GenStateSuspendedStart; return function invoke(method, arg) { if (state === GenStateExecuting) { throw new Error("Generator is already running"); } if (state === GenStateCompleted) { if (method === "throw") { throw arg; } // Be forgiving, per 25.3.3.3.3 of the spec: // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume return doneResult(); } context.method = method; context.arg = arg; while (true) { var delegate = context.delegate; if (delegate) { var delegateResult = maybeInvokeDelegate(delegate, context); if (delegateResult) { if (delegateResult === ContinueSentinel) continue; return delegateResult; } } if (context.method === "next") { // Setting context._sent for legacy support of Babel's // function.sent implementation. context.sent = context._sent = context.arg; } else if (context.method === "throw") { if (state === GenStateSuspendedStart) { state = GenStateCompleted; throw context.arg; } context.dispatchException(context.arg); } else if (context.method === "return") { context.abrupt("return", context.arg); } state = GenStateExecuting; var record = tryCatch(innerFn, self, context); if (record.type === "normal") { // If an exception is thrown from innerFn, we leave state === // GenStateExecuting and loop back for another invocation. state = context.done ? GenStateCompleted : GenStateSuspendedYield; if (record.arg === ContinueSentinel) { continue; } return { value: record.arg, done: context.done }; } else if (record.type === "throw") { state = GenStateCompleted; // Dispatch the exception by looping back around to the // context.dispatchException(context.arg) call above. context.method = "throw"; context.arg = record.arg; } } }; } // Call delegate.iterator[context.method](context.arg) and handle the // result, either by returning a { value, done } result from the // delegate iterator, or by modifying context.method and context.arg, // setting context.delegate to null, and returning the ContinueSentinel. function maybeInvokeDelegate(delegate, context) { var method = delegate.iterator[context.method]; if (method === undefined) { // A .throw or .return when the delegate iterator has no .throw // method always terminates the yield* loop. context.delegate = null; if (context.method === "throw") { // Note: ["return"] must be used for ES3 parsing compatibility. if (delegate.iterator["return"]) { // If the delegate iterator has a return method, give it a // chance to clean up. context.method = "return"; context.arg = undefined; maybeInvokeDelegate(delegate, context); if (context.method === "throw") { // If maybeInvokeDelegate(context) changed context.method from // "return" to "throw", let that override the TypeError below. return ContinueSentinel; } } context.method = "throw"; context.arg = new TypeError( "The iterator does not provide a 'throw' method"); } return ContinueSentinel; } var record = tryCatch(method, delegate.iterator, context.arg); if (record.type === "throw") { context.method = "throw"; context.arg = record.arg; context.delegate = null; return ContinueSentinel; } var info = record.arg; if (! info) { context.method = "throw"; context.arg = new TypeError("iterator result is not an object"); context.delegate = null; return ContinueSentinel; } if (info.done) { // Assign the result of the finished delegate to the temporary // variable specified by delegate.resultName (see delegateYield). context[delegate.resultName] = info.value; // Resume execution at the desired location (see delegateYield). context.next = delegate.nextLoc; // If context.method was "throw" but the delegate handled the // exception, let the outer generator proceed normally. If // context.method was "next", forget context.arg since it has been // "consumed" by the delegate iterator. If context.method was // "return", allow the original .return call to continue in the // outer generator. if (context.method !== "return") { context.method = "next"; context.arg = undefined; } } else { // Re-yield the result returned by the delegate method. return info; } // The delegate iterator is finished, so forget it and continue with // the outer generator. context.delegate = null; return ContinueSentinel; } // Define Generator.prototype.{next,throw,return} in terms of the // unified ._invoke helper method. defineIteratorMethods(Gp); Gp[toStringTagSymbol] = "Generator"; // A Generator should always return itself as the iterator object when the // @@iterator function is called on it. Some browsers' implementations of the // iterator prototype chain incorrectly implement this, causing the Generator // object to not be returned from this call. This ensures that doesn't happen. // See https://github.com/facebook/regenerator/issues/274 for more details. Gp[iteratorSymbol] = function() { return this; }; Gp.toString = function() { return "[object Generator]"; }; function pushTryEntry(locs) { var entry = { tryLoc: locs[0] }; if (1 in locs) { entry.catchLoc = locs[1]; } if (2 in locs) { entry.finallyLoc = locs[2]; entry.afterLoc = locs[3]; } this.tryEntries.push(entry); } function resetTryEntry(entry) { var record = entry.completion || {}; record.type = "normal"; delete record.arg; entry.completion = record; } function Context(tryLocsList) { // The root entry object (effectively a try statement without a catch // or a finally block) gives us a place to store values thrown from // locations where there is no enclosing try statement. this.tryEntries = [{ tryLoc: "root" }]; tryLocsList.forEach(pushTryEntry, this); this.reset(true); } exports.keys = function(object) { var keys = []; for (var key in object) { keys.push(key); } keys.reverse(); // Rather than returning an object with a next method, we keep // things simple and return the next function itself. return function next() { while (keys.length) { var key = keys.pop(); if (key in object) { next.value = key; next.done = false; return next; } } // To avoid creating an additional object, we just hang the .value // and .done properties off the next function object itself. This // also ensures that the minifier will not anonymize the function. next.done = true; return next; }; }; function values(iterable) { if (iterable) { var iteratorMethod = iterable[iteratorSymbol]; if (iteratorMethod) { return iteratorMethod.call(iterable); } if (typeof iterable.next === "function") { return iterable; } if (!isNaN(iterable.length)) { var i = -1, next = function next() { while (++i < iterable.length) { if (hasOwn.call(iterable, i)) { next.value = iterable[i]; next.done = false; return next; } } next.value = undefined; next.done = true; return next; }; return next.next = next; } } // Return an iterator with no values. return { next: doneResult }; } exports.values = values; function doneResult() { return { value: undefined, done: true }; } Context.prototype = { constructor: Context, reset: function(skipTempReset) { this.prev = 0; this.next = 0; // Resetting context._sent for legacy support of Babel's // function.sent implementation. this.sent = this._sent = undefined; this.done = false; this.delegate = null; this.method = "next"; this.arg = undefined; this.tryEntries.forEach(resetTryEntry); if (!skipTempReset) { for (var name in this) { // Not sure about the optimal order of these conditions: if (name.charAt(0) === "t" && hasOwn.call(this, name) && !isNaN(+name.slice(1))) { this[name] = undefined; } } } }, stop: function() { this.done = true; var rootEntry = this.tryEntries[0]; var rootRecord = rootEntry.completion; if (rootRecord.type === "throw") { throw rootRecord.arg; } return this.rval; }, dispatchException: function(exception) { if (this.done) { throw exception; } var context = this; function handle(loc, caught) { record.type = "throw"; record.arg = exception; context.next = loc; if (caught) { // If the dispatched exception was caught by a catch block, // then let that catch block handle the exception normally. context.method = "next"; context.arg = undefined; } return !! caught; } for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; var record = entry.completion; if (entry.tryLoc === "root") { // Exception thrown outside of any try block that could handle // it, so set the completion value of the entire function to // throw the exception. return handle("end"); } if (entry.tryLoc <= this.prev) { var hasCatch = hasOwn.call(entry, "catchLoc"); var hasFinally = hasOwn.call(entry, "finallyLoc"); if (hasCatch && hasFinally) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } else if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else if (hasCatch) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } } else if (hasFinally) { if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else { throw new Error("try statement without catch or finally"); } } } }, abrupt: function(type, arg) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { var finallyEntry = entry; break; } } if (finallyEntry && (type === "break" || type === "continue") && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc) { // Ignore the finally entry if control is not jumping to a // location outside the try/catch block. finallyEntry = null; } var record = finallyEntry ? finallyEntry.completion : {}; record.type = type; record.arg = arg; if (finallyEntry) { this.method = "next"; this.next = finallyEntry.finallyLoc; return ContinueSentinel; } return this.complete(record); }, complete: function(record, afterLoc) { if (record.type === "throw") { throw record.arg; } if (record.type === "break" || record.type === "continue") { this.next = record.arg; } else if (record.type === "return") { this.rval = this.arg = record.arg; this.method = "return"; this.next = "end"; } else if (record.type === "normal" && afterLoc) { this.next = afterLoc; } return ContinueSentinel; }, finish: function(finallyLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.finallyLoc === finallyLoc) { this.complete(entry.completion, entry.afterLoc); resetTryEntry(entry); return ContinueSentinel; } } }, "catch": function(tryLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc === tryLoc) { var record = entry.completion; if (record.type === "throw") { var thrown = record.arg; resetTryEntry(entry); } return thrown; } } // The context.catch method must only be called with a location // argument that corresponds to a known catch block. throw new Error("illegal catch attempt"); }, delegateYield: function(iterable, resultName, nextLoc) { this.delegate = { iterator: values(iterable), resultName: resultName, nextLoc: nextLoc }; if (this.method === "next") { // Deliberately forget the last sent value so that we don't // accidentally pass it on to the delegate. this.arg = undefined; } return ContinueSentinel; } }; // Regardless of whether this script is executing as a CommonJS module // or not, return the runtime object so that we can declare the variable // regeneratorRuntime in the outer scope, which allows this module to be // injected easily by `bin/regenerator --include-runtime script.js`. return exports; }( // If this script is executing as a CommonJS module, use module.exports // as the regeneratorRuntime namespace. Otherwise create a new empty // object. Either way, the resulting object will be used to initialize // the regeneratorRuntime variable at the top of this file. typeof module === "object" ? module.exports : {} )); [代码] 后端 [代码]const typeDefs = gql` # schema 下面是根类型,约定是 RootQuery 和 RootMutation schema { query: Query } # 定义具体的 Query 的结构 type Query { # 登陆接口 login(code: String!, encryptedData: String, iv: String): Login } type Login { token: String! userInfo: UserInfo } type UserInfo { nickName: String gender: String avatarUrl: String } `; const resolvers = { Query: { async login(parent, { code, encryptedData, iv }) { const { sessionKey, openId, unionId } = await wxService.code2Session(code); const userInfo = encryptedData && iv ? wxService.decryptData(sessionKey, encryptedData, iv) : { openId, unionId }; if (userInfo.nickName) { userService.createOrUpdateWxUser(userInfo); } const token = await userService.generateJwtToken(userInfo); return { token, userInfo }; }, }, }; [代码]
2019-04-21 - 小程序里可以使用async await语法吗
这几天在云开发开发小程序的过程中遇到一个很棘手的逻辑问题, 需求是这样的: 在线考试小程序场景,每个科目会把题库(按100个题算)分成10天来完成 (比如day1是[01,02,03,04,05,06,07,08,09,10]这10个题), 每天只能做一个Day的试题,现在问题是:当我们来到小程序需要答题的时候,是从哪一个Day开始做? 数据库有以下两个集合 1、days,用于维护每个day跟试题id的信息, [ {day1,[01,02,03,04,05,06,07,08,09,10]}, {day2,[11,12,13,14,15,16,17,18,19,20]} ] 2、historys,用于维护用户答题的历史,比如 [ {openid,day1}, {openid,day2} ] 实现的方式有很多种: 1、promise.all 2、async await 本文采用async await方式,那么在小程序中可以使用这种语法吗?经过了解,目前不需要引入其他库是可以正常使用的,但是需要设置一下。 小程序代码中如果用上述语法,在本地设置里面必须选中下图的增强编译,其实这里也是默认选中的 占位符 [图片] 占位符 那么在进考试的时候就要确定是做哪一天的?具体逻辑如下 1、取这个科目所有的day信息,这里面每条记录包含了当天的题目编号列表,比如[day1,day2,day3,day4,day5,day6,day7,day8,day9,day10] 2、取当前微信用户已经做过的day信息,比如这个用户已经做完了[day1,day2] 3、取上面两个集合的差集[day3,day4,day5,day6,day7,day8,day9,day10],取差集中的第一项也就是day3作为当前答题的试卷 这个逻辑可以用promise.all,但是我在实现的时候选择了async,具体代码如下 [图片] 占位符 [图片] 占位符 [图片] 当求得这两个数组之后,我们取数组的差集,就找出所有未做的day,从中任取一天便可以完成上述需求。 通过该文,我们学习了以下知识点 1、async await在小程序中是否可用 2、如何实现两个数组的差集 3、小程序云开发,数据查询具体如何使用async await 本文完
2020-02-22 - 全平台(Vue、React、微信小程序)任意角度旋转 图片裁剪组件
SimpleCrop全网唯一支持裁剪图片任意角度旋转、交互体验媲美原生客户端的全平台图片裁剪组件。 Github 地址:https://github.com/newbieYoung/Simple-Crop 特性及优势和目前流行的图片裁剪组件相比,其优势在于以下几点: 裁剪图片支持任意角度旋转;支持 Script 标签、微信小程序、React、Vue;支持移动和 PC 设备;支持边界判断、当裁剪框里出现空白时,图片自动吸附至完全填满裁剪框;移动端缩放以双指中心为基准点;交互体验媲美原生客户端。示例微信小程序示例[图片] 移动端示例[图片] 左侧是 IOS 系统相册中原生的图片裁剪功能,右侧为 SimpleCrop 移动端示例。 可以扫描二维码体验: [图片] 或者访问以下链接: https://newbieyoung.github.io/Simple-Crop/examples/test-2.html PC 示例[图片] 链接如下: https://newbieyoung.github.io/Simple-Crop/examples/test-1.html 安装npm install simple-crop 用法Script 用法微信小程序用法React 用法Vue 用法开源许可协议MIT License. 原理及实现[代码]全平台(Vue、React、微信小程序)任意角度旋转 图片裁剪组件[代码] https://newbieweb.lione.me/2019/05/16/simple-crop/
2020-03-04 - 公众平台/小程序服务端API的access_token的内部设计
一、背景 对于使用过公众平台的API功能的开发者来说,access_token绝对不会陌生,它就像一个打开家门的钥匙,只要拿着它,就能使用公众平台绝大部分的API功能。因此,对于开发者而言,access_token的使用方式就变得尤其的重要。在日常API接口的运营中,经常遇到各种的疑问:为什么我的access_token突然非法了?为什么刚刚拿到的access_token,用了10min就过期了?对于这些疑问,我们提供出access_token的设计方案,便于开发者对access_token使用方式上的理解。 对于access_token的获取,可以参考公众平台的官方文档:auth.getAccessToken、获取Access token 二、access_token的内部设计 2.1 access_token的时效性 众所周知,access_token是通过appid和appsecret来生成的。内部设计的步骤如下: (1)开发者通过https请求方式: GET https://API.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET,传入appid及apppsecret的参数 (2)公众平台后台会校验appid和哈希(appsecret)是否与存储匹配,若匹配,结合当前时间戳,生成新的access_token。 (3)生成新的access_token的同时,会对老的access_token的过期时间戳更新为当前时间戳。 (4)返回新的access_token给开发者。 这里以图示的方式说明一下,新旧token交替过程: [图片] 从上图需要注意的几点: (1)公众平台存储层只会存储新老两个access_token,意味着假设开发者重复调用3次接口,则会导致最早的access_token立刻失效。 (2)虽然请求新的access_token后,老的access_token过期时间会更新为当前时间,但也不会立刻失效,原理请参考 【2.2 access_token 的逐渐失效性】 (3)出于信息安全考虑,公众平台并不会明文存储appsecret,仅存储appid以及appsecret的哈希值。因此开发者要妥善保管appsecret。当appsecret疑似泄露时,需要及时登录mp.weixin.qq.com重置appsecret。 2.2 access_token 的逐渐失效性 从【access_token的时效性】了解到,当开发者请求获取新的access_token时,老的access_token过期时间会被更新为当前时间,但此时不会立刻失效,因为公众平台会提供【5分钟的新老access_token交替缓冲时间】,因此也称为access_token 的逐渐失效性。 实现的原理是: 1. 由于老的access_token过期时间戳已被刷新,所以在API接口请求期间,带上的access_token解开后,过期时间戳会加上5分钟,然后和当前设备时间进行比对,若超过当前设备时间,判断为失效。 2. 公众平台的设备会保持时钟同步,但设备之间仍然可能会存在1-2分钟的时间差异,所以【5分钟】并非绝对的时间值。当开发者获取到新的access_token后应该尽快切换到新的access_token。 [图片] 从上图需要注意的几点: (1)由于存在设备时间同步的差异,可能会导致开发者遇到拿着老的access_token请求API接口,部分请求成功,部分请求失败的情况,建议开发者获取到新的access_token后尽快使用。 (2)通过理解两个图示,对开发者来说,access_token是相当关键且不能乱调的接口,建议开发者统一管理access_token,以免造成多次请求导致access_token失效。
2021-05-11 - 设计一个小程序请求库?
前言 最近想写一个可以适配多平台的请求库,在研究 xhr 和 fetch 发现二者的参数、响应、回调函数等差别很大。想到如果请求库想要适配多平台,需要统一的传参和响应格式,那么势必会在请求库内部做大量的判断,这样不但费时费力,还会屏蔽掉底层请求内核差异。 阅读 axios 和 umi-request 源码时想到,请求库其实基本都包含了拦截器、中间件和快捷请求等几个通用的,与具体请求过程无关的功能。然后通过传参,让用户接触底层请求内核。问题在于,请求库内置多个底层请求内核,内核支持的参数是不一样的,上层库可能做一些处理,抹平一些参数的差异化,但对于底层内核的特有的功能,要么放弃,要么只能在参数列表中加入一些具体内核的特有的参数。比如在 axios 中,它的请求配置参数列表中,罗列了一些 browser only的参数,那对于只需要在 node 环境中运行的 axios 来说,参数多少有些冗余,并且如果 axios 要支持其他请求内核(比如小程序、快应用、华为鸿蒙等),那么参数冗余也将越来越大,扩展性也差。 换个思路来想,既然实现一个适配多平台的统一的请求库有这些问题,那么是否可以从底向上的,针对不同的请求内核,提供一种方式可以很方便的为其赋予拦截器、中间件、快捷请求等几个通用功能,并且保留不同请求内核的差异化? 设计实现 我们的请求库要想与请求内核无关,那么只能采用内核与请求库相分离的模式。使用时,需要将请求内核传入,初始化一个实例,再进行使用。或者基于请求库,传入内核,预置请求参数来进行二次封装。 基本架构 首先实现一个基本的架构 [代码]class PreQuest { constructor(private adapter) request(opt) { return this.adapter(opt) } } const adapter = (opt) => nativeRequestApi(opt) // eg: const adapter = (opt) => fetch(opt).then(res => res.json()) // 创建实例 const prequest = new PreQuest(adapter) // 这里实际调用的是 adapter 函数 prequest.request({ url: 'http://localhost:3000/api' }) [代码] 可以看到,这里饶了个弯,通过实例方法调用了 adapter 函数。 这样的话,为修改请求和响应提供了想象空间。 [代码]class PreQuest { // ...some code async request(opt){ const options = modifyReqOpt(opt) const res = await this.adapter(options) return modifyRes(res) } // ...some code } [代码] 中间件 可以采用 koa 的洋葱模型,对请求进行拦截和修改。 中间件调用示例: [代码]const prequest = new PreQuest(adapter) prequest.use(async (ctx, next) => { ctx.request.path = '/perfix' + ctx.request.path await next() ctx.response.body = JSON.parse(ctx.response.body) }) [代码] 实现中间件基本模型? [代码]const compose = require('koa-compose') class Middleware { // 中间件列表 cbs = [] // 注册中间件 use(cb) { this.cbs.push(cb) return this } // 执行中间件 exec(ctx, next){ // 中间件执行细节不是重点,所以直接使用 koa-compose 库 return compose(this.cbs)(ctx, next) } } [代码] 全局中间件,只需要添加一个 use 和 exec 的静态方法即可。 PreQuest 继承自 Middleware 类,即可在实例上注册中间件。 那么怎么在请求前调用中间件? [代码]class PreQuest extends Middleware { // ...some code async request(opt) { const ctx = { request: opt, response: {} } // 执行中间件 async this.exec(ctx, async (ctx) => { ctx.response = await this.adapter(ctx.request) }) return ctx.response } // ...some code } [代码] 中间件模型中,前一个中间件的返回值是传不到下一个中间件中,所以是通过一个对象在中间件中传递和赋值。 拦截器 拦截器是修改参数和响应的另一种方式。 首先看一下 axios 中拦截器是怎么用的。 [代码]import axios from 'axios' const instance = axios.create() instance.interceptor.request.use( (opt) => modifyOpt(opt), (e) => handleError(e) ) [代码] 根据用法,我们可以实现一个基本结构 [代码]class Interceptor { cbs = [] // 注册拦截器 use(successHandler, errorHandler) { this.cbs.push({ successHandler, errorHandler }) } exec(opt) { return this.cbs.reduce( (t, c, idx) => t.then(c.successHandler, this.handles[idx - 1]?.errorHandler), Promise.resolve(opt) ) .catch(this.handles[this.handles.length - 1].errorHandler) } } [代码] 代码很简单,有点难度的就是拦截器的执行了。这里主要有两个知识点: Array.reduce 和 Promise.then 第二个参数的使用。 注册拦截器时,[代码]successHandler[代码] 与 [代码]errorHandler[代码] 是成对的, successHandler 中抛出的错误,要在对应的 errorHandler 中处理,所以 errorHandler 接收到的错误,是上一个拦截器中抛出的。 拦截器怎么使用呢? [代码]class PreQuest { // ... some code interceptor = { request: new Interceptor() response: new Interceptor() } // ...some code async request(opt){ // 执行拦截器,修改请求参数 const options = await this.interceptor.request.exec(opt) const res = await this.adapter(options) // 执行拦截器,修改响应数据 const response = await this.interceptor.response.exec(res) return response } } [代码] 拦截器中间件 拦截器也可以是一个中间件,可以通过注册中间件来实现。请求拦截器在 [代码]await next()[代码] 前执行,响应拦截器在其后。 [代码]const instance = new Middleware() instance.use(async (ctx, next) => { // Promise 链式调用,更改请求参数 await Promise.resolve().then(reqInterceptor1).then(reqInterceptor2)... // 执行下一个中间件、或执行到 this.adapter 函数 await next() // Promise 链式调用,更改响应数据 await Promise.resolve().then(resInterceptor1).then(resInterceptor2)... }) [代码] 拦截器有请求拦截器和响应拦截器两类。 [代码]class InterceptorMiddleware { request = new Interceptor() response = new Interceptor() // 注册中间件 register: async (ctx, next) { ctx.request = await this.request.exec(ctx.request) await next() ctx.response = await thie.response.exec(ctx.response) } } [代码] 使用 [代码]const instance = new Middleware() const interceptor = new InterceptorMiddleware() // 注册拦截器 interceptor.request.use( (opt) => modifyOpt(opt), (e) => handleError(e) ) // 注册到中间中 instance.use(interceptor.register) [代码] 类型请求 这里我把类似 [代码]instance.get('/api')[代码] 这样的请求叫做类型请求。库中集成类型请求的话,难免会对外部传入的adapter 函数的参数进行污染。因为需要为请求方式 [代码]get[代码] 和路径 [代码]/api[代码] 分配键名,并且将其混入到参数中,通常在中间件中会有修改路径的需求。 实现很简单,只需要遍历 HTTP 请求类型,并将其挂在 this 下即可 [代码]class PreQuest { constructor(private adapter) { this.mount() } // 挂载所有类型的别名请求 mount() { methods.forEach(method => { this[method] = (path, opt) => { // 混入 path 和 method 参数 return this.request({ path, method, ...opt }) } }) } // ...some code request(opt) { // ...some code } } [代码] 简单请求 axios 中,可以直接使用下面这种形式进行调用 [代码]axios('http://localhost:3000/api').then(res => console.log(res)) [代码] 我将这种请求方式称之为简单请求。 我们这里怎么实现这种写法的请求方式呢? 不使用 class ,使用传统函数类写法的话比较好实现,只需要判断函数是否是 new 调用,然后在函数内部执行不同的逻辑即可。 demo 如下 [代码]function PreQuest() { if(!(this instanceof PreQuest)) { console.log('不是new 调用') return // ...some code } console.log('new调用') //... some code } // new 调用 const instance = new PreQuest(adapter) instance.get('/api').then(res => console.log(res)) // 简单调用 PreQuest('/api').then(res => console.log(res)) [代码] class 写法的话,不能进行函数调用。我们可以在 class 实例上做文章。 首先初始化一个实例,看一下用法 [代码]const prequest = new PreQuest(adapter) prequest.get('http://localhost:3000/api') prequest('http://localhost:3000/api') [代码] 通过 new 实例化出来的是一个对象,对象是不能够当做函数来执行,所以不能用 new 的形式来创建对象。 再看一下 axios 中生成实例的方法 [代码]axios.create[代码], 可以从中得到灵感,如果 [代码].create[代码] 方法返回的是一个函数,函数上挂上了所有 new 出来对象上的方法,这样的话,就可以实现我们的需求。 简单设计一下: 方式一: 拷贝原型上的方法 [代码]class PreQuest { static create(adapter) { const instance = new PreQuest(adapter) function inner(opt) { return instance.request(opt) } for(let key in instance) { inner[key] = instance[key] } return inner } } [代码] 注意: 在某些版本的 es 中,[代码]for in[代码] 循环遍历不出 class 生成实例原型上的方法。 方式二: 还可以使用 Proxy 代理一个空函数,来劫持访问。 [代码]class PreQuest { // ...some code static create(adapter) { const instance = new PreQuest(adapter) return new Proxy(function (){}, { get(_, name) { return Reflect.get(instance, name) }, apply(_, __, args) { return Reflect.apply(instance.request, instance, args) }, }) } } [代码] 上面两种方法的缺点在于,通过 [代码]create[代码] 方法返回的将不再是 [代码]PreQuest[代码] 的实例,即 [代码]const prequest = PreQuest.create(adapter) prequest instanceof PreQuest // false [代码] 个人目前还没有想到,判断 [代码]prequest[代码] 是不是 [代码]PreQuest[代码] 实例有什么用,并且也没有想到好的解决办法。有解决方案的请在评论里告诉我。 使用 [代码].create[代码] 创建 ‘实例’ 的方式可能不符合直觉,我们还可以通过 Proxy 劫持 new 操作。 Demo如下: [代码]class InnerPreQuest { create() { // ...some code } } const PreQuest = new Proxy(InnerPreQuest, { construct(_, args) { return () => InnerPreQuest.create(...args) } }) [代码] 请求锁 如何实现在请求接口前,先拿到 token 再去请求? 下面的例子中,页面同时发起多个请求 [代码]const prequest = PreQuest.create(adapter) prequest('/api/1').catch(e => e) // auth fail prequest('/api/2').catch(e => e) // auth fail prequest('/api/3').catch(e => e) // auth fail [代码] 首先很容易想到,我们可以使用中间件为其添加 token [代码]prequest.use(async (ctx, next) => { ctx.request.headers['token'] = token await next() }) [代码] 但 token 值从何而来?token 需要请求接口得来,并且需要重新创建请求实例,以避免重新走添加 token 的中间件的逻辑。 简单实现一下 [代码]const tokenRequest = PreQuest.create(adapter) let token = null prequest.use(async (ctx, next) => { if(!token) { token = await tokenRequest('/token') } ctx.request.headers['token'] = token await next() }) [代码] 这里使用了 token 变量,来避免每次请求接口,都去调接口拿 token。 代码乍一看没有问题,但仔细一想,当同时请求多个接口,tokenRequest 请求还没有得到响应时,后面的请求又都走到这个中间件,此时 token 值为空,会造成多次调用 tokenRequest。那么如何解决这个问题? 很容易想到,可以加个锁机制来实现 [代码]let token = null let pending = false prequest.use(async (ctx, next) => { if(!token) { if(pending) return pending = true token = await tokenRequest('/token') pending = flase } ctx.request.headers['token'] = token await next() }) [代码] 这里我们加了 pending 来判断 tokenRequest 的执行,成功解决了 tokenRequest 执行多次的问题,但又引入了新的问题,在执行 tokenRequest 时,后面到来的请求应当怎么处理?上面的代码,直接 return 掉了,请求将被丢弃。实际上,我们希望,请求可以在这里暂停,当拿到 token 时,再请求后面的中间件。 暂停,我们也可以很容想到使用 async、await 或者 promise 来实现。但在这里如何用呢? 我从 axios 的 cancelToken 实现中得到了灵感。axios 中,利用 promise 简单实现了一个状态机,将 Promise 中的 resolve 赋值到外部局部变量,实现对 promise 流程的控制。 简单实现一下 [代码]let token = null let pending = false let resolvePromise let promise = new Promise((resolve) => resolvePromise = resolve) prequest.use(async (ctx, next) => { if(!token) { if(pending) { // promise 控制流程 token = await promise } else { pending = true token = await tokenRequest('/token') // 调用 resolve,使得 promise 可以执行剩余的流程 resolvePromise(token) pending = flase } } ctx.request.headers['Authorization'] = `bearer ${token}` await next() }) [代码] 当执行 tokenRequest 时,其余请求的接口,都会进入到一个 promise 控制的流程中,当 token 得到后,通过外部 resolve, 控制 promise 继续执行,以此设置请求头,和执行剩余中间件。 这种方式虽然实现了需求,但代码丑陋不美观。 我们可以将状态都封装到一个函数中。以实现类似下面这种调用。这样的调用符合直觉且美观。 [代码]prequest.use(async (ctx, next) => { const token = await wrapper(tokenRequest) ctx.request.headers['Authorization'] = `bearer ${token}` await next() }) [代码] 怎么实现这样一个 wrapper 函数? 首先,状态不能封装到 wrapper 函数中,否则每次都会生成新的状态,wrapper 将形同虚设。可以使用闭包函数将状态保存。 [代码]function createWrapper() { let token = null let pending = false let resolvePromise let promise = new Promise((resolve) => resolvePromise = resolve) return function (fn) { if(pending) return promise if(token) return token pending = true token = await fn() pending = false resolvePromise(token) return token } } [代码] 使用时,只需要利用 [代码]createWrapper[代码] 生成一个 [代码]wrapper[代码] 即可 [代码]const wrapper = createWrapper() prequest.use(async (ctx, next) => { const token = await wrapper(tokenRequest) ctx.request.headers['Authorization'] = `bearer ${token}` await next() }) [代码] 这样的话,就可以实现我们的目的。 但,这里的代码还有问题,状态封装在 createWrapper 内部,当 token 失效后,我们将无从处理。 比较好的做法是,将状态从 [代码]createWrapper[代码] 参数中传入。 实战 以微信小程序为例。小程序中自带的 [代码]wx.request[代码] 并不好用。使用上面我们封装的代码,可以很容易的打造出一个小程序请求库。 封装小程序原生请求 将原生小程序请求 Promise 化,设计传参 opt 对象 [代码]function adapter(opt) { const { path, method, baseURL, ...options } = opt const url = baseURL + path return new Promise((resolve, reject) => { wx.request({ ...options, url, method, success: resolve, fail: reject, }) }) } [代码] 调用 [代码]const instance = PreQuest.create(adapter) // 中间件模式 instance.use(async (ctx, next) => { // 修改请求参数 ctx.request.path = '/prefix' + ctx.request.path await next() // 修改响应 ctx.response.body = JSON.parse(ctx.response.body) }) // 拦截器模式 instance.interecptor.request.use( (opt) => { opt.path = '/prefix' + opt.path return opt } ) instance.request({ path: '/api', baseURL: 'http://localhost:3000' }) instance.get('http://localhost:3000/api') instance.post('/api', { baseURL: 'http://loclahost:3000' }) [代码] 获取原生请求实例 首先看一下在小程序中怎样中断请求 [代码]const request = wx.request({ // ...some code }) request.abort() [代码] 使用我们封装的这一层,将拿不到原生请求实例。 那么怎么办呢?我们可以从传参中入手 [代码]function adapter(opt) { const { getNativeRequestInstance } = opt let resolvePromise: any getNativeRequestInstance(new Promise(resolve => (resolvePromise = resolve))) return new Promise(() => { const nativeInstance = wx.request( // some code ) resolvePromise(nativeInstance) }) } [代码] 用法如下: [代码]const instance = PreQuest.create(adapter) instance.post('http://localhost:3000/api', { getNativeRequestInstance(promise) { promise.then(instance => { instance.abort() }) } }) [代码] 兼容多平台小程序 查看了几个小程序平台和快应用,发现请求方式都是小程序的那一套,那其实我们完全可以将 [代码]wx.request[代码] 拿出来,创建实例的时候再传进去。 结语 上面的内容中,我们基本实现了一个与请求内核无关的请求库,并且设计了两种拦截请求和响应的方式,我们可以根据自己的需求和喜好自由选择。 这种内核装卸的方式非常容易扩展。当面对一个 axios 不支持的平台时,也不用费劲的去找开源好用的请求库了。我相信很多人在开发小程序的时候,基本都有去找 axios-miniprogram 的解决方案。通过我们的 PreQuest 项目,可以体验到类似 axios 的能力。 本文涉及到的代码,请参阅这里 参考 axios: https://github.com/axios/axios umi-request:https://github.com/umijs/umi-request
2021-05-31 - 便携小空调小程序(附源码)
今天看到大家都在转发便携小空调,搜了一下,原版是一个H5版本的,没有小程序版本的,就随手写了一个小程序版本的便携小空调。文末有源码链接,有需要的自取。 先附上正宗原版链接https://ac.yunyoujun.cn/(这个应该是原版吧) 再看下小程序版本的效果图 [图片] [图片] 这个小程序很简单,主要就是:空调开关、温度调节、模式设置、声音控制。 1.空调开关:控制声音、图片、温度的展示/隐藏 2.温度调节:空调可调节温度限制在16-31度之间,并切换数字 3.模式设置:制冷功能及icon、制热功能及icon 4.声音控制:声音的播放加载和循环播放 功能实现起来很简单,重点是素材处理起来比较麻烦,在这里再次感谢便携小空调的原作者。 便携小空调小程序源码:https://gitee.com/dashuaixiaomo/xiaokongtiao 如果觉得还不错,顺手点个👍呗
2021-05-11 - JS中的二进制数据处理
前言 在现有的计算机中,二进制常常以字节数组的形式存在于程序当中。例如在C#里面,就用byte[],标准C里面没有byte类型,但可以通过typedef把byte定义为unsigned char的别名,效果是一样的。JS设计之初似乎就没想过要处理二进制,对于字节的概念可以说是非常非常的模糊。如果要表达字节数组,那么似乎只能用一个普通数组来表示。 然而随着业务需求的逐渐发展,出现了WebGL这样的技术。所谓WebGL,就是指浏览器与显卡之间的通信接口。为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。类型化数组(Typed Array)就是在这种背景下诞生的。而类型化数组是建立在ArrayBuffer对象的基础上的。下面介绍一下Arraybuffer。 一、Arraybuffer1.1 基本概念 ArrayBuffer 对象是 ES6 才纳入正式 ECMAScript 规范,是 JavaScript 操作二进制数据的一个接口。ArrayBuffer 对象是以数组的语法处理二进制数据,也称二进制数组。它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写。 ❝ArrayBuffer 简单说是一片内存,但是你不能直接用它。这就好比你在 C 里面,malloc 一片内存出来,你也会把它转换成 unsigned_int32 或者 int16 这些你需要的实际类型的数组/指针来用。这就是 JS 里的 TypedArray 的作用,那些 Uint32Array 也好,Int16Array 也好,都是给 ArrayBuffer 提供了一个 “View”,MDN 上的原话叫做 “Multiple views on the same data”,对它们进行下标读写,最终都会反应到它所建立在的 ArrayBuffer 之上。❝1.2 基本操作「语法」 new ArrayBuffer(length) 参数:length 表示要创建的 ArrayBuffer 的大小,单位为字节;返回值:ArrayBuffer 对象;异常:如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常;「示例」 const buffer = new ArrayBuffer(32); buffer.byteLength; // 32 const v = new Int32Array(buffer); ArrayBuffer.isView(v) // true const buffer2 = buffer.slice(0, 1); 上面代码表示实例对象 buffer 占用 32 个字节。 它有实例属性 byteLength ,表示当前实例占用的内存字节长度。 它拥有一个静态方法isView(),这个方法可以用来判断是否为TypedArray实例或DataView实例。 它拥有实例方法 slice(),用来复制一部分内存,使用方式同数组的slice方法。 除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。 二、视图2.1 TypedArray TypedArray一共包含九种类型,每一种都是一个构造函数。(DataView视图不支持Uint8ClampedArray,其他都支持) 名称描述长度(字节)Int8Array8位有符号整数1Uint8Array8位无符号整数1Uint8ClampedArray8位无符号整型固定数组(数值在0~255之间)1Int16Array16位有符号整数2Uint16Array16位无符号整数2Int32Array32位有符号整数4Uint32Array32 位无符号整数4Float32Array32 位 IEEE 浮点数4Float64Array64 位 IEEE 浮点数8 每一种视图都有一个BYTES_PER_ELEMENT常数,表示这种数据类型占据的字节数。 Int8Array.BYTES_PER_ELEMENT // 1 Uint8Array.BYTES_PER_ELEMENT // 1 Int16Array.BYTES_PER_ELEMENT // 2 Uint16Array.BYTES_PER_ELEMENT // 2 Int32Array.BYTES_PER_ELEMENT // 4 Uint32Array.BYTES_PER_ELEMENT // 4 Float32Array.BYTES_PER_ELEMENT // 4 Float64Array.BYTES_PER_ELEMENT // 8 这 9 个构造函数生成的数组,统称为TypedArray视图。它们很像普通数组,都有length属性,普通数组的操作方法和属性,对TypedArray 数组完全适用。 普通数组与 TypedArray 数组的差异主要在以下方面: [图片] TypedArray和Array之间也可以互相转换 const typedArray = new Uint8Array([1, 2, 3, 4]); const normalArray = Array.apply([], typedArray); 「建立TypedArray视图」 // 创建一个8字节的ArrayBuffer const a = new ArrayBuffer(8); // 创建一个指向a的Int32视图,开始于字节0,直到缓冲区的末尾 const a1 = new Int32Array(a); // 创建一个指向a的Uint8视图,开始于字节4,直到缓冲区的末尾 const a2 = new Uint8Array(a, 4); // 创建一个指向a的Int16视图,开始于字节4,长度为2 const a3 = new Int16Array(a, 4, 2); 上面代码在一段长度为 8 个字节的内存(a)之上,生成了三个视图:a1、a2和a3。 视图的构造函数可以接受三个参数: 第一个参数(必选):视图对应的底层ArrayBuffer对象;第二个参数:视图开始的字节序号,默认从 0 开始;第三个参数:视图包含的数据个数,默认直到本段内存区域结束; 建立了视图以后,就可以进行各种操作了。这里需要明确的是,视图其实就是普通数组,语法完全没有什么不同,只不过它直接针对内存进行操作,而且每个成员都有确定的数据类型。所以,视图就被叫做“类型化数组”。 「TypedArray视图操作」 const buffer = new ArrayBuffer(8); const int16View = new Int16Array(buffer); for (let i = 0; i < int16View.length; i++) { int16View[i] = i * 2; } console.log(int16View) // [0, 2, 4, 6] 上面代码生成一个8字节的ArrayBuffer对象,然后在它的基础上,建立了一个16位整数的视图。由于每个字节占据8位,那么16位就占据了2个字节(1个字节等于8位),所以一共可以写入4个整数,依次为0,2,4,6。 如果在这段数据上接着建立一个8位整数的视图,则可以读出完全不一样的结果。 const int8View = new Int8Array(buffer); for (let i = 0; i < int8View.length; i++) { int8View[i] = i; } console.log(int8View) // [0, 0, 2, 0, 4, 0, 6, 0] 首先整个ArrayBuffer对象会被分成8段。然后,由于x86体系的计算机都采用小端字节序(具体概念理解请自主查询),相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址,所以就得到了上面的结果。还可以看到下面这个例子 const buffer = new ArrayBuffer(4); const v1 = new Uint8Array(buffer); v1[0] = 10; v1[1] = 3; v1[2] = 11; v1[3] = 8; console.log(v1) // [10, 3, 11, 8] const uInt16View = new Uint16Array(buffer); // [0xa, 0x3, 0xb, 0x8] console.log(uInt16View) // 计算机采用小端字节序 [0x030a, 0x080b] => [778, 2059] 如果一段数据是大端字节序(大端字节序主要用于数据传输),TypedArray 数组将无法正确解析,因为它只能处理小端字节序!为了解决这个问题,JavaScript 引入DataView对象,可以设定字节序。 2.2 DataView DataView 视图是一个可以从二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。 ❝ 字节顺序,又称端序或尾序(英语:Endianness),在计算机科学领域中,指存储器中或在数字通信链路中,组成多字节的字的字节的排列顺序。字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。例如假设上述变量 x 类型为int,位于地址 0x100 处,它的值为 0x01234567,地址范围为 0x100~0x103字节,其内部排列顺序依赖于机器的类型。大端法从首位开始将是:0x100: 01, 0x101: 23,..。而小端法将是:0x100: 67, 0x101: 45,..。❝「语法」 new DataView(buffer [, byteOffset [, byteLength]]) 相关的参数说明如下: buffer:ArrayBuffer 对象 或 SharedArrayBuffer 对象;byteOffset(可选):此 DataView 对象的第一个字节在 buffer 中的字节偏移。如果未指定,则默认从第一个字节开始;异常:此 DataView 对象的字节长度。如果未指定,这个视图的长度将匹配 buffer 的长度;「示例」 const buffer = new ArrayBuffer(16); const view = new DataView(buffer, 0); view.setInt8(1, 68); view.getInt8(1); // 68 如果一次操作(get或者set)两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。DataView的操作方法默认使用大端字节序解读数据,如果需要使用小端字节序解读,必须在操作方法中指定参数为true(get方法的第二个参数和set方法的第三个参数)。 const buffer = new ArrayBuffer(24); const dv = new DataView(buffer); // 1个字节,默认大端字节序 const v1 = dv.getUint8(0); // 小端字节序 const v1 = dv.getUint16(1, true); // 大端字节序 const v2 = dv.getUint16(3, false); // 在第5个字节,以小端字节序写入值为11的32位整数 dv.setInt32(4, 11, true); 对于直接处理ArrayBuffer对象的业务场景不是特别多,特别是写页面比较多的同学。笔者深刻认识并运用的场景,主要是在处理比较复杂且数据量比较大的点云数据,前端接收到的点云数据已经是原始采集数据转换过的二进制数据,前端需要对二进制数据进行解析,运用的解析方法就是上述提到的各种方法。下面介绍一下业务场景中比较常见到的一种二进制表示类型——Blob。 三、Blob3.1 基本介绍 Blob 对象比较常用于文件上传、文件读写操作等。在对文件读写的时候,我们更多的时候只是操作File对象,而File继承了所有Blob的属性。所以在我们看来,File对象可以看作一种特殊的Blob对象。 而Blob 对象与 ArrayBuffer 的区别在于,Blob 对象用于操作二进制文件, ArrayBuffer 用于直接操作内存,所以他们有如下图的关系: [图片] 「语法」 const blob = new Blob(array [, options]); 相关的参数说明如下: array:字符串或二进制对象,表示新生成的Blob实例对象的内容;options(可选):比较常用的属性 type,表示数据的 MIME 类型,默认空字符串;「示例」 const array = ['Hello World! ']; const blob = new Blob(array, {type : 'text/html'}); 「属性和方法」 [图片] 由上图可以看到,Blob对象拥有size和type两个属性,以及多种自有方法。比较常用的方法slice、arrayBuffer等;slice方法主要用来拷贝原来的数据,返回的也是一个Blob实例,这个方法可以用来做切片上传。arrayBuffer方法返回一个 Promise 对象,包含 blob 中的数据,并在 ArrayBuffer 中以二进制数据的形式呈现。 const blob = new Blob([]); blob.slice(0, 1); blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer 数据的代码……*/); 3.2 运用场景通过window.URL.createObjectURL方法可以把一个blob转化为一个Blob URL,并且用做文件下载或者图片显示的链接。 Blob URL所实现的下载或者显示等功能,仅仅可以在单个浏览器内部进行。而不能在服务器上进行存储,亦或者说它没有在服务器端存储的意义。 下面是一个Blob的例子,可以看到它很短 blob:d3958f5c-0777-0845-9dcf-2cb28783acaf 和冗长的Base64格式的Data URL相比,Blob URL的长度显然不能够存储足够的信息,这也就意味着它只是类似于一个浏览器内部的“引用“。从这个角度看,Blob URL是一个浏览器自行制定的一个伪协议。 「文件下载」 [图片] 「图片显示」 [图片] 「切片上传」 [图片] 「本地文件读取」 [图片]
2021-05-10 - 按照顺序上传图片获取fileID(原方案数据混乱、递归上传方案)
原方案是for遍历(没有改良) 异步导致获取的fileID混乱 云存储里图片是按照顺序上传的,原本以为团队做了递归优化,后来发现不对,再看了看时间确认了异步处理。 [图片] temp为待上传图片数组 [代码]//for遍历原方案:虽然有promise,但是没有抛出数据直接在循环提内setData导致数据混乱 for (let i = 0, len = temp.length; i < len; i++) { httpData.push( new Promise((reslove, reject) => { //cloudPath为文件名 let cloudPath = "postImage/" + new Date().getTime() + temp[i].match(/\.[^.]+?$/)[0]; //上传存储 wx.cloud.uploadFile({ cloudPath, filePath: temp[i], success: (res) => { this.setData({ fileIDs: this.data.fileIDs.concat(res.fileID), }); console.log(res.fileID); reslove(); }, fail: (res) => { console.log(res); }, }); }) ); } //promise后面将得到的数据一起上传数据库 promise.all(httpData).then(){ //下一步 } [代码] 递归上传方案:顺序正确,耗时增加 [代码]//递归上传方案 const upload = function (imgArr, i = 0) { return new Promise((reslove, reject) => { //迭代体放在promise中 let fileIDs = []; function uploadImg(imgArr, i) { if (imgArr.length <= i) { //将fileIDs放入reslove中 reslove(fileIDs); return; } else { let cloudPath = "postImage/" + new Date().getTime() + imgArr[i].match(/\.[^.]+?$/)[0]; //文件名 wx.cloud.uploadFile({ cloudPath, filePath: imgArr[i], success: (res) => { fileIDs.push(res.fileID); uploadImg(imgArr, i + 1); }, }); } } //不要忘记调用 uploadImg(imgArr, i); }); }; //upload传入图片数据等待完成 upload(temp).then(res=>{ //将获取的fileID转换为真实链接 //...... }) [代码] 递归方案之后获取的fileID顺序正确,处理时间也正确 [图片]
2021-05-11 - 社群运营工具「知晓社群精灵」,以数字化模式赋能企业「连接客户,创造收益」
公域流量和私域流量是相对的概念,公域的流量不归企业所有,如淘宝、抖音、微博、门店客流等,是需要企业通过付费或其他营销方式去获取的,而私域的流量是归企业所有和掌控的,是可以反复使用。公域流量的弊端: 公域流量越来越贵 用户不属于自己,无法反复触达 用户只依赖于第三方平台,对企业品牌认知度低 获取用户难、成本高,越来越多的企业开始意识到搭建私域流量池的重要性和必要性。如何构建自己的私域流量池?如何让攒起来流量动起来? 微信集社交、内容(公众号、视频号)、服务(小程序、直播、支付)于一体,是企业连接用户、提供产品和服务的最佳载体,微信个人号、微信群则成为企业构建私域流量重要方式之一。但在实际的应用中,由于微信好友数有限、好友管理不够自动化、无实时数据统计等约束,限制了企业私域流量的持久运营。 企业微信不仅连接 12 亿微信用户和微信生态,同时又集成了日程、会议等效率工具,真正做到,既满足了企业的运营需求,又带给微信用户无感化体验。企业微信凭借其优势成为众多企业搭建私域流量的新载体。 [图片] [图片] 知晓云正式推出「知晓社群精灵」,以数字化新模式,赋能企业实现客户全生命周期管理。 知晓社群精灵基于企业微信的社群运营功能,打通微信小程序、公众号、App 等渠道,融合更自动化、智能化的管理工具、营销工具、数据分析,进行客户全生命周期管理,帮助企业构建私域流量池,驱动收益持续增长。 [图片] **即日起至 2021 年 5 月 31 日,报名申请知晓社群精灵公测,将有机会获得 1 年的免费使用资格。**此外,受邀用户还可以加入我们的「体验者交流群」,成为「知晓产品顾问」,一起参与到知晓社群精灵的升级迭代中。 点击链接(https://jinshuju.net/f/DbQv5g)申请知晓社群公测资格。 知晓社群精灵是企业搭建私域流量池的得力帮手,提供覆盖获客、留存、转化、分享全链路的社群管理工具,满足企业的各个业务场景需求。如何运用「社群精灵」,打造一个自动化、智能化、精细化的社群运营流程呢? 前提:了解客户,精准定位 企业在确定通过企业微信群构建企业私域流量之前,必须先明确几点: 明确客户群体:客户是谁?在哪里? 结合企业产品和服务自问:客户需要什么? 对外的企业微信号的形象定位? 精准定位社群属性,提供什么内容和服务? 在明确好以上几个问题后,就可以开始策划和执行社群运营了。 获客:采用「标签」进行客户分层管理 将客户引流到企业微信个人号和客户群,不是为了一尘不变的推销产品,而是为了更好的了解客户需求,以提供客户想要的产品和服务,才能真正将「流量」变成「留量」。 企业在开展全渠道的客户引流时,需要结合不同渠道的客户特点,进行客户的分层和管理。 以汽车企业为例: 汽车企业的客户可能有汽车爱好者、刚需者、设计师等等,虽然都属于汽车这一产品品类中,但客户的兴趣却截然不同。 从汽车兴趣社区引流过来的客户,自动为其打上「发烧友」标签,自动加入专属的社群,并提供精细化的服务。将这一客户群体打造成品牌忠实粉丝,对于企业来说,既是培养潜在客户,也是培养品牌宣传者。同理,从门店和商城引流过来的客户,为其打上「购买刚需」;从学校、学习网站等渠道引流的客户,为其打上「设计」。 [图片] 留存:结合社群定位,打造一套专属的运营方案 根据标签分类将具有共同需求的客户邀请到同一个群聊,进行标准化、精细化运营,是实现「留量」的重要一步。 为不同定位的社群设置不同的欢迎语,不仅让社群变得更有「仪式感」,还可以让客户更加了解社群服务。 不同定位的社群对应的欢迎语类型如下: 福利领取群:客户关注优惠和福利,需要指出群内有哪些福利、在哪里领取,甚至可以为首次进群的客户,提供优惠券等专属福利。 产品推荐群:客户关注新产品、重体验,可以在客户进群时,推荐近期的新品和测评、免费体验活动等等,让客户第一时间获得满足感。 兴趣小组群:客户基于同一个兴趣聚集在一起,可以先介绍群内的行业 KOL,邀请新成员自我介绍,帮助客户快速融入其中。 技能学习群:客户重在学习内容,第一时间介绍近期的课程安排、分享往期的学习资料,让客户进群就能收获满满。 而接下来的所有社群运营动作,便是不断提高客户的「信任感、参与感、价值感」的过程了。 信任感:企业微信个人号连接客户,提供一对一咨询服务;社群结合「快捷回复」功能,第一时间解答客户的疑虑,打造一个专业、贴心的品牌形象,获取客户的信任。 参与感:定期发布平台最新活动,结合社群运营工具「抽奖、投票、群接龙」等,让每一个客户都可以参与其中,提升客户的参与度。 价值感:包括群价值和客户自我价值。以少打扰、多服务为原则,持续向客户提供有价值的内容、社群专属福利等,可以提高社群价值。邀请客户参与到企业的品牌建设中、让客户成为讲师,提供专业的课程与分享,能提高客户的自我价值,从而提高客户对社群的价值感。 [图片] 转化:连接企业服务,打通多端平台,实现营销闭环 企业微信可以通过 UnionID 实现与微信生态(公众号、小程序)和平台(网站、App)的数据互通,帮助企业构建统一身份的客户体系。基于客户的兴趣、行为等数据,生成统一客户画像,从而反向指导企业的运营策略。 在更加了解客户需求的前提下,企业可以根据标签向客户和客户群「群发消息」,包括推荐产品、传达活动信息、发送祝福等,打造智能化、人情化的客户管理。群发消息除了文字外,还支持带上小程序、网页等内容,有利于引导客户到企业平台,实现营销闭环,从而提高客户的转化率,为企业带来更可观的收益。 分享:丰富的营销工具,客户增长更轻松 提供优质的产品和服务,树立良好的品牌口碑,让客户主动分享。此外,企业可以结合红包、抽奖、投票、直播等营销工具开展相关的裂变活动,为社群注入更多新鲜的血液,帮助企业获得更多的流量。 在以往的群裂变活动中,社群运营人员需要手动@新成员完成指定动作,才能获得相应的福利,但企业微信可以将这一流程变得更自动化: 设置加群二维码,群满 200 人自动建新群,无需工作人员手动操作 设置新成员进群欢迎语和分享图片,引导用户将图片转发到朋友圈或外部群 为完成任务的客户打上标签,并利用群发消息向该标签客户发送福利 通过企业微信,打通企业平台,构建私域流量池,进一步精细化运营客户,提高客户转化和企业营收。如何分析企业自身处境?如何结合企业的发展需求和客户特点,何时做?如何做?是需要企业持续思考、反复试验,才能找到一条更适合自己的有效路径。 如果你想了解更多成功案例,请在文末留言「案例」;如果你想了解某个行业如何打造私域,也请在文末留言具体行业。我们将根据大家的需求,输出更多内容,一起交流学习。如果你有成功的运营经验,非常期待你分享给我们。
2021-05-10 - 聊聊如何给一个小程序历史老项目“减压”
前言 在日常的工作中,由于业务或者工作安排的需要,有时候需要我们参与到一些曾经没有接触过却 历史悠久 的项目当中,如果这个项目创建初期,创建者有很好的 前瞻性,并且严格遵循 code preview 等项目开发工作流,那代码看上去就会像是同一个人写出来一样,十分规范;否则,将会逐渐沦为一个 茅坑代码集合。 接下来我想分享一下近期对公司的一个小程序项目做的一些优化工作,会分别从以下几个方面进行阐述: 项目现状 项目拆解 搭建工具平台 项目地址 项目现状 1. 子模块分包不完全,存在子包内文件相互引用的情况 2. 个别没有用到的图片等静态资源文件没有及时删除,导致包体积过大,无法生成预览码 3. 测试同学反馈小程序测试流程过于繁琐,复杂,加大测试工作量和小程序的出错率 项目拆解 因此,需要针对以上提到的三个问题对这个项目进行初步的基于项目目录结构层面上的优化(不涉及到项目里面的业务代码,组件等冗余代码的优化) 其实也很容易理解,当你刚接手一个项目的时候,想必是先对这个项目的目录结构有一个总体初步的认识。 一、 “子模块分包不完全,存在子包内文件相互引用的情况” 1. 分析 如果是微信小程序项目,我们可以通过以下两种方法去快速了解一个项目的模块分包情况: 打开根目录下的 [代码]app.json[代码] 文件,找到 [代码]subPackages[代码] 字段,这就是当前项目的所有子模块数组集合;(缺点:人工肉眼查找,不智能) [代码]// app.json { "pages": [], "subPackages": [ { "root": "A", "pages": [ "pages/A-a", "pages/A-b", .... ] }, { "root": "B", "pages": [ "pages/B-a", "pages/B-b", .... ] } ] } [代码] 通过微信提供的 cli 命令行工具,查看当前的分包情况;(优点:不仅智能,还能查看每个子模块压缩后的包大小) [代码]cli preview --project f://workspace/rainbow-mp/xinyu [代码] 效果如下: [图片] 通过cli工具分析出来的结果,我们可以很明显看出当前项目总共分了哪几个子模块,以及这些子模块经过微信压缩工具(实则微信开发者工具编译)之后的大小,由此得出的当前项目存在的问题有如下几点: 主包([代码]main[代码])体积已经超过了微信规定的 [代码]2MB[代码]最大值,无法生成预览码用于移动端测试(问题很严重); 子包分包不合理,将子包的子目录作为分包的入口(如:[代码]/daojia/pages[代码], [代码]/deptstore/pages[代码], [代码]/index/pages/userMaterial[代码], [代码]/shopping/pages[代码]),而不是将子包根目录本身作为拆包的入口,导致其余目录下的文件统一打包到了主包中,造成主包体积变大; 2. 细分拆包 换句话说,如果我们把子包的分包不合理的问题给解决了,主包([代码]main[代码])的体积过大的问题自然而然也就解决了。 定位到问题就相当于解决了一半。 接下来就是想办法把子包根目录更改为模块打包的入口,这时候有人会说了,把 [代码]app.json[代码]文件下的[代码]subPackages[代码]模块数组字段的每个模块的[代码]root[代码]的值都改成子模块的根目录不就完事了吗? 没毛病,做法就是这样 但是,在修改之前得保证拆出来子包根目录下的其余子目录下的文件并没有被别的模块引用,否则就会出现文件引用错误的bug。 因此,大致有以下两种做法可以参考一下,我采用的是第一种: 对当前拆解子包外的其余模块(包括主包[代码]main[代码]) 进行全文件扫描,通过正则的方式过滤出 [代码]require[代码]引用到的文件路径,进而分析是否有子包下的文件被别的模块引用; 复写 [代码]require[代码] 方法(因为我们项目中文件引入的方式是[代码]require[代码]方式); 这里简单说明一下我不采用第二种方法的原因: [代码]require[代码]方法没有挂载在[代码]global[代码]全局下(因为接下来我需要写脚本在node环境下运行),因此需要重写一个如[代码]myRequire[代码]的自定义函数,然后挂载到[代码]global[代码]对象下,接着全局匹配所有文件的[代码]require[代码]字符替换为[代码]myRequire[代码]; [代码]require[代码]是动态引入,也就是说,可以在[代码]js[代码]文件的任意处进行引入,写在了小程序的业务代码中,因为接下来的脚本文件是运行在微信开发者工具以外的环境,缺失了微信小程序需要的模块包,会导致编写的脚本分析文件报错; 3. 编写脚本 接下来就正式步入编码阶段了,其实思路比较简单,我大致从以下几点进行这次脚本文件的编写: 1. 获取当前项目的所有一级目录:除去当前需要拆解的子包以外的所有一级目录都需要进行全局文件扫描 [代码] * 获取当前路径下的第一层目录 * @param {*} path 项目路径 * @param {*} targetDir 子包的目录名 */ const getOwnDirectorys = async(path, targetDir) => { const dir = await fs.promises.opendir(path) const result = [] for await (const dirent of dir) { const isDir = await isDirectory(`${path}/${dirent.name}`) // 也就是说,除去子包以外的目录都需要进行全局文件扫描 if (isDir && dirent.name !== targetDir) { result.push(`${path}/${dirent.name}`) } } return result } [代码] 2.过滤出每个一级目录下所有js和json文件 读取到目录了,那接下来自然就是遍历这些一级目录,然后获取到这些目录下的所有资源文件,那为什么只是过滤其中的[代码]js[代码]和[代码]json[代码]文件出来呢? 经过一段时间的接触之后,我发现: 子包的组件存在被别的模块包引用的情况,而小程序的组件引入主要是通过[代码]json[代码]文件的[代码]usingComponents[代码]字段; 子包的[代码]js[代码]文件也存在被别的模块包引用的情况,多数发生在一些工具函数,接口调用文件上; 因此,为了减少扫描文件的数量和提高效率,先针对项目中每个模块的[代码]js[代码]和[代码]json[代码]文件进行扫描匹配。 [代码]const filterJsAndJsonFiles = async (dirItem, filterDirs) => { const subDir = await fs.promises.opendir(dirItem) const jsFiles = [] const jsonFiles = [] for await (const dirent of subDir) { // 不需要分析的目录直接跳过 if (!filterDirs.includes(dirent.name)) continue const currentFiles = getAllFiles(`${PROJECT_NAME}${dirItem}/${dirent.name}`) // 过滤若干不同类型的文件数组 currentFiles.forEach(fileItem => { const extname = path.extname(fileItem) if (extname === '.json') { jsonFiles.push(fileItem) } if (extname === '.js') { jsFiles.push(fileItem) } }) } return { jsFiles, jsonFiles, } } [代码] 3. 文件查找 & 匹配 到这里,我们已经拿到了每个模块对应下的所有[代码]js[代码],[代码]json[代码]文件,接下来就需要针对这些文件进行分析了,大致思路分为以下两点: [代码]json[代码]文件分析:读取文件内容,将[代码]json[代码]字符串转为[代码]json[代码]对象格式,过滤出[代码]usingComponents[代码]字段,查找匹配出拆解子包的组件; [代码]{ "usingComponents": { "a": "./A/a", "b": "../B/b", "c": "../C/c" } } [代码] [代码]js[代码]文件分析:读取文件内容,通过[代码]正则表达式[代码]过滤出[代码]require[代码]引入的文件字符数组,从中查找匹配出拆解子包内的文件引用; [代码]const a = require('../../a.js') const b = require('./b.js') .... [代码] 脚本编写: json文件组件引入分析: [代码]/** * 统计json文件引入到的组件数组 * @param {*} jsonFile */ const listComponents = (jsonFile) => { if (!jsonFile) return const jsonDataStr = fs.readFileSync(jsonFile) const jsonData = JSON.parse(jsonDataStr) const componentList = [] if (jsonData) { const { usingComponents } = jsonData for (let key in usingComponents) { componentList.push({ name: key, path: usingComponents[key], filePath: jsonFile, }) } } return componentList } [代码] js文件[代码]require[代码]引入分析: [代码]const lineReg = /require\s*\([\'\"][\w\W]*[\'\"]\)/g // 子模块初始化 moduleResultMap[dirKey] = { componentImport: [], fileImport: {}, } jsFiles.forEach(filePath => { const fileContent = fs.readFileSync(filePath, 'utf8') // 为了避免无用查找,只针对前30行文本进行内容分析 const lines = fileContent.split(/\r?\n/).splice(0, 30) // 初始化子包目录文件名 moduleResultMap[dirKey]['fileImport'][filePath] = lines.reduce((acc, current) => { const matchArr = current.match(lineReg) return matchArr && matchArr.length > 0 && matchArr[0].indexOf('/daojia/') > -1 ? [...acc, matchArr[0]] : acc} , []) }) [代码] 4. 效果展示 最后,我是将分析出来的结果导出到了[代码]csv[代码]文件中,以便于为我接下来的拆包提供一份相对有保障的可视化的支持: [图片] 因为我这次主要是针对项目中[代码]daojia[代码]这个子模块进行一个拆包,因此分析的也是针对项目中其余子模块对该模块文件的一个引用情况做一个分析,表格中的每个字段所代表的意思我也大概说明一下: [代码]interface Table { module: string //子模块 type: string // 分析的文件类型 name: string // 分析的文件名 import: string // 引用的组件 || 引用的文件 filePath: string // 分析的文件路径 } [代码] 5. 终极展示 我们再回过头来看这幅图: [图片] 当我们成功地都将以下几个子包根目录从项目中剥离抽身之后,才会真的有底气地说:把[代码]app.json[代码]文件下的[代码]subPackages[代码]改下就好了 [代码]/daojia/pages -> /daojia /deptstore/pages -> /deptstore /index/pages/userMaterial -> /index /shopping/pages -> /shopping [代码] 再来看看现在的模块包分析表: [图片] 结论:经过合理化的分包之后,优化后的主包体积比优化前整整减少了35% 二、“个别没有用到的图片等静态资源文件没有及时删除,导致包体积过大,无法生成预览码” 1. 分析 在上一节里,我的做法概括起来:拆解子包,合理化模块打包 由于各种原因,一是在当前项目里面存在了过多的活动图片,重复的icon等等,但是当活动下架之后,这些图片并没有得到及时移除;二是组件引用混乱,相同组件的代码会同时出现在各个子模块里面; 这些无疑都是导致 项目体积过大 和造成 项目难以维护 的主要原因; 所以,在这一节里,也可以概括一句话:剔除无用资源,减少项目文件 2. 思路 总体来说,我也是通过写脚本来分析这些资源文件,思路如下: 无用图片资源查找 ① 根据不同模块配置信息,依次读取当前模块图片目录下的所有图片文件,过滤出图片文件名,存储在一个数组内; ② 然后全扫描这个项目内的所有文件,通过[代码]fs[代码]模块读取到文件的字符串内容,遍历图片数组,根据字符串匹配[代码]indexOf[代码],如果存在,则标记图片的引用路径;文件全扫描之后,如果找不到,则在路径一栏标记为“没有用到”; ③ 又或者匹配到的图片,则从数组内剔除出去,当扫描完所有的文件之后,剩下的就是没有引用到的图片文件了;(以上方法很蠢,但是胜在简单粗暴,希望有更好方法的朋友可以给我留言,不胜感激。) 组件引用分析 ① 根据不同模块的配置信息,依次读取当前模块内[代码]pages[代码]和[代码]components[代码]目录下的[代码]json[代码]文件(组件引入的入口),实则一个JSON字符串的转成JSON对象;[代码]JSON.parse(jsonstring)[代码] [代码]{ "usingComponents": { "a": "./A/a", "b": "../B/b", "c": "../C/c" } } [代码] ② 然后获取其相同文件名的[代码]js[代码]文件(页面或者组件的主体文件),通过[代码]fs[代码]模块读取文件内容,注意,这时候是得将这些富文本字符串转为DOM节点树结构对象,然后遍历节点对象,去匹配解析出对应的[代码]json[代码]组件引入入口文件下的json对象,然后分析出引用到的组件,实际就是节点标签名的匹配过程。 3. 脚本编写 这里就把一些核心代码贴出来就好,大家看看就好,不做过多阐述了 无用图片资源查找脚本 [代码] // 需要分析的图片目录地址 const imgDirPath = path.resolve(__dirname + '/../..' + imagesEntry); const imgFiles = getAllFiles(imgDirPath) if (imgFiles.length === 0) return // 只保留图片的文件名数组 const allImageFiles = imgFiles.map(imgItem => path.basename(imgItem)) // 查找所有的wxml, js文件 const allWxmlFiles = targetEntrys.reduce((acc, targetEntry) => { const targetDirPath = path.resolve(__dirname + '/../..' + targetEntry) const targetAllFiles = getAllFiles(targetDirPath, true) const allWxmlFiles = targetAllFiles.filter(filePath => { const extname = path.extname(filePath) return ['.wxml', '.js'].indexOf(extname) > -1 }) return [...acc, ...allWxmlFiles] }, []) // 遍历图片集数组,查找文件是否有引入 const result = allImageFiles.reduce((acc, imgName) => { const rowItems = allWxmlFiles.reduce((childAcc, filePath) => { const fileStr = fs.readFileSync(filePath, 'utf8') return fileStr.indexOf(imgName) === -1 ? childAcc : [...childAcc, { image: imgName, existPath: filePath, }] }, []) // 如果查找完毕数组为空,则说明没有引入到该图片 return rowItems.length === 0 ? [...acc, { image: imgName, existPath: '没有用到' }] : [...acc, ...rowItems] }, []) // 导出csv文件 const csv = new ObjectsToCsv(result) const exportPath = `${__dirname}${'/../..'}${BASE_EXPORT_IMG}/${imageReportFile}` await csv.toDisk(exportPath) [代码] 组件引用分析脚本 [代码] // 解析入口目录 const entryDir = path.resolve(__dirname + '/../..' + entry) const allFiles = getAllFiles(entryDir) if (allFiles.length === 0) return const filterFiles = getFilterFiles(allFiles, ['wxml', 'json']) // 组装导出对象数组数据 const pageWithComponents = filterFiles.reduce((acc, { jsonFile }) => { const current = path.basename(jsonFile, '.json') const currentDir = path.dirname(jsonFile) const components = listComponents(jsonFile) || [] if (components.length == 0) { return [...acc, { page: current, directory: currentDir, }] } else { // 输入wxml地址,转化为json标签对象 const fileJsonData = getFileJsonData(currentDir + `/${current}.wxml`) const childs = components.reduce((childAcc, { name, path: compPath }) => { let used if (fileJsonData) { used = isWxmlImportComponent(fileJsonData, name) used = used ? 'true' : 'false' } else { used = '解析出错' } return [...childAcc, { page: current, directory: path.resolve(currentDir), component: name, componentPath: compPath, used, }] }, []) return [...acc, ...childs] } }, []) // 导出csv文件 const csv = new ObjectsToCsv(pageWithComponents) const exportPath = `${__dirname}/../..${BASE_EXPORT_COMPONENT}/${exportFileName}` await csv.toDisk(exportPath) [代码] 结论:剔除没有引入的图片资源,减少项目体积;分析页面的组件引入,为项目的组件库的搭建提供数据支持。 三、“测试同学反馈小程序测试流程过于繁琐,复杂,加大测试工作量和小程序的出错率” 1. 分析 小程序测试步骤如下: 开发同学在功能提测阶段,需提供功能分支名给到测试同学,比如说:[代码]feature/monthcard[代码] 测试同学需要切换功能分支,并且拉取最新代码,执行 [代码]git checkout feature/montcard[代码] [代码]git pull origin feature/monthcard[代码] 打开 [代码]小程序开发者工具[代码],更改配置文件环境参数,如:[代码]config.js[代码],比如说修改成 [代码]env = test/dev/pre/pro[代码] 等等,切换到对应的接口环境进行测试 如只需要本地测试,直接在工具上面测试即可,如需要移动端测试,则需要点击 [代码]编译执行[代码] 生成小程序预览码,手机扫码测试 后期开发同学推了代码,需要同步测试同学定期去更新代码,执行: [代码]git pull origin 分支名[代码] 上面就是我司的关于小程序提测时的做法,相信这也是一部分公司的关于小程序的测试流程,又或者一部分公司的做法如下: 开发同学在本地生成测试预览码,然后将预览码截图发给测试同学进行测试(测试预览码有时效限制,需要开发每隔一段时间去重新生成一个新的预览码); 开发同学编写工具,将整个小程序代码包压缩放在内网的一个网页下,每次由测试下载到本地,解压,然后用开发者工具打开测试(一定程度自动化了测试流程和简化了测试同学的流程,但是依然很麻烦); 结论:总得来说,开发和测试同学都没有成功从上面的开发工作流中解耦出来。 2. 解决方案 基于上述的一些问题,我发现这一系列的测试步骤可以通过微信官方提供的ci命令行工具,是完全可以抽象出来,做成一个可以简化测试工作流的工具平台,听着是不是很棒? 下面就是我的一些调研发现: miniprogram-ci 是从微信开发者工具中抽离的关于小程序/小游戏项目代码的编译模块。 开发者可不打开小程序开发者工具,独立使用 miniprogram-ci 进行小程序代码的上传、预览等操作。 微信ci文档地址 搭建工具平台 前端(js) React 搭建前端骨架(借用facebook提供 [代码]create-react-app[代码] 脚手架即可) Bootstrap 作为前端界面布局的ui框架库 后端(nodejs) 采用 [代码]Express[代码] web应用开发框架搭建即可 安装 [代码]miniprogram-ci[代码] 包(构建预览码,提交发版等) 安装 [代码]html2json[代码], [代码]objects-to-csv[代码] 包(用于项目静态资源使用分析等) ps: 这里就不对里面的技术细节做过多阐述了,具体可以查看文末的项目地址,我已经开源出来了。 效果展示: [图片] 项目地址 出于代码保密考虑,开源项目里面分析的小程序源码采用的是自己的一个小程序项目作为分析的基础项目: 小程序项目脚本分析项目地址 https://github.com/csonchen/wx-mall-components 小程序项目的自动化构建服务平台 https://github.com/csonchen/mpcode-manage
2021-02-23 - 最新小程序业界动态
[微信]小程序类目审核加急通道开放[微信]微信开发者工具 1.05.2105100 Stable 更新说明[百度] 搜索算法课程回放来袭,小程序学院巡回公开课报名中!—【5.1—5.10】每周速报[百度] 3.29.1 以下版本百度开发者工具支持将于 2021 年 5 月 14 日终止[百度]智能小程序学院巡回公开课火热报名中![头条]抖音小程序学院:听说他用10分钟就学会了开发小程序?
2021-05-13 - 富文本组件体验小程序
简介 上周发布的 新富文本显示组件 收获了许多关注,为方便大家了解和体验,[代码]demo[代码] 小程序上线啦 [图片] 大家可以在这里查看介绍和示例或者进行自定义的测试,发现任何问题都欢迎反馈哦 立即体验 [图片] GitHub链接 Github链接
2020-12-27 - 小程序CSS-JS Shared variable,共享变量,减少CDN文件路径变量重复定义
CSS变量与JS共享: CSS Module的:export方法,功能上类似ES6的export关键字,即导出一个变量对象 首先定义一份sass文件如下: $COMMON_ARROW_RIGHT: 'https://you.domain.com/123dfdds768fkasfhja3.png'; $INDEX_OLD: 'https://you.domain.com/s7812312312dsvasty8jkassd.png'; $MINE_USER_AVATAR: 'https://you.domain.com/79jksadsuek32423saasfh4.png'; // :export { COMMON_ARROW_RIGHT: $COMMON_ARROW_RIGHT; INDEX_OLD: $INDEX_OLD; MINE_USER_AVATAR: $SERVICE_WARNING; } 然后定义cloud.js文件如下: /** * CDN,图片云,资源云,CSS-JS 变量共享 */ import urls from './index.module.scss'; const clouds = {}; for(let i in urls) clouds[i] = urls[i].split('"')[1]; export default clouds; CSS中使用 @import "~@cloud/index.module.scss"; .container { position: relative; width: 100%; height: 100vh; background: #f3f7f9 url($INDEX_OLD) top center no-repeat; } JS中使用: JS中使用: import Clouds from '@cloud/index'; const { MINE_USER_AVATAR } = Clouds; // Render函数部分伪代码: <Image className={'global-user-avatar'} src={MINE_USER_AVATAR} />
2021-04-29 - 微信小程序页面监听全局变量的改变
app.js------------------------------------------------ watch: function (method) { //监听函数 var obj = this.globalData Object.defineProperty(obj, 'clock', { configurable: true, enumerable: true, set: function (value) { this._name = value; method(value); }, get: function () { return this._name } }) }, globalData: { clock:""//要监听的变量 } ———————————————— index.js------------------------------- onLoad:function(options){ getApp().watch(this.watchBack)//注册监听 }, watchBack: function (value){ //要执行的方法 this.setData({ clock: value }) } 亲测可用。 但是文档上没有不知道为啥,如果有坑请留言哈。
2021-04-28 - 关于textarea/input层级太高,有内样式或元素对不齐,问题解决
1、如果是textarea或者input层级太高 解决方法:给标签加上always-embed属性(基础库需要2.10.4以上)[图片] 如果兼容低版本基础库,需要使用cover-view写样式覆盖(标签文档:https://developers.weixin.qq.com/miniprogram/dev/component/cover-view.html) 2、如果发现textarea和其它元素需要一排对齐,但是ios有内样式,无法和安卓一样对齐 解决方法:给标签加上disable-default-padding属性(基础库需要2.10.0以上) [图片] 如果需要兼容低版本基础库,需要判断当前是否为ios系统,如果是不加任何padding,否则给textarea加上padding样式,padding: 18rpx 10rpx具体可以根据真机观察给大小。 写文章不容易,有帮助希望点下赞,程序员不难为程序员。
2021-04-27 - 微信支付用云函数实现notify_url
没有自己的服务器,怎么用云函数来接收微信支付成功异步通知呢。 步骤如下: (因论坛审核机制,就不上图片了,否则可能触发人工审核) 打开小程序开发工具; 1、云控制台中,点击:设置--环境设置--充值与账单; 此时会跳到腾讯云; 2、点击:账号中心--访问管理; 此时会跳到腾讯云控制台; 3、在左上角点击:云产品--找到:云开发CloudBase; 此时出现小程序的云环境列表 4、选择某个云环境; 5、点击:访问服务; 6、新建一个HTTP访问服务;将一个公网域名URL与一个云函数关联; 比如云函数名:pay_notify与https://<http访问服务的默认域名>/pay_notify关联。 7、在统一下单里,将notify_url设为:https://<http访问服务的默认域名或自定义域名>/pay_notify 此时,可以在pay_notify中处理来自微信支付的异步通知了; 那这个云函数的入口参数是什么样的呢?接口文档: https://cloud.tencent.com/document/product/876/41776 8、固定IP的配置:可以在此处腾讯云中配置,也可以在小程序开发工具的云控制台配置:某云函数--配置--高级设置--固定IP。 9、pay_notify的代码实例:以微信支付V2为例。 const cloud = require('wx-server-sdk') const xml2js = require('xml2js') const crypto = require('crypto') const config = require('./config.js') const key = config.key cloud.init({env: cloud.DYNAMIC_CURRENT_ENV}) const db = cloud.database() const _ = db.command const col = db.collection('payments') exports.main = async event => { let xml = Buffer.from(event.body, 'base64').toString() let payment = await parseXML(xml) if(signVerify(payment)){}else return 'denied' await onPayment(payment) //业务处理 return `` } 至此,不用服务器,通过云函数,就实现了微信支付notify_url的全部功能。
2021-04-28 - 如何彻底解决小程序滚动穿透问题
背景 俗话说,产品有三宝:弹窗、浮层加引导,足以见弹窗在产品同学心目中的地位。对任意一个刚入门的前端同学来说,实现一个模态框基本都可以达到信手拈来的地步,但是,当模态框里边的内容滚动起来以后,就会出现各种各样的让人摸不着头脑的问题,其中,最出名的想必就是滚动穿透。 什么是滚动穿透? 滚动穿透的定义:指我们滑动顶层的弹窗,但效果上却滑动了底层的内容。 具体解决方案分析如下: 改变顶层:从穿透的思路考虑,如果顶层不会穿透过去,那么问题就解决了,所以我们尝试给蒙层加catchtouchmove,但是发现部分场景无效果,那么就不再赘述了。 改变底层:既然是顶层影响了底层,要是底层不会滚动,那就没这个问题了。 如何改变底层解决该问题呢? 不成熟方案: 底部页面最外层view设置position: fixed;页面不可滚动,但是这个时候会导致页面回到顶部。 滚动时监听滚动距离,弹窗时记录滚动位置,关闭弹窗后使用wx.pageScrollTo回滚到记录的位置。 成熟方案 使用page-meta组件,通过该组件我们可以操作Page的style样式,类似于h5里body设置overflow: hidden; 控制页面不可滚动。文档地址:https://developers.weixin.qq.com/miniprogram/dev/component/page-meta.html 使用wx.setPageStyle设置overflow: hidden, 也可以实现给Page组件设置样式。) page-meta组件: 通过该组件我们可以直接操作[代码]Page[代码]组件 ,我们给它的wxss样式overflow动态设置[代码]hidden[代码]or[代码]visible[代码]or[代码]auto[代码] 就可以控制整个页面是否可以滚动。 [图片] wx.setPageStyle方法: 调用这个api,动态设置它为hidden/auto,用于控制页面是否可滚动,主要用于页面组件内使用,比如封装好的弹窗组件,就不用单独写page-meta组件了。。 [代码]wx.setPageStyle({ style: { overflow: 'hidden' // ‘auto’ } }) [代码] 老规矩,结尾放代码片段: https://developers.weixin.qq.com/s/U6ItgQmP7upQ 拓展 支付宝小程序虽然存在page-meta组件,但是由于内核为69版本,给page设置overflow: hidden 也无法控制底部元素不可滚动,目前已联系支付宝的底层开发同学提供API控制页面disableScroll,目前正在封装Appx,近期开放。 [代码] my.setPageScrollable({ scrollable: true, success: res => { console.log(res); }, fail: err => { console.log(err); }, complete: res => { console.log(res); } }) [代码] 20250618. API已开放,支付宝小程序测试时发现bug,安卓设置禁止滚动后,弹窗内的可滚动区域也会被禁止,IOS正常,且该问题暂时无法解决。 原因: 由于系统实现层面的差异,安卓与 iOS 对于滚动禁止的层级控制存在区别: 安卓端采用 Webview 级滚动限制(全页面锁定),生效时界面及所有弹层均不可滚动; iOS 端采用组件级滚动限制(局部锁定),当弹层激活时会智能区分层级,仅限制底层页面滚动而保持弹层可滚动。
星期三 16:30 - 小程序三级联动,实现三级分类,顶部导航栏,左侧分类栏,右侧数据列表
如果大家一直读石头哥的文章,或者看石头哥的视频,肯定知道,石头哥的点餐小程序有实现二级菜品或者商品分类。 如下图 [图片] 但是有时候我们想实现三级分类,该怎么做呢,今天就来教大家如何实现三级分类。随便教下大家如何把excel数据批量的导入到云开发数据库 一,老规矩,先看效果图 [图片] 先来给大家分析下原理 二,原理分析 首先来分析下有那三级 [图片] 可以看出,我们最顶部是一级菜单,左侧是二级菜单,右侧是最终的三级列表。 我们来理一理层级关系 =宿舍楼号 ====宿舍号 ========学生 聪明的人肯定知道,我们是一个宿舍楼里包含很多宿舍,宿舍里有包含很多学生。这样我们的三级就是 楼号》宿舍》学生。 当我们切换楼号时,就会重新获取当前楼号包含的宿舍。 比如下图左为惠兰楼,右为学苑楼的数据,可以看出每个楼里的宿舍和学生信息。 [图片] 分析完原理,我们就来看技术实现了。 三,获取分类数据 这里先给大家说下,我这里是用一张表来存的所有信息 [图片] 既然是一张表存所有数据,我们就要做下分组,看数据里都有哪些楼号。 3-1,借助group实现楼号分组(一级数据) [图片] 具体代码如下 [图片] 然后获取到的数据如下 [图片] 可以看出我们一共有11个宿舍楼。就是我们顶部的这些区域 [图片] 3-2,借助group和match实现宿舍分组(二级数据) [图片] 这个时候,我们就要根据用户选择的楼号,来对当前楼号下所有数据进行分组了 [图片] 分组后的数据如下 [图片] 可以看出,前进楼有两个宿舍 3-3,借助where获取宿舍里的学生数据(三级) [图片] 获取的数据如下 [图片] 到这里我们的三级分类就实现了 四,完整项目代码 下面把完整项目代码,贴出来给大家 4-1,wxml [代码]<!-- 导航栏 --> <scroll-view scroll-x class="navbar" scroll-with-animation scroll-left="{{scrollLeft}}rpx"> <view class="nav-item" wx:for="{{tabs}}" wx:key="id" bindtap="tabSelect" data-id="{{index}}"> <view class="nav-text {{index==tabCur?'tab-on':''}}">{{item._id}}</view> </view> </scroll-view> <view class="container"> <!-- 左边的 --> <scroll-view class='nav_left' scroll-y='true'> <block wx:for="{{lefts}}" wx:key="{{index}}"> <view class="nav_left_items {{leftCur==index?'active':''}}" bindtap="switchLeftTab" data-index='{{index}}'> {{item._id}}</view> </block> </scroll-view> <!-- 右边的 --> <scroll-view class="nav_right" scroll-y="true"> <view class="{{topx}}"> <block wx:for="{{rights}}" wx:key="index"> <view class="nav_right_items" data-id="{{item._id}}"> <image src="{{item.touxiang}}"></image> <text>{{item.mingzi}}</text> </view> </block> </view> </scroll-view> </view> [代码] 4-2,wxss样式 [代码]/* 导航栏布局相关 */ .navbar { width: 100%; height: 90rpx; /* 文本不换行 */ white-space: nowrap; display: flex; box-sizing: border-box; border-bottom: 1rpx solid #eee; background: #fff; align-items: center; /* 固定在顶部 */ position: fixed; left: 0rpx; top: 0rpx; } .nav-item { padding-left: 25rpx; padding-right: 25rpx; height: 100%; display: inline-block; /* 普通文字大小 */ font-size: 28rpx; } .nav-text { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; letter-spacing: 4rpx; box-sizing: border-box; } .tab-on { color: #000080; /* 选中放大 */ font-size: 38rpx !important; font-weight: 600; border-bottom: 4rpx solid #000080 !important; } /* 正文部分 */ .container { position: fixed; width: 100%; height: 90%; top: 100rpx; background-color: #FFF; } .nav_left { width: 25%; height: 100%; background: #F2F2F2; text-align: center; position: absolute; top: 0; left: 0; } .nav_left .nav_left_items { height: 100rpx; line-height: 100rpx; font-size: 28rpx; } .nav_left .nav_left_items.active { position: relative; background: #FFF; color: #000080; } .nav_left .nav_left_items.active::before { display: inline-block; width: 6rpx; height: 60rpx; background: #000080; content: ''; position: absolute; top: 20rpx; left: 0; } .nav_right { position: absolute; top: 0; right: 0; width: 75%; height: 100%; } .nav_right .nav_right_items { float: left; width: 33.33%; text-align: center; padding: 30rpx 0; } .nav_right .nav_right_items image { width: 120rpx; height: 160rpx; } .nav_right .nav_right_items text { display: block; margin-top: 20rpx; font-size: 28rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .nocate { padding: 100rpx; text-align: center; } .nocate image { width: 70rpx; height: 70rpx; } .nocate text { font-size: 28rpx; display: block; color: #BBB; } /*隐藏滚动条*/ ::-webkit-scrollbar { width: 0; height: 0; color: transparent; } .topx { width: 90%; /* height: 700rpx; */ margin: 30rpx auto; z-index: 1; border-radius: 10rpx; background-size: cover; } [代码] 4-3,js实现分类逻辑 [代码]const db = wx.cloud.database() const $ = db.command.aggregate Page({ data: { tabs: [], tabCur: 0, //默认选中 lefts: [], leftCur: 0, rights: [], }, onLoad: function (options) { db.collection('demo').aggregate() .group({ _id: '$louhao' }) .end() .then(res => { console.log('楼号列表', res) this.setData({ tabs: res.list }) this.sushehao(res.list[0]._id) }) }, //加载当前楼号所有的宿舍号 sushehao() { let louhao = this.data.tabs[this.data.tabCur]._id db.collection('demo').aggregate() .match({ louhao }) .group({ _id: '$sushe' }) .sort({ sushe: -1 //宿舍号升序排列 }) .end() .then(res => { console.log(louhao + '宿舍号列表', res) this.setData({ lefts: res.list }) this.xuesheng() }) }, //加载当前宿舍号里所有的学生 xuesheng() { let louhao = this.data.tabs[this.data.tabCur]._id let sushe = this.data.lefts[this.data.leftCur]._id db.collection('demo') .where({ louhao, sushe }).get() .then(res => { console.log(louhao + sushe + '室学生列表', res) this.setData({ rights: res.data }) }) }, //顶部选择分类条目 tabSelect(e) { this.setData({ tabCur: e.currentTarget.dataset.id, scrollLeft: (e.currentTarget.dataset.id - 2) * 200 }, success => { this.sushehao() }) }, //左侧条目选择 switchLeftTab(e) { let index = e.target.dataset.index; this.setData({ leftCur: index, }, success => { this.xuesheng() }) }, }) [代码] 4-4,记得修改数据表权限 修改权限为所有用户可读,仅创建者可读写 [图片] 到这里我们的三级分类就完整的实现了。关于excel数据批量导入,我下节再做讲解的。欢迎关注,欢迎留言交流。
2021-03-28 - 云开发数据库如何实现动态排行榜?
现在做的小程序里有个排行榜,用户可以多次更新自己的成绩,我要取一个时间范围内的每个用户的最好值,并且从高到低排序,用云开发里的数据库怎么实现?各位大佬指教~ 如果要体验查看具体问题,请到个人中心扫码~
2019-07-20 - 云开发 数据库可以 动态拼接 查询指令吗?
因为mysql的查询指令是字符串..所以可以动态拼接.例如.如果数组 a.length!=0 就添加条件,..等于0则不添加 云开发 数据库可以 动态拼接 查询指令吗?
2019-11-06 - 微信群聊的一点小建议
因为工作需求的原因,会和不同的客户以及某一些人,临时拉起一个群聊,如果当时忙的话,就没有设置针对性的群聊名称。 然后如果在只要要去找这个群聊的时候,很麻烦,且很不好找到。 现在标签功能只是针对联系人,无法对群聊设置标签。 能否加上一个类似标签树的功能,比如:1下面有2和3, 2和3里面可以有不同的群组。 虽然听起来有些像QQ的某些功能,但还是希望可以吸纳一些。 如果有些功能我没有找到,且可以实现我上面的需求,麻烦大佬不吝告知~~~~
2021-04-23 - 关于云开发数据库的使用经验和建议
作者:布道师 shaohui_xia 小程序·云开发是微信团队联合腾讯云推出的专业的小程序开发服务。 开发者可以使用云开发快速开发小程序、小游戏、公众号网页等,并且原生打通微信开放能力。 开发者无需搭建服务器,可免鉴权直接使用平台提供的 API 进行业务开发。 数据库的上手、初始化等可参看官方链接:小程序·云开发 二、使用经验直接使用云开发API场景:页面或方法的逻辑简单,关联一个数据库,无联表查询 例子: db.collection('todos').doc('todo-identifiant-aleatoire').get({ success: function(res) { // res.data 包含该记录的数据 console.log(res.data) } }) 使用数据聚合能力场景:页面或方法的逻辑中等,关联多个数据库,可能存在联表查询或数据处理 例子: const db = wx.cloud.database() const $ = db.command.aggregate db.collection('books').aggregate() .group({ // 按 category 字段分组 _id: '$category', // 让输出的每组记录有一个 avgSales 字段,其值是组内所有记录的 sales 字段的平均值 avgSales: $.avg('$sales') }) .end() 借助promise,async等场景:页面或方法的逻辑较为复杂,关联多个数据库,可能存在多次查询以及云函数或https请求 以下是对云开发CMS导出数据的扩展案例 其中整合了上述的几种方式 例子: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) var xlsx = require('node-xlsx'); const db = cloud.database(); const MAX_LIMIT = 100; const _ = db.command; exports.main = async (event, context) => { console.log(event) event.queryStringParameters = event.queryStringParameters||{}; const collection = event.collection || event.queryStringParameters.collection; const params = event.params || event.queryStringParameters.params || {}; // const acceptType = ["String", "Tel", "Array", "Number", "Connect", "Boolean", "Enum", "Date", "DateTime"]; //"File","Image" const unacceptType = ["File", "Image"]; const schemasRes = await db.collection("tcb-ext-cms-schemas").where({ collectionName: collection }).get(); const schemas = schemasRes.data[0]; let connectList = []; const title = event.title || event.queryStringParameters.title || schemas.displayName || "数据"; // 先取出集合记录总数 const countRes = await db.collection(collection).where(params).count(); const fields = schemas.fields.filter(function (schemas) { return unacceptType.indexOf(schemas.type) == -1 && (!schemas.isHidden); }); const connectResourcenList = []; fields.forEach(field => { if (field.type == "Connect") { connectList.push(field); connectResourcenList.push(field.connectResource) } }); const schemasListRes = await db.collection("tcb-ext-cms-schemas").where({ _id: _.in(connectResourcenList) }).limit(MAX_LIMIT).get(); const schemasList = schemasListRes.data || []; // console.log("fields==============================") console.log(schemasList) const total = countRes.total // 计算需分几次取 const batchTimes = Math.ceil(total / MAX_LIMIT) // 承载所有读操作的 promise 的数组 const tasks = [] for (let i = 0; i < batchTimes; i++) { //console.log(connectList.length) if (connectList.length > 0) { let lookupList = []; connectList.forEach(connect => { const connectschemas = schemasList.filter(function (schemas) { return schemas._id == connect.connectResource; })[0]; lookupList.push({ from: connectschemas.collectionName, localField: connect.name, foreignField: '_id', as: "connect" + connect.name }) }); let aggregate = db.collection(collection).aggregate().match(params).skip(i * MAX_LIMIT).limit(MAX_LIMIT); for (let index = 0; index < connectList.length; index++) { aggregate = aggregate.lookup(lookupList[index]); } aggregate = aggregate.end(); tasks.push(aggregate) } else { const promise = db.collection(collection).where(params).skip(i * MAX_LIMIT).limit(MAX_LIMIT).get(); tasks.push(promise) } } console.log(tasks) // 等待所有 let recordRes = (await Promise.all(tasks)).reduce((acc, cur) => { return { list: (acc.list || []).concat(cur.list || []), data: (acc.data || []).concat(cur.data || []), } }) let records = (recordRes.list || []).concat(recordRes.data || []) || []; //1.定义表格名 let dataCVS = title + '.xlsx'; let excelData = []; let row = []; fields.forEach(field => { row.push(field.displayName) }); excelData.push(row); records.forEach(record => { let arr = []; fields.forEach(field => { if (!record.hasOwnProperty(field.name)) { arr.push("") } else { switch (field.type) { case "Connect": arr.push(join2Str(record["connect" + field.name], field.connectField)) break; case "DateTime": arr.push(formatDateTime(record[field.name])) break; case "Date": arr.push(formatDate(record[field.name])) break; case "Boolean": arr.push(record[field.name] ? "是" : "否") break; case "Enum": let enumElements = field.enumElements; let enumElement= enumElements.find(function(item){ return item.value = record[field.name]; }) arr.push(enumElement.label) break; default: arr.push(record[field.name]) break; } } }); excelData.push(arr); }); //3,把数据保存到excel里 var buffer = await xlsx.build([{ name: title, data: excelData }]); //4,把excel文件保存到云存储里 const excelFileIdRes = await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }); return await cloud.getTempFileURL({ fileList: [excelFileIdRes.fileID] }).then(function (res) { return res.fileList[0].tempFileURL }) } function join2Str(obj, fieldName) { if (Object.prototype.toString.call(obj) == "[object Array]") { let resultArr = []; obj.forEach(item => { if (item.hasOwnProperty(fieldName)) resultArr.push(item[fieldName]) }); return resultArr.join(",") } else { if (obj.hasOwnProperty(fieldName)) return obj[fieldName] } } function formatDateTime(inputTime) { var date = new Date(inputTime); var y = date.getFullYear(); var m = date.getMonth() + 1; m = m < 10 ? ('0' + m) : m; var d = date.getDate(); d = d < 10 ? ('0' + d) : d; var h = date.getHours(); h = h < 10 ? ('0' + h) : h; var minute = date.getMinutes(); var second = date.getSeconds(); minute = minute < 10 ? ('0' + minute) : minute; second = second < 10 ? ('0' + second) : second; return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second; }; function formatDate(inputTime) { var date = new Date(inputTime); var y = date.getFullYear(); var m = date.getMonth() + 1; m = m < 10 ? ('0' + m) : m; var d = date.getDate(); d = d < 10 ? ('0' + d) : d; return y + '-' + m + '-' + d; }; 整合数据库框架场景:小程序或APP的业务逻辑复杂,模板页面的开发,组件的开发和统一异常处理 例子: 以下例子引用了wxboot的小程序框架 //app.js // const {WXBoot} = require('wxbootstart'); require('./lib-webpack/wxboot'); import login from "./login/login" import utils from "./utils/utils" import constants from "./constants/constants" App.A({ config: { initCloud:{ // env: '', traceUser: true,}, route: '/pages/$page/$page', pageApi: utils, consts: constants, updata:{ arrObjPath:false, arrCover:false }, mixins:[login,App.A.Options] , }, getOpenidFunc: function(){ return this.cloud.callFunction({ name:"getWXContext" }).then(res=>{ return res.result.openid; }).catch(err=>{ console.error(err) return "" }) }, onLaunch: function (opts) { App.A.on('some_message', function (msg) { console.log('Receive message:', msg) }) console.log('APP is Running', opts) }, store: { id: 0 }, auth:{ canUseXXX:false }, globalData: { version: "v1.0.0", id: 0, userInfo: null, addressInfo: null, sessionKey: null, loginTime: 0, openid: "", theme: { color: "#FFFFFF" }, share: { title: "开启一天好运", imageUrl: "https://XXX.jpg", path: "/pages/index/index" }, settings: null }, onAwake: function (time) { console.log('onAwake, after', time, 'ms') }, onShow: function () { console.log('App onShow') }, /*小程序主动更新 */ updateManager() { if (!wx.canIUse('getUpdateManager')) { return false; } const updateManager = wx.getUpdateManager(); updateManager.onCheckForUpdate(function (res) {}); updateManager.onUpdateReady(function () { wx.showModal({ title: '有新版本', content: '新版本已经准备好,即将重启', showCancel: false, success(res) { if (res.confirm) { updateManager.applyUpdate() } } }); }); updateManager.onUpdateFailed(function () { wx.showModal({ title: '更新提示', content: '新版本下载失败', showCancel: false }) }); }, "navigateToMiniProgramAppIdList": [ "wx8abaf00ee8c3202e" ] }) 全局封装增删改 ,我们更专注的关注于业务逻辑,统一异常处理 module.exports = { $callFun: callFunction, $add: add, $get: get, $update: update, $remove: remove, $count:count } //取数据库实例。一个数据库对应一个实例 /** * 封装查询操作 * 增 查 改 删 * */ //增 async function add(collectionName, data, openParse = false) { if (openParse) { data = await parseQuery(data, this) } return this.$collection(collectionName).add({ data }).then(res => { return res._id }).catch(res => { return "" }) } //查询 //对应id取不到的时候,返回{} async function get(collectionName, query, openParse = false) { switch (type(query)) { case "string": return this.$collection(collectionName).doc(query).get().then(res => { return res.data }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return {} }) case "object": const defaultOptions = { where: null, order: null, skip: 0, limit: 20, field: null, pageIndex: 1 } const parsequery = setDefaultOptions(query, defaultOptions); let { where, order, skip, limit, field, pageIndex } = parsequery; let collectionGet = this.$collection(collectionName); if (where != null) { if (openParse) { where = await parseQuery(where, this) } collectionGet = collectionGet.where(where) } if (order != null) { if (type(order) == "object") { collectionGet = collectionGet.orderBy(order.name, order.value); } if (type(order) == "array") { order.forEach(orderItem => { collectionGet = collectionGet.orderBy(orderItem.name, orderItem.value); }); } } if (field) { collectionGet = collectionGet.field(field); } if (pageIndex > 1) { collectionGet = collectionGet.skip((pageIndex - 1) * limit).limit(limit); } else { collectionGet = collectionGet.skip(skip).limit(limit); } return collectionGet.get().then(res => { return res.data }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return [] }) default: console.warn(`"query":参数类型错误不存在`) return null; } } async function count(collectionName, query, openParse = false) { switch (type(query)) { case "object": let collectionUpdate = this.$collection(collectionName); if (openParse) { query = await parseQuery(query, this) } collectionUpdate = collectionUpdate.where(query) return collectionUpdate.count().then(res => { return res.total }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) default: return this.$collection(collectionName).count().then(res => { return res.total }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) } } //修改 async function update(collectionName, query, updata, openParse = false) { switch (type(query)) { case "string": return this.$collection(collectionName).doc(query).update({ data: updata }).then(res => { return res.stats.updated }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return 0 }) case "object": let collectionUpdate = this.$collection(collectionName); if (openParse) { query = await parseQuery(query, this) } collectionUpdate = collectionUpdate.where(query) return collectionUpdate.update({ data: updata }).then(res => { return res.stats.updated }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) default: console.warn(`"query":参数类型错误不存在`) return 0 } } //删除 async function remove(collectionName, query, openParse=false) { switch (type(query)) { case "string": return this.$collection(collectionName).doc(query).remove().then(res => { return res }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return {} }) case "object": let collectionRemove = this.$collection(collectionName); if (openParse) { query = await parseQuery(query, this) } collectionRemove = collectionRemove.where(query) return collectionRemove.remove().then(res => { return res }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return [] }) default: console.warn(`"query":参数类型错误不存在`) return 0 } } function setDefaultOptions(options = {}, defaultOptions = {}) { return Object.assign(defaultOptions, options); } function promisify(api) { return (options, ...query) => { return new Promise((resolve, reject) => { api(Object.assign({}, options, { success: resolve, fail: reject }), ...query); }) } } async function callFunction(options) { return await this.cloud.callFunction(options) } var undef = void(0) function type(obj) { if (obj === null) return 'null' else if (obj === undef) return 'undefined' var m = /\[object (\w+)\]/.exec(Object.prototype.toString.call(obj)) return m ? m[1].toLowerCase() : '' } async function parseQuery(query, self) { let queryStr = JSON.stringify(query); if (queryStr.indexOf("{openid}") > -1) { let openid = await self.$getOpenid(); return JSON.parse(queryStr.replace(/{openid}/g, openid)); } else { return query } } 高级用法,结合云函数和https 以及封装api ,实现统一对外接口,对接其他语言场景:多项目,多后台,多端打通,数据迁移等 // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); // 云函数入口函数 exports.main = async (event, context) => { let body = event.body; let cloudParams = urlToObj(decodeURIComponent(body)); let { cloudType, collectionName } = cloudParams; let data = JSON.parse(cloudParams.data || "{}"); let query = JSON.parse(cloudParams.query || "{}"); if(type(query)=="object"){ query.where = JSON.parse(query.where ||"{}" ); if(query.field) query.field = JSON.parse(query.field ||"{}" ); } console.log(query) let promise = null; switch (cloudType) { case "ADD": promise = add(collectionName, data); break; case "GET": promise = get(collectionName, query) break; case "UPDATE": promise = update(collectionName, query, data) break; case "REMOVE": promise = remove(collectionName, query) break; case "COUNT": let countquery = null; if (type(query) == "string") { countquery = query } else { countquery = query.where || null } promise = count(collectionName, countquery) break; default: break; } return promise; } function urlToObj(str) { var obj = {}; var arr2 = str.split("&"); for (var i = 0; i < arr2.length; i++) { var res = arr2[i].split("="); obj[res[0]] = res[1] || ""; } return obj; } //增 async function add(collectionName, data, openParse = false) { if (openParse) { data = await parseQuery(data) } return db.collection(collectionName).add({ data }).then(res => { return res._ids || res._id; }).catch(res => { return "" }) } //查询 //对应id取不到的时候,返回{} async function get(collectionName, query, openParse = false) { if (query.limit && query.limit == "all") { let countquery = null; if (type(query) == "string") { countquery = query } else { countquery = query.where || null } // 先取出集合记录总数 const total = await count(collectionName, countquery); // 计算需分几次取 const batchTimes = Math.ceil(total / 20) // 承载所有读操作的 promise 的数组 const tasks = [] for (let i = 0; i < batchTimes; i++) { query.limit = 20; query.pageIndex = i + 1; const promise = get(collectionName, query); tasks.push(promise) } // 等待所有 return (await Promise.all(tasks)).reduce((acc, cur) => { acc = acc || []; cur = cur || []; return acc.concat(cur); }) } switch (type(query)) { case "string": return db.collection(collectionName).doc(query).get().then(res => { return res.data }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return {} }) case "object": const defaultOptions = { where: null, order: null, skip: 0, limit: 20, field: null, pageIndex: 1 } const parsequery = setDefaultOptions(query, defaultOptions); let { where, order, skip, limit, field, pageIndex } = parsequery; let collectionGet = db.collection(collectionName); if (where != null) { if (openParse) { where = await parseQuery(where) } collectionGet = collectionGet.where(where) } if (order != null) { if (type(order) == "object") { collectionGet = collectionGet.orderBy(order.name, order.value); } if (type(order) == "array") { order.forEach(orderItem => { collectionGet = collectionGet.orderBy(orderItem.name, orderItem.value); }); } } if (field) { collectionGet = collectionGet.field(field); } if (pageIndex > 1) { collectionGet = collectionGet.skip((pageIndex - 1) * limit).limit(limit); } else { collectionGet = collectionGet.skip(skip).limit(limit); } return collectionGet.get().then(res => { return res.data }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return [] }) default: console.warn(`"query":参数类型错误不存在`) return null; } } async function count(collectionName, query, openParse = false) { switch (type(query)) { case "object": let collectionUpdate = db.collection(collectionName); if (openParse) { query = await parseQuery(query) } collectionUpdate = collectionUpdate.where(query) return collectionUpdate.count().then(res => { return res.total }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) default: return db.collection(collectionName).count().then(res => { return res.total }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) } } //修改 async function update(collectionName, query, updata, openParse = false) { switch (type(query)) { case "string": return db.collection(collectionName).doc(query).update({ data: updata }).then(res => { return res.stats.updated }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return 0 }) case "object": let collectionUpdate = db.collection(collectionName); if (openParse) { query = await parseQuery(query) } collectionUpdate = collectionUpdate.where(query) return collectionUpdate.update({ data: updata }).then(res => { return res.stats.updated }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) default: console.warn(`"query":参数类型错误不存在`) return 0 } } //删除 async function remove(collectionName, query, openParse = false) { switch (type(query)) { case "string": return db.collection(collectionName).doc(query).remove().then(res => { return res }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return {} }) case "object": let collectionRemove = db.collection(collectionName); if (openParse) { query = await parseQuery(query) } collectionRemove = collectionRemove.where(query) return collectionRemove.remove().then(res => { return res }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return [] }) default: console.warn(`"query":参数类型错误不存在`) return 0 } } function setDefaultOptions(options = {}, defaultOptions = {}) { return Object.assign(defaultOptions, options); } function promisify(api) { return (options, ...query) => { return new Promise((resolve, reject) => { api(Object.assign({}, options, { success: resolve, fail: reject }), ...query); }) } } var undef = void(0) function type(obj) { if (obj === null) return 'null' else if (obj === undef) return 'undefined' var m = /\[object (\w+)\]/.exec(Object.prototype.toString.call(obj)) return m ? m[1].toLowerCase() : '' } async function parseQuery(query) { let queryStr = JSON.stringify(query); if (queryStr.indexOf("{openid}") > -1) { let openid = cloud.getWXContext().OPENID; return JSON.parse(queryStr.replace(/{openid}/g, openid)); } else { return query } } 三、建议云开发是主要是类似mongdb的非关系数据库,可以保存json的数据,我们可以多直接保存复杂的值尝试使用自己封装的业务逻辑来全局控制异常等数据库的权限、索引等可以对数据库检索性能进一步优化产品介绍云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 技术交流群、最新资讯关注微信公众号【腾讯云云开发】
2021-06-10 - 微信公众号头条文章设置2张封面图的方法!
同一篇文章,显示两种封面,并且可以看出小图并不是从大图里直接截取的,这是怎么做到的呢?今天小妹儿就来给大家演示一下。 [图片] 接触过公众号的小伙伴都知道封面图分为两种尺寸。一种封面尺寸为2.35:1(900*383px), 另一种封面尺寸为1:1(200*200px),公众号头条文章的封面都是2.35:1的尺寸,次条的封面图以及头条分享好友之后的封面图都是1:1尺寸的。 [图片] 想要微信公众号的头条推文同时设置2张封面图,教你两种办法! 第一种方法,根据官方提供的比例(2.35:1)来做,推荐尺寸900*383px。制作封面图时,除了考虑整体的美观外,注意,需要将主要信息布局在一个正方形区域内,保证传达信息完善。 [图片] 第二种,根据两种比例尺寸,分别制作出两张封面图:长图和方图。因为是为同一篇文章设计的,所以两张图的风格应保持一致,方图的信息也尽量全面。 [图片] 然后,打开 Ps,可以将两张图片拼在一起 ,上下排列放在一张图中,接着保存图片并导出即可。 [图片] 最后,将拼好导出的图片上传至微信公众号后台,利用上传封面时的【剪裁】功能 ,对照预览封面,按照要求选择不同的图片区域就可以啦! [图片] 当然,拼接图片的时候不仅可以将图片进行上下拼接,也可以将图片进行左右拼接,一样的道理。这个方法你学会了吗?
2020-12-07 - getUserInfo接口如何替换成getUserProfile超详细说明
微信小程序API近期又做了调整,之前用的好好的getUserInfo做了重大调整,无法直接获取用户信息了,比如昵称头像等等,当然2021年4月13日上线前的小程序不受影响,如果想要再次升级新版本,即必须涉及到更换获取用户授权的修改,将getUserInfo改成getUserProfile接口。 [图片] 这就代表着之前用的获取授权信息的方法要做调整了,先看看哪些模块受到影响。 之前使用如下代码,可以获取用户的相关信息: 之前:(使用上面的方式,会出现授权弹窗,同意后可以直接获取到nickName昵称及avatarUrl用户头像) [图片] 现在:(不会弹窗,直接获取用户信息nickName变成了匿名,avatarUrl用户头像变成了灰色头像) [图片] 授权过后鉴定是否授权同样也无法使用了,下面是app.js中的代码同过wx.getSetting获取scope.userInfo判断是否授权,现在获取不该参数,所有这个在app.js中的写法就无法判断用户有没有登录了。 [图片] 针对上面的文字,下面开始来介绍下我的实现方式,如何用好新接口getUserProfile。 先来看看wx.getUserProfile怎么用:https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html 上面是官方地址,里面有详细的说明。 getUserProfile不像之前的getUserInfo一样必须放在按钮button上,而且要使用open-type="getUserInfo",新接口直接可以应用在任何标签上,使用点击时间或者其他触发事件直接执行getUserProfile接口,如下所示: 注:desc属性必须填写,不填写可能获取不到数据; 如下图所示,将会出现弹窗授权 [图片] 每次点击都会获取一次授权,这样有个好处就是之前getUserInfo时候拒绝了之后再想同意需要操作很大一圈代码,这个的话,每次点击都会重新弹窗一次这样倒是解决了一大难题。 [图片] 正确的用户信息,包含了昵称及头像等等... 存在的问题: 如果每次点击都授权的话用户体验非常的不友好,比如做了一个博客系统,每次用户想要给某一条信息点赞,点赞前都要授权一次,用户肯定很烦,所以,获取授权第一次的时候就要存储下来,然后再做表单提交或者点赞评论时候,判断数据库中用户信息是否存在就好了。 解决方案: user页面,提醒客户点击登录授权,默认头像及文字提醒,授权过后显示头像及昵称;[图片] user.wxml页面 {{userInfo.nickName}} {{userInfo.country+userInfo.province+userInfo.city}} 点击登录 user.js页面 //没有授权过的话,不要在当前页面存储用户信息,直接跳转到login页面同意处理用户信息 goLogin(){ wx.navigateTo({ url: '/pages/login/login' }) } login登录页面的操作,点击确认授权弹出授权浮窗。[图片] login.wxml页面 确认授权 暂不授权 login.js页面 //获取授权信息 clickUserProfile(){ wx.getUserProfile({ desc: '业务需要', lang:'zh_CN', success:res=>{ this.saveUserInfo(res.userInfo) } }) }, //保存用户信息 saveUserInfo(userInfo){ app.globalData.userInfo=userInfo //使用页面栈的方式,获取了授权信息接着更改用户页面的userInfo属性 var page=getCurrentPages()[getCurrentPages().length-2]; page.setData({ userInfo }) //使用云函数saveuser将用户信息存储到云数据库中 wx.cloud.callFunction({ name:"saveuser", data:{ userInfo } }).then(res=>{ wx.showToast({ title: '授权成功' }) setTimeout(()=>{ this.noLogin(); },1500) }) } saveuser云函数页面 // 云函数入口函数 exports.main = async (event, context) => { const openid = cloud.getWXContext().OPENID const {userInfo}=event; userInfo.openid=openid; //获取数据库中有没有当前用户的信息 var res= await db.collection("userAll").where({ openid:openid }).count() if(res.total>0){ return await db.collection('userAll').where({ openid }).update({ data: userInfo }) }else{ return await db.collection('userAll').add({ data: userInfo }) } } 一旦获取了用户信息,自动会从login页面跳转到user页面,同理user页面中的userInfo就变成了最新的用户数据,user页面也就变成了这样; [图片] 首次进入user页面时候需要从数据库判断是否已经存在该用户信息 app.js页面 //定义hasUserInfo函数,发送云函数,同过返回true和false判断是否已经授权 async hasUserInfo(){ if (this.globalData.userInfo && this.globalData.userInfo.nickName && this.globalData.userInfo.avatarUrl) return true var res= await wx.cloud.callFunction({ name:"getuser" }) if(res.result.code==200){ this.globalData.userInfo=res.result.data[0] return true }else{ return false } } getuser云函数页面 // 云函数入口函数 exports.main = async (event, context) => { const openid = cloud.getWXContext().OPENID var res=await db.collection("userAll").where({ openid }).get(); if(res.data.length){ return {data:res.data,code:200} }else{ return {code:400} } } 在需要的位置就可以使用app.js中的hasUserInfo方法了,比如user页面 user.js页面 //页面载入时 onLoad:async function (options) { await app.hasUserInfo() this.setData({ userInfo:app.globalData.userInfo }) } 比如对一个点赞按钮操作时候先判断有没有用户信息时候: //点赞操作 async clickZan(){ if(await app.hasUserInfo()){ console.log("可以点赞"); }else{ wx.navigateTo({ url: '/pages/login/login' }) } } 还有一种不保存用户信息,只负责在页面中展现的可以直接使用open-data组件,不用授权就可以轻松获取用户信息; 组件地址如下: https://developers.weixin.qq.com/miniprogram/dev/component/open-data.html 演示代码如下: 最终效果 [图片] 如果文章没有看懂,还有视频的介绍 https://www.bilibili.com/video/BV1s64y1i7Rw
2021-04-22 - 一个云函数五行代码搞定云调用openapi
云调用接口如下: https://developers.weixin.qq.com/miniprogram/dev/api-backend/ 1、该文档中的几十个接口,全部可由下面5行代码实现: 2、同时支持共享环境下的云调用 云函数名:openapi index.js代码: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async event => { let appid = cloud.getWXContext().FROM_APPID || cloud.getWXContext().APPID return await cloud.openapi({appid})[event.action](event.body) } 小程序端调用代码: onOpenapi: function () { wx.cloud.callFunction({ name: 'openapi', data: { action: 'urlscheme.generate', body: {} } }).then(res => { console.log(res) }) }, 将云调用相关的云函数合并成一个。 而且,极简。。。
2022-09-29 - 5大要点,打造企业微信个人IP
成功的企业微信个人IP打造一定是能最大化展示自己的价值和特征,让人第一次见到你就能记住你,有需求的时候,他就会想起来,即使你没有和他对话,他也能通过你的对外形象了解到,你公司的业务是什么。我们想实现这样的效果,就要从5个方向入手,企业微信的定位,昵称、头像、对外信息页、欢迎语,称之为企业微信IP打造5件套。 企业微信的定位企业微信的定位是我们在做企业微信运营规划时第一时间要确定的,原则就是能够让客户知道我们提供的价值和服务,成为客户的第一选择。 定位要考虑两个方面,一是企业的主营业务,二是目标用户需求。 比如完美日记的IP是小完子,是一个懂美妆爱生活的可爱的女生形象。它的核心价值是私人美妆顾问,通过彩妆测评、专业教程等高度活跃的文字把“私人美妆顾问”的人设给立了起来,让用户感受到,和她沟通的是一个鲜活的人,在彩妆领域是专业的可以信任的。它的附加价值是专属福利官,好物种草,宠粉福利,整个IP打造综合考虑了两方面,一个是完美日记的彩妆业务,另外就是年轻消费者对于美妆种草和好物推荐的需求。 再比如瑞幸咖啡的首席福利官lucky,这个号定位是通过发不同产品不同类型的优惠券去提升销量和复购。 企业微信定位我们可以前期基于企业的业务去大胆尝试,再基于用户的需求和反馈进行优化,定位是企业本身的业务和用户需求相平衡的一个结果。 过目不忘的昵称企业微信官方没有个性签名和朋友圈封面设置这些入口,所以要优先去考虑昵称和头像的设置。 企业微信自带高亮的品牌后缀,具有比较强的信任背书。好的昵称设置可以让客户过目不忘,增强信任,关键时刻能够想起你。这里和大家分享昵称设置的3个小技巧。 1、容易记 昵称要便于客户记忆,或者是非常独特,有记忆点的,最好是让客户听说之后就不会忘记。 最好用中文昵称,而且要起的通俗易懂,朗朗上口。 可以用一些简单的昵称,比如说阿兰、小狸这样的,或者佳佳、娜娜这样的叠词。 呢称可以艺术化,将呢称与商品联络起來从而产生自身的特点。例如,完美日记的企业微信昵称“小完子”。 2、业务相关 微信昵称最好跟我们的核心业务,或者品牌名称有一定的关联度。另外还需要考虑是整个企业使用统一的对外的昵称,还是各个业务同事之间的昵称不相同。比如说瑞幸咖啡的首席福利官lucky就是一个统一的昵称,比如掌门小学助教小晨老师,掌门高中助教小狸老师就是不同业务同事前缀相同昵称不同。 3、方便搜索 便于搜索就是说他有需求的时候能够第时一间找到你,如果你的昵称中有特殊的标点、小表情、火星文,那就会把这个搜索难度给大大提升了。 加分的企业微信头像人都是视觉动物,我们企业微信号的头像其实就决定了客户对我们的第一印象。 在企业微信号的头像设计上,不建议大家直接去用品牌的Logo或者品牌的一些产品来做头像。虽然看起来比较跟品牌关联度高,有的品牌Logo也非常的高大上,但是给客户的距离感和营销感太强,不利于去做后续私域转化。 好的头像可以让用户倍感亲切,建议使用真人形象照或者比较整洁,清新的个人生活照作为头像。真实的头像,能够让用户感知到,人设的颜值、仪表、穿着、消除距离感、拉近和用户的关系。在利用网络经营客户关系时,最重要的就是要给客户一个真实存在的人形象,这样可以加深信任和提升企业微信的温度。比如完美日记的小完子使用的就是一个真人头像。 如果不想用真人头像,用优质的卡通形象也是一个不错的选择,比如说瑞幸咖啡lucky的头像就是一个小鹿的,会随着季节或节日主题去更换头像背景增加一些小的装饰。 对外信息页用企业微信的神奇之处就在于,一旦有用户添加你,就意味着转化开始了。企业微信有2个隐藏的转化入口,对外信息页和欢迎语,很多企业都没有注意到这个入口的存在,白白浪费了很多流量。 对外信息页是新用户添加企业微信好友之后第一时间看到的地方,有非常大的曝光量,是一个重要的变现入口。还是一个持续拉新的入口。 比如西贝的对外信息页展示了线上商城、会员中心、西贝外卖,掌门的对外信息展示了免费领课、0元领书、学习资料,非常吸引人,家长看到就会想点击看一下。 [图片] 大家在添加微信后一般第一个动作就是翻一下这个人的朋友圈,企业微信自带的朋友圈功能是只有在刷朋友圈的时候能看到,而且1个用户1天只能看到企业微信的1条朋友圈。但是在企业微信的对外信息页,我们可以打造一个虚拟朋友圈。比如几何裂变青妹对外信息页的朋友圈。 [图片] 企业成员对外信息的设置是在企业微信管理后台,通讯录管理中,找到成员对外信息展示,点击添加自定义信息。 [图片] 客户欢迎语能在客户添加好友之后第一时间做转化的,除了企业微信成员的名片之外,还有一个关键能力就是欢迎语。 有温度的欢迎语能够提升用户对品牌以及员工的好感。欢迎语有两个原则,第一个原则就是我们要传达清楚企业微信号的定位和价值,让用户知道你是做什么的,对我有什么价值,可以降低好友流失率。第二要跟客户破冰去产生互动,有了这两点,我们再去做一些营销动作客户是不会过于抵触的。 比如完美日记的小完子,添加企业微信后先做自我介绍,是你的专属福利官和美妆顾问,然后告诉你稍后拉你进群领取福利,给了用户一个等待和期待的理由,同时打消用户疑虑,这个号不是机器人,用户好感度蹭蹭蹭上涨,通过这样一个简单又高情商等待互动引导,营造朋友的感觉,后续这个小丸子她去给用户发进群链接或者是推送一些消息的时候,用户的接受度都是会比较高的。 [图片] 如果我们的用户是通过多个渠道引流过来,比如门店、包裹卡、老带新裂变、渠道投放等,不同渠道可以配备不同场景的欢迎语,自动打用户标签,还可以统计不同渠道用户数量,这个就是企业微信的渠道码功能。 以上就是企业微信IP打造5件套的分享。
2021-04-14 - [开盖即食]九宫格抽奖component组件分享
[图片] 这次继续分享第二个抽奖组件,参考了网上多个版本,本人根据实际工作中进行了一些优化,并将其做成component组件方便大家食用~ [图片] 1、现在上吃的,呸,上代码 页面引用部分: [代码]<!-- 数据是根据外部配置的,同时也修改组件自定义callback返回内容 --> <LuckComponent lucks-data="{{lucksData}}" bind:callBack="luckCb"></LuckComponent> <view class="roll">当前抽奖结果index:{{luck_num}}</view> [代码] [代码]Page({ data: {}, onLoad() { //在这里配置显示数据,未来还能添加图片等等 let lucksData = [{ //这里修改后,可以通过后台请求配置 "key": "baofu", "name": "暴富", "indexli": 1 }...]; this.setData({ lucksData }) }, /** * 结果回调函数 * @param {*} e */ luckCb(e){ console.log(e); if(e.detail){ this.setData({ luck_num:e.detail }) } } }) [代码] Component组件部分 [代码]<view class="luck_box"> <view class="luck"> <view class='li {{amplification_index===item.indexli?"indexli":""}}' wx:key="item" wx:for="{{lucksData}}"> <!-- 开始 --> <view bindtap="startrolling" class="startrolling" wx:if="{{item.indexli === -1}}"> <view class="st1">抽奖</view> </view> <block wx:if="{{item.indexli !== -1}}"> <view class="setup_title"> <view class="txt">{{item.name}}</view> <view class="index">当前index:{{item.indexli}}</view> <view wx:if="{{item.parentsClass}}" class="^parentsClass">{{item.parentsClass}}</view> </view> <view class="indexli_view"></view> </block> </view> </view> </view> [代码] [代码]Component({ /** * 组件的属性列表 */ properties: { lucksData: { type: Array, value: [] }, }, /** * 组件的初始数据 */ data: { amplification_index: 0, //轮盘的当前滚动位置 roll_flag: true, //是否允许滚动 max_number: 8, //轮盘的全部数量 speed: 300, //速度,速度值越大,则越慢 初始化为300 myInterval: "", //定时器 max_speed: 40, //滚盘的最大速度 minturns: 8, //最小的圈数为2 runs_now: 0, //当前已跑步数 luck_num: 0, // 中奖位置!!!!!!!!!!!!!!!!!!!!!!!!! end_amp: 0, //上一次滚动的位置 start_flag: true, lucksData: [], //这里是渲染数据 }, /** * 组件的方法列表 */ methods: { //开始滚动 startrolling: function () { let _this = this; //roll点 let random = parseInt(Math.random() * 8 + 1); if (this.data.start_flag == true) { _this.setData({ luck_num: random, start_flag: false }) //初始化步数 _this.data.runs_now = 0; //当前可以点击的状态下 if (_this.data.roll_flag) { _this.data.roll_flag = false; //启动滚盘, _this.rolling(); } }; //回调行数,把结果传出去 this.triggerEvent('callBack', random); }, //滚动轮盘的动画效果 rolling: function (amplification_index) { let _this = this; this.data.myInterval = setTimeout(function () { _this.rolling(); }, this.data.speed); this.data.runs_now++; //已经跑步数加一 this.data.amplification_index++; //当前的加一 //获取总步数,接口延迟问题,所以最后还是设置成1s以上 let count_num = this.data.minturns * this.data.max_number + this.data.luck_num - this.data.end_amp; //上升期间 if (this.data.runs_now <= (count_num / 3) * 2) { this.data.speed -= 30; //加速 if (this.data.speed <= this.data.max_speed) { this.data.speed = this.data.max_speed; //最高速度为40; } } //抽奖结束 else if (this.data.runs_now >= count_num) { clearInterval(this.data.myInterval); this.data.roll_flag = true; this.setData({ end_amp: _this.data.amplification_index, start_flag: true }) if (_this.data.is_selected == 0) { wx.showModal({ title: '很遗憾', content: _this.data.prize_name, showCancel: false, success(res) { } }) } else if (_this.data.is_selected == 1) { wx.showModal({ title: '恭喜您', content: _this.data.prize_name, showCancel: false, success(res) { } }) } } //下降期间 else if (count_num - this.data.runs_now <= 10) { this.data.speed += 20; } //缓冲区间 else { this.data.speed += 10; if (this.data.speed >= 100) { this.data.speed = 100; //最低速度为100; } } if (this.data.amplification_index > this.data.max_number) { //判定!是否大于最大数 this.data.amplification_index = 1; } this.setData(this.data); }, } }) [代码] 2、食用指南 可以通过 [代码]<slot>[代码] 、 [代码]^class[代码] 和 [代码]~class[代码] 等方法外部配置组件的样式,使其能在多个地方复用 如果还想配置如起始点,速度等,可以统一通过option传参的方式,二次开发下这个组件。 可以通过修改组件让callback返回更多参数 [图片] 3、具体代码片段 地址: https://developers.weixin.qq.com/s/a5NiCwms7gpI 建议将IDE工具升级到 1.03.24以上,避免一些BUG [图片] 如有疑问请留言~ 觉得有用,请点个赞哦,让我继续分享更有动力~
2021-04-13 - 自开发小程序关联视频号解决办法
众所周知目前自开发小程序暂时不支持关联视频号。但是现在甲方爸爸需要真个功能,跟甲方爸爸说微信不支持,甲方爸爸不满意,给甲方爸爸实现这个功能,小程序不支持,掉了三根头发后终于想出了一个折中的办法:借用公众号。 具体思路如下:1,公众号添加一篇文章,插入视频号视频2,小程序webview 加载这篇公众号链接。具体操作: 1,添加公众号文章插入视频号视频 [图片] 2,添加好文章后,微信打开这篇文章,复制该文章链接 [图片] 3,微信小程序用webview 加载该链接,效果如下 [图片]
2020-12-26 - 微信小程序图表工具chart.js
介绍 适用于avm多端以及原生微信小程序图表库 github地址: https://github.com/apicloudcom/chart.js 例子: https://www.chartjs.org/samples/latest 此源码是基于chart.js(2.9)改造的avm版本,可适用于多端以及原生微信小程序。一般情况你不需要关心此源码,只需要使用即可 使用 使用同chart.js基本一致,点击查看文档。 在avm使用 使用 APICloud Studio 3 作为开发工具。 下载本源码,二级目录avm-demo为demo目录。 在开发工具中新建项目,并将demo导入新建的项目中,注意更新 config.xml 中的 appid 为你项目的 appid。 使用 AppLoader 进行真机同步调试预览。 或者提交项目源码,并为当前项目云编译自定义 Loader 进行真机同步调试预览。 云编译 生成 Android & iOS App 以及微信小程序源码包。 在原生微信小程序中使用 下载本源码,二级目录wx-demo为微信的demo目录。 打开微信开发者工具,导入wx-demo 在浏览器中运行使用 直接运行二级目录samples中的index.html在浏览器 License MIT © APICloud
2021-04-02 - eventChannel的用法的一些经验,既好理解,又简单好用。
eventChannel具体用法不多介绍,看文档: https://developers.weixin.qq.com/miniprogram/dev/api/route/wx.navigateTo.html eventChannel对于初学者,是有点弯绕,不好理解的,我们对其用法做了修改: 1、父页打开子页,用globalData传参; 2、子页返回父页,用eventChannel传参; 具体代码如下: pageFather.js: 原做法: fatherEC1:function(){ wx.navigateTo({ url: './child', events: { ec: e => console.log(e) }, success: res => { res.eventChannel.emit('ec', 'father') } }) }, 现做法: fatherEC2:function(){ app.globalData.ecData = 'father' wx.navigateTo({ url: './child', events: { ec: e => console.log(e) } }) }, pageChild.js: 原做法: childEC2:function(){ this.ec = this.getOpenerEventChannel() this.ec.once && this.ec.once('ec', e => { console.log(e) //'father' }) }, 现做法: childEC1:function(){ console.log(app.globalData.ecData) //'father' }, 当然从子页里往父页传参还是保持不变: onSubmit: function () { this.getOpenerEventChannel().emit('ec', 'child') wx.navigateBack() }, onDelete: function () { this.getOpenerEventChannel().emit('del', true) wx.navigateBack() }, onUpdate: function () { this.getOpenerEventChannel().emit('update', {data}) wx.navigateBack() }, 这样改一下,是不是简单了,好理解多了? 实际上效果也很好,而且大概率不会发生一种叫“21 events balabala”的告警。
2021-04-12 - 小程序·云开发实战 - 体重记录小程序
前一段看到朋友圈里总是有人用txt记录体重,就特别想写一个记录体重的小程序, 现在小程序的云开发有云函数、数据库,真的挺好用,很适合个人开发者,服务器域名什么都不用管,云开发让你完全不用操心这些东西。 先看看页面效果图吧: [图片] [图片] [图片] [图片] [图片] [图片] [图片] 记录的几个点: 1.全局变量 globalData 2.npm 的使用 3.云函数 4.数据库操作 5.async 的使用 6.分享的配置 7.antV使用 8.tabBar地址跳转 9.切换页面刷新 1.全局变量 globalData 首次进入后,要存储openId给其他页面使用,使用globalData共享。 [代码]<!--app.js 设置 globalData.openid --> App({ onLaunch: function () { this.globalData = {} wx.cloud.init({}) wx.cloud.callFunction({ name: 'login', data: {}, success: res => { this.globalData.openid = res.result.openid wx.switchTab({ url: '/pages/add/add', fail: function(e) {} }) }, fail: err => { } }) } }) <!--其他页面引用--> const app = getApp() // 获得实例 app.globalData.openid // 直接引用即可 [代码] 2.npm 的使用 1.进入小程序源码[代码]miniprogram[代码] 目录,创建 [代码]package.json[代码] 文件(使用 [代码]npm init[代码] 一路回车) 2.[代码]npm i --save[代码] 我们要安装的 [代码]npm[代码] 包 3.设置微信开发者工具 构建 [代码]npm[代码] 4.[代码]package.json[代码] 增加 [代码]"miniprogram": "dist"[代码] 打包目录字段,如果不设置的话上传和预览不成功,提示文件包过大。 [代码]cd miniprogram npm init npm i @antv/f2-canvas --save // 我用到了f2,可以换成其他包 [代码] 设置微信开发者工具 [图片] 构建 [代码]npm[代码] [图片] 最后,务必添加 [代码]miniprogram[代码] 字段 [代码]{ "name": "21Day", "version": "1.1.0", "miniprogram": "dist", "description": "一个21天体重记录的app", "license": "MIT", "dependencies": { "@antv/f2-canvas": "~1.0.5", "@antv/wx-f2": "~1.1.4" }, "devDependencies": {} } [代码] 3.云函数 官方解释 [代码]云函数即在云端(服务器端)运行的函数[代码] ,服务端是 [代码]node.js[代码] ,都是 [代码]JavaScript[代码] 。官方有数据库的操作,但是更新的操作强制要求使用云函数, 另外,如果云函数中使用了 [代码]npm[代码] 包,记得在所在云函数文件夹右键上传并部署,不然运行失败。 [图片] 上一个例子,更新体重的云函数 [代码]// 云函数 const cloud = require('wx-server-sdk') const moment = require('moment') cloud.init( { traceUser: true } ) const db = cloud.database() const wxContext = cloud.getWXContext() exports.main = async (event, context) => { // event 入参参数 delete event.userInfo try { return await db.collection('list').where({ _openid:wxContext.OPENID, date:moment().format('YYYY-MM-DD') }) .update({ data: { ...event }, }) } catch(e) { console.error(e) } } [代码] 小程序端调用 [代码]wx.cloud.callFunction({ name: 'add', data: { ...Param }, success: res => { wx.showToast({ title: '新增记录成功', }) }, fail: err => { wx.showToast({ icon: 'none', title: '新增记录失败' }) } }) [代码] 4.数据库操作 其实是接入的 [代码]MongoDB[代码] ,封装了一部分 [代码]api[代码] 出来,详细的就看官方文档吧,有区分服务端和小程序段。 [代码]const db = wx.cloud.database() // 查询数据 db.collection('list').where({ _openid: app.globalData.openid, date: moment().subtract(1, 'days').format('YYYY-MM-DD'), }).get({ success: function (res) { // do someThing } }) [代码] 5.async 的使用 [图片] 官方文档提示不支持 [代码]async[代码],需要引入 [代码]regeneratorRuntime[代码] 这个包,先 [代码]npm i regenerator[代码] 。 然后把 [代码]node_modules[代码] 文件夹下的 [代码]regenerator-runtime[代码] 的 [代码]runtime-module.js[代码] 和 [代码]runtime.js[代码] 两个文件拷贝到lib目录下,在页面上引入即可。 [代码]<!--事例--> const regeneratorRuntime = require('../../lib/runtime.js') onLoad: async function (options) { // 获取当天数据 await this.step1() // 时间类型设置 let nowHour = moment().hour(),timeType nowHour > 12 ? timeType = 'evening' : timeType = 'morning' this.setData({timeType}) } [代码] 6.分享的配置 分享很简单,有区分右上角的直接分享和点击按钮分享 [代码]onShareAppMessage: function (res) { // 右上角分享 let ShareOption = { title: '21天体重减肥记录', path: '/pages/index/index', } // 按钮分享 if(res.from == "button"){ ShareOption = { title: '来呀 看看我的减肥记录呀', path: '/pages/detail/detail?item=' + app.globalData.openid, } } return ShareOption } [代码] 分享后,他人点击页面,跳转到对应 [代码]pages[代码] 地址,从 [代码]onLoad[代码] 的 [代码]options[代码]中拿入参请求数即可 [代码]onLoad: function (options) { const db = wx.cloud.database() let This = this let resault = {} db.collection('list').where({ _openid: options.item }).get({ success: function (res) { resault = res.data This.setData({ resault:resault }) } }) }, [代码] 7.antV使用 上边第二小节有提到 [代码]antV[代码] 的安装,就不再赘述,直接说一下再页面中引用。 说下使用,需要设置一个全局变量储存图表的实例,然后在钩子函数内容使用 [代码]changeData[代码] 方法修改数据。 [代码]index.json[代码] 中引入包名 [代码]{ "usingComponents": { "ff-canvas": "@antv/f2-canvas" } } [代码] [代码]// 引入F2 import F2 from '@antv/wx-f2'; // 设置实例全局变量(务必) let chart = null; function initChart(canvas, width, height, F2) { // 使用 F2 绘制图表 let data = [ // { timestamp: '1951 年', step: 38 }, ]; chart = new F2.Chart({ el: canvas, width, height }); chart.source(data, { step: { tickCount: 5 }, timestamp: { tickCount: 8 }, }); chart.axis('timestamp', { label(text, index, total) { const textCfg = {}; if (index === 0) { textCfg.textAlign = 'left'; } if (index === total - 1) { textCfg.textAlign = 'right'; } return textCfg; } }); chart.axis('step', { label(text) { return { text: text / 1000 + 'k步' }; } }); chart.tooltip({ showItemMarker: false, onShow(ev) { const { items } = ev; items[0].name = null; items[0].name = items[0].title; items[0].value = items[0].value + '步'; } }); chart.area().position('timestamp*step').shape('smooth').color('l(0) 0:#F2C587 0.5:#ED7973 1:#8659AF'); chart.line().position('timestamp*step').shape('smooth').color('l(0) 0:#F2C587 0.5:#ED7973 1:#8659AF'); chart.render(); return chart; } // 生命周期函数 onLoad(){ // 使用changeData赋值 chart.changeData(stepInfoList) } [代码] 8.tabBar地址跳转 如果要跳转的地址不在 [代码]app.json[代码] 的 [代码]tabBar[代码] 内可以使用 [代码]wx.navigateTo[代码] ,如果在死活跳不过去,要使用[代码]wx.switchTab[代码] 方法跳转。 [代码]wx.switchTab({ url: '/pages/add/add', fail: function(e) {} }) wx.navigateTo({ url: '../deployFunctions/deployFunctions', }) [代码] 9.切换页面刷新 切换几个tabBar的时候,需要刷新数据。 在 [代码]onShow[代码] 方法中再调用一下 [代码]onLoad[代码] 方法就可以了。 [代码]onShow: function () { this.onLoad() } [代码] 源码链接 https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心! [图片]
2019-08-05 - 校友会小程序开发笔记二十八:小程序启动性能评测与优化(1)
校友会小程序启动是用户体验中极为重要的一环,启动耗时过长会造成校友会小程序用户流失 开发者代码注入(逻辑层)校友会小程序启动时需要从代码包内读取小程序的配置和代码,并注入到 JS 引擎中。 在校友会主包代码注入过程中,会触发小程序的 [代码]App.onLaunch[代码] 和首次 [代码]App.onShow[代码] 生命周期。 在校友会开发者代码注入完成后,框架侧会根据校友会用户访问的页面进行一些页面数据初始化工作,触发首页的 [代码]Page.onLoad[代码], [代码]Page.onShow[代码] 事件。 对启动耗时的影响 校友会开发者代码的注入耗时直接影响小程序的启动耗时。 在主流的 JS 引擎中,代码注入耗时包括编译和执行等环节,代码量、同步接口调用和一些复杂的计算,都会影响注入耗时。 由于校友会首页渲染需要使用逻辑层发送的数据,如果开发者代码注入耗时过长,也会延迟首页渲染开始的时间。 在部分平台校友会上,微信客户端会使用 V8 引擎的 Code Caching 技术对代码编译结果进行缓存,降低二次注入时的编译耗时 开发者代码注入(渲染层)校友会开发者的 wxss 和 wxml 会经过编译注入到渲染层,包含页面渲染需要的页面结构和样式信息。 渲染层的注入耗时主要和校友会页面结构复杂度和使用的自定义组件数量有关。 渲染层和逻辑层的校友会开发者代码注入是并行进行的。 对启动耗时的影响 由于校友会首页渲染需要使用渲染层的页面结构和样式信息,如果开发者代码注入耗时过长,会延迟校友会首页渲染开始的时间。 首页(初次)渲染在校友会开发者代码注入完成后,结合逻辑层得到的数据和渲染层得到的页面结构和样式信息,校友会小程序框架会进行小程序首页的渲染, 展示小程序首屏,并触发首页的 [代码]Page.onReady[代码] 事件。[代码]Page.onReady[代码] 事件触发标志小程序启动过程完成。 对启动耗时的影响 校友会首页渲染耗时主要受页面结构和参与渲染的数据量影响 小程序首屏渲染完成从开发者角度看,校友会小程序首屏渲染完成的标志是首页 [代码]Page.onReady[代码] 事件触发。 从框架的角度来说,校友会小程序的首屏的内容是基于小程序的初始数据,以及在渲染开始前到达的 setData 数据渲染的。 首屏渲染完成不表示小程序页面一定有完整内容,开发者触发的 [代码]setData[代码](例如通过 [代码]wx.request[代码] 异步请求数据)不一定能参与到首屏渲染中。 由于框架和启动流程的差异,小程序定义的首屏渲染完成不等同于浏览器的 load 事件。 小程序启动阶段从校友会用户点击访问小程序到小程序首屏渲染完成(首页 [代码]Page.onReady[代码] 事件触发)。 打开率/到达率校友会小程序首屏渲染完成 PV 与 用户点击访问小程序 PV 的比值。[代码]流失率 = 1 - 打开率[代码] 小程序代码包优化代码包优化的核心手段是降低代码包大小,校友会小程序代码包大小直接影响了下载耗时,影响用户启动校友会小程序时的体验。 开发者可以采取以下手段优化校友会小程序代码包大小 1 校友会小程序分包加载2 校友会小程序代码重构和优化3 控制代码包内图片等资源4 及时清理没有使用到的校友会小程序代码和资源
2021-04-10 - 基于腾讯地图定位组件实现周边公用厕所远近排序分布图
前言 地图应用非常广泛,目前地图服务,都提供地图操作、标注、地点搜索、出行规划、地址解析、街景等接口,功能非常丰富。在实际开发过程中,各有优劣。本次基于需求,使用腾讯位置服务作为一个公用厕所位置标注的H5页面开发。 本次使用版本: JavaScript API 2.0版本。 项目需求 1、项目需求 基于腾讯位置服务,实现微信扫描二维码后,在微信浏览器内,展示某县城的公用厕所分布图,按照用户当前定位与各个厕所之间的距离远近排序,点击标注点跳转到腾讯地图进行导航。 [图片] 2、需求分解 基于上述需求,对使用到的腾讯位置服务接口予以分解如下: [代码]腾讯地图加载; 自动定位; 信息点(POINTS)标注maker; 计算标注点之间的距离; 导航跳转链接API接口; 街道与卫星地图切换控件; 缩放控件; [代码] 开发实战 1、引入功能库和附件库 [代码] <script charset="utf-8" src="https://map.qq.com/api/js?v=2.exp&libraries=drawing,geometry,autocomplete,convertor&key={$appkey}"></script> <script type="text/javascript" src="https://3gimg.qq.com/lightmap/components/geolocation/geolocation.min.js"></script> [代码] 2、构建腾讯地图容器 [代码]<!--地图加载--> <div id="location" onclick="getLocation();"><img src="{$url}{$STATIC}images/location.png" alt=""></div> <div id="txmap"></div> [代码] 3、调用前端定位组件 由于项目需要多次调用地图和定位,为此,在script脚本中map和geolocation都设置为全局函数。 [代码] var map;//全局函数 var geolocation = new qq.maps.Geolocation(appkey, "{$referer}"); var options = {timeout: 8000}; function getLocation() { geolocation.getLocation(showPosition, showErr, options); } [代码] getLocation(sucCallback, errCallback, [options: {timeout: number, failTipFlag: boolean}])方法 [代码]获取当前所在地理位置,调用一次即重新定位一次,定位数据比较精确。 sucCallback为定位成功回调函数,必填; errCallback为定位失败回调函数,选填,如果不填,请设为null; options为定位选项,选填,可以通过timeout参数设置定位的超时时间,默认值为10s; failTipFlag: 是否在定位失败时给出提示引导用户打开授权或打开定位开关。(即将支持) [代码] 1)定位成功回调函数 [代码]function showPosition(position) { } [代码] 获取位置坐标显示地图 [代码] map = new qq.maps.Map(document.getElementById("txmap"), { // 地图的中心地理坐标。 center: new qq.maps.LatLng(position.lat, position.lng), zoom: 15 }); [代码] 定义当前位置maker样式图片 [代码] var imgUrl = "static/rooted/images/icon.png"; var anchor = new qq.maps.Point(6, 6), size = new qq.maps.Size(45, 46), origin = new qq.maps.Point(0, 0), icon = new qq.maps.MarkerImage(imgUrl, size, origin, anchor); var marker2 = new qq.maps.Marker({ icon: icon, map: map, position: new qq.maps.LatLng(position.lat, position.lng) }); [代码] 读取信息点(POINTS)并在地图上标注 1、标准JSON数据格式 为方便展示,此处仅展示数据格式,实际应用做,使用ajax获取即可。 [代码][ { "toilet_id": "9", "toilet_name": "智慧广场", "toilet_address": "西溪路 智慧中心南", "toilet_url": "upload/preview/2020-11/15784affe0de0d45c5f33625851527e9.jpg", "toilet_lon": "115.965248", "toilet_lat": "35.597050" }, { "toilet_id": "14", "toilet_name": "唐塔公厕", "toilet_address": "东门街北段唐塔广场", "toilet_url": "upload/preview/2020-11/8e5bda8c5b12f87ebad80c247d8f2b26.jpg", "toilet_lon": "115.946365", "toilet_lat": "35.602218" } ] [代码] 2、地图标注并计算距离 [代码] //地图标注; getTxMap(newData, latlngs); //两点间的距离; getDistance(newData, latlngs); [代码] 经纬度标注封装函数 [代码] function getTxMap(newData, latlngs) { for (var i = 0; i < newData.length; i++) { (function (n) { var marker = new qq.maps.Marker({ position: latlngs[n], map: map }); qq.maps.event.addListener(marker, 'click', function () { var popHtml = '<div class="pop">到这里: <a href="https://apis.map.qq.com/uri/v1/routeplan?type=walk&from=起步位置&fromcoord=' + position.lat + ',' + position.lng + '&to=' + newData[n].toilet_name + '&tocoord=' + newData[n].toilet_lat + ',' + newData[n].toilet_lon + '&policy=0&referer={$referer}">' + newData[n].toilet_name + '</a></div>'; infoWin.open(); infoWin.setContent(popHtml); infoWin.setPosition(latlngs[n]); }); })(i); } } [代码] 计算两点间的距离函数封装 [代码] function getDistance(newData, latlngs) { var newArr = []; var start = new qq.maps.LatLng(position.lat, position.lng); for (var i = 0; i < latlngs.length; i++) { var end = latlngs[i]; var distance = Math.round(qq.maps.geometry.spherical.computeDistanceBetween(start, end) * 10) / 10; //拼接新的距离数组数据; newArr.push({ toilet_id: newData[i].toilet_id, toilet_name: newData[i].toilet_name, toilet_address: newData[i].toilet_address, toilet_url: newData[i].toilet_url, toilet_lon: newData[i].toilet_lon, toilet_lat: newData[i].toilet_lat, distance: distance }) } //升序排列; function compare(key) { return function (value1, value2) { var val1 = value1[key]; var val2 = value2[key]; return val1 - val2; } } newArr.sort(compare('distance')); console.log(newArr); [代码] 2、定位失败回调函数 [代码] //定位失败,自动跳转页面; function showErr() { //alert("定位失败!"); window.location.href = "?m=Index&a=error" } [代码] 坐标经纬度拾取 1、腾讯坐标拾取器 项目开发过程中,需要自己拾取坐标经纬度,以满足初始数据的测试和演示使用。一般会使用腾讯提供的坐标拾取器。链接地址:https://lbs.qq.com/tool/getpoint/index.html。 [代码]支持地址 精确/模糊 查询; 支持POI点坐标显示; 坐标鼠标跟随显示; [代码] 如果非要挑出点毛病的话,地图拾取框太小了,想随心所欲的拾取坐标,要缩放或拖拽很多次,心累。 2、WebService API地址解析(地址转坐标) 在项目完成测试后,如果遇到成千上百的地址时,一个一个的拾取,好像不是一个合格的开发者的所为。此时,就需要使用到地址解析和逆解析的API接口,即:在数据导入到数据库的过程中,自动批量地将地址转化为经纬度坐标,满足前端的调用。 本例中使用了腾讯位置服务的WebService API,后端语言使用PHP,简要的将该过程予以呈现。 1、封装WebService API接口函数 官方实例,如果在前端直接使用getJSON函数,会出现“同源策略”被阻止,为此需要后端爬取后,“曲线救国”。 [代码]//GET请求示例,注意参数值要进行URL编码 https://apis.map.qq.com/ws/geocoder/v1/?address=北京市海淀区彩和坊路海淀西大街74号&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77 [代码] [代码]/*地址转坐标封装函数,文件名称为points.php *$address,需要转化的地址,越详细经纬度精度越高; */ function getGeoCoding($address) { $url = "https://apis.map.qq.com/ws/geocoder/v1/?address=" . $address . "&key={$key}"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); curl_close($ch); return $output; } //获取前端传入的地址参数; $address = $_GET["address"]; //输出json数据格式,供前端调用; die(getGeoCoding($address)); [代码] 2、前端调用 [代码] //自動獲取經緯度; var getAddress = function transAddress() { var address = $("#address").val(); getPoints(address); } //前端页面输出; function getPoints(address) { $.getJSON("points.php", {address: address}, function (res) { if (res.status == 0) { $("#lng").val(res.result.location.lng); $("#lat").val(res.result.location.lat); } else { $("#message").html(res.message); } }); } [代码] 3、效果演示 [图片] 在导入地址数据的时候,一定要是省市区街道门牌号,地址越详细精度越高,否则会解析不出来,谨记! 注意事项 1、script标签加载API服务 [代码]<script charset="utf-8" src="https://map.qq.com/api/js?v=2.exp&key=YOUR_KEY"></script> [代码] 在开发过程中,默认会这样引入到前端文件。测试环境和生成环境一致,或者更换环境也是一直,不会出现问题的。但是如果是http和https不一致的协议环境下,引入文件就会出现错误提示。 建议的加载方式:src不使用协议名称,让其自动匹配。如: [代码]<script charset="utf-8" src="//map.qq.com/api/js?v=2.exp&key=YOUR_KEY"></script> [代码] 2、附加库的引入 学习一个新项目的最快捷方式是学会使用官方文档,因为这些文档是基础中的基础。但官方文档的有时太官方,有些细节无法清楚的展示出来。 官方文档不能解决的问题时,会“面对CSDN编程”,每个开发者遇到的问题不同,开发经验不同,在CSDN上的记录更多的是为了避免自己下次“入坑”提醒,无法完整的将项目的细节描述清楚,也是初学者看到人家明明解决了,为什么自己不可以的。 这里就牵涉到腾讯地图附加库的引入。 [代码] <script charset="utf-8" src="://map.qq.com/api/js?v=2.exp&libraries=drawing,geometry,autocomplete,convertor&key={$appkey}"></script> [代码] 本项目中就遇见需要计算自动定位的经纬度和各个厕所之间的距离,需要使用[代码]geometry[代码]几何运算库。在未理解官方文档的前提下,强行CSDN,走路很多弯路才发现:开发语法明明对了,但是却没有计算出距离,就是没引入对应的附加库。 3、自动定位组件库 [代码] <script type="text/javascript" src="//3gimg.qq.com/lightmap/components/geolocation/geolocation.min.js"></script> [代码] 使用自动定位功能,必须引入自动定位的[代码]geolocation.min.js[代码]附加库,无须多言。 4、经纬度位置 如果是首次开发地图就使用腾讯地图的话,出现这个错误的可能性比较低。如果有百度和高德地图开发的经验话,千万不要想当然。在这个问题上浪费了半个小时才发现,腾讯的经纬度和百度、高德的问题是互换的。 腾讯经纬度 [代码]new qq.maps.LatLng(39.914850, 116.403765); //构建对象的是(纬度,经度) [代码] 百度经纬度 [代码]map.centerAndZoom(new BMap.Point(116.4035,39.915),8); //构建对象的是(经度,纬度) [代码] 高德经纬度 [代码]position: new AMap.LngLat(116.39, 39.9),//构建点对象的是(经度,纬度) [代码] 在使用坐标拾取器时,一定要选择各个对应的工具,导航等牵涉到坐标的地方一定要注意。 5、腾讯、百度和高德地图开发比较 对于不同的厂家地图的使用,一般都有“先入为主” 的刻板印象,也有甲方原因的客观要求。 对比项 腾讯地图 百度地图 高德地图 功能 标注、信息框、覆盖物、计算距离、轨迹、导航等常用功能 同前 同前 坐标 火星坐标 BD-09坐标 火星坐标 坐标结构 (39.914850, 116.403765) (116.4035,39.915) (116.39, 39.9) 语法结构 同高德 百度自有语法 同腾讯 开发文档 相对集中 百度地图开发平台已升级到3.0版本,文档多,类库多 相对集中 延伸 数据可视化API服务 同前 同前 总结 本次使用版本: JavaScript API 2.0版本,目前我们提供的JavaScript API GL版本,功能更炫酷齐全,大家可以尝试接入使用。 作者:漏刻有时 链接:https://lockdatav.blog.csdn.net/article/details/113412823 来源:CSDN 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2021-04-09 - 【汇总】wx.getUserProfile 改造常见问题
书接上文 1、如何做版本兼容? 我在项目中使用的是wx.canIUse('getUserProfile')判断getUserProfile API 是否可以使用(切换版本库2.10以下可以模拟旧场景),如果有其他好方法,欢迎在评论区指出。 2、问什么改造过程中遇到了报错?'getUserProfile:fail can only be invoked by user TAP gesture' 一般由于直接使用了这种写法。 应该把wx.login和wx.getUserProfile分开调用,(建议wxlogin获取的code单独保存,每用一次单独刷新一次(code5分钟有效)),据说反着写也行,就是getUserProfile的success 里再调wx.login。 3、授权弹窗没有弹出? 检查下wx.getUserProfile 中的desc字段是否填写(desc为必填,官方意思后续可以展示在弹窗内)。 ⚠️ wx.getUserProfile 调用必须要在catchtap 、bindtap、showmodal 里绑定方法,依旧需要用户主动触发。 手写不易,麻烦乡亲们点个赞,我好完成主人的任务🤓。
2021-04-09 - 安卓在scroll-view中使用filter: blur()导致border-radius失效的解决方法
这是一个比较偏的用法,踩了半天坑,最后很蹊跷地解决了,记录下来希望能给有需要的人提供帮助。 首先,如果你正好在用scroll-view, 然后你也正好在每个item里用上了css滤镜filter(我用的是模糊blur), 然后你也正好同时需要用上圆角border-radius, 于是你会发现在安卓下圆角失效了。。。 最后建议你给item加个transform: translate3d(0, 0, 0); 然后,这个bug就奇迹般地解决了。。。 以上!
2021-04-09 - 小程序有什么办法完美实现,上滑隐藏导航栏,下拉显示导航栏
类似这种 [图片]
2018-08-20 - [拆弹时刻]4月13日前更新wx.getUserInfo和getUserProfile授权获取问题的解决方案
[图片] 论坛里有不少人疑惑新版的getUserProfile是不是已经上,同时发现开发环境中原有的老方法已经不支持了,这里我为大家集中解决下疑惑~ 1、线上是否已经不支持wx.getUserInfo老方法了? 支持!目前,根据官方文档说明:在4月13日前发布的,线上环境2.16.0基础库以下已经不支持老版方法。(4月8日更新,怀疑官方已提前发布) [图片] 本人今天4月6日10点半线上支持老方法,但是4月8日发布2.16.0以下低版本已经去掉弹窗授权了。请大家尽快更新发布新版方法 2、哪些环境已经是新方法了? 开发环境(包括但不限于IDE工具,真机调试),微信后台提供的体验版环境,且已不支持老办法。 3、新老两种方法是否并行? 线上环境:目前并行(4月13日前),但getUserProfile新方法 只在2.10.4以上版本支持。 开发环境和体验版:不并行,不支持左右横跳哈。 4、如何解决兼容性适配? 上才艺啦,呸,上代码。 先来看下原本代码的授权逻辑 [代码]//老的逻辑 wx.getSetting({ async success(res) { console.log(res.authSetting); //判断小程序用户是否授权 if (res.authSetting['scope.userInfo']) { //已授权 } else { //未授权情况 } } }) [代码] 之前主要通过wx.getSetting的方法来判断,而现在重大的改变是老方法getUserInfo不再弹窗,就算改成getUserProfile弹窗授权,新方法中getSetting中scope.userInfo 这个值并没有返回(这里跟文档有些出入,不知道官方后面会不会修正) [图片] [图片] 同时,这里需要做兼容判断,把获取到内容存在数据库中,避免反复弹窗骚扰用户。 [代码]//根据官方文档 做了一些修改 Page({ data: { userInfo: {}, hasUserInfo: false, canIUseGetUserProfile: false, }, onLoad() { //先请求自定义接口,获取上次存的useInfo wx.request({ url: 'test.api', //仅为示例,并非真实的接口地址 data: {}, success (res) { console.log(res.useInfo) //判断之前是否已获取并存储过用户信息 if(res.useInfo){ }else { //这里不要使用 wx.canIuse来判断,避免一些适配问题 if (wx.getUserProfile) { //直接使用官方推荐的方法 this.setData({ canIUseGetUserProfile: true }) } } } }) }, getUserProfile(e) { // 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认 // 开发者妥善保管用户快速填写的头像昵称,避免重复弹窗 wx.getUserProfile({ desc: '需要你的信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写 success: (res) => { //这里需要将获取的 res.userInfo 存起来,你可以存在数据库,也可以存在local storage里 //wx.request...请求接口 this.setData({ userInfo: res.userInfo, hasUserInfo: true }) } }) }, getUserInfo(e) { // 不推荐使用getUserInfo获取用户信息,预计自2021年4月13日起,getUserInfo将不再弹出弹窗,并直接返回匿名的用户个人信息 this.setData({ userInfo: e.detail.userInfo, hasUserInfo: true }) }, }) [代码] [代码]<!-- html部分 ---> <block wx:if="{{!hasUserInfo}}"> <button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button> <button wx:else open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 获取头像昵称 </button> </block> <block wx:else> <image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname">{{userInfo.nickName}}</text> </block> [代码] [图片] 5、如何更新用户信息? 首先,目前的规则如果不弹窗,肯定是无法每次拿到用户的最新信息,为了避免每次弹窗请求授权骚扰用户,所以最好根据产品规划,定期获取用户的信息(看心情)。 6、官方接口文档 https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801 觉得有用,请点个赞哦,让我继续分享更有动力~
2021-04-08 - 小程序防止重复点击或重复触发事件
可设置全局变量和全局函数,直接使用app唯一实例调用,方便快捷。 在app.js下 App({ globalData: { PageActive: true }, preventActive (fn) { const self = this if (this.globalData.PageActive) { this.globalData.PageActive = false if (fn) fn() setTimeout(() => { self.globalData.PageActive = true }, 1500); //设置该时间内重复触发只执行第一次,单位ms,按实际设置 } else { console.log('重复点击或触发') } } }) 其他page下调用 index.wxml <button bindtap="tap">点击</button> index.js Page({ tap (e) { getApp().preventActive(()=>{ //code... }) } })
2021-01-12 - 程序员的万圣节-开源云开发小程序-丧尸头像
3万圣节马上就到啦,有没有想好今年的万圣节干点啥?幽默的程序员可不会这样普通的过节,接下来带你们看看,程序员写的万圣节小程序-丧尸头像。 名字听着有些可怕,但是功能很搞怪,适合万圣节主题,话不多说上图 [图片] 大家看出来了吧,左边是我,右边是生成的丧尸头像,好吓人。 下面给大家解析一下实现效果,首先我们要做的就是图片安全识别,不能上传违规的头像哦~ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { // console.log(event, "1111111111") if (event.type == 'imgSecCheck') { return imgSecCheck(event); } else if (event.type == 'msgSecCheck') { return msgSecCheck(event); } else { return ''; } } // 图片内容检测 async function imgSecCheck(event) { try { const res = await cloud.downloadFile({ fileID: event.value, }) const imgResult = await cloud.openapi.security.imgSecCheck({ media: { header: { 'Content-Type': 'application/octet-stream' }, contentType: 'image/png', value: res.fileContent } }) return imgResult; } catch (err) { return err; } } 然后我们再上传图片时调用图片安全检测 // 内容安全检测(图片)。判断图片是否合法,不含有色情,等内容。 function imgSecCheck(imgUrl) { wx.showLoading({ title: '检测图片中', }) return new Promise((resolve, reject)=>{ wx.cloud.callFunction({ name: 'contentCheck', data: { value: imgUrl, type:"imgSecCheck" }, success: res => { wx.hideLoading(); console.log(res, '检查结果') if (res.result.errCode == 0) { // 没问题 resolve(TIPS.SUCCESS); }else if (res.result.errCode == 87014) { wx.showToast({ title: '图片含有敏感违法内容!', icon: 'none' }); // 违法删除图片 wx.cloud.deleteFile({ fileList: [imgUrl] }).then(resu => { console.log(resu,'删除图片') }) }else{ TIPS.error(res) } }, fail: res => { console.log(res, '报错结果') wx.hideLoading(); TIPS.error(res) } }) }) } 上传图片变异接口调用 wx.uploadFile({ url: 'https://deepgrave-image-processor-no7pxf7mmq-uc.a.run.app/transform', filePath, name: 'image', header: { 'Content-Type': 'multipart/form-data' }, formData: { method: 'POST' //请求方式 }, success(res) { const data = res.data //do something wx.hideLoading() if (data == 'No face found') { return wx.showToast({ title: '未检测到人物图像', icon: 'none', duration: 2500 }) } _this.setData({ zombie: data }) }, fail: () => { wx.hideLoading() } }) 到此结果就出来了,功能很简单,玩儿法很特别。下面给大家上一个体验码: [图片] 开源链接:https://citizenfour.coding.net/public/zombie-head/zombie-head/git/files 作者:小码农
2020-11-02 - 云调用能力—客服消息
在前面的章节,我们已经在小程序端将 button 组件 open-type 的值设置为 contact ,点击 button 就可以进入客服消息。不过这个客服消息使用的是官方的后台,没法进行深度的定制,我们可以使用云开发作为后台来自定义客服消息来实现快捷回复、添加常用回答等功能。 如果是使用传统的开发方式,需要填写服务器地址(URL)、令牌(Token) 和 消息加密密钥(EncodingAESKey)等信息,然后结合将 token、timestamp、nonce 三个参数进行字典序排序、拼接、并进行 sha1 加密,然后将加密后的字符串与 signature 对比来验证消息的确来自微信服务器,之后再来进行接收消息和事件的处理,可谓十分繁琐,而使用云开发相对简单很多。 13.8.1 客服消息的配置与说明使用开发者工具新建一个云函数,比如 customer,在 config.json 里,设置以下权限后部署上传到服务端。 { "permissions": { "openapi": [ "customerServiceMessage.send", "customerServiceMessage.getTempMedia", "customerServiceMessage.setTyping", "customerServiceMessage.uploadTempMedia" ] } } 然后再打开云开发控制台,点击右上角的设置,选择全局设置,开启云函数接收消息推送,添加消息推送配置。为了学习方便我们将所有的消息类型都指定推送到 customer 云函数里。 text,文本消息image,图片消息miniprogram,小程序卡片event,事件类型 user_enter_tempsession,进入客服消息时就会触发以上有四种消息类型,但是发送客服消息的 customerServiceMessage.send 的 msgtype 属性的合法值有 text、image、link(图文链接消息)、miniprogrampage 四种,也就是我们还可以发图文链接消息。 13.8.2 自动回复文本消息和链接1、自动回复文本消息使用开发者工具新建一个页面,比如 customer,然后在 customer.wxml 里输入以下按钮, 进入客服button> 当用户通过 button 进入到客服消息之后,在聊天界面回复信息,就能触发设置好的 customer 云函数,比如下面的例子就是当用户发一条消息(包括表情)到客服消息会话界面,云函数就会给调用 customerServiceMessage.send 接口给用户回复两条文本消息(一次性可以回复多条),内容分别为[代码]等候您多时啦[代码]和[代码]欢迎关注云开发技术训练营[代码],一个云函数里也是可以多次调用接口的: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); try { const result = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "text", text: { content: "等候您多时啦", }, }); const result2 = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "text", text: { content: "欢迎关注云开发技术训练营", }, }); return event; } catch (err) { console.log(err); return err; } }; 发送文本消息时,支持插入跳小程序的文字链接的,比如我们把上面的文本消息改为以下代码: content: '欢迎浏览点击跳小程序a>'; data-miniprogram-appid 项,填写小程序 appid,则表示该链接跳小程序;data-miniprogram-path 项,填写小程序路径,路径与 app.json 中保持一致,可带参数;对于不支持 data-miniprogram-appid 项的客户端版本,如果有 herf 项,则仍然保持跳 href 中的网页链接;data-miniprogram-appid 对应的小程序必须与公众号有绑定关系。 2、自动回复链接我们还可以给用户回复链接,我们可以把 customer 云函数修改为以下代码,当用户向微信聊天对话界面发送一条消息时,就会回复给用户一个链接,这个链接可以是外部链接哦。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); try { const result = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "link", link: { title: "快来加入云开发技术训练营", description: "零基础也能在10天内学会开发一个小程序", url: "https://cloud.tencent.com/", thumbUrl: "https://tcb-1251009918.cos.ap-guangzhou.myqcloud.com/love.png", }, }); return event; } catch (err) { console.log(err); return err; } }; 3、根据关键词来回复用户将上面的云函数部署之后,当用户向客服消息的聊天会话里输入内容时,不管用户发送的是什么内容,云函数都会回给用户相同的内容,这未免有点过于死板,客服消息能否根据用户发送的关键词回复用户不同的内容呢?要做到这一点我们需要能够获取到用户发送的内容。 我们可以留意云开发控制台云函数日志里看到,customer 云函数返回的 event 对象里的 Content 属性就会记录用户发到聊天会话里的内容: {"Content":"请问怎么加入云开发训练营", "CreateTime":1582877109, "FromUserName":"oUL-mu...XbuEDsn8", "MsgId":22661351901594052, "MsgType":"text", "ToUserName":"gh_b2bbe22535e4", "userInfo":{"appId":"wxda99ae4531b57046","openId":"oUL-m5FuRmuVmxvbYOGuXbuEDsn8"}} 由于 Content 是字符串,那这个关键词既可以是非常精准的,比如“训练营”,或“云开发训练营”,还可以是非常模糊的“请问怎么加入云开发训练营”,我们只需要对字符串进行正则匹配处理即可,比如当用户只要发的内容包含“训练营”,就会收到链接: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); const keyword = event.Content; try { if (keyword.search(/训练营/i) != -1) { const result = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "link", link: { title: "快来加入云开发技术训练营", description: "零基础也能在10天内学会开发一个小程序", url: "https://cloud.tencent.com/", thumbUrl: "https://tcb-1251009918.cos.ap-guangzhou.myqcloud.com/love.png", }, }); } return event; } catch (err) { console.log(err); return err; } }; 在前面的案例里,我们都是使用[代码]touser: wxContext.OPENID,[代码], 13.8.2 自动触发 event 事件要触发 event 事件,我们可以将 customer.wxml 的按钮改为如下代码,这里的 session-from 是用户从该按钮进入客服消息会话界面时,开发者将收到带上本参数的事件推送,可用于区分用户进入客服会话的来源。 进入客服button> 由于我们开启了 event 类型的客服消息,事件类型的值为 user_enter_tempsession,当用户点击 button 进入客服时,就会触发云函数,不用用户发消息就能触发,同时我们返回 event 对象. const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); try { const result = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "text", text: { content: "欢迎来到等候您多时啦", }, }); return event; } catch (err) { console.log(err); return err; } }; 我们可以去云开发控制台查看返回的 event 对象 {"CreateTime":1582876587, "Event":"user_enter_tempsession", "FromUserName":"oUL-m5F...8", "MsgType":"event", "SessionFrom":"文章详情的客服按钮", "ToUserName":"gh_b2bbe22535e4", "userInfo":{"appId":"wxda9...57046", "openId":"oUL-m5FuRmuVmx...sn8"}} 在云函数端,我们是可以通过 event.SessionFrom 来获取到用户到底是点击了哪个按钮从而进入客服对话的,也可以根据用户进入客服会话的来源不同,给用户推送不同类型,比如我们可以给 session-from 的值设置为“训练营”,当用户进入客服消息会话就能推送相关的信息给到用户。 还有一点就是,bindcontact 是给客服按钮绑定了了一个事件处理函数,这里为 onCustomerServiceButtonClick,通过事件处理函数我们可以在小程序端做很多事情,比如记录用户点击了多少次带有标记(比如 session-from 的值设置为“训练营”)的客服消息的按钮等功能。 13.8.3 自动回复图片要在客服消息里给用户回复图片,这个图片的来源只能是来源于微信服务器,我们需要先使用 customerServiceMessage.uploadTempMedia,把图片文件上传到微信服务器,获取到 mediaId(有点类似于微信服务器的 fileID),然后才能在客服消息里使用。 在 customer 云函数的 index.js 里输入以下代码并部署上线,我们将获取到的 mediaId 使用 cloud.openapi.customerServiceMessage.send 发给用户: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); try { //我们通常会将云存储的图片作为客服消息媒体文件的素材 const fileID = "cloud://xly-xrlur.786c-xly-xrlur-1300446086/1572315793628-366.png"; //uploadTempMedia的图片类型为Buffer,而从存储下载的图片格式也是Buffer const res = await cloud.downloadFile({ fileID: fileID, }); const Buffer = res.fileContent; const result = await cloud.openapi.customerServiceMessage.uploadTempMedia({ type: "image", media: { contentType: "image/png", value: Buffer, }, }); console.log(result.mediaId); const mediaId = result.mediaId; const wxContext = cloud.getWXContext(); const result2 = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "image", image: { mediaId: mediaId, }, }); return event; } catch (err) { console.log(err); return err; } }; 客服消息还能给用户回复小程序消息卡片,以及客服当前的输入状态给用户(使用 customerServiceMessage.setTyping 接口)。
2021-09-10 - 借助云开发实现免登录资源导航小程序
作者:布道师 鱼皮 用云开发实现一个资源导航小程序,要实现的功能很简单:所有用户都可以查看和推荐资源,被推荐的资源的相关信息会以清单的方式显示。 [图片] 主要目的是通过实战,帮助大家快速了解下 小程序开发流程 和 云开发技术,学习更高效的小程序开发方式。 技术选型首先选择小程序开发技术。开发小程序的过程和开发网站类似,都是要写前端(界面交互)和后端(请求处理逻辑)代码。 前端前端方面我选择用 Taro 框架 + Taro UI 开发。Taro 是一个基于 React 的跨端开发框架,支持写一套代码,自动生成微信小程序、H5、APP 等应用,再加上框架为很多复杂的功能提供了函数封装,可以大大提升开发效率。而 Taro UI 是基于 Taro 的 UI 库,提供了很多现成的组件,比如图片上传、选择器等,可以满足常见的开发需求。 [图片] 后端后端就简单了,传统的方式是使用编程语言提供的后端开发框架,比如 Java 的 SpringBoot、PHP 的 Laravel、Python 的 Django 等,但往往需要自己搭建服务器、数据库、日志、监控、运维等等,对于只会前端或者想要快速开发小程序的同学来讲,简直就是噩梦! 因此我选择更高效便捷的方式,腾讯小程序云开发! 什么是云开发?小程序云开发是微信团队联合腾讯云推出的专业的小程序开发服务,帮助大家快速开发小程序、小游戏、公众号网页等,并且原生打通微信开放能力。 云开发的优势有哪些呢? 1.开发者无需搭建后端服务器,只需使用平台提供的各项能力(比如云数据库、云存储、音视频、AI 等),即可快速开发业务。 2. 安全易接入:无需管理证书、签名、秘钥,直接调用微信 API 。复用微信私有协议及链路,保证业务安全性。 3. 多端复用:支持环境共享,一个后端环境可开发多个小程序、公众号、网页等,便捷复用业务代码与数据。 4. 不限开发语言和框架:开发者可以使用任意语言和框架进行代码开发,构建为容器后,快速将其托管至云开发。 5. 按量计费,成本更低,支持自动扩缩容 6. 扩展能力:支持一键部署静态网站,并能用云 CMS 管理数据内容 其中,最吸引我的就是云开发的高效便捷,不用自己搞服务器、搭数据库,也不用处理和微信对接的复杂逻辑,只需要专注于实现功能本身即可,而且可以直接用云开发 SDK 提供的各种函数,开发效率拉满! 比如查询数据,几行代码搞定! [图片] 应用开发下面来开发小程序,包含初始项目搭建、前端页面开发、接入云开发等步骤。 项目搭建首先我们参照 Taro 框架官方文档的快速开始部分,安装 Taro 命令行工具,并初始化一个小程序应用。 [图片] 注意初始化时会让你选择模板,此处选择云开发即可,Taro 会自动生成包含云开发的示例代码,目录结构如下: [图片] 前端开发我们总共要创建两个页面,资源列表页和推荐资源页,需要用到的组件有列表、表单、输入框、按钮、图像上传等。 Taro UI 支持以上所有组件,按照 Taro UI 的官方文档接入,复制组件代码到页面中修改即可,很快就能开发出这两个页面。 资源列表页的示例代码如下: {resourcesView} navTo(xx)}> 可以打开微信开发者工具查看页面效果: [图片] 页面开发完成后,我们来搭建后端服务,使得用户可以通过界面插入和读取数据。 接入云开发区别于自己搭建后端服务,使用云开发会更简洁快速,直接在微信开发者工具中点击云开发,开通环境即可,每位用户都可以享有一定数量的免费环境! [图片] 在云开发界面中,可以对云数据库、云存储、云函数等资源进行监控和管理。 我们可以在云数据库中创建一张 资源表,用于读写资源数据。云开发控制台支持可视化的数据库管理,比如记录、索引、数据权限等,非常方便! [图片] 每个环境都有唯一的 id,用于区分,可以在前端引入云开发 SDK,并传入环境 id 来初始化。 前端用 Taro 的话,可以用它封装好的 [代码]<span>init</span>[代码] 方法: Taro.cloud.init({ // 环境 id env: 'xx', }) 然后,就可以在前端 直接调用 云开发提供的操作数据库的接口,比如插入数据、查询数据,不用自己开发后台了! 比如插入数据: const db = Taro.cloud.database(); // 添加数据到 resource 表 db.collection('resource').add({ data }).then(res => { // 成功 return res; }).catch(err => { // 失败 console.error(err); }); 在推荐资源时需要让用户上传图片,以前我们需要自己找地儿存放,现在可以在前端 直接调用云存储,几行代码搞定: // 上传文件 const res = await Taro.cloud.uploadFile({ cloudPath: '上传到云存储的位置', // 要上传图片的本地路径 filePath: pictureUrl, }) // 获取图片 id,可下载或直接展示 picture = res.fileID; 可以在云开发控制台管理云存储中的文件、配置权限、缓存等: [图片] 如果云开发默认提供的接口不能满足需求,那可以自己写后台接口,作为一个云函数部署到腾讯云上。然后在前端请求即可,和自己开发后端类似。 比如部署一个登录函数,可以获取用户在小程序中的唯一 id,在控制台中还能看到函数的调用日志、管理权限等。 [图片] 实现无登陆调用按照上面的流程开发完后,在微信开发者工具中,能够顺利地推荐和展示资源。但是如果将这个小程序上线并分享给其他用户,就会出现权限问题,所有功能都会失效! [图片] 这是因为云开发为了保证资源的安全性、灵活控制资源调用权限,制定了安全规则,默认不允许未登录用户访问。 [图片] 假如我们把小程序分享到朋友圈,必须要朋友们登录才能查看资源列表,那这用户体验就太差了,所以下面我们要实现无登录调用。 小程序云开发考虑到了种种场景,因此提供了 未登录模式。 在未登录模式中,不存在用户的登录态,应用场景有: 1.单页模式:小程序/小游戏分享到朋友圈被打开时 2. Web 未登录模式:没有登录的 Web 环境中 该模式默认关闭,需要在 “云控制台 - 设置 - 权限设置” 中手动为云环境开启允许未登录访问。 [图片] [图片] 一旦开启了未登录模式,客户端(前端)的权限控制 必须使用安全规则,即云函数、数据库和文件存储的访问都必须通过安全规则。 因此,除了在控制台开启允许未登录访问云环境外,还必须在云数据库、云存储和云函数的权限设置中分别选择安全规则并配置。 安全规则有一套自己的语法,以云数据库为例,选择自定义安全规则,查看原本的规则: [图片] 在上述规则中,[代码]<span>read</span>[代码]、[代码]<span>write</span>[代码] 分别代表读写权限,[代码]<span>doc</span>[代码] 表示当前的一条数据,[代码]<span>auth</span>[代码] 表示当前登录的用户,表达式为 [代码]<span>true</span>[代码] 时允许访问,即当前登录的用户必须是该条数据的创建者才能读写。 未登录用户访问时,安全规则的 auth 字段为空,如果要允许所有用户读写所有资源,可以直接将表达式值设置为 true: [图片] 再修改下云存储的安全规则,原规则如下: [图片] 上述规则中,[代码]<span>resource</span>[代码] 表示一个资源,将表达式改为 true,则允许所有用户读写存储的所有文件! 同理,也要修改云函数的安全规则,可以为不同云函数设置不同规则,比如 [代码]<span>login</span>[代码] 函数允许所有用户访问,而其他函数仅允许已登录用户访问: [图片] 安全规则非常灵活,合理运用,可以在满足资源调用需求的同时,增加资源的安全性,为应用安全保驾护航。 最后总结,通过本文,我们了解了小程序的开发过程,以及小程序云开发的用法、无登录资源调用的方式。相对于代码,思路更重要,也强烈建议大家试一试云开发,感受高效,轻松地做出自己的应用! 产品介绍云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2021-06-10 - 校友会小程序开发笔记二十六:自定义组件的设计与实现
功能说明 一轮密集的校友会小程序开发下来,发现有很多东西是复用的,可以抽象出来, 正好小程序提供了这种抽象的方法和能力 于是我将校友录小程序页面内的功能模块抽象成自定义组件, 以便在不同的校友录小程序页面中重复使用; 同时还有一个更大的好处:将复杂的校友会小程序页面拆分成多个低耦合的模块,有助于代码维护。 自定义校友录小程序组件在使用时与基础组件非常相似。 一些需要注意的细节:因为 WXML 节点标签名只能是小写字母、中划线和下划线的组合,所以自定义校友录小程序组件的标签名也只能包含这些字符。自定义校友录小程序组件也是可以引用自定义组件的,引用方法类似于页面引用自定义组件的方式(使用 [代码]usingComponents[代码] 字段)。自定义校友录小程序组件和页面所在项目根目录名不能以“wx-”为前缀,否则会报错。注意,是否在校友录小程序页面文件中使用 [代码]usingComponents[代码] 会使得页面的 [代码]this[代码] 对象的原型稍有差异,包括: 使用 [代码]usingComponents[代码] 页面的原型与不使用时不一致,即 [代码]Object.getPrototypeOf(this)[代码] 结果不同。使用 [代码]usingComponents[代码] 时会多一些方法,如 [代码]selectComponent[代码] 。出于性能考虑,使用 [代码]usingComponents[代码] 时, [代码]setData[代码] 内容不会被直接深复制,即 [代码]this.setData({ field: obj })[代码] 后 [代码]this.data.field === obj[代码] 。(深复制会在这个值被组件间传递时发生。)如果校友录小程序页面比较复杂,新增或删除 [代码]usingComponents[代码] 定义段时建议重新测试一下。 总结下来我们有校友评论组件,通用校友列表组件,校友图像上传组件,校友详情页面组件,Footer组件,校友会后台侧边组件等大的组件模块 UI设计 [图片] [图片] [图片] 前端代码逻辑 [图片] 后端代码逻辑[图片] 作者交流微信:cclinux0730 项目代码GIT: https://gitee.com/cclinux2/cc-alumni 分类: 小程序开发心得笔记, 小程序云开发, 校友会小程序开发笔记
2021-04-01 - 数据预拉取支持小程序云开发么?
小程序的云开发,是否支持数据预拉取,还是只支持第三方服务器
2020-09-27 - 校友会小程序开发笔记二十四:不同学校校友会动态换肤方案设计
功能说明 在开发校友会小程序的时候,只需要开发一套模板, 但是可能不同学校校友会小程序需要做 [代码]定制化配色方案[代码],很多学校都有自己的VI(视觉识别系统),校友会小程序也是延续该VI风格 比如上海交大红色主体的 VI: [图片] 中国农业大学绿色VI[图片] 北京理工大学深绿色VI[图片] 北京大学深红色VI[图片] [图片] 也就是说,同一个校友会校友录小程序个体需要对页面的元素(比如:按钮,字体等)进行不同的配色设置, 一般来说,有两种解决方案可以解决小程序动态换肤的需求: 小程序内置几种主题样式,通过更换类名来实现动态改变校友会小程序页面的元素色值;后端接口返回色值字段,前端通过 [代码]内联[代码] 方式对页面元素进行色值设置。 当然,每种方案都有一些问题,问题如下: 方案1较为死板,每次更改主题样式都需要发版小程序,如果主题样式变动不大,可以考虑这种;方案2对于前端的改动很大,[代码]内联[代码] 也就是通过 [代码]style[代码] 的方式内嵌到[代码]wxml[代码] 代码中,代码的阅读性会变差,但是可以解决主题样式变动不用发版校友会小程序的问题。 UI设计[图片][图片][图片][图片] 数据库设计[图片] 后台管理[图片] [图片] 作者交流微信:cclinux0730 项目代码GIT: https://gitee.com/cclinux2/cc-alumni
2021-03-31 - uniapp组件-微头条动态卡片,类似朋友圈,qq说说,微头条,实现多张图片自适应排列布局
# 微头条动态卡片 ## 说明 本组件仿写今日头条的微头条,适用于朋友圈,朋友圈动态,空间说说,微头条等。 组件主要包含三部分 1.头像,名称,发布时间 2.文字内容,图片内容 3.对微头条的操作样式,包含数字显示,高亮显示。 图片说明:本组件根据可用屏幕高宽度自动排列布局,可适应各种屏幕,多张图片布局和。 ## 基本用法 组件引用了uniapp部分组件,所以得先导入uniapp组件,如果用不到该功能可不导入 [Grid 宫格](https://ext.dcloud.net.cn/plugin?id=27) [Icons 图标](https://ext.dcloud.net.cn/plugin?id=28) 在template中使用组件 ```html <Dynamic v-for="(item,index) in list" key="id" :imgList="item.imgList" :avatar="item.avatar" :name="item.name" :publishTime="item.publishTime" :content="item.content" :isLike="item.isLike" :isGiveReward="item.isGiveReward" :likeNumber="item.likeNumber" :giveRewardNumber="item.giveRewardNumber" :chatNumber="item.chatNumber" @clickDynamic="clickDynamic(index)" @clickUser="clickUser(item.id)" @clickFocus="clickFocus(index)" @clickThumbsup="clickThumbsup(item.id)" @clickGiveReward="clickGiveReward(item.id)" @clickChat="clickChat(item.id)"> </Dynamic> ``` ```javascript import Dynamic from '../../components/Dynamic/Dynamic.vue' export default { components: { Dynamic }, data() { return { title: 'Hello', list:[ { id:1, avatar:'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1950846641,3729028697&fm=26&gp=0.jpg', name:'小新', publishTime:1617086756, content:'中国外交官这样讽加拿大总理,算不算骂?该不该骂?', imgList:[ 'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1976832114,2993359804&fm=26&gp=0.jpg', 'https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2369680151,826506100&fm=26&gp=0.jpg', ], isLike:true, isGiveReward:true, likeNumber:2, giveRewardNumber:2, chatNumber:2, isFocusOn:true, }, { id:2, avatar:'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2291332875,175289127&fm=26&gp=0.jpg', name:'小白', publishTime:1617036656, content:' 足不出户享国内核医学领域顶级专家云诊断,“中山-联影”分子影像远程互联融合创新中心揭牌 ', imgList:[ 'https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2369680151,826506100&fm=26&gp=0.jpg', ], isLike:false, isGiveReward:false, likeNumber:0, giveRewardNumber:0, chatNumber:2, isFocusOn:false, }, { id:3, avatar:'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1950846641,3729028697&fm=26&gp=0.jpg', name:'小新', publishTime:1617046556, content:' 外交部:一小撮国家和个人编造所谓新疆“强迫劳动”的故事,其心何其毒也! ', imgList:[ 'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1976832114,2993359804&fm=26&gp=0.jpg', 'https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2369680151,826506100&fm=26&gp=0.jpg', 'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1976832114,2993359804&fm=26&gp=0.jpg', 'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1976832114,2993359804&fm=26&gp=0.jpg', 'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1976832114,2993359804&fm=26&gp=0.jpg', ], isLike:true, isGiveReward:false, likeNumber:4, giveRewardNumber:22, chatNumber:52, }, { id:4, avatar:'https://dss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3717120934,3932520698&fm=26&gp=0.jpg', name:'小龙马', publishTime:1616086456, content:'DCloud有800万开发者,uni统计手机端月活12亿。是开发者数量和案例最丰富的多端开发框架。 欢迎知名开发商提交案例或接入uni统计。 新冠抗疫专区案例 uni-app助力', imgList:[ 'https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2369680151,826506100&fm=26&gp=0.jpg', 'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1976832114,2993359804&fm=26&gp=0.jpg', 'https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2369680151,826506100&fm=26&gp=0.jpg', ], isLike:true, isGiveReward:false, likeNumber:25, giveRewardNumber:0, chatNumber:7, }, { id:5, avatar:'https://dss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2590128318,632998727&fm=26&gp=0.jpg', name:'风清扬', publishTime:1607086356, content:'划个水', imgList:[ 'https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2369680151,826506100&fm=26&gp=0.jpg', 'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1976832114,2993359804&fm=26&gp=0.jpg', 'https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2369680151,826506100&fm=26&gp=0.jpg', 'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1976832114,2993359804&fm=26&gp=0.jpg', ], isLike:true, isGiveReward:true, likeNumber:3, giveRewardNumber:2, chatNumber:2, } ] } }, methods:{ clickDynamic(e){ console.log('childDynamic'); }, // 点击用户信息 clickUser(e){ console.log(e); console.log('childUser'); }, // 点击关注 clickFocus(e){ this.list[e].isFocusOn = this.list[e].isFocusOn ? false : true; console.log(e); console.log('childUser'); }, // 点赞 clickThumbsup(e){ console.log(e); console.log('childThumbsup'); }, // 点击打赏 clickGiveReward(e){ console.log(e); console.log('clickGiveReward'); }, // 点击聊天 clickChat(e){ console.log(e); console.log('clickChat'); } } } ``` ## API **属性说明** |属性名|类型|默认值|说明| :---:|:----:|:---:|:--:| |avatar|String|null|头像路径| | name | String | null | 名称 | | publishTime | Number | null | 发布时间 | | isFocusOn | Boolean | null | 是否已关注。 | | content | String | null | 内容 | | imgList | Array | null | 显示的图片路径列表 | | isLike | Boolean | null | 是否已点赞,已点赞会高亮显示 | | isGiveReward | Boolean | null | 是否已打赏,已打赏会高亮显示 | | likeNumber | Boolean | null | 点赞数 | | giveRewardNumber | Number | null | 打赏数 | | chatNumber | Number | null | 评论数 | | chatNumber | Number | null | 评论数 | | userNoShow | Boolean | null | 是否不显示用户信息。包括头像,名称,发布时间 | | operateNoShow | Boolean | null | 是否不显示操作信息。| **事件说明** | 事件名 | 说明 |返回值 | | :--- : | :--: | :----: | | @clickDynamic | 点击动态触发 | 按传参原值返回 | | @clickUser | 点击用户信息触发。包括头像,名称| 按传参原值返回 | | @clickFocus | 点击关注触发 | 按传参原值返回 | | @clickThumbsup | 点赞触发 | 按传参原值返回 | | @clickGiveReward | 点击打赏触发 | 按传参原值返回 | | @clickChat | 点击评论触发 | null | 按传参原值返回 | ## 源文件 [微头条卡片](https://ext.dcloud.net.cn/plugin?id=4583) 补充:有任何问题联系wx:chwlzgz 。在线求打扰~
2021-03-30 - 04.轻松对接 GetUserProfile,完美解决老版本兼容问题
背景:https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801?blockType=1 流程图 [图片] userProfile组件 userProfile/index.wxml <view> <!-- getUserProfile --> <button wx:if="{{canIUseGetUserProfile}}" bindtap="handleGetUserProfile"> <text>获取头像昵称</text> </button> <!-- getUserInfo --> <block wx:else> <!-- getUserInfo auto --> <block wx:if="{{!canAutoGetUserInfo}}"> <button open-type="getUserInfo" bindgetuserinfo="handleGetUserInfo"> <text>获取头像昵称</text> </button> </block> <!-- getUserInfo manual --> <block wx:else> <button bindtap="handleGetUserInfo"> <text>获取头像昵称</text> </button> </block> </block> </view> userProfile/index.js Component({ properties: { scopeCfg: { type: Object, value: { USER_PROFILE: { order: 1, handleEvtCallBack: function () { }, }, } }, }, data: { canIUseGetUserProfile: false, canAutoGetUserInfo: false, }, lifetimes: { attached: function () { if (wx.getUserProfile) { this.setData({ canIUseGetUserProfile: true, }); } this.checkGetUserInfo(); } }, methods: { handleGetUserProfile: function () { var _this = this; wx.getUserProfile({ desc: '用于完善会员资料', success: function (res) { _this.handleEvtCallBack({ scope: 'USER_PROFILE', auth: true, data: res.userInfo, }); }, fail: function (err) { _this.handleEvtCallBack({ scope: 'USER_PROFILE', auth: false, data: err, }); } }); }, checkGetUserInfo: function () { var _this = this; wx.getSetting({ success: function (res) { if (res.authSetting['scope.userInfo']) { _this.setData({ canAutoGetUserInfo: true, }); } }, }); }, handleGetUserInfo: function () { var _this = this; wx.getUserInfo({ success: function (res) { _this.handleEvtCallBack({ scope: 'USER_PROFILE', auth: true, data: res.userInfo, }); }, fail: function (err) { _this.handleEvtCallBack({ scope: 'USER_PROFILE', auth: false, data: err, }); } }); }, handleEvtCallBack: function (param) { var scope = param.scope, auth = param.auth, data = param.data; var callBack = (this.data.scopeCfg["" + scope].handleEvtCallBack); callBack(auth, data); } }, }); userProfile/index.json { "component": true, "usingComponents": {} } 业务怎么使用 引入组件(index.json) { "usingComponents": { "user-profile": "../userProfile/index" } } 定义参数 Page({ data: { scopeCfg: { USER_PROFILE: { order: 1, handleEvtCallBack: function () { } } }, pageCfg: { useSlot: true, } }, onLoad: function () { var scopeCfg = this.data.scopeCfg; scopeCfg.USER_PROFILE.handleEvtCallBack = this.handleGetUserProfile; this.setData({ scopeCfg: scopeCfg, }); }, handleGetUserProfile: function (status, data) { console.log(status, data); } }); 使用组件 <view class="container"> <!-- 默认的授权样式 --> <user-profile scopeCfg="{{scopeCfg}}"></user-profile> </view>
2021-03-31 - 小程序联盟公测
各位微信开发者: 你们好。 为了更好的帮助小程序商家提高商品销量,微信官方提供的推广工具“小程序联盟”,于2021年3月1日开始公测。 小程序联盟具有“先成交后付费”的特点,商家在管理后台发布商品推广需求和佣金,佣金在推客(推广者)成功完成推广后才会结算。 功能简介与接入标准请参考下方内容。 一、功能简介 小程序联盟分别为商家和推客(推广者)提供了管理后台: 商家可在管理后台设置商品推广佣金,查看推广效果,具体说明请查看《商家端功能说明》;[图片] 2.推客可在管理后台挑选商品,获取推广素材,查看推广效果,提现佣金,具体说明请查看《推客端功能说明》; [图片] 二、接入要求 拥有商品,希望被推广的小程序商家可申请成为小程序联盟商家。没有货源,希望通过分享商品创造价值的推广者,可以申请成为小程序联盟推客。 具体接入条件如下: 1. 商家 满足以下条件之一,即可开通小程序联盟: 已开张的企业/个体工商户的小商店; 已有小程序并完成标准版交易组件接入。 接入指引,请参考《商家接入指引》。 企业/个体工商户为主体的小商店可直接前往PC端后台开通联盟功能。 [图片] 2. 推客: 支持企业/个体工商户主体接入,通过主体认证即可。 暂未向个人开放。 接入指引,请参考《推客接入指引》。
2021-03-03 - mina-lazy-image: 图片懒加载自定义组件
Github: https://github.com/alexayan/mina-lazy-image 功能 图片在视口中出现才进行加载显示,优化页面性能 使用方法 安装组件 [代码]npm install --save mina-lazy-image [代码] 在页面的 json 配置文件中添加 mina-lazy-image 使用此组件需要依赖小程序基础库 2.2.2 版本,同时依赖开发者工具的 npm 构建。具体详情可查阅官方 npm 文档。 [代码]{ "usingComponents": { "mina-lazy-image": "mina-lazy-image/index" } } [代码] WXML 文件中引用 mina-lazy-image [代码]<mina-lazy-image src="{{src}}" mode="widthFIx" image-class="custom-class-name"/> [代码] mina-lazy-image 的属性介绍如下: 字段名 类型 必填 描述 src String 是 图片链接 placeholder String 否 占位图片链接 mode String 否 请参考 image 组件 mode 属性 webp Number 否 请参考 image 组件 webp 属性 showMenuByLongpress Boolean 否 请参考 image 组件 show-menu-by-longpress 属性 styles String 否 设置图片样式 viewport Object 否 默认为 {bottom: 0},配置图片显示区域 mina-lazy-image 外部样式类 [代码]image-class[代码], [代码]image-container-class[代码]
2020-01-09 - 【一】从零实现商城多规格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 - 前端加载优化及实践
大家都知道产品体验的重要性,而其中最重要的就是加载速度,一个产品如果打开都很慢,可能也就没有后面更多的事情了。这篇文章是我最近项目中的一些加载优化总结,欢迎大家一起讨论交流。 内容包括: 性能指标及数据采集 性能分析方法 性能优化方法 性能优化具体实践 第一部分:性能指标及数据采集 要优化性能首先需要有一套用来评估性能的指标,这套指标应该是是可度量、可线上精确采集分析的。现在来一起看看如何选择性能指标吧。 1. 性能指标 加载的过程是一个用户的感知变化的过程。所以我们的页面性能指标也是要以用户感知为中心的。下面是google定义了几个以用户感知为中心的性能指标。 1.1 以用户感知为中心的性能指标 首先确定页面视觉的变化传递给用户的感知变化关键点: 感知点 说明 发生了吗? 浏览是否成功。 有用了吗? 是否有足够的内容呈现给用户。 可用了吗? 用户是否可以和页面交互了。 好用吗? 用户和应用交互是否流畅自然。 我这里讲的是加载优化,所以第四点暂时不讨论。下面是感知点相关的性能指标。 First paint(FP) and first contentful paint(FCP) FP: Webview跳转到应用的首次渲染时间。 FCP:Webview首次渲染内容的时间:文本,图像(包括背景图像),非白色画布或SVG。这是用户第一次消费内容的时间。 Chrome支持用Paint Timing API获取这两个值: [代码] performance.getEntriesByType("paint") [代码] First meaningful paint(FMP) 首次绘制有效内容的时间,用来表明这个应用是否绘制了有效内容。比如天气应用可以看到天气了,商品列表可以看到商品了。 Time to Interactive(TTI) 应用可交互时间,这时应用渲染完成且可以响应用户输入的时间。这种情况下JS已经加载完成且主线程处于空闲状态。 Speed index 速度指标:代表填充页面内容的速度。要想降低速度指标分数,您需要让加载速度从视觉上显得更快,也就是渐进式展示。 上面指标对应的感知点如下: 感知点 说明 发生了吗? FP/FCP 有用了吗? FMP 可用了吗? TTI Speed index是个整体效果指标所以没有对应上面的任何一个,但也同时对应任何一个。 对于实际项目中我们选取指标要便于采集,下面是针对我的实际项目(APP内的单页面应用)选取的性能指标。 1.2 实际项目选取的性能指标 Webview加载时间 反应Webview性能。这样就可以更真实的知道我们应用的加载情况。 页面下载时间 反应浏览成功时间。 应用启动时间 反应应用启动完成时间,这个时候页面初始化完成,是JS首次执行完成的时间,应用所需异步请求都已经发出去了。 首次有效绘制内容时间 已经有足够的内容呈现给用户,是首屏所需重要接口返回且DOM渲染完成的时间,这个时间由程序员自行判断。 应用加载完成时间 应用完整的呈现给了用户,这个时候页面中所有资源都已经下载好,包括图片等资源。 这里我们的性能指标确定了,下面看看这些数据怎么采集吧。 2. 数据采集 performance.timing为我们提供页面加载每个过程的精确时间,如下图: [图片] 是不是很完美,这足够了?还不够,我们还需要加上原生APP为我们提供的点击我们应用的时间和我们自己确定的FMP才够完美。 下面是每个指标的获取方法: 公用代码部分 [代码]let performance = window.performance || window.msPerformance || window.webkitPerformance; if (performance && performance.timing) { let t = performance.timing; let navigationStart = t.navigationStart; //跳转开始时间 let enterTime = ""; //app提供的用户点击应用的时间,需要和app沟通传递方式 //... 性能指标部分 } [代码] Webview加载时间 [代码] let webviewLoaded = navigationStart - enterTime; [代码] 注意:enterTime应该是客户端ms时间戳,不是服务器时间。 页面下载时间 [代码] let pageDownLoadedTime = t.responseEnd - navigationStart; [代码] 应用启动时间 [代码]let appStartTime = t.domContentLoadedEventStart - navigationStart; [代码] 首次有效绘制内容时间 这里我们需要在有效绘制后调用 [代码]window._fmpTime = +(new Date())[代码]获取当前时间戳。 [代码]let fmpTime = window._fmpTime - navigationStart; [代码] 应用加载完成时间 [代码]let domCompleteTime = t.domComplete - navigationStart; [代码] 最后在document load以后使用上面代码就可以收集到性能数据了,然后就可以上报给后台了。 [代码]if (document.readyState == 'complete') { _report(); } else { window.addEventListener("load", _report, false); } [代码] 这样就封装了一个简单性能数据采集上报组件,这是非常通用的可以用在类似项目中使用只要按照标准提供enterTime和window._fmpTime就可以。 3. 数据分析 有了上面的原始数据,我们需要一些统计方法来观察性能效果和变化趋势,所以我们选取下面一些统计指标。 平均值 注意在平均值计算的时候要设置一个取值范围比如:0~10s以防脏数据污染。 平均值的趋势用折线图展示: [图片] 分布占比 可以清晰的看到用户访问时间的分布,这样你就可以知道有多少用户是秒开的了。 分布占比可以使用折线图、堆积图、饼状图展示: [图片] [图片] [图片] 第二部分:性能分析方法 上面有了性能指标和性能数据,现在我们来学习一下性能分析的一些方法,这样我们才能知道性能到底哪里不行、为什么不行。 1. 影响性能的外部因素 分析性能最重要的一点要确定外部因素。经常会有这种情况,有人反应页面打开速度很慢,而你打开速度很快,其实可能并不是页面性能不好,只是外部因素不同而已。 所以做好性能优化不能只考虑外部因素好的情况,也要让用户能在恶劣条件(如弱网络情况)下也有满足预期的表现。下面看看影响性能的外部因素主要有哪些。 1.1 网络 网络可以说是最影响页面性能最重要的外部因素了,网络的主要指标有: 带宽:表示通信线路传送数据的能力,即在单位时间内通过网络中某一点的最高数据率,单位有bps(b/s)、Kbps(kb/s)、Mbps(mb/s)等。常说的百兆带宽100M就是100Mbps,理论下载最大速度12.5MB/s。 时延:Delay,指数据从网络的一端传送到另一端所需的时间,反应的网络畅通程度。 往返时间RTT:Round-Trip Time,是指从发送端发送数据到接收端接受到确认的总时间。我们经常用的ping命令就是用这个指标表明我们和目标主机的网络顺畅程度。比如我们要对比几个翻墙代理哪里个好,我们就可以ping一下,看看这几个代理哪个RTT低来作出选择。 [图片] 这三个主要指标中后面两个类似,在Chrome中模拟网络主要用设置带宽和网络延迟(往返时间RTT出现最小延迟)来模拟网络。我们电脑一般用的是WI-FI(百兆),那么我们模拟网络,主要模拟常见3G(1兆)、4G(10兆)网络就好,这样我们就覆盖了三个级别的网络情况了。 可以在Chrome的NetWork面板直接选取Chrome模拟好的网络,这个项目network-emulation-conditions中有默认模拟网络的速度。 [图片] 如果默认不满足,你也可以自己配置网络参数,在设置面板的Throttling。 [图片] 上面设置的3G接近100KB/s,4G 0.5MB/s。你可以根据自己的需要来调整这个值,这两个值的差异应该能很好两种不同的网络情况了。设置模拟网络只要能覆盖不同的带宽情况就好,也不用那么真实因为真实情况很复杂。网络部分就介绍完了,接着看其他因素。 1.2 用户机器性能 经常会有这种情况,一个应用在别人手机上打开速度那么快、那么流畅,为啥到我这里就不行了呢?原因很简单人家手机好,自然有更好的配置、更多的资源让程序运行的更快。 Chrome现在非常强大你可以通过performance面板来模拟cpu性能。也可以让你看到应用在低性能机器上的表现。 [图片] 1.3 用户访问次:首次访问、2次访问、发版本访问 用户访问次数也是分析性能的重要外部因素,当用户第一次访问要请求所有资源,后面在访问因为有些资源缓存了访问速度也会不同。当我们开发者又发版本,会更新部分资源,这样访问速度又会跟着变。因为缓存的效果存在,所以这三种情况要分开分析。同时也要注意我们是否要支持用户离线访问。 通过在Chrome中的Network面板中选中Disable cache就可以强制不缓存了,来模拟首次访问。 [图片] 1.4 因素对选取 上面的外部因素虽然只有3种但相乘也有不少情况,为了简化我们性能分析,要选取代表性的因素去分析我们的性能。下面是指导因素对: 网络:WIFI 3G 4G 用户访问状态:首次 2次 这样有6种情况不算特别多,也能很好反应我们应用在不同情况下的性能。 2. devtools具体分析性能 通过devtools可以观察在不同外部因素下代码具体加载执行情况,这个工具是我们性能分析中最重要的工具,加载优化这里我们主要关注两个面板:Network、Performance。 先看Network面板的列表页: [图片] 这是网络请求的列表,右击表头可以增删属性列,根据自己需要作出调整。 下面我介绍网络列表中的几个重点属性: Protocol:网络协议,h2说明你的请求是http2协议的了。 Initiator:可以查到这个资源是哪里引用的。 Status:网络状态码。 Waterfall:资源加载瀑布流。 下面在看看Network面板中单个请求的详情页: [图片] 这里可以看到具体的请求情况,Timing面板是用来观察这次网络的请求时间占用的具体情况,对我们性能分析非常重要。具体每个时间段介绍可以点击Explanation。 虽然Network面板可以让我看到了网络请求的整体和单个请求的具体情况,但Network面板整体请求情况看着并不友好,而且也只有加载情况没有浏览器线程的执行情况。下面看看强大的Performance面板的吧。 [图片] 这里可以清晰看到浏览器如何加载资源如何解析html、解析css、执行js和渲染绘制的。 Performance简直太强大了,所以请你务必要掌握它的使用,这里篇幅有限,只能介绍了个大概,建议到google网站仔细学习一下。 3. Lighthouse整体分析性能 使用Lighthouse可以对应用做整体性能分析评分,并且会给我们专业的指导建议。我们可以安装Lighthouse插件或者安装Lighthouse npm包来使用它。 检测结果中可以看到很多性能指标的分值和建议。你也可以去测试下你的应用表现。 4. 线上用户统计分析性能 虽然使用devtools和Lighthouse可以知道页面的性能情况,但我们还要观察用户的真实访问情况,这才能真实反映我们应用的性能。线上数据采集分析,第一步部分已经介绍过了,这里就不在多说了。优化完看看自己对线上数据到底造成了什么影响。 上面介绍了性能分析的方法,可以很好帮你去分析性能,有了性能分析的基础,下面我们在来看看怎么做性能优化吧。 第三部分:性能优化方法 1. 微观:优化单次网络请求时间 在性能分析知道Network面板可以看到单次网络请求的详情 [图片] 从图可以看出请求包括:DNS时间、TCP时间、SSL时间(https)、TTFB时间(服务器处理时间)、ContentLoaded内容下载时间,所以有下面公式: [代码]requestTime = DNS + TCP + SSL+ TTFB +ContentLoaded [代码] 所以只要我们降低这里面任意一个值就可以降低单次网络请求的时间了。 2. 宏观:优化整体加载过程 加载过程的优化就是不断让第一部分的性能指标感知点提前的过程。通过关键路径优化、渐进式展示、内容效率优化手段,来优化资源调度。 2.1 加载过程 在介绍页面加载过程,先看看渲染绘制过程: [图片] Javascript:操作DOM和CSSOM。 样式计算:根据选择器应用规则并计算每个元素的最终样式。 布局:浏览器计算它要占据的空间大小及其在屏幕的位置。 绘制:绘制是填充像素的过程。 合成。由于页面的各部分可能被绘制到多层,合成是将他们按正确顺序绘制到屏幕上,正确渲染页面。 渲染其实是很复杂的过程这里只简单了解一下,想深入了解可以看看这篇文章。 了解了渲染绘制过程,在学习加载过程的时候就可以把它当作黑盒了,黑盒只包括渲染过程从样式计算开始,因为上面的Javascript主要是用来输入DOM、CSSOM。 浏览器加载过程: Webview加载 下载HTML 解析HTML:根据资源优先级加载资源并构建DOM树 遇到加载同步JS资源暂停DOM构建,等待CSSOM树构建 CSS返回构建CSSOM树 用已经构建的DOM、CSSOM树进行渲染绘制 JS返回执行继续构建DOM树,进行渲染绘制 当HTML中的JS执行完成,DOM树第一次完整构建完成触发:domContentLoaded 当所有异步接口返回后渲染制完成,并且外部加载完成触发:onload 注意点: CSSOM未构建好页面不会进行任何渲染 脚本在文档的何处插入,就在何处执行 脚本会阻塞DOM构建 脚本执行要等待CSSOM构建完成后执行 下面看看如何在加载过程提前感知点。 2.2 优化关键路径 把关键路径定义为:从页面请求到应用启动完成这个过程,也就是到JS执行完domContentLoaded触发的过程。 主要指标有: 关键资源: 影响应用启动完成的资源。 关键资源的数量:这个过程中加载的资源数据。 关键路径长度:关键资源请求的串行长度。 关键字节的数量:关键资源大小总和。 [图片] 上图关键资源有:html、css、3个js。关键资源数量:5个。关键字节的数量:5个资源的总大小。关键路径长度:2,html+剩余其他资源。 关键优化路径优化,就是要降低关键路径长度、关键字节的数量,在http1时代还要降低关键资源的数量,现在http2资源数不用关心。 2.3 优化内容效率 主要是关注的应用加载完成这个时间点,由首页加载完成所需的资源量决定。我们要尽量减少加载资源的大小,避免不必要加载的资源,比如做一些图片压缩懒加载尽快让应用加载完成。 主要指标有: 应用加载完成字节数:应用加载完成,所需的资源大小。 这个指标可以从Chrome上观察到,不过要剔除prefetch的资源。这个指标一般不太稳定,因为页面展示的内容不太相同,所以最好在相同内容相同情况下对比。 2.4 渐进式展示 从上面的加载过程中,可以知道渲染是多次的。那样我们可以先让用户看到一个Loading提示、先展示首屏内容。Loading主要优化的是FP/FCP这两个指标,先展示首屏主要是优化FMP。 3. 缓存:优化多次访问 缓存重点强调的是二次访问、发版访问、离线访问情况下的优化。 通过缓存有效减少二次访问、发版访问所要加载资源,甚至可以让应用支持离线访问,而且是对弱网络环境是最有效的手段,一定要善于使用缓存这是你性能优化的利器。 4. 优化手段 优化手段我归纳为5类:small(更小)、pre(更早)、delay(更晚)、concurrent(并发)、cache(缓存)。性能优化就是将这5种手段应用于上面的优化点:网络请求优化、关键路径优化、内容效率优化、多次访问优化。 5. 构建自己可动态改变的优化方法表和检查表 Checklist包括两部分,一个优化方法表,另外一个优化方法检查表。优化方法表是让我们对我们的性能优化方法有个评估和认识,优化方法检查表的好处是,可以清晰的知道你的项目用了哪些优化方法,还有哪些可以尝试做进一步优化,同时作为一个新项目的指导。 优化名:优化方法的名字。 优化介绍:对优化方法做简单的介绍。 优化点:网络请求优化、关键路径优化、内容效率优化、多次访问优化。 优化手段:small、pre、delay、concurrent、cache。 本地效果:选取合适的因素对,进行效果分析,确定预期作用大小。 线上效果:线上效果对比,确定这个优化方案的有效性及实际作用大小。 这样我们就能大概了解了这个效果的好处。我们新引入了一种优化方法都要按这张表的方法进行操作。 优化方法表: 名称 内容 优化名 JS压缩 优化介绍 压缩JS 优化点 关键路径优化 优化手段 small 本地效果 具体本地效果对比 线上效果 线上数据效果 上面是以JS压缩为例的优化方法表。 优化方法检查表: 分类 优化点 是否使用 不适用 问题说明 small JS压缩 √ pre preload/prefetch √ 不需要 通过这张表就能看出我们使用了哪些方法,还有哪些没使用,哪些方法不适用我们。可以很方便的应用于任何一个新项目。 第四部分:性能优化具体实践 现在就看看我在项目中的具体实践吧,项目中使用的技术栈是:Webpack3+Babel7+Vue2,下面我按照优化手段介绍: 1. small(更小) scope-hoisting scope-hoisting(作用域提升):Webpack分析出模块之间的依赖关系,把可以合并到一起模块合并到一起,但不造成冗余,因此只有被一个地方引用的代码可以合并到一起。这样做函数声明会变少,可以让代码更小、执行更快。 这个功能从Webpack3开始引入,依赖于ES2015模块的静态分析,所以要把Babel的preset要设置成[代码]"modules": false[代码]: [代码] ... [ "@babel/preset-env", { "modules": false ... [代码] Webpack3要引入ModuleConcatenationPlugin插件,Webpack4 product模式已经预置该插件: [代码]... new webpack.optimize.ModuleConcatenationPlugin(), ... [代码] [图片] 如上图,不压缩的JS中可以文件中看到CONCATENATED MODULE这就说明生效了。 tree-shaking 摇树:通常用于描述移除JavaScript上下文中的未引用代码,在webpack2中开始内置。依赖于ES2105模块的静态分析,所以我们使用babel同样要设置成 [代码]"modules": false[代码]。 [图片] 如上图,不压缩的JS中可以文件中看到unused harmony这就说明摇树成功了。 code-splitting(按需加载) 代码分片,将代码分离到不同的js中,进行并行加载和按需加载。 代码分片主要有两种: 按需加载:动态导入 vendor提取:业务代码和公共库分离 这里只介绍按需加载部分,动态导入Webpack提供了两个类似的技术。1. Webpack特定的动态导入require.ensure。2.ECMAScript提案[代码]import()[代码]。这里我只介绍我使用的[代码]import()[代码]这种方法。因为是推荐方法。 代码如下: Babel配置支持动态导入语法: [代码]... "@babel/plugin-syntax-dynamic-import", ... [代码] 代码中使用: [代码]... if(isDevtools()){ import(/* webpackChunkName: "devtools" */'./comm/devtools').then((devtools)=>{ let initDevtools = devtools.default; initDevtools(); }); } ... [代码] polyfill按需加载 我们代码是ES2015以上版本的要真正能在浏览器上能使用要通过babel进行编译转化,还要使用polyfill来支持新的对象方法,如:Promise、Array.from等。对于不同环境来说需要polyfill的对象方法是不一样的,所以到了Babel7支持了按需加载polyfill。 下面是我项目中的配置,看完以后我会介绍一下几个关键点: [代码]module.exports = function (api) { api.cache(true); const sourceType = "unambiguous"; const presets = [ [ "@babel/preset-env", { "modules": false, "useBuiltIns": "usage", // "debug": true, "targets": { "browsers": ["Android >= 4.0", "ios >= 8"] } } ] ]; const plugins= [ "@babel/plugin-syntax-dynamic-import", "@babel/plugin-transform-strict-mode", "@babel/plugin-proposal-object-rest-spread", [ "@babel/plugin-transform-runtime", { "corejs": false, "helpers": true, "regenerator": false, "useESModules": false } ] ]; return { sourceType, presets, plugins } } [代码] @babel/preset-env preset是预置的语法转化插件的集合。原来有很多preset如:@babel/preset-es2015。直到出现了@babel/preset-env,它可以根据目标环境来动态的选择语法转化插件和polyfill,统一了preset众多的局面。 [代码]targets[代码]:是我们用来设置环境的,我的应用支持移动端所以设置了上面那样,这样就可以只加载这个环境需要的插件了。如果不设置[代码]targets[代码]通过@babel/preset-env引入的插件是 @babel/preset-es2015、@babel/preset-es2016和@babel/preset-es2017插件的集合。 [代码]"useBuiltIns": "usage"[代码]:将useBuiltIns设置为usage就会根据执行环境和代码按需加载polyfill。 @babel/plugin-transform-runtime 和polyfill不同,@babel/plugin-transform-runtime可以在不污染全局变量的情况下,使用新的对象和方法,并且可以移除内联的Babel语法转化时候的辅助函数。 我们这里只用它来移除辅助函数,不需要它来帮我处理其他对象方法,因为我们在开发应用不是做组件不怕全局污染。 sourceType:“unambiguous” 一个文件混用了ES2015模块导入导出和CJS模块导入导出。需要设置[代码]sourceType:"unambiguous"[代码],需要让babel自己猜测类型。如果你的代码都很合规不用加这个的。 压缩:js、css js、css压缩应该最基本的了。我在项目中使用的是[代码]UglifyJsPlugin[代码]和[代码]optimize-css-assets-webpack-plugin[代码],这里不做过多介绍。 压缩图片 通过对图片压缩来进行内容效率优化,可以极大的提前应用加载完成时间,我在项目中做了下面两件事。 广告图片,限制大小50K以内。原来基本会上传超过100K的广告图。 项目中图片使用的[代码]img-loader[代码]对图片进行压缩。 HTTP2支持,去掉css中base64图片 先看看HTTP1.1中的问题: 同一域名浏览器做了TCP连接数的限制,如:Chrome中只能有6个。 一个TCP连接只能同时处理一个请求响应。 在看看HTTP2的优势: 二进制分帧:HTTP2的性能增强的核心在于新的二进制分帧层。帧是最小传输单位,帧组成消息,数据以消息形式发送。 多路复用:所有请求在一个连接上完成,可以支持多数据流混合传输,在接收端拼接。 头部压缩:使用HPACK对头部压缩,网络中可以传递更少的数据。 服务端推送:服务端可以主动向客户端推送资源。 有了HTTP2我们在也不用担心资源数量,不用在考虑减少请求了。像:base64图片打到css、合并js、域名分片、精灵图都不要去做了。 这里我把原来base64压缩图片从css中去除了。 2. pre(更早) preload prefetch preload:将资源加载和执行分离,你可以根据你的需要指定要强制加载的资源,比如后面css要用到一个字体文件就可以在preload中指定加载,这样提高了页面展示效果。建议把首页展示必须的资源指定到preload中。 prefetch:用来告诉浏览器我将来会用到什么资源,这样浏览器会在空闲的时候加载。比如我在列表页将详情页js设置成prefetch,这样在进入详情页的时候速度就会快很多,因为我提前加载好了。 这里我用的是来使用[代码]preload-webpack-plugin[代码]preload和prefetch的。 代码: [代码]... const PreloadWebpackPlugin = require('preload-webpack-plugin'); ... new PreloadWebpackPlugin({ rel: 'prefetch', include: ['devtools','detail','VideoPlayer'] }), ... [代码] dns-prefetch preconnect dns-prefetch:在页面中请求该域名下资源前提前进行dns解析。preconnect:比dns-prefetch更近一步连TCP和SSL都为我们处理好了。 使用注意点:1. 考虑到兼容性问题,我们对一个域名两个都设置 2. 对于应用中不一定会使用的域名我们设置dns-prefetch就好以防占用资源。 代码如下: [代码]... <link rel="preconnect" href="//game.gtimg.cn"> ... <link rel="dns-prefetch" href="//game.gtimg.cn"> ... [代码] 3. delay(更晚) lazyload 对图片进行懒加载,我使用的是[代码]vue-lazyload[代码]。 代码如下: [代码]... import VueLazyload from 'vue-lazyload' ... Vue.use(VueLazyload, { preLoad: 1.3, error: '...', loading: '...', attempt: 1 }); ... <div class='v-fullpage' v-lazy:background-image="item.roomPic" :key="item.roomPic"></div> ... [代码] 这里的:key特别注意,如果你的列表数据是动态变化的一定要设置,否则图片是最开始一次的。 code-splitting(按需加载) code-splitting(按需加载)前面已经介绍过这里只是强调下它的delay作用,不使用的部分先不加载。 4. concurrent(并发) HTTP2 HTTP2前面已经应用在了css体积减少,这里主要强调它的多路复用。需要大家看看自己的项目是否升级到HTTP2,是否所有资源都是HTTP2的,如果不是的,需要推进升级。 code-splitting(vendor提取) vendor提取是把业务代码和公共库分离并发加载,这样有两个好处: 下次发版本这部分不用在加载(缓存的作用)。 JS并发加载:让先到并在前面的部分先编译执行,让加载和执行并发。 Webpack配置: [代码] ... entry:{ "bundle":["./src/index.js"], "vendor":["vue","vue-router","vuex","url","fastclick","axios","qs","vue-lazyload"] }, ... new webpack.optimize.CommonsChunkPlugin({ name: "vendor", minChunks: Infinity }), new webpack.optimize.CommonsChunkPlugin({ name: 'manifest' }), ... [代码] 5. cache(缓存) HTTP缓存 HTTP缓存对我们来说是非常有用的。 下面介绍下HTTP缓存的重点: Last-Modified/ETag:用来让服务器判断文件是否过期。 Cache-Control:用来控制缓存行为。 max-age: 当请求头设置max-age=deta-time,如果上次请求和这次请求时间小于deta-time服务端直接返回304。当响应头设置max-age=deta-time,客户端在小于deta-time使用客户端缓存。 强制缓存:这主要把不经常变化的文件设置强制缓存,这样就不需要在发起HTTP请求了。通过设置响应头Cache-Control的max-age设置。 如果像缓存很久设置一个很大的值,如果不想缓存设置成:Cache-Control:no-cahce。 协商缓存:如果没有走强制缓存就要走协商缓存,服务器根据Last-Modified/ETag来判断文件是否变动,如果没变动就直接返回304。 这里我们做的就是让运维调整资源的强制缓存时间,前端在结合文件hash命名就可以进行资源更新了。 ServiceWorker ServiceWorker是Web应用和浏览器之间的代理服务器,可以用来拦截网络来进行资源缓存、离线体验,还可以进行推送通知和后台同步。功能非常强大,我们这里使用的是资源缓存功能,看看和HTTP缓存比有什么优势: 功能多:支持离线访问、资源缓存、推送通知、后台同步。 控制力更强:缓存操作+络拦截功能都由开发者控制,可以做出很多你想做的事情比如动态缓存。 仅HTTPS下可用,更安全。 看看我在项目中的使用: js使用HTTP缓存和ServiceWorker双重缓存在cacheid变化后依然可以缓存。 不得对service-worker.js缓存,因为我们要用这个更新应用。在Chrome中看到请求的cache-control被默认设置了no-cache。 我们项目中使是Google的Workbox,Webpack中插件是 workbox-webpack-plugin。 [代码]... const WorkboxPlugin = require('workbox-webpack-plugin'); ... new WorkboxPlugin.GenerateSW({ cacheId: 'sw-wzzs-v1', // 缓存id skipWaiting: true, clientsClaim: true, swDest: './html/service-worker.js', include: [/\.js(.*)$/,/\.css$/], importsDirectory:'./swmainfest', importWorkboxFrom: 'local', ignoreUrlParametersMatching: [/./] }), ... [代码] localStorage localStorage项目中主要做接口数据缓存。通常localStorage是没有缓存时间的我们将其封装成了有时间的缓存,并且在应用启动的时候对过期的缓存清理。 code-splitting(vendor提取) 这里在提vendor提取主要是说明它发版本时候的缓存价值,前面介绍过了。 6. 整体优化效果评价 经过上面的优化,看看效果提升吧。 主要增长点来源: 关键路径资源:698.6K降低到538.6K降低22.9% 内容效率提升:广告图由原来的基本100K以上降低到现在50K以下,页面内图片全部走强制缓存。 缓存加快多次访问速度:js+css强制缓存加ServiceWorker。 线上数据效果: 页面下载时间: 平均值下降:25.74%左右 应用启动完成时间: 平均值下降:33.45%左右 秒开占比提高:23.42%左右 应用加载完成时间: 平均值下降:48.02%左右 第六部分:总结 以上就是我在加载优化方面的一些总结,希望对您有所帮助,个人理解有限,欢迎一起讨论交流。
2019-03-11 - 小程序/小游戏动态生成分享海报 - 技术方案分享
在应用开发过程中,我们会遇到各种各样的分享场景,例如邀请、拉新、分享内容等。分享链接是 Web 时代常见的分享形式,实现也相对容易。但是现在人们时间大都花在了 APP 上,所以应用之间的分享越来越重要,然而应用之间分享链接却不是那么顺利和有效果。往往受以下制约: 纯文字链接,依靠文字向外界传达信息,信息量小、可信度低。群里丢了一个链接进来,什么描述都没有,大多数人都不会去点。链接的描述一般也不会太长,信息不会太多。分享的目标应用的外链策略。淘宝购物链接不能分享到微信、营销链接容易被微信封禁,都是受微信外链策略的影响。平台分享机制限制。小程序的转发功能允许用户直接将小程序分享到联系人中,却无法分享到朋友圈。若开发者希望用户可以分享小程序到朋友圈中,通常需要生成分享海报图片,分享图片到朋友圈中。所以 APP、H5、小程序等应用中分享功能,除了实现分享链接以外,还需要生成分享海报图片,在图片上展现更丰富的内容,一图胜千言。 如何低成本地生成内容丰富的海报图片,就是我们想要解决的问题。 常见技术方案从生成图片的位置划分,可以将方案划分为两种:客户端生成、服务端生成。 在客户端生成图片是将图片中的元素都下载到本地,然后使用绘图 API 进行绘制,典型方案就是使用 Canvas 来绘制图片。 在服务端生成图片,又可以分为两种,一种是在服务端使用绘图库绘制,然后返回图片或图片链接给客户端;另一种是在服务端使用HTML + CSS 生成带有样式的网页,然后使用无头浏览器截图,返回图片或图片链接给客户端。 简而言之,一般会使用下面这三种方式: 在客户端使用 Canvas 生成图片在服务器上使用网页完成样式渲染,然后截图返回给客户端使用后端绘图库绘制,然后返回给客户端下面逐一分析各种方案生成海报图片的优缺点。 客户端使用 Canvas 生成海报图片优点: 渲染过程在每个客户端中完成,渲染相对独立,基本上不需要考虑并发的问题。Canvas 特性丰富,可以实现样式复杂的图片渲染。缺点也很明显: 上手门槛高,需要灵活使用 Canvas API代码可读性差,调试过程复杂。代码复用程度低,每个端都需要重新编码。客户端型号众多,用户设备上的表现还可能与在开发机上的表现存在差异。兼容性太差,这是客户端渲染最大的痛点。如果有远程图片,可能会因跨域,无法下载,导致绘制失败。推荐阅读:小程序canvas绘制海报 服务端浏览器,网页截图在服务器上使用 HTML + CSS 在无头浏览器中完成网页样式布局与内容填充,然后使用无头浏览器提供的截图 API,将生成的网页截图保存。无头浏览器一般会选用 [代码]Puppeteer[代码]。 [代码]Puppeteer[代码] 是谷歌官方团队开发的一个 Node.js 库,它提供了一些 API 用来控制浏览器的行为,比如打开网页、模拟输入、点击按钮、屏幕截图等操作,通过这些 API 可以完成很多有趣的事情,比如本文要讲的海报渲染服务,就会用到屏幕截图功能。 这种方案的优缺点也很明显。 优点: 上手简单,只需要了解 HTML 、CSS 就可以代码可读性高,易于调试得力于HTML、CSS,表达力强。只要在网页上能实现,就可以应用到海报图片中。返回给客户端的是图片链接,不用考虑兼容性。服务端生成图片带来的最大好处是多端兼容。但这也会引入一个问题,成本高。 在后端需要运行一个 Node 服务来跑[代码]Puppeteer[代码] 控制一个浏览器,性能太低。一个4核16G内存的服务器生成图片,峰值QPS只有 10-20,在较差情况下每秒只能生成10张图片。 推荐阅读:使用 Puppeteer 搭建统一海报渲染服务 服务端绘图库绘制图片在服务端中,使用绘图库,绘制图片,然后将图片保存至 CDN 中,再返回图片链接给客户端。常用编程语言都有绘图库,例如 PHP 的 GD 库。相较于控制浏览器截图,这个方案的性能更高,也具有服务端渲染的好处,但灵活性却没有使用CSS控制样式高。 优点: 性能高服务端架构统一,开发者不用单独维护一个Node.js 服务。代码可读性高缺点: 复杂样式,开发时间长,需要微调。自适应布局困难。推荐阅读:PHP 使用GD库合成带二维码的海报步骤以及源码实现 上面介绍了三种生成分享海报图片的常用方案,了解了实现原理。开发者在实现这些方案时都需要进行独立的开发,维护复杂的样式代码,每增加一种海报,就需要维护一份样式代码。 海报只是业务中很小的一环,自己维护一个海报渲染服务,付出的成本与收益之间不成正比。所以我们更推荐使用第三方海报/图片渲染服务,来完成实现我们的想法。 imgrender.cn 动态图片渲染服务Imgrender 是一个免费的图片渲染服务,通过一个API,根据配置动态渲染图片,快速生成不同内容的图片。渲染模板配置简单,特别适合拥有不同分享海报的应用,快速、动态地生成分享海报。Imgrender 支持「文本」、「图片」、「二维码」、「矩形」、「线段」五种组件,可满足绝大多数海报的渲染需求。 👏 免费📝 API 优先🛣 动态渲染,内容可动态调整🖥 易于配置,方便调试📱 兼容性高,渲染结果只与配置有关⚡️ 快速、稳定,平均响应时间 400msImgrender 只有一个核心 API,通过 API 传递海报内容,就可以动态生成不同内容的图片。海报内容完全配置化,在完成设计稿后,按照设计尺寸和位置生成配置即可。 使用服务也很简单,只需要请求 [代码]POST https://api.imgrender.net/open/v1/pics[代码]。将下面的 curl 命令复制到终端请求一下,就可以得到渲染好的海报图片链接。 curl 命令中 Apikey 是临时的,可能会失效,你可以在 imgrender 中免费获取 API Key。 curl -X "POST" "https://api.imgrender.net/open/v1/pics" \ -H "Authorization: Apikey 183666749185461475.PLbfIpBpeMkpgbj1Tr+177Mv3Jo3wIIySyf8V5ZeDhs=" \ -H "Content-Type: application/json; charset=utf-8" \ -d '{ "width": 640, "height": 1050, "backgroundColor": "#d75650", "blocks":[ { "x": 15, "y": 268, "width": 610, "height": 770, "backgroundColor": "#fff", "borderColor": "#fff" } ], "texts":[ { "x": 320, "y": 185, "text": "Davinci Li", "font": "jiangxizhuokai", "fontSize": 22, "color": "#fff", "width": 320, "textAlign": "center" }, { "x": 320, "y": 220, "text": "邀请你来参加抽奖", "font": "jiangxizhuokai", "fontSize": 22, "color": "#fff", "width": 320, "textAlign": "center" }, { "x": 30, "y": 640, "text": "奖品: 本田-CB650R 摩托车", "font": "jiangxizhuokai", "fontSize": 22, "color": "#000", "width": 580, "textAlign": "left" }, { "x": 30, "y": 676, "text": "01 月 31 日 18:00 自动开奖", "font": "jiangxizhuokai", "fontSize": 18, "color": "#9a9a9a", "width": 580, "textAlign": "left" }, { "x": 320, "y": 960, "text": "长按识别二维码,参与抽奖", "font": "jiangxizhuokai", "fontSize": 22, "color": "#9a9a9a", "width": 580, "textAlign": "center" } ], "lines":[ { "startX": 30, "startY": 696, "endX": 610, "endY": 696, "width": 1, "color": "#E1E1E1", "zIndex": 1 } ], "images":[ { "x": 248, "y": 25, "width": 120, "height": 120, "url": "https://img-chengxiaoli-1253325493.cos.ap-beijing.myqcloud.com/bikers_327390-13.jpg", "borderRadius": 60, "zIndex": 1 }, { "x": 108, "y": 285, "width": 400, "height": 300, "url": "https://img-chengxiaoli-1253325493.cos.ap-beijing.myqcloud.com/cb650R.jpeg", "zIndex": 1 } ], "qrcodes":[ { "x": 208, "y": 726, "size": 200, "content": "http://weixin.qq.com/r/yRzk-JbEbMsTrdKf90nb", "foregroundColor": "#000", "backgroundColor": "#fff", "zIndex": 1 } ] }' 请求返回内容: { "code":0, "message":"OK", "data":"https://davinci.imgrender.cn/c3037d467c163bd903760f96a34f3bcd.jpg?sign=1616722062-plQZQ4xtth9tEthx-0-2a98ba98e5fd44cc6dffb3aec6d3398f" } [代码]data[代码] 字段就是动态渲染好的海报图片链接,下载或打开链接就可保存图片。查看详细使用方法。 [图片] imgrender.net 推荐按以下最佳实践来使用海报生成服务: [图片] 所有的海报配置全都管理在服务端中,服务端只需要提供一个 API 给客户端。客户端通过这个 API 请求不同的分享图,服务端接收到请求后,先检查服务端是否缓存分享图。若没有缓存图片,则使用海报配置去 imgrender 动态生成海报,然后将生成的图片链接返回给客户端,供用户下载保存。 使用 imgrender 动态渲染海报,在满足需求的同时,可以大幅度降低开发成本,提高多端兼容性。无论是开发小程序海报,还是原生应用中的海报,一套代码即可搞定。 [图片] 原文链接:https://mp.weixin.qq.com/s/6ss1D4wneNDuhUfplualQQ 关键词:海报图、海报分享图、海报生成、图片生成、小程序海报生成
2023-11-11 - 小程序快速注册免认证费教程
功能描述 快速创建小程序功能优化了小程序注册认证的流程,采用法人人脸识别方式替代小额打款等认证流程,极大的减轻了小程序主体、类目资质信息收集的人力成本。第三方只需收集法人姓名、法人微信、企业名称、企业代码信息四个信息,便可以向企业法人下发一条模板消息来采集法人人脸信息,完成全部注册、认证流程。快速创建小程序接口能帮助第三方迅速拓展线下商户,拓展商户的服务范围,占领小程序线下商业先机。 通过该接口创建小程序默认“已认证”。为降低接入小程序的成本门槛,通过该接口创建的小程序无需交300元认证费。 注:该功能只能创建线下类目小程序,创建线上类目小程序将被驳回,且影响第三方调用该接口的quota。 小程序类目参考: 使用教程注册流程第三方平台1. 第三方平台需具有以下权限集(更新权限集后,需通过审核并全网发布后才可生效) 2. 全网发布必须审核通过[图片] 3. 微擎或独立版平台配置微信开放平台信息 提交申请1. 收集法人微信、法人姓名、企业名称、信用代码四个商户信息外加第三方客服电话,方便商家与第三方联系(建议填写第三方客服电话);企业名称需与工商部门登记信息一致;法人姓名与绑定微信银行卡的姓名一致。信息收集时要确保四个信息的对应关系,否则接口无法成功调用。 2. 登录应用系统,填写对应信息(【小程序】->【快速注册】[图片] 3. 注册记录显示已经提交的注册信息,查询进度功能由于微信官方接口限制,每条记录半个小时以内只能查询一次。 法人认证 通过法人&企业主体校验,微信平台向法人微信下发模板消息。法人需在24小时内点击消息,进行身份证信息与人脸识别信息收集;[图片] 注意事项1. 注意保证主体信息与工商部门登记一致。如:广州和广州市;国家企业信用信息公示系统:http://www.gsxt.gov.cn/index.html 2. 微信号填写错误,需正确引导获取位置“微信”-“我”(不能使用手机号、QQ号) 3. 确保微信号主人和微信支付绑定银行卡的主人姓名一致。核实用户是否有改过名字,或者近期有做身份证升级(从15位身份证升级成18位身份证)。 4. 刚提交任务不会马上收到,会有几分钟或者十几分钟延迟(实际时间取决于信息收集的准确程度) 5. 若提示法人验证失败,查看服务器消息状态码(status),调整信息,重新提交任务。 登录信息完善法人信息认证通过后,用户需通过微信公众平台找回账号密码,管理小程序。 获取小程序的原始ID 获取方式:关注公众平台安全助手。点击菜单栏【绑定查询】,选择微信号绑定账号,选择小程序,可以看到新认证的小程序的原始ID。 注:这个新注册的小程序没有头像和名称,而且后方还带有已授权第三方的字样。 找回账号密码访问网址:https://mp.weixin.qq.com/,进入微信公众平台后台的登录界面,点击 找回账号密码,选择找回账号,输入获取到的原始ID。 [图片] [图片] [图片] 按照要求填写企业信息、公户信息、管理员信息以及验证邮箱,管理员会收到一条打款验证的模板消息,根据微信提供的账号和金额给微信打款 [图片] 1. 打款成功后会受到一条提示模板消息,同时,验证邮箱内会收到一条重置密码的链接。点击链接进行密码重置。成功获取小程序的账号密码,登陆后填写基本信息,而且小程序是已经认证的! 注意事项1. 提前准备一个未注册过小程序公众号的邮箱,找回密码后这个邮箱将会作为小程序的登录邮箱! 2. 没有公户的商家请根据微信提示进行验证。 3. 选择类目时必须选择线下类目
2020-07-05 - 小程序支付用户不点击“完成”的处理方案
小程序支付,用户在付费成功后,没有点击“完成”,就没有支付成功回调触发,只能通过notify_url来接收异步回调通知,处理流程,当然这是正常的处理流程。 以下是另类的处理流程: 1、不处理notify_url; 2、在wx.requestPayment之前,将流程进度缓存,比如wx.setStorageSync('out_trade_no',out_trade_no); 3、那么,如果用户点击了“完成”或者取消支付,则必然会触发wx.requestPayment的回调success或者fail,则清除缓存,wx.removeStorageSync('out_trade_no'); 4、如果用户没有点击“完成”,则用户下次打开小程序,一定是冷启动即重启小程序,因为如果是热启动,将还是停留在支付界面,用户可以继续点击“完成”,继续业务流程; 5、因为是冷启动,所以在pages/index/index的onLoad里常驻一个进程,检查是否有支付未完成缓存,如果有,则按照out_trade_no查询订单支付状态,再继续支付流程。 以上只是另类做法,仅供参考。 虽然不符合支付的标准流程,但是可以不需要专门的进程来负责notify_url。
2021-03-22 - 如何通过抽奖小程序系统助力线下门店营销引流?
受疫情影响,今年各行业都不太景气,特别是线下门店,到现在大多数也没有恢复到以前的营业水平。生意不好做,竞争又激烈,身边不少做生意的朋友,都跟我咨询:“有什么方式可以低成本实现精准营销?”、“有什么系统或软件能帮助线下门店引流?”、“抽奖系统现在还能玩得起来吗?”等诸如此类的问题。今天湖北诚万兴科技这篇文章,就针对这些问题,跟大家做下深度分享与解答! 首先,好的营销往往都是简单,且直指人性的。营销中最常利用的人性,莫过于“贪”,说白了,很多人都贪小便宜,这就是最有驱动力的人性。而说到简单,指的是:用户参与要简单,不能太繁琐。所以,抽奖就是一个不错的方式,用户只需轻轻一点,就可以有机会免费获得奖品——占到便宜。 接下来,就要跟大家详细介绍下,我们湖北诚万兴科技,最近自主研发的“微信多商户大转盘抽奖营销系统”。这个抽奖营销系统,目的简单直接,主要有以下三点: 1.通过抽奖引流:奖品有现金红包和实物,用户获得实物奖品,需到店领取,这样就可能吸引用户到店消费。2.通过奖品视频曝光:奖品可上传视频,用户中奖后,需观看10秒视频才能获取奖品兑换码,能有效提高商家信息展示及曝光。3.通过系统体现会员身份:不同会员等级可在线下享受不同的优惠或福利。解决传统会员卡,成本高,且易丢失,无法进行数据统计和分析等缺点。 [图片] 为了让大家,对微信多商户大转盘抽奖营销系统,有更全面的了解,下面将从系统后台、商户后台、H5管理端和前端,分别对关键功能或模块,做具体的讲解。 一.系统后台商户信息添加:登录帐号、初始密码、商户名称、联系人、联系电话、邮箱、备注、到期时间商户信息管理:可按商户名称、联系电话搜索商户信息二.商户后台1.奖品设置可设置奖品、奖品数量和中奖率;奖品可以为:现金红包、实物2.抽奖活动设置设置用户可参与抽奖次数,间隔几天可抽1次;实物奖品可上传10MB以内的短视频设置可抽奖区域,如:北京市海淀区;添加抽奖活动,需选择所属门店可生成抽奖活动二维码,用户扫码可进入抽奖页面3.用户抽奖记录抽奖时间、是否中奖、奖品及领取状态4.用户管理可查看所有用户信息(微信昵称、头像、性别、所在城市、姓名、手机号码、会员等级、注册时间)可按 手机号码 和 会员等级搜索可设置用户为“门店管理员”,需选择所属门店(可多选)5.门店管理门店添加:名称、地址等信息门店列表查看6.微信公众号和红包配置公众号开发相关信息配置红包祝福语等配置7.微信公众号菜单管理8.微信公众号关键词管理9.会员等级管理可添加会员等级(等级名称、等级描述、级别)10.门店管理员门店管理员列表查看门店管理员删除门店管理员“操作记录查看”:可查看 奖品核销 和 会员等级设置等记录11.商户信息登录帐号和密码修改帐号到期时间查看三.H5管理端“门店管理员”可扫描用户的奖品二维码进行奖品发放核销“门店管理员”可查看用户信息(姓名、电话、会员等级),通过以下两种方式: a.扫描用户的会员二维码 b.搜索用户手机号码 “门店管理员”可扫描用户的会员二维码 或 搜索用户手机号码,设置用户会员等级四.前端参与抽奖活动:用户可点击抽奖链接,进入“大转盘”抽奖页面参与抽奖;如果中奖, 实物奖品如果有视频,则显示视频弹框,如下图 [图片] 2.公众号内回复兑换码,如果是现金红包,则直接发放;实物,则提示用户奖品已兑换成功,请点击“我的奖品”查看领取(点击跳转到“我的奖品”页面) 3.“门店详情”页面:可查看门店名称、地址、电话和地图信息,并可一键查看高德地图驾车 [图片] 4.“我的奖品”页面: 用户可查看获得的奖品列表:奖品名称、中奖时间、门店名称(可点击进入“门店详情”页面)、奖品状态(未兑换、未领取、已领取)对于“未兑换”奖品,可查看奖品兑换码对于“未领取”实物奖品,可查看兑换二维码,到店后让店员核销领取 好了,就写这么多吧,希望湖北诚万兴科技这篇文章对您能有所帮助。如果您在抽奖系统、红包营销系统等方面存在疑问,或想搭建微信抽奖营销系统,可通过以下方式跟我们联系咨询! ☆湖北诚万兴科技官网:www.hbcwxkj.com ☆一物一码营销平台:一码物联 www.yimawulian.com ☆合作咨询电话:18062239856(微信同号) ☆原文链接:https://www.hbcwxkj.com/newsshow/59
2021-03-21 - 更新数据库数组中指定下标元素的的某字段的值,指定下标是个变量,不知能不能做到?
请教 更新数据库数组中指定下标元素的的某字段的值,指定下标是个变量,不知能不能做到? 官方文档下标是数字,如果是变量呢通过doc(id).update如何去做?
2020-07-26 - 小技巧!CSS 整块文本溢出省略特性探究
今天的文章很有意思,讲一讲整块文本溢出省略打点的一些有意思的细节。 文本超长打点 我们都知道,到今天(2020/03/06),CSS 提供了两种方式便于我们进行文本超长的打点省略。 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 对于单行文本,使用单行省略: { width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } [图片] 而对于多行文本的超长省略,使用 [代码]-webkit-line-clamp[代码] 相关属性,兼容性也已经非常好了: { width: 200px; overflow : hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } [图片] CodePen Demo -- inline-block 实现整块的溢出打点 问题一:超长文本整块省略 基于上述的超长打点省略方案之下,会有一些变化的需求。譬如,我们有如下结构: Sb Coco FEUIUX Designer前端工程师 [图片] 对于上述超出的情况,我们希望对于超出文本长度的整一块 -- 前端工程师,整体被省略。 如果我们直接使用上述的方案,使用如下的 CSS,结果会是这样,并非我们期待的整块省略: .person-card__desc { width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } [图片] 将 [代码]display: inline[代码] 改为 [代码]display: inline-block[代码] 实现整块省略 这里,如果我们需要实现一整块的省略,只需要将包裹整块标签元素的 [代码]span[代码] 的 [代码]display[代码] 由 [代码]inline[代码] 改为 [代码]inline-block[代码] 即可。 .person-card__desc span { display: inline-block; } [图片] 这样,就可以实现,基于整块的内容的溢出省略了。完整的 Demo,你可以戳这里: CodePen Demo - 整块超长溢出打点省略 问题二:iOS 不支持整块超长溢出打点省略 然而,上述方案并非完美的。经过实测,上述方案在 iOS 和 Safari 下,没能生效,表现为这样: [图片] 查看规范 - CSS Basic User Interface Module Level 3 - text-overflow,究其原因,在于 [代码]text-overflow[代码] 只能对内联元素进行打点省略。(Chrome 对此可能做了一些优化,所以上述非 iOS 和 Safari 的场景是正常的) 所以猜测是因为经过了 [代码]display: inline-block[代码] 的转化后,已经不再是严格意义上的内联元素了。 解决方案,使用多行省略替代单行省略 当然,这里经过试验后,发现还是有解的,我们在开头还提到了一种多行省略的方案,我们将多行省略的代码替换单行省略,只是行数 [代码]-webkit-line-clamp: 2[代码] 改成一行即可 [代码]-webkit-line-clamp: 1[代码]。 .person-card__desc { width: 200px; white-space: normal; overflow : hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; } .person-card__desc span { display: inline-block; } 这样,在 iOS/Safari 下也能完美实现整块的超长打点省略: [图片] CodePen Demo -- iOS 下的整块超长溢出打点省略方案 值得注意的是,在使用 [代码] -webkit-line-clamp[代码] 的方案的时候,一定要配合 [代码]white-space: normal[代码] 允许换行,而不是不换行。这一点,非常重要。 这样,我们就实现了全兼容的整块的超长打点省略了。 当然,[代码] -webkit-line-clamp[代码] 本身也是存在一定的兼容性问题的,实际使用的时候还需要具体去取舍。 最后 好了,本文到此结束,一个简单的 CSS 小技巧,希望对你有帮助 :) 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 作者:chokcoco
2021-03-15 - 【微信小程序】【数据库】【聚合】能否先输出统计数值count,再输出聚合结果?
const res = await db_contract.where({ countrys:event.countrys }).count()//先获取符合结果的数量 e.total = res.total if(e.total!=0){ await db_contract.aggregate() .lookup({ from:'MC_users', localField: 'no', foreignField: 'no', as: 'user', }).match({//匹配结果 reach:false, countrys:event.countrys }).sort({ _id:1 }).//COUNT()这样不行 .skip(event.start) .end() .then(r => {//返回匹配值,数据量大于20 e.list = r.list console.log(r) }) .catch(err =>{ e.errCode = err.errCode console.error(err) }) 这是我现在在用的代码,但是这样有点蠢,我只想要个计数和匹配数组,但是却做了两次查询,有没有什么办法能整合到一起吗?
2020-07-11 - 云函数之间的相互调用
为什么输出结果不是15呢????????[图片] [图片] [图片]
2019-03-18 - 小程序设置字体问题,能设置字体吗?
小程序默认字体是什么? 小程序能否设置字体? 小程序会跟手机系统的字体一样么? 小程序如果能设置字体的话,权重会大于手机系统字体么?
2019-09-20 - 云函数触发定时器的时间可以动态设置吗?
动态传递一个时间给定时器应该怎么做?
2020-07-18 - 添加到我的小程序引导组件
出发点 开发了一个小程序,经过一段时间的观察,发现访问人数和添加到我的小程序的数据差异比较大,我想可能存在2种可能: 用户不清楚有添加到我的小程序功能 本身产品做的不够好,不愿意添加 那为了验证可能性,我就做了这个引导添加的提示组件,组件开发非常简单,这里分享给大家。 代码实现 制作一张gif动图或者静态提示图片也行 [图片] wxml布局如下 [代码]<view wx:if="{{showTip}}" class="tip-wraper"> <image class="tip-gif" mode="widthFix" src="./images/add_tip.gif"></image> <image bindtap="closeTip" class="tip-close" src='./images/close.png'></image> </view> [代码] js代码如下 [代码]methods: { // 初始化关注提示 initTip() { let showTip = wx.getStorageSync('showTip') this.setData({ showTip: typeof showTip=='boolean'?showTip:true }) }, // 关闭提示 closeTip() { wx.setStorageSync('showTip', false) this.setData({ showTip: false }) } }, lifetimes:{ attached:function(){ this.initTip() } } [代码] [图片] 结果 上线2天后,发现添加人数相比之前是倍数增长,这也就验证了产品本身其实没有什么大问题,大多数用户是不知道这个功能,或者说用户的行为需要我们去小小的引导一下,就能产生意想不到的收获。 组件源码在minicode-debug项目的[代码]/add-tip[代码]目录下,可直接拿去复用或者参照修改。
2021-03-10 - 抽奖类小程序,具体抽奖逻辑如何实现
本文背景本人运营一个抽奖类小程序已步入正轨,期间虽然也出过大大的问题,好在吃一堑长一智,现在一切都比较稳定,特别是在抽奖环节。 本文内容本文依托我运营的小程序,来分享下在具体抽奖环节的逻辑是如何实现的 首先要说下目前小程序的实现机制,目前抽奖小程序主要有三步 (1)开~奖、所谓开奖就是将当前奖项根据时间,从未开奖,标记为可开奖状态 (2)抽~奖、所谓抽奖就是,在可开奖的奖项里面,根据当前奖项参与的用户,以及奖品设置,把具体的奖项给对应的某个参与用户 (3)推~送、所谓推送就是在开奖完成后,推送订阅消息给所有参与抽奖的用户 对应这三步,该小程序有三个核心的云函数 (1)run,触发器,每个整点的1分开始执行,具体逻辑是根据当前时间和开奖时间进行比较,如果当前时间大于开奖时间,那么标记状态位为可开奖 (2)draw,触发器,每个整点的5分开始执行,具体抽奖的逻辑,也是本文具体分享的环节 (3)sendmore,触发器,每个整点的10分开始执行,进行推送订阅消息 f 本文的重点是在上面的第二步 在具体实现抽奖的逻辑,本文分享两个,所谓抽奖无非就是根据奖项设置的奖品个数随机从参与用户那里选取两个用户,这里注意一个关键词,是随机 随机就代表公平,这是该小程序的核心 方法1、云函数的sample 这个是云开发里面提供的随机检索的函数,小程序云开发支持 方法2、第三方库的suffle http://underscorejs.org/ [图片] [图片] 本文总结本文通过分享抽奖类小程序核心逻辑场景,然后给出具体抽奖环节的解决方案以及具体代码
2021-01-10 - #小程序能力 小程序内如何关注公众号思路
方式一:官方组件->official-account 方式二:官方组件->webview打开公众号文章引导用户关注 方式三:官方组件->button open-type='contact' 打开客服窗口给用户推送关注公众号引导 方式四、小程序内嵌公众号名称或者图片 方式一:官方组件->official-account https://developers.weixin.qq.com/miniprogram/dev/component/official-account.html 前提:当用户扫小程序码打开小程序时,开发者可在小程序内配置公众号关注组件,方便用户快捷关注公众号,可嵌套在原生组件内。 Tips 使用组件前,需前往小程序后台,在“设置”->“关注公众号”中设置要展示的公众号。注:设置的公众号需与小程序主体一致。在一个小程序的生命周期内,只有从以下场景进入小程序,才具有展示引导关注公众号组件的能力:当小程序从扫小程序码场景(场景值1047,场景值1124)打开时当小程序从聊天顶部场景(场景值1089)中的「最近使用」内打开时,若小程序之前未被销毁,则该组件保持上一次打开小程序时的状态当从其他小程序返回小程序(场景值1038)时,若小程序之前未被销毁,则该组件保持上一次打开小程序时的状态为便于开发者调试,基础库 2.7.3 版本起开发版小程序增加以下场景展示公众号组件:开发版小程序从扫二维码(场景值 1011)打开 — 体验版小程序打开组件限定最小宽度为300px,高度为定值84px。每个页面只能配置一个该组件。 方式二:官方组件->webview打开公众号文章引导用户关注 小程序中使用webview组件打开一篇要关注公众号的文章,引导用户点击公众号名称关注; 需设置:在公众号中关联小程序,否则不能打开公众号文章 [图片] 方式三:官方组件->button open-type='contact' 打开客服窗口给用户推送关注公众号引导 [图片] 技能get: 1.如果获取公众号历史连接:PC微信客户端获取 [图片] 2.快捷获取公众号二维码:https://open.weixin.qq.com/qr/code?username=dyh_mirsh 方式四、小程序内嵌公众号名称或者图片 图片:引导用户保存图片在微信扫一扫识别 名字:提供复制能力,引导用户复制到搜索框搜索; [图片]
2020-10-14 - 尊敬的张小龙张总您好:关于视频号直播限流问题,希望您看完!
我是一名视频号运营者,目前负责视频号运营工作,近期我出现了一个问题,视频号直播限流问题情况是这样的 2021年3月4号进行视频号直播,然后流量明显降低了很多,同城附近的人不显示,然后我通过微信开放平台,以及知乎,百度搜索清楚了明白了目前视频号直播违规,但是目前视频号并未出现任何通知,同时我也找到了申诉电话,进行申诉,具体情况是这样的: [图片]说是出现了敏感内容,但是并未说明具体是什么敏感内容以及解封时间,希望能够得到解决 微信号是pingguo26688 视频号叫做:源玉兴 这并不是我一个人出现这样的情况,有很多同行也出现了这样的情况,因为我是负责这个项目,如果目前并未有任何通知情况下,出现封锁,那么我们项目就没办法前进,我们相信视频号一定可以起来,但是希望官方可以看到我们努力的成果,以下是我看到的 [图片]
2021-03-05 - 实战:图片处理服务之快速压缩模版
前言 在昨天发布的《实战:如何降低云开发服务器成本? 》文章,评论区有提到需要「关于cloudbase的扩展能力-图像处理-快速缩略模板的用法」今天我就来和大家分享一下具体用法和效果。 安装 地址:https://console.cloud.tencent.com/tcb/env/overview 选中「扩展能力」菜单下面的「扩展应用」 [图片] 选择「图片处理」服务进行「安装」 [图片] 安装过程一直「下一步」就行没有需要配置的地方,需要等待几分钟 [图片] 查看文档 安装完成之后我们就可以使用了 首先看下文档:https://cloud.tencent.com/document/product/876/42103 [图片] 找到我们要用的「快速压缩模版」。 地址:https://cloud.tencent.com/document/product/460/6929 使用方法,直接在图片后面来评价参数即可。 [图片] 实战使用 通常使用在列表场景中,本来就不要高清图,所以可以进行压缩也不会影响用户体验。 [图片] 我们找一个图片链接放在浏览器上来看 [图片] 然后使用下快速压缩模版拼接参数 ?imageView2/1/w/100/h/100 [图片] 把两张图片下载下来对比一下大小 [图片] 压缩后小了54倍 总结 这样以来不仅让用户能够更快的加载出图片,并且还能降低服务器资源成本。
2020-12-02 - 调用云函数获取openid、联表查询、分页数据
为首页获取内容设计了用户集合、表单集合 用户集合:是用户同意授权之后将userInfo信息上传该集合 表单集合是在用户授权之后,通过表单发帖 利用封装好的一个云函数(多个方法)获取openid赋值给userId作为表单合集的字段,准备与用户集合的openid联表查询 封装分页功能到同一云函数,在首页获取你所需要的用户数据 [代码]// 云函数入口文件 const cloud = require("wx-server-sdk"); cloud.init({ env: "你自己的环境名", traceUser: true, }); const db = cloud.database(); const _ = db.command; const $ = db.command.aggregate; //开始封装云函数 exports.main = async (event, context) => { switch (event.action) { case "getid": return getId(); //用户集合与表单集合的联合查询+分页数据 case "reload": return reLoad(event); } // 封装接口 user_openid 不要忘记async与await async function getId() { const wxContext = await cloud.getWXContext(); return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, }; } //联表查询+分页数据 async function reLoad(e){ //集合名称 let dbName = e.dbName; //筛选条件 默认为空 let filter = e.filter ? e.filter : null; //当前页 默认为1 let pageIndex = e.pageIndex ? e.pageIndex : 1; //数据显示条数 默认为20 let pageSize = e.pageSize ? e.pageSize : 20; //获取集合中的总记录 let countResult = await db.collection(dbName).where(filter).count(); //总记录数 let countTotal = countResult; //计算需要多少页 取整数 let totalSize = Math.ceil(countTotal / 20); //将最后的查询结果返回给小程序端 return db .collection(e.dbName) .aggregate() .lookup({ //要匹配userIndex集合的openid字段 from: "userIndex", localField: "userId", foreignField: "_openid", as: "userlist", }) .match(filter) .skip((pageIndex - 1) * pageSize) .limit(pageSize) .end() .then((res) => { return res; }) .catch((err) => { return err; }); } } [代码] 写好云函数记得部署 [图片] 点击授权触发云函数获取openid 赋值给userId [代码]wx.cloud.callFunction({ name:"云函数名", data:{ action:"getid", }, complete: (res) => { console.log("openid: ", res); this.setData({ userId: res.result.openid, }); }, } }) [代码] 截图 [图片] 之后将openid与表单数据一起上传到表单集合,之后通过openid获取两集合的联合查询数据,同时分页 [代码]wx.cloud.callFunction({ name:"云函数名", data:{ action:"reload", dbName:"表单集合名", filter:{限制条件对象}, }, success: (res) => { console.log(res); }, fail: (err) => { console.log(err); }, } }) [代码] 获取的数据截图 [图片]
2021-03-04 - 03.getUserInfo和getUserProfile 对比
最近动态 wx.getUserProFile() 在2.16.0成功回调有iv、encryptedData,具体看这里https://developers.weixin.qq.com/community/develop/doc/000c04d0490118d8a6ebf675a56c00 调整背景 很多开发者在打开小程序时就通过组件方式唤起 getUserInfo 弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。详情可以点击官方调整链接(https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801) 调整前后API功能的对比[图片] [图片] 能力检测 两个前提条件: 1.开发者工具版本不低于 1.05.21030222.基础库版本不低于 2.10.4[图片] 代码片段: https://developers.weixin.qq.com/s/odMs3wmX7Ko3 测试过程 step1: 在开发工具设置清除全部缓存step2: 点击 getUserInfo 按钮,会弹出用户授权,允许后会得到这些信息,见截图[图片] step3: 在终端输入下面代码,也可以获取上面截图数据(今天还不到截止时间,还能获取完整的用户头像和昵称)wx.getUserInfo({ complete: (res) => { console.log(res) } }) step4: 点击 getUserProfile 按钮,会弹出用户授权,允许后会得到这些信息,见截图(只有用户昵称和头像信息)[图片] step5: 通用在终端输入下面代码,获取不到任何信息,符合`若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,且开发者每次通过该接口获取用户个人信息均需用户确认`wx.getUserProfile({ complete: (res) => { console.log(res) } }) step6: 可以重复点击 getUserInfo 按钮和 getUserProfile 按钮进行测试。功能对比讲解 1.4月13日前未发布的,wx.getUserInfo 能力 wx.getUserInfo(Object object) 会返回 encryptedData、signature、rawData,通过将返回的数据传递给服务器,服务端能解析出用户的身份标识,即 unionId(unionId 获取机制:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html) 【对我们业务来说】 从 wx.getUserInfo 就是要两样东西:unionId和用户信息(头像和昵称)。 但从 2021年2月23日起,可以通过 wx.login 接口获取的登录凭证可直接换取 unionID,可以替代一部分wx.getUserInfo 的功能了。 2.新增 getUserProfile 能力 wx.getUserProfile 能获取到头像和昵称,可以替代 wx.getUserInfo 的另外一部分功能。 3.小结 从这里是不是可以得出,wx.login + wx.getUserProfile 基础可以替代之前的 4月13日前未发布的,wx.getUserInfo 能力。其实不然,如果真是这样的,官方是不是没必要这样搞,咱们接着看。 4.wx.getUserInfo 和 wx.getUserProfile 区别 1.功能上是 wx.getUserInfo 不在返回用户授权的头像昵称,只返回匿名信息,但 wx.getUserProfile 会返回用户授权的头像昵称。2.wx.getUserInfo 授权成功后,当下次调用时,可以直接获取授权成功返回数据,不需要每次都需要用户确认,但 wx.getUserProfile 每次都需要用户确认允许后才能拿到用户信息3.对于业务来说,可以通过 wx.getUserProfile 获取用户信息和昵称后,要存在自己服务器,不能像之前那样每次都通过 wx.getUserInfo 方式获取,否则体验会比较差疑问 1.4月13日后发布的新版本小程序,如果用户未更新到新版本,此时调用 wx.getUserInfo 会不会返回用户授权的头像昵称(如果不确定,业务可能需要兼容处理)2.4月13日后发布的新版本小程序,用户更新到新版本,调用 wx.getUserInfo 返回匿名的头像昵称支持服务器解密吗? 常见问题汇总 1.wx.canIUse 判断getUserProfile结果是false,可以通过直接判断 wx.getUserProfile 即可,类似问题可以查看官方知识库(https://developers.weixin.qq.com/community/develop/doc/000cac40cf0eb8d3e429647c351c09?_at=1614912876047)
2021-04-02 - getUserInfo更新为getUserProfile使用方法
wxml [代码] <button class="login-bn" type='primary' size="mini" bindtap='getUserProfile'> <image src='/assets/images/me.png'></image> <text>\n授权登录</text> </button> [代码] js [代码] getUserProfile: function (e) { wx.getUserProfile({ desc: '业务需要', success: res => { //拿到信息处理业务 } }) }, [代码] 以上就是本次登陆接口又又又又又被修改后的使用方式
2021-04-15 - 答题活动小程序抽奖概率实现方案分享
答题活动小程序抽奖概率实现方案分享 ~ 首先写这篇文章一方面为了消化今天学到的抽奖概率方案,另一方面呢,也为了梳理思路,做出权衡,希望在写完这篇文章的时候,能确定该选用哪一个方案 如果大家用过问卷星,那么肯定非常熟悉下面的图 你 [图片] 笑起来真好看 本文讨论的抽奖概率方案,就是基于上述截图的水果机,也叫大转盘,共8个奖品位,如果活动中真实奖品不够8个,那么可用谢谢参与来填充 具体的抽奖界面如下所示 [图片] 本文具体讨论的话题是 当前答题活动如下所示 你 [图片] 笑起来真好看 该活动共有五种奖品,总计是115个名额 方案1: [图片] 方案2: 你 [图片] 笑 [图片] 起来真好看 特别感谢群里参与讨论的几位同学,谢谢
2021-02-27 - 登录接口又双叕变了,三行代码挑战全网最少修改工作量
小程序登录、用户信息样关接口又双叕变了。 https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801 几家悲伤几家愁。。。 微信的一小步,人猿的一大步。。。 没办法,改吧。。。 翻出以前小程序这部分的代码,惊喜地发现,只需要三行代码,就能平滑过渡; 感谢我以前看似丑陋却很省事的登录代码逻辑!!! 登录逻辑如下: 1、判断库里有用户的信息没有,没有,则wx.navigateTo一个专门的授权页面:auth 2、授权成功后获得userInfo,保存到库里; auth页代码修改如下: auth.wxml: 修改一行代码 <button style='margin:15px;font-size:16px' type='primary' size="mini" bindtap='getUserProfile'>授权微信头像和昵称</button> auth.js: 修改两行代码 //原wx.getUserInfo接口 getUserInfo: function (e) { let userInfo = e.detail.userInfo if (userInfo) this.onSaveUserInfo(userInfo) }, //新增wx.getUserProfile接口 getUserProfile: function (e) { wx.getUserProfile({ desc: '业务需要', success: res => this.onSaveUserInfo(res.userInfo) }) }, //保存userInfo到DB onSaveUserInfo:function(userInfo){ console.log(app.globalData.userInfo = userInfo) db.collection('user') .where({ _id: this.openid }) .count() .then(res => { if (res.total > 0) { //doc.update db.collection('user').doc(this.openid).update({ data: userInfo }).then(res => console.log(res)) } else { //doc.add db.collection('user').doc(this.openid).add({ data: userInfo }).then(res => console.log(res)) } }) wx.navigateBack() }, 以下是判断用户信息是否存在的代码: xxxx.js: onSubmit:async function () { if (await app.hasUserInfo()) { } else return //其他代码 }, app.js: hasUserInfo: async function () { if (this.globalData.userInfo && this.globalData.userInfo.nickName && this.globalData.userInfo.avatarUrl) return true let res = await wx.cloud.database().collection('user').doc(this.openid).get().catch(err => console.log(err)) if (res && res.data && res.data.nickName && res.data.avatarUrl) { this.globalData.userInfo = res.data return true } else { wx.navigateTo({ url: '/base/auth/auth' }) return false } }, 关于用户信息自动更新: 我们一直以来的方法如下: 1、留给用户手动授权的入口,用户更换头像后,发现自己的头像不显示,则需要手动授权刷新userInfo; 2、一般会在这个页面:我的--个人信息--授权微信头像和昵称,用户点击后,wx.navigateTo到授权页。 笔者团队认为:用户信息自动更新其实是个伪需求。理由如下: 假设某用户修改了头像: 1、用户自己打开小程序,发现头像和昵称怎么没有改过来,那么手动更新一下。用户体验没毛病,没必要非要自动更新; 2、用户如果后来不再进入小程序,别人看到的都是一张碎的头像,那么此时,自动更新也毫无作用,因为该用户都不打开小程序。 3、微信团队肯定考虑过自动更新这种要求,但他们宁愿千夫所指,也依然坚持推出新的登录接口,那就肯定是已经经过了中国最牛逼团队的全面考衡了。 补充: 登录和授权其实是两码事,可以毫无关系这么说,以上的内容主要都是关于授权微信用户信息的,下面补充一下登录的内容: 登录其实就是获取用户的openid,我们一直采用云函数来获取openid。方案如下: 在每个页面:page.js: onLoad: async function (options) { this.openid = await app.getOpenid() }, 在app.js: getOpenid: async function () { if (this.openid) return this.openid let res = await this.globalData.cloud.callFunction({ name: 'login' }) console.log(res) return this.openid = res.result.FROM_OPENID||res.result.OPENID },
2021-04-07 - 推荐 4 款值得学习的小程序源码
头像小程序 [图片] 技术:基于uniapp使用vue快速实现。 功能:头像加口罩、头像加字、头像加福、聊天背景图、生日宇宙图 地址:https://github.com/infinityu/mina-wear-mask 租房小程序 [图片] 技术:基于Cloud Base(TCB)云开发,小程序端集成了管理后台。 功能:用户可以发布新房、二手房、租房等委托,中介机构审核发布、推荐,客户挑好房子后可以直接中介或者房源发布者。 地址:https://github.com/lx164/house 优惠券小程序 [图片] 技术:源码为uniapp项目,需下载hbuilder导入项目打包 功能:美团饿了吗CPS红包小程序 地址:https://github.com/zwpro/coupons 王者荣耀故事站小程序 [图片] 技术:小程序 + nuxt + koa2 + vue2.0 + vuex + nginx + pm2 功能:显示所有王者荣耀的故事 地址:https://github.com/naihe138/heroStory
2021-02-25 - 抽奖活动小程序如何兑奖逻辑简析
抽奖活动小程序如何兑奖逻辑简析 ~ 前几天在我的抽奖活动小程序沟通见面会上,有一个朋友提出下面这个问题 用户中奖后,如果微信昵称和头像发生了变更的话,如何辨别用户的真实性 你 [图片] 笑 起 来 首先我讲下目前我的抽奖活动小程序的兑奖操作 在用户中奖结果页,会有客服入口,直接跟运营者进行联系,能交给运营者证明自己中奖的也只有当前结果页的中奖截图,运营者通过该截图判断是否是中间者本人, 这个场景中,确实存在上述的问题 ~~ 这确实是个好问题,暴露了我的抽奖活动小程序在兑奖环节的不够精细,这也提醒了我后续抽奖活动小程序开发该往什么方向走 ,我调研了目前头部的几个抽奖小程序, 抽奖助手是采用兑奖码的形式,给运营者来核销,具体如下截图所示 你 [图片] 笑 [图片] 起 来 ~ 目前头部抽奖就抽奖助手和活动抽奖,上面几个截图为抽奖助手界面截图,下面几个截图为活动抽奖的界面截图 你 [图片] 笑 [图片] 起 [图片] 来 真 通过上述截图,我们不难看出,抽奖助手和活动抽奖,均是通过对接码来进行核销的。
2021-02-25 - 快速实现 添加到我的小程序 - menu-popover
menu-popover [代码]menu-popover[代码],快速实现 添加到我的小程序 先看效果 [图片] [图片] 胶囊气泡 扫码体验 使用方法 大致可以分为 2 步: npm 安装 [代码]mina-popups[代码],开发工具构建 npm 引入并使用 [代码]mina-popups/menu-popover[代码] 组件 命令行 [代码]npm install mina-popups[代码] 安装完成后,开发工具构建 npm *.json 引入组件 [代码]{ "usingComponents": { "menu-popover": "mina-popups/menu-popover" } } [代码] *.wxml 使用组件 [代码]<menu-popover show="{{show}}"> <view class="popover-inner"> <text class="tip">点击 ...“添加到我的小程序”\n领币更方便</text> <view class="close" bindtap="close">X</view> </view> </menu-popover> [代码] 属性 类型 默认值 描述 show Boolean false 是否显示 popover 以上简单几步即可实现 引导添加我的小程序 具体使用请查看 Github https://github.com/Yrobot/mina-popups 如果喜欢 mina-popups,记得在 github 点个 start 哦!🌟🌟🌟
2021-02-23 - 校友会小程序开发笔记十三: 小程序前端缓存体系的设计与实现
存储每个校友录小程序都可以有自己的本地缓存,可以通过 wx.setStorage/wx.setStorageSync、wx.getStorage/wx.getStorageSync、wx.clearStorage/wx.clearStorageSync,wx.removeStorage/wx.removeStorageSync 对本地校友录小程序缓存进行读写和清理。 校友会小程序隔离策略同一个微信校友录小程序用户,同一个校友录小程序 storage 上限为 10MB。storage 以校友录用户维度隔离,同一台设备上,A 用户无法读取到 B 用户的数据;不同小程序之间也无法互相读写数据。 插件隔离策略同一校友录小程序使用不同插件:不同插件之间,插件与小程序之间 storage 不互通。 不同校友录小程序使用同一插件:同一插件 storage 不互通。 清理策略 本地缓存的清理时机跟代码包一样,只有在校友录小程序代码包被清理的时候本地缓存才会被清理。 校友会小程序缓存方法的类封装微信缓存二次封装,有设置时效性的封装 /** * 写校友录小程序缓存 * k 键key * v 值value * t 秒(有效时间) */ function set(k, v, t = 86400 * 30) { if (!k) return null; wx.setStorageSync(k, v); let seconds = parseInt(t); if (seconds > 0) { let newtime = Date.parse(new Date()); newtime = newtime / 1000 + seconds; wx.setStorageSync(k + TIME_SUFFIX, newtime + ""); } else { wx.removeStorageSync(k + TIME_SUFFIX); } } /** * 获取校友录小程序缓存 * k 键key * def 默认值 */ function get(k, def = null) { if (!k) return null; let deadtime = parseInt(wx.getStorageSync(k + TIME_SUFFIX)); if (deadtime) { if (parseInt(deadtime) < Date.parse(new Date()) / 1000) { wx.removeStorageSync(k); wx.removeStorageSync(k + TIME_SUFFIX); return def; } } let res = wx.getStorageSync(k); if (res) { return res; } else { return def; } } /** * 删除校友录小程序缓存 * k键值 */ function remove(k) { if (!k) return null; wx.removeStorageSync(k); wx.removeStorageSync(k + TIME_SUFFIX); } /** * 清除所有校友录小程序key */ function clear() { wx.clearStorageSync(); } 代码网址: https://gitee.com/cclinux2/cc-alumni
2021-01-07 - 小程序的各种炫酷样式、炫酷动画
收集了上百种的小程序样式,拿来即用,源码公开 各种各样的样式都有,只有你想不到,没有做不到的 什么样的场景都有 源码公开,自行下载 可通过扫描二维码查看样式效果 ## 1、初衷就是收集,后续也会一直的收集并更新下去 *** ## 2、项目只在小程序上测试过,其他平台还需自行测试使用 *** ## 3、如果你有好的样式,可以发送到我的邮箱 1228742150@qq.com *** ## 4、部分样式是从别的地方拿过来的,我也有注明出处,如果你发布到其他的地方,也请尊重别人的劳动成果,注明出处。 *** ## 5、如有任何冒犯的地方,可联系作者 [图片]
2021-07-24 - mina-popups 小程序弹出组件集合
mina-popups [代码]mina-popups[代码],一个方便、轻量的 小程序 弹出组件集合 change log: 2021.02.22 init package 层叠顺序规范 mask: 100 popups: 200 所以 page 下一层的业务样式层叠顺序层级应 < 100 主要的组件 popup 组件整合 [代码]popup[代码] 的通用逻辑:弹出位置,背景 mask,函数式控制显隐 并对 fixed 模式升级,不仅支持直接传入 left、top 控制 [代码]popup[代码] 位置,还支持传入 selector 自动设置 [代码]popup[代码] 位置 [图片] [图片] [图片] [图片] [图片] [代码]left[代码] [代码]top[代码] [代码]right[代码] [代码]bottom[代码] [代码]center[代码] [图片] [图片] [图片] [代码]fixed[代码] [代码]selector[代码] [代码]fixed[代码] [代码]left&top[代码] popover 在 [代码]popup[代码] 的基础上,完善气泡菜单的通用逻辑 使用者只需要在 slot 里添加提示或者菜单内容即可 [代码]popover[代码] 会根据触发位置自动改变展示方向 [图片] [图片] [图片] 气泡菜单 tooltip menu-popover 在 [代码]popover[代码] 的基础上,针对小程序引导添加我的小程序的场景,自动将 popover 定位到小程序胶囊下方 组件自动识别页面 [代码]navigationStyle: custom[代码] 属性,优化展示位,使用者无需关心适配问题 [图片] [图片] 胶囊气泡 使用方法 大致可以分为 2 步: npm 安装 [代码]mina-popups[代码],开发工具构建 npm 引入并使用 [代码]mina-popups[代码] 组件 命令行 [代码]npm install mina-popups[代码] 安装完成后,开发工具构建 npm *.json [代码]{ "usingComponents": { "popup": "mina-popups/popup", "popover": "mina-popups/popover", "menu-popover": "mina-popups/menu-popover" } } [代码] *.wxml 在 view 上利用 popups 处理渲染逻辑 具体属性使用介绍请点击文章下方Github连接进行查看 [代码]<popup show="{{popup.show}}" position="{{popup.position}}" mask="{{popup.mask}}" catchScroll="{{popup.catchScroll}}" tapMaskClose="{{popup.tapMaskClose}}" scrollMaskClose="{{popup.scrollMaskClose}}" maskColor="{{popup.maskColor}}" selector="{{popup.selector}}" left="{{popup.left}}" top="{{popup.top}}" unit="{{popup.unit}}" bind:position="position" > <!-- popup-inner-wxml --> </popup> <popover show="{{popover.show}}" mask="{{popover.mask}}" catchScroll="{{popover.catchScroll}}" tapMaskClose="{{popover.tapMaskClose}}" scrollMaskClose="{{popover.scrollMaskClose}}" maskColor="{{popover.maskColor}}" left="{{popover.left}}" top="{{popover.top}}" unit="{{popover.unit}}" translateX="{{popover.translateX}}" > <!-- popover-inner-wxml --> </popover> <menu-popover show="{{show}}"> <!-- menu-popover-inner-wxml --> </menu-popover> [代码] 具体属性使用介绍请点击文章下方Github连接进行查看 注意事项 popups 对于层叠顺序的设计为:mask-100,popup-200,所以为了保证 popups 在页面不被遮挡,Page 下一层的业务样式层叠顺序不要超过 100。 以上简单几步即可使用 mina-popups 具体使用请查看Github https://github.com/Yrobot/mina-popups 如果喜欢mina-popups,记得在github点个 start 哦!🌟🌟🌟
2021-02-22 - [开盖即食]小程序Canvas官方新版API实战
[图片] [图片] 最近本人在开发一个新项目的时候,注意到官方在2.9.0开始支持了一个canvas 2D的新API,同时对webGL上支持也有了很大的改进,相信很多人用canvas的组件做一些分享海报,战绩和新闻帖功能。 [图片] 这里是新的引入方式。 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html 那么新的canvas2D API有啥好处呢? 原本的API微信有做一定的修改,现在全面支持源生H5 JS的写法,迁移H5的老代码变成更加容易,学习成本更低 修复了一些诡异的BUG,例如原本在IOS早期版本写法顺序会导致clip()图片裁切失效等~ 性能上的优化和提升,复杂动画上帧数明显 举例写法上的一些改变: 1、设置font的写法: [代码]//原本(传值的写法) ctx.setFontSize(20); ctx.fillText('MINA', 100, 100) ctx.draw() //现在(和源生H5写法一致,赋值) ctx.font = "16px"; ctx.fillStyle = 'blue'; //可以直接写颜色,原本的不支持 //不需要 ctx.draw() [代码] 2、获取并添加图片写法: [代码]//原本 //使用的是 wx.getImageInfo的方法 wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { console.log(res); ctx.drawImage(res.path, 0, 0); ctx.draw(true); }, fail: function (res) { //失败回调 } }); //现在 //可以直接img.onload调用 const headerImg = canvas.createImage(); headerImg.src = headImage;//微信请求返回头像 headerImg.onload = () => { ctx.save(); ctx.beginPath()//开始创建一个路径 ctx.arc(38, 288, 18, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.drawImage(headerImg,0,0); ctx.closePath(); ctx.restore(); } [代码] 3、将canvas生成虚拟地址便于下载(重点): [图片] 由于官方文档没有写清楚,误导了挺多人的。这里canvas对象必须通过选择器获取,并获得对应的node节点。 [代码]async saveImg() { let self = this; //这里是重点 新版本的type 2d 获取方法 const query = wx.createSelectorQuery(); const canvasObj = await new Promise((resolve, reject) => { query.select('#posterCanvas') .fields({ node: true, size: true }) .exec(async (res) => { resolve(res[0].node); }) }); console.log(canvasObj); wx.canvasToTempFilePath({ //fileType: 'jpg', //canvasId: 'posterCanvas', //之前的写法 canvas: canvasObj, //现在的写法 success: (res) => { console.log(res); self.setData({ canClose: true }); //保存图片 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '已保存到相册', icon: 'success', duration: 2000 }) // setTimeout(() => { // self.setData({show: false}) // }, 6000); }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") } else { util.showToast("请截屏保存分享"); } }, complete(res) { wx.hideLoading(); console.log(res); } }) }, fail(res) { console.log(res); } }, this) }, [代码] 分享个canvas海报的代码片段: [图片] 片段名: PoCf4emw7TgE 片段link: https://developers.weixin.qq.com/s/PoCf4emw7TgE [图片] [图片] 总结,相对之前还要看官方文档的canvas自定义API,现在写起来更加的方便,老代码迁移起来得心应手,只要你之前会canvas,那么各种效果和动画,拿来就怼,没什么大问题~ 一些奇怪的问题(注意!!!) canvas 2d 目前(2020年4月3日)还不支持真机调试,会报错!!! IDE工具 1.02.2003190 直接保存新版本canvas的API图片是打不开的,但是直接用手机保存在相册是没问题的,请更新到1.02.2003250 最新版即可解决~ 一些老款手机用新的API保存图片会有报错问题,如华为NOTE10,请更新系统到能支持的最新,且微信也是,即可解决~ 部分Android设备诡异的闪退和报错 [图片] 这种有可能是代码写法的问题,比如: [代码]//缺省写法 会导致部分Android机器 闪退 ctx.font = "bold 16px"; ctx.fillStyle = "#000" //在canvas 2D的写法中,所以写法必须规范且完整 ctx.font = "normal bold 12px sans-serif"; ctx.fillStyle = '#707070'; [代码] 所以在canvas 2D 的环境,所以写法必须原始且规范,不能用缺省写法,不然就会有诡异的闪退/报错。 后续:官方在7.0.13的Android版本已修复。 https://developers.weixin.qq.com/community/develop/doc/00088c13e1437890692afd8d85ec00 看完觉得有帮助记得点个赞哦~ 你的赞是我继续分享的最大动力!^-^
2020-05-09 - 小程序-swiper高度自适应,改变默认高度,嵌套scroll-view【转载】
swiper高度问题一直困扰我。今天终于有时间来解决一下。因为他的高度不能固定死,写死其他数据展示不完全,不写或者100%,auto都不行。翻了一堆资料也查了很多,最后总结一下。 1.很多人都说用一种方法。就是高度*数量,也就是所说的获取数据数组长度,根据数据长度来动态改变每页的长度,因为字号啊什么的在各个手机显示不一定都相同,总感觉不是解决问题的最佳方法。 2.使用Swiper+scroll-view 先设置swiper高度 <swiper style="height: {{clientHeight?clientHeight+'px':'auto'}}" class='videoSwiper' current="{{currentTab}}" duration="200" bindchange="swiperchange"></swiper> 在swiper-item中嵌套一个scroll-view <swiper-item > <scroll-view scroll-y="{{true}}" style="height: {{clientHeight?clientHeight+'px':'auto'}}" bindscrolltolower="scrollbot"> </scroll-view> </swiper-item > 在js中获取设备可视窗口高度(我是写在onload里的) onLoad:function(){ var that = this wx.getSystemInfo({ success: function (res) { that.setData({ clientHeight: res.windowHeight }); } }) } 切换的js swiperchange: function(e) { var that = this console.log(e.detail.current) that.setData({ 'currentTab': e.detail.current }) }, 这就可以完美解决了。 PS:上述经本人亲自测试,是成功的!为便于查找,故转载存档,原址(https://blog.csdn.net/u012054869/article/details/88018966),感谢原作者的倾力奉献!
2020-11-01 - 用picker自定义的省市区选择器,数据用公司自己的数据
最近做地址选择器,考虑到不宜大动后台项目,所以地区数据用的是去年8月自己从高德地图导出的数据,为了契合这个情况,所以用picker写了一个mode为multiSelector的省市区选择器。文章发在了segmentfault,对地址数据获取及处理封装,地址选择三级联动逻辑,取消事件,及数据回显都有提及。话不多说,上传送门: https://segmentfault.com/a/1190000039183917
2021-02-07 - 延迟显示loading组件
一、npm包介绍 可控制延迟显示的微信小程序 loading 组件,默认请求超过0.5s才显示loading动画;支持 slot 自定义 loading 显示内容。 在项目中,若网络良好的情况下,每次请求都显示loading动画,会导致页面短时间内频繁闪现loading动画,用户体验不佳。本组件可自定义请求延时,只有当请求超过设置的时间未完成时,才显示loading动画,减少loading动画出现的次数。 点击查看 demo 二、使用 1.安装 [代码]npm i wx-delay-loading[代码] 2.组件初始化:在 app.js-onLaunch 执行组件实例初始化方法,并传入用于控制 loading 组件显示隐藏的页面 data 内变量的名称(注:参数类型为string,本例使用 isLoading) [代码]// app.js import DelayLoading from 'wx-delay-loading/utils' App({ onLaunch: function () { const Loading = DelayLoading.getInstance() Loading.initComponent('isLoading') // 注意:此处没有写错,传入的是 key,而不是 value } }) [代码] 3.在使用组件的页面或组件的 json 配置内,引入组件(注:微信小程序组件名不允许使用 wx 做前缀) [代码]// page.json "usingComponents": { // 微信小程序组件名不允许使用wx做前缀 "delay-loading": "wx-delay-loading/index" } [代码] 4.在页面 wxml 中使用, 注:isShow 属性传入的页面属性必须对应第1步 initComponent 传入的参数名(本例使用 isLoading) [代码]// page.wxml // 不使用 slot <delay-loading isShow="{{isLoading}}" /> // 使用 slot 自定义内容 <delay-loading customLoading="{{true}}" isShow="{{isLoading}}"> <view class="container"> <image class="logo" src="/static/image/logo.png" mode="widthFix" /> <view class="text">加载中...</view> </view> </delay-loading> [代码] 5.请求开始时(例如 wx.request),调用实例方法 setDelayLoading(delaytime) delaytime 默认为500,即 0.5s; 请求结束时,调用实例方法 checkReqCountClear() [代码]// page.js import DelayLoading from 'wx-delay-loading/utils' const Loading = DelayLoading.getInstance() Page({ data: { isLoading: false }, // 仅为示例 exampleRequest () { // 请求开始 Loading.setDelayLoading(300) // 请求超过0.3秒没完成,显示 loading 组件 wx.request({ url: 'https://example.com/getData', complete () { // 请求完成 Loading.checkReqCountClear() } }) }, }) [代码] 进阶:在统一封装请求 request.js 内使用 项目开发中,通常会针对请求和响应进行统一处理,封装成一个 request.js 使用。注意:在使用组件的页面内依然要保留传递给 isShow 属性值的 data 属性 [代码]// request.js import DelayLoading from 'wx-delay-loading/utils' const Loading = DelayLoading.getInstance() const request = (options) => { return new Promise ((resolve, reject) => { // 请求开始前调用设置延时 Loading.setDelayLoading() wx.request({ ...options, success (res) { // 请求成功后的各种处理操作... resolve(res.data) }, fail (err) { // 请求失败后的各种处理操作... reject(err) }, complete () { // 请求完成 Loading.checkReqCountClear() } }) }) } export default request [代码] [代码]// page.js import request from request.js Page({ data: { isLoading: false }, // 仅为示例 exampleRequest () { // 使用封装后的request request({ url: 'https://example.com/getData' }).then(res => { // 对返回数据的处理... }) }, }) [代码] 三、调试——模拟低网速情况 通常在网络环境良好的情况下,请求都会很快完成,不会超过0.5s。可以通过微信开发者工具-调试器-Network,把网络设置 Online,更改为 Slow 3G,或者使用 Custom 自定义网络速度。 四、文档 1、组件 options 参数 说明 类型 默认值 customLoading 是否使用 slot 插槽自定义 loading 内容 boolean false isShow 是否显示 loading boolean false 2、实例 methods 方法名 说明 参数 参数类型 返回值 getInstance 调用其它方法前,获取唯一实例 - - 实例 object initComponent 全局安装组件,挂载必要属性 页面 data 内传入组件属性 isShow 的变量的名称(告知组件,你使用 data 哪个属性控制组件显示隐藏,必须与传入组件的 isShow 的属性对应) string - setDelayLoading 标记请求开始并设置延迟显示的时间 延迟的时间,单位毫秒 number - checkReqCountClear 检测正在进行的请求数,若清零则隐藏 loading 组件 - - - 五、示例 点击查看 demo
2021-03-01 - 小程序搜索功能,云开发搜索,小程序云开发模糊搜索,同时搜索多个字段
今天来给大家讲讲小程序的搜索功能。我这里后台数据库用的是小程序云开发的云数据库。所以我们搜索的时候就要借助云开发来实现。 一,需求 比如我这里有如下的一些数据 [图片] 我们想实现如下搜索需求 1,搜索标题(title)包含‘小石头’的数据 2,搜索标题(title)或者描述(desc)包含‘小石头’的数据 3,搜索标题(title)描述(desc)都包含‘小石头’的数据 我们知道数据库查询的时候有个where语句,但是where语句是查询某个字段全部包含你输入的内容时才可以,所以单纯用where语句来做搜索的话,结果太单一。所以我们今天就来学习下模糊搜索功能的实现。我们以上面三个需求为例,来一个个讲解。 二,实现原理 我们做模糊搜索的时候,其实就是查询某个字段里是否包含我们的搜索词。而模糊搜索需要借助RegExp,来看看RegExp是什么。 [图片] 官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/Database.RegExp.html 再来看看官方示例 [图片] 可能看官方示例会有点糊涂,那么我们接下来就结合具体代码来给大家做下讲解。 三,模糊搜索的代码实现 3-1,模糊搜索单个字段 需求:搜索标题(title)包含‘小石头’的数据 代码如下 [图片] 查询结果如下: [图片] 可以看到我们成功的查询到了标题里包含‘小石头的数据’ 3-2,模糊搜索多个字段(满足一个即可) 需求:搜索标题(title)或者描述(desc)包含‘小石头’的数据 由于我们要查询多个字段,所以我们这里用到了command高级操作符里的or [图片] 代码如下: [图片] 查询结果: [图片] 我们来分析下这两条数据 1,标题和描述都包含‘小石头’,符合 2,虽然标题里没有‘小石头’,但是描述里有,所以也符合。 3,title和desc里都没有‘小石头’,所以不符合。 [图片] 3-3,模糊搜索多个字段(要同时满足) 需求:搜索标题(title)描述(desc)都包含‘小石头’的数据 由于我们要查询多个字段,所以我们这里用到了command高级操作符里的and [图片] 代码如下: [图片] 查询结果: [图片] 我们来分析下这两条数据 1,标题和描述都包含‘小石头’,符合 2,虽然desc里没有‘小石头’,但是title里没有,所以也不符合。 3,title和desc里都没有‘小石头’,所以也不符合。 [图片] 四,源码 为例方便大家使用,我把完整的代码贴到这里,后面大家使用时,直接复制这里的代码,略微改造下就可以了。 [代码] //我这里简单起见就把搜索词写死,正常应该用户输入的 let searchKey = '小石头' let db = wx.cloud.database() let _ = db.command db.collection('news') .where(_.or([ {//标题 title: db.RegExp({ //使用正则查询,实现对搜索的模糊查询 regexp: searchKey, options: 'i', //大小写不区分 }), }, {//描述 desc: db.RegExp({ regexp: searchKey, options: 'i', }), } ])).get() .then(res => { console.log('查询成功', res) }) .catch(res => { console.log('查询失败', res) }) [代码] 到这里就讲完了,我后面会专门在云开发入门的课程里作为实战案例录制视频给到大家的: 《小程序云开发入门视频》
2021-02-08 - 新的canvas的drawImage不支持本地tmp路径的临时文件吗?
【貌似官方修复了这个问题。现在可以了。】 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.drawImage.html drawImage( )不支持本地tmp路径的临时文件,谨慎使用,太坑了。 写法1. ``` path = 'tmp/wxf65e9ae5f68283d2.o6zAJs5h4IkyHaGS7_j6gUPGTR9c.arwyj04Eq2ok341457e272957e237fa21d743912f60b.jpg' ctx.drawImage(path, 0, 0, width, height, 0, 0, canvasWidth, canvasHeight) ``` 提示:Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The provided value is not of type '(CSSImageValue or HTMLImageElement or SVGImageElement or HTMLVideoElement or HTMLCanvasElement or ImageBitmap or OffscreenCanvas)';at SelectorQuery callback function 写法2. ``` const img = canvas.createImage() img.src = path img.onload = () =>{ ctx.drawImage(img, 0, 0, width, height, 0, 0, canvasWidth, canvasHeight) } ``` 提示:tmp/wxf65e9ae5f68283d2.o6zAJs5h4IkyHaGS7_j6gUPGTR9c.arwyj04Eq2ok341457e272957e237fa21d743912f60b.jpg:1 GET http://tmp/wxf65e9ae5f68283d2.o6zAJs5h4IkyHaGS7_j6gUPGTR9c.arwyj04Eq2ok341457e272957e237fa21d743912f60b.jpg net::ERR_PROXY_CONNECTION_FAILED
2020-03-29 - 小程序canvas绘制海报
2020年第一篇文章,年初忙着复习刷题一直没空去写东西,书看的越多感觉越技不如人,始终徘徊在小菜鸡的行列中,最近项目里正好有一个canvas的业务,突然又燃起了我一个UI前端的火种,记下了踩坑和思考🤔。 踩坑💥 问题1:为什么在canvas上画图片模糊? 在canvas上绘制图片/文字的时候,我们设定canvas:375*667的宽高,会发现绘制出来的图片很模糊,感觉像是一张分辨率很差的图片,文字看起来也会有叠影。 [图片] 注意:物理像素是指手机屏幕上显示的最小单元,而设备独立像素(逻辑像素)计算机设备中的一个点,css 中设置的像素指的就是该像素。 原因:在前端开发中我们知道一个属性叫[代码]devicePixelRatio(设备像素比)[代码],该属性决定了在渲染界面时会用几个(通常是2个)物理像素来渲染一个设备独立像素。 举个例,一张100*100像素大小的图片,在retina屏幕下,会用2个像素点去渲染图片的一个像素点,相当于图片放大了一倍,因此图片会变得模糊,这也是1px在retina 屏上变粗的原因。 [图片] 解决: 将canvas-width和canvas-height都放大2倍,在通过style将canvas的显示width,height缩小2 倍. 例如: [代码]<canvas width="320" height="180" style="width:160px;height:90px;"></canvas> [代码] 问题2:如何处理px和rpx的转换? rpx是小程序里特有的尺寸单位,可以根据屏幕的宽度进行自适应,而在iphone6/iphonex上,1rpx等于不同的px。所以很可能会导致在不同手机下,你的canvas展示不一致。 在绘制海报的之前,我们拿到的设计稿一般都是基于iphone6的2倍图。而且从上一个问题的解决,我们知道canvas的大小也是2倍的,所以我们可以直接量取2倍图的设计稿直接绘制canvas,而尺寸需要注意一下rpxtoPx. [代码]/** * * @param {*} rpx * @param {*} int //是否变成整数 factor => 0.5 //iphone6 pixelRatio => 2 像素比 */ toPx(rpx, int) { if (int) { return parseInt(rpx * this.factor * this.pixelRatio) } return rpx * this.factor * this.pixelRatio } [代码] 问题3:关于canvasContext.measureText计算纯数字的时候手机上为0 在小程序中提供[代码]this.ctx.measureText(text).width[代码]去计算文本的长度,但是如果你全[代码]数字[代码] 的话,你会发现该API永远都计算成0.所以,最后采用模拟measureText方法去计算文本长度。 [代码]measureText(text, fontSize = 10) { text = String(text) text = text.split('') let width = 0 text.forEach(function(item) { if (/[a-zA-Z]/.test(item)) { width += 7 } else if (/[0-9]/.test(item)) { width += 5.5 } else if (/\./.test(item)) { width += 2.7 } else if (/-/.test(item)) { width += 3.25 } else if (/[\u4e00-\u9fa5]/.test(item)) { // 中文匹配 width += 10 } else if (/\(|\)/.test(item)) { width += 3.73 } else if (/\s/.test(item)) { width += 2.5 } else if (/%/.test(item)) { width += 8 } else { width += 10 } }) return width * fontSize / 10 } [代码] 问题4:如何保证一行字体的居中展示?多行呢? 字体的如果过长,会超出canvas画布,造成绘制难看,这个时候我们就应该让超出的部分变成[代码]...[代码] 你可以设置一个width并且循环计算计算出文本的宽度,如果超出则利用substring截取后补充[代码]...[代码]即可。 [代码]let fillText='' let width = 350 for (let i = 0; i <= text.length - 1; i++) { // 将文字转为数组,一行文字一个元素 fillText = fillText + text[i] // 判断截断的位置 if (this.measureText(fillText, this.toPx(fontSize, true)) >= width) { if (line === lineNum) { if (i !== text.length - 1) { fillText = fillText.substring(0, fillText.length - 1) + '...' } } if (line <= lineNum) { textArr.push(fillText) } fillText = '' line++ } else { if (line <= lineNum) { if (i === text.length - 1) { textArr.push(fillText) } } } } [代码] 文字剧中展示计算公式: 居中在canvas中可以用(canvas的宽度-文字宽度)/2 + x (x为字体的x轴的推移) [代码]let w = this.measureText(text, this.toPx(fontSize, true)) this.ctx.fillText(text, this.toPx((this.canvas.width - w) / 2 + x), this.toPx(y + (lineHeight || fontSize) * index)) [代码] 问题5:在小程序中如何处理网络图? 关于在小程序里使用网络图片,比如cdn上的图片,是需要down到微信本地进行 LRU 管理,让后续绘制同样图片时,节省下载时间。所以首先需要你在微信小程序的后台配置downloadFile合法域名,其次你可以在canvas绘制之前,最好提前去down图片,等待图片下载好了,再开始绘制,以避免一些绘制失败的问题。 问题6:在 IDE 中可设置 base64 的图片数据进行绘制,但真机上无用? 先把 base64 转成 [代码]Uint8ClampedArray[代码] 格式。然后再通过 [代码]wx.canvasPutImageData(OBJECT, this)[代码] 绘制到画布上,然后把画布导出为图片。 <!–### 问题6:如何画一个圆角图片?–> 问题7:关于wx.canvasToTempFilePath 使用 Canvas 绘图成功后,直接调用该方法生成图片,在IDE上没有问题,但在真机上会出现生成的图片不完整的情况,可以使用一个setTimeout来解决这个问题。 [代码]this.ctx.draw(false, () => { setTimeout(() => { Taro.canvasToTempFilePath({ canvasId: 'canvasid', success: async(res) => { this.props.onSavePoster(res.tempFilePath)//回调事件 // 清空画布 this.ctx.clearRect(0, 0, canvas_width, canvas_height) }, fail: (err) => { console.log(err) } }, this.$scope) }, time) }) [代码] 问题8:关于canvasContext.font fontsize 不能使用小数 如果设置 font 中字体大小部分包含小数,则会导致整个 font 设置无效。 问题9:安卓下字体渲染错位? [图片] 这个问题出现在安卓手机上,ios表现正常。一开始看到这个问题,摸不着头脑,为什么有的正常居中有的却往前了很多。后面发现是安卓下[代码]this.ctx.setTextAlign(textAlign)[代码] 默认是为center,所以导致了错乱,改成left后就正常了。 问题10:绘制一个折线图 [图片] 利用canvas绘制一个简单的折线图,只需要利用[代码]lineTo[代码]和[代码]moveTo[代码]俩个API将点连接即可。利用[代码]createLinearGradient[代码]绘制阴影。 思考💡 思考1:用json配置表生成海报的局限 现在的海报生成只需要按照设计稿去量取尺寸就可以,但是量取的过程还是很繁琐的,在设计稿量不到的地方还需要手动微调一下。 后续还可以做一个web端使用拖拽的方式去完成设计稿的事情,自动生成json应用到小程序的海报上。 思考2:后端生成海报的局限 海报一开始是后端同学生成的,优点是不需要前端绘制时间,也不需要去踩微信API的坑,接口返回拿到url即可展示,但是在后端生成出来的效果不佳,毕竟这种工作更加前端一些。 思考3:前端生成海报的局限 前端生成海报的时候我发现耗时更长,包括图片的下载本地而且还需要给安卓一个特意写一个setTimeout去确保绘制正常。各种兼容性问题、手机的dpr、安卓和ios等不间断彩蛋踩到你头秃~ 哈哈哈哈~ 彩蛋 采用了最新的canvas-2d背景图确无法绘制全部? 在canvas开发的过程中,小程序里一直有一束微光提醒我。 [图片] 我也试了试最新的canvas2d的api,的确同步了web端,写法也更流畅,在开发者工具中看是一切正常,跑在手机上则,只显示宽度的一半在各种机型下测试也是一样。 [图片] 后面改成原始的canvas就又好了。。。具体原因也还没有在微信社区里找到,后续迭代升级的时候再研究阿吧啊吧啊吧。 [图片]
2020-07-09 - 如何从零实现上拉无限加载瀑布流组件
代码已优化请查看另外一篇文章 https://developers.weixin.qq.com/community/develop/article/doc/00026c521ece40c2d2db97f7156013 小程序瀑布流组件 前言:为了实现这个组件也花费了些时间,以前也做过瀑布流的功能,不过是利用 js 去 计算图片的高度,然后通过 css 的绝对定位去改变位置。不过这种要提前加载完一个列 表的图片,然后通过排列的算法生成排序的数组。总之就是太复杂了,后来在网上也看到 纯 css 实现,比如 flex 两列布局,columns 等,不做过多的阐述,下面分享下自己项 目中实现的瀑布流过程。 Css Grid 布局 Css3 变量属性 Js 动态修改 css 变量属性 Wxs 小程序脚本语言 Wxml 节点 Api Component 自定义组件 效果图 代码片段 [图片] Css Grid 网格布局实现多列多行布局 [代码]<view class="c-waterfall"> <view wx:for="{{ 10 }}" wx:key="item" class="view-container" > {{ item }} </view> </view> [代码] [代码].c-waterfall { display: grid; grid-template-columns: repeat(2, 1fr); grid-auto-flow: row dense; grid-auto-rows: 10px; grid-gap: 10px; } .view-container { width: 100%; grid-row: auto / span 20; } [代码] Css3 变量,可以通过[代码]js动态[代码]改变 [代码].c-waterfall { --grid-span: 10; --grid-column: 2; --grid-gap: 10px; --grid-rows: 10px; width: 100%; display: grid; grid-template-columns: repeat(var(--grid-column), 1fr); grid-auto-flow: row dense; grid-auto-rows: var(--grid-rows); grid-gap: var(--grid-gap); } .view-container { width: 100%; grid-row: auto / span var(--grid-span); } [代码] 动态修改 css 变量,实现遍历的节点都有独立的样式 [代码]<view class="c-waterfall" style="{{ style }}"> <view wx:for="{{ 10 }}" wx:key="item" class="view-container style="grid-row: auto / span var(--grid-row-{{ index }})" > {{ item }} </view> </view> [代码] [代码]Page({ data: { span: 20, style: '' }, onReady() { this.setData({ style: '--grid-row-0: 10;--grid-row-1: 10;' // 0-9... }) } }) [代码] 显然通过这种方式去修改emmm,有点不尽人意,当view渲染的时候,通过[代码]index[代码]下标给每个view都设置独立的[代码]grid-row[代码]样式,然后在修改view父级的style,将[代码]--grid-row-xxx[代码]变量写进去实现子类继承,虽然比直接去修改每个view的样式要优雅些,但是一旦views的节点多了,100个、1000个、没上限呢,那这个父级的style真的惨不忍睹。。比如100个view,那么style将会是下面这样,所以需要换个思路还是得单独去设置view的样式。 [代码]const views = [...99].map((v, k) => `--grid-row-${k}: 10;`) console.log(views) // ["--grid-row-0: 10;", "--grid-row-1: 10;", ... "--grid-row-2: 10;", "--grid-row-3: 10;", "--grid-row-98: 10;", "--grid-row-99: 10;"] [代码] 通过Wxs脚本语言来修改view的样式,相比较通过[代码]setData[代码]去修改view的样式,wxs的性能绝对比js强。 WXS 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行。 WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。 WXS 的运行环境和其他 JavaScript 代码是隔离的,WXS 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的API。 WXS 函数不能作为组件的事件回调。 由于运行环境的差异,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备上二者运行效率无差异。 一般在对wxs的使用场景上大多数用来做[代码]computed[代码]计算,因为在[代码]wxml[代码]模板语法里只能进行简单的三元运算,所以一些复杂的运算、逻辑判断等都会放到wxs里面去处理,然后返回给wxml。 [代码]// index.wxs var format = function(string) { return string + 'px' } module.exports = { format: format } [代码] [代码]<!-- index.wxml --> <wxs src="./index.wxs" module="wxs"></wxs> <view>{{ wxs.format('100') }}</view> <view>{{ wxs.format(span) }}</view> <button bind:tap="modifySpan">修改span的值</button> [代码] [代码]// index.js page({ data: { span }, modifySpan() { this.setData({ span: '200' }) } }) [代码] 通过WXS响应事件来修改视图层[代码]Webview[代码],跳过逻辑层[代码]App Service[代码],减少性能开销,比如一些频繁响应的事件监听,滚动条位置,手指滑动位置等,通过wxs来做视图层的修改,大大提升了流畅度。 通过wxs响应原生组件的事件,[代码]image[代码]组件的[代码]bind:load[代码]事件 [代码]<!-- index.html --> <wxs src="./index.wxs" module="wxs"></wxs> <image class="image" src="https://hbimg.huabanimg.com/ccf4a904deaebc25990a47471c61ea1c765694f82633b-71iPZs_/fw/480/format/webp" bind:load="{{ wxs.loadImg }}" /> [代码] [代码]// index.wxs var loadImg = function(event, ownerInstance) { // image组件load加载完返回图片的信息 var image = event.detail // 获取image的实例 var imageDom = ownerInstance.selectComponent('.image') // 设置image的样式 imageDom.setStyle({ height: image.height + 'px', background: 'red' // ... }) // 给image添加class imageDom.addClass('.loaded') // 更多的功能请参考文档 // https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html } module.exports = { loadImg: loadImg } [代码] wxs监听data的值 [代码]<!-- index.html --> <wxs src="./index.wxs" module="wxs"></wxs> <view class="container"> <view change:text="{{ wxs.changeText }}" text="{{ text }}" class="text" data-options="{{ options }}" > {{ text }} </view> <view class="child-node"> this is childNode </view> <!-- 某个自定义组件 --> <test-component class="other-node" /> </view> [代码] [代码]// index.wxs var changeText = function(newValue, oldValue, ownerInstance, instance) { // 获取修改后的text var text = newValue // 获取data-options var options = instance.getDataset() // 获取当前页面的任意节点实例 var childNode = instance.selectComponent('.container .child-node') // 修改childNode样式 childNode.setStyle({ color: 'gree' }) // 获取页面的自定义组件 var otherNode = instance.selectComponent('.container .other-node') // 获取自定义组件内的节点实例 // 通过css选择器 > var otherChildNode = instance.selectComponent('.container .other-node >>> .other-child-node') // 获取自定义组件内部节点的样式 var style = otherChildNode.getComputedStyle(['width', 'height']) // 更多功能看文档 } module.exports = { changeText: changeText } [代码] 通过[代码]createSelectorQuery[代码]获取节点的信息,用来后续计算[代码]grid-row[代码]的参数 [代码]Page({ onReady() { wx.createSelectorQuery(this) .select('.view-container') .fields({size: true}) .exec((res) => { console.log(res) // [{width: 375, height: 390}] }) } }) [代码] 创建waterfall自定义组件 waterfall组件的职责,做成组件有什么好处,不做成组件又有什么好处,以及通过抽象节点来实现多组件复用。 prop的基本设置参数 [代码]Component({ properties: { views: Array, // 需要渲染的瀑布流视图列表 options: { // 瀑布流的参数定义 type: Object, default: { span: 20, // 节点高度比 column: 2, // 显示几列 gap: [10, 10], // xy轴边距,单位px rows: 2, // 网格的高度,单位px }, } } }) [代码] 组件内部默认的样式 [代码].c-waterfall { --grid-span: 10; --grid-column: 2; --grid-gap: 10px; --grid-rows: 10px; width: 100%; display: grid; grid-template-columns: repeat(var(--grid-column), 1fr); grid-auto-flow: row dense; grid-auto-rows: var(--grid-rows); grid-gap: var(--grid-gap); } .view-container { width: 100%; grid-row: auto / span var(--grid-span); } [代码] 组件的骨架 [代码]<wxs src="./index.wxs" module="wx" ></wxs> <!-- 样式承载节点 --> <view class="c-waterfall" change:loadStatus="{{ wx.load }}" loadStatus="{{ childNode }}" data-options="{{ options }}" style="{{ wx.setStyle(options) }}" > <!-- 抽象节点 --> <selectable class="view-container" id="view-{{ index }}" wx:for="{{ views }}" wx:key="item" value="{{ item }}" index="{{ index }}" bind:load="load" > </selectable> </view> [代码] 抽象节点 [代码]{ "component": true, "usingComponents": {}, "componentGenerics": { "selectable": true } } [代码] 抽象节点应该遵循什么 [代码]Component({ properties: { value: Object, // 组件自身需要的数据 index: Number, // 下标值 }, methods: { load(event) { // load节点响应事件 this.triggerEvent('load', { ...this.data, // value必填参数 {width,height} value: { ...event.detail }, }) }, }, }) [代码] 组件wxs响应事件 [代码].c-waterfall[代码]样式承载节点,主要是设置options传入的参数 [代码] var _getGap = function (gaps) { return gaps .map(function (v) { return v + 'px' }) .join(' ') } var setStyle = function (options) { if (!options) return var style = [ '--grid-span: ' + options.span || 10, '--grid-column: ' + options.column || 2, '--grid-gap: ' + _getGap(options.gap || [10, 10]), '--grid-rows: ' + (options.rows || 10) + 'px', ] return style.join(';') } [代码] 获取瀑布流样式承载节点实例 [代码] var _getWaterfall = function (dom) { var waterfallDom = dom.selectComponent('.c-waterfall') return { dom: waterfallDom, options: waterfallDom.getDataset().options, } } [代码] 获取事件触发的节点实例 [代码] var _getView = function (index, dom) { var viewDom = dom.selectComponent('.c-waterfall >>> #view-' + index) return { dom: viewDom, style: viewDom.getComputedStyle(['width', 'height']), } } [代码] 获取虚拟节点自定义组件load节点实例,初始化渲染时,节点是未知的,比如image组件,图片的宽高是未知的,需要等到image加载完成才会知道宽高,该节点用于存放异步视图展示,然后通过事件回调计算出节点高度。 [代码] var _getLoadView = function (index, dom) { return { dom: dom.selectComponent( '.c-waterfall >>> #view-' + index + '>>>.waterfall-load-node' ), } } [代码] 获取虚拟节点自定义组件other节点实例,初始化渲染就存在节点,比如一些文字就放在该节点,具体由组件的创造者去自定义。 [代码] var _getOtherView = function (index, dom) { var other = dom.selectComponent( '.c-waterfall >>> #view-' + index + '>>> .waterfall-load-other' ) return { dom: other, style: other.getComputedStyle(['height', 'width']), } } [代码] 已知瀑布流样式承载节点的宽度,等load节点异步视图回调时,获取到load节点的实际高度,比如一张400*800的图片,如果要显示在一个宽度180px的视图里,注意:[代码]image[代码]组件会有默认高度240px,或者用户自己设置了高度。如果要实现瀑布流,还是需要通过计算图片的宽高比例得到图片在视图中宽高,然后再通过计算grid布局的span值实现填充。 [代码] var fix = function (string) { if (typeof string === 'number') return string return Number(string.replace('px', '')) } var computedContainerHeight = function (node, view) { var vW = fix(view.width) var nW = fix(node.width) var nH = fix(node.height) var scale = nW / vW return { width: vW, height: nH / scale, } } [代码] 通过公式计算span的值,这个公式也是花了我不少时间去研究的,对grid布局使用也不多,很多潜在用法并不知道,所以通过大量的随机数据对比查找规律所在。[代码]gap为数组[x, y][代码],我们要取y计算,已知gap、rows求视图中节点高度[代码](gap[y] + rows) * span - gap[y] = height[代码],有了求height的公式,那么求span就简单了,[代码](height + gap[y]) / (gap[y] + rows) = span[代码],最终视图里的高度会跟计算出来的结果几个像素的误差,因为[代码]grid-row[代码]设置span不能为小数,只能为整数,而我们瀑布流的高度是未知的,通过计算有多位浮点数,所以只能向上取整了导致有几个像素的误差。 [代码] var computedSpan = function (height, options) { var rows = options.rows var gap = options.gap[1] var span = Math.ceil((height + gap) / (gap + rows)) return span } [代码] 最后我们能得到[代码]span[代码]的值了,只需要将[代码]load完成的视图修改样式即可[代码] [代码] var load = function (node, oldNode, dom) { if (!node.value) return false var index = node.index var waterfall = _getWaterfall(dom) // 获取虚拟组件,通过index下标确认是哪个,获取宽度高度 var view = _getView(index, dom) var otherView = _getOtherView(index, dom) var otherViewHeight = fix(otherView.style.height) // 计算虚拟组件的高度,其实就是计算图片在当前视图节点里的宽高比例 // image组件的mode="widthFix"也是这样计算的额 var virtualStyle = computedContainerHeight(node.value, view.style) // span取值,此处计算的高度应该是整个虚拟节点视图的高度 // load事件回调里,我们只传了load视图节点的宽高 // 后续通过selectComponent获取到了other视图节点的高度 var span = computedSpan( otherViewHeight + virtualStyle.height, waterfall.options ) // 设置虚拟组件的样式 view.dom.setStyle({ 'grid-row': 'auto / span ' + span, }) // 获取重新渲染后的虚拟组件高度 var viewHeight = view.dom.getComputedStyle(['width', 'height']) viewHeight = fix(viewHeight.height) // 上面说了因为浮点数的计算会导致有几个像素的误差 // 为了视图美观,我们将load视图节点的高度设置成虚拟视图节点的总高度减去静态节点的高度 var loadView = _getLoadView(index, dom) loadView.dom.setStyle({ width: virtualStyle.width + 'px', height: parseInt(viewHeight - otherViewHeight) + 'px', opacity: 1, visibility: 'visible', }) return false } module.exports = { load: load, setStyle: setStyle, } [代码] 抽离成虚拟节点自定义组件的利弊 利: 符合观察者模式的设计模式 降低代码耦合度 扩展性强 代码清晰 弊: 节点增加,如果视图节点过多会造成小程序性能警告 样式编写不便捷,需要写过多的判断代码去实现外部样式覆盖 wxs只能监听原生组件的事件,所以image的load事件触发时本可以直接去修改页面视图节点样式,不需要传回给父组件,然后父组件setData下标,wxs监听事件触发在去修改视图样式,多了一次setData的开销。 合: 时间有限没有扩展样式覆盖了,可以开启自定义组件的外部样式引入 节点过多的问题,在我自己电脑上,开发工具插入100个组件时,出现了卡顿,样式错乱,真机上目前还没发现上限。 后续想实现长列表功能,有回收机制,这样视图内的节点有限了,降低了性能开销,因为之前版本的长列表组件是通过[代码]createSelectorQuery[代码]获取节点信息,然后记录高度,通过创建[代码]createIntersectionObserver[代码]监听视图节点是否在视图来判断是否渲染。但是瀑布流有异步视图,初次渲染的高度跟异步加载完的高度是不一样,所以创建监听事件高度会不准确,若等到load完再创建监听事件,父级容器的高度又要经过计算,因为子节点会去填充空白区域实现瀑布流,目前项目中为了避免节点过大造成性能警告,加了item的个数限制,如果超过100或者1000个就清空数组,类似分页的功能。不过上面总结的思路可以去试试。 等把功能完善了,发布npm依赖包安装。 后续有时间会将项目里比较实用的组件抽离出来。。 自定义tabbar 自定义navbar 长列表 下拉刷新 上拉加载 购物车sku … Demo page调用页面 [代码]<view class="container"> <waterfall wx:if="{{ _type === 0 }}" generic:selectable="test-view" views="{{ views }}" options="{{ options }}" /> <waterfall wx:else generic:selectable="image-view" views="{{ images }}" options="{{ options }}" /> </view> <view class="btns"> <button bind:tap="loadView">模拟节点</button> <button bind:tap="loadImage">远程图片</button> </view> [代码] [代码]Page({ data: { views: [], loading: false, options: { span: 30, column: 2, gap: [10, 10], rows: 2, }, images: [], _page: 1, _type: 0, }, onLoad() { // 生成随机数据 // this.generateViews() // this.getHuaBanList() }, loadView() { this.data._page = 1 this.setData({ images: [], _type: 0 }) this.generateViews() }, loadImage() { this.data._type = 1 this.setData({ views: [], _type: 1 }) this.getHuaBanList() }, getHuaBanList() { let { images, _page } = this.data wx.request({ url: `https://huaban.com/search/?q=随机&page=${_page}&per_page=10&wfl=1`, header: { accept: 'application/json', 'accept-language': 'zh-CN,zh;q=0.9', 'x-request': 'JSON', 'x-requested-with': 'XMLHttpRequest', }, success: (res) => { res.data.pins.map((v) => { images.push({ url: `https://hbimg.huabanimg.com/${v.file.key}_/fw/480/format/webp`, title: v.raw_text, }) }) this.setData({ images, _page: ++_page }) wx.hideLoading() }, }) }, generateViews() { const { views } = this.data for (let i = 0; i < 10; i++) { views.push({ width: this._randomNum(150, 500) + 'px', height: this._randomNum(200, 600) + 'px', }) } this.setData({ views, }) }, _randomNum(minNum, maxNum) { switch (arguments.length) { case 1: return parseInt(String(Math.random() * minNum + 1), 10) break case 2: return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10) break default: return 0 break } }, onReachBottom() { let { loading, _type } = this.data if (!loading) { wx.showLoading({ title: 'loading...', }) loading = true setTimeout(() => { _type === 0 ? this.generateViews() : this.getHuaBanList() wx.hideLoading() loading = false }, 1000) } }, }) [代码] [代码]{ "usingComponents": { "waterfall": "/components/waterfall/index", "test-view": "/components/test-view/index", "image-view": "/components/image-view/index" } } [代码] 模拟load异步的自定义组件 [代码]<view class="c-test-view"> <view class="waterfall-load-node"> {{value.width}}*{{value.height}} </view> <view class="waterfall-load-other">模拟加载图片</view> </view> [代码] [代码]Component({ properties: { value: Object, index: Number, }, lifetimes: { ready() { const { index } = this.data const timer = 1000 + 300 * String(index).charAt(index.length - 1) setTimeout(() => this.load(), timer) }, }, methods: { load() { this.triggerEvent('load', { ...this.data, }) }, }, }) [代码] [代码].c-test-view { width: 100%; height: 100%; display: flex; flex-flow: column; justify-content: center; align-items: center; background: white; } .c-test-view .waterfall-load-node { height: 50%; flex-grow: 1; transition: all 0.3s; display: inline-flex; flex-flow: column; justify-content: center; align-items: center; background: #eeeeee; width: 100%; opacity: 0; } .c-test-view .waterfall-load-other { width: 100%; height: 80rpx; display: inline-flex; justify-content: center; align-items: center; background: cornflowerblue; color: white; } [代码] 随机获取花瓣网图片的自定义组件 [代码]<view class="c-image-view"> <view class="waterfall-load-node"> <image class="load-image" src="{{ value.url }}" bind:load="load" /> </view> <view class="waterfall-load-other">{{ value.title }}</view> </view> [代码] [代码]Component({ properties: { value: Object, index: Number, }, lifetimes: { ready() {}, }, methods: { load(event) { this.triggerEvent('load', { ...this.data, value: { ...event.detail }, }) }, }, }) [代码] [代码].c-image-view { width: 100%; display: inline-flex; flex-flow: column; background: white; border-radius: 10px; overflow: hidden; height: 100%; } .c-image-view .waterfall-load-node { width: 100%; height: 50%; display: inline-flex; flex-grow: 1; background: gainsboro; transition: opacity 0.3s; opacity: 0; overflow: hidden; visibility: hidden; } .c-image-view .waterfall-load-node .load-image { width: 100%; height: 100%; overflow: hidden; } .c-image-view .waterfall-load-other { font-size: 30rpx; background: white; min-height: 60rpx; padding: 10px; display: flex; align-items: center; } [代码] 代码片段 https://developers.weixin.qq.com/s/Q02FETmW7ind
2021-03-19 - 按钮扩散动画效果
背景 作为一个后端程序员,实在是对css是头疼的不行,最近有个需求针对按钮来个波纹涟漪效果。 [图片] 直接上代码 [代码] .bowen { display: inline-block; width: 200rpx; height: 200rpx; position: relative; } .bowen::before { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); content: ""; width: 100%; height: 100%; background: inherit; border-radius: inherit; animation: wave 1.5s ease-out infinite; } @keyframes wave { 50%, 75% { width: 230rpx; height: 230rpx; } 80%, 100% { opacity: 0; } } [代码]
2021-02-04 - 针对同层渲染偶发失败,导致原生组件把web组件给遮住的问题,在官方彻底解决之前,老夫有一曲线救国之妙计
官方在19年就发布支持原生组件的同层渲染,但众所周知在这之后官方一直无法确保此功能在所有机型有效,我也理解官方的难处,毕竟同层渲染的实现原理确实有点取巧,同层渲染让我们可以将web组件渲染在原生组件之上,支持完整的样式、动画,这是令人心动的。 但是。同层渲染存在失败几率,需要通过bindrendererror来进行效果降级,如果就此为止那也就没啥了,大不了渲染失败时我用cover系列组件顶上,丑就丑点。 问题是!同层渲染已完成后我们发现web组件偶发性被渲染到原生组件底下去了并且不会触发bindrendererror!特别是涉及滑动这类操作时更为明显。 因此,我在此先给出一个曲线救国的办法,我发现发生此类情况时,只需要切换页面并回来或者点击vConsole或者后台切回都会恢复正常,那就是意味着,只要原生组件上有其它组件需要渲染时会重新进行同层渲染!那么我只要确保有一个看不见的元素不断的显示和隐藏就可以一直触发同层渲染!只要速度够快用户就看不见bug! 以下是实现前(上拉并拉回时一个按钮和一条白边被渲染到原生组件底下去了): [图片] 实现后(正常渲染)的效果: [图片] 以下是实现方法,虽然不太好,但我目前只能想到这个,后续看看有没有更好的优化方法: 方案一: 建议使用方案二,不会直接对DOM树进行增加和删除元素,对性能会友好一点,当然,如果方案二你们测试有问题可以使用方案一。 1.给原生组件上面放一个0宽高的元素,由一个变量控制显示或隐藏 [图片] 2.写一个定时器控制该元素一直显示和隐藏,我这里只设100ms,低了怕影响性能,这个定时器只放在需要用同层渲染的时候,并且卸载页面时要记得停掉 [图片] 3.然后由于这个元素一直被显示和隐藏,所以一直触发了同层渲染,就可以让错误渲染的web组件重新回到最前面 方案二: 1.给原生组件上面放一个0宽高的元素,由一个变量控制display样式来实现显示或隐藏 [图片] 2.写一个定时器控制该元素一直显示和隐藏,我这里只设100ms,低了怕影响性能,这个定时器只放在需要用同层渲染的时候,并且卸载页面时要记得停掉 [图片] 3.然后由于这个元素一直被显示和隐藏,所以一直触发了同层渲染,就可以让错误渲染的web组件重新回到最前面
2021-02-05 - 云开发中字段为对象数组,如何用变量加字符串表示字段数组的更新条件?
db.collection('todos').doc('test').update({ data: { 'numbers.1': 30 }, }) 上面的'numbers.1'中‘1’,我想用‘numbers' 字符串加上小程序端传过来的变量来表示,可以实现吗
2021-01-11 - 如何只使用一个云函数搞定一个庞大而复杂的系统
吐槽 翻遍社区的文章,关于云开发的干货,少之又少,大部分都还是官方文档的搬来搬去,没啥营养,是时候放出一点技术"干货"了(有经验的开发者都能想到的方案)! 正题 小程序云开发的云函数的最大限制是 [代码]50[代码] 个,假设每个接口都使用 [代码]1[代码] 个云函数的话,有 [代码]10[代码] 张表,每张表都有 [代码]增删改查[代码] 四个接口,那么就会有 [代码]40[代码] 个接口,再加上一些其他接口,差不多刚刚好够用,那如果有 [代码]20[代码] 张表,甚至更多的表、更多的接口呢?对于中小型的小程序来说足够使用,那如果一个非常庞大而复杂的系统该怎么办呢? 而且每一个云函数的运行环境是独立的,想要共享一些数据也不是特别方便,那么有没有什么办法突破这样的限制呢? 其实解决方案很简单,只需要一点点的 [代码]OOP[代码] 思想和利用 [代码]JavaScript[代码] 的特性,一个云函数就可以搞定所有的接口。 具体的实现请往下看。 思路 云函数的运行环境是 [代码]Nodejs[代码] , 那么使用的语言就是 [代码]JavaScript[代码] ,可以充分的利用 [代码]JavaScript[代码] 的特性。 [代码]JavaScript[代码] 中的 [代码]属性访问表达式[代码] 有两种语法 [代码]expression . identifier expression [ expression ] [代码] 第一种写法是一个表达式后跟随一个句点 [代码].[代码] 和一个标识符。表达式指定对象,标识符则指定需要访问的属性的名称。 第二种写法是使用方括号 [代码][][代码],方括号内是另一个表达式(这种方法适用于对象和数组)。第二个表达式指定要访问的属性的名称或者代表要访问数组元素的索引。 不管使用哪种形式的属性访问表达式,在 [代码].[代码] 和 [代码][][代码] 之前的表达式总是会首先计算。 虽然 [代码].[代码] 的写法更加简单,但这种方式只适用于要访问的属性名称的合法标识符,并需要准确知道访问的属性的名字,如果属性的名称是一个保留字或者包含空格和标点符号,或者是一个数字(对于数组来说),则必须使用方括号 [代码][][代码] 的写法。当属性名是通过运算得出的值而不是固定值的时候,这时也必须使用方括号 [代码][][代码] 写法。 感谢社区大神 @卢霄霄 提供参考资料,详见 [代码]JavaScript权威指南[代码] (犀牛书)4.4章节。 可以使用 [代码][][代码] 的形式来完成动态的属性访问。具体实现请往下看。 实现 上面说了太多废话了,下面直接开干吧。 新建云函数 在云开发目录中新建一个云函数,我这里命名为 [代码]cloud[代码]。 打开 [代码]index.js[代码] 文件你会看到下面这段代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') // 初始化 cloud cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } } [代码] 这个云函数仅作为入口使用,上面提到了云函数的运行环境是 [代码]Nodejs[代码] 那么 [代码]Nodejs[代码] 的特性也是可以使用的,这里主要用到的是全局对象 [代码]global[代码],详见文档 在文件中,写入一些必要的全局变量,主要还是云数据库方面的,方便后面使用。 在初始化后面插入代码 [代码]global.cloud = cloud global.db = cloud.database() global._ = db.command global.$ = _.aggregate [代码] 这样就可以在同一个云函数环境中直接访问这些全局变量。 创建公共类 然后新建一个文件夹,我这里命名为 [代码]controllers[代码] ,这个文件夹用于存放所有的接口。 在 [代码]controllers[代码] 中新建一个 [代码]base-controller.js[代码] 文件,创建一个叫做 [代码]BaseController[代码] 的类,用于提供一些公用的方法。 内容如下: [代码]class BaseController { /** * 调用成功 */ success (data) { return { code: 0, data } } /** * 调用失败 */ fail (erroCode = 0, msg = '') { return { erroCode, msg, code: -1 } } } module.exports = BaseController [代码] 看到这里大家可能有点没看懂在做什么,那么请继续往下看。 创建接口 假设创建一些要操作用户相关的的接口,可以在 [代码]controllers[代码] 文件夹中新建一个 [代码]user-controller.js[代码] 的文件,创建一个名为 [代码]UserController[代码] 的类,并继承上面的 [代码]BaseController[代码] 类,内容如下: [代码]const BaseController = require('./base-controller.js') class UserController extends BaseController { // ... } module.exports = UserController [代码] 可以在这个类中编写所有关于 [代码]user[代码] 的接口方法。 编写接口 假设要分页查询用户信息,可以在 [代码]UserController[代码] 类中创建一个 [代码]list[代码] 方法。 代码如下: [代码]async list (data) { const { pageIndex, pageSize } = data let result = await db.collection('users') .skip((pageIndex - 1) * pageSize) .limit(pageSize) .get() .then(result => this.success(result.data)) .catch(() => this.fail([])) return result } [代码] 由于上面已经定义了全局变量 [代码]db[代码] 所以在 [代码]UserController[代码] 中无需引入 [代码]wx-server-sdk[代码] 引入接口类 写到这里接口已经完成了,还需要再引入这些接口类才可以进行访问。在 [代码]index.js[代码] 中引入 [代码]user-controller.js[代码] [代码]const User = require('./controllers/user-controller.js') [代码] 然后创建一个 [代码]api[代码] 变量,[代码]new[代码] 一个 [代码]User[代码] 实例 [代码]const api = { user: new User() } [代码] 在 [代码]main[代码] 方法中调用 [代码]UserController[代码] 中的方法。 [代码]exports.main = async (event, context) => { const { data } = event let result = await api['user']['list'](data) return result } [代码] 写到这里基本已经完成了接口的调用,但想要一个云函数动态调用所有接口还需要做一些改动。 动态调用接口 刚开始的时候介绍了 [代码]属性访问表达式[代码],限制稍微改动一下 [代码]main[代码] 方法 [代码]exports.main = async (event, context) => { const { controller, action, data } = event const result = await api[controller][action](data) return result } [代码] 在小程序调用云函数时,需要传入 [代码]controller[代码]、[代码]action[代码] 和 [代码]data[代码] 参数即可 [代码]const result = await wx.cloud.callFunction({ name: 'cloud', data: { controller, action, data } }) [代码] 完整 [代码]index.js[代码] 文件的代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const User = require('./controllers/user-controller.js') const api = { user: new User() } // 初始化 cloud cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) global.cloud = cloud global.db = cloud.database() global._ = db.command global.$ = _.aggregate // 云函数入口 exports.main = async (event, context) => { exports.main = async (event, context) => { const { controller, action, data } = event const result = await api[controller][action](data) return result } } [代码] 其他实现 云开发官方团队打造的轮子 tcb-router
2020-05-26 - 按照幸运值进行随机抽奖的算法
碰到这样一个问题,觉得有点意思,随手把算法写出来: 多人随机抽奖,每个人的幸运值不一样,幸运值越大,概率越高,这样的抽奖算法怎么写?原贴如下: https://developers.weixin.qq.com/community/develop/doc/000ac0127f058824e69b9d6f15b800 解决方案如下: testLuckRandom: function () { let luckers = [ { openid: '1', luckValue: 3, }, { openid: '2', luckValue: 4, }, { openid: '3', luckValue: 5, }, { openid: '4', luckValue: 2, }, { openid: '5', luckValue: 6, }, { openid: '6', luckValue: 8, }, ] //获得所有幸运值的和 let total = luckers.reduce((t, v) => { v.scope = [t, t + v.luckValue] //设置该幸运儿的区间值 return t + v.luckValue }, 0) let ran = total * Math.random() //获得基于幸运值总和的随机值 let lucker = luckers.find(v => ran > v.scope[0] && ran <= v.scope[1]) //找到这个随机值落在哪个幸运儿的区间里。 console.log(total, ran, lucker) }, 至于luckers的构建,原问题里说到共有4000多个人参加抽奖,怎么一次从库里获取超过1000条记录呢? 有现成的方案,参考老张的贴子: https://developers.weixin.qq.com/community/develop/article/doc/0008ea04120a18f6988b86d065b013
2021-02-18 - 使用云开发的分类查询group 可以按时间分类查询吗?
我现在有个订单表,订单表有个字段为付款时间,还有金额,这时候,我想根据付款时间生成例如每日收入统计表啊,每月统计表啊,,这样可以实现吗?可以用group实现吗? 是在是不知道怎么写。。。求大神指教。
2019-11-10 - 小程序粘性布局组件实现
一、前言 开发中,我们经常会遇需要让组件在屏幕范围内时,按照正常布局排列,而组件滚出屏幕范围时,让其始终固定在屏幕顶部的情况,也就是常说的粘性布局。今天我们就一起用小程序来实现一个适用于不同场景下的粘性布局组件。 二、demo演示 如图,实现的组件主要适用于以下几种场景: 吸顶页面最上方; 吸顶与页面有固定距离的位置; 在指定容器内吸顶; 嵌套在scroll-view中吸顶。 [图片] 三、代码演示 其中,粘性组件通过<weimob-sticky></weimob-sticky>调用,参数信息用法如下: 参数 说明 类型 默认值 offset-top 吸顶时与顶部的距离,单位px number 0 z-index 吸顶时的 z-index number 99 container 一个函数,返回容器对应的 NodesRef 节点 function - scroll-top 当前滚动区域的滚动位置,非 null 时会禁用页面滚动事件的监听 number - 滚动时触发scroll函数,其中isFixed为是否吸顶,scrollTop为距离顶部的位置。详细代码如下。 3.1 页面代码 3.1.1 基础用法 [代码]<view class="weimob-block"> <view class="weimob-title">基础用法</view> <view class="weimob-body"> <weimob-sticky> <!-- 需要粘性的部分 --> <button class="margin-left-base" size="mini"> 基础用法 </button> </weimob-sticky> </view> </view> [代码] 3.1.2 吸顶距离 [代码]<view class="weimob-block"> <view class="weimob-title">吸顶距离</view> <view class="weimob-body"> <!-- 吸顶时与顶部的距离,单位px --> <weimob-sticky offset-top="{{ 50 }}"> <!-- 需要粘性的部分 --> <button class="margin-left-top" type="primary" size="mini"> 吸顶距离 </button> </weimob-sticky> </view> </view> [代码] 3.1.3 指定容器 [代码]<view class="weimob-block"> <view class="weimob-title">指定容器</view> <view class="weimob-body"> <!-- 这里需要固定高度 --> <view id="container" style="height: 300rpx;background-color: #fff"> <weimob-sticky container="{{ container }}"> <button size="mini" class="margin-left-special"> 指定容器 </button> </weimob-sticky> </view> </view> </view> [代码] 3.1.4 嵌套在scroll-view使用 [代码]<view class="weimob-block"> <view class="weimob-title">嵌套在 scroll-view 内使用</view> <!-- 这里需要固定高度,scroll-view里的元素高度需要大于其高度 --> <scroll-view bind:scroll="onScroll" scroll-y id="scroller" style="height: 400rpx; background-color: #fff;margin-top: 40rpx;" > <view style="height: 800rpx"> <weimob-sticky scroll-top="{{ scrollTop }}" offset-top="{{ offsetTop }}" > <button size="mini" class="margin-left-scoll"> 嵌套在 scroll-view 内 </button> </weimob-sticky> </view> </scroll-view> </view> [代码] 页面js [代码]Page({ data: { container: null, //一个函数,返回容器对应的 NodesRef 节点 scrollTop: 60, // 当前滚动区域的滚动位置,非null时会禁用页面滚动事件的监听 offsetTop: 0 // 吸顶时与顶部的距离,单位px }, onReady() { // 页面渲染完,获取节点信息 this.setData({ container: () => wx.createSelectorQuery().select('#container'), }); }, onScroll(event) { // 容器滚动时获取节点信息 wx.createSelectorQuery() .select('#scroller') .boundingClientRect((res) => { this.setData({ scrollTop: event.detail.scrollTop, offsetTop: res.top, }); }) .exec(); } }); [代码] 3.2 组件代码 组件wxml [代码]<wxs src="./index.wxs" module="computed" /> <view class="weimob-sticky" style="{{ computed.containerStyle({ fixed, height, zIndex }) }}" > <view class="{{ fixed ? 'weimob-sticky-wrap--fixed' : ''}}" style="{{ computed.wrapStyle({ fixed, offsetTop, transform, zIndex }) }}" > <slot /> </view> </view> [代码] 组件wxs 这里使用使用小程序的wxs对吸顶元素的transform,top,height,z-index元素进行实时渲染,ios设备在滚动监听时性能会优于在js 2-20倍,androd设备效率暂无差异。 [代码]function wrapStyle(data) { var style = ""; if (data.transform) { style += 'transform: translate3d(0, ' + data.transform + 'px, 0);' } if (data.fixed) { style += 'top: ' + data.offsetTop + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } function containerStyle(data) { var style = ""; if (data.fixed) { style += 'height: ' + data.height + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } module.exports = { wrapStyle: wrapStyle, containerStyle: containerStyle } [代码] 组件js [代码]import pageScrollMixin from "./page-scroll"; const ROOT_ELEMENT = ".weimob-sticky"; Component({ options: { multipleSlots: true }, properties: { zIndex: { type: Number, value: 99 }, offsetTop: { type: Number, value: 0, observer: "onScroll" }, disabled: { type: Boolean, observer: "onScroll" }, container: { type: null, observer: "onScroll" }, scrollTop: { type: null, observer(val) { this.onScroll({ scrollTop: val }); } } }, data: { height: 0, fixed: false, transform: 0 }, behaviors: [pageScrollMixin(function pageScrollMixinCallback(event) { // 非null时会禁用页面滚动事件的监听 if (this.data.scrollTop != null) { return; } this.onScroll(event); })], lifetimes: { attached() { this.onScroll(); } }, methods: { onScroll({ scrollTop } = {}) { const { container, offsetTop, disabled } = this.data; if (disabled) { this.setDataAfterDiff({ fixed: false, transform: 0 }); return; } this.scrollTop = scrollTop || this.scrollTop; if (typeof container === "function") { // 情况一:指定容器下时,吸顶距离+吸顶元素高度>容器高度+容器距顶部距离,随页面滚动; // 情况二:指定容器下时,吸顶距离>吸顶元素高度,元素固定; // 情况三:元素初始化。 // this.getRect获取节点ROOT_ELEMENT相对于显示区域的top,height等信息,通过root获取 // this.getContainerRect获取父容器相对于显示区域的top,height等信息,通过container获取 Promise.all([this.getRect(ROOT_ELEMENT), this.getContainerRect()]).then( ([root, container]) => { if (offsetTop + root.height > container.height + container.top) { this.setDataAfterDiff({ fixed: false, transform: container.height - root.height }); } else if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height, transform: 0 }); } else { this.setDataAfterDiff({ fixed: false, transform: 0 }); } }); return; }else{ this.getRect(ROOT_ELEMENT).then(root => { // 吸顶时与顶部的距离小于可视区域的top距离时,随着滚动条滚动,否则吸顶 if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height }); this.transform = 0; } else { this.setDataAfterDiff({ fixed: false }); } return Promise.resolve(); }); } }, setDataAfterDiff(data) { // 比较数据是否与上次相同,不同则触发父组件scroll事件更新isFixed,scrollTop。 wx.nextTick(() => { const diff = Object.keys(data).reduce((prev, key) => { const prevCopy = prev; if (data[key] !== this.data[key]) { prevCopy[key] = data[key]; } return prevCopy; }, {}); this.setData(diff); this.triggerEvent("scroll", { scrollTop: this.scrollTop, isFixed: data.fixed || this.data.fixed }); }); }, getContainerRect() { const nodesRef = this.data.container(); return new Promise(resolve => nodesRef.boundingClientRect(resolve).exec()); }, getRect(selector) { return new Promise(resolve => { wx.createSelectorQuery().in(this).select(selector).boundingClientRect(rect => { resolve(rect); }).exec(); }); } } }); [代码] page-scroll.js 滚动事件在页面进入和离开时共享的pageScrollMixin函数。 [代码]function getCurrentPage() { const pages = getCurrentPages(); return pages[pages.length - 1] || {}; } function onPageScroll(event) { const { weimobPageScroller = [] } = getCurrentPage(); weimobPageScroller.forEach(scroller => { if (typeof scroller === "function" && event) { // @ts-ignore scroller(event); } }); } const pageScrollMixin = scroller => Behavior({ attached() { const page = getCurrentPage(); if (Array.isArray(page.weimobPageScroller)) { page.weimobPageScroller.push(scroller.bind(this)); } else { page.weimobPageScroller = typeof page.onPageScroll === "function" ? [page.onPageScroll.bind(page), scroller.bind(this)] : [scroller.bind(this)]; } page.onPageScroll = onPageScroll; }, detached() { const page = getCurrentPage(); page.weimobPageScroller = (page.weimobPageScroller || []).filter(item => item !== scroller); } }); export default pageScrollMixin; [代码] 总结 最后,我将上述代码放在了代码片段中供大家使用了解,https://developers.weixin.qq.com/s/qiym3wmr7znx ,希望能够帮到小伙伴们,欢迎评论区建议或指教哦~
2021-01-26 - 传统原生支付用云开发实现(非云支付)
本文的代码已过时,请勿照抄。建议改用云支付。 本文的代码被论坛自动过滤了所有XML的标签,所以照抄是会出错的。需要代码的话,看以前的老版本: https://developers.weixin.qq.com/community/develop/doc/000620ec5acb482103b7bf41d51804?jumpto=comment&commentid=000ea67d7b4da8d6c47acd1e05b8 代码前提:只需要替换两个与自己相关的参数key和mch_id 1、小程序开通微信支付成功,去公众平台(https://mp.weixin.qq.com/); 成功后可以知道自己的mch_id,即商户号。 2、去这里:商户平台(https://pay.weixin.qq.com/),获取key = API密钥,如果是退款的话,还需要下载API证书。 [图片] 以下代码仅包含统一下单,以及小程序端拉起支付的代码。 小程序端: testWxCloudPay: function () { wx.cloud.callFunction({ name: 'getPay', // data: {body:"body",attach:"attach",total_fee:1}, // 可传入相关参数。 success: res => { console.log(res.result) if (!res.result.appId) return wx.requestPayment({ ...res.result, success: res => { console.log(res) } }) } }) }, 云函数getPay: const key = "ABC...XYZ" //换成你的商户key,32位 const mch_id = "1413092000" //换成你的商户号 //以下全部照抄即可 const cloud = require('wx-server-sdk') const rp = require('request-promise') const crypto = require('crypto') cloud.init() function getSign(args) { let sa = [] for (let k in args) sa.push(k + '=' + args[k]) sa.push('key=' + key) return crypto.createHash('md5').update(sa.join('&'), 'utf8').digest('hex').toUpperCase() } function getXml(args) { let sa = [] for (let k in args) sa.push('<' + k + '>' + args[k] + '') sa.push('' + getSign(args) + '') return '' + sa.join('') + '' } exports.main = async(event, context) => { const wxContext = cloud.getWXContext() const appId = appid = wxContext.APPID const openid = wxContext.OPENID const attach = 'attach' const body = 'body' const total_fee = 1 const notify_url = "https://mysite.com/notify" const spbill_create_ip = "118.89.40.200" const nonceStr = nonce_str = Math.random().toString(36).substr(2, 15) const timeStamp = parseInt(Date.now() / 1000) + '' const out_trade_no = "otn" + nonce_str + timeStamp const trade_type = "JSAPI" const xmlArgs = { appid, openid, attach, body, mch_id, nonce_str, notify_url, out_trade_no, spbill_create_ip, total_fee, trade_type } let xml = (await rp({ url: "https://api.mch.weixin.qq.com/pay/unifiedorder", method: 'POST', body: getXml(xmlArgs) })).toString("utf-8") if (xml.indexOf('prepay_id') < 0) return xml let prepay_id = xml.split("")[0] let payArgs = { appId, nonceStr, package: ('prepay_id=' + prepay_id), signType: 'MD5', timeStamp } return { ...payArgs, paySign: getSign(payArgs) } } packge.json: { "name": "getPay", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "zfe", "license": "ISC", "dependencies": { "wx-server-sdk": "latest", "crypto": "^1.0.1", "request-promise": "^4.2.2" } } 附:完整代码片段:如果你觉得在这上面花的时间超过一天了,就去下载代码片段吧。 [图片]
2020-10-20 - 小程序支付003~借助云开发10行代码快速实现小程序支付
接上篇,上一篇我们已经注册完企业小程序,并成功的完成了微信认证。这一节我们就来开始正式的关联微信支付了,给我们的小程序接入支付功能。 传送门:《企业微信小程序的注册图文详解》 必备条件 1,必须注册微信支付的商户号 2,企业小程序必须通过认证 3,小程序关联微信支付商户号 一,小程序关联微信商户 1,登录小程序后台,点击关联更多商户号 [图片] 2,关联商户号需要用到appid,点击如下所示的关联更多AppID [图片] 把我们小程序的appid复制下 [图片] 然后去授权关联我们的微信支付商户号 [图片] 授权完成以后,我们的小程序端会出现下面这样的,点击下确认即可。 [图片] [图片] 这样我们就可以成功的关联微信支付商户号了 [图片] 点击下上图的确认,然后再点击下图所示的授权。 [图片] 可以看到我们的小程序和微信商户号成功的关联起来了 [图片] 二,开通云开发并绑定微信商户号 1,然后新建小程序,开始代码部分。 这里的appid一定要是你关联过微信支付商户的,并且还得是企业小程序。这里创建项目时记得选择不使用云服务,因为使用默认云开发的话,会创建一大堆无用的文件。 [图片] 2,开通云开发功能 [图片] 3,给你的云开发环境起个名,英文或者拼音 [图片] 然后点击确定,等待创建云开发,创建好以后如下。 [图片] 4,然后点击设置,全局配置,可以看到有个微信支付配置 [图片] 有的同学这里看不到微信支付配置,是因为你的小程序开发工具版本过低。最好下载最新版本的开发者工具。 5,云开发配置微信商户号。 [图片] 添加完以后还需要手机上进行授权确认 [图片] 6,手机微信上进行确认 [图片] [图片] 可以看到这里已经授权绑定了 [图片] 退款的我们后面会再讲。 这个时候我们准备工作就全部做好了,接下来就要愉快的写代码。 三,云开发支付代码的编写 1,看官方文档,其实说的很详细了,接下来我带大家过一遍。 [图片] 这里也把官方链接贴出来给大家。 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/open/pay/CloudPay.unifiedOrder.html 其实官方有给我们完整的示例。 [图片] 我们只需要把这段代码复制到我们自己的云函数里就行了。 2,创建云开发统一支付的云函数 我们首先要创建云函数的根目录 [图片] 然后新建云函数pay0610 [图片] 然后把官方示例直接复制到我们自己的云函数里 [图片] 3,把云函数里的信息替换成我们自己的 [图片] 上面标注重要的是一定要替换成自己的。然后保存修改,部署云函数 [图片] 4,编写页面 在index.wxml里写一个按钮,点击的时候调起我们的支付云函数 [图片] 然后在index.js里编写点击事件 [图片] 我们这个时候直接点击支付,看看会不会调起支付 [图片] 这个时候一大堆爆红,仔细看下,可以看出我们云开发环境id没有初始化。 5,app.js里配置云开发环境id 这里取到环境id [图片] 然后在app.js里配置 [图片] 然后我们再点击下支付,可以看到我们成功的调起了支付 [图片] 6,然后我用手机微信支付下试试 [图片] [图片] [图片] 支付成功后,我们的控制台也会有相应的日志打印。 [图片] 到这里我们就可以成功的在小程序里使用微信支付了,后面无非把价格和商品名字做活,做成动态传入的。 后面我也会把源码放到网盘里,有需要的同学,去我公号‘编程小石头’里回复‘云开发支付’就可以获取了。
2020-06-11 - 几行代码实现小程序云开发提现功能
先看效果: [图片] 纯云开发实现,下面说使用步骤: 一:开通商户的企业付款到领取功能 说明地址: https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1 使用条件 1、商户号(或同主体其他非服务商商户号)已入驻90日 2、截止今日回推30天,商户号(或同主体其他非服务商商户号)连续不间断保持有交易 使用条件是第一难,第二难在下面这里 [图片] 在网上找了很多,感觉是云开发这里的一个不完善地方,如果不填ip,会报这种错 [代码]{"errorCode":1,"errorMessage":"user code exception caught","stackTrace":"NO_AUTH"} [代码] [代码]<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[此IP地址不允许调用接口,如有需要请登录微信支付商户平台更改配置]]></return_msg> <mch_appid><![CDATA[wx383426ad9ffe1111]]></mch_appid> <mchid><![CDATA[1536511111]]></mchid> <result_code><![CDATA[FAIL]]></result_code> <err_code><![CDATA[NO_AUTH]]></err_code> <err_code_des><![CDATA[此IP地址不允许调用接口,如有需要请登录微信支付商户平台更改配置]]></err_code_des> </xml> [代码] 云开发没有ip这个概念,所以这里有些无从下手,不过这里我采用了个替代方案,参考了社区帖子: https://developers.weixin.qq.com/community/develop/doc/00088cff3a40d87d80f7267b65b800 之后我也亲自验证了,基本上就是这几个,当然肯定不够,但是可以自己在逻辑上进行处理,ip以下: [代码]172.81.207.12 172.81.212.74 172.81.236.99 172.81.235.12 172.81.245.51 212.64.65.131 212.64.84.22 212.64.85.35 212.64.85.139 212.64.87.134 [代码] 接着,可以动手了 二、云开发部分 1、设置云存储 证书配置地址: [图片] 下载后有三个文件,我们只需要p12结尾的那个 [图片] 然后,将这个apiclient_cert.p12文件上传到你的云存储 [图片] 这里处理完了,我们只需要一个东西,就是fileID也就是常说的云存储ID(上图红框内容) 2、配置云函数 新建云函数ref云函数 [图片] 代码如下: [代码]const config = { appid: 'wx383426ad9ffe1111', //小程序Appid envName: 'zf-shcud', // 小程序云开发环境ID mchid: '1111111111', //商户号 partnerKey: '1111111111111111111111', //此处填服务商密钥 pfx: '', //证书初始化 fileID: 'cloud://zf-shcud.11111111111111111/apiclient_cert.p12' //证书云存储id }; const cloud = require('wx-server-sdk') cloud.init({ env: config.envName }) const db = cloud.database(); const tenpay = require('tenpay'); //支付核心模块 exports.main = async(event, context) => { //首先获取证书文件 const res = await cloud.downloadFile({ fileID: config.fileID, }) config.pfx = res.fileContent let pay = new tenpay(config,true) let result = await pay.transfers({ //这部分参数含义参考https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 partner_trade_no: 'bookreflect' + Date.now() + event.num, openid: event.userinfo._openid, check_name: 'NO_CHECK', amount: parseInt(event.num) * 100, desc: '二手书小程序提现', }); if (result.result_code == 'SUCCESS') { //如果提现成功后的操作 //以下是进行余额计算 let re=await db.collection('user').doc(event.userinfo._id).update({ data: { parse: event.userinfo.parse - parseInt(event.num) } }); return re } } [代码] 需安装的依赖:wx-server-sdk、tenpay 这里只是实现了简单原始的提现操作,关于提现后,比如防止重复提交,提现限额这些,在开源二手书商城上有完整流程,地址: https://github.com/xuhuai66/used-book-pro 这种办法,不是每次都能成功提现,小概率遇到ip未在白名单情况,还是希望,云开发团队能尽快出一个更好的解决方案吧
2019-09-21 - 抽奖活动小程序运营合规记录
本文背景是这样的,我的抽奖小程序今天被封了,不管什么原因被封的,虽然让我很被动,也带来很大的损失,但是我本身是有预期和能够接受的, [图片] 因为抽奖的属性就决定了,我不能让所有人都满意,因为我的奖项设置是每个奖项只能有一人中奖,几百人参与,只有一个会中奖,所以其他参与的用户心里有怨言,我是承认的,换位思考下,我懂 当有人因为屡次抽奖不中的情况下,通过方便的投诉入口,对小程序进行投诉,当这种投诉进入官方审核人员视线,官方说你不行你就不行,行也不行,同样的抽奖场景,我对比过累计用户过亿的抽奖助手小程序,存在同样的交互,即看激励式视频广告,然后再抽奖 但是我不能理解的是,小程序官方的审核人员、以及广告的审核是怎么让我的审核通过的,让我的小程序处于封禁的处境,说审核通过的是你们,说违规的还是你们,当然可能你们是两拨人,要不这不是啪啪打自己脸嘛 上面这一行可以忽略,我真想一个大耳刮子扇向自己,为了流量主那点收益,冲昏了头脑,抽奖还让用户看多达30秒的激励式视频广告,这太狠了,能活6天,我是不是应该高兴才对~~ 下图为被封当天早上的截图,可见每天UV破5000那是很轻松的 [图片] 上线第一天的流量主收益,每天达到了150元 [图片] 哦,写了这么多,感觉自己变成了一个怨妇了,回归本文,今天我不是来抱怨,更不是来发牢骚的 本文内容具体被封的原因如下所示 通过下图我们不难看出,是由于转发的原因(除了转发,小程序不提供其他截图里面的功能),说是利诱那没有错,我抽奖,有利诱,目前我定位到问题所在就是因为抽奖之后有个转发的功能 f [图片] f [图片] f 这是审核人员给出的被封理由截图, [图片] f [图片] f 但是我想说的是,激励式视频广告本身的场景就是用于这种情况的,抽奖之前,让用户看广告,这个路线,难道存在违规风险, 我对这一点始终不能理解 本文总结我总结了下被封的原因是因为:小程序里面有以下两个功能 (1)抽奖之前会提示用户看激励式视频 (2)抽奖之后,提供用户分享的入口,没有强制分享,分享也不会增加中奖机会,仅仅做为一个普通的分享入口存在 所以对于我们以后在类似场景下,在小程序运营红线面前,要始终保持清醒,不要为金钱冲昏了头脑。 我对小程序做了以下两个改造重新换了小程序上线,目前已发布 (1)去掉抽奖之前的激励式广告 (2)去掉小程序里面的分享入口,不提供任何分享操作 f 2020-07-28补充 那些说,抽奖送皮肤属于网赚的, 我只能用的小程序以身试法了,目前第二个小程序已上线运营,如果再被封掉,就可以断定是抽奖送皮肤是被封的源头了 关于网赚的定义,群里小伙伴提供了官方的定义 “网赚”小程序,你只了解1%? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/000a0646f480f013c179a01f951409 申诉结果出来了,是激励式视频惹的祸 [图片] 再次重申下,我不是来寻求说法的,官方永久封禁,我会通过正常渠道申诉,至于申诉是否通过,我相信官方有定论,即使不通过,我也不委屈= 2020-07-30 抽奖助手小程序截图,具体的抽奖交互就是首先要看激励是视频广告,这一步是必要条件,绕不过去的,当然并不是所有抽奖都是这种交互,但是肯定是存在的,截图为证 f [图片] [图片] f
2021-01-11 - 如何突破一次只能获取20条记录的limit限制?只需要一行代码。
笔者刚遇到需要一次性拉取超过100条(云函数里超过1000条)记录的这种需求。 一般情况下,会有下面两种处理方式: 1、先获取总数,再for循环,每次拉取limit条记录;(可结合Promise.all并发) 2、递归拉取,每次拉取limit条记录,直到拉取的记录数量小于limit。 以上两种方式都比较麻烦,于是动了一脑筋,以最简单的方式实现上面的需求。 极简代码如下: db.collection('order').aggregate() .match({ status:'已付费' }) .addFields({ tempTag:1 //增加一个临时标签;也可以不要addFields这个阶段; }) .group({ _id:'$tempTag', orders:$.push('$$ROOT') //一次性拉取超过100条或者1000条记录 }) .end() .then(res=>{ let orders = res.list[0].orders console.log(orders) }) 一个临时标签,搞定。 小心数据量太大搞崩了,崩溃的极限是多少,需要各位自行摸索了。 需要注意的是,如果是云函数里执行以上代码(比如lookup),返回小程序端的数据量不要超过1M。
2021-03-15 - wx.chooseImage上传图片后图片的返回顺序和上传的不一样?
请问有什么办法解决,跪求大佬
2019-12-21 - wx.chooseimage上传多图success获取的图片顺序和选择顺序不一致
wx.chooseimage上传多图success获取的图片顺序和选择顺序不一致 请问wx.chooseimage 获取的顺序是按照什么排序的,为什么和手动选择的顺序不一致呢? 求解答,非常感谢
2018-09-05 - 小程序实现列表拖拽排序
小程序列表拖拽排序 [图片] wxml [代码]<view class='listbox'> <view class='list kelong' hidden='{{!showkelong}}' style='top:{{kelong.top}}px'> <view class='index'>?</view> <image src='{{kelong.xt}}' class='xt'></image> <view class='info'> <view class="name">{{kelong.name}}</view> <view class='sub-name'>{{kelong.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> </view> <view class='list' wx:for="{{optionList}}" wx:key=""> <view class='index'>{{index+1}}</view> <image src='{{item.xt}}' class='xt'></image> <view class='info'> <view class="name">{{item.name}}</view> <view class='sub-name'>{{item.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> <view class='moreiconpl' data-index='{{index}}' catchtouchstart='dragStart' catchtouchmove='dragMove' catchtouchend='dragEnd'></view> </view> </view> [代码] wxss [代码].map-list .list { position: relative; height: 120rpx; } .map-list .list::after { content: ''; width: 660rpx; height: 2rpx; background-color: #eee; position: absolute; right: 0; bottom: 0; } .map-list .list .xt { display: block; width: 95rpx; height: 77rpx; position: absolute; left: 93rpx; top: 20rpx; } .map-list .list .more { display: block; width: 48rpx; height: 38rpx; position: absolute; right: 30rpx; top: 40rpx; } .map-list .list .info { display: block; width: 380rpx; height: 80rpx; position: absolute; left: 220rpx; top: 20rpx; font-size: 30rpx; } .map-list .list .info .sub-name { font-size: 28rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #646567; } .map-list .list .index { color: #e4463b; font-size: 32rpx; font-weight: bold; position: absolute; left: 35rpx; top: 40rpx; } [代码] js [代码]data:{ kelong: { top: 0, xt: '', name: '', subname: '' }, replace: { xt: '', name: '', subname: '' }, }, dragStart: function(e) { var that = this var kelong = that.data.kelong var i = e.currentTarget.dataset.index kelong.xt = this.data.optionList[i].xt kelong.name = this.data.optionList[i].name kelong.subname = this.data.optionList[i].subname var query = wx.createSelectorQuery(); //选择id query.select('.listbox').boundingClientRect(function(rect) { // console.log(rect.top) kelong.top = e.changedTouches[0].clientY - rect.top - 30 that.setData({ kelong: kelong, showkelong: true }) }).exec(); }, dragMove: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function(rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top < -60) { kelong.top = -60 } else if (kelong.top > rect.height) { kelong.top = rect.height - 60 } that.setData({ kelong: kelong, }) }).exec(); }, dragEnd: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function (rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top<-20){ wx.showModal({ title: '删除提示', content: '确定要删除此条记录?', confirmColor:'#e4463b' }) } var target = parseInt(kelong.top / 60) var replace = that.data.replace if (target >= 0) { replace.xt = optionList[target].xt replace.name = optionList[target].name replace.subname = optionList[target].subname optionList[target].xt = optionList[i].xt optionList[target].name = optionList[i].name optionList[target].subname = optionList[i].subname optionList[i].xt = replace.xt optionList[i].name = replace.name optionList[i].subname = replace.subname } that.setData({ optionList: optionList, showkelong:false }) }).exec(); }, [代码]
2019-07-28 - 云开发之图片压缩裁剪(CloudBase图像处理扩展实战)
1、大约半年前在论坛里寻求云开发后端图片处理方案无果,无奈退而求其次使用小程序端canvas做图片处理: https://developers.weixin.qq.com/community/develop/doc/000c00a3d74758caca2a2b3ef5b400 (寻求方案发帖) 2、canvas做图片处理,代码量比较大,对手机性能要求比较高,而且如果一次处理图片多,还会偶现各种奇怪的不稳定问题。 3、最近iPhone微信更新到7.0.20更是直接不能使用了: https://developers.weixin.qq.com/community/develop/doc/000cc4b48a4378003b7b2f97d51400 (bug反馈发帖) 4、更换图片处理的方案刻不容缓,上次云开发峰会上陈宇明大佬分享案例中提到一嘴CloudBase的相关支持,于是翻到了相关文章,一步步跟着操作,在此感谢大佬指路: https://developers.weixin.qq.com/community/develop/article/doc/0004ec150708d0b57d5bd532a53413 (大佬文章) https://cloud.tencent.com/document/product/876/42103 (开发指南) 5、本人电商项目中有多处图片处理需求,比较典型的一个业务是上传商品主图,当用户任意上传一个图片后,自动居中裁剪生成一大一小两张正方形的图,大的用在详情页,小的用在列表页。CloudBase支持两种方式:获取图片时处理、持久化图像处理。本人业务采用后者。 6、代码示例: 小程序端选择图片,上传到云存储 wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: res => { const tempFilePaths = res.tempFilePaths const tempFile = tempFilePaths[0] let pictureLarge = tempFile let fileName = pictureLarge.split('.') let format = fileName[fileName.length -1] let cloudPath = 'products/sellerId/original-' + (new Date()).valueOf() + (format.length < 5 ? '.' + format : '') wx.cloud.uploadFile({ cloudPath: cloudPath, filePath: pictureLarge, success: res => { const pictureOrignial = res.fileID wx.cloud.callFunction({ name: 'addProduct', data: { operation: 'addPicture', pictureOrignial, cloudPath } }).then(res => { if (res.result.errCode) { wx.showModal({ title: '主图处理失败', content: res.result.errMsg, showCancel: false, confirmColor: '#67ACEB' }) } else { //拿到云文件ID做后续处理 res.result.picture } }).catch(err => { console.error(err) wx.showModal({ title: '主图处理失败', content: '主图处理失败,请重试', showCancel: false, confirmColor: '#67ACEB' }) }) }, fail: err => { console.error(err) wx.showModal({ title: '主图上传失败', content: '主图上传失败,请重试', showCancel: false, confirmColor: '#67ACEB' }) } }) } }) 云函数端处理图片,先放大到最小边大于1125px,再分别裁剪出1125px和258px的两张图,存到同一目录下,返回云文件ID 先安装包: npm install --save @cloudbase/extension-ci@latest 云函数: // 云函数入口文件 const cloud = require('wx-server-sdk') const extCi = require("@cloudbase/extension-ci") const tcb = require("tcb-admin-node") cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) tcb.init({ env: cloud.DYNAMIC_CURRENT_ENV }) tcb.registerExtension(extCi) // 云函数入口函数 exports.main = async (event) => { const wxContext = cloud.getWXContext() if (event.operation == 'addPicture') { return await addPicture(event.pictureOrignial, event.cloudPath) } else { } } async function addPicture(pictureOrignial, cloudPath) { //process picture const res = await process(cloudPath) if (res.errCode !== 0) { return { errCode: 100, errMsg: '商品主图处理失败' } } else { const pictureIDLarge = pictureOrignial.replace(/original/, 'large') const pictureID = pictureOrignial.replace(/original/, 'normal') return { errCode: 0, picture: { pictureIDLarge, pictureID } } } } async function process(cloudPath) { try { const opts = { //scale to 1125 rules: [ { fileid: '/' + cloudPath, // 处理结果的文件路径,如以’/’开头,则存入指定文件夹中,否则,存入原图文件存储的同目录 rule: "imageMogr2/thumbnail/!1125x1125r" // 处理样式参数,与下载时处理图像在url拼接的参数一致 } ] } await tcb.invokeExtension("CloudInfinite", { action: "ImageProcess", cloudPath: cloudPath, // 图像在云存储中的路径,与tcb.uploadFile中一致 operations: opts }) } catch (err) { return JSON.stringify(err, null, 4) } try { const opts = { rules: [ //crop large { fileid: '/' + cloudPath.replace(/original/, 'large'), rule: "imageView2/1/w/1125/h/1125/q/85" }, //crop normal { fileid: '/' + cloudPath.replace(/original/, 'normal'), rule: "imageView2/1/w/258/h/258/q/85" } ] } await tcb.invokeExtension("CloudInfinite", { action: "ImageProcess", cloudPath: cloudPath, // 图像在云存储中的路径,与tcb.uploadFile中一致 operations: opts }) } catch (err) { return JSON.stringify(err, null, 4) } return { "errCode": 0, "errMsg": "ok" } }
2022-04-26 - (10)群聊能力
我们在后台收到很多朋友的反馈,希望更好的运用小程序群聊的能力。于是我们想写写群聊的故事。 微信群是小程序在微信这个社交工具下传播的重要途径,我们经常能通过群聊看见小程序的身影。我们希望开发者在实现小程序逻辑的时候,能理解每一个群聊,可实现小程序与各个群聊紧密相关的功能。 基于此,我们开放了群聊 ID(openGID)的功能,供开发者区分标识每个群聊。对于每个群聊而言,小程序所获取到的 openGID 是不变的。但对于同一个群,不同的小程序内获得的 openGID 是不一样的。这一特性类似于标识用户身份的 openID。 拥有了群聊 ID,开发者可以把用户的操作按照群聊 ID 来聚合、沉淀信息,实现群协作功能。此外,通过 openID+openGID 的方式,还可以实现群排行的功能。 例如“群影”小程序以群聊ID聚合用户上传的图片,实现群相册的功能。 [图片] (“群影”小程序) 01 如何获取群聊ID 开发者获取 openGID 要依托于用户转发到群聊的小程序卡片,具体步骤如下: 1 设置带 shareTicket 的分享 在小程序内,开发者调用接口wx.updateShareMenu 带参数withShareTicket:true ,设置当前页面分享到群聊时能获取 openGID。而shareTicket本身就是获取 openGID 的凭证。 [图片] 而 iOS/Android App 分享场景当中,微信SDK也支持把所分享的消息设置成带 shareTicket。 值得注意的是,带 shareTicket 的分享卡片会被固定在某个群聊的,也就是说分享卡片会变成不能被长按转发。 2 由启动参数获取 shareTicket 当用户从某个带 shareTicket 的卡片进入小程序时,开发者可以在App.onLaunch 或者App.onShow 获取 shareTicket,而在小游戏上开发者可以通过监听 wx.onShow 或者同步调用wx.getLaunchOptionsSync 获取shareTicket。 shareTicket 实际上是小程序启动时临时生成的变量,在小程序生命周期内仅作为调用接口的凭证。生命周期结束后 shareTicket 就没有意义了。 3 通过 shareTicket 获取 openGID 开发者调 wx.getShareInfo 接口以 shareTicket 换取 openGID 的加密数据包,这是为了保证开发者服务器收到的 openGID 是可信的,开发者需要把加密数据交由后台解密,拿到真实的 openGID。数据加密机制更多请参看[数据加密相关文档]。 注意事项 ▷▷ 由于2018年7月5日起,新提交发布的小程序版本将无法通过用户分享获得群ID,即开发者通过wx.onShareAppMessage获取群 ID 的方式将不再支持,后续仅支持通过启动参数获取群 ID。请开发者及时调整。 02 群聊名称组件 除了群聊 ID 以外,开发者还能使用群聊对应的名称。出于保护用户隐私的考虑,我们不会把真实的群聊名称暴露给开发者,而是通过 open-data 组件让开发者在小程序前端展示某个 openGID 对应的群名称。 [图片] 其中 openGID 就是小程序获取到的群聊 ID。 open-data 组件只展示那些用户所在群聊ID对应的名称。如果设置了非微信提供的群聊 ID,将无法展现群聊名称。 03 群聊功能 1 分享设置 wx.updateShareMenu : [查看文档] 2 小程序启动参数 App.onLaunch / App.onShow : [查看文档] 3 小游戏启动参数 wx.onShow : [查看文档] getLaunchOptionsSync : [查看文档] 4 获取 openGID wx.getShareInfo : [查看文档] 5 群名称组件 open-data>/open-data>: [查看文档]
2018-08-17 - 只有三行代码的神奇云函数的功能之五:获取群id
这是一个神奇的网站,哦不,神奇的云函数,它只有三行代码:(真的只有三行哦) 云函数:login index.js: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event) => { return { ...event, ...cloud.getWXContext() } } 神奇功能之五:获取群id: 将小程序分享到某群里,可获得该群的群id, page.js: onShareAppMessage: function () { wx.showShareMenu({ withShareTicket: true }) let path = '/pages/xxx/xxx' let title = 'title' let imageUrl = `http://xxx.com/100.jpg` return {title,imageUrl,path} }, app.js: onLaunch: function (options) { options.shareTicket && this.getOpenGId(options.shareTicket) }, getOpenGId: function (shareTicket) { wx.getShareInfo({ shareTicket, success: function (res) { wx.cloud.callFunction({ name: 'login', data: { weRunData: wx.cloud.CloudID(res.cloudID) }, success: res=> { console.log(res.result.weRunData.data.openGId); } }) } }) }, 需要说明一下的是:从群里点击分享卡片进入小程序,必须是重启的小程序,不能是已经打开的小程序,否则得不到shareTicket。 其他功能: 神奇功能之一:获取openid: https://developers.weixin.qq.com/community/develop/article/doc/00080c6e3746d8a940f9b43e55fc13 神奇功能之二:不用授权获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/000a0c6b580338e947f9db0c65b813 神奇功能之三:100%成功获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/00066a967c4e384949f93fe1151413 神奇功能之四:获取电话号码: https://developers.weixin.qq.com/community/develop/article/doc/0006a8ec7ac860c94bf90a34f5d813 [图片]
2020-10-20 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 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 - 对数组push()后,下次push()会覆盖这次push的内容?但数组长度没错
基础库版本 2.11.2 部分代码如下: drawing:function(e){ var that = this; console.log(e); if(that.data.drawing){ that.data.polygon.push(that.data.example); that.setData({ drawing:false, polygon:that.data.polygon }) }else{ that.data.example['points']=[]; that.data.example['id']++; that.setData({ drawing:true }) } }, 这是一个按钮事件,polygon被 加值后,在下次开启加值时,会将之前加的值替换,但对原本的值无影响,即几次push后,所有push的值都会变成最后一次push的值
2020-06-23 - 微信小程序this.setData如何修改对象、数组中的值
在微信小程序的前端开发中,使用this.setData方法修改data中的值,其格式为 this.setData({ '参数名1': 值1, '参数名2': 值2 )} 需要注意的是,如果是简单变量,这里的参数名可以不加引号。 经过测试,可以使用3种方式对data中的对象、数组中的数据进行修改。 假设原数据为: data: { user_info:{ name: 'li', age: 10 }, cars:['nio', 'bmw', 'wolks'] }, 方式一: 使用['字符串'],例如 this.setData({ ['user_info.age']: 20, ['cars[0]']: 'tesla' }) 方式二: 构造变量,重新赋值,例如 var temp = this.data.user_info temp.age = 30 this.setData({ user_info: temp }) var temp = this.data.cars temp[0] = 'volvo' this.setData({ cars: temp }) 方式三: 直接使用字符串,此种方式之前不可以,现在可以了,估计小程序库升级了。 注意和第一种方法的对比,推荐还是使用第一种方法。 this.setData({ 'user_info.age': 40, 'cars[0]': 'ford' }) 完整代码: Page({ /** * 页面的初始数据 */ data: { user_info:{ name: 'li', age: 10 }, cars:['nio', 'bmw', 'wolks'] }, change_data: function(){ console.log('对象-修改前:', this.data.user_info) this.setData({ ['user_info.age']: 20 }) console.log('对象-修改后1:', this.data.user_info) var temp = this.data.user_info temp.age = 30 this.setData({ user_info: temp }) console.log('对象-修改后2:', this.data.user_info) this.setData({ 'user_info.age': 40 }) console.log('对象-修改后3:', this.data.user_info) console.log('数组-修改前:', this.data.cars) this.setData({ ['cars[0]']: 'tesla' }) console.log('数组-修改后1:', this.data.cars) var temp = this.data.cars temp[0] = 'volvo' this.setData({ cars: temp }) console.log('数组-修改后2:', this.data.cars) this.setData({ 'cars[0]': 'ford' }) console.log('数组-修改后3:', this.data.cars) } }) 效果: [图片]
2020-08-26 - 成语答题小程序V3.0
自从开源成语答题小程序以来不断完善功能,并且不断修复bug,成语答题小程序v3版本完善了很多功能 1.增加了原生模板广告,设置原生模板广告后可以设置首页或答题页是否显示原生模板广告 2.增加了背景设置功能,可以设置页面背景,让小程序看起来更加高大上。 3.优化了插屏广告显示 4.优化了奖品兑换数量限制,兑换完后不能兑换奖品 5.优化了奖品兑换记录查询 6.增加了自定义分享图片设置标题和图片内容。 7.增加了奖品发放已发放和待发放类别 [图片] [图片] 成语答题小程序常见问题: 成语答题小程序源码下载 https://developers.weixin.qq.com/community/develop/article/doc/000e04213603609b533bb53515fc13 成语答题小程序源码安装 https://developers.weixin.qq.com/community/develop/article/doc/000e04213603609b533bb53515fc13 成语答题小程序安装无法登录解决 https://developers.weixin.qq.com/community/develop/article/doc/0002acfd998858e81e2b2f7345b013 成语答题小程序红包设置教程 https://developers.weixin.qq.com/community/develop/article/doc/00084e795741007f2c2ba737d56413 成语答题小程序后台管理 https://developers.weixin.qq.com/community/develop/article/doc/000028a3134b603a0c3b0514e52813 成语答题小程序用户数据重复设置方法 https://developers.weixin.qq.com/community/develop/article/doc/000c8cde3cc258ea6a2bbfb165ec13 闯关答题小程序 (已完成) 竞赛答题小程序 (已完成) 答题考试小程序 (开发中) 出题答题小程序 (优化中) 如果搭建过程中遇到问题可到程序员锤哥公众号提问。
2020-11-23 - 一套代码发布多个微信小程序的实践
之前接手了公司的一个微信小程序项目,上线了一段时间之后,需要以这个项目为基础再发布多个小程序。这些小程序的内容基本上都是一样的,只不过它们有不同的名称、主题、图标等等;或者,某几个小程序需要加一些定制化页面,功能等。本文主要记录下我从纯手工复制项目进化到使用命令行工具复制项目的实践过程。这个简单的命令行工具简单粗暴地叫做 quickcopy,文档在这里。 我是使用 Taro 2.2.13 开发小程序的,所以这个工具目前也是在这个环境下开发的。除了 Taro 插件功能 以外,2.x 都可以使用这个工具。 开发工具前 defineConstants 一开始,因为项目也不多,所以我就直接纯手工操作了。比如,我们已经有了一个小程序叫做 小程序A,现在我们需要以这个小程序为基础复制出一个新的小程序,并且在打包之后实现以下 3 个简单的需求: 设置 [代码]config.window.navigationBarTitleText[代码],在 navigation bar 显示各自的小程序名称; 设置 [代码]config.tabBar.selectedColor[代码],在 tabBar 选中时显示不同的颜色; 为新的小程序定制 [代码]config.tabBar[代码] 图标,小程序A 则继续使用原来的图标。 首先,我们使用全局常量来改造 app.jsx 中的 [代码]config[代码]: [代码]// app.jsx config = { tabBar: { // 改造前: selectedColor: '#000' selectedColor: __MAIN_COLOR, list: [ { // 改造前: 'assets/icons/tabbar-home-s.png' selectedIconPath: 'assets/' + __ICON_DIR + '/tabbar-home-s.png' } ] }, window: { // 改造前: navigaionBarTitleText: '小程序A' navigationBarTitleText: __APP_NAME } } [代码] 然后,在 config 目录下分别为这两个小程序创建 Taro 编译配置文件 build-configA.js 和 build-configB.js,写入 [代码]defineConstants[代码]: [代码]// build-configA.js module.exports = { defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#000'), __ICON_DIR: JSON.stringify('icons') } } // build-configB.js module.exports = { defineConstants: { __APP_NAME: JSON.stringify('小程序B'), __MAIN_COLOR: JSON.stringify('#111'), __ICON_DIR: JSON.stringify('icons-b') } } [代码] 最后,编译打包 小程序A 的时候,我需要在 config/index.js 的最后将 build-configA.js 与基础的编译配置 [代码]merge[代码]。当编译打包 小程序B 的时候也是一样。 [代码]module.exports = function(merge) { return merge({}, config, require('./dev'), require('./build-configA.js')) } [代码] 运行这两个小程序,我们就可以看到它们会显示各自的名称与主题色,小程序B 还会显示定制化的 tabBar 图标。 sass.resource 既然在上面的全局常量中我们已经定义了一个主题色 [代码]__MAIN_COLOR[代码],那么,我们肯定也需要为不同的小程序编写不同的主题样式。 首先,在 src/style/themes 目录下分别为两个小程序创建主题样式文件。然后在 build-configA.js 以及 build-configB.js 中进行全局注入: [代码]// build-configA.js sass: { resource: [ 'src/style/themes/themeA.scss', // build-configB.js 中写 src/style/themes/themeB.scss 'src/style/variable.scss', 'src/style/mixins.scss' ] } [代码] 全局注入后也就不需要在样式文件中一次次写 [代码]@import 'xxx.scss'[代码] 了。但在这里需要注意的是,必须完整的列出需要注入的 3 个文件。虽然像 variable.scss 和 mixins.scss 这种样式文件明显可以在所有项目共享,但如果只在 config/index.js 中注入,而在 build-configA.js 或者 build-configB.js 中只注入主题样式文件的话,是行不通的。 [代码]// build-configA.js sass: { resource: ['src/style/themes/themeA.scss'] } // config/index.js sass: { resource: [ 'src/style/variable.scss', 'src/style/mixins.scss' ], projectDirectory: path.resolve(__dirname, '..') } // 以上两个配置 `merge` 之后的结果是 sass: { resource: [ 'src/style/themes/themeA.scss', 'src/style/mixins.scss' ], projectDirectory: path.resolve(__dirname, '..') } [代码] 也就是说,对于数组来说,是按索引位置进行 [代码]merge[代码] 的。 到现在为止,我们实现了为不同的小程序配置不同的名称,icon 以及主题样式。但是本着能偷懒就偷懒的原则,我觉得这些步骤已经有点麻烦了,可以想象下,如果又有新的项目需要发布,我们需要手动做这些事情: 在 config 目录下创建项目的编译配置文件 config-project.js; 如果需要,为新项目建立定制化的 icons 目录; 为新项目创建主题样式文件,并在 config-project.js 中全局注入; 在 config-project.js 编写 [代码]defineConstants[代码],写入不同项目间有差异的常量,其他的常量则写入 config/index.js; 在 config/index.js 合并新项目的编译配置; 目前所有的项目都共享了根目录下的 project.config.json,所以在编译前需要修改 [代码]appid[代码]。 如果哪一天这些项目都需要进行更新,可以想象下: 首先修改 config/index.js 中需要 [代码]merge[代码] 的项目配置路径; 然后修改 project.config.json 中的 [代码]appid[代码]; 最后编译打包; 如此循环; 那么,上面这些步骤是不是可以交给程序来完成呢?为了尽可能偷懒,我就写了一个简单的命令行工具。它可以代替我们完成以下事情: 以 config/index.js 为模版,提取部分编译配置,创建并写入到新项目的 Taro 编译配置文件; 以根目录 project.config.json 为模版,创建新项目的小程序项目配置文件; 创建新项目的主题样式文件,并在编译配置全局注入; 在打包时寻找新项目有没有定制化图标,如果有,则替换。 开发工具后 假定我们已经有了一份现有项目 小程序A 的编译配置: [代码]// config/index.js const config = { projectName: 'projectA', outputRoot: 'dist', copy: { patterns: [ { from: 'src/components/wxs', to: 'dist/components/wxs' }, // ... ] }, sass: { resource: [ 'src/style/variable.scss', 'src/style/mixins.scss', // ... ], projectDirectory: path.resolve(__dirname, '..') }, defineConstants: { HOST: JSON.stringify('www.baidu.com'), APP_NAME: JSON.stringify('小程序A'), MAIN_COLOR: JSON.stringify('#999'), // ... } } [代码] 在这份编译配置里,指定了项目的输出目录是 dist,全局注入了 variable.scss 和 mixins.scss 文件,并指定了 3 个常量。由于 Taro 不会打包 wxs,所以在 [代码]copy.patterns[代码] 手动将 wxs 复制到了输出目录。 在复制项目之前,我们先对编译配置进行一点改造。在 [代码]defineConstants[代码] 中,我们找到那些不同项目间存在差异的常量,在这里就是 [代码]APP_NAME[代码] 和 [代码]MAIN_COLOR[代码],添加双下划线 [代码]__[代码] 作为开头,这样工具就知道这些常量是存在差异的,而剩余的常量在所有项目中都是一样的。然后在 variable.scss 中找到那些与主题有关的变量,这些变量随后需要写入项目各自的主题样式文件中。 对于已存在的项目 projectA,我们最好也进行一次复制操作。这样一来它就可以拥有独立的编译配置,而 config/index.js 不仅会作为一份基础的编译配置被所有项目共享,也会作为创建新项目独立编译配置时的一份模版。 复制项目 以分离已有的 projectA 项目为例(复制新项目也是类似的),在根目录执行: [代码]qc copy projectA wx123456789a [代码] 工具可以代替我们完成这些工作: 创建 Taro 编译配置文件,路径为 config/config-projectA/index.js; 以根目录 project.config.json 为模版创建微信小程序项目配置文件,路径为 config/config-prjectA/project.config.json; [代码]{ "miniprogramRoot": "dist-projectA/", "projectname": "projectA", "appid": "wx123456789a" } [代码] 其余的内容则会与根目录下的 project.config.json 保持一致; 以 src/style、src/styles 以及 src/css 为顺序查找是否存在这些样式目录。如果存在,则在对应目录下创建 themes/projectA.scss 主题样式文件;如果以上几个目录都不存在,则默认在 src/style 下创建。具体的样式则需要手动写入; 从 config/index.js 找到需要全局注入的样式文件,即 [代码]sass.resource[代码],与上一步创建的主题样式文件一同注入到 config/config-projectA/index.js: [代码]sass: { resource: [ 'src/style/themes/projectA.scss', 'src/style/variable.scss', 'src/style/mixins.scss', ] } [代码] 主题样式文件会放在第一位,以便 variable.scss 和 mixins.scss 可以依赖主题样式。 从 config/index.js 找到需要复制到输出目录的文件,即 [代码]copy.patterns[代码],修改 [代码]to[代码] 指定的路径; [代码]copy: { patterns: [ { from: 'src/components/wxs', to: 'dist-projectA/components/wxs' } ] } [代码] 从 config/index.js 中找到不同项目间具有差异的常量,即 [代码]defineConstants[代码] 中 [代码]__[代码] 开头的常量,并自动添加一个名为 [代码]__PROJECT[代码] 的新常量; [代码]defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#999'), __PROJECT: JSON.stringify('projectA') } [代码] 所以最终的 config/config-projectA/index.js 就像这样: [代码]module.exports = { projectName: 'projectA', outputRoot: 'dist-projectA', defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#999'), __PROJECT: JSON.stringify('prjectA') }, copy: { patterns: [ { from: 'src/components/wxs', to: 'dist-projectA/components/wxs' } ] }, sass: { resource: [ 'src/style/themes/projectA.scss', 'src/style/variable.scss', 'src/style/mixins.scss' ] } } [代码] 至于上文的 icon 问题,因为 Taro 提供了插件能力,所以我们不再需要像上文一样引入 [代码]__ICON_DIR[代码] 常量并改造 [代码]selectedIconPath[代码]。只需要在 config/index.js 的 [代码]plugins[代码] 中添加 [代码]quickcopy/plugin-copy-assets[代码] 即可。 举个例子,我们原本将 icon 放在 src/assets/icons 目录下,如果我们想为 projectA 指定定制化的 [代码]tabBar.list.selectedIconPath[代码],只需要新建一个名为 src/assets/icons-projectA 的目录,在这个目录下存放 projectA 定制化的 icon 即可。 当打包 projectA 的时候,这个插件会去 assets/icons-projectA 查找是否存在定制化的 icon,如果存在,则使用这个 icon,如果不存在,则使用 assets/icons 中默认的 icon。 其他的 icon 也是同样的道理。 编译前准备 当我们需要编译 projectA 的时候,在根目录执行: [代码]qc prep projectA [代码] 工具会做以下两件事情: 创建 config/build.export.js 文件,并将 config/config-projectA/index.js 导出; [代码]const buildConfig = require('./config-projectA/index') module.exports = buildConfig [代码] 将 config/config-projectA/project.config.json 复制到根目录。 我们只需要在 config/index.js 的最后 [代码]merge[代码] build.export.js,随后在根目录执行 Taro 编译指令。 如何添加定制化页面 也许在未来的有一天,我们接到一个需求,需要为 小程序A 添加一个定制化的页面。我们将这个页面路径添加到 app.jsx 的 [代码]config[代码],但又不希望其他小程序打包的时候把这个页面也打包进去。 一开始我使用的方法简单粗暴:在打包其他小程序的时候把这个页面路径注释起来,在打包 小程序A 的时候再把注释打开。 我们可以借助 babel-plugin-preval(在 Taro 文档中也有提到)以及上文的 [代码]__PROJECT[代码] 常量编写逻辑,来确定哪个项目需要打包定制化页面,哪些项目又不需要打包。 首先,把 [代码]config.pages[代码] 提取出来作为一个独立文件,比如: [代码]// pages.js module.exports = function(project) { const pages = [ 'pages/tabBar/home/index', 'pages/tabBar/profile/index' ] if (project == 'projectA') { pages.push('pages/special/index') } return pages } [代码] 然后改造 app.jsx: [代码]const project = __PROJECT class App extends Component { config = { // 这里使用了 project 而没有直接传入 __PROJECT 是因为我在测试的时候发现直接使用 __PROJECT 编译的时候会报错 pages: preval.require('./pages.js', project) } } [代码] 这样一来我们只需要修改 pages.js 就可以添加定制化页面,不仅避免被不需要的项目打包,也能清楚地看出哪些项目有定制化页面哪些没有。对于 [代码]subpackages[代码] 和 [代码]tabBar.list[代码] 也可以做同样的处理。 最后 这个工具到目前为止是根据公司的业务需求开发的,主要功能也并不多,还是有挺大的局限。我也还在探索如何更方便地打包为不同项目编写的定制化页面,所以这个工具还会继续更新下去。
2020-11-18 - 一种小程序内使用setInterval循环调用有参函数的方法
首先介绍一下setInterval() setInterval(functionName,millisec[,"lang"]) setInterval() 方法可按照指定的周期(以毫秒计)来调用函数或计算表达式。 setInterval() 方法会不停地调用函数,直到 clearInterval() 被调用或窗口被关闭。由 setInterval() 返回的 ID 值可用作 clearInterval() 方法的参数。 setInterval()第一个参数是函数类型,意味着你的函数没法加参数。如果加了参数就变成functionName(params),这是你函数的结果,而不是函数类型。 举个错误的例子,用setInterval实现文字循环播放 function a(that){ var s=that.data.motto console.log(s) var len= s.length var s=s.substring(1,len)+s[0]; that.setData({ motto:s }) } Page({ data: { motto: 'Hello World', }, onShow:function(){ var that=this setInterval(a(that),300) //这里a(that)会立即执行,并返回值 } }) 执行之后会报以下错误: [图片] 它说setInterval的第一个参数期望一个function但是没找到,就是因为a(that)返回的不是函数类型。 既然如此,解决办法也就随之而来了。它既然想要一个函数类型,我们给它返回一个函数类型不就行了。 修改后代码如下: function a(that){ var s=that.data.motto console.log(s) var len= s.length var s=s.substring(1,len)+s[0]; that.setData({ motto:s }) return function(){ a(that) } } Page({ data: { motto: 'Hello World', }, onShow:function(){ var that=this setInterval(a(that),300) //这里a(that)会立即执行,并返回值 } }) 返回一个匿名函数继续调用a(that) 解决。 水平有限,欢迎交流。 引用请加说明。 参考:https://www.cnblogs.com/superdg003/p/5996360.html https://www.w3school.com.cn/jsref/met_win_setinterval.asp
2020-11-04 - 微信小程序页面停留时间统计
近来在研究微信小程用户是否在使用小程序或者查看用户在小程序停留的时间,无意中在git上找到了相关的解决问题方法,希望正在开发这个功能的的你,能帮助你解决! [图片]但是好像有 收到一个需求,要统计一个用户在我们小程序的每个页面的停留时间。 看了下现成的API,除了这个好像也没有别的可以用:https://mp.weixin.qq.com/debug/wxadoc/dev/api/analysis-visit.html#访问趋势, 这个里面貌似有页面停留时间的数据, 参数说明ref_date时间,如:"20170306-20170312"session_cnt打开次数(自然周内汇总)visit_pv访问次数(自然周内汇总)visit_uv访问人数(自然周内去重)visit_uv_new新用户数(自然周内去重)stay_time_uv人均停留时长 (浮点型,单位:秒)stay_time_session次均停留时长 (浮点型,单位:秒)visit_depth平均访问深度 (浮点型) 但是好像有查询时间限制,只能查询一天的数据。毕竟小程序数据很大,估计也是怕数据量太大查询慢吧。 算了,自己写一个吧, 初步想法,在页面的[代码]onShow[代码]事件里面,打一个开始的时间戳,然后在[代码]onHide[代码]里面再弄一个时间戳,两个一减,然后把得出来的数据,一提交,齐活。 BUT~,尼玛,[代码]onShow[代码]和[代码]onHide[代码]不仅在页面切换的时候会触发,小程序切换到后台和回到前台,也会触发,这就有干扰了。 但是在[代码]app.js[代码]里面的[代码]onShow[代码]和[代码]onHide[代码]事件只在小程序前后台切换的时候才会触发,不会在页面切换的时候触发,利用这点,把前后台切换排除掉,只在页面切换的时候,上报页面停留时间就好了 在[代码]app.js[代码]里面,初始化以下三个状态, globalData: { firstIn:1, onShow: 0, onHide: 0 } [代码]onShow[代码]和[代码]onHide[代码]的值默认为[代码]0[代码],当小程序进入后台或者返回前台的时候,给这两个值变为[代码]1[代码],用来告诉页面,刚才的切换是前后台切换,不是页面切换,不用上报页面停留时间。代码如下: 依旧是在[代码]app.js[代码]里面 onShow(){ if(this.globalData.firstIn){ this.globalData.firstIn = 0; } else{ this.globalData.onShow = 1; } }, onHide(){ this.globalData.onHide = 1; } 里面的[代码]firstIn[代码]表示是不是第一次进入小程,因为第一次进入的时候也会触发[代码]onShow[代码](相当于从后台切换到前台了),要把这个也排除在外。默认是第一次进入,进入之后就把这个值置为[代码]0[代码] OK,[代码]app.js[代码]准备好了,然后看下具体页面的, 在页面里面,先声明两个变量,一个[代码]startTime[代码],一个[代码]endTime[代码]分别来存储用户进入页面的时间和离开的时间 var startTime, endTime, app = getApp(); Page({ onShow(){ setTimeout(function () { if (app.globalData.onShow) { app.globalData.onShow = 0; console.log("demo前后台切换之切到前台") } else { console.log("demo页面被切换显示") startTime = +new Date(); } }, 100) }, onHide(){ setTimeout(function () { if (app.globalData.onHide) { app.globalData.onHide = 0; console.log("还在当前页面活动") } else { endTime = +new Date(); console.log("demo页面停留时间:" + (endTime - startTime)) var stayTime = endTime - startTime; //这里获取到页面停留时间stayTime,然后了可以上报了 } }, 100) } }) 有几个页面要统计的,就把这几个页面都加一下。 嫌麻烦的话,可以修改一下[代码]Page[代码]方法,默认自带[代码]onShow[代码]和[代码]onHide[代码],然后如果外面有传入的话,可以合并。页面在使用的时候,直接用这个心的[代码]Page[代码],就不用每个页面都[代码]onHide[代码]、[代码]onShow[代码]了,这里就不上具体的代码了。 关于[代码]setTimeout[代码]的说明: 页面的[代码]onShow[代码]和[代码]onHide[代码]会在[代码]app.js[代码]的[代码]onShow[代码]和[代码]onHide[代码]之前执行,加个延迟,放到后面执行,这样每次都可以先检测是页面切换还是前后台切换,然后再去做对应的逻辑,不然就反了。 参考地址:https://github.com/ireeoome/reeoome/issues/3 作者:Ams
2020-12-01 - #小程序云开发挑战赛#-大学校园闲置物品交易平台-HANG_IN_THERE
项目名称:大学校园闲置物品交易平台 1. 应用场景 作为一个大学生,经常会有一些闲置的物品需要处理,物品仍有使用价值,直接扔掉有些可惜,只好寻找再次出售的途径;或许也想要买一些物品,但不需要全新的,如二手自行车等。购买出售的途径一般有两个: 二手物品交易平台,如闲鱼等。但是,这种途径并不是十分非常适合大学生,本来就学业繁忙的我们需要抽出时间去寄送包裹,而且如果买到假货后甚至无从申诉,权益可能受到损害。 校内的各种闲置物品交换群(QQ,微信):此类途径具备了一定的安全性,而且方便快捷,因为都是本校的学生线上联系后线下交易。但是仍然存在信息获取效率低下的问题,很难从几百条群消息中准确的找到自己想要的物品,自己发布的商品也可能被群消息淹没。除此之外,信息的时效性很难得到保障,看到发布的商品后,很可能那件商品已经出售,需要要麻烦卖家亲自删除消息或说明商品已卖出。 针对上述途径存在的问题,我们设计了“大学校园闲置物品交易平台”的微信小程序,使用学生验证(暂未完成)、各大学相互隔离、线下交易的方式确保安全性,提供线上发布、商品列表与商品详情展示、商品检索的功能以保障较高的消息获取效率,采用商品问答、商品状态自动更新的方式确保信息的时效性。在大学校园闲置物品交易平台中,大学生能够在不涉及线上支付的情况下安全快捷地出售与购买二手物品。 2. 各页面功能展示(效果截图) 视频展示链接:https://v.qq.com/x/page/x31519ac9h3.html 2.1 商品列表与搜索 [图片] 首页为商品列表展示界面。 首页上方显示用户所在大学与搜索框,搜索框下方为大屏轮播图(暂未完成),可用来展示商品或广告。 轮播图下方为商品分类栏,包含了大多数常用分类,用户可以浏览自己感兴趣的分类。 主体部分为商品列表展示卡片,展示商品图片、标题、简介、状态、价格及数量。列表展示采用分页加载,每次加载10条商品信息,下滑到底部后,会自动加载下面10条商品信息,直到加载完所有商品。 搜索后的商品展示与首页的展示方式类似,采用模糊搜索,查询匹配到的商品的标题。 2.2 商品详情页与商品问答 [图片] 点击商品卡片后,进入商品详情界面。 界面上方为商品详情图片的轮播图,点击图片可以查看具体的图片,左右滑动查看列表中所有图片。 详情图下方为商品详情信息,包括标题、状态、价格、简介、数量、备注及原始购买链接。 详情信息下方为商品的问答区,可以在此询问卖家关于商品的问题,卖家可以在此回复用户。 [图片] 点击提问/回复后可以发表提问/回复内容,并在问答区展示。 商品问题仍然采用分页加载模式。当问题的回复超过2条时,回复卡片将自动折叠,点击查看全部回答可以跳转至问题详情界面,采用分页加载的模式展示所有回复。 2.3 商品发布 [图片] 点击底部Tab Bar的加号可以进入商品发布界面,上传前会进行表单验证,防止非法的数据存入数据库。上传时会让用户选择是否接受新交易推送,无论是否同意均不影响商品上传。上传成功后会自动跳转到商品列表界面,用户可以看到自己刚发布的商品。 2.4 发起交易与交易操作 [图片] 点击商品详情界面的发起交易后,若商品能够被购买,则进入确认交易界面。用户可以选择商品数量(不超过库存),查看总价格,最后点击确认交易。 若商品能够被购买,则更新商品库存,有必要的话更新商品状态,生成交易详情,跳转至交易详情界面。 至此,线上的活动暂告一段落,点击查看对方联系方式,通过对方的联系方式自行进行线下交易,结束后,当双方都点击确认交易完成后,交易结束。若任一方想要中止交易,直接点击取消交易即可。进行中的交易若无人点击确认完成,将在7天后状态自动变为已完成。 2.5 用户信息管理 [图片] 点击底部Tab Bar我的,可以进入管理界面。 点击头像/昵称/学校或在我的信息中,可以编辑个人信息,修改昵称、微信QQ联系方式与大学。 2.6 交易与商品管理 [图片] 在“我的交易”与“我发布的商品”中,可以查看交易详情,进行交易操作,或者查看发布的商品,选择删除商品。加载方式均为分页加载。 2.7 新交易推送 [图片] 为了提醒卖家有人购买其发布的商品,小程序加入消息推送功能。在发布商品时,会让用户选择允许接受新交易通知。点击允许后,若有人对卖家发布的商品成功发起交易,卖家便会收到消息推送,点击推送内容可直接查看交易详情,进行交易操作。 由于微信小程序对于用户隐私的保护,个人小程序的消息订阅仅是一次性的。若想再次收到交易推送,则需要在“我的”界面中点击“接受新交易推送一次”。 2.8 其他 其他界面包括index页、用户注册页、小程序介绍页等等,均为辅助功能,在此不再赘述。 3. 项目架构 下面时此项目的详细架构,对此项目感兴趣的小伙伴可以仔细阅读,如有不妥,敬请指正。 3.1 总体架构 [图片] 本项目以云开发为核心,主要包括:云函数,云数据库,云存储,云调用和HTTP API(暂未完成)五个部分。除了云开发外,还有小程序端,后台管理系统(CMS),第三方服务器等部分。 云函数: 接收小程序端发来的请求 接收CMS通过HTTP API发来的请求(暂未完成) 访问云数据库和云存储获取数据,然后发送回复 使用云调用,如消息推送 向第三方服务器发送请求,如用于学生验证的学校服务器(暂未完成) 云数据库: 被云函数访问 通过HTTP API被访问(暂未完成) 云存储: 被云函数访问 通过HTTP API被访问(暂未完成) 云调用: 通过云函数被调用 访问腾讯云服务,如消息推送 HTTP API(暂未完成): 被后台管理系统调用 调用云函数,访问云数据库,云存储 小程序端: 只访问云函数获取服务 后台管理系统(暂未完成): 只访问HTTP API获取服务 下面将对上述架构的每一部分进行详述。 3.2 云数据库表结构 [图片] 由于项目较大,涉及到的实体较多,故先画出该项目的ER Model(为了便于展示,略去了attribute)。实体共有8个:用户,大学,商品,商品分类,商品问题,问题回复,交易,轮播图(暂未完成)。上述实体的关系如图片所示。 根据模型图,可以在云数据库中建立8张数据表,对于特定的键建立索引。本项目,除了图片以外,删除方式都是软删除,故添加[代码]is_deleted[代码]字段。 [图片] 3.3 小程序端架构 [图片] 小程序端共分为以下几个部分:用户模块、商品模块、交易模块、工具类、学生验证(暂未完成)、云函数统一接口、缓存管理、组件库和CSS库。 用户模块:包括用户注册、学生身份验证和用户信息管理。 商品模块:包括商品列表、商品搜索、商品详情、发布商品、商品提问、提问回复和商品管理。 交易模块:包括发起交易、交易操作和交易管理。 工具类:返回内容格式化、时间展示格式化、表单验证。 学生验证(暂未完成):对于特点操作,访问云函数之前先验证学生身份。 云函数统一接口:将云函数返回的数据加工成合适的格式,直接供页面逻辑层使用。 缓存管理:将商品列表,用户信息,商品分类等数据缓存在本地,提高小程序性能,合适的时候清除缓存,重新访问云函数统一接口。 组件库:为了加快开发速度,专注云开发功能,本项目使用vant-weapp组件库。 CSS库:为了小程序的样式更加美观,本项目使用Color-UI库。 3.4 云函数结构 [图片] 本项目一共创建了10个云函数,大多与云数据库中的数据表一一对应。由于业务功能较多,所以使用[代码]tcb-router[代码]进行路由转发,增加服务的数量。每个云函数中的方法不再赘述,见其名就可知其意,都是基本的CURD操作。 需要说明的是: [代码]subscribeMsg[代码]函数:使用云调用,向用户推送消息(新交易提醒)。 [代码]del_trigger[代码]函数:定时触发器,每天定时删除一定时间之前的商品、问答、交易等。 [代码]transaction[代码]函数中的发起交易、取消交易、确认交易完成,以及[代码]commodity[代码]函数中的删除商品,这几个操作均涉及到多个数据表的改动,为了保障ACID(atomic, consistency, isolation, durable),都应采用数据库事务去完成数据库的操作。 3.5 云存储结构 [图片] 云存储中主要存放商品的缩略图和详情图的[代码]fileIDs[代码]、小程序背景图片及轮播图(暂未完成)。 3.6 云调用 [图片] 本项目的云调用主要是实现消息推送的功能,先在小程序端获取卖家的授权,然后由买家触发推送消息的云函数。 3.7 HTTP API,后台管理及第三方服务器(暂未完成) 由于参加比赛时间较晚,再加上临近开学,时间仓促,故无法完成该平台后台管理系统的搭建。待时间允许,将考虑建立后台管理系统,方便快捷地管理商品、用户、交易、轮播图的数据,通过HTTP API访问云函数,复用写好的方法,或者直接访问云数据库和云存储。 关于第三方服务器的学生验证功能,暂时还无法实现。 4.代码链接 github地址: https://github.com/2horse9sun/University_O2O 点个star吧! 5. 体验二维码 由于该项目涉及到信息发布内容且是个人开发,故无法上线,只有体验版。 希望日后能争取上线投入使用吧。 6.团队作者简介 我们是大二升大三的大学生,爱好旅游、美食、看书。希望能够和大家在github上交流技术、结识朋友。
2020-09-17 - #云开发挑战赛#-山大clubs-SXU1902
山大clubs 一个山大社团矩阵小程序,另外还可以添加收藏社团,还有社团管理员发布端和小程序负责人管理端 目的 解决校园信息分散以及有些社团人员过少宣传力度不足,导致学生获取信息不及时 结合当下的疫情和学生安全,学校内部应该经可能的减少人员的走动,这就不能用原来的宣传方法(走宿舍、走新生楼),通过这种线上的宣传让萌新也能了解学校的社团 目标用户 各个高校的学生 实现思路 对社团协会分类索引,新生可以通过校区、学术类、艺术类、娱乐类等选择自己感兴趣社团协会浏览收藏; 社团负责人通过权限申请将自己社团分类上传发布信息,主要信息有社团名称、迎新群QQ、社团简介; 小程序管理员可以管理社团,包括增加、删除等 用户权限 用户 收藏功能 权限申请 管理社团 管理申请 游客 √ √ ✖ ✖ 社团负责人 √ √ √ ✖ 小程序运营者 √ √ √ √ 小程序总负责人 √ √ √ √ 如何为自己学校制作一个这样的小程序? 小程序完全使用小程序的云开发,所以需要开通小程序云开发(真的是方便,免费的配额就够用,也不需要维护服务器) 将cloudfunctions函数部署到云,数据库建两个集合users和clubs 然后就进行简单的修改社团分类就可以发布适合自己学校的小程序 社团信息实时更新 为了让社团信息实时更新,动手做之前想了不少方法,但翻阅云开发文档发现,原来云数据库已经有这样的功能(厉害啦), 云开发文档 [代码] db.collection('clubs') .watch({ onChange: snapshot=>{ wx.showLoading({ title: '加载中...', mask: true }); this.dealdata(snapshot.docs) console.log('is init data', snapshot.type === 'init') }, onError: function (err) { console.error('the watch closed because of error', err) } }) [代码] 部分页面截图 首页 [图片] 社团详细信息展示(社团介绍、迎新QQ群等) [图片] 用户信息界面(超级管理员截图,其他人功能没有这么多) [图片] 管理社团界面 [图片] 视频介绍 点这里 作者介绍 一个发愁找不到工作的大四学生 一个想着出国留学的大二学生 进一步 提供订阅信息给用户提醒 使用微信支付,为社团报名收款 二流的程序猿,即使使用了这么优秀的前端UI框架,前端依旧这么丑,未来可以优化一下 体验 [图片] 致谢 ColorUI Mini-add-tips 云开发 开源地址 开源地址给个star好不好
2020-09-09 - #小程序云开发挑战赛#-答案sou-芝麻西瓜
1.应用场景与解决问题 调查研究表明,当前高校学生在课后习题上所花的时间主要集中在思考以及查阅相关资料上。尽管这两不是必不可少的,但是查询相关资料所用的时间往往占有重大比重,所以如何有效的缩减资料查询时间,是本程序解决的问题。因此,答案sou小程序的使用场景也显而易见:该小程序针对的是高校学生,在大学生解决课后习题时,想要查看习题的相关题解可以通过该小程序实现。最便捷的方法是用户通过程序扫描图书背后的条形码,获取图书题解信息,用户之后可以通过对应的章节获取用户所需的题解信息。当然上述方法只是方法之一,用户同样可以通过书籍分类、书籍搜索查找。课后习题的查找只是应用场景之一,该小程序同样适用于考研学生或者等级考试。考研学生或者等级考试学生可以在该系统上获取历年的考研真题、英语四六级等级考试真题及其解析。 2.目标用户 答案sou是一款用于解决大学生搜索答案困难而诞生的小程序。 3.实现思路 该系统的实现通过前端微信小程序以及配合云开发技术实现整个系统架构。前端小程序界面的构建部分使用了目前比较流行的小程序前端ui框架—Vant Weapp,vant ui封装了许多美观,可靠的组件,除了借用vant ui之外,系统自身也封装了许多可以复用的自定义组件。微信小程序云开发使得在小程序端可以直接操作云端的数据库,当然这有查询条目的限制,但这种限制可以通过云函数突破。该系统创建了许多云函数与用户的操作相对应。用户相应的操作会调用相关的云函数,通过云函数实现对云数据库,云存储进行操作。通过小程序以及云开发技术的实现大大降低了开发整个系统的周期。整个项目的难点在于数据的收集。该系统的所有数据通过python scrapy框架爬取而得,并通过相关的处理函数对爬取而得的数据进行一定的格式处理,使得数据成为符合系统要求的数据。之后将所有有效的数据上传至小程序云中。至此便可操控数据库,对云文件可以根据云id进行相关操作。资源的爬取是耗时耗力的,其中还要处理各种异常。 4.运行效果图 [图片][图片][图片] 5.功能代码展示 //获取热门书籍 async getHotBook () { const { data: data } = await db.collection('hotBook').field({ id:true, isbn:true, name:true, author:true, cover:true, view_num:true, publisher:true, }).orderBy('view_num','desc').get() return data }, 6.作品体验 [图片]
2020-09-06 - #小程序云开发挑战赛#-社区速修-爱咋咋地
1、应用场景 社区居户当家里出现家电、地板、水电等需要请专业人员维修的问题时,可能苦于无法快速找到专业的人员。此时通过该小程序进行简易问题查询、复杂问题申报等形式,排查解决问题,并且可以实时查看问题进展。 维修团队,可以拉取问题列表,更新问题进展,派专门的人员联系居户,进行电话解答或者上门维修。 2、目标用户 社区居户、维修团队 3、实现思路 通过输入校验码进行居户位置及维修团队负责区域定位; 调用云函数实现数据的上传、更新; 4、效果截图 [图片] [图片] [图片] [图片] [图片] 5、团队介绍 业余时间自己学习的菜鸟一枚,主要为了提升自己,当然还有很大的进步空间,谢谢大家
2020-08-31 - #小程序云开发挑战赛#-微信云商城-风车队
目标用户 剁手党,专指沉溺于网路给的人群,以女生居多。这些人每日游荡于各大购物网站,兴致勃勃地搜索、比价、秒拍、购物。周而复始,乐此不疲。结果往往是看似货比三家精打细算,实际上买回了大量没有实用价值的物品,造成造成大量时间、金钱的浪费。这类人自己在冷静之后也会意识到问题所在,甚至有痛定思痛、剁手明志的冲动,但购物瘾一犯即把决心忘得一干二净。 应用场景 当剁手党购物瘾又犯了的时候,可以打开这个小程序尽情购物(不用花钱事后不会产生罪恶感) 实现思路 写出静态页面获取云开发数据库中存储的数据使用云函数调用第三方API 获取数据渲染动态数据代码链接 github: https://github.com/yyskyrr/cloudMall 效果截图 首页[图片] 商品分类[图片] 商品详情[图片] 商品列表[图片] 购物车空[图片] 购物车[图片] 支付[图片] 个人中心[图片] 搜索中心[图片] 团队简介 个人开发
2021-04-19 - 小程序云开发挑战赛大众投票,你最想点赞哪个作品?
[图片] 小程序云开发挑战赛作品创作环节于9月20日正式落下帷幕。 自8月6日起至9月20日,在为期45天的小程序云开发的创作环节中,参赛者们通过线上线下寻找志同道合的好友组成参赛队伍或以自己为中坚力量独挑大梁成立一人队伍,组织各种头脑风暴并发挥自己的聪明才智,将一个个灵感迸发的瞬间无限延展,经过多方位思考确定方案,最终在短时间内通过云开发平台实现成触手可及的优秀小程序作品。 本次挑战赛共有近1700支队伍踊跃报名参赛,大家都基于现实场景打造出一个个极有现实使用价值的激情之作,经过大赛组委会评委评审,有以下作品进入到初赛阶段。点击下面作品链接一起来了解并体验,pick出你最喜欢的作品,快来给你喜欢的作品点赞投票吧! 你的投票将影响参赛队伍的复赛晋级,每个赛道我们将根据公投结果+评委评分,分别筛选10支队伍进参与10月19日线上复赛路演。 【由于作品较多,建议通过浏览器的查找功能搜索作品编号进行快速定位】 [图片] 如在了解体验环节中,发现有抄袭或直接挪用的作品,可点击下方投诉按钮进行作品文章投诉并写明投诉理由及细节信息,大赛组委会收到后会及时核实并进行作品违规处理。 [图片] 以下作品根据提交时间顺序排序 1 号作品 《大学生记账本》 队伍:阳光队 成员:常延威 作品简介:系统提供支出、收入、转账、余额、借贷五大记账模块,内含多种情景账本,以满足不同时期的记账需要。 2 号作品 《爱心收发室》 队伍:爱心收发室 成员:毕远萌、张宝政 作品简介:爱心收发室是一款与校自管会爱心收发室联名推出的信息查询类小程序。旨在为科大师生提供更好的校园服务。 3 号作品 《宝贝积分管理》 队伍:微喵网络 成员:董小白、周政 作品简介:“宝贝积分管理”是为了方便家长记录孩子平时行为,帮助孩子养成奖惩机制的工具型小程序。 4 号作品 《健身助手力量日记》 队伍:陆地能量队 成员:陆俊龙 作品简介: 为喜欢健身的人服务。无论是新手还是高手,每次健身之前都该有一个训练计划,督促自己去完成计划,也能节约时间。 5 号作品 《咸鱼记账》 队伍:咸鱼有梦 成员:薛国鹏 作品简介:记录用户的消费行踪,让用户在消费时能够对今天、本月和本年的消费有个直观的感受,并能够时时知道自己的消费。 6 号作品 《云享Music》 队伍:创新突击队 成员:卿大山、谢晨晨 作品简介:一款能快速加载歌单和歌曲,并且还可以通过小程序内部帖子,对爱好相同相互留言评论的音乐小程序。 7 号作品 《鹦鹉AI端侧识别》 队伍:仗剑把酒行且歌 成员:夏凡 作品简介:虽然法律严令禁止,但是仍然有人不惜铤而走险狩猎珍惜濒危野生动物,小程序可以查询野生动物信息,避免误伤。 8 号作品 《人体生理指标》 队伍:辰宝宝 成员:董湘宁、杨柳 作品简介:人体生理指标这个小程序主要是记录自己的血压脉搏数据,方便自己查看了解自己的血压数值数据。 9 号作品 《阮薇薇点名啦》 队伍:电子科技大学微软学生俱乐部 成员:刘俨晖 作品简介:阮薇薇点名啦小程序为学校社团活动发布、查看、签到、管理提供平台。 10 号作品 《错题小本本》 队伍:Meteor 成员:康广慧 作品简介:错题小本本是一款用于记录错题的微信小程序,适合具有良好自律性和学习习惯的初、高中学生。 11 号作品 《西瓜清单》 队伍:小白学编程 成员:舒健 作品简介:西瓜清单时间管理,有 每日清单、月历、月目标、随笔 等功能,帮助您更好地管理时间。 12 号作品 《阿里嘻嘻》 队伍:碰一碰运气的队 成员:张川 作品简介:校园导航类小程序 13 号作品 《为高考加分》 队伍:美的人生 成员:黄超辉 作品简介:为高考加分是一款用于高中生学习英语和检测生物学习情况的微信小程序。 14 号作品 《趣婚礼》 队伍:趣婚礼 成员:魏国 作品简介:基于Taro2 + 云开发 打造婚礼邀请函小程序,云开发CMS维护数据,使用方便快捷部署。 15 号作品 《分录英雄》 队伍:test 成员:张琦、段文慧 作品简介:为参加初级会计考试考试的考生提供了实时的答题和错题备忘平台。 16 号作品 《KrisQin记账本》 队伍:KrisQin 成员:吕正钦 作品简介:简易实现一个记账功能 还有一个对于语言备注的处理 17 号作品 《微信云商城》 队伍:风车 成员:闫雅帅 作品简介:当剁手党购物瘾又犯了的时候,可以打开这个小程序尽情购物(不用花钱事后不会产生罪恶感) 18 号作品 《科普小程序》 队伍:铁憨憨 成员:蔡金伟、李文贵 作品简介:用户想入手文玩但又不知道如何选购,或者遇到不确定情况不知道是不是商家的骗局的时候可以打开本程序查看,避免受骗。 19 号作品 《sentry 小程序客户端》 队伍:我做的都 成员:贺乐 作品简介:闲着没事看一下线上的 bug 情况如何,需不需要处理。适合在带薪拉屎或者给小孩换尿布的时候使用 20 号作品 《百词百科》 队伍:百词解百科 成员:季恩会 作品简介:用于展示具备内容短、量多、公认、中立、非商业、稳定、独立、有意义的单词内容 21 号作品 《莉龙美颜工具》 队伍:莉龙 成员:羊莉、周李龙 作品简介:莉龙美颜工具是一款好用的图像处理工具! 22 号作品 《吃否CHIFOU》 队伍:HUIBUR科技 成员:张睿 、温志杰 作品简介:吃否是一款集分享与购买为一体的社交电商型小程序。欢迎大家扫描文章中的小程序二维码。即刻加入我们。 23 号作品 《班级价值分》 队伍:价值分 成员:于殿文 作品简介:校园管理小程序,企业管理小程序,班级管理小程序,团队管理小程序。 24 号作品 《CEnews》 队伍:关于我啥也不懂来比赛这档事 成员:李昆鸿、谭浩源 作品简介:CE双语新闻,中英无门槛快速阅读,文章点击自动翻译,最新资讯随时看,陌生单词一点明。 25 号作品 《MY备忘》 队伍:码友 成员:薄纯三 作品简介:MY备忘,备忘至简,记录习惯!简洁易用的备忘录工具! 26 号作品 《社区速修》 队伍:爱咋咋地 成员:季运康 作品简介:小程序功能主要为社区居户简易故障维修排查、复杂故障申报,维修人员查看信息并服务等。 27 号作品 《海豚时光瓶》 队伍:时光瓶 成员:范文敬 作品简介:定时留言:发送一条信息,在设置时间后方可显示 。保证安全:使用密码才能提取到信息。 28 号作品 《虚拟社区》 队伍:逍遥派 成员:孙潇然 作品简介:虚拟社区是一款以地理位置为核心的社交小程序,包含了社交、群组、活动、比赛等功能,主要面向中老年用户。 29 号作品 《个人简历Plus》 队伍:Kindred 成员:贺鑫、张晟睿 作品简介:个人简历将电子的简历用小程序进行展示,决定任用后直接拨打电话或者添加微信直接联系对方。 30 号作品 《吃药小助》 队伍:大瘤子战队 成员:冯倩楠、刘紫旌 作品简介:吃药小助-小程序,提醒大家按时吃药,保护好自己和家人的身体健康。 31 号作品 《己目》 队伍:己目 成员:刘新 作品简介:己目,一款帮助长期在电脑前办公、手机控、学生党预防近视的小工具。 32 号作品 《Killkinfe》 队伍:桂航理学院 成员:韦明忠 作品简介:这是一款英语阅读小程序,定位为打发零散时间,满足用户了解国内外突发资讯需求。 33 号作品 《GitPark》 队伍:AtomLab 成员:陈晓平、周爱林 作品简介:GitPark是一款专门为编程爱好者随时随地浏览、交流、分享github仓库的GitHub小程序。 34 号作品 《体重MM》 队伍:小朋友与小三哥 成员:肖鹏、于海彬 作品简介:体重MM小程序是一款体重管理小程序,它会已折线图的方式对你的体重进行展示,并对变化趋势做简单的分析。 35 号作品 《口算助手》 队伍:薛定喵君 成员:薛刚 作品简介:口算助手是一款辅助家长给小孩练习口算的轻量级工具型小程序 36 号作品 《软著助手》 队伍:全村希望 成员:黄鹏、王亚明 作品简介:软著助手,帮助中小企业快速申请软著,一简化操作 37 号作品 《语音倒计时器》 队伍:跑得脱马脑壳 成员:赖德忠 作品简介:码如其名,一个有语音功能的倒计时器。 38 号作品 《活力健身房》 队伍:GuStudio 成员:黄子权 作品简介:一款基于手机加速度传感器的跑步记录小程序。 39 号作品 《云端商城小程序》 队伍:云端特产 成员:杨明 作品简介:线上特色百货销售小程序,支持会员等级、积分、卖手分享、微信支付、快递查询、独立后台管理等功能。 40 号作品 《家庭多用记事本》 队伍:逍遥 成员:周亮 作品简介:一款简单便捷的多功能记事本,可以邀请家人一同记事,共同查看,也可以私人记事,快速轻便~ 41 号作品 《king电影》 队伍:KingJ 成员:王佳、伟峰 作品简介:主要是一个简单的电影查询,因为豆瓣那边的接口问题,所以有些东西就有点问题,作为一个新人路过,我尽力了 42 号作品 《答案sou》 队伍:芝麻西瓜 成员:苏严 作品简介:答案sou小程序为大学生提供大学教材课后答案查询、考研真题及解析、英语四六级真题 43 号作品 《高级打卡鸡》 队伍:高级打卡鸡 成员:徐林 作品简介:随时随地打卡,记录足迹,看看世界地图上你遍布的足迹 44 号作品 《一眼天气》 队伍:心悦 成员:王辉 作品简介:这可能是最简单的天气预报,也是用户看天气所要获得的核心信息 45 号作品 《趣味游乐城》 队伍:superQ 成员:周龙江 作品简介:通过游乐放松心情,感悟人生、扩展社交、增进友谊。本应用最大的特色是处处充满随机。 46 号作品 《ygjtools》 队伍:跌打的小脆骨 成员:杨国杰 作品简介:一个记录消费的并附带数据分析统计的功能完整的小程序 47 号作品 《来这儿学》 队伍:来这儿学 成员:刘海、丘金龙 作品简介:一款为解决学生等需求人群寻找线下机构与线下机构寻找渠道宣传服务之间的现实问题的微信小程序 48 号作品 《北院守夜人》 队伍:Simple 成员:刘国坤、张满培 作品简介:这个小程序类似于小论坛,专为北院(河北北方学院)同学设计,为的是让更多的同学能够融入校园 49 号作品 《小酒馆》 队伍:小酒馆 成员:陈洪宇 作品简介:上传云函数运行后无需用户自己建立数据库后台会自动生成,邀请亲朋参加婚礼的云开发小程序,基本功能已实现 50 号作品 《西红柿时间管理》 队伍:非龙在天 成员:韩宇非、张龙 作品简介:本小程序旨在应用于关于学生,工作者,健身者等需要专注进行某事者小程序提供一个计时功能。 51 号作品 《高校联盟-快递代取》 队伍:云小白 成员:史连强、张广凌风 作品简介:高校联盟 快递代取,一款为在校大学生提供服务代取快递,接收快递来做兼职的小程序 52 号作品 《乐考吧》 队伍:鸡蛋汤不加糖 成员:茹东廉、王艳辉 作品简介:你是不是已经记不起来你家孩子一年前、一个月前、甚至一周前的考试成绩了?来乐考吧,还你一个有历史的成绩 53 号作品 《码农SHOW营》 队伍:码农SHOW营 成员:陈德达 作品简介:该项目主要服务的人群是喜欢进行文章创作的朋友和做微商的朋友。主要分类:首页和我的两大模块 54 号作品 《易约行》 队伍:pepsi 成员:李懿霏、张钱钱 作品简介:用于快速预约校园内的公共设施 55 号作品 《开心小杜》 队伍:duing 成员:杜俊 作品简介:基于云函数和云数据库开发的生活娱乐类小程序——开心小杜 56 号作品 《菲特日记》 队伍:冲天敖广 成员:郭函、王建行 作品简介:可以随时查看各种数据变化,帮助用户进行 身材管理,引导用户合理饮食和科学锻炼,适应现代健康生活的需要 57 号作品 《山大clubs》 队伍:sxu1902 成员:刘子龙、赵亮 作品简介:对学校社团进行分类,便于新生选择自己喜欢的社团,小程序分为社团负责人和小程序管理员,便于人员管理 58 号作品 《省计数字监理》 队伍:东莞队 成员:万霞光 作品简介:企业内部应用系统,围绕信息系统监理工作而开发的一款内部应用系统,目前已经完成了部分功能并上线使用。 59 号作品 《BOSS百科》 队伍:乐多多BOSS百科 成员:付小琴、朱忠进 作品简介:BOSS百科----一个提升BOSS逼格的学习交流平台 61 号作品 《校园墙》 队伍:代码能跑就行了 成员:黄俊涛、许粤军 作品简介:校园里面的信息发布平台,给所有的在校生提供一个交流平台,可以发布表白,失物招领,二手买卖等信息 62 号作品 《图像复原微信小程序》 队伍:咸鱼翻身 成员:朱晋 作品简介:对曝光欠佳图像进行一定程度图像复原 63 号作品 《WiFi生成码》 队伍:决明子 成员:李浩凡 作品简介:生成WiFi二维码快速连接WiFi,展示小程序和云开发能力 64 号作品 《摄影地图游客版》 队伍:垫底 成员:林伟佳、杨灿 作品简介:摄影地图游客版是为了广大摄影爱好者提供机位信息的而设计的工具类小程序。 65 号作品 《医医查》 队伍:一叶峰 成员:王朋飞、张海峰 作品简介:通过每个人生日,解读性格分析 66 号作品 《树洞》 队伍:G 成员:高瑞光 作品简介:介于现在很多人会有方方面面的压力,却又不能很好的释放,借此成为这个小程序的设计思路 67 号作品 《全国核酸检测资质医院查询》 队伍:酷亿队 成员:姚颖、张文正 作品简介:为需要出具核酸检测的人群制作,快速获取医院位置信息以及电话进行咨询。便民工具 68 号作品 《Do More打卡小程序》 队伍:寂寞君的微信小程序学习 成员:王雨宸 作品简介:个人微信小程序练手,腾讯视频无法审核通过,备用视频地址在github中 69 号作品 《ai视觉测试》 队伍:为了梦 成员:钟梓俊 作品简介:日常生活和学习用到的辅助工具,主要应用于办公和文本编辑。 70 号作品 《倒计时》 队伍:奔跑队 成员:金程 作品简介:我的小程序名称是倒计时,一个拥有倒计时功能的计时器,主要功能让你无法自律的身体,得到行动。 71 号作品 《tomato clock》 队伍:JLS 成员:陈俊良、闫硕 作品简介:番茄钟,助你专心工作学习。 72 号作品 《古老的API小工具》 队伍:归藏易文化 成员:方大伟 作品简介:整合常见的api组合的小工具库,表现形式为刷新,问答,搜索。云数据库交互为收藏和底部推广差异化。 73 号作品 《大学校园闲置物品交易平台》 队伍:HANG_IN_THERE 成员:冯旭、李家正 作品简介:大学校园闲置物品交易平台,大学生能够在不涉及线上支付的情况下安全快捷地出售与购买二手物品。 74 号作品 《OA外勤管家》 队伍:大叔战队 成员:彭刚、谭广健 作品简介:谭广健---草根开发者,一直关注微信生态圈的发展。也是第一批参与小程序、微信小游戏及云开发的体验者。 75 号作品 《雨中送伞》 队伍:键盘起火 成员:郑文鸿 作品简介:下雨天时,可为无伞的人提供求助服务,有伞的人可以看到求助标注并提供帮助。 76 号作品 《实验室设备预约助手》 队伍:hello522 成员:郑启文 作品简介:实验室预约助手,用低成本的云开发实现对实验室设备的预约使用管理。 77 号作品 《日程管家》 队伍:洛 成员:骆永生 作品简介:日程管家小程序就是为了帮助用户管理日程生活,提高工作效率,养成良好习惯 78 号作品 《素拓百分百》 队伍:许一世不嵩手 成员:张宏伟 作品简介:该小程序用作于浙水院发布活动,自动统计学生素拓分。 79 号作品 《小小微距》 队伍:汇溪和他们的小伙伴们 成员:李辉、张乐平 作品简介:一款 UGC 软件,想法是我躺床上想到的。独在异乡,如果生病了,去医院都没人陪一陪,该多难受呀。 80 号作品 《来一杯a》 队伍:Ysgming 成员:杨开澳、詹阳天 作品简介:针对店铺的一个点单系统,主要包括下单、取餐、外卖三个功能模块,方便顾客的购买体验,减少商家的工作量 81 号作品 《微学堂(在线学习平台)》 队伍:若有光 成员:肖航 作品简介:#小程序云开发挑战赛#-微学堂(在线学习平台) 82 号作品 《购物》 队伍:MB 成员:徐沛东 作品简介:学习 83 号作品 《日常工具box》 队伍:七西队 成员:王家慧、吴鸿萱 作品简介:一个较为实用的日常小工具,分为五大功能:手持弹幕、九宫切图、任务清单、写字板、指南针 84 号作品 《简物业》 队伍:DIB 成员:仝乐 作品简介:该小程序致力于将现代化管理手段有机融入物业行业。节约物业管理投入的同时,方便用户居住生活。 85 号作品 《恋人小清单》 队伍:404 成员:魏云飞 作品简介:恋人小设计主旨是用照片和文字来记录美好瞬间,记录那些在多年以后再看到时,内心还会为之感动的点点滴滴 86 号作品 《校园缺勤录》 队伍:这未免有点 成员:芦星宇 作品简介:人工智能先进技术引入课堂,实现轻松课堂签到。 87 号作品 《帮寻小站》 队伍:炸锅蚂蚁 成员:姚史展 作品简介:帮寻小站小程序旨在方便用户发布公告,让失主和好心人之间的沟通更加安全和高效。 88 号作品 《美食屋》 队伍:焱魂 成员:贺帅 作品简介:作品是一个点餐小程序,类似于现在很普遍的外卖程序,主要是现场点餐,叫号取餐,没有线上支付。 89 号作品 《假如生命很短暂》 队伍:一切为了T恤 成员:苏南、房泽锐 作品简介:15分钟之中你可以在这个世界四处游荡,但是特定不可发言交流 ,除此之外可以互动交流 90 号作品 《快速找工作》 队伍:mrhuostudio 成员:霍小平、张善友 作品简介:在微信小程序、云函数开发中提供一套通用的C#解决方案,以小程序中快速找工作做应用场景验证技术方案 91 号作品 《酷传CoolTran》 队伍:上上下下左右左右BABA 成员:吉元昊 作品简介:凭借微信云开发提供的云端存储能力,使得"CoolTran"可以让人们可以随时随地分享文件! 92 号作品 《娱乐投票小程序》 队伍:艾黎 成员:黎艳红、王宋强 作品简介:一个小程序云开发的投票小程序,可以免登陆投票,并且能确保每一张票都是真实有效,能合理的追溯票源 93 号作品 《租户在线》 队伍:火柴小分队 成员:黄志健 作品简介:基于云开发的微信小程序,用于房客日常提交问题反馈信息给房东,并实时查看问题是否已被处理 94 号作品 《step by step》 队伍:tqszbd 成员:汤琪、唐文静 作品简介:step by step小程序,记录前进的每一步。大至一个目标,小至每日清单,全周期时间管理。 95 号作品 《同学在哪儿》 队伍:你说的都队 成员:邹明远 作品简介:「同学在哪儿」是一个地图小程序,可以在地图上查看班级同学的毕业去向以及地域分布,多联(蹭)系(饭) 96 号作品 《图文识别》 队伍:Minggo 成员:戴统民 作品简介:拍照识别文字、翻译、整理笔记便捷工具型小程序。 97 号作品 《报工小助手》 队伍:乘风破浪 成员:王霄鹏 作品简介:为了方便大家的及时填报项目工作量,基于网页版的研发管理系统报工模块,开发了报工小助手微信小程序。 98 号作品 《磁力积木3D预览》 队伍:超级像素 成员:雷攀 作品简介:3D软件制作成3D作品,导出后可以通过本小程序快捷方便分享及展示,极大增强用户体验 99 号作品 《苦海匿舟》 队伍:魔幻小程序 成员:张鹏广 作品简介:“苦海匿舟”是一个真正【完全匿名】发表心情的小程序。要你存在压力需要宣泄、倾诉,都可以来苦海匿舟。 100 号作品 《大学生资源共享平台》 队伍:加油队 成员:郭龙庭、王湘茹 作品简介:一个可以方便大学生进行学习资源共享的小程序 104 号作品 《球员搜搜》 队伍:不爱请别伤害 成员:黎泓希、钟智浩 作品简介:让用户在日常使用微信过程中可以通过此小程序,了解在美国打球的球员的详细信息。 105 号作品 《深大小树洞》 队伍:自由组 成员:范家宝 作品简介:深大小树洞目的在于给在校大学生一个匿名的社交平台。社交的形式分为树洞、在线聊天、评论回复、专业求助。 106 号作品 《Hi头像》 队伍:Hi头像 成员:盛瀚钦、夏雪 作品简介:Hi头像集人脸智能识别、节日主题切换、贴纸自由添加、照片一键美化等功能于一体,完美实现节日头像制作。 107 号作品 《QSCamera》 队伍:Incas 成员:沈吕可晟、寿佳涵 作品简介:小程序面向浙大求是潮摄影/视频两部门,用于部门内/跨部门的器材借还登记、借用记录及器材状态查询等场景 108 号作品 《MusicColorBlock-Detail》 队伍:A-star 成员:封磊、彭济东 作品简介:初音未来版本的通过点击/触摸播放声音并出现变化图案的互动内容 109 号作品 《旅小布短视频》 队伍:程序员方方 成员:何方、王霞 作品简介:旅小布是做短视频、直播、旅游攻略的平台,平台希望疫情宅家的人都能分享自己家乡的美景、美食、人文等。 110 号作品 《每日步数打卡》 队伍:鸡蛋汤 成员:戚建萍、谢浩 作品简介:基于小程序云开发的简单步数打卡 111 号作品 《月见》 队伍:月见 成员:陈金生、吴泽锋 作品简介:月见是通过展示走失人物信息的微信小程序,主要通过不同省份展示信息、姓名搜索查询的方式展示走失人物信息 112 号作品 《活动栏》 队伍:炫光 成员:陈旭炫、彭启瑤 作品简介:为了方便各位活动组织者进行消息的生成并对参加的人数进行统计,同时可能对参加者进行时间,地点的提醒功能 113 号作品 《民大新生助手》 队伍:梦的开始 成员:田保书 作品简介:对西北民族大学的每一届新生提供了解学校的平台。有宿舍、食堂、环境、校园视频、交友等服务。 114 号作品 《教资易取》 队伍:教资易取 成员:丁嘉欣、林远棋 作品简介:教资易取小程序,含有同步观看视频,记录笔记的功能。 115 号作品 《历史日历》 队伍:不服就干 成员:马英臣 作品简介:用3D翻页的形式展示历史上以及今天发生的大事,更好的体验,更丰富的内容; 116 号作品 《快刷题库answer question》 队伍:老师说啥就是啥 成员:杨增润 作品简介:#云开发挑战赛#-快刷题库answer question-老师说啥就是啥 117 号作品 《猿宝典》 队伍:五里墩 成员:李洋 作品简介:为了方便各位猿同胞在求职中获取面试信息 118 号作品 《我是主角》 队伍:乐呵 成员:孙凤齐、杨良文 作品简介:选择小程序里的自己喜欢的动漫人物模板,然后上传自己的正脸头像,得到融合后的头像。 119 号作品 《博客系统》 队伍:轻吻也飘然 成员:张建宏 作品简介:就是简单的一个博客系统 120 号作品 《预约班车》 队伍:来拿T恤 成员:陈俊涛 作品简介:预约班车小程序是个人开发的云开发小程序。专门为有班车业务的公司,使用的班车预约小程序。 121 号作品 《天翊图书馆预约》 队伍:天翊 成员:赵逸飞、周卓雅 作品简介:“天翊图书馆预约”是为了解决图书馆占座问题的预约性工具型小程序。 122 号作品 《教务小助手》 队伍:我是来拿衣服的 成员:岑毅鹏、曹彦博 作品简介:由于部分高校的教务系统不兼容移动端,为了方便同学们使用,故开发教务小程序 123 号作品 《外卖系统》 队伍:自由人 成员:桑祺玥、苏娜 作品简介:该作品是外卖系统,主要服务于高校学生点餐及外送等工作,后续部分功能还会持续开发。 124 号作品 《生活智打卡》 队伍:菜鸟玩家 成员:冯天华、韦永恒 作品简介:可自定义每个时间段的打卡任务,记录打卡定位,为个人量身定制的一款计划打卡小程序。 125 号作品 《宝宝约玩》 队伍:Allen 成员:杨健 作品简介:实现绘本馆的线上线下一体化服务,为绘本馆打造借阅一站式服务解决方案 126 号作品 《心灵鸡汤大全》 队伍:小斌 成员:吴学斌 作品简介:在这个压力巨大的社会,每个人都有自己的负面情绪、迷茫、不知所措,心灵鸡汤小程序运用而生 127 号作品 《垃圾问问》 队伍:微旺网络 成员:林涵、杨泉 作品简介:“垃圾问问”是为了方便居民日差查询垃圾分类、了解垃圾分类政策和知识的小程序。 128 号作品 《萌宠创造营》 队伍:我是咸鱼 成员:田煜、王雨晨 作品简介:萌宠创造营是专为爱宠人士中因无法饲养真实宠物或养宠前希望做足准备的铲屎官开发的一款趣味小程序。 129 号作品 《北邮宣讲通》 队伍:TrWyFowrd 成员:付东源 作品简介:基于云开发,为北邮吉林宣讲团开发的内容型小程序。具有文章资讯、评论、聊天室、相册、反馈等板块。 130 号作品 《联系群众客户端》 队伍:独狼 成员:张明东 作品简介:“联系群众客户端”方便群众和联系干部及职能部门的沟通,及时解决及反馈群众提出的诉求需求。 131 号作品 《垃圾分类小程序》 队伍:初生牛犊 成员:韩瑞祺、谢俊强 作品简介:现在的垃圾满天飞,已经造成了比较严重的污染事件,所以垃圾分类很重要! 133 号作品 《轨道nighty night》 队伍:风陵夜话 成员:陈创智、邱彬泳、张楚涛 作品简介:与喜欢的句子、音乐不期而遇。安静,优雅,文艺,走心,随机听一首歌,读一句话,放空自己。 134 号作品 《打卡日历》 队伍:电光闪烁 成员:刘晓阳 作品简介:每日打卡,每天可以对自己需要做的事做一个简单的记录 135 号作品 《便利下单助手》 队伍:天天开心 成员:高全华、赵志超 作品简介:小区-学校熟客下单助手,帮助菜店、便利店、小饭店、水果店等店主打造线上店铺平台以及管理订单 136 号作品 《天天读书》 队伍:爱抓水母的派大星 成员:张渝菱、朱廷果 作品简介:给喜欢读书的用户提供一个记录自己成长的空间,提醒自己不忘记读书 137 号作品 《make的测评程序》 队伍:KEKE 成员:贾为征、马柯 作品简介:make的测评程序是用于对于考核人员进行打分测评,并且记录到数据库中,方便进行测评管理。 138 号作品 《拾一英语》 队伍:云锦 成员:代云皓 作品简介:拾一英语 - 年轻人学习成长的平台,主要针对 二本,三本以及英语不好的人群········ 139 号作品 《云航助手》 队伍:CAUC-GT 成员:郭学宇、陶英杰 作品简介:一个可以为旅客日常和出行提供便利的微信小程序“云航助手”,可添加普通和航班代办,航班和行李查询。 140 号作品 《lononiot》 队伍:软硬天师 成员:陈沛林、陈庆朗 作品简介:用户可以通过小程序云开发对接到腾讯云物联网平台实现控制智能设备。 141 号作品 《消灭癌细胞》 队伍:休闲玩家 成员:江贤河 作品简介:人体中可能有很多癌前细胞随时会变异。作为健康卫士,携带免疫T细胞去消灭他们。为了健康战斗吧! 142 号作品 《柠檬商城》 队伍:过于执 成员:畅羡斌、阴元元 作品简介:该项目用于实现商城的功能,适用于各个年龄段的人来使用,可在娱乐时间放松,轻便快捷。 143 号作品 《myVlog》 队伍:划水水水水 成员:王松 作品简介:模拟实现Instagram的第二页的功能。 145 号作品 《自闭间预定》 队伍:这时代 成员:孙悦 作品简介:这是一款预约自闭间的简单小程序,具有较大的发展和提升空间 146 号作品 《我存》 队伍:好奇工作室 成员:宋洋 作品简介:我存小程序,留住世间一切美好 147 号作品 《心暖农侬》 队伍:智联组合 成员:李宏林、秦宇泽 作品简介:“心暖农浓”是用于心理咨询预约的一款小程序,用于学生心理咨询时与老师进行预约,为有需要的同学提供便利 148 号作品 《行程助手Plus》 队伍:1+1 成员:满萍、石孝辉 作品简介:行程助手Plus 是一款针对当前疫情的综合类小程序,助力于疫情防控的第一线! 149 号作品 《云智慧收银》 队伍:橙光突击队 成员:宋福勇、周俊伟 作品简介:云智慧收银"小程序致力打造一款掌上就能完成收银的小程序,实现一个人,一台手机,即可管理整个商铺。 150 号作品 《红小包抽奖》 队伍:赞过 成员:卢泰城 作品简介:一款模拟微信红包的抽奖小程序,可绑定多个微信群,可切换排序:手气最佳、手气最差、手速最快。 151 号作品 《美今管家》 队伍:美今管家 成员:来成 作品简介:将现有线上成熟项目,使用云开发来实现部分功能 152 号作品 《书香长大》 队伍:Ftime 成员:胡基雄 作品简介:为了解决查阅图书馆书籍在馆信息和个人借阅信息不方便,催还书信息入口深的痛点,而开发此小程序 153 号作品 《红推》 队伍:三十出头 成员:梁少峰、乔育森 作品简介:撮合网红和商家达成合作 154 号作品 《我的旅行箱》 队伍:地表最强 成员:王耀 作品简介:<我的旅行箱>服务于热爱旅游的年轻人,希望能够让你们的旅行变得更加轻松,更加有意义。 155 号作品 《精简之校园二手交易平台》 队伍:红黑 成员:李旺、田璞尧 作品简介:提供给当代大学生的一个方便快速进行二手交易的平台 156 号作品 《AIB校友会》 队伍:miniFlash 成员:刘东盛、张新健 作品简介:主要针对某校学生用户群体,主要功能为团队招募,心里话,网管通道等为学生群体提供便利的功能。 157 号作品 《武冈微商城》 队伍:努力吧 成员:孙婷婷、谢烨烽 作品简介:参赛作品是基于微信小程序云开发,使用云数据库+云函数+云存储完成项目; 158 号作品 《薇科技弹幕墙》 队伍:单身狗 成员:王钊、周笛 作品简介:实时弹幕功能用于晚会等活动现场,扫码即可发弹幕实现显示在墙上; 159 号作品 《RedPoint红点》 队伍:WeClimb 成员:任蓓瑛 作品简介:RedPoint红点是一款面向攀岩爱好者的小程序,提供了攀岩相关信息的交流平台。 160 号作品 《悦分享》 队伍:好想ak 成员:苏明炯 作品简介:这是一款为在校大学生服务的博客分享小程序,通过它,你可以分享各学科知识,收藏他人博文,关注博主。 161 号作品 《实验室管理小程序》 队伍:Rick&Morty 成员:蔡棠汉、陈龙 作品简介:"实验室管理小程序”是一款结合管理实验室出入,记录出入日志,活跃度查看的小程序。 162 号作品 《铁路生涯》 队伍:铁路生涯 成员:徐大治 作品简介:待就业的铁道学院学生可以通过本程序向在职学长进行提问,全面了解各个铁路公司工作和生活信息。 163 号作品 《工程课表》 队伍:张张 成员:张成、张小龙 作品简介:依托微信#云开发#,校园助手类小程序。为用户提供 校园地图、校园Tips、校历查询、上课提醒等 164 号作品 《普罗名特》 队伍:大连队 成员:刘晓龙 作品简介:用于公司产品介绍,有主页,公司详情,产品列表,资料库及常用术语 165 号作品 《趣酿》 队伍:Jump 成员:金伟强、刘茜 作品简介:简单有趣的酿酒 app,用最简单的方式,带你领略啤酒酿造的过程。 166 号作品 《宠幸治疗》 队伍:still 成员:白书博、杨旭旺 作品简介:里面有针对宠物的护理活动,并且提供添加购物车、订单服务,以及展示了关于宠物的动态视频 167 号作品 《论坛小程序》 队伍:Two Pissed Men 成员:付阳烨、秦学聪 作品简介:论坛小程序致力于打造一款大家可以畅聊心得感受的发帖回帖、评论点赞,可在校园、职场等使用的社交平台。 169 号作品 《柠檬收纳》 队伍:蓝柠檬科技 成员:李文华、于杰 作品简介:柠檬收纳微信小程序,一款物品收纳、整理、记录、提醒的实用工具。 170 号作品 《糖友饮食助手》 队伍:A1 成员:田文生 作品简介:糖友饮食助手调用云函数实现对日常饮食中食物含糖量的查询,方便糖尿病患者日常饮食管理,控制血糖水平。 171 号作品 《魅力单词》 队伍:未央队 成员:曹禄丰 作品简介:魅力单词是一款面向学生的背单词小程序,提供了丰富的单词词库以及好玩的背单词模式。 172 号作品 《云迎新》 队伍:脱贫攻坚队 成员:柯华富、彭及钰 作品简介:“云迎新”是为了方便学校新学生及其家长前来报道时,快速了解校园环境的服务型小程序。 173 号作品 《电影周周荐》 队伍:同路人 成员:刘媛婷、尹佳怡 作品简介:本款小程序是专为年轻人开发的,设计的功能非常实用,有较高的利用价值。 174 号作品 《多源在线翻译》 队伍:qyy 成员:邱洋 作品简介:该小程序提供用户随时随地对疑难词汇进行查询的功能,并可进行收藏,帮助用户多次利用碎片化时间进行记忆。 175 号作品 《长大寻物》 队伍:明远 成员:靳明辉、徐源 作品简介:通过小程序良好特性的支撑,“长大寻物”运用信息技术解决人们寻物难,寻主难的问题,促进和谐校园生活。 176 号作品 《AI物以类聚》 队伍:云分类 成员:何鹏辉、李桢 作品简介:物以类聚小程序,依托国家标准,提供垃圾分类查询服务,文字搜索、拍照识图、AI智能机器人等功能便捷生活 177 号作品 《魔方训练计时器》 队伍:mainhanu 成员:马兴驰 作品简介:魔方训练计时器是专为魔方玩家打造的专业计时器,支持数十种打乱公式生成,支持数据记录。 178 号作品 《盲小鹿》 队伍:布丁与画家 成员:李冬冬 作品简介:帮助盲人用户在生活里,识别到取景器前方的道路是否存在盲道,为盲人发现盲道走上第一步 179 号作品 《星河意见箱》 队伍:金钥匙 成员:颜书豪、张辉 作品简介:星河意见箱小程序可以针对个人,集体或企业进行线上意见收集,同时可以对意见进行评论投票 180 号作品 《7日天气》 队伍:Meiōsei 成员:李展鹏 作品简介:7日天气天气预报平台 181 号作品 《每天都要上报体温》 队伍:一个人的队伍 成员:姜英豪、张连龙 作品简介:这是一款上报体温数据的小程序,可以应用在学校以及其他需要统计体温的群体组织。 182 号作品 《简约约拍》 队伍:自习社 成员:林志煌 作品简介:小程序为用户线上可随时随地约拍符合自己风格的摄影师、为消费者和热爱拍摄的摄影师构建一个联通的平台。 183 号作品 《历史上的今天TIH》 队伍:专业团队 成员:郑钰莹 作品简介:微信小程序“历史上的今天”主打历史阅读类,以日期分类历史事件方式,更直观展示人类千年变更进程。 184 号作品 《校园招聘》 队伍:一凡风顺 成员:杨伊凡 作品简介:本作品适用于定制企业的招聘项目,功能完全,页面简洁 185 号作品 《速派递》 队伍:泉知晓 成员:郑运杰 作品简介:速派递是一款校园智慧物流小程序,疫情期间起到了非常重要的作用,在大学校园内可以减少学生们的聚集程度。 186 号作品 《BT清单》 队伍:BT工作助手 成员:崔柏通 作品简介:BT清单是一个结合计时与清单功能的效率类小程序,给您的工作助一臂之力 187 号作品 《火查查》 队伍:火 成员:张洋、金立义 作品简介:消防检查工具,方便专业人员现场进行消防安全检查 188 号作品 《志愿校园》 队伍:菜鸟小程序 成员:孔明林、陆建丞 作品简介:对广西的几所大学进行简单展示,用户可以点赞表示他们的喜欢 189 号作品 《头马报名》 队伍:借书巴巴 成员:陈刚 作品简介:这是一个头马活动报名的小程序,用户可以发起报名活动,参加角色报名,投票和活动计时等功能. 190 号作品 《CAN课程表》 队伍:CTi 成员:王炜皓 作品简介:提醒学生上课的课程表。主要面向高校学生。定位是简洁高效,尽可能保证功能简单且实用。 191 号作品 《地摊生活》 队伍:itboy 成员:马浩阳、秦卫星 作品简介:地摊生活,助力城市夜经济,为周边群众提供多样化生活服务,让地摊生活成为城市的一道风景线。 192 号作品 《诗华社》 队伍:小小一秋 成员:陈小弟 作品简介:一个可以添加诗词阅读的小程序 193 号作品 《狗头的店,狗头管理》 队伍:GOUTOU_PLUS 成员:杨雪健 作品简介:动手打造属于自己的小商店。 194 号作品 《浙里淘志愿》 队伍:卡普中将 成员:孙昌谱、孙思思 作品简介:浙江高考淘志愿:解决考生如何填写志愿 195 号作品 《OTP动态验证码》 队伍:TORSER 成员:傅渭军、王晋鑫 作品简介:基于TOTP算法的动态验证码微信小程序,兼容Google Authenticator二步验证。 196 号作品 《一瞬相册》 队伍:就是随便起个名字 成员:刘钟钰、庞博 作品简介:一个上传自己照片的地方。 197 号作品 《急速查病》 队伍:YnnnP 成员:陈燕超、黄攀 作品简介:急速查病是一个以提供健康资讯、疾病预防、疾病数据查询、医患交流为主的健康小程序。 198 号作品 《蓝医先生》 队伍:蓝医先生 成员:时上、费吕宗 作品简介:本系统针对体检项目分离,采用线上预约的方式直接预约体检项目,为医患就诊关系提供了很大程度上的便捷。 199 号作品 《大师请提笔》 队伍:965 成员:苏鹏飞、朱令超 作品简介:生成你独特又搞笑的藏头诗,表爱,娱乐,吐槽,各种套路,尽由大师您来操刀 200 号作品 《tusake Today》 队伍:-404 成员:王耀、张煜 作品简介:本作品共三个模块为用户提供信息来准备一天的事项 201 号作品 《汉泰小词典》 队伍:掺点水分儿 成员:潘小龙、王子乾 作品简介:汉泰小词典是一款针对小语种开发的小程序 202 号作品 《次元乌托邦云网盘》 队伍:次元乌托邦 成员:康涛、王南恺 作品简介:次元乌托邦云网盘 203 号作品 《云线名片》 队伍:跨界王者 成员:曹文锋、卢聪 作品简介:一款简单的个人名片小程序。将云线名片分享给朋友,即使没有使用过这个产品也能快速完成名片交换。 204 号作品 《微源库》 队伍:微源 成员:黄涛、谢先锋 作品简介:微源库小程序就是微信资源库,为大家免费提供资源。 205 号作品 《focusair》 队伍:不知道对不队 成员:禤晓铖、姚森涛 作品简介:focusair一款为解决备考复习过程中低效率、无针对性的问题所搭建的复习资料与学习交流的共享平台。 206 号作品 《逛逛地摊》 队伍:特斯拉梦之队 成员:李子康、桑子灿 作品简介:本产品可显示周围的地摊,如果想自己摆摊,则可以注册摊主身份将自己的地摊显示给别人 207 号作品 《Y计算器》 队伍:去菲律宾冲浪 成员:刘昕怡、杨健聪 作品简介:「Y计算器」是一款旨在提高数学计算效率的工具型小程序。 208 号作品 《云表白》 队伍:姚一姚队 成员:姚泽盛 作品简介:小程序 云表白 打造大学生的表白平台 209 号作品 《林林的妙妙屋》 队伍:酸菜鱼 成员:刘柏成、刘振林 作品简介:林林的妙妙屋 210 号作品 《宝塔出行》 队伍:圣漫 成员:延绥强 作品简介:生活出行工具 211 号作品 《宠物营地》 队伍:巅峰战队 成员:邓旭晨、王辰然 作品简介:给现猫主人和未来得猫主人打开一条通道,更方便找到等待领取得猫猫狗狗,也可以发布自家得猫猫狗狗。 212 号作品 《YAccount记账助手》 队伍:红鲤鱼与绿鲤鱼与驴 成员:雷鸣宇、颜子清 作品简介:养成记账的好习惯,有助于您掌握个人或家庭收支情况,合理规划消费和投资。 213 号作品 《云校知》 队伍:天策阁 成员:李天赐 作品简介:校内里的百度,生活的助手,娱乐的指南一个能解决你在校园中的一半问题的小程序 214 号作品 《滑伴》 队伍:认真一伴 成员:张一男 作品简介:滑伴是一款面向滑雪用户的约滑平台、私教平台,用户可以在上面找到一起滑雪的同伴,进行技术交流。 216 号作品 《情侣券》 队伍:想做就做 成员:陈宇明、王丝雨 作品简介:恋爱必备,和好神器,大大增加情侣之间的感情。 217 号作品 《简单的课表小程序》 队伍:我是橙小白 成员:陈晓培 作品简介:简单的课表、校内通知小程序 218 号作品 《LE编程》 队伍:做梦都在敲代码 成员:赵敬轩、李家辉 作品简介:该小程序内置有行业动态,竞赛通告和算法知识,旨在为广大计算机爱好者提供便利。 219 号作品 《超市Boss助手(零售助手)》 队伍:焕然天成 成员:徐恒 作品简介:零售管理助手,用于货物管理、单据打印、数据监控分析、扫码查询等,让客户更加“便捷”、“高效”进行管理 220 号作品 《InterviewHub》 队伍:我一般一个人上单 成员:谢广平、尹可汗 作品简介:该项目是一个收录互联网行业开发岗方向常见面试题目的工具类小程序,希望能帮助开发者寻找到心仪的工作。 221 号作品 《私约团课》 队伍:莹仔 成员:王莹 作品简介:展示课程,可以看到课表的一个小程序 222 号作品 《顾家》 队伍:秋城与星辰大海 成员:林景恒 作品简介:家是一个实实在在,是父母为我们提供的温馨港湾,这里有亲情,有对我们生活的牵肠挂肚的人。 223 号作品 《买它or not》 队伍:KX 成员:谢彦恺 作品简介:想要做一些大额开销之前,先问问自己的钱包有没有准备好。良心劝退冲动消费小程序。 224 号作品 《Mayday Online》 队伍:Mayday Online 成员:朱顺意、朱照擎 作品简介:涉及技术为常用组件、API、订阅消息、数据库操作、定时触发器、云开发 225 号作品 《修补匠》 队伍:为梦而战 成员:欧阳艺铭、赵亚肖 作品简介:运用云函数、云存储、数据库 开发一款简洁的信息发布平台 商品修复展示 226 号作品 《趣答星球》 队伍:叶老板请吃饭 成员:邓裕发、王天池 作品简介:趣答星球是一款趣味答题小程序,让大家能够更好地了解到生活中一些小常识和有趣的事情 227 号作品 《流浪猫速查手册》 队伍:猫 成员:杨先锋 作品简介:快速查询附近的流浪猫 帮你找到"梦中情猫" 228 号作品 《运动会管理系统》 队伍:业余爱好队 成员:张建明 作品简介:运动会管理系统适用于各级各类学校(单位)召开运动会进行在线报名管理查询,可以同时有多个单位使用。 229 号作品 《为特餐饮助手》 队伍:白马为科技 成员:黄日强 作品简介:基于微信小程序.云开发技术 结合物联网云打印服务,实现低成本,高效能的互联网+餐饮解决方案 230 号作品 《我车呢》 队伍:try 成员:刘畅 作品简介:本项目为解决经常忘记自己车辆所放位置而设计,提高人们的工作与生活效率。 231 号作品 《校园疫情管理小程序》 队伍:放我出去 成员:段奔飞、徐胜 作品简介:为方便被封校管理的大学生以及配合校园疫情管理,设计了一款包含请假,体温上报,出行记录等功能的小程序。 232 号作品 《AI写诗》 队伍:Citizen Four 成员:姜波、王世龙 作品简介:人工智能帮你写诗,人人都是徐志摩 233 号作品 《芳甸鲜花商城》 队伍:小哥别哭 成员:黄娟、孙乐进 作品简介:乐闻商城是一个服务独立商户的内容完全可配置的商城微信小程序。 234 号作品 《席博》 队伍: 王泽中 成员:王泽中 作品简介:这个作品是一个博客平台。 235 号作品 《垃圾分类赢好礼》 队伍:我爱吃雪糕 成员:李旭杰 作品简介:在日常生活中帮助用户垃圾分类,并通过动态发表的形式积累环保积分兑换奖品,鼓励人们垃圾分类! 236 号作品 《企业招聘》 队伍:云中君 成员:黄文宝、谢建辉 作品简介:企业招聘小程序是基于纯云开发进行开发的,企业进行注册和发布岗位等,为毕业生提供更方便的就业渠道和方向 237 号作品 《LoRa智能家居管理》 队伍:智能家居冲冲冲 成员:蔡伟钧、吴浩明 作品简介:LoRa智能家居云开发小程序能实现获取web端通过mqtt协议收集的智能终端数据并显示,以及远程控制 238 号作品 《悦寻失物招领》 队伍:百分之五 成员:王浩、卓圣洁 作品简介:悦寻失物招领设计了一个无需工作人员管理、无需建立失主与拾物者联系的自助失物招领中心 239 号作品 《嘿!我在这儿!》 队伍:星辰大海 成员:姚海 作品简介:朋友圈不一样的地方,在空间和时间上去发现陌生人吧 240 号作品 《实用小工具》 队伍:我来打酱油 成员:林再飞、刘桥敏 作品简介:可订阅天气小程序 241 号作品 《梦凡云OA》 队伍:梦凡 成员:孟雪、赵晨辉 作品简介:梦凡云OA,实现公司的通知动态展示,内部收发信件, 请假审批功能, 工资查询功能 242 号作品 《重邮课后小程序》 队伍:金皆 成员:钟锴锴 作品简介:重邮课后小程序拥有活动发布、二手发布和根据学校课表自动生成值班表三大功能。 243 号作品 《每日戒糖》 队伍:冲鸭 成员:赵鹤宇 作品简介:过度吃糖危害皮肤健康等,使用每日戒糖小程序,让您更具智慧的面对糖。 244 号作品 《高级工匠心录》 队伍:小北鱼 成员:刘畅畅 作品简介:一个练习题目的工具,特色多人聊天室答题,专用的答题按钮,方便快速,独特的拟态风设计 245 号作品 《农大饭食》 队伍:博瑞12313 成员:刘国瑞、杨树博 作品简介:校内约饭 246 号作品 《Holly食刻》 队伍:蓝天白云 成员:许堃莹、周巍 作品简介:一款为提供点餐的在线餐厅点餐的小程序,其主要功能为:到店堂食点餐。服务覆盖午餐、水果、甜品饮品等。 247 号作品 《JF校园云招聘平台》 队伍:雏鹰团队 成员:李海亮、梅树正 作品简介:该项目的宗旨是以广东石油化工学院为起始,以小程序为运营主体的校园招聘平台! 248 号作品 《学辰ing》 队伍:知吾煮 成员:陈鸿基、王祺珑 作品简介:项目致力于为校园生活提供一个完整的二手交易社区 249 号作品 《LMSH7TH》 队伍:lmsh7 成员:袁承勋 作品简介:使用云开发功能写的树洞,目前支持注册登录,树洞广场,个人树洞,发布树洞。 250 号作品 《微信小程序工具箱》 队伍:逍遥子 成员:陈卓鑫 作品简介:一款后端完全基于云函数,云存储,云数据库实现实用工具小程序 251 号作品 《微助helper》 队伍:微光 成员:周芒 作品简介:一款以上传排班表和管理排班表为主题的小程序 252 号作品 《小记易》 队伍:尽力而为 成员:练方梯、李俊峰 作品简介:语音识别,富文本编辑器,图片识别,文字大爆炸。超棒的用户体验。 253 号作品 《随变记账》 队伍:随便蛐蛐 成员:黄美、黄鑫 作品简介:随变记账——基于云开发的记账微信小程序 254 号作品 《叮咚倒数日》 队伍:咚咚宇宙无敌舰队 成员:左太宇 作品简介:⛅️ 微信小程序,叮咚倒数日-生活提醒好帮手 255 号作品 《画画的北鼻》 队伍:黑镜 成员:陈家辉、陈光亮 作品简介:类似你画我猜,让用户可以通过画一个东西和填充它的答案,进去详情页可以猜测这张图片的是什么 256 号作品 《吃吃等你》 队伍:吃吃等你 成员:陈佳键、刘桂冕 作品简介:“吃吃等你”小程序,集分享与互动于一体的菜谱小程序 257 号作品 《待办事项工具》 队伍:腾沐之光 成员:林佳鸿 作品简介:直观便捷地管理待办事项,帮你保持专注与高效工具型小程序。 258 号作品 《男人买菜》 队伍:JS起舞 成员:贾涛 作品简介:小程序通过对菜肴的食材进行分析介绍和种类数量的分割重组,为买菜的男人提供一份简明扼要的列表清单!!! 259 号作品 《高校信息共享平台》 队伍:这是个队 成员:李吉望、张维科 作品简介:小程序集查询专业信息、高校状况、校园生活服务、综合性强,建立社团或学生组织运营平台,实现良性循环。 260 号作品 《一只书匣》 队伍:天城软件 成员:王心圆 作品简介:一只书匣,用于记录自己读书计划以及,养成习惯的读书小程序 261 号作品 《PicGo图旅》 队伍:jotang 成员:马世豪、王省 作品简介:PicGo是一款微信小程序,借助云开发平台,它的拍照搜索,智能推荐能为用户提供低成本智慧旅行解决方案 262 号作品 《云享坊》 队伍:学习小鑫 成员:陈泽鑫 作品简介:云享坊(cloudshare)是一个分享与交友平台 263 号作品 《拯救不开心》 队伍:不吃辣条了 成员:管政、孟笑晨 作品简介:每个人都或多或少会有一些抑郁甚至压抑的感觉。俗话说,没有音乐不能治愈的心灵,没有交流不能放大的美好。 264 号作品 《旅梦恋爱》 队伍:金龙 成员:段鑫、吴季龙 作品简介:都说有情人终成眷属,但没有好的方法去解决问题,也是徒劳而旅梦恋爱便是这样一款解决男生恋爱问题的小程序 265 号作品 《一起来学计组叭》 队伍:行健622 成员:付航、范晰雯 作品简介:学习计算机组成原理的小程序 266 号作品 《科联答题》 队伍:可达鸭小团队 成员:林科达、毛蕴祺 作品简介:用户在小程序中参与与社团有关的答题以及在社团vr实景中寻找线索进行解密,以达到线上招新宣传的作用。 267 号作品 《祥云》 队伍:llx 成员:林立祥 作品简介:图片的存储转发分享等功能的实现,实现拍照上传,添加照片,分享链接小程序码,下载,删除照片等功能 268 号作品 《群消息公示》 队伍:flyingman 成员:石鹏、孙硕硕 作品简介:为群管理员提供可编辑图文公告的工具 269 号作品 《准到聚餐》 队伍:准到科技 成员:陈洋、徐呈龙 作品简介:准到聚餐是一款能够解决朋友间聚餐最佳时间选择、精准人数统计、人员准时到达的小程序。 270 号作品 《课室助手》 队伍:队雷队去 成员:罗俊杰、潘金泉 作品简介:去纸化申请预约课室,方便快捷,高效率。 271 号作品 《泊宠》 队伍:猫南北 成员:故里、钟粤 作品简介:宠物上门喂养服务 让您的爱宠安心在家,铲屎官定时上门照顾。 272 号作品 《会议邀请函》 队伍:我们队 成员:林浩昌、杨浩 作品简介:“会议邀请函”是一个基于云开发的微信小程序,方便人们在线生成与分享会议邀请 273 号作品 《电魔方智能家居》 队伍:羊羔队 成员:高飞翔、杨承举 作品简介:羊羔队高飞翔与杨承举的小程序作品 274 号作品 《安全帽智慧监控小程序》 队伍:魔法少女 成员:罗姣、谭秋璐 作品简介:智能监控能够保证在没有人员监管的时候提醒相关人员佩戴安全帽 275 号作品 《社交平台》 队伍:ccc 成员:陈源坤、徐健猇 作品简介:该小程序给用户提供了交友的平台,用户可修改自己的信息,并与其他用户进行互动(点赞、分享位置、加好友) 276 号作品 《weSport》 队伍: emBus 成员:陈晓俊 作品简介:约球小程序:方便球队直接的约球; 277 号作品 《微信自助点餐小程序》 队伍:Never Give Up 成员:钟朋 作品简介:1.微信位置定位和显示 2.主页轮播图 3.无缝滚动 4.横向滑动tab选项卡 5.Menu菜单栏 278 号作品 《二手市场》 队伍:生榨米粉 成员:李燕华、周志明 作品简介:在平台上发布二手商品的消息,同时享受线上交易商品,发布商品交易并交易完成可增加用户在平台的信誉积分。 279 号作品 《校园简单易》 队伍:白嫖怪队 成员:汤国龙 作品简介:校园简单易-科创中心智能收取系统,旨再为各高校科创赛事提供一个收取、审核、汇总于一体的智能化服务平台 280 号作品 《温湿度实时监控及开关控制小demo的设计》 队伍:漂泊的太阳 成员:李焰权 作品简介:温度实时监控到小程序,小程序也能通过button组件控制灯的开关 281 号作品 《图迹圈》 队伍:Ylhaaa 成员:杨良浩 作品简介:提供一个平台给予用户分享自己喜欢的图片或视频,并提供诸如点赞,评论之类的社交功能。 282 号作品 《缸中之鱼导购系统》 队伍:缸中之鱼 成员:石洪玮 作品简介:缸中之鱼 一款能把针对买点的调查问卷过程和消费者网购过程二合一的小程序 283 号作品 《识译小程序》 队伍:秃头小队 成员:李鑫桃、黄智毅 作品简介:一个集图像识别、菜品识别、实时语音识别的小程序。主要面向用户群体为境外游的旅客 284 号作品 《数字余杭》 队伍:长天 成员:陈好好 作品简介:“数字余杭”构建余杭本地区域数字化 依托本地信息资讯,构建本地用户画像 285 号作品 《MallBook》 队伍:方丘子 成员:曾钱松、兰汶鑫 作品简介:本产品的针对人群是在校大学生。设计一款能够帮助学生学习又能实现便捷校园服务的应用、新鲜的体验的产品 286 号作品 《成长课程表》 队伍:老干妈战队 成员:王勃涛 作品简介:一个课外补习班的课程表 287 号作品 《小婷和小天一起记账》 队伍:Sunday 成员:何胜天 作品简介:一名普通程序员甲 288 号作品 《心里有树》 队伍:心里有树 成员:黎钎桦、周灿基 作品简介:心里有树是一款打卡小程序,为了让人们更好地发现目标,坚持目标,完成目标。该项目由两位广州大学学生完成 289 号作品 《优鲜配送联盟》 队伍:JYTX 成员:陈梓杰 作品简介:线上买菜服务平台,轻松便捷,高效省时,助力打造“智慧防疫农贸市场”。 290 号作品 《青存纪》 队伍:奖你一条鱼 成员:蒋吉平、余淑豪 作品简介:人生是酸甜苦辣的,至少在某一刻你应该记录下来 291 号作品 《酒肆 家谱》 队伍:酒肆 成员:韩佩珍 作品简介:酒肆家谱:①基于云开发的家谱管理②家谱信息保存在云数据库③成员编码由云函数生成④照片保存在云存储 292 号作品 《天天诗词》 队伍:绯光深林虹彩桥 成员:林纪元 作品简介:用于查看日期 293 号作品 《城市预警系统》 队伍:喵星队 成员:林妙妙 作品简介:城市预警系统,用于辅助城市设施管理部门管理城市,借助城市居民的力量,帮助城市问题的快速发现和解决 294 号作品 《契约farm》 队伍:twelve 成员:曹月星、吴静怡 作品简介:契约farm,拥有自己的虚拟farm,同时与商家交互,实现现实化。 295 号作品 《糗事》 队伍:奥利给 成员:马意然、唐滢沛 作品简介:为各类人群提供有趣的短文章短视频 296 号作品 《CUMTB疫情管控期间学生外出申请系统》 队伍:未命名 成员:高扬 作品简介:疫情管控期间,矿大北京学生出入校园可在本小程序中提交申请,由辅导员审核后,获得对应时间段的外出许可。 297 号作品 《比斯兔u》 队伍:好听的音乐 成员:陶俊岚 作品简介:给自己学校设计的圈子应用的demo。主要功能有:问答式贴吧论坛,提高寻找信息的效率。校内参数组队。 298 号作品 《校园书友》 队伍:一人一队 成员:许乐怡 作品简介:校园书友小程序的主要功能是读书分享和督促阅读,可以给用户提供一个专门用于读书分享的平台 299 号作品 《汇尤e家》 队伍:尤佳 成员:苏姗、张正午 作品简介:汇尤e 家小程序是基于云开发的社区服务商城,与普通商城不同的是我们特别推出了资源置换的功能。 300 号作品 《哏儿通》 队伍:哏儿通 成员:刘雨洋、李英军 作品简介:致力于推广哏都传统文化,专注于分享优质城市信息,打卡地点,特色美食······ 301 号作品 《FTodoList》 队伍:107 成员:曹瑞辰、王涛 作品简介:todolist,帮助计划管理的一款小程序,简约的封面和简单的操作 302 号作品 《统一运维平台》 队伍:蓝天碧水 成员:宋志军 作品简介:小程序实现页面 303 号作品 《虾麦》 队伍:动感JY 成员:王嘉懿 作品简介:二手物品信息分享平台,一键生成长海报 利用分享到微信群获取群id,看群成员都在卖什么 304 号作品 《餐饮流水记账》 队伍:鑫队 成员:袁鑫 作品简介:餐饮流水记账小程序是用于中小餐厅流水记账的小程序,在记账能力不强的餐馆中,帮助商家进行交易管理。 305 号作品 《迷你小摊》 队伍:我的小摊 成员:胡其洋、李浩楠 作品简介:给地摊经济下的摊主提供一个信息发布渠道,让喜爱逛夜市的人多一个信息获取的渠道 306 号作品 《抽屉表情》 队伍:为了获得T恤 成员:黄志杰 作品简介:帮助用户存放相册中杂乱的表情包,提供添加标签快速搜索分享,还有趣味的表情包加字功能。 307 号作品 《招新Plus》 队伍:对怼堆兑队 成员:金奕辰 作品简介:适用于大学校级组织大规模招新,完全基于微信小程序原生云开发能力开发的小程序 308 号作品 《中北请假助手》 队伍:田园创新队 成员:宋文暄、樊旭超 作品简介:解决高校请假事务管理处理效率较低的问题,利用高校校园网络和人手一部的学生智能手机解决。 310 号作品 《初心日历》 队伍:嘟嘟队 成员:王泽世 作品简介:一款记录亲人好友的生日日期的小程序 311 号作品 《知侬》 队伍:知侬 成员:黄彭志 作品简介:本小程序用于华中农业大学活动信息实时预告、本科毕业生情况查询、各类考 试信息实时提醒、个人目标制定。 312 号作品 《“倾听者”综合型语音评价系统》 队伍:驭键师 成员:潘晨杰 作品简介:“倾听者”综合型语音评价系统是一个智能的口语评测系统,对用户的发音问题进行分析和纠正。 313 号作品 《快表白》 队伍:小猿人 成员:刘永建新 作品简介:本小程序是一个表白小程序,有为五个页面,分别是:首页、结果页、个人中心页、预览页、创建页。 314 号作品 《校园二手购》 队伍:大王叫我来巡山 成员:连梓煜、黎烨亮 作品简介:校园二手购是一个供学生进行二手物品交易的平台,可以在此发布或者寻找自己闲置或者喜欢的物品。 315 号作品 《CC交个朋友》 队伍:余衫马 成员:马建生、邹烨 作品简介:一个轻量简洁的图片社交小程序,愿你在这里能够找到分享的欢乐和志同道合的朋友。 316 号作品 《校园小唤》 队伍:校园小唤 成员:蒙焕厅、谢先晖 作品简介:“校园小唤”,一款专门为大学生提供服务的社区平台 317 号作品 《校园寻回》 队伍:小旅分队 成员:郭逸涵、吴海兵 作品简介:校园失物招领平台,详情请见文章链接 318 号作品 《seven取餐小程序》 队伍:Seven 成员:叶尔扎提•赛热哈力、哈那拜·巴依居玛 作品简介:此项目用于高校拥挤的餐厅排队打饭,这个可以很好的节省排队时间,还可以再等餐的同时能够干其他事。 319 号作品 《闪加》 队伍:zaaaap 成员:胡泰康、唐源 作品简介:闪加,一款快速分享您的社交app的小程序 320 号作品 《格式转换工厂》 队伍:One-YuTian 成员:李进 作品简介:文件格式转换工具,PDF免费转Word,200种文件格式在线转换。 321 号作品 《点滴互助》 队伍:小鱼队 成员:刘宁波 作品简介:点滴互助,互相帮助平台 322 号作品 《小青考证》 队伍:做什么都队 成员:许芸、钟卓伦 作品简介:小青考证,一个“想练即练,快速刷题,随时回顾”的考证刷题小程序 323 号作品 《IAI CDS》 队伍:disassembly 成员:侯力新、焦丹阳 作品简介:工业智能组件拆解小程序 324 号作品 《人脸识别虚拟仿真实验》 队伍:科学探索小队 成员:陈鹏宇、耿雪纯 作品简介:人脸识别虚拟仿真实验平台是将人脸识别的每一步进行可视化的过程,并且可以自定义一些参数。 325 号作品 《广大搜搜》 队伍:特种部队 成员:黄思豪、吴坤 作品简介:广大搜搜小程序是为了方便学生交流与更高效地获取对自己有用的信息而产生。 326 号作品 《寝记账》 队伍:秃如其来 成员:归律发、王宇海 作品简介:让您的记账和分帐变得简单,算账交给寝记账,收支管理更清晰。 327 号作品 《大宗交易数据查询分析助手》 队伍:Y2020 成员:杨坤、杨威 作品简介:A股历史大宗交易数据条件查询、A股历史大宗交易数据云端计算分析、分析结果可视化。 328 号作品 《無音不泉》 队伍:無音不全 成员:付永杰、马菀君 作品简介:無音不泉小程序,集歌曲播放,图片社交为一体,让现当代青年在音乐中找寻没有烦恼的自己。 329 号作品 《ITD智慧校园》 队伍:ITD智慧校园 成员:梁奕隽、张镜濠 作品简介:ITD智慧校园是套基于微信小程序+web的校园生活平台,可对在校生实施精准管理,并连接家校关系。 330 号作品 《We广油》 队伍:快乐sleep 成员:张伟、陈伟达 作品简介:致力于帮助我们广东石油化工学院的师生更方便的查询教务信息,测试账号和密码为test和test 331 号作品 《地图留言》 队伍:Nodejs单推人 成员:桑子瑞、易立 作品简介:一款可以在地图上留言的小程序 332 号作品 《照片时光机》 队伍:AmazingPromise 成员:何长霖 作品简介:一个帮助你在同一地点同不同时间,拍出相同角度照片的小程序。实现便捷的查看功能和惊艳的照片效果。 333 号作品 《恋爱空间》 队伍:君吹 成员:何嘉润 作品简介:一个私密的二人空间,帮您记录恋爱中的点点滴滴。 334 号作品 《文艺比赛小行家》 队伍:流浪者 成员:张达 作品简介:主要用于公司内部的文艺比赛活动,小程序成了普通观众的投票器,主办方控制屏幕的遥控器。 335 号作品 《哪天约》 队伍:BK 成员:高亦非、王帅雨 作品简介:「哪天约」小程序是群约时间协调的效率类工具。可以根据活动类型,通过群聊或二维码分享群约,获得最优解。 336 号作品 《社交点评》 队伍:123 成员:黄义斌 作品简介:社交点评 337 号作品 《疫简签》 队伍:L.Y 成员:李旭利、薛涛 作品简介:疫简签小程序用于各大高校对于疫情现状的查询以及以自己所在班级为单位汇报体温,确认学生身体的健康情况。 338 号作品 《小神助手》 队伍:小龙虾队 成员:齐若涵 作品简介:生活中并不起眼的却很有趣的小功能集合。 339 号作品 《HomeAssistant》 队伍:远哥制造 成员:高晨远 作品简介:查看家中传感器数据并控制各种开关的物联网微信小程序 340 号作品 《健身小程序简介》 队伍:昆工代码疯狗 成员:马一文、张学晨 作品简介:健身小程序 341 号作品 《东方小游戏》 队伍:一枝花 成员:李君豪 作品简介:基于云开发的小程序文字小游戏,包含了一个玩家可以留言的留言板。 342 号作品 《郑州限行查询》 队伍:鲤鱼绿与路驴绿 成员:杜明亮、李文 作品简介:采用了云函数+云数据库的形式,判断当天的限行数字 343 号作品 《布告》 队伍:一杯茶一包烟 成员:陈嘉鹏、赵宇凯 作品简介:小程序希望打造一个大学生活动信息交流平台,使得参与者有渠道寻找活动并参加,发布者有渠道发布活动并引流 344 号作品 《心跑道》 队伍:敲菜队 成员:黄承大、邱宣富 作品简介:本小程序适用于心理知识竞赛,旨在为举办方和参赛选手提供“一站式”快捷,便利的服务平台。 345 号作品 《义思丽代办平台》 队伍:翱翔队 成员:伊力哈木·阿布都克热木 作品简介:一般用户可以通过此小程序提交个体工商注册订单,平台管理员可以代用户办理对应的执照 346 号作品 《江大电服》 队伍:lime2019 成员:夏家华、夏天 作品简介:基于腾讯云开发环境的Web网页和小程序,主要功能为部门内成员管理和全校师生电子技术问题填报。 347 号作品 《BJUT活动助手》 队伍:工大小分队 成员:李泊岩、李金星 作品简介:《BJUT活动助手》是一款集发布、报名、汇总于一体的活动助手小程序,从而提升学生和老师的活动参与度。 348 号作品 《佤山行》 队伍:寻影智联 成员:裴晓航、成广彦 作品简介:以临沧市沧源佤族自治县的佤族特色为契机,结合当地旅游现状、农副产品、民族饰品,开创的微信小程序平台。 349 号作品 《定约》 队伍:三十而已 成员:李亚飞、陈文烽 作品简介:帮助你和朋友建立一个未来时间、地理位置的见面协议。小小的约定,也有我们一起见证,一起积极参与生活! 350 号作品 《云开发带后台商城系统》 队伍:半瓶子 成员:宋文凯 作品简介:全由云开发实现的带后台的商城系统,所有的图片和文字可以改动,后台是手机直接操作的。 351 号作品 《流量计设备性能测试平台》 队伍:北京素水 成员:王建存、赵子坤 作品简介:腾讯云物联网+云开发+小程序,轻松实现设备临时测试和长期监测,综合成本低,稳定性高。 352 号作品 《预付费机票销售小程序》 队伍:我行我素 成员:宋清华 作品简介:预付费机票销售预约小程序 353 号作品 《失全拾美》 队伍: 周易 成员:张毅、周鼎举 作品简介:该小程序为失物招领平台。针对在校大学生,使用该小程序后可快速物归原主失主 354 号作品 《微购收单》 队伍:讯洲科技 成员:周靖 作品简介:微购收单是一款辅助线下团购、拼单类的工具应用。主体逻辑是线上预约,微信转账,到店自提 355 号作品 《校拍》 队伍:校拍 成员:李超超、张砚耕 作品简介:校拍小程序和同名公众号主要是为了展示用户学校的校园美景以及分享各种摄影入门小技巧,吸引更多的人参与。 357 号作品 《校内外卖》 队伍:996秃头小分队 成员:邓嘉豪、胡严 作品简介:一个针对于学校饭堂或小卖部商家们的一个外卖小程序 358 号作品 《秀食餐饮小程序》 队伍:橙启盈 成员:王皓 作品简介:本作品主要的应用场景是为线下的小型餐饮企业打造一个低成本的私域部署的电商O2O平台。 359 号作品 《自助心理成长》 队伍:小黑 成员:程思、刘韬 作品简介:自助心理成长,靠自己的力量,觉察自己的内心,应用心理学知识,使我们此刻更心安,未来更幸福。 360 号作品 《xcu许院生活》 队伍:codeOne 成员:杨少博 作品简介:许院生活是一款生活交流类小程序,针对同校同学需求实现了二手交易、兼职发布接取、校园动态展示等功能。 362 号作品 《零工哥》 队伍:阿司匹林 成员:叶世红、周清平 作品简介:零工哥小程序是给需要雇佣临时工的企业,和需要打零工的工人和团队搭建一个平台。 363 号作品 《预约挂号小程序》 队伍:三缺一 成员:王泰禹、余建诚 作品简介:每个去医院看过病的人都经历过很长的队伍,白白浪费了大半天时间,于是乎,一个预约挂号小程序由此诞生。 364 号作品 《智慧账本》 队伍:为了苹果 成员:杨凯博、蔡利江 作品简介:小程序主要用于方便学生统计日常开销,提升学生的开源节流能力 365 号作品 《印纪》 队伍:残阳如血 成员:刘烨 作品简介:一款基于现实事件的通用成就系统 记录自己高光时刻,发现分享成就,结识更多志同道合小伙伴的平台 366 号作品 《我家的WIFI》 队伍:延旭未来 成员:晏旭 作品简介:利用云开发实现wifi密码备份,联网的小工具,再也不会忘记密码了,也可以把密码分享给朋友使用! 367 号作品 《瓜大e拼车》 队伍:四四四一队 成员:马翔、宋文超 作品简介:拼车 368 号作品 《校云通》 队伍:校云通 成员:朱子言、周何顺 作品简介:校云通是一个基于云开发的校园商家点单、排队微信小程序,未来加入跳转功能实现校园内商家互通。 369 号作品 《迷你论坛》 队伍:sfasheep 成员:冯学煜 作品简介:本微信小程序于通过无干扰项的简陋论坛,弥补社团微信群交流的不足 370 号作品 《袋鼠培培》 队伍:阿婕和她的叔叔 成员:钟雅婕、张宇浩 作品简介:袋鼠培培 带你飞!袋鼠培培是一个专注教育培训行业,以个人兴趣爱好、才能类培训为主的O2O平台。 371 号作品 《物流小程序》 队伍:不重要二队 成员:薛凯 作品简介:物流小程序,方便用户下单,司机和管理员管理订单 372 号作品 《Simple Note 短记》 队伍:代码工坊 成员:李忠义、张京 作品简介:小程序云开发挑战赛作品——Simple Note 373 号作品 《见字如面》 队伍:雏鹰队 成员:王庆洲、陈月 作品简介:公众号有留言、消息时运营人员第一时间接到推送通知,并回复处理。 374 号作品 《易珠》 队伍:拈花我把酒 成员:陈炼鹏、陶绍基 作品简介:本小程序为了服务学校师生毕业季物品和平时二手书的交易问题。既环保又方便
2020-09-25 - 开放报名:微信开放平台公交地铁行业小程序乘车码激励活动
开放报名:微信开放平台公交地铁行业小程序乘车码激励活动 为更好的鼓励服务商开拓公交地铁行业小程序乘车码场景业务,为广大用户提供高效、便捷、贴心的出行体验,微信开放平台推出本激励活动,服务商代所授权的小程序商户报名成功并满足规定的条件后,服务商可获得相应的奖励。 一.活动规则1.有效期:2020年8月1日到2021年1月31日 2.行业范围:公交地铁行业小程序乘车码业务(需开通微信小程序广告流量主功能并接入小程序广告) 3.奖励对象:取得公交、地铁、城市通卡公司官方授权的小程序服务商(非腾讯主体) 4.奖励规则: 4.1.奖励计算方式 以小程序APPID为计算单位,符合准入条件的服务商报名并经审核通过后方可参与本活动,参与本活动并满足达标条件的小程序,给予服务商按小程序广告流量主的收入流水的18%进行奖励。 5.准入条件: 5.1.服务商与小程序具备绑定授权关系; 5.2.小程序类目为公交地铁且具备乘车码功能; 5.3.小程序的公交地铁行业的上个自然月代扣笔数>100; 5.4.提供公交地铁行业公司关联性证明材料。 6.达标要求:详情请登录服务平台查看。 二.参与流程1. 服务商注册open账号并创建第三方平台 1.1.第三方平台的申请和上线流程参照点击查看【注册第三方平台操作指引】; 1.2.如果在创建第三方平台时选择的是“定制化开发服务商“,则需要做如下操作:可将自己已经开发出的定制化小程序关联到服务商平台中,生成凭证(票据)填充到小程序代码包中进行关联,平台获取开发关系,点击查看【生成凭证操作指引】; 2.服务商入驻服务平台 完成第三方平台创建后需入驻服务平台,点击查看【入驻服务平台操作指引】; 3.服务商报名激励活动 登录服务平台-服务商激励,按照页面提示完成服务商及小程序报名流程,报名审核通过方可参与本激励活动,点击查看【报名激励活动操作指引】。 具体的活动方案细则请登录服务平台查看。 微信团队 2020年08月24日
2020-08-24 - 小程序批量删除云数据库里的数据
我们用云开发的云数据库存数据,难免会遇到数据过多,或者一些过时数据要删除的需求。之前云开发删除数据库只能一条条的删除。要想批量删除很麻烦,近期云开发推出了批量删除数据的方法。甚至可以稍微改造下实现数据库里某个集合(表)里所有数据的删除操作。 老规矩,先看效果图 如删除工资表中2019年9月份的工资 [图片] 可以看到我们成功删除7条数据。 删除所有的工资数据 [图片] 可以看到我们把工资表里768条数据,全部删除了。 接下来我们就来看下具体的实现代码 一,先看官方文档如何写的 [图片] 通过上图可以看到,我们既可以删除单条,又可以删除多条。 [图片] 通过上图可以看到,我们只能结合where语句才能实现批量删除。 再来看下官方给的demo [图片] 一看我们就能知道这是写在云函数里的。所以我们批量删除数据库里的数据,必须是通过云函数来实现批量。 官方文档的地址:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-server-api/database/collection.remove.html 二,我们就结合具体业务来实现批量删除 1, 首先看下我们的工资表里,有yuefen这个字段 [图片] 比如我们2019年11月所有的人工资填写有误,我们想批量的删除所有 yuefen为 2019.11的数据,对应的代码如下图红色框里的代码。 [图片] 2,作为一个业务代码,我们肯定要把数据做活 所以定义一个输入框,用来输入你要删除的月份。如下图所示 [图片] 3,删除所有数据 同样的我们想删除所有数据,也比较依赖where。那门我们删除所有数据,该怎么匹配where语句呢。翻看官方文档,可以看到官方文档有判断某一个字段是否有值。所以我们编写的删除所有数据的代码如下。 [图片] 这样我们就可以通过判断月份存在,就可以删除所有数据了,因为所有的数据都有月份的。 这样我们就可以实现小程序数据库里数据的批量删除操作了,官方其实也有为我们提供批量更新的操作,感兴趣的同学去官方文档看下就可以了。 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-server-api/database/collection.update.html [图片] 完整的云函数源码直接给大家贴出来吧。 [代码]const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async(event, context) => { let { type, yuefen } = event try { if (type == 'all') { const _ = db.command return await db.collection('gongzi').where({ yuefen: _.exists(true) //只要月份字段存在,就删除 }).remove() } else { return await db.collection('gongzi').where({ yuefen: yuefen }).remove() } } catch (e) { console.error(e) } } [代码] 后面我会写更多关于小程序,云开发,云数据库的文章,请持续关注。
2019-11-20 - 好友助力分享功能浅析
本文背景首先本文总结基于我的刷题小程序,在该小程序里面存在好友助力得积分的功能,最近几天我在梳理这块逻辑,趁机总结下 参考文档在小程序内部好友助力始终是一个敏感的功能点,稍有不慎,就有可能导致小程序审核被拒,更有甚者小程序被封,本人之前运营的一个抽奖类小程序就是因为分享被永久禁封了 滥用分享违规案例集锦官方 https://developers.weixin.qq.com/community/develop/doc/000086404ac470274a1acc2a851809 好友助力被判定为诱导分享,请问现在助力不行了吗 https://developers.weixin.qq.com/community/develop/doc/0004206e0c8a58d255194fd6e56800 分享功能调整背后的故事 https://developers.weixin.qq.com/community/develop/doc/0006823675c0e82a8307c6db25bc09 本文内容本文 计划从开发的角度来谈谈好友助力实现的细节 在目前的小程序中有以下两种好友助力的方式 (1)直接分享好友 (2)生成小程序码,进而实现分享好友 具体截图如下所示 (1) [图片] (2) [图片] 在这上面两个场景下,第一种方式直接分享好友,好友完成助力后可以增加20积分,在第二种方式好友扫码进入后,可以增加30个积分 [图片] 上面描述了具体的场景,那么具体实现是怎么样的呢? 具体在实现的时候,是这样的 (1)直接分享会在分享的链接里面带openid (2)生成海报分享,会在分享里面带参数scene 如果用户打开小程序或者扫码识别海报,在option里面会带对应的参数,如果存在openid会走分享的流程,增加20积分,如果存在scene会在海报的分享流程,增加30积分 在具体判断当前助力是否有效,有个逻辑判断当前助力用户是否为新用户 至于新用户判断逻辑是通过判断某个集合是否存在该openid 本文总结本文通过总结刷题小程序内部两种分享得积分的场景,具体描述了好友助力的实现方式,以及在开发过程中根据目前的分享运营规范要注意的事项,希望对大家有所帮助
2020-08-26 - 云数据库聚合查询,利用$.setUnion或$.setDifference对数组型字段的元素去重
一、查遍文档也没找到去重的办法,最后发现这两个函数可以曲线救国 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/command/aggregate/AggregateCommand.setUnion.html https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/command/aggregate/AggregateCommand.setDifference.html 假设记录中存在一个数组型字段suppliers,里面存在重复的元素,我们在输出时希望去重,给前台一份干净的数据。 suppliers: ['a', 'a', 'b', 'c'] 二、先试试setUnion,函数的作用是输出两个数组的并集,那么加入一个空数组,相当于去重,示例如下: await db.collection('orders') .aggregate() .addFields({ suppliersDistinct: $.setUnion(['$suppliers', []]) }).end() 输出结果为: suppliersDistinct: ['a', 'b', 'c'] 三、再试试setDifference,函数作用是输出仅存在第一个数组中的值,可以用于先排除一个指定值,假如想排除c,示例如下 await db.collection('orders') .aggregate() .addFields({ suppliersDistinct: $.setDifference(['$suppliers', ['c']]) }).end() 输出结果为: suppliersDistinct: ['a', 'b']
2020-08-03 - 云数据库数组字段查询相关聚合函数in和indexOfArray的使用心得
一、$.in: db.command.aggregate.in([, ]) https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/command/aggregate/AggregateCommand.in.html 给定一个值和一个数组,如果值在数组中则返回 true,否则返回 false。 示例: $.in(['x', '$field']) //如果field字段中包含x则输出true 相当于: where({ filed: 'x' }) 特别注意: 由于数据库没有做兼容处理,如果有些记录中没有field字段,就会直接报错,告诉你第二个参数不是数组。为了避免这个问题,需要增加一个field非空的查询条件。 二、$.indexOfArray: db.command.aggregate.in([, ]) https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/command/aggregate/AggregateCommand.indexOfArray.html 在数组中找出等于给定值的第一个元素的下标,如果找不到则返回 -1。 示例: $.indexOfArray(['x', '$field']) //输出x在field数组中的位置,如果没找到则输出-1 特别注意: 由于数据库做了兼容处理,如果有些记录中没有field字段,则输出null,所以,要甄别到底输出的是自然数index,-1,还是null。同样的,增加一个field非空的查询条件,就排除了输出为null的情况,让输出结果更简单一些。 下面给一个实例: 该实例中,要统计所有frozen字段中包含x的记录条数。通过$.ifNull对frozen字段不存在的情况做好处理,然后用in还是用indexOfArray来实现都可以。(第一个条件arrayElemAt放在这,是为了说明整个查询比较复杂,只能通过aggregate函数,而不能通过filed: value简单匹配来完成查询。) let supplierId = 'x' const allProductsFrozen = await db.collection('products').where(_.expr($.and([ $.eq([ $.arrayElemAt(['$field', -1]), 'y' ]), $.ifNull(['$frozen', false]), $.neq([$.indexOfArray(['$frozen', supplierId]), -1]) ]))).count()
2020-08-19 - 小程序使用Grid和css变量实现瀑布流布局
前言 要实现如下瀑布流效果,动态图片,动态高度 [图片] 我知道使用JS能够实现完美瀑布流,但小程序不比web,坑点会比较多,因此我决定先使用CSS看能不能解决,最后实在不行在使用JS来实现 根据网上的教程尝试使用css的方式(column和flex)实现效果,但排列顺序都是竖排而不是横排,不符合产品需求,实现效果如下 [图片] GRID瀑布流 如此看来只剩grid这一条路了,还好成功了 基础版 下列摘自网上使用GRID实现瀑布流的实例 模板 [代码]
1/view> 2/view> 3/view> 4/view> 5/view> 6/view> ... /view> [代码] wxss [代码].waterfall { display: grid; grid-template-columns: repeat(2, 1fr); // 指定两列,自动宽度 grid-gap: 0.25em; // 横向,纵向间隔 grid-auto-flow: row dense; // 是否自动补齐空白 grid-auto-rows: 20px; // base高度,grid-row基于此运算 } .waterfall .item { width: 100%; background: #222; color: #ddd; } .waterfall .item:nth-of-type(3n+1) { grid-row: auto / span 5; } .waterfall .item:nth-of-type(3n+2) { grid-row: auto / span 6; } .waterfall .item:nth-of-type(3n+3) { grid-row: auto / span 8; } [代码] 效果 [图片] 基础版的问题 看看上面的css是如何使用grid实现 [代码].waterfall .item:nth-of-type(3n+1) { grid-row: auto / span 5; } [代码] 上述代码指定[代码]1,4,7,10...[代码]等item的高度,[代码]auto[代码]为grid自动设置该item的起始位置,[代码]span 5[代码]则指定该item的高度为[代码]grid-auto-rows * 5[代码], [代码]grid-auto-rows[代码]在CSS的设定中为20px,在源码中我做了说明,它是一个基础高度 [代码].waterfall .item:nth-of-type(3n+2) { grid-row: auto / span 6; } [代码] 上述代码指定[代码]2,8,11,14...[代码]等item的高度,[代码]auto[代码]为grid自动设置该item的起始位置,[代码]span 6[代码]则指定该item的高度为[代码]grid-auto-rows * 6[代码] [代码].waterfall .item:nth-of-type(3n+3) { grid-row: auto / span 8; } [代码] 上述代码指定[代码]3,6,12,15...[代码]等item的高度,[代码]auto[代码]为grid自动设置该item的起始位置,[代码]span 8[代码]则指定该item的高度为[代码]grid-auto-rows * 8[代码] 基础版虽然看上去基本符合我们的产品需求,但由css可以知道,它的问题是__高度固定__,但业务上我们并不确切知道每个item的高度及所包含的图片的高度。所以接下来我们要解决__动态高度__设定的问题,让每一个item都自动计算自己的高度,而不是通过CSS来手动指定 css变量登场 微信小程序swiper的自适应高度 小程序中使用css var变量,使js可以动态设置css样式属性 上面两篇文章是之前写得关于css变量的一些巧妙的用法,css变量确实能够解决很多之前很棘手的问题,此时我脑海里面迸发出了一个绝佳的IDEA 仔细观察这段css [代码].waterfall .item:nth-of-type(3n+2) { grid-row: auto / span 6; } [代码] 唯一不确定的就是[代码]6[代码],对,它应该是一个变量而不是一个恒量,它应该是一个与高度关联的比值,而我们可以通过css变量动态设置这个比值,它大概应该长这样 [代码]page{ --item-span: x // 需要使用setData设置x值 } .waterfall .item { grid-row: var(--item-span); } [代码] 考虑到需要设置每个item的高度,应该为每一个item设定独立的样式 [代码].waterfall{ --item-span-1: x; // 使用setData设置x值 --item-span-2: y // 使用setData设置y值 } .waterfall .item-1 { grid-row: var(--item-span-1); } .waterfall .item-2 { grid-row: var(--item-span-2); } [代码] 原理 到此我们就可以讲通如何实现的原理了 注意:下面的例子使用内联样式代替上面的样式设定,因为内联样式可以由JS动态输出 模板 [代码] .../view> /view> .../view> /view> /page> [代码] Page [代码]Page({ data: { waterStyle: '', items: [...] }, onReady(){ let query = wx.createSelectorQuery().in(this) query.selectAll('.waterfall .item').boundingClientRect(ret=>{ let styleStr = ''; ret.forEach((ele, ii) => { let height = ele.height let span = parseInt(height/ 20) // 20 = grid-auto-row styleStr += `--item-span-${ii}: auto / span ${span};` }) this.setData({ waterStyle: styleStr }) }) } }) // 注释一 // 所有item的css变量合集 waterStyle /* xn在onReady方法中计算得出 --item-span-1: auto/span x1; --item-span-2: auto/span x2; --item-span-3: auto/span x3; ... */ [代码] 使用内联样式而不是.item-n [代码][代码]item元素使用内联样式,因为我们不确定item的数量。 高度计算 通过query我们可以获取所有item子元素的rect属性(长宽高…),计算height属性与grid-auto-row的比值,即我们需要的设定值 waterStyle 参考上述代码的注释一(动态计算每一个item的span比值),通过setData方法设置生效(设置在父级[代码]view.waterfall[代码]上),如此grid会自动设置每一个item子元素的位置 优化 以上基本将如何使用grid实现瀑布流的原理阐述了一遍,实际代码中需要注意[代码]grid-auto-row[代码]值的设定,在我们的项目中省略了此项css属性的设置,即span比值实际是由[代码]height/grid-gap[代码]得出,反而效果更好,具体原因我也一脸懵逼,如果有知道的留言告诉我 [图片] 2020-08-15 - 使用聚合函数实现打卡排行榜
效果展示 先上图,有图有真相。 [图片] 需求实现分析 需求:根据打卡天数进行排序,实现累积排行榜,查询前100名。 这里涉及到两张表:用户表,打卡记录表 实现思路 用户表和打卡记录表通过openid进行联合查询 统计每个用户到打卡次数 根据次数进行排序 查询前100名 代码实现 根据以上思路实现代码如下: exports.main = async (event, context) => { // 获取操作符 const $ = db.command.aggregate // 用户表和打卡记录表联合查询 let res = await db.collection(‘用户表’).aggregate() .lookup({ from: ‘打卡记录表’, localField: ‘_openid’, foreignField: ‘_openid’, as: ‘list’, }) // 统计每个人打卡次数 .project({ userInfo:1, _openid:1, size: $.size(’$list’) }) // 进行排序 .sort({ size: -1, }) // 限制100名 .limit(100) .end() return res } 总结 这里面用到了4个关键函数:lookup、project、sort、limit。 lookup:官方文档传送门 project:官方文档传送门 sort:官方文档传送门 limit:官方文档传送门
2020-08-13 - 云开发聚合函数group,如何对按照日期字段进行group分组?
如下图,左边是静态页面显示的数据格式,右边是某个相册数据表。请问如何根据createTime字段分组。 [图片] 期望查询出来的数据格式: [图片] 别人实现参考:https://developers.weixin.qq.com/community/develop/doc/000c42f02783600cff69816075bc00?highLine=%25E4%25BA%2591%25E5%25BC%2580%25E5%258F%2591%2520%25E6%2597%25A5%25E6%259C%259F%25E5%2588%2586%25E7%25BB%2584
2020-05-08 - 【笔记】小程序云开发数据库查询可以先where再group吗
一如既往开发小程序, 场景 云开发数据库,在聚合的时候,先where,再group 问题: 那么云开放官方文档支持这种情况吗? 官方文档链接: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/aggregate/Aggregate.match.html 官方示例代码 const _ = db.command const $ = _.aggregate db.collection('articles') .aggregate() .match({ score: _.gt(80) }) .group({ _id: null, count: $.sum(1) }) .end() 本文场景,统计historys集合里面,在保证status=1下,根据subjectid来统计记录条数 [图片] 具体代码 getGroupData: function(openid){ let that = this; const db = wx.cloud.database(); const $ = db.command.aggregate; // 使用操作符 const _ = db.command; db.collection('historys').aggregate() .match({ _openid: openid, status: _.eq(1) }) .group({ _id: { subjectid: '$subjectid' }, nums: $.sum(1) }) .end() .then((res)=>{ console.log('20200223'); console.log(res); let groupDatas = res.list; }) } 输出结果 [图片] 注意事项: 注意这个api是小程序基础库2.8.3,而云开发默认的基础库是2.8.1,当然随着时间的推移这些都是变化的,只是开发的时候如果代码不work,核对下基础库版本是否匹配得上。
2020-02-24 - 【代码片段】云开发数据库查询,先group,再sort
今天在开发在线答题小程序的热度排名模块,需要统计每个部门的用户数,以及按照部门人数倒序 排列,马上就先到了group by xx sort by num desc 但是云开发数据库支持吗 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/aggregate/Aggregate.group.html 1 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/aggregate/Aggregate.sort.html#%E7%A4%BA%E4%BE%8B 2 经亲测支持,代码如下所示 0 [图片] 1 db.collection('profiles') .aggregate() .group({ _id: '$dept', count: $.sum(1) }) .sort({ count: -1, }) .end() .then(res => { // output: res.result === 3 }).catch(err => { // handle error }) 2 具体的界面截图如下所示 [图片] 3 4 5
2020-04-11 - 云开发数据库分组查询返回数据对象列表
直接上代码 db.collection('t_task') .aggregate() .match({enabled: 1}) .bucket({ groupBy: '$type', boundaries: [0, 1, 2, 3, 4], output: { tasks: $.push({ _id:'$_id', type: "$type", name: "$name", remark: "$remark", date: '$date', time: '$time', isWarn: '$isWarn', deadline: '$deadline', }) } }).end({ success: res => { console.log('getTask res: ', res) }, fail: err => { console.error('getTask err: ', err) } 代码很简单,需要注意的点在于,返回结果需要将返回的字段push到json对象里即可。参数说明可自行查询文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/aggregate/Aggregate.bucket.html
2020-06-26 - 云数据库聚合(aggregate)时如何操作数组型字段的查询与分组?
[代码]{[代码][代码] [代码][代码]"_id"[代码][代码]:[代码][代码]"f885cb355d9ad18d0cc3aed45dc42e87"[代码][代码],[代码][代码] [代码][代码]"status"[代码][代码]:0,[代码][代码] [代码][代码]"items"[代码][代码]:[代码][代码] [代码][代码][[代码][代码] [代码][代码]{[代码][代码]"pId"[代码][代码]:[代码][代码]"075734515d99df300c4f12df68415e50"[代码][代码],[代码][代码]"pPrice"[代码][代码]:110.0,[代码][代码]"pCost"[代码][代码]:85.0,[代码][代码]"quantity"[代码][代码]:1.0},[代码][代码] [代码][代码]{[代码][代码]"pId"[代码][代码]:[代码][代码]"392890432d99df300c4f12df68415f99"[代码][代码],[代码][代码]"pPrice"[代码][代码]:110.0,[代码][代码]"pCost"[代码][代码]:85.0,[代码][代码]"quantity"[代码][代码]:2.0}[代码][代码] [代码][代码]],[代码][代码] [代码][代码]"_openid"[代码][代码]:[代码][代码]"oVCJa5DGovfnzgKr0u2Gn5viMHug"[代码][代码]}[代码] 类似于这种典型的数据结构,items为一个对象数组型字段,相当于关系型数据库的子表数据。我业务上需要对items做一些匹配和分组查询。如下语句: [代码]const ordersRes = await db.collection([代码][代码]'orders'[代码][代码])[代码][代码] [代码][代码].aggregate()[代码][代码] [代码][代码].addFields({[代码][代码] [代码][代码]matched: $.[代码][代码]in[代码][代码]([[代码][代码]'$items.pId'[代码][代码],pIds])[代码][代码] [代码][代码]})[代码][代码] [代码][代码].match({[代码][代码] [代码][代码]sellerId: sellerId,[代码][代码] [代码][代码]status: $.neq(-1),[代码][代码] [代码][代码]matched: [代码][代码]true[代码][代码] [代码][代码]})[代码][代码] [代码][代码].group({[代码][代码] [代码][代码]_id: [代码][代码]'$items.pId'[代码][代码],[代码][代码] [代码][代码]pQuantity: $.sum([代码][代码]'$items.quantity'[代码][代码])[代码][代码] [代码][代码]})[代码][代码] [代码][代码].end()[代码] 遇到两个问题: 问题1:查询时,想针对pId进行筛选,我事先准好了一个有效的pIds数组,match有效的pId,但输出为0。而我用类似的语法,使用where可以工作,能把匹配上的记录输出。 [代码].where({[代码][代码] [代码][代码]sellerId: sellerId,[代码][代码] [代码][代码]status: _.neq(-1),[代码][代码] [代码][代码]'items.pId'[代码][代码]: _.[代码][代码]in[代码][代码](pIds)[代码][代码] [代码][代码]})[代码] 问题2,想针对pId进行分组,合计对应pId的quantity,但是并不能有效分组。实际输出的group条件是一个pId数组,也就是把items下面的多个pId抽取出来组成数组作为group条件了,并不是我所期望的单个pId进行分组,输出结果如下所示: [代码]list: Array(10)[代码][代码]0: {_id: Array(2), pQuantity: 0}[代码][代码]1: {_id: Array(2), pQuantity: 0}[代码][代码]2: {_id: Array(3), pQuantity: 0}[代码][代码]3: {_id: Array(3), pQuantity: 0}[代码][代码]4: {_id: Array(2), pQuantity: 0}[代码][代码]5: {_id: Array(2), pQuantity: 0}[代码][代码]6: {_id: Array(1), pQuantity: 0}[代码][代码]7: {_id: Array(1), pQuantity: 0}[代码][代码]8: {_id: Array(1), pQuantity: 0}[代码][代码]9: {_id: Array(1), pQuantity: 0}[代码]
2019-10-09 - 删除数组中的某个对象元素,根据对象中的某个属性删除该对象元素
wxml页面代码: <block wx:for="{{list}}" wx:key="index"> <view class="content"> <text>{{item.id}}</text> <text>{{item.name}}</text> <button size="mini" bindtap="delete" data-id="{{item.id}}">删除</button> </view> </block> js模块代码: Page({ data:{ list:[ { id: '1001', name: 'tom1' }, { id: '1002', name: 'tom2' }, { id: '1003', name: 'tom3' }] }, //删除数组中的某个对象元素,根据对象中的某个属性删除该对象元素 delete(e){ let eid = e.currentTarget.dataset.id let myList = this.data.list for (var i = 0; i < this.data.list.length; i++) { if (this.data.list[i].id == eid){ myList.splice(i,1) this.setData({ list: myList }) break } } } })
2020-04-26 - 云开发 数组遍历与lookup如何结合?
[图片] 需求是这样,我有一个订单表,里面有商品id和购买数量,先在希望通过lookup去扩展这个表,将商品名称、图片一起查出来。 如果单独试用lookup,查出来的结果和原有的goods是两个单独的字段,需要本地for循环2次重新组合,比较麻烦。 我猜测map和lookup是否有更好的组合试用方式,或者有其他的方法可以解决我刚才说的问题。谢谢!
2020-06-08 - 云函数从一个集合中条件查询随机?
[代码]table: {[代码][代码] [代码][代码]{[代码][代码] [代码][代码]_id:[代码][代码]123132[代码][代码],[代码][代码] [代码][代码]type:[代码][代码]"type1"[代码][代码] [代码][代码]msg:[代码][代码]""[代码][代码] [代码][代码]},{[代码][代码] [代码][代码]_id:[代码][代码]123154[代码][代码],[代码][代码] [代码][代码]type:[代码][代码]"type1"[代码][代码] [代码][代码]msg:[代码][代码]""[代码][代码] [代码][代码]},{[代码][代码] [代码][代码]_id:[代码][代码]123113[代码][代码],[代码][代码] [代码][代码]type:[代码][代码]"type2"[代码][代码] [代码][代码]msg:[代码][代码]""[代码][代码] [代码][代码]},{[代码][代码] [代码][代码]_id:[代码][代码]123134[代码][代码],[代码][代码] [代码][代码]type:[代码][代码]"type2"[代码][代码] [代码][代码]msg:[代码][代码]""[代码][代码] [代码][代码]}[代码][代码]}[代码]例如数据表结构如上,想要同时(一条查询语句)查询type="type1"的随机一条记录和type="type2"的随机一条记录,分开两次查询已经学会了。如果要一次查询出来应该怎么写呢?
2019-11-03 - 云函数里如何随机获取数据库的一个记录的某一个字段?
在云函数里随机获取数据库的一个记录的某一个字段,但是报错了 代码如下: let word = await db.collection('word') .aggregate() .sample({ size: 1 }) .end() // .then(res => {console.log("随机数结果",res.data[0].eng,res.data[0].chs); }) .then(res => { console.log("随机数结果", res); }) .catch(err => {console.log("随机数错误",err); }) console.log("中文",word.data[0].chs) 返回结果: {"errorCode":1,"errorMessage":"user code exception caught","stackTrace":"Cannot read property 'data' of undefined"} 日志: START RequestId: f2f88334-f188-11e9-aabf-525400697544 Event RequestId: f2f88334-f188-11e9-aabf-525400697544 2019-10-18T09:23:30.696Z f2f88334-f188-11e9-aabf-525400697544 随机数结果 { list: [ { _id: 'XakbZfdsX1oQevCz', chs: '钢笔', eng: 'pen' } ], errMsg: 'collection.aggregate:ok' } TypeError: Cannot read property 'data' of undefined at EventHandler.exports.main [as realHandler] (/var/user/index.js:28:27) at <anonymous> at process._tickCallback (internal/process/next_tick.js:188:7) END RequestId: f2f88334-f188-11e9-aabf-525400697544 Report RequestId: f2f88334-f188-11e9-aabf-525400697544 Duration:38ms Memory:256MB MaxMemoryUsed:30.636719MB
2019-10-18 - 让小程序页面和自定义组件支持 computed 和 watch 数据监听器
习惯于 VUE 或其他一些框架的同学们可能会经常使用它们的 [代码]computed[代码] 和 [代码]watch[代码] 。 小程序框架本身并没有提供这个功能,但我们基于现有的特性,做了一个 npm 模块来提供 [代码]computed[代码] 和 [代码]watch[代码] 功能。 先来个 GitHub 链接:https://github.com/wechat-miniprogram/computed 如何使用? 安装 npm 模块 [代码]npm install --save miniprogram-computed [代码] 示例代码 [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, }, computed: { sum(data) { return data.a + data.b }, }, }) [代码] [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, sum: 2, }, watch: { 'a, b': function(a, b) { this.setData({ sum: a + b }) }, }, }) [代码] 怎么在页面中使用? 其实上面的示例不仅在自定义组件中可以使用,在页面中也是可以的——因为小程序的页面也可用 [代码]Component[代码] 构造器来创建! 如果你已经有一个这样的页面: [代码]Page({ data: { a: 1, b: 1, }, onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }) [代码] 可以先把它改成: [代码]Component({ data: { a: 1, b: 1, }, methods: { onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }, }) [代码] 然后就可以用了: [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, }, computed: { sum(data) { return data.a + data.b }, }, methods: { onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }, }) [代码] 应该使用 [代码]computed[代码] 还是 [代码]watch[代码] ? 看起来 [代码]computed[代码] 和 [代码]watch[代码] 具有类似的功能,应该使用哪个呢? 一个简单的原则: [代码]computed[代码] 只有 [代码]data[代码] 可以访问,不能访问组件的 [代码]methods[代码] (但可以访问组件外的通用函数)。如果满足这个需要,使用 [代码]computed[代码] ,否则使用 [代码]watch[代码] 。 想知道原理? [代码]computed[代码] 和 [代码]watch[代码] 主要基于两个自定义组件特性: 数据监听器 和 自定义组件扩展 。其中,数据监听器 [代码]observers[代码] 可以用来监听数据被 [代码]setData[代码] 操作。 对于 [代码]computed[代码] ,每次执行 [代码]computed[代码] 函数时,记录下有哪些 data 中的字段被依赖。如果下一次 [代码]setData[代码] 后这些字段被改变了,就重新执行这个 [代码]computed[代码] 函数。 对于 [代码]watch[代码] ,它和 [代码]observers[代码] 的区别不大。区别在于,如果一个 data 中的字段被设置但未被改变,普通的 [代码]observers[代码] 会触发,但 [代码]watch[代码] 不会。 如果遇到问题或者有好的建议,可以在 GitHub 提 issue 。
2019-07-24 - 实战丨如何制作一个完整的外卖小程序(已开源)
最近微信小店开放了,赶着微信全面开放之前,把自己的小程序开源出来给大家使用~ 小程序效果 [图片] [图片] [图片] 开发心得 如何在项目中集成云开发 一开始项目并非基于云开发而开发的,目前考虑用云开发,因此,需要在项目中开启云开发的相关选项。 首先,在小程序文件夹中建立 [代码]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 - 小程序海报生成工具,可视化编辑直接生成代码使用,你的海报你自己做主
开门见山 工具地址 点我直达>>painter-custom-poster 由于挂载在github page上,打开速度会慢一些,请耐心等待或自行解决git网速问题 背景 在做小程序时候,我们经常会有一个需求,需要将小程序分享到朋友圈,但是朋友圈是不允许直接分享小程序,那我们还有其他的办法解决吗?答案肯定是有的,即 canvas 生成个性化海报分享图片到朋友圈 分析 小程序中有大量的生成图片需求,但是使用过 canvas 的人,都会发现一些难以预料的问题>>有关小程序的坑 直接在 canvas 上绘制图形,对于普通开发者来说代码会特别凌乱并且难以维护,经常会花费很久的时间去优化代码 不同的环境渲染问题,例如在开发者工具看起来好好的,一到 Android 真机,就出现图片不显示,位置不对应等等问题 解决 那可不可以开发一款生成海报的插件库呢? 首先,只需要提供一份简单的参数配置文件即可 解决掉小程序Canvas遇到的一些大大小小的坑 有严苛的测试环节,解决各种环境和各种机型遇到的问题,并提供稳定的线上版本 长期维护,并有专人更新迭代更新颖的功能 以上的要求当然是可以的,曾经的我也想尝试开发一款出来,但是后来尝试了几款现成的工具之后就放弃了,毕竟轮子这个东西,是需要不断维护更新的,另外已经有这么多优秀现成的插件了,我为何还要费力去写呢,贡献代码岂不更美哉,以下是我收集的几款 小程序生成图片库,轻松通过 json 方式绘制一张可以发到朋友圈的图片>>Painter 小程序组件-小程序海报组件>>wxa-plugin-canvas 微信小程序:一个 json 帮你完成分享朋友圈图片>>mp_canvas_drawer 我想干什么 唠了这么多,好像提供给大家插件就没我什么事情了…想走是不可能的 为了能够制作出更酷炫的海报,我思考了许久 虽然有了插件后,只需要提供配置代码就能够制作出一款海报来,但是我发现还是有些许问题 制作海报效率还是不够高,微调一个元素的大小和位置,就需要不断的修改保存代码,等待片刻,查看效果,真的烦 一个小小的位置调整可能就需要来回调整无数次,这种最简单的机械化劳动,这辈子是不可能的 拿着完美的稿子,递给设计师看,这个位置不对,这个线太粗,这个颜色太重…你信不信我打死你 对于一些精美复杂的海报,实现起来真的不太现实 那我需要怎么做呢,请点击这个链接体验>>painter-custom-poster 点击左侧例子展示中的任意一个例子,然后导入代码就能看到效果图,这下你应该能猜到了我的想法了 如何实现 刚开始我想用简单的html和css加拖动功能实现,通过简单尝试之后就放弃了,因为这个功能真的太复杂了,简单的工具肯定是不行的 中间这个计划停滞了很长时间,一度已经放弃 直到发现了这个库fabric.js,真的太太优秀了,赞美之词无以言表,唯一的缺点就是中文教程太少,必须生啃英文加谷歌翻译 fabric介绍,你可以很容易地创建任何一个简单的形状,复杂的形状,图像;将它们添加到画布中,并以任何你想要的方式进行修改:位置、尺寸、角度、颜色、笔画、不透明度等 How To Use 目前工具一共分成4部分 例子展示 用来将一些用户设计的精美海报显示出来,通过点击对应的例子并将代码导入画布中 画布区 显示真实的海报效果,画布里添加的元素,都可以直接用鼠标进行拖动,旋转,缩放操作 操作区 第一排四个按钮 复制代码 将画布的展示效果转化成小程序海报插件库所需要的json配置代码,目前我使用的是Painter库,默认会转化成这个插件的配置代码,将代码直接复制到card.js即可 查看代码 这个功能用不用无所谓,可以直观的看到生成的代码 导出json 将画布转化成fabric所需要的json代码,方便将自己设计的海报代码保存下来 导入json 将第3步导出的json代码导入,会在画布上显示已设计的海报样式 第二排五个按钮 画布 画布的属性参数 详解见下方 文字 添加文字的属性参数 详解见下方 矩形 添加矩形的属性参数 详解见下方 图片 添加图片的属性参数 详解见下方 二维码 添加二维码的属性参数 详解见下方 第三排 各种元素的详细设置参数 激活区 激活对象是指鼠标点击画布上的元素,该对象会被蓝色的边框覆盖,此时该对象被激活,可以执行拖动 旋转 缩放等操作 激活区只有对象被激活才会出来,用来设置激活对象的各种配置参数,修改value值后,实时更新当前激活对象的对应状态,点击其他区域,此模块将隐藏 快捷键 ‘←’ 左移一像素 ‘→’ 右移一像素 ‘↑’ 上移一像素 ‘↓’ 下移一像素 ‘ctrl + z’ 撤销 ‘ctrl + y’ 恢复 ‘delete’ 删除 ‘[’ 提高元素的层级 ‘]’ 降低元素的层级 布局属性 通用布局属性 属性 说明 默认 rotate 旋转,按照顺时针旋转的度数 0 width、height view 的宽度和高度 top、left 如 css 中为 absolute 布局时的作用 0 background 背景颜色 rgba(0,0,0,0) borderRadius 边框圆角 0 borderWidth 边框宽 0 borderColor 边框颜色 #000000 shadow 阴影 ‘’ shadow 可以同时修饰 image、rect、text 等 。在修饰 text 时则相当于 text-shadow;修饰 image 和 rect 时相当于 box-shadow 使用方法: [代码]shadow: 'h-shadow v-shadow blur color'; h-shadow: 必需。水平阴影的位置。允许负值。 v-shadow: 必需。垂直阴影的位置。允许负值。 blur: 必需。模糊的距离。 color: 必需。阴影的颜色。 举例: shadow:10 10 5 #888888 [代码] 渐变色支持 你可以在画布的 background 属性中使用以下方式实现 css 3 的渐变色,其中 radial-gradient 渐变的圆心为 中点,半径为最长边,目前不支持自己设置。 [代码]linear-gradient(-135deg, blue 0%, rgba(18, 52, 86, 1) 20%, #987 80%) radial-gradient(rgba(0, 0, 0, 0) 5%, #0ff 15%, #f0f 60%) [代码] !!!注意:颜色后面的百分比一定得写。 画布属性 属性 说明 默认 times 控制生成插件代码的宽度大小,比如画布宽100,times为2,生成的值为200 1 文字属性 属性名称 说明 默认值 text 字体内容 别跟我谈感情,谈感情伤钱 maxLines 最大行数 不限,根据 width 来 lineHeight 行高(上下两行文字baseline的距离) 1.3 fontSize 字体大小 30 color 字体颜色 #000000 fontWeight 字体粗细。仅支持 normal, bold normal textDecoration 文本修饰,支持none underline、 overline、 linethrough none textStyle fill: 填充样式,stroke:镂空样式 fill fontFamily 字体 sans-serif textAlign 文字的对齐方式,分为 left, center, right left 备注: fontFamily,工具中的第一个例子支持文字字体,但是导入小程序为什么看不到呢,小程序官网加载网络字体方法>> 加载字体教程>> 文字高度 是maxLines lineHeight2个字段一起计算出来的 图片属性 属性 说明 默认 url 图片路径 mode 图片裁剪、缩放的模式 aspectFill mode参数详解 scaleToFill 缩放图片到固定的宽高 aspectFill 图片裁剪显示对应的宽高 auto 自动填充 宽度全显示 高度自适应居中显示 Tips(一定要看哦~) 本工具不考虑兼容性,如发现不兼容请使用google浏览器 painter现在只支持这几种图形,所以暂不支持圆,线等 如果编辑过程,一个元素被挡住了,无法操作,请选择对象并通过[ ]快捷键提高降低元素的层级 文字暂不支持直接缩放操作,因为文字大小和元素高度不容易计算,可以通过修改激活栏目maxLines lineHeight fontSize值来动态改变元素 如发现导出的代码一个元素被另一个元素挡住了,请手动调整元素的位置,json数组中元素越往后层级显示就越高,由于painter没有提供层级参数,所以目前只能这样做 本工具导出代码全是以px为单位,为什么不支持rpx, 因为painter在rpx单位下,阴影和边框宽会出现大小计算问题,由于原例子没有提供px生成图片方案,可以下载我这里修改过的demo>>Painter即可解决 文本宽度随着字数不同而动态变化,想在文本后面加个图标根据文本区域长度布局, 请参考Painter文档这块教程直接修改源码 由于本工具开发有些许难度,如出现bug,建议或者使用上的问题,请提issue,源码地址>>painter-custom-poster 海报贡献 如果你设计的海报很好看,并且愿意开源贡献,可以贡献你的海报代码和缩略图,例子代码文件在example中,按顺序排列,例如现在库里例子是example2.js,那你添加example3.js和example3.jpg图片,事例可以参考一下文件夹中源码,然后在index.js中导出一下 导出代码 代码不要格式化,会报错,请原模原样复制到json字段里 生成缩略图 刚开始我想在此工具中直接生成图片,但是由于浏览器图片跨域问题导致报错失败 所以请去小程序中生成保存图片,图片质量设置0.2,并去tinypng压缩一下图片 找到painter.js,替换下边这个方法,可以生成0.2质量的图片,代码如下 [代码] saveImgToLocal() { const that = this; setTimeout(() => { wx.canvasToTempFilePath( { canvasId: 'k-canvas', fileType: 'jpg', quality: 0.2, success: function(res) { that.getImageInfo(res.tempFilePath); }, fail: function(error) { console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`); that.triggerEvent('imgErr', { error: error }); } }, this ); }, 300); } [代码] TODO 颜色值选择支持调色板工具 文字padding支持 缩放位置弹跳问题优化 假如需求大的话,支持其他几款插件库代码的生成 ~ 创作不易,如果对你有帮助,请给个星星 star✨✨ 谢谢 ~
2019-09-27 - 2019-08-26
- 开源的抽奖助手小程序
发现开源小程序之美三-抽奖助手小程序 发现开源小程序之美一,个人博客小程序 https://developers.weixin.qq.com/community/develop/article/doc/000a40e13ec550274e2a9addd56413发现开源小程序之美二,微慕WordPress小程序 https://developers.weixin.qq.com/community/develop/article/doc/000c44945dc728ab9c2aff2a55b013发现开源小程序之美三,抽奖助手小程序 https://developers.weixin.qq.com/community/develop/article/doc/0002846854056847b66a2d13451013发现开源小程序之美四,在线答题小程序 https://developers.weixin.qq.com/community/develop/article/doc/00040af07005609a223acee0151413发现开源小程序之美五,营销组件库 https://developers.weixin.qq.com/community/develop/article/doc/000c4235c98740a1dc2a1a6045b013发现开源小程序之美六,酱茄小程序 https://developers.weixin.qq.com/community/develop/article/doc/00040ede6d0388082a3aeb49b57813发现开源小程序之美七,二手书商场 https://developers.weixin.qq.com/community/develop/article/doc/0006ceb61a87182a4b3a1b32a5bc13发现开源小程序之美八,我要戴口罩https://developers.weixin.qq.com/community/develop/article/doc/0006a047b0cee0d5713ad731f5b813发现开源小程序之美九,失物招领小程序 https://developers.weixin.qq.com/community/develop/article/doc/000ca6a3b28ce8857b5a8bb3351c13发现开源小程序之美十,旅游攻略方面的微信小程序 https://developers.weixin.qq.com/community/develop/article/doc/000cc694e9c790ce755aee41556013 这个小程序是身边小伙伴开发的,基于云开发的一个抽奖助手小程序,我今天clone下代码,花了不到10分钟就运行成功了, 值得推荐给大家 先上截图大家参观下 [图片] 1 [图片] 2 [图片] 2 码云地址 https://gitee.com/xiaofeiyang3369/wechatlottery 我在调试过程中做了略微的改动 部署步骤建议按照下面三步走 第一步:创建集合,并将集合权限设置为:所有人可读,仅创建者可读写 第二步:将data里面的lottery.json文件导入到lottery集合 第三步:部署云函数 如没有意外就可以正常运行了,部署过程中遇到任何问题,请评论席留言。 更新记录 2020-07-20 重写了核心逻辑 ①开奖逻辑 ②抽奖逻辑 开奖逻辑目前是按照时间维度,到了时间不管人数有没有凑够都会进行开奖,开奖五分钟后,进行抽奖,确定中奖名额。 具体规则: (1)每个整点的1分去检测,根据当前时间检测是否有需要开奖的 (2)每个整点的5分去检测,是否有开奖未抽奖的,如果有,确定中奖名额 ~~
2021-01-25 - 微信小程序页面之间正向传值和逆向传值的方法
微信小程序页面之间正向传值和逆向传值 正向传值 一 直接使用URL传值 [代码] wx.navigateTo({ url: `/pages/contacts-edit/contacts-edit?name=zhangsan&idx=1`, }) [代码] 但是如果一个对象结构比较复杂, 数据量比较大, 即使转换成JSON也有可能会被莫名其妙的截取. 所以使用URL传值的时候, 需要先编码 我是这样做的 [代码]// A页面触发事件, 跳转到B页面 _onClickCell: function (e) { let contacts = { name: '张三', phone: '13800001111', safePhone: '138****1111', idCard: '230524202113324455', safeIdCard: '230524********4455', typeStr: '成人', gender: '0', genderStr: '保密' } // 先对数据进行JSON let jsonStr = JSON.stringify(contacts) // 对数据进行URI编码, 如果不进行这一步操作, 数据有可能会被截断, 少量数据没有问题, 如果是一个大的对象, 就容易被截断获取不到完整的数据 let data = encodeURIComponent(jsonStr) wx.navigateTo({ url: `/pages/contacts-edit/contacts-edit?contacts=${data}&idx=${idx}`, }) }, // B页面再onLoad方法中接收参数 onLoad: function (options) { let idx = (!!options.idx) ? Number(options.idx) : -1 let contacts = {} if (!!options.contacts) { let jsonStr = decodeURIComponent(options.contacts) contacts = JSON.parse(jsonStr) } this.setData({ contacts, idx }) }, [代码] 二 使用eventChannel来传递 [代码]//A页面准备跳转到B页面 _onClickCell: function (e) { let address = { id: 457, name: '小艾-3', countryCode: '86', phone: '13892292222', reginoCode: '871', city: '市辖区', area: '海淀区', street: '东北旺路8号院中关村软件园8号楼华夏科技大厦', address: '中国北京市市辖区海淀区东北旺路8号院中关村软件园8号楼华夏科技大厦' }, wx.navigateTo({ url: '/pages/address-edit/address-edit', success: res => { // 这里给要打开的页面传递数据. 第一个参数:方法key, 第二个参数:需要传递的数据 res.eventChannel.emit('setAddressEditData', address) } }) } //B页面在onLoad方法中接收参数 onLoad: function (options) { // 接收上个页面传递来的数据 let eventChannel = this.getOpenerEventChannel() // setAddressEditData和上个页面设置的相同即可 eventChannel.on('setAddressEditData', (address) => { this.setData({ address: address || {}, }) }) }, [代码] 逆向传值 一 使用全局对象, 获取全部页面来逆向传值 [代码] _onClickComplete: function () { // 获取当前全部的页面栈 let arr = getCurrentPages() // 获取到要逆向传值的上一个页面 let lastPage = (arr.length >= 2) ? arr[arr.length - 2] : undefined // 判断拿到的上一个页面是不是我们要的页面 if (!!lastPage && lastPage.route == 'pages/contacts-list/contacts-list') { /* 这里我们就拿到了上一个页面的页面对象, 这里其实我们就可以使用lastPage做很多事情了, 例如直接操作lastPage.data, 修改上一个页面的数据 或者调用这个页面内的方法, 我上一个页面预留了一个更新方法, 所以这里就直接用上一个页面调用数据刷新的方法, 我这里给赋值, 就可以携带数据回上一个页面了 */ lastPage.updateContactList(this.data.contacts, this.data.idx) // 返回上一个页面 wx.navigateBack() } }, [代码] 二 使用eventChannel来逆向传值 B->A [代码]// B页面 _onClickComplete: function (e) { let eventChannel = this.getOpenerEventChannel() // updateAddressListData 这个方法需要上一个页面的支持, 上一个页面在navigateTo方法中的events数据中定义这个方法来接收数据 eventChannel.emit('updateAddressListData', this.data.address, this.data.idx) wx.navigateBack() }, // A页面需要的支持 _onClickCell: function (e) { wx.navigateTo({ url: '/pages/address-edit/address-edit', events: { // 这里用来接收后面页面传递回来的数据 updateAddressListData: (address, index) => { // 这里处理数据即可 } } }) } [代码] 代码片段
2020-07-21 - 微信小程序订阅消息订单通知体验,因为是一次性订阅,所以需要用户每次的同意订阅,然后用户拒绝授权之后及处理
[图片] 微信小程序消息推送,模板消息已经不能用了,只能使用订阅消息,一来要基础库高,二来,用户还是一次性订阅,每次都要授权同意,我们要做的就是每次都弹窗出来,并且要做用户拒绝后的处理,我只能做到这样了。被拒绝后的授权回调,打开设置,还是有点处理太强硬。这里应该可以引导用户授权,引导用户勾选总是同意这个订阅消息。希望给一个永久性订阅的,然后被拒绝后,可以更换的引导用户。
2020-03-03 - 「笔记」订阅消息-订阅次数维护
前言 距离1月10日模板消息下架只有2天了,在社区里经常能看到有帖子在问关于怎么记录订阅次数的问题,这里在这里介绍一下自己用的简单方案,仅供参考。 误区一 [图片] 上面这个图大家应该都比较熟悉了,很多人总是误以为勾选“总是保持以上选择,不再询问”,就可以无限发送订阅消息,这个是错误的想法,勾选和不勾选唯一的区别就是每次触发订阅的时候会不会弹授权窗口而已。 误区二 订阅消息不能通过bindsubmit的方式触发,必须通过bindtap的方式触发。 误区三 触发订阅窗口后,不管用户点击了允许还是取消,都会进入订阅消息的success回调中,所以通过这个来判断用户是否订阅是错误的。 订阅次数的维护 先看下官方的文档: [图片] 那么我们该如何使用呢? 我们通过 wx.requestSubscribeMessage 接口发送的时候是知道需要让用户订阅哪几个模板的,就是 tmplIds 这个参数填的数组。那么根据官方文档的回调内容,我们就可以直接在success内去获取对应的key所返回的状态。把获取到的状态分别存入自己的数据库里。发送的时候去数据库里查询需要发送的模板并且状态为accept的去发送,如果发送成功则删除一条记录(因为没有过期一说,所以随便删除哪一条记录都不影响)。 参考代码 [图片] 查询模板订阅状态 需要基础库大等于2.10.0才支持。 wx.getSetting({ withSubscriptions: true, success (res) { console.log(res) } }) 官方文档 补充 如果用户选择了不再接收消息会清空之前的订阅次数,但是这个不会主动告诉开发者,所以发送订阅消息失败后,需要根据返回内容自行清空记录,重新计算。 相关文章 「笔记」订阅消息-订阅次数维护(2020年3月更新改动) 「笔记」订阅消息体验踩坑
2020-03-06 - 抽奖助手小程序二
本文背景在前面我分享过一个抽奖助手小程序,为避免引起误会,我先解释下,首先这里的抽奖助手小程序并非抽奖助手,而是指能提供抽奖服务的小程序,之前那次分享纯粹是从如何搭建运行这个维度展开 本文内容本文主要是在开源小程序做了几个改造,把这个记录下来 (1)开奖模式,之前小程序是通过人数,如果人数达到设定的开奖人数才会开奖,我把这个模式支持了日期,不管是否达到开奖人数都会在某个设定的时间到来自动开奖,具体位于云函数run (2)抽奖逻辑,其实说时候我没有很仔细的看开源小程序里面的抽奖逻辑,我把这块重写了,逻辑位于云函数draw (3)首页增加了插屏广告 (4)抽奖按钮接入了订阅消息以及激励式广告 总体评价这个抽奖助手小程序UI是非常不错的,通过这种简单的改造就可以完成期望的目的。 界面截图 f[图片] f [图片] [图片] √ [图片] f 本文总结本文主要讲述我在开源的抽奖助手小程序上进行了几个改造,使其符合我的用途,通过这种改造使我对抽奖类小程序有了更深的理解,比如具体的抽奖逻辑,开奖逻辑等等
2020-07-20 - 发现开源项目之美一,博客类小程序
从今天开始会找一些优秀的开源小程序项目,每一个开源项目,我都会通过我本人主体小程序进行发布,将个人主体不允许的功能砍掉,审核通过后,才写文章,有一点时间,计划每周出一篇 发现开源小程序之美一,个人博客小程序 https://developers.weixin.qq.com/community/develop/article/doc/000a40e13ec550274e2a9addd56413发现开源小程序之美二,微慕WordPress小程序 https://developers.weixin.qq.com/community/develop/article/doc/000c44945dc728ab9c2aff2a55b013 发现开源小程序之美三,维修上报小程序发现开源小程序之美四,在线答题小程序发现开源小程序之美五,营销组件库 https://developers.weixin.qq.com/community/develop/article/doc/000c4235c98740a1dc2a1a6045b013 第一个开源小程序为个人博客类小程序, 该小程序实现的功能很多,但是我觉得几个核心出彩功能有 1、富文本展示 2、海报生成 3、留言之后自动推送订阅消息 4、留言已经做过内容安全检查 5、版本更新机制,这个功能还是第一次体验到,虽然官方文档有介绍,但是这时第一次体验到这种热更新方式 [图片] 2 [图片] 3 [图片] 4 [图片] 4 特色 该小程序有一点非常棒,就是可以同步公众号文章,通过公众号提供的素材api,可以将公众号的文章定时拉取到小程序云开发数据库 从功能上讲,已经非常完善,从技术角度,涉及到了海报生成、富文本展示、订阅消息、云函数 项目开源地址: https://github.com/CavinCao/mini-blog 具体数据库集合如下所示 [图片] [图片] 这样,为方便大家看数据库的结构,我把这个项目在码云传了一份,数据库文件在data目录里面,这里面仅仅是包含了有数据的集合,没有数据的集合不在,总共有这么11个集合 1、//缓存小程序or公众号的accessToken access_token //小程序文章集合 2、mini_posts //小程序评论内容集合 3、mini_comments //小程序用户操作文章关联(收藏、点赞) 4、mini_posts_related //小程序博客相关配置集合 5、mini_config //小程序博客相关操作日志 6、mini_logs //小程序博客用户FormID(用于模板消息推送)[已经废弃] 7、mini_formids //会员信息表 8、mini_member //签到明细表 9、mini_sign_detail //积分明细表 10、mini_point_detail //订阅消息记录表 11、mini_subcribute 正式一些的话,应该是绑定公众好,通过云函数把公众号的图文数据拉下来,如果不做,可以用我data目录的数据集合,展示的时候也有数据。 上面是数据库,下面我截图下云函数 [图片] 环境变量配置 [图片] 具体通过syncService云函数来同步公众号文章到小程序,直接点击云端测试就可以。 [图片] 部署过程中会遇到各种问题,我应该是花了两个晚上调通了,具体问题我大致回忆下 1、公众号素材拉取的时候要设置白名单 2、有几个云函数要设置环境变量 3、在我的模块有个后台管理,是通过环境变量来配置openid来展示的 4、海报生成要安装npm第三方组件,这个不清楚的,规矩要琢磨半天 5、海报生成模块,必须要等小程序上线之后才可以的 6、留言需要配置订阅消息,订阅消息id记得要变更下 大致记着这么多 为了方便大家部署,我在码云重新创建了下,里面有个data目录,存放着云开发数据库有数据的集合json文件,如果集合中的没有数据,是导不出json文件的,所以建议先手工把集合都创建下,然后,将有数据的集合json导入进去,并改下数据库权限,就能正常跑起来了 如果部署过程中还遇到其他问题,或者遇到问题过不去,都欢迎通过社区私信跟我联系,由于社区运营规范要求,微信联系方式暂不方便在社区发布。 https://gitee.com/xiaofeiyang3369/blog 解决的问题: 2018年之后申请的公众号都不会开放留言功能,通过这个方式,可以将公众号文章通过公众号开放的api,整体拉取下来,到云开发的数据库,然后通过这种方式留言, 注意留言一定要做内容安全检查 从规则来说,除了留言这里有风险之外,技术层面是通的。 2020-04-19更新 新增集合 mini_point_detail 用于积分模块
2020-06-08 - 关于云开发的一次性订阅消息
前段时间看到了这位老哥的一篇关于订阅消息的文章:https://developers.weixin.qq.com/community/develop/article/doc/0008802e8381e0eeabb92c9975b013 这篇文章对于程序员来说非常直观的说明了一次性订阅消息的逻辑:订阅1次,可以收到订阅消息一次,订阅10次,可以收到订阅消息10次。 但是我觉得这个方案对于一个普通用户来说,并不够友好,如果我是一个不懂订阅消息的普通用户,我根本不会花时间去点这样一个点1加1的订阅消息。我觉得对于开发者来说,用户能够点一次允许并且勾上不在询问就已经很不错了,剩下的完全可以交给程序来处理。下面是我的方案。让一次性订阅消息达到长期订阅的效果。 首先明确以下逻辑: 通过 wx.getSetting({ withSubscriptions: true }) 的 success 回调 res 可以得出订阅消息的以下5种状态[图片]当用户勾选了“不在询问”之后,不管你后面怎么调 wx.requestSubscribeMessage ,订阅消息的弹窗都是不会弹起的wx.requestSubscribeMessage 需要用户手动点击触发当得到以上几种状态之后,接下来就可以根据需要做自己想要做的操作 如我的小程序首页是一个版本列表 [图片] 我在列表的头部设计了一个跟小程序同风格的授权卡片,这样不会显得突兀同时告诉用户点击授权并且勾选“不在询问”,并告诉用户这样做的目的是什么。 然后根据上面得到的不同状态来显示不同的提示语: 总开关关闭了: [图片] 勾选了“不在询问”并且选项是取消 [图片] 接下来就是实现订阅消息+1的步骤,上面提到了当用户勾选了“不在询问”之后,不管你后面怎么调 wx.requestSubscribeMessage ,订阅消息的弹窗都是不会弹起的 这时在用户点击你应用中必点的操作时,比如知乎微博的点击列表进入详情,或者我这个小程序点击版本列表进入版本详情时就可以根据以上得到的状态来判断:当授权状态是“选择了不在询问并且选项是允许” 时,直接调用 wx.requestSubscribeMessage ,这时 wx.requestSubscribeMessage 的回调必定是success,而且不会出现授权弹窗,自然也就实现了+1效果。 最后把订阅次数+1记录到数据库,推送时推送订阅次数大于0的就ok了 [图片] 这样一个普通用户需要做的操作就只有点击授权-勾选不在询问-允许 这样一个步骤,同时就实现了无形中增加订阅次数的效果,替代让用户手动去点+1增加订阅消息的操作。 另外不用担心这种操作会使用户感觉像垃圾广告一样一直被推送,因为不管是在服务通知页面,还是在设置页面,用户都是可以很轻松的一键关闭通知。 [图片] 然后说下订阅消息的几个特殊情况: 1.当你的账号在开发者工具上面点过允许或取消的时候,wx.getSetting({ withSubscriptions: true }) 的 success 回调结果是这样的 [图片] 手机上的设置界面是这样的 [图片] 回调的itemSettings属性消失了,界面上有订阅消息的开关,但是订阅消息的选项却没了,正常情况应该是这样的 [图片] 这样的 [图片] 2.当用户点了“不在询问并允许”但是又手动通过服务通知页面,或者设置页面关闭了消息通知,这时就算该用户之前已经订阅过了很多次,都会被系统自动清0,这时你的数据库可能存的该用户还有比如5次订阅消息,但是通过cloud.openapi.subscribeMessage.send推送消息的时候,会进catch,errCode是43101。 3.当用户手速过快连续点击了授权按钮触发wx.requestSubscribeMessage时,会进入fail回调,errMsg是 requestSubscribeMessage:fail last call,这个文档是没写的。 最后可以扫码体验一下: [图片]
2020-07-14 - 将小程序原生异步函数promisify后,在async/await中使用
目前,小程序中支持使用async/await有三种模式: 1、不勾选es6转es5,不勾选增强编译;该模式是纯es7的async/await,需要基础库高版本。 2、勾选es6转es5,勾选增强编译;一般是因为调用了第三方的es5插件,通过增强编译支持async/await。 3、勾选es6转es5,不勾选增强编译;手工引入runtime.js支持async/await。 据最近更新情况,原生的函数已经大部分同时原生支持同步化了,不需要本方案转化了,直接加上await即可;比如wx.chooseImage、wx.showModal。。。具体有哪些,可以自己试。 如果只是wx.request的同步化,可参考: https://developers.weixin.qq.com/community/develop/article/doc/0004cc839407a069f77a416c056813 app.js代码: function promisify(api) { return (opt, ...arg) => { return new Promise((resolve, reject) => { api(Object.assign({}, opt, { success: resolve, fail: reject }), ...arg) }) } } App({ globalData: {}, chooseImage: promisify(wx.chooseImage), request: promisify(wx.request), getUserInfo: promisify(wx.getUserInfo), onLaunch: function () { }, }) 某page的index.js代码: const app = getApp() testAsync: async function(){ let res = await app.chooseImage() console.log(res) res = await app.request({url:'url',method:'POST',data:{x:0,y:1}}) console.log(res) }, [图片]
2020-10-20 - 云开发中云函数聚合阶段怎样检索对象数组中的值一个对象值?
[代码]{ [代码][代码] [代码][代码]id: 1234, [代码][代码] [代码][代码]type: [代码][代码]'a'[代码][代码], [代码][代码] [代码][代码]subs: [[代码][代码] [代码][代码]{ time: 123001, val: [代码][代码]'a'[代码] [代码]},[代码][代码] [代码][代码]{ time: 123002, val: [代码][代码]'b'[代码] [代码]},[代码][代码] [代码][代码]{ time: 123003, val: [代码][代码]'c'[代码] [代码]}[代码][代码] [代码][代码]][代码][代码]}[代码]在云开发中的聚合阶段里,怎样查找对象里的一个数组对象中一个字段是否含有指定的值,比如说 val是否含有a?
2019-10-30 - 小程序改造成async/await模式
补充:以下是原生用法: https://developers.weixin.qq.com/community/develop/article/doc/00028cbc2e04e0ddf549d535351c13 简单两步: 1、把这个文件下载并引用进来: https://github.com/facebook/regenerator/blob/master/packages/regenerator-runtime/runtime.js 2、在使用时声明一下: const regeneratorRuntime = require('./lib/runtime.js') 然后就可以使用async/await了。 补充如下:以上方案已经过期作废,小程序原生支持async/await了,(es6转es5别勾)
2020-04-01 - 实战分享: 小程序云开发玩转订阅消息(二)
[图片]这是实战分享: 小程序云开发玩转订阅消息的第二部分 第一部分链接 《实战分享: 小程序云开发玩转订阅消息(一)》 将订阅消息存入云开发数据库接下来我们创建一个云函数 [代码]subscribe[代码] ,这个云函数的作用是将用户的订阅信息存入云开发数据库的集合 [代码]messages[代码] 中,等待将来需要通知用户时进行调用。 在微信开发者工具的云开发面板中创建数据库集合 [代码]messages[代码] [图片]微信开发者工具新增数据库集合 创建一个 [代码]subscribe[代码] 云函数,在云函数中我们将小程序端发送过来的课程订阅信息,存储在云开发数据库集合中,开发完成后,在微信开发者工具中右键上传并部署云函数。 cloudfunctions/subscribe/index.js [代码]const cloud = require('wx-server-sdk'); cloud.init(); const db = cloud.database(); exports.main = async (event, context) => { try { const {OPENID} = cloud.getWXContext(); // 在云开发数据库中存储用户订阅的课程 const result = await db.collection('messages').add({ data: { touser: OPENID, // 订阅者的openid page: 'index', // 订阅消息卡片点击后会打开小程序的哪个页面 data: event.data, // 订阅消息的数据 templateId: event.templateId, // 订阅消息模板ID done: false, // 消息发送状态设置为 false }, }); return result; } catch (err) { console.log(err); return err; } }; [代码]利用定时触发器来定期发送订阅消息接下来我们需要实现一个定时执行的云函数[代码]send[代码],来检查数据库中是否有需要发送给用户的订阅消息。如果有需要发送的订阅消息,会通过云调用 [代码]cloud.openapi.subscribeMessage.send[代码] 将订阅消息发送给用户。 创建一个名叫 [代码]send[代码] 的云函数,首先要配置云函数,在 [代码]config.json[代码] 的 [代码]permissions[代码] 中新增 [代码]subscribeMessage.send[代码]的云调用权限,然后新增一个 [代码]sendMessagerTimer[代码] 的定时触发器,定时触发器的语法和 [代码]linux[代码] 的 [代码]crontab[代码] 类似,比如,我们配置的 [代码]"0 * * * * * *"[代码] 代表每分钟执行一次云函数。 cloudfunctions/send/config.json [代码]{ "permissions": { "openapi": ["subscribeMessage.send"] }, "triggers": [ { "name": "sendMessagerTimer", "type": "timer", "config": "0 * * * * * *" } ] } [代码]接下来是实现发送订阅消息的云函数,这个云函数会从云开发数据库集合[代码]messages[代码]中查询等待发送的消息列表,检查数据库中是否有需要发送给用户的订阅消息,发送条件可以根据自己的业务实现,比如开课提醒可以根据课程开课日期来检查是否需要发送订阅消息,在我们下面的代码示例里做了简化,筛选条件只检查了状态为未发送。 查询到待发送的消息列表之后,我们会循环消息列表,依次发送每条订阅消息,发送成功后将数据库中消息的状态改为已发送。 cloudfunctions/send/index.js [代码]const cloud = require('wx-server-sdk'); exports.main = async (event, context) => { cloud.init(); const db = cloud.database(); try { // 从云开发数据库中查询等待发送的消息列表 const messages = await db .collection('messages') // 查询条件这里做了简化,只查找了状态为未发送的消息 // 在真正的生产环境,可以根据开课日期等条件筛选应该发送哪些消息 .where({ done: false, }) .get(); // 循环消息列表 const sendPromises = messages.data.map(async message => { try { // 发送订阅消息 await cloud.openapi.subscribeMessage.send({ touser: message.touser, page: message.page, data: message.data, templateId: message.templateId, }); // 发送成功后将消息的状态改为已发送 return db .collection('messages') .doc(message._id) .update({ data: { done: true, }, }); } catch (e) { return e; } }); return Promise.all(sendPromises); } catch (err) { console.log(err); return err; } }; [代码]最终效果 [图片]开课提醒订阅消息截图 源代码https://github.com/binggg/tcb-subscribe-demo[3] 参考资料 [1]注册小程序帐号: https://tencentcloudbase.github.io/2019-09-03-wx-dev-guide-register/ [2]开通云开发服务: https://tencentcloudbase.github.io/2019-09-03-wx-dev-guide-service/ [3]https://github.com/binggg/tcb-subscribe-demo: https://github.com/binggg/tcb-subscribe-demo
2019-10-23 - 小程序特效、看他就够(欢迎大家收藏、点赞)
1、文字跑马灯效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1038 2、触摸水波涟漪效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1350 3、下拉菜单效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1875 4、五星评分效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1876 5、数字累加,动态效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=1694 6、星战字幕效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=1689 7、动画卡片效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=2193 8、列表项左滑删除效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=2189 9、图片的滤镜效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=3949 10、黑客帝国metrix效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4670 11、CSS3动画效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4628 12、仿直播点赞气泡效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=2833 13、文字弹幕效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4713 14、仿UC宣传页面的简单动画效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4266 15、发短信验证码倒计时:http://www.wxapp-union.com/portal.php?mod=view&aid=1671 16、弹出菜单特效:http://www.wxapp-union.com/portal.php?mod=view&aid=1659 17、滚动动画:http://www.wxapp-union.com/portal.php?mod=view&aid=1538 18、实时圆形进度条:http://www.wxapp-union.com/portal.php?mod=view&aid=1456 19、遮罩层:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=3617 20、仿Table效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1038 21、操作按钮悬浮固定在底部:http://www.wxapp-union.com/portal.php?mod=view&aid=1029 22、支付倒计时效果:http://www.wxapp-union.com/portal.php?mod=view&aid=890 23、文字单行背景自适应带角标:http://www.wxapp-union.com/portal.php?mod=view&aid=636 24、侧边栏滑动特效;http://www.wxapp-union.com/forum.php?mod=viewthread&tid=1202 25、顶部导航效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1665 26、弹出和隐藏动画:http://www.wxapp-union.com/portal.php?mod=view&aid=1449 27、切换动画:http://www.wxapp-union.com/portal.php?mod=view&aid=1113
2020-07-14 - 【笔记】云开发通过客服消息实现自动回复进群,同时兼容客服小助手
小程序不具备小程序内扫描二维码的能力,因此如果要实现关注公众号或加用户群功能大家一般都利用微信客服功能的自动回复来实现。此时如果自己去实现微信客服自动回复,客服小助手就不能用了,很令人纠结。经过我的研究,借助云开发找到了一个方案,可以实现当用户想获取微信群走自动回复的接口,真正咨询时,直接到客服小助手进行回复。 效果如下 [图片] 原理解析 云开发在做消息推送配置的时候可以配置消息类型,这个时候如果我们只配置一种类型(小程序卡片),那么就只有卡片才会被云函数接管做自动回复,其他消息类型(图片、文字)则正常走小程序客服的通道。 实现步骤 1.小程序端设置按钮属性open-type="contact",用于用户点击时带上定义的卡片跳到客服消息界面。 申请加入 2.新建云端的函数,设置config.json定义权限,如下config.json { "permissions": { "openapi": [ "customerServiceMessage.send", "customerServiceMessage.uploadTempMedia", "customerServiceMessage.setTyping" ] } } 3.写云函数端代码,如下 if (event.Title == "我要进用户群"||event.Title =="关注公众号"||event) { //设置输入状态 cloud.openapi.customerServiceMessage.setTyping({ touser: OPENID, command: 'Typing' }) //从云储存中拉取图片数据 const qunimg = await cloud.downloadFile({ fileID: "cloud://pm-hsfip.706d-pm-hsfip-1259751853/img/qun.png", }) //上传图片素材到媒体库,并获取到图片id const qunmedia = await cloud.openapi.customerServiceMessage.uploadTempMedia({ type: 'image', media: { contentType: 'image/png', value: qunimg.fileContent } }) //向用户发送群二维码 await cloud.openapi.customerServiceMessage.send({ touser: OPENID, msgtype: 'image', image: { mediaId: qunmedia.mediaId, } }) //取消状态设置 cloud.openapi.customerServiceMessage.setTyping({ touser: OPENID, command: 'CancelTyping' }) } 4.设置消息推送,路径如下 云开发-设置-全局设置-云函数接收消息推送 中添加消息类型为miniprogrampage,绑定云函数为新建的云函数。 [图片] 5.微信公众平台绑定客服[图片] 注意事项 如果按照本教程 客服小助手无法收到消息 或 无法自动回复,可以先将以上消息推送配置删除,然后再微信后台绑定客服后,再重新进行消息推送配置。
2020-07-29 - 基于云开发的商城小程序开发之路系列之后台管理
经过上篇文章的准备工作,现在就可以正式开始写商城了 后台管理 后台管理分以下几大块 : 用户 、 商品管理 、 订单管理、优惠券及设置。由于小程序云函数有最多50个的限制,故而需要把云函数按照木块综合处理(刚开始的时候没注意,写着写着方法数量超了,没办法只好又把所有的方法汇总分模块了) 1,用户管理,用户管理包括 用户登录注册、更新用户信息(getuserInfo提供的信息,可根据情况保存) ,获取用户列表,获取打印员(本小程序接入快递助手),更新打印员,添加虚拟用户,删除虚拟用户,获取虚拟用户 , 创建名字为 user_manage的函数, req_type对应函数可根据需求实现 user_manage // 云函数入口函数 exports.main = async(event, context) => { const wxContext = cloud.getWXContext() const result={} const req_type = event.req_type const data = event.data switch (req_type) { case "login": return await login(wxContext) case "up_data_msg": return await updataUserMsg(wxContext, data) case "get_user": return await getUserList(data) case "get_printer": return await getPrinter() case "up_printer": return await upPrinter(data) case "add_invented_user": for (let key in data) { if (!keyIsInPara(key, data[key])) { result.code = 202 result.msg = "参数错误" return result } } return await addUser(data) case "del_invented_user": for (let key in data) { if (!keyIsInPara(key, data[key])) { result.code = 202 result.msg = "参数错误" return result } } return await delUser(data) case "get_invented_user": for (let key in data) { if (!keyIsInPara(key, data[key])) { result.code = 202 result.msg = "参数错误" return result } } return await getInventedUser(data) case "cleant_invented_user": for (let key in data) { if (!keyIsInPara(key, data[key])) { result.code = 202 result.msg = "参数错误" return result } } return await cleanUser(data) default : result.code = 203 result.msg = "req_type 不存在" return result } } 2,商品管理 商品管理包括 商品的增删改查复制等等 创建名字为 goods_manage的函数, req_type对应函数可根据需求实现 // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() const result = {} const req_type = event.req_type const data = event.data switch (req_type) { case "add_clazz"://添加分类 return await addClazz(data) case "del_clazz"://删除分类 return await delClazz(data) case "edit_clazz"://编辑分类 return await editClazz(data) case "get_clazz"://获取分类列表 return await getClazz(data) case "get_clazz_item"://获取单个分类信息 return await getClazzItem(data) // case "clean_clazz": // return await cleanClazz() case "add_goods"://添加商品 return await addGoods(data) case "del_goods"://删除商品 return await delGoods(data) case "edit_goods"://编辑商品 return await editGoods(data) case "get_goods_web"://获取商品列表(后台管理用) return await getGoodsForWeb(data) case "get_goods_wechat"://获取商品列表(小程序理用) return await getGoodsForWechat() case "get_goods_by_clazz"://根据分类获取商品列表 return await getGoodsBylazz(data) case "get_goods_info"://商品详情 return await getGoodsInfo(data) case "copy_goods"://复制商品 return await copyGoods(data) //添加评论开始 case "add_comment_web"://添加商品评论(后台管理用) return await addCommentWeb(data) case "get_comment_web"://获取商品评论(后台管理用) return await getCommentWeb(data) case "del_comment"://删除评论 return await delComment(data) default: result.code = 203 result.msg = "req_type 不存在" return result } } 3,订单管理(暂未写到后续补上) 4,设置 设置包括优惠券 规则图文 支付方式 发货地址 等等一些操作 创建名为setting_manage的函数,req_type对应函数可根据需求实现 // 云函数入口函数 exports.main = async(event, context) => { const result = {} const req_type = event.req_type const data = event.data switch (req_type) { case "add_banner"://添加首页banner return await addBanner(data) case "del_banner"://删除banner return await delBanner(data) case "edit_banner"://编辑banner return await editBanner(data) case "get_banner"://获取banner列表 return await getBanner() case "get_banner_item"://获取单个banner信息 return await getBannerItem(data) case "clean_banner"://请空banner return await cleanBanner() // 一下为图文规则接口 case "add_rule"://添加图文规则 return await addUseingHelp(data) case "del_rule"://删除图文规则 return await delUseingHelp(data) case "edit_rule"://修改图文规则 return await editUseingHelp(data) case "get_rule"://获取图文规则 return await getUseingHelp() case "get_rule_by_type"://根据type获取图文规则 return await getUseingHelpItemByType(data) case "get_rule_item"://获取单个图文规则 return await getUseingHelpItem(data) case "clean_rule"://清空图文规则 return await cleanUseingHelp() //发货地址(快递助手发货时用) case "get_send_address"://获取发货地址 return await getSendAddress() case "set_send_address"://设置发货地址 return await editSendAddress(data) //支付方式 case "get_pay_way"://获取支付方式(发货方式需小程序集合内置几种发货方式,后台只可控制起是否可用) return await getPayWay() case "edit_pay_way": return await editPayWay(data) //运费模板 case "get_postage":// return await getPostageList() case "add_postage": return await addPostage(data) case "del_postage": return await delPostage(data) //优惠券 case "add_coupon": return await addCoupon(data) case "get_coupon_list": return await getCouponList(data) case "get_coupon": return await getCoupon(data) case "del_coupon": return await delCoupon(data) case "edit_coupon": return await editCoupon(data) case "get_coupon_user": return await getCouponUser(data) default: result.code = 203 result.msg = "req_type 不存在" return result } } 后台管理调小程序云函数详见 http调用云函数API文档
2020-01-11 - 【笔记】云开发聚合实现分页,涉及跨表查询、逻辑计算、判断权限、数据格式化、限制输出
背景: 之前不会用聚合,因此把数据库结构分为了用户表、帖子表、喜欢表。小程序端请求一次列表,要根据帖子列表,循环查询用户表,并且还要做一系列的逻辑运算处理,计算当前帖子的权限、是否喜欢过、喜欢人数、是否有这个帖子管理权限等信息。 这样做有很多弊端: 处理速度慢,资源耗费严重,循环查询肯定慢且耗费资源,一个列表需要21次查询。需要写大量逻辑处理代码,如计算管理权限,喜欢数量、当前用户是否喜欢,格式处理等等。于是使用聚合进行了优化: 跨表查询数据格式化逻辑计算,权限判断、是否喜欢等数据统计,喜欢总人数权限判断,是否为管理员限制输出效果: 之前:上百行代码,多次查询,需要单独判断函数,处理时间在3000ms以上之后:几行代码,一次查询,直接查询时算出结果,处理时间在300ms以内 数据库结构 [图片] 代码实现: const { OPENID } = cloud.getWXContext(context) //构建查询条件 let query = null switch (Number(event.listType)) { case 0: query = db.collection('post').aggregate() .match({ //0我的 '_openid': OPENID }) .sort({ createTime: -1 }) .skip(20 * (event.pageNum - 1)) .limit(20) break; case 1: //1 随机 query = db.collection('post').aggregate() .match({ public: true, // feeling: _.gte(50) }) .sample({ size: 20 }) break; case 2: query = db.collection('post').aggregate() .match({ //2喜欢 likes: _.all([OPENID]) }) .sort({ createTime: -1 }) .skip(20 * (event.pageNum - 1)) .limit(20) break; case 4: query = db.collection('post').aggregate() .match({ //4指定 _id: event.id }) .sort({ createTime: -1 }) .skip(20 * (event.pageNum - 1)) .limit(20) break; } //使用聚合处理后续数据 let listData = await query .lookup({ from: "user", localField: "_openid", foreignField: "_id", as: "postList" })//联表查询用户表 .replaceRoot({ newRoot: $.mergeObjects([$.arrayElemAt(['$postList', 0]), '$$ROOT']) })//将用户表输出到根节点 .addFields({ day: $.dayOfMonth('$createTime'), month: $.month('$createTime'), year: $.year('$createTime'), isLike: $.in([OPENID, '$likes']), //是否喜欢 isLiked: $.in([OPENID, '$liked']), //是否喜欢过 isAdmin: $.eq([OPENID, 'oy0T-4yk7lCRFGDefpFC4Yvx_ppU']),//是否管理员 isAuthor: $.eq(['$_openid', OPENID]),//是否为作者 like: $.size('$likes'), //喜欢该帖子数 face: $.switch({ branches: [ { case: $.gte(['$feeling', 90]), then: 9 }, { case: $.gte(['$feeling', 80]), then: 8 }, { case: $.gte(['$feeling', 70]), then: 7 }, { case: $.gte(['$feeling', 60]), then: 6 }, { case: $.gte(['$feeling', 50]), then: 5 }, { case: $.gte(['$feeling', 40]), then: 4 }, { case: $.gte(['$feeling', 30]), then: 3 }, { case: $.gte(['$feeling', 20]), then: 2 }, { case: $.gte(['$feeling', 10]), then: 1 } ], default: 0 }) //根据心情值判断对应表情 }) .project({ postList: 0, userInfo: 0, liked: 0, likes: 0, city: 0, province: 0, country: 0, language: 0, nlp: 0, saveType: 0, }) //清楚掉不需要的数据 .end() return listData
2020-05-26 - 微信小程序云开发,对象数组条件查询结果没有筛选,全量返回了?
第一种写法: const db = wx.cloud.database() const _ = db.command db.collection('vegetable_list').where({ list:{ kind:'根茎类' } }).get({ success: res => { console.log("res:", res.data) }, fail: err => { console.log("error:", err) } }) 第二种写法: db.collection('vegetable_list').where({ list:_.elemMatch({ kind:'根茎类' }) }).get({ }) 都是全量返回,筛选条件没起作用!求官方同学尽快帮忙解决一下
2020-04-30 - 如何使用云开发数据库pull删除数组某一个下标呢?
[图片] 想要删除coverImg数组下面第一个下标值,但是都不能实现。。 db.collection('goods').doc('${ids.id}').update({ data: { coverImg : _.pull({ ${ids.fileid} //ids.fileid是值 }) } })
2020-04-25 - 云开发,如何根据数组下标,删除对应的对象?紧急求救
[图片] 比如这里,应该如何删除videolist[0].video[0]的对象。目前虽然通过js的splice删除后,再整个数组更新回去。但是感觉太麻烦,push和pushAll是如何使用的,请各位大佬指点
2020-04-16 - 云开发 security.imgSecCheck 调用响应错误
使用云函数调用 security.imgSecCheck ,对图片进行鉴黄请求响应为: [代码]{[代码] [代码]errMsg: [代码][代码]"cloud.callFunction:ok",[代码] [代码]result:{[代码][代码] errCode: 41005[代码][代码] errMsg: [代码][代码]"openapi.security.imgSecCheck:fail media data missing hint: [9ZGoCA02628622]" [代码] [代码] }[代码] [代码]}[代码] 云函数代码如下: [代码]const cloud = require([代码][代码]'wx-server-sdk'[代码][代码])[代码] [代码]cloud.init();[代码] [代码]// 云函数入口函数[代码][代码]exports.main = (event) => {[代码][代码] [代码][代码]console.log(event);[代码][代码] [代码][代码]return[代码] [代码]cloud.openapi.security[代码][代码] [代码][代码].imgSecCheck({[代码][代码] [代码][代码]media: {[代码][代码] [代码][代码]contentType: [代码][代码]'image/png'[代码][代码],[代码][代码] [代码][代码]value: event.img[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码].then(result => {[代码][代码] [代码][代码]return[代码] [代码]result;[代码][代码] [代码][代码]})[代码][代码] [代码][代码].[代码][代码]catch[代码][代码](err => {[代码][代码] [代码][代码]return[代码] [代码]err;[代码][代码] [代码][代码]})[代码][代码]}[代码] 调用代码如下: [代码]uploadImg: [代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]this[代码][代码].selectImg().then(img => {[代码][代码] [代码][代码]console.log(img);[代码][代码] [代码][代码]return[代码] [代码]this[代码][代码].imgSecCheck(img);[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]console.log([代码][代码]"success:"[代码][代码], res);[代码][代码] [代码][代码]}).[代码][代码]catch[代码][代码](err => {[代码][代码] [代码][代码]console.log([代码][代码]"fail"[代码][代码], err);[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码] // 选择图片并转为 buffer [代码] [代码][代码]selectImg: [代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]return[代码] [代码]new[代码] [代码]Promise((resolve, reject) => {[代码][代码] [代码][代码]wx.chooseImage({[代码][代码] [代码][代码]count: 1,[代码][代码] [代码][代码]sizeType: [[代码][代码]'original'[代码][代码], [代码][代码]'compressed'[代码][代码]],[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]let params = {[代码][代码] [代码][代码]filePath: res.tempFilePaths[0][代码][代码] [代码][代码]};[代码][代码] [代码][代码]wx.getFileSystemManager()[代码][代码] [代码][代码].readFile({[代码][代码] [代码][代码]filePath: res.tempFilePaths[0],[代码][代码] [代码][代码]success: res => {[代码][代码] [代码][代码]console.log([代码][代码]"readSuccess:"[代码][代码], res);[代码][代码] [代码][代码]resolve(res.data);[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: err => {[代码][代码] [代码][代码]console.log([代码][代码]"readFail:"[代码][代码], err);[代码][代码] [代码][代码]reject(err);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码] [代码] [代码][代码]},[代码][代码] [代码][代码]})[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码] // 调用云函数[代码] [代码][代码]imgSecCheck: [代码][代码]function[代码][代码](img) {[代码][代码] [代码][代码]return[代码] [代码]wx.cloud.callFunction({[代码][代码] [代码][代码]name: [代码][代码]"imgSecCheck"[代码][代码],[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]img: img[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码]
2019-07-18 - 云服务器调用security.imgSecCheck完成代码分享
云服务器代码: // 云函数入口文件 const cloud = require(‘wx-server-sdk’) cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const {value} = event; try { const res = await cloud.openapi.security.imgSecCheck({ media: { header: {‘Content-Type’: ‘application/octet-stream’}, contentType: ‘image/png’, value:Buffer.from(value) } }) return res; } catch (err) { return err; } } 本地函数: wx.chooseImage({count: 1}).then((res) => { if(!res.tempFilePaths[0]){ return; } console.log(JSON.stringify(res)) if (res.tempFiles[0] && res.tempFiles[0].size > 1024 * 1024) { wx.showToast({ title: ‘图片不能大于1M’, icon: ‘none’ }) return; } wx.getFileSystemManager().readFile({ filePath: res.tempFilePaths[0], success: buffer => { console.log(buffer.data) wx.cloud.callFunction({ name: ‘checkImg’, data: { value: buffer.data } }).then( imgRes => { console.log(JSON.stringify(imgRes)) if(imgRes.result.errorCode == ‘87014’){ wx.showToast({ title:‘图片含有违法违规内容’, icon:‘none’ }) return }else{ //图片正常 } [代码] } ) }, fail: err => { console.log(err) } } ) 我相信做出来的人很多,但是没有分享出来,我今天分享出来就是为了避免更多程序员不要在这种简单的问题上,浪费太多的时间,我就浪费了很多时间,兼职太坑爹了[代码]
2019-07-26 - 基于腾讯云开发在小程序和Web中实现数据可视化
1.数据可视化相关的库D3 是一个JavaScript数据可视化库用于HTML和SVG。它旨在将数据带入生活,强调Web标准,将强大的可视化技术与数据驱动的文档对象模型(DOM)操作方法相结合。 D3是Github上最流行的数据可视化项目,在数据科学界有很好的表现。点击去往官方网址。ECharts提供了常规的折线图、柱状图等;用于地理数据可视化的图;用于关系数据可视化的图表;还有用于 BI 的漏斗图,仪表盘,并且支持图与图之间的混搭。除了已经内置的包含了丰富功能的图表,ECharts 还提供了自定义系列,只需要传入一个renderItem函数,就可以从数据映射到任何你想要的图形。点击去往官方网址。chartjs是一个图表控件集合,使用html5的canvas进行绘制。点击去往官方网址。Highcharts 系列软件包含 Highcharts JS,Highstock JS,Highmaps JS 共三款软件,均为纯 JavaScript 编写的 HTML5 图表库,全部源码开放,个人及非商业用途可以任意使用及源代码编辑。点击去往官方网址。AntV 3.0 已全新升级,主要包含 G2、G6、F2、L7 以及一套完整的图表使用和设计规范。得益于丰富的业务场景和用户需求挑战,AntV 经历多年积累与不断打磨,已支撑阿里集团内外 20000+ 业务系统,通过了日均千万级 UV 产品的严苛考验后方敢与君见。点击去往官方网址。2.使用ECharts实现一周的盈亏可视化图表1.在Web端实现ECharts[图片] 下载ECharts相关的资源文件,官方为我们提供了多种下载方式,选择自己可以操作的下载方式即可,本文主要使用dist文件夹下的echarts.min.js;在Visual Studio Code中新建一个echarts01.html,用来展示可视化图表;在代码中的option是数据可视化的关键数据,在使用过程中可根据具体需求更换option来展示其它饼状图、雷达图、柱状图等。2.结合腾讯云实现以上案例登录腾讯云官方注册账号,登录成功之后,在左上角找到云产品>云开发>云开发CloudBase>新建环境即可使用;如图所示,在左侧找到数据库>添加集合Echarts>在桌面新建txt文件,并把option数据放入文件中,保存为json文件>导入json文件到Echarts集合中[图片] [图片] [图片] 说明:其中点击红色箭头导入json文件,蓝色箭头部分为导入成功的一条数据。 在代码中使用云开发来实现ECharts,引入<script src="https://imgcache.qq.com/qcloud/tcbjs/1.5.1/tcb.js"></script>,代码如下所示;const app = tcb.init({ env: '你的环境ID' // 此处填入你的环境ID }); app.auth({ persistence: 'session' //在窗口关闭时清除身份验证状态 }) .anonymousAuthProvider() .signIn() //AnonymousAuthProvider.signIn() 匿名登录云开发 .then(() => { //登录成功 const db = app.database() db.collection("Echarts").where({ _id: "数据id", }).get().then(res => { const option = res.data[0] // 基于准备好的dom,初始化echarts实例 var myChart = echarts.init(document.getElementById('main')); // 使用刚指定的配置项和数据显示图表。 myChart.setOption(option); }) }).catch(err => { console.log("登录失败", err) //登录失败 }) 4.在腾讯云开发左侧找到静态网站托管,这里是把自己的HTML文件和ECharts文件上传到文件管理中,以便我们在浏览器访问 打开静态网站托管>文件管理>上传文件(找到自己HTML文件的存放位置上传即可)打开静态网站托管>基础配置(找到自己的默认域名)>在浏览器中通过“域名/文件路径”即可访问[图片] 3.在微信小程序中使用ECharts[图片] 下载微信开发者工具,申请小程序appid下载githup上的开源echarts小程序在微信开发者创建一个新项目,创建成功之后把其它不需要的文件删除干净把githup上下载的小程序中的ec-canvas、img文件复制到自己创建的项目中把app.wxss中的代码复制到自己项目中的app.wxss.container { position: absolute; top: 0; bottom: 0; left: 0; right: 0; display: flex; flex-direction: column; align-items: center; justify-content: space-between; box-sizing: border-box; } 把其中一个page页面复制到自己项目中,其中wxml、wxss、json文件都是通用的,使用时把js文件中的option替换掉就可以使用其它页面4.小程序云开发实现ECharts下载微信开发者工具,申请小程序appid下载githup上的开源echarts小程序在微信开发者创建一个新项目,并点击云开发,创建成功之后把其它不需要的文件删除干净,并在app.js中初始化云开发,云开发环境id在小程序云开发控制台中设置找到onLaunch: function () { wx.cloud.init({ env:"云开发环境id", }) } 在云开发控制台>数据库>新建集合Echarts>高级操作>add模板,其中data为option的数据,点击执行即可[图片] [图片] 把githup上下载的小程序中的ec-canvas、img文件复制到自己创建的项目中把app.wxss中的代码复制到自己项目中的app.wxss,代码同上把其中一个page页面复制到自己项目中,其中wxml、wxss、json文件都是通用的,使用时把js文件中的option替换掉就可以使用其它页面,这里的option通过云开发获得import * as echarts from '../../ec-canvas/echarts'; const app = getApp() const db = wx.cloud.database() async function initChart(canvas, width, height, dpr) { const option = (await db.collection("Echarts").where({ _id:"baada3ac5ed4ab250022ec955b2a7fec", }).get()).data[0] const chart = echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr }) canvas.setChart(chart); chart.setOption(option, true); return chart; } Page({ onShareAppMessage: function (res) { return { title: 'ECharts 可以在微信小程序中使用啦!', path: '/pages/index/index', success: function () { }, fail: function () { } } }, data: { ec: { onInit: initChart } }, });
2020-06-01 - 免费ICP备案攻略。不花1分钱拥有一台云服务器并顺利ICP备案。
写在前面: 大家不要将ICP证和ICP备案搞混了。 ICP证指的是【电信增值业务经营许可证】,这个资质需要企业主体至少100万注金,去工信部办理,比较难办理;社交-交友需要ICP证。 而ICP备案,【非经营性互联网信息服务备案核准】仅仅是指企业主体的域名备案,可以简单的按以下步骤免费办理成功,其他社交类目如社区、论坛、笔记等,只需要ICP备案即可。 1、在腾讯云注册一个账号并认证企业主体(不吹不黑,开发小程序当然首选腾讯云,好用)。http://www.qcloud.com/ 如果你是个人主体,就不要往下看了,没必要折腾了。 2、找到腾讯云免费活动页:https://cloud.tencent.com/act/free?from=10107 3、选择一款云服务器,180天免费试用。 云服务器申请成功后,它的使命就完成了,没用了,让它自生自灭吧。 在整个备案过程中,也不需要部署网站(域名都没有备案,哪来的网站?)。 [图片] 云服务器180天到期后,可以自己决定是否续费,每个月也才99元,促销期甚至更低,完全可以接受吧。 备案成功后,该服务器就没什么作用了,让它180天后自然欠费销毁得了。 服务器销毁后会有什么影响?答:没有任何影响。 但是。。。。。 你备案的域名最后还得指向一个网站,因为腾讯云会应工信部的要求定期检查网站是否合规,所以你还是要建一个简单的网站,(备案期间,可以暂时不管网站的事,等将来需要的时候再管理)。 至于有多简单,答,多简单都行。此时你可以在七牛、腾讯云、阿里云租点免费的对象存储空间,做个简单的网站。 4、在进行ICP备案之前,你需要在腾讯云注册你的域名地址,如果你已有域名,但不在腾讯云,建议先将要域名过户到腾讯云的账号上。 5、进入控制台,开始ICP备案,这个流程就不介绍了,因为完全一看就懂。而且现在使用备案小程序后,不需要幕布或现场拍照了,极其方便,大家跟着流程走就一点问题没有,有人脸识别和在线拍一段小视频。另外,大家可以随便作,随便填,填错或者填得不合适也不用怕,会有专门的备案客服打电话告诉你哪哪要改,还会告诉你应该怎么填才更容易通过工信部的审核,客服的态度好得发指。 仅说一点其中的几个小坑: a、人脸识别的时候,白色背景、白色背景、白色背景,笔者在人脸识别的时候,满世界找白墙,结果还被打回来重拍了3次。 b、网站用途一律写:公司官网,好通过工信部审核。 6、腾讯云提交资料到工信部审核。这是一个漫长的让人无语的等待,20-30天。笔者最近两次都是20天才过审;不要幻想会有可能提前完成审核,这是政府部门在审核,提前完成说明某政府人员的工作安排有问题,会犯错误的。 7、备案成功后,会有短信通知你,但是,你需要去工信部网站查询结果,并将结果切屏拷贝下来,因为小程序类目审核需要上传这张图片。http://beian.miit.gov.cn/publish/query/indexFirst.action [图片] 把上面这张图片保存好,小程序类目审核的时候需要上传。收到通知后,如果在这里查不到结果,也别急,据说需要24小时。 8、接下来是小程序上线审核。 因为ICP备案的小程序内容肯定涉及到社交,最后小程序上线时还要提交到工信部审核,还需要7天左右的时间,加上前面ICP备案的时间,加起来怎么也得30-40天。大家估计时间,别影响小程序上线。这7天也是政府部门在审核,不要幻想会提前。 9、计算一下时间: 腾讯云注册账号和认证:1-3天; 域名备案:腾讯云环节:1-3天; 域名备案:工信部环节:20-30天; 小程序添加服务类目:社交类目审核:1-3天; 小程序上线审核:腾讯环节:1-2天; 小程序上线审核:工信部环节:7+天; 总天数:30-40天; 10、节省时间的一些建议: 在开发小程序之前,就开始备案工作,小程序可以同时开发,相互不影响; 在开发完成之前一、两星期之内,先发布一版小程序,别管功能是不是完整,能通过审核就行,这样会有7天的等待类目审核的时间,这个时间里,小程序可以照常开发,不影响进度; 只要是社交类,基本需要有文字和图片安全检查功能,别忘了加上,别到时审核通过不了。 11、结束。 [图片]
2021-01-19 - 小程序中通过CSS实现炫酷的动画效果
1.Animate.css简介Animate.css是一个可在您的Web项目中使用的即用型跨浏览器动画库。非常适合强调,首页,滑块和引导注意的提示。它是一个来自国外的 CSS3 动画库,它预设了抖动(shake)、闪烁(flash)、弹跳(bounce)、翻转(flip)、旋转(rotateIn/rotateOut)、淡入淡出(fadeIn/fadeOut)等多达 60 多种动画效果,几乎包含了所有常见的动画效果。虽然借助Animate.css 能够很方便、快速的制作 CSS3 动画效果,但还是建议看看Animate.css 的代码,也许你能从中学到一些东西。不论是在Web端和小程序内都可以正常使用,详细内容请到官方地址学习。 2.动画效果的实现在使用过程中,可以根据自己的喜好来改造css代码来达到你想要的效果,文中动图显示可能不是特别直观,建议自己写一遍代码,即利于学习,又能够直观的体会到动画效果。 1.发光的盒子 [图片] wxml代码: <view id="box">I am LetCode!</view> wxss代码: @keyframes animated-border { 0% { box-shadow: 0 0 0 0 rgba(255,255,255,0.4); } 100% { box-shadow: 0 0 0 20px rgba(255,255,255,0); } } #box { animation: animated-border 1.5s infinite; height: 100rpx; font-family: Arial; font-size: 18px; font-weight: bold; color: white; border: 2px solid; border-radius: 10px; margin: 100px 15px; line-height: 100rpx; text-align: center; } 2.文字的缩放效果 [图片] wxml代码: <view class="animate_zoomOutDown">关注公众号“Let编程”,有更多分享!</view> wxss代码: @keyframes zoomOutDown { 40% { opacity: 1; transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } to { opacity: 0; transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } .animate_zoomOutDown { animation:2s linear 0s infinite alternate zoomOutDown; font-family: Arial; font-size: 18px; font-weight: bold; color: white; margin-top: 70px; text-align: center; margin-top: 15px; } 3.加载动画 [图片] wxml代码: <view class="load-container load"> <view class="loader"> </view> </view> <view class="txt">关注公众号“Let编程”,有更多分享!</view> wxss代码: .load-container { width: 240px; height: 240px; margin: 0 auto; position: relative; overflow: hidden; box-sizing: border-box; } .load .loader { color: #ffffff; font-size: 90px; text-indent: -9999em; overflow: hidden; width: 1em; height: 1em; border-radius: 50%; margin: 72px auto; position: relative; transform: translateZ(0); animation: load 1.7s infinite ease, round 1.7s infinite ease; } @keyframes load { 0% { box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;} 5%, 95% { box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;} 10%, 59% { box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;} 20% { box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;} 38% { box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;} 100% { box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;} } @keyframes round{ 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } 4.抖动的文字 [图片] wxml代码: <view class="shake-slow txt">关注公众号“Let编程”,有更多分享!</view> wxss代码: @keyframes shake-slow { 2% { transform: translate(6px, -2px) rotate(3.5deg); } 4% { transform: translate(5px, 8px) rotate(-0.5deg); } 6% { transform: translate(6px, -3px) rotate(-2.5deg); } 8% { transform: translate(4px, -2px) rotate(1.5deg); } 10% { transform: translate(-6px, 8px) rotate(-1.5deg); } 12% { transform: translate(-5px, 5px) rotate(1.5deg); } 14% { transform: translate(4px, 10px) rotate(3.5deg); } 16% { transform: translate(0px, 4px) rotate(1.5deg); } 18% { transform: translate(-1px, -6px) rotate(-0.5deg); } 20% { transform: translate(6px, -9px) rotate(2.5deg); } 22% { transform: translate(1px, -5px) rotate(-1.5deg); } 24% { transform: translate(-9px, 6px) rotate(-0.5deg); } 26% { transform: translate(8px, -2px) rotate(-1.5deg); } 28% { transform: translate(2px, -3px) rotate(-2.5deg); } 30% { transform: translate(9px, -7px) rotate(-0.5deg); } 32% { transform: translate(8px, -6px) rotate(-2.5deg); } 34% { transform: translate(-5px, 1px) rotate(3.5deg); } 36% { transform: translate(0px, -5px) rotate(2.5deg); } 38% { transform: translate(2px, 7px) rotate(-1.5deg); } 40% { transform: translate(6px, 3px) rotate(-1.5deg); } 42% { transform: translate(1px, -5px) rotate(-1.5deg); } 44% { transform: translate(10px, -4px) rotate(-0.5deg); } 46% { transform: translate(-2px, 2px) rotate(3.5deg); } 48% { transform: translate(3px, 4px) rotate(-0.5deg); } 50% { transform: translate(8px, 1px) rotate(-1.5deg); } 52% { transform: translate(7px, 4px) rotate(-1.5deg); } 54% { transform: translate(10px, 8px) rotate(-1.5deg); } 56% { transform: translate(-3px, 0px) rotate(-0.5deg); } 58% { transform: translate(0px, -1px) rotate(1.5deg); } 60% { transform: translate(6px, 9px) rotate(-1.5deg); } 62% { transform: translate(-9px, 8px) rotate(0.5deg); } 64% { transform: translate(-6px, 10px) rotate(0.5deg); } 66% { transform: translate(7px, 0px) rotate(0.5deg); } 68% { transform: translate(3px, 8px) rotate(-0.5deg); } 70% { transform: translate(-2px, -9px) rotate(1.5deg); } 72% { transform: translate(-6px, 2px) rotate(1.5deg); } 74% { transform: translate(-2px, 10px) rotate(-1.5deg); } 76% { transform: translate(2px, 8px) rotate(2.5deg); } 78% { transform: translate(6px, -2px) rotate(-0.5deg); } 80% { transform: translate(6px, 8px) rotate(0.5deg); } 82% { transform: translate(10px, 9px) rotate(3.5deg); } 84% { transform: translate(-3px, -1px) rotate(3.5deg); } 86% { transform: translate(1px, 8px) rotate(-2.5deg); } 88% { transform: translate(-5px, -9px) rotate(2.5deg); } 90% { transform: translate(2px, 8px) rotate(0.5deg); } 92% { transform: translate(0px, -1px) rotate(1.5deg); } 94% { transform: translate(-8px, -1px) rotate(0.5deg); } 96% { transform: translate(-3px, 8px) rotate(-1.5deg); } 98% { transform: translate(4px, 8px) rotate(0.5deg); } 0%, 100% { transform: translate(0, 0) rotate(0); } } .shake-slow{ animation:shake-slow 5s infinite ease-in-out; } 在实际开发过程中,远不止这些炫酷的动画效果,在互联网迅速的发展状态下,还需要更多的程序员来实现功能需求,因此本文只做简单的介绍,未完待续.....
2020-06-04 - 【笔记】解决用户头像过期无法显示问题
小根据官方规则,用户如果修改了头像,那么一段时间之后,用户原始的头像链接会失效。而因为我们一般用户资料储存的时候只储存了链接,就会造成失效,因此需要把用户头像转换成base64直接存数据库中,这样就不怕失效了。 云开发代码 /** * 插入用户数据 */ function addUserData(openid, userInfo) { if (!userInfo) { console.log('无用户信息,更新失败') } // 将头像图片转换为base64 http.get(userInfo.avatarUrl.replace("https", "http"), function (res) { let chunks = []; //用于保存不断加载的缓冲数据 let size = 0; //保存缓冲数据的总长度 res.on('data', function (chunk) { chunks.push(chunk); //把接受到的数据逐段保存在缓冲区(Buffer size += chunk.length;//累加缓冲数据的长度 }); res.on('end', function () { var data = Buffer.concat(chunks, size);//Buffer.concat将chunks数组中的缓冲数据拼接起来 if (Buffer.isBuffer(data)) { //如果为Buffer转换为base64并赋值给avatarImg var base64Img = 'data:image/png;base64,' + data.toString('base64'); userInfo.avatarImg = base64Img } db.collection('user').doc(openid).set({ data: userInfo }).then(e => { console.log('用户数据更新成功', e) }) }); }); } 小程序端直接渲染 <!-- 直接渲染到页面 page.wxml --> <view style="background-image:url({{detail.avatarImg||detail.avatarUrl}});"></view> 小程序端将图片保存到本地 //如果需要将头像转成图片保存,如cavans绘图场景 page.js const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(src) || []; if (format) { const filePath = `${wx.env.USER_DATA_PATH}/tmp_base64src.${format}`; // console.log(filePath) // const buffer = wx.base64ToArrayBuffer(bodyData); FileSystemManager.writeFile({ filePath, data: bodyData, encoding: 'base64', success() { console.log(filePath) }, fail() { console.log (new Error('ERROR_BASE64SRC_WRITE')); }, }); } 小程序端 已授权用户进入时自动更新 //进入小程序时,自动更新授权用户的信息到云端 app.js onLaunch: function () { this.getUserAuth(); } getUserAuth: function () { wx.getSetting({ success: res => { res.authSetting['scope.userInfo'] && wx.getUserInfo({ success: res => { wx.cloud.callFunction({ name: 'user', data: { userData: res.userInfo, } }) } }) } }) },
2020-07-07 - 微信小程序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 - 小程序map组件,marker只能显示一个(IOS )(Android和IOS有时会名字重复)
[图片] 在基础库为2.10.4的情况下,可以完整显示marker 2.11.0 只会显示一个,显示不全 附截图: IOS(Iphone XR)[图片] 只显示一个marker ———————————————————————————————————— Android(findx colors7.0)[图片](出现了marker名称重复的情况,之前没有遇到过)
2020-05-05 - 【个人笔记】小程序帐号登录功能暂未符合规范要求
审核被拒消息如下 你的小程序"在线答题小程序",提审时间2020-04-26 21:32:13,版本审核未通过。 1: 你好,小程序帐号登录功能暂未符合规范要求,请在用户了解体验小程序功能后,再要求用户进行帐号登录。请整改后再重新提交审核,具体登录规范及整改可参考:https://developers.weixin.qq.com/community/operate/doc/000640bb8441b82900e89f48351401 整改方案 在进行授权之前,提供游客模式,如果用户想体验功能,可以用游客模式进入。 具体被拒场景: 其实我已经很熟悉这个登录规范了,之前在设计的时候,也是有考虑这块,增加了,授权被拒时的游客模式,但是这次提交连续被拒了三次,第一次正常提交被拒,第二、三是由于反馈继续被拒,所以说这种方案被审核认定为不符合目前的规范,那么我就改,现在的方案改为,在需要授权时,跳转授权之前就提示是采用游客模式,还是前去授权,这样就可以在授权之前体验小程序的功能了。 1 [图片] 2 3 4
2020-04-27 - 【好文】小程序动态换肤解决方案 - 接口篇
需求说明 上一篇文章我是先通过在小程序本地预设几种主题样式,然后改变类名的方式来实现小程序的换肤功能的; 但是产品经理觉得每次改主题配置文件,都要发版,觉得太麻烦了,于是发话了:我想在管理后台有一个界面,可以让运营自行设置颜色,然后小程序这边根据运营在后台设置的色值来实现动态换肤,你们来帮我实现一下。 方案和问题 首先我们知道小程序是不能动态引入 [代码]wxss[代码] 文件的,这时候的色值字段是需要从后端接口获取之后,然后通过 [代码]style[代码] 内联的方式动态写入到需要改变色值的页面元素的标签上; 工作量之大,可想而知,因此,我们需要思考下面几个问题,然后尽可能写出可维护性,可扩展性的代码来: 页面元素组件化 —— 像[代码]按钮[代码] [代码]标签[代码] [代码]选项卡[代码] [代码]价格字体[代码] [代码]模态窗[代码]等组件抽离出来,认真考虑需要换肤的页面元素,避免二次编写; 避免内联样式直接编写,提高代码可阅读性 —— 内联编写样式会导致大量的 [代码]wxml[代码] 和 [代码]wxss[代码] 代码耦合一起,可考虑采用 [代码]wxs[代码] 编写模板字符串,动态引入,减少耦合; 避免色值字段频繁赋值 —— 页面或者组件引入 [代码]behaviors[代码] 混入色值字段,减少色值赋值代码编写; ps: 后续我会想办法尽可能通过别的手段,来通过js结合stylus预编译语言的方式,将本地篇和接口篇两种方案结合起来,即读取接口返回的色值之后,动态改变stylus文件的预设色值变量,而不是内联的方式实现小程序动态换肤 实现 接下来具体来详细详解一下我的思路和如何实现这一过程: model层: 接口会返回色值配置信息,我创建了一个 [代码]model[代码] 来存储这些信息,于是,我用单例的方式创建一个全局唯一的 [代码]model[代码] 对象 —— [代码]ViModel[代码] [代码]// viModel.js /** * 主题对象:是一个单例 * @param {*} mainColor 主色值 * @param {*} subColor 辅色值 */ function ViModel(mainColor, subColor) { if (typeof ViModel.instance == 'object') { return ViModel.instance } this.mainColor = mainColor this.subColor = subColor ViModel.instance = this return this } module.exports = { save: function(mainColor = '', subColor = '') { return new ViModel(mainColor, subColor) }, get: function() { return new ViModel() } } [代码] service层: 这是接口层,封装了读取主题样式的接口,比较简单,用 [代码]setTimeout[代码] 模拟了请求接口访问的延时,默认设置了 [代码]500[代码] ms,如果大家想要更清楚的观察 observer 监听器 的处理,可以将值调大若干倍 [代码]// service.js const getSkinSettings = () => { return new Promise((resolve, reject) => { // 模拟后端接口访问,暂时用500ms作为延时处理请求 setTimeout(() => { const resData = { code: 200, data: { mainColor: '#ff9e00', subColor: '#454545' } } // 判断状态码是否为200 if (resData.code == 200) { resolve(resData) } else { reject({ code: resData.code, message: '网络出错了' }) } }, 500) }) } module.exports = { getSkinSettings, } [代码] view层: 视图层,这只是一个内联css属性转化字符串的过程,我美其名曰视图层,正如我开篇所说的,[代码]内联[代码] 样式的编写会导致大量的 [代码]wxml[代码] 和 [代码]wxss[代码]代码冗余在一起,如果换肤的元素涉及到的 [代码]css[代码] 属性改动过多,再加上一堆的 [代码]js[代码] 的逻辑代码,后期维护代码必定是灾难性的,根本无法下手,大家可以看下我优化后的处理方式: [代码]// vi.wxs /** * css属性模板字符串构造 * * color => color属性字符串赋值构造 * background => background属性字符串赋值构造 */ var STYLE_TEMPLATE = { color: function(val) { return 'color: ' + val + '!important;' }, background: function(val) { return 'background: ' + val + '!important;' } } module.exports = { /** * 模板字符串方法 * * @param theme 主题样式对象 * @param key 需要构建内联css属性 * @param second 是否需要用到辅色 */ s: function(theme, key, second = false) { theme = theme || {} if (typeof theme === 'object') { var color = second ? theme.subColor : theme.mainColor return STYLE_TEMPLATE[key](color) } } } [代码] 注意:wxs文件的编写不能出现es6以后的语法,只能用es5及以下的语法进行编写 mixin: 上面解决完 [代码]wxml[代码] 和 [代码]wxss[代码] 代码混合的问题之后,接下来就是 [代码]js[代码] 的冗余问题了;我们获取到接口的色值信息之后,还需要将其赋值到[代码]Page[代码] 或者 [代码]Component[代码] 对象中去,也就是 [代码]this.setData({....})[代码]的方式, 才能使得页面重新 [代码]render[代码],进行换肤;</br> 微信小程序原生提供一种 [代码]Behavior[代码] 的属性,使我们避免反复 [代码]setData[代码] 操作,十分方便: [代码]// viBehaviors.js const observer = require('./observer'); const viModel = require('./viModel'); module.exports = Behavior({ data: { vi: null }, attached() { // 1. 如果接口响应过长,创建监听,回调函数中读取结果进行换肤 observer.addNotice('kNoticeVi', function(res) { this.setData({ vi: res }) }.bind(this)) // 2. 如果接口响应较快,modal有值,直接赋值,进行换肤 var modal = viModel.get() if (modal.mainColor || modal.subColor) { this.setData({ vi: modal }) } }, detached() { observer.removeNotice('kNoticeVi') } }) [代码] 到这里为止,基本的功能性代码就已经完成了,接下来我们来看一下具体的使用方法吧 具体使用 小程序启动,我们就需要去请求色值配置接口,获取主题样式,如果是需要从后台返回前台的时候也要考虑主题变动,可以在 [代码]onShow[代码] 方法处理 [代码]// app.js const { getSkinSettings } = require('./js/service'); const observer = require('./js/observer'); const viModel = require('./js/viModel'); App({ onLaunch: function () { // 页面启动,请求接口 getSkinSettings().then(res => { // 获取色值,保存到modal对象中 const { mainColor, subColor } = res.data viModel.save(mainColor, subColor) // 发送通知,变更色值 observer.postNotice('kNoticeVi', res.data) }).catch(err => { console.log(err) }) } }) [代码] 混入主题样式字段 [代码]Page[代码] 页面混入 [代码]// interface.js const viBehaviors = require('../../js/viBehaviors'); Page({ behaviors: [viBehaviors], onLoad() {} }) [代码] [代码]Component[代码] 组件混入 [代码]// wxButton.js const viBehaviors = require('../../js/viBehaviors'); Component({ behaviors: [viBehaviors], properties: { // 按钮文本 btnText: { type: String, value: '' }, // 是否为辅助按钮,更换辅色皮肤 secondary: { type: Boolean, value: false } } }) [代码] 内联样式动态换肤 [代码]Page[代码] 页面动态换肤 [代码]<view class="intro"> <view class="font mb10">正常字体</view> <view class="font font-vi mb10" style="{{_.s(vi, 'color')}}">vi色字体</view> <view class="btn main-btn mb10" style="{{_.s(vi, 'background')}}">主色按钮</view> <view class="btn sub-btn" style="{{_.s(vi, 'background', true)}}">辅色按钮</view> <!-- 按钮组件 --> <wxButton class="mb10" btnText="组件按钮(主色)" /> <wxButton class="mb10" btnText="组件按钮(辅色)" secondary /> </view> <!-- 引入模板函数 --> <wxs module="_" src="../../wxs/vi.wxs"></wxs> [代码] [代码]Component[代码] 组件动态换肤 [代码]<view class="btn" style="{{_.s(vi, 'background', secondary)}}">{{ btnText }}</view> <!-- 模板函数 --> <wxs module="_" src="../../wxs/vi.wxs" /> [代码] 再来对比一下传统的内联方式处理换肤功能的实现: [代码]<view style="color: {{ mainColor }}; background: {{ background }}">vi色字体</view> [代码] 如果后期再加入复杂的逻辑代码,开发人员后期再去阅读代码简直就是要抓狂的; 当然了,这篇文章的方案只是一定程度上简化了内联代码的编写,原理还是内联样式的注入; 我目前有一个想法,想通过某种手段在获取接口主题样式字段之后,借助 [代码]stylus[代码] 等预编译语言的变量机制,动态修改其变量,改变主题样式,方为上策; 总结:这两篇文章只是给大家提供一下小程序换肤的解决思路,文中如有不足之处,希望大家多多留言,或者去github上给我提issue,必定及时处理,谢谢大家。 效果预览 接口响应较快 —— [代码]ViModel[代码] 取值换肤 [图片] 接口响应过慢 —— [代码]observer[代码] 监听器回调取值换肤 [图片] 项目地址 项目地址:https://github.com/csonchen/wxSkin 这是本文案例的项目地址,为了方便大家浏览项目,我把编译后的wxss文件也一并上传了,大家打开就能预览,码字不易,大家如果觉得好,希望大家都去点下star哈,谢谢大家。。。
2020-04-24 - 智能垃圾分类小程序
之前我一直有参与垃圾分类小程序的开发,但是在拍照识别和语音识别这块没有涉及,最近几天我一直在调研,最后把这块打通了。 整理下来,希望对大家有用 第一:在拍照识别,以及语音识别这块用到百度的ai服务,具体注册链接如下 https://ai.baidu.com/ 智能垃圾回收小程序, 目前已经支持拍照识别、语音录入识别,还可以参与垃圾分类题库测试。 小程序采用云开发,无需单独搭建服务器、有不需要准备域名,只需要注册一个小程序便可以完成 3 相关截图 1 [图片] 2 [图片] 3 [图片] 4 [图片] 5 [图片] 6 [图片] 7 代码上传到了码云,请需要的自取 https://gitee.com/xiaofeiyang3369/fenleiminicode
2020-06-08 - 【笔记】小程序图表组件库
可能大家都知道echarts, 百度 echarts阿里 AntV 传送门 https://www.echartsjs.com/zh/tutorial.html#%E5%9C%A8%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E4%B8%AD%E4%BD%BF%E7%94%A8%20ECharts 这里笔记记录下一个新的图表组件选择 微信小程序图表charts组件,Charts for WeChat small app https://github.com/xiaolin3303/wx-charts 有没有用过的同学,谈谈
2020-03-23 - 基于云开发的垃圾分类小程序
基于云开发的垃圾分类小程序 开源的垃圾分类小程序 在2019年7月份的时候做过一个自建服务器的垃圾分类小程序,后来由于流量不好,就换掉下线了,最近正好有空,把自建服务器的垃圾分类小程序改成云开发的模式 基于自建服务器的垃圾分类小程序请移步 https://zhuanlan.zhihu.com/p/73508491 目前已经实现的功能有: 1、垃圾分类文章科普 2、垃圾分类查询 3、垃圾分类测试,支持单选和多选 4、 [图片] [图片] [图片] [图片] [图片][图片] [图片] [图片] 1 2 基于云开发的垃圾分类小程序代码地址 https://gitee.com/xiaofeiyang3369/fenlei 数据库 [图片] 一共8个集合 [图片] 备注: 该项目可以用来做为云开发初学者的一个入门练手项目,整体逻辑非常简单,云开发数据库、云存储、云函数都有用到,希望对您有用。
2020-06-08 - 干货:如何借助小程序云开发实现小程序支付功能(含源码)
正文共:5081 字 13 图 预计阅读时间:13 分钟 我们在做小程序支付相关的开发时总会遇到这些难题 1.小程序调用微信支付时必须要有自己的服务器 2.有自己的备案域名 3.有自己的后台开发 这就导致我们做小程序支付时的成本很大 本节就来教大家如何使用小程序云开发实现小程序支付功能的开发,不用搭建自己的服务器,不用有自己的备案域名,只需要简简单单的使用小程序云开发。 老规矩先看效果图: [图片] 本节知识点 1.云开发的部署和使用 2.支付相关的云函数开发 3.商品列表 4.订单列表 5.微信支付与支付成功回调 [图片] 支付成功给用户发送推送消息的功能会在后面讲解 下面就来教大家如何借助云开发使用小程序支付功能 支付所需要用到的配置信息 1.小程序appid 2.云开发环境id 3.微信商户号 4.商户密匙 一、准备工作 1.已经申请小程序,获取小程序 AppID 和 Secret 在小程序管理后台中——【设置】 →【开发设置】 可以获取微信小程序 AppID 和 Secret。 [图片] 2.微信支付商户号,获取商户号和商户密钥在微信支付商户管理平台中——【账户中心】→【商户信息】 可以获取微信支付商户号。 [图片] 在【账户中心】 ‒> 【API安全】 可以设置商户密钥。 [图片] 这里特殊说明下——个人小程序是没有办法使用微信支付的,所以如果想使用微信支付功能必须是非个人账号(当然个人可以办个体户工商执照来注册非个人小程序账号) 3.微信开发者 IDE https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 4.开通小程序云开发功能 https://edu.csdn.net/course/play/9604/204526 二、商品列表的实现 效果图如下 由于本节重点是支付的实现所以这里只简单贴出关键代码 [图片] wxml布局如下: [代码]<view class="container"> <view class="good-item" wx:for="{{goods}}" wx:key="*this" ontap="getDetail" data-goodid="{{item._id}}"> <view class="good-image"> <image src="{{pic}}"></image> </view> <view class="good-detail"> <view class="title">商品: {{item.name}}</view> <view class="content">价格: {{item.price / 100}} 元 </view> <button class="button" type="primary" bindtap="makeOrder" data-goodid="{{item._id}}" >下单</button> </view> </view></view> [代码] 我们所需要做的就是借助云开发获取云数据库里的商品信息然后展示到商品列表,关于云开发获取商品列表并展示本节不做讲解(感兴趣的同学可以翻看作者历史博客,有写过的) [图片] 三、支付云函数的创建 首先看下我们支付云函数都包含那些内容 [图片] 简单先讲解下每个的用处 config下的index.js是做支付配置用的,主要配置支付相关的账号信息 lib是用的第三方的支付库,这里不做讲解 重点讲解的是云函数入口 index.js 下面就来教大家如何去配置 1.配置config下的index.js, 这一步所需要做的就是把小程序appid、云开发环境ID、商户id、商户密匙填进去。 [图片] 2.配置入口云函数 [图片] 详细代码如下 代码里注释很清楚了这里不再做单独讲解: [代码]const cloud = require('wx-server-sdk') cloud.init()const app = require('tcb-admin-node');const pay = require('./lib/pay');const { mpAppId, KEY } = require('./config/index');const { WXPayConstants, WXPayUtil } = require('wx-js-utils'); const Res= require('./lib/res'); const ip = require('ip');/** * * @param {obj} event * @param {string} event.type 功能类型 * @param {} userInfo.openId 用户的openid */exports.main = async function(event, context) { const { type, data, userInfo } = event; onst wxContext = cloud.getWXContext() const openid = userInfo.openId; app.init(); const db = app.database (); const goodCollection = db.collection('goods'); const orderCollection = db.collection('order');// 订单文档的status 0 未支付 1 已支付 2 已关闭 switch (type) { // [在此处放置 unifiedorder 的相关代码] case 'unifiedorder': { // 查询该商品 ID 是否存在于数据库中,并将数据提取出来 const goodId = data.goodId let goods = await goodCollection.doc(goodId).get(); if (!goods.data.length) { return new Res ({ code: 1, message: '找不到商品' }); } // 在云函数中提取数据,包括名称、价格才更合理安全, // 因为从端里传过来的商品数据都是不可靠的 let good = goods.data[0]; // 拼凑微信支付统一下单的参数 const curTime = Date.now(); const tradeNo =`${goodId}-${curTime}`; const body = good.name; const spbill_create_ip = ip.address() || '127.0.0.1'; // 云函数暂不支付 http 触发器,因此这里回调 notify_url 可以先随便填。 const notify_url = 'http://www.qq.com'; // '127.0.0.1'; const total_fee = good.price; const time_stamp = '' + Math.ceil(Date.now() / 1000); const out_trade_no = `${tradeNo}`; const sign_type = WXPayConstants.SIGN_TYPE_MD5; let orderParam = { body, spill_create_ip, notify_url, out_trade_no, total_fee, openid, trade_type: 'JSAPI', timeStamp: time_stamp, }; // 调用 wx-js-utils 中的统一下单方法 const { return_code, ...restData } = await pay.unifiedOrder(orderParam); let order_id = null; if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { const { prepay_id, nonce_str } = restData; // 微信小程序支付要单独进地签名,并返回给小程序端 const sign = WXPayUtil.generateSignature ({ appId: mpAppId, nonceStr: nonce_str, package: `prepay_id=${prepay_id}`, signType: 'MD5', timeStamp: time_stamp }, KEY); let orderData = { out_trade_no, time_stamp, nonce_str, sign, sign_type, body, total_fee, prepay_id, sign, status: 0, // 订单文档的status 0 未支付 1 已支付 2 已关闭 _openid: openid, }; let order = await orderCollection.add(orderData); order_id = order.id; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { out_trade_no, time_stamp, order_id, ...restData } }); } // [在此处放置 payorder 的相关代码] case 'payorder': { // 从端里出来相关的订单相信 const { out_trade_no, prepay_id, body, total_fee } = data; // 到微信支付侧查询是否存在该订单,并查询订单状态,看看是否已经支付成功了。 const { return_code, ...restData } = await pay.orderQuery({ out_trade_no }); // 若订单存在并支付成功,则开始处理支付 if (restData.trade_state === 'SUCCESS') { let result = await orderCollection .where({ out_trade_no }) .update({ status: 1, trade_state: restData.trade_state, trade_state_desc: restData.trade_state_desc }); let curDate = new Date(); let time = `${curDate.getFullYear()}-${curDate.getMonth() + 1}-${curDate.getDate()} ${curDate.getHours()}:${curDate.getMinutes()}:${curDate.getSeconds()}`; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } case 'orderquery': { const { transaction_id, out_trade_no } = data; // 查询订单 const { data: dbData } = await orderCollection .where({ out_trade_no }).get(); const { return_code, ...restData } = await pay.orderQuery({ transaction_id, out_trade_no }); return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { ...restData, ...dbData[0] } }); } case 'closeorder': { // 关闭订单 const { out_trade_no } = data; const { return_code, ...restData } = await pay.closeOrder({ out_trade_no }); if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { await orderCollection .where({ out_trade_no }) .update({ status: 2, trade_state: 'CLOSED', trade_state_desc: '订单已关闭' }); } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } } } [代码] 其实我们支付的关键功能都在上面这些代码里面了 [图片] 再来看下支付的相关流程截图 [图片] 上图就涉及到了我们的订单列表、支付状态、支付成功后的回调 今天就先讲到这里后面会继续给大家讲解支付的其他功能——比如支付成功后的消息推送也是可以借助云开发实现的 [图片] 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心!
2019-09-19 - 云开发,从0到1实现小程序内微信支付功能
首先很感谢云开发提供的生态以及示例代码,安耐不住内心的激动,在这一刻实现了小程序内微信支付,在3年前做过基于公众号的微信支付以及企业红包当时玩的很溜,动不动就给群里的小伙伴定向推送红包 本次微信支付参考以下官方开源项目 https://github.com/TencentCloudBase/mp-book https://github.com/TencentCloudBase/tcb-demo-basic 具体交互截图如下所示: [图片] [图片] [图片] [图片] 我总结以下几点把,以下3点不全,但是对于一个有云开发经验的同学,这足够了。 1、在微信企业支付后台 进行相应的设置 [图片] 2、在上面源代码的基础上,配置,appi d,商户 号,安全 密钥以及api证书 module.exports = { ENV: 'xxx', // TCB环境ID MCHID: 'xxx',//商户id KEY: '0123456789abcdefghijklmnopqrstuv', CERT_FILE_CONTENT: fs.existsSync(CERT_PATH) ? fs.readFileSync(CERT_PATH) : null, TIMEOUT: 10000 // 毫秒 }; 3、云开发数据库新增goods、orders两个集合,并赋予所有可读写权限,这一点很重要。 开发过程中遇到的问题: 1、云函数执行失败 这是由于在之前没有在数据库里面创建goods集合和orders集合 2、签名错误 这是在正确配置之后还报这个错误,这个时候不要慌,首先核对自己的配置有没有问题,在确保配置没问题的前提下,相信自己,重新运行下就好了。 [图片] 备注:想了解更多关于微信支付的逻辑流转,请别走开,继续阅读,下面是腾讯云课堂,有视频有文档,不容错过。 附几个微信支付实现的文档 https://cloud.tencent.com/developer/team/tcb/courses https://cloud.tencent.com/edu/learning/course-1276-4318 https://cloud.tencent.com/edu/learning/learn-1276-3815
2020-03-02 - 自定义客服 消息卡片
小程序如何合理合规引导用户关注公众号或者添加客服微信点击客服,出现"你可能发送的图片"或者"你可能发送的小程序" 在之前的文章中我写过,如果通过小程序合理合规引导用户关注公众号,具体见下文 https://developers.weixin.qq.com/community/develop/article/doc/000208cb3fc438638ae9fd6d451013 其中提到一种方式就是客服消息, 自定义卡片消息交互设计示例 具体示例图如下所示: 以下截图来自小程序抽奖助手 [图片] [图片] [图片] [图片] 自定义卡片消息代码片段 https://developers.weixin.qq.com/miniprogram/dev/component/button.html 该技术是自定义客服卡片,具体代码可以参考 欢迎使用代码片段,可在控制台查看代码片段的说明和文档 联系客服 https://developers.weixin.qq.com/s/NHTz9tml76c3 [图片]
2020-03-14 - 如何实现一个简单的http请求的封装
好久没发文章了,最近浏览社区看到比较多的请求封装,以及还有在使用原始请求的童鞋。为了减少代码,提升观赏性,我也水一篇吧,希望对大家有所帮助。 默认请求方式,大家每次都这样一些写相同的代码,会不会觉得烦,反正我是觉得头大 😂 [代码]wx.request({ url: 'test.php', //仅为示例,并非真实的接口地址 data: { x: '', y: '' }, header: { 'content-type': 'application/json' // 默认值 }, success (res) { console.log(res.data) } }) [代码] 来,进入正题吧,把这块代码封装下。 首先新建个request文件夹,内含request.js 代码如下: [代码]/** * 网络请求封装 */ import config from '../config/config.js' import util from '../util/util.js' // 获取接口地址 const _getPath = path => (config.DOMAIN + path) // 封装接口公共参数 const _getParams = (data = {}) => { const timestamp = Date.now() //时间戳 const deviceId = Math.random() //随机数 const version = data.version || config.version //当前版本号,自定或者取小程序的都行 const appKey = data.appKey || config.appKey //某个小程序或者客户端的字段区分 //加密下,防止其他人随意刷接口,加密目前采用的md5,后端进行校验,这段里面的参数你们自定,别让其他人知道就行,我这里就是举个例子 const sign = data.sign || util.md5(config.appKey + timestamp + deviceId) return Object.assign({}, { timestamp, sign, deviceId, version, appKey }, data) } // 修改接口默认content-type请求头 const _getHeader = (headers = {}) => { return Object.assign({ 'content-type': `application/x-www-form-urlencoded` }, headers) } // 存储登录态失效的跳转 const _handleCode = (res) => { const {statusCode} = res const {msg, code} = res.data // code为 4004 时一般表示storage里存储的token失效或者未登录 if (statusCode === 200 && (code === 4004)) { wx.navigateTo({ url: '/pages/login/login' }) } return true } /** * get 请求, post 请求 * @param {String} path 请求url,必须 * @param {Object} params 请求参数,可选 * @param {String} method 请求方式 默认为 POST * @param {Object} option 可选配置,如设置请求头 { headers:{} } * * option = { * headers: {} // 请求头 * } * */ export const postAjax = (path, params) => { const url = _getPath(path) const data = _getParams(params) //如果某个参数值为undefined,则删掉该字段,不传给后端 for (let e in data) { if (data[e] === 'undefined') { delete data[e] } } // 处理请求头,加上最近比较流行的jwtToken(具体的自己百度去) const header = util.extend( true, { "content-type": "application/x-www-form-urlencoded", 'Authorization': wx.getStorageSync('jwtToken') ? `Bearer ${wx.getStorageSync('jwtToken')}` : '', }, header ); const method = 'POST' return new Promise((resolve, reject) => { wx.request({ url, method, data, header, success: (res) => { const result = _handleCode(res) result && resolve(res.data) }, fail: function (res) { reject(res.data) } }); }) } [代码] 那么如何调用呢? [代码]//把request的 postAjax注册到getApp()下,调用时: const app = getApp() let postData = { //这里填写请求参数,基础参数里的appKey等参数可在这里覆盖传入。 } app.postAjax(url, postData).then((res) => { if (res.success) { //这里处理请求成功逻辑。 } else { //wx.showToast大家觉得麻烦也可以写到util.js里,调用时:util.toast(msg) 即可。 wx.showToast({ title: res.msg || '服务器错误,请稍后重试', icon: "none" }) } }).catch(err => { //这里根据自己场景看是否封装到request.js里 console.log(err) }) [代码] config.js 主要是处理正式环境、预发环境、测试环境、开发环境的配置 [代码]//发版须修改version, env const env = { dev: { DOMAIN: 'https://dev-api.weixin.com' }, test: { DOMAIN: 'https://test-api.weixin.com', }, pro: { DOMAIN: 'https://api.qtshe.com' } } module.exports = { ...env.pro } [代码] 以上就是简单的一个request的封装,包含登录态失效统一跳转、包含公共参数的统一封装。 老规矩,最后放代码片段,util里内置了md5方法以及深拷贝方法,具体的我也不啰嗦,大家自行查看即可~ https://developers.weixin.qq.com/s/gbPSLOmd7Aft
2020-04-03 - 云开发数据库命令之地理位置查询
在进行云开发的数据更新的时候,我们可以进行不同条件的查询,从而提升我们更新的效率。在云开发支持了 Geo Point 等相关数据类型以后,我们可以用云开发实现多种不同地理位置的数据存储和利用,对于我们开发基于地理位置的小程序来说,有很大的帮助。 今天的课程中,我们来介绍小程序数据更新命令中的「地理位置查询」命令。 geoNear:查询特定点附近的数据 查询特定点附近的数据可以说是我们在进行应用开发时,最为常用的功能,它可以应用于诸如查询当前用户坐标周围的店铺、查询距离我最近的公交站等场景,这个时候,我们需要使用 [代码]geoNear[代码] 来进行数据库查询。 数据结构需求 如果你想要使用 [代码]geoNear[代码],则要求你在数据库中存储的数据对于地理位置的存储是基于 [代码]db.Geo.Point[代码] 进行的,这样你就可以完成使用 [代码]geoNear[代码] 进行查询。 数据查询实例 假设我们当前数据库内数据的结构是这样的,每一个数据下有一个 point 属性,这个属性是通过 [代码]db.Geo.Point[代码] 添加的。 [图片] 如果我们希望查询距离当前位置,1000米 ~ 2000米的数据,则可以执行这样的命令 [代码]const db = wx.cloud.database() const _ = db.command db.collection('items').where({ location: _.geoNear({ geometry: db.Geo.Point(113.323809, 23.097732), minDistance: 1000, maxDistance: 2000, }) }).get() [代码] 这里的 [代码]geometry[代码] 中的 [代码]Point[代码] 的数据是获取到的当前地理位置信息,它必须是 [代码]db.Geo.Point[代码] 类型的,你可以通过小程序的 [代码]wx.getLocation[代码] 方法获取当前的地址信息,并将其作为参数设置在这里。 [代码]minDistance[代码] 则是距离中心点的最小距离,单位是米,所以这里将其设置为 1000。类似的,[代码]maxDistance[代码] 则是距离中心点的最大距离,单位也是米,所以这里将其设置为 2000。 通过这样的方法,我们就可以查询出数据了。 在我们去做一些基于地理位置的应用的时候,[代码]db.command.geoNear[代码] 可以很大程度上简化我们的开发。 geoWithin:查询特定区域内的数据 在开发地理位置应用时,除了基于某一个点的位置进行查询以外,我们还会查询某一个区域内的数据,比如查询北京市昌平区内的所有的酒吧、查询深圳南山区内所有的博物馆,这样的需求也是切实存在,并且十分常见的需求。这个时候,我们可以考虑,使用 [代码]geoWithin[代码] 来进行数据查询。 数据结构需求 如果你想要使用 [代码]geoWithin[代码],则要求你在数据库中存储的数据对于地理位置的存储是基于 [代码]db.Geo.Point[代码] 进行的,这样你就可以完成使用 [代码]geoWithin[代码] 进行查询。 数据查询实例 假设我们当前数据库内数据的结构是这样的,每一个数据下有一个 point 属性,这个属性是通过 [代码]db.Geo.Point[代码] 添加的。 [图片] 如果我们希望查询在东经 112 度 ~ 东经 114 度,北纬 22 度 到 北纬 24 度范围内的数据,则可以执行这样的数据查询 [代码]const db = wx.cloud.database() const _ = db.command const { Point, LineString, Polygon } = db.Geo db.collection('items').where({ location: _.geoWithin({ geometry: Polygon([ LineString([ Point(112, 22), Point(112, 24), Point(114, 24), Point(114, 22), Point(112,22) ]) ]), }) }).get() [代码] 我们通过 [代码]Polygon[代码] 和 [代码]LineString[代码] 构建出了一个正方形,从而实现了查询一个特定的正方形区域内的数据。 如果你希望查询一个其他形状的范围内的数据,只需要传入多个 [代码]Point[代码] 的数据就可以完成,比如如果你要查询一个五边形内部的数据,也只需在构建 LineString 时,传入 6 个 Point的数据即可。 为什么五边形却是 6 个 Point 呢? 因为在云开发中,如果你要构建一个多边形,则需要使得整个多边形是闭合的,也就是说,你的起始点是一样的,因此,如果你想要构建一个四边形,则需要五个点。如果是构建五边形,则是六个点。 geoIntersects:查询与特定区域相交的数据 [代码]geoIntersects[代码] 是用于查询所有数据中和给定数据相交的数据,我们可以将其用作判断某一些特定的点、线、面是否在一个特定区域内。举个例子,假设你已经有了用户当前的活动范围,比如某一条街道,那么你可以基于 [代码]geoIntersects[代码] 来构建一条线,并基于这条线查询,所有数据中,是否有数据与这个线相交,如果相交,则说明对应的数据点是在用户所在的街道上。你就可以将这个数据告诉用户,让用户去找这些点。 相比于 [代码]geoWithin[代码],[代码]geoIntersects[代码] 对于当前用户的位置数据更为随意,支持 [代码]Point[代码]、[代码]LineString[代码]、[代码]MultiPoint[代码]、 [代码]MultiLineString[代码]、 [代码]Polygon[代码]、 [代码]MultiPolygon[代码] 等多种不同的数据结构,在进行查询的时候,更加的方便。 数据结构需求 如果你想要使用 [代码]geoIntersects[代码],则要求你在数据库中存储的数据对于地理位置的存储是基于 [代码]db.Geo.Point[代码] 进行的,这样你就可以完成使用 [代码]geoIntersects[代码] 进行查询。 数据查询实例 假设我们当前数据库内数据的结构是这样的,每一个数据下有一个 point 属性,这个属性是通过 [代码]db.Geo.Point[代码] 添加的。 [图片] 如果我们希望查询在东经 112 度 ~ 东经 114 度,北纬 22 度 到 北纬 24 度范围内的数据,则可以执行这样的数据查询 [代码]const db = wx.cloud.database() const _ = db.command const { Point, LineString, Polygon } = db.Geo db.collection('items').where({ location: _.geoIntersects({ geometry: Polygon([ LineString([ Point(112, 22), Point(112, 24), Point(114, 24), Point(114, 22), Point(112,22) ]) ]), }) }).get() [代码] 我们通过 [代码]Polygon[代码] 和 [代码]LineString[代码] 构建出了一个正方形,从而实现了查询一个特定的正方形区域内的数据。 如果你希望查询一个其他形状的范围内的数据,只需要传入多个 [代码]Point[代码] 的数据就可以完成,比如如果你要查询一个五边形内部的数据,也只需在构建 LineString 时,传入 6 个 Point的数据即可。 当然,在使用时,你可以根据自己的实际情况,设定不同的图形类型,作为数据库查询的对象,完成自己的数据查询需求。 总结 这节课,我们介绍了 [代码]geoNear[代码]、[代码]geoWithin[代码]、[代码]geoIntersects[代码] 三个 API,帮助大家理解其各自在什么样的场景下使用,下一节课,我们将介绍 eq、neq、lt、lte、gt、gte 几个命令。
2019-09-23 - 单张、多张图片上传(图片转base64格式)实践经验
定义初始数据: data: { imgList: [], // 图片集合 baseImg: [], // base64图片集合 maxImg: 8, // 图片上传最高数量(根据需求设置) } 第一步:从本地相册选择图片或使用相机拍照(wx.chooseImage) // 选择图片 selectPictures: function() { const that = this; // 最多上传图片数量 if (that.data.imgList.length < that.data.maxImg) { wx.chooseImage({ count: that.data.maxImg - that.data.imgList.length, // 最多可以选择的图片张数(最大数量-当前已上传数量=当前可上传最大数量) sizeType: "compressed", success: function(res) { for (let i = 0; i < res.tempFilePaths.length; i++) { that.data.imgList.push(res.tempFilePaths[i]); } // 显示图片(同步渲染到页面) that.setData({ imgList: that.data.imgList }) } }) } else { wx.showToast({ title: "最多上传" + that.data.maxImg + "张照片!" }) } } count:最多可以选择的图片张数(默认9) sizeType:所选的图片的尺寸(original-原图,compressed-压缩图) sourceType:选择图片的来源(album-从相册选图,camera-使用相机) 第二步:将图片本地路径转为base64图片格式(wx.getFileSystemManager().readFile) // 图片转base64 conversionAddress: function() { const that = this; // 判断是否有图片 if (that.data.imgList.length !== 0) { for (let i = 0; i < that.data.imgList.length; i++) { // 转base64 wx.getFileSystemManager().readFile({ filePath: that.data.imgList[i], encoding: "base64", success: function(res) { that.data.baseImg.push('data:image/png;base64,' + res.data); //转换完毕,执行上传 if (that.data.imgList.length == that.data.baseImg.length) { that.upCont(that.data.textCont, that.data.baseImg); } } }) } } else { wx.showToast({ title: "请先选择图片!" }) } } filePath:要读取的文件的路径 (本地路径) encoding:指定读取文件的字符编码(ascii,base64,binary,hex......) 第三步:执行上传,把图片数组传输给后端即可 // 执行上传 upCont: function (baseImg) { const that = this; wx.request({ url: "上传地址", method: "POST", data: { imglist: baseImg }, success: function (res) { if (res.data.code == 200) { wx.showModal({ title: "提示", content: "提交成功,棒棒哒!" }) // 清空当前数据 that.data.imgList = []; } else { wx.showModal({ title: "提示", content: "上传失败!" }) } } }) } 删除功能:被选中图片移除当前图片数组 // 删除图片(选中图片移除) delImg: function(e) { const that = this; const index = e.currentTarget.dataset.index; // 当前点击图片索引 that.data.imgList.splice(index, 1); that.setData({ imgList: that.data.imgList }) } tips:点击提交按钮后可以增加显示loading提示框:wx.showLoading(),返回结果后隐藏loading提示框:wx.hideLoading(),此方法可以避免重复点击! 完整代码: 1.js代码(直接复制文中代码即可) 2.wxml <view class="img-list"> <view class="txt">图片 {{imgList.length}} / {{maxImg}}</view> <view class="list"> <!-- 图片展示列表 --> <view class="li" wx:for="{{imgList}}" wx:key="index"> <image class="file" src="{{item}}"></image> <!-- 删除图片 --> <image class="close" src="/images/close.png" data-index="{{index}}" bindtap="delImg"></image> </view> <!-- 添加图片 --> <view class="li" bindtap="selectPictures"> <image class="file" src="/images/upload.jpg"></image> </view> </view> </view> <view class="btn" bindtap="conversionAddress">提 交</view> 3.wxss .img-list{ width: 700rpx; margin: 0 auto;} .img-list .txt{ width: 680rpx; padding: 40rpx 0 20rpx; margin: 0 auto; color: #b2b2b2;} .img-list .list{ width: 700rpx; overflow: hidden;} .img-list .list .li{ width: 160rpx; margin: 10rpx 0 0 10rpx; height: 160rpx; border: 1rpx solid #fff; float: left; position: relative;} .img-list .list .li:last-child{ border: 1rpx solid #f7f7f7;} .img-list .list .li .file{ display: block; width: 160rpx; height: 160rpx;} .img-list .list .li .close{ position: absolute; top: 0; right: 0; width: 44rpx; height: 44rpx; background: #fff;} .btn{ background: #f60; width: 680rpx; border-radius: 10rpx; line-height: 88rpx; color: #fff; text-align: center; margin: 50rpx auto 0;} 效果图: [图片]
2020-03-12 - sticky三个应用场景
1. 吸顶导航 <view class="item_title" bindtap="toBack">返回上一页</view> <view class="item" wx:for='{{10}}' wx:key='index'>{{item+1}}</view> .item_title { width: 700rpx; height: 90rpx; line-height: 90rpx; text-align: center; background-color: #ccc; border-radius: 8rpx; position: sticky; top: 6rpx; margin: 25rpx; } .item{ height: 200rpx; border-bottom: 2rpx solid #ccc; line-height: 200rpx; text-align: center; } [图片] 2. 标题推开 <block wx:for='{{list}}' wx:key='index'> <view class="title">{{item.title}}</view> <view class="content" wx:for='{{item.content}}' wx:key='index2' wx:for-item='item2' wx:for-index='index2'>{{item2}}</view> </block> .title { line-height: 80rpx; font-size: 38rpx; position: sticky; top: 0; background-color: #ccc; } .content { line-height: 60rpx; } [图片] 3. 相对父级固定定位 <view class='page'> <view class="box"> <view class="box_title">砍价规则</view> <view class="box_cotent" wx:for="{{rule}}" wx:key='index'>{{item}}</view> <button class="box_btn" type="primary">已了解</button> </view> <view class="body">占位符</view> </view> .box{ height: 500rpx; border: 2rpx solid #ccc; margin: 10rpx; overflow: scroll; padding: 0 20rpx; } .box_title{ text-align: center; line-height: 80rpx; font-size: 38rpx; position: sticky; left: 0; top: 0; background-color: #fff; } .box_cotent{ line-height: 60rpx; color: #333; padding-bottom: 10rpx; } .body{ line-height: 1500rpx; text-align: center; } .box_btn{ position: sticky; left: 0; bottom: 10rpx; } [图片] 代码片段: https://developers.weixin.qq.com/s/uTR93LmD7cfM
2020-03-11 - 开发福利——免费API,为您收集免费的接口服务,做一个api的搬运工
为了方便广大的开发者,特此统计了网上诸多的免费API,为您收集免费的接口服务,做一个api的搬运工,以后会每月定时更新新的接口。有些接口来自第三方,在第三方注册就可以成为他们的会员,免费使用他们的部分接口。 百度AccessToken:针对HTTP API调用者,百度AIP开...——接口地址 语音识别:通过场景识别优化,为车载导航,智能家居和...——接口地址 语音合成:将用户输入的文字,转换成流畅自然的语音输...——接口地址 出租车票识别(可在线调用):针对出租车票(现支持北京、上海、深圳)的...——接口地址 火车票识别(可在线调用):支持对大陆火车票的车票号、始发站、目的站...——接口地址 数字识别(可在线调用):对图像中的阿拉伯数字进行识别提取,适用于...——接口地址 通用文字识别(可在线调用):支持多场景下的文字检测识别,多项ICDA...——接口地址 网络图片文字识别(可在线调用):能够快速准确识别各种网络图片中的文字,在...——接口地址 身份证识别(可在线调用):支持对二代居民身份证正反面的关键字段识别...——接口地址 银行卡识别(可在线调用):支持对主流银行卡卡号识别,并返回发卡行和...——接口地址 驾驶证识别(可在线调用):支持对机动车驾驶证正页的关键字段识别,包...——接口地址 行驶证识别(可在线调用):支持对机动车行驶证正页的关键字段识别,包...——接口地址 手写文字识别(可在线调用):能够对手写汉字和手写数字进行识别——接口地址 增值税发票识别(可在线调用):识别并结构化返回增值税发票的各个字段及其...——接口地址 营业执照识别(可在线调用):支持对营业执照关键字段的识别,包括单位名...——接口地址 车牌识别(可在线调用):支持对中国大陆机动车车牌的识别,包括地域...——接口地址 票据识别(可在线调用):支持对增值税发票、火车票、出租车票(支持...——接口地址 表格文字识别:自动识别表格线及表格内容,结构化输出表头...——接口地址 通用物体和场景识别(可在线调用):支持超过10万类物体和场景识别,接口返回...——接口地址 图像主体检测(可在线调用):检测图片中关键主体位置,接口支持检测单张...——接口地址 品牌logo识别(可在线调用):实现2万类品牌logo识别,接口返回品牌...——接口地址 植物识别(可在线调用):植物识别支持2万多种通用植物识别、近...——接口地址 动物识别(可在线调用):支持数千种动物识别,接口返回名称——接口地址 菜品识别(可在线调用):识别超过5万个菜品,接口返回菜品的名称、...——接口地址 地标识别(可在线调用):支持识别约5万中外著名地标、景点,接口返...——接口地址 车型识别(可在线调用):识别车辆的具体车型,以小汽车为主,输出图...——接口地址 车辆检测:识别图像中所有机动车辆的类型和位置,并对...——接口地址 GIF色情图像识别(可在线调用):人工智能鉴黄技术,智能识别图片和视频中的...——接口地址 图像审核(可在线调用):通过人脸检测、文字识别、色情识别、暴恐识...——接口地址 人脸检测与属性分析(可在线调用):检测图中的人脸,并为人脸标记出边框。检测...——接口地址 在线活体检测(可在线调用):提供在线方式的人脸活体检测能力,在人脸识...——接口地址 人体关键点识别(可在线调用):检测人体并返回人体矩形框位置,精准定位1...——接口地址 人流量统计(可在线调用):统计图像中的人体个数和流动趋势,以头肩为...——接口地址 人体检测与属性识别(可在线调用):检测图像中的所有人体,识别人体的20类属...——接口地址 手势识别(可在线调用):识别图片中的手部位置和手势类型,可识别2...——接口地址 人像分割(可在线调用):识别图像中的人体轮廓,与背景进行分离——接口地址 驾驶行为分析(可在线调用):针对车载场景,识别驾驶员使用手机、抽烟、...——接口地址 词法分析(可在线调用):基于大数据和用户行为的分词、词性标注、命...——接口地址 词向量表示(可在线调用):词向量计算是通过训练的方法,将语言词表中...——接口地址 词义相似度(可在线调用):用于计算两个给定词语的语义相似度,基于自...——接口地址 依存句法分析(可在线调用):利用句子中词与词之间的依存关系来表示词语...——接口地址 DNN语言模型(可在线调用):语言模型是通过计算给定词组成的句子的概率...——接口地址 短文本相似度(可在线调用):短文本相似度计算服务能够提供不同短文本之...——接口地址 文本纠错(可在线调用):文本纠错支持短文本、长文本、语音识别结果...——接口地址 情感倾向分析(可在线调用):针对带有主观描述的中文文本,可自动判断该...——接口地址 评论观点抽取(可在线调用):自动分析评论关注点和评论观点,并输出评论...——接口地址 对话情绪识别(可在线调用):针对一段对话文本,自动识别出当前会话者所...——接口地址 文章标签(可在线调用):文章标签服务对文章的标题和内容进行深度分...——接口地址 文章分类(可在线调用):文章分类服务对文章内容进行深度分析,输出...——接口地址 新闻摘要(可在线调用):基于深度语义分析模型,自动抽取新闻文本中...——接口地址 通用翻译(可在线调用):支持28种语言实时互译,覆盖中、英、日、...——接口地址 实体标注(可在线调用):结合上下文,识别文本中的实体并将其关联到...——接口地址 新闻头条(可在线调用):最新新闻头条,各类社会、国内、国际、体育...——接口地址 手机号码归属地(可在线调用):根据手机号码或手机号码的前7位,查询手机...——接口地址 彩票开奖结果查询(可在线调用):目前支持双色球、大乐透、七乐彩、七星彩、...——接口地址 天气预报(可在线调用):查询天气情况:温度、湿度、AQI、天气、...——接口地址 二维码生成(可在线调用):按照设定的参数、生成二维码——接口地址 汇率(可在线调用):外汇报价,货币汇率——接口地址 历史上的今天(可在线调用):回顾历史的长河,历史是生活的一面镜子——接口地址 成语词典(可在线调用):新华字典在线查字,最新最全——接口地址 新华字典(可在线调用):最大最全的新华汉语词典,按拼音查、按部首...——接口地址 微信精选(可在线调用):微信精选文章——接口地址 笑话大全(可在线调用):搜集网络幽默、搞笑、内涵段子,不间断更新——接口地址 全国WIFI(可在线调用):全国免费的WIFI热点分布——接口地址 货币汇率(可在线调用):支持人民币牌价、外汇汇率查询;数据仅供参...——接口地址 手机固话来电显示:查询手机/固话号码归属地,是否诈骗、营销...——接口地址 简/繁/火星字体转换(可在线调用):实现简体、繁体、火星文之间的转换,转换字...——接口地址 全国邮编查询(可在线调用):提供全国邮政编码大全,为你快速准确查邮编——接口地址 老黄历(可在线调用):提供老黄历查询,黄历每日吉凶宜忌查询——接口地址 周公解梦(可在线调用):周公解梦,周公解梦大全,周公解梦查询,免...——接口地址 净值数据(可在线调用):根据基金类型及分页参数来获取数据(开放式...——接口地址 星座运势(可在线调用):十二星座每日、每月、每年运势——接口地址 图书电商数据(可在线调用):于万千之中选择你所爱--好书推荐,值得你...——接口地址 身份证查询(可在线调用):身份证归属地信息查询——接口地址 黄金数据(可在线调用):黄金品种、最新价、开盘价、最高价等信息——接口地址 IP地址(可在线调用):根据查询的IP地址或者域名,查询该IP所...——接口地址 笑话大全——文字(可在线调用):每小时更新。文字笑话大全,信息搜集整理于...——接口地址 笑话大全——图片(可在线调用):每小时更新。图片笑话大全,信息搜集整理于...——接口地址 最新新闻:新闻API接口 官方自营 会员接口...——接口地址 美图大全:根据几十个种类获取图片列表,每日更新。种...——接口地址 手机归属地查询:最全、最新的手机号段数据库。本地找不到的...——接口地址 历史上的今天:回顾历史的长河,历史是生活的一面镜子;历...——接口地址 来福岛笑话——图片:来福岛爆笑娱乐网创建于2000年,是国内...——接口地址 来福岛笑话——文字:来福岛爆笑娱乐网创建于2000年,是国内...——接口地址 全国景点查询:全国景点查询接口(来自同程网的合作数据)...——接口地址 健康知识:根据养生、用药、两性等频道内容获取健康知...——接口地址 猜一猜:随机返回谜语,有2.5万谜语,每日更新。...——接口地址 身份证查询:可根据身份证号,查询其签发地、生日、性别...——接口地址 爱飞天气插件:爱飞天气是ShowAPI官方天气接口的一...——接口地址 PM2.5空气质量指数:本接口每小时更新1次。空气质量指数提供实...——接口地址 全球IP地址查询:全球IP地址——接口地址 域名查询:域名查询地理位置——接口地址 汉字转拼音:将汉字转换为拼音和拼音首字母缩写——接口地址 中文分词:中文分词接口。将长段中文切词分开。使用场...——接口地址 图片验证码生成:图形|图片验证码生成,支持自定义高宽,文...——接口地址 新闻、网页正文抽取:传入一个新网或网页地址,接口将返回此ur...——接口地址 实时IP代理查询:代理数量并不是越多越好,可以用才是真正的...——接口地址 今日油价:今日油价,可查询全国31个省的油价。每天...——接口地址 QQ号码测凶吉:输入qq号码,得到此号码的算命情况,例如...——接口地址 地址转换经纬度:根据城市和名称转换为相应的经纬度——接口地址 经纬度转换地址:根据经纬度转换成相应地址——接口地址 黄历运势:根据输入日期,查看某一天的黄历运势——接口地址 十大银行实时汇率:包括工商银行、中国银行、农业银行、交通银...——接口地址 汇率转换:1分钟更新1次。当前十大银行,包括工商银...——接口地址 添加图片水印:传入底板图及水印图,根据位置参数,接口把...——接口地址 图像裁剪:裁剪原图的部份区域——接口地址 生成缩略图:根据传入的比率将原图生成缩略图——接口地址 星座运势:每天1点、7点、17点更新。包含十二星座...——接口地址 PDF文件正文抽取:抽取PDF文件中的文字信息——接口地址 网络搜索热词排行:每2小时更新一次。根据分类查询网络最热的...——接口地址 二维码识别:根据图片的Base64信息,识别图片中的...——接口地址 二维码生成:生成二维码图片 图片存放在showapi...——接口地址 中文文本相似度检测:通过计算向量间的夹角(余弦公式),来判断...——接口地址 全国火车票查询:数据来源于12306。 包括城市列表\列...——接口地址 药品查询:药品信息——接口地址 菜谱大全:本菜谱的信息来源于网络,所以本信息仅用于...——接口地址 台风最新坐标轨迹:可查询当前存在威胁的台风列表,每个台风的...——接口地址 网页级别查询:google的pr查询——接口地址 关键词抽取:根据传入的大段文字,使用TextRank...——接口地址 全国行政区划分:最新最全的全国省、市、区县、乡镇的分级查...——接口地址 微信小程序查询:搜索查询已经上架的微信小程序。包括基本信...——接口地址 生成文章摘要:根据传入的长篇文章,系统使用智能算法抽取...——接口地址 藏头诗生成:藏头诗生成器。可输入人名生成藏头、藏尾、...——接口地址 国际原油价格查询:WTI和布伦特的油价查询——接口地址 水质查询:根据地点和时间查询水质——接口地址 条码生成:提供EAN_8、EAN_13、CODE_...——接口地址 条码识别:提供EAN_8、EAN_13、CODE_...——接口地址 全国站点换乘线路查询:提供全国站点换乘线路查询——接口地址 全国公交换乘查询:提供全国公交换乘查询——接口地址 汽车品牌查询:收录了近200个品牌/子品牌,上万辆车型...——接口地址 周公解梦:根据周公解梦全书提供相关信息——接口地址 正能量新闻:社会正能量的新闻资讯,每天更新——接口地址 全国酒店查询:该接口所返回的所有图片链接将在12小时内...——接口地址 经典语句:根据名人,查询经典名言——接口地址 商品比价:搜索商品,根据商品url搜索各大商城的历...——接口地址 姓名打分:根据姓名,返回此姓名的运势得分——接口地址 公司名测吉凶:根据公司名,返回此公司的运势得分——接口地址 车牌号测吉凶:根据车牌名,返回此车牌号的运势得分——接口地址 手机号测吉凶:根据手机号码,返回此号码的运势得分——接口地址 图书ISBN查询:通过国际图书号查询图书相关信息,目前只支...——接口地址 影讯查询:影讯查询——接口地址 手机套餐售价:全国手机流量充值,4G流量,当月有效——接口地址 紫微斗数:根据出生时间定紫薇斗数命盘,供命理研究,...——接口地址 唐诗宋词元曲等诗词查询:根据朝代Id或诗人名称查询诗人信息——接口地址 脑筋急转弯:查询常见的脑筋急转弯金句——接口地址 虚拟数字币|比特币行情:查询主流虚拟货币实时行情,例如btc(比...——接口地址 全国房产信息:搜索最新楼盘开盘信息、最新市场房价信息——接口地址 手游排行榜:手游最热排行榜及最期待榜——接口地址 网游排行榜:网游最热排行榜及最期待榜——接口地址 黄金行情:上金所黄金行情——接口地址 电商淘宝平台联想词:提供淘宝联想词查询——接口地址 中文反义词:中文反义词——接口地址 中文近义词:中文近义词——接口地址 歇后语查询:查询歇后语列表——接口地址 中国互联网络信息:中国互联网络信息——接口地址 实时票房排行:实时票房中国(包括香港)、北美、全球票房...——接口地址 爱奇艺热点趋势:爱奇艺视频指数——接口地址 空气质量指数(可在线调用):空气质量指数提供实时空气质量情况,目前支...——接口地址 IP地址查询(可在线调用):提供rest风格的IP地址查询接口,只需...——接口地址 天气预报(可在线调用):全国天气预报,预报7天天气,以及当天的生...——接口地址 人脸识别(可在线调用):检测图片(Image)中的人脸(Face...——接口地址 指纹识别:检测图片(Image)中的指纹(Fing...——接口地址 医疗科室(可在线调用):医药健康接口专用的医疗科室字典项获取——接口地址 健康菜谱(可在线调用):健康菜谱,让人们在宣泄的都市中体验在家常...——接口地址 疾病信息(可在线调用):通过名称取得疾病详情——接口地址 药品查询(可在线调用):通过药品名字直接得到药品说明书、价格、生...——接口地址 食疗大全(可在线调用):通过名称取得食品详情只要是食品都有它...——接口地址 手术项目(可在线调用):通过名称取得手术详情通过名称取得手术...——接口地址 药房药店(可在线调用):通过名称取得药店信息通过名称取得药店...——接口地址 病状信息(可在线调用):通过名称取得病状详情——接口地址 微信精选(可在线调用):微信热门精选文章,实时更新——接口地址 国内新闻(可在线调用):国内新闻数据,实时更新——接口地址 国际新闻(可在线调用):国际新闻数据,实时更新——接口地址 体育新闻(可在线调用):体育新闻数据,实时更新——接口地址 科技新闻(可在线调用):科技新闻数据,实时更新——接口地址 奇闻轶事(可在线调用):奇闻轶事数据,实时更新——接口地址 旅游新闻(可在线调用):旅游热点数据,实时更新——接口地址 新华字典(可在线调用):新华字典数据库,可查字的拼音、读音、偏旁...——接口地址 五笔字根(可在线调用):查询汉字的五笔字根——接口地址 简繁体火星文转换(可在线调用):汉字的简体、繁体、火星文转换——接口地址 成语词典(可在线调用):成语查询——接口地址 歇后语(可在线调用):歇后语查询,根据关键字搜索歇后语——接口地址 唐诗宋词(可在线调用):根据关键字搜索唐诗宋词——接口地址 历史上的今天(可在线调用):以史为镜,可以知兴替。借历史上的成败得失...——接口地址 辞海(可在线调用):查询词语解释——接口地址 手机号码归属地(可在线调用):通过手机号码查询归属地、运营商、号码类型...——接口地址 笑话大全(可在线调用):海量互联网幽默、内涵段子、趣味图片,不间...——接口地址 同义词:通过输入的词语查询对应的同义词——接口地址 全国WIFI(可在线调用):查询周边免费WIFI热点;全国免费WIF...——接口地址 NBA赛事(可在线调用):NBA赛事赛程信息,球队赛程赛事查询——接口地址 全国邮编查询(可在线调用):通过地名查询地区邮编,精确到街道——接口地址 周公解梦(可在线调用):周公解梦大全——接口地址 名人名言(可在线调用):通过关键字查询名人名言——接口地址 今日油价(可在线调用):可查询全国31个省的油价——接口地址 国际白银实时价格:国际交易市场白银实时价格(美元/盎司),...——接口地址 时事新闻检索(可在线调用):时事新闻,新闻检索等,实时更新——接口地址 号码吉凶(可在线调用):手机号码、QQ号码、车牌号等所有数字类型...——接口地址 金额小写转大写(可在线调用):人民币金额小写转大写——接口地址 电影票房(可在线调用):最新票房榜,网票票房——接口地址 全国长途汽车(可在线调用):全国长途汽车时刻表查询——接口地址 足球联赛(可在线调用):目前支持 英超,西甲,德甲,意甲,法甲,...——接口地址 影视影讯(可在线调用):影视信息播放链接检索,城市影讯检索——接口地址 标准电码查询(可在线调用):提供的标准中文电码查询程序结果——接口地址 火车时刻表(可在线调用):火车时刻表,站到站检索——接口地址 姓氏起源(可在线调用):《百家姓》是我国汉族姓氏总集,载有四百多...——接口地址 短链接生成(可在线调用):查找网提供长的网址链接缩短为新浪短网扯,...——接口地址 翻译(可在线调用):翻译API提供免费开放接口,覆盖中、英、...——接口地址 乌云漏洞(可在线调用):查看乌云最新的安全漏洞——接口地址 微信公众号查询(可在线调用):根据关键字搜索热门微信文章、微信公众号等...——接口地址 在线分词(可在线调用):基于深度学习的中文在线抽词——接口地址 MD5破解(可在线调用):md5密文:16位,32位,sha1(4...——接口地址 星座配对(可在线调用):星座配对测姻缘——接口地址 生肖配对(可在线调用):生肖配对测姻缘——接口地址 获取外网IP信息(可在线调用):取得客户端访问互联网时的外网ip及对应的...——接口地址 百度权重(可在线调用):根据网址查询百度权重——接口地址 新闻头条(可在线调用):最新新闻头条——接口地址 星座运势(可在线调用):黄道十二星座每日、每月、每年运势、不间断...——接口地址 2019.7.3更新接口 实时段子(可在线调用):实时段子,神评版本——接口地址 音乐搜索(可在线调用):根据音乐名称返回音乐详情——接口地址 小说查询(可在线调用):获取小说的详细信息——接口地址 天气查询(可在线调用):获取最近天气情况——接口地址 音悦tai搜索:音悦Tai-是以高清MV为主的娱乐视频网...——接口地址 识别身份证文字(可在线调用):AI人工智能识别身份证图像文字 URL图...——接口地址 编码解码:常见的编码和解码——接口地址 网站备案查询:网站备案信息查询接口——接口地址 身份证信息查询(可在线调用):身份证信息查询(不支持查询百岁老人)——接口地址 图片PS:在线图片加文字,返回为字符串,需要处理下——接口地址 一言:随机返回一句话——接口地址 短链接生成与还原:短链接生成与还原,包括新浪、腾讯、百度——接口地址 获取用户大致信息:获取用户信息如网络运营商等等——接口地址 三合一收款码:包括支付宝,qq,微信——接口地址 IP查询:根据ip地址获取其所在省市区——接口地址 IP经纬度查询:根据ip地址和经纬度获取其所在省市区——接口地址 2019.8.7更新接口 淘宝ip(可在线调用):来自淘宝的ip查询,可以根据ip地址查询...——接口地址 360ip(可在线调用):来自360的ip查询,可以根据ip地址查...——接口地址 地理编码(可在线调用):将详细的结构化地址转换为高德经纬度坐标。...——接口地址 逆地理编码(可在线调用):将经纬度转换为详细结构化的地址,且返回附...——接口地址 步行路径规划:可以规划100KM以内的步行通勤方案,并...——接口地址 驾车路径规划(可在线调用):规划以小客车、轿车通勤出行的方案,并且返...——接口地址 公交路径规划:规划综合各类公共(火车、公交、地铁)交通...——接口地址 骑行路径规划(可在线调用):用于规划骑行通勤方案,规划时不会考虑路况...——接口地址 距离测量:根据经纬度测量距离——接口地址 行政区域查询(可在线调用):根据用户输入的搜索条件可以帮助用户快速的...——接口地址 矩形区域交通态势:能够确定矩形交通态势情况,路况信息2分钟...——接口地址 圆形区域交通态势:能够确定圆形交通态势情况,路况信息2分钟...——接口地址 指定线路交通态势:能够确定指定线路交通情况,路况信息2分钟...——接口地址 输入提示(可在线调用):提供根据用户输入的关键词查询返回建议列表——接口地址 天气查询(可在线调用):查询目标区域当前/未来的天气情况——接口地址 IP定位(可在线调用):将IP信息转换为地理位置信息——接口地址 地点范围查询(可在线调用):根据经纬度查询查询其地址相关信息——接口地址 2019.9.9更新接口 行政区划区域检索(可在线调用):开发者可通过该功能,检索某一行政区划内(...——接口地址 圆形区域检索(可在线调用):开发者可设置圆心和半径,检索圆形区域内的...——接口地址 矩形区域检索(可在线调用):开发者可设置检索区域左下角和右上角坐标,...——接口地址 地点详情检索(可在线调用):地点详情检索针对指定POI,检索其相关的...——接口地址 地点输入提示(可在线调用):用户可通过该服务,匹配用户输入关键词的地...——接口地址 地理编码服务(可在线调用):用户可通过该功能,将结构化地址(省/市/...——接口地址 全球逆地理编码(可在线调用):用户可通过该功能,将位置坐标解析成对应的...——接口地址 公交路线规划(可在线调用):根据起点和终点检索符合条件的公共交通方案...——接口地址 骑行路线规划(可在线调用):根据起终点坐标检索符合条件的骑行路线规划...——接口地址 驾车路线规划(可在线调用):根据起终点坐标检索符合条件的驾车路线规划...——接口地址 批量算路:用户可通过该服务,根据起点和终点坐标计算...——接口地址 普通IP定位(可在线调用):用户可以通过该服务,根据IP定位来获取大...——接口地址 道路实时路况查询(可在线调用):查询具体道路的实时拥堵评价和拥堵路段、拥...——接口地址 时间偏移查询(可在线调用):查询坐标所在地与协调世界时的时间偏移信息...——接口地址 周边上车点推荐(可在线调用):用户可通过该功能检索坐标点周围的上车点。...——接口地址 非百度坐标系转换(可在线调用):用户可通过该服务,实现 非百度坐标系→百...——接口地址 快递查询(可在线调用):可根据快递单号查询大部分主流快递的快递信...——接口地址 文件转换:文件转换成指定格式,成功则返回成功转换的...——接口地址 获取文件转换内容:根据文件转换成功所获取的id,查询转换成...——接口地址 2019.10.8更新接口 全球IP地理位置(可在线调用):单个IPv4 / IPv6地址或域名...——接口地址 域名备案(可在线调用):根据域名查询域名备案状态——接口地址 十五天天气预报:采用城市ID来精准查询15天内的天气,接...——接口地址 农历查询(可在线调用):根据日期获取农历、黄历、禁忌、星期、生肖...——接口地址 ICP备案查询(可在线调用):根据域名查询ICP备案号——接口地址 三合一收款二维码:将QQ、微信、支付宝收款集合到一起,省去...——接口地址 二维码生成:将网址直接转换成二维码图片——接口地址 二维码解码(可在线调用):将二维码图片进行解码,解析处理——接口地址 短网址生成(可在线调用):将长网址进行缩短,支持百度、新浪、腾讯短...——接口地址 短网址还原(可在线调用):将缩短的短网址进行还原,支持常见的短网址——接口地址 网易云音乐随机歌曲(可在线调用):网易云音乐,随机歌曲输出——接口地址 获取访客相关信息(可在线调用):根据访客IP地址,操作系统,浏览器,访问...——接口地址 随机头像输出(可在线调用):随机头像输出——接口地址 2019.11.5更新接口 文章短篇:根据日期获取一篇文章,有网上的,也有名家...——接口地址 必应故事:随机获取来自必应的故事,可根据pid获取...——接口地址 每日一言:获取来自一言、有道或金山词霸的每日一言内...——接口地址 二维码解析:还原二维码的原始URL,支持支付宝,微信...——接口地址 生成海报:根据提交的内容格式化生成可分享的精美海报——接口地址 历史上的今天:历史上的今天——接口地址 生成二维码:可根据传入的内容,生成对应的二维码,还可...——接口地址 IP地址详情信息:IP地址详情信息查询——接口地址 XLS生成:生成XLS——接口地址 土味情话:和妹妹说的情话,返回一句随机的内容——接口地址 随机笑话(可在线调用):随机的笑话——接口地址 2019.12.4更新接口 经纬度信息(可在线调用):获取当前经纬度信息——接口地址 历史上的今天(可在线调用):历史上的今天——接口地址 Bing 壁纸获取(可在线调用):获取最近的Bing 壁纸——接口地址 天气查询(可在线调用):获取今天和未来三天的天气情况,来源于高德——接口地址 天气查询(可在线调用):根据城市名,获取今天和未来三天的天气情况...——接口地址 天气查询(可在线调用):根据城市id,获取未来15天的天气情况,...——接口地址 手机归属地查询(可在线调用):根据手机号码查询手机号的归属地信息——接口地址 手机归属地查询:根据手机号码查询手机号的归属地信息,来源...——接口地址 IP域名归属地查询(可在线调用):查询IP或者域名归属地——接口地址 身份证查询(可在线调用):根据身份证获取该身份证号码的籍贯,出生年...——接口地址 淘宝关键字(可在线调用):淘宝搜索关键字——接口地址 百度关键字:百度搜索关键字——接口地址 Bing关键字(可在线调用):Bing搜索关键字——接口地址 获取用户设备信息(可在线调用):通过 user-agent 分析用户设备...——接口地址 百度音乐搜索(可在线调用):根据关键字获取音乐的相关信息——接口地址 2020.1.9更新接口 手机号码归属地:免费手机号码归属地API查询接口,来源于...——接口地址 手机号码归属地:免费手机号码归属地API查询接口,来源于...——接口地址 图灵机器人(可在线调用):基于图灵机器人平台语义理解、深度学习等核...——接口地址 实况天气(可在线调用):根据城市信息获取其实况天气——接口地址 3天天气预报(可在线调用):根据城市信息获取其3天天气预报——接口地址 生活指数(可在线调用):根据城市信息获取其生活指数——接口地址 空气质量(可在线调用):通过空气质量数据接口,可获取空气质量相关...——接口地址 译云机器翻译:译云机器翻译开放API是中译语通面向广大...——接口地址 2020.2.5更新接口 汇率与货币兑换率(可在线调用):汇率API是针对欧洲中央银行发布的当前汇...——接口地址 随机活动(可在线调用):寻找随机活动来对抗无聊,前提是你看明白英...——接口地址 地点搜索(可在线调用):提供三类范围条件的搜索功能: 指定城市...——接口地址 关键词输入提示(可在线调用):用于获取输入关键字的补完与提示,帮助用户...——接口地址 坐标位置描述(可在线调用):本接口提供由坐标到坐标所在位置的文字描述...——接口地址 地址转坐标(可在线调用):本接口提供由地址描述到所述位置坐标的转换...——接口地址 行政区划:本接口提供中国标准行政区划数据,可用于生...——接口地址 距离计算(一对多)(可在线调用):本服务用于单起点到多终点,或多起点到单终...——接口地址 距离矩阵(多对多)(可在线调用):距离矩阵(DistanceMatrix)...——接口地址 坐标转换(可在线调用):实现从其它地图供应商坐标系或标准GPS坐...——接口地址 IP定位(可在线调用):通过终端设备IP地址获取其当前所在地理位...——接口地址 努力添加中......https://github.com/fangzesheng/free-api
2020-03-10 - [填坑手册]小程序新版订阅消息+云开发实战与跳坑
[图片] 老版本的订阅消息在2020年1月10日就下线了,相信不少人在接入新版本订阅系统的时候,或多或少会遇到一些问题,这里智库君跟大家介绍下新版订阅的机制和不需要node/后端的情况下 独立完成功能开发。 一、新版订阅的机制 其实开发过程不难,但是要理清楚它里面的机制,智库君还是花了一些时间的,也踩了不少坑 先来看下官方介绍: [图片] 可以设置多个订阅选项 感叹号里面可以看到详情 有个默认不被选中的“总是”选项 这些就是新不同的地方,智库君在开发的时候也有很多疑问,点了“总是”再点“取消”按钮会怎样?部分选择订阅会怎样?下面为大家一一梳理 (1)部分选中 [图片] 比如现在有三个选项 A,B,C,用户**“部分选中”**返回的情况: [图片] 这里用真机调试可以看到,有个返回值状态为“reject”。 如果我们反复几点点击同一个订阅后,这些值是如何计算的呢? 举例: [图片] 从这里看出,微信系统会自动记录用户点击的次数,并且做累加记录,如果用户只允许2次发送,而开发者发送了3次,最后一次将会被拒绝。 (2)点击“总是保持以上选择,不再询问”的情况 [图片] 当用户点击“总是”之后,同一个类型的订阅将不再弹出,那如果有多个订阅选项呢? 举例 订阅AAA 三个订阅模板为 X Y Z 订阅BBB 二个订阅模板为 Y W 这时候如果“订阅AAA”按钮选择了“总是”,那么再点击“订阅BBB”按钮,将只会弹出一个选项“W”,不会有 “Y” 的模板,因为在之前 “订阅AAA” 按钮中已经包含了。 [代码]wx.requestSubscribeMessage({ tmplIds: ["MECDDOdhbC3SrQmMY5XrfqiIGbMTzpEN8Z7ScXJfcd0", "iSb2NIlNnnO60wlI-8Wx5Pe82jR7TRdwjotSXtM1-ww"], success(res) { console.log(res); } }) [代码] 显示内容仅一个选项: [图片] 这里需要注意,“总是”选项是全局有效,不区分页面,选中“总是”的 W,X,Y,Z的模板,在全局任意页面中再次调用,再次调用将不再会显示! [图片] 返回值无提示用户是否选中“总是”。 (3)用户点击“总是”后,获取状态 [图片] [代码]wx.getSetting({ withSubscriptions: true, success(res) { console.log(res.authSetting) // res.authSetting = { // "scope.userInfo": true, // "scope.subscribeMessage": true // } console.log(res.subscriptionsSetting) // res.subscriptionsSetting = { // SYS_MSG_TYPE_INTERACTIVE: 'accept', // SYS_MSG_TYPE_RANK: 'accept', // zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE: 'reject', // ke_OZC_66gZxALLcsuI7ilCJSP2OJ2vWo2ooUPpkWrw: 'ban', // } } }); [代码] [图片] 这里可以调用wx.getSetting方法,但是需要注意:如果用户第一次选“总是”后点击“取消”按钮或者订阅模板全部是未选中/reject的,那将获取不到状态(这里可能是BUG,期待官方未来修复)。 (4)用户点击“总是”后,让用户手动修改 前面说到用户点击“总是”后,系统将不再弹窗,但是我们可以通过**“wx.openSetting”**引导用户手动修改。 [代码]wx.openSetting({ success(res) { console.log(res.authSetting) // res.authSetting = { // "scope.userInfo": true, // "scope.userLocation": true // } } }) [代码] [图片] [图片] 当然用户自己也可以修改 [图片] 总结 【重点】选择“总是”,很多人认为就可无限发送订阅消息,这个是错误的,勾选和不勾选唯一的区别就是每次触发订阅的时候会不会弹授权窗口!!! 用户点击次数系统会自动累加,直接影响后台发送通知的次数。 用户选择“总是”后,小程序界面不再弹窗,但仍然有回调/callback。 任意订阅模板在用户选中“总是”(包括接受/拒绝2个状态)后,全局有效,就算其他订阅包含“此模板”也不再显示/弹出 当用户选择“总是”中“accept/选中/接受”的状态后,可以在wx.getSetting查询到用户是否选择“总是”。 当用户选择“总是”中“reject/未选中/拒绝”的状态后,返回值“无感知”(这里可能是BUG) 二、功能开发 使用微信自带的云开发,可以在没有node/后端开发支持下,完成整个订阅流程的开发。 (1)微信后台设置订阅模板和获取模板ID 1、打开小程序后台,找到订阅消息设置 [图片] 2、在公共模板库找模板或者自己申请新模板,建议能用现成模板用现成的,因为申请周期可能较长,且容易被拒 [图片] 3、选好模板后,点击详情 [图片] 4、查看模板内容和发送DATA的结构 [图片] 5、复制模板ID (2)配置云函数 [图片] [图片] 1、新建getOpenId云函数,用于获取用户的openID [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } } [代码] 2、新建订阅推送通知云函数 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() //订阅推送通知 exports.main = async (event, context) => { try { const result = await cloud.openapi.subscribeMessage.send({ touser: event.openid, //接收用户的openId page: 'pages/my/index', //订阅通知 需要跳转的页面 data: { //设置通知的内容 thing1: { value: '小程序订阅填坑' }, thing2: { value: '智库方程式' }, thing3: { value: '一起学习,一起进步' } }, templateId: '5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac' //模板id }) console.log(result) return result } catch (err) { console.log(err) return err } } [代码] 写完云函数记得右键部署下!!! (3)小程序代码部分 [代码]<!------------html -------------> <button bindtap="getOpenId" type='primary'>获取openId</button> <view class="subBtn" catch:tap="sub">订阅AAA</view> <view class="subBtn" catch:tap="send">订阅推送测试</view> <view class="subBtn" catch:tap="setting">设置“总是”后,跳转修改</view> [代码] [代码]//JS 部分 //获取用户的openid getOpenId() { wx.cloud.callFunction({ name: "getOpenId" }).then(res => { let openid = res.result.openid console.log("获取openid成功", openid) }).catch(res => { console.log("获取openid失败", res) }) }, //发送模板消息给指定的openId用户 send(openid){ wx.cloud.callFunction({ name: "sendSub", data: { openid: openid } }).then(res => { console.log("发送通知成功", res) }).catch(res => { console.log("发送通知失败", res) }); }, //消息订阅 sub: function () { wx.requestSubscribeMessage({ tmplIds: ["5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac"], success(res) { console.log("订阅授权成功:"+res); }, fail(res){ console.log("订阅授权失败:" + res); } }) }, //帮助用户跳转修改订阅状态 setting:function(){ wx.openSetting({ success(res) { console.log(res.authSetting) // res.authSetting = { // "scope.userInfo": true, // "scope.userLocation": true // } } }) }, [代码] (4)测试流程 点击发送通知后,获得这样的效果: [图片] [图片] 获得对应返回值: [图片] 当errCode为0时,即发送通知成功。 当errCode为43101,说明用户只授权了一次,但是你发送了2次,超过用户授权次数。 [图片] 三、进阶与思考 1、当你有多个订阅模板同时需要用户选择时,你可以通过以下代码记录,用户哪些选了,哪些没选。 [代码]wx.requestSubscribeMessage({ tmplIds: ["5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac", "OBB_Z10eh_Inm9p8EU6Ml_NS_mijXgTz3T07cxgKvX0","5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac"], success(res) { //console.log(res); if (res.errMsg == "requestSubscribeMessage:ok") { let acceptArray = []; //用户授权模板列表 for (let i = 0; i < tmplIds.length; i++) { const element = tmplIds[i]; if (res[element] == "accept") { acceptArray.push(element); } }; console.log(acceptArray); if (acceptArray.length > 0) { //执行下一步函数 } } } }) [代码] 2、一个关于是否需要记录用户对某个“订阅模板授权的次数”,以控制后台“发送的次数”,智库君在实战中认为,其实没有必要,顶多就是你发送返回一个错误码,微信之所有记录用户授权次数,也是为了保护用户不被骚扰。 3、你只需要记录用户点击了哪些需要授权的模板就行,为了是用户点击订阅后,改变按钮的状态,避免订阅按钮反复弹窗的问题,同时当检测到用户点错“总是”按钮后,可以自动跳转到“设置”界面。 4、这次智库君主要给大家简单介绍了下订阅全流程。后面大家可以根据自己的需要,添加和改进这些代码。比如: 配置云函数中的node函数,实现定时发送 配置云函数中的数据库,实现内容的自定义发送 最后,希望这篇文章能帮助到大家,一起学习,一起进步! (官方文档地址:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html) 往期回顾: [打怪升级]小程序自定义头部导航栏“完美”解决方案 [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二)
2021-09-13 - 小程序悬浮按钮,悬浮导航球
[图片] 一个开源的悬浮按钮组件,小程序原生支持。 一直很喜欢华为的导航按钮,能够完美适合大屏手机,自由停放位置,不论是左手习惯还是右手习惯,都很方便(可能我手比较小,左右上角够不着)。 支持功能 支持自由拖动,停放 支持自定义事件(单击,双击,长按) 支持自定义导航球中间的文字/图片 开发难点 使用wxs 悬浮球的开发思路比较简单,一个view,样式[代码]position:fixed[代码],支持拖动。在web开发中,我们能够比较容易实现这样的功能。要想在小程序中实现高性能的交互动画(touch类),一定要了解如何使用页面的[代码]wxs[代码]这个残疾JS来操作对象(调试很麻烦,js极度残疾) [代码]<wxs module="tool"> function tStart(e, ins){} function tMove(e, ins){ e.instance.setStyle('transform: translate3d(...)') // e.instance指向当前操作对象 // setStyle 设置该对象的style样式 } function tEnd(e, ins){} module.exports = { tStart: tStart, tMove: tMove, tEnd: tEnd } </wxs> <view catch:touchstart="{{tool.tStart}}" catch:touchmove="{{tool.tMove}}" ... /> [代码] 这里使用catch,而不是使用bind来绑定事件,事件指向[代码]wxs[代码]的方法。考虑到悬浮导航球是作为工具在其他场景中使用,为了不会污染touch事件,或者导致页面不必要的滚动。 位移距离 手机宽高不一致,即x轴的运动距离小于y轴运动距离(单位时间),假定手机宽高比为1:2,x轴运动1px,y轴则运动了2px,我们可以设置一定的系数,使得拖动效果符合预期。 监听事件 最终的事件响应一定是在page页面(或者组件内部)实现事件监听,wxs有一套事件调用机制 [代码]function tStart(e, ins){ ins.callMethod('onTouchStart', e) // 调用当前组件/页面在逻辑层(App Service)定义的函数。funcName表示函数名称,args表示函数的参数 } [代码] wxs相关文档 GITHUB开源 DEMO及文档关注小程序 [图片]
2020-03-06 - 小程序10行代码实现微信头像挂红旗,国庆节个性化头像
最近朋友圈里经常有看到这样的头像 [图片] 既然这么火,大家要图又这么难,作为程序员的自己当然要自己动手实现一个。 老规矩,先看效果图 [图片] 仔细研究了下,发现实现起来并不难,核心代码只有下面10行。 [代码] wx.canvasToTempFilePath({ x: 0, y: 0, width: num, height: num, destWidth: num, destHeight: num, canvasId: 'shareImg', success: function(res) { that.setData({ prurl: res.tempFilePath }) wx.hideLoading() }, fail: function(res) { wx.hideLoading() } }) [代码] 一,首先要创建一个小程序 至于如何创建小程序,我这里就不在细讲了,我也有写过创建小程序的文章,也有路过相关的学习视频,去翻下我历史文章找找就行。 二,创建好小程序后,我们就开始来布局 布局很简单,只有下面几行代码。 [代码]<!-- 画布大小按需定制 这里我按照背景图的尺寸定的 --> <canvas canvas-id="shareImg"></canvas> <!-- 预览区域 --> <view class='preview'> <image src='{{prurl}}' mode='aspectFit'></image> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="1">生成头像1</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="2">生成头像2</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="3">生成头像3</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="4">生成头像4</button> <button type='primary' bindtap='save'>保存分享图</button> </view> [代码] 实现效果图如下 [图片] 三,使用canvas来画图 其实我们实现微信头像挂红旗,原理很简单,就是把头像放在下面,然后把有红旗的相框盖在头像上面 [图片] 下面就直接把核心代码贴给大家 [代码]let promise1 = new Promise(function(resolve, reject) { wx.getImageInfo({ src: "../../images/xiaoshitou.jpg", success: function(res) { console.log("promise1", res) resolve(res); } }) }); let promise2 = new Promise(function(resolve, reject) { wx.getImageInfo({ src: `../../images/head${index}.png`, success: function(res) { console.log(res) resolve(res); } }) }); Promise.all([ promise1, promise2 ]).then(res => { console.log("Promise.all", res) //主要就是计算好各个图文的位置 let num = 1125; ctx.drawImage('../../'+res[0].path, 0, 0, num, num) ctx.drawImage('../../' + res[1].path, 0, 0, num, num) ctx.stroke() ctx.draw(false, () => { wx.canvasToTempFilePath({ x: 0, y: 0, width: num, height: num, destWidth: num, destHeight: num, canvasId: 'shareImg', success: function(res) { that.setData({ prurl: res.tempFilePath }) wx.hideLoading() }, fail: function(res) { wx.hideLoading() } }) }) }) [代码] 来看下画出来的效果图 [图片] 四,头像加红旗画好以后,我们就要想办法把图片保存到本地了 [图片] 保存图片的代码也很简单。 [代码]save: function() { var that = this wx.saveImageToPhotosAlbum({ filePath: that.data.prurl, success(res) { wx.showModal({ content: '图片已保存到相册,赶紧晒一下吧~', showCancel: false, confirmText: '好哒', confirmColor: '#72B9C3', success: function(res) { if (res.confirm) { console.log('用户点击确定'); } } }) } }) } [代码] 来看下保存后的效果图 [图片] 到这里,我的微信头像就成功的加上了小红旗了。 [图片] 源码我也已经给大家准备好了,有需要的同学在文末留言即可。 [图片] 后面我准备录制一门视频课程出来,来详细教大家实现这个功能,敬请关注。
2019-09-26 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: 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 - createAnimation菜单下拉动画 [即抄即用,拎包入住]
短评createAnimation 垃圾:1.为什么不能是%??还要计算下拉ctr的高度。 2.rpx不彻底,和px要慢慢换算,开发者你写出来自己有没有用? 优秀:比@keyframes能省出50%的代码。 注:上下拉都有动画。 [图片] <view bindtap="mClick" class="mClick">下拉</view><view class="ctr" animation='{{ani}}'> <view class="item">内容1</view> <view class="item">内容2</view> <view class="item">内容3</view></view> .mClick{ position: fixed; width: 100vw; height: 50px; background-color: yellow; z-index: 1; } .ctr{ position: fixed; width: 100vw; top: -100px; } .item{ width: 100vw; height: 50px; background-color: red; } Page({ data: { dur: 300, ty: 150 }, onLoad() { var dur = this.dur this.ani = wx.createAnimation({ duration: dur, timingFunction: 'ease' }) this.setData({ ani: this.ani.export() }) }, mClick() { var ani = this.ani var ty = this.data.ty ani.translateY(ty).step() var altTy = (ty == 150) ? 0 : 150 this.setData({ dur: 300, ty: altTy, ani: ani.export() }) } }) end
2021-12-06 - 小程序卡券开发记录
实现功能:发送指定Code优惠券给用户 实现步骤: 1、创建自定义Code卡券 接口:https://api.weixin.qq.com/card/create?access_token={AccessToken} 注意事项:设置use_custom_code=true,不能设置get_custom_code_mode="GET_CUSTOM_CODE_MODE_DEPOSIT" 2、设置卡券Code 接口:http://api.weixin.qq.com/card/code/deposit?access_token={AccessToken} 同时更新卡券库存,库存数=succ_code大小 接口:https://api.weixin.qq.com/card/modifystock?access_token={AccessToken} 最好是需要增加多少库存就是设置多少code,两个接口在一个方法中调用 3、添加卡券 wx.addCard({ cardList: [ { cardId: '', cardExt: '{"code": "", "openid": "", "timestamp": "", "signature":""}' }, { cardId: '', cardExt: '{"code": "", "openid": "", "timestamp": "", "signature":""}' } ], success (res) { console.log(res.cardList) // 卡券添加结果 } }) 4、查看卡券 wx.openCard({ cardList: [{ cardId: '', code: '' }, { cardId: '', code: '' }], success (res) { } })
2020-02-18 - 多张图片上传(源码分享+实现分析)
本篇文章以小程序中的代表【微信小程序】为例,分享一下在微信小程序中实现多图上传的源码实现。 代码片段(可导入微信WEB开发者工具体验):https://developers.weixin.qq.com/s/DHrt69mk7af3 两种不同实现方法的优缺点,请查看我的 博客原创文章,在文章中有详细的说明 小程序 多张图片上传 文章地址:https://blog.csdn.net/u013350495/article/details/104326088。 源码: const app = getApp() Page({ data: { // 已选择上传的本地图片地址 urlArr:['helang.jpg','1846492969.jpg','web.jpg'] }, onLoad: function () { }, // 多图上传-回调式 uploadCallback(){ let index = 0; // 当前位置,标识已上传到第几张图片 let newUrls = []; // 上传成功后的图片地址数组 // 图片上传方法 let upload = ()=>{ let nowUrl = this.data.urlArr[index]; //当前待上传的图片地址 wx.showLoading({ title: '正在上传', }); /* 无图片上传接口,收setTimeout 模拟延迟状态 项目中替换为 wx.uploadFile 即可 */ // 假设每 1000ms 上传一张图片 setTimeout(()=>{ // 此处为已上传成功后的回调函数内容 let resUrl = `服务器返回上传后的地址 ${nowUrl}`; //假设这是上传成功后返回的地址 newUrls.push(resUrl); // 将上传后的地址添加到成功数组中 // 判断图片是否已经全部上传完成 if (index >= (this.data.urlArr.length-1)){ send(); }else{ //未全部上传完时标识位置+1并再次调用上传方法 index++; upload(); } },1000); } // 发送方法,用作图片上传完后,得到图片地址提交给其它接口或其它操作 let send = () => { // 关闭加载提示 wx.hideLoading(); wx.showToast({ title: '上传成功', icon:'success' }) // 输出已经上传完的图片地址,请查看控制台结果 console.log(newUrls); } // 调用上传方法 upload(); }, // 多图上传-Promise uploadPromise(){ /* Promise 对象数组 */ let p_arr = []; /* 新建 Promise 方法,nowUrl参数为当前上传的图片地址 */ let new_p = (nowUrl) => { return new Promise((resolve, reject) => { /* 无图片上传接口,收setTimeout 模拟延迟状态 项目中替换为 wx.uploadFile 即可 */ // 假设每 1000ms 上传一张图片 setTimeout(()=>{ // 此处为已上传成功后的回调函数内容 let resUrl = `服务器返回上传后的地址 ${nowUrl}`; //假设这是上传成功后返回的地址 resolve(resUrl); },1000); }) } // 遍历数据,创建相应的 Promise 数组数据 this.data.urlArr.forEach((item, index) => { let nowUrl = this.data.urlArr[index]; //当前待上传的图片地址 p_arr.push(new_p(nowUrl)); }); wx.showLoading({ title: '正在上传', }); /* 所有图片上传完成后调用该方法 */ Promise.all(p_arr).then((res) => { // 关闭加载提示 wx.hideLoading(); wx.showToast({ title: '上传成功', icon: 'success' }) // 输出已经上传完的图片地址,请查看控制台结果 console.log(res); }); } })
2020-02-15 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 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 - 分享一个小程序支付密码框
效果如下: [图片] ## Install ```bash npm install weapp-input-frame ``` ## Usage *.json ```json "usingComponents": { "input-frame": "miniprogram_npm/weapp-input-frame/index" } ``` *.wxml ``` <input-frame /> ``` ## API - value 输入框默认值 - plaintext 是否明文显示, 默认 false - focus 是否获取焦点, 默认 false - bind:change 输入发生变化触发 - bind:finished 输入完成时触发 ```html <input-frame value="" plaintext focus bind:change bind:finished /> ``` # Methods - getValue 获取输入框值 wxml ```html <input-frame id="input-frame /> ``` js ```js Page({ onLoad() { const el = this.selectComponent('#input-frame'); el.getValue(); } }) ``` 组件地址:https://github.com/xjh22222228/weapp-input-frame
2020-02-02 - 页面骨架图加载
什么是骨架图加载? 看图 [图片] 具体实现 1. 引入vant-weapp有赞小程序UI框架提供的van-skeleton组件 2. 代码 [代码]// 请求接口时设置indexGetIng为true,就会显示这个block,就达到了在请求数据的时候显示列表骨架图 <block wx:if="{{indexGetIng||indexList.length}}"> <view class="list"> <list-item type="index" list="{{indexGetIng?5:indexList}}"></list-item> </view> <load-more hasMore="{{indexHasMore}}"></load-more> </block> // 接口请求完毕了,设置indexGetIng为false,indexList.length=0,就显示这个block,提示无内容 <block wx:else> <null-page> <view class="null-tip">暂无相关内容哦~</view> </null-page> </block> [代码] 上面代码运行如下: 1)、加载中显示骨架图: [图片] 2)、加载完,如果请求到数据了: [图片] 3)、加载完,如果没有请求到数据: [图片] 3. 接下来说说list-item子组件中是如何显示骨架图的: 从上面的代码看出,在请求数据的时候给子组件的list-item传了个5,这个5就是在子组件需要渲染的骨架图列表数量 [代码] // 通过判断父组件传过来的list是不是数组,来判断是否要显示列表数据 <block wx:if="{{util.isArray(list)}}"> <view>列表数据展示</view> </block> // 如果父组件传过来的不是数组,而是5,即父组件在请求列表数据,这里就循环显示5个骨架图列表,具体属性参照van-skeleton文档 <block wx:else> <!--这里的list 为渲染的骨架图数量--> <view class="item" wx:for="{{list}}"> <van-skeleton title avatar avatar-size="85px" avatar-shape="square" row="3" row-width="{{['100%','100%','80%']}}" row-height="{{['100px','16px','16px']}}" loading="{{true}}" > </van-skeleton> </view> </block> [代码] 通过查阅van-skeleton文档,发现并没有 row-height属性,这是我自己改的van-skeleton组件的源码,(因为这个组件只能设置骨架灰图的宽,不能设置高,默认高度为16px),具体改动如下: [代码]// skeleton.js props: { rowWidth: { type: null, value: '100%', observer(val) { this.setData({ isArray: val instanceof Array }); } }, /*TODO 新增*/ rowHeight: { type: null, value: '16px', observer(val) { this.setData({ isArrayHeight: val instanceof Array }); } }, }, // skeleton.wxml // 在这个view中的style属性中增加 height:' + (isArrayHeight ? rowHeight[index] : rowHeight) <view wx:for="row" wx:key="index" wx:for-index="index" class="{{ utils.bem('skeleton__row') }}" style="{{ 'width:' + (isArray ? rowWidth[index] : rowWidth) + ';height:' + (isArrayHeight ? rowHeight[index] : rowHeight) }}" /> [代码] 这样就可以设置骨架灰图的高度了,效果如下 [图片] 第一次写技术分享文章,这是小白我在工作中遇到的问题的解决方案,抛砖引玉,麻烦大佬们多多提建议!
2020-01-07 - 教大家用20行js代码,开发好小程序订阅消息
微信小程序官方决定在2020-1-10全面线下小程序模板消息,要去替换为订阅消息。那对开发者而言,又要一个一个地方去修改代码兼容.... 所以我替大家写了段代码,来快速解决问题。复制下面这段代码到app.js文件最上面即可解决问题。代码的主要功能是在每一个tap类型的点击事件中触发订阅弹窗,这样用户点几次界面,你就可以发几次消息。这也是让发送次数最大化,不可能比这个次数还多了。 预期结果是:用户点几次弹窗,就会注意到有一个不再提醒按钮,一旦选了它,那你就可以随便发订阅消息了! // 记录原Page方法 const originPage = Page; // 重写Page方法 Page = (page) => { Object.keys(page).forEach(function(key){ if(key !== 'data'){ let originMethod = page[key]; page[key] = function () { let e = arguments[0]; //给所有的点击事件增加订阅消息弹窗 if(!!e && !!e.type && e.type === 'tap'){ wx.requestSubscribeMessage({ tmplIds: ['3E66jPXafsnikZoQR5uk0OUzIUVASZE5scyAu5YCHPI'], ////////这里替换为自己的模板ID///// success (res) { // console.log(res) }, fail (res) { // console.log('订阅消息失败',res) } }) } return originMethod.call(this,...arguments) } } }); return originPage(page); };
2020-01-09 - 简单实现一个多级不同深度层级选择器。可用于省市区选择
项目需要构建省市区级联器,看遍了千篇一路的分栏多级选择器,公司设计厌了。 找了找相关的组件库有类似的。于是就手撸了一个。简单测了一下,不能保证没有bug,暂未商用,会持续优化。需要的朋友可以关注一下,欢迎对代码进行审查修改。 源码也比较简单,感兴趣的看过来 github 理论上支持多级不同深度混合的联动选择 ##效果 此处以省市区三级联动演示 [图片] ##说明 [代码]/demos/city[代码] 使用全国省市区信息,演示省市区三级联动选择器 中国行政区数据来源 : https://github.com/dwqs/area-data [代码]/components/cascader[代码] 多级联动组件 使用 引入组件 拷贝/components/cascader 到项目中 在需要使用的页面内引入 [代码] "usingComponents": { "cascader": "/components/cascader/cascader" } [代码] 使用组件 [代码] <cascader></cascader> [代码] api props 属性 类型 说明 height Number 级联器高度,最小值300,默认500,单位rpx placeholder String 占位提示符 value [ Number, String, Array ] 初始化选中值, 单级级联时可传递 Number、String 。 多级级联请传入 Array options Array 待选项,格式如下 options格式 属性 说明 label 显示在选择器上的文本 value 对应的值,唯一标识 children 子级元素,集合数组。 格式相同 [代码][{ "label":"江苏省", "value":"10001", "children":[{ "label":"南京市", "value":"100011", "children": ... } ... ] ... [代码] Events 事件名 说明 回调参数 confirm 选择 [代码]Event.detail : Array[代码] cancel 取消 – error 组件内部错误 [代码]Event.detail : String[代码]
2020-01-09 - 一眼告诉你什么是订阅消息了,看完就懂订阅消息。
消息通知有两种: 一、A的动作后,发消息给A自己,这种容易解决,不多说明; 二、A动作后,发消息给B(比如管理员、店家、楼主),如何保证B收到消息?这种是本方案要解决的问题。 一张图片一眼告诉你什么是订阅消息,产品经理的设计UI居然让人一眼就知道订阅消息是什么玩意。 [图片] 用户 B (管理员、商家、组长、楼主)在知道订阅数不足后,打开小程序来续订阅数,否则没法收到订阅消息。 [图片] 补充一: 关于勾选按钮,请注意话述是:“总是保持以上选择,不再询问”,而不是:“总是同意接收订阅消息”,不要幻想就成了永久性订阅消息; 相当于你打电话订外卖,对店家说“老样子”,店家只会马上送一次外卖,而不是会以后每天自动给你送外卖了。 勾选和不勾选的区别是什么呢? 区别仅仅是:不勾选时,必须点击订阅10次,弹窗10次;勾选后,仍然必须点击订阅10次,但是不弹窗。无论如何“订阅”这个点击n次的动作少不了。 补充二: 一旦勾选后,就不可逆了,没有任何办法恢复或取消勾选了,除非你小程序MP后台换一次消息模板号(删除模板,重新添加一次)。 补充三: 关于如何保存订阅数。 保存在数据库中,笔者用的是云开发,数据库表user结构如下: { _id:'openid1', nickName:'老张', msg:{ "tempId1":5, "tempId2":7, } } 补充四: 关于如何获取订阅数。两种方式: 一、wx.requestSubscribeMessage的回调success里获取; 二、消息推送机制获取;https://developers.weixin.qq.com/miniprogram/dev/framework/server-ability/message-push.html
2022-09-21 - 小程序生成海报经验分享
场景 由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有二维码的图片,然后引导用户下载图片到本地后再分享到朋友圈 遇到的难点 本来打算用canvas自己去画,但是在操作的过程中遇到两个问题 1、文本太长,需要换行的问题 技术方案 https://github.com/Kujiale-Mobile/Painter 演示 [图片] 以上文章参考并摘录自下面相关链接 小程序海报生成工具,可视化编辑直接生成代码使用,你的海报你自己做主 https://developers.weixin.qq.com/community/develop/article/doc/000e222d9bcc305c5739c718d56813 Painter 一款轻量级的小程序海报生成组件 https://developers.weixin.qq.com/community/develop/doc/000048447844f80b9107d64ab51006 利用云开发实现个性海报制作 https://developers.weixin.qq.com/community/develop/article/doc/000ea8817d0b504e08b921b275b413
2020-01-17 - 利用云开发实现个性海报制作
#0 小程序海报对用户拉新、留存、回流都有着非常重要的作用。 个性海报的制作也就成了小程序开发者的必要功课。 结合 BBC English Podcast 小程序,今天我分享一下怎么利用云开发完成个性化海报的制作。 #1 个性海报可以放在云函数里用图片处理 npm 包来制作,也可以放在小程序端制作。 为了节省云函数资源,我们放在小程序端来制作。 在开始之前,我们理一下个性化海报生成的流程: 步骤一、确定海报内容 步骤二、确定海报样式 步骤三、获取小程序页面的小程序码或者二维码 步骤四、合成海报 步骤五、让用户下载保存 步骤六、上传生成的海报并添加记录到云数据库给下一个用户分享里直接下载使用 这里我手写了一个简单的流程图。 [图片] #2 海报内容和海报样式都是个性化比较强的,就不多作介绍了。 从生成小程序或者二维码开始。 开始之前,先看一下我已经实现的效果。 [图片] [图片] [图片] 在用户点击生成时,需要先判断之前是否有用户已经生成过。 如果已生成,则直接展示生成好的图片。 [图片] 如果没有已生成的海报时,需要生成海报,在生成之前也需要先判断是否已有生成好的小程序码, 没有则先生成小程序码 [图片] 获取小程序码需要在云函数里面操作,这里需要注意给云函数配置调用生成小程序码的权限 [图片] 获取到小程序码的 fileID 之后需要转换成 url。 给海报生成函数调用。 [图片] 生成海报我们用开源库https://github.com/Kujiale-Mobile/Painter 海报样式也可以用这个在线工具拖拽生成https://lingxiaoyi.github.io/painter-custom-poster/ 通过上面的工具按自己需求生成对应的代码后, 我们可以精简一些空属性和把动态内容改成对应的参数传递进去, [图片] 模版定义好后就可以生成海报。 [图片] 生成海报之后,我们需要上传到云存储并添加记录到对应的数据库里面方便下一个用户分享时直接下载使用 [图片] 最后,就是给用户提供保存到相册的功能了。 [图片] #3 首发在 ikeeplearn 公众号 到这里,利用云开发实现个性海报就结束了。 如果你有其他疑问或者需求欢迎加我微信一起交流 [图片]
2020-01-01 - 多形态小程序日历组件,轻松搞定项目需求
小程序日历组件 小程序日历组件,支持多种模式,简单易用好上手。 4种日历模式 3种日期选择方式 支持自定义节假日 支持自定义日期内容 懒加载保证渲染性能 支持农历 支持根据指定日期自动生成 支持跨无数据月份 [图片] [图片] [图片] [图片] [图片] 日历组件基础配置 wxml模板 [代码]<ui-calendar dataSource="{{config}}" /> [代码] 配置日历组件 [代码]Pager({ data: { source: { $$id: 'calendar', mode: 1, // 纵向日历 type: 'range', // 区域选择 tap: 'onTap', // page响应事件 total: 365, // 指定日历总天数 data: [], // 按给定日期计算total值,自动构建日历 rangeCount: 28, // 区选区间28天 rangeMode: 2, // 区选模式 rangeTip: ['入住', '离店'], // 区选提示 festival: true, // 开启节假日显示 alignMonth: false, // 月份对齐,swiper切换时 lunar: false, // 是否显示农历 date: [], // 指定日期显示的内容 value: ['2019-12-24', '2020-01-05'], // 默认值 toolbox: { monthHeader: true, // 是否显示月头 discontinue: false, // 自动构建时,是否省略无数据的月份 }, methods: { // 响应 tap事件 onTap(e, param, inst) { // param.date 选中的当前日期 // 当区选模式时 // param.range === 'start' 区选第一天 // param.range === 'end' 区选最后一天 } } } } }) [代码] github地址:https://github.com/webkixi/aotoo-xquery 小程序demo演示 [图片]
2020-06-30 - 小程序实现全屏幕高斯模糊背景图
我们在做小程序开发过程中,有时候会遇到这样的需求,用一张图片做全屏幕背景图。 并且实现毛玻璃效果(高斯模糊),今天就来带大家一步步的实现这个效果 老规矩,先看效果图 1,用网络图片实现 [图片] 2,用本地图片实现 [图片] 通过上面两张图可以看出来,我们既可以用网络图片来实现高斯模糊,有可以用本地图片来实现。 一,先来用本地图片做全屏背景 1,先在wxml文件里引入本地图片 [图片] 2,然后设置wxss样式 通过下图几段样式代码,就可以轻松实现全屏背景 [图片] 这个图片大家应该熟悉吧,这是石头哥的头像。原本是哥正方形,我们要想实现全屏背景,就要用到下面这几行代码了。 [代码].gaoshi-bendi { /* 这一步设置是关键设置 */ position: absolute; width: 100%; height: 100%; top: 0; bottom: 0; left: 0; right: 0; } [代码] 这样我们就实现了全屏背景(图片背景)了,接下来我们来做模糊效果 二,实现模糊效果 这里主要用到了 CSS3的 filter(滤镜) 属性 [图片] 通过上面这张图和下面这张图对比,可以看到filter的值越大越模糊。 [图片] 这样我们就轻松的实现了本地图片的高斯模糊效果。 但是有时候我们不仅仅是用到本地图片,我们还需要用到网络图片。那这时候该怎么办呢? 三,网络图片实现高斯模糊效果 1,不管是本地图片还是网络图片,首先我们还是要让图片做全局拉伸。 [图片] 原图长这样,可以看到我们做全屏背景的时候把这个图片从中间裁剪拉伸了 background属性里的 center/cover起了主要作用。 [图片] 2,然后就是用filter做模糊效果了 [图片] 到这里我们小程序就轻松的实现高斯模糊效果了。是不是很简单。 今天就到这里了,后面我还会分享更多小程序相关的知识出来。请持续关注。
2019-12-11 - 地图选择地址 及 输入地址在地图定位
准备工作: 需要去腾讯地图开放平台申请一个账号,获取到一个key参数 下载SDK;地址:https://lbs.qq.com/qqmap_wx_jssdk/index.html 注意事项: 1:调试的时候使用真机去调试! 2:在发布这篇文章之后就经历了一小段的煎熬,怎么测试版本好好的,正式版本就不行了,兼容性问题?还是其他的?后来想起开发工具去可以不检测合法域名的,去掉勾勾,果然,开发工具也不行了,加上合法域名就可以开开心心玩耍了! 我们在使用调试版本的时候,是可以 不检测合法域名 的,但是发布的正式版本却不能,所以我们一定要去微信小程序的管理平台去将request的合法域名给加上; https://apis.map.qq.com https://restapi.amap.com https://tcb-api.tencentcloudapi.com [图片] 效果截图: [图片] 代码片段:https://developers.weixin.qq.com/s/13WYowmG7fdC 注:不需要自己去实现服务端代码,相关API均来自于腾讯地图
2019-12-11 - 小程序奇技淫巧之 -- globalDataBehavior管理全局状态
Behaviors 自定义组件中,提供了[代码]behaviors[代码]的使用和定义。 从官方文档我们能看到: [代码]behaviors[代码]是用于组件间代码共享的特性,类似于一些编程语言中的“mixins”或“traits”。 每个[代码]behavior[代码]可以包含一组属性、数据、生命周期函数和方法,组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。每个组件可以引用多个[代码]behavior[代码]。 简单来说,我们能通过[代码]behaviors[代码]来重构[代码]Component[代码]的能力。Behavior的用处很多,前面也有介绍 computed 计算属性、watch 观察属性的实现,都是使用的 Behavior。 全局状态管理 我们希望全局共享一些数据状态,如果只是通过一个文件的方式进行维护,那么我们无法在状态更新的时候及时地同步到页面。我们需要额外调用 setData 才能更新页面中的 data 数据,才能告诉渲染层这块的数据渲染需要变更,而很多的 Store 状态管理库也是通过这样的方式实现的(事件通知 + setData + 全局状态)。 在小程序 Behavior 能力的支持下,我们可以通过一个全局的 globalData Behavior 注入到每个需要用到的 Component 中,这样就可以在需要的页面中直接引入该 Behavior,就能获取到了。不啰嗦,Behavior的实现如下: [代码]// globalDataStore 用来全局记录 globalData,为了跨页面同步 globalData 用 export let globalDataStore = {}; // 获取本地的 gloabalData 缓存 try { const gloabalData = wx.getStorageSync("gloabalData"); // 有缓存的时候加上 if (gloabalData) { globalDataStore = { ...gloabalData }; } } catch (error) { console.error("gloabalData getStorageSync error", "e =", error); } // globalCount 用来全局记录 setGlobalData 的调用次数,为了在 B 页面回到 A 页面的时候, // 检查页面 __setGlobalDataCount 和 globalCount 是否一致来判断在 B 页面是否有 setGlobalData, // 以此来同步 globalData let globalCount = 0; export default Behavior({ data: { globalData: Object.assign({}, globalDataStore) }, lifetimes: { attached() { // 页面 onLoad 的时候同步一下 globalCount this.__setGlobalDataCount = globalCount; // 同步 globalDataStore 的内容 this.setData({ globalData: Object.assign( {}, this.data.globalData || {}, globalDataStore ) }); } }, pageLifetimes: { show() { // 为了在 B 页面回到 A 页面的时候,检查页面 __setGlobalDataCount 和 globalCount 是否一致来判断在 B 页面是否有 setGlobalData if (this.__setGlobalDataCount != globalCount) { // 同步 globalData this.__setGlobalDataCount = globalCount; this.setGlobalData(Object.assign({}, globalDataStore)); } } }, methods: { // setGlobalData 实现,主要内容为将 globalDataStore 的内容设置进页面的 data 的 globalData 属性中。 setGlobalData(obj: any) { globalCount = globalCount + 1; this.__setGlobalDataCount = this.__setGlobalDataCount + 1; obj = obj || {}; let outObj = Object.keys(obj).reduce((sum, key) => { let _key = "globalData." + key; sum[_key] = obj[key]; return sum; }, {}); this.setData(outObj, () => { globalDataStore = this.data.globalData; }); }, // setGlobalDataAndStorage 实现,先调用 setGlobalData,然后存到 storage 里 setGlobalDataAndStorage(obj: any) { this.setGlobalData(obj); try { let gloabalData = wx.getStorageSync("gloabalData"); // 有缓存的时候加上 if (gloabalData) { gloabalData = { ...gloabalData, ...obj }; } else { gloabalData = { ...obj }; } wx.setStorageSync("gloabalData", gloabalData); } catch (e) { console.error("gloabalData setStorageSync error", "e =", e); } } } }); [代码] 显然,该 Behavior 主要提供了几个能力: 会在小程序 data 添加 globalData 的属性,在 WXML 文件中可以直接通过[代码]{{globalData.xxxx}}[代码]获取到 提供[代码]setGlobalData()[代码]方法,用于更新全局状态 提供[代码]setGlobalDataAndStorage()[代码]方法,用于更新全局状态,同时写入缓存(会在下次启动应用的时候自动获取缓存数据) 这样,我们在初始化 Component 的时候直接引入就可以使用: [代码]Component({ // 在behaviors中引入globalDataBehavior behaviors: [globalDataBehavior], // 其他选项 methods: { test() { // 使用this.setGlobalData可以更新全局的数据状态 this.setGlobalData({ test: "hello world" }); // 使用this.setGlobalDataAndStorage可以更新全局的数据状态,并写入缓存 // 下次globalDataBehavior会默认从缓存中获取 this.setGlobalDataAndStorage({ test: "hello world" }); } } }); [代码] 在引入了 globalDataBehavior 之后,我们的 WXML 就可以直接使用了: [代码]<view>{{ globalData.test }}</view> [代码] 页面如何使用 Behavior [代码]Component[代码]是[代码]Page[代码]的超集,因此可以使用[代码]Component[代码]构造器构造页面。 看看官方文档:事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用[代码]Component[代码]构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应[代码]json[代码]文件中包含[代码]usingComponents[代码]定义段。 更详细的使用方法,在 computed 计算属性、watch 观察属性两篇文章中也有描述,大家可以自行参考。 或者直接查看最终的项目代码:wxapp-typescript-demo。 参考 Component构造器 behaviors 结束语 Behavior 其实是很强大的一个能力,我们能用它来对自己的小程序做很多的能力拓展,缺啥补啥,还可以“混入”给每个 Component 每个方法打入日志,就不用每个组件自己手动打印代码拉。
2019-12-10 - 教你怎么监听小程序的返回键
更新:2020年7月28日08:51:11 基础库2.12.0起,可以调用wx.enableAlertBeforeUnload监听原生右上角返回、物理返回以及wx.navigateBack时弹框提示 AIP详情请看: https://developers.weixin.qq.com/miniprogram/dev/api/ui/interaction/wx.enableAlertBeforeUnload.html //======================================== 怎么监听小程序的返回键? 应该有很多人想要监听用户的这个动作吧,但是很遗憾,小程序不会给你这个API的,那是不是就没辙了? 幸好我们还可以自定义导航栏,这样一来我们就可以监听用户的这一动作了。 什么?这你已经知道啦? 那好咱们就不说自定义导航栏的返回监听了,说一下物理返回和左滑?右滑?(不管了,反正是滑)返回上一页怎么监听。 监听物理返回 首先说一下这个监听方法的缺点,虽说是监听,但是还是无法真正意义上的监听并拦截来阻止页面跳转,页面还是会返回上一页,而后重新载入刚刚的页面,如果这不是你想要的,那可以不用往下看了 其次说一下用到什么东西: wx.onAppRoute、wx.showModal 最后是一些主要代码: 重写wx.showModal,主要是加个confirmStay参数和使wx.showModal Promise化 [代码]const { showModal } = wx; Object.defineProperty(wx, 'showModal', { configurable: false, // 是否可以配置 enumerable: false, // 是否可迭代 writable: false, // 是否可重写 value(...param) { return new Promise(function (rs, rj) { let { success, fail, complete, confirmStay } = param[0] param[0].success = (res) => { res.navBack = (res.confirm && !confirmStay) || (res.cancel && confirmStay) wx.setStorageSync('showBackModal', !res.navBack) success && success(res) rs(res) } param[0].fail = (res) => { fail && fail(res) rj(res) } param[0].complete = (res) => { complete && complete(res) (res.confirm || res.cancel) ? rs(res) : rj(res) } return showModal.apply(this, param); // 原样移交函数参数和this }.bind(this)) } }); [代码] 使用wx.onAppRoute实现返回原来的页面 [代码]wx.onAppRoute(function (res) { var a = getApp(), ps = getCurrentPages(), t = ps[ps.length - 1], b = a && a.globalData && a.globalData.pageBeforeBacks || {}, c = a && a.globalData && a.globalData.lastPage || {} if (res.openType == 'navigateBack') { var showBackModal = wx.getStorageSync('showBackModal') if (c.route && showBackModal && typeof b[c.route] == 'function') { wx.navigateTo({ url: '/' + c.route + '?useCache=1', }) b[c.route]().then(res => { if (res.navBack){ a.globalData.pageBeforeBacks = {} wx.navigateBack({ delta: 1 }) } }) } } else if (res.openType == 'navigateTo' || res.openType == 'redirectTo') { if (!a.hasOwnProperty('globalData')) a.globalData = {} if (!a.globalData.hasOwnProperty('lastPage')) a.globalData.lastPage = {} if (!a.globalData.hasOwnProperty('pageBeforeBacks')) a.globalData.pageBeforeBacks = {} if (ps.length >= 2 && t.onBeforeBack && typeof t.onBeforeBack == 'function') { let { onUnload } = t wx.setStorageSync('showBackModal', !0) t.onUnload = function () { a.globalData.lastPage = { route: t.route, data: t.data } onUnload() } } t.onBeforeBack && typeof t.onBeforeBack == 'function' && (a.globalData.pageBeforeBacks[t.route] = t.onBeforeBack) } }) [代码] 改造Page [代码]const myPage = Page Page = function(e){ let { onLoad, onShow, onUnload } = e e.onLoad = (() => { return function (res) { this.app = getApp() this.app.globalData = this.app.globalData || {} let reinit = () => { if (this.app.globalData.lastPage && this.app.globalData.lastPage.route == this.route) { this.app.globalData.lastPage.data && this.setData(this.app.globalData.lastPage.data) Object.assign(this, this.app.globalData.lastPage.syncProps || {}) } } this.useCache = res.useCache res.useCache ? reinit() : (onLoad && onLoad.call(this, res)) } })() e.onShow = (() => { return function (res) { !this.useCache && onShow && onShow.call(this, res) } })() e.onUnload = (() => { return function (res) { this.app.globalData = Object.assign(this.app.globalData || {}, { lastPage: this }) onUnload && onUnload.call(this, res) } })() return myPage.call(this, e) } [代码] 在需要监听的页面加个onBeforeBack方法,方法返回Promise化的wx.showModal [代码]onBeforeBack: function () { return wx.showModal({ title: '提示', content: '信息尚未保存,确定要返回吗?', confirmStay: !1 //结合content意思,点击确定按钮,是否留在原来页面,confirmStay默认false }) } [代码] 运行测试,Oj8K 是不是很简单,马上去试试水吧,效果图就不放了,静态图也看不出效果,动态图懒得弄,想看效果的自己运行代码片段吧 代码片段 https://developers.weixin.qq.com/s/hc2tyrmw79hg
2020-07-28 - 小程序map组件,marker是否能自定义样式
- 需求的场景描述(希望解决的问题) 头像、经纬度都是动态从后端获取的,我想弄成圆形的。但是好像是不行 [图片] - 希望提供的能力
2019-07-16 - dragUI 基于uni的可拖拽可视化编程,自动生成项目,自动生成代码
github项目地址 uni-app插件市场地址 在线演示 demo地址 一个简单创建hello world 界面的视频,github不会放视频,放在bilibli了 dragUI 演示视频 如果您觉得想法还行,希望能有人一起完善这个项目,Fork他
2019-12-02 - 一个奇葩思路实现的瀑布流双列布局
传统的瀑布流布局实现一般关键是去计算每一列的高度,从而判断下一个元素应该插入到哪一列(当然是最短的那列)。 这个奇葩思路没有任何计算,主要思路如下: 在瀑布流容器底部加入一根细线 利用微信小程序的IntersectionObserver,为每一列和细线添加监听 逐个加入要插入的item元素 根据监听相交变化结果判断下一个item应该插入哪一列(简单来说就是插入到当前不与细线相交的那一列,因为比较短) 这个思路实际上就是把计算高度换成了监听判断哪列更高,因此也不必知道每个元素的高度。 目前只能支持两列布局的情况,如果列数更多我没办法不通过计算来知道哪列最短,如果有思路或想法的童鞋欢迎交流~ 实现过程也比较简单,就分享个思路,不贴代码了(问就是懒!) 感兴趣的童鞋可以看代码片段,里面有完整的实现代码: https://developers.weixin.qq.com/s/nH5pg4mE78dG
2019-11-23 - 使用animation实现列表顺序加载动画
[图片] 之前使用纯transition实现动画时, 发现在部分手机上效果不是很好, 会有不流畅掉帧的现象! 现在换animation方法实现, 不知各位是否有什么高见, 大家一起交流交流 代码片段如下 https://developers.weixin.qq.com/s/pEBv6emG7Cdt
2019-11-29 - 懒人操作:放弃重复代码,数据—双向绑定—策略,输入框全匹配输入事件与输入框全匹配正则校验事件
代码片段: https://developers.weixin.qq.com/s/Ku7Daamt70dh 在代码片段中版本库不能使用2.9.3,这个版本输入框输入事件不会触发,修改2.9.2OK 文章详情:代码预览——https://developers.weixin.qq.com/community/develop/article/doc/000ae2410fcad8e49b797b31751413
2019-12-30 - 状态管理工具
我一直希望只用小程序的原生框架进行开发,之前为Nike和沃尔玛开发过小程序,发现大多数小程序在功能上也不会像web前端那样复杂,所以再引入一个开发框架难免会觉得是在增加复杂度。 而用原生框架开发时,我觉得唯一缺少的就是一个全局的状态管理框架,所以我自己写了一个,使用风格上有点偏向mobx,大家如果有想法和意见,欢迎告诉我。 Github: https://github.com/wwayne/minii 举个栗子如何使用:(只有两个api,mapToData 和 observe) [图片]
2018-10-29 - 小程序人脸核身开发流程
1、先去腾讯云平台开通人脸核身功能,需要填写正确的小程序APPID [图片] 2、开通后、进入账号信息中->访问管理->访问秘钥,可得到调用API接口的参数: [图片] 3、进入微信小程序中申请类目:政府/明生 申请成功后、可在接口设置授权中使用。 [图片] 4、在小程序设置中关联第三方平台,不要分配开发权限 [图片] 5、下载小程序SDK ,下载位置见步骤1的图。 6、根据文档、在小程序代码中做相应的配置 https://cloud.tencent.com/document/product/1007/31071 7、需要在自己的服务器中接入腾讯接口调用API 、去获取小程序所需的 BizToken [图片] 8、访问链接:https://console.cloud.tencent.com/api/explorer?Product=faceid&Version=2018-03-01&Action=DetectAuth&SignVersion= 如下图1 获取BizToken,2、获取人脸核身后的相关信息 [图片]
2019-11-22 - 小程序顶部导航栏,可滑动,可动态选中放大
最近在研究小程序顶部导航栏时,学到了一个不错的导航栏,今天就来分享给大家。 老规矩,先看效果图 [图片] 可以看到我们实现了如下功能 1,顶部导航栏 2,可以左右滑动的导航栏 3,选中条目放大 原理其实很简单,我这里把我研究后的源码发给大家吧。 wxml文件如下 [代码]<!-- 导航栏 --> <scroll-view scroll-x class="navbar" scroll-with-animation scroll-left="{{scrollLeft}}rpx"> <view class="nav-item" wx:for="{{tabs}}" wx:key="id" bindtap="tabSelect" data-id="{{index}}"> <view class="nav-text {{index==tabCur?'tab-on':''}}">{{item.name}}</view> </view> </scroll-view> [代码] wxss文件如下 [代码]/* 导航栏布局相关 */ .navbar { width: 100%; height: 90rpx; /* 文本不换行 */ white-space: nowrap; display: flex; box-sizing: border-box; border-bottom: 1rpx solid #eee; background: #fff; align-items: center; /* 固定在顶部 */ position: fixed; left: 0rpx; top: 0rpx; } .nav-item { padding-left: 25rpx; padding-right: 25rpx; height: 100%; display: inline-block; /* 普通文字大小 */ font-size: 28rpx; } .nav-text { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; letter-spacing: 4rpx; box-sizing: border-box; } .tab-on { color: #fbbd08; /* 选中放大 */ font-size: 38rpx !important; font-weight: 600; border-bottom: 4rpx solid #fbbd08 !important; } [代码] js文件如下 [代码]// pages/test2/test2.js Page({ data: { tabCur: 0, //默认选中 tabs: [{ name: '等待支付', id: 0 }, { name: '待发货', id: 1 }, { name: '待收货', id: 2 }, { name: '待签字', id: 3 }, { name: '待评价', id: 4 }, { name: '五星好评', id: 5 }, { name: '差评订单', id: 6 }, { name: '编程小石头', id: 8 }, { name: '小石头', id: 9 } ] }, //选择条目 tabSelect(e) { this.setData({ tabCur: e.currentTarget.dataset.id, scrollLeft: (e.currentTarget.dataset.id - 2) * 200 }) } }) [代码] 代码里注释很明白了,大家自己跟着多敲几遍就可以了。后面会更新更多小程序相关的知识,请持续关注。
2019-11-22 - 项目停止服务了,但是技术的学习分享永不停鸭
github仓库地址-点这里看源码 xxx项目 开发时集成eslint,框架使用原生 + westore + iview weapp 部分ui样式组件代码 。去除了真实的请求地址,部分配置和页面。方便大家学习交流 [暗门-彩蛋] 点击顶部bar 15下可弹出暗门操作,方面调试定位 自己修改appid 并关闭安全域名校验 运行一下就知道喽~ 库插件 1. wxApi [代码]const wxApi = function (keyName = "", obj = {}) { return new Promise((resolve, reject) => { // 去除方法里面的空格服 if (keyName && typeof keyName === "string") { keyName = keyName.replace(/\s/g, "") } else { console.error("keyName值不能为空哦且必须是string类型") return false } // 判断方法名 是否再wx 对象中 const wxHasOwnProperty = wx.hasOwnProperty(keyName) if (!wxHasOwnProperty) { console.error(`你输入的方法[${keyName}]在wx中找不到,请检查是否输入正确`) return false } if (keyName && wx[keyName] && wxHasOwnProperty) { wx[keyName]({ ...obj, success(data) { resolve(data) }, fail(data) { reject(data) } }) } }) } export default wxApi // 仅支持wx 的异步方法 wxApi("fnName",params).then(res=>{}).catch(error=>{}) [代码] 2. request [代码] /** * 1. 接口文件单独维护 * 2. 设置token * 3. 中断请求 * 4. 401 重试示范 * */ import http from "xxx/xxx/request.js" // 普通的请求 http.get("YOUR_API") // resful 拼接的情况 ,目前只支持三个自定义参数,多的自己再修改代码,或者去怼后端吧 http.get({ key:"YOUR_API", p1:"hello", p2:"world", }) // 所有的请求都被拦截包装过的,可根据自己的业务进行包装 [代码] 功能点 1 快速新建组件/pages模板 命令:cnpm/npm run page 并且可以顺便给你app.json 中添加了这个路由 2 eslint/prettier 集成 cnpm run lint/fix 3 小程序全局状态库westore 和 登录解决方案 [图片] 4 自定义顶部tabBar [图片] [图片] 5 海报图分享 [图片] 5.全局按需登录的组件 [图片] [图片]
2019-11-19 - 小程序简单两栏瀑布流效果
瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。视觉表现为参差不齐的多栏布局,即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次放入到高度最低的那一栏。 先上代码:https://developers.weixin.qq.com/s/Fgm5s1mz7Wdm 所谓简单,是指只考虑图片,图片之外的其他元素高度固定,不在考虑范围内。 说一下基本的实现思路: 1、加载列表数据 2、在一个隐藏的view中加载图片,通过image组件的bindload获取图片的实际宽高并存储 3、等所有图片加载完成后遍历列表,将图片插入到高度低的那一栏,同时更新该栏高度 我也考虑过在第二步bindload获取到宽高后就直接插入到栏位中,但是会出现小的图片先加载完先出现到页面中,虽然瀑布流不是普通的列表那样的排序,但是也不能小的图片在上面这样太乱顺序,所以就改成了获取宽高先存储,等所有图片加载完成后再往页面上渲染。 来看看实际的代码 不需要渲染到wxml中的数据,我放到了jsData中,主要是两栏的高度和是否在加载数据的标记。 tempPics是第一次加载的数据,临时存放,用于加载图片宽高 columns是两个栏位的实际展示数据 [代码]jsData: { columnsHeight: [0, 0], isLoading: false }, data: { columns: [ [], [] ], tempPics: [] } [代码] 1、加载列表数据 这一步没什么好说的,主要是触发方式,我的代码里是放在页面加载以及拉到页面底部时触发 [代码]onLoad: function() { this.loadData() }, onReachBottom: function() { this.loadData() } [代码] 加载后将列表数据存到tempPics中,用于页面加载获取宽高 2、在一个隐藏的view中加载图片,通过image组件的bindload获取图片的实际宽高并存储 [代码]<view class="hide"> <image wx:for="{{tempPics}}" src="{{item.pic}}" bindload="loadPic" binderror="loadPicError" data-index="{{index}}" /> </view> [代码] 主要是image组件的bindload来获取实际宽高,这里还增加了binderror,防止出现图片加载出错的时候卡死 [代码]loadPic: function(e) { var that = this, data = that.data, tempPics = data.tempPics, index = e.currentTarget.dataset.index if (tempPics[index]) { //以750为宽度算出相对应的高度 tempPics[index].height = e.detail.height * 750 / e.detail.width tempPics[index].isLoad = true } that.setData({ tempPics: tempPics }, function() { that.finLoadPic() }) } [代码] 获取到宽高后,以750为宽度计算出相对应的高度并存储,然后增加一个加载完成的标记。加载出错后就强制高度为750,这样展示的时候就是一个正方形。 单个图片加载完成并存储后调用finLoadPic方法来判断所有图片是否都加载完成。 遍历列表,只要有一个图片没有加载完成的标记,就判断为没有加载完成。 加载完成后进入下一步。 [代码]finLoadPic: function() { var that = this, data = that.data, tempPics = data.tempPics, length = tempPics.length, fin = true for (var i = 0; i < length; i++) { if (!tempPics[i].isLoad) { fin = false break } } if (fin) { wx.hideLoading() if (that.jsData.isLoading) { that.jsData.isLoading = false that.renderPage() } } } [代码] 3、等所有图片加载完成后遍历列表,将图片插入到高度低的那一栏,同时更新该栏高度 这里需要再便利一遍列表,根据当前栏位的高度情况,将图片插入到高度底的那一栏,同时把这一栏高度加上当前图片的高度(不是实际高度,是上一步以750为宽度算出来的高度) [代码]renderPage: function() { var that = this, data = that.data, columns = data.columns, tempPics = data.tempPics, length = tempPics.length, columnsHeight = that.jsData.columnsHeight, index = 0 for (var i = 0; i < length; i++) { index = columnsHeight[1] < columnsHeight[0] ? 1 : 0 columns[index].push(tempPics[i]) columnsHeight[index] += tempPics[i].height } that.setData({ columns: columns, tempPics: [] }) that.jsData.columnsHeight = columnsHeight } [代码] 在wxml中展示的时候image组件的mode要使用widthFix,同时wxss中图片的高度和宽度一样,这样加载出错的图片可以正方形展示 11月21日增加: 根据@杨泉的建议,也尝试了使用wx.getImageInfo来获取图片的宽高(具体代码可以参考评论区),代码也精简了很多。但是实际比较下来速度要比用image组件慢,初步推测原因是[代码]wx.getImageInfo[代码]会返回本地路径,多了写本地临时文件的时间 ps:用到瀑布流的地方,最好能后端直接返回图片的宽高,省去小程序端获取宽高的麻烦 再ps:我个人并不建议小程序端使用瀑布流
2020-01-14 - 用云开发快速制作精美互动打卡小程序丨实战
个人介绍 大家好,我是Zero,一名大四的前端开发爱好者,目前主要研究微信小程序和iOS开发。 这是第二次参加微信小程序应用开发赛,2018年我们设计了一款通过二维码寻找丢失物品的小程序《蝴蝶寻物》,获得了华北赛区三等奖。 今年,在小程序云开发功能的大力推广下,我决定采用云开发的方式,实现一个双人互动打卡互动的小程序《Mango Daily》(中文名称《芒果日常》)。(由于是个人开发者账号,所以暂时还未上架) [图片] 得力于云开发提供的API,本项目在较短的时间内就实现了比较理想的效果。 接下来,我想从本项目入手,讲讲我是如何依靠小程序 云开发平台将想法快速实现的。 1. 技术准备 在去年的项目中,我们采用ThinkPHP开发了一套API系统,其中需要实现小程序的授权登录,设置鉴权来保证数据安全等操作。整个过程只有我一名开发人员,所以大致就是“先搞定后端,其次搞定界面,最后进行联调”的一个过程。 后来在云 社区看到一篇文章:《1个开发如何撑起一个过亿用户的小程序》,觉得可以尝试一下新的开发方式。通读小程序云开发文档之后,发现并不需要学习新的技术就可以快速上手。 2. 开发 Mango Daily 使用的是小程序原生开发 云开发结合的方式进行开发的。 2.1 界面开发 界面没有使用第三方UI框架,而是自己将常用的模块封装成了组件。 [图片] 图中比较核心的模块包括 TabBar、Toast、Modal、Nav等。 [图片] 2.2 云开发 云开发包括云数据库,云函数和云存储。本项目中三个功能均使用到。 2.2.1 云数据库 云数据库是一个非关系型数据库,在实际开发中基本符合本项目的需求。部分表关联查询则是通过分步查询的方式代替。 云数据库已经实现了自动鉴权,可以保证数据的安全性。目前云数据库只支持以下几种权限: 所有用户可读,仅创建者可读写 仅创建者可读写 所有用户可读 所有用户不可读写 默认情况下是***仅创建者可读写***,所以在首次开发时,手动插入的测试数据并不一定可以在前端顺利读取,需要修改集合的权限。 云数据库的调用在前端代码中即可完成。但是从上面几种读写权限来看,并没有办法实现对另一个用户创建的数据进行修改或者删除的操作(当然这也是非常不可取的),于是云函数就派上用场了。 2.2.2 云函数 我理解的云函数,则是跑在云端的一个函数脚本文件。 在接触云开发之前,如果我们想要去调用微信公众平台提供的API(例如发起退款、发送模板消息等),则需要在后端代码去实现,然后只需要给前端返回一个JSON表示请求状态即可。或者想要去实现上述描述中,修改一条由他人创建的数据的功能时,都是有后端工程师去完成的。 在本次开发中,我深刻体会到了云函数的强大,以及微信公众平台工程师设计产品的严谨性。 Mango Daily用到了微信公众平台的模板消息功能,所以需要在合适的时机请求微信官方提供的API。 因为取消了后端的开发,所以一开始打算直接在小程序端去请求官方API。但是失败了。因为此请求涉及APPKEY等重要信息,禁止在小程序端代码中直接请求官方API。这样就可以通过云函数去代替先前的后端开发,最后将状态返回给小程序端即可。 例如想给新用户发送一条短信,以往的做法是客户端请求后端API,然后由后端完成发送短信操作。这里云函数就代替了后端开发。如果仅仅依靠小程序JS代码去发送短信,是非常不可取的。 另外,云函数对云数据库有更高的操作权限,所以想要修改、删除他人生成的数据时,云函数可以直接进行操作。 云函数还提供定时触发功能,不过在本项目中暂未涉及。 2.2.3 云存储 本次开发省去了使用其他服务商的存储服务,全部得力于云存储功能。云存储允许上传多种文件类型,像图片、音频等文件还可以直接在小程序端调用。这里我们使用云存储实现了文章插图的功能。 [图片] 2.3 优化 2.3.1 数据层封装 Mango Daily 数据操作进行了两次封装,一层是对云数据库API进行封装,第二层是每一个数据集合都对应一个Manager管理层。 以用户集合 User,Article 为例,项目中的结构如下: util |- db.js manager |- Article.js |- User.js db.js 是对云数据库API的封装,实现了增删查改等操作,以更新数据为例。 [代码]/** * 更新数据 */ const update = (collection, _id, data) => { return new Promise((resolve, reject) => { if (!exist(collection)) { reject(401, resCode[401]); } db.collection(collection).doc(_id).update({ data: data }).then(res => { resolve(res); }).catch((code, msg) => { reject(code, msg); }); }); } [代码] Article.js 是文章集合的管理类,同样实现了增删查改等操作,不过其是基于 db.js 进行扩展的。以更新文章操作为例: [代码]/** * 更新 */ const update = (_id, data) => { return new Promise((resolve, reject) => { db.update(collection, _id, data).then(res => { resolve(res); }).catch((code, msg) => { reject(db.errMsg); }); }); } [代码] 之所以封装两层,是想尽量减少Page对象中对云数据库的直接调用。这样在页面js文件中只需要调用某一个Manager提供的函数即可。 2.3.3 后台上传策略 Mango Daily还实现了发送模板消息的功能,这就涉及到了FromID的收集。目前FromID的收集大部分采用埋点的方式。 如果每次采集到新的FromID都直接上传到数据库存储,可能会造成网络资源的浪费,所以需要选择合适的时机上传数据。 在本项目中,每次采集到FromID,首先存到 globalData 中,当小程序进入后台状态时,再进行数据的上传。 app.js 中的实现: [代码]/** * 后台监听 */ onHide: function() { this.uploadFormID(); }, /** * 上传token */ uploadFormID: function() { let ids = this.globalData.formIds; if (ids.length == 0) { return ; } let formId = ids.pop(); this.push.upload(formId).then(_ => { console.log("上传formID:" , formId); this.uploadFormID(); }).catch(err => { console.log(err); }); }, [代码] 3. 维护 很遗憾,这一部分可能没有太多需要写的。 在18年的项目中,需要考虑数据库的维护问题。但是使用了云开发之后,Serverless的优点就表现出来了。我无须将太多的精力放在后端的维护上。 4. 总结 在本次项目开发中,我深刻体会到了云开发的便捷性。无须自己实现鉴权,对接第三方存储。数据方面,增删查改功能非常方便。云开发提供的种种便利,让我在有新创意的时候,优先选择小程序 云开发的方式去实现。 [代码]你好,你的小程序涉及用户自行生成内容的发布/分享/交流,属社交范畴,为个人主体小程序未开放类目,建议申请企业主体小程序 [代码] 另外,Mango Daily中的随笔功能属于用户自行生成内容功能,所以在上架的时候,个人开发者账号是不被允许的,所以在考虑上架产品的时候,请按照实际情况酌情考虑选择账号主体类型。 源码地址 https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 如果你想要了解更多关于云开发CloudBase相关的技术故事/技术实战经验,请扫码关注【腾讯云云开发】公众号~ [图片]
2019-11-11 - 小程序使用防抖函数的简单方法
废话不多说,上代码: Page构造器内部使用,不需要使用外部模块。 [代码]onLoad: function (options) { console.log(options); this.debounce = this.debounce();// 防抖函数,在此处初始化 // 若不初始化,函数主体不执行 } // debounce函数,就是事件触发的函数,名字可以随意取名 debounce : function () { var timeOut = null; return () => { clearTimeout(timeOut); timeOut = setTimeout(() => { // 事件函数中要执行的代码块 // 改写原函数异常方便、简洁 }, 300); } } [代码] 如果这个有问题,欢迎指点。
2019-11-12 - 微信小程序如何在循环之中使用 slot
component.js: [代码]Component({ properties: { thing: Object }, options: { multipleSlots: true } }) [代码] component.wxml: [代码]<block wx:for="{{thing}}" wx:for-item="t" wx:for-index="i" > <view>Heading: {{t}}</view> <slot name="slot-{{i}}" /> </block> [代码] index.js: [代码]Page({ data: { // ... someArray: [1, 2, 233] } }) [代码] index.wxml: [代码]<test-component thing="{{someArray}}"> <view wx:for="{{someArray}}" wx:for-index="i" slot="{{i}}">test</view> </test-component> [代码] 最终效果: [图片] 以上。
2019-11-11 - 开发者工具那些你未必知道的console命令
build 编译 相当于点击“编译”按钮 [图片] preview 预览 [图片] upload 上传代码 [图片] cleanAppCache 清除应用缓存 清除完需要重新编译 [图片] showRequestInfo 查看请求过链接的信息 检查证书一大利器 [图片] showSystemInfo 显示当前开发者工具占用内存及其他信息 [图片] checkProxy 检测目标网址是否启用代理 [图片] openToolsLog 打开开发者工具日志目录 openPlugin 打开插件目录 openVendor 打开供应商目录 更多详细的命令使用 help 进行获取 [图片]
2019-11-08 - 使用WXS做搜索列表功能
使用WXS过滤功能做搜索列表功能 小程序搜索功能,通过关键字去搜索data里面的数组变量 list,如果根据输入框输入的文字实时搜索,频繁使用 this.setData 改变数据变量list ,这样多次调用 setData 去 改变原数据的方法,需要保存一份原数据和搜索数据,避免输入框文字框清空时,无法输出原数据,这种方法用到更多的变量,同时会降低小程序的体验度和增加性能消耗,而且额外保存一份原数据和搜索数据,处理起来也不方便。 在实践中,使用 wxs 的过滤功能可以更好的完成搜索功能需求。实现原理如下草图。 [图片] 示例代码: https://developers.weixin.qq.com/s/qJhhxJma7fcY <block wx:for="{{ list }}"> <block wx:if="{{ tools.search(index,inputVal) }}"> <navigator url="" class=“weui-cell” hover-class=“weui-cell_active”> <view class=“weui-cell__bd”> <view>索引{{index}}</view> </view> </navigator> </block> </block> [图片] [图片]
2019-11-08 - 内容安全检测图片API:openapi.security.imgSecCheck完美解决方案。
背景需求: 我个人做了一款小程序的小游戏,本质是小程序。里面有个自定义图片的功能。用户从本地相册选一张图片进行裁剪,之后保存到缓存中或者上传到服务器。然后用户再用这张图片作为素材进行其它操作。这里就涉及到内容安全了,提交审核没有通过也是因为这个没有做内容安全。防止一些色情低俗的事情发生。 正文: 思路:相册选图片 --> 裁剪小的图片 --> 内容安全检测 --> 通过 --> 裁剪大的图片 --> 保存。 失败的原因:绝大多数是因为检测图片不能大于1M,而导致超时,或者是errCode:-1,又或者是其它问题。 [图片] [图片] 核心代码图片: [代码]默认裁剪小尺寸图片 (我的业务需求是正方形图片,也可动态计算宽高比例) [代码] [图片] 检测图片 部分iOS不兼容encoding: ‘ucs2’。注释掉就好了 [图片] [图片] 云函数 [图片] 测试情况: 正常图片不含违法违规,测试20次,全部通过。小程序上线后暂无发现检测失败情况。百度搜索的“人体油画”等等均可通过。 PS:第一次写经验分享哈,看不懂可以问我。体验一下我的小程序想问我这个小程序其它的功能点也可以喔! 技术会迭代更新,用到的技术会有时效性,看编辑时间,可能当时的技术现在不适用了
2020-10-22 - 使用 MobX 来管理小程序的跨页面数据
在小程序中,常常有些数据需要在几个页面或组件中共享。对于这样的数据,在 web 开发中,有些朋友使用过 redux 、 vuex 之类的 状态管理 框架。在小程序开发中,也有不少朋友喜欢用 MobX ,说明这类框架在实际开发中非常实用。 小程序团队近期也开源了 MobX 的辅助模块,使用 MobX 也更加方便。那么,在这篇文章中就来介绍一下 MobX 在小程序中的一个简单用例! 在小程序中引入 MobX 在小程序项目中,可以通过 npm 的方式引入 MobX 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 引入 MobX : [代码]npm install --save mobx-miniprogram mobx-miniprogram-bindings [代码] (这里用到了 mobx-miniprogram-bindings 模块,模块说明在这里: https://developers.weixin.qq.com/miniprogram/dev/extended/functional/mobx.html 。) npm 命令执行完后,记得在开发者工具的项目中点一下菜单栏中的 [代码]工具[代码] - [代码]构建 npm[代码] 。 MobX 有什么用呢? 试想这样一个场景:制作一个天气预报资讯小程序,首页是列表,点击列表中的项目可以进入到详情页。 首页如下: [图片] 详情页如下: [图片] 每次进入首页时,需要使用 [代码]wx.request[代码] 获取天气列表数据,之后将数据使用 setData 应用到界面上。进入详情页之后,再次获取指定日期的天气详情数据,展示在详情页中。 这样做的坏处是,进入了详情页之后需要再次通过网络获取一次数据,等待网络返回后才能将数据展示出来。 事实上,可以在首页获取天气列表数据时,就一并将所有的天气详情数据一同获取回来,存放在一个 数据仓库 中,需要的时候从仓库中取出来就可以了。这样,只需要进入首页时获取一次网络数据就可以了。 MobX 可以帮助我们很方便地建立数据仓库。接下来就讲解一下具体怎么建立和使用 MobX 数据仓库。 建立数据仓库 数据仓库通常专门写在一个独立的 js 文件中。 [代码]import { observable, action } from 'mobx-miniprogram' // 数据仓库 export const store = observable({ list: [], // 天气数据(包含列表和详情) // 设置天气列表,从网络上获取到数据之后调用 setList: action(function (list) { this.list = list }), }) [代码] 在上面数据仓库中,包含有数据 [代码]list[代码] (即天气数据),还包括了一个名为 [代码]setList[代码] 的 action ,用于更改数据仓库中的数据。 在首页中使用数据仓库 如果需要在页面中使用数据仓库里的数据,需要调用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中,然后就可以在页面中直接使用仓库数据了。 [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad() { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 actions: ['setList'], // 将 this.setList 绑定为仓库中的 setList action }) // 从服务器端读取数据 wx.showLoading() wx.request({ // 请求网络数据 // ... success: (data) => { wx.hideLoading() // 调用 setList action ,将数据写入 store this.setList(data) } }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,可以在 wxml 中直接使用 list : [代码]<view class="item" wx:for="{{list}}" wx:key="date" data-index="{{index}}"> <!-- 这里可以使用 list 中的数据了! --> <view class="title">{{item.date}} {{item.summary}}</view> <view class="abstract">{{item.temperature}}</view> </view> [代码] 在详情页中使用数据仓库 在详情页中,同样可以使用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中: [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad(args) { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 }) // 页面参数 `index` 表示要展示哪一条天气详情数据,将它用 setData 设置到界面上 this.setData({ index: args.index }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,这个页面 wxml 中也可以直接使用 list : [代码]<view class="title">{{list[index].date}}</view> <view class="content">温度 {{list[index].temperature}}</view> <view class="content">天气 {{list[index].weather}}</view> <view class="content">空气质量 {{list[index].airQuality}}</view> <view class="content">{{list[index].details}}</view> [代码] 完整示例 完整例子可以在这个代码片段中体验: https://developers.weixin.qq.com/s/YhfvpxmN7HcV 这个就是 MobX 在小程序中最基础的玩法了。相关的 npm 模块文档可参考 mobx-miniprogram-bindings 和 mobx-miniprogram 。 MobX 在实际使用时还有很多好的实践经验,感兴趣的话,可以阅读一些其他相关的文章。
2019-11-01 - kbone,十分钟让 Vue 项目同时支持小程序
什么是kbone 微信小程序开发过程中,许多开发者会遇到 小程序 与 Web 端一起的需求,由于 小程序 与 Web 端的运行环境不同,开发者往往需要维护两套类似的代码,这对开发者来说比较耗费力气,并且会出现不同步的情况。 为了解决上述问题,微信小程序推出了同构解决方案 [代码]kbone[代码] 来解决此问题。 那么,[代码]kbone[代码] 要怎么使用呢?这里我们将通过一个 [代码]todo[代码] 的例子来跟大家讲解。 基本结构 首先,我们来看下一个基本的 kbone 项目的目录结构(这里的 [代码]todo[代码] 是基于 [代码]Vue[代码] 的示例,[代码]kbone[代码] 也有 [代码]React[代码],[代码]Preact[代码],[代码]Omi[代码] 等版本,详情可移步 kbone github)。 因为 kbone 是为了解决 小程序 与 Web 端的问题,所以每个目录下的配置都会有两份(小程序 与 Web 端各一份) [图片] 入口 不管是 小程序 端还是 Web 端,都需要入口文件。在 [代码]src/index[代码] 目录下,[代码]main.js[代码] 为 Web 端用主入口,[代码]main.mp.js[代码] 则为 小程序 端用主入口。 当然,Web 端会比 小程序 多一个入口页面,即 [代码]index.html[代码](位于根目录下)。 [图片] 下面两段代码分别是 小程序端 入口与 Web 端入口的代码,可以看到 小程序端的入口代码封装在 [代码]createApp[代码] 函数里面(这里固定即可),内部会比 Web 端多一个创建 [代码]app[代码] 节点的操作,其他的基本就是一致的。 [代码]// 小程序端入口 import Vue from 'vue' import todo from './todo.vue' export default function createApp() { // 创建app节点用于绑定 const container = document.createElement('div') container.id = 'app' document.body.appendChild(container) return new Vue({ el: '#app', render: h => h(todo) }) } [代码] [代码]// web端入口 import Vue from 'vue' import todo from './todo.vue' new Vue({ el: '#app', render: h => h(todo) }) [代码] todo.vue 在上面的入口图可以看到,源码目录中,除了入口文件分开之前,页面文件就是共用的了,这里直接使用 Vue 的写法即可,不用做特殊的适应。 配置 写完代码之后,我们要怎么跑项目呢?这时,配置就派上用场啦。 Web 端配置为正常的 Vue 配置,小程序端配置与 Web 端配置的唯一不同就是需要引入 [代码]mp-webpack-plugin[代码] 插件来将 Vue 组件转化为小程序代码。 [图片] 构建代码 接着,我们需要构建代码,让代码可以运行到各自的运行环境中去。构建完成后,生产代码会位于 dist 目录中。 [代码]// 构建 web 端代码 // 目标代码在 dist/web npm run build // 构建小程序端代码 // 目标代码在 dist/mp npm run mp [代码] 小程序端 的构建会比 Web 端的构建多一个步骤,就是 npm 构建。 进入 [代码]dist/mp[代码] 目录,执行 [代码]npm install[代码] 安装依赖,用开发者工具将 [代码]dist/mp[代码] 目录作为小程序项目导入之后,点击工具栏下的 [代码]构建 npm[代码],即可预览效果。 效果 最后,我们来看一下 todo 的效果。kbone 初体验,done~ todo 代码可到 kbone/demo13 自提。 [图片] 最后 如果你想了解更多 kbone 相关的使用及详情,可移步 kbone github。 如有疑问,可到 Kbone小主页 发帖沟通。
2020-04-22 - 微信小程序 文字过多如何优化处理?
微信小程序开发,当文字过多的时候,想要超出三行的部分显示省略号并在文字下方显示出一个全文按钮,点击全文显示所有文字,那么各位大佬,我该如何处理,判断是否显示全文按钮?
2018-09-13 - 微信小程序开发UI组件样式库推荐
做为微信开发的程序员来说,写一些WXSS页面样式最头疼了。往往做出来的界面虽然功能一个不少,但显示的效果简陋而达不到用户满意。 我们推荐了以下几款微信小程序的组件库,可以让你不用懂WXSS也不用设计感,照样能做出很漂亮的小程序。 一、WeUIWEUI是一套基于样式库weui-wxss开发的小程序扩展组件库,同微信原生视觉体验一致的UI组件库,由微信官方设计团队和小程序团队为微信小程序量身设计,令用户的使用感知更加统一。 [图片] [图片] 官方组件库能够满足基础的界面需求,但是,如果你想要更加饱满的视觉,更加活泼的动效,恐怕 WeUI 就满足不了你的需要了。 GitHub 地址:https://github.com/Tencent/weui 二、ColorUI 组件库ColorUI 是一款高颜值组件库,侧重于视觉交互。比起 WeUI 的低调克制,ColorUI 色彩鲜亮,样式繁多。除了拥有非常丰富的原生组件的自定义样式,它还提供一些常见的页面元素,比如时间轴、步骤条、聊天页、模态窗口等等。 [图片] 这些页面元素通常应用在哪些场景下呢? 如果你想做一款诸如日记类、记账类、博客类、Vlog 类的小程序,这时就需要用到「时间轴」。 如果你想做一款涉及流程的小程序,比如物流跟踪,工作审批等,「步骤条」就可以派上用场了。 如果你想做一款社交类小程序,那么,当然少不得要用到「聊天」的界面。 而「模态窗口」则可以应用于各类小程序中出现弹框、侧边栏的地方。 [图片] 此外,ColorUI 还引入了插件扩展,也就是更为复杂的组件。目前已有的扩展包括索引列表、微动画、全屏抽屉以及垂直导航。引用这几项扩展,只需编写少量代码,就能实现较炫的视觉交互,进一步简化了开发工作。 [图片] 前面我们已经提到,ColorUI 是侧重于视觉交互的组件库,这方面的表现,还在于它为用户提供了色彩的搭配方案。打开「背景」,可以看到深色、淡色、渐变等多种配色。 [图片] ColorUI 还有许多值得推荐的地方。多样化的示例就是其中之一,它详尽地向用户展示了各种情况下,开发者可能需要编写的样式。 比如,打开「头像」,就会看到被一一列举的圆形头像、圆角矩形头像、各种尺寸头像、默认头像、文字头像、彩色头像、头像组、贴标签头像等等。一个这么简单的组件,也可以有许多种不同的呈现方式。 [图片] 又比如,打开「列表」,不仅可以看到宫格列表、菜单列表、消息列表、左滑列表等基本的样式,还可以设置一些可选项,像边框、箭头等,在细节处也有多种可选样式。 [图片] ColorUI 给大家提供了高度自定义的组件,一些比较麻烦的样式,开发者只需调用其组件就能得以实现。不过,ColorUI 也不是万能的,比如,它尚未涉及购物类小程序所需的组件。 GitHub 地址:https://github.com/weilanwl/ColorUI [图片] 三、Vant 组件库演示Vant 是由有赞发布的,轻量的小程序 UI 组件库。如果你想制作一款电商、餐饮、外卖平台、票务预订等购物类小程序,选用 Vant 是较为合适的。为什么这么说呢? [图片] 首先,我们来看「业务组件」这一块。可以看到,「商品卡片」与「提交订单栏」两个组件可以构成一个基本的「购物车」页面;而「商品卡片」与「商品导航」二者又可以组成一个简单的商店页面。 [图片] 我们再看看其他琐碎的组件,比如「表单组件」中的「评分」、「搜索」、「步进器」,都属于购物类小程序需要用到的组件。 [图片] 「导航组件」中的「徽章」与「展示组件」中的「分类选择」,都可以用于商品品类的选择切换。 [图片] 「展示组件」中的「折叠面板」与「面板」可以用作详细介绍商品的组件,「步骤条」则可以用于显示物流跟踪信息。 [图片] 使用 Vant 组件库,除了可以用常用的 Toast 方法,向用户弹出提醒消息,还可以引用「反馈组件」中的「消息通知」以及「展示组件」中的「通告栏」,向用户输出通知信息。 [图片] 除了以上可用于购物类小程序的组件,Vant 组件库当然还有那些比较通用基本元素、弹出层、Transition 动画等。值得一提的是,Vant 还支持自定义 Actionsheet,在「反馈组件」的「上拉菜单」中,有三种不同的自定义 Actionsheet。 [图片] Vant 对开发者非常友好,文档可以说是事无巨细了,而且在文档右侧,还可以预览样式哦。 开发文档:https://youzan.github.io/vant-weapp/#/intro GitHub 地址:https://github.com/youzan/vant-weapp [图片] 四、iViewUIiViewUI 是由 TalkingData 发布的组件库。作为一款好用的组件库,布局、面板、列表、表单、顶部导航栏、底部导航栏等组件当然必不可少,那么 iViewUI 除了具备这些标配的组件,还有哪些亮点呢? [图片] 在「导航」分类下,「分页」、「索引选择器」以及「吸顶容器」都是比较实用的组件。 其中,「索引选择器」与 ColorUI 中的「索引列表」是同类组件,不同的是,ColorUI 的「索引列表」中每一项可以包含图片、名字与描述,且支持搜索,而 iViewUI 的「索引选择器」中每一项只包含名字,且不支持搜索。 而「吸顶容器」在上文中尚未提及,这一组件适合用于分级长列表的显示。 [图片] 在「视图」分类下的「倒计时」一项中,提供了多种倒计时的显示格式。 [图片] iViewUI 同样有详细的文档,但是不支持网页预览,只能打开小程序预览。 开发文档:https://weapp.iviewui.com/docs/guide/start GitHub 地址:https://github.com/TalkingData/iview-weapp [图片] 五、MinUI 组件库MinUI 是由蘑菇街发布的组件库。与其他组件库不同的是,MinUI 更注重一些细节的处理。 [图片] 调用「基础元件」中的「文本截断」,可以控制长文本的显示行数,文本超长的用省略号结尾。「页底提示」可以用在上拉加载中的过程中。而「价格」则提供了各种样式的价格及货币符号。 [图片] 「功能组件」的「异常流展示」为开发者提供了各种异常状态下,向用户展示的界面。「遮罩层」则提供了各种效果的遮罩层,及其显示、隐藏方式。 [图片] 相比其他组件库,MinUI 将各种组件拆分得更细,真正使用时,需要开发者更多的对各个组件进行再次结合,但也因此 MinUI 显得更加通用。 开发文档:https://meili.github.io/min/docs/minui/index.html#README GitHub 地址:https://github.com/meili/min-cli [图片] 六、TaroUITaroUI 是由京东·凹凸实验室发布的多端 UI 组件库。这套组件库,可以在 H5、微信小程序、支付宝小程序、百度小程序多端适配运行。TaroUI 的整体风格简约、清新、统一,适合工具、读书、资讯、教育、商务等类型的小程序。 [图片] 除了拥有上文所提及的组件之外,TaroUI 还有几个特别的组件。在「表单」中有一项「范围选择器」,可以通过滑动条指定数值范围。在「高阶组件」中,可以显示「日历」,并且支持多种日期选择样式。 [图片] TaroUI 同样拥有健全的开发文档,也支持在网页中预览手机效果。 开发文档:https://taro-ui.aotu.io/#/docs/introduction GitHub 地址:https://github.com/NervJS/taro-ui [图片] 七、WuxUI这套组件库所包含的组件最为丰富。不仅我们前文提到的各类组件都可以在 Wux 中找到,而且还有进度环、骨架屏、筛选栏、数字键盘、结果页等实用工具类组件。如果你想开发一款工具类小程序,Wux 是个不错的选择。 [图片] 开发文档:https://wux-weapp.github.io/wux-weapp-docs/#/introduce GitHub 地址:https://github.com/wux-weapp/wux-weapp/ [图片] 这 7 款 UI 组件库各有所长,适合不同的小程序类型,Vant 适合电商类的,TaroUI 与 Wux 适合工具类的,而蘑菇街的 MinUI 当然更适合社区类的了。 大家可以根据自己的需求来选择相应的UI组件库来创建制作微信小程序。有微信小程序开发需求也可以联系云梁网络(https://www.yunliangwang.com)
2019-10-29 - 如何wx:for嵌套循环遍历两个不同的数组?其中已知两个数组中的id和categories值一致。
已知存在有两个固定写法的API数组(下面)。 其中cat.api里的id值等于post.api里的categories值。需要在前端页面循环显示分类和分类下的文章。第二个循环不知道如何关联写法。请大神们看看 [代码]<block wx:key=[代码][代码]"id"[代码] [代码]wx:for=[代码][代码]"{{categoriesList}}"[代码][代码]>[代码][代码] [代码][代码]<view class=[代码][代码]"list-item"[代码][代码]>[代码][代码] [代码][代码]<view class=[代码][代码]"content-title"[代码] [代码]data-item=[代码][代码]"{{item.name}}"[代码] [代码]data-id=[代码][代码]"{{item.id}}"[代码] [代码]bindtap=[代码][代码]"redictIndex"[代码][代码]>[代码][代码] [代码][代码]<view class=[代码][代码]"topic-name"[代码][代码]>[代码][代码] [代码][代码]<text>{{item.name}}</text>[代码][代码] [代码][代码]</view>[代码][代码] [代码][代码]</view>[代码][代码] [代码][代码]<view class=[代码][代码]"common-list"[代码][代码]>[代码][代码] [代码][代码]<block wx:key=[代码][代码]"id"[代码] [代码]wx:for=[代码][代码]"{{postsList}}"[代码][代码]>[代码][代码] [代码][代码]<image src=[代码][代码]"{{item.post_medium_image}}"[代码] [代码]mode=[代码][代码]"aspectFill"[代码] [代码]class=[代码][代码]"cover"[代码][代码]></image>[代码][代码] [代码][代码]<view class=[代码][代码]"content-title"[代码][代码]>[代码][代码] [代码][代码]<text>{{item.title.rendered}}</text>[代码][代码] [代码][代码]</view> [代码][代码] [代码][代码]</block>[代码][代码] [代码][代码]</view>[代码][代码] [代码][代码]</view>[代码][代码]</block>[代码] 需要的前端效果: [图片] 文章分类cat.api。其中cat.api里的id值等于post.api里的categories值。 [代码][[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"id"[代码][代码]: 112,[代码][代码] [代码][代码]"count"[代码][代码]: 7,[代码][代码] [代码][代码]"description"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"link"[代码][代码]: [代码][代码]"https://"[代码][代码],[代码][代码] [代码][代码]"name"[代码][代码]: [代码][代码]"分类CAT1"[代码][代码],[代码][代码] [代码][代码]"parent"[代码][代码]: 120,[代码][代码] [代码][代码]"meta"[代码][代码]: [],[代码][代码] [代码][代码]"category_thumbnail_image"[代码][代码]: [代码][代码]""[代码][代码] [代码][代码]},[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"id"[代码][代码]: 113,[代码][代码] [代码][代码]"count"[代码][代码]: 0,[代码][代码] [代码][代码]"description"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"link"[代码][代码]: [代码][代码]"https://"[代码][代码],[代码][代码] [代码][代码]"name"[代码][代码]: [代码][代码]"分类CAT2"[代码][代码],[代码][代码] [代码][代码]"parent"[代码][代码]: 120,[代码][代码] [代码][代码]"meta"[代码][代码]: [],[代码][代码] [代码][代码]"category_thumbnail_image"[代码][代码]: [代码][代码]""[代码][代码] [代码][代码]},[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"id"[代码][代码]: 126,[代码][代码] [代码][代码]"count"[代码][代码]: 4,[代码][代码] [代码][代码]"description"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"link"[代码][代码]: [代码][代码]"https://"[代码][代码],[代码][代码] [代码][代码]"name"[代码][代码]: [代码][代码]"分类CAT3"[代码][代码],[代码][代码] [代码][代码]"parent"[代码][代码]: 120,[代码][代码] [代码][代码]"meta"[代码][代码]: [],[代码][代码] [代码][代码]"category_thumbnail_image"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]}[代码][代码]][代码] 文章post.api [代码][[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"id"[代码][代码]: 1134,[代码][代码] [代码][代码]"date"[代码][代码]: [代码][代码]"2019-10-08T14:13:41"[代码][代码],[代码][代码] [代码][代码]"date_gmt"[代码][代码]: [代码][代码]"2019-10-08T06:13:41"[代码][代码],[代码][代码] [代码][代码]"type"[代码][代码]: [代码][代码]"post"[代码][代码],[代码][代码] [代码][代码]"link"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"title"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"文章1"[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"excerpt"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"DESC1"[代码][代码],[代码][代码] [代码][代码]"protected"[代码][代码]: false[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"author"[代码][代码]: 1,[代码][代码] [代码][代码]"categories"[代码][代码]: [[代码][代码] [代码][代码]112[代码][代码] [代码][代码]],[代码][代码] [代码][代码]"post_thumbnail_image"[代码][代码]: [代码][代码]"https://"[代码][代码],[代码] [代码] [代码][代码]},[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"id"[代码][代码]: 1131,[代码][代码] [代码][代码]"date"[代码][代码]: [代码][代码]"2019-10-08T14:13:41"[代码][代码],[代码][代码] [代码][代码]"date_gmt"[代码][代码]: [代码][代码]"2019-10-08T06:13:41"[代码][代码],[代码][代码] [代码][代码]"type"[代码][代码]: [代码][代码]"post"[代码][代码],[代码][代码] [代码][代码]"link"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"title"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"文章2"[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"excerpt"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"DESC2"[代码][代码],[代码][代码] [代码][代码]"protected"[代码][代码]: false[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"author"[代码][代码]: 1,[代码][代码] [代码][代码]"categories"[代码][代码]: [[代码][代码] [代码][代码]112[代码][代码] [代码][代码]],[代码][代码] [代码][代码]"post_thumbnail_image"[代码][代码]: [代码][代码]"https://"[代码][代码],[代码][代码] [代码][代码]},[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"id"[代码][代码]: 1128,[代码][代码] [代码][代码]"date"[代码][代码]: [代码][代码]"2019-10-08T14:13:41"[代码][代码],[代码][代码] [代码][代码]"date_gmt"[代码][代码]: [代码][代码]"2019-10-08T06:13:41"[代码][代码],[代码][代码] [代码][代码]"type"[代码][代码]: [代码][代码]"post"[代码][代码],[代码][代码] [代码][代码]"link"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"title"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"文章3"[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"excerpt"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"DESC3"[代码][代码],[代码][代码] [代码][代码]"protected"[代码][代码]: false[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"author"[代码][代码]: 1,[代码][代码] [代码][代码]"categories"[代码][代码]: [[代码][代码] [代码][代码]113[代码][代码] [代码][代码]],[代码][代码] [代码][代码]"post_thumbnail_image"[代码][代码]: [代码][代码]"https://"[代码][代码],[代码][代码] [代码][代码]},[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"id"[代码][代码]: 1125,[代码][代码] [代码][代码]"date"[代码][代码]: [代码][代码]"2019-10-08T14:13:41"[代码][代码],[代码][代码] [代码][代码]"date_gmt"[代码][代码]: [代码][代码]"2019-10-08T06:13:41"[代码][代码],[代码][代码] [代码][代码]"type"[代码][代码]: [代码][代码]"post"[代码][代码],[代码][代码] [代码][代码]"link"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"title"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"文章4"[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"excerpt"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"DESC4"[代码][代码],[代码][代码] [代码][代码]"protected"[代码][代码]: false[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"author"[代码][代码]: 1,[代码][代码] [代码][代码]"categories"[代码][代码]: [[代码][代码] [代码][代码]113[代码][代码] [代码][代码]],[代码][代码] [代码][代码]"post_thumbnail_image"[代码][代码]: [代码][代码]"https://"[代码][代码],[代码][代码] [代码][代码]},[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"id"[代码][代码]: 1022,[代码][代码] [代码][代码]"date"[代码][代码]: [代码][代码]"2019-10-08T14:13:41"[代码][代码],[代码][代码] [代码][代码]"date_gmt"[代码][代码]: [代码][代码]"2019-10-08T06:13:41"[代码][代码],[代码][代码] [代码][代码]"type"[代码][代码]: [代码][代码]"post"[代码][代码],[代码][代码] [代码][代码]"link"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"title"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"文章5"[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"excerpt"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"DESC5"[代码][代码],[代码][代码] [代码][代码]"protected"[代码][代码]: false[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"author"[代码][代码]: 1,[代码][代码] [代码][代码]"categories"[代码][代码]: [[代码][代码] [代码][代码]126[代码][代码] [代码][代码]],[代码][代码] [代码][代码]"post_thumbnail_image"[代码][代码]: [代码][代码]"https://"[代码][代码],[代码][代码] [代码][代码]},[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"id"[代码][代码]: 1075,[代码][代码] [代码][代码]"date"[代码][代码]: [代码][代码]"2019-10-08T14:13:41"[代码][代码],[代码][代码] [代码][代码]"date_gmt"[代码][代码]: [代码][代码]"2019-10-08T06:13:41"[代码][代码],[代码][代码] [代码][代码]"type"[代码][代码]: [代码][代码]"post"[代码][代码],[代码][代码] [代码][代码]"link"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"title"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"文章6"[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"excerpt"[代码][代码]: {[代码][代码] [代码][代码]"rendered"[代码][代码]: [代码][代码]"DESC6"[代码][代码],[代码][代码] [代码][代码]"protected"[代码][代码]: false[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"author"[代码][代码]: 1,[代码][代码] [代码][代码]"categories"[代码][代码]: [[代码][代码] [代码][代码]126[代码][代码] [代码][代码]],[代码][代码] [代码][代码]"post_thumbnail_image"[代码][代码]: [代码][代码]"https://"[代码][代码],[代码][代码] [代码][代码]}[代码][代码]][代码]
2019-10-15 - 小程序顶部自定义导航组件实现原理及坑分享
为什么使用自定义导航 对比默认导航栏,我们会更需要: 统一Android、IOS手机对于页面title的展示样式及位置 更丰富的导航栏定制效果,如添加home图标等 左上角返回事件的监听处理 统一实现双击返回顶部功能 自定义导航组件实现思路 自定义导航组件实现的核心是需要计算导航栏的真实高度 这里以官方文档->扩展能力中的Navigation组件为例分析实现思路。当使用"navigationStyle": "custom"时,默认导航被移除,页面的开始位置变成了屏幕顶部,这时我们需要实现的导航栏是在状态栏下面。 导航栏的真实高度=状态栏高度+导航栏内容。 [图片] 使用wx.getSystemInfo获取到statusBarHeight便是导航栏的高度,但是导航栏内容高度呢? 有人可能觉得导航栏内容高度顾名思义就是导航栏内容高度啊,内容撑起还用管嘛!要,必须要! 因为右上角胶囊按钮是原生加载的,我们的导航栏内容需要正好贴在胶囊的下方且垂直居中。 导航栏内容高度=(胶囊按钮的顶部距离 - 状态高度)*2 + 胶囊高度 [图片] 如何计算胶囊的数据呢?幸运的是我们有 wx.getMenuButtonBoundingClientRect() 获取胶囊按钮的布局位置信息,那么动态计算导航栏的内容高度就很方便啦。 好了,以上就是动态计算的核心思路,我们再来看官方Navigation组件高度是怎么实现的 [代码]page{--height:44px;--right:190rpx;} .weui-navigation-bar .android{--height:48px;--right:222rpx} .weui-navigation-bar__inner{ position:fixed;top:0;left:0;z-index:5001;display:flex;align-items:center; height:var(--height);padding-right:var(--right);width:calc(100% - var(--right)) } [代码] 导航栏内容的高度是通过- -height这个css变量提前声明好的,安卓机型会重新覆盖为新的css变量值,目前没发现有适配问题。 官方就是官方啊,具体尺寸都知道,那就不用一番计算周折啦,直接拿来主义即可。 导航的布局位置已经搞定啦,剩下就是写具体的内容,不同业务实现需求不同这里就不一一赘述了。 完善官方顶部导航组件 本着拿来主义,直接使用官方Navigation组件,但在实际业务开发中还是遇到不少需要自定义的需求,就比如: loadding样式没实现 标题内容超出没有出现省略号 和原生顶部的样式不兼容,导致单个页面引入时跳转有明显差异出现 没有双击返回顶部功能开关功能 引入页面需要获取导航栏的高度,来控制其他元素距离顶部的位置, 不能根据页面栈数据动态显示隐藏back按钮, 针对以上需求,我们对官方的组件进行二次完善开发,满足常规的自定义需求绰绰有余,直接引入开箱即用。 源码使用示例 https://github.com/YuniorZen/minicode-debug/tree/master/minicode02 [图片] 使用说明 [代码]/*自定义头部导航组件,基于官方组件Navigation开发。*/ <navigation-bar title="会员中心" bindgetBarInfo="getBarInfo"></navigation-bar> [代码] 组件属性列表 属性 类型 描述 bindgetBarInfo function 组件实例载入页面时触发此事件,首参为event对象,event.detail携带当前导航栏信息,如导航栏高度 event.detail.topBarHeight bindback function 点击back按钮触发此事件响应函数 backImage string back按钮的图标地址 homeImage string home按钮的图标地址 ext-class string 添加在组件内部结构的class,可用于修改组件内部的样式 title string 导航标题,如果不提供为空 background string 导航背景色,默认#ffffff color string 导航字体颜色 dbclickBackTop boolean 是否开启双击返回顶部功能,默认true border boolean 是否显示顶部导航下边框分割线,默认false loading boolean 是否显示标题左侧的loading,默认false show boolean 显示隐藏导航,隐藏的时候navigation的高度占位还在,默认true left boolean 左侧区域是否使用slot内容,默认false center boolean 中间区域是否使用slot内容,默认false Slot name 描述 left 左侧slot,在back按钮位置显示,当left属性为true的时候有效 center 标题slot,在标题位置显示,当center属性为true的时候有效 自定义顶部导航目前存在的坑 弹窗的背景蒙层无法覆盖原生胶囊按钮 页面下拉刷新的圆点会被自定义导航遮盖 如果要自定义顶部导航,以上问题避免不了,只能忍着接受。 目前还没遇到完美的解决方案,针对下拉刷新圆点被遮挡的问题微信官方还在需求开发中,如果你有好的想法欢迎留言反馈,一起学习交流。
2019-10-31 - 小程序生成分享图片方案分享
小程序生成图片分享朋友圈 小程序开发者都希望自己的小程序得以广泛传播,因为不少小程序都设计了很多转发激励行为,但分享小程序到朋友圈(或其他外部平台)一直是一个难题。一个常见的方案就是生成分享海报、分享图片。但生成分享图片在技术上却也是一个难题。 技术选型 目前常用技术方案基本分为三种: 使用 canvas 绘图并生成 使用后端绘图库进行绘制,返回给小程序端 使用服务端开一个浏览器进行 HTML 渲染,并截图返回给小程序端 第一种方案:要求较高,canvas 和纯 html 布局相去甚远,零基础学习成本较高,而且在不同的微信浏览器中效果不可预期,想短时间内做出精美可控的生成图片不容易。实操的时候发现了一个非常麻烦的事情:网络图片或者 base64 图片都无法直接在 canvas 里渲染显示,要先下载好传进去。 第二种方案:后端库可以完成较为简单的需求,但字体加载、阴影、圆角、透明等方案效果需要精调,如果文字需要截断或动态伸缩长度时并不容易处理。图片的截取和伸缩自适应也不灵活。而且选用这种方案相当于需要把 UI 布局的工作丢给后端工程师去解决,这不是他们擅长的范围,效果未必会好。 第三种方案:页面的绘制方面,纯前端技术即可完成,难度低,完成度高,但是需要在后端起一个 node 服务开启 puppeteer 去控制服务端 Chrome 浏览器。这种方案的缺点就是成本太高,我们和业界同行都测算过,结果差不多:4 核 16G 的服务器生成图片的 QPS 大概只有 10-20,相当于一秒钟较差情况只能生成 10 张图片,这对于突发的大量分享需求并不能满足,而且这种配置的服务器,不能部署其他服务,只跑这个服务就会用尽大部分资源。 费用上:只单单算 5M 带宽的服务器费用一个月就要 700+ 人民币,流量和图片托管费用另算。此方案的最小化实现:至少需要 1 核 2G 的服务器才能较为顺畅地完成一次顺利截图,但是还是要处理浏览器无响应假死等情况,较为复杂。但综合来看,这种方案是效果最好最为灵活的。 快海报小程序分享图生成服务 快海报 kuaihaibao.com 是专门提供小程序分享海报生成服务的,技术上用的就是上面所述的第三种方案,但是只需要调用他的 API 就可以完成,不需要开发者维护 puppeteer 和 headless Chrome,而且成本较低,一张分享图的最低生成成本是 0.01 元。 其实真正集成到自己的服务中时,平均成本要比这个低,因为有些生成的图片的二维码,如果不带用户个人信息(不给分享的用户返利)时,可以生成一次之后永久缓存起来,其他用户再分享同一个东西都用缓存好的图片,综合成本就降下来了。 算一下成本: 比方说一个刚起步的小程序日活 5000(对于刚起步的小程序其实已经很高了吧) 假设有 5% 的用户生成分享图 也就是每天生成 250 张分享图,一个月会生成 7500 张分享图 这样的话每个月成本就是 75 元人民币左右,相比 700+ 人民币的服务器成本省太多了。这是测算比较高的指标,而且是完全不应用缓存方案的情况。 如果你的小程序还处于冷启动的阶段: 日活 500 假设有 5% 的用户生成分享图 也就是每天生成 25 张分享图,一个月会生成 750 张分享图 每月成本 7.5 元。比 1 核 2G 的最小化自部署方案也要便宜。但带来的收益是无穷的,750 张分享图发到朋友圈,每张分享图 1000 受众浏览,一个月就是将近 750000 人次分享受众。 调用 API 首先去 https://kuaihaibao.com/ 注册账号,验证邮箱激活之后,其实就可以先测试用了,每个账号有 100 次测试额度,测试生成的图片带水印。 网站左侧的 [文档] 页面能找到集成文档,非常简单,一共就只有一个核心 API,通过 HTTP 调用的。 先在【开发】->【设置】中激活 token [图片] 目前支持三种生成方式: 直接传 URL 进行渲染 传 HTML 渲染 使用内置的模板进行选择 这里演示使用模板渲染,因为比较简单 打开 【开发】->【模板】中,找到自己喜欢的模版。因为我只想生成一个简单的分享图片,所以最简单的方式就是使用网站内置的模版,内置模板目前有 8 款,应该能满足大部分小程序的需求了,抽奖、打卡、图文、文字、电商都有,改一改文案和图片就可以了。 我选了这个抽奖模板: [图片] 按照 https://kuaihaibao.com/doc/docs/template/kzccda95.html 文档描述的 JSON 改成我需要的: [代码]{ "backgroundColor": "#fafafa", "backgroundImage": "", "user": { "avatar": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/girl_2.jpg", "nickname": "我是测试账号", "color": "#666" }, "tip": "邀请你来抽奖", "qrcode": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/sample_qr_0.png", "records": [ { "title": "一等奖", "desc": "2019 年 11 月 16 日 10:00 开奖", "image": "https://s3.cn-northwest-1.amazonaws.com.cn/res.weiyidan.com/production/10000002/4109f8e51a8f43b9816dbc8fe636e22a.jpeg" } ], "brand": "我的测试抽奖小程序", "slogan": "快来和我一起抽吧!", "metaColor": "#999" } [代码] 然后打开 Terminal 做一次请求试试: [代码]curl -X "POST" "https://api.kuaihaibao.com/services/screenshot" \ -H 'Authorization: Bearer 这里写你自己的 token' \ -H 'Content-Type: application/json; charset=utf-8' \ -d /pre>{ "template": "kzccda95", "data": { "qrcode": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/sample_qr_0.png", "records": [ { "title": "一等奖", "desc": "2019 年 11 月 16 日 10:00 开奖", "image": "https://s3.cn-northwest-1.amazonaws.com.cn/res.weiyidan.com/production/10000002/4109f8e51a8f43b9816dbc8fe636e22a.jpeg" } ], "tip": "邀请你来抽奖", "slogan": "快来和我一起抽吧!", "metaColor": "#999", "brand": "我的测试抽奖小程序", "backgroundImage": "", "backgroundColor": "#fafafa", "user": { "avatar": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/girl_2.jpg", "nickname": "我是测试账号", "color": "#666" } } }' [代码] 返回了结果: [代码]{ "success": true, "data": { "name": "iPhone 5", "image": "https://khb-test-oss.oss-cn-shanghai.aliyuncs.com/screenshot/4fa63f2a3605cbdece90c659cbccea619d9cf9fa?x-oss-process=style/test_watermark" } } [代码] 打开图片地址看看: [图片] 速度很快,图片很漂亮,只是中间带水印,充值后成为付费用户,再生成的图片水印就自动取掉了。 后端集成 这里参考快海报官方给的最佳实践的逻辑参考图: [图片] 所以后端只需要做一件事,就是提供一个 API 给客户端用,这个 API 被调用的时候去请求快海报的服务器,再把结果返回给小程序就好了。
2020-10-20 - [打怪升级]小程序自定义头部导航栏“完美”解决方案
[图片] 为什么要做这个? 主要是在项目中,智酷君发现的一些问题 一些页面是通过扫码和订阅消息访问后,没有直接可以点击去首页的,需要添加一个home链接 需要添加自定义搜索功能 需要自定义一些功能按钮 [图片] 其实,第一个问题,在最近的微信版本更新中已经优化了,通过 小程序模板消息 过来的,系统会自动加上home按钮,但对于其他的访问方式则没有支持~ 一个不大不小的问题:两边ICON不对齐问题 [图片] 智酷君之前尝试了各种解决方法,发现有一个问题,就是现在手机屏幕太多种多样,有 传统头部、宽/窄刘海屏、水滴屏等等,无法八门,很多解决方案都无法解决特殊头部,系统**“胶囊按钮”** 和 自定义按钮在Android屏幕可能有 几像素不对齐 的问题(强迫症的噩梦)。 下面分享下一个相对比较完善的解决方案: [图片] 小程序代码段DEMO Link: https://developers.weixin.qq.com/s/cuUaCimT72cH ID: cuUaCimT72cH 智酷君做了一个demo代码段,方便大家直接用IDE工具查看源码~ [图片] 页面配置 1、页面JSON配置 [代码]{ "usingComponents": { "NavComponent": "/components/nav/common" //以插件的方式引入 }, "navigationStyle": "custom" //自定义头部需要设置 } [代码] 如果需要自定义头部,需要设置navigationStyle为 “custom” 2、页面代码 [代码]<!-- home 类型的菜单 --> <NavComponent v-title="自定义头部" bind:commonNavAttr="commonNavAttr"></NavComponent> <!-- 搜索菜单 --> <NavComponent is-search="true" bind:commonNavAttr="commonNavAttr"></NavComponent> [代码] 可以在自定义导航标签上添加属性配置来设置功能,具体按照实际需要来 3、目录结构 [代码]│ ├─components │ └─nav │ common.js │ common.json │ common.wxml │ common.wxss │ ├─images │ back.png │ home.png │ └─index index.js index.json index.wxml index.wxss search.js search.json search.wxml search.wxss [代码] 仅供参考 插件对应的JS部分 components/nav/common.js部分 [代码]const app = getApp(); Component({ properties: { vTitle: { type: String, value: "" }, isSearch:{ type: Boolean, value: false } }, data: { haveBack: true, // 是否有返回按钮,true 有 false 没有 若从分享页进入则没有返回按钮 statusBarHeight: 0, // 状态栏高度 navbarHeight: 0, // 顶部导航栏高度 navbarBtn: { // 胶囊位置信息 height: 0, width: 0, top: 0, bottom: 0, right: 0 }, cusnavH: 0, //title高度 }, // 微信7.0.0支持wx.getMenuButtonBoundingClientRect()获得胶囊按钮高度 attached: function () { if (!app.globalData.systeminfo) { app.globalData.systeminfo = wx.getSystemInfoSync(); } if (!app.globalData.headerBtnPosi) app.globalData.headerBtnPosi = wx.getMenuButtonBoundingClientRect(); console.log(app.globalData) let statusBarHeight = app.globalData.systeminfo.statusBarHeight // 状态栏高度 let headerPosi = app.globalData.headerBtnPosi // 胶囊位置信息 console.log(statusBarHeight) console.log(headerPosi) let btnPosi = { // 胶囊实际位置,坐标信息不是左上角原点 height: headerPosi.height, width: headerPosi.width, top: headerPosi.top - statusBarHeight, // 胶囊top - 状态栏高度 bottom: headerPosi.bottom - headerPosi.height - statusBarHeight, // 胶囊bottom - 胶囊height - 状态栏height (胶囊实际bottom 为距离导航栏底部的长度) right: app.globalData.systeminfo.windowWidth - headerPosi.right // 这里不能获取 屏幕宽度,PC端打开小程序会有BUG,要获取窗口高度 - 胶囊right } let haveBack; if (getCurrentPages().length != 1) { // 当只有一个页面时,并且是从分享页进入 haveBack = false; } else { haveBack = true; } var cusnavH = btnPosi.height + btnPosi.top + btnPosi.bottom // 导航高度 console.log( app.globalData.systeminfo.windowWidth, headerPosi.width) this.setData({ haveBack: haveBack, // 获取是否是通过分享进入的小程序 statusBarHeight: statusBarHeight, navbarHeight: headerPosi.bottom + btnPosi.bottom, // 胶囊bottom + 胶囊实际bottom navbarBtn: btnPosi, cusnavH: cusnavH }); //将实际nav高度传给父类页面 this.triggerEvent('commonNavAttr',{ height: headerPosi.bottom + btnPosi.bottom }); }, methods: { _goBack: function () { wx.navigateBack({ delta: 1 }); }, bindKeyInput:function(e){ console.log(e.detail.value); } } }) [代码] 解决不同屏幕头部不对齐问题的终极办法是 wx.getMenuButtonBoundingClientRect() 这个方法从微信7.0.0开始支持,通过这个方法我们可以获取到右边系统胶囊的top、height、right等属性,这样无论是水滴屏、刘海屏、异形屏,都能完美对齐右边系统默认的胶囊bar,完美治愈强迫症~ APP.js 部分 [代码]//app.js App({ /** * 加载页面 * @param {*} options */ onShow: function (options) { }, onLaunch: async function () { let self = this; //设置默认分享 this.globalData.shareData = { title: "智酷方程式" } // this.getSysInfo(); }, globalData: { //默认分享文案 shareData: {}, qrCodeScene: false, //二维码扫码进入传参 systeminfo: false, //系统信息 headerBtnPosi: false, //头部菜单高度 } }); [代码] 将获取的参数存储在一个全局变量globalData中,可以减少反复调用的性能消耗。 插件HTML部分 [代码]<view class="custom_nav" style="height:{{navbarHeight}}px;"> <view class="custom_nav_box" style="height:{{navbarHeight}}px;"> <view class="custom_nav_bar" style="top:{{statusBarHeight}}px; height:{{cusnavH}}px;"> <!-- 搜索部分--> <block wx:if="{{isSearch}}"> <input class="navSearch" style="height:{{navbarBtn.height-2}}px;line-height:{{navbarBtn.height-4}}px; top:{{navbarBtn.top+1}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;" maxlength="10" bindinput="bindKeyInput" placeholder="输入文字搜索" /> </block> <!-- HOME 部分--> <block wx:else> <view class="custom_nav_icon {{!haveBack||'borderLine'}}" style="height:{{navbarBtn.height}}px;line-height:{{navbarBtn.height-2}}px; top:{{navbarBtn.top}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;"> <view wx:if="{{haveBack}}" class="icon-back" bindtap='_goBack'> <image src='/images/back.png' mode='aspectFill' class='back-pre'></image> </view> <view wx:if="{{haveBack}}" class='navbar-v-line'></view> <view class="icon-home"> <navigator class="home_a" url="/pages/home/index" open-type="switchTab"> <image src='/images/home.png' mode='aspectFill' class='back-home'></image> </navigator> </view> </view> <view class="nav_title" style="height:{{cusnavH}}px; line-height:{{cusnavH}}px;"> {{vTitle}} </view> </block> </view> </view> </view> [代码] 主要是对几种状态的判断和定位的计算。 插件CSS部分 [代码]/* components/nav/test.wxss */ .custom_nav { width: 100%; background: #3a7dd7; position: relative; z-index: 99999; } .custom_nav_box { position: fixed; width: 100%; background: #3a7dd7; z-index: 99999; border-bottom: 1rpx solid rgba(255, 255, 255, 0.3); } .custom_nav_bar { position: relative; z-index: 9; } .custom_nav_box .nav_title { font-size: 28rpx; color: #fff; text-align: center; position: absolute; max-width: 360rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; top: 0; left: 0; right: 0; bottom: 0; margin: auto; z-index: 1; } .custom_nav_box .custom_nav_icon { position:absolute; z-index: 2; display: inline-block; border-radius: 50%; vertical-align: top; font-size:0; box-sizing: border-box; } .custom_nav_box .custom_nav_icon.borderLine { border: 1rpx solid rgba(255, 255, 255, 0.3); background: rgba(0, 0, 0, 0.1); } .navbar-v-line { width: 1px; margin-top: 14rpx; height: 32rpx; background-color: rgba(255, 255, 255, 0.3); display: inline-block; vertical-align: top; } .icon-back { display: inline-block; width: 74rpx; padding-left: 20rpx; vertical-align: top; /* margin-top: 12rpx; vertical-align: top; */ height: 100%; } .icon-home { /* margin-top: 8rpx; vertical-align: top; */ display: inline-block; width: 80rpx; text-align: center; vertical-align: top; height: 100%; } .icon-home .home_a { height: 100%; display: inline-block; vertical-align: top; width: 35rpx; } .custom_nav_box .back-pre, .custom_nav_box .back-home { width: 35rpx; height: 35rpx; vertical-align: middle; } .navSearch { width: 200px; background: #fff; font-size: 14px; position: absolute; padding: 0 20rpx; z-index: 9; } [代码] 总结: 通过微信API: getMenuButtonBoundingClientRect(),结果各类手机屏幕的适配问题 将算好的参数存储在全局变量中,一次计算全局使用,爽YY~ 往期回顾: [填坑手册]小程序PC版来了,如何做PC端的兼容?! [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二)
2021-09-13