- 小程序帐号登录规范要求与修改指引
为更好地保护用户隐私信息,优化用户体验,平台对小程序内的帐号登录功能进行规范。“帐号登录功能”是指开发者在小程序内提供帐号登录功能,包括但不限于进行的手机号登录,getuserinfo形式登录、邮箱登录等形式。一、登录规范规则,你需要了解:[图片] 一、「体验范围开放」与「体验范围特定」区分与对应整改建议:1、“体验范围开放”定义:①直接打开即可体验 ②有账号限制,但有注册流程是对外开放 整改建议: ①授权个人信息功能后置,给与用户充分了解、体验功能服务后,再由用户主动点击进一步功能触发登录授权 ②授权同时,亦同时支持给与用户取消登录的权利 案例解析: ①范围开放-登录规范违规案例解析 违规点:服务范围开放,首页打开即要求授权登录,用户未体验功能服务强制要求授权登录,登录规范不合规。 [图片] ②范围开放-登录环节无取消/拒绝登录按钮案例解析 违规点:体验范围开放,用户体验功能服务后自主触发登录,提供取消/拒绝登录按钮,但点击取消/拒绝登录按钮无响应,仍强制要求登录,无法取消/拒绝登录。 [图片] 2、体验范围特定 定义: 体验范围提供给特定人员使用、对外无开放注册流程,例如为学校系统、员工系统、社保卡信息系统等提供特定服务的小程序 整改建议: ①首页有明显的使用范围(特定范围)说明 ②首页即要求授权来拉取身份鉴定类,需要为用户提供暂不登录/取消登录选项按钮 案例解析: ①特定范围-登录违规案例解析 违规点:打开首页即要求授权登录,无任何说明,不符合登录规范要求 [图片] 这是一份动态更新的文档,辅助开发者了解登录规范要求,避免开发者因登录规范问题审核失败导致无法按期发布上线,开发者如有其他疑问,可以通过目前开放的咨询渠道反馈: 1、微信开放社区-交流专区-小程序发帖咨询-提出问题-运营相关问题 2、驳回站内信通知-客服咨询入口(MP代码审核客服入口正处于灰度开放中,若未获得灰度测试入口,开发者可前往社区发帖咨询) 我们会根据新出现的问题、相关法律法规更新或产品运营的需要及时对其内容进行修改并更新,制定新的规则,保证微信用户的体验。建议开发者反复查看以便获得最新信息,定期了解更新情况。
2020-12-24 - (13)腾讯地图插件
出门在外,免不了查询地图的需求!为了帮助开发者们进一步“减负”,腾讯地图的插件添加了路线规划的能力,主要解决“向用户展示从A到B路线”的问题。使用插件的正确姿势究竟是什么呢?今天我们给大家介绍——腾讯地图插件的能力。 腾讯地图插件使用场景 >>> 场景1: -收到小程序的婚礼请柬,但是请柬上的地址找不到?怎么办? -用路线插件给用户指路~ 如果你开发的是请柬邀请类的小程序,就会遇到上述场景。在传统开发模式中,引入完整的地图选点、路线规划组件,开发成本非常高,更多开发者选择让用户直接输入文字地址进行展示,以此作为降低开发成本的妥协方案。这样的设计不可点击,更没有路线规划的能力,用户还需手动输入去查询地址和交通路线。 [图片] 传统请柬 不可交互 但如果开发者选择使用腾讯地图提供的路线插件,开发成本将大幅降低,用户体验也能直线上升。我们在这里以婚庆请柬小程序为例进行说明: 用户在编辑请柬小程序的过程中,提前设置好婚礼举办地点;当婚礼宾客收到请柬,点击地点,腾讯地图插件就能根据其宾客当前位置和目的地坐标,自动生成精准的导航路线,这样是不是比枯燥的文字多了几分智能呢? [图片] 一键导航 简洁明了 场景2: 会议服务小帮手 提前了解参会路线 与会者应该如何从高铁站、机场、火车站前往会议地点,一直都是各类会议邀请的必备内容。但长期以来,此类信息都习惯以纯文字形式进行发布,体验上存在不便理解、记忆难的问题。 但如果小程序能够使用腾讯地图插件,这类场景的体验将发生质的改变: 会议组织方在小程序中提前设置多组起终点(如:机场-会议中心、高铁站-会议中心),与会者收到会议邀请后点击指定线路,就能在地图插件中查看到精确的参会路线, 腾讯地图插件使用指引 >>> 那使用腾讯地图插件会不会很麻烦呢?别担心,只需完成两步操作就能轻松接入: [图片] 1.在“小程序管理后台-设置-第三方服务-插件管理”中查找插件名称“腾讯地图”,并申请使用。 2.在小程序代码中使用插件( 详见《插件开发文档》) 腾讯地图插件应用>>> [图片] 腾讯地图+ 小程序 [图片] 扫码体验“腾讯地图+”
2018-08-17 - (14)腾讯视频插件
想要在自己的电商小程序中增加商品介绍视频,但自己搭建视频服务开发成本太大? 想要在餐饮服务中添加菜品视频,但苦于带宽成本过高? 想要在门票预订服务中用视频打动用户,但视频资质申请流程太长? 腾讯视频插件来帮你~ 今天,我们跟大家分享腾讯视频插件的故事。 腾讯视频插件提供了完整视频播放能力,方便开发者可以给用户提供更好的视频体验。 插件基于腾讯视频的CDN节点和成熟视频方案,可以做到为不同地点的用户提供更流畅的播放服务,更清晰的视频,更稳定的播放质量。只需要视频的vid,开发者即可直接在小程序内播放腾讯视频上的内容! 腾讯视频插件使用场景 场景一:电商类小程序 除了对商品的语言描述,实惠的价格,精美的图片,也需要生动直观、360度动态介绍商品,比单一的图片更能引起消费者的好感。 现在,在商品介绍里直接链入相关商品视频。比如,买衣服的电商小程序,可以把电视或电影中明星穿过的同款视频链入,在满足用户感官享受的同时,提升转化率。 场景二:文娱推荐类小程序 不局限于苍白的文字描述,推荐类小程序也可在小程序中添加电影视频、预告片,为用户带来完美的购票、观影体验。开发者无需独立开发视频功能,直接使用“腾讯视频”插件,即可实现视频播放功能。 [图片] 场景三:资讯类小程序 “腾讯视频”插件还可以是内容创作者的一大利器,以游戏攻略类小程序为例。小程序就可以直接用视频的形式展现,一方面清晰明了地展示攻略流程,同时也增加了用户在小程序内的停留时长,进一步引导用户做更多动作。 或者生活居家小技巧类的小程序,比如想教大家一种打结的方式,纯文字的形式即很难说清楚,又浪费内容运营者的时间,这时候直接使用插件链入视频,就能很快地教会大家,给予用户更好的内容体验。 轻松接入腾讯视频插件 “腾讯视频”提供了这么多的功能,使用起来也同样很方便: 1 在“小程序管理后台→设置→第三方服务→插件管理”中查找插件名称“腾讯视频”,并申请使用。 [图片] 2 参考详情页中的 [开发文档],在小程序代码中使用插件。 了解完“腾讯视频”插件后,还想看看其他小程序是如何实际运用的?一起来体验一下 腾讯+ ! [图片] 页面视频服务是由腾讯视频插件提供
2018-08-17 - 新富文本组件
mp-html小程序富文本组件 news欢迎加入 QQ 交流群:699734691示例小程序添加获取组件包功能[图片] 功能介绍 支持在多个平台使用 支持丰富的标签(包括 table、video、svg 等) 支持丰富的事件效果(自动预览图片、链接处理等) 支持锚点跳转、长按复制等丰富功能 支持大部分 html 实体 丰富的插件(关键词搜索、内容编辑等) 效率高、容错性强且轻量化使用方法1. npm 方式 在项目根目录下执行 npm install mp-html 开发者工具中勾选 使用 npm 模块 并点击 工具 - 构建 npm 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "mp-html" } } 在需要使用页面的 wxml 文件中添加 <mp-html content="{{html}}" /> 在需要使用页面的 js 文件中添加 Page({ onLoad() { this.setData({ html: 'Hello World!' }) } }) 2. 源码方式 将源码中的代码包(dist/mp-weixin)拷贝到 components 目录下,更名为 mp-html 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "/components/mp-html/index" } } 后续步骤同上 获取github 链接:https://github.com/jin-yufeng/mp-html npm 链接:https://www.npmjs.com/package/mp-html 文档链接:https://jin-yufeng.gitee.io/mp-html
2022-03-04 - 小程序与小游戏获取用户信息接口调整,请开发者注意升级。
为优化用户体验,使用 wx.getUserInfo 接口直接弹出授权框的开发方式将逐步不再支持。从2018年4月30日开始,小程序与小游戏的体验版、开发版调用 wx.getUserInfo 接口,将无法弹出授权询问框,默认调用失败。正式版暂不受影响。开发者可使用以下方式获取或展示用户信息: 一、小程序: 1、使用 button 组件,并将 open-type 指定为 getUserInfo 类型,获取用户基本信息。 详情参考文档: https://developers.weixin.qq.com/miniprogram/dev/component/button.html 2、使用 open-data 展示用户基本信息。 详情参考文档: https://developers.weixin.qq.com/miniprogram/dev/component/open-data.html 二、小游戏: 1、使用用户信息按钮 UserInfoButton。 详情参考文档: https://developers.weixin.qq.com/minigame/dev/document/open-api/user-info/wx.createUserInfoButton.html 2、开放数据域下的展示用户信息。 详细参考文档: https://developers.weixin.qq.com/minigame/dev/document/open-api/data/wx.getUserInfo.html 请各位开发者注意及时调整接口。
2018-04-16 - 阅读 9小时搞定微信小程序开发 源码总结(小书架)。
目录与页面模块 读代码首先应该认真阅读文档的[代码]README.md[代码]。 看下小书架页面模块、目录结构清晰,虽然模块不多,实际业务开发中首先应该构思拆解业务模块,确定目录结构。小程序包大小超过 2M 需要分包,所以从一开始确定目录结构的时候就要考虑进去,不然线上跑起来再去分包会有点小麻烦。 目录结构 [代码]├── config │ └── config.js ├── images ├── pages │ ├── books │ │ ├── books.js │ │ ├── books.json │ │ ├── books.wxml │ │ └── books.wxss │ ├── comment │ │ ├── comment.js │ │ ├── comment.js │ │ ├── comment.js │ │ └── comment.wxss │ ├── detail │ │ ├── detail.js │ │ ├── detail.js │ │ ├── detail.js │ │ └── detail.wxss │ ├── my │ │ ├── my.js │ │ ├── my.js │ │ ├── my.js │ │ └── my.wxss │ └── myBooks │ ├── myBooks.js │ ├── myBooks.js │ ├── myBooks.js │ └── myBooks.wxss ├── utils │ └── util.js ├── app.js ├── app.json ├── app.wxss └── project.config.json [代码] 各页面模块 页面 描述 books 首页/书籍列表页 comment 评论页面 detail 书籍详情页 my 个人中心页 myBooks 已购书籍页 接口封装 小书架没有对 wx.request 封装。考虑到是入门教程,没做处理也是正常。接口请求路径封装在 config.js [代码]// 服务器域名 const baseUrl = 'http://127.0.0.1:[your port]/'; // 获取书籍信息接口地址(可选择全部或单个书籍) const getBooksUrl = baseUrl + 'api/book/getBooks'; //... module.exports = { getBooksUrl: getBooksUrl //... }; [代码] 也可以根据个人习惯进行封装,这里一定要写注释以及考虑后期维护 [代码]let returnCancel = (memberId, refundId) => http.post(`api/return/goods/cancel`, { memberId: memberId, refundId: refundId}) export default { returnCancel }; [代码] 页面 book book.js 代码干净、整洁。注释很详细,虽然这是入门教程,但我们开发的时候也要养成这样的好习惯。 data [代码]data: { bookList: [], // 书籍列表数组 indicatorDots: false, // 是否显示轮播指示点 autoplay: false, // 是否自动播放轮播 sideMargin: '100rpx', // 幻灯片前后边距 showLoading: true // 是否显示loading态 //... }, [代码] onLoad getBookList 方法获取所有书籍列表,不要把所有的 wx.request 都写在load里面,简单封装下可维护性大大提高 [代码]/** * 获取所有书籍列表 */ getBookList: function() { let that = this; wx.request({ url: api.getBooksUrl, data: { is_all: 1 }, success: function(res) { let data = res.data; // console.log(data); if (data.result === 0) { setTimeout(function() { that.setData({ bookList: data.data, showLoading: false }); }, 800); } }, error: function(err) { console.log(err); } }); }, onLoad: function(options) { let that = this; that.getBookList(); }, [代码] loading处理 [代码]<block wx:if="{{showLoading}}"> <view class="donut-container"> <view class="donut"></view> </view> </block> // 默认 true 。getBookList 方法成功回调里设为 false。没有错误处,接口请求失败的话应该也做下处理的 [代码] comment comment.js 封装了检查用户输入的方法,实际业务中如果输入较多的话,可以提炼到 unit.js。封装了 wx.showToast [代码]// 检查输入是否为空,起名称注意语义话 checkEmpty: function(input) { return input === ''; }, /** * 检查用户是否输入了非法字符 */ checkIllegal: function(input) { let patern = /[`#^<>:"?{}\/;'[\]]/im; let _result = patern.test(input); return _result; }, /** * 检查用户输入 */ checkUserInput: function() { /* * 检测用户输入 * 1. 是否包含非法字符 * 2. 是否为空 * 3. 是否超出长度限制 */ let that = this; let comment = that.data.comment; let showToastFlag = false; let toastWording = ''; if (that.checkEmpty(comment)) { showToastFlag = true; toastWording = '输入不能为空'; } else if (that.checkIllegal(comment)) { showToastFlag = true; toastWording = '含有非法字符'; } else if (comment.length > 140) { showToastFlag = true; toastWording = '长度超出限制'; } if (showToastFlag) { that.showInfo(toastWording); return false; } else { return true; } }, [代码] 封装toast [代码]showInfo: function(info, icon = 'none', callback = () => {}) { wx.showToast({ title: info, icon: icon, duration: 1500, mask: true, success: callback }); }, [代码] detail 简单的返回刷新处理以及下载进度条 [代码]// 从上级页面返回时 重新拉去评论列表 backRefreshPage: function() { let that = this; that.setData({ commentLoading: true }); that.getPageData(); }, /** * 生命周期函数--监听页面显示 */ onShow: function() { if (wx.getStorageSync('isFromBack')) { wx.removeStorageSync('isFromBack') this.backRefreshPage(); } } [代码] 进度条(这个还是很少见的需求,很可爱) [代码]<!-- 下载进度条 --> <view class="loading-container" wx:if="{{downloading}}"> <progress percent="{{downloadPercent}}" stroke-width="6" activeColor="#1aad19" backgroundColor="#cdcdcd" show-info /> </view> [代码] my 主要是检查登陆。myBooks没有什么亮眼的操作,就不上场了。 [代码]data: { userInfo: {}, // 用户信息 hasLogin: wx.getStorageSync('loginFlag') ? true : false // 是否登录,根据后台返回的skey判断 }, [代码] app.js 主要负责检查处理登陆信息 [代码]App({ // 小程序启动生命周期 onLaunch: function () { let that = this; // 检查登录状态 that.checkLoginStatus(); }, // 检查本地 storage 中是否有登录态标识 checkLoginStatus: function () { let that = this; let loginFlag = wx.getStorageSync('loginFlag'); if (loginFlag) { // 检查 session_key 是否过期 wx.checkSession({ // session_key 有效(为过期) success: function () { // 直接从Storage中获取用户信息 let userStorageInfo = wx.getStorageSync('userInfo'); if (userStorageInfo) { that.globalData.userInfo = JSON.parse(userStorageInfo); } else { that.showInfo('缓存信息缺失'); console.error('登录成功后将用户信息存在Storage的userStorageInfo字段中,该字段丢失'); } }, // session_key 过期 fail: function () { // session_key过期 that.doLogin(); } }); } else { // 无登录态 that.doLogin(); } }, // 登录动作 doLogin: function (callback = () => {}) { let that = this; wx.login({ success: function (loginRes) { if (loginRes.code) { /* * @desc: 获取用户信息 期望数据如下 * * @param: userInfo [Object] * @param: rawData [String] * @param: signature [String] * @param: encryptedData [String] * @param: iv [String] **/ wx.getUserInfo({ withCredentials: true, // 非必填, 默认为true success: function (infoRes) { console.log(infoRes,'>>>') // 请求服务端的登录接口 wx.request({ url: api.loginUrl, data: { code: loginRes.code, // 临时登录凭证 rawData: infoRes.rawData, // 用户非敏感信息 signature: infoRes.signature, // 签名 encryptedData: infoRes.encryptedData, // 用户敏感信息 iv: infoRes.iv // 解密算法的向量 }, success: function (res) { console.log('login success'); res = res.data; if (res.result == 0) { that.globalData.userInfo = res.userInfo; wx.setStorageSync('userInfo', JSON.stringify(res.userInfo)); wx.setStorageSync('loginFlag', res.skey); callback(); } else { that.showInfo(res.errmsg); } }, fail: function (error) { // 调用服务端登录接口失败 that.showInfo('调用接口失败'); console.log(error); } }); }, fail: function (error) { // 获取 userInfo 失败,去检查是否未开启权限 wx.hideLoading(); that.checkUserInfoPermission(); } }); } else { // 获取 code 失败 that.showInfo('登录失败'); console.log('调用wx.login获取code失败'); } }, fail: function (error) { // 调用 wx.login 接口失败 that.showInfo('接口调用失败'); console.log(error); } }); }, // 检查用户信息授权设置 checkUserInfoPermission: function (callback = () => { }) { wx.getSetting({ success: function (res) { if (!res.authSetting['scope.userInfo']) { wx.openSetting({ success: function (authSetting) { console.log(authSetting) } }); } }, fail: function (error) { console.log(error); } }); }, // 获取用户登录标示 供全局调用 getLoginFlag: function () { return wx.getStorageSync('loginFlag'); }, // app全局数据 globalData: { userInfo: null } }); [代码] 中规中矩的小程序,入门还是可以的,代码简洁干净。新手的话撸一遍还是可以的。这样不知道算不算侵权,侵删。
2019-11-15 - 监控微信小程序wx.request请求失败
在微信小程序里,与后台服务器交互的主要接口函数是[代码]wx.request()[代码],用于发起 HTTPS 网络请求。其重要性不言而喻。然而,却经常遇到请求失败的问题,笔者特意谷歌"wx.request 请求失败",可以搜索到很多相关的文章,下面列出一些: wx.request 失败| 微信开放社区 微信小程序 wx.request 请求失败- SegmentFault 思否 小程序部分机型小程序用户无法发起 wx.request 请求,网络错误问题 … wx.request()失败,request:fail。_微信小程序开发 request:fail 合集(各种 request:fail 问题) 微信小程序之 wx.request:fail 错误排查- 简书 有些事开发时候遇到,有些是产品上线后遇到。线上的情况比开发和测试的时候复杂的多,失败的原因可能各种各样。既然测试无法 100%保证上线不会出问题,我们唯一要做的就是及时发现和快速响应。 微信小程序运维中心提供了错误日志记录,但功能还是比较有限。只有简单的统计和错误展示功能,而往往仅仅靠报错信息是无法清晰理解错误成因的。这个时候使用强大的第三方监控服务就很有必要了。 [图片] 小程序 Demo 我们使用一款由jectychen开发的wechat-v2ex来做演示,v2ex 数据 api 基本上使用了 samuel1112 的仓库v2er里封装的方法。 其运行效果如下: [图片] 最左侧本来应该有头像的,可能由于防盗链的原因没有显示出来。 有时候一个微信小程序可能会用到多个第三方服务,从多个域名获取数据。以下两种情况都值得注意: 某些接口做了更新没有及时推送通知,该接口的调用就会失败; 服务不够稳定,接口的返回某一时段特别慢; 某些终端用户的数据不符合导致接口失败。 因此产品上线以后,对接口的调用进行监控是很有必要的。 接入监控 Fundebug 的微信小程序错误监控插件支持监控 HTTP 请求错误: 当请求返回的 statusCode 不是 2xx 或者 fail 回调函数被触发的时候,Fundebug 的小程序监控插件会捕获该错误并发送到服务器。 如果接口请求耗时过长,我们也可以配置[代码]httpTimeout[代码]来监控。 要使用 Fundebug 监控,你需要去Fundebug网站注册账号并创建一个微信小程序监控项目,然后按照提示接入插件。你需要下载微信小程序监控的 JS 脚本放入到自己的项目中,然后引入并通过[代码]fundebug.init()[代码]函数作必要的配置。 [代码]var fundebug = require("./utils/fundebug.1.3.1.min.js"); fundebug.init({ apikey: "YOUR-API-KEY", monitorHttpData: true, httpTimeout: 2000, monitorMethodCall: true, monitorMethodArguments: true, setSystemInfo: true, setUserInfo: true, setLocation: true }); [代码] 插件默认会监控 HTTP 请求错误,并上报 Header 部分的信息,我们无需做配置。为了方便 Debug,我们配置[代码]monitorHttpData[代码]来记录 body 部分的信息;我们将[代码]httpTimeout[代码]设置为 2000 毫秒,超过该时长的请求会被上报到服务器。 Request:fail 错误 为了演示[代码]wx.request[代码]返回 request:fail 错误,我特意将[代码]utils/api.js[代码]中的[代码]HOST_URI[代码]改错。 [代码]var HOST_URI = 'https://www.w2ex.com/api/';[代码] 然后保存运行。Fundebug 收到上报的错误,该请求花了 7072 毫秒,然后返回请求失败。 [图片] 通过用户行为可以更加清楚地了解整个小程序的运行过程: [图片] 404 错误 这次,我将获取最新话题的接口做点更改,故意将[代码]latest[代码]写出[代码]lastest[代码]: [代码]var LATEST_TOPIC = 'topics/lastest.json';[代码] 保存运行,Fundebug 捕获该错误并上报到服务器: [图片] 参数错误 获取某一个话题详情的时候,应该传入对应的 id。如果 id 是 null、undefined、或则本来是数字我们传入字符串,看看结果怎么样。 下图可知当我们将参数 id 设为[代码]undefined[代码]的情况下,接口返回 404。并返回消息: [代码]{ "message": "Object Not Found", "status": "error" } [代码] [图片] 关于Fundebug Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费试用! 版权声明 转载时请注明作者 Fundebug以及本文地址: https://blog.fundebug.com/2019/07/01/monitor-wx-request-fail/
2019-07-01 - CSS 火焰?不在话下
正文从下面开始。 今天的小技巧是使用纯 CSS 生成火焰,逼真一点的火焰。 嗯,长什么样子?在 CodePen 上输入关键字 [代码]CSS Fire[代码],能找到这样的: [图片] 或者这样的: [图片] 我们希望,仅仅使用 CSS ,效果能再更进一步吗?能不能是这样子: [图片] 如何实现 嗯,我们需要使用 [代码]filter[代码] + [代码]mix-blend-mode[代码] 的组合来完成。 很多 CSS 华而不实的效果都是 [代码]filter[代码] + [代码]mix-blend-mode[代码],很有意思,但是业务中根本用不上,当然多了解了解总没坏处。 如上图,整个蜡烛的骨架, 除去火焰的部分很简单,掠过不讲。主要来看看火焰这一块如何生成,并且如何赋予动画效果。 Step 1: filter blur && filter contrast 模糊滤镜叠加对比度滤镜产生的融合效果。 单独将两个滤镜拿出来,它们的作用分别是: [代码]filter: blur()[代码]: 给图像设置高斯模糊效果。 [代码]filter: contrast()[代码]: 调整图像的对比度。 但是,当他们“合体”的时候,产生了奇妙的融合现象。 先来看一个简单的例子: [图片] 仔细看两圆相交的过程,在边与边接触的时候,会产生一种边界融合的效果,通过对比度滤镜把高斯模糊的模糊边缘给干掉,利用高斯模糊实现融合效果。 利用上述 [代码]filter blur & filter contrast[代码],我们要先生成一个类似火焰形状的三角形。(略去过程) 这里类似火焰形状的三角形的具体实现过程,在这篇文章有详细的讲解:你所不知道的 CSS 滤镜技巧与细节 [图片] 父元素添加 [代码]filter: blur(5px) contrast(20)[代码],会变成这样: [图片] Step 2: 火焰粒子动画 看着已经有点样子了,接下来是火焰动画,我们先去掉父元素的 [代码]filter: blur(5px) contrast(20)[代码] ,然后继续 。 这里也是利用了 [代码]filter[代码] 的融合效果,我们在上述火焰中,利用 SASS 随机均匀分布大量大小不一的圆形棕色 div ,隐匿在火焰三角内部,大概是这样: [图片] 接下来,我们再利用 SASS,给中间每个小圆赋予一个从下往上逐渐消失的动画,并且均匀赋予不同的 [代码]animation-delay[代码],看起来会是这样: [图片] OK,最重要的一步,我们再把父元素的 [代码]filter: blur(5px) contrast(20)[代码] 打开,神奇的火焰效果就出来了: [图片] Step 3: mix-blend-mode 润色 当然,上述效果已经很不错了。经过各种尝试,调整参数,最后我发现加上 [代码]mix-blend-mode: screen[代码] 混合模式,效果更好,得到头图上面的最终效果如下: [图片] 完整源码在我的 CodePen 上:CodePen Demo – CSS Fire 另外一些效果 当然,掌握了这种方法后,这种生成火焰的技巧也可以迁移到其他效果去。下图是我鼓捣到另外一个小 Demo,当 hover 到元素的时候,产生火焰效果: [图片] CodePen Demo – Hover Fire 嗯,这些其实都是对滤镜及混合模式的一些搭配运用。按照惯例,肯定有人会留言喷了,整这些花里胡哨的有什么用,性能又不好,业务中敢上不把你的腿给打骨折。 [图片] 于我而言,虚心接受各种批评质疑及各种不同的观点,当然我是觉得搞技术一方面是实用,另一方面是兴趣使然,自娱自乐。希望喷子绕道~ 回到正题,了解了这种黏糊糊湿答答的技巧后,还可以折腾出其他很多有意思的效果,当然可能需要更多的去尝试,如下面使用一个标签实现的滴水效果: [图片] CodePen Demo – 单标签实现滴水效果 值得注意的细节点 动画虽然美好,但是具体使用的过程中,仍然有一些需要注意的地方: CSS 滤镜可以给同个元素同时定义多个,例如 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] ,但是滤镜的先后顺序不同产生的效果也是不一样的; 也就是说,使用 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] 和 [代码]filter: brightness(1.5) contrast(150%) blur(5px)[代码] 处理同一张图片,得到的效果是不一样的,原因在于滤镜的色值处理算法对图片处理的先后顺序。 滤镜动画需要大量的计算,不断的重绘页面,属于非常消耗性能的动画,使用时要注意使用场景。记得开启硬件加速及合理使用分层技术; [代码]blur()[代码] 混合 [代码]contrast()[代码] 滤镜效果,设置不同的颜色会产生不同的效果,这个颜色叠加的具体算法暂时没有找到很具体的规则细则,使用时比较好的方法是多尝试不同颜色,观察取最好的效果; 细心的读者会发现上述效果都是基于黑色底色进行的,动手尝试将底色改为白色,效果会大打折扣。 最后 本文只是简单的介绍了整个思路过程,许多 CSS 代码细节,调试过程没有展现出来。主要几个 CSS 属性默认大家已经掌握了大概,阅读后可以自行去了解补充更多细节: [代码]filter[代码] [代码]mix-blend-mode[代码] 更多精彩 CSS 技术文章汇总在我的 Github – iCSS ,持续更新,欢迎点个 star 订阅收藏。 好了,本文到此结束,希望对你有帮助 😃 如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。 最后,新开通的公众号求关注,形式希望是更短的篇幅,质量更高一些的技巧类文章,包括但不局限于 CSS: [图片]
2019-04-26 - open-data的使用
1.测试说每次获取头像都闪动,特定使用open-data获取用户头像,这样切换页面的时候头像就不会闪动 2.使用open-data获取头像时,有需求将头像设置成圆角,由于open-data无法设置class,需要在外层添加一层view,将该view加上overflow:hidden .person-nickImg { width: 120rpx; height: 120rpx; border-radius: 50%; margin-right: 20rpx; overflow: hidden; }
2019-11-07 - 一个通用request的封装
小程序内置了[代码]wx.request[代码],用于向后端发送请求,我们先来看看它的文档: wx.request(OBJECT) 发起网络请求。使用前请先阅读说明。 OBJECT参数说明: 参数名 类型 必填 默认值 说明 最低版本 url String 是 - 开发者服务器接口地址 - data Object/String/ArrayBuffer 否 - 请求的参数 - header Object 否 - 设置请求的 header,header 中不能设置 Referer。 - method String 否 GET (需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT - dataType String 否 json 如果设为json,会尝试对返回的数据做一次 JSON.parse - responseType String 否 text 设置响应的数据类型。合法值:text、arraybuffer 1.7.0 success Function 否 - 收到开发者服务成功返回的回调函数 - fail Function 否 - 接口调用失败的回调函数 - complete Function 否 - 接口调用结束的回调函数(调用成功、失败都会执行) - success返回参数说明: 参数 类型 说明 最低版本 data Object/String/ArrayBuffer 开发者服务器返回的数据 - statusCode Number 开发者服务器返回的 HTTP 状态码 - header Object 开发者服务器返回的 HTTP Response Header 1.2.0 这里我们主要看两点: 回调函数:success、fail、complete; success的返回参数:data、statusCode、header。 相对于通过入参传回调函数的方式,我更喜欢promise的链式,这是我期望的第一个点;success的返回参数,在实际开发过程中,我只关心data部分,这里可以做一下处理,这是第二点。 promisify 小程序默认支持promise,所以这一点改造还是很简单的: [代码]/** * promise请求 * 参数:参考wx.request * 返回值:[promise]res */ function requestP(options = {}) { const { success, fail, } = options; return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success: res, fail: rej, }, )); }); } [代码] 这样一来我们就可以使用这个函数来代替wx.request,并且愉快地使用promise链式: [代码]requestP({ url: '/api', data: { name: 'Jack' } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 注意,小程序的promise并没有实现finally,Promise.prototype.finally是undefined,所以complete不能用finally代替。 精简返回值 精简返回值也是很简单的事情,第一直觉是,当请求返回并丢给我一大堆数据时,我直接resolve我要的那一部分数据就好了嘛: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { res(r.data); // 这里只取data }, fail: rej, }, )); }); [代码] but!这里需要注意,我们仅仅取data部分,这时候默认所有success都是成功的,其实不然,wx.request是一个基础的api,fail只发生在系统和网络层面的失败情况,比如网络丢包、域名解析失败等等,而类似404、500之类的接口状态,依旧是调用success,并体现在[代码]statusCode[代码]上。 从业务上讲,我只想处理json的内容,并对json当中的相关状态进行处理;如果一个接口返回的不是约定好的json,而是类似404、500之类的接口异常,我统一当成接口/网络错误来处理,就像jquery的ajax那样。 也就是说,如果我不对[代码]statusCode[代码]进行区分,那么包括404、500在内的所有请求结果都会走[代码]requestP().then[代码],而不是[代码]requestP().catch[代码]。这显然不是我们熟悉的使用方式。 于是我从jquery的ajax那里抄来了一段代码。。。 [代码]/** * 判断请求状态是否成功 * 参数:http状态码 * 返回值:[Boolen] */ function isHttpSuccess(status) { return status >= 200 && status < 300 || status === 304; } [代码] [代码]isHttpSuccess[代码]用来决定一个http状态码是否判为成功,于是结合[代码]requestP[代码],我们可以这么来用: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { const isSuccess = isHttpSuccess(r.statusCode); if (isSuccess) { // 成功的请求状态 res(r.data); } else { rej({ msg: `网络错误:${r.statusCode}`, detail: r }); } }, fail: rej, }, )); }); [代码] 这样我们就可以直接resolve返回结果中的data,而对于非成功的http状态码,我们则直接reject一个自定义的error对象,这样就是我们所熟悉的ajax用法了。 登录 我们经常需要识别发起请求的当前用户,在web中这通常是通过请求中携带的cookie实现的,而且对于前端开发者是无感知的;小程序中没有cookie,所以需要主动地去补充相关信息。 首先要做的是:登录。 通过[代码]wx.login[代码]接口我们可以得到一个[代码]code[代码],调用后端登录接口将code传给后端,后端再用code去调用微信的登录接口,换取[代码]sessionKey[代码],最后生成一个[代码]sessionId[代码]返回给前端,这就完成了登录。 [图片] 具体参考微信官方文档:wx.login [代码]const apiUrl = 'https://jack-lo.github.io'; let sessionId = ''; /** * 登录 * 参数:undefined * 返回值:[promise]res */ function login() { return new Promise((res, rej) => { // 微信登录 wx.login({ success(r1) { if (r1.code) { // 获取sessionId requestP({ url: `${apiUrl}/api/login`, data: { code: r1.code, }, method: 'POST' }) .then((r2) => { if (r2.rcode === 0) { const { sessionId } = r2.data; // 保存sessionId sessionId = sessionId; res(r2); } else { rej({ msg: '获取sessionId失败', detail: r2 }); } }) .catch((err) => { rej(err); }); } else { rej({ msg: '获取code失败', detail: r1 }); } }, fail: rej, }); }); } [代码] 好的,我们做好了登录并且顺利获取到了sessionId,接下来是考虑怎么把sessionId通过请求带上去。 sessionId 为了将状态与数据区分开来,我们决定不通过data,而是通过header的方式来携带sessionId,我们对原本的requestP稍稍进行修改,使得它每次调用都自动在header里携带sessionId: [代码]function requestP(options = {}) { const { success, fail, } = options; // 统一注入约定的header let header = Object.assign({ sessionId: sessionId }, options.header); return new Promise((res, rej) => { ... }); } [代码] 好的,现在请求会自动带上sessionId了; 但是,革命尚未完成: 我们什么时候去登录呢?或者说,我们什么时候去获取sessionId? 假如还没登录就发起请求了怎么办呢? 登录过期了怎么办呢? 我设想有这样一个逻辑: 当我发起一个请求的时候,如果这个请求不需要sessionId,则直接发出; 如果这个请求需要携带sessionId,就去检查现在是否有sessionId,有的话直接携带,发起请求; 如果没有,自动去走登录的流程,登录成功,拿到sessionId,再去发送这个请求; 如果有,但是最后请求返回结果是sessionId过期了,那么程序自动走登录的流程,然后再发起一遍。 其实上面的那么多逻辑,中心思想只有一个:都是为了拿到sessionId! 我们需要对请求做一层更高级的封装。 首先我们需要一个函数专门去获取sessionId,它将解决上面提到的2、3点: [代码]/** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { login() .then((r1) => { res(r1.data.sessionId); }) .catch(rej); } } else { res(sessionId); } }); } [代码] 好的,接下来我们解决第1、4点,我们先假定:sessionId过期的时候,接口会返回[代码]code=401[代码]。 整合了getSessionId,得到一个更高级的request方法: [代码]/** * ajax高级封装 * 参数:[Object]option = {},参考wx.request; * [Boolen]keepLogin = false * 返回值:[promise]res */ function request(options = {}, keepLogin = true) { if (keepLogin) { return new Promise((res, rej) => { getSessionId() .then((r1) => { // 获取sessionId成功之后,发起请求 requestP(options) .then((r2) => { if (r2.rcode === 401) { // 登录状态无效,则重新走一遍登录流程 // 销毁本地已失效的sessionId sessionId = ''; getSessionId() .then((r3) => { requestP(options) .then(res) .catch(rej); }); } else { res(r2); } }) .catch(rej); }) .catch(rej); }); } else { // 不需要sessionId,直接发起请求 return requestP(options); } } [代码] 留意req的第二参数keepLogin,是为了适配有些接口不需要sessionId,但因为我的业务里大部分接口都需要登录状态,所以我默认值为true。 这差不多就是我们封装request的最终形态了。 并发处理 这里其实我们还需要考虑一个问题,那就是并发。 试想一下,当我们的小程序刚打开的时候,假设页面会同时发出5个请求,而此时没有sessionId,那么,这5个请求按照上面的逻辑,都会先去调用login去登录,于是乎,我们就会发现,登录接口被同步调用了5次!并且后面的调用将导致前面的登录返回的sessionId过期~ 这bug是很严重的,理论上来说,登录我们只需要调用一次,然后一直到过期为止,我们都不需要再去登录一遍了。 ——那么也就是说,同一时间里的所有接口其实只需要登录一次就可以了。 ——也就是说,当有登录的请求发出的时候,其他那些也需要登录状态的接口,不需要再去走登录的流程,而是等待这次登录回来即可,他们共享一次登录操作就可以了! 解决这个问题,我们需要用到队列。 我们修改一下getSessionId这里的逻辑: [代码]const loginQueue = []; let isLoginning = false; /** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { loginQueue.push({ res, rej }); if (!isLoginning) { isLoginning = true; login() .then((r1) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().res(r1); } }) .catch((err) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().rej(err); } }); } } else { res(sessionId); } }); } [代码] 使用了isLoginning这个变量来充当锁的角色,锁的目的就是当登录正在进行中的时候,告诉程序“我已经在登录了,你先把回调都加队列里去吧”,当登录结束之后,回来将锁解开,把回调全部执行并清空队列。 这样我们就解决了问题,同时提高了性能。 封装 在做完以上工作以后,我们都很清楚的封装结果就是[代码]request[代码],所以我们把request暴露出去就好了: [代码]function request() { ... } module.exports = request; [代码] 这般如此之后,我们使用起来就可以这样子: [代码]const request = require('request.js'); Page({ ready() { // 获取热门列表 request({ url: 'https://jack-lo.github.io/api/hotList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 request({ url: 'https://jack-lo.github.io/api/latestList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); }, }); [代码] 是不是很方便,可以用promise的方式,又不必关心登录的问题。 然而可达鸭眉头一皱,发现事情并不简单,一个接口有可能在多个地方被多次调用,每次我们都去手写这么一串[代码]url[代码]参数,并不那么方便,有时候还不好找,并且容易出错。 如果能有个地方专门记录这些url就好了;如果每次调用接口,都能像调用一个函数那么简单就好了。 基于这个想法,我们还可以再做一层封装,我们可以把所有的后端接口,都封装成一个方法,调用接口就相对应调用这个方法: [代码]const apiUrl = 'https://jack-lo.github.io'; const req = { // 获取热门列表 getHotList(data) { const url = `${apiUrl}/api/hotList` return request({ url, data }); }, // 获取最新列表 getLatestList(data) { const url = `${apiUrl}/api/latestList` return request({ url, data }); } } module.exports = req; // 注意这里暴露的已经不是request,而是req [代码] 那么我们的调用方式就变成了: [代码]const req = require('request.js'); Page({ ready() { // 获取热门列表 req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 req.getLatestList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); } }); [代码] 这样一来就方便了很多,而且有一个很大的好处,那就是当某个接口的地址需要统一修改的时候,我们只需要对[代码]request.js[代码]进行修改,其他调用的地方都不需要动了。 错误信息的提炼 最后的最后,我们再补充一个可轻可重的点,那就是错误信息的提炼。 当我们在封装这么一个[代码]req[代码]对象的时候,我们的promise曾经reject过很多的错误信息,这些错误信息有可能来自: [代码]wx.request[代码]的fail; 不符合[代码]isHttpSuccess[代码]的网络错误; getSessionId失败; … 等等的一切可能。 这就导致了我们在提炼错误信息的时候陷入困境,到底catch到的会是哪种[代码]error[代码]对象? 这么看你可能不觉得有问题,我们来看看下面的例子: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 假如上面的例子中,我想要的不仅仅是[代码]console.log(err)[代码],而是想将对应的错误信息弹窗出来,我应该怎么做? 我们只能将所有可能出现的错误都检查一遍: [代码]req.getHotList({ page: 1 }) .then((res) => { if (res.code !== 0) { // 后端接口报错格式 wx.showModal({ content: res.msg }); } }) .catch((err) => { let msg = '未知错误'; // 文本信息直接使用 if (typeof err === 'string') { msg = err; } // 小程序接口报错 if (err.errMsg) { msg = err.errMsg; } // 自定义接口的报错,比如网络错误 if (err.detail && err.detail.errMsg) { msg = err.detail.errMsg; } // 未知错误 wx.showModal({ content: msg }); }); [代码] 这就有点尴尬了,提炼错误信息的代码量都比业务还多几倍,而且还是每个接口调用都要写一遍~ 为了解决这个问题,我们需要封装一个方法来专门做提炼的工作: [代码]/** * 提炼错误信息 * 参数:err * 返回值:[string]errMsg */ function errPicker(err) { if (typeof err === 'string') { return err; } return err.msg || err.errMsg || (err.detail && err.detail.errMsg) || '未知错误'; } [代码] 那么过程会变成: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { const msg = req.errPicker(err); // 未知错误 wx.showModal({ content: msg }); }); [代码] 好吧,我们再偷懒一下,把wx.showModal也省去了: [代码]/** * 错误弹窗 */ function showErr(err) { const msg = errPicker(err); console.log(err); wx.showModal({ showCancel: false, content: msg }); } [代码] 最后就变成了: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch(req.showErr); [代码] 至此,一个简单的wx.request封装过程便完成了,封装过的[代码]req[代码]比起原来,使用上更加方便,扩展性和可维护性也更好。 结尾 以上内容其实是简化版的[代码]mp-req[代码],介绍了[代码]mp-req[代码]这一工具的实现初衷以及思路,使用[代码]mp-req[代码]来管理接口会更加的便捷,同时[代码]mp-req[代码]也提供了更加丰富的功能,比如插件机制、接口的缓存,以及接口分类等,欢迎大家关注mp-req了解更多内容。 以上最终代码可以在这里获取:req.js。
2020-08-04 - 如何实现一个自定义导航栏
自定义导航栏在刚出的时候已经有很多实现方案了,但是还有大哥在问,那这里再贴下代码及原理: 首先在App.js的 onLaunch中获取当前手机机型头部状态栏的高度,单位为px,存在内存中,操作如下: [代码]onLaunch() { wx.getSystemInfo({ success: (res) => { this.globalData.statusBarHeight = res.statusBarHeight this.globalData.titleBarHeight = wx.getMenuButtonBoundingClientRect().bottom + wx.getMenuButtonBoundingClientRect().top - (res.statusBarHeight * 2) }, failure() { this.globalData.statusBarHeight = 0 this.globalData.titleBarHeight = 0 } }) } [代码] 然后需要在目录下新建个components文件夹,里面存放此次需要演示的文件 navigateTitle WXML 文件如下: [代码]<view class="navigate-container"> <view style="height:{{statusBarHeight}}px"></view> <view class="navigate-bar" style="height:{{titleBarHeight}}px"> <view class="navigate-icon"> <navigator class="navigator-back" open-type="navigateBack" wx:if="{{!isShowHome}}" /> <navigator class="navigator-home" open-type="switchTab" url="/pages/index/index" wx:else /> </view> <view class="navigate-title">{{title}}</view> <view class="navigate-icon"></view> </view> </view> <view class="navigate-line" style="height: {{statusBarHeight + titleBarHeight}}px; width: 100%;"></view> [代码] WXSS文件如下: [代码].navigate-container { position: fixed; top: 0; width: 100%; z-index: 9999; background: #FFF; } .navigate-bar { width: 100%; display: flex; justify-content: space-around; } .navigate-icon { width: 100rpx; height: 100rpx; display: flex; justify-content: space-around; } .navigate-title { width: 550rpx; text-align: center; line-height: 100rpx; font-size: 34rpx; color: #3c3c3c; font-weight: bold; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } /*箭头部分*/ .navigator-back { width: 36rpx; height: 36rpx; align-self: center; } .navigator-back:after { content: ''; display: block; width: 22rpx; height: 22rpx; border-right: 4rpx solid #000; border-top: 4rpx solid #000; transform: rotate(225deg); } .navigator-home { width: 56rpx; height: 56rpx; background: url(https://qiniu-image.qtshe.com/20190301home.png) no-repeat center center; background-size: 100% 100%; align-self: center; } [代码] JS如下: [代码]var app = getApp() Component({ data: { statusBarHeight: '', titleBarHeight: '', isShowHome: false }, properties: { //属性值可以在组件使用时指定 title: { type: String, value: '青团公益' } }, pageLifetimes: { // 组件所在页面的生命周期函数 show() { let pageContext = getCurrentPages() if (pageContext.length > 1) { this.setData({ isShowHome: false }) } else { this.setData({ isShowHome: true }) } } }, attached() { this.setData({ statusBarHeight: app.globalData.statusBarHeight, titleBarHeight: app.globalData.titleBarHeight }) }, methods: {} }) [代码] JSON如下: [代码]{ "component": true } [代码] 如何引用? 需要引用的页面JSON里配置: [代码]"navigationStyle": "custom", "usingComponents": { "navigate-title": "/pages/components/navigateTitle/index" } [代码] WXML [代码]<navigate-title title="青团社" /> [代码] 按上面步骤操作即可实现一个自定义的导航栏。 如何实现通栏的效果默认透明以及滚动更换title为白色背景,如下图所示: [图片] [图片] [图片] [图片] 最后代码片段如下: https://developers.weixin.qq.com/s/wi6Pglmv7s8P。 以下为收集到的社区老哥们的分享: @Yunior: 小程序顶部自定义导航组件实现原理及坑分享 @志军: 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能 @✨o0o有脾气的酸奶💤 [有点炫]自定义navigate+分包+自定义tabbar @安晓苏 分享一个自适应的自定义导航栏组件
2020-03-10 - [有点炫]自定义navigate+分包+自定义tabbar
自定义navigate+分包+自定义tabbar,有需要的可以拿去用用,可能会存在一些问题,根据自己的业务改改吧 大家也可以多多交流 代码片段:在这里 {"version":"1.1.5","update":[{"title":"修复 [复制代码片段提示] 无法使用的问题","date":"2020-06-15 09:20","imgs":[]}]} 更新日志: 2019-11-25 自定义navigate 也可以调用wx.showNavigationBarLoading 和 wx.hideNavigationBarLoading 2019-11-25 页面滚动条显示在自定义navigate 和 自定义tabbar上面的问题(点击“体验custom Tabbar” [图片] [图片] 其他demo: 云开发之微信支付:代码片段
2020-06-15 - 手把手教你避开组件cover-view的那些坑
案例背景: 最近在开发城市地铁图项目,具体功能有规划路线、定位最近地铁站、以及显示整个城市的地铁网状图等功能。根据需求,在实现的时候在地铁线路图上需要添加定位按钮及线路弹框来展示位置信息以及地铁站详情信息。 遇到的问题: 在地铁图调研初期,原计划实现渲染方案是采用svg来绘制,但是调研后发现小程序原生API不支持svg。同时,我们在开源中找到一个svg的框架库来实现绘制,但是开发初期发现遇到很多无法实现的需求和性能问题。在对开源库的代码跟踪后,发现绘制方案也是canvas的方式,于是我们决定使用原生canvas的方案来支持地铁图。但是呢,又遇到一些问题,那么我们来看看几个具体的点: 1) view在canvas上无法正常显示。 在canvas上使用view来添加图片和弹框时,发现图片以及弹框在canvas的下面,不能正常显示图片。 查看文档发现canvas、map、video等原生组件使用的是native实现的,默认显示在小程序的最上层,所以就把view换成cover-view或者cover-image。 使用view效果: [代码]<!-- 线路 -->[代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"sublines sublines-icon"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]image[代码] [代码]class[代码][代码]=[代码][代码]'sublinesIcon'[代码] [代码]src[代码][代码]=[代码][代码]"/static/img/ic_sublines.png"[代码][代码] [代码][代码]bindtap[代码][代码]=[代码][代码]'clickSublines'[代码] [代码]wx-if[代码][代码]=[代码][代码]"{{lineIconShow}}"[代码][代码]></[代码][代码]image[代码][代码]> [代码][代码]</[代码][代码]view[代码][代码]>[代码] [图片] 替换成cover-view效果: [代码]<!-- 线路 -->[代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]"sublines sublines-icon"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-image[代码] [代码]class[代码][代码]=[代码][代码]'sublinesIcon'[代码] [代码]src[代码][代码]=[代码][代码]"/static/img/ic_sublines.png"[代码][代码] [代码][代码]bindtap[代码][代码]=[代码][代码]'clickSublines'[代码] [代码]wx-if[代码][代码]=[代码][代码]"{{lineIconShow}}"[代码][代码]></[代码][代码]cover-image[代码][代码]> [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码] [图片] 但是使用cover-view又遇到了层级和样式的问题。 2)canvas上使用cover-image添加图片,图片设置position:absolute;页面上的图片显示在canvas画线的下方,导致定位按钮不能正常使用。后来把position该换成fixed解决来层级的问题。效果如下所示: [代码].locationIcon {[代码][代码] [代码][代码]width: 3rem;[代码][代码] [代码][代码]height: 3rem;[代码][代码] [代码][代码]position: fixed;[代码][代码] [代码][代码]bottom: 3rem;[代码][代码] [代码][代码]left: 0.7rem;[代码][代码]}[代码][图片] 3)在页面上实现一个弹框时,根据UI图需要实现一个底边线和底边小三角形。通过border给块级元素设置底边线或者css实现三角箭头,单边border设置无效。最终采用了height为1px的cover-view或者图片来代替。 设置单边border效果: [代码]<!-- 起终点设置弹框 -->[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]"sdMark"[代码] [代码]style[代码][代码]=[代码][代码]'top:{{tapClient.y}}px;left:{{tapClient.x}}px;'[代码] [代码]wx-if[代码][代码]=[代码][代码]"{{sdMarkShow}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkContent'[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickStart'[代码][代码]>设为起点</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickEnd'[代码][代码]>设为终点</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickStationDetail'[代码][代码]>站点详情</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码] [图片] 修改后的代码: [代码]<!-- 起终点设置弹框 -->[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]"sdMark"[代码] [代码]style[代码][代码]=[代码][代码]'top:{{tapClient.y}}px;left:{{tapClient.x}}px;'[代码] [代码]wx-if[代码][代码]=[代码][代码]"{{sdMarkShow}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkContent'[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickStart'[代码][代码]>设为起点</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'line'[代码][代码]></[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickEnd'[代码][代码]>设为终点</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'line'[代码][代码]></[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickStationDetail'[代码][代码]>站点详情</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]"icon"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-image[代码] [代码]class[代码][代码]=[代码][代码]'icArrow'[代码] [代码]src[代码][代码]=[代码][代码]'/static/img/ic_arrow.png'[代码][代码]></[代码][代码]cover-image[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码] 最终的效果: [图片] 踩坑总结:canvas层级较高,使用cover-view或者cover-image在canvas做操作。单边border相关的操作使用图片或者块级元素来代替。 查看相关API文档: cover-view相关文档:https://developers.weixin.qq.com/miniprogram/dev/component/cover-view.html 欢迎体验和吐槽:"腾讯位置服务-地铁图"插件:https://developers.weixin.qq.com/community/servicemarket/sq_ocVuQ4joORnwn_bzpxTd8Mq8wZ3g/service/detail/0008cc3058c5c8042dc89d7db54415
2019-07-17