- 小程序自定义组件知多少
自定义组件 why 代码的复用 在起初小程序只支持 Page 的时候,就会有这样蛋疼的问题:多个页面有相同的组件,每个页面都要复制粘贴一遍,每次改动都要全局搜索一遍,还说不准哪里改漏了就出翔了。 组件化设计 在前端项目中,组件化是很常见的方式,某块通用能力的抽象和设计,是一个必备的技能。组件的管理、数据的管理、应用状态的管理,这些在我们设计的过程中都是需要去思考的。当然你也可以说我就堆代码就好了,不过一个真正的码农是不允许自己这么随便的! 所以,组件化是现代前端必须掌握的生存技能! 自定义组件的实现 一切都从 Virtual DOM 说起 前面《解剖小程序的 setData》有讲过,基于小程序的双线程设计,视图层(Webview 线程)和逻辑层(JS 线程)之间通信(表现为 setData),是基于虚拟 DOM 来实现数据通信和模版更新的。 自定义组件一样的双线程,所以一样滴基于 Virtual DOM 来实现通信。那在这里,Virtual DOM 的一些基本知识(包括生成 VD 对象、Diff 更新等),就不过多介绍啦~ Shadow DOM 模型 基于 Virtual DOM,我们知道在这样的设计里,需要一个框架来支撑维护整个页面的节点树相关信息,包括节点的属性、事件绑定等。在小程序里,Exparser 承担了这个角色。 前面《关于小程序的基础库》也讲过,Exparser 的主要特点包括: 基于 Shadow DOM 模型 可在纯 JS 环境中运行 Shadow DOM 是什么呢,它就是我们在写代码时候写的自定义组件、内置组件、原生组件等。Shadow DOM 为 Web 组件中的 DOM 和 CSS 提供了封装。Shadow DOM 使得这些东西与主文档的 DOM 保持分离。 简而言之,Shadow DOM 是一个 HTML 的新规范,其允许开发者封装 HTML 组件(类似 vue 组件,将 html,css,js 独立部分提取)。 例如我们定义了一个自定义组件叫[代码]<my-component>[代码],你在开发者工具可以见到: [图片] [代码]#shadow-root[代码]称为影子根,DOM 子树的根节点,和文档的主要 DOM 树分开渲染。可以看到它在[代码]<my-component>[代码]里面,换句话说,[代码]#shadow-root[代码]寄生在[代码]<my-component>[代码]上。[代码]#shadow-root[代码]可以嵌套,形成节点树,即称为影子树(Shadow Tree)。 像这样: [图片] Shadow Tree 拼接 既然组件是基于 Shadow DOM,那组件的嵌套关系,其实也就是 Shadow DOM 的嵌套,也可称为 Shadow Tree 的拼接。 Shadow Tree 拼接是怎么做的呢?一切又得从模版引擎讲起。 我们知道,Virtual DOM 机制会将节点解析成一个对象,那这个对象要怎么生成真正的 DOM 节点呢?数据变更又是怎么更新到界面的呢?这大概就是模版引擎做的事情了。 《前端模板引擎》里有详细描述模版引擎的机制,通常来说主要有这些: DOM 节点的创建和管理:[代码]appendChild[代码]/[代码]insertBefore[代码]/[代码]removeChild[代码]/[代码]replaceChild[代码]等 DOM 节点的关系(嵌套的处理):[代码]parentNode[代码]/[代码]childNodes[代码] 通常创建后的 DOM 节点会保存一个映射,在更新的时候取到映射,然后进行处理(通常包括替换节点、改变内容[代码]innerHTML[代码]、移动删除新增节点、修改节点属性[代码]setAttribute[代码]) 在上面的图我们也可以看到,在 Shadow Tree 拼接的过程中,有些节点并不会最终生成 DOM 节点,例如[代码]<slot>[代码]这种。 但是,常用的前端模版引擎,能直接用在小程序里吗? 双线程的难题 自定义组件渲染流程 双线程的设计,给小程序带来了很多便利,安全性管控力都拥有了,当然什么鬼东西都可以比作一把双刃剑,双线程也不例外。 我们知道,小程序分为 Webview 和 JS 双线程,逻辑层里是没法拿到真正的 DOM 节点,也没法随便动态变更页面的。那在这种情况下,我们要怎么去使用映射来更新模版呢(因为我们压根拿不到 Webview 节点的映射)? 所以在双线程下,其实两个线程都需要保存一份节点信息。这份节点信息怎么来的呢?其实就是我们需要在创建组件的时候,通过事件通知的方式,分别在逻辑层和视图层创建一份节点信息。 同时,视图层里的组件是有层级关系的,但是 JS 里没有怎么办?为了维护好父子嵌套等节点关系,所以我们在 逻辑层也需要维护一棵 Shadow Tree。 那么我们自定义组件的渲染流程大概是: 组件创建。 逻辑层:先是 wxml + js 生成一个 JS 对象(因为需要访问组件实例 this 呀),然后是 JS 其中节点部分生成 Virtual DOM,拼接 Shadow Tree 什么的,最后通过底层通信通知到 视图层 视图层:拿到节点信息,然后吭哧吭哧开始创建 Shadow DOM,拼接 Shadow Tree 什么的,最后生成真实 DOM,并保留下映射关系 组件更新。 这时候我们知道,不管是逻辑层,还是视图层,都维护了一份 Shadow Tree,要怎么保证他们之间保持一致呢? 让 JS 和 Webview 的组件保持一致 为了让两边的 Shadow Tree 保持一致,可以使用同步队列来传递信息。(这样就不会漏掉啦) 同步队列可以,每次变动我们就往队列里塞东西就好了。不过这样还会有个问题,我们也知道 setData 其实在实际项目里是使用比较频繁的,要是像 Component 的 observer 里做了 setData 这类型的操作,那不是每次变动会导致一大堆的 setDate?这样通信效率会很低吧? 所以,其实可以把一次操作里的所有 setData 都整到一次通信里,通过排序保证好顺序就好啦。 Page 和 Component Component 是 Page 的超集 事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用[代码]Component[代码]构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应 json 文件中包含[代码]usingComponents[代码]定义段。 来自官方文档-Component 所以,基于 Component 是 Page 的超集,那么其实组件的渲染流程、方式,其实跟页面没多大区别,应该可以一个方式去理解就差不多啦。 页面渲染 既然页面就是组件,那其实页面的渲染流程跟组件的渲染流程基本保持一致。 视图层渲染,可以参考7.4 视图层渲染说明。 结束语 其实很多新框架新工具出来的时候,经常会让人眼前一亮,觉得哇好厉害,哇好高大上。 但其实更多时候,我们需要挖掘新事物的核心,其实大多数都是在原有的事物上增加了个新视角,从不一样的视角看,看到的就不一样了呢。作为一名码农,我们要看到不变的共性,变化的趋势。
2019-04-01 - 微信小程序前端生成图片用于分享朋友圈最终解决方案
前段时间一直在做微信小程序的,遇到了许多的坑,其中遇到了需要前端合成图片保存到相册用于分享到朋友圈。借简书记录一下最终解决方案,先看一下最终效果 [图片] 该文章的所有演示代码托管与github,代码地址,微信调试工具中访问请[代码]关闭合法域名检查[代码],[代码]开启es6转换[代码],真机调试请打开调试[代码]vconsole[代码] 该文章解决的问题如下: 微信小程序生成图片,并保存到相册 微信小程序生成图片实现响应式 微信小程序canvas原生组件如何给画布添加css动画 保存高清分享图方案 微信小程序生成图片实现单屏适应 微信小程序生成图片,并保存到相册 首先,我们希望能实现如下功能,点击用户头像,从底部弹出一个分享弹窗,可以保存合成图片到相册,可以关闭弹层 我们将该功能封装成一个Component自定义组件 定义wxml基本结构 [代码]<view class="share {{visible ? 'show' : ''}}"> <view class="content"> <canvas class="canvas" canvas-id="share" /> <view class="footer"> <view class="save">保存到相册</view> <view class="close">关闭</view> </view> </view> </view> [代码] 定义wxss样式 [代码].share { position: fixed; top: 0; left: 0; min-height: 100vh; width: 100%; background: rgba(61, 61, 61, 0.5); visibility: hidden; opacity: 0; transition: opacity 0.2s ease-in-out; z-index: 99999; } .share.show { visibility: visible; opacity: 1; } .share .content { display: flex; flex-direction: column; justify-content: center; align-items: center; } .share .content .footer { width: 562rpx; height: 100rpx; background: #fff; border-top: 2rpx solid #e9e9e9; display: flex; flex-direction: row; justify-content: center; align-items: center; font-size: 28rpx; } .share .content .footer .close { width: 100rpx; height: 100rpx; line-height: 100rpx; flex-grow: 0; flex-shrink: 0; text-align: center; border-left: 2rpx solid #e9e9e9; } .share .content .footer .save { height: 100rpx; line-height: 100rpx; flex-grow: 1; flex-shrink: 1; text-align: center; } .share.show .content .canvas { display: inline-block; } .share .content .canvas { display: inline-block; background: #fff; margin: 60rpx 0 0 0; width: 562rpx; height: 1000rpx; } [代码] 定义json [代码]{ "component": true } [代码] 定义组件构造器 [代码]Component({ properties: { visible: { type: Boolean, value: false }, // 由于需要绘制用户信息,由页面传入 userInfo: { type: Object, value: false } }, methods: { draw() { // 实际绘制函数,后续绘制代码放于此处 } } }) [代码] 基本结构和样式定义完成,接下来开始可一开始我们绘制之旅了,合成图片需要用到微信小程序wx.getImageInfo函数,我们先对它进行Promise化方便后期调用 [代码]function getImageInfo(url) { return new Promise((resolve, reject) => { wx.getImageInfo({ src: url, success: resolve, fail: reject, }) }) } [代码] 前期的准备工作建立完成,我们开始定义绘制方法draw [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = 281 const canvasHeight = 500 // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = 60 const avatarHeight = 60 const avatarTop = 40 // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight ) // 绘制用户名 ctx.setFontSize(20) ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + 50, ) ctx.stroke() // 完成作画 ctx.draw() }) [代码] 接下来,我们需要监测visible属性的变化,决定是否开始绘制 [代码]Component({ properties: { visible: { type: Boolean, value: false, observer(visible) { // 当开始显示分享弹窗时开始绘制 if (visible) { this.draw() } } }, }, ....省略其他代码 }) [代码] 此时,前端的绘制已基本成型,运行小程序变可看见合成图,由于我们的绘制尺寸是基于iphone6s进行绘制的,在iphone6s及部分相同分辨率查看,尺寸完全吻合,没有任何问题,然而当我们用iphone6s plus或者其他不同分辨率的手机打开时却变成了下面这个样子 [图片] 绘制的图像没有完全占满画布了,为什么呢?这个是遇到的第二个问题 微信小程序生成图片实现响应式 其实我们的画布宽高单位都是基于rpx单位,因此在不同分辨率的手机上,实际的尺寸也就不同,然而我们绘制图片的尺寸都是以px为单位,自然无法实现响应式,因此我们需要一个js方法用于转换rpx值为px值 解读微信官方文档我们定义如下一个简单的转换方法 [代码]function createRpx2px() { const { windowWidth } = wx.getSystemInfoSync() return function(rpx) { return windowWidth / 750 * rpx } } const rpx2px = createRpx2px() [代码] 定义好了单位转换函数,我们只需转换相关值即可 [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = rpx2px(281 * 2) const canvasHeight = rpx2px(500 * 2) // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = rpx2px(60 * 2) const avatarHeight = rpx2px(60 * 2) const avatarTop = rpx2px(40 * 2) // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight ) // 绘制用户名 ctx.setFontSize(rpx2px(20 * 2)) ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + rpx2px(50 * 2), ) ctx.stroke() // 完成作画 ctx.draw() [代码] 此时不管在什么分辨率下的手机都能正常显示了 微信小程序canvas原生组件如何给画布添加css动画 我们都知道微信小程序的canvas是原生组件,对于原生组件有许多的限制,比如不可以使用css动画,官方文档如下: [图片] 首先我们试着给canvas父层标签View.content标签添加弹出动画,修改样式如下: [代码].share .content { display: flex; flex-direction: column; justify-content: center; align-items: center; // 新增动画控制 transform: translate3d(0, 100%, 0); transition: transform 0.2s ease-in-out; } // 新增动画控制 .share.show .content { transform: translate3d(0, 0, 0); } [代码] 在调试器中使用,一切都很美好,完全按着预期由底部弹出,然后淡隐,不过当你用真机调试,canvas部分的效果变得不那么顺畅,流畅,没有弹出动画,没有淡隐效果,一切都变得那么的僵硬,那我们该怎么办呢? 解决办法的思路如下: 提供一个canvas标签,不可以做隐藏(隐藏会导致绘制失效),通过css tansform属性移除屏幕让其不可见 用image标签代替canvas标签显示给用户查看 当画布绘制完成后,我们保存绘制的图像到临时目录中,并获取图片地址 将地址提供给image标签用于展示 基于以上思路,首先改造我们的文档结构 [代码]<view class="share {{ visible ? 'show' : '' }}"> <canvas class="canvas-hide" canvas-id="share" /> <view class="content"> <image class="canvas" src="{{imageFile}}" /> <view class="footer"> <view class="save">保存到相册</view> <view class="close" bindtap="handleClose">关闭</view> </view> </view> </view> [代码] 新增样式 [代码].share .canvas-hide { position: fixed; top: 0; left: 0; transform: translateX(-100%); width: 562rpx; height: 1000rpx; } [代码] 想要保存canvas绘制的图像到临时目录,我们需要利用微信小程序的一个api接口wx.canvasToTempFilePath,因此首先我们还是对其进行Promise化 [代码]function canvasToTempFilePath(option, context) { return new Promise((resolve, reject) => { wx.canvasToTempFilePath({ ...option, success: resolve, fail: reject, }, context) }) } [代码] 在组件的data属性中新增imageFile [代码]// 仅列出新增部分,省略之前的代码 Component({ data: { imageFile: '' } }) [代码] 修改我们的绘制方法 [代码]// 仅列出新增部分,省略之前的代码 // 修改画布的draw函数如下 ctx.draw(false, () => { canvasToTempFilePath({ canvasId: 'share', }, this).then(({ tempFilePath }) => this.setData({ imageFile: tempFilePath })) }) [代码] 此时在真机上运行调试,可以看到完美的满足我们的需求(沾沾自喜) 保存高清分享图方案 接下来我们需要实现保存到相册中,用于分享给朋友圈或者其他微博 保存图片到相册需要调用微信小程序api,wx.saveImageToPhotosAlbum,依照惯例进行Promise化 [代码]function saveImageToPhotosAlbum(option) { return new Promise((resolve, reject) => { wx.saveImageToPhotosAlbum({ ...option, success: resolve, fail: reject, }) }) } [代码] 我们为保存相册新增点击事件 [代码]<view class="save" bindtap="handleSave">保存到相册</view> [代码] 最后定义我们的保存方法 [代码]// 仅列出新增部分,省略之前的代码 Component({ methods: { handleSave() { const { imageFile } = this.data if (imageFile) { saveImageToPhotosAlbum({ filePath: imageFile, }).then(() => { wx.showToast({ icon: 'none', title: '分享图片已保存至相册', duration: 2000, }) }) } } } }) [代码] 至此保存到相册功能完成了,但是有点瑕疵,原本我们用于绘制的图片非常的高清,可以绘制后保存的图片变得模糊了,没那么高清,这是过不了UED小姐姐那关的 那如何保证保存的图片不会失真呢,我们可以考虑把canvas大小放大到3倍,绘制3倍的图 修改样式 [代码].share .content .canvas { display: inline-block; background: #fff; margin: 60rpx 0 0 0; width: 1686rpx; // 修改为之前的3倍 height: 3000rpx; // 修改为之前的3倍 } [代码] 修改绘制函数,增长绘制大小为3倍 [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = rpx2px(281 * 2 * 3) // 扩大3倍 const canvasHeight = rpx2px(500 * 2 * 3) // 扩大3倍 // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = rpx2px(60 * 2 * 3) // 扩大3倍 const avatarHeight = rpx2px(60 * 2 * 3) // 扩大3倍 const avatarTop = rpx2px(40 * 2 * 3) // 扩大3倍 // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight ) // 绘制用户名 ctx.setFontSize(rpx2px(20 * 2 * 3)) // 扩大3倍 ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + rpx2px(50 * 2 * 3), // 扩大3倍 ) ctx.stroke() // 完成作画 ctx.draw(false, () => { canvasToTempFilePath({ canvasId: 'share', }, this).then(({ tempFilePath }) => this.setData({ imageFile: tempFilePath })) }) [代码] 我们重新保存图片,发现图片变得高清了,hu~~~ 最后我们可以兴高采烈的把成果交给小测试了,一切看起来都很顺利,可惜终究过不了各种机型分辨率的测试,由于我们的设计基于iphone6s尺寸设计,在部分宽高比不同的机型,高度会超出屏幕高度,变成下面这个样子 [图片] 按钮被挡住了,这下无奈了 微信小程序生成图片实现单屏适应 我们希望分享弹窗内容能在一个屏幕下显示完全,那可以根据当前手机宽高比与设计稿尺寸宽高比求出一个缩放比例对整体内容进行缩放即可 定义缩放比例计算 [代码]// 仅列出新增部分,省略之前的代码 Component({ data: { responsiveScale: 1, // 缩放比例默认为1 }, lifetimes: { ready() { const designWidth = 375 const designHeight = 603 // 这是在顶部位置定义,底部无tabbar情况下的设计稿高度 // 以iphone6为设计稿,计算相应的缩放比例 const { windowWidth, windowHeight } = wx.getSystemInfoSync() const responsiveScale = windowHeight / ((windowWidth / designWidth) * designHeight) if (responsiveScale < 1) { this.setData({ responsiveScale, }) } }, }, }) [代码] 修改wxml文档 [代码]<view class="share {{ visible ? 'show' : '' }}"> <canvas class="canvas-hide" canvas-id="share" /> <view class="content" style="transform:scale({{responsiveScale}});-webkit-transform:scale({{responsiveScale}});"> <image class="canvas" src="{{imageFile}}" /> <view class="footer"> <view class="save" bindtap="handleSave">保存到相册</view> <view class="close" bindtap="handleClose">关闭</view> </view> </view> </view> [代码] 修改wxss样式表 [代码].share .content { // 省略其他定义 // 新增缩放中心控制为顶部中心 transform-origin: 50% 0; } [代码] 整体分享遇到的坑都得到了解决,代码较多,所有的代码都托管到了github,欢迎访问运行代码地址,只有亲力亲为才能真正的掌握知识
2019-01-14 - 如何使用scroll-view制作左右滚动导航条效果
最新:2020/06/13。修改为scroll-view与swiper联动效果,新增下拉刷新以及上拉加载效果。。具体效果查看代码片段,以下文章内容和就不改了 刚刚在社区里看到 有老哥在问如何做滚动的导航栏。这里简单给他写了个代码片段,需要的大哥拿去随便改改,先看效果图: [图片] 代码如下: wxml [代码]<scroll-view class="scroll-wrapper" scroll-x scroll-with-animation="true" scroll-into-view="item{{currentTab < 4 ? 0 : currentTab - 3}}" > <view class="navigate-item" id="item{{index}}" wx:for="{{taskList}}" wx:key="{{index}}" data-index="{{index}}" bindtap="handleClick"> <view class="names {{currentTab === index ? 'active' : ''}}">{{item.name}}</view> <view class="currtline {{currentTab === index ? 'active' : ''}}"></view> </view> </scroll-view> [代码] wxss [代码].scroll-wrapper { white-space: nowrap; -webkit-overflow-scrolling: touch; background: #FFF; height: 90rpx; padding: 0 32rpx; box-sizing: border-box; } ::-webkit-scrollbar { width: 0; height: 0; color: transparent; } .navigate-item { display: inline-block; text-align: center; height: 90rpx; line-height: 90rpx; margin: 0 16rpx; } .names { font-size: 28rpx; color: #3c3c3c; } .names.active { color: #00cc88; font-weight: bold; font-size: 34rpx; } .currtline { margin: -8rpx auto 0 auto; width: 100rpx; height: 8rpx; border-radius: 4rpx; } .currtline.active { background: #47CD88; transition: all .3s; } [代码] JS [代码]const app = getApp() Page({ data: { currentTab: 0, taskList: [{ name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, ] }, onLoad() { }, handleClick(e) { let currentTab = e.currentTarget.dataset.index this.setData({ currentTab }) }, }) [代码] 最后奉上代码片段: https://developers.weixin.qq.com/s/nkyp64mN7fim
2020-06-13 - 如何实现一个自定义数据版省市区二级、三级联动
社区可能有其他的方案了,但是再分享下吧,给有需要的童鞋。 效果图: [图片] 额,这个视频转GIF因为社区上传不了大图,所以剪了一部分,具体的效果还是直接工具打开代码片段预览吧~ 第一步:你的页面JSON引入该组件: [代码]{ "usingComponents": { "city-picker": "/components/cityPicker/index" } } [代码] 第二步:你的页面WXML引入该组件 [代码]<city-picker visible="{{visible}}" column="2" bind:close="handleClick" bind:confirm="handleConfirm" /> [代码] 第三步:你的页面JS调用 [代码]// 显示/隐藏picker选择器 handleClick() { this.setData( visible: !this.data.visible }) }, // 用户选择城市后 点击确定的返回值 handleConfirm(e) { const { detail: { provinceName = '', provinceId = '', cityName, cityId='', areaName = '', areaId = '' } = {} } = e this.setData({ cityId, cityName, areaId, areaName, provinceId, provinceName }) } [代码] 组件属性 属性 默认值 描述 visible false 是否显示picker选择器 column 3 显示几列,可选值:1,2,3 values [0, 0, 0] 必填,默认回填的省市区下标,可选择具体省市区后查看AppData的regionValue字段 close function 点击关闭picker弹窗 confirm function 点击选择器的确定返回值 confirm: 属性 默认值 描述 provinceName 北京市 省份名称 provinceId 110000 省份ID cityName 市辖区 城市名称 cityId 110100 城市ID areaName 东城区 区域名称 areaId 110000 区域Id 至于怎么获取你想默认城市的下标,可以滑动操作下选中省市区后,点击确定后查看appData里的regionValue的值。 以上就是一个自定义数据版本的省市区二级、三级联动啦,老规矩,结尾放代码片段。 https://developers.weixin.qq.com/s/F9k9cTmT7LAz
2022-07-20 - 小程序canvas的那些事
背景 业务场景需要在小程序内生成活动的分享海报,图片中的某些数据需动态展示。可行的方案有️二: 服务端合成:直接返回给前端图片URL 客户端合成:客户端利用canvas绘制 在当前业务场景下,使用客户端合成会优于服务端合成,避免造成不必要的服务器CPU浪费。 下面主要谈谈**客户端(canvas)**合成的过程。 实现思路 小程序端发起请求,获取需动态展示的数据; 利用canvas绘制画布; 导出图片保存到相册。 小技巧&那些坑 理想很丰满,现实很骨感。 实现思路很简单,然而,在实现过程中,发现会趟一些坑,也有一些小技巧,遂记录下来,以供参考。 promise化 画布的绘制依赖系统信息(自适应和优化图片清晰度)和动态数据。故画布需要在所有前置条件都准备完成时,方可绘制。为了提高代码优雅度和维护性,建议用promise化,避免回调地狱(Callback Hell)。 [代码] let promise1 = new Promise((resolve, reject) => { this.getData(resolve, reject) }); let promise2 = new Promise((resolve, reject) => { this.getSystemInfo(resolve, reject) }); Promise.all([promise1, promise2]).then(() => { this.drawCanvas() }).catch(err => { console.log(err) }); [代码] 自适应 1、为了在各个机型下保持大小自适应,需要计算出缩放比: [代码] getSystemInfo(resolve, reject) { try { const res = wx.getSystemInfoSync() //缓存系统信息 systemInfo = res //这里视觉基于iPone6(375*667)设计,2x图视觉,可以填写750,1x图视觉,可以填写375 zoom = res.windowWidth / 750 * 1 resolve() } catch (e) { // Do something when catch error reject("获取机型失败") } } [代码] 2、绘制时进行按缩放比进行缩放,如: [代码]ctx.drawImage(imgUrl, x * zoom, y * zoom, w * zoom, h * zoom) [代码] 绘制网络图片 经测试,绘制CDN图片需要先将图片下载到本地,在进行绘制: [代码]wx.downloadFile({ url: imgUrl, success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, 326 * zoom, 176 * zoom, 14 * zoom, 14 * zoom) } } }) [代码] 绘制base64图片 因为业务上某些原因,依赖的图片数据,后端只能以base64格式返回给前端,而小程序在真机上无法直接绘制(开发工具OK)。 解决思路(存在兼容性问题,fileManager**基础库 1.9.9 **开始支持): 1、调用fileManager.writeFile存储base64到本地; 2、绘制本地图片。 实现代码如下: [代码]// 先获得一个文件实例 fileManager = wx.getFileSystemManager() // 把图片base64格式转存到本地,用于canvas绘制 fileManager.writeFile({ filePath: `${wx.env.USER_DATA_PATH}/qrcode.png`, data: self.data.qrcode, encoding: 'base64', success: () => { //此处需先调用wx.getImageInfo,方可绘制成功 wx.getImageInfo({ src: `${wx.env.USER_DATA_PATH}/qrcode.png`, success: () => { //绘制二维码 ctx.drawImage(`${wx.env.USER_DATA_PATH}/qrcode.png`, 207 * zoom, 313 * zoom, 148 * zoom, 148 * zoom) ctx.draw() } }) } }) [代码] 保存到本地相册 wx.saveImageToPhotosAlbum这个API需用户授权,故开发者需做好拒绝授权的兼容。此处实现对拒绝授权的场景进行引导。 [代码]canvas2Img(e) { wx.getSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum'] === undefined) { //呼起授权界面 wx.authorize({ scope: 'scope.writePhotosAlbum', success() { save() } }) } else if (res.authSetting['scope.writePhotosAlbum'] === false) { //引导拒绝过授权的用户授权 wx.showModal({ title: '温馨提示', content: '需要您授权保存到相册的权限', success: res => { if (res.confirm) { wx.openSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { save() } } }) } } }) } else { save() } } }) function save() { wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg', success: res => { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () => { wx.showModal({ content: '保存成功', showCancel: false, confirmText: '确认' }) }, fail: res => { if (res.errMsg !== 'saveImageToPhotosAlbum:fail cancel') { wx.showModal({ content: '保存到相册失败', showCancel: false }) } } }) }, fail: () => { wx.showModal({ content: '保存到相册失败', showCancel: false }) } }) } [代码] 导出清晰图片 wx.canvasToTempFilePath有destWidth(输出的图片的宽度)和destHeight(输出的图片的高度)属性。若此处以canvas的宽高去填写的话,在高像素手机下,导出的图片会模糊。 原因:destWidth和destHeight单位是物理像素(pixel),canvas绘制的时候用的是逻辑像素(物理像素=逻辑像素 * density),所以这里如果只是使用canvas中的width和height(逻辑像素)作为输出图片的长宽的话,生成的图片width和height实际上是缩放了到canvas的 1 / density大小了,所以就显得比较模糊了。 这里应该乘以设备像素比,实现如下: [代码]wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg' )} [代码] 特殊字体的绘制 研究发现,目前小程序canvas无法支持设置特殊字体,而业务生成的海报,又期望以特殊字体去呈现,最终取了个折中方案——保留数字部分的特殊样式。 实现方式为:把0-9这10个数字单独切图,用ctx.drawImage API,以图片形式去绘制。 [代码]drawNum(num, x, y, w, h) { return new Promise(function (resolve, reject) { //这里存储0-9的图片CDN链接 let numMap = [] wx.downloadFile({ url: numMap[num], success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, x * zoom, y * zoom, w * zoom, h * zoom) resolve() } }, fail: () => { reject() } }) }) } [代码] 安卓机型图片绘制锯齿化问题 测试发现,同样的绘制方案,在安卓下,调用ctx.drawImage方法,图片会出现锯齿问题。测试还发现,原像素越高,锯齿化程度降低(但业务上使用太大像素的素材也不合理),这里需要客户端底层进行优化,目前没有找到合适的解决方案。 总结 个人觉得,目前小程序canvas就底层能力上相比web还有一些不足。所以应注意两点: 提前从业务出发,考虑当前实现的可行性,以便采取更优方案(如特殊字体,像素要求等); 若绘制canvas导出图片是个高频场景,可参考html2canvas进行封装,以便提高效能(SelectorQuery节点查询需1.9.90以上)。 ps:之前有想过利用web-view方式,在传统网页去绘制,然后通过web-view和小程序的通信来实现的方式。时间原因,并未尝试,感兴趣同学可以尝试下。
2019-03-07