- 小程序字体重复加载的优化方案
最近有一个需求,需求方一定要求加载自有的字体,一共5个包,加起来要60M,而且因为有后台上传的文章,还不能用字体精简工具压缩字体。 在调用官方的wx.loadfontface的时候,开始看着一切正常,但真机调试的时候,发现每个页面都在重新下载字体。 先不说每个页面抖动的问题,就是流量也扛不住他这么造啊。 社区里面看到有很多人在反馈这个bug,从20年到23年都有 https://developers.weixin.qq.com/community/develop/doc/0006e6ad5b49b02e3bc98398d51000?highLine=loadfontface%2520%25E7%25BC%2593%25E5%25AD%2598 https://developers.weixin.qq.com/community/develop/doc/00084c852f40280d6b2f6461550000?highLine=loadfontface%2520%25E9%2587%258D%25E5%25A4%258D 本来给客户反馈说微信bug,他们也勉强接受,但是上线10天,跑了1000多rmb的流量钱,实在是顶不住,压力又给到开发这边。。。 后面在掘金看到一篇文章(https://juejin.cn/post/7103928278225780743?searchId=2023081018280858E8AD21B4FDD711555A) 本地路径不行,那把本地文件转成base64曲线救国确实是一条思路。 原文是behavior做的,而且是用云开发,但我们的业务场景是需要全局生效,每个页面都写一个behavior也太累了。 于是改造成以下模式。 先创建一个loadFont.js,代码如下 注释写的比较详细了,简述思路: 先用wx.getFileSystemManager().access()看本地有没有,有就直接load,没有就先下载 wx.downloadFile把字体下到本地,再从本地wx.getFileSystemManager().readFile()取出文件并转Base64 最后source: `url("data:font/woff2;charset=utf-8;base64,${res.data}")`,搞定! // 建议循环调用方法,而不是这个方法内循环下载 // 下载字体文件,注意要把字体域名加到后台downloadFile白名单中 function _downloadFont(fontUrl, filePath, fontFamily) { wx.downloadFile({ url: fontUrl, success: res => { wx.getFileSystemManager().saveFile({ // 下载成功后保存到本地 tempFilePath: res.tempFilePath, filePath, success: res => { // 加载字体 _loadFontFace(fontFamily, res.savedFilePath) } }) } }) } // 加载文件字体转 base64,load function _loadFontFace(fontFamily, filePath) { // 读文件 wx.getFileSystemManager().readFile({ filePath, // 本地文件地址 encoding: 'base64', success: res => { wx.loadFontFace({ global: true, // 是否全局生效 scopes: ['webview', 'native'], //native可能有点问题,超哥生个海报试一下 family: fontFamily, // 字体名称 source: `url("data:font/woff2;charset=utf-8;base64,${res.data}")`, success(res) { console.log(fontFamily + '加载成功:' + res.status) }, fail: function (res) { console.log(fontFamily + '加载失败' + res) }, }) } }) } // fontUrl: 字体地址 // filename: 存储文件路径 // fontFamily: css 中字体的 family function loadCloudFontFace(fontUrl, filename, fontFamily) { const filePath = `${wx.env.USER_DATA_PATH}/${filename}` wx.getFileSystemManager().access({ path: filePath, success: () => { _loadFontFace(fontFamily, filePath) console.log('从本地加载了字体'); }, fail: () => { _downloadFont(fontUrl, filePath, fontFamily) console.log('从外部加载了字体', fontUrl); } }) } module.exports = { loadCloudFontFace } 由于业务本身的需求,是全局所有字体都用,所以,直接在app.js中(实际上我们是抽了一个文件单独维护的,这里就简化吧) const loadFont = require('./loadFont.js'); loadFont.loadCloudFontFace('https://xxx.com/wxapp/fonts/STHeiti.woff2', 'STHeiti.woff2', 'STHeiti') 并且,在app.wxss,让font-family全局给一个默认字体(事实上还有英文字体,这里简化了) page { font-family: 'STHeiti'; } 当然也支持每个页面独立配置不同字体,方法大同小异。 真机测试,终于只加载一次网络字体了。 其他注意的事项: 由于第一次仍然要加载字体,会产生抖动,我们的做法是,尽量不在首页用文字,大量用图片替代,同时在后台加载字体,供其他页面使用。对于个别页面需要加载字体时,可以出个loading框(虽然体验依然不好,但我尽力了)直接load网络字体,会存在跨域的问题,有的时候,这个问题确实很难解决(字体一般都用cdn、对象存储一类的,如果有权限操作配置很简单,但很多情况下开发人员确实不好增加跨域配置)。采用这个方案,由于是本地,就不存在跨域问题了,一举多得。 最后还是要吐槽下,这个需求,并不小众。最早从20年就有bug反馈,官方也有回复,但到23年了这个bug还存在,也着实无奈。
2023-08-10 - wx.getSavedFileList获取本地临时文件或本地缓存文件为什么为空?
[图片] [图片]
2019-08-23 - getPhoneNumber|agreePrivacyAuthorization无效?
从基础库 2.33.1 版本起,该组件支持与手机号快速验证组件、手机号实时验证组件耦合使用,调用方式为 <button open-type="getPhoneNumber|agreePrivacyAuthorization"> 这个没用啊 单个agreePrivacyAuthorization倒是可以 [图片] [图片]
2023-08-16 - 共享云环境时代来了,解决fileID带来不兼容问题。
云开发收费了,不管你怎么选择,只要你还继续使用云开发,共享云环境的课题就不可避免。 我们知道,共享云环境下,fileID是无法使用的,怎么兼容,一个最简的方法如下: <wxs module="wxs"> module.exports = { getUrl: function (link) { if (link) { } else return '' if (link.substring(0, 5) == 'cloud') { } else return link var arr = link.split('/') arr[0] = 'https:' arr[2] = arr[2].split('.')[1] + '.tcb.qcloud.la' return arr.join('/') } } </wxs> <image src="{{wxs.getUrl(link)}}"></image> 可见:只要将原项目所有的fileID换成wxs.getUrl(link) 其他代码可以一分不动,也不需要用到wx.cloud.getTempFileURL 可以将wxs.getUrl放在lib.wxs里,任何wxml引用即可。
2022-10-28 - 服务器迁移后,DNS更新了一下,现在小程序无法访问,这是怎么回事?
因为服务器到期,所以迁移了一下服务器,然后DNS也做了相对更新,同时相应的接口(https)的,都可以正常访问。结果 就手机上的小程序无法访问。在小程序菜单里选择用电脑打开,可以访问。 手机端报的是600001, code:-101,net:err_connection_failed。 但mac端的小程序却又可以访问。。好纠结
2022-10-21 - request:fail net::ERR_NAME_NOT_RESOLVED
同一请求同一个人,请求地址参数都相同,一会可以正常返回,一会就报错request:fail net::ERR_NAME_NOT_RESOLVED。 有人遇到过这种情况吗?[图片]
2021-10-18 - wx.request经常报request:fail-118:net::ERR_CONNECTION?
request:fail -118:net::ERR_CONNECTION_TIMED_OUT {\"errno\":5,\"errMsg\":\"request:fail timeout\"} {\"errno\":600001,\"errMsg\":\"request:fail errcode:-105 cronet_error_code:-105 error_msg:net::ERR_NAME_NOT_RESOLVED\"} {\"errno\":600003,\"errMsg\":\"request:fail interrupted\"}
2022-09-20 - request请求接口报错,请问是什么原因导致?是否有好的解决办法?
我用实时日志打印,用户一直在登录界面报错,进不去,网络正常,就是一直报错导致体验感很差,有没有好的解决办法呢?已经做了超时限制。 报错太多了,我只能把重复的全部罗列出来。 {"errMsg":"request:fail -118:net::ERR_CONNECTION_TIMED_OUT"} {"errMsg":"request:fail -103:net::ERR_CONNECTION_ABORTED"} {"errMsg":"request:fail -105:net::ERR_NAME_NOT_RESOLVED"} {"errno":600001,"errMsg":"request:fail -103:net::ERR_CONNECTION_ABORTED"} {"errno":600001,"errMsg":"request:fail errcode:-7 cronet_error_code:-7 error_msg:net::ERR_TIMED_OUT"} {"errno":600003,"errMsg":"request:fail interrupted"} {"errno":5,"errMsg":"request:fail timeout"} [图片] [图片]
2022-05-07 - 微信小程序云开发,对象数组条件查询结果没有筛选,全量返回了?
第一种写法: const db = wx.cloud.database() const _ = db.command db.collection('vegetable_list').where({ list:{ kind:'根茎类' } }).get({ success: res => { console.log("res:", res.data) }, fail: err => { console.log("error:", err) } }) 第二种写法: db.collection('vegetable_list').where({ list:_.elemMatch({ kind:'根茎类' }) }).get({ }) 都是全量返回,筛选条件没起作用!求官方同学尽快帮忙解决一下
2020-04-30 - 云开发where模糊查询怎么使用普通正则?
官方说一般情况下用这个原生的正则去匹配,可是具体要怎么用呢?比如我数据库user表有两条数据,一条{name:''刘德华''},一条是{name:"张学友}我现在想通过一个“刘”字来找到其中一条数据,要怎么写呢?求大神告知![图片]
2020-11-14 - sendSocketMessage: fail taskID not exist
[代码]//创建连接[代码] [代码]if[代码] [代码](![代码][代码]this[代码][代码]._socketTask || [代码][代码]this[代码][代码]._socketTask.readyState!=1) {[代码] [代码] this[代码][代码]._socketTask = wx.connectSocket({[代码][代码] url: option.url,[代码][代码] header: {[代码][代码] ...option.header,[代码][代码] 'content-type'[代码][代码]: [代码][代码]'application/json'[代码][代码] },[代码][代码] method: [代码][代码]'GET'[代码][代码],[代码][代码] fail: [代码][代码]function[代码][代码](res){[代码][代码] self._isLogin = [代码][代码]false[代码][代码] self.reConnect()[代码][代码] }[代码][代码] })[代码] [代码]}[代码][代码]sendHeartBreakMsg() {[代码][代码] [代码][代码]let self = [代码][代码]this[代码][代码] [代码][代码]try[代码][代码]{[代码] [代码] //wx.sendSocketMessage ({})使用sendSocketMessage 同样会出现[代码] [代码] [代码][代码]self._socketTask.send({[代码][代码] [代码][代码]data: [代码][代码]'HB_'[代码][代码]+wx.getStorageSync([代码][代码]'loginInfo'[代码][代码]).token,[代码][代码] [代码][代码]success: (e) => {[代码][代码] [代码][代码]// console.log('HB')[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: (res) => {[代码][代码] [代码][代码]console.log(res, self._socketTask, self._options, self._socketTask.readyState)[代码][代码] [代码][代码]if[代码] [代码](self._socketTask && self._socketTask.readyState != 1) {[代码][代码] [代码][代码]self.reConnect()[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码][代码](!self._socketTask) {[代码][代码] [代码][代码]self.createConnection()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码]catch[代码][代码](e){[代码][代码] [代码][代码]console.log(e)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码]错误日志已上传2019-05-21 正常connectSocket创建连接,测试使用两种方法发送message:sendSocketMessage / socketTask.send 发送。 一切在3-5分钟内是好的。切到后台,回到微信连天页面,再切回小程序,消息还是正常发送。操作3-5分钟后,发送消息就会报 sendSocketMessage: fail 。taskID not exist。此时websocket是连通的,readystate是open状态。我就想问一下这个报错是什么意思?导致sendSocketMessage一直报错。只有关闭微信重开才可以解决。kill吊销程序重启才能解决。 手机:iphoneXr IOS 12.2 微信版本: 7.0.4 基础库版本:2.7.0 [图片] [图片]
2019-05-21 - SocketTask.onError(function callback)错误信息的具体含义?
微信版本升级到7.0.8,音视频通话建立websocket连接后,监听SocketTask.onError返回:errMsg:“Invalid HTTP status“,这个错误信息是什么意思啊 出现问题的手机信息:微信版本:2.0.8,小程序库:2.8.3,iOS:12.4 安卓没有该问题和ios7.0.8以下的没有问题
2019-10-16 - 小程序页面所有点击事件失效
用的wepy框架,进入页面20次会有1次卡死的情况 卡死:这里所说的卡死指的是页面可以上下滑动,轮播可以左右滚动,这个页面的10几个不同组件的点击事件都失效(所有的点击事件都失效,只有真机模式下会出现这种情况,线上已经发布的小程序也会有这种情况),包括页面只留下一个点击事件也会偶现无法触发事件的情况,按钮有点击效果就是没有触发点击事件,官方底部导航可以切换。[图片]
2019-07-09 - 请教lookup pipeline如何查询二级数组?
表一: "_id":"2a0398605f1114*****d69a167ebf9ed" "alist":[[100,101],[200,201],[300,301]] 表二: "_id":"xxxxxxxxx" "name":"A" "aAid":100 ------------ "_id":"xxxxxxxxx" "name":"B" "aAid":101 -------------- "_id":"xxxxxxxxx" "name":"C" "aAid":200 依次 现在想查找出这样的结果 alist:[ { {aAid:100,name:"A"}, {aAid:101,name:"B"}, }{ {aAid:200,name:"C"}, {aAid:201,name:"C"}, } ... ] alist如果是一级数组,我知道直接用.match(_.expr($.in(['$aAid', '$$alist'])))这种方式就可以了,但是二级数组我就抓瞎了.
2021-09-22 - 如何彻底解决小程序滚动穿透问题
背景 俗话说,产品有三宝:弹窗、浮层加引导,足以见弹窗在产品同学心目中的地位。对任意一个刚入门的前端同学来说,实现一个模态框基本都可以达到信手拈来的地步,但是,当模态框里边的内容滚动起来以后,就会出现各种各样的让人摸不着头脑的问题,其中,最出名的想必就是滚动穿透。 什么是滚动穿透? 滚动穿透的定义:指我们滑动顶层的弹窗,但效果上却滑动了底层的内容。 具体解决方案分析如下: 改变顶层:从穿透的思路考虑,如果顶层不会穿透过去,那么问题就解决了,所以我们尝试给蒙层加catchtouchmove,但是发现部分场景无效果,那么就不再赘述了。 改变底层:既然是顶层影响了底层,要是底层不会滚动,那就没这个问题了。 如何改变底层解决该问题呢? 不成熟方案: 底部页面最外层view设置position: fixed;页面不可滚动,但是这个时候会导致页面回到顶部。 滚动时监听滚动距离,弹窗时记录滚动位置,关闭弹窗后使用wx.pageScrollTo回滚到记录的位置。 成熟方案 使用page-meta组件,通过该组件我们可以操作Page的style样式,类似于h5里body设置overflow: hidden; 控制页面不可滚动。文档地址:https://developers.weixin.qq.com/miniprogram/dev/component/page-meta.html 使用wx.setPageStyle设置overflow: hidden, 也可以实现给Page组件设置样式。) page-meta组件: 通过该组件我们可以直接操作[代码]Page[代码]组件 ,我们给它的wxss样式overflow动态设置[代码]hidden[代码]or[代码]visible[代码]or[代码]auto[代码] 就可以控制整个页面是否可以滚动。 [图片] wx.setPageStyle方法: 调用这个api,动态设置它为hidden/auto,用于控制页面是否可滚动,主要用于页面组件内使用,比如封装好的弹窗组件,就不用单独写page-meta组件了。。 [代码]wx.setPageStyle({ style: { overflow: 'hidden' // ‘auto’ } }) [代码] 老规矩,结尾放代码片段: https://developers.weixin.qq.com/s/U6ItgQmP7upQ 拓展 支付宝小程序虽然存在page-meta组件,但是由于内核为69版本,给page设置overflow: hidden 也无法控制底部元素不可滚动,目前已联系支付宝的底层开发同学提供API控制页面disableScroll,目前正在封装Appx,近期开放。
08-06 - 小程序滚动穿透,弹层滚动导致背景滚动的问题标准解决方案
先上代码 https://developers.weixin.qq.com/s/uI82fqmD7mmY 需求场景描述页面有弹层,弹层有滚动要求弹层滚动时背景不滚动解决办法解决标准很多方法都是要改原来的页面(也就是触发弹层的背景页面),给原来的页面加样式等操作,不符合本次解决的标准本次解决的标准希望是在弹层中处理,把弹层当做一个业务无关的组件,不做关联依赖处理小程序中的情景模拟器(符合预期)在弹层的滚动区域只会触发弹层的滚动,不会触发背景页面的滚动Android机(华为Mate30 5G, Android 10)在弹层区域能滚动的时候只触发弹层的滚动在弹层区域不能滚动的时候(比如滚到头或者滚到尾的时候)会触发背景页面的滚动iOS机(iPhone X ,iOS 13.4.1)在弹层滚动条出现的时候正常,弹层滚动条不出现的时候滚动背景不但有背景的本身的滚动,还有弹性滚动小程序中解决办法在modal 最外层加catchtouchmove="emptyFunction"(别给"true"这样会报警告的方法)这样在模拟器上touchpad还可以滚动,真机和模拟器上都没法拖动仅适合弹层没有滚动条的情况在modal 最外层加catchtouchmove="emptyFunction",弹层滚动区域用scroll-view,并设置scroll-y为true只要有弹层,背景就不会滚动弹层内部的滚动会正常的scroll-view滚动
2020-12-07 - 教你解决showLoading 和 showToast显示异常的问题
问题描述 当wx.showLoading 和 wx.showToast 混合使用时,showLoading和showToast会相互覆盖对方,调用hideLoading时也会将toast内容进行隐藏。 触发场景 当我们给一个网络请求增加Loading态时,如果同时存在多个请求(A和B),如果A请求失败需要将错误信息以Toast形式展示,B请求完成后又调用了wx.hideLoading来结束Loading态,此时Toast也会立即消失,不符合展示一段时间后再隐藏的预期。 解决思路 这个问题的出现,其实是因为小程序将Toast和Loading放到同一层渲染引起的,而且缺乏一个优先级判断,也没有提供Toast、Loading是否正在显示的接口供业务侧判断。所以实现的方案是我们自己实现这套逻辑,可以使用Object.defineProperty方法重新定义原生API,业务使用方式不需要任何修改。 代码参考 [代码]// 注意此代码应该在调用原生api之前执行 let isShowLoading = false; let isShowToast = false; const { showLoading, hideLoading, showToast, hideToast } = wx; Object.defineProperty(wx, 'showLoading', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowToast) { // Toast优先级更高 return; } isShowLoading = true; console.log('--------showLoading--------') return showLoading.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'hideLoading', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowToast) { // Toast优先级更高 return; } isShowLoading = false; console.log('--------hideLoading--------') return hideLoading.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'showToast', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowLoading) { // Toast优先级更高 wx.hideLoading(); } isShowToast = true; console.error('--------showToast--------') return showToast.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'hideToast', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { isShowToast = false; console.error('--------hideToast--------') return hideToast.apply(this, param); // 原样移交函数参数和this } }); [代码] 调整后展示逻辑为: 优先级:Toast>Loading,如果Toast正在显示,调用showLoading、hideLoading将无效 调用showToast时,如果Loading正在显示,则先调用 wx.hideLoading 隐藏Loading
2019-10-30 - 云开发怎么批量删除一个字段?
[图片] 如图,我想把集合里所有的document的openid全部剔除掉,请大家指点一下。
2020-07-13 - 最佳实践丨云数据库实现联表+聚合查询
聚合是云开发 CloudBase 数据库中非常重要的一种数据批处理操作方式。聚合操作可以将数据分组(或者不分组,即只有一组/每个记录都是一组),然后对每组数据执行多种批处理操作,最后返回结果。 有了聚合能力,可以方便的解决很多没有聚合能力时无法实现或只能低效实现的场景,包括分组查询、只取某些字段的统计值或变换值返回、流水线式分阶段批处理、获取唯一值(去重)等。 本文就以一个简单的实例解释如何在云数据库中,实现十分常用的联表+聚合查询操作。 场景说明假设数据库内存在两个集合:[代码]class[代码] 与 [代码]student[代码],存在以下数据: class(班级信息): [图片] student(学生信息): [图片] 现在需要查询徐老师所带的班级里面所有学生的平均成绩。 代码示例1、lookup 联表查询首先我们需要把 student 内的所有数据,按照 class_id 进行分组,这里我们使用云数据库的 lookup 操作符: lookup({ from: "student", //要关联的表student localField: "id", //class表中的关联字段 foreignField: "class_id", //student表中关联字段 as: "stu" //定义输出数组的别名 }).end(); 这个语句会查出来下面的结果,会查出班级的信息以及该班级所对应的所有学生的信息: {"list": [{ "id":1, "teacher":"王老师", "cname":"一班", "stu":[ { "sname":"宁一", "class_id":1, "score":90 } ] }, { "id":2, "teacher":"徐老师", "cname":"二班", "stu":[ { "class_id":2, "sname":"张二", "score":100 }, { "class_id":2, "sname":"李二", "score":80 } ] }] } 但是我们只需要徐老师所在班级学生的数据,所以需要进一步过滤。 2、match 条件匹配现在就只是返回徐老师所在班级的学生数据了,学生数据在 stu 对应的数组里面: .lookup({ from: 'student', localField: 'id', foreignField: 'class_id', as: 'stu' }) .match({ teacher:"徐老师" }) .end() 现在就只是返回徐老师所在班级的学生数据了,学生数据在 stu 对应的数组里面: { "list": [ { "_id": "5e847ab25eb9428600a512352fa6c7c4", "id": 2, "teacher": "徐老师", "cname": "二班", //学生数据 "stu": [ { "_id": "37e26adb5eb945a70084351e57f6d717", "class_id": 2, "sname": "张二", "score": 100 }, { "_id": "5e847ab25eb945cf00a5884204297ed8", "class_id": 2, "sname": "李二", "score": 80 } ] } ] } 接下来我们继续优化代码,直接返回学生的平均分数。 3、直接返回学生成绩平均值如果想要在被连接的表格中(本课程中的 student)做聚合操作,就用 pipeline 方法: .lookup({ from: 'student', pipeline: $.pipeline() .group({ _id: null, score: $.avg('$score') }) .done(), as: 'stu' }) .match({ teacher:"徐老师" }) .end() 现在输出的数据是这样的: { "list": [ { "_id": "5e847ab25eb9428600a512352fa6c7c4", "id": 2, "teacher": "徐老师", "cname": "二班", "stu": [{ "_id": null, "score": 90 }] } ] } 但是现在输出的数据有点复杂,如果只想显示 teacher 和 score 这两个值,我们再进行下面的操作。 4. 只显示 teacher 和 score 这两个值我们使用 replaceRoot、mergeObjects 和 project 进行最后的处理: .lookup({ from: 'student', pipeline: $.pipeline() .group({ _id: null, score: $.avg('$score') }) .done(), as: 'stu' }) .match({ teacher:"徐老师" }) .replaceRoot({ newRoot: $.mergeObjects([$.arrayElemAt(['$stu', 0]), '$$ROOT']) }) .project({ _id:0, teacher:1, score:1 }) .end() 现在输出的数据是这样的: { "list": [{ "score": 90, "teacher": "徐老师" }] } 相关文档:云开发聚合搜索:https://docs.cloudbase.net/database/aggregate.html 产品介绍云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2021-04-08 - 云开发中字段为对象数组,如何用变量加字符串表示字段数组的更新条件?
db.collection('todos').doc('test').update({ data: { 'numbers.1': 30 }, }) 上面的'numbers.1'中‘1’,我想用‘numbers' 字符串加上小程序端传过来的变量来表示,可以实现吗
2021-01-11 - 领域建模在有赞客户领域的实践
一、What's DDD?从定义入手:DDD全称Domain-Driven Design,即领域驱动设计,由Eric Evans于2003年提出。那既然是一种设计方法,ddd的作用对象是什么呢?这个问题光从定义是看不出来的,我们再往下看看。 换一个更高的视角:我们在谈论到架构设计的时候,可以简化为三个层面:系统架构、技术架构和业务架构,这三者从三个不同的视角来描述我们的系统。系统架构关注系统的架构分层,技术架构决定使用的技术栈和框架。而作为一个偏向业务开发的工程师,我们日常施展拳脚的平台离不开业务架构这一层面,它根据业务需求设计相应的业务模块及其关系,决定了业务系统是否有足够的灵活性来面对业务的发展。领域驱动设计 就是用来做业务架构设计的一种思想方法。 [图片] 二、为什么要使用DDD当前业务系统的现状是?把笔者接手过的应用比喻成一个大泥球是一点也不为过,各种各样的业务功能杂糅在一起,不论是从应用名还是目录都无法确认该应用的功能边界。这种大泥球形态的杂糅不仅造成业务边界的不清晰,维护困难,也加大了系统崩溃的风险,你永远不知道哪个业务方会突然写出一个bug从而影响应用上的所有功能。 分而治之的思想分治的思想在降低复杂度的问题上从来都没有过时。比起所有功能都糅合在一起的大泥球,DDD通过在其战略设计层面对限界上下文的划分来做到这一点。各个业务领域内关注自身能力的内聚,明确分工,不耦合其他业务领域的能力,这不仅符合 高内聚-低耦合 的架构思路,更是与当下 微服务 拆分的思想不谋而合。 编程是对现实世界的抽象这句话深得我心,尤其是在面对复杂业务设计的时候,过于流程化的视角容易使我们的抽象失去重心。传统分层开发模式 action/service/dao 所导致的贫血模式就存在这样的问题,service层只关注行为,而do/dto等对象只关注数据,这在简单业务阶段其实是很敏捷的一种开发思路。然而随著业务趋于复杂,你可能会发现service层的行为逻辑很难再直观地与业务功能点对应起来。从一个功能接口进去,直面的就是层层嵌套的判断循环逻辑与数据对象,而数据对象又不一定能够直接映射为业务核心对象,这无疑是增加了业务理解的难度的。 相反地,DDD通过在其战术设计里提倡的富血模型(实体里既有数据又有行为),使得我们的代码抽象能够更加贴近业务与现实,毕竟在现实场景里,数据模型与行为总是不可分割的。 三、我们对DDD的一点实践出于本身所负责的业务关系,接下来我们会以客户领域来作为DDD设计实践的切入点。这个设计又分为两部分,战略设计 & 战术设计。 3.1 战略设计3.1.1 划分限界上下文战略设计的概念其实通过各种DDD书籍博客都能找到相关的定义,我们更想传达的是一种战略设计的思考方式,其最终目的是通过建模,划分"清楚"这个业务领域的边界,以指导开发。 对业务领域的设计,我们的做法是先梳理业务流程。客户领域这个定义本身很宽泛,那我们要怎么来描述它呢?通常人在没有经过专业的模块化思维训练的情况下,时间是我们描述一个业务的基本维度。而在美业的场景下,商家与顾客作为业务的参与者,在同一时间下的行为表现又各有不同,因此他们被选取为描述流程的另一个维度,即业务角色。除此之外,不论是在流程梳理还是后续的模块划分中,我们始终要聚焦的一个点就是业务领域的业务目标,对于当前美业客户领域来说,业务目标就是帮助商家提升用户的ARPU值。而ARPU值是通过顾客的消费行为来累计的。综合上述的三点思考,我们整理了以下流程: [图片] 整体流程以客户消费的阶段作为划分,从商家和顾客的视角描述了与客户领域相关的业务行为。其实从严格意义上来说,居中的消费中阶段并不是客户领域要关注的东西,只保留消费前和消费后的两个时段也能覆盖客户领域的业务内容。这里我们为了时序完整性而保留了这个阶段,但只对其作笼统的描述。 业务流程图在这里起到了一个帮我们梳理业务行为的作用,但要进一步完成战略建模设计,接下来还要对这些行为进行合理划分。实践中我们还是围绕着业务目标来做,流程图帮我们筛选出了有助于提升客户ARPU值的行为,那这些行为分别是怎么对我们的业务目标产生影响的呢?按照这个思路,我们将客户领域划分成两个子域:客户成长 和 客户运营客户。 [图片] 成长领域 :关注点落在客户个人的生命周期。从客户注册到产生第一笔消费并通过消费行为获得成长值、积分等一系列跟生命周期相关的为提升客户ARPU值而做的业务,都纳入客户成长领域 客户运营领域 :则更加侧重从群体视角和活动的维度去把控对应要采取的运营动作(例如对客户进行分群营销、做一些客户跟进、做RFM客群筛选等) 很多的文章在做战略建模的时候都会提到问题空间和解空间的说法,我们的实践中好像都没有体现这两个部分。其实我们回头看看业务流程中的那些业务行为,不就是一个个问题的解法嘛。而在梳理过程中我们聚焦的业务目标,不也正是问题空间拆分的源头。两者本质上的思想都是为了解决一定的业务问题而梳理出来的一系列业务行为和解法。 以客户成长领域为例 划分完大方向后,我们需要更进一步,对各自领域的内部进行更细粒度的划分,识别子领域。在对客户成长领域的定义中,其实我们已经对它的职责划分得很清晰了: [图片] 来看下客户领域内部的划分:首先,客户子域作为核心域,自然包含了客户领域最核心的客户对象,客户生命周期的相关行为都归属于它。除此之外还涵盖了客户的个人档案信息、行为数据等核心的内容。其次,等级会员和付费会员的两套体系服务于客户身份转变的能力,自然抽象成两个支撑子域,涵盖了相应的付费规则、入会门槛等功能。标签子域则将客户打标筛选的能力收入囊中。最后,在积分子域的划归上其实我们经过了比较激烈的思想斗争。首先是潜意识里,当我们听到积分的概念很容易都会跟客户联系起来,但是从业务性质的定义上又有一个声音告诉我们积分其实是一种用户资产,似乎应该跟卡、券一道列入资产领域。 两种声音互相争执不下的时候,我们最终还是回归到业务目标,积分是一种为了提高客户ARPU值,提高复购率而诞生的产物,这样来看将其纳入客户领域就比较合理了。从上述子域识别的过程来看,我们会发现子领域的拆分基本都是通过抽象出与客户核心不直接相关的内容来实现的,比如入会门槛的计算、打标规则的计算等等,但是他们最终又要回归到服务于核心域的目的。 3.2.2 上下文映射关系一份清晰的领域上下文映射关系图,能够帮助我们明确各个业务之间的关系,在进行业务分工的时候也能够有所依据。在ddd中各个上下文之间有多种关系,包括合作、防腐、遵奉等等,我们基于简单认知对客户领域的领域关系做了如下梳理(为了图例简单,这里并不包含所有的和外部领域的交互,只是列出几个作为代表): [图片] 左上角的五个领域都是归属于客户领域,互相之间都是共生共存的关系(PartnerShip,简称PS)。而右下角的几个上下文都是外部领域,需要通过防腐层(Anticorruption Layer,ACL)来转换交互,以隔离业务。箭头的方向标明了各个子域之间的上下游业务关系。 3.2 战术设计3.2.1 客户核心域上述第一part的战略设计帮助我们划分清楚了业务边界,梳理清楚了不同领域之间的交互关系。但是仅依靠战略设计并不能有效落地开发。为此,我们需要更加详细的战术设计,来指导代码层面的开发。战术设计中涉及的一些概念,比如实体、值对象、领域服务等的概念在此就不作赘述。我们下面看看客户核心域内,详细的战术设计: [图片] 核心域里面我们以客户实体为中心,同时关联了客户档案和行为数据等值对象,这些值对象不具有唯一性标识,只关心本身属性值。客户实体本身具有一系列的行为,是客户核心域的核心行为,如客户身份变更,客户档案变更等。同时我们定义了一个聚合,这个客户聚合以客户实体为聚合根,外加客户档案和行为数据两个值对象,代表着像客户行为数据等的值对象访问都需要通过客户实体来进行,他们的生命周期也是同步的。 在定义客户实体的行为的时候,我们也遇到了一些难以一下想清楚的问题。由于客户这个领域的特殊性,我们很容易把客户跟自然人的概念联想在一起或者直接等同,按照这个思路,我们就会发现客户有很多的行为,比如客户执行了下单,客户进行了预约,客户进行划卡操作等等,所有行为都向人看齐,最终形成所有业务都耦合到自然人身上的大泥球。自然也失去了领域划分的意义。那么,我们要怎么分辨,哪些才是客户本身真正的行为呢?其实截止目前我们也没有一个很系统的说法,但是我自己思考的实践是,分辨一个行为是否归属于客户实体,可以把这个行为涉及到的领域先罗列出来,再把他们放到一起衡量一下。举个例子:客户下单行为,这里字面上看会涉及到客户和交易两个关键词,但仔细斟酌后你会发现,下单过程中交易系统是主要工作的地方,而客户顶多充当一个订单归属人的角色,因此我们认为下单这个动作不归属于客户领域,而是交易领域的行为。 做战术设计时候遇到的另一个问题,就是容易从现有的数据库表设计去倒推我们的业务设计。美业现有的数据库表结构里,成长值是存储在客户表里的,在行成这种思维定式之后,我们一度在战术设计的时候都把成长值归为客户实体本身的属性之一。这样的想法导致的后果是,在成长值相关的开发的时候我们必须强依赖于客户实体。等级会员作为客户领域的一部分业务,却在每次开发的时候都需要牵扯到核心域的改动,这是不能容忍的。而如果把成长值独立抽象出来,就可以在等级会员的领域内完成自己的能力闭环,这才是符合我们预期的。 3.2.2 付费会员子域看完核心域的设计,我们再用付费会员子域来举例,其余的支撑域都是类似的设计方法: [图片] 付费会员子域作为客户核心域的支撑域,囊括了对付费会员付费时长的计算逻辑和规则配置数据,为客户核心域的客户身份转变等提供逻辑支撑。我们不难从分析中看到,作为客户支撑领域的几个子域都有类似的结构,通过配置规则实体和配置记录值对象来构成单独的子域,并为核心域某一个行为提供支撑。例如:打标签、客户身份转变、会员成长等...... 鉴于上述支撑子域类似的结构,我们一度也想把这几个子域统一成一个配置域(或叫规则域),但是后面发现规则域这样的领域过于笼统,因此最后还是按照业务将几个配置规则划分开,在战略设计的时候也能更好地反映业务内容。 3.3 DDD的工程实践详细的项目模块划分我们这里就不作讲解,主要的几个核心模块就是 domain/infrastructure/service/dependency 。 工程实践上我们基本参照CQRS的思路,以客户领域为例,我们会拆分出两个领域服务,以期做到普通查询和业务操作分离。 [图片] 下面我们也是以这两个维度来看看具体的一些实践代码: 3.3.1 单纯读操作本人日常业务开发上面各种情况的读操作占据了相当的百分比,在刚开始接触ddd的时候,秉承着要构造富血对象的理念,将所有的读操作都封装到实体对象里去,以至于在开发修改的过程中真切感受到什么叫冗余,明明只是一个简单查询,却要经过service -> entity -> repository -> mapper的4层铺垫,并且在实体操作的时候,代码与面向对象的设计理念有明显的违和。举个栗子,我们要去查询一个客户信息: CustomerEntity customer = CustomerFactory.buildCustomer("123"); CustomerEntity targetCustomer = customer.getById(); 这里的代码是先通过一个工厂去构造一个客户对象实体,然后再通过实体行为去获取一个对象。第一步明明都已经构造出实体了,居然还要自己去查自己,这里给人的感觉就很变扭。如果说单查尚且能忍受,那批量查询使用这种写法就更离谱了,看下面一段: CustomerEntity customer = CustomerFactory.buildCustomer(); List customerList = customer.batchGet() 操作的目的原本是批量查,结果要去强行构造一个实体来执行,我们都知道实体有唯一id,那么这个来执行批量查询行为的客户实体到底是哪一个呢?难道是随便一个?不管怎么样,如果把类似的批量查询行为归结到某一个实体上,是显然不合适的,也违背了实体行为抽象的初衷(让业务逻辑看起来更清晰易懂)。解决上述问题的方法其实也很简单,我们知道领域服务层是可以直接访问到仓储层的,通过service -> repository -> mapper这样的访问形式就省略了我们实体层对读操作违和的烦恼,而不需要通过实体再委托给仓储层进行读操作。这样看起来跟我们原先的 service -> manager -> mapper操作就无二致,也提高了开发效率。 3.3.2 复杂业务操作这里我们通过一个付费会员订购后履约的例子来给大家展示一下(以下代码参照原实现进行了阅读体验优化,仅供参考): [图片] 结合我们上面战略设计的时候对付费会员领域的能力划分,我们看到在领域服务里的操作逻辑已经很清晰,先直接通过仓储层获取到这个付费客户的实体和对应付费模板。调用客户实体的能力判断当前客户是否已经是付费会员,区分新老客的付费逻辑后,调用付费模板的能力计算出对应的付费时长,再将结果交由客户实体,进行客户身份转变的操作,每一步操作都符合我们面向对象的设计理念。最后跟权益领域的交互,我们通过封装的rightClient来进行,避免外部领域对象对客户领域的侵入。 到这里可能会有细心的读者有疑惑,前面做客户核心域的战略设计的时候,划分的有一个客户聚合,怎么在具体代码里都没有体现呢。这里就涉及到一个具体业务场景的问题了,聚合从理论设计上是包含了实体和值对象的,但是在付费后履约这个业务场景下,我们是没有需要去表达这个客户聚合的概念的,因此从开发成本和简洁程度上考虑,当时在这个场景下我们就没有引入聚合这一层概念。这个理念跟在读操作的时候我们绕过了实体直接走repository层是一样的,任何的架构设计理论在落地时,都需要结合工程的实际情况来灵活调整,而不是生搬硬套。 四、结语到这里我们对ddd实践的介绍就告一段落了,之所以文章中基本没有对ddd相关术语的介绍,是因为我觉得这篇文章的目标读者是那些已经对领域驱动设计有基本入门概念的小伙伴。我相信很多人接触学习ddd都是从基本术语概念开始的,但是在实战应用的时候却往往觉得不知道从何下手,因此希望自己的一点粗浅的经验总结能够给到大家一点思路。 另外,文中也提到:当你的业务还处在很简单的阶段的时候,传统的三层模式可能是你更好的选择。就算真的需要应用到领域设计的理念的时候,也需要紧靠高内聚低耦合的本质和本着提高开发效率,写出清晰易懂的代码为目的,结合自身理解来落地。 鉴于作者经验有限,对领域驱动设计的理解难免会有不足。其实从最初着手客户领域设计到这篇文章撰写完成,整个领域模型也经历了数次推倒重构。每过一段时间回过头来看就会发现之前设计中一些逻辑无法自洽的地方。并且这些问题有的是在设计中暴露的,有的是要到具体开发编码的时候才会发现。这也反映了理论设计与实践相结合的重要性,不加以实践探究,理论终归是无法真正消化理解的。 最后,也希望这篇拙文能起到抛砖引玉的作用,欢迎各位共同探讨进步。 感兴趣的商家,可点击 → 免费试用有赞店铺~ [图片]
2020-12-07 - 开源的基于云开发的激活码领取小程序v2
我之前写过基于云开发的激活码领取小程序, 开源的基于云开发的激活码领取小程序? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/0006c481e94530defc2a2e87d56813 由于该开源小程序用户已经超过1000个UV,所以我开通了流量主,在原先的版本上进行完善,具体内容如下 1、新增激励式视频广告, 本文的重点也是讲解如何接入激励式视频广告的 具体参考官方文档 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/ad/rewarded-video-ad.html 广告创建 1 [图片] 2 [图片] 3 [图片] 1 小程序截图如下 2 [图片] 3 [图片] 4 2020-06-08补充 因为流量主广告会根据用户的广告点击情况区别展示,也就是说,有时候你看激励式视频广告看多了,这种广告对你是不会展示的,所以这种情况要特别处理下。 具体拉取失败的原因可以参考官方文档 https://developers.weixin.qq.com/miniprogram/dev/api/ad/RewardedVideoAd.onError.html 所以当广告拉取失败时,要做对应的逻辑处理,否则在逻辑上是不完备的,这一点要特别注意。 [图片] 4 4 总结 当广告拉取失败时的逻辑处理是要格外考虑的。
2020-06-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 - 订阅信息恶心至极?没有服务器的我们也能直接发消息到微信(公众号)! [即抄即用,拎包入住]
更新时间(2020/12/2) 大家好,众所周知,今年左右新出的订阅消息对商家和用户的友好度都极低,少了一个直接发信息到微信的最重要的渠道。 [图片] 那么我们动动歪心思,直接发消息到公众号(其实这是不是wx的本意???)。 所有服务号都可以在功能->添加功能插件处看到申请模板消息功能的入口,但只有认证后的服务号才可以申请模板消息的使用权限并获得该权限。 为方便公众号用户方便、快捷地接入小程序服务,公众号用户可复用公众号资质创建小程序。当前每个账号的模板消息的日调用上限为10万次,单个模板没有特殊限制。当账号粉丝数超过10W/100W/1000W时,模板消息的日调用上限会相应提升。这还不美滋滋吗,对于小商家来说等于无限次了。 所以我们一开始最好就在公众号进行微信认证,复用公众号的资质来注册小程序(注意,可以复用5个(好像))。 (我付出了¥300的惨痛代价)。 -------------------------------------------------------------------------------- 以上搞定后,流程走起 重点中的重点:为您的函数申请公网固定ip 有了这个ip之后,没有服务器的我们(穷)就能绕过ip白名单,这不正是真正的云开发精神吗? [图片] 0.用你的小程序账号登陆腾讯云,并在里面新建一个云函数 [图片]>>[图片] 选择你要发消息到公众号的小程序 [图片] [图片] -----------------我是2020/10/23的拎包哥----------------------- 创建并选择角色 记得在 https://console.cloud.tencent.com/cam/role 创建角色 [图片] 勾选tcb,scf [图片] 在搜索框里搜scf,tcb后,有什么策略就勾选什么策略 [图片] [图片] 创建角色名 [图片] 在云函数启用刚刚新建的角色 [图片] ps. 记得做好这一步,不然各种报错 missing authorationo key // 报错:缺失授权键 you are not authorized to xxx // 你没有权限去xxx 1.打开 https://cloud.tencent.com/document/product/583/38198 ,申请白名单 [图片] 2.审核通过后,再跟着步骤走。 [图片] 最后你的云函数会得到一个公网固定ip [图片] 3.开始码云函数的代码 如下,记得把wx-server-sdk和request-promise的包都npm下来。 依赖包可以通过这里上传上来 [图片] 我的做法 把获取的accessToken储存在云开发的数据库里。这样就不用担心access token的生成次数超过限制了 'use strict'; const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const resInfo = db.collection('resInfo') const appid = '公众号的APPID'; // APPID const secret = '公众号的密钥'; // Secret const rp = require('request-promise') exports.main_handler = async (event, context, callback) => { var that = this var options = { url: 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + appid + '&secret=' + secret, json: true } return await rp(options) .then(async res1 => { return await resInfo.where({name:'outInfo'}).update({ data: { access_token: res1.access_token } }).then(res2 => { return res2 }) }).catch(err => { return err }) } 公众平台以access_token为接口调用凭据,来调用接口,所有接口的调用需要先获取access_token,access_token在2小时内有效,过期需要重新获取,但1天内获取次数有限,开发者需自行存储,详见获取接口调用凭据(access_token)文档。4.设置触发器(触发器偶尔会失灵,所以最好是59min触发一次)。 [图片] 权限设置 由于云函数的访问不存在openid,所以安全规则必须为任何人可读可写。 [图片] 5.有了可以稳定刷新的access_token后,根据需求挑选你的公众号模板消息,开始你的表演。 例如:做餐饮小程序的朋友都想用户下单后发送订单信息到商户。那么就需要 获取商户的openid 步骤 注:公众号的openid在小程序开发工具就可以查出来 5.1 获取公众号openid列表 https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN 注意我没有加上next_openid = NEXT_OPENID,是为了取出公众号的所有的openidopenid列表在 res.data.openid5.2 for循环openid列表,根据商户的微信信息(nickname,city等等)找出只属于商户的openid https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN --------------------------------------- var openidList = openid列表 for(var i in openidList){ wx.request({ url:'https://api.weixin.qq.com/cgi-bin/user/info?access_token='+ openList[i] + '&lang=zh_CN', success(){ console.log(....) } }) } 这里就不用unionid了,不用浪费时间在上面), 就可以使用只发给商户的模板消息了。 公众号对应文档链接 https://developers.weixin.qq.com/doc/offiaccount/User_Management/Getting_a_User_List.htmlhttps://developers.weixin.qq.com/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html#UinonId, -----------------------------------------效果图--------------------------------------- [图片] -------------------------------------------------------------------------------- 哎,差不多了,感觉是有点折腾,如果感觉还是不够直白的,可以指出来我继续补充 。 求点赞,你的评论就是对拎包哥最大的支持。 [图片][图片] ===================更新于2020/10/23======================
2020-12-02 - 一眼告诉你什么是订阅消息了,看完就懂订阅消息。
消息通知有两种: 一、A的动作后,发消息给A自己,这种容易解决,不多说明; 二、A动作后,发消息给B(比如管理员、店家、楼主),如何保证B收到消息?这种是本方案要解决的问题。 一张图片一眼告诉你什么是订阅消息,产品经理的设计UI居然让人一眼就知道订阅消息是什么玩意。 [图片] 用户 B (管理员、商家、组长、楼主)在知道订阅数不足后,打开小程序来续订阅数,否则没法收到订阅消息。 [图片] 补充一: 关于勾选按钮,请注意话述是:“总是保持以上选择,不再询问”,而不是:“总是同意接收订阅消息”,不要幻想就成了永久性订阅消息; 相当于你打电话订外卖,对店家说“老样子”,店家只会马上送一次外卖,而不是会以后每天自动给你送外卖了。 勾选和不勾选的区别是什么呢? 区别仅仅是:不勾选时,必须点击订阅10次,弹窗10次;勾选后,仍然必须点击订阅10次,但是不弹窗。无论如何“订阅”这个点击n次的动作少不了。 补充二: 一旦勾选后,就不可逆了,没有任何办法恢复或取消勾选了,除非你小程序MP后台换一次消息模板号(删除模板,重新添加一次)。 补充三: 关于如何保存订阅数。 保存在数据库中,笔者用的是云开发,数据库表user结构如下: { _id:'openid1', nickName:'老张', msg:{ "tempId1":5, "tempId2":7, } } 补充四: 关于如何获取订阅数。两种方式: 一、wx.requestSubscribeMessage的回调success里获取; 二、消息推送机制获取;https://developers.weixin.qq.com/miniprogram/dev/framework/server-ability/message-push.html
2022-09-21 - 云函数下载文件保存到临时文件再读取的时候报错,为什么?
[代码]const cloud = require([代码][代码]'wx-server-sdk'[代码][代码])[代码][代码]const request = require([代码][代码]'request'[代码][代码])[代码][代码]const fs = require([代码][代码]'fs'[代码][代码])[代码][代码]const path = require([代码][代码]'path'[代码][代码])[代码] [代码]return[代码] [代码]new[代码] [代码]Promise([代码][代码]function[代码] [代码](resolve, reject) {[代码][代码] [代码][代码]request(options, (error, response, body) => {[代码][代码] [代码][代码]let jsonBody = JSON.parse(body)[代码][代码] [代码][代码]downUrl = jsonBody.download_url[代码][代码] [代码][代码]let p = path.join(__dirname, [代码][代码]'/tmp/file.mp4'[代码][代码])[代码][代码] [代码][代码]request(downUrl).pipe(fs.createWriteStream(p)).on([代码][代码]'close'[代码][代码], () => {[代码][代码] [代码][代码]const fileStream = fs.createReadStream(p)[代码][代码] [代码][代码]resolve(cloud.uploadFile({[代码][代码] [代码][代码]cloudPath: [代码][代码]'file.mp4'[代码][代码],[代码][代码] [代码][代码]fileContent: fileStream,[代码][代码] [代码][代码]})[代码][代码] [代码][代码])[代码] [代码] [代码][代码]})[代码][代码] [代码][代码]})[代码][代码] [代码][代码]})[代码] ##报错日志 2019-08-10T03:38:05.684Z { Error: ENOENT: no such file or directory, open '/var/user/tmp/file.mp4' errno: -2, code: 'ENOENT', syscall: 'open', path: '/var/user/tmp/file.mp4' } END RequestId: 430c6bf0-bb20-11e9-9e13-5254007aa7a1 Report RequestId: 430c6bf0-bb20-11e9-9e13-5254007aa7a1 Duration:797ms Memory:256MB MaxMemoryUsed:3.015625MB
2019-08-10 - 试了一下小程序的websocket,提供个简单的代码吧
[代码]var[代码] [代码]util = require([代码][代码]'../../../utils/util.js'[代码][代码]);[代码] [代码]var[代码] [代码]socketOpen = [代码][代码]false[代码][代码];[代码] [代码]var[代码] [代码]frameBuffer_Data, session, SocketTask;[代码] [代码]var[代码] [代码]url = [代码][代码]'wss://wss......./Chat'[代码][代码];[代码] [代码]Page({[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 页面的初始数据[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]data: {[代码] [代码] [代码][代码]toView: [代码][代码]'green'[代码][代码],[代码] [代码] [代码][代码]windowH: [代码][代码]"1000"[代码][代码],[代码] [代码] [代码][代码]user_input_text: [代码][代码]''[代码][代码], [代码][代码]//用户输入文字[代码] [代码] [代码][代码]inputValue: [代码][代码]''[代码][代码],[代码] [代码] [代码][代码]returnValue: [代码][代码]''[代码][代码],[代码] [代码] [代码][代码]addImg: [代码][代码]false[代码][代码],[代码] [代码] [代码][代码]//格式示例数据,可为空[代码] [代码] [代码][代码]allContentList: [],[代码] [代码] [代码][代码]num: 0,[代码] [代码] [代码][代码]wo: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]ta: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]youImg: [代码][代码]""[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]scrollToTop() {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]this[代码][代码].setAction({[代码] [代码] [代码][代码]scrollTop: that.windowH[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]bindKeyInput: [代码][代码]function[代码][代码](e) {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]inputValue: e.detail.value[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]// 提交文字[代码] [代码] [代码][代码]submitTo: [代码][代码]function[代码][代码](e) {[代码] [代码] [代码][代码]let that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]content = that.data.inputValue;[代码] [代码] [代码][代码]var[代码] [代码]data = {[代码] [代码] [代码][代码]// body: that.data.inputValue,[代码] [代码] [代码][代码]"name"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"content"[代码][代码]: content,[代码] [代码] [代码][代码]"type"[代码][代码]: 5,[代码] [代码] [代码][代码]"toName"[代码][代码]: that.data.ta[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//静态显示[代码] [代码] [代码][代码]this[代码][代码].data.allContentList.push({[代码] [代码] [代码][代码]"id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"hx_id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"wo"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"ta"[代码][代码]: that.data.ta,[代码] [代码] [代码][代码]"content"[代码][代码]: content,[代码] [代码] [代码][代码]"voice_url"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"fileurl"[代码][代码]: [代码][代码]null[代码][代码],[代码] [代码] [代码][代码]"create_date"[代码][代码]: util.formatTime[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]allContentList: [代码][代码]this[代码][代码].data.allContentList,[代码] [代码] [代码][代码]inputValue: [代码][代码]''[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]//至底[代码] [代码] [代码][代码]that.bottom();[代码] [代码] [代码][代码]//socket[代码] [代码] [代码][代码]if[代码] [代码](socketOpen) {[代码] [代码] [代码][代码]// 如果打开了socket就发送数据给服务器[代码] [代码] [代码][代码]that.sendSocketMessage(data);[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]//通过 WebSocket 连接发送数据,需要先 wx.connectSocket,并在 wx.onSocketOpen 回调之后才能发送。[代码] [代码] [代码][代码]sendSocketMessage: [代码][代码]function[代码][代码](msg) {[代码] [代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]console.log([代码][代码]'通过 WebSocket 连接发送数据'[代码][代码], JSON.stringify(msg))[代码] [代码] [代码][代码]// debugger[代码] [代码] [代码][代码]SocketTask.send([代码] [代码] [代码][代码]{[代码] [代码] [代码][代码]data: JSON.stringify(msg)[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]function[代码] [代码](res){[代码] [代码] [代码][代码]console.log([代码][代码]'已发送'[代码][代码], res)[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码])[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]addImg: [代码][代码]function[代码][代码](e) {[代码] [代码] [代码][代码]var[代码] [代码]a = [代码][代码]this[代码][代码].data.addImg;[代码] [代码] [代码][代码]if[代码] [代码](a == [代码][代码]true[代码][代码]) {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]addImg: [代码][代码]false[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]addImg: [代码][代码]true[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]tap() {[代码] [代码] [代码][代码]for[代码] [代码](let i = 0; i < order.length; ++i) {[代码] [代码] [代码][代码]if[代码] [代码](order[i] === [代码][代码]this[代码][代码].data.toView) {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]toView: order[i + 1],[代码] [代码] [代码][代码]scrollTop: (i + 1) * 200[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]break[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]tapMove() {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]scrollTop: [代码][代码]this[代码][代码].data.scrollTop + 10[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面加载[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onLoad: [代码][代码]function[代码][代码](options) {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]that.setData({[代码] [代码] [代码][代码]ta: options.tel,[代码] [代码] [代码][代码]youImg: options.youImg,[代码] [代码] [代码][代码]wo: util.phone[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]// 获取设备高度[代码] [代码] [代码][代码]wx.getSystemInfo({[代码] [代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码] [代码] [代码][代码]var[代码] [代码]clientHeight = res.windowHeight,[代码] [代码] [代码][代码]clientWidth = res.windowWidth,[代码] [代码] [代码][代码]rpxR = 750 / clientWidth;[代码] [代码] [代码][代码]var[代码] [代码]helfH = clientHeight * 1 * rpxR;[代码] [代码] [代码][代码]//var textH = helfH - 100;[代码] [代码] [代码][代码]that.setData({[代码] [代码] [代码][代码]windowH: helfH[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]//加载历史数据[代码] [代码] [代码][代码]// util.apiHtml +http://localhost:9193[代码] [代码] [代码] [代码] [代码][代码]//修改已读[代码] [代码] [代码] [代码] [代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面初次渲染完成[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onReady: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码] [代码] [代码] [代码] [代码][代码]SocketTask.onOpen(res => {[代码] [代码] [代码][代码]socketOpen = [代码][代码]true[代码][代码];[代码] [代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 连接打开事件。'[代码][代码], res)[代码] [代码] [代码][代码]//发送登陆信息[代码] [代码] [代码][代码]var[代码] [代码]data = {[代码] [代码] [代码][代码]// body: that.data.inputValue,[代码] [代码] [代码][代码]"Name"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"content"[代码][代码]: [代码][代码]"login"[代码][代码],[代码] [代码] [代码][代码]"type"[代码][代码]: 4[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]that.sendSocketMessage(data);[代码] [代码] [代码][代码]//循环发送心跳[代码] [代码] [代码][代码]setInterval([代码] [代码] [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]var[代码] [代码]ping = { [代码][代码]"type"[代码][代码]: [代码][代码]"ping"[代码] [代码]};[代码] [代码] [代码][代码]that.sendSocketMessage(ping);[代码] [代码] [代码][代码]}, 20000[代码] [代码] [代码][代码]); [代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]SocketTask.onClose(onClose => {[代码] [代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 连接关闭事件。'[代码][代码], onClose)[代码] [代码] [代码][代码]socketOpen = [代码][代码]false[代码][代码];[代码] [代码] [代码][代码]this[代码][代码].webSocket()[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]SocketTask.onError(onError => {[代码] [代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 错误。错误信息'[代码][代码], onError)[代码] [代码] [代码][代码]socketOpen = [代码][代码]false[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]SocketTask.onMessage(onMessage => {[代码] [代码] [代码][代码]console.log([代码][代码]"onMessage:::::"[代码][代码]+onMessage.data);[代码] [代码] [代码][代码]if[代码] [代码](onMessage.data.indexOf([代码][代码]"上线"[代码][代码]) != -1 || onMessage.data.indexOf([代码][代码]"下线"[代码][代码]) != -1) {[代码] [代码] [代码][代码]return[代码][代码];[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]console.log([代码][代码]'监听WebSocket接受到服务器的消息事件。服务器返回的消息'[代码][代码], JSON.parse(onMessage.data))[代码] [代码] [代码][代码]var[代码] [代码]onMessage_data = JSON.parse(onMessage.data)[代码] [代码] [代码][代码]if[代码] [代码](onMessage_data.toName == that.data.wo && onMessage_data.name == that.data.ta) {[代码] [代码] [代码][代码]// addmsglist1(msg1.name, msg1.content)[代码] [代码] [代码][代码]that.data.allContentList.push({[代码] [代码] [代码][代码]"id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"hx_id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"wo"[代码][代码]: that.data.ta,[代码] [代码] [代码][代码]"ta"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"content"[代码][代码]: onMessage_data.content,[代码] [代码] [代码][代码]"voice_url"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"fileurl"[代码][代码]: [代码][代码]null[代码][代码],[代码] [代码] [代码][代码]"create_date"[代码][代码]: util.formatTime[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]that.setData({[代码] [代码] [代码][代码]allContentList: that.data.allContentList[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]that.bottom();[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]// 获取hei的id节点然后屏幕焦点调转到这个节点 [代码] [代码] [代码][代码]bottom: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]scrollTop: 1000000[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面显示[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onShow: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]if[代码] [代码](!socketOpen) {[代码] [代码] [代码][代码]this[代码][代码].webSocket()[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]webSocket: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]// 创建Socket[代码] [代码] [代码][代码]SocketTask = wx.connectSocket({[代码] [代码] [代码][代码]url: url,[代码] [代码] [代码][代码]data: [代码][代码]'data'[代码][代码],[代码] [代码] [代码][代码]header: {[代码] [代码] [代码][代码]'content-type'[代码][代码]: [代码][代码]'application/json'[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]method: [代码][代码]'post'[代码][代码],[代码] [代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码] [代码] [代码][代码]console.log([代码][代码]'WebSocket连接创建'[代码][代码], res)[代码] [代码] [代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]fail: [代码][代码]function[代码][代码](err) {[代码] [代码] [代码][代码]wx.showToast({[代码] [代码] [代码][代码]title: [代码][代码]'网络异常!'[代码][代码],[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]console.log(err)[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]})[代码] [代码] [代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面隐藏[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onHide: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]SocketTask.close([代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]console.log(res);[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面卸载[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onUnload: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]SocketTask.close([代码][代码]function[代码][代码](res){[代码] [代码] [代码][代码]console.log(res);[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 页面相关事件处理函数--监听用户下拉动作[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onPullDownRefresh: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 页面上拉触底事件的处理函数[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onReachBottom: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 用户点击右上角分享[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onShareAppMessage: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]}[代码] [代码]})[代码]
2019-05-16 - 十分钟理解 Git 原理
历史 2002 年,Linus(Linux 作者)决定使用 BitKeeper 作为 Linux 内核主要的版本控制系统。Linus 曾考虑过采用现成软件作为版本控制系统,但这些软件都存在一些问题,比如:性能不佳。 2005 年,Andrew(Samba 作者)写了一个简单程序,可以连接 BitKeeper 的存储库,BitKeeper 作者反对这种逆向工程的行为,因此收回了无偿使用 BitKeeper 的许可。Linux 内核开发团队与其磋商无果后,Linus 决定自行开发版本控制系统替代 BitKeeper,然后用了十天的时间编写出 Git。 Git(英国俚语“混帐”)名字来源于 Linus 的自嘲: I’m an egotistical bastard, and I name all my projects after myself. First Linux, now Git. 基本原理 因为 Git 是一个分布式版本控制系统,因此 Git 的操作大部分都是在本地的,除非明确说明,下面的原理或命令都是本地操作。 每个 Git 项目的根目录下有一个 [代码].git[代码] 目录,它是 Git 默默进行版本控制时读写的“数据库”。有几个概念需要提一下: 工作区:代码所在目录; 暂存区: [代码].git/index[代码] 文件; 本地仓库: [代码].git[代码] 目录; 一个典型的工作流程如下图,绿色部分为工作区(Working Directory),对它进行任何修改(包括:新建文件、删除文件、文件重命名等)都和单纯的修改文件一样,不会涉及到版本控制。 只有当你把工作区的修改提交(commit)到仓库([代码].git[代码] 目录)中,Git 才会真正的进行版本控制。 [图片] 暂存区是一个包含文件索引的目录树([代码].git/index[代码] 文件),记录了文件的元数据(文件名、文件长度、修改时间等),而文件内容则存放在 [代码].git/objects[代码] 目录下。 用 Git 进行版本控制,实际上就是在工作区、暂存区、仓库三个地方进行文件信息的记录。 [图片] Git 将提交(commit)、文件、目录统统视为对象。对象以 [代码]SHA1[代码] 值作为指纹,与其他对象相区分。Git 命令操作的最小单位是对象。 Git 会将文件的副本存放在 [代码].git[代码] 文件夹下,每个文件都根据文件内容进行操作。以下图为例: [代码]98ca9[代码] 对象是一次提交,它记录了本次提交的元信息以及 [代码]92ec2[代码] 树对象; [代码]92ec2[代码] 树对象记录了文件名和对象的印射关系; 本次提交修改的三个文件一一对应了一个对象; [图片] Git 项目的文件始终在四种状态之间迁移,如下图所示: [图片] 如果是新文件,典型的操作流程如下: 创建一个新文件 [代码]foo.txt[代码],此时它处于未跟踪(Untracked)的状态,未被 Git 进行版本控制; [图片] 通过 [代码]add[代码] 命令将它纳入 Git 管理,此时 [代码]foo.txt[代码] 变为已暂存(Staged)状态; [图片] 提交此次操作,[代码]foo.txt[代码] 转变为未修改(Unmodified)状态; [图片] 如果是修改文件,典型的操作流程如下: 修改 [代码]foo.txt[代码] 文件,它转变为已修改(Modified)状态; [图片] 通过 [代码]add[代码] 命令将它的修改记录到暂存区,为已暂存(Staged)状态; [图片] 提交此次操作,[代码]foo.txt[代码] 转变为未修改(Unmodified)状态; [图片] 如果是删除文件,典型的操作流程如下: 删除 [代码]foo.txt[代码] 文件,它转变为已修改(Modified)状态; [图片] 通过 [代码]add[代码] 命令将操作记录到暂存区,为已暂存(Staged)状态; [图片] 提交此次操作,[代码]foo.txt[代码] 转变为未修改(Unmodified)状态(文件历史版本依然在 [代码].git[代码] 中被记录着); [图片] 常用命令 下面列举一下常用的 Git 命令,方便速查。更详细的速查可以看 快速回忆你用过的 Git。 查看信息 [代码]# 列出处于未跟踪、已修改或已暂存状态的文件 git status # 查看提交日志 git log # 查看已暂存文件和最后一次提交相比的详细修改 git diff --staged [代码] 状态变更 [代码]# 暂存或添加 git add . # 提交 git commit -m "commit description" [代码] 远程仓库(Remote Repository) [代码]# 下载到本地 git clone http://git.code.oa.com/<USER>/<PROJECT>.git # 更新代码 git pull # 将本地仓库的 commit 提交到远程仓库 git push origin master # 显示远程仓库信息,比如:url(不涉及网络通信) git remote -v [代码]
2019-03-19 - 【优化】小程序优化-代码篇
本文主要是从代码方面跟大家分享我自己在开发小程序的一些做法,希望能帮到一些同学。 前言 不知道大家有没有这种体会,刚到公司时,领导要你维护之前别人写的代码,你看着别人写的代码陷入了深深的思考:“这谁写的代码,这么残忍” [图片] 俗话说“不怕自己写代码,就怕改别人的代码”,一言不和就改到你吐血,所以为了别人好,也为了自己好,代码规范,从我做起。 项目目录结构 在开发之前,首先要明确你要做什么,不要一上来就是干,咱们先把项目结构搭好。一般来说,开发工具初始化的项目基本可以满足需求,如果你的项目比较复杂又有一定的结构的话就要考虑分好目录结构了,我的做法如下图: [图片] component文件夹是放自定义组件的 pages放页面 public放公共资源如样式表和公共图标 units放各种公共api文件和封装的一些js文件 config.js是配置文件 这么分已经足以满足我的需求,你可以根据自己的项目灵活拆分。 配置文件 我的项目中有个config.js,这个文件是用来配置项目中要用到的一些接口和其它私有字段,我们知道在开发时通常会有测试环境和正式环境,而测试环境跟正式环境的域名可能会不一样,如果不做好配置的话直接写死接口那等到上线的时候一个个改会非常麻烦,所以做好配置是必需的,文件大致如下: [图片] 首先是定义域名,然后在config对象里定义接口名称,getAPI(key)是获取接口方法,最后通过module暴露出去就可以了.引用的时候只要在页面引入 import domain from ‘…/…/config’;,然后wx.request的时候url的获取方式是domain.getAPI(’’) 代码健壮性、容错性 例子 代码的健壮性、容错性也是我们应该要考虑的一点,移动端的项目不像pc端的网络那么稳定,很多时候网络一不稳定就决定我们的项目是否能正常运行,而一个好的项目就一定要有良好的容错性,就是说在网络异常或其它因素导致我们的项目不能运行时程序要有一个友好的反馈,下面是一个网络请求的例子: [图片] 相信多数人请求的方式是这样,包括我以前刚接触小程序的时候也是这样写,这样写不是说不好,而是不太严谨,如果能够正常获取数据那还好,但是一旦请求出现错误那程序可以到此就没法运行下去了,有些比较好的会加上faill失败回调,但也只是请求失败时的判断,在请求成功到获取数据的这段流程内其实是还有一些需要我们判断的,一般我的做法是这样: [图片] 在请求成功后小程序会进行如下判断: 判断是否返回200,是则进行一下步操作,否则抛出错误 判断数据结构是否完整,是则进行一下步操作,否则抛出错误 然后就可以在页面根据情况进行相应的操作了。 定制错误提示码 可以看到上面的截图的错误打印后面会带一个gde0或gde1的英文代码,这个代码是干嘛用的呢,其实是用来报障的,当我们的小程序上线后可能会遇到一些用户发来的报障,一般是通过截图发给我们,之前没有做错误提示码的时候可能只是根据一句错误提示来定位错误,但是很多时候误提示语都是一样的,我们根本不知道是哪里错了,这样一来就不能很快的定位的错误,所以加上这样一个提示码,到时用户一发截图来,我们只要根据这个错误码就能很快的定位错误并解决了,错误提示码建议命名如下: 不宜过长,3个字母左右 唯一性 意义明确 像上面gde表示获取草稿失败,后面加上数字表示是哪一步出错。 模块化 我们组内的大神说过, 模块化的意义在义分治,不在于复用。 之前我以为模块化只是为了可以复用,其实不然,无论模块多么小也是可以模块化,哪怕只是一个简单的样式也一样,并是不为了复用,而是管理起来方便。 很多同学经常将一些公共的样式事js放在app.wxss和app.js里以便调用,这样做其实有一个坏处,就是维护性比较差,如果是比较小的项目还好,项目一大问题就来了。而且项目是会迭代的,不可能总是一个人开发,可能后面会交接给其他人开发,所以会造成的问题就是: app.wxss和app.js里的内容只会越来越多,因为别人不确定哪些是没用的也不敢删,只能往里加东西,造成文件臃肿,不利于维护。 app.wxss和app.js对于每个页面都有效,可读性方面比较差。 所以模块化的意义就出来了,将公共的部分进行模块化统一管理,也便于维护。 样式模块化 公共样式根据上面的目录结构我是放在public里的css里,每个文件命名好说明是哪个部分的模块化,比如下面这个就表示一个按钮的模块化 [图片] 前面说过模块化不在于大小,就算只是一个简单的样式也可以进行模块化,只要在用到的地方import一下就行了,就知道哪里有用到,哪里没有用到,清晰明了。 js模块化 js模块化这里分为两个部分的模块化,一部分是公共js的模块化,另一部分是页面js的模块化即业务与数据的拆分。 公共js模块化 比较常用的公共js有微信登录,弹窗,请求等,一般我是放在units文件夹里,这里经微信弹窗api为例: [图片] 如图是在小程序中经常会用到的弹窗提示,这里进行封装,定义变量,只要在页面中引入就能直接调用了,不用每次都写一大串。比如在请求的时候是这样用的 [图片] toast()就是封装的弹窗api,这样看起来是不是清爽多了! 业务与数据模块化 业务与数据模块化就是指业务和数据分开,互不影响,业务只负责业务,数据只负责数据,可以看到页面会比普通的页面多了一个api.js [图片] 这个文件主要就是用来获取数据的,而index.js主要用来处理数据,这样分工明确,相比以往获取数据和处理数据都在一个页面要好很多,而且我这里获取数据是返回一个promise对象的,也方便处理一些异步操作。 组件化 组件化相信大家都不陌生了,自从小程序支持自定义组件,可以说是大大地提高了开发效率,我们可以将一些公共的部分进行组件化,这部分就不详细介绍,大家可以去看文档。组件化对于我们的项目来说有很大的好处,而且组件化的可移植性强,从一个项目复用到另一个项目基本不需要做什么改动。 总结 这篇文章通过我自己的一些经验来给大家介绍如何优化自己的代码,主要有以下几点 分好项目目录结构 做好接口配置文件 代码健壮性、容错性的处理 定制错误提示码方便定位错误 样式模块化和js模块化 组件化 最后放上项目目录结构的代码片段,大家可以研究一下,有问题一起探讨:https://developers.weixin.qq.com/s/1uVHRDmT7j6l
2019-03-07