- uni-app适配自定义tabBar
版权说明 本文首发于指尖魔法屋>uni-app对微信小程序云函数的适配. 转载请附上原地址 引言:此方法可用作大部分微信小程序支持,但uni-app文档中却找不到相关说明的API 需求 需要在微信小程序中,实现一个中间图标突出显示的异形导航栏。 如下图 [图片] 实现方法设计 要做这种异形的导航栏,用直接在配置文件里面写list的方法肯定做不到。那么,就有以下两种可替代方法。 在每一个页面都加载一个tabBar组件,与页面同时渲染。 设置自定义tabBar,修改tabBar的样式。 优缺点分析:方法1实现起来略为简单,但是会出现代码可重用率低,降低性能,已经界面跳动等问题。方法2则是微信官方提供的,自定义方式,相信在性能方面也会有很大的优势。故选择方法2。 1. 查看文档及官方Demo 官方文档 简要描述一下就是需要在根目录中加入一个[代码]custom-tab-bar[代码]目录,里面的文件结构与自定义组件的结构一致。然后再在小程序配置文件中修改tabbar为custom模式。 官方代码 主要重点为三个部分 配置文件 [图片] custom-tab-bar目录 [图片] 页面生命周期中的设置索引方法 [图片] 这段代码其实很容易理解,pageLifetimes就是监听组件所在页面的生命周期。上述代码就是监听页面显示。当页面显示后,获取到tabBar的对象,然后再设置tabBar中的index索引。 2. 迁移到uni-app框架 上面的方法是使用微信小程序的开发方式,而我使用的是uni-app框架开发微信小程序的。所以我们需要把它们移植到uni-app框架内。 配置文件的修改 uni-app中,page.json被编译为微信小程序的app.json。所以,我们直接修改page.json [图片] custom-tab-bar目录的适配 我们知道,uni-app使用的是类Vue开发,将一个Vue文件编译为四个微信页面文件(wxml,wxss,json,js)。那么,是否可以直接写一个[代码]custom-tab-bar.vue[代码]的文件呢?刚开始我也是这么想的,后来发现uni-app只会编译page目录和component目录下的vue文件。而微信小程序要求[代码]custom-tab-bar[代码]必须在项目的根目录下。那么就只能在uni-app下创建一个[代码]custom-tab-bar[代码]目录,并老老实实写微信四件套了。 [图片] 写完后,uni-app会将该目录完美的复制至微信小程序项目的根目录。 tab页面内的适配方法 这个在我实际开发中,是最令我头痛的了。因为微信小程序的[代码]this[代码]引用与uni-app的[代码]this[代码]引用并不相同。所以如果直接复制代码是会编译出错的。而另一个问题则是,uni-app并未提供[代码]pageLifetimes[代码]的事件监听。 在我经过一番摸索之后,发现将设置索引方法写在onShow事件里面,效果是等效的。接下来便只剩下this的问题了。 如果直接复制的话,会出现无任何效果的情况 [图片] 因为uni-app的this引用不一样,所以它在判断[代码]getTabBar[代码]的时候,获取的是“undefined”所以不会执行下面的操作。如果你将判断去掉,则会直接报“undefined”错误。 难道实现不了?其实不然,万变不离其宗。uni-app也是编译到小程序的,所以绝对有迹可循。 我们首先看看uni-app里面this的内容。 [图片] 我们可以很明显的看到里面有个[代码]$mp[代码]的对象,说明这应该是微信小程序专用的对象。接下来我们继续分析[代码]$mp[代码]。 [图片] 这里面有一个隐藏很深的[代码]getTabBar[代码]方法,我们直接调用它,和在微信小程序里面调用[代码]this.getTabBar[代码]是等效的。 所以我们就可以把[代码]onShow[代码]里面的内容写成这样。 [图片] 一些优雅点的封装 设置索引方法独立出来 在methods对象中,添加 [代码]setTabBarIndex(index){ if (typeof this.$mp.page.getTabBar === 'function' && this.$mp.page.getTabBar()) { this.$mp.page.getTabBar().setData({ selected:index }) } } [代码] 使用[代码]mixin[代码]避免重复书写复制 在[代码]main.js[代码]中,添加 [代码]Vue.mixin({ methods:{ setTabBarIndex(index){ if (typeof this.$mp.page.getTabBar === 'function' && this.$mp.page.getTabBar()) { this.$mp.page.getTabBar().setData({ selected:index }) } } } }) [代码] 混入后的使用 在页面文件中 [代码]onShow() { this.setTabBarIndex(0) //index为当前tab的索引 } [代码] over!
2019-11-25 - 小程序进入微信卡包异常?
wx.addCard({ cardList: [{ cardId: res.cardId, cardExt: res.cardExt }], success(res) { console.log(res, '打开微信卡包后回调') // 卡券添加结果 }, fail(res) { console.log(res, '打开微信卡包失败') // 失败回调 } }) [图片] 正常打开的参数打印 pmm8_tyDfDbwFgNOWYXlftqk43Bk {"nonce_str":"12b2945b-356e-4341-b7fa-cd1fac069c80","signature":"f6d5d85ebaf087592bb98ea1f355e11f48a50f25","outer_str":"87110:0:0","timestamp":"1724411647"} 异常打开的参数打印 p2nhh5uffx2bAcuBVXXQp6B0Iv-w {"nonce_str":"cfbc44d3-d69d-4ee7-acf1-2a5dcaff5bfd","signature":"83ad234bd790a18bb7f94893ba5e8ff6040a3906","outer_str":"238086:0:0","timestamp":"1724411767"} 目前从参数看不出啥异常,除非是签名异常。异常的,进入后就是一个灰色蒙层,目前不知道原因。异常的进入后页面是这样的。 [图片][图片]
2024-08-27 - 小程序虚拟滚动实现两种方式
前言 大部分小程序都会有这样的需求,页面有长列表,需要下拉到底时请求后台数据,一直渲染数据,当数据列表长时,会发现明显的卡顿,页面白屏闪顿现象。 分析 请求后台数据,需要不断的setData,不断的合并数据,导致后期数据渲染过大. 渲染的DOM 结构多,每次渲染都会进行dom比较,相关的diff算法比较耗时都大. DOM数目多,占用的内存大,造成页面卡顿白屏. 初始渲染方法 [代码] * @Descripttion: * @version: * @Author: shijuwang * @Date: 2021-07-14 16:40:22 * @LastEditors: shijuwang * @LastEditTime: 2021-07-14 17:10:13 */ //Page Object Page({ data: { list: [], // 所有数据 }, //options(Object) onLoad: function (options) { this.index = 0 const arr = [ { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, ] this.setData({ list: arr }) }, onReachBottom: function () { const arr = [ { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, ] let { list } = this.data this.setData({ list: [...list, ...arr] }) }, }); // wxml <view class="container"> <view class="item-list" wx:for="{{list}}"> <text class=""> {{item.idx}} </text> </view> </view> [代码] 每次触底重新请求数据,合并数组并重新setData,列表负责时,会出现卡顿白屏,每次setData的数据越来越大,增加了通讯时间,也渲染了过多的dom元素,如下图: [图片] 初步优化 1.将一维数组改为二位数组 通常情况下,setData是进行了全部的数据重新渲染. 分页请求,对服务器压力相对较小,分页增量渲染对setData压力也小 setData对二维数组查找对应索引的那条数据的下标,用 setData 进行局部刷新. setData的时候,只是将当前这一屏幕的数据赋值. [代码]// wxml <view class="container"> <block wx:for="{{list}}" wx:for-index="pageNuma"> <view class="item-list" wx:for="{{item}}"> <text class="">{{item.idx}}</text> </view> </block> </view> // wx.js Page({ data: { list: [], // 所有数据 }, //options(Object) onLoad: function (options) { this.index = 0; this.currentIndex = 0; // 当前页数 pageNuma const arr = [ { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, ] this.setData({ [`list[${this.currentIndex}]`]: arr }) }, onReachBottom: function () { this.currentIndex++; // 触底+1 const arr = [ { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, ] this.setData({ [`list[${this.currentIndex}]`]: arr }) }, }); [代码] [图片] 这样我们就可以看到,整个渲染是以屏为分页来渲染,请求几个pageNum,那么就渲染几屏,没屏里再进行没个列表渲染。 进一步优化 2.我们可以只渲染可视区域的一屏、或者渲染可视区域附近的几屏,其他区域用view占位,不进行具体渲染,从而减少dom渲染,减少节点过多造成的问题。 做这一步的话需要拆分以下几步: 获取当前屏幕高度、渲染时获取每屏高度、滚动距离计算 存储获取到的全部渲染数据、存储每屏高度、通过onPageScroll计算可视区域处于那一屏 除了可视区域其他区域数据为空,用view占位 [代码] is.currentIndex = 0; // 当前页数 pageNum this.pageHeight = []; // 每屏高度存储 this.allList = []; // 获取到的所有数据 this.systemHeight = 0; // 屏幕高度 this.visualIndex = []; // 存储可视区域pageNum [代码] 进入页面时,挂载相关数据: [代码] // wxml <view wx:for="{{list}}" wx:for-index="pageNum" id="item{{pageNum}}"> <view class="item-list" wx:for="{{item}}"> <text class="">{{item.idx}}</text> </view> </view> onLoad: function (options) { this.index = 0; this.currentIndex = 0; // 当前页数 pageNum this.pageHeight = []; // 每屏高度存储 this.allList = []; // 获取到的所有数据 this.systemHeight = 0; // 屏幕高度 const arr = [ { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ }, { idx: this.index++ } ] this.setData({ [`list[${this.currentIndex}]`]: arr }, () => { this.setPageHeight(); }); this.getSystemInfo(); }, [代码] 给每一屏的view动态的设置id名字,方便在渲染时获取每屏高度,并进行存储,为后续不在可视区域占位设置高度使用 循环pageHeight,相加每个分页高度和当前滚动距离进行比较,获取到当前渲染的pageNum,然后把当前选渲染的pageNum -+1,上一屏幕和下一屏幕放入数组中,当前三个分页数据展示,其他屏幕的数据进行view占位,高度为存储高度。 [代码] // 滚动距离计算 onPageScroll: throttle(function (e) { let pageScrollTop = e[0].scrollTop; let that = this; // 滚动计算现在处于那一个分页的屏 let scrollTop = 0; let currentIndex = this.currentIndex; for (var i = 0; i < this.pageHeight.length; i++) { scrollTop = scrollTop + this.pageHeight[i]; if (scrollTop > pageScrollTop + this.systemHeight - 50) { this.currentIndex = i; this.visualIndex = [i - 1, i, i + 1]; that.setData({ visualIndex: this.visualIndex }) break; } } },200) [代码] 实时监控滚动,做节流优化处理 [代码]const throttle = (fn, interval) => { var enterTime = 0; //触发的时间 var gapTime = interval || 300; //间隔时间,如果interval不传值,默认为300ms return function () { var that = this; var backTime = new Date(); //第一次函数return即触发的时间 if (backTime - enterTime > gapTime) { fn.call(that, arguments); enterTime = backTime; //赋值给第一次触发的时间 保存第二次触发时间 } }; } [代码] 获取到可视区域数组,然后判断当前pageNum是否在visualIndex里,是的话进行渲染,不是的话,view占位,高度获取存储高度 [代码]<wxs module='filter'> var includesList = function(list,currentIndex){ if(list){ return list.indexOf(currentIndex) > -1 } } module.exports.includesList = includesList; </wxs> <view class="container"> <view wx:for="{{list}}" wx:for-index="pageNum" id="item{{pageNum}}" wx:key="pageNum"> <block wx:if="{{filter.includesList(visualIndex,pageNum)}}"> <view class="item-list" wx:for="{{item}}"> <text class="">{{item.idx}}</text> </view> </block> <block wx:else> <view class="item-visible" style="height:{{pageHeight[pageNum]}}px"></view> </block> </view> </view> [代码] 在可视区域的数组的pageNum 则循环当前数组,如果不在的话就设置高度占位,渲染效果如下: [图片] 这种方法就大功告成了! 方法二 使用微信小程序api IntersectionObserver [代码] //视图监听 observePage: function (pageNum) { const that = this; const observerView = wx.createIntersectionObserver(this).relativeToViewport({ top: 0, bottom: 0}); observerView.observe(`#item${pageNum}`, (res) => { console.log(res,'res'); if (res.intersectionRatio > 0) { that.setData({ visualIndex: [pageNum - 1, pageNum, pageNum + 1] }) } }) } [代码] 利用oberserve监听当前页面是否在在可视区域内,是的话将当前pageNum -+1,pageNum放入可视区域数组visualIndex中,在wxs进行判断可视区域数组是否存在当前pageNum,是的话渲染,不存在的话用view占位。 方案一 滚动计算 >>>代码片段:(滚动虚拟列表) 方案二 IntersectionObserver api >>> 代码片段:intersectionObserver 到此这篇关于微信小程序虚拟列表的实现示例的文章就介绍到这了,感谢大家支持!
2023-08-24 - 你可能不知道的mpvue性能优化技巧
最近一直在折腾[代码]mpvue[代码]写的微信小程序的性能优化,分享下实战的过程。 先上个优化前后的图: [图片] 可以看到打包后的代码量从[代码]813KB[代码]减少到[代码]387KB[代码],Audits体验评分从[代码]B[代码]到[代码]A[代码],效果还是比较明显的。其实这个指标说明不了什么,而且轻易就可以做到,更重要的是优化小程序运行过程中的卡顿感,请耐心往下看。 常规优化 常规的Web端优化方法在小程序中也是适用的,而且不可忽视。 一、压缩图片 这一步最简单,但是容易被忽视。在tiny上在线压缩,然后下载替换即可。 [图片] 我这项目的压缩率高达[代码]72%[代码],可以说打包后的代码从[代码]813KB[代码]降到[代码]387KB[代码]大部分都是归功于压缩图片了。 二、移除无用的库 我之前在项目中使用了Vant Weapp,在[代码]static[代码]目录下引入了整个库,但实际上我只使用了[代码]button[代码],[代码]field[代码],[代码]dialog[代码]等几个组件,实在是没必要。 所以干脆移除掉了,微信小程序自身提供的[代码]button[代码],[代码]wx.showModal[代码]等一些组件基本可以满足需求,自己手写一下样式也不用花什么时间。 在这里建议大家,在微信小程序中,尽量避免使用过多的依赖库。 不要贪图方便而引入一些比较大的库,小程序不同于[代码]Web[代码],限制比较多,能自己写一下就尽量自己写一下吧。 小程序的优化 咱们首先得看一下官方优化建议,大多是围绕这个建议去做。 一、开启Vue.config._mpTrace = true 这个是[代码]mpvue[代码]性能优化的一个黑科技啊,可能大多数同学都不知道这个,我在官方文档都没有搜到到这个配置,我真的是服了。 我能找到这个配置也是Google机缘巧合下看到的,出处:mpvue重要更新,页面更新机制进行全面升级 具体做法是在[代码]/src/main.js[代码]添加[代码]Vue.config._mpTrace = true[代码],如: [代码]Vue.config._mpTrace = true Vue.config.productionTip = false App.mpType = 'app' [代码] 添加了[代码]Vue.config._mpTrace[代码]属性,这样就可以看到console里会打印每500ms更新的数据量。如图: [图片] 如果数据更新量很大,会明显感觉小程序运行卡顿,反之就流畅。因此我们可以根据这个指标,逐步找出性能瓶颈并解决掉。 二、精简data 1. 过滤api返回的冗余数据 后端的api可能是需要同时为iOS,Android,H5等提供服务的,往往会有些冗余的数据小程序是用不到的。比如api返回的一个[代码]文章列表[代码]数据有很多字段: [代码]this.articleList = [ { articleId: 1, desc: 'xxxxxx', author: 'fengxianqi', time: 'xxx', comments: [ { userId: 2, conent: 'xxx' } ] }, { articleId: 2 // ... }, // ... ] [代码] 假设我们在小程序中只需要用到列表中的部分字段,如果不对数据做处理,将整个[代码]articleList[代码]都[代码]setData[代码]进去,是不明智的。 小程序官方文档: 单次设置的数据不能超过1024kB,请尽量避免一次设置过多的数据。 可以看出,内存是很宝贵的,当[代码]articleList[代码]数据量非常大超过1M时,某些机型就会爆掉(我在iOS中遇到过很多次)。 因此,需要将接口返回的数据剔除掉不需要的,再[代码]setData[代码],回到我们上面的[代码]articleList[代码]例子,假设我们只需要用[代码]articleId[代码]和[代码]author[代码]这两个字段,可以这样: [代码]import { getArticleList } from '@/api/article' export default { data () { return { articleList: [] } } methods: { getList () { getArticleList().then(res => { let rawList = res.list this.articleList = this.simplifyArticleList(rawList) }) }, simplifyArticleList (list) { return list.map(item => { return { articleId: item.articleId, author: item.author // 需要哪些字段就加上哪些字段 } }) } } } [代码] 这里我们将返回的数据通过[代码]simplifyArticleList[代码]来精简数据,此时过滤后的[代码]articleList[代码]中的数据类似: [代码][ {articleId: 1, author: 'fengxianqi'}, {articleId: 2, author: 'others'} // ... ] [代码] 当然,如果你的需求中是所有数据都要用到(或者大部分数据),就没必要做一层精简了,收益不大。毕竟精简数据的函数中具体的字段,是会增加维护成本的。 PS: 在我个人的实际操作中,做数据过滤虽然增加了维护的成本,但一般收益都很大,因次这个方法比较推荐。 2. data()中只放需要的数据 [代码]import xx from 'xx.js' export default { data () { return { xx, otherXX: '2' } } } [代码] 有些同学可能会习惯将[代码]import[代码]的东西都先放进[代码]data[代码]中,再在[代码]methods[代码]中使用,在小程序中可能是个不好的习惯。 因为通过[代码]Vue.config._mpTrace = true[代码]在更新某个数据时,我对比放进data和不放进data中的两种情况会有差别。 所以我猜测可能是data是会一起更新的,比如只是想更新[代码]otherXX[代码]时,会同时将[代码]xx[代码]也一起合起来[代码]setData[代码]了。 3. 静态图片放进static 这个问题和上面的问题其实是一样的,有时候我们会通过[代码]import[代码]的方式引入,比如这样: [代码]<template> <img :src="UserIcon"> </template> <script> import UserIcon from '@/assets/images/user_icon.png' export default { data () { return { UserIcon } } } </script> [代码] 这样会导致打包后的代码,图片是[代码]base64[代码]形式(很长的一段字符串)存放在[代码]data[代码]中,不利于精简data。同时当该组件多个地方使用时,每个组件实例都会携带这一段很长的[代码]base64[代码]代码,进一步导致数据的冗余。 因此,建议将静态图片放到[代码]static[代码]目录下,这样引用: [代码]<template> <img src="/static/images/user_icon.png"> </template> [代码] 代码也更简洁清爽。 看一下做了上面操作的前后对比图,使用体验上也流畅了很多。 [图片] 三、swiper优化 小程序自身提供的[代码]swiper[代码]组件性能上不是很好,使用时要注意。参考着两个思路: 【优化】解决swiper渲染很多图片时的卡顿 想请教一下小程序swiper组件的问题 在我使用时,由于需求原因,动态删掉swiper-item的思路不可行(手滑时会造成抖动)。因此只能作罢。但仍然可以优化一下: 将未显示的[代码]swiper-item[代码]中的图片用[代码]v-if[代码]隐藏到,当判断到current时才显示,防止大量图片的渲染导致的性能问题。 四、vuex使用注意事项 我之前写过的一篇mpvue开发音频类小程序踩坑和建议里面有讲如何在小程序中使用[代码]vuex[代码]。但遇到了个比较严重的性能问题。 1. 问题描述 我开发的是一个音频类的小程序,所以需要将播放列表[代码]playList[代码],当前索引[代码]currentIndex[代码]和当前时长[代码]currentTime[代码]放进[代码]state.js[代码]中: [代码]const state = { currentIndex: 0, // playList当前索引 currentTime: 0, // 当前播放的进度 playList: [], // {title: '', url: '', singer: ''} } [代码] 每次用户点击播放音频时,都会先加载音频的播放列表[代码]playList[代码],然后播放时更新当前时长[代码]currentTime[代码],发现有时候播音频时整个小程序非常卡顿。 注意到,音频需每秒就得更新一次[代码]currentTime[代码],即每秒就做一次[代码]setData[代码]操作,稍微有些卡顿是可以理解的。但我发现是播放列表数据比较多时会特别卡,比如playList的长度是100条以上时。 2. 问题原因 我开启[代码]Vue.config._mpTrace = true[代码]后发现一个规律: 当[代码]palyList[代码]数据量小时,[代码]console[代码]显示造成的数据量更新数值比较小;当[代码]playList[代码]比较大时,[代码]console[代码]显示造成的数据量更新数值比较大。 PS: 我曾尝试将playList数据量增加到200条,每500ms的数据量更新达到800KB左右。 到这里基本可以确定一个事实就是:更新[代码]state[代码]中的任何一个字段,将导致整个[代码]state[代码]全量一起[代码]setData[代码]。在我这里的例子,虽然我每次只是更新[代码]currentTime[代码]这个字段的值,但依然导致将state中的其他字段如[代码]playList[代码],[代码]currentIndex[代码]都一起做了一次[代码]setData[代码]操作。 3. 解决问题 有两个思路: 精简state中保存的数据,即限制[代码]playList[代码]的数据不能太多,可将一些数据暂存在[代码]storage[代码]中 [代码]vuex[代码]采用[代码]Module[代码]的写法能改善这个问题,虽然使用时命名空间造成一定的麻烦。vuex传送门 一般情况下,推荐使用后者。我在项目中尝试使用了前者,同样能达到很好的效果,请继续看下面的分享。 五、善用storage 1.为什么说要善用storage 由于小程序的内存非常宝贵,占用内存过大会非常卡顿,因此最好尽可能少的将数据放到内存中,即[代码]vuex[代码]存的数据要尽可能少。而小程序的[代码]storage[代码]支持单个 key允许存储的最大数据长度为 [代码]1MB[代码],所有数据存储上限为 [代码]10MB[代码]。 所以可以将一些相对取用不频繁的数据放进[代码]storage[代码]中,需要时再将这些数据放进内存,从而缓解内存的紧张,有点类似Windows中[代码]虚拟内存[代码]的概念。 2.storage换内存的实例 这个例子讲的会有点啰嗦,真正能用到的朋友可以详细看下。 上面讲到[代码]playList[代码]数据量太多,播放一条音频时其实只需要最多保证3条数据在内存中即可,即[代码]上一首[代码],[代码]播放中的[代码],[代码]下一首[代码],我们可以将多余的播放列表存放在[代码]storage[代码]中。 PS: 为了保证更平滑地连续切换下一首,我们可以稍微保存多几条,比如我这里选择保存5条数据在vuex中,播放时始终保证当前播放的音频前后都有两条数据。 [代码]// 首次播放背景音频的方法 async function playAudio (audioId) { // 拿到播放列表,此时的playList最多只有5条数据。getPlayList方法看下面 const playList = await getPlayList(audioId) // 当前音频在vuex中的currentIndex const currentIndex = playList.findIndex(item => item.audioId === audioId) // 播放背景音频 this.audio = wx.getBackgroundAudioManager() this.audio.title = playList[currentIndex].title this.audio.src = playList[currentIndex].url // 通过mapActions将播放列表和currentIndex更新到vuex中 this.updateCurrentIndex(index) this.updatePlayList(playList) // updateCurrentIndex和updatePlayList是vuex写好的方法 } // 播放音频时获取播放列表的方法,将所有数据存在storage,然后返回当前音频的前后2条数据,保证最多5条数据 import { loadPlayList } from '@/api/audio' async function getPlayList (courseId, currentAudioId) { // 从api中请求得到播放列表 // loadPlayList是api的方法, courseId是获取列表的参数,表示当前课程下的播放列表 let rawList = await loadPlayList(courseId) // simplifyPlayList过滤掉一些字段 const list = this.simplifyPlayList(rawList) // 将列表存到storage中 wx.setStorage({ key: 'playList', data: list }) return subPlayList(list, currentAudioId) } [代码] 重点是[代码]subPlayList[代码]方法,这个方法保证了拿到的播放列表是最多5条数据。 [代码]function subPlayList(playList, currentAudioId) { let tempArr = [...playList] const count = 5 // 保持vuex中最多5条数据 const middle = parseInt(count / 2) // 中点的索引 const len = tempArr.length // 如果整个原始的播放列表本来就少于5条数据,说明不需要裁剪,直接返回 if (len <= count) { return tempArr } // 找到当前要播放的音频的所在位置 const index = tempArr.findIndex(item => item.audioId === currentAudioId) // 截取当前音频的前后两条数据 tempArr = tempArr.splice(Math.max(0, Math.min(len - count, index - middle)), count) return tempArr } [代码] [代码]tempArr.splice(Math.max(0, index - middle), count)[代码]可能有些同学比较难理解,需要仔细琢磨一下。假设[代码]playList[代码]有10条数据: 当前音频是列表中的第1条(索引是0),截取前5个:[代码]playList.splice(0, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]0[代码],没有[代码]上一首[代码],有4个[代码]下一首[代码] 当前音频是列表中的第2条(索引是1),截取前5个:[代码]playList.splice(0, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]1[代码],有1个[代码]上一首[代码],3个[代码]下一首[代码] 当前音频是列表中的第3条(索引是2),截取前5个:[代码]playList.splice(0, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]2[代码],有2个[代码]上一首[代码],2个[代码]下一首[代码] 当前音频是列表中的第4条(索引是3),截取第1到6个:[代码]playList.splice(1, 5)[代码] ,此时[代码]currentAudio[代码]在这5个数据的索引是[代码]2[代码],有2个[代码]上一首[代码],2个[代码]下一首[代码] 当前音频是列表中的第5条(索引是4),截取第2到7个:[代码]playList.splice(2, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]2[代码],有2个[代码]上一首[代码],2个[代码]下一首[代码] … 当前音频是列表中的第9条(索引是[代码]8[代码]),截取后5个:[代码]playList.splice(4, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]3[代码],有3个[代码]上一首[代码],1个[代码]下一首[代码] 当前音频是列表中的最后1条(索引是[代码]9[代码]),截取后的5个:[代码]playList.splice(4, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]4[代码],有4个[代码]上一首[代码],没有[代码]下一首[代码] 有点啰嗦,感兴趣的同学仔细琢磨下,无论当前音频在哪,都始终保证了拿到当前音频前后的最多5条数据。 接下来就是维护播放上一首或下一首时保证当前vuex中的[代码]playList[代码]始终是包含当前音频的前后2条。 播放下一首 [代码]function playNextAudio() { const nextIndex = this.currentIndex + 1 if (nextIndex < this.playList.length) { // 没有超出数组长度,说明在vuex的列表中,可以直接播放 this.audio = wx.getBackgroundAudioManager() this.audio.src = this.playList[nextIndex].url this.audio.title = this.playList[nextIndex].title this.updateCurrentIndex(nextIndex) // 当判断到已经到vuex的playList的边界了,重新从storage中拿数据补充到playList if (nextIndex === this.playList.length - 1 || nextIndex === 0) { // 拿到只有当前音频前后最多5条数据的列表 const newList = getPlayList(this.playList[nextIndex].courseId, this.playList[nextIndex].audioId) // 当前音频在这5条数据中的索引 const index = newList.findIndex(item => item.audioId === this.playList[nextIndex].audioId) // 更新到vuex this.updateCurrentIndex(index) this.updatePlayList(newList) } } } [代码] 这里的[代码]getPlayList[代码]方法是上面讲过的,本来是从api中直接获取的,为了避免每次都从api直接获取,所以需要改一下,先读storage,若无则从api获取: [代码]import { loadPlayList } from '@/api/audio' async function getPlayList (courseId, currentAudioId) { // 先从缓存列表中拿 const playList = wx.getStorageSync('playList') if (playList && playList.length > 0 && courseId === playList[0].courseId) { // 命中缓存,则从直接返回 return subPlayList(playList, currentAudioId) } else { // 没有命中缓存,则从api中获取 const list = await loadPlayList(courseId) wx.setStorage({ key: 'playList', data: list }) return subPlayList(list, currentAudioId) } } [代码] 播放上一首也是同理,就不赘述了。 PS: 将vuex中的数据精简后,我所做的小程序在播放音频时刷其他页面已经非常流畅啦,效果非常好。 六、动画优化 这个问题在mpvue开发音频类小程序踩坑和建议已经讲过了,感兴趣的可以移步看一眼,这里只写下概述: 如果要使用动画,尽量用css动画代替wx.createAnimation 使用css动画时建议开启硬件加速 最后 大致总结一下上面所讲的几个要点: 开发时打开[代码]Vue.config._mpTrace = true[代码]。 谨慎引入第三方库,权衡收益。 添加数据到data中时要克制,能精简尽量精简。 图片记得要压缩,图片在显示时才渲染。 vuex保持数据精简,必要时可先存storage。 性能优化是一个永不止步的话题,我也还在摸索,不足之处还请大家指点和分享。 欢迎关注,会持续分享前端实战中遇到的一些问题和解决办法。
2019-05-15 - scroll-view的下拉刷新上拉加载
- 需求的场景描述(希望解决的问题) 实现小程序的下拉刷新上拉加载更多功能 - 希望提供的能力 目前小程序的上拉加载下拉刷新有2种解决办法 利用小程序自带的的下拉刷新,和触底事件实现上拉加载下拉刷新。 利用scroll-view实现 由于是订单列表页面设计到选项卡的切换,所以选择了scroll-view来实现。sroll-view是上拉加载更多是可以实现,但下拉刷新出现问题。scroll-vew下拉刷新时会出现需要向上滑动一下,才能下拉,否则向下滑不动。这个问题之前在博客上也看到了。请问大佬有解决的方案么。或者可以提供个更实用与选项卡的上拉加载下拉刷新实现方式。 感谢了。
2019-03-19 - TypeScript入门完全指南(基础篇)
[TOC] 为什么JS需要类型检查 TypeScript的设计目标在这里可以查看到,简单概括为两点: 为JavaScript提供一个可选择的类型检查系统; 为JavaScript提供一个包含将来新特性的版本。 TypeScript的核心价值体现在第一点,第二点可以认为是TypeScript的向后兼容性保证,也是TypeScript必须要做到的。 那么为什么JS需要做静态类型检查呢?在几年前这个问题也许还会存在比较大的争议,在前端日趋复杂的今天,经过像Google、Microsoft、FaceBook这样的大公司实践表明,类型检查对于代码可维护性和可读性是有非常大的帮助的,尤其针对于需要长期维护的规模性系统。 TypeScript优势 在我看来,TypeScript能够带来最直观上的好处有三点: 帮助更好地重构代码; 类型声明本身是最好查阅的文档。 编辑器的智能提示更加友好。 一个好的代码习惯是时常对自己写过的代码进行小的重构,让代码往更可维护的方向去发展。然而对于已经上线的业务代码,往往测试覆盖率不会很高,当我们想要重构时,经常会担心自己的改动会产生各种不可预知的bug。哪怕是一个小的重命名,也有可能照顾不到所有的调用处造成问题。 如果是一个TypeScript项目,这种担心就会大大降低,我们可以依赖于TypeScript的静态检查特性帮助找出一个小的改动(如重命名)带来的其他模块的问题,甚至对于模块文件来说,我们可以直接借助编辑器的能力进行[代码]“一键重命名”[代码]操作。 另外一个问题,如果你接手过一个老项目,肯定会头痛于各种文档的缺失和几乎没有注释的代码,一个好的TypeScript项目,是可以做到代码即文档的,通过声明文件我们可以很好地看出各个字段的含义以及哪些是前端必须字段: [代码]// 砍价用户信息 export interface BargainJoinData { curr_price: number; // 当前价 curr_ts: number; // 当前时间 init_ts: number; // 创建时间 is_bottom_price: number; // 砍到底价 } [代码] TypeScript对开发者是友好的 TypeScript在设计之初,就确定了他们的目标并不是要做多么严格完备的类型强校验系统,而是能够更好地兼容JS,更贴合JS开发者的开发习惯。可以说这是MS的商业战略,也是TS能够成功的关键性因素之一。它对JS的兼容性主要表现为以下三个方面: 隐式的类型推断 [代码]var foo = 123; foo = "456"; // Error: cannot assign `string` to `number` [代码] 当我们对一个变量或函数等进行赋值时,TypeScript能够自动推断类型赋予变量,TypeScript背后有非常强大的自推断算法帮助识别类型,这个特性无疑可以帮助我们简化一些声明,不必像其他语言那样处处是声明,也可以让我们看代码时更加轻松。 结构化的类型 TypeScript旨在让JS开发者更简单地上手,因此将类型设计为“结构化”(Structural)的而非“名义式”(Nominal)的。 什么意思呢?意味着TypeScript的类型并不根据定义的名字绑定,只要是形似的类型,不管名称相不相同,都可以作为兼容类型(这很像所谓的duck typing),也就是说,下面的代码在TypeScript中是完全合法的: [代码]class Foo { method(input: string) { /* ... */ } } class Bar { method(input: string) { /* ... */ } } let test: Foo = new Bar(); // no Error! [代码] 这样实际上可以做到类型的最大化复用,只要形似,对于开发者也是最好理解的。(当然对于这个示例最好的做法是抽出一个公共的interface) 知名的JS库支持 TypeScript有强大的DefinitelyTyped社区支持,目前类型声明文件基本上已经覆盖了90%以上的常用JS库,在编写代码时我们的提示是非常友好的,也能做到安全的类型检查。(在使用第三方库时,可以现在这个项目中检索一下有没有该库的TS声明,直接引入即可) 回顾两个基础知识 在进入正式的TS类型介绍之前,让我们先回顾一下JS的两个基础: 相等性判断 我们都知道,在JS里,两个等号的判断会进行隐式的类型转换,如: [代码]console.log(5 == "5"); // true console.log(0 == ""); // true [代码] 在TS中,因为有了类型声明,因此这两个结果在TS的类型系统中恒为false,因此会有报错: [代码]This condition will always return 'false' since the types '5' and '"5"' have no overlap. [代码] 所以在代码层面,一方面我们要避免这样两个不同类型的比较,另一方面使用全等来代替两个等号,保证在编译期和运行期具有相同的语义。 对于TypeScript而言,只有[代码]null[代码]和[代码]undefined[代码]的隐式转换是合理的: [代码]console.log(undefined == undefined); // true console.log(null == undefined); // true console.log(0 == undefined); // false console.log('' == undefined); // false console.log(false == undefined); // false [代码] 类(Class) 对于ES6的Class,我们本身已经很熟悉了,值得一提的是,目前对于类的静态属性、成员属性等有一个提案——proposal-class-fields已经进入了Stage3,这个提案包含了很多东西,主要是类的静态属性、成员属性、公有属性和私有属性。其中,私有属性的提案在社区内引起了非常大的争议,由于它的丑陋和怪异遭受各路人马的抨击,现TC39委员会已决定重新思考该提案。 现在让我们来看看TypeScript对属性访问控制的情况: 可访问性 public protected private 类本身 是 是 是 子类 是 是 否 类的实例 是 否 否 可以看到,TS中的类成员访问和其他语言非常类似: [代码]class FooBase { public x: number; private y: number; protected z: number; } [代码] 对于类的成员构造函数初始化,TS提供了一个简单的声明方式: [代码]class Foo { constructor(public x:number) { } } [代码] 这段代码和下面是等同的: [代码]class Foo { x: number; constructor(x:number) { this.x = x; } } [代码] TS类型系统基础 基本性准则 在正式了解TypeScript之前,首先要明确两个基本概念: TypeScript的类型系统设计是可选的,意味着JavaScript就是TypeScript。 TypeScript的报错并不会阻止JS代码的生成,你可以渐进式地将JS逐步迁移为TS。 基本语法 [代码]:<TypeAnnotation> [代码] TypeScript的基本类型语法是在变量之后使用冒号进行类型标识,这种语法也揭示了TypeScript的类型声明实际上是可选的。 原始值类型 [代码]var num: number; var str: string; var bool: boolean; [代码] TypeScript支持三种原始值类型的声明,分别是[代码]number[代码]、[代码]string[代码]和[代码]boolean[代码]。 对于这三种原始值,TS同样支持以它们的字面量为类型: [代码]var num: 123; var str: '123'; var bool: true; [代码] 这类字面量类型配合上联合类型还是十分有用的,我们后面再讲。 数组类型 对于数组的声明也非常简单,只需要加上一个中括号声明类型即可: [代码]var boolArray: boolean[]; [代码] 以上就简单地定义了一个布尔类型的数组,大多数情况下,我们数组的元素类型是固定的,如果我们数组内存在不同类型的元素怎么办? 如果元素的个数是已知有限的,可以使用TS的元组类型: [代码]var nameNumber: [string, number]; [代码] 该声明也非常的形象直观,如果元素个数不固定且类型未知,这种情况较为罕见,可直接声明成any类型: [代码]var arr: any[] [代码] 接口类型 接口类型是TypeScript中最常见的组合类型,它能够将不同类型的字段组合在一起形成一个新的类型,这对于JS中的对象声明是十分友好的: [代码]interface Name { first: string; second: string; } var personName:Name = { first: '张三' } // Property 'second' is missing in type '{ first: string; }' but required in type 'Name' [代码] 上述例子可见,TypeScript对每一个字段都做了检查,若未定义接口声明的字段(非可选),则检查会抛出错误。 内联接口 对于对象来说,我们也可以使用内联接口来快速声明类型: [代码]var personName:{ first: string, second: string } = { first: '张三' } // Property 'second' is missing in type '{ first: string; }' but required in type 'Name' [代码] 内联接口可以帮助我们快速声明类型,但建议谨慎使用,对于可复用以及一般性的接口声明建议使用interface声明。 索引类型 对于对象而言,我们可以使用中括号的方式去存取值,对TS而言,同样支持相应的索引类型: [代码]interface Foo { [key:string]: number } [代码] 对于索引的key类型,TypeScript只支持[代码]number[代码]和[代码]string[代码]两种类型,且Number是string的一种特殊情况。 对于索引类型,我们在一般化的使用场景上更方便: [代码]interface NestedCSS { color?: string; nest?: { [selector: string]: NestedCSS; } } const example: NestedCSS = { color: 'red', nest: { '.subclass': { color: 'blue' } } } [代码] 类的接口 对于接口而言,另一个重要作用就是类可以实现接口: [代码]interface Point { x: number; y: number; z: number; // New member } class MyPoint implements Point { // ERROR : missing member `z` x: number; y: number; } [代码] 对类而言,实现接口,意味着需要实现接口的所有属性和方法,这和其他语言是类似的。 函数类型 函数是TypeScript中最常见的组成单元: [代码]interface Foo { foo: string; } // Return type annotated as `: Foo` function foo(sample: Foo): Foo { return sample; } [代码] 对于函数而言,本身有参数类型和返回值类型,都可进行声明。 可选参数 对于参数,我们可以声明可选参数,即在声明之后加一个问号: [代码]function foo(bar: number, bas?: string): void { // .. } [代码] void和never类型 另外,上述例子也表明,当函数没有返回值时,可以用[代码]void[代码]来表示。 当一个函数永远不会返回时,我们可以声明返回值类型为[代码]never[代码]: [代码]function bar(): never { throw new Error('never reach'); } [代码] callable和newable 我们还可以使用接口来定义函数,在这种函数实现接口的情形下,我们称这种定义为[代码]callable[代码]: [代码]interface Complex { (bar?: number, ...others: boolean[]): number; } var foo: Complex; [代码] 这种定义方式在可复用的函数声明中非常有用。 callable还有一种特殊的情况,该声明中指定了[代码]new[代码]的方法名,称之为[代码]newable[代码]: [代码]interface CallMeWithNewToGetString { new(): string } var foo: CallMeWithNewToGetString; new foo(); [代码] 这个在构造函数的声明时非常有用。 函数重载 最后,一个函数可以支持多种传参形式,这时候仅仅使用可选参数的约束可能是不够的,如: [代码]unction padding(a: number, b?: number, c?: number, d?: number) { if (b === undefined && c === undefined && d === undefined) { b = c = d = a; } else if (c === undefined && d === undefined) { c = a; d = b; } return { top: a, right: b, bottom: c, left: d }; } [代码] 这个函数可以支持四个参数、两个参数和一个参数,如果我们粗略的将后三个参数都设置为可选参数,那么当传入三个参数时,TS也会认为它是合法的,此时就失去了类型安全,更好的方式是声明函数重载: [代码]function padding(all: number); function padding(topAndBottom: number, leftAndRight: number); function padding(top: number, right: number, bottom: number, left: number); function padding(a: number, b?: number, c?: number, d?: number) { //... } [代码] 函数重载写法也非常简单,就是重复声明不同参数的函数类型,最后一个声明包含了兼容所有重载声明的实现。这样,TS类型系统就能准确的判断出该函数的多态性质了。 使用[代码]callable[代码]的方式也可以声明重载: [代码]interface Padding { (all: number): any (topAndBottom: number, leftAndRight: number): any (top: number, right: number, bottom: number, left: number): any } [代码] 特殊类型 any [代码]any[代码]在TypeScript中是一个比较特殊的类型,声明为[代码]any[代码]类型的变量就像动态语言一样不受约束,好像关闭了TS的类型检查一般。对于[代码]any[代码]类型的变量,可以将其赋予任何类型的值: [代码]var power: any; power = '123'; power = 123; [代码] [代码]any[代码]对于JS代码的迁移是十分友好的,在已经成型的TypeScript项目中,我们要慎用[代码]any[代码]类型,当你设置为[代码]any[代码]时,意味着告诉编辑器不要对它进行任何检查。 null和undefined [代码]null[代码]和[代码]undefined[代码]作为TypeScript的特殊类型,它同样有字面量的含义,之前我们已经了解到。 值得注意的是,[代码]null[代码]和[代码]undefined[代码]可以赋值给任意类型的变量: [代码]var num: number; var str: string; // 赋值给任意类型的变量都是合法的 num = null; str = undefined; [代码] void和never 在函数类型中,我们已经介绍了两种类型,专门修饰函数返回值。 readonly [代码]readonly[代码]是只读属性的修饰符,当我们的属性是只读时,可以用该修饰符加以约束,在类中,用[代码]readonly[代码]修饰的属性仅可以在构造函数中初始化: [代码]class Foo { readonly bar = 1; // OK readonly baz: string; constructor() { this.baz = "hello"; // OK } } [代码] 一个实用场景是在[代码]react[代码]中,[代码]props[代码]和[代码]state[代码]都是只读的: [代码]interface Props { readonly foo: number; } interface State { readonly bar: number; } export class Something extends React.Component<Props,State> { someMethod() { this.props.foo = 123; // ERROR: (props are immutable) this.state.baz = 456; // ERROR: (one should use this.setState) } } [代码] 当然,[代码]React[代码]本身在类的声明时会对传入的[代码]props[代码]和[代码]state[代码]做一层[代码]ReadOnly[代码]的包裹,因此无论我们是否在外面显式声明,赋值给[代码]props[代码]和[代码]state[代码]的行为都是会报错的。 注意,[代码]readonly[代码]听起来和[代码]const[代码]有点像,需要时刻保持一个概念: [代码]readonly[代码]是修饰属性的 [代码]const[代码]是声明变量的 泛型 在更加一般化的场景,我们的类型可能并不固定已知,它和[代码]any[代码]有点像,只不过我们希望在[代码]any[代码]的基础上能够有更近一步的约束,比如: [代码]function reverse<T>(items: T[]): T[] { var toreturn = []; for (let i = items.length - 1; i >= 0; i--) { toreturn.push(items[i]); } return toreturn; } [代码] [代码]reverse[代码]函数是一个很好的示例,对于一个通用的函数[代码]reverse[代码]来说,数组元素的类型是未知的,可以是任意类型,但[代码]reverse[代码]函数的返回值也是个数组,它和传入的数组类型是相同的,对于这个约束,我们可以使用泛型,其语法是尖括号,内置泛型变量,多个泛型变量用逗号隔开,泛型变量名称没有限制,一般而言我们以大写字母开头,多个泛型变量使用其语义命名,加上[代码]T[代码]为前缀。 在调用时,可以显示的指定泛型类型: [代码]var reversed = reverse<number>([1, 2, 3]); [代码] 也可以利用TypeScript的类型推断,进行隐式调用: [代码]var reversed = reverse([1, 2, 3]); [代码] 由于我们的参数类型是[代码]T[][代码],而传入的数组类型是一个[代码]number[][代码],此时[代码]T[代码]的类型被TypeScript自动推断为[代码]number[代码]。 对于泛型而言,我们同样可以作用于接口和类: [代码]interface Array<T> { reverse(): T[]; // ... } [代码] 联合类型 在JS中,一个变量的类型可能拥有多个,比如: [代码]function formatCommandline(command: string[]|string) { var line = ''; if (typeof command === 'string') { line = command.trim(); } else { line = command.join(' ').trim(); } } [代码] 此时我们可以使用一个[代码]|[代码]分割符来分割多种类型,对于这种复合类型,我们称之为[代码]联合类型[代码]。 交叉类型 如果说联合类型的语义等同于[代码]或者[代码],那么交叉类型的语义等同于集合中的[代码]并集[代码],下面的[代码]extend[代码]函数是最好的说明: [代码]function extend<T, U>(first: T, second: U): T & U { let result = <T & U> {}; for (let id in first) { result[id] = first[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { result[id] = second[id]; } } return result; } [代码] 该函数最终以[代码]T&U[代码]作为返回值值,该类型既包含了[代码]T[代码]的字段,也包含了[代码]U[代码]的字段,可以看做是两个类型的[代码]并集[代码]。 类型别名 TypeScript为类型的复用提供了更便捷的方式——类型别名。当你想复用类型时,可能在该场景下要为已经声明的类型换一个名字,此时可以使用type关键字来进行类型别名的定义: [代码]interface state { a: 1 } export type userState = state; [代码] 我们同样可以使用type来声明一个类型: [代码]type Text = string | { text: string }; type Coordinates = [number, number]; type Callback = (data: string) => void; [代码] 对于type和interface的取舍: 如果要用交叉类型或联合类型,使用type。 如果要用extend或implement,使用interface。 其余情况可看个人喜好,个人建议type更多应当用于需要起别名时,其他情况尽量使用interface。 枚举类型 对于组织一系列相关值的集合,最好的方式应当是枚举,比如一系列状态集合,一系列归类集合等等。 在TypeScript中,枚举的方式非常简单: [代码]enum Color { Red, Green, Blue } var col = Color.Red; [代码] 默认的枚举值是从0开始,如上述代码,[代码]Red=0[代码],[代码]Green=1[代码]依次类推。 当然我们还可以指定初始值: [代码]enum Color { Red = 3, Green, Blue } [代码] 此时[代码]Red=3[代码], [代码]Green=4[代码]依次类推。 大家知道在JavaScript中是不存在枚举类型的,那么TypeScript的枚举最终转换为JavaScript是什么样呢? [代码]var Color; (function (Color) { Color[Color["Red"] = 0] = "Red"; Color[Color["Green"] = 1] = "Green"; Color[Color["Blue"] = 2] = "Blue"; })(Color || (Color = {})); [代码] 从编译后的代码可以看到,转换为一个key-value的对象后,我们的访问也非常方便: [代码]var red = Color.Red; // 0 var redKey = Color[0]; // 'Red' var redKey = Color[Color.Red]; // 'Red' [代码] 既可以通过key来访问到值,也可以通过值来访问到key。 Flag标识位 对于枚举,有一种很实用的设计模式是使用位运算来标识(Flag)状态: [代码]enum EnvFlags { None = 0, QQ = 1 << 0, Weixin = 1 << 1 } function initShare(flags: EnvFlags) { if (flags & EnvFlags.QQ) { initQQShare(); } if (flags & EnvFlags.Weixin) { initWeixinShare(); } } [代码] 在我们使用标识位时,可以遵循以下规则: 使用 [代码]|=[代码] 增加标志位 使用 [代码]&=[代码] 和 [代码]~[代码]清除标志位 使用 [代码]|[代码] 联合标识位 如: [代码]var flag = EnvFlags.None; flag |= EnvFlags.QQ; // 加入QQ标识位 Flag &= ~EnvFlags.QQ; // 清除QQ标识位 Flag |= EnvFlags.QQ | EnvFlags.Weixin; // 加入QQ和微信标识位 [代码] 常量枚举 在枚举定义加上[代码]const[代码]声明,即可定义一个常量枚举: [代码]enum Color { Red = 3, Green, Blue } [代码] 对于常量枚举,TypeScript在编译后不会产生任何运行时代码,因此在一般情况下,应当优先使用常量枚举,减少不必要代码的产生。 字符串枚举 TypeScript还支持非数字类型的枚举——字符串枚举 [代码]export enum EvidenceTypeEnum { UNKNOWN = '', PASSPORT_VISA = 'passport_visa', PASSPORT = 'passport', SIGHTED_STUDENT_CARD = 'sighted_tertiary_edu_id', SIGHTED_KEYPASS_CARD = 'sighted_keypass_card', SIGHTED_PROOF_OF_AGE_CARD = 'sighted_proof_of_age_card', } [代码] 这类枚举和我们之前使用JavaScript定义常量集合的方式很像,好处在于调试或日志输出时,字符串比数字要包含更多的语义。 命名空间 在没有模块化的时代,我们为了防止全局的命名冲突,经常会以命名空间的形式组织代码: [代码](function(something) { something.foo = 123; })(something || (something = {})) [代码] TypeScript内置了[代码]namespace[代码]变量帮助定义命名空间: [代码]namespace Utility { export function log(msg) { console.log(msg); } export function error(msg) { console.error(msg); } } [代码] 对于我们自己的工程项目而言,一般建议使用ES6模块的方式去组织代码,而命名空间的模式可适用于对一些全局库的声明,如jQuery: [代码]namespace $ { export function ajax(//...) {} } [代码] 当然,命名空间还可以便捷地帮助我们声明静态方法,如和[代码]enum[代码]的结合使用: [代码]enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } namespace Weekday { export function isBusinessDay(day: Weekday) { switch (day) { case Weekday.Saturday: case Weekday.Sunday: return false; default: return true; } } } const mon = Weekday.Monday; const sun = Weekday.Sunday; console.log(Weekday.isBusinessDay(mon)); // true console.log(Weekday.isBusinessDay(sun)); // false [代码] 关于命名规范 变量名、函数和文件名 推荐使用驼峰命名。 [代码]// Bad var FooVar; function BarFunc() { } // Good var fooVar; function barFunc() { } [代码] 类、命名空间 推荐使用帕斯卡命名。 成员变量和方法推荐使用驼峰命名。 [代码]// Bad class foo { } // Good class Foo { } // Bad class Foo { Bar: number; Baz() { } } // Good class Foo { bar: number; baz() { } } [代码] Interface、type 推荐使用帕斯卡命名。 成员字段推荐使用驼峰命名。 [代码]// Bad interface foo { } // Good interface Foo { } // Bad interface Foo { Bar: number; } // Good interface Foo { bar: number; } [代码] 关于模块规范 [代码]export default[代码]的争论 关于是否应该使用[代码]export default[代码]在这里有详尽的讨论,在AirBnb规范中也有[代码]prefer-default-export[代码]这条规则,但我认为在TypeScript中应当尽量不使用[代码]export default[代码]: 关于链接中提到的重命名问题, 甚至自动import,其实export default也是可以做到的,借助编辑器和TypeScript的静态能力。所以这一点还不是关键因素。 不过使用一般化的[代码]export[代码]更让我们容易获得智能提示: [代码]import /* here */ from 'something'; [代码] 在这种情况下,一般编辑器是不会给出智能提示的。 而这种: [代码]import { /* here */ } from 'something'; [代码] 我们可以通过智能提示做到快速引入。 除了这一点外,还有以下几点好处: 对CommonJS是友好的,如果使用export default,在commonJS下需要这样引入: [代码]const {default} = require('module/foo'); [代码] 多了个default无疑感觉非常奇怪。 对动态import是友好的,如果使用export default,还需要显示的通过default字段来访问: [代码]const HighChart = await import('https://code.highcharts.com/js/es-modules/masters/highcharts.src.js'); Highcharts.default.chart('container', { ... }); // 注意 `.default` [代码] 对于[代码]re-exporting[代码]是友好的,如果使用export default,那么进行[代码]re-export[代码]会比较麻烦: [代码]import Foo from "./foo"; export { Foo } [代码] 相比之下,如果没有[代码]export default[代码],我们可以直接使用: [代码]export * from "./foo" [代码] 实践中的一些坑 实践篇即将到来,敬请期待~
2019-03-18 - checkbox的样式无法修改
- 当前 Bug 的表现(可附上截图) - 预期表现 - 复现路径 - 提供一个最简复现 Demo 其他的是生效的。伪类那是不生效的,用display:none;也不起作用。求大佬解答。 [图片]
2019-03-18