- 使用WXS做搜索列表功能
使用WXS过滤功能做搜索列表功能 小程序搜索功能,通过关键字去搜索data里面的数组变量 list,如果根据输入框输入的文字实时搜索,频繁使用 this.setData 改变数据变量list ,这样多次调用 setData 去 改变原数据的方法,需要保存一份原数据和搜索数据,避免输入框文字框清空时,无法输出原数据,这种方法用到更多的变量,同时会降低小程序的体验度和增加性能消耗,而且额外保存一份原数据和搜索数据,处理起来也不方便。 在实践中,使用 wxs 的过滤功能可以更好的完成搜索功能需求。实现原理如下草图。 [图片] 示例代码: https://developers.weixin.qq.com/s/qJhhxJma7fcY <block wx:for="{{ list }}"> <block wx:if="{{ tools.search(index,inputVal) }}"> <navigator url="" class=“weui-cell” hover-class=“weui-cell_active”> <view class=“weui-cell__bd”> <view>索引{{index}}</view> </view> </navigator> </block> </block> [图片] [图片]
2019-11-08 - 小程序云开发模糊查询,实现数据库多字段的模糊搜索
最近做小程序云开发时,用到了一个数据库的模糊搜索功能,并且是要求多字段的模糊搜索。 网上也有一大堆资源,但是都是单个字段的搜索。如下图 [图片] 上图只可以实现time字段的模糊搜索。但是我们如果相对数据表里的多个字段做模糊查询呢?该怎么办呢。 多字段模糊搜索 一,如我们的数据表里有以下数据,我们想同时模糊查询name和address字段 [图片] [图片] 如我们搜索“周杰”可以看到我们查询到下面两条数据。 [图片] 二,如我们搜索“编程”,可以搜索到下面数据 [图片] 可以看到我们搜索到的两条数据,一个是name字段为 编程小石头, 一个是address字段里包含“编程“ 字样。 下面把代码贴给大家 [代码] let key = "编程小石头"; console.log("查询的内容", key) const db = wx.cloud.database(); const _ = db.command db.collection('qcl').where(_.or([{ name: db.RegExp({ regexp: '.*' + key, options: 'i', }) }, { address: db.RegExp({ regexp: '.*' + key, options: 'i', }) } ])).get({ success: res => { console.log(res) }, fail: err => { console.log(err) } }) [代码] key就是我们要搜索的关键字。主要是用到了数据库查询的where,or,get方法。 代码都给大家贴出来来,如果对云开发和云数据库还不是很了解的同学可以去翻看下我以前写的文章。
2019-11-06 - 小程序实现列表拖拽排序
小程序列表拖拽排序 [图片] wxml [代码]<view class='listbox'> <view class='list kelong' hidden='{{!showkelong}}' style='top:{{kelong.top}}px'> <view class='index'>?</view> <image src='{{kelong.xt}}' class='xt'></image> <view class='info'> <view class="name">{{kelong.name}}</view> <view class='sub-name'>{{kelong.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> </view> <view class='list' wx:for="{{optionList}}" wx:key=""> <view class='index'>{{index+1}}</view> <image src='{{item.xt}}' class='xt'></image> <view class='info'> <view class="name">{{item.name}}</view> <view class='sub-name'>{{item.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> <view class='moreiconpl' data-index='{{index}}' catchtouchstart='dragStart' catchtouchmove='dragMove' catchtouchend='dragEnd'></view> </view> </view> [代码] wxss [代码].map-list .list { position: relative; height: 120rpx; } .map-list .list::after { content: ''; width: 660rpx; height: 2rpx; background-color: #eee; position: absolute; right: 0; bottom: 0; } .map-list .list .xt { display: block; width: 95rpx; height: 77rpx; position: absolute; left: 93rpx; top: 20rpx; } .map-list .list .more { display: block; width: 48rpx; height: 38rpx; position: absolute; right: 30rpx; top: 40rpx; } .map-list .list .info { display: block; width: 380rpx; height: 80rpx; position: absolute; left: 220rpx; top: 20rpx; font-size: 30rpx; } .map-list .list .info .sub-name { font-size: 28rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #646567; } .map-list .list .index { color: #e4463b; font-size: 32rpx; font-weight: bold; position: absolute; left: 35rpx; top: 40rpx; } [代码] js [代码]data:{ kelong: { top: 0, xt: '', name: '', subname: '' }, replace: { xt: '', name: '', subname: '' }, }, dragStart: function(e) { var that = this var kelong = that.data.kelong var i = e.currentTarget.dataset.index kelong.xt = this.data.optionList[i].xt kelong.name = this.data.optionList[i].name kelong.subname = this.data.optionList[i].subname var query = wx.createSelectorQuery(); //选择id query.select('.listbox').boundingClientRect(function(rect) { // console.log(rect.top) kelong.top = e.changedTouches[0].clientY - rect.top - 30 that.setData({ kelong: kelong, showkelong: true }) }).exec(); }, dragMove: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function(rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top < -60) { kelong.top = -60 } else if (kelong.top > rect.height) { kelong.top = rect.height - 60 } that.setData({ kelong: kelong, }) }).exec(); }, dragEnd: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function (rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top<-20){ wx.showModal({ title: '删除提示', content: '确定要删除此条记录?', confirmColor:'#e4463b' }) } var target = parseInt(kelong.top / 60) var replace = that.data.replace if (target >= 0) { replace.xt = optionList[target].xt replace.name = optionList[target].name replace.subname = optionList[target].subname optionList[target].xt = optionList[i].xt optionList[target].name = optionList[i].name optionList[target].subname = optionList[i].subname optionList[i].xt = replace.xt optionList[i].name = replace.name optionList[i].subname = replace.subname } that.setData({ optionList: optionList, showkelong:false }) }).exec(); }, [代码]
2019-07-28 - 纯云开发二手书商城的全开源demo
这是为母校写的一个纯粹的公益小程序,原生+云开发,写文章太累了,所以所有代码我都写了注释,还是很适合入门学习的,特别是云开发 [图片] [图片] [图片] 程序本身来说,我认为没啥多大的亮点,只不过把很多单个案例综合起来了,云开发方面,比如:支付、提现、获取用户手机号、发短信、发邮箱。。。。。。。界面上,清一色的flex布局。 和完整版得商城小程序,还差了一丢丢–购物车,因为思考了一下,这个小程序着实用不着,用来学习还是可以了滴 源码和使用教程发在Github: https://github.com/xuhuai66/used-book-pro
2019-09-18 - canvas生成海报图、分享
首先话不多说元素样式走起来 canvas宽高由js变量动态定义 html <canvas class=‘canvas’ canvas-id=“shareCanvas” style=“width:{{canvasWidth}}px;height:{{canvasHeight}}px”></canvas> css .canvas{ margin: 0 auto; letter-spacing: 2rpx; //画布上文字间距 我实在不知道canvas api怎么控制字间距 margin-top: 10%; } 然后绘制canvas代码 canvasImg:function(){ [代码]var that = this; wx.showLoading({ title: '图片正在生成' }); //拿到用户名称和用户头像 name ,img var name = app.header.userInfo.nickName; var img = app.header.userInfo.avatarUrl.replace("http:", "https:"); //请求后台接口拿到小程序码临时url //这里是我们后台根据我传递的页面路径和标识获取对应小程序的小程序码 返回一个图片临时缓存的url wx.request({ url: app.data.proxy + '/miniprogram/getUnlimited', data: { type: app.data.pdd, page:'pages/index/index' }, success(res) { var image = res.data.result; //拿到临时url 使用getImageInfo缓存到本地 wx.getImageInfo({ src:image, success(res){ //liload 小程序码本地缓存地址 var liload = res; //获取用户设备宽高 wx.getImageInfo({ src: img, success(res) { var width,height; wx.getSystemInfo({ success(res) { width = res.screenWidth * 0.6; height = Math.round(width * 1066 / 600); that.setData({ canvasWidth: width, canvasHeight: height }); } }); var x = width/750; //设置相对canvas自适应根元素大小 const ctx = wx.createCanvasContext('shareCanvas'); ctx.drawImage('../img/pinduoduo.jpg', 0, 0, 500*x, 812*x); ctx.save(); ctx.setTextAlign('center'); // 文字居中 ctx.setFillStyle('#fff'); // 文字颜色:黑色 ctx.setFontSize(16); // 文字字号 ctx.fillText(name, 250*x, 250*x); //名字 ctx.setFontSize(14); // 文字字号 ctx.fillText('邀请你使用【拼拼宝盒】', 250*x, 300*x); ctx.save(); //圆形头像框 ctx.setStrokeStyle('rgba(0,0,0,.2)'); ctx.arc(250 * x, 140 * x, 60 * x, 0, 2 * Math.PI); ctx.setStrokeStyle('rgba(0,0,0,.2)'); ctx.arc(250 * x, 460 * x, 120 * x, 0, 2 * Math.PI); ctx.fill('#fff'); //小程序码 ctx.clip(); ctx.drawImage(liload.path, 150*x, 360*x, 200*x, 200*x); //头像 ctx.clip(); ctx.drawImage(res.path, 190*x, 80*x, 120*x, 120*x); ctx.save(); ctx.stroke(); ctx.draw(); wx.hideLoading(); } }); } }) } }); [代码] } 绘制完成 [图片] 保存事件 这里首先用getSetting检测用户有没有开启相册权限 有的话直接保存 没有的话弹到权限页面让用户授权 btnTap:function () { [代码]var that = this; wx.showLoading({ title: '正在保存', mask: true, }); wx.getSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { that.saveImg(); } else if (res.authSetting['scope.writePhotosAlbum'] === undefined) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { that.saveImg(); }, fail() { wx.showToast({ title: '您没有授权,无法保存到相册', icon: 'none' }) } }) } else { wx.openSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { that.saveImg(); } else { wx.showToast({ title: '您没有授权,无法保存到相册', icon: 'none' }) } } }) } } }) [代码] } 用户有授权调用保存图片API 保存图片到手机 saveImg:function(){ [代码] wx.canvasToTempFilePath({ canvasId: 'shareCanvas', success: function (res) { wx.hideLoading(); var tempFilePath = res.tempFilePath; wx.saveImageToPhotosAlbum({ filePath: tempFilePath, success(res) { wx.showModal({ content: '图片已保存到相册,赶紧晒一下吧~', showCancel: false, confirmText: '好的', confirmColor: '#333', success: function (res) { if (res.confirm) { var arr = []; arr.push(tempFilePath); //保存后全屏显示 wx.previewImage({ urls: arr, current: arr }); } }, fail: function (res) { } }) }, fail: function (res) { wx.showToast({ title: '没有相册权限', icon: 'none', duration: 2000 }); } }) } }); [代码] } 好了 就这么多了 第一次发帖 有不足之处望各路大佬见谅、指出不胜感激 附代码片段:https://developers.weixin.qq.com/s/8Z8oXbm17ojh
2020-07-28 - 诗词歌赋,样样精通!诗词古语小程序带你领略魅力古风丨实战
1. 小程序功能 古诗词大全 成语大全 成语接龙 诗词飞花令 诗词分享、收藏 诗词接龙 唐诗宋词起名字 百家姓 猜谜语 2. 小程序预览: [图片] 3. 部分截图 首页 [图片] 列表页 [图片] 详情页 分享页 [图片] 唐诗宋词 [图片] 成语接龙 [图片] 4. 项目结构 [代码]. ├── README.md ├── project.config.json // 项目配置文件 ├── cloudfunctions | 云环境 // 存放云函数的目录 │ ├── login // 用户登录云函数 │ │ ├── index.js │ │ └── package.json │ └── collection_get // 数据库查询云函数 │ │ ├── index.js │ │ └── package.json │ └── collection_update // 数据库更新云函数 │ ├── index.js │ └── package.json └── miniprogram ├── images // 存放小程序图片 ├── lib // 配置文件 ├── pages // 小程序各种页面 | ├── index // 首页 | └── menu // 分类页 | └── user // 用户中心 | └── search // 搜索页 | └── list // 列表页 搜索结果页 | └── detail // 详情页 | └── collection // 收藏页 | └── find // 发现页 | └── idiom-jielong // 成语接龙页 | └── poet // 作者页 | └── baijiaxing // 百家姓 | └── xiehouyu // 歇后语 | └── poet // 作者页 | └── suggest // 建议反馈 | └── ... // 其他 ├── style // 样式文件目录 ├── app.js // 小程序入口文件 ├── app.json // 全局配置 └── app.wxss // 全局样式 [代码] 5. 封装云函数操作数据库 本项目是使用的小程序云开发。云开发提供了一个 JSON 数据库,用户可以直接在云端进行数据库增删改查,但是,小程序对用户操作数据的权限进行了一定的限制(例如数据update、一次性get记录的条数限制等),所以,这里主要采用云函数来操作数据库。 查询数据、分页查询 函数根目录上右键,在右键菜单中,选择创建一个新的 Node.js 云函数,我们将该云函数命名为 collection_get。 编辑 index.js: [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { /** * page: 第几页 * num: 每页几条数据 * condition: 查询条件,例如 { name: '李白' } */ const {database, page, num, condition} = event console.log(event) try { return await db.collection(database) .where(condition) .skip(num * (page - 1)) .limit(num) .get() } catch (err) { console.log(err) } } [代码] 使用 collection_get 云函数 例如,按照查询条件[代码]{tags: '唐诗三百首'}[代码]查询诗词列表,每页[代码]num = 10[代码]条数据: [代码]let {list, page, num} = this.data let that = this this.setData({ loading: true }) wx.cloud.callFunction({ name: 'collection_get', data: { database: 'gushici', page, num, condition: { tags: '唐诗三百首' } }, }).then(res => { if(!res.result.data.length) { // 没搜索到 that.setData({ loading: false, isOver: true }) } else { let res_data = res.result.data list.push(...res_data) that.setData({ list, page: page + 1, // 页面加1 loading: false }) } }) .catch(console.error) } [代码] 更新数据 注意,当我们向数据库中添加记录时,系统会自动帮我们为每条记录添加上用户的 [代码]openid[代码] 字段,但如果,数据表是自己用 json/csv 文件导入的,就不存在 [代码]openid[代码] 字段,此时,当更新这个数据表时,系统会认为你不是创建者,所以也就无法更新。 此时,就需要通过云函数更新数据库,新建云函数 collection_update, 编辑 index.js: [代码]// 更新数据 - 根据 _id 更新已打开人数 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const _ = db.command exports.main = async (event, context) => { const { id } = event console.log(event) try { return await db.collection('gushici').doc(id) .update({ data: { opened: _.inc(1) }, }) } catch (e) { console.error(e) } } [代码] 使用 collection_update 云函数 更新某_id数据的打开人数: [代码]let _id = e.currentTarget.dataset.id wx.cloud.callFunction({ name: 'collection_update', data: { id: _id }, }).then(res => { console.log(res.data) }) .catch(console.error) [代码] 6. 数据库模糊查询 小程序云开发可以使用正则表达式进行模糊查询。例如, 根据用户输入关键词,查询标题中存在改关键词的古诗词。 [代码]let database = 'gushici' let condition = { name: { $regex:'.*'+ inputValue, $options: 'i' } } let { list, page, num } = this.data let that = this this.setData({ loading: true }) // 模糊查询 wx.cloud.callFunction({ name: 'collection_get', data: { database, page, num, condition }, }).then(res => { if (!res.result.data.length) { // 没搜索到 that.setData({ loading: false, isOver: true }) } else { let res_data = res.result.data list.push(...res_data) that.setData({ list, loading: false }) } }) .catch(console.error) [代码] 7. 分享或转发功能 小程序中页面触发转发的方式有两种: 1.在小程序的右上角选择转发,需要定义函数 Page.onShareAppMessage,如果当前页面没有定义此事件,则点击后无效果。 2.通过给 [代码]button[代码] 组件设置属性 [代码]open-type="share"[代码],可以在用户点击按钮后触发 Page.onShareAppMessage 事件,如果当前页面没有定义此事件,则点击后无效果。 用户还可以在 Page.onShareAppMessage 事件中自定义转发后显示的标题、图片、路径: [代码]onShareAppMessage(res) { let id = wx.getStorageSync('shareId') if (res.from === 'button') { // 来自页面内转发按钮 console.log(res.target) } return { title: `跟我一起挑战最长的成语接龙吧!`, path: `pages/find/find`, imageUrl: '/images/img.jpg', } }, [代码] 注意:转发成功/失败的 callback 已经被官方废弃,所以理论上小程序是无法得知用户是否将页面分享成功的 8. 用户授权 详情请参考文章:微信小程序之授权 9. 需要注意的几个坑 查询不到数据 数据表中明明有数据,但是 collection.get 到的却为空。解决:可以在云开发控制台中打开数据库权限设置,设置权限。 更新数据失败 collection.update 函数调用成功单返回的却是0行记录被更新,因为小程序端不允许更新没有 openid 字段的数据。解决:可以通过云函数更新数据库。 background 图片 url 不能为本地图片 解决:1:将图片上传到服务器,填写服务器上的图片路径地址。2:将图片转为 base64 编码。 往云数据库中批量导入 json 数据失败 原因:请看文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/import.html 解决:去掉json数据 [代码]{}[代码]之间的逗号, 如果最外层为 [代码][][代码],也必须去掉, 最终形如: [代码]{ "index": "作者_1", "type": "作者", "poet": "李白", "abstract": "李白(701年-762年),字太白,号青莲居士,唐朝浪漫主义诗人,被后人誉为“诗仙”..." } { "index": "作者_2", "type": "作者", "poet": "白居易", "abstract": "白居易(772年-846年),字乐天,号香山居士..." } [代码] 源码链接 https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心! [图片]
2019-08-09 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - 混迹社区后,给微信小程序开发者的一些建议
引言 我几乎每天都逛社区,但是由于水平有限参与的回答并不多。但是在我的长期窥屏之下。发现很多开发者的问题极其相似或者同一类型的问题出现频率非常高。故想总结一点建议,希望可以对自称新手的开发者一些帮助。 正文 写代码时请做好备份,一定要养成良好的代码编写习惯。及时将代码push到git或类似的版本控制平台 在提问题之前,请先阅读微信开发者文档,不要遇到问题就在社区里问,能不能,怎么做诸如此类的 如果真的在文档中没有找到或者解决不了,你也要学会利用搜索引擎,不能用谷歌的话,百度也是可以解决大部分问题的,同时善用搜索引擎你将会受益终生(不夸张)。 如果以上两点还是解决不了,请先搜索一下社区中有没有人提过类似问题。 提问请勿标题党,虽然真的实现了你的目的。比如我每次都会点进去,然后看到的却是一大堆吐槽。比如常见的问题:复制粘贴不能用,广告为什会被封,广告组件审核触发机制,流量主收益之类的。我知道这些让你很不爽,但是我还是觉得既然来社区讨论,大家还是文雅点比较好。 请不要让社区的同学来帮你思考,你有个什么需求,你就去想该怎么做,如果想不到,没有人禁止你参考别人的软件流程。比如,提问:做一个新闻小程序要怎么做之类的,我建议你先去看看头条。 提某些需求建议时,请先考虑合不合理的问题。比如,如何获取微信用户零钱?等诸如此类微信官方不可能也不需要去提供的功能。(这是哪个老板提的需求?和我老板有的一拼) 善待微信官方人员 结语 目前就想到这么多,如果有什么遗漏的,欢迎评论区补充。
2019-05-21 - 5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)
由于小程序2020年1月10日以后改模板消息为订阅消息,所以我写了一篇新的文章来更新这个知识点 《小程序订阅消息推送(含源码)java实现小程序推送,springboot实现微信消息推送》 我们在做小程序开发时,消息推送是不可避免的。今天就来教大家如何实现小程序消息推送的后台和前台开发。源码会在文章末尾贴出来。 其实我之前有写过一篇:《springboot实现微信消息推送,java实现小程序推送,含小程序端实现代码》 但是有同学反应这篇文章里的代码太繁琐,接入也比较麻烦。今天就来给大家写个精简版的,基本上只需要几行代码,就能实现小程序模版消息推送功能。 老规矩先看效果图 [图片] 这是我们最终推送给用户的模版消息。这是用户手机微信上显示的推送消息截图。 本节知识点 1,java开发推送后台 2,springboot实现推送功能 3,小程序获取用户openid 4,小程序获取fromid用来推送 先来看后台推送功能的实现 只有下面一个简单的PushController类,就可以实现小程序消息的推送 [图片] 再来看下PushController类,你没看错,实现小程序消息推送,就需要下面这几行代码就可以实现了。 [图片] 由于本推送代码是用springboot来实现的,下面就来简单的讲下。我我们需要注意的几点内容。 1,需要在pom.xml引入一个三方类库(推送的三方类库) [图片] pom.xml的完整代码如下 [代码]<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.qcl</groupId> <artifactId>wxapppush</artifactId> <version>0.0.1-SNAPSHOT</version> <name>wxapppush</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--微信小程序模版推送--> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>3.4.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> [代码] 其实到这里我们java后台的推送功能,就已经实现了。我们只需要运行springboot项目,就可以实现推送了。 下面贴出完整的PushController.java类。里面注释很详细了。 [代码]package com.qcl.wxapppush; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; import cn.binarywang.wx.miniapp.bean.WxMaTemplateData; import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage; import cn.binarywang.wx.miniapp.config.WxMaInMemoryConfig; import me.chanjar.weixin.common.error.WxErrorException; /** * Created by qcl on 2019-05-20 * 微信:2501902696 * desc: 微信小程序模版推送实现 */ @RestController public class PushController { @GetMapping("/push") public String push(@RequestParam String openid, @RequestParam String formid) { //1,配置小程序信息 WxMaInMemoryConfig wxConfig = new WxMaInMemoryConfig(); wxConfig.setAppid("XXX");//小程序appid wxConfig.setSecret("xxx");//小程序AppSecret WxMaService wxMaService = new WxMaServiceImpl(); wxMaService.setWxMaConfig(wxConfig); //2,设置模版信息(keyword1:类型,keyword2:内容) List<WxMaTemplateData> templateDataList = new ArrayList<>(2); WxMaTemplateData data1 = new WxMaTemplateData("keyword1", "获取老师微信"); WxMaTemplateData data2 = new WxMaTemplateData("keyword2", "2501902696"); templateDataList.add(data1); templateDataList.add(data2); //3,设置推送消息 WxMaTemplateMessage templateMessage = WxMaTemplateMessage.builder() .toUser(openid)//要推送的用户openid .formId(formid)//收集到的formid .templateId("eDZCu__qIz64Xx19dAoKg0Taf5AAoDmhUHprF6CAd4A")//推送的模版id(在小程序后台设置) .data(templateDataList)//模版信息 .page("pages/index/index")//要跳转到小程序那个页面 .build(); //4,发起推送 try { wxMaService.getMsgService().sendTemplateMsg(templateMessage); } catch (WxErrorException e) { System.out.println("推送失败:" + e.getMessage()); return e.getMessage(); } return "推送成功"; } } [代码] 看代码我们可以知道,我们需要做一些配置,需要下面信息 1,小程序appid 2,小程序AppSecret(密匙) 3,小程序推送模版id 4,用户的openid 5,用户的formid(一个formid只能用一次) 下面就是小程序部分,来教大家如何获取上面所需的5个信息。 1,appid和AppSecret的获取(登录小程序管理后台) [图片] 2,推送模版id [图片] 3,用户openid的获取,可以看下面的这篇文章,也可以看源码,这里不做具体讲解 小程序开发如何获取用户openid 4,获取formid [图片] 看官方文档,可以知道我们的formid有效期是7天,并且一个form_id只能使用一次,所以我们小程序端所需要做的就是尽可能的多拿些formid,然后传个后台,让后台存到数据库中,这样7天有效期内,想怎么用就怎么用了。 所以接下来要讲的就是小程序开发怎么尽可能多的拿到formid了 [图片] 看下官方提供的,只有在表单提交时把report-submit设为true时才能拿到formid,比如这样 [代码] <form report-submit='true' > <button form-type='submit'>获取formid</button> </form> [代码] 所以我们就要在这里下功夫了,既然只能在form组件获取,我们能不能把我们小程序里用到最多的地方用form来伪装呢。 下面简单写个获取formid和openid的完整示例,方便大家学习 效果图 [图片] 我们要做的就是点击获取formid按钮,可以获取到用户的formid和openid,正常我们开发时,是需要把openid和formid传给后台的,这里简单起见,我们直接用获取到的formid和openid实现推送功能 下面来看小程序端的实现代码 1,index.wxml [图片] 2,index.js [图片] 到这里我们小程序端的代码也实现了,接下来测试下推送。 [代码]formid: 6ee9ce80c1ed4a2f887fccddf87686eb openid o3DoL0Uusu1URBJK0NJ4jD1LrRe0 [代码] [图片] 可以看到我们用了上面获取到的openid和formid做了一次推送,显示推送成功 [图片] [图片] 到这里我们小程序消息推送的后台和小程序端都讲完了。 这里有两点需要大家注意 1,推送的openid和formid必须对应。 2,一个formid只能用一次,多次使用会报一下错误。 [代码]{"errcode":41029,"errmsg":"form id used count reach limit hint: [ssun8a09984113]"} [代码] 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 这里就不单独贴出源码下载链接了,大家感兴趣的话,可以私信我,或者在底部留言,我会把源码下载链接贴在留言区。 单独找我要源码也行(微信2501902696) 视频讲解:https://edu.csdn.net/course/detail/23750 源码链接:https://github.com/qiushi123/wxapppush
2020-01-08 - 借助小程序云开发实现小程序支付功能(含源码)
我们在做小程序支付相关的开发时,总会遇到这些难题。小程序调用微信支付时,必须要有自己的服务器,有自己的备案域名,有自己的后台开发。这就导致我们做小程序支付时的成本很大。本节就来教大家如何使用小程序云开发实现小程序支付功能的开发。不用搭建自己的服务器,不用有自己的备案域名。只需要简简单单的使用小程序云开发。 老规矩先看效果图: [图片] 本节知识点 1,云开发的部署和使用 2,支付相关的云函数开发 3,商品列表 4,订单列表 5,微信支付与支付成功回调 支付成功给用户发送推送消息的功能会在后面讲解。 下面就来教大家如何借助云开发使用小程序支付功能。 支付所需要用到的配置信息 1,小程序appid 2,云开发环境id 3,微信商户号 4,商户密匙 一,准备工作 1,已经申请小程序,获取小程序 AppID 和 Secret 在小程序管理后台中,【设置】 →【开发设置】 下可以获取微信小程序 AppID 和 Secret。 [图片] 2,微信支付商户号,获取商户号和商户密钥在微信支付商户管理平台中,【账户中心】→【商户信息】 下可以获取微信支付商户号。 [图片] 在【账户中心】 ‒> 【API安全】 下可以设置商户密钥。 [图片] 这里特殊说明下,个人小程序是没有办法使用微信支付的。所以如果想使用微信支付功能,必须是非个人账号(当然个人可以办个体户工商执照来注册非个人小程序账号) 3,微信开发者 IDE https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 4,开通小程序云开发功能:https://edu.csdn.net/course/play/9604/204526 二,商品列表的实现 效果图如下,由于本节重点是支付的实现,所以这里只简单贴出关键代码。 [图片] wxml布局如下: [代码]<view class="container"> <view class="good-item" wx:for="{{goods}}" wx:key="*this" ontap="getDetail" data-goodid="{{item._id}}"> <view class="good-image"> <image src="{{pic}}"></image> </view> <view class="good-detail"> <view class="title">商品: {{item.name}}</view> <view class="content">价格: {{item.price / 100}} 元 </view> <button class="button" type="primary" bindtap="makeOrder" data-goodid="{{item._id}}" >下单</button> </view> </view> </view> [代码] 我们所需要做的就是借助云开发获取云数据库里的商品信息,然后展示到商品列表,关于云开发获取商品列表并展示本节不做讲解(感兴趣的同学可以翻看我的历史博客,有写过的) 也有视频讲解: https://edu.csdn.net/course/detail/9604 [图片] 三,支付云函数的创建 首先看下我们支付云函数都包含那些内容 [图片] 简单先讲解下每个的用处 config下的index.js是做支付配置用的,主要配置支付相关的账号信息 lib是用的第三方的支付库,这里不做讲解。 重点讲解的是云函数入口 index.js 下面就来教大家如何去配置 1,配置config下的index.js, 这一步所需要做的就是把小程序appid,云开发环境ID,商户id,商户密匙。填进去。 [图片] 2,配置入口云函数 [图片] 详细代码如下,代码里注释很清除了,这里不再做单独讲解: [代码]const cloud = require('wx-server-sdk') cloud.init() const app = require('tcb-admin-node'); const pay = require('./lib/pay'); const { mpAppId, KEY } = require('./config/index'); const { WXPayConstants, WXPayUtil } = require('wx-js-utils'); const Res = require('./lib/res'); const ip = require('ip'); /** * * @param {obj} event * @param {string} event.type 功能类型 * @param {} userInfo.openId 用户的openid */ exports.main = async function(event, context) { const { type, data, userInfo } = event; const wxContext = cloud.getWXContext() const openid = userInfo.openId; app.init(); const db = app.database(); const goodCollection = db.collection('goods'); const orderCollection = db.collection('order'); // 订单文档的status 0 未支付 1 已支付 2 已关闭 switch (type) { // [在此处放置 unifiedorder 的相关代码] case 'unifiedorder': { // 查询该商品 ID 是否存在于数据库中,并将数据提取出来 const goodId = data.goodId let goods = await goodCollection.doc(goodId).get(); if (!goods.data.length) { return new Res({ code: 1, message: '找不到商品' }); } // 在云函数中提取数据,包括名称、价格才更合理安全, // 因为从端里传过来的商品数据都是不可靠的 let good = goods.data[0]; // 拼凑微信支付统一下单的参数 const curTime = Date.now(); const tradeNo = `${goodId}-${curTime}`; const body = good.name; const spbill_create_ip = ip.address() || '127.0.0.1'; // 云函数暂不支付 http 触发器,因此这里回调 notify_url 可以先随便填。 const notify_url = 'http://www.qq.com'; //'127.0.0.1'; const total_fee = good.price; const time_stamp = '' + Math.ceil(Date.now() / 1000); const out_trade_no = `${tradeNo}`; const sign_type = WXPayConstants.SIGN_TYPE_MD5; let orderParam = { body, spbill_create_ip, notify_url, out_trade_no, total_fee, openid, trade_type: 'JSAPI', timeStamp: time_stamp, }; // 调用 wx-js-utils 中的统一下单方法 const { return_code, ...restData } = await pay.unifiedOrder(orderParam); let order_id = null; if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { const { prepay_id, nonce_str } = restData; // 微信小程序支付要单独进地签名,并返回给小程序端 const sign = WXPayUtil.generateSignature({ appId: mpAppId, nonceStr: nonce_str, package: `prepay_id=${prepay_id}`, signType: 'MD5', timeStamp: time_stamp }, KEY); let orderData = { out_trade_no, time_stamp, nonce_str, sign, sign_type, body, total_fee, prepay_id, sign, status: 0, // 订单文档的status 0 未支付 1 已支付 2 已关闭 _openid: openid, }; let order = await orderCollection.add(orderData); order_id = order.id; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { out_trade_no, time_stamp, order_id, ...restData } }); } // [在此处放置 payorder 的相关代码] case 'payorder': { // 从端里出来相关的订单相信 const { out_trade_no, prepay_id, body, total_fee } = data; // 到微信支付侧查询是否存在该订单,并查询订单状态,看看是否已经支付成功了。 const { return_code, ...restData } = await pay.orderQuery({ out_trade_no }); // 若订单存在并支付成功,则开始处理支付 if (restData.trade_state === 'SUCCESS') { let result = await orderCollection .where({ out_trade_no }) .update({ status: 1, trade_state: restData.trade_state, trade_state_desc: restData.trade_state_desc }); let curDate = new Date(); let time = `${curDate.getFullYear()}-${curDate.getMonth() + 1}-${curDate.getDate()} ${curDate.getHours()}:${curDate.getMinutes()}:${curDate.getSeconds()}`; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } case 'orderquery': { const { transaction_id, out_trade_no } = data; // 查询订单 const { data: dbData } = await orderCollection .where({ out_trade_no }) .get(); const { return_code, ...restData } = await pay.orderQuery({ transaction_id, out_trade_no }); return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { ...restData, ...dbData[0] } }); } case 'closeorder': { // 关闭订单 const { out_trade_no } = data; const { return_code, ...restData } = await pay.closeOrder({ out_trade_no }); if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { await orderCollection .where({ out_trade_no }) .update({ status: 2, trade_state: 'CLOSED', trade_state_desc: '订单已关闭' }); } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } } } [代码] 其实我们支付的关键功能都在上面这些代码里面了。 [图片] 再来看下,支付的相关流程截图 [图片] 上图就涉及到了我们的订单列表,支付状态,支付成功后的回调。 今天就先讲到这里,后面会继续给大家讲解支付的其他功能。比如支付成功后的消息推送,也是可以借助云开发实现的。 由于源码里涉及到一些私密信息,这里就不单独贴出源码下载链接了,大家感兴趣的话,可以私信我,或者在底部留言。单独找我要源码也行(微信2501902696) 视频讲解地址:https://edu.csdn.net/course/detail/24770
2019-06-11 - 【高校开发者】双生日记开发经验分享
双生日记开发经验分享 Hello,我是双生日记的 Founder & Developer Airing。该项目的小程序端获得了 2018 C4——微信小程序应用开发赛的一等奖,而 iOS 端则获得了 2018 C4——移动应用创新赛的一等奖,目前累计注册用户已达 1 万+,并仍在不断开发维护中~ 本文将简要概括我们团队在产品的整个研发流程中的所做的工作,更侧重于介绍产品研发与团队管理的方法。 我将整个产品的研发分为以下四步: 立项 设计 开发 维护 [图片] 可以看到,以上四步形成了项目流程的闭环,使得产品能够良性发展。接下来我来具体谈谈这四步工作的内容。 1. 立项 项目立项是所有环节最开始的部分,我觉得也是最重要的部分,它的工作内容类似于“产品经理”的职责。虽然我是 Founder,但产品的探讨还是与大家共同完成的。具体而言,这个环节有两个内容: 产品脑暴 文档整理 1.1 产品脑暴 首先,我会先在团队中提出我的想法,并创建一个讨论区供大家讨论。我们是一个非常大的兴趣团队,虽然参与双生研发的只有寥寥三人,但是在产品脑暴的时候,团队的成员都提出了各自的见解与建议。例如,下图是我们在团队研发中讨论的内容。 [图片] 这里我们团队用的是产品是“语雀”,当然工具是随意的,用腾讯文档我觉得也非常方便,重要的是一定要形成电子版记录材料,如果只是在微信群里讨论或者线下简单聊聊,那讨论了、忘记了,那就相当低效,约等于没有讨论。 1.2 文档整理 第二步,整理脑暴的文档并撰写相关的研发文档,具体来说,包括但不限于: 需求文档 产品文档 模型文档 接口文档 [图片] PS. 这是我们团队的文档库,仅供参考:零熊 | 语雀 [图片] 2. 设计 设计工具我们用的 Sketch,但是不会把源文件直接发给研发同学,因为正版 Sketch 挺贵的,而且只支持 mac 系统。这里我们使用的工具是蓝湖,开始用的也是语雀的画板,但是发现实在是太难用了…另外,在蓝湖中的设计稿是可以分享的,并且邀请团队里的同学进行点评。 设计稿的内容具体包括: 规范 原型 UI 切图 规范重点是色彩规范、组件规范、和字体规范。原型更多的是交互说明,这里我们只是用批注的方式在 ui 上详细说明了一下交互,但如果直接用 flinto 去做也是可以的。flinto 的好处是更加直观,但是开发人员不一定能 get 到设计同学的全部内容。 [图片] 3. 开发 本部分分享的是产品研发的核心环节:项目开发。本环节我分享的内容会稍微多一些,但也略微零散,主要包含三个内容: 规划记录 开发工具 建议事项 3.1 规划记录 在开发之前,我习惯于自己先列一个 todolist 去罗列出项目中的各个需求点或技术点,从整体上会有一个直观的感受,也方便我去安排和规划自己的开发任务。这里我使用的工具是 Notion,我先按照重要的模块把产品分割成了 8 个部分,然后再在每个部分里写各自的 todolist,以免单文件 todolist 过长。 [图片] [图片] 当然,todolist 不单单记录待办事项而已,它更多的是承担一个开发日记的作用。我个人倾向于把开发中遇到的难点问题及解决方法,或者用到的资源顺手记录下来。我认为开发是一个学习和成长的过程,而不仅仅只是完成业务需求。 随手记录是方便日后整理为博客或者再遇到类似的问题可以快速定位,若不记录则很容易忘记。因此,做开发日记对学习的成效是非常大的。 [图片] 3.2 开发工具 针对微信小程序开发,我建议对开发很熟悉的同学可以尝试去使用 VS Code + 扩展 + 真机的模式进行开发,个人觉得这套流程既高效又不会出错。“高效”是 VS Code 自身的高效,而“出错”指的则是模拟器有时候效果与真机不同。 这里顺便安利一下我自己的 VS Code 配置: [图片] 我喜欢把资源管理器放在右边,有两个原因:一是左边是人的注意区,故应该放代码编辑器;二是我随时按 Cmd + B 可以隐藏资源管理器而同时不改变编辑器的位置,如果放左边,隐藏的时候编辑器会有一个位移,眼睛会很不舒服。 对于扩展,我这里用了几个比较有意思的: Color Highlight:颜色值高亮可视化为颜色本身,方便前端样式开发 TODO Highlight:高亮 TODO 与 FIXME miniapp:小程序标签与属性自动补全 Bracket Pair Colorizer:括号着色配对,这个特别方便。 Image preview:方便在代码里预览 uri 上的图片,我是用来看看自己资源路径有没有引错。 REST Client:HTTP 测试,方便开发、分享与 mock。 主题我用的是 Winter is Coming Theme + Material Icon Theme,我个人觉得黑色默认也非常好看。 3.3 注意事项 如果是协同开发我推荐搭配 Git History + Eslint 插件,当然如果自己开发,也免不了 Eslint。Git Commit 规范我们用的是这套: Commit 提交规范 [图片] 开发的时候也别忘记埋点,做一些打点统计,需要打点的地方根据项目需要检测的内容来定。如 PV、UV 这些小程序自带帮你统计了你可以不用打,但其他项目还是要统计的,或者直接规划好 Nginx 的日志,再对日志做分析也是 ok 的~ 如果前后端分离开发,前端同学可以自己接 mockjs 做一套符合接口文档规范的 mock 接口。 4. 维护 对于用户的反馈,我们智能筛选后自动提交到 github issue,再针对 issue 进行 label 和优先级分配。这是我们项目开源的主地址:oh-bear/2life。 [图片] 可以看到 issue 是比较杂乱的,所以还需要 github 的 project 去做一个任务画板。 [图片] by the way,安利一下小工具Devhub,可以很方便的检测自己负责项目的 issue。 [图片] 针对这些 issue,可以做一个阶段性的文档,回归到“立项”步骤,进行下一个小版本的开发。 [图片] 可以发现,我始终没有去选择使用甘特图软件,虽然甘特图更加直观,但是我不太喜欢把任务排的满满的、紧紧的,这样会不自觉地产生工作压力。最重要的是,我们毕竟不是工作嘛,只是一个兴趣开发,所以还是遵循自己的喜好来便好~ 好了,这次的分享就到这里。我是 Airing,我的个人博客是:https://me.ursb.me,欢迎大家来访交流~
2019-05-14 - 小程序地图学习之获取位置 获取经纬度 获取地名 获取地址
我们在做小程序开发时,难免会遇到地图相关的开发,而小程序已经为我们提供的比较完善的地图组件。我们只需要调用相关的api就可以实现大致的功能。如:获取经纬度,获取位置,获取地址,获取地名。结下来就具体给大家讲解。 老规矩先看效果图 [图片] 接下来我们就来看看具体实现步骤 一,定义一个按钮来调用位置获取的api [代码]<!--index.wxml--> <button bindtap='getLocation'>获取位置信息</button> <text>{{jingwei}}</text> <text>{{address}}</text> <text>{{name}}</text> [代码] 二,调用获取地理位置的方法 [代码]//index.js Page({ getLocation() { let that = this; wx.chooseLocation({ success: function(res) { console.log(res) var latitude = res.latitude var longitude = res.longitude; that.setData({ jingwei: "经纬度:" + longitude + ", " + latitude, address: " 地址:" + res.address, name: " 地名:" + res.name }) } }); } }) [代码] 其实到这里我们就可以实现获取经纬度,获取位置信息的功能了。 但是呢??现在小程序调用用户位置信息时,需要用户授权,如下图,如果用户点击了拒绝,我们就没有办法调用地图获取位置信息了。 [图片] 所以呢,我们要想实现一个完整的获取用户位置信息的功能,就要在监测到用户拒绝的位置权限时,引导用户去重新授权。这样才是一个友好的健壮的程序。下面就来教大家如何引导用户去打开授权。 三,在app.json里注册位置权限 [图片] 上图红色框里就是我们的位置权限的注册代码,app.json的完整代码如下。 [代码]{ "pages": [ "pages/index/index", "pages/setting/setting" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "WeChat", "navigationBarTextStyle": "black" }, "permission": { "scope.userLocation": { "desc": "你的位置信息将用于小程序位置接口的效果展示" } }, "sitemapLocation": "sitemap.json" } [代码] 四,定义检查位置权限是否打开的方法 [代码] //校验位置权限是否打开 checkLocation() { let that = this; //选择位置,需要用户授权 wx.getSetting({ success(res) { if (!res.authSetting['scope.userLocation']) { wx.authorize({ scope: 'scope.userLocation', success() { wx.showToast({ //这里提示失败原因 title: '授权成功!', duration: 1500 }) }, fail() { that.showSettingToast('需要授权位置信息'); } }) } } }) }, [代码] 这个方法就是来检查用户的位置权限是否授权,如果没有授权,就弹窗提示用户去授权页授权。弹窗代码如下: [代码] // 打开权限设置页提示框 showSettingToast: function(e) { wx.showModal({ title: '提示!', confirmText: '去设置', showCancel: false, content: e, success: function(res) { if (res.confirm) { wx.navigateTo({ url: '../setting/setting', }) } } }) }, [代码] 至此就可以实现一个完整的获取用户位置信息的小程序了,index.js完整代码如下 [代码]//index.js Page({ getLocation() { this.checkLocation(); let that = this; wx.chooseLocation({ success: function(res) { console.log(res) var latitude = res.latitude var longitude = res.longitude; that.setData({ jingwei: "经纬度:" + longitude + ", " + latitude, address: " 地址:" + res.address, name: " 地名:" + res.name }) } }); }, //校验位置权限是否打开 checkLocation() { let that = this; //选择位置,需要用户授权 wx.getSetting({ success(res) { if (!res.authSetting['scope.userLocation']) { wx.authorize({ scope: 'scope.userLocation', success() { wx.showToast({ //这里提示失败原因 title: '授权成功!', duration: 1500 }) }, fail() { that.showSettingToast('需要授权位置信息'); } }) } } }) }, // 打开权限设置页提示框 showSettingToast: function(e) { wx.showModal({ title: '提示!', confirmText: '去设置', showCancel: false, content: e, success: function(res) { if (res.confirm) { wx.navigateTo({ url: '../setting/setting', }) } } }) }, }) [代码] 从代码中可以看到,我们在用户拒绝授权时的提示框,点击会跳转到setting页,setting也是我们自己的页面,但是这个页面特别简单。就定义一个button。 [代码]<!--pages/setting/setting.wxml--> <button class="button" open-type="openSetting" type='primary'> 打开授权设置页 </button> [代码] 为什么要这么做呢,因为微信不允许我们直接打开权限设置页,必须通过button组件提供的开发能力去到设置页,这里的开放能力就是open-type=“openSetting” 中的openSetting。我们点击按钮后就到了权限设置页。 [图片] 这样就可以引导用户再次授权了。 有任何关于编程的问题都可以加我微信2501902696(备注编程开发) 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 完整的源码可以加老师微信获取,也可以关注下面老师公号,回复“地图源码” 获取。 [图片]
2019-05-08 - 小程序图片上传,存储,获取,显示
我们在做小程序开发时,难免会遇到图片上传的功能,我们如果自己搭建图片服务器的话,成功太大了,并且还要写后台程序来接收上传的图片,还要有存储服务器。好在小程序云开发为我们提供了云存储的功能,这样我们就可以轻松的实现小程序图片的上传和存储。 老规矩,先看效果图 [图片] 本节知识点 1,小程序图片的选取 2,小程序图片的上传 3,小程序图片的存储 4,获取云端图片并显示 下面就来具体讲解下具体实现步骤 图片的选择和上传 index.wxml文件如下 [代码] <view class='item_root' bindtap='chuantupian'> <text>{{zhaopian}}</text> <view class='right_arrow' /> </view> [代码] index.js文件如下 [代码] //上传图片 chuantupian() { let that = this; let timestamp = (new Date()).valueOf(); wx.chooseImage({ success: chooseResult => { wx.showLoading({ title: '上传中。。。', }) // 将图片上传至云存储空间 wx.cloud.uploadFile({ // 指定上传到的云路径 cloudPath: timestamp + '.png', // 指定要上传的文件的小程序临时文件路径 filePath: chooseResult.tempFilePaths[0], // 成功回调 success: res => { console.log('上传成功', res) wx.hideLoading() wx.showToast({ title: '上传图片成功', }) if (res.fileID) { that.setData({ zhaopian: '图片如下', imgUrl: res.fileID }) } }, }) }, }) }, [代码] 到这里其实我们就可以实现图片的选取和上传功能了。 下面讲讲具体是如何实现的 首先我们通过wx.chooseImage来获取相册里的图片 再获取照片成功后,我们用当前时间戳命名图片,然后使用 wx.cloud.uploadFile方法来实现图片的上传 在上传成功后,会有如下回调。下图中的filenId就是我们在云存储中的路径,可以直接用这个路径来获取图片并显示的。 [图片] 到这里我们就轻松的实现了小程序图片上传的功能,是不是很简单。 有任何关于编程的问题都可以加我微信2501902696(备注编程开发) 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 视频讲解地址:https://edu.csdn.net/course/play/9604/281187 [图片]
2019-06-11 - 巧用云调用,实现【共享名片夹】小程序
原创: 锋少 一、前言 从一个较早的小程序开发者到第一批使用小程序·云开发的开发者,这期间一直在关注关于小程序各方面的更新,同时也用小程序·云开发做了几款产品,其中包括上次分享的随手记Lite小程序,比较上次,这次分享的技术点相对更加全面和实用一些。 涉及的技术点有: 数据上传、数据更新、分页读取、数据删除,AI智能名片识别读取。 单图上传、多图上传,图片URL获取,带参小成码生成。 下发模板消息,云调用使用。 二、主要功能 创建电子名片:信息存储,图片上传,名片读取(AI智能名片识别) 转发电子名片:专属名片海报(带参小程序码生成) 电子名片被访问:下发模板消息(云调用) 三、功能实现 3.1、准备工作 1、注册微信小程序账号: 方式一:直接注册(https://mp.weixin.qq.com/wxopen/waregister?action=step1) 方式二:已经有微信公众号(已认证)朋友可以直接【登录公众号】 -> 【小程序管理】 -> 【添加】->【快速注册并认证小程序】,注册完成后,找到小程序的 AppID和 AppSecret [图片] 2、下载微信开发者工具、创建项目 ,打开开发者工具,键入项目目录、项目名称、刚才的 AppID,此时项目创建成功,然后点击开发者工具上方的【云开发】开通云开发。 3.2功能实现一:【创建电子名片】 信息存储,图片上传,名片读取(AI智能名片识别) 1.功能简要描述 对于一个名片的小程序,第一步肯定是创建电子名片,除此之外,可以用传统信息录入的方式创建名片,同时也支持纸质名片的识别读取,快速创建名片,这里本地需要导入 [代码]mapping.js[代码]框架,接下来以纸质名片识别为例。 2.核心代码 [代码] // 上传名片后获取零时链接 getTempFileURL() { wx.cloud.getTempFileURL({ fileList: [{ fileID: this.data.fileID, }], }).then(res => { console.log('获取成功', res); if (res.fileList.length) { this.setData({ coverImage: res.fileList[0].tempFileURL }, () => { this.parseNameCard(); }); } else { Toast('获取图片地址失败'); } }).catch(err => { Toast('获取图片地址失败'); }); }, // 读取名片 parseNameCard() { wx.cloud.callFunction({ name: 'parseCard', data: { url: this.data.coverImage } }).then(res => { if (res.result.data.length == 0) { Toast('解析失败,请上传【纸质名片】或【手动创建】'); return; } let data = this.transformMapping(res.result.data); wx.setStorageSync("parseCardData", data) Toast('解析成功'); }).catch(err => { console.error('解析失败,请上传【纸质名片】或【手动创建】', err); Toast('解析失败,请上传【纸质名片】或【手动创建】'); }); }, // 名片数据解析 transformMapping(data) { let record = {}; let returnData = []; data.map((item) => { let name = null; if (mapping.hasOwnProperty(item.item)) { name = mapping[item.item]; // 写入英文名 item.name = name; } return item; }); // 过滤重复的字段 data.forEach((item) => { if (!record.hasOwnProperty(item.item)) { returnData.push(item); record[item.item] = true; } }); return returnData; }, [代码] 3.3功能实现二:【转发电子名片】 专属名片海报(带参小程序码生成) 1.功能简要描述:转发电子名片有两种方式。 1.以小程序的形式直接转发给好友或微信群。 2.生成专属名片海报分享到朋友圈长按进入对应的电子名片页面。名片海报上除了有对应用户的姓名之外,还有专属的名片小程序码,效果如下: [图片] 2.核心代码 [代码]const cloud = require('wx-server-sdk') const axios = require('axios') var rp = require('request-promise'); cloud.init() // 云函数入口函数,小程序端传过来页面和名片id exports.main = async (event, context) => { console.log(event) try { const resultValue = await rp('https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=appid&secret=secret') const token = JSON.parse(resultValue).access_token; const response = await axios({ method: 'post', url: 'https://api.weixin.qq.com/wxa/getwxacodeunlimit', responseType: 'stream', params: { access_token: token, }, data: { page: event.page, width: 300, scene: event.id, }, }); return await cloud.uploadFile({ cloudPath: 'xcxcodeimages/' + Date.now() + '.png', fileContent: response.data, }); } catch (err) { console.log('>>>>>> ERROR:', err) } } [代码] 3.4功能实现三:【电子名片被访问】 下发模板消息(云调用) 1.功能简要描述 用户名片被访问的时候,用户者会收到【客户来访提醒】的模板消息,同时提醒用户完善名片信息。 2.核心代码 [代码]const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { try { const result = await cloud.openapi.templateMessage.send({ touser: event.toUser, page: "pages/index/index", data: { keyword1: { value: event.visitDate }, keyword2: { value: "刚刚有人深度访问了您的名片,经常完善名片信息,更容易被查找和访问。" }, }, templateId: 'templateId', formId: event.formId, }) return result } catch (err) { throw err } } [代码] 四、总结 和传统的小程序 + WEB后台开发模式比起来,云开发在精力和人力上真的是节省了很多,这能使开发者将大部分精力和时间放到功能的开发上。 云开发上线时间不算太长,但逐步有新的功能开放出来,比如云控制台数据的导入导出、云调用等,希望小程序·云开发开放出更多的接口和功能… 五、项目预览 [图片]
2019-05-05 - 如何获取微信小程序用户流量?
场景一:社交分享,用户裂变 你要买一件商品,设想一下这样的情境:A:淘宝评价都表示商品没有出现问题。B:身边朋友曾经买过同类产品。你会选择哪一款?相信很多人都会选择B。微信小程序商城,最棒的一点,就是基于微信社交关系背书,我们更快地买买买。 社交分享,用户裂变对于小程序商城流量获取至关重要。一生二,二生三,三生万物。微信小程序是社交分享的最佳选择 简单来讲,9个字,最友好、最习惯、最便捷。最友好。 区别于图片、图文、语音的形式,小程序分享采用截屏当前页面开始,高度为80%屏幕宽度的图像作为转发图片,更加丰富的展示分享内容形态。基于微信用户基数和分享习惯,在互动分享中将小程序推广。一键转发到好友/群更加便捷、随时随地使用。 [图片] 场景二:门店终端关系链社交分享 试想每一个品牌都拥有大量的实体门店和终端导购,每个门店导购就是一个流量节点。利用好品牌导购私域流量,核心两个:第一是微信群,第二个是个人微信。 商城用户社交分享,流量裂变 社交+电商,是小程序流量崛起最畅通的商业模式,用小程序商城连接用户,让零售有了更大的想象空间。比如,像我们常见的,在商城中利用「拼单」、「团购」、「秒杀」等手段,鼓励用户分享、点击,进而转化裂变。 [图片] 场景三:微信营销刺激流量 有人说,微信像一个蓄水池,通过公众号、社群把粉丝沉淀下来。小程序商城进而用更直接的方式将商品和用户连接起来,低成本引入高忠诚度用户,进行零售变现。小程序就像微信中的“任意门”,打通了微信的支付、卡券、广告和内容体系。 抛开众所周知的小程序多入口,比如用户可从“会话列表”、“发现入口”、“公众号主页”、“自定义菜单”、“图文消息”等入口进入小程序,我们从三个微信营销场景来获取用户流量。 电子优惠券:举一个实际案例,“i麦当劳”小程序每天约20万访问人数,平均停留时间30秒。麦当劳在小程序中不断迭代多种多样的优惠券活动,“免费试吃券”、“买一赠一券”、“限时特价券”。麦当劳认为,放进小程序中的优惠券给未到店的消费者多了一个回到门店的理由,这点很重要。 社交立减金:微信小程序也逐渐开放了社交立减金功能。当用户在小程序支付成功后,就会自动生成小程序立减金,用户通过邀请好友进行领取,当领取后在下次微信支付时候就能够进行兑换,一方面获取到更多新用户,也能激励用户的二次购买。 社交广告:目前社交广告支持投放品牌小程序,“公众号底部广告”、“公众号文中广告”,“朋友圈广告” 成为诸多品牌的选择。社交广告基于丰富的用户画像精准头像,帮助品牌获取茫茫人海中合适的潜在用户。 场景四:门店阵地不可忽视 流量价格越来越高,是我们不得不面临的共识。我们需要的不是一夜昙花,而是持续存储转化,吸纳门店线下流量不容忽视。小程序在门店有着丰富的应用场景,优化门店消费体验。有人借由商品带有自身信息标识便捷自助购物,比如家乐福;有人通过微信扫一扫直接体验穿搭,预约试衣,比如都市丽人。 麦当劳小程序也与线下门店消费场景结合,充分利用了线下门店的位置对小程序进行推广。在麦当劳中国2600家门店,都会有相应的易拉宝、桌贴等POP物料,引导顾客使用相应的服务。 在我们看来,成功收割小程序商城用户流量包括两方面内涵:获得大量高效低成本流量+成功变现。发力4种用户场景,开启小程序商城用户流量之路。 文章最后分享一下我收集的小程序: https://www.sucaihuo.com/source/0-0-266-0-0-0
2019-05-02 - 发送短信验证码后60秒倒计时
微信小程序发送短信验证码后60秒倒计时功能,效果图: [图片] 完整代码 index.wxml [代码]<!--index.wxml-->[代码][代码]<view class=[代码][代码]"container"[代码][代码]>[代码][代码] [代码][代码]<view class=[代码][代码]"section"[代码][代码]>[代码][代码] [代码][代码]<text>手机号码:</text>[代码][代码] [代码][代码]<input placeholder=[代码][代码]"请输入手机号码"[代码] [代码]type=[代码][代码]"number"[代码] [代码]maxlength=[代码][代码]"11"[代码] [代码]bindinput=[代码][代码]"inputPhoneNum"[代码] [代码]auto-focus />[代码][代码] [代码][代码]<text wx:if=[代码][代码]"{{send}}"[代码] [代码]class=[代码][代码]"sendMsg"[代码] [代码]bindtap=[代码][代码]"sendMsg"[代码][代码]>发送</text>[代码][代码] [代码][代码]<text wx:if=[代码][代码]"{{alreadySend}}"[代码] [代码]class=[代码][代码]"sendMsg"[代码] [代码]>{{second+[代码][代码]"s"[代码][代码]}}</text>[代码][代码] [代码][代码]</view>[代码][代码]</view>[代码] index.wxss [代码]/**index.wxss**/[代码][代码].userinfo {[代码][代码] [代码][代码]display[代码][代码]: flex;[代码][代码] [代码][代码]flex-[代码][代码]direction[代码][代码]: column;[代码][代码] [代码][代码]align-items: [代码][代码]center[代码][代码];[代码][代码]}[代码][代码].section {[代码][代码]display[代码][代码]: flex;[代码][代码]margin[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]padding[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]border-bottom[代码][代码]: [代码][代码]1[代码][代码]rpx [代码][代码]solid[代码] [代码]#CFD8DC[代码][代码];[代码][代码]}[代码][代码] [代码] [代码]text {[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]200[代码][代码]rpx;[代码][代码]}[代码][代码] [代码] [代码]button {[代码][代码] [代码][代码]margin[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]}[代码][代码] [代码] [代码].sendMsg {[代码][代码] [代码][代码]font-size[代码][代码]: [代码][代码]12[代码][代码];[代码][代码] [代码][代码]margin-right[代码][代码]: [代码][代码]0[代码][代码];[代码][代码] [代码][代码]padding[代码][代码]: [代码][代码]0[代码][代码];[代码][代码] [代码][代码]height[代码][代码]: inherit;[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]80[代码][代码]rpx;[代码][代码]}[代码]index.js [代码]//index.js[代码][代码]//获取应用实例[代码][代码]const app = getApp()[代码][代码] [代码] [代码]Page({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]send: true,[代码][代码] [代码][代码]alreadySend: false,[代码][代码] [代码][代码]second: [代码][代码]60[代码][代码],[代码][代码] [代码][代码]disabled: true,[代码][代码] [代码][代码]phoneNum: [代码][代码]''[代码][代码] [代码][代码]},[代码][代码] [代码][代码]// 手机号部分[代码][代码] [代码][代码]inputPhoneNum: function (e) {[代码][代码] [代码][代码]let phoneNum = e.detail.value[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]phoneNum: phoneNum[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]sendMsg: function () {[代码][代码] [代码][代码]var phoneNum = this.data.phoneNum;[代码][代码] [代码][代码]if(phoneNum == [代码][代码]''[代码][代码]){[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'请输入手机号码'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码],[代码][代码] [代码][代码]duration: [代码][代码]2000[代码][代码] [代码][代码]})[代码][代码] [代码][代码]return ;[代码][代码] [代码][代码]}[代码][代码] [代码][代码]//此处省略发送短信验证码功能[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]alreadySend: true,[代码][代码] [代码][代码]send: false[代码][代码] [代码][代码]})[代码][代码] [代码][代码]this.timer()[代码][代码] [代码][代码]},[代码][代码] [代码][代码]showSendMsg: function () {[代码][代码] [代码][代码]if (!this.data.alreadySend) {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]send: true[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]hideSendMsg: function () {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]send: false,[代码][代码] [代码][代码]disabled: true,[代码][代码] [代码][代码]buttonType: [代码][代码]'default'[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]timer: function () {[代码][代码] [代码][代码]let promise = new Promise((resolve, reject) => {[代码][代码] [代码][代码]let setTimer = setInterval([代码][代码] [代码][代码]() => {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]second: this.data.second - [代码][代码]1[代码][代码] [代码][代码]})[代码][代码] [代码][代码]if (this.data.second <= [代码][代码]0[代码][代码]) {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]second: [代码][代码]60[代码][代码],[代码][代码] [代码][代码]alreadySend: false,[代码][代码] [代码][代码]send: true[代码][代码] [代码][代码]})[代码][代码] [代码][代码]resolve(setTimer)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码], [代码][代码]1000[代码][代码])[代码][代码] [代码][代码]})[代码][代码] [代码][代码]promise.then((setTimer) => {[代码][代码] [代码][代码]clearInterval(setTimer)[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码]})[代码]完整的短信验证码登录实例参考: https://blog.csdn.net/zuoliangzhu/article/details/81219900
2019-04-17 - 图片实现渐变/透明效果
众所周知,图片等一些盒子都可以利用opacity属性来设置不透明度,但是前两天我朋友忽然给我一个截图,截图效果如下 [图片] 图中红框圈住的位置图片或者说摄像头采集的画面出现了渐变到透明,可以清楚的看到可以看到后面小哥的胳膊,然后问我如何实现这种效果,这下把我难住了(呵 天天给我出难题),我开始在个大论坛开始寻找解决方案; 忽然在前天,日常逛论坛时看到一个文字投影的效果,而后忽然灵机一动就想,能不能变相的实现前两天我想要的那种效果,于是乎赶紧打开编辑器试了下,发现确实可以把我想要的图片或者盒子进行投影并给投影设置上渐变颜色及透明,结果出来了,只不过出来的效果他反了 [图片] 随后利用transform: rotate(180deg);控制他使出倒挂金钩此等功夫,果然不负所望,成功翻转过来 [图片] 但是我想要的只有投影,因为我想要效果目前只能用投影去实现去控制,但是他却本体与投影共同出现了,我不想看到本体,太丑了,怎么办呢,那就给他装个position: absolute; top给他爸爸装个position: relative; overflow: hidden;让他滚出~,结果显而易见,我胜利了; [图片] 我得到了我想要的结果,为了验证结果,我用文字放在他的下方 看看是否透明; [图片] 我真的成功了,哈哈(小开心一会儿),为了再次确认他真是的图片实现了渐变透明,我把渐变的透明度改成了1(也就是不透明) [图片] 事实证明,我真的成功了!!! 吹完牛皮,赶紧附上完成代码: css: [图片] html: [图片] 最终效果图: [图片] 呃…其实核心就是利用投影来完成的-webkit-box-reflect: below 0 linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 1) 100%); https://www.w3cschool.cn/css3/box-reflect.html 当然 肯定有大佬在我之前发现这种实现方式,不过当时我找了很久都没找到实现方式的写法,想了想 就发出来吧,如果有什么不对的地方,或者有其他方式也可以实现同等效果的话 还劳请告知,在下多谢各位大佬了!!!
2019-04-19 - 小程序+公众号结合的营销玩法有哪些?
在商家运营小程序的过程中,有很多商家都忽略到了微信公众号的作用,事实上,将小程序与微信公众号结合使用,可以给商家带来不错的营销效果。那么,小程序+公众号结合的营销玩法有哪些呢? 公众号+小程序=工具+内容 公众号有着比较强的内容属性,用户关注的主要目的主要就是阅读、观看或收听,能够帮助商家积聚与沉淀粉丝。公众号之间的竞争,主要是内容能力的竞争。小程序比起公众号,它的工具属性更强,主要是为用户解决实际的问题,点餐、排队、购票等等都是典型的应用场景。小程序之间的竞争,主要是用户体验之间的竞争。 关于用户体验这方面,商家无需过于担心,得有店团队有着十年的产品经验,能够帮助商家不断地优化用户的购物体验,可以保障商家的小程序的稳定性,安全性,为用户打造优质的用户体验。对于商户,最理想的情况就是:公众号生产内容,触达粉丝和用户,小程序做商业服务和交易变现,将营销在微信的生态体系内完成。 小程序+公众号=引流+沉淀 商家可以将微信公众号与小程序绑定,通过利用微信公众号与小程序的不同属性,实现用户的拉新引流和累积沉淀! 多种形式引流,高效拓客 小程序本身有着免安装免卸载、即开即用、即用即走的特点,用户的使用成本比较低,并且小程序本身也有着丰富的入口场景,用户可以通过【搜一搜】、【扫一扫】、【附近的小程序】、【小程序卡片】等多种方式进入到商家的小程序店铺中,推广引流的效果比较好。 内容互动,促进沉淀 基于微信公众号的内容属性,商家可以通过微信公众号与用户建立持久联系,积累和沉淀商家的用户。 公众号跳转小程序的方式主要有四种:公众号介绍页、自定义菜单栏、文章内图文展示、模板消息。商家可以在日常的图文推送中引导用户跳转商家的小程序店铺中去,刺激用户二次消费,提高用户复购率。 公众号+小程序,自媒体变现更加快速 自媒体变现难一直以来都是自媒体行业的一个痛点。小程序便可以很好地解决这一行业痛点,商家可以将小程序与微信公众号关联,通过图文展示、自定义菜单栏等方式大大缩短了用户的购买路径,让用户可以在购买欲产生的第一时间内可以完成购买。 公众号+小程序,企业营销更加丰富 营销活动是商家日常经营中必不可少的一个环节,微信公众号可以让商家的小程序营销变得更加丰富。商家可以通过微信公公众号的图文发送将活动信息直接触达到用户,在信息传达方面更加及时。并且,商家也可以通过微信公众号的模板消息,实时地告知用户活动情况,让用户可以在第一时间内获取到活动的进程。 显然,小程序已经成为了企业的一个标配。商家可以通过微信公众号与小程序相互配合,做好线上店铺经营。 还没有小程序的话,可以看看这里: https://www.sucaihuo.com/source/0-0-266-0-0-0
2019-04-19 - 品牌营销为什么需要小程序?
小程序这三个字想必大家都是耳熟能详,作为一个互联网的新人,它在2017-2018年中发光发热,以往,微博、自媒体公众号都是营销的主展场,但如今,阵营却慢慢发生了变化:向小程序靠近。互联网的发展趋势仿佛慢慢有了小程序的一席之地,这个事物仿佛突然又好似在我们生活中存在了很久。 然而还有很多实体店对于小程序不是很了解,不明白为什么自己要花钱去开发一款小程序,也不知道自己的店铺开发小程序能否盈利,或是否适合开发小程序。那么营销是否真的需要小程序呢?答案是:必然需要的。 首先我们来谈谈小程序可以为商家带来什么? 一、无需下载安装,用完即走 小程序是一种无需下载安装即可使用的类APP应用,用户用完即走,高效利用,无缝连接微信10亿用户。未来的几年内,许多APP会被小程序取代,小程序即将改变传统的线上营销市场。 二、小程序是最轻量级的品牌营销 轻薄,无需安装,不占过多内存,只要打开即可使用。 便捷,保留了最基础的功能,即使你并没有很多这方面的经验,也可以马上上手操作。 酷炫,所有以前你想做的做不到的事情,小程序如今都能快速的帮你解决,让他变得触手可及。 小程序对于品牌的意义,除了是品牌+的作用,能提供更大的便利性,让用户的体验得到提升,进而让用户对品牌的评价提升。更重要的是展示类的小程序,可以通过“附近的小程序”、“搜索小程序”等入口为品牌获得最直接的曝光。 三、多渠道发展与留住客户 互联网+时代的到来,无数个案例都在向人们证明:靠单一平台维系客户、挖掘新客户就想持续获得影响力是不可能的,平台可能会消失,用户必须掌握在自己手中。所以品牌必须通过多渠道来留住客户,并搭建属于自己的空间,无论是开发 APP ,还是通过指尖时代搭建官方网站,总之,把对平台的依赖度降低,再把客户资源掌握在自己手中才是王道。 四、更多的曝光率 小程序可分享给好友、分享到群,中小型商家可利用这个特性,开展线上活动裂变客户,进行优质的社群营销,以达到获取更多客户的效果。 文章最后分享一下我收集的小程序: https://www.sucaihuo.com/source/0-0-266-0-0-0
2019-04-25 - 使用云开发接入阿里云短信SDK,实现自给自足!
发送手机短信验证码 按需求自行修改函数内容 1.前往阿里云申请短信服务 1.短信服务 > 国内消息 > 添加签名 [图片] 1.短信服务 > 国内消息 > 添加模板 [图片] 2.引入 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const Core = require('@alicloud/pop-core'); const accessKeyId = 'xxx' // 你的appid const accessKeySecret = 'xxx' // 你的secret const SignName = 'xxx' // 你的签名 const TemplateCode = 'xxx' // 你的模版CODE var client = new Core({ accessKeyId, accessKeySecret, endpoint: 'https://dysmsapi.aliyuncs.com', apiVersion: '2017-05-25' }) let params = { SignNameJson: JSON.stringify([SignName]), TemplateCode: TemplateCode, } cloud.init({ env: 'xxx' // 你的环境id }) // 云函数入口函数 /** * 发送模板消息 */ exports.main = async(event, context) => { let { OPENID, APPID, UNIONID } = cloud.getWXContext() const db = cloud.database() return new Promise(async(resolve, reject) => { try { if(!event.phone) throw {code: 7322, data: [],info: '手机不能为空!'} if(!/^[1][3,4,5,6,7,8,9][0-9]{9}$/.test(event.phone)) throw {code: 7321, data: [],info: '手机号码格式错误!'} // 获取数据 let { data } = await db.collection('sms-record').where({ phone: event.phone, openid: OPENID, is_used: 1 }).orderBy('created_at', 'desc').skip(0).limit(1).get(), code = null // 计算时间 if(data.length != 0 && (Number(new Date()) - Number(new Date(data[0].created_at))) < 60000) { throw {code: 7323, data: [],info: '一分钟内,不能重复发送!'} } else if(data.length != 0 && (Number(new Date()) - Number(new Date(data[0].created_at))) < 1800000){ code = data[0].code } else { // 生成六位随机数 code = Math.floor(Math.random() * 900000) + 100000 } //发送短信 let { Code } = await client.request('SendBatchSms', Object.assign({ PhoneNumberJson: JSON.stringify([event.phone]), TemplateParamJson: JSON.stringify([{code}]) },params), { method: 'POST' }) if(Code !== 'OK') throw {code: 7321, data: [],info: '发送短信失败!'} // 新增数据 await db.collection('sms-record').add({ data: { phone: event.phone, code, openid: OPENID, is_used: 1, created_at: db.serverDate() } }) resolve({ code: 0, data: [], info: '操作成功!' }) } catch (error) { console.log(error) if(!error.code) reject(error) resolve(error) } }) } [代码] 3.参数 属性 类型 默认值 必填 说明 phone string 是 国内手机号码 4.使用 [代码] // 返回Promise wx.cloud.callFunction({ name: 'sendSms', data: { phone } }).then(res => { console.log(res) }) async sendSms(){ try { let { data } = await wx.cloud.callFunction({ name: 'sendSms', data: { phone } }) console.log(data) } catch (error) { console.log(error) } } [代码] 当然你也可以使用旧版的sdk 更多云函数模板 另外求个流量 和 star 模板使用云开发实现,接入百度AI平台API图像识别系统,无需另外搭建服务器,只需修改文件内配置项 一款方便快捷识别AI,可根据您拍摄或相册中照片识别出您所需要知道的物种(植物,动物,图文,菜品类型),相关知识,帮助您了解该物种,打开新世界! [图片]
2019-04-26 - 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 - 腾讯课堂小程序详情页开发总结
状态管理 一开始为了借鉴和复用课堂H5详情页的状态管理,引入 redux ,但由于 reducer 总是返回一个新的更新后的对象,这意味着每次 setData 时会传递全量的数据,而在小程序双线程界面渲染的数据通信模型下,传输数据量与性能正相关,因此对于数据量比较大的详情页来说,每次 action 操作都比较耗性能,体验不好。 于是改用腾讯开源的小程序状态管理方案 westore, 它利用小程序 setData 函数支持以数据路径的形式传递数据的特点,通过 update 函数先进行 diff 得到最小更新的数据路径集合,然后再调用 setData 函数传递变化的数据以达到更优的性能。 可是 westore 是基于页面路径来同步数据的,如果同时存在两个相同路径的页面,则只有最新的页面会更新;例如当前页面 A (pages/course?cid=A)打开相同路径的页面 B (pages/course?cid=B)时,由于 store 数据是共享的,这时页面 B 持有页面 A 的数据,同时页面 A、B 路径(pages/course)相同,此时 westore 已经丢掉页面 A 的引用,当 westore 更新数据时只会影响到页面 B ,页面 B 返回页面 A 后,已经无法再更新页面 A 了。 对于这个问题,只要增加一个栈来记录页面路径实例,新开页面时,重置 westore 数据,页面返回时,将旧页面实例的数据同步到 westore 即可。 [图片] 除此之外,H5详情页中很多复合的状态逻辑都放在嵌套较深的自定义组件中,可在小程序环境下就有点力不从心了,所以必须要将这部分常变状态和遍历逻辑提前计算,以便 westore diff 局部更新。 富文本 [图片] 课堂详情页中需要展示由富文本编辑器 CKEditor 生成的课程详情,里面可能包含视频,但小程序提供的 rich-text 组件无法支持 video 标签,因此用到 wxParse 来将 HTML 文本解析成 JSON 树,然后通过 view + css 来模拟 HTML 元素进行渲染。 可是 wxParse 已经很久没有更新了,在使用过程中发现它有很多问题和局限性,以下是踩坑改造优化经验: 缺少解码、解析和渲染完成等钩子:由于后台 CGI 返回的 HTML 文本存在二次编码的情况,只经过 wxParse 的一次解码后仍有部分字串没有被正确解析,同时针对某些解析后的 HTML 标签需要扩展其属性等等。 因此只能修改源码增加 beforeDiscode、afterDiscode、parsedStartTag、parsedEndTag、parsed 和 complete 等钩子来提高其灵活性。 含有较多复杂属性的 HTML 标签无法解析出来:主要是 wxParse 中 startTag 的正则表达式不够全面导致的。 [图片] 上图无法解析出第一个 p 标签。 修改一下 startTag 的正则表达式即可。 [图片] 12个相同 template:wxParse 定义了 wxParse0 到 wxParse11 共 12 个 template,这 12 个 template 除了子结构调用不同的 wxParseXX template 之外其余代码都是一样,究其原因是因为小程序 template 不能递归引用,当然这种变通的处理方式有个局限性,就是它处理不了超过 12 层的结构,超过以后就解析不了,再加上小程序的机制,这样是不会报错的,导致查 bug 很困难。要解决这个问题,除了官方支持 template 递归,可以将 wxParse 改为自定义组件(暂未尝试),或者尽可能的合并 HTML 结构。例如 [图片] wxParse 解析渲染后的结果 [图片] 这里可以发现每个 wxParse-inline 元素的样式完全可以合并,同时形如 wxParse-s 等元素是通过 css 来模拟 HTML 元素的,因此对于这样嵌套的行内元素,可以进行合并 [图片] a 标签作为块元素:由于 a 标签允许包裹其中没有交互内容的块元素,wxParse 把 a 标签视为块级元素,导致解析 a 时将其前一个行内元素提前闭合了,造成显示错误。解决办法是将 a 标签从块级元素中剔除。 不支持腾讯视频 vid:小程序 video 仅支持视频地址和云文件 ID,但课程详情会包含腾讯视频,而腾讯视频播放路径需要通过腾讯视频 SDK 将视频 vid 转换出来,由于已经引入腾讯视频组件,VID 转换这一步可以省略交给腾讯视频组件,只需要将 wxParse template 中 video 标签改为 txv-video,同时在 wxParse 解析出 video 数据时计算出 authExt ,连同 vid 等必要字段一并提供给 txv-video 即可播放视频。考虑到课程详情中的视频播放频次不高,没必要详情展示时就生成腾讯视频组件,因此使用封面 + 播放按钮来替代,等待用户点击封面时才生成。 wxParse 样式污染全局:定义了 view 样式,但没有限定在 .wxParse 作用域下生效,导致影响了页面全局。 标签内文本含有 < 则解析结束标签有误 setData含有较多与界面渲染无关的数据 … WXML WXML(WeiXin Markup Language)是小程序视图层的一套标签语言,它与 Vue 的模板语法很相似,但在实际开发过程中经常会遇到一些问题与限制。 数据绑定中的数据处理 在 WXML 中,数据绑定只支持简单的 js 表达式,不能调用方法。例如保留数据的小数点后两位 [代码]<view>{{num.toFixed(2)}}</view> [代码] 这种写法是不会生效的,为了弥补 WXML 中数据处理的短板,小程序提供了 WXS(WeiXin Script)脚本,可以这么做 [代码]<view>{{tools2.toFixed(num, 2)}}</view> <wxs module="tools2"> function toFixed(num, len) { return num.toFixed(len); } module.exports = { toFixed: toFixed } </wxs> [代码] 但要注意的是 wxs 与 javascript 是不同的语言,有自己的语法,并不和 javascript 一致,更不能使用 es6 语法。 wxs 的运行环境和其他 javascript 代码是隔离的,wxs 中不能调用其他 javascript 文件中定义的函数,也不能调用小程序提供的API。 wxs 函数不能作为组件的事件回调。 wxs 目前共有以下几种数据类型:number,string,boolean,object,function,array,date,regexp template 的 data 传参 如果只看 官方文档 template 说明,你可能不知道 template 的 data 传参有三种方式: 格式一:data="{{ …value1,…value2,… }}",value 前面的 [代码]...[代码] 是扩展运算符。 格式二:data="{{ value1,value2,… }}"。 格式三:data="{{ key1: value1,key2: value2,… }}"。 value 可以是 boolean、number、string、null、object、array。 例如 [代码]value = { a: 1, b: 2, c: 3}[代码],那么在 template 中的使用如下: [代码]<!-- 格式一 --> <template name="example1"> <view>{{a}}: {{b}}: {{c}}</view> </template> <template is="example1" data="{{...value}}" /> <!-- 格式二 --> <template name="example2"> <view>{{value.a}}: {{value.b}}: {{value.c}}</view> </template> <template is="example2" data="{{value}}" /> <!-- 格式三 --> <template name="example3"> <view>{{k.a}}: {{k.b}}: {{k.c}}</view> </template> <template is="example3" data="{{k:value}}" /> [代码] 如果在列表渲染时,想要将列表的索引 index 在 template 中使用,可以这样做 [代码]<template name="example4"> <view>{{index}}: {{msg}}: {{time}}</view> </template> <template is="example4" wx:for="{{items}}" data="{{index, ...item}}" /> [代码] 除此之外还可以结合 wxs [代码]<template name="example5"> <view>{{msg}}: {{time}}</view> </template> <template is="example5" data="{{...tools.getLast(items)}}" /> <wxs module="tools"> function getLast(items) { return items[items.length - 1]; } module.exports = { getLast: getLast }; </wxs> [代码] 数据缓存与自定义组件和 wx:if 在做页面数据缓存时,由于页面数据字段比较多且嵌套深,有时图方便,我们会省略嵌套深的字段定义同时将缓存赋给 data,然后直接在 wxml 中使用 [图片] 如果 wxml 中刚好使用了 wx:if 和自定义组件,那么在小程序基础库 2.4.0 及以下,从第二次进入该页面时就会报错 [代码]Expect FLOW_CREATE_NODE but get another[代码],对于这个问题,有几种解决办法: _list 列出所有字段定义。 data 中 list 不直接赋值 _list,改在 onLoad 时通过 setData 传递。 wxml 中 wx:if 改为 hidden 处理,或者不适用自定义组件。 上述问题出现的条件比较特殊,很大部分是编码问题,但从小程序基础库 2.4.1 开始就不会出现。对比了不同版本基础库在 onLoad 阶段输出的 data 信息,发现 2.4.1 及以上 data 的初始值不再等于当前缓存的 _list 值。 2.4.0 及以下第一次和第二次进入该页面时的 data 值,第二次进入已有缓存 [图片] 2.4.1 及以上第一次和第二次进入该页面时的 data 值相同 [图片] 其他 template 模板与 component 组件 template 模块与 component 组件是小程序中组件化的方式。二者的区别: template 模块主要是展示,交互需要在使用 template 的页面中定义。 component 组件拥有自己的数据处理与交互逻辑,类似一个 page 页面。 在需要频繁更新的场景下或者在列表中涉及到列表子项独立的操作时,使用自定义组件可以只在组件内部进行更新,即实现页面局部更新,而不受页面其他部分内容的影响。 onPageScroll 与 IntersectionObserver 在做图片懒加载、元素曝光上报和元素吸顶展示时,离不开元素位置与页面滚动位置的判断,与之相关的事件或API有: onPageScroll:page 中监听用户滑动页面的事件。自定义组件无法使用,只能通过传参或事件总线来获取变化状态。 IntersectionObserver:监听某些节点与参照物边界相交状态的对象。参照物可以是指定一个节点或者页面显示区域。 从触发回调频次来看,onPageScroll 远远高于 IntersectionObserver,而且每一次事件回调都是一次视图到逻辑的通信过程。因此应该只在必要的时候才使用 onPageScroll,其他情况使用 IntersectionObserver 替代较好。
2019-05-07 - 驾校答题小程序实战全过程-1.数据库设计
1. 项目要求 小程序端开发 WEB管理后台开发 2. 项目功能 主栏目分为科目一答题练习、科目四答题练习、满分学习、注销恢复、资格证,包含小车、客车、摩托车等,功能包含视频讲解、章节练习、模拟考试、错题、收藏等等。 支持支付、分享、提成、提现等 3. 项目选型 小程序选择 Serverless架构,以最高效率开发 这里使用以下技术产品 小程序端: Bmob后端云+iview VUE + Bmob后端云 Bmob后端云,自带了支付,生成二维码等接口,这里集成可以快速上线。VUE 有丰富的后台模板,可以更快的做出管理后台。 4.开发过程 第一步:清晰了解需求 参考、分析行业有关产品:驾校一点通与驾考宝典。 第二步:数据库设计 思路:数据库参考其他驾考类小程序,优先把核心字段建立出来,尽量考虑周全, 不够后面再补。 这里注意的是,Serverless架构,主要适合做一些中小型应用,如果应用数据量很大, 尽量提前考虑分表。 数据表: 题目表 题目类别表(题形) 错题表 收藏表 成绩表 用户表 用户表(Bmob系统自带 )了 数据库文档 由于使用Bmob后端云数据库,默认每条记录都带id、创建时间与更新时间,此数据表说明不带这2个字段。 题目表(question) 名称 类型 描述 title string 题目名称 choseList Array 选择列表 [{“item”:“选选一”},{“item”:“选选二”,“isChose”:true}] type int 类型 1.单选 2.多选 3.判断 pic string 图片地址 video string 视频地址 help string 帮助描述 bSubjects int 所属科目 1.科目一 2.科目四 bModels int 所属车型 1.小车 2.货车 3.客车 4.摩托车 bType string 所属类别 关联类别表 题目类别表(questionType) 名称 类型 描述 bSubjects string 所属科目 1.科目一 2.科目四 title sting 类别名称 错题表 名称 类型 描述 id string 题目id uid string 用户id 收藏表 名称 类型 描述 id string 题目id uid string 用户id 成绩表 名称 类型 描述 time string 用时 score int 成绩 设计好数据表之后,做准备工作 拿到小程序的 AppID填写到Bmob控制台,应用设置-》应用配置 把上面所有想好建立好的数据表添加到数据库 小程序开发工具新建一个空白项目,复制BmobSDK,到utils目录 引入SDK,初始化。 引入SDK,初始化。文档在这里: https://bmob.github.io/hydrogen-js-sdk/#/?id=%E5%88%9D%E5%A7%8B%E5%8C%96 最终效果如下: 效果一:所写代码➕预览 [图片] 效果二:数据表变化 [图片] 这样我们就实现了,整个小程序开发的第一步,自动注册登陆,获取openid。 后面将继续讲解该小程序的答题功能、顺序练习、模拟考试、题目采集、语音读题等功能开发,敬请关注
2019-03-21 - 假期为啥不用加班,因为小程序云开发上线了这个功能!
清明小长假就要来了,今天你可以按时下班吗? 开发阿杰早在假期前就计划好了和女友的巴厘岛之旅,也提前订好了机票。可偏偏就在放假前一天,因公司接待需要得紧急上线一个访客预约的小程序解决自主预约及访客通知的需求,由于没有通过小程序发通知的经验沉淀,从阅读文档到产品发布可能需要耗费不少的时间,这使阿杰犯了难,原定于19:00起飞的飞机,他还能赶上吗? 好在继云函数、云存储、云数据库之后,又一提高开发效率的神器——云调用上线了! 啥是云调用? 云调用简单来说是一种免Token调用微信API的能力。传统的微信小程序开发,如果需要调用服务端的API,需要拿着 appid 和 appsecret 换取微信小程序全局唯一后台接口调用凭证 access_token ,而且有效期仅有2小时,有了这个凭证才能开始调用诸如模版消息、客服消息等API。而云调用的诞生,大大简化了调用微信API的鉴权步骤,实现一行代码即调即用。 为什么要用云调用? 微信小程序使用云调用能力后,开发者能够—— 1、一行代码调用API 云调用允许在没有获取 access_token 的情况下调用大部分小程序服务端的API,开发者只需关心业务逻辑本身及调用API的时机,真正实现一行代码调用API。 2、无需担心凭证安全 支持云调用的接口无需获取 access_token 即可调用,换句话说,就是开发者无需关心 access_token 的保管及失效问题,即可获得天然、安全、可靠的接口调用条件,一切接口调用的鉴权机制都交由云开发处理。 如何使用云调用? 阿杰阅读了云调用的文档后,瞬间理解了云调用的实现方法。结合实际产品需求,公司预期的小程序需要在用户输入预约信息提交后,向用户推送模板消息进行通知。 [图片] 就微信小程序推送模板消息,传统实现路径:用户预约成功 - 检查 access_token 是否在有效期内 - 获取 access_token - 调用 templateMessage.send - 推送模板消息。 而云调用实现路径:用户预约成功 - 调用templateMessage.send - 推送模板消息,完全无需关心 access_token 的获取、保管、失效等问题。 结合 wx-server-sdk 提供的 getWXContext 方法获取登录用户的 openid ,调用 templateMessage.send 这个方法,传入接收者 openid 、模板消息内容、模板消息id等参数,即可完成模板消息的下发。发送模板消息的核心代码如下: [图片] 将写好的云函数部署至云端,当用户预约成功后就能收到模板消息通知了。至此大功告成,自测无误提审! [图片] 阿杰巧借云开发的云调用能力,免除了 access_token 获取、校验、保管等相关处理机制的设计,在不到一下午的时间完成了整个项目。最后也如期赶到了机场,阿杰将经过转述给了在机场没等多久的女友,看着女友膜拜的眼神,阿杰拉着女友的手开心地走向了登机口... (故事纯属虚构,如有雷同... 那就是雷同) 巧用云开发,不加班不是梦 云调用的上线将进一步降低微信小程序的开发门槛,提升开发效率。免 access_token 调用 API ,距离早点下班又进了一步![图片] 除了云调用外,云开发同时还免费提供云函数、云数据库、云存储等Serverless(无服务器计算)能力,助力微信小程序开发者! [图片] (微信开发者工具内置的云开发入口) 目前 微信开发者工具版本 >= 1.02.1903251 且 云函数 wx-server-sdk >= 0.4.0 的开发者可以直接使用云调用能力,具体使用方法详见《微信小程序开发文档》,直通车:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/openapi.html
2019-04-08