个人案例
- wx.openOfflinePayView接口 还能申请吗?
wx.openOfflinePayView接口 还能申请吗?
07-12 - taiwindcss在小程序中的应用
css 工程化思想有很多(sass,less,css-in-js…),基于原子类书写思想的 taiwindcss(以下用 tw 代替)风头很盛,去年底 tw3 上线,搭配新的引擎让[代码]just-in-time[代码]模式平稳落地。 tw 的思想和好处这里不在赘述,官网介绍的很清楚(tw2 中文官网)。 这里主要描述下如何在小程序中使用tw书写样式。 题外话:很难想象前端最佳实践到来的那一天,但 css 最佳实现或许已初见苗头… tw书写小程序 tw3 是即时(实时)生成类样式。tw 会根据您的配置文件,匹配您指定文件中包含的关键字,自动生成对应配置的样式。 在小程序中,只需要在 app.wxss 中引入 tw 生成的样式文件(记得组件中配置 addGlobalClass:true),就不必在书写组件时,关心 wxss 文件了。 base类 base类好比预设一些样式 (由于官方默认配置基于web端,小程序中只能自己定义了) [代码] /* wxss */ page { height: 100%; line-height: 1.2; -webkit-text-size-adjust: 100%; font-family: "fontFamily.sans", ui-sans-serif, system-ui" } [class], view, image, ::before, ::after{ box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; } /* variables */ page{ --color-primary:#22c55e; --color-primary-dark: #16a34a; /* ... */ } @media (prefers-color-scheme: dark) { page { --color-primary:#ef4444; --color-primary-dark: #dc2626; /* ... */ } } [代码] utilities(工具类) tw 中最核心的思想(工具类 css,也有叫原子化 css),大多数内部模块可以基于官方默认配置,有些还需要自定义配置。 Demo1 [代码] <view> <text class="text-red text-40 ml-10 px-20 "> 我的字体颜色是红色,大小为40rpx,左外边距10rpx,内边距x轴20rpx</text > </view> <!-- wxss新增 .text-red{ color:red }, .text-40{ font-size: 40rpx }, .ml-10{ margin-left:10rpx } .px-20{ padding: 0 20rpx } --> [代码] components(组件类) 有时几个工具类样式会经常出现,我们可以把他们自定义为组件类(比如下面 Demo3 中的[代码]ellipsis[代码]类) Demo2 [代码] <view> <button class="bg-primary hover_bg-primary-dark inline-block w-100 h-60 whitespace-nowrap overflow-hidden overflow-ellipsis" > 工具类写法--按钮1 </button> </view> <view> <button class="btn ellipsis">组件类写法--按钮2</button> </view> <!-- wxss新增 .bg-primary{ color:var(--color-primary) } .hover_bg-primary-dark:hover{ color:var(--color-primary-dark) } .inline-block { display: inline-block; } .w-100 { width: 100rpx; } .h-60 { height: 60rpx; } .whitespace-nowrap{ white-space: nowrap; } .overflow-hidden{ overflow: hidden; } .overflow-ellipsis{ text-overflow: ellipsis; } .btn{ // 组件类 color:var(--color-primary); display: inline-block; width: 100rpx; height: 60rpx; } .btn:hover{ // 组件类 color:var(--color-primary-dark); } .ellipsis { // 组件类 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } --> [代码] 不必纠结这么多工具类样式很难记,因为这些类都基于原生样式且可以自定义的,搭配官方插件 Tailwind CSS IntelliSense还可以得到代码提示(小程序编辑器也支持)。 先写这么多,不知道是否有人关注tw在小程序中的应用,下篇预期发具体核心类编译配置,如果您感兴趣请点赞支持。
2022-01-29 - wxml-to-canvas 使用规则
先说结论: 仅适用生成简单布局的页面,复杂页面会有大量性能开销。 复杂页面可以其他使用webview嵌入的方式用原生H5代替。 安装使用见官方文档,本文主要提及重要使用规则。 需要为wxml-to-canvas指定width和height属性,默认为400*300,当该尺寸与wxss最大容器尺寸不一致时会有异常只能使用view、text、image标签view只用于布局、text只用于文字、image只用于图片(可绝对定位作为背景)text、image必须指定width、heightview可以不指定height、只指定width,由子元素动态撑高。渲染高度限制:IOS实际像素 < 4096,实际高度需 < 4096/3 = 1356动态数据可通过函数结合模板字符串实现画布高度最好是确定的数字(<1356)lineHeight可使text文本居中,lineHeight值等于height即可多个absolute元素时,因为没有z-index,template元素自上而下渲染,对应z-index依次增高元素默认为flex布局,子元素会继承父元素的flex布局属性(row或column)背景颜色仅可使用backgroundColor,而非background。性能优化指南 在canvas绘制完成后,可以使用this.widget.canvasToTempFilePath来保存并获取本地临时路径,但是图片可能会比较大,通常会1-2M,实际我们应该尽可能在保证清晰的情况下减小文件大小,我们可以通过canvasToTempFilePath的参数配置来进行调整,如下通常可以保持较小的体积及基本合适的清晰度。更多的配置可详细参考canvasToTempFilePath文档{ destWidth: canvas组件的宽度, destHeight: canvas组件的高度, fileType: 'jpg', quality: 0.9 } 以下是动态数据与动态高度及基本使用的示例: //动态wxml : poster-data.js const getWxml = (info)=>{ return `${info.a?`${info.b}`:''}` } //动态css const getStyle = (info)=>{ return { textTest: { width: 100, height: 20, fontSize: info.fontSize } } } //页面使用 : index.js const { wxml, style } = require('./poster-data.js'); //... data:{ canvasHeight: 0, info:{ a:1, b:2, fontSize: 16 } } onLoad(){ const tempHeight = this.computeContentHeight() this.setData({ canvasHeight:tempHeight },()=>{ this.widget = this.selectComponent('.widget');//数据就绪后canvas高度已确定,再获取canvas组件实例 }) } computeContentHeight(){ //do something 根据数据动态现实隐藏而修改canvas高度 return 500; } saveImage(){ wx.showLoading({ title: '生成中请稍后.', mask: false, }); const template = getWxml(this.data.info);//获取template const css = getStyle(this.data.info)//获取canvas const p1=this.widget.renderToCanvas({wxml:template,style:css}); p1.then((res)=>{ const p2 =this.widget.canvasToTempFilePath();//canvas图片保存到本地临时路径 p2.then(res=>{ wx.hideLoading(); this.data.poster = res.tempFilePath // 使用微信分享 wx.showShareImageMenu({ path: res.tempFilePath }); // 或保存到本地 // wx.saveImageToPhotosAlbum({ // filePath: res.tempFilePath, // success(res){ // console.log(res); // }, // fail(res){ // console.log(res); // } // }); }); }); } //隐藏canvas .widget { position: absolute; left: -999px; } 参考 小程序中实现页面截图 | 微信开放社区wxml-to-canvas 组件中高度不能超过 4096?,否则无法生成图片,报错 | 微信开放社区微信扩展组件wxml-to-canvas支持动态传入数据吗?急 | 微信开放社区小程序使用wxml-to-canvas截屏_培根芝士的博客-CSDN博客_小程序 截屏
2023-08-01 - 「笔记」订阅消息-订阅次数维护
前言 距离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 - 小程序中使用npm安装vant组件实现按需引入减少代码包大小,避免触发用户隐私协议
在小程序中使用 vant 组件库主要有以下两种方式: 下载源代码包放入项目中,可以自己删掉没用到的组件,不过后期只能自己手动更新,会不太好维护 通过 npm 的方式安装管理依赖,后期更新可以直接交给 npm 来管理,方便维护 正常项目中我们可能都会选择 npm 的方式,但是这种方式 vant 和小程序并不支持像我们一般的前端项目中的按需引入,小程序开发工具构建 npm 时会把整个 vant 的组件编译到 miniprogram_npm 目录中,即使我们在项目中没有通过 usingComponents 申明引用的组件也会被打包进代码包中。 [图片] 减少代码包大小 因为小程序主包有 2M 的限制,如果我们本身只用到了几个组件,最终却打包进了整个组件库,这样不仅不合理也额外占用了咱小程序的包大小。想要按需引入的办法只能自己手动去把 miniprogram_npm 目录中没用到的组件删掉,然后再打包上传。不过每次我们提交版本都要这样去操作的话,不光容易出错也很费时间,这里我们就可以借助 node 和 npm 的 script 脚本来自动处理。 大体思路就是,先直接用 node 去自动扫描读取项目中所有 json 文件 中的 usingComponents,找出项目中实际有用到的 vant 组件,然后再去 miniprogram_npm/@vant/weapp 目录下将没有用到的多余组件删除掉就行了,最后直接把相关代码放到项目中的 script 脚本中操作,这样通过类似 npm run vant 这样一条命令 1 秒钟就可以删除掉未使用到的多余组件,实现了按需引入了。 未使用到的 vant 组件也会触发隐私协议 除了减少代码包大小这一项外,其实还有一个更大的痛点,vant 的部分组件会自动触发小程序的隐私协议,比如上传组件 uploader 中用到的:收集你选中的照片或视频信息(wx.chooseImage、wx.chooseMedia、wx.chooseVideo)、收集你选中的文件(wx.chooseMessageFile),这类 api 会自动触发隐私协议授权。 即使你的项目中压根没使用这类组件,上传版本提审的时候小程序还是会自动扫描你 miniprogram_npm 目录下的所有文件,只要代码中有相关的 api 代码就会认为你用到了,然后霸道地强制要求你填写和更新相关隐私说明,随便瞎填一个 99% 会被拒,也不能填写项目中未使用,这样那你自己说未使用就会让你先去把项目中相关的代码删掉再来提审。 vant-tree-shaking 为了方便使用,脚本代码已经封装成了一个 npm 包 vant-tree-shaking(https://www.npmjs.com/package/vant-tree-shaking )上传到了 npm 公共仓库中,大家可以直接通过 npm 来下载使用: 全局安装 [代码]npm install -g vant-tree-shaking [代码] 在小程序开发者工具中上传小程序代码前,直接在项目根目录终端中运行命令:vant-tree-shaking,成功后会在控制台打印出:vant-tree-shaking success。 本地安装 [代码]npm install -D vant-tree-shaking [代码] 需要自己在 package.json 配置文件中配置 script 脚本命令,如直接配置自定义命令 vant: [代码]{ "name": "miniapp", "version": "1.0.0", "main": "app.js", "scripts": { "vant": "vant-tree-shaking", }, "dependencies": { "@vant/weapp": "^1.11.5" }, "devDependencies": { "vant-tree-shaking": "^1.0.0" } } [代码] 在小程序开发者工具中上传小程序代码前,直接在项目根目录终端中运行命令:npm run vant,成功后会在控制台打印出:vant-tree-shaking success。 核心代码 主要用到了 node 的 fs、path 这两个模块,来处理文件、目录读取删除和路径的处理,其实也很简单,完整的源码可以参考 github 仓库(https://github.com/cafehaus/vant-tree-shaking ),有什么使用问题或建议也欢迎大家积极反馈。 [代码]// 1、扫描项目中的所有 json 文件,找出项目中使用到的所有 vant 组件 function readFile(filePath) { if (!filePath || !fs.existsSync(filePath)) return if (fs.statSync(filePath).isDirectory()) { let files = fs.readdirSync(filePath) || [] files.map(m => { if (!IGNORE_DIRS.includes(m)) { let curPath = path.join(filePath, m) readFile(curPath) } }) } else { getUsingComponents(filePath, vantSet) } } // 2、项目中使用到的 vant 组件依赖的其他 vant 组件 function readVantDir() { const files = fs.readdirSync(VANT_PATH) || [] files.map(m => { if (!COMMON_DIRS.includes(m) && vantSet.has(`van-${m}`)) { const curPath = path.join(VANT_PATH, m, 'index.json') getUsingComponents(curPath, dependentSet) } }) // 3、删除未使用到 vant 组件目录 const usedVant = new Set([...vantSet, ...dependentSet]) for (let i = files.length - 1; i >= 0; i--) { const cur = files[i] if (!COMMON_DIRS.includes(cur) && !usedVant.has(`van-${cur}`)) { const curPath = path.join(VANT_PATH, cur) deleteDir(curPath) } } console.log('vant-tree-shaking success') } /** * 读取 json 文件中的 usingComponents 属性值 * @param {*} filePath 路径 * @param {*} setList 存符合条件的 vant 组件名的列表 */ function getUsingComponents(filePath, setList) { if (!filePath || !fs.existsSync(filePath) || !isJsonFile(filePath)) return try { const data = fs.readFileSync(filePath, 'utf8') const json = JSON.parse(data) const usingComponents = json.usingComponents || {} for (const key in usingComponents) { if (key.startsWith('van-')) { setList.add(key) } } } catch (err) { console.error(err) } } [代码] 实际测试的一个项目,其中只使用到了 vant 的自定义导航栏 van-nav-bar 组件,没有按需引入时整个代码包大小 544KB,按需引入之后只有 156KB。除了代码包减少了以外,也不用再担心其他未使用到的组件默认触发隐私协议而被拒审了。
05-25 - 小程序基础库 3.4.1 更新
各位微信开发者: 小程序基础库 3.4.1 已经开始灰度开发者,请大家基于业务情况关注相关变更。如遇问题请及时在该帖下方留言或在小程序交流专区发表标题包含「基础库3.4.1」的帖子反馈。本次更新如下: 更新 框架 getSystemInfo 接口不再维护,建议开发者使用 getSystemSetting / getAppAuthorizeSetting / getDeviceInfo / getWindowInfo / getAppBaseInfo 详情更新 框架 xr-frame 点击事件迁移更新 框架 wx.downloadFile、wx.uploadFile 接口新增 useHighPerformanceMode 参数 详情更新 框架 授权弹窗上耦合隐私勾选 详情更新 组件 skyline canvas 触摸事件支持 x,y 详情更新 API 支持插件代宿主调用订阅消息接口 详情更新 API TCP接口 onConnect 和 onMessage 回调参数增加 remoteInfo 和 localInfo 详情修复 组件 修复 canvas 默认滚动修复 组件 修复安卓同层 input 微信团队 2024年04月09日
04-09 - Webview、Skyline 混用切换耗时吗?
在学习 Skyline 的过程中,许多开发者会有一个疑问:是否可以将小程序的部分页面迁移到 Skyline? 对 Skyline 感兴趣但还没有完全决定是否要使用的开发者来说,可能只想先尝试一下 Skyline 的功能。 实际上,Skyline 支持最小粒度的页面配置,意味着我们可以为某个页面单独开启 Skyline,而不必将整个小程序迁移到 Skyline 上。 开发者可以更加灵活地使用 Skyline,并逐步将小程序迁移到 Skyline 上,从而获得更好的性能和用户体验。 我们知道 Webview 和 Skyline 是两个渲染引擎,对于 Webview 和 Skyline 混用,大家又有新的疑问:当进行页面切换的时,混用是否会增加耗时? 这里需要分三种情况: 1、Skyline -> Webview:这种情况取决于 app.json 里配置的全局 renderer,即小程序设置的默认渲染引擎 如果全局 renderer 是 Skyline,那么 Webview 不会被预加载,此时 Skyline 跳转 Webview 耗时会增加,开发者需要手动调用 wx.preloadWebview 做预加载。如果全局 renderer 是 Webview,由于 Webview 默认会预加载,所以 Skyline -> Webview 和 Webview -> Webview 耗时一样,不会增加耗时。2、Webview -> Skyline:Skyline 默认都不会被预加载,开发者需要手动调用 wx.preloadSkylineView 做预加载。 3、Skyline -> Skyline:速度变快,因为多个页面复用同一个 Skyline 实例。 根据上述三种情况的分析,为了保证混用渲染引擎的页面切换耗时最短,我们需要在以下时机进行预加载。 wx.preloadWebview 当 Skyline 页面跳转到 Webview 页面时并且全局 renderer 是 Skyline 由于 Skyline 不影响渲染线程,所以预加载 Webview 的时机只需要在主要逻辑完成后即可 // Skyline page.js Page({ onShow() { // 等待执行完主要逻辑后进行预加载 wx.preloadWebview() } }) wx.preloadSkylineView 当 Webview 页面跳转 Skyline 页面时,因为 Skyline 默认不预加载,所以我们需要手动预加载。 建议大家在 Skyline 页面的 onShow 生命周期里延迟一段时间后调用,这样可以保证在 Skyline 页面被返回时也能够重新预加载。 注意:预加载会影响当前页面的渲染,建议异步延迟去执行预加载操作 // Webview page.js Page({ onShow() { // 延迟 200ms 预加载 Skyline // 建议这个延迟时机在页面渲染完成之后 setTimeout(() => { wx.preloadSkylineView() }, 200) } }) 做好预加载是提高 Webview 和 Skyline 混用体验的有效方式,需要根据实际情况进行调整和优化,以达到最佳的预加载效果。
03-07 - 组件内无法import其他js文件?
报错信息如下: SyntaxError: Cannot use import statement outside a module(env: macOS,mp,1.05.2111051; lib: 2.19.6) 开发者工具内报错: [图片]
2021-11-21 - draggable-sheet 组件:快速实现半屏拖拽
在日常应用中,半屏可拖拽交互已经成为了用户体验的重要组成部分。页面内的分段式拖拽、半屏拖拽放大等等,这些常见的场景使用半屏拖拽交互可以很好的提升用户的体验感。 目前,实现半屏拖拽交互可以使用手势协商系统来实现,不过需要理解手势协商系统并编写相关代码。 考虑到半屏拖拽交互比较常见,为了让小程序开发者更加轻松地实现半屏拖拽交互,小程序 Skyline 渲染引擎推出了 draggable-sheet 组件。这个组件自带半屏拖拽交互,可以帮助开发者省去手势协商相关的代码,只需要简单配置容器大小、吸附阈值等参数即可实现半屏拖拽交互。无论是半屏弹窗、分段式半屏还是其他场景,draggable-sheet 组件都能帮助你实现更加出色的用户体验。 快速接入 使用 draggable-sheet 组件的流程如下: Step 1. 配置 draggable-sheet 组件 在页面中引入 draggable-sheet 组件,并且配置 draggable-sheet 组件的参数,包括容器大小、吸附阈值等。 在 draggable-sheet 组件内嵌套 scroll-view 组件,且声明 associative-container="draggable-sheet" 将 scroll-view 组件与 draggable-sheet 组件进行关联即可。 <!-- page.wxml --> <draggable-sheet class="sheet" initial-child-size="0.5" min-child-size="0.2" max-child-size="0.8" snap="{{true}}" snap-sizes="{{[0.4, 0.6]}}" > <scroll-view scroll-y="{{true}}" type="list" associative-container="draggable-sheet" bounces="{{true}}" > <!-- 这里放置半屏内容 --> </scroll-view> </draggable-sheet> Step 2. 使用 DraggableSheetContext 接口控制滚动 使用 createSelectorQuery 方法获取 draggable-sheet 组件的节点,然后通过 node 方法获取 DraggableSheetContext 实例。调用 DraggableSheetContext.scrollTo 即可将 draggable-sheet 组件 滚动到指定位置。 // page.js Page({ onReady() { this.createSelectorQuery().select('.sheet').node().exec(res => { // 使用 sheetContext 控制 draggable-sheet 组件的滚动 const sheetContext = res[0].node sheetContext.scrollTo({ size: 0.7, animated: true, duration: 300, easingFunction: 'ease' }) }); }, }); 使用 draggable-sheet 组件,你可以轻松实现半屏拖拽交互,让用户体验更加自由。同时,使用 DraggableSheetContext 接口,你可以动态控制 draggable-sheet 组件的行为,实现更加灵活的交互效果。 例如,我们这里演示了使用 draggable-sheet 实现的 分享弹窗效果 和 分段式拖拽查看旅游景点列表~ [图片][图片] 与其他组件联动 除了简单的半屏弹窗之外,我们还可以通过 worklet:onsizeupdate 回调,在 draggable-sheet 组件尺寸发生变化时,将 draggable-sheet 组件与其他组件进行动画交互。 例如,我们这里在 draggable-sheet 接近搜索框时,与搜索框衔接为一体,隐藏后面的页面,以此实现更流畅的展示效果。 首先,给 draggable-sheet 配置监听函数 worklet:onsizeupdate="onSizeUpdate" <!-- page.wxml --> <view class="search-wrp"> ... </view> <draggable-sheet class="sheet" ... worklet:onsizeupdate="onSizeUpdate" > 然后,我们在 onSizeUpdate 发生变化时,通过 worklet 函数改变共享变量的值即可 Page({ // 监听 draggable-sheet 尺寸变化去改变 progress onSizeUpdate(e) { 'worklet' const distance = sheetHeight - e.pixels this.progress.value = distance >= 20 ? 1 : distance / 20 }, onLoad(options) { const progress = wx.worklet.shared(1) // 绑定 worklet 动画,progress 变化时改变搜索的背景来显示/隐藏 map this.applyAnimatedStyle('.search-wrp', () => { 'worklet' const t = progress.value return { backgroundColor: `rgb(245, 242, 241, ${1 - t})` } }) ... this.progress = progress }, }) [图片] 如果想要快速了解和实践本文中的案例,可在 PC 端点击 代码片段 进行调试。 注意:draggable-sheet 组件支持页面内的拖拽放大功能,如果需要实现页面返回的功能,可以使用 预设路由 来实现。 总结 draggable-sheet 组件通过拖拽的方式来展开或关闭一个面板,提供更加直观、自然的用户交互体验。不仅能够节省屏幕空间,让用户更加高效地利用界面,适用于展示评论列表、商品列表、展开筛选条件、展示更多信息等场景。对于需要提供更加灵活、自由的用户交互体验的小程序来说,draggable-sheet 组件是一个非常有用的功能。 因此,我们邀请大家尝试将这个能力应用到自己的小程序上,通过这个组件来提升用户体验和交互效果。结合其他组件和功能,打造出更加丰富、多样的小程序~
02-26 - Skyline | 快速搞定复杂的分享海报
在小程序中生成海报是一种非常有效的推广方式 用户可以使用小程序的过程中生成小程序海报并分享给他人 通过海报的形式,用户可以直观地了解产品或服务的特点和优势 [图片] 常见绘制海报方式 目前,小程序海报有两种常见的实现方式: · canvas 绘制海报 · 服务端绘制海报 这两种方式各有千秋 canvas 绘制海报使用 canvas 绘制海报主要有以下几个步骤 1、创建 [代码]canvasContext[代码] 2、获取网络图片的本地路径 3、绘制图片、文字等到 [代码]canvas[代码] 4、调用 [代码]wx.canvasToTempFilePath[代码] 导出图片 尽管 canvas 绘制功能强大,但实际使用中,这些操作看似简单,但调试起来却比较麻烦 而且面对一些复杂的排版时,使用 canvas 绘制相较于使用 CSS 绘制来说困难许多 除此之外,canvas 的宽高有最大限制,超出限制则会绘制空白 服务端绘制 小程序也可以通过调用服务端接口,将需要生成海报的数据传递给服务端, 由服务端使用 Canvas API 等第三方库来生成图片。 然而,这种绘制方式需要走网络请求,如果量大会给服务器带来一定的成本压力。 此外,对于复杂排版的实现,使用 Canvas 绘制也有一定的难度。 尽管小程序海报虽然好用,但是当遇到要求比较高的设计稿需要还原海报时,对小程序开发者来说是一个十分让人头疼的问题 考虑到海报在小程序中使用的广泛性,我们把开发者的烦恼交给官方来处理~ 小程序官方推出了 [代码]snapshot[代码] 组件,可以直接将小程序 wxml 导出图片。 snapshot 生成海报 当使用 canvas 或 服务端绘制海报遇到复杂排版时,如 圆角、百分比、自定义字体 等等,实现比较困难。 但是使用 wxml 实现却很简单 👇 下面的例子我们使用 wxml 实现海报 <view class="snapshot-box"> <view class="poster-container"> <view class="poster-header"> <image /> ... </view> <view class="description"> ... </view> <view class="footer"> ... </view> </view> </view> [图片] 接着,我们就可以导出海报啦,使用非常简单: 1、用 [代码]snapshot[代码] 组件包裹海报的 wxml 2、调用 [代码]takeSnapshot[代码] 获取图片数据 3、调用 [代码]fs.writeFileSync[代码] 将海报数据写入本地文件 4、调用 [代码]wx.saveImageToPhotosAlbum[代码] 将海报保存到本地 <snapshot id="view"> <!-- 这里是要海报的 wxml --> </snapshot> <button bindtap="tap">保存海报</button> tap() { this.createSelectorQuery().select("#view") .node().exec(res => { const node = res[0].node // 保存海报 node.takeSnapshot({ type: 'arraybuffer', format: 'png', success: (res) => { const f = `${wx.env.USER_DATA_PATH}/hello.png` const fs = wx.getFileSystemManager(); // 将海报数据写入本地文件 fs.writeFileSync(f, res.data, 'binary') this.setData({ img: f }) // 把海报图片保存到本地 wx.saveImageToPhotosAlbum({ filePath: f }) } }) }) } 最后我们来看看使用 [代码]snapshot[代码] 组件生成海报的效果吧~ [图片] 除了普通尺寸分享海报之外,对于 canvas 无法搞定的超长海报,[代码]snapshot[代码] 后续也会支持超长海报的导出~ [图片] 你的小程序也有海报生成需求吗? 赶紧 mark 下这个 代码片段 来接入使用吧~
2023-09-06 - Wxml2Canvas -- 快速生成小程序分享图通用方案
Wxml2Canvas库,可以将指定的wxml节点直接转换成canvas元素,并且保存成分享图,极大地提升了绘制分享图的效率。目前被应用于微信游戏圈、王者荣耀、刺激战场助手等小程序中。 github地址:https://github.com/wg-front/wxml2canvas 一、背景 随着小程序应用的日渐成熟,多处场景需要能够生成分享图便于用户进行二次传播,从而提升小程序的传播率以及加强品牌效应。 对于简单的分享图,比如固定大小的背景图加几行简短文字构成的分享小图,我们可以利用官方提供的canvas接口将元素直接绘制, 虽然繁琐了些,但能满足基本要求。 对于复杂的分享图,比如用户在微信游戏圈发表完话题后,需要将图文混排的富文本内容生成分享图,对于这种长度不定,内容动态变化的图片生成需求,直接利用官方的canvas接口绘制是十分困难的,包括但不限于文字换行、表情文字图片混排、文字加粗、子标题等元素都需要一一绘制。又如王者荣耀助手小程序,需要将十人对局的详细战绩绘制成分享图,包含英雄数据、装备、技能、对局结果等信息,要绘制100多张图片和大量的文字信息,如果依旧使用官方的接口一步一步绘制,对开发者来说简直就是一场噩梦。我们急需一种通用、高效的方式完成上述的工作。 在这样的背景下,wxml2cavnas诞生了,作为一种分享图绘制的通用方案,它不仅能快速的绘制简单的固定小图,还能直接将wxml元素真实地转换成canvas元素,并且适配各种机型。无论是复杂的图文混排的富文本内容,还是展现形式多样的战绩结果页,都可以利用wxml2cavnas完美地快速绘制并生成所期望的分享图片。 二、Wxml2Canvas介绍及示例 1. 介绍 Wxml2Cavnas库,是一个生成小程序分享图的通用方案,提供了两种绘制方式: 封装基础图形的绘制接口,包括矩形、圆形、线条、图片、圆角图片、纯文本等,使用时只需要声明元素类型并提供关键数据即可,不需要再关注canvas的具体绘制过程; wxml直接转换成canvas元素,使用时传入待绘制的wxml节点的class类名,并且声明绘制此节点的类型(图片、文字等),会自动读取此节点的computedStyle,利用这些数据完成元素的绘制。 2. 生成图示例 下面是两张极端复杂的分享图。 2.1 游戏圈话题 [图片] 点击查看完整长图 2.2.2 王者荣耀战绩 [图片] 点击查看完整大图 三、小程序的特性及局限 小程序提供了如下特性,可供我们便捷使用: measureText接口能直接测量出文本的宽度; SelectorQuery可以查询到节点对应的computedStyle。 利用第一条,我们在绘制超长文本时便于文本的省略或者换行,从而避免文字溢出。 利用第二条,我们可以根据class类名,直接拿到节点的样式,然后将style转换成canvas可识别的内容。 但是和html的canvas相比,小程序的canvas局限性很多。主要体现在如下几点: 不支持base64图片; 图片必须下载到本地后才能绘制到画布上; 图片域名需要在管理平台加入downFile安全域名; canvas属于原生组件,在移动端会置于最顶层; 通过SelectorQuery只能拿到节点的style,而无法获取文本节点的内容以及图片节点的链接。 针对以上问题,我们需要将base64图片转换jpg或png格式的图片,实现图片的统一下载逻辑,并且离屏绘制内容。针对第五条,好在SelectorQuery可以获取到节点的dataset属性,所以我们需要在待绘制的节点上显示地声明其类型(imgae、text等),并且显示地传入文本内容或图片链接,后文会有示例。 四、Wxml2Canvas使用方式 1. 初始化 首先在wxml中创建canvas节点,指定宽高: [代码] <canvas canvas-id="share" style="height: {{ height * zoom }}px; width: {{ width * zoom }}px;"> </canvas> [代码] 引入代码库,创建DrawImage实例,并传入如下参数: [代码] let DrawImage = require('./wxml2canvas/index.js'); let zoom = this.device.windowWidth / 375; let width = 375; let height = width * 3; let drawImage = new DrawImage({ element: 'share', // canvas节点的id, obj: this, // 在组件中使用时,需要传入当前组件的this width: width, // 宽高 height: height, background: '#161C3A', // 默认背景色 gradientBackground: { // 默认的渐变背景色,与background互斥 color: ['#17326b', '#340821'], line: [0, 0, 0, height] }, progress (percent) { // 绘制进度 }, finish (url) { // 画完后返回url }, error (res) { console.log(res); // 画失败的原因 } }); [代码] 所有的数字参数均以iphone6为基准,其中参数width和height决定了canvas画布的大小,规定值是在iphone6机型下的固定数值; zoom参数的作用是控制画布的缩放比例,如果要求画布自适应,则应传入 windowWidth / 375,windowWidth为手机屏幕的宽度。 2. 传入数据,生成图片 执行绘制操作: [代码] drawImage.draw(data, this); [代码] 执行绘制时需要传入数据data,数据的格式分为两种,下面展开介绍。 2.1 基础图形 第一种为基础的图形、图文绘制,直接使用官方提供接口,下面代码是一个基本的格式: [代码] let data = { list: [{ type: 'image', url: 'https://xxx', class: 'background_image', // delay: true, x: 0, y: 0, style: { width: width, height: width } }, { type: 'text', text: '文字', class: 'title', x: 0, y: 0, style: { fontSize: 14, lineHeight: 20, color: '#353535', fontFamily: 'PingFangSC-Regular' } }] } [代码] 如上,type声明了要元素的类型,有image、text、rect、line、circle、redius_image(圆角图)等,能满足绝大多数情况。 class类名指定了使用的样式,需要在style中写出,符合css样式规范。 delay参数用来异步绘制元素,会把此元素放在第二个循环中绘制。 x,y用来指定元素的起始坐标。 将css样式与元素分离的目的是便于管理与复用。 此种方式每个元素都相互独立,互不影响,能够满足自由度要求高的情况,可控性高。 2.2 wxml转换 第二种方式为指定wxml元素,自动获取,下面是示例: [代码] let data = { list: [{ type: 'wxml', class: '.panel .draw_canvas', limit: '.panel' x: 0, y: 0 }] } [代码] 如上,type声明为wxml时,会查找所有类名为draw_canvas的节点,并且加入到绘制队列中。 class传入的第一个类名限定了查询的范围,可以不传,第二个用来指定查找的节点,可以定义为任意不影响样式展现的通用类名。 limit属性用来限定相对位置,例如,一个文本的位置(left, top) = (50, 80), class为panel的节点的位置为(left, top) = (20, 40),则文本canvas上实际绘制的位置(x, y) = (50 - 20, 80 -40) = (30, 40)。如果不传入limit,则以实际的位置(x, y) = (50, 80)绘制。 由于小程序节点元素查询接口的局限,无法直接获取节点的文本内容和图片标签的src属性,也无法直接区分是文本还是图片,但是可以获取到dataset,所以我们需要在节点上显示地声明data-type来指明类型,再声明data-text传入文字或data-url传入图片链接。下面是个示例: [代码] <view class="panel"> <view class="panel__img draw_canvas" data-type="image" data-url="https://xxx"></view> <view class="panel__text draw_canvas" data-type="text" data-text="文字">文字</view> </view> [代码] 如上,会查询到两个节点符合条件,第一个为image图片,第二个为text文本,利用SelectorQuery查询它们的computedStyle,分别得到left、top、width、height等数据后,转换成canvas支持的格式,完成绘制。 除此之外,下面的示例功能更加丰富: [代码] <view class="panel"> <view class="panel__text draw_canvas" data-type="background-image" data-radius="1" data-shadow="" data-border="2px solid #000"></view> <view class="panel__text draw_canvas" data-type="text" data-background="#ffffff" data-padding="2 3 0 0" data-delay="1" data-left="10" data-top="10" data-maxlength="4" data-text="这是个文字">这是个文字</view> </view> [代码] 如上,第一个data-type为background-image,表示读取此节点的背景图片,因为可以通过computedStyle直接获取图片链接,所以不需要显示传入url。声明data-radius属性,表示要将此图绘成乘圆形图片。data-border属性表示要绘制图片的边框,虽然也可以通过computedStyle直接获取,但是为了避免非预期的结果,还是要声明传入,border格式应符合css标准。此外,图片的box-shadow等样式都会根据声明绘制出来。 第二个文本节点,声明了data-background,则会根据节点的位置属性给文字增加背景。 data-padding属性用来修正背景的位置和宽高。data-delay属性用来延迟绘制,可以根据值的大小,来控制元素的层级,data-left和data-top用来修正位置,支持负值。data-maxlength用来限制文本的最大长度,超长时会截取并追加’…’。 此外,data-type还有inline-text,inline-image等行内元素的绘制,其实现较为复杂,会在后文介绍。 五、Wxml2Canvas实现原理 1. 绘制流程 整个绘制流程如下: [图片] 因为小程序的限制,只能在画布上绘制本地图片,所以统一先对图片提前下载,然后再绘制,为了避免图片重复下载,内部维护一个图片列表,会对相同的图片链接去重,减少等待时间。 2. 基本图形的实现 基础图形的绘制比较简单,内部实现只是对基础能力的封装,使用者不用再关注canvas的绘制过程,只需要提供关键数据即可,下面是一个图片绘制的实现示例: [代码] function drawImage (item, style) { if(item.delay) { this.asyncList.push({item, style}); }else { if(item.y < 0) { item.y = this.height + item.y * zoom - style.height * zoom; }else { item.y = item.y * zoom; } if(item.x < 0) { item.x = this.width + item.x * zoom - style.width * zoom; }else { item.x = item.x * zoom; } ctx.drawImage(item.url, item.x, item.y, style.width * zoom, style.height * zoom); ctx.draw(true); } } [代码] 如上,x,y值坐标支持传入负值,表示从画布的底部和右侧计算位置。 3. Wxml转Canvas元素的实现 3.1 computedStyle的获取 首先需要获取wxml的样式,代码示例如下: [代码] query.selectAll(`${item.class}`).fields({ dataset: true, size: true, rect: true, computedStyle: ['width', 'height', ...] }, (res) => { self.drawWxml(res); }) [代码] 3.2 块级元素的绘制 对于声明为image、text的元素,默认为块级元素,它们的绘制都是独立进行的,不需要考虑其他的元素的影响,以wxml节点为圆形的image为例,下面是部分代码: [代码] if(sub.dataset.type === 'image') { let r = sub.width / 2; let x = sub.left + item.x * zoom; let y = sub.top + item.y * zoom; let leftFix = +sub.dataset.left || 0; let topFix = +sub.dataset.top || 0; let borderWidth = sub.borderWidth || 0; let borderColor = sub.borderColor; // 如果是圆形图片 if(sub.dataset.radius) { // 绘制圆形的border if(borderWidth) { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setStrokeStyle(borderColor) ctx.setLineWidth(borderWidth) ctx.stroke() ctx.closePath() } // 绘制圆形图片的阴影 if(sub.boxShadow !== 'none') { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setFillStyle(borderColor); setBoxShadow(sub.boxShadow); ctx.fill() ctx.closePath() } // 最后绘制圆形图片 ctx.save(); ctx.beginPath(); ctx.arc((x + r), (y + r) - limitTop, r, 0, 2 * Math.PI); ctx.clip(); ctx.drawImage(url, x + leftFix * zoom, y + topFix * zoom, sub.width, sub.height); ctx.closePath(); ctx.restore(); }else { // 常规图片 } } [代码] 如上,块级元素的绘制和基础图形的绘制差异不大,理解起来也很容易,不再多述。 3.3 令人头疼的行内元素的绘制 当wxml的data-type声明为inline-image或者inline-text时,我们认为是行内元素。行内元素的绘制是一个难点,因为元素之前存在关联,所以不得不考虑各种临界情况。下面展开细述。 3.3.1 纯文本换行 对于长度超过一行的行内元素,需要计算出合适的换行位置,下图所示的是两种临界情况: [图片] [图片] 如上图所示,第一种情况为最后一行只有一个文字,第二种情况最后一行的文字长度和宽度相同。虽然长度不同,但都可通过下面代码绘制: [代码] let lineNum = Math.ceil(measureWidth(text) / maxWidth); // 文字行数 let sinleLineLength = Math.floor(text.length / lineNume); // 向下取整,保证多于实际每行字数 let currentIndex = 0; // 记录文字的索引位置 for(let i = 0; i < lineNum; i++) { let offset = 0; // singleLineLength并不是精确的每行文字数,要校正 let endIndex = currentIndex + sinleLineLength + offset; let single = text.substring(currentIndex, endIndex); // 截取本行文字 let singleWidth = measureWidth(single); // 超长时,左移一位,直至正好 while(singleWidth > maxWidth) { offset--; endIndex = currentIndex + sinleLineLength + offset; single = text.substring(currentIndex, endIndex); singleWidth = measureWidth(single); } currentIndex = endIndex; ctx.fillText(single, item.x, item.y + i * style.lineHeight); } // 绘制剩余的 if(currentIndex < text.length) { let last = text.substring(currentIndex, text.length); ctx.fillText(last, item.x, item.y + lineNum * style.lineHeight); } [代码] 为了避免计算太多次,首先算出大致的行数,求出每行的文字数,然后移位索引下标,求出实际的每行的字数,再下移一行继续绘制,直到结束。 3.3.2 非换行的图文混排 [图片] 上图是一个包含表情图片和加粗文字的混排内容,当使用Wxml2Canvas查询元素时,会将第一行的内容分为五部分: 文本内容:这是段文字; 表情图片:发呆表情(非系统表情,image节点展现); 表情图片:发呆表情; 文本内容:这也; 加粗文本内容:是一段文字,这也是文字。 对于这种情况,执行查询computedStyle后,会返回相同的top值。我们把top值相同的元素聚合在一起,认为它们是同一行内容,事实也是如此。因为表情大小的差异以及其他影响,默认规定top值在±2的范围内都是同一行内容。然后将top值的聚合结果按照left的大小从左往右排列,再一一绘制,即可完美还原此种情况。 3.3.3 换行的图文混排 当混排内容出现了换行情况时,如下图所示: [图片] 此时的加粗内容占据了两行,当我们依旧根据top值归类时,却发现加粗文字的left值取的是第二行的left值。这就导致加粗文字和第一部分的文字的top值和left值相同,如果直接绘制,两部分会发生重叠。 为了避免这种尴尬的情况,我们可以利用加粗文字的height值与第一部分文字的height值比较,显然前者是后者的两倍,可以得知加粗部分出现了换行情况,直接将其放在同组top列表的最后位置。换行的部分根据lineHeight下移绘制,同时做记录。 最后一部分的文本内容也出现了换行情况,同样无法得到真正的起始left值,并且其top值与上一部分换行后的top值相同。此时应该将他的left值追加加粗换行部分的宽度,正好得到真正的left值,最后再绘制。 大多数的行内元素的展现形式都能以上述的逻辑完美还原。 六、总结 基于基础图形封装和wxml转换这两种绘制方式,可以满足绝大多数的场景,能够极大地减少工作量,而不需要再关注内部实现。在实际使用中,二者并非孤立存在,而更多的是一起使用。 [图片] 如上图所示,对于列表内容我们利用wxml读取绘制,对于下部的白色区域,不是wxml节点内容,我们可以使用基础图形绘制方式实现。二者的结合更加灵活高效。 目前Wxml2Canvas已经在公司内部开源,不久会放到github上,同时也在不断完善中,旨在实现更多的样式展现与提升稳定性和绘制速度。 如果有更好的建议与想法,请联系我。
2019-02-28 - [开盖即食]小程序图表插件 ECharts 实战
[图片] H5时代用来做图表的插件有很多比如:[代码]ECharts[代码]、[代码]Bizcharts[代码]、[代码]JSCharts[代码]等,而这次的小程序本人选用了 ECharts 作为图表组件。 1、选择原因主要有3点: 官方某度在持续维护这个插件 官方推出了直接适配小程序的版本,且有demo,开盖即食,不用迁移 简单实用,覆盖面广且可通过配置控制包的大小,小程序毕竟大小有限制~ eCharts来自BAT中的B前端团队,对应的小程序版本为:echarts-for-weixin 官网地址 https://echarts.apache.org/ github地址 https://github.com/ecomfe/echarts-for-weixin 小程序demo地址 https://github.com/ecomfe/echarts-examples 2、用法 (1)官方教程 [代码]index.json[代码] 配置如下: [代码]{ "usingComponents": { "ec-canvas": "../../ec-canvas/ec-canvas" } } [代码] 这一配置的作用是,允许我们在 [代码]pages/bar/index.wxml[代码] 中使用 [代码]<ec-canvas>[代码] 组件。注意路径的相对位置要写对,如果目录结构和本例相同,就应该像上面这样配置。 [代码]index.wxml[代码] 中创建了一个 [代码]<ec-canvas>[代码] 组件: [代码]<view class="container"> <ec-canvas id="mychart-dom-bar" canvas-id="mychart-bar" ec="{{ ec }}"></ec-canvas> </view> [代码] 其中 [代码]ec[代码] 是一个我们在 [代码]index.js[代码] 中定义的对象,它使得图表能够在页面加载后被初始化并设置。 [代码]index.js[代码] 配置: [代码]function initChart(canvas, width, height, dpr) { const chart = echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr // 像素 }); canvas.setChart(chart); var option = { ... }; chart.setOption(option); return chart; } Page({ data: { ec: { onInit: initChart } } }); [代码] 这对于所有 ECharts 图表都是通用的,用户只需要修改上面 [代码]option[代码] 的内容,即可改变图表。[代码]option[代码] 的使用方法参见 ECharts 配置项文档。 官方demo里的一些用法指导: 如何延迟加载图表? 参见 [代码]pages/lazyLoad[代码] 的例子,可以在获取数据后再初始化数据。 如何在一个页面中加载多个图表? 参见 [代码]pages/multiCharts[代码] 的例子。 如何使用 Tooltip? 目前,本项目已支持 ECharts Tooltip,但是由于 ECharts 相关功能尚未发版,因此需要使用当前本项目中 [代码]ec-canvas/echarts.js[代码],这个文件包含了可以在微信中使用 Tooltip 的相关代码。目前在 ECharts 官网下载的 [代码]echarts.js[代码] 还不能直接替换使用,等 ECharts 正式发版后即可。 具体使用方法和 ECharts 相同,例子参见 [代码]pages/line/index.js[代码]。 如何保存为图片? 参见 [代码]pages/saveCanvas[代码] 的例子。 (2)本人实战操作 [图片] [代码]import * as echarts from '../ec-canvas/echarts'; const app = getApp(); let chart; function initChart(canvas, width, height, dpr) { chart = echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr // new }); canvas.setChart(chart); chart.setOption(option); return chart; } var option = { title: { text: '智酷君 echarts 切换效果测试', left: 'center' }, tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' }, legend: { orient: 'vertical', left: 10, data: ['AAA', 'BBB', 'CCC', 'DDD', 'EEE'] }, series: [ { name: '访问来源', type: 'pie', radius: ['50%', '70%'], avoidLabelOverlap: false, label: { show: false, position: 'center' }, emphasis: { label: { show: true, fontSize: '30', fontWeight: 'bold' } }, labelLine: { show: false }, data: [ {value: 335, name: 'AAA'}, {value: 310, name: 'BBB'}, {value: 234, name: 'CCC'}, {value: 135, name: 'DDD'}, {value: 1548, name: 'EEE'} ] } ] }; Page({ data: { ec: { onInit: initChart } }, onLoad: function () {}, //单曲线 line() { let option2 = { title: { text: '同一canvas更新成折线图', left: 'center' }, xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [{ data: [820, 932, 901, 934, 1290, 1330, 1320], type: 'line' }] }; chart.setOption(option2) }, //切换柱状图 bar(){ let option3 = { title: { text: '直接更新数据,减少性能消耗', left: 'center' }, xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [{ data: [120, 200, 150, 80, 70, 110, 130], type: 'bar', showBackground: true, backgroundStyle: { color: 'rgba(220, 220, 220, 0.8)' } }] }; chart.setOption(option3) } }) [代码] 建议大家尽量使用同一个canvas对象来切换不同的图表效果,而不是初始加载多个不同的,我们可以将 [代码]chart[代码] 对象设置为全局,然后通过 [代码]chart.setOption()[代码] 的方法来更新配置数据,可以减少性能消耗避免闪退等 (3)代码片段 代码段:https://developers.weixin.qq.com/s/OOTwnsms7Cin 建议将IDE工具升级到 1.02.18以上,避免一些BUG [图片] 3、Tips (1)包大小可以配置 在线定制地址: https://echarts.apache.org/zh/builder.html [图片] [图片] 通过选择和配置想要的功能,可以大大减少原本JS包的尺寸。 (2)Canvas 2d 版本要求 最新版的 ECharts 微信小程序支持微信 Canvas 2d,当用户的基础库版本 >= 2.9.0 且没有设置 [代码]force-use-old-canvas="true"[代码] 的情况下,使用新的 Canvas 2d(默认)。 使用新的 Canvas 2d 可以提升渲染性能,解决非同层渲染问题,强烈建议开启 如果仍需使用旧版 Canvas,使用方法如下: [代码]<ec-canvas id="xxx" canvas-id="xxx" ec="{{ ec }}" force-use-old-canvas="true"></ec-canvas> [代码] (3)数据点过多造成闪退和卡死 本人简单测试了下,iphone7p手机在[代码]1500个[代码]左右数据点的时候,出现了小程序闪退,iphoneX 测试下来大概在[代码]2500个[代码]左右,猜测可能由于微信本身给小程序的内存有限,所以建议大家控制数据点的个数 (4)单页面图表canvas加载过多卡死 建议单页面图表加载不要超过[代码]5个canvas[代码],尽可能共用一个图表Canvas对象,通过动态更新数据的方式来展示内容(还有帅气的特效),如果一定要加载多个canvas的话,建议控制数量,提供复用性~ 看完觉得有帮助记得点个赞哦~ 你的赞是我继续分享的最大动力!^-^
2020-06-29 - 小程序推荐图表库uchart
话说这三个图表库,我都有用过,echart,wx-chart,ucharts, wx-chart由于个人目前没有维护了,echart,总的来说,还是更倾向于pc,而ucharts就不一样了,他的诞生,就是为小程序而生的,他是伴随小程序跨端框架uni-app而来的,所以uchart更小程序更具有一种天然的属性。 ----- 高性能跨平台图表库,支持H5图表、APP图表、小程序图表(微信小程序、支付宝小程序、百度小程序、头条小程序、QQ小程序、360小程序),支持饼图、圆环图、线图、柱状图、区域图、雷达图、圆弧进度图、仪表盘、K线图、条状图、混合图、玫瑰图、漏斗图、词云图、地图。 首页 https://www.ucharts.cn 码云主页 https://gitee.com/uCharts/uCharts 扫码体验地址 [图片] - - - - - - - - - - - - - - - - H5端流行的echart报表因为涉及大量dom操作,无法跨端使用,而wx-chart在跨端和更新方面都不足,如果要做小程序,推荐使用全端可用的[uChart](https://ext.dcloud.net.cn/plugin?id=271)。 - 如只考虑H5端,也可以继续使用echart、f2等常规web图表。 - 如不考虑小程序,那么App端和H5,还可以通过renderjs技术来使用echart、f2等web图表,功能性能比uchart更好。[什么是renderjs](https://uniapp.dcloud.io/frame?id=renderjs)、[基于renderjs使用echart的示例](https://ext.dcloud.net.cn/plugin?id=1207)
2020-04-12 - 小程序快速接入 ECharts 图表插件功能
[图片] ECharts 图表组件插件可以让小程序轻松使用 ECharts 图表功能,不用担心接入成本以及占用较大体积的问题,以及较为复杂的接入使用问题。详细接入文档:https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx1db9e5ab1149ea03 接入步骤1、插件申请首先要在小程序管理后台的“设置-第三方服务-插件管理”中添加插件。开发者可登录小程序管理后台,通过 [代码]wx1db9e5ab1149ea03[代码] 查找插件并添加。通过申请后,方可在小程序中使用相应的插件 2、引入插件配置在 app.json 中插件如下代码,注意 AppId 为 [代码]wx1db9e5ab1149ea03[代码] { "plugins": { "echarts": { "version": "1.0.2", "provider": "wx1db9e5ab1149ea03" } } } 3、使用方式在使用的页面中的 json 配置文件中,插件如下代码: { "usingComponents": { "chart": "plugin://echarts/chart" } } 在 wxml 中需要展示图表的位置,插入如下代码: <chart chart-class="chart" option="{{ option }}" bindinstance="onInstance" /> 其中 chart-class 为样式类,option 为 ECharts 中的 option 配置对象,bindinstance 会回调 ECharts 实例对象,如果需要操作 ECharts 实例对象可以实现此方法 4、效果展示[图片] 注意事项如果需要兼容低于 2.6.1 的小程序,在更新 option 的时候需要手动 setOption,具体代码如下: // 支持 observers 的版本可以直接修改 option if (echarts.isSupportObservers()) { this.setData({ option, }); } else if (this.instance) { // 在不支持 observers 的小程序版本中需要手动通过 ECharts 实例更新 option 配置 this.instance.setOption(option, true); } 如果你的小程序最低支持版本大于 2.6.1,或者你不会动态改变 option 配置,那么你不需要关系这段代码,也不需要处理 bindinstance 回调。 关于使用中遇到任何问题可以添加微信反馈,添加微信时可以备注 ECharts。 [图片]
2020-06-16 - App跳转小程序失败?
App跳转小程序失败,开发者可先按照以下指引自查: 1、AppID和BundleId的绑定关系,还有AppID和小程序userName的绑定关系是否正确 2、参数是否正确(AppID或userName) Android开发示例: https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Launching_a_Mini_Program/Android_Development_example.html iOS开发示例: https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Launching_a_Mini_Program/iOS_Development_example.html
2020-01-14 - mp-privacy小程序隐私保护授权弹窗组件
mp-privacy (小程序隐私保护授权弹窗组件) 微信发布关于小程序隐私保护指引设置的公告,为规范开发者的用户个人信息处理行为,保障用户的合法权益,自2023年9月15日起,对于涉及处理用户个人信息的小程序开发者,微信要求,仅当开发者主动向平台同步用户已阅读并同意了小程序的隐私保护指引等信息处理规则后,方可调用微信提供的隐私接口。 使用mp-privacy小程序隐私保护授权弹窗组件可降低开发成本,简单配置就可使用。需要授权时展示弹窗,用户点击“拒绝”授权隐私接口时,不能调用隐私接口,但不影响小程序其他功能的使用,点击“同意”后继续调用隐私接口。 [图片] 1 - 仓库地址 mp-privacy【github】 mp-privacy【码云gitee】 2 - 测试环境 微信小程序基础库版本:2.32.3 及以上 3 - 安装方法:npm安装 [代码]npm i mp-privacy --save [代码] 3.1 依赖包安装后,需要在小程序开发者工具中:构建npm 3.2 如果不想使用npm安装,可以直接下载源码,然后复制mp-privacy组件进行使用 4 - 配置与使用(两种使用方式都需要以下配置) 4.1 小程序配置文件 app.json (2023年9月15日前调试需要此步配置) [代码]{ "__usePrivacyCheck__": true, } [代码] 4.2 小程序 app.js [代码]App({ onLaunch: function (options) { // 监听隐私接口需要用户授权事件 if (wx.onNeedPrivacyAuthorization) { wx.onNeedPrivacyAuthorization(resolve => { // 需要用户同意隐私授权时,弹出开发者自定义的隐私授权弹窗 this.globalData.showPrivacy = true; this.globalData.resolvePrivacyAuthorization = resolve }) } }, globalData: { showPrivacy: false, resolvePrivacyAuthorization: null, }, }) [代码] 4.3 使用到隐私接口的页面的配置文件 index.json [代码]{ "usingComponents": { "mp-privacy": "mp-privacy" } } [代码] 4.4 使用到隐私接口的页面文件 index.wxml [代码]<mp-privacy /> [代码] 4.5 所有使用到的隐私接口必须要在「小程序管理后台」设置《小程序用户隐私保护指引》,否则无法触发wx.onNeedPrivacyAuthorization 监听
2023-08-31 - 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 - 试着解读一下《小程序隐私协议开发指南》3个官方demo的应用场景
隐私协议开发 关于小程序隐私协议开发, 官方最近给出了3个demo,看起来都不太一样: 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/4X7yyGmQ7EKp demo3: 演示 wx.onNeedPrivacyAuthorization、wx.requirePrivacyAuthorize、<button open-type=“agreePrivacyAuthorization”> 和 <input type=“nickname”> 组件如何结合使用 https://developers.weixin.qq.com/s/jX7xWGmA7UKa 说明也比较简单,我结合我查看demo源码说一下我理解的使用场景 demo1 这个是最简单的。调用的隐私协议接口最少,只有一个[代码]wx.getPrivacySetting[代码]。([代码]wx.openPrivacyContract[代码]不算)。 其逻辑就是在调用任何隐私接口之前检查授权状态,已授权则啥也不做,未授权则弹窗。 特点: 开发者主动检查后弹窗,而不是隐私接口调用触发。 简单,代码量最少。 流程:主动检查->弹窗->用户同意->调用隐私接口 适用场景: 小程序启动过程没有调用隐私接口,换句话说,隐私接口需要由用户动作才会调用,如用户点击按钮选择图片。 强隐私需求小程序,用户不给授权就不能用,直接退出那种。 极简小程序,比如就一个页面,或者就只调用一个隐私接口。 以上几种情况就可以使用demo1, 在小程序启动时做检查,没授权就弹窗,用户不同意就退出; 或者对于极简小程序,比如就一个页面点击按钮选个图片,可以在处理按钮事件时调用[代码]wx.getPrivacySetting[代码]。弹窗后用户不同意就啥也不干或给个toast,下次用户再点按钮就再检查再弹窗。 demo2 demo2就复杂一些了,需要用到[代码]wx.onNeedPrivacyAuthorization[代码]接口了,和demo1的区别就是,弹窗是由调用隐私接口触发的,而不是开发者主动检查触发 特点: 弹窗是由调用隐私接口触发。 复杂,代码量多。 流程:调用隐私接口->弹窗(同时隐私接口pending)->用户同意/不同意->隐私接口返回成功或失败 适用场景: 普遍场景,多个页面,多个隐私接口。 被动触发,无需关心隐私接口何时被调用。 demo3 demo3在demo2的基础上,就多了[代码]wx.requirePrivacyAuthorize[代码]调用,官方说法是这个接口用来模拟隐私接口调用。 特点: 弹窗可以由调用隐私接口触发,也可以由[代码]wx.requirePrivacyAuthorize[代码]触发 复杂,代码量多。 流程:调用隐私接口(wx.requirePrivacyAuthorize)->弹窗(同时隐私接口pending)->用户同意/不同意->隐私接口返回成功或失败 相较于demo1,相同的地方在于开发者可主动触发隐私弹窗,不同的地方在于demo3更复杂一些 相较于demo2,相同的地方在于其逻辑和调用真实隐私接口是一致的,不同之处在于给开发者提供了一种更灵活的可主动触发弹窗的模式 你可以认为 demo3=demo1 + demo2 适用场景: 同demo2。 无接口的隐私组件,如<input type=“nickname”> 比demo2更灵活的需求. 对于[代码]wx.requirePrivacyAuthorize[代码]接口,大家不要把这个接口局限于仅在开发时调试用。这个接口也是可以用在线上的。可以用来实现更灵活的逻辑。 以上就是我对这3个demo的理解,大家有什么样的想法欢迎在评论区留言大家一起讨论。
2023-08-24 - 关于新版隐私协议接口wx.onNeedPrivacyAuthorization的解读以及实现代码(二)
官方公告地址: https://developers.weixin.qq.com/community/develop/doc/00042e3ef54940ce8520e38db61801 目前,开发工具或者体验版的小程序,调试基础库改成3.0.0可以适配测试,线上版本9月15日之后生效,所以这之前需要尽快改完,发布一版,否则到了9月15号之后 线上就会生效报错了。 这是官方的Demo地址,也是每个页引用一个全局的组件,个人感觉太过于繁琐了,可以参考以下我整理的代码。 https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html 其实改起来也很简单,以下是实现步骤和代码: 1、首先看一下这个网址,里边包含涉及到的隐私的接口,这些接口都要适配一下 https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/miniprogram-intro.html [图片] 在以上接口用到的页面,需要画一下类似上边的弹窗(这个弹窗可以全局定义个组件,方便多个页面共用),然后里边蓝字可以点击后调用wx.openPrivacyContract(Object object)接口即可,会自动跳转打开隐私协议页面。 拒绝按钮可以加一个点击事件,然后在事件里这样写,官方demo里也没有加id。 <button class="btn-refuse" catch:tap="clickRefuse">拒绝</button> refuse() { this.resolvePrivacyAuthorization({ event: 'disagree' }) }, 同意按钮比较特殊,布局需要用button这样写,记得给button加一个Id <button id="agree-btn" class="btn-agree" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacyAuthorization">同意</button> 然后在handleAgreePrivacyAuthorization里就可以获取到点击事件,这样写 handleAgreePrivacyAuthorization() { this.resolvePrivacyAuthorization({ buttonId: 'agree-btn', event: 'agree' }) }, 2、然后每个有隐私接口的页面引用自己的组件,代码如下 { "usingComponents": { "agreement": "/components/agreement/agreement" } } 布局引用 <!-- 隐私授权弹窗 --> <agreement id="agreement" frameTitle="温馨提示" bind:refuse="refuse" bind:agree="agree"></agreement> 让组件弹窗显示,这样写: this.selectComponent('#agreement').show(); 3、最后需要在用到隐私接口的页面的onShow里加上以下监听代码,在这里边让自定义的隐私弹窗显示出来即可。 onShow: function () { let that = this; if (wx.onNeedPrivacyAuthorization) { wx.onNeedPrivacyAuthorization(resolve => { this.selectComponent('#agreement').show();//这里是让组件弹窗显示 this.resolvePrivacyAuthorization = resolve }) } } 以上代码加上就可以了,如果业务逻辑用到了需要判断是否授权过,可以加上 wx.getPrivacySetting(Object object)去获取是否授权过,用不到可以不加这个判断。 //=======================以下是针对昵称输入框input的type="nickname"的代码=========================== 注意:以下代码只是针对input另外加的,上边提到的代码也都要加上才能正常弹出弹窗 布局,在input的外层View加touch事件,然后加一个focus动态控制焦点。 <view catch:touchstart="handleTouchInput"> <input type="nickname" class="text" bindinput="listenerContent" placeholder="输入名称" maxlength="10" value='{{name}}' focus="{{focus}}" /> </view> 然后,touch事件: handleTouchInput() { let that = this if (wx.getPrivacySetting) { wx.getPrivacySetting({//获取是否需要弹出 success: res => { if (res.needAuthorization) { wx.requirePrivacyAuthorize({//该接口请求隐私协议,会自动触发上边的wx.onNeedPrivacyAuthorization success: () => { that.setData({ focus: true, }) }, fail: () => {}, complete: () => {} }) } else { that.setData({ focus: true, }) } }, fail: () => {}, complete: () => {} }) } else { this.setData({ focus: true }) } }, 这样就可以了,记得把上边提到的监听代码都加上。
2023-08-23 - canvas绘制水印的问题?
[图片] 上面是代码 [图片] 下面是效果 我想实现全屏铺满 就是屏幕里多有几个文字 但是没办法实现
2023-05-22 - 小程序页面加水印,防止用户截图分享隐私数据
为了防止用户将小程序内的隐私数据进行截图或者录屏分享导致信息泄露,我们会在小程序全局添加一个水印浮层。这样即使被截图或者拍照,也能轻松地确定泄露的源头。 小程序防止用户截屏的方法有很多,其中一种常见的方法就是在小程序的页面中添加水印。具体实现方法如下: 在小程序中的页面中添加水印浮层,一般通过绝对定位来实现,这样可以使水印在页面的最上层,无法被其他元素覆盖。设计水印的样式和位置,通常可以在小程序的样式文件中设置,例如设置水印的位置为右下角,样式为半透明的字体,以免影响正常的内容展示。对于不同类型的页面,可以根据需要添加不同的水印,例如在敏感信息页面添加比较醒目的水印,而在其他普通页面只添加轻微的水印。如果需要防止用户截屏或者拍照,可以在小程序中添加截屏监听事件,并在用户进行截屏或者拍照操作时,自动添加水印。 总的来说,小程序防止用户截屏的方法还有很多,例如使用安全键盘等,但是添加水印仍然是最为常见的一种方法。通过添加水印,可以有效地防止用户截屏和分享敏感信息,保护用户和单位的信息安全。 下面是一个简单的示例,position选择固定定位fixed,固定定位会固定在浏览器窗口某个位置,不会随滚动条滚动。用z-index将元素的层级设置为最低,将view旋转45度,效果就出来啦 [图片] <view style="position: fixed;top: -10vh; left:-100vw;width: 250vw; height: 100vh; z-index: -999;transform: rotate(-45deg);"> <block wx:for="{{30}}" wx:key="index" wx:if="{{userInfo}}"> <view style="color:gray; margin:30rpx; padding:20rpx; opacity: 0.15;"> {{userInfo.user_name + ' ' + userInfo.user_phone}} {{userInfo.user_name + ' ' + userInfo.user_phone}} {{userInfo.user_name + ' ' + userInfo.user_phone}} {{userInfo.user_name + ' ' + userInfo.user_phone}} </view> </block> </view>
2023-02-23 - 在工具内从开发板切稳定版后Mock数据丢失
由于前几天用着开发版的工具,选择元素无法选中自定义组件内元素,于是在工具这个位置点击了切换最新稳定版本,稳定版没有这个问题了。 [图片] 但之后发现原先工具内的mock数据就不见了 [图片] 请问这些mock的数据有没有存在哪里?
2022-05-10 - 微信小程序生成海报图片导出相册
前言 小程序内通过静态模板和样式绘制 canvas ,导出图片,可用于生成分享图等场景 一、效果预览 点击生成海报 [图片] 这个是生成的保存到相册的图片 [图片] 二、使用步骤 1.安装引入wxml-to-canvas Step1.运行小程序npm安装命令 [代码]npm install --save wxml-to-canvas [代码] Step2.JSON 组件声明 [代码]{ "usingComponents": { "wxml-to-canvas": "wxml-to-canvas", } } [代码] 文章最后会有wxml-to-canvas的其它详细教程 2.创建js 创建的js放到页面文件夹即可 [图片] 代码如下(demo.js): [代码]var calendar = require('../../../utils/calendar'); var d=new Date(); let Year = d.getFullYear(); let Month = (d.getMonth()+1).toString().padStart(2,'0'); let Day = (d.getDate()).toString().padStart(2,'0'); let time = Month+ '/' + Day; let xq = "星期"+"天一二三四五六".charAt(d.getDay()); const wxml = (hb_describe,hb_bg,hb_wx2code)=>{ return ` <view class="container" > <view class="itembox1"> <text class="time">`+time+`</text> <view class="xqview"> <text class="xq">`+xq+`</text> <text class="xq2">`+calendar.default.solar2lunar(Year,Month,Day).gzYear+'年'+calendar.default.solar2lunar(Year,Month,Day).IMonthCn+calendar.default.solar2lunar(Year,Month,Day).IDayCn+`</text> </view> </view> <view class="itembox2"> <image class="img" mode="aspectFit" src="`+hb_bg+`"></image> </view> <view class="itembox3"> <text class="text">`+hb_describe+`</text> <image class="imgcode" src="`+hb_wx2code+`"></image> </view> </view> ` } let width=wx.getSystemInfoSync().windowWidth*0.8; let widthCenter=width*0.9; let width6=widthCenter*0.6;//描述占60% let width4=widthCenter*0.4; const style = { container: { width: width, height: 450, flexDirection: 'column', backgroundColor: '#FFFFFF', alignItems: 'center' }, itembox1: { width: widthCenter, height: 130 }, itembox2: { width: widthCenter, height: 200 }, itembox3: { width: widthCenter, height: 120, flexDirection: 'row', justifyContent:'space-around', alignItems: 'center', //paddingTop:20, //paddingBottom:20 }, time:{ fontSize:88, color:'#0222AC', textAlign:'center', fontStyle:'italic' }, xqview:{ fontSize:16, color:'#0222AC', marginTop:106 }, xq:{ textAlign:'left' }, xq2:{ textAlign:'right' }, img: { width: widthCenter, minHeight: 190, maxHeight: 200, }, imgcode: { width: 80, height: 80 }, text: { width: width6, height: 60, textAlign: 'center', verticalAlign: 'middle' } } module.exports = { wxml, style } [代码] 3.小程序页面 wxml: [代码]<!-- 海报 --> <view class="hbbox" wx:if="{{maskHidden}}"> <wxml-to-canvas class="widget" height='{{canvasH}}'></wxml-to-canvas> </view> <view class="padding-sm margin-top flex flex-direction renderToCanvas" wx:if="{{hexiao}}"> <button bindtap="renderToCanvas" class="cu-btn">活动打卡</button> </view> <view class='hbmask' hidden="{{maskHidden == false}}"> <button class='hbbaocun' hidden="{{maskHidden == false}}" bindtap='extraImage'>保存相册</button> </view> <!-- 海报end --> [代码] js: 博主这里是打开一个弹窗 在弹窗里展示生成海报,js代码全部放出来,可自行取用。 [代码]const { wxml, style } = require('./demo.js') Page({ data: { //海报 hb_wx2code: "", hb_bg: "", hb_describe: '', maskHidden: false, canvasH: 0, number:0, flag:'' }, //生成海报 makeHB(){ let that = this; that.setData({ maskHidden: true, canvasH:570 }) setTimeout(()=>{ const _wxml = wxml(that.data.hb_describe,that.data.hb_bg,that.data.hb_wx2code) wx.nextTick(()=>{ that.widget = that.selectComponent('.widget') const p1 = that.selectComponent('.widget').renderToCanvas({ wxml:_wxml , style }) p1.then((res) => { wx.hideToast(); that.container = res; }).catch(err=>{ }) }) },500) }, renderToCanvas() { let that = this; if(that.data.hb_wx2code){ wx.showToast({ title: '海报生成中...', icon: 'loading', duration: 1000 }); that.makeHB(); } else{ wx.showToast({ title: '海报生成中...', icon: 'loading', duration: 1000 }); const data={ open_id : wx.getStorageSync('openId'), type: '2', video_id:that.data.id } mycomm.ajaxDataGet("/inviteFriends/ordinary", data, function (res) { that.setData({ hb_wx2code: res.val.qr_code, hb_bg:res.val.background_img, hb_describe:res.val.describe }); that.makeHB(); }, true); } }, extraImage() { const p2 = this.widget.canvasToTempFilePath() p2.then(res => { // this.setData({ // src: res.tempFilePath, // width: this.container.layoutBox.width, // height: this.container.layoutBox.height // }) this.baocun(res.tempFilePath); }) }, //点击保存到相册 baocun: function (imagePath) { var that = this wx.saveImageToPhotosAlbum({ filePath: imagePath, success(res) { wx.showModal({ content: '海报已保存到相册', showCancel: false, confirmText: '确定', confirmColor: '#333', success: function (res) { if (res.confirm) { /* 该隐藏的隐藏 */ that.setData({ maskHidden: false }) } }, fail: function (res) {} }) } }) }, }) [代码] 4.农历日期js 这个是海报上根据当前日期对应的农历日期,不需要可以自行删除掉。 [代码]var calendar = { /** * 农历1900-2100的润大小信息表 * @Array Of Property * @return Hex */ lunarInfo:[0x04bd8,0x04ae0,0x0a570,0x054d5,0x0d260,0x0d950,0x16554,0x056a0,0x09ad0,0x055d2,//1900-1909 0x04ae0,0x0a5b6,0x0a4d0,0x0d250,0x1d255,0x0b540,0x0d6a0,0x0ada2,0x095b0,0x14977,//1910-1919 0x04970,0x0a4b0,0x0b4b5,0x06a50,0x06d40,0x1ab54,0x02b60,0x09570,0x052f2,0x04970,//1920-1929 0x06566,0x0d4a0,0x0ea50,0x16a95,0x05ad0,0x02b60,0x186e3,0x092e0,0x1c8d7,0x0c950,//1930-1939 0x0d4a0,0x1d8a6,0x0b550,0x056a0,0x1a5b4,0x025d0,0x092d0,0x0d2b2,0x0a950,0x0b557,//1940-1949 0x06ca0,0x0b550,0x15355,0x04da0,0x0a5b0,0x14573,0x052b0,0x0a9a8,0x0e950,0x06aa0,//1950-1959 0x0aea6,0x0ab50,0x04b60,0x0aae4,0x0a570,0x05260,0x0f263,0x0d950,0x05b57,0x056a0,//1960-1969 0x096d0,0x04dd5,0x04ad0,0x0a4d0,0x0d4d4,0x0d250,0x0d558,0x0b540,0x0b6a0,0x195a6,//1970-1979 0x095b0,0x049b0,0x0a974,0x0a4b0,0x0b27a,0x06a50,0x06d40,0x0af46,0x0ab60,0x09570,//1980-1989 0x04af5,0x04970,0x064b0,0x074a3,0x0ea50,0x06b58,0x05ac0,0x0ab60,0x096d5,0x092e0,//1990-1999 0x0c960,0x0d954,0x0d4a0,0x0da50,0x07552,0x056a0,0x0abb7,0x025d0,0x092d0,0x0cab5,//2000-2009 0x0a950,0x0b4a0,0x0baa4,0x0ad50,0x055d9,0x04ba0,0x0a5b0,0x15176,0x052b0,0x0a930,//2010-2019 0x07954,0x06aa0,0x0ad50,0x05b52,0x04b60,0x0a6e6,0x0a4e0,0x0d260,0x0ea65,0x0d530,//2020-2029 0x05aa0,0x076a3,0x096d0,0x04afb,0x04ad0,0x0a4d0,0x1d0b6,0x0d250,0x0d520,0x0dd45,//2030-2039 0x0b5a0,0x056d0,0x055b2,0x049b0,0x0a577,0x0a4b0,0x0aa50,0x1b255,0x06d20,0x0ada0,//2040-2049 /**Add By JJonline@JJonline.Cn**/ 0x14b63,0x09370,0x049f8,0x04970,0x064b0,0x168a6,0x0ea50, 0x06b20,0x1a6c4,0x0aae0,//2050-2059 0x092e0,0x0d2e3,0x0c960,0x0d557,0x0d4a0,0x0da50,0x05d55,0x056a0,0x0a6d0,0x055d4,//2060-2069 0x052d0,0x0a9b8,0x0a950,0x0b4a0,0x0b6a6,0x0ad50,0x055a0,0x0aba4,0x0a5b0,0x052b0,//2070-2079 0x0b273,0x06930,0x07337,0x06aa0,0x0ad50,0x14b55,0x04b60,0x0a570,0x054e4,0x0d160,//2080-2089 0x0e968,0x0d520,0x0daa0,0x16aa6,0x056d0,0x04ae0,0x0a9d4,0x0a2d0,0x0d150,0x0f252,//2090-2099 0x0d520],//2100 /** * 公历每个月份的天数普通表 * @Array Of Property * @return Number */ solarMonth:[31,28,31,30,31,30,31,31,30,31,30,31], /** * 天干地支之天干速查表 * @Array Of Property trans["甲","乙","丙","丁","戊","己","庚","辛","壬","癸"] * @return Cn string */ Gan:["\u7532","\u4e59","\u4e19","\u4e01","\u620a","\u5df1","\u5e9a","\u8f9b","\u58ec","\u7678"], /** * 天干地支之地支速查表 * @Array Of Property * @trans["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"] * @return Cn string */ Zhi:["\u5b50","\u4e11","\u5bc5","\u536f","\u8fb0","\u5df3","\u5348","\u672a","\u7533","\u9149","\u620c","\u4ea5"], /** * 天干地支之地支速查表<=>生肖 * @Array Of Property * @trans["鼠","牛","虎","兔","龙","蛇","马","羊","猴","鸡","狗","猪"] * @return Cn string */ Animals:["\u9f20","\u725b","\u864e","\u5154","\u9f99","\u86c7","\u9a6c","\u7f8a","\u7334","\u9e21","\u72d7","\u732a"], /** * 阳历节日 */ festival: { '1-1': {title: '元旦节'}, '2-14': {title: '情人节'}, '5-1': {title: '劳动节'}, '5-4': {title: '青年节'}, '6-1': {title: '儿童节'}, '9-10': {title: '教师节'}, '10-1': {title: '国庆节'}, '12-25': {title: '圣诞节'}, '3-8': {title: '妇女节'}, '3-12': {title: '植树节'}, '4-1': {title: '愚人节'}, '5-12': {title: '护士节'}, '7-1': {title: '建党节'}, '8-1': {title: '建军节'}, '12-24': {title: '平安夜'}, }, /** * 农历节日 */ lfestival: { '12-30': {title: '除夕'}, '1-1': {title: '春节'}, '1-15': {title: '元宵节'}, '5-5': {title: '端午节'}, '8-15': {title: '中秋节'}, '9-9': {title: '重阳节'}, }, /** * 返回默认定义的阳历节日 */ getFestival(){ return this.festival }, /** * 返回默认定义的内容里节日 */ getLunarFestival(){ return this.lfestival }, /** * * @param {Object} 按照festival的格式输入数据,设置阳历节日 */ setFestival(param={}){ this.festival = param }, /** * * @param {Object} 按照lfestival的格式输入数据,设置农历节日 */ setLunarFestival(param={}){ this.lfestival = param }, /** * 24节气速查表 * @Array Of Property * @trans["小寒","大寒","立春","雨水","惊蛰","春分","清明","谷雨","立夏","小满","芒种","夏至","小暑","大暑","立秋","处暑","白露","秋分","寒露","霜降","立冬","小雪","大雪","冬至"] * @return Cn string */ solarTerm:["\u5c0f\u5bd2","\u5927\u5bd2","\u7acb\u6625","\u96e8\u6c34","\u60ca\u86f0","\u6625\u5206","\u6e05\u660e","\u8c37\u96e8","\u7acb\u590f","\u5c0f\u6ee1","\u8292\u79cd","\u590f\u81f3","\u5c0f\u6691","\u5927\u6691","\u7acb\u79cb","\u5904\u6691","\u767d\u9732","\u79cb\u5206","\u5bd2\u9732","\u971c\u964d","\u7acb\u51ac","\u5c0f\u96ea","\u5927\u96ea","\u51ac\u81f3"], /** * 1900-2100各年的24节气日期速查表 * @Array Of Property * @return 0x string For splice */ sTermInfo:['9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e','97bcf97c3598082c95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f','b027097bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f','97bd0b06bdb0722c965ce1cfcc920f','b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e','97bcf97c359801ec95f8c965cc920f','97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa','9778397bd19801ec9210c965cc920e','97b6b97bd19801ec95f8c965cc920f', '97bd09801d98082c95f8e1cfcc920f','97bd097bd097c36b0b6fc9210c8dc2','9778397bd197c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e','97bd09801d98082c95f8e1cfcc920f','97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa','97b6b97bd19801ec95f8c965cc920e','97bcf97c3598082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2','9778397bd097c36c9210c9274c91aa','97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f','97bd097bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e','97bcf97c3598082c95f8c965cc920f','97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e','97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f','97bd097bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e','97bcf97c359801ec95f8c965cc920f','97bd097bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9210c8dc2','9778397bd19801ec9210c9274c920e','97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722','7f0e397bd097c36b0b6fc9210c8dc2','9778397bd097c36c9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f','97bd07f5307f595b0b0bc920fb0722','7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa','97b6b97bd19801ec9210c965cc920e','97bd07f1487f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2','9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e','97bcf7f1487f595b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e','97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722','7f0e397bd07f595b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c9274c920e','97bcf7f0e47f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c91aa','97b6b97bd197c36c9210c9274c920e','97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722','9778397bd097c36b0b6fc9210c8dc2','9778397bd097c36c9210c9274c920e', '97b6b7f0e47f531b0723b0b6fb0722','7f0e37f5307f595b0b0bc920fb0722','7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b70c9274c91aa','97b6b7f0e47f531b0723b0b6fb0721','7f0e37f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc9210c8dc2','9778397bd097c36b0b6fc9274c91aa','97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f595b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa','97b6b7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa','97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0787b0721','7f0e27f0e47f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c91aa','97b6b7f0e47f149b0723b0787b0721','7f0e27f0e47f531b0723b0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722','9778397bd097c36b0b6fc9210c8dc2','977837f0e37f149b0723b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722','7f0e37f5307f595b0b0bc920fb0722','7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b0721','7f07e7f0e47f531b0723b0b6fb0721','7f0e37f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc9210c8dc2','977837f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722','977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722','977837f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722','977837f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721','7f0e27f0e47f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0723b06bd','7f07e7f0e37f149b0723b0787b0721','7f0e27f0e47f531b0723b0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722','977837f0e37f14898082b0723b02d5','7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722','7f0e37f1487f595b0b0bb0b6fb0722','7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721','7f07e7f0e47f531b0723b0b6fb0722','7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5','7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f531b0b0bb0b6fb0722','7f0e37f0e37f14898082b072297c35','7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722','7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35','7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f149b0723b0787b0721', '7f0e27f1487f531b0b0bb0b6fb0722','7f0e37f0e366aa89801eb072297c35','7ec967f0e37f14998082b0723b06bd', '7f07e7f0e47f149b0723b0787b0721','7f0e27f0e47f531b0723b0b6fb0722','7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd','7f07e7f0e37f14998083b0787b0721','7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35','7ec967f0e37f14898082b0723b02d5','7f07e7f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722','7f0e36665b66aa89801e9808297c35','665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721','7f07e7f0e47f531b0723b0b6fb0722','7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b0723b02d5','7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721', '7f0e36665b66a449801e9808297c35','665f67f0e37f14898082b072297c35','7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721','7f0e26665b66a449801e9808297c35','665f67f0e37f1489801eb072297c35', '7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722'], /** * 数字转中文速查表 * @Array Of Property * @trans ['日','一','二','三','四','五','六','七','八','九','十'] * @return Cn string */ nStr1:["\u65e5","\u4e00","\u4e8c","\u4e09","\u56db","\u4e94","\u516d","\u4e03","\u516b","\u4e5d","\u5341"], /** * 日期转农历称呼速查表 * @Array Of Property * @trans ['初','十','廿','卅'] * @return Cn string */ nStr2:["\u521d","\u5341","\u5eff","\u5345"], /** * 月份转农历称呼速查表 * @Array Of Property * @trans ['正','一','二','三','四','五','六','七','八','九','十','冬','腊'] * @return Cn string */ nStr3:["\u6b63","\u4e8c","\u4e09","\u56db","\u4e94","\u516d","\u4e03","\u516b","\u4e5d","\u5341","\u51ac","\u814a"], /** * 农历年份数字称呼速查表 * @Array Of Property * @trans ['零','一','二','三','四','五','六','七','八','九','十'] * @return Cn string */ nStr4:["\u96f6","\u4e00","\u4e8c","\u4e09","\u56db","\u4e94","\u516d","\u4e03","\u516b","\u4e5d","\u5341"], /** * 返回农历y年一整年的总天数 * @param lunar Year * @return Number * @eg:var count = calendar.lYearDays(1987) ;//count=387 */ lYearDays:function(y) { var i, sum = 348; for(i=0x8000; i>0x8; i>>=1) { sum += (this.lunarInfo[y-1900] & i)? 1: 0; } return(sum+this.leapDays(y)); }, /** * 返回农历y年闰月是哪个月;若y年没有闰月 则返回0 * @param lunar Year * @return Number (0-12) * @eg:var leapMonth = calendar.leapMonth(1987) ;//leapMonth=6 */ leapMonth:function(y) { //闰字编码 \u95f0 return(this.lunarInfo[y-1900] & 0xf); }, /** * 返回农历y年闰月的天数 若该年没有闰月则返回0 * @param lunar Year * @return Number (0、29、30) * @eg:var leapMonthDay = calendar.leapDays(1987) ;//leapMonthDay=29 */ leapDays:function(y) { if(this.leapMonth(y)) { return((this.lunarInfo[y-1900] & 0x10000)? 30: 29); } return(0); }, /** * 返回农历y年m月(非闰月)的总天数,计算m为闰月时的天数请使用leapDays方法 * @param lunar Year * @return Number (-1、29、30) * @eg:var MonthDay = calendar.monthDays(1987,9) ;//MonthDay=29 */ monthDays:function(y,m) { if(m>12 || m<1) {return -1}//月份参数从1至12,参数错误返回-1 return( (this.lunarInfo[y-1900] & (0x10000>>m))? 30: 29 ); }, /** * 返回公历(!)y年m月的天数 * @param solar Year * @return Number (-1、28、29、30、31) * @eg:var solarMonthDay = calendar.leapDays(1987) ;//solarMonthDay=30 */ solarDays:function(y,m) { if(m>12 || m<1) {return -1} //若参数错误 返回-1 var ms = m-1; if(ms==1) { //2月份的闰平规律测算后确认返回28或29 return(((y%4 == 0) && (y%100 != 0) || (y%400 == 0))? 29: 28); }else { return(this.solarMonth[ms]); } }, /** * 农历年份转换为干支纪年 * @param lYear 农历年的年份数 * @return Cn string */ toGanZhiYear:function(lYear) { var ganKey = (lYear - 3) % 10; var zhiKey = (lYear - 3) % 12; if(ganKey == 0) ganKey = 10;//如果余数为0则为最后一个天干 if(zhiKey == 0) zhiKey = 12;//如果余数为0则为最后一个地支 return this.Gan[ganKey-1] + this.Zhi[zhiKey-1]; }, /** * 公历月、日判断所属星座 * @param cMonth [description] * @param cDay [description] * @return Cn string */ toAstro:function(cMonth,cDay) { var s = "\u9b54\u7faf\u6c34\u74f6\u53cc\u9c7c\u767d\u7f8a\u91d1\u725b\u53cc\u5b50\u5de8\u87f9\u72ee\u5b50\u5904\u5973\u5929\u79e4\u5929\u874e\u5c04\u624b\u9b54\u7faf"; var arr = [20,19,21,21,21,22,23,23,23,23,22,22]; return s.substr(cMonth*2 - (cDay < arr[cMonth-1] ? 2 : 0),2) + "\u5ea7";//座 }, /** * 传入offset偏移量返回干支 * @param offset 相对甲子的偏移量 * @return Cn string */ toGanZhi:function(offset) { return this.Gan[offset%10] + this.Zhi[offset%12]; }, /** * 传入公历(!)y年获得该年第n个节气的公历日期 * @param y公历年(1900-2100);n二十四节气中的第几个节气(1~24);从n=1(小寒)算起 * @return day Number * @eg:var _24 = calendar.getTerm(1987,3) ;//_24=4;意即1987年2月4日立春 */ getTerm:function(y,n) { if(y<1900 || y>2100) {return -1;} if(n<1 || n>24) {return -1;} var _table = this.sTermInfo[y-1900]; var _info = [ parseInt('0x'+_table.substr(0,5)).toString() , parseInt('0x'+_table.substr(5,5)).toString(), parseInt('0x'+_table.substr(10,5)).toString(), parseInt('0x'+_table.substr(15,5)).toString(), parseInt('0x'+_table.substr(20,5)).toString(), parseInt('0x'+_table.substr(25,5)).toString() ]; var _calday = [ _info[0].substr(0,1), _info[0].substr(1,2), _info[0].substr(3,1), _info[0].substr(4,2), _info[1].substr(0,1), _info[1].substr(1,2), _info[1].substr(3,1), _info[1].substr(4,2), _info[2].substr(0,1), _info[2].substr(1,2), _info[2].substr(3,1), _info[2].substr(4,2), _info[3].substr(0,1), _info[3].substr(1,2), _info[3].substr(3,1), _info[3].substr(4,2), _info[4].substr(0,1), _info[4].substr(1,2), _info[4].substr(3,1), _info[4].substr(4,2), _info[5].substr(0,1), _info[5].substr(1,2), _info[5].substr(3,1), _info[5].substr(4,2), ]; return parseInt(_calday[n-1]); }, /** * 传入年份返回汉语通俗表示法 * @param {lunar} year * @return CN {string} * @eg:let cnYear = calendar.toChinaYear(1960);//cnYear = '一九六零' */ toChinaYear:function(y){ //年 let year = y.toString().split(""); return `${this.nStr4[year[0]]}${this.nStr4[year[1]]}${this.nStr4[year[2]]}${this.nStr4[year[3]]}`; }, /** * 传入农历数字月份返回汉语通俗表示法 * @param lunar month * @return Cn string * @eg:var cnMonth = calendar.toChinaMonth(12) ;//cnMonth='腊月' */ toChinaMonth:function(m) { // 月 => \u6708 if(m>12 || m<1) {return -1} //若参数错误 返回-1 var s = this.nStr3[m-1]; s+= "\u6708";//加上月字 return s; }, /** * 传入农历日期数字返回汉字表示法 * @param lunar day * @return Cn string * @eg:var cnDay = calendar.toChinaDay(21) ;//cnMonth='廿一' */ toChinaDay:function(d){ //日 => \u65e5 var s; switch (d) { case 10: s = '\u521d\u5341'; break; case 20: s = '\u4e8c\u5341'; break; break; case 30: s = '\u4e09\u5341'; break; break; default : s = this.nStr2[Math.floor(d/10)]; s += this.nStr1[d%10]; } return(s); }, /** * 年份转生肖[!仅能大致转换] => 精确划分生肖分界线是“立春” * @param y year * @return Cn string * @eg:var animal = calendar.getAnimal(1987) ;//animal='兔' */ getAnimal: function(y) { return this.Animals[(y - 4) % 12] }, /** * 传入阳历年月日获得详细的公历、农历object信息 <=>JSON * @param y solar year * @param m solar month * @param d solar day * @return JSON object * @eg:console.log(calendar.solar2lunar(1987,11,01)); */ solar2lunar:function (y,m,d) { //参数区间1900.1.31~2100.12.31 y = parseInt(y) m = parseInt(m) d = parseInt(d) //年份限定、上限 if(y<1900 || y>2100) { return -1;// undefined转换为数字变为NaN } //公历传参最下限 if(y==1900&&m==1&&d<31) { return -1; } //未传参 获得当天 if(!y) { var objDate = new Date(); }else { var objDate = new Date(y,parseInt(m)-1,d) } var i, leap=0, temp=0; //修正ymd参数 var y = objDate.getFullYear(), m = objDate.getMonth()+1, d = objDate.getDate(); var offset = (Date.UTC(objDate.getFullYear(),objDate.getMonth(),objDate.getDate()) - Date.UTC(1900,0,31))/86400000; for(i=1900; i<2101 && offset>0; i++) { temp = this.lYearDays(i); offset -= temp; } if(offset<0) { offset+=temp; i--; } //是否今天 var isTodayObj = new Date(), isToday = false; if(isTodayObj.getFullYear()==y && isTodayObj.getMonth()+1==m && isTodayObj.getDate()==d) { isToday = true; } //星期几 var nWeek = objDate.getDay(), cWeek = this.nStr1[nWeek]; //数字表示周几顺应天朝周一开始的惯例 if(nWeek==0) { nWeek = 7; } //农历年 var year = i; var leap = this.leapMonth(i); //闰哪个月 var isLeap = false; //效验闰月 for(i=1; i<13 && offset>0; i++) { //闰月 if(leap>0 && i==(leap+1) && isLeap==false){ --i; isLeap = true; temp = this.leapDays(year); //计算农历闰月天数 } else{ temp = this.monthDays(year, i);//计算农历普通月天数 } //解除闰月 if(isLeap==true && i==(leap+1)) { isLeap = false; } offset -= temp; } // 闰月导致数组下标重叠取反 if(offset==0 && leap>0 && i==leap+1) { if(isLeap){ isLeap = false; }else{ isLeap = true; --i; } } if(offset<0) { offset += temp; --i; } //农历月 var month = i; //农历日 var day = offset + 1; //天干地支处理 var sm = m-1; var gzY = this.toGanZhiYear(year); // 当月的两个节气 // bugfix-2017-7-24 11:03:38 use lunar Year Param `y` Not `year` var firstNode = this.getTerm(y,(m*2-1));//返回当月「节」为几日开始 var secondNode = this.getTerm(y,(m*2));//返回当月「节」为几日开始 // 依据12节气修正干支月 var gzM = this.toGanZhi((y-1900)*12+m+11); if(d>=firstNode) { gzM = this.toGanZhi((y-1900)*12+m+12); } //传入的日期的节气与否 var isTerm = false; var Term = null; if(firstNode==d) { isTerm = true; Term = this.solarTerm[m*2-2]; } if(secondNode==d) { isTerm = true; Term = this.solarTerm[m*2-1]; } //日柱 当月一日与 1900/1/1 相差天数 var dayCyclical = Date.UTC(y,sm,1,0,0,0,0)/86400000+25567+10; var gzD = this.toGanZhi(dayCyclical+d-1); //该日期所属的星座 var astro = this.toAstro(m,d); var solarDate = y+'-'+m+'-'+d var lunarDate = year+'-'+month+'-'+day var festival = this.festival var lfestival = this.lfestival var festivalDate = m+'-'+d var lunarFestivalDate = month+'-'+day return { date: solarDate, lunarDate: lunarDate, festival: festival[festivalDate] ? festival[festivalDate].title : null, lunarFestival: lfestival[lunarFestivalDate] ? lfestival[lunarFestivalDate].title : null, 'lYear':year, 'lMonth':month, 'lDay':day, 'Animal':this.getAnimal(year), 'IMonthCn':(isLeap?"\u95f0":'')+this.toChinaMonth(month), 'IDayCn':this.toChinaDay(day), 'cYear':y, 'cMonth':m, 'cDay':d, 'gzYear':gzY, 'gzMonth':gzM, 'gzDay':gzD, 'isToday':isToday, 'isLeap':isLeap, 'nWeek':nWeek, 'ncWeek':"\u661f\u671f"+cWeek, 'isTerm':isTerm, 'Term':Term, 'astro':astro }; }, /** * 传入农历年月日以及传入的月份是否闰月获得详细的公历、农历object信息 <=>JSON * @param y lunar year * @param m lunar month * @param d lunar day * @param isLeapMonth lunar month is leap or not.[如果是农历闰月第四个参数赋值true即可] * @return JSON object * @eg:console.log(calendar.lunar2solar(1987,9,10)); */ lunar2solar:function(y,m,d,isLeapMonth) { //参数区间1900.1.31~2100.12.1 y = parseInt(y) m = parseInt(m) d = parseInt(d) var isLeapMonth = !!isLeapMonth; var leapOffset = 0; var leapMonth = this.leapMonth(y); var leapDay = this.leapDays(y); if(isLeapMonth&&(leapMonth!=m)) {return -1;}//传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同 if(y==2100&&m==12&&d>1 || y==1900&&m==1&&d<31) {return -1;}//超出了最大极限值 var day = this.monthDays(y,m); var _day = day; //bugFix 2016-9-25 //if month is leap, _day use leapDays method if(isLeapMonth) { _day = this.leapDays(y,m); } if(y < 1900 || y > 2100 || d > _day) {return -1;}//参数合法性效验 //计算农历的时间差 var offset = 0; for(var i=1900;i<y;i++) { offset+=this.lYearDays(i); } var leap = 0,isAdd= false; for(var i=1;i<m;i++) { leap = this.leapMonth(y); if(!isAdd) {//处理闰月 if(leap<=i && leap>0) { offset+=this.leapDays(y);isAdd = true; } } offset+=this.monthDays(y,i); } //转换闰月农历 需补充该年闰月的前一个月的时差 if(isLeapMonth) {offset+=day;} //1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点) var stmap = Date.UTC(1900,1,30,0,0,0); var calObj = new Date((offset+d-31)*86400000+stmap); var cY = calObj.getUTCFullYear(); var cM = calObj.getUTCMonth()+1; var cD = calObj.getUTCDate(); return this.solar2lunar(cY,cM,cD); } }; export default calendar [代码] 附其它: wxml-to-canvas的使用 wxml 引入组件 [代码]<video class="video" src="{{src}}"> <wxml-to-canvas class="widget"></wxml-to-canvas> </video> <image src="{{src}}" style="width: {{width}}px; height: {{height}}px"></image> [代码] js 获取实例 [代码]const {wxml, style} = require('./demo.js') Page({ data: { src: '' }, onLoad() { this.widget = this.selectComponent('.widget') }, renderToCanvas() { const p1 = this.widget.renderToCanvas({ wxml, style }) p1.then((res) => { this.container = res this.extraImage() }) }, extraImage() { const p2 = this.widget.canvasToTempFilePath() p2.then(res => { this.setData({ src: res.tempFilePath, width: this.container.layoutBox.width, height: this.container.layoutBox.height }) }) } }) [代码] wxml模板支持 view、text、image 三种标签,通过 class 匹配 style 对象中的样式。 样式对象属性值为对应 wxml 标签的 class 驼峰形式。需为每个元素指定 width 和 height 属性,否则会导致布局错误。 存在多个 className 时,位置靠后的优先级更高,子元素会继承父级元素的可继承属性。 元素均为 flex 布局。left/top 等 仅在 absolute 定位下生效。
2023-06-07 - 关于跳转空白页的问题?
今天测试出来一个bug,各种方法试了都没有解决,求教大神! 问题如下:登录页登录后 navigateTo 跳转到 个人中心页,从个人中心页的一个入口 navigateTo跳转到一个 管理页面会出现一个空白页,但是重新进入小程序后就能正常的跳转!另外,PC端的开发者工具一切正常,手机打开调试安卓正常,IOS不正常,不打开调试就一直有这个问题!我把那个管理页面的 js什么都注释掉,只写了一个静态页面测试还有这个问题,使用redirectTo进行跳转也不行。。。求教大神
2022-10-19 - 如何彻底解决小程序滚动穿透问题
背景 俗话说,产品有三宝:弹窗、浮层加引导,足以见弹窗在产品同学心目中的地位。对任意一个刚入门的前端同学来说,实现一个模态框基本都可以达到信手拈来的地步,但是,当模态框里边的内容滚动起来以后,就会出现各种各样的让人摸不着头脑的问题,其中,最出名的想必就是滚动穿透。 什么是滚动穿透? 滚动穿透的定义:指我们滑动顶层的弹窗,但效果上却滑动了底层的内容。 具体解决方案分析如下: 改变顶层:从穿透的思路考虑,如果顶层不会穿透过去,那么问题就解决了,所以我们尝试给蒙层加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,近期开放。
08-06 - 小程序用户头像昵称获取规则调整公告
更新时间:2022年11月9日由于 PC/macOS 平台「头像昵称填写能力」存在兼容性问题,对于来自低于2.27.1版本的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称。 更新时间:2022年9月28日考虑到近期开发者对小程序用户头像昵称获取规则调整的相关反馈,平台将接口回收的截止时间由2022年10月25日延期至2022年11月8日24时。 调整背景在小程序内,开发者可以通过 wx.login 接口直接获取用户的 openId 与 unionId 信息,实现微信身份登录,支持开发者在多个小程序或其它应用间匿名关联同一用户。 同时,为了满足部分小程序业务中需要创建用户的昵称与头像的诉求,平台提供了 wx.getUserProfile 接口,支持在用户授权的前提下,快速使用自己的微信昵称头像。 但实践中发现有部分小程序,在用户刚打开小程序时就要求收集用户的微信昵称头像,或者在支付前等不合理路径上要求授权。如果用户拒绝授权,则无法使用小程序或相关功能。在已经获取用户的 openId 与 unionId 信息情况下,用户的微信昵称与头像并不是用户使用小程序的必要条件。为减少此类不合理的强迫授权情况,作出如下调整。 调整说明自 2022 年 10 月 25 日 24 时后(以下统称 “生效期” ),用户头像昵称获取规则将进行如下调整: 自生效期起,小程序 wx.getUserProfile 接口将被收回:生效期后发布的小程序新版本,通过 wx.getUserProfile 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。自生效期起,插件通过 wx.getUserInfo 接口获取用户昵称头像将被收回:生效期后发布的插件新版本,通过 wx.getUserInfo 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的插件版本不受影响,但如果要进行版本更新则需要进行适配。通过 wx.login 与 wx.getUserInfo 接口获取 openId、unionId 能力不受影响。「头像昵称填写能力」支持获取用户头像昵称:如业务需获取用户头像昵称,可以使用「头像昵称填写能力」(基础库 2.21.2 版本开始支持,覆盖iOS与安卓微信 8.0.16 以上版本),具体实践可见下方《最佳实践》。小程序 wx.getUserProfile 与插件 wx.getUserInfo 接口兼容基础库 2.27.1 以下版本的头像昵称获取需求:对于来自低版本的基础库与微信客户端的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称,开发者可继续使用以上能力做向下兼容。对于上述 3,wx.getUserProfile 接口、wx.getUserInfo 接口、头像昵称填写能力的基础库版本支持能力详细对比见下表: [图片] *针对低版本基础库,兼容处理可参考 兼容文档 请已使用 wx.getUserProfile 接口的小程序开发者和已使用 wx.getUserInfo 接口的插件开发者尽快适配。小游戏不受本次调整影响。 最佳实践小程序可在个人中心或设置等页面使用头像昵称填写能力让用户完善个人资料: [图片] 微信团队 2022年5月9日
2023-09-26 - 那些年你没权限调用的API
api 顾名思义 wx.openMiniProgramHistoryList wx.openMiniProgramProfile wx.openMiniProgramSearch wx.openMiniProgramStarList 不再多BB直接上图 1.隐藏/显示右上角胶囊按钮 [图片] 2.截屏 [图片] 最近玩console有点上头 现在的小程序api已经达到了373个了 我是怎么知道的?看图 [图片] 往下拉一看 373 [图片] 这里边有很多的api 是文档中没写的(也可能永远不会写) 先说下 普通的小程序里边是没有权限的调用会提示 {errMsg: “openUserProfile:fail:access denied”} 或者 {errMsg: “getABTestConfig:fail permission denied”, errCode: 1} 代码片段:https://developers.weixin.qq.com/s/FgIssdmP7jdv 可用的api wx.navigateBackH5 --webview 通过这个api可以返回上一个web页面 更多的BUG等你们去处理 我吃饭去咯 end
2019-11-28 - 小技巧!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 - 安卓企业微信中分享小程序webview卡片,打开链接变about:blank,页面空白
荣耀手机和华为P30手机的3.1.6版本企业微信客户端进入小程序分享小程序的webview卡片,不管分享到企业微信还是微信,打开分享卡片h5链接都变成了about:blank,页面空白。 尝试用3.0.30版本企业微信分享无问题,用微信客户端分享无问题。 尝试用iphone的3.1.6版本企业微信分享也无问题。 初步判断是3.1.6版本安卓端企业微信客户端的问题。
2021-04-09 - 提高性能的几种写法
任何优秀的大软件里面都是一个优秀的小程序。<br> 点完赞,在查看哦 setData 频繁使用setData会造成js阻塞问题,甚至卡顿,崩溃(崩溃没试过哈);所以我们应该如何用正确的姿势提高性能呢? 尽量把需要setData的内容放到一起setData,不要出现一个函数多个或者同时使用setData; 更新对象内的某个值,不应该整个对象重新更新,只需要更新对应的值即可,例如<br> [代码]this.setData({'userInfo.headImg': '更新的内容'})[代码]; 如果是列表数据,需要改变某个值变成true,也不应该全部数据更新一遍,只需要更新某一个值即可,例如<br> [代码]this.setData({[`list[${index}].show`]: true})[代码]; 页面不需要渲染的数据,尽量不要写在js里面的data里面,你可以定义一个全局参数都可以或者this.timer等生成一个全局变量; setData单次不能超过1M,超过会导致设置不成功,建议分页数据可以使用二维数组设置,例如 [代码]// 1.通过一个二维数组来存储数据 let feedList = [[array]]; // 2.维护一个页面变量值,加载完一次数据page++ let page = 1 // 3.页面每次滚动到底部,通过数据路径更新数据 onReachBottom:()=>{ fetchNewData().then((newVal)=>{ this.setData({ ['feedList[' + (page - 1) + ']']: newVal, }) } } // 4.最终我们的数据是[[array1],[array2]]这样的格式,然后通过wx:for遍历渲染数据 [代码] relativeToViewport 我们会发现很多时候,列表展示都是图文展示,而图片后端又没有提供缩略图,这会导致浏览器一次请求多张图片会非常消耗http请求,并且浏览器也有请求数量限制,例如chrome浏览器就限制一次性最多6个,严重还会影响我们业务接口请求,这时我们使用[代码]IntersectionObserver[代码]就太美妙了 解决思路 先创建对象实例[代码]wx.createIntersectionObserver()[代码]; 用数据列表遍历监听页面渲染的元素; 条件判断,动态设置状态显示图片,如果返回滚动则过滤不需要在setData,减轻js工作任务; js代码 [代码] data: { list: [ // 测试数据自己随便造 {id: 0, url: 'https://devapicard.itop123.com/files/img/20201213/20201213141209123634220.png'}, {id: 1, url: 'https://devapicard.itop123.com/files/img/20201213/20201213141209123634220.png'}, {id: 2, url: 'https://devapicard.itop123.com/files/img/20201213/20201213141209123634220.png'}, ] }, onReady() { let list = this.data.list; list.forEach((item,index)=>{ // 遍历监听元素在页面的位置 wx.createIntersectionObserver().relativeToViewport({bottom: 0}).observe(`.img-${index}`,res=>{ if (res.intersectionRatio > 0 && !list[index].show){ // intersectionRatio值大于0,说明元素出现在视图中了 // console.log(item, 'list', res) this.setData({ [`list[${index}].show`]: true }) } }) }) }, [代码] wxml代码 [代码] <view> <block wx:for="{{list}}" mode="aspectFill" wx:key="index"> <view class="test-img img-{{index}}"> <image class="image" wx:if="{{item.show}}" src="{{item.url}}"></image> </view> </block> </view> [代码] wxss代码 [代码].test-img{ width: 600rpx; height: 400rpx; margin-bottom: 20rpx; overflow: hidden; } .image{ width: 100%; } [代码] onPageScroll 这个监听页面滚动事件,输出顶部滚出页面多少距离,单位是px;这个事件非必要时不要写也不要输出内容,这样会很消耗性能,如果必须要使用到,我们要学会写节流或者防抖事件,减少过快的去setData;下面代码是网上复制,结合自己业务改造下即可 [代码]// 防抖 function debounce(fn, wait) { var timeout = null; return function() { if(timeout !== null) clearTimeout(timeout); timeout = setTimeout(fn, wait); } } // 处理函数 function handle() { console.log(Math.random()); } // 滚动事件 window.addEventListener('scroll', debounce(handle, 1000)); // 节流throttle代码(定时器): var throttle = function(func, delay) { var timer = null; return function() { var context = this; var args = arguments; if (!timer) { timer = setTimeout(function() { func.apply(context, args); timer = null; }, delay); } } } function handle() { console.log(Math.random()); } window.addEventListener('scroll', throttle(handle, 1000)); [代码] 代码分包 代码肯定是越写越多,需求也是越来越多的,小程序单个分包最大只能是2M,这就不能让我们的业务代码都写在主包,这会导致我们无法提交代码,并且也会导致我们打开小程序会很慢,体验很差。 为了解决这个问题,微信提供了小程序总共代码包支持最大16M,还支持我们分包功能,这样我们就可以开发更强大的功能,分包又分为普通分包和独立分包,他们又是什么关系,有何不同? 总的不同是,主包不能调用任何分包的东西,例如js,组件等,但是分包或者独立分包是可以调用或者使用主包的任何东西 分包:分包就是一个可以节省主包代码的一个普通分包,启动分包页面,会把主包下载,普通分包可以使用主包的 js 文件、template、wxss、自定义组件、插件等 独立分包:独立分包不依赖主包即可运行,不需要下载主包,可以很大程度上提升分包页面的启动速度,固并不能使用主包的 js,组件,wxss,插件等 预加载:分包后,如果想打开主包后,跳转不出现加载模块中的等待提示,可以使用预加载,这样就可以做到无缝跳转,增强了用户体验,跳转就像主包跳转主包页面一样顺滑。更多分包讲解请查看微信文档 [代码]{ "pages": [ "pages/index", "pages/logs" ], "subpackages": [ { "root": "moduleA", // 分包 "pages": [ "pages/rabbit", "pages/squirrel" ] }, { "root": "moduleB", // 独立分包 "pages": [ "pages/pear", "pages/pineapple" ], "independent": true // 独立分包的标志 } ], "preloadRule": { // 预加载写法 "pages/index": { "network": "all", "packages": ["moduleA"] // 需要预加载的分包 }, "pages/logs": { "packages": ["moduleB"] // 需要预加载的分包 } } } [代码] 结束 本次写作先到这里,更多丰富好用的内容,请等待下一期~ 都看完了,别忘了给个赞再走~,创作不容易,谢谢哦 本文与掘金号文章同步,欢迎查阅掘金对应的文章
2021-07-09 - 通过微信首页的浮窗从后台调出小程序时常常出现内容空白?
请问下各位,此问题可能原因有哪些了?此问题出现在ios上。从微信浮窗热启动(测试发现短时间内ios,android都是热启动)小程序,此机制是官方维护,不在开发者控制范围。白屏,可能原因是小程序放到浮窗长时间,然后再点击进入。解决此问题,把微信进程删掉,重开应该能可以。 以下为问题截图,切换3个tabbar都是白屏。 [图片]
2020-05-29 - 如何在微信公众号的文章中插入答题活动小程序的链接
如果你是新媒体运营者,如果你也有自己的微信公众号,那么,如果你想结合微信答题小程序,来做知识竞赛活动的话,会存在这样的场景,只有关注微信公众号才能参与答题活动,或者是在微信公众号的文章中插入答题活动小程序的链接。 场景案例: [图片] 如何在微信公众号的文章中插入答题活动小程序的链接?其实非常简单,操作步骤如下。 1)登录微信公众号后,点击“图文素材”,点击“新的创作”,如下图所示: [图片] 2)在图文编辑界面,点击“小程序”,如下图所示: [图片] 3)在弹出的界面,输入小程序名称/AppID,按回车键进行搜索,搜索到后,点击“下一步” [图片] 4)可以看见,有4种展示方式,我们不妨分别试试看,都有什么效果,然后选择适配文章意境的其中一种 [图片] ①展示方式一:文字 [图片] 展示效果 [图片] ②展示方式二:图片 [图片] 展示效果 [图片] ③展示方式三:小程序卡片 [图片] 展示效果 [图片] ④展示方式四:小程序码 [图片] 展示效果 我是一张小程序码图片.jpg~ 总结 在微信公众号的运营当中,会经常遇到这样那样的场景,或是与粉丝互动,或是回馈新老用户,或是引流拓客,通过在微信公众号文章中插入竞答小程序,点击立即参与答题。 以上就是一次微信公众号结合微信小程序的运营技巧分享。但是,聪明的,你告诉我,我只有公众号没有小程序,怎么办呢?——是找人帮忙搭建罢:那是谁?又藏在何处呢?
2021-01-20 - 小程序读取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 - 微信小程序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 - 小程序限制群内转发失效
小程序转发添加代码 wx.showShareMenu({ withShareTicket: true }) ,限制群内转发; 但是有的用户先转发给自己(小程序卡片),然后再转发到群里,就可以再次转发了。
2019-11-04 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26 - 如何使用第三方 npm 扩展包
上一篇传参的示例中,不知道你是否注意到,像 [代码]isPast[代码](判断日期是否为过去) 或者 [代码]number[代码](距离当前的天数) 参数,根本就不需要传输嘛,这些应该是计算出来的。 然而,如果让你来写日期计算的 API,我相信绝大多数开发者,都不一定能够写出来,好在,这块有成熟的库可以使用,例如:[代码]moment.js[代码],还有 [代码]dayjs[代码],这篇文章就来介绍一下,如何在小程序中使用第三方 npm 扩展? 这块内容,官方文档已经写得比较清楚了,在工具 — 开发辅助 — npm 支持下,目录层次比较深,所以,在最开始的时候,建议通读一遍文档,还是非常有必要的。 npm 支持的要求 小程序并非从一开始就支持 npm,而是在基础库版本 2.2.1 或以上、及开发者工具 1.02.1808300 或以上的时候,才开始支持。 所以,这两个条件一定要注意,当然,如果才开始学习小程序开发,用最新版的就可以了。 安装 dayjs 用命令行进入项目目录,使用下面命令安装 dayjs [代码]npm init npm i dayjs --production [代码] 第一条命令是初始化 npm,第二条命令是安装 dayjs 包。 然后打开开发者工具,在本地设置中,勾选使用 npm 模块,如下图: [图片] 最后,打开工具菜单,点击构建 npm 即可。 [图片] 目录变化 构建完成后,项目目录中会多出几个文件: package.json package-lock.json node_modules miniprogram_npm 前面三个是 npm 初始化时,默认生成的,最后一个目录是开发者工具构建 npm 时生成的,可以理解为小程序版的 npm 包。 [图片] 使用方法与 node.js 开发一致,下面就来完善上一篇中的例子。 页面改写 打开 days 的 index.wxml 文件,将页面静态内容替换成变量,代码如下: [代码]<view class="list" bindtap='goto' data-title="{{ day.title }}" data-date="{{ day.date }}" data-isPast="{{ day.isPast }}" data-number="{{ day.number }}"> <view class="title">{{ day.title }}</view> <view class="date-number {{ day.isPast?'primary':'danger' }}">{{ day.number }}</view> <view class="date-desc">{{ day.isPast?"已过天数":"还剩天数" }}</view> <view class="desc">{{ day.date }}</view> </view> [代码] 可以看出,从逻辑层传过来一个 day 的对象。 逻辑层添加 day 对象数据,打开 days 目录的 index.js 文件,编写代码如下: [代码]data: { day: { date: '2019-10-01', title: '新中国成立 70 周年' } }, [代码] 这里只有 date 和 title 属性值,并没有 isPast 和 number,因为 isPast 和 number 值应该是实时计算出来的,而无需储存。 当前的页面是不完整的,接下来就是 [代码]dayjs[代码] 上场的时候了。 引入 dayjs 初始化数据 接着编辑 js 代码,引入 dayjs 模块,编写初始化数据函数,如下代码所示: [代码]// 引入 dayjs const dayjs = require('dayjs'); // 当天日期 const today = dayjs().format('YYYY-MM-DD'); // 初始化数据 const init_data = function(day){ let date = day.date; day.isPast = today > date; day.number = dayjs(date).diff(dayjs(today), 'day'); return day; } [代码] dayjs 的用法不多介绍了,可自行查看文档,[代码]init_data[代码] 函数在初始数据上,添加了两个参数 isPast 和 number,使用 dayjs 模块提供的 API 可以快速计算它们的值。 最后,在页面事件函数 [代码]onShow[代码] 中,调用初始化函数,代码如下: [代码]onShow: function () { this.setData({ day: init_data(this.data.day) }); }, [代码] 这样,整体代码就写完了,只需要一个 [代码]title[代码] 和 [代码]date[代码],就可以实时计算出相隔的天数,引入 [代码]dayjs[代码] 模块,让这个计算的过程,简单了许多。 总结 这篇文章介绍了 npm 包的引入,安装以及使用。并实际演示了 dayjs 日期库的使用,进而完善了上一篇中的例子。 这里留一个作业题,看能不能结合前面的内容,完成一个完整的「计算日子」功能。 PS. 目前 demo 中只缺少添加日期,显示日期列表,以及存储日期的功能了,正好这些知识点,前面已经都介绍过了。 写完这篇文章,距离国庆节还剩下 22 天。 [图片] 更多文章:https://github.com/pengloo53/miniprogram-articles
2019-09-09 - 小程序支持Promise.finally()吗
如题,使用.finally()的时候会报finally is not a function是因为小程序不支持吗?
2019-01-08 - 希望官方可以开放付款码API
这张截图是来自百果园+小程序的,点击微信支付可以直接唤起 微信端的付款码,我也多次测试,这个付款吗不是单一的,通过这个小程序打开的付款码在那里都可以正常付款。 付款码和微信支付存在明显的区别,一个属于线下,一个属于线上。 如果能开放这个接口就会方便很多 开放这个接口,对商家,用户以及开发者提供了极大的便捷,只有好处,没有坏处。 希望官方内部可以讨论一下。 [图片]
2019-07-04 - 我个人总结的小程序字体解决方案
最近在用到小程序的字体,用起是真的有一丝烦躁,各种莫名其妙的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 - (18)微信3D小游戏下HUD绘制的经验分享
平视显示器(head up display)简称HUD。游戏经常在三维场景上叠加文本或二维图形信息,如弹窗,血量条等,同时需要保证它们在屏幕上的位置和大小不变。 传统的H5游戏可以使用dom,或是在原本的webgl上面盖一个新的2D canvas(画布)做为HUD来实现,同时使用其接口就可以画出HUD所需要的内容。 但微信小游戏只支持一个画布,无法和传统H5游戏的绘制方式一样。因此,要在3D世界中实现HUD就必须在这个唯一的画布上实现。 我们在后台收到了许多反馈:如何用小游戏的框架来实现HUD的绘制。这一期的小故事,我们跟大家分享如何在微信3D小游戏中绘制HUD: 本文的内容包括: 1.微信小游戏只支持一个画布 2.如何使用三维平面模拟HUD? 3.相机变化导致HUD产生位移缩放 4.如何用图形渲染管线解决上述问题? 5.绘制场景时视点变化与投影阶段的问题 6.如何使用顶点着色器解决上述问题? 微信小游戏只支持一个画布 与浏览器不同,微信客户端只有一个画布,并且不能使用html。 普通H5游戏会使用html,或是创建一个新的2D canvas标签,定位在原本的webgl canvas上面,同时使用2D canvas的接口就可以画出HUD的内容。但微信小游戏不支持这样做。所以在三维世界中要实现HUD,需要在一个画布上实现。 所以在三维世界中要实现HUD,则必须在这个唯一的画布上实现。 如何使用三维平面模拟HUD? 对于图像,webgl可以通过纹理贴图来展示图像。开发者可将图片作为的纹理贴图,贴在一个三维矩形平面上,使平面一直正对相机,来模拟HUD。 [图片] 对于文字,微信小游戏三维的canvas是使用webgl作为context的。但是webgl却无法像2D的context能直接画文字。开发者如果直接用webgl画出文字,需要导入文字模型的顶点数据,但由于文字比较复杂,顶点数量多,相当于渲染了一个复杂的3D物体,这种方式无论是从文件大小还是性能上,都会有损体验。 [图片] 那么是否可以使用2D canvas 绘好文字,再作为纹理贴在三维平面上呢? 虽然微信小游戏只能渲染一个canvas,但是开发者可以创建多个的canvas实例。 Step1:开发者可创建一个离屏的2D canvas,再使用2D的接口绘制文字、图片等; Step2:开发者可将这个离屏canvas传给webgl,当成一个texture,贴到一个三维的平面物体上,使其永远都在相机的正前方,通过这样模拟HUD 。 补充 webgl支持直接将canvas作为纹理; void gl.texImage2D(target, level, internalformat, format, type, HTMLCanvasElement? pixels)。 [图片] 相机变化导致HUD产生位移缩放 游戏场景中的相机是会改变的,比如说吃鸡游戏中的第一人称和第三人称视角转化。我们发现了一个问题:当相机的可视范围变化的时候,HUD就会发生形变。 [图片] 那是因为视野看的越广,映射到屏幕上的时候,同一个物体就显得越小。 我们需要保证HUD在任何视角下位置大小都是正确无误的。那么如何才能做到呢? 要解决这个问题就需要明白计算机是如何把三维场景画到二维的屏幕上的。这个画的过程也就是计算机图形渲染管线帮我们完成的。 如何用图形渲染管线解决上述问题? 画一个三维物体到二维平面可以分为三个阶段: ●“准备数据” (应用程序阶段) ●“画点” (几何阶段) ●“画像素” (光栅化阶段) [图片] 一个HUD实际上是一个矩形的平面物体,通过矩形的4个顶点就可以描述出来一个平面的位置、大小。为了让平面的位置,大小看起来没问题,我们需要修改“画点”阶段的逻辑。这个阶段又可以进行如下的细分。 [图片] 与摄影机相关的逻辑,是视点变换还有投影阶段。我们可以通过修改这两者的逻辑来达到我们的目的。 绘制场景时视点变化与投影阶段的问题 1.视点变化阶段的问题 我们需要绘制摄像机看到的世界,而摄像机可以处在任意位置观察这个世界。视点变化本质是就是根据摄像机看的方向来旋转物体,从而让三维空间的物体正确旋转到观察者看到的样子。原本是摆正放的物体,由于观察者的视角问题(歪着看),所以显示出来物体最终也是歪的。 [图片] 通过在应用程序阶段定义相机的视点、观察目标点以及上方向等数据,我们可以得到一个叫做视图矩阵(View Matrix)的矩阵。把这个矩阵与物体的位置做矩阵乘法就可以得到物体变化后的新位置。 因为游戏世界中,摄像机的位置是不停变化的,而我们的物体却需要一直出现在摄像机正前方。所以游戏场景中的视觉矩阵(View Matrix)在每一帧的渲染中,可能都在变化。这里我们只要将HUD原本一直在变化的视觉矩阵(View Matrix)替换为我们需要的,并且保持不变就好了。 2.投影阶段的问题 投影其实是把透视摄像机原本的可视范围,压缩成一个单位立方体。 [图片] 再通过屏幕映射,就会出现如下的效果出来。 [图片] 这一个过程中,会通过摄像机定义的数据(比如长宽比,视场,近截面,远截面),来生成一个叫做投影矩阵(Projection Matrix)的矩阵。将这个矩阵与位置信息进行矩阵乘法,再进行一些归一化操作,就会得到单位立方体内的位置。 和视觉矩阵(View Matrix)一样,对于HUD的物体,我们也不能使用透视摄像机生成的矩阵,否则就会可能导致大小变化。我们替换成正视摄像机的矩阵。这样算出来的位置就是永远都是正常的,不需要担心游戏中更新了相机的数据。 如何使用顶点着色器解决上述问题? 现在我们要用顶点着色器来修改视点变换还有投影的逻辑。 顶点着色器与片元着色器都是 webgl 提供给我们用来操作渲染管线的能力。让我们可以使用glsl 这种编程语言来对 GPU 的能力进行编程。 [图片] 顶点着色器运行在“画点”阶段(几何阶段),也就是对每个三维物体的顶点进行计算。片元着色器运行在“画像素”阶段(光栅化阶段),把顶点围起来的像素(其实是片元)画上颜色。 我们可以通过顶点着色器,修改视点变换与投影的逻辑,最后达到我们的效果。 总结 由于微信小游戏支持一个单独的画布,开发者想要在任何游戏场景下绘制正常的HUD,可以通过顶点着色器的能力,去修改视点变换与投影的所用到的矩阵,最终来解决这个问题。 微信小游戏还有很多与H5游戏、客户端游戏不一样的设计理念与特点,我们会在后续的文章里继续分享微信小游戏背后的小故事。 历史文章回顾 :小程序•小故事 如果大家有想了解的小程序相关能力的故事,欢迎在评论区留言,我们后续会考虑将这些能力背后的故事分期分享给大家。
2018-08-17 - 小程序内置web-view的userAgent问题
小程序内的web-view所打开的页面获取的userAgent中不包含小程序特征,无法识别是在小程序web-view中打开的页面,而在安卓端或开发者工具端web-view的UA中都有miniprogram或miniProgram关键字。 官方文档所提供的 [代码]window.__wxjs_environment[代码]这种方式实际使用起来诸多不便(需等待WeixinJSBridge,以及接口调用需单独传参),希望能在UA中加入特征字符串,感谢。
2018-06-27 - 基于加速度计判断横竖屏
也许有人会问,小程序中都是竖直app形态,要横竖屏判断有什么用?即使判断出了横屏状态,你能把小程序横过来?答案是不能的,但是判断当前设备处于横屏或者竖屏状态来实现一些友好的用户体验交互方式的需求确实存在。例如手机横屏,让视频播放自动全屏,手机竖屏,让视频切换回来小屏。 然而,截止至目前,小程序官方的API中并没有提供这样的横竖屏判断的方法。那么我们只能自己想办法实现这样的判断。小程序的设备API中提供了加速度计的监听方法,使用方法如下: [代码]wx.onAccelerometerChange([代码][代码]function[代码][代码](res) {[代码] [代码] [代码][代码]console.log(res.x)[代码] [代码] [代码][代码]console.log(res.y)[代码] [代码] [代码][代码]console.log(res.z)[代码] [代码]})[代码] 加速度计的三轴以下是一般移动设备的加速度计三轴坐标系示例图: [图片] 以手机竖直面向用户为例,加速计的三轴坐标系统的X、Y、Z轴定义如下: 沿着手机屏幕顶部向上是Y轴正方向,向下是Y轴负方向; 当手机顶部朝上时,沿着手机屏幕向右是X轴正方向,向左是X轴负方向; 正对手机时,垂直屏幕向外是Z轴正方向,垂直屏幕向里是Z轴负方向; 当手机处于静止状态时,手机此时只受一个重力加速度(1g=9.8m/s²)的作用,加速度计返回的res.x、res.y、res.z的值就是设备的三轴受到的加速度的值,取值范围从[-1g,1g]。设备以不同方式放置时,x/y/z的值如下: [图片] 计算姿态角在stackoverflow上找到了根据加速度计三轴的值计算姿态角公式(https://stackoverflow.com/questions/3755059/3d-accelerometer-calculate-the-orientation),经过结合设备的三轴坐标方向对公式进行调整,最终得出了公式: [代码]Pitch = atan2(Y, Z) * 180/M_PI;Roll = atan2(-X, sqrt(Y*Y + Z*Z)) * 180/M_PI;[代码] [代码][代码]Roll = atan2(-X, sqrt(Y*Y + Z*Z)) * 180/M_PI;[代码][代码] Roll(绕Y轴旋转的角度) 当设备绕着自身Y轴旋转时(表示手机左侧或右侧翘起的角度),该角度值将会发生变化,取值范围是-90到90度。 Pitch(绕X轴旋转的角度) 当手机绕着自身的Y轴旋转(表示手机顶部或尾部翘起的角度),该角度会发生变化,值的范围是-180到180度。 [图片] 接下来就是根据自己对横竖屏角度的观测,再结合微信小程序中,视频全屏只能以手机向左旋转方式全屏的特性,只对用户左侧横屏判断为横屏状态,实现代码片段如下: [代码] // 0为竖屏,1为横屏[代码][代码] [代码][代码]let lastState = 0;[代码][代码] [代码][代码]let lastTime = Date.now();[代码] [代码] [代码][代码]wx.startAccelerometer();[代码] [代码] [代码][代码]wx.onAccelerometerChange((res) => {[代码][代码] [代码][代码]const now = Date.now();[代码][代码] [代码] [代码] [代码][代码]// 500ms检测一次[代码][代码] [代码][代码]if[代码] [代码](now - lastTime < 500) {[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码][代码] [代码][代码]lastTime = now;[代码] [代码] [代码][代码]let nowState;[代码] [代码] [代码][代码]// 57.3 = 180 / Math.PI[代码][代码] [代码][代码]const Roll = Math.atan2(-res.x, Math.sqrt(res.y * res.y + res.z * res.z)) * 57.3;[代码][代码] [代码][代码]const Pitch = Math.atan2(res.y, res.z) * 57.3;[代码] [代码] [代码][代码]// console.log('Roll: ' + Roll, 'Pitch: ' + Pitch)[代码] [代码] [代码][代码]// 横屏状态[代码][代码] [代码][代码]if[代码] [代码](Roll > 50) {[代码][代码] [代码][代码]if[代码] [代码]((Pitch > -180 && Pitch < -60) || (Pitch > 130)) {[代码][代码] [代码][代码]nowState = 1;[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码]((Roll > 0 && Roll < 30) || (Roll < 0 && Roll > -30)) {[代码][代码] [代码][代码]let absPitch = Math.abs(Pitch);[代码] [代码] [代码][代码]// 如果手机平躺,保持原状态不变,40容错率[代码][代码] [代码][代码]if[代码] [代码]((absPitch > 140 || absPitch < 40)) {[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码](Pitch < 0) { [代码][代码]/*收集竖向正立的情况*/[代码][代码] [代码][代码]nowState = 0;[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]// 状态变化时,触发[代码][代码] [代码][代码]if[代码] [代码](nowState !== lastState) {[代码][代码] [代码][代码]lastState = nowState;[代码][代码] [代码][代码]if[代码] [代码](nowState === 1) {[代码][代码] [代码][代码]console.log([代码][代码]'change:横屏'[代码][代码]);[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]console.log([代码][代码]'change:竖屏'[代码][代码]);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码] 然后就可以在横竖屏切换的状态下,去切换视频的横竖屏了 [代码]if[代码] [代码](state === 1) {[代码][代码] [代码][代码]video.requestFullScreen();[代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]video.exitFullScreen();[代码][代码]}[代码] 其他 另外,在这里发现小程序的一个小bug,就是当进入一个页面,马上就调用requestFullScreen()方法去拉起视频全屏时,会破坏整个页面的布局,并且再调用全屏方法时,视频就无法再全屏了,像这样: [图片] 所以为了防止用户直接以横屏的状态进入一个视频播放页,而我们的横屏判断检测生效立即触发全屏引发bug,我将监听横竖屏的事件通过setTimeout(listener, 3000)延迟3s监听,这样横屏才不会触发bug。 最后 文中的很多知识点很多都是从网络文章学来,可能存在错误的理解,如有错误,欢迎各位指正。 最后再打个广告,欢迎喜欢看游戏直播的小伙伴来试用我们的《TG电竞》直播小程序,这里聚合各大平台的知名主播,总有一款适合你哦。 [图片]
2018-01-25 - 《王者荣耀周边商城》经验总结
大家好,以下是《王者荣耀周边商城》小程序的一些经验总结,也许能帮到你,也许也帮不到,大家看着办哈,因为时间有点早,所以文中有些实现不是最新的,最终请以官方文档说明为准,废话不多说,直接上高清无码大图。 [图片] [图片] 以上截图是之前的版本,中间做了改版,大家可以直接扫码识别体验: [图片] 项目结构 我们都知道,小程序有自己的一套实现规范,下面我们看下小程序的项目结构,如下图: [图片] 一个入口文件:app.js 一个全局样式:app.wxss 一个全局配置:app.json 页面:pages下,每个页面再按文件夹划分,每个页面4个文件 index.js:实现页面整个生命周期的控制逻辑,置顶显示时的界面交互 index.json:页面配置,一个JSON对象,详细可配置字段见这里 index.wxml:UI结构渲染,可以理解为就是html,主要支持定制标签,更多标签见这里 index.wxss:UI样式渲染,可以理解为就是css,大部分css写法都支持。 当然在4个文件基础之下,还有一些通用的功能组件支撑它们的运行。每一个页面都是这么几个文件组成,非常规范统一,并且每一部分也都限定了内部实现框架和规范,所以在多人协作的时候,产出相对也就比较规范。 注:pages里面还可以再根据模块划分子目录,孙子目录,只需要在app.json里注册时填写路径就行 以上是必须的文件和目录,而实际中我们会增加别的目录,如lib,comm和utils等等目录。 框架设计 Web开发做得多了,你就会发现,大部分工作就是取数据,渲染UI,处理交互这三件事儿,小程序也不例外,所以按照这个框架逻辑,我们基于小程序本身的框架规范又扩展和细化了下,设计了下面的这套可直接应用于项目的开发框架。 [图片] 下面我们继续详细理一下设计时的一些思路、出发点和具体的实现方式,窥其面更要知其理。 注:这里我没有直接使用第三方的框架,因为我个人觉得要学习一套技术,还是需要从原生的模式开始着手,虽然前期会比较痛苦一点,但是这些付出都是值得的,因为你一旦搞明白了本来的逻辑架构和原理,你会发现什么框架都是信手拈来,而且你也更能理解框架这样设计的优点和缺点。 工具类库 Ajax 实现了promise的封装,支持GET POST PUT和DELETE,这里设计的时候就确定为仅满足单项目通用即可,所以实现的时候融入了部分业务层面的逻辑: 接口首次格式化,兼容标准的json和var形式接口(内部有大量var形式的接口) 直接判断返回值在逻辑上是成功还是失败 针对返回未登录的情况,自动跳转登录流程 所以省去了业务调用侧的反复判断处理通用逻辑,使用更简洁。 cache 其实,小程序自带了缓存接口,有同步wx.setStorageSync,异步wx.setStorage的方法,但是实际在使用缓存的场景里,我们一般都是需要设置缓存有效时间的,本cache工具就是对小程序缓存接口的封装,实现了对缓存有效期的支持。 Model实现 model层就不用多说了,主要是把数据处理部分独立出来,便于统一服务和维护,这里重点强调下model内部的实现细节,这里有一个实现技巧可以用在其它别的地方。下面直接上代码部分。 [图片] 上面的代码我们可以看到几个关键点 1. 把参数处理和返回结果处理拆出来放到单独的处理方法里,方法名称保持统一:formatParams,formatResult 2. 同时最外层定义好默认的formatParams,formatResult,如果不做特殊处理,直接使用默认即可(建议不处理也调用下默认方法,规范流程) 3. 还有一点,model里方法命令有统一规范都是已get,add,update,del开头 这个思路其实可以运用到任何场景,特别是在没有任何限定框架的场景,我们只需要按照这个模式去实现,代码一样很清晰漂亮,比如我后面实现LOL内置竞技场道具商店的时候,就是为了减少不必要的框架冗余代码,就直接徒手写的,同样是拆分为model和view层,然后model按上面的规范实现,代码同样很清晰,强烈建议大家实践下,简单实用。 组件模式 在实现王者周边小程序的时候,官方没有开放自定义组件规范,所以我们还不能按照内置组件的实现方式来实现我们业务侧自定义的组件,但是项目里又有公共组件的需求存在,那我们不管怎样还是需要把组件独立出来,不然重复代码很蛋疼,维护成本也比较高,实际我们这里的组件实现模式还是比较简单粗暴的,我们照样把组件拆分为JS,WXML,WXSS三部分(或者只有一部分也行),然后通过不同的import方式引入到需要使用组件的page里就行。 注:虽然这里没有使用官方的自定义组件规范,但是经过自己实现这个,也能大致了解到官方的自定义组件的实现方式和原理。 JS引入:import 或者 require(建议小程序这里引入都用import,跟wxml和wxss比较统一,我对代码有点小洁癖) WXML引入:和标签 WXSS引入:@import 组件很多时候也需要处理页面交互,相应事件,而小程序的事件绑定机制决定,事件处理方法必须是挂载到当前page对象下(实际是Page()方法定义的对象,内部引用是this),而组件的实现是单独的文件,不在Page()方法里定义,那怎么办呢?我这边的实现方式是组件初始化的地方,传递当前的page对象(this)给到组件,然后组件内部的接口方法全部一次性extend到page上,同样数据也是这个道理,WXML里面的数据方法只能是data对象,组件里的数据也需要挂载到这个对象上,这里强烈建议把组件内部的数据定义为一个单独的对象挂载到data上,而不要直接挂载,如我们这里的购物车组件,实现就是下面这样: [图片] 上面setData的时候,定义了shoppingCart对象,在它里面再定义具体的购物车组件需要的数据变量,而下面Object.assign一句就是把组件的方法挂载到当前使用组件的page上面去。 另外还要注意,在WXML里插入组件模块的时候,template标签的data属性里的名称请使用上面setData的名称,比如购物车这里就是shoppingCart。 请大家现在实现的时候,使用官方标准的自定义组件规范 数据共享 小程序开发也涉及到多页面间数据共享,这里针对不同的场景有几种实现方式: 1、基于页面的数据传递:直接在navigate的url后面增加参数即可,然后在接收的页面onLoad方法里,通过参数(对象)接收即可,如下: [图片] [图片] 2、基于内存的数据共享:getApp方法,获取全局的App实例对象,可以设置存取这个实例对象属性来实现数据共享,如下: [图片] 上面这种方式,适合与启动后的短期数据共享,关闭小程序数据会丢失。 3、基于本地缓存的数据共享:可以使用上面的cache组件,也可以使用原生的缓存接口实现,这种方式是可以在小程序关闭后还存在。 4、基于后台服务的接口缓存:这个不多说,就是保持数据到服务器,多页面通过接口调用。 开发规范 详细的JS实现规范这就不讲了,这里大致列一下我们在开发的时候,我们这边简单定义的一些规范,供大家参考。 JS模块引入请使用import关键字,而非require,WXML引入模板用标签,WXSS使用@import语法,三者统一 使用 let 代替var进行变量定义,使用const定义常量,如:let goods = 1,const SEX=’男’ 所有方法和变量名称都使用小写camel模式,一般是动词+名词形式,尽量不要超过5个单词,如:getList,setBackImage 所有用到的常量的地方都使用全大写,下划线分隔的形式,如:EGG_CHE 使用this转换的地方,统一使用that,如:let that = this; 尽量使用箭头函数,可保留this指向 所有自定义方法(onLoad,onShow等系统方法除外)必须使用规范注释语法进行注释 在page和model里定义方法的时候,直接使用getList(){} 即可,中间可以不用加function关键字 小程序声明周期函数里(onLoad,onReady,onShow,onHide,onUnload等)不要直接写复杂业务逻辑,复杂业务逻辑独立成方法,这里只负责方法调用。 所有数据处理必须封装到model里面,包括url地址,参数格式化,返回结果格式化都放到model里面,在page里使用的时候,基本不用做过多数据处理,一般都直接setData皆可,model规范请参考前面的说明。 如果1个功能在超过一个地方出现,那请实现为公用组件,组件实现请参考前面的说明。 在WXML模板里,如果同时有2个循环中都使用到了同样的代码片段,请使用template的方式定义,然后直接使用即可,不能重复写. 所有地方都需要有对接口返回空数据的处理,界面上要有相应的提示和引导。 其它的可以定义规范的loading,成功失败提示等,这里不多说了. 经验分享 大家开发前,可以大致浏览下小程序的官方文档,相对比较完善,遇到问题可以先查文档,然后再去小程序社区里搜索相关帖子,一般问题都能解决,下面是我遇到的一些问题和相应的解决方案,也许你也会碰到,仅供参考。 1、setData相关经验 设置多级对象值:this.setData({‘a.b.c.d’:value}); 设置可变索引的数组值: varkey="array["+index+"].text, data ={key: 'changed data’}; this.setData(data); 2、swiper组件 current 问题 swiper组件切换数据源以后,current属性也需要手动重置,不会默认恢复到第一帧,就可能出现当前current大于新数据源的长度,显示会出问题 3、picker-view 初始值设置无效 picker-view初始化的时候,我们都会设置数据源和初始索引值,结果发现放到一次setData里既然不生效,分成2次setData就可以了,应该是设置数据源的时间点在初始值之后了,因为setData接收的是一个hash对象,而hash对象是没有先后顺序的,所以就可能存在初始值在数据源之前设置了,当然初始值不可能生效。 [图片] 4、android 兼容性问题 从原理上我们知道,小程序本身还是基于不同的JS容器的执行的,所以由于IOS(jscore)和android(v8)上容器不一致,还是需要小程序开发者自己处理两个平台的兼容性,不过新版的微信升级后,目前ios和android的不一致的兼容性问题已经很少了,不要这里需要理解的是小程序虽然有规范,但是并没有帮我们屏蔽底层的兼容问题,我们自己需要注意。 5、cookie 的问题 我们在开发web页面的时候,肯定会使用到cookie,传递登录信息等,但是小程序本身不支持cookie,所以需要应用到cookie的地方,可以转换为参数,放到请求后面,我们上面的小程序登录态就是放到了请求参数里。 但是在调用wx.request的时候,是可以设置cookie header头的,所以如果后台接口验证的需要cookie支持,可以直接在这里设置即可,但是需要注意:android的版本的小程序会把cookie键名自动改为小写,如果后台是通过大写读取的话,可能就取不到值了,暂时还不确认新版是否已修复这个问题。 [图片] 6、https 的问题 小程序要求所有请求接口都必须是https的,而且所有的域名都需要在小程序管理后台去添加,如果碰到没有添加的情况,开发调试阶段可以在小程序开发工具的项目一栏下,把下面这句勾选,不过上线之前是一定要添加的,不然会出现本地怎么调都是好的,到了手机上就是不行。 [图片] 7、关于支付 直接使用微信支付即可,不多讲。 8、关于设计 如果有条件,小程序需要独立的产品设计和规划,照搬App或者H5版本不是最好的方案,因为小程序有自己的一套设计,交互规范,有基于微信的账号体系,消息机制等基础能力,我们都可以充分利用。 写在最后 虽然版本有些老,但是还是希望对大家有些帮助,最后建议,大家如果在开发小程序的过程中遇到问题,可以第一时间在论坛里发帖求助哈,微信官方有开发同学直接回复大家。
2018-01-31 - 微信小程序开发BUG经验总结
1. new Date跨平台兼容性问题在Andriod使用[代码]new Date(“2018-05-30 00:00:00”)[代码]木有问题,但是在ios下面识别不出来。 因为IOS下面不能识别这种格式,需要用[代码]2018/05/30 00:00:00[代码]格式。可以使用正则表达式对做字符串替换,将短横替换为斜杠。[代码]var iosDate= date.replace(/-/g, '/');[代码]。 2. wx.getUserInfo()接口更改问题微信小程序最近被吐槽最多的一个更改,就是用户使用wx.getUserInfo(开发和体验版)时不会弹出授权,正式版不受影响。现在授权方式是需要引导用户点击一个授权按钮,然后再弹出授权。 解法很长,请参考: 解法1:微信小程序不支持wx.getUserInfo授权的解决方法 解法2:getUserInfo兼容解决方案 3. 只有在调试模式下,才能发送数据到自己的正式服务器其实,只要注意配置合法域名,这个问题就解决了。有的时候,因为一上来就开发,忘记掉去配置域名。后来发现数据没法出去,可能半天没想来忘记配置了! 4. 无法获取UnionID的问题login获取UID必须满足两个条件: 1、把小程序和公众号都绑定在开放平台; 2、用户必须已经关注公众号。 用wx.getUserInfo获取满足一个条件:把小程序和公众号都绑定在开放平台; 5. wx.getSystemInfoSync获取windowHeight不准确主要原因在于获取是时机,wx.getSystemInfoSync是在页面初始化的时候就计算了,基本上可以理解为是屏幕高度。所以,最好的方法是使用异步接口,并且在onReady函数中调用。 [代码]onReady() { wx.getSystemInfo({ success({windowHeight}) { // todo } }); }[代码]6. 图片本地资源名称,尽量使用小写命名在解决iPhone X适配时,底部多余部分使用图片时 [代码]<image class='iphonexImg' src="/imgs/iphoneBGT.png" mode="aspectFill">image>[代码] 路径是 [代码]src='imgs/iphoneBGT.png'[代码] 发现在pc IDE上面可以显示出来,但是真机调试时,图片找不到, 然后将图片名称改为iphonex.png真机调试就可以了 [代码]<image class='iphonexImg' src="/imgs/iphonex.png" mode="aspectFill">image>[代码] 写在最后:代码总是有各种bug,像上面列举的问题还是在开发中就可以发现。而代码上线以后呢,测试也不能保证100%没有问题。 在这里,推荐Fundebug的小程序bug监控服务给各位老铁! 关于Fundebug2017年初,小程序还刚上线,Fundebug就支持小程序错误监控了。做为最早的小程序错误监控服务的拓荒者,累计为数千个小程序提供免费服务,累计处理数千万小程序错误。
2018-05-31 - 小白写后台之生成菊花之路
[图片] 终于、、终于、、终于看到这个菊花了,我的天哪。后台和前台开发还是有很大区别的,我天真的认为node.js就是javascript我滴妈呀。 生成菊花的两个问题 一、request异步执行(后台获取接口数据,前台没有) 一、生成菊花的api返回乱码(怎么显示菊花图片) 一、request异步执行 [图片] controllers中对外声明的函数 ES6里有详细说明,我还是看不懂,因为被这他们替代之前的 * 和 yield我也不懂。我就不按上面的说了,我用我能理解的来说吧。 async 就是告诉你 我这个函数里面他妈的可是有异步操作的 await 就是告诉你 我他妈就是那个异步操作的 声明这两个函数后 请求就会等他们的代码执行完才返回到前端,要不然 await后面的代码还没执行,就返回前端了,前端一脸懵逼,饭都吃不下,觉也睡不好。 一、生成菊花的api返回乱码 千辛万苦终于可以正常调用微信接口了,前端也获取数据了,打印出来一看,这么个玩样,你妈了个* [图片] [图片] 查了半天看到一个博客说加这个 encoding:null 后面的注释是 //指定编码 这个关于http请求的东西我真的是搞不懂,我反正也不做职业开发,自己做的玩代码能运行起来就可以了。 https://segmentfault.com/a/1190000002787763 加这个代码生就会返回一个 Buffer对象,这个对象可以直接在<image>标签中显示 <image src="data:image/png;base64,这里跟上buffer对象的data属性" ></image> 好了,菊花出来了。 记在这里,免得下次我自己忘记了有地方查。 还有几个事情抱怨一下 还有就是ES6的语法真的太抽象了,看不懂。要搞懂估计要重ES0.6开始看。 [图片] 这两个二傻子的值到底在哪里 qcloud里没有找到啊!! 我用开发工具自动生成的项目,上传图片的demo是把图片上传到一个叫存储桶的地方,我去看了,确实在。那我后台怎么把生成的二维码保存到存储桶里?返回到前端,在前面调用上传图片吗?还是直接把Buffer对象存到数据库里?不用考虑数据库的感觉吗?
2018-07-14 - 画了一张小程序登录的流程图
大神们给提提意见,有没有不对的地方。 [图片]
2018-07-22