个人案例
- TDesign组件库
TDesign 是腾讯各业务团队在服务业务过程中沉淀的一套企业级设计体系。
TDesign 组件库扫码体验
- 程序员做饭
严谨、可量化便是程序员做饭指南的核心
程序员做饭指南扫码体验
- 分享壁纸
通过爬虫,将互联网上好看的壁纸都纳入囊中,分享给大家
网罗各种好看的壁纸扫码体验
- 深入理解小程序——生命周期
生命周期类型 不像其他框架,小程序是有页面(page) 和组件(Component) 两个概念,所以可以理解有两种生命周期。不过,你也可以使用组件也注册页面,然后在 [代码]lifetimes[代码] 字段里声明组件的生命周期,在 [代码]pageLifetimes[代码] 里声明页面的生命周期。 页面的生命周期有: onLoad onShow onReady onHide onUnload 而组件的生命周期有: created attached ready moved detached error 这里其实还有一个 linked 生命周期,存在父子关系时会触发 条件渲染的影响 组件可以通过 [代码]wx:if[代码] 和 [代码]hidden[代码] 来控制渲染的,这里对生命周期的触发也有影响。 如定义这么一个组件 [代码]log[代码]: [代码]Component({ lifetimes: { attached() { console.log('log attached') } } }) [代码] 使用 wx:if 然后在页面中使用 [代码]wx:if[代码] 条件渲染: [代码]<view wx:if="{{false}}"> <log /> </view> [代码] 此时不会触发 [代码]attached[代码],因此控制台没有输出。 使用 hidden 反而如果使用 [代码]hidden[代码] 条件渲染: [代码]<view hidden="{{true}}"> <log /> </view> [代码] 此时反而控制台会输出 [代码]log attached[代码]。 两者差异 其实两者的差异在于,[代码]hidden[代码] 会正常渲染 DOM,而 [代码]wx:if[代码] 则不会渲染。 如果组件的父元素使用 [代码]hidden[代码] 进行隐藏,那么此时 [代码]created[代码]、[代码]attached[代码]、[代码]ready[代码] 生命周期均会正常触发。如果在这些生命周期里获取子元素的尺寸,则所有值均返回 0。 如 TDesign 里面的 [代码]swipe-cell[代码] 需要计算 left 和 right 区域的大小;[代码]tabs[代码] 需要计算下划线的位置。 解决方案 比较简单的处理方式:建议用户使用 [代码]wx:if[代码] 而不是 [代码]hidden[代码],不过这明显是治标不治本的方案。 前文也提到了,问题的根本是没有正确地获取到元素的尺寸,因此可以在获取元素尺寸的地方做兼容处理。异常触发的条件则是 [代码]width == 0 && right == 0[代码] 知道在哪里需要兼容处理之后,需要解决的则是:如何在可以获取到正确的尺寸的时候重新获取尺寸呢? 此时可以使用 [代码]wx.createIntersectionObserver[代码] 这个 API。当 [代码]hidden = false[代码] 的时候,组件会重新出现在视图里,Observer 就会被触发,此时重新获取尺寸就能得到正确的尺寸信息了。以下是简易的封装: [代码]const getObserver = (context, selector) => { return new Promise((resolve, reject) => { wx.createIntersectionObserver(context) .relativeToViewport() .observe(selector, (res) => { resolve(res); }); }); }; const getRect = function (context:, selector) { return new Promise((resolve, reject) => { wx.createSelectorQuery() .in(context) .select(selector) .boundingClientRect((rect) => { if (rect) { resolve(rect); } else { reject(rect); } }) .exec(); }); }; export const getRectFinally = (context, selector) => { return new Promise((resolve, reject) => { getRect(context, selector).then(rect => { if (rect.width === 0 && rect.height === 0) { getObserver(context, selector).then(res => { resolve(res) }).catch(reject) } else { resolve(rect) } }).catch(reject) }) } [代码] 父子组件的影响 当存在父子组件的时候,可能很多人根本不知道各种生命周期的触发顺序。 之前 TDesign 的 [代码]cell-group[代码] 有个错误的实现,在 linked 生命周期里获取子元素进行操作: [代码]Component({ relations: { '../cell/cell': { type: 'child', linked() { this.updateLastChid(); }, }, }, updateLastChid() { const items = this.$children; items.forEach((child, index) => child.setData({ isLastChild: index === items.length - 1 })); }, }) [代码] 其实,存在父子组件的时候,生命周期是这么触发的: 父组件 created 子组件 created 父组件 attached 子组件 attached 父组件 linked(触发多次,次数 = 子组件数量) 子组件 linked 父组件 ready 子组件 ready 因此如果是这么使用 [代码]t-cell-group[代码]: [代码]<t-cell-group> <t-cell title="cell1" /> <t-cell title="cell2" /> <t-cell title="cell3" /> </t-cell-group> [代码] 那么子组件 cell 的 [代码]setData[代码] 触发次数为:1 + 2 + 3 = 6 次。 但其实开发者的预期应该是 1 次,所以 [代码]updateLastChid[代码] 应该放在父组件的 ready 方法里才符合预期。 总结 以上是在小程序开发的过程中,常遇到的问题。但如果没有像 TDesign 组件库这样深入开发小程序,可能并不会去深入钻研生命周期的细节。但在日常的业务开发当中,如果开发者能够清晰地理解各种生命周期的本质,在遇到其他问题的时候,也能比较快速地定位问题的关键点。 如果能到达这样的效果,也是笔者写下这篇文章的初心。 更多内容关注:https://github.com/LeeJim
2023-08-18 - 程序员做饭指南
在网上找找菜谱和做法。但是这些菜谱往往写法千奇百怪,经常中间莫名出来一些材料。对于习惯了形式语言的程序员来说极其不友好。 因此精确可量化,便很重要。 [图片] 友好的视觉 [图片] 内嵌计时器 [图片]
2022-03-08 - 如何使用Node.js的Buffers
为什么要有Buffers? 在纯[代码]JavaScript[代码]开发中,unicode编码的字符串也够好用的了,并不需要直接处理二进制数据(straight binary data)。在浏览器环境,大部分数据都是字符串的形式,这是足够的。然而,Node.js是服务器环境,必须要处理TCP流还有文件系统的读取和写入流,这就让[代码]JavaScript[代码]需要处理纯二进制数据了。 其实,要解决这个问题直接使用字符串也是可以的,这也是Node.js一开始的做法。然而,这样的做法有许多问题,也很慢。 所以,记住了,别使用二进制字符串(binary strings),用buffers代替它! 什么是Buffers? 在Node.js里,Buffers是专门设计来处理原始二进制数据的,是Buffer这个类的实例。 每个buffer在V8引擎外都有内存分配。Buffer操作起来和包含数字的数组一样,但是不像数组那样自由设置大小的。并且buffer拥有一系列操作二进制数据的方法。 另外,buffer里的“数字”代表的是byte并且限制大小是0到255(2^8-1) 在哪里可以看到buffers 一般情况,buffer经常可以在读取二进制数据流的时候看到,比如[代码]fs.createReadStream[代码] 用法: 创建buffer 有许多方法可以生成新的buffers: [代码]var buffer = new Buffer(8); [代码] 这个buffer是未初始化的,且包含8个字节(bytes)。 [代码]var buffer = new Buffer([ 8, 6, 7, 5, 3, 0, 9]); [代码] 这个buffer用一个数组的内容来初始化。记住了,数组里的数字表示的是字节(bytes) [代码]var buffer = new Buffer("I'm a string!", "utf-8") [代码] 通过第二个参数来指定编码(默认是utf-8)的字符串来初始化buffer。utf-8是在Node.js里最常用的编码,但是buffer还支持其他编码: “ascii”:这个编码方式很快,但是只限制ascii字符集。而且这个编码会将null转换成空格,而不像utf-8编码。 “ucs2”:一种双字节,小端存储的编码。可以编码一个unicode的子集。 “base64”:Base64字符串编码。 “binary”:这个“二进制字符串”前面提到过,这个编码即将被弃用,避免使用这个。 写入buffer 创建一个buffer: [代码]> var buffer = new Buffer(16); [代码] 开始写入字符串: [代码]> buffer.write("Hello", "utf-8") 5 [代码] [代码]buffer.write[代码]的第一个参数是写入buffer的字符串,而第二个参数是这个字符串的编码方式。如果字符串的编码是utf-8,那么这个参数是多余的。 [代码]buffer.write[代码]返回5,这代表我们写入了5个字节到这个buffer。事实上,“Hello“这个字符串也刚好是5个字符。这是因为刚好每个字符都是8位(bits)。这对补全字符串很重要: [代码]> buffer.write(" world!", 5, "utf-8") 7 [代码] 当[代码]buffer.write[代码]有3个参数的时候,第二个参数代表是偏移量,或者说是buffer开始写入的位置。 读取buffer toString: 这个方法可能是读取buffer最通用的方法了,因为很多buffer都包含文本: [代码]> buffer.toString('utf-8') 'Hello world!\u0000�k\t' [代码] 再一次,第一个参数代表编码方式。这里可以看到并没有用完整个buffer。幸运的是,我们知道写入了多少字节到这个buffer,我们可以简单地增加参数去割开这个字符串: [代码]> buffer.toString("utf-8", 0, 12) 'Hello world!' [代码] 独立字节: 你可以看到用类似数组的语法来设置独立位(individual bits) [代码]> buffer[12] = buffer[11]; 33 > buffer[13] = "1".charCodeAt(); 49 > buffer[14] = buffer[13]; 49 > buffer[15] = 33 33 > buffer.toString("utf-8") 'Hello world!!11!' [代码] 在这个例子里,手动地设置剩余的字节,这样就代表了“utf-8”编码的“!”和“1“字符了。 更多有趣用法 Buffer.isBuffer(object) 这个方法是检测一个对象是否是buffer,类似于[代码]Array.isArray[代码] Buffer.byteLength(string, encoding) 通过这个方法,你可以获取字符串(默认utf-8编码)的字节数。这个长度和字符串的长度(string length)不一样,因为很多字符需要更多的字节,例如: [代码]> var snowman = "☃"; > snowman.length 1 > Buffer.byteLength(snowman) 3 [代码] 这个unicode的雪人只有两个字符,却占了3个字节。 buffer.length 这个是buffer的长度,也代表分配了多少内存。这个不等于buffer内容的大小,因为buffer有可能是没满的,比如: [代码]> var buffer = new Buffer(16) > buffer.write(snowman) 3 > buffer.length 16 [代码] 在这个例子里,我们只写入了3个字符,但是长度依然是16,因为这是已经初始化了的。 buffer.copy(target, targetStart=0, sourceStart=0, sourceEnd=buffer.length) [代码]buffer.copy[代码]允许拷贝一个buffer的内容到另一个buffer。 第一个参数表示目标buffer,就是要写入内容的buffer。 另外一个参数是指定需要拷贝到目标buffer的开始位置。看个例子: [代码]> var frosty = new Buffer(24) > var snowman = new Buffer("☃", "utf-8") > frosty.write("Happy birthday! ", "utf-8") 16 > snowman.copy(frosty, 16) 3 > frosty.toString("utf-8", 0, 19) 'Happy birthday! ☃' [代码] 在这个例子,拷贝了含有3个字节长度的“snowman”buffer到“forsty”buffer。 其中forsty一开始写入了前16个字节,而snowman有3个字节长,因此结果就是19个字节长。 buffer.slice(start, end=buffer.length) 这个方法的API可以说和[代码]Array.prototype.slice[代码]是一样的。 不过其中一个特别重要的区别是:这个slice方法不是简单地返回一个新的buffer,也不仅仅是内存中子集的引用。这个slice会改变原来的buffer!举例: [代码]> var puddle = frosty.slice(16, 19) > puddle.toString() '☃' > puddle.write("___") 3 > frosty.toString("utf-8", 0, 19) 'Happy birthday! ___' [代码] 完。 原文链接:https://docs.nodejitsu.com/articles/advanced/buffers/how-to-use-buffers/
2020-03-11 - 浅谈小程序路由的封装设计
微信官方提供了基础的路由能力,在日常的开发中虽已够用,但随着开发的深入,会遇到许多值得思考提炼的问题。本文将探讨作者在微信小程序(以下简称小程序)开发当中遇到的问题,以及解决方案设计。 参考“WHY-HOW-WHAT"黄金圈思维法则,首先讲述为什么小程序的路由需要封装设计,也就是存在哪些问题,需要封装处理? 存在的问题 路由跳转的路径与文件路径耦合 小程序的路由跳转使用的是真实文件路径,因此若文件的结构发生变化,必会影响到所有的页面的跳转路径。 笔者在实际开发中就遇到这个问题,以小程序分包举例。 小程序的分包是以文件夹为单位的。如果要将一系列的页面拆分成分包,则需要将这些文件移至同个目录之下,因此必然导致路由的跳转路径发生变更。如果此时路由跳转均是直接通过文件路径跳转的话,则需要全局改动,导致的工作量不少。 另外,当开发团队比较庞大时,不同的业务之间总会存在互相跳转的情况。当其中一个页面地址发生变更时,其他业务跳转到该页面的路径都需要手动变更。若此时通知不及时,或者遗漏了一些地方,导致跳转失败,终会酿成大错。 路由传参 目前小程序支持的传参方式,即通过跳转路径上的query查询参数。 通过query传参的问题,与在Web上URL传参是一致的,比如: query的参数长度有限 query只能传递可序列化的数据 导航前需手动序列化,到达目标页面后需反序列化 条件导航 在日常业务中,会存在一些页面需要一定条件才允许进入的。 举个例子,会员服务是一种很常见的能力,而会员中心的进入条件是: 该用户已经完成登录 该用户是本产品的会员 一般情况下,这有特定准入资格的页面的导航逻辑是这样的: [图片] 这种方式简单明了,但存在一个问题:需要每次跳转前主动判断,逻辑冗余以外,还可能被遗漏。 思路 由于小程序本身已提供了基础的路由导航能力,不像react、vue.js那样需要从底层进行封装,从而提供路由能力。但是,本质上小程序可以理解成类vue.js这样的框架,因此可以从vue.js的路由库vue-router上找到灵感,从而解决以上问题。 命名路由 使用 命名路由 的方式可以解决前文提及的跳转路径和文件真实路径耦合问题。 通过[代码]Map[代码]来映射 页面ID 和 页面地址,路由跳转时,仅能使用 页面ID 进行路由跳转。 下面以导航至首页举例: [代码]// before wx.switchTab('pages/home/index') // after router.go('home') [代码] 由于小程序有tab页面和普通页面之分,因此导航至tab页时需使用switchTab 细心的读者可能会发现上文使用了[代码]go[代码]方法,而不是[代码]switchTab[代码]。其实,具体哪些页面属于tab页面,在[代码]app.json[代码]已经明确配置。对于使用者来说,不需要关心跳转的页面是属于哪种类型,这些细节都应该统一在底层封装好。下面罗列[代码]Router[代码]与官方API的对应关系: [图片] Router API的设计原则是保持简单,以及尽量保持与web规范一致 传递参数 微信官方提供的query方式传参,若参数是普通数据类型(如[代码]Number[代码]、[代码]String[代码])时可以直接使用;但若是涉及到复杂数据类型(如[代码]Array[代码]、[代码]Object[代码])时,需要先做序列化处理,当数据较为庞大时,性能的损耗还是比较明显的。 因此,在内存上传递参数是比较便利且容易想到的办法。 利用数据字典,将[代码]页面ID[代码]作为[代码]key[代码]、传递的参数作为[代码]value[代码],写入[代码]Router[代码]的[代码]state[代码]: [代码]router.go = function(pageID, params) { // do something... router.state[pageID] = params } [代码] 在目标页面上,可以通过[代码]router.getParams()[代码]方法,获取传递的参数。 由于采用了命名路由的方式,可以使用[代码]页面ID[代码]作为[代码]key[代码],避免了使用跳转路径做[代码]key[代码]时,涉及到的绝对与相对路径问题。 条件导航 条件导航可以使用类似vue-router的导航守卫来解决问题。 由于路由的能力是微信官方提供的,因此无法像 vue-router 那样提供多类型的导航守卫,但仅有全局导航守卫也足够使用。 以下仍以“会员中心”的进入逻辑举例,并简要介绍实现思路: [图片] 其中,to和from目前是pageID,其实可以封装更多信息,以保证导航守卫可以尽可能拥有更多的信息。因此to可以理解成是即将进入的页面路由对象,而from则是当前正要离开的路由对象。 路由对象可以包含以下信息: [代码]pageID[代码]:页面ID [代码]path[代码]:页面ID对应的path [代码]params[代码]:传递的参数 [代码]query[代码]: URL的查询参数 配置信息 由前文提到的 命名路由 做法需要一个配置文件来关联[代码]页面ID[代码]与[代码]页面路径[代码]的关系。 页面的配置信息,则是使用[代码]router.config.js[代码]设置,然后通过构建工具编译转成[代码]app.json[代码]。 以下是[代码]route.config.js[代码]: [图片] 其中,跳转首页则是[代码]router.go('home')[代码];而跳转分包[代码]health[代码]的首页则是使用[代码]router.go('health.home')[代码] 通过以上的配置文件,使用构建工具转换成微信官方可识别的[代码]app.json[代码]配置: [图片] 辅助函数 在日常开发当中,经常会用到一些和路由相关的通用辅助函数,如获取当前页面,获取上个页面等。这些辅助函数都应该统一抽象封装,避免代码冗余。 [代码]router.utils = { getCurPage() { // 获取当前页面信息 let pages = getCurrentPages() let len = pages.length return pages[len - 1] }, getPrePage() {}, // 获取上个页面信息 getParams() {}, // 获取传递的参数 getPageID(path) {} // 通过path找到pageID } [代码] navigator组件 微信官方除了提供[代码]API[代码]用于导航以外,还提供了[代码]navigator[代码]组件。 另外还有[代码]functional-page-navigator[代码]是用于插件当中,不能在小程序包使用,因此本文暂且将其忽略。 由于[代码]navigator[代码]的跳转参数仍是使用[代码]path[代码],因此笔者将其进行二次封装,改造成可以通过[代码]pageID[代码]跳转: [图片] 总结 由于小程序相对比较封闭,因此在路由上能做的东西比较有限。 但路由又与许多概念有千丝万缕的关系。比如路由与文件结构关联,而文件结构又影响到分包的设计,环环相扣,影响到的地方则会越来越多。 因此,能提前看到本文提到的可能出现的问题,也许对后续的小程序开发有一定的参考意义。 另外,前文提到的很多问题,在早期开发,或者没有深入开发之前,都不会遇到。但是当你开始经历前文提到的那些问题时,往往此时的改造成本已经很大了。因此希望本文能给你带了一些启发,在早期规避这些问题,那本文的使命就达到了。
2020-06-15 - 自动更新机制
本文是我的小程序开发日记的其中一篇, GitHub 原文地址 欢迎star,感谢万分! 前言 小程序的更新机制与它的运行机制有关。 为了保证用户能尽可能快得打开小程序,只会在后台更新,不会主动等待更新完毕才进入最新版小程序。 运行机制 首先,先看下小程序的运行机制: [图片] 与APP的概念有些类似,初次打开即为冷启动,若启动之后,在被系统回收之前再次打开,则称之为热启动。 更新机制 小程序的更新机制分为: 未启动时更新 启动时更新 未启动时更新:意味着微信客户端会在用户不在访问小程序期间,主动触发更新,最慢24小时内覆盖所有用户。如果用户在未覆盖期间进入小程序,则触发了启动时更新。 启动时更新:用户冷启动进入小程序时,均会检测小程序是否有更新版本,若有则后台默默更新,准备为下次冷启动时使用。需要注意的是,此时访问的仍是旧版本的小程序。如果此时想手动使用新版小程序,则可以使用官方API: [代码]const updateManager = wx.getUpdateManager() updateManager.onCheckForUpdate(function (res) { // 请求完新版本信息的回调 console.log(res.hasUpdate) }) updateManager.onUpdateReady(function () { wx.showModal({ title: '更新提示', content: '新版本已经准备好,是否重启应用?', success(res) { if (res.confirm) { // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 updateManager.applyUpdate() } } }) }) updateManager.onUpdateFailed(function () { // 新版本下载失败 }) [代码] 如若用户是第一次打开小程序(即新用户),则会直接打开最新版本的小程序。此时不需要考虑更新机制。 根据微信提供的能力,小程序的更新流程大致如下: [图片] 由于官方API没有提供主动下载新版本小程序的能力,仅提供了检测的能力。因此,当新版本下载失败时,没法主动触发重试,只能让用户继续访问旧版本的小程序。 下载失败之后,小程序的重试机制不得而知。可能需要等待小程序被销毁之后,再次冷启动时才会再次主动更新;又或者等待24小时之后。 由于可能存在下载新版本失败的用户,因此小程序的后端服务需要考虑向后兼容。另外,可以在下载失败的回调函数里加入数据统计,用于计算更新失败的概率。 测试 更新机制的测试工作比较麻烦,因为可能要上生产环境测试,风险极大。 笔者尝试在体验版上做测试:先打开[代码]v0.0.1[代码]版本的小程序,然后在开发者工具上传新的版本,再通过最近访问的列表里再次打开小程序,结果发现直接打开的就是[代码]v0.0.2[代码],根本没有还原小程序的更新机制。 因此可以得出结论:体验版无法测试更新机制。 模拟更新 另外,开发者工具的编译模式提供模拟更新: [图片] 兼容处理 由于存在用户访问旧版小程序的可能,因此与后端的接口设计需要特别关注,尤其是在更新接口时,如果没有做到向后兼容,则会出现旧前端访问新后端的现象,从而产生不可预期的后果。 最简单的方式:每次升级接口时,均采用新接口。
2020-05-08 - 浅谈小程序的错误处理
其实,错误(异常)处理在任何编程语言里,都是不可避免的。正确处理异常,是一个程序/应用保持健壮的关键。 现实 从小程序的API文档可以看出,每个异步方法都支持传入一个[代码]fail[代码]方法,用于异常处理,例如: [代码]wx.login({ success (res) { console.log(res) }, fail(err) { // handle error } }) [代码] 会存在这样一种情况:开发人员会因为惰性直接忽略这个参数;而测试人员由于无法mock这些错误情况,导致测试用例没有覆盖,最终可能会因此流失用户。 还有一种情况,当这些API调用是在非关键流程上。若调用成功,则继续执行;若调用失败,直接忽略也不会影响。 对于第一种情况,除了在fail里做异常处理以外,别无他法。 本文将进一步讨论第二种情况。 分析 官方提供的API,在发生异常时,均会通过回调函数[代码]fail[代码]回传错误信息。如果我们能采集这些数据,进行统计分析能有这些作用: 为后续技术优化提供指导方向 了解用户设备的兼容性,预防踩重复的坑 由于官方提供的API,所有的异步方法都需要手动传入[代码]fail[代码],因此手动给每个方法传入[代码]fail[代码]可能是不可行的。 另外,小程序的更新频率很高,每隔一段时间就会出现许多新的API。 因此,最佳的实践即是封装全局对象[代码]wx[代码] 实践 封装[代码]wx[代码]的方案有很多,这里就列出两种比较常规的方案: 较安全的方案:在全局变量[代码]global[代码]上新增方法(如:[代码]global.wx[代码]) 较激进的方案:劫持[代码]wx[代码],直接在[代码]wx[代码]上动刀 两种方案可有利弊,要看如何权衡。以下我将以第二种方案举例: [代码]// global.js let originalWX = wx; wx = new Proxy({}, { // [0] get(target, name) { if (name in originalWX ) { let isSyncFunction = name.endsWith('Sync'); // 同步函数 [1] let isNotFunction = typeof originalWX[name] !== 'function'; // 非函数 [2] if (isSyncFunction || isNotFunction) return originalWX[name]; return function(obj) { if (typeof obj === 'object') { // [3] let originalFail = function() {}; if ('fail' in obj) { originalFail = obj.fail; } obj.fail = function() { // todo 上报数据到后端 [4] console.log('hijack success'); originalFail(); }; } return originalWX[name](obj); }; } } }); [代码] 代码注释: [0]: 这里使用的是ES6提供的[代码]Proxy[代码]代理对象;会有一定的兼容性,如果需要兼容更低版本的机型,可采用其他方案(感兴趣的人多的话,后续补上) [1]:前文也提到,只有异步方法才会有回调,因此同步方法直接返回原[代码]wx[代码]的方法 [2]:非函数;wx对象里有非函数的值,如 wx.env [3]:wx对象里的函数,可能传入非对象参数。如:wx.canIUse [4]:请看下一章节 [图片] 进阶 其实上述的代码,还不是最终版本。因为数据上报部分,还依赖后端提供接口。 按理说,日志系统也算是通用的服务。我很早前就在思考,为什么微信官方不提供呢?细心的读者可能会反驳说,微信有提供类似的功能:wx.reportMonitor(业务数据监控上报接口)。 其实,用过的读者应该了解,这个接口是非实时的,不能算是日志服务。 如果你有不定时翻看微信小程序开发文档的习惯的话,你总会有这样的感觉:时不时就新增了一个特性,塞在了一个不容易发现的角落。接下来要讲的新特性,就是官方提供的 实时日志: [代码]var log = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null log && log.info.apply(log, arguments) [代码] 所有的日志,都可以通过 小程序管理后台 查看。 访问路径:[ 开发->运维中心->实时日志 ] [图片]
2020-03-23 - 小程序利用safe-area-inset-*兼容iPhoneX
分别创建屏幕上边框,右边框,下边框,左边框安全距离: safe-area-inset-top, safe-area-inset-right, safe-area-inset-bottom, safe-area-inset-left 使用: iOS 11 padding-top: constant(safe-area-inset-top); padding-right: constant(safe-area-inset-right); padding-bottom: constant(safe-area-inset-bottom); padding-left: constant(safe-area-inset-left); iOS 11.2 beta及其后 padding-top: env(safe-area-inset-top); padding-right: env(safe-area-inset-right); padding-bottom: env(safe-area-inset-bottom); padding-left: env(safe-area-inset-left); 兼容性写法: padding-top: 10px; padding-top: constant(safe-area-inset-top); padding-top: env(safe-area-inset-top); 与calc合用: padding-top: 10px; padding-top: calc(10px + constant(safe-area-inset-top)); padding-top: calc(10px + env(safe-area-inset-top)); 终!使用sass@mixin: @mixin x-padding-bottom($val:0px) { padding-bottom: $val; padding-bottom: calc(#{$val / 2} + constant(safe-area-inset-bottom)); /* no */ padding-bottom: calc(#{$val / 2} + env(safe-area-inset-bottom)); /* no */ } 注意!!! 1、默认值为0px,不是0,原因是calc不支持与0计算。 2、小程序单位为rpx,一般都会转换为rpx,但是calc不支持,所以不允许转换,保持px。 参考文档:苹果官方文档
2019-10-11 - 正确使用JavaScript数组
首先,我们可以简单地认为缩进就是代码复杂性的指标(尽管很粗略)。因为缩进越多代表我们的嵌套越多,因此代码就越复杂。今天就拿数组来做具体的例子,来展示以下如何抛弃循环,减少缩进,正确地使用JavaScript数组。 “…a loop is an imperative control structure that’s hard to reuse and difficult to plug in to other operations. In addition, it implies code that’s constantly changing or mutating in response to new iterations.” -Luis Atencio 循环 我们都知道,循环结构就是会无形地提高代码的复杂性。那我们现在看看在JavaScript上的循环是如何工作的。 在JavaScript上至少有四五种循环的方式,其中最基础的就是[代码]while[代码]循环了。讲例子前,先设定一个函数和数组: [代码]// oodlify :: String -> String function oodlify(s) { return s.replace(/[aeiou]/g, 'oodle'); } const input = [ 'John', 'Paul', 'George', 'Ringo', ]; [代码] 那么,如果我们现在要使用[代码]oodlify[代码]函数操作一下数组里每个元素的话,如果我们使用[代码]while[代码]循环的话,是这样子的: [代码]let i = 0; const len = input.length; let output = []; while (i < len) { let item = input[i]; let newItem = oodlify(item); output.push(newItem); i = i + 1; } [代码] 这里就有许多无谓的,但是又不得不做的工作。比如用[代码]i[代码]这个计数器来记住当前循环的位置,而且需要把[代码]i[代码]初始化成0,每次循环还要加一;比如要拿[代码]i[代码]和数组的长度[代码]len[代码]对比,这样才知道循环到什么时候停止。 这时为了让清晰一点,我们可以使用JavaScript为我们提供的[代码]for[代码]循环: [代码]const len = input.length; let output = []; for (let i = 0; i < len; i = i + 1) { let item = input[i]; let newItem = oodlify(item); output.push(newItem); } [代码] [代码]for[代码]循环的好处就是把与业务代码无关的计数逻辑放在了括号里面了。 对比起[代码]while[代码]循环虽有一定改进,但是也会发生类似忘记给计数器[代码]i[代码]加一而导致死循环的情况。 现在回想一下我们的最初目的:就只是给数组的每一个元素执行一下[代码]oodlify[代码]函数而已。其实我们真的不想关什么计数器。 因此,[代码]ES2015[代码]就为我们提供了一个全新的可以让我们忽略计数器的循环结构- [代码]for...of[代码]循环 : [代码]let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } [代码] 这个方式是不是简单多了!我们可以注意到,计数器和对比语句都没了。 如果我们这就满足的话,我们的目标也算完成了,代码的确是简洁了不少。 但是其实,我们可以对JavaScript的数组再深入挖掘一下,更上一层楼。 Mapping [代码]for...of[代码]循环的确比[代码]for[代码]循环简洁不少,但是我们仍然写了一些不必要的初始化代码,比如[代码]output[代码]数组,以及把每个操作过后的值push进去。 其实我们有办法写得更简单明了一点的。不过,现在我们来放大一下这个问题先: 如果我们有两个数组需要使用[代码]oodlify[代码]函数操作的话呢? [代码]const fellowship = [ 'frodo', 'sam', 'gandalf', 'aragorn', 'boromir', 'legolas', 'gimli', ]; const band = [ 'John', 'Paul', 'George', 'Ringo', ]; [代码] 很明显,我们就要这样循环两个数组: [代码]let bandoodle = []; for (let item of band) { let newItem = oodlify(item); bandoodle.push(newItem); } let floodleship = []; for (let item of fellowship) { let newItem = oodlify(item); floodleship.push(newItem); } [代码] 这的确可以完成我们的目标,但是这样写得有点累赘。我们可以重构一下以减少重复的代码。因此我们可以创建一个函数: [代码]function oodlifyArray(input) { let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } return output; } let bandoodle = oodlifyArray(band); let floodleship = oodlifyArray(fellowship); [代码] 这样是不是好看多了。但是问题来了,如果我们要使用其他函数来操作这个数组的话呢? [代码]function izzlify(s) { return s.replace(/[aeiou]+/g, 'izzle'); } [代码] 这时,我们前面创建的[代码]oodlifyArray[代码]函数帮不了我们了。不过如果我们这时创建[代码]izzlifyArray[代码]函数的话,代码不就又有许多重复的部分了吗? [代码]function oodlifyArray(input) { let output = []; for (let item of input) { let newItem = oodlify(item); output.push(newItem); } return output; } function izzlifyArray(input) { let output = []; for (let item of input) { let newItem = izzlify(item); output.push(newItem); } return output; } [代码] 这两个函数是不是及其相似呢。 如果此时我们将其抽象成一个模式的话呢:我们希望传入一个数组和一个函数,然后映射每个数组元素,最后输出一个数组。这个模式就称为[代码]mapping[代码]: [代码]function map(f, a) { let output = []; for (let item of a) { output.push(f(item)); } return output; } [代码] 其实我们并不需要自己手动写[代码]mapping[代码]函数,因为JavaScript提供了内置的[代码]map[代码]函数给我们使用,此时我们的代码是这样的: [代码]let bandoodle = band.map(oodlify); let floodleship = fellowship.map(oodlify); let bandizzle = band.map(izzlify); let fellowshizzle = fellowship.map(izzlify); [代码] Reducing 此时[代码]map[代码]是很方便了,但是并不能覆盖我们所有的循环需要。 如果此时我们需要累计数组中的所有数组呢。我们假设有一个这样的数组: [代码]const heroes = [ {name: 'Hulk', strength: 90000}, {name: 'Spider-Man', strength: 25000}, {name: 'Hawk Eye', strength: 136}, {name: 'Thor', strength: 100000}, {name: 'Black Widow', strength: 136}, {name: 'Vision', strength: 5000}, {name: 'Scarlet Witch', strength: 60}, {name: 'Mystique', strength: 120}, {name: 'Namora', strength: 75000}, ]; [代码] 如果我们要找到[代码]strength[代码]最大的那个的元素的话,使用[代码]for...of[代码]循环是这样的: [代码]let strongest = {strength: 0}; for (hero of heroes) { if (hero.strength > strongest.strength) { strongest = hero; } } [代码] 如果此时我们想累计一下所有的[代码]strength[代码]的话,循环里面就是这样的了: [代码]let combinedStrength = 0; for (hero of heroes) { combinedStrength += hero.strength; } [代码] 这两个例子我们都需要初始化一个变量来配合我们的操作。合并两个例子的话就是这样的: [代码]function greaterStrength(champion, contender) { return (contender.strength > champion.strength) ? contender : champion; } function addStrength(tally, hero) { return tally + hero.strength; } // 例子 1 const initialStrongest = {strength: 0}; let working = initialStrongest; for (hero of heroes) { working = greaterStrength(working, hero); } const strongest = working; // 例子 2 const initialCombinedStrength = 0; working = initialCombinedStrength; for (hero of heroes) { working = addStrength(working, hero); } const combinedStrength = working; [代码] 此时我们可以抽象成这样一个函数: [代码]function reduce(f, initialVal, a) { let working = initialVal; for (item of a) { working = f(working, item); } return working; } [代码] 其实这个方法JavaScript也提供了内置函数,就是[代码]reduce[代码]函数。这时代码是这样的: [代码]const strongestHero = heroes.reduce(greaterStrength, {strength: 0}); const combinedStrength = heroes.reduce(addStrength, 0); [代码] Filtering 前面的[代码]map[代码]函数是将数组的全部元素执行同个操作之后输出一个同样大小的数组; [代码]reduce[代码]则是将数组的全部值执行操作之后,最终输出一个值。 如果此时我们只是需要提取几个元素到一个数组内呢?为了更好得解释,我们来扩充一下之前的例子: [代码]const heroes = [ {name: 'Hulk', strength: 90000, sex: 'm'}, {name: 'Spider-Man', strength: 25000, sex: 'm'}, {name: 'Hawk Eye', strength: 136, sex: 'm'}, {name: 'Thor', strength: 100000, sex: 'm'}, {name: 'Black Widow', strength: 136, sex: 'f'}, {name: 'Vision', strength: 5000, sex: 'm'}, {name: 'Scarlet Witch', strength: 60, sex: 'f'}, {name: 'Mystique', strength: 120, sex: 'f'}, {name: 'Namora', strength: 75000, sex: 'f'}, ]; [代码] 现在假设我们要做的两件事: 找到[代码]sex = f[代码]的元素 找到[代码]strength > 500[代码]的元素 如果使用[代码]for...of[代码]循环的话,是这样的: [代码]let femaleHeroes = []; for (let hero of heroes) { if (hero.sex === 'f') { femaleHeroes.push(hero); } } let superhumans = []; for (let hero of heroes) { if (hero.strength >= 500) { superhumans.push(hero); } } [代码] 由于有重复的地方,那么我们就把不同的地方抽取出来: [代码]function isFemaleHero(hero) { return (hero.sex === 'f'); } function isSuperhuman(hero) { return (hero.strength >= 500); } let femaleHeroes = []; for (let hero of heroes) { if (isFemaleHero(hero)) { femaleHeroes.push(hero); } } let superhumans = []; for (let hero of heroes) { if (isSuperhuman(hero)) { superhumans.push(hero); } } [代码] 此时就可以抽象成JavaScript内置的[代码]filter[代码]函数: [代码]function filter(predicate, arr) { let working = []; for (let item of arr) { if (predicate(item)) { working = working.concat(item); } } } const femaleHeroes = filter(isFemaleHero, heroes); const superhumans = filter(isSuperhuman, heroes); [代码] Finding [代码]filter[代码]搞定了,那么如果我们只要找到一个元素呢。 的确,我们同样可以使用[代码]filter[代码]函数完成这个目标,比如: [代码]function isBlackWidow(hero) { return (hero.name === 'Black Widow'); } const blackWidow = heroes.filter(isBlackWidow)[0]; [代码] 当然我们也同样会发现,这样的效率并不高。因为[代码]filter[代码]函数会过滤所有的元素,尽管在前面已经找到了应该要找到的元素。因此我们可以写一个这样的查找函数: [代码]function find(predicate, arr) { for (let item of arr) { if (predicate(item)) { return item; } } } const blackWidow = find(isBlackWidow, heroes); [代码] 正如大家所预期那样,JavaScript也同样提供了内置方法[代码]find[代码]给我们,因此我们最终的代码是这样的: [代码]const blackWidow = heroes.find(isBlackWidow); [代码] 总结 这些JavaScript内置的数组函数就是很好的例子,让我们学会了如何去抽象提取共同部分,以创造一个可以复用的函数。 现在我们可以用内置函数完成几乎所有的数组操作。分析一下,我们可以看出每个函数都有以下特点: 摒弃了循环的控制结构,使代码更容易阅读。 通过使用适当的方法名称描述我们正在使用的方法。 减少了处理整个数组的问题,只需要关注我们的业务代码。 在每种情况下,JavaScript的内置函数都已经将问题分解为使用小的纯函数的解决方案。通过学习这几种内置函数能让我们消除几乎所有的循环结构,这是因为我们写的几乎所有循环都是在处理数组或者构建数组或者两者都有。因此使用内置函数不仅让我们在消除循环的同时,也为我们的代码增加了不少地可维护性。 本文翻译自:JavaScript Without Loops
2020-03-11 - 适配刘海屏和全面屏的一些小心得
今年开始各路刘海和全面屏手势的手机已经开始霸占市场,全面屏和刘海屏的适配也必须提上日程。 相信大家也一定会有第一次将未适配的小程序放到全面屏或刘海屏手机上的尴尬体验。 尤其是在导航栏设置为custom时,标题与胶囊对不齐简直逼死强迫症。。 微信官方也没有出一个官方的指导贴帮助开发者。 这里仅总结一下个人关于这个问题的一些处理方式,如有疏漏烦请指正补充。 适配的关键在两个位置即额头和下巴,头不用说自然是关于刘海的。 小程序的头的高度主要分为2个部分 1.statusBarHeight 该值可以在app onLaunch 调用wx.getSystemInfoSync() 获取到 a)刘海 高度44 [图片] b)无刘海 ios高度20 安卓各不相同 [图片] 2.胶囊高度 即下图高度 [图片] 在查阅社区问答后了解到小程序给到的策略是ios在模拟器下统一是44px,ios在真机下统一是40px(感谢指正@bug之所措 ),而安卓下统一是48px,因此我们又可以在wx.getSystemInfoSync() 中获取到系统之后得到胶囊高度。 总的导航栏高度即这两个高度之合。本人项目中是将导航做成组件并给到slot,方便各个页面配置。 开发者工具 1.02.1810190 及以上版本支持在 app.json 中声明 usingComponents 字段,在此处声明的自定义组件视为全局自定义组件,在小程序内的页面或自定义组件中可以直接使用而无需再声明。 目前小程序还支持在单个页面配置custom,也可以配合使用~ 另一个需要关注的则是底部,参考的文章是 https://www.jianshu.com/p/a1e8c7cf8821 重点是在于在全面屏的手机的底部需要流出34px的空白给到全面屏返回手势操作,此外由于全面屏屏幕圆边还可能使一些按钮或功能无法正常使用。 那么首先如何判断是否是全面屏呢?个人的做法是判断屏幕高度是否大于750,iphone的plus系列高度在736,正好在这个范围之内,当然750不一定准确,如果出现疏漏烦请补充。 涉及到底部的主要是弹出的操作菜单、tabBar和底部定位的按钮等。这里做了一个简单的代码片段。 https://developers.weixin.qq.com/s/fnU0n8mv7o5M 希望能够帮助到大家,也欢迎交流~
2019-01-03