个人案例
暂无发表的小程序案例
- 使用并发复合命令模式对齐代码执行点
[视频] 你好,我是李艺。 上节课我们主要学习了异步转同步的编程范式,这节课学习使用复合命令模式对齐代码的执行点。 复合命令模式是一个设计模式,它和异步转同步的编程范式一样主要作用在于使我们这个代码结构变得更加清晰 易于维护,其次它还可以优化代码的调用逻辑,统筹安排代码的一个执行时机。 首先我们看一下问题,在编程开发里面,尤其在JS语言的异步编程里面经常会涉及这样一个场景,条件A和条件B全部达成以后才可以执行这个代码C,并且条件A和条件B它不一定是在一个地方,例如不在一个页面里面它们达成的先后的时机也不确定,完全具有随机性,在这种场景之下我们绝不能采用排队的方式,先达成条件A然后再达成条件B 或者是反过来,这样都是不行,因为这样本来可以并发提早完成的这个事情人为的被我们延后了。我们应该怎么办?可以使用并发的复合命令模式,将两个或者是多个操作封装成命令对象,让它们并发竞速完成,当它们全部完成以后对齐这个时间点,再执行下一步的逻辑代码。接下来看项目实践。 首先看实践一,实现并发的复合命令模式。 在我们源码里面,在optimus/command目录下面需要实现一套命令模式,包括普通的单一命令、闭包函数命令还有并发命令和串发命令,并发命令ParallelCommand对象是我们将要用到的一个类对象。 接下来我们看实践一的代码演示。 我们需要在这个library下面,这个optimus这个目录下添加一个子目录叫做command,所有的命令对象全部在这个目录下进行定义,为了编码方便我们仍然使用VSCode进行编写,首先我们要将基础的一个类把它给定义出来,找到我们的最终源码里面 library然后optimus command,基础类command,这是我们那个基础类,我们把这个文件给它拷贝过来放在command目录下面,这个里面我们间接地还引入了一个event_dispatcher,这样的基类它是一个事件派发对象,它的实现其实很简单,实现的是一个观察者模式,一个观察者模式 主要的方法是on就是监听事件,off是移除事件监听,然后once这个是只监听一次事件,emit是发布一个订阅通知,也就是派发事件,通知其他的监听者开始执行代码,我们在这个地方Command其实因为我们要监听和派发事件,所以我们让它继承于我们EventDispatcher。 这个里面我们简单看一下,有临时数据仓,然后有一个只读的一个属性complete代表的是否已完成,这个是一个excute是执行 是一个很关键的方法,调用这个方法代表我们当前的命令对象开始执行,markComplete是标记我们当前的命令完成,这个地方是一个onComplete,是添加一个事件的监听,因为EventDispatcher它本身也是有on这个方法的,但是我们为了方便的话,这个地方多加了一个方法可以直接调这个onComplete然后监听,这个事件的名称就不需要写了。 同时这个地方我们还做了一个,就是在我们添加监听的时候,如果你本身这个事件它已经完成了,直接就派发一个complete的事件,然后监听这个事件的代码都可以得到通知,最后面是一个dispose,它是释放,首先我们调父类里面的off,将我们所有的监听全部去掉,再往下就是要把data数据对象然后置空,这个过程其实是为了方便GC进行垃圾内存回收的这样的一个设置,这是基类,有了基类以后接下来我们还要去定义复合类型。 第一个类型我们看一下这个目录,是CommandGroup,这个类会继承于我们Command,我们将这个类先给它拷贝一下,同时还有另外还有两个,一个是parallel_command,还有一个serial_command,同时我们也拷贝一下,我们先看command_group。CommandGroup继承于它,它主要的一个不同是它是可以复合的,它里面有一个subCommands这样的一个数组,在这个里面我们可以添加子命令,但是它同时它本身也是一个Command,它也是可以执行的,当它执行的时候它怎么样去做事情呢,其实它相当于是所有的子命令然后依次去执行,但是执行的策略不是它本身决定的,而是在它的子类里面。 这个地方有两个子类,一个是parallel是并发复合命令对象,在这个里面,我们可以看到这ParallelCommand它继承于我们的CommandGroup,继承以后,我们在这个地方主要是重写了execute的方法,也就是执行方法,它是并发的是相当于我们要让所有的子命令全部的同时的去执行,然后执行我们还要去监听它是否完成,完成以后我们要将完成的数字加1 加1,直到所有完成的数字达到了我们子命令的数字 就是个数,所有子命令都全部完成以后再将我们自身的命令,就复合命令,它作为一个命令,然后再把它的markComplete这个方法调用,调用就代表把它置为已完成的这样一种状态。 这个方法里面我们还用了一个Iterator,这是一个迭代器,迭代器的实现其实也在我们的里面,大概是在这个地方 这个实现其实也很简单,它主要是传入一个可迭代的对象,可以传ObjectArray String Set Map和TypedArray,传进来以后去调它这个方法进行一个迭代 这样的实现,这是Parallel它的规则就是所有的子命令一起执行,直到最后一个完成,当然这个里面它还有一种竞赛模式,isRaceMode它默认等于false,如果我们将它改成true的话,稍后我们会看到,当然这个模式稍后我们还会用到,如果我们将它改成true的话,只要有一个子命令它完成了就意味着我们复合命令的完成,这是它的策略的小小的改变,另外一个serial_command,这是一个串发复合命令对象,在这个里面我们看它的execute的方法,也是拿iterator一个迭代器,跟刚才复合命令对象最大的一个不同就是它的执行策略,它是一个一个执行 就是排队 一个个执行按顺序来,前面的先执行,前面执行了后面再执行,直到后面的一个子命令执行完成以后才视为整体的串发的复合命令对象,然后执行完成了,就是这样的一个策略,这是复合的命令对象。 下面还有两个,一个是closure_command,还有一个sync_closure_command,这两个文件我们也给它拿过来,简单说一下这个closure_command是什么意思,就是我们可以传一个闭包,传个闭包,然后execute的时候,就是执行的时候我们让闭包去执行,但是什么时候完成,其实完成也是我们在外部调用的,我们外部可以调用它的markComplete的方法来标记一个指令对象的完成,另外还有一个sync_closure_command ,这个是另外的一个,相当于是一个异步的,就是同步的一个同步闭包命令,它的特点就是执行以后它可以马上完成,所以我们这个地方看到它execute的方法会重写了父类以后马上调用它自身的markComplete,就把自身给它标记为完成了,这个类型适合我们做一些不占用时间的,它执行以后马上就可以完成了,用于在一个比较长的执行序列里边,去在中间去执行一些额外的一些事情 做一些设置,做这个事情,这就是我们要用到的,接下来要用到的命令对象,所有的文件都在这里了,代码演示我们就说到这里。 下面看实践二,在App.onLaunch时就开始拉取首页动态数据。 我们在首页需要的列表数据,原来是在首页的onReady周期函数里面开始加载的,拉取的时机说实话有些晚了,其实在App.onLaunch事件派发的时候就已经可以执行网络请求了,但是App.onLaunch这个周期函数它在app.js这个文件里边,如果我们在这里开始拉取这个数据的话,怎么样把拉取得到的数据传递给我们首页并且在首页上它还有一个onLoad和onReady时间节点,只有这个页面Load事件触发以后才可以修改data数据对象,然后对这个页面才可以触发渲染,只有在主页的onReady这个事件派发以后才可以访问这个页面上的这些元素节点。 在这种情况之下,我们可以将App.onLaunch和首页的Page.onLoad这两个时间节点使用并发的复合命令模式管理起来,让它们可以并发执行,实际上其实是前者先执行,在它们全部这个条件达成以后再把这个数据取出来,修改这个页面的data数据对象,为了方便在两个文件里面同时引用,我们可以创建一个单独的叫做retrieve_home_data这样的JS文件,这个文件主要代码就是为了实现这个数据的拉取,同时在这两个文件里面可以不同的引用和调用。 在并发命令对象里面可以添加两个ClosureCommand实例,第一个命令实例它默认在开始的时候就开始执行,开始拉取这个数据,执行以后然后设置完成,第二个命令实例是留给首页里面的onReady周期函数去调用的,然后我们在app.js文件里面去引入和调用retrieve_home_data.js文件,让复合命令开始执行,在主页的JS文件里面,在onReady周期函数里面再将第二个子命令实例设置完成,因为这时候长列表元素它已经可以查询到了,在onLoad的周期函数里面可以使用立即执行函数监听数据,加载完成的事件,那么为什么在这里要使用IIEF,IIEF也就是立即执行函数,这是因为将整个代码逻辑挂载到onLoad的周期函数里面,同时方便在立即执行函数内部使用了一个async和await关键字,我们也可以将立即执行函数里面这个代码拿出来,放在一个单独的方法里面去定义,但是对于只执行一次的这样的一个代码,这样做的意义其实并不大。 下面我们看实践二的代码演示。 首先我们要定义一个retrieve_home_data的这样的一个文件,在这里面要实现数据拉取的一个代码,这个文件我们要放在我们的library,optimus放在这个下面,目前还没有,这个地方有一个services,但下面没有代码,我们就去最终源码里面去看一眼,找到6.2.2,这是我们需要的文件,将这个文件给它拷贝一下放到我们library,然后services下面,这个文件我们看一下它主要干了一个什么事情,主要是导出一个方法,它直接上来就是export default function导出一个方法,在这个里边首先是对三个模块的一个引入,引入了三个模块,第一个是ClosureCommand,这个是闭包指令对象对吧,第二个是ParallelCommand,这是我们的并发复合命令对象,最后这个地方引入了promisify这个工具函数,我们在这个地方我们可以看到前面我们所有的代码,基本上你可以看到在定义它的时候 导出的时候,我用的是ES6的Module这样的一个写法,而我们在引入的时候用的却是CommonJS。 CommonJS它其实有一个优势就是在于它可以运行时动态导入,比如说这个地方 这本来是函数内,函数内它需要这个方法的时候,我们就把它引入,引入之后然后直接就使用,对于这样一次性 使用一次的这样的一个代码,其实我们很多时候没有必要把它写到这个页面的顶部统一使用,其实是没有必要的,然后这个地方它如果只使用一次的话,那就放在这里就可以了,再往下我们这个地方有一个定义的requestCmd ,这是一个ClosureCommand,它是这样的一个实例,在这个里面。这个代码我们看起来很熟悉,这就是我们原来的拉取,从后端拉取首页数据的代码,但是拉完以后我们没有马上触发这个页面的渲染,因为这个代码现在不在那个页面里面,拉完以后其实将我们这个数据存在了global.retrieveHomeData对象的data上面,这个是我们ParallelCommand的一个复合命令。 前面我们提到了我们这个命令对象,它里面其实是有一个data的临时数据仓,临时数据仓就是我们可以在这个上面挂载一些,就在我们请求这个序列里面挂载一些自己的临时的一些数据,放在这个地方,以便我们后续的指令对象在执行的时候可以直接取用,这个数据我们取完以后直接就挂在这个上面,同时在最后这个地方设置我们requestCmd 设置它已完成,最后在这个地方retrieveHomeData 等于ParallelCommand,在这个地方看一下它的示例 它的子命令,第一个子命令是requestCmd,就是前面这个代码 我们已经让它默认执行了,然后执行完它会自己标记为完成,第二个就是,我们新建了一个ClosureCommand,但是我们没有给它传任何的闭包函数,也没有给它传任何的代码,其实在这个地方它的存在的意义仅限于它本身,它本身就是它存在的一个意义,稍后我们还会在后面还会看到,我们对它其实是有调用的。 初始化以后默认就调它的execute方法,因为这个方法它会返回自身,所以调用以后,我们再给它赋值一点问题都没有,这是这个方法 我们现在已经给它搞进来了,接下来第二步我们要做什么事情,就是在我们app.js里面要引入我们的文件,我们看一下我们怎么引入和调用的,找到app.js,然后这个地方 现在就开始加载主页数据,是在这个地方,我们仍然使用的是CommonJS的模块规范,把它取到以后找到我们的现在的项目文件,找到onLaunch这个时间节点,因为我们的本意就是要在这个地方,在这个程序已启动的时候,马上就开始加载我们主页数据,这是我们可以掌控的最好的一个加载时机,最靠前的一个加载时机了。 这个地方有一点我们还要说一下,你看到这个地方我把这个模块引入以后马上就去调用这个方法了,这样调用是没有问题的,因为我们在这个地方没有使用await关键字,也就是我们没有故意人为去阻塞这个代码的一个执行,它其实只是调用,其实无所谓的 ,这样可以直接这样去调用的,调用以后,接下来我们再看另外一个地方,就是在主页文件里面我们也要做事情,看一下我们主页文件里面做了一个什么事情,首先我们要找到onLoad 页面的onLoad周期函数,onLoad周期函数在这里有一个主页已经加载,可以设置数据了,如果已经请求完成 有这样的一段代码对吧,这个是IIE,刚才我们说到了它是一个立即执行函数在我们这个项目里面,其实用了很多这样的一个代码,将这个代码然后复制到我们这个项目里面来,找到onLoad,在这里对吧,放到这个地方,看一下这个地方,首先是我们判断一下global.retrieveHomeData它是否存在,为什么要有这个判断,因为有时候我们这个页面其实不是从我们默认的主包开始启动的,有时候它是从独立页面开始启动的,它如果从另外一个地方启动的话,可能是一个null是不存在的,如果它不存在的话,我们可以在这个地方再次去引入和调用它,这样也是没有问题的,一般情况下这个代码其实它是不会执行的。 然后在这个地方我们去在对象上面去用onComplete监听了它的完成的事件,完成以后我们就调dealWithListData,后面这个是数据,把这个数据然后传递给它,让它进行渲染这样的一个事情,另外还有一个事情就是我们要在onReady这个地方去干的一个事情 我们看一下,在这个地方 只有这一行代码 在这个地方,把这个代码拷贝一下放在我们的文件里边onReady,onReady我们要靠前放,让它尽快地执行,这个地方就是我们要调用我们复合命令对象里面它的第二个子指令,也就是子命令,这个地方我们传1对不对 因为它是从0开始,所以传1其实就是第二个,然后把第二个Complete给它标记为完成,这个非常重要,然后通过这个事件就将两个代码的执行点这个条件已经对齐了。 这个标记为完成,同时我们前面的从主页拉数据那个代码,它再执行完成,两个都执行完成以后它会走到这里,因为我们在这个地方监听了它的复合的命令,它完成时间对不对,它两个条件都具备了就会走到这个地方来,然后去触发页面数据的设置以及渲染,执行我们这个代码,在我们这个里边原来还有一个数据,就这个地方 这是我们原来的代码,从后端拉取数据我们可以看到我们原来的拉取时机它是在什么地方拉的,开始拉取点,是在我们主页的Page.onReady,页面完成以后你才开始去拉取数据,前面其实已经浪费了很多时间,所以从优化上来讲,执行的时机已经很晚了,所以我们这个代码其实已经不需要了,可以把它给它删掉,这个代码已经不需要,看一下执行效果。 首先看调试区有没有编译错误,没有错误我们就看首页的数据它显示是否正常,能否显示,数据也可以显示没有问题,在我们调试区其实有一个Network面板,我们所有的数据的拉取在这个地方其实都可以看到,比如这个地方就有一个home,这就是我们的主页的列表数据的一个拉取,这个地方我们可以看到,但这个地方其实它也是有问题的,我们可以看到我们接口调用以后,然后它返回数据size竟然是8.3kB,这个数据量其实已经是很大的,这个地方还是有优化空间的,这个代码演示我们就说到这里。 最后我们总结一下,使用并发的复合命令对象可以将跨文件的异步执行的随机达成的多个条件,在一个节点对齐,然后再执行其他的一个逻辑,在这里已经定义的命令模式,它们其实只是结构 并且提供释放方法,这样的一个实现方式方便开发者编写自己的业务逻辑代码还有在主页的JS文件里面,当我们监听并发复合事件的完成事件的时候,我们并没有将这个代码写在别的地方而是直接写在了周期函数里面,而且是以立即执行函数这样的一种方式去写的,在哪里需要就写在哪里,这样的一种编码方式会使我们这个代码更加的紧凑 易读,这节课我们就讲到这里,我们这节课用到的文档就如我们现在屏幕上显示的网址。 点击查看开放文档: 小程序框架 /逻辑层 /页面生命周期 这节课我们主要学习了如何使用并发复合命令对齐跨文件的异步时间点,下节课我们学习使用worker在小程序里面开启新线程。 最后说一下思考题。这里有个问题请你思考一下,JS基本上可以说是单线程语言,但是在HTML5开发里面,我们可以通过开一个worker,通过这样一种方式可以开启一个新线程,然后在新线程里面可以执行与主线程并行的代码,在这个小程序里面可以这样操作吗?如果可以这样操作的话,相当于我们可以控制一个异步线程来帮助我们执行一些较为费时的代码,这个问题先留给你思考一下。下节课我们来一起深入探讨一下这个问题。
2022-07-14 - 使用异步转同步的编程范式
[视频] 你好,我是李艺。 上节课我们主要学习了如何在小程序里面使用WebAssembly技术,这节课学习异步转同步的编程范式。 首先我们看一个问题,JS它作为一门应用最广泛的前端编程语言,主要有两大特点,第一个特点是单线程,第二个特点是异步编程。对于第一点单线程很好理解,不是JS不能支持多线程,而是JS在诞生的时候它本身就是作为HTML页面的交互语言而存在的,人类的屏幕交互本身就是一个单线程的,这要求JS也必须是单线程的,如果是在一个页面之上同时有多个交互控件需要人类去处理,那么这个用户他能处理过来吗,显然这可能是为章鱼而设计的对吧,不是为人类而设计的。 至于第二点异步编程,这是JS语言区别于Go语言、C、C++、Java等后端编程语言最主要的一个特征,在后端编程语言里边如果我们要读取文件或者是要发起一个网络请求,必须要等待这个结果返回,返回以后才可以进一步执行下面的一个代码,在编程里面这叫做同步开发 JS不一样,除了主线程以外还有一个或者多个异步线程,异步线程处理worker timer定时器、网络请求、用户输入监听、事件派发等等这些任务,当异步线程有回调函数代码需要执行的时候,异步代码它将这些代码推入到主线程的执行队列里面去,由主线程在不是很忙碌的时候尽快将这些代码进行执行,这种方式就叫做异步编程。 在异步编程里面如果有后续逻辑是依赖前面这个代码请求成功的前提才可以执行的,这些代码将写在回调函数里面,如果后续又有异步线程需要执行,还要进一步的内嵌,这种方式就使得我们代码可读性非常差,当然有经验的程序员可能会讲,你可以另外再创建一个函数,然后在这里去调用它,没有必要一定要把这个代码写成俄罗斯套娃的这样一种形式,这样当然也可以。但是另外定义函数上下交织进行调用,这又会破坏我们整体上代码线性逻辑的一个连续性,可读性又不能得到根本性的一个保证,计算机CPU的工作模式是多核并发的,但本质上人类的思维却是线性的,就像我们阅读小说一样,我们没有办法同时阅读多页或者是同时阅读多本书,这样也是不可以的,上下连贯的连续的逻辑更容易被人所理解和接受,为了解决异步线程在多层异步代码嵌套情况下带来的代码可读性差这个问题,我们可以使用异步转同步编程技巧,它依赖于ES6的async/await一套语法而实现,它本身是异步的,但是却可以以同步的方式编写和呈现,它既有后端同步编程的表又兼具我们前端异步编程的里,是一个非常不错的通用的代码优化技巧之一,下面我们看项目实践。 首先看实践一,编写和实现异步转同步编程范式的这个工具函数。 首先我们需要创建一个promisify的JS文件,这个文件里面会导出一个promisify函数,这个函数它最大的一个作用就是将异步请求的wx Api转化为可以同步调用的方法,小程序里面大多数接口都已经支持了Promise风格调用,凡是支持的在官方文档上都有标注,但是目前仍有少部分接口它是不支持的,不管是支持还是不支持,我们都可以用这个工具函数进行转化。在这个地方我们说一下,自定义的全局的require方法到底能不能使用,前面我们提到过这个问题,在使用require关键字引入模块的时候,有时候我们想使用绝对路径以保持在各个纵深层次文件里面引用同一个文件时的一个路径一致性,这时候我们可以在app.js文件里面,在这个App实例上定义一个替代的require方法,这样的一个require方法不仅可以用于分包异步化的一个加载,还可以用于普通的CommonJS模块加载,并且保持路径的写法一致性,好处实在是多多,但是有一点我们需要特别注意,替代的require方法使用了路径变量,这可能会给我们项目带来隐藏的麻烦,在微信小程序里面,使用require加载模块的时候,有时候我们可以使用变量来标识这个路径,有时候却不可以,为了保险起见,项目里面我们最好不要用这样的一个全局方法。 下面我们看实践一的代码演示。 我们这个函数要写在我们项目下面有一个特殊的目录叫做library,library目录本质上就是我们要放一些JS代码,并且是在所有分包里面基本上都要使用的一个JS代码,在下面有一个optimus这样的一个子目录,我们request替代的异步转同步的工具函数promisify就定义在这里,这个方法我们可以看到它其实已经存在了 对不对,就是这样的一个方法,本身导出,这是一个方法名字,然后它会传进来一个方法或者是函数,传进来以后,我们会导出,就返回这样的一个,本身它返回的也是一个函数,所以返回的结果我们首先要去调用,调用以后它会返回一个Promise的对象,Promise对象里边又有fn的一个调用,其中在这个地方我们重点是做了一个Object.assign将我们的后面的对象 自定义对象写到args对象里面去,这个地方我们重点是做了一个success向resolve以及fail向reject它本身的一个映射,从而实现我们这个方法调用的时候让它的调用结果变成一个Promise这样的结果,本身我们Promise对象是我们ES6,使用ES6这种语法以后,它本身就支持了,所以我们这个地方可以直接使用,这就是我们的promisify工具函数的一个实现了。 另外我们还需要再看一下我们前面提到的,在这个app.js里边,我们提到的会有require这个函数 还有requireAsync,本质上它们两个原理是一样的,这种方式最好不要去用,当然你如果大胆一点你也可以用,但是如果是你项目里面出现了不可理解的一个异常的时候,你如果要排查错误,可以先把它给它注掉,注掉后看看它的一个表现,默认情况下我们就不使用这样的一个方法了,至于路径写法稍微麻烦我们就麻烦一点,其实麻烦一点,本质上也只需要改一次,我们路径拷贝以后只需要修改一次就可以了,它也不需要重复进行变动,代码演示我们就说到这里。 接下来我们看实践二,改写wx.request的请求。 接下来开始改写,使用wx.request编写的网络请求代码,这是一个标准的wx API,根据官方文档显示,它本身是不支持Promise风格调用的,它也有返回值,但是这些都不影响我们使用统一的编程范式去改写它,使用统一的编程风格可以有效减少我们脑细胞的一些浪费,以最经济的一种方式实现,具有良好的可阅读性,以及可维护性的一些代码,具体实践的时候可以在这个项目里面搜索,然后wx.request,看看有哪些代码用到这个接口了,再以promisify函数修改为伪同步调用的方式。 在我们这个项目里面大概会有三处以上的代码涉及到修改,在改写的时候我们主要有三点需要注意,第一点就是引入这个工具函数promisify的时候,因为我们的工具函数本身是用ES Module这种规范编写的,所以在引入的时候如果是用CommonJS模块规范进行动态引入的话,要从default上面做解构赋值,第二点就是给父方法添加async标记,为什么要加这个标记,因为在这个方法内部要使用await关键字,第三点就是在promisify这个工具函数改造wx.request接口并且调用的时候,主体代码的逻辑基本是保持不变的,其实只需要将上下这个代码位置稍微变一下就可以了。 下面我们看实践二的代码演示。 这个代码演示在微信开发者工具里面不是特别的好改写,不过我们同时也有VSCode,我们用VSCode来改写,首先找到我们的项目目录,找到小程序的根目录,然后在上面用这个文件夹里边搜索这个功能,让我们搜索wx.request,这个地方注意 可以看一下这个地方有1个 2个 3个 4个,一共涉及到3个文件里面4处代码的一个修改。 我们先看第一个,这个地方首先这是我们的商品详情页,这个地方首先我们要把我们这个工具函数进行引入,引入这个地方要用一个析构赋值 default等于promisify等于request,这个地方就要写相对路径了,好在VSCode有着很不错的一个代码提示功能,当我们敲这个路径的时候它可以自动帮我们完成,这样就可以了,引入了对吧,引入了以后接下来改写,怎么改写 首先第一步前面这个地方要加async,然后这个地方我们将下面先关掉,在前面加一个 res等于await然后等于它,后面我们要用promisify工具函数把它转一下,把它当成一个参数传进去,传进去以后,后面这个参数其实不变,在这个地方正常的结果我们要放在它的下面,改变一下它的位置,然后这个代码现在要给它去掉 就不需要了,success也不需要了可以放在,直接这么写是不可以的,要加一个catch,这样就可以了。后面这个是对前面返回结果的一个调用,这样就可以了,这是第一个修改。 接下来我们再看第二个修改,这个地方第一步仍然是引入default:promisify等于require然后路径library optimus Promisify.js已经引入了,找到下面这个的位置,在这个位置首先一定别忘了里面前面加async关键字,然后在这个地方const res等于await,然后promisify把它转化一下,这个代码是最终正确逻辑的一个处理代码,然后拷贝到下面,这个缩进稍微处理一下,这个给它去掉,然后这个地方加上catch,这个就改写完了。 下面还有一个,前面那个工具方法已经引入过了,它是同一个文件,所以我们直接使用就可以了,同样的方式,也是将它转化,转化以后调用,然后正确处理的代码拷贝一下 放在下面,这个地方删掉加一个默认的catch,是不是很简单。 还有最后一个文件,最后一个文件改造方法是类似的,等于require,找到合适的路径,再选择我们这个方法,这个地方加上res等于await,然后转化,这个地方不要忘了加async关键字对吧,我们要加在这个地方,因为它本身这个是一个函数常量,正确的处理结果放在下面对吧,方式是一样的,改写方式是相同的,然后最后是catch,这样就结束了,我们的改写已经完成了。 回到我们小程序里面单击编译,重新测试一下效果,首先看看有没有编译错误,目前来看没有什么编译错误,然后看一下表现,我们发现这些加载的数据也如期加载了没有问题,然后在商品详情页里边好像也没有什么问题,我们可以点一点看看有没有问题,也没有问题。这就是使用promisify对我们wx.request的接口的一个改写,这个里边我们有一点可以多说一点,就是在我们目录下面还有另外的一个文件同时还有一个promisify_on,这是做什么用的,它这个其实是改写我们小程序里面有一些以ws开头的onXxx这样的一类的接口,将这类接口然后改写为这个,就是异步转同步这样同样的一个编程方式这样的一种写法,当然我们不常用,所以在我们项目里面其实也没有用到,我们最常用的其实就是这样一个方法,这个代码演示就说到这里。 最后我们总结一下,因为小程序里边require它不支持绝对路径,而使用这个相对路径又会给我们编程带来额外的一些麻烦,一个路径长不说,在拷贝的时候还不能直接复用原来的模块引入代码,为什么在app.js文件里面定义全局的require方法可以使用绝对的路径,主要有两点原因 一是全局的程序的实例app,它可以通过getApp进行取用,然后取到它以后就可以调用它上面的方法了;二是app.js这个文件里面本身这个文件它位于项目的根目录下,我们要利用它得天独厚的位置以便可以从这个项目的根目录进行路径的一个标记,工具函数promisify在使用的时候有两点我们需要特别注意,第一点,函数在调用的时候为了避免这个程序报错,我们一般在后面一定要加一个默认的catch设置,回调函数如果没有特别的需要,我们可以默认传递console.log这样的一个方法,如果程序对这个错误处理有特别的一个需求,在传递这个console.log这个方法的一个地方,我们还可以进一步的进行完善和优化,第二点就是使用promisify函数的父函数,由于我们在这个里边使用了这个await 所以在外面,一定要添加async关键字,在添加async关键字以后,它已经不是一个异步执行的这样一个函数了,我们一定要避免在主线程上以阻塞的方式,也就是我们前面提到的添加await这样的一种方式直接去调用这个函数,关于第二点它很重要,如果使用不好的话可能会有副作用,我们可以再进一步的来阐述一下这个问题,在主线程上面可以挂很多使用async标记的函数,但是这些函数它其实都应该像葡萄一样挂在主线程之上,它们执行不应该阻塞主线程的一个执行,async这个关键字 这个单词它本身是异步的意思,放在这个函数前面表示这个函数里面有await起始的异步代码,在运行的时候我们可以理解为它执行了两次,第一次是执行到await关键字的时候,这个代码停在这里,第二次它拿到异步的结果以后继续从await后面的代码处继续往前执行,async await这一对关键字它是ES6的语法,它编译为ES5代码以后和我们原来使用回调函数的写法是类似的,但是这种写法前面我们讲了,它有助于让我们这个代码更加清晰 易读,代码的执行效率很重要,代码的易读性对开发者来说也很重要,所以异步转同步的编程范式是JS开发里面也是它最重要的一个优化技巧之一,这节课我们就讲到这里。上面的网址是本课涉及的文档地址。 点击查看开放文档: 网络 /发起请求 /wx.request 这节课主要学习了异步转同步的一个编程范式,那下节课我们学习并使用并发复合命令模式对齐代码的执行点。 最后我们说一下思考题。这里有个问题请你思考一下,在这个项目开发里面我们经常会遇到这样的一个问题,某一处代码C的执行需要同时满足条件A和条件B,假设获得条件A需要三秒,获得条件B需要两秒,那么最快我们可以多久可以执行这个代码C,在数学上这个答案很明显 是三秒对不对,但是开发里面,因为获得条件A和获得条件B的代码可能不在同一个文件里面,并且有时候它们也不一定可以同时开始,谁先开始 谁后开始不一定,那么在这种情况下,我们应该如何从整体上设计代码才能让最后这个代码C尽快得到执行呢?这个问题先留给你思考一下,下节课我们一起来深入探讨一下这个问题。
2022-07-14 - 使用页面容器
[视频] 你好,我是李艺。上节课我们学习了使用recycle-view组件渲染长列表内容。这节课我们学习使用页面容器。 有时候我们在页面上会弹出一些特定的半屏窗口,例如登录窗口。这时候如果用户在iOS设备上使用了左滑手势或者是在Android设备上单击了物理返回键,会造成页面跳转到上一个页面,这在大多数情况下它并不是用户的真实本意,用户可能只是想将当前弹出的半屏的假页面给它关掉。 为了解决这个矛盾,小程序提供了页面容器组件,使用这个组件,在iOS用户做出左滑手势及Android用户单击物理返回键以及在代码里边调用了wx.navigateBack接口的时候仅是关闭这个容器,它并不会跳出当前这个页面。同时该页面容器还可以放在自定义的组件里面,像一个独立的组件那样去定义和使用。这可以让项目代码结构变得更加的清晰。下面我们看实例演示。 如何使用页面容器及如何自定义组件page-container? 就是这个页面容器组件,它是一个标准组件,它不需要引入就可以使用了,下面我们会创建一个自定义的组件login,在自定义组件里面使用page-container组件,当需要登录的时候,我们让login组件从这个页面的底部划出来然后再取消,或者是完成登录的时候调用自定义组件的close方法将半屏的假页面进行关掉,完成以后这个页面效果大概是像我们现在屏幕上看到的这样一个截图这个样子。 现在我们在屏幕上看到的这个是组件的wxml代码,其中第二行里面有一个position属性它是控制面板从底部划出的。它的show属性控制面板是否显示有两处wxml,这个节点的tap事件绑定到了这个close方法之上,无论是单击到哪一个地方close方法都是会被调用的,它是用于关闭我们当前的登录面板了。我们现在在屏幕上看到的是组件的JS代码,在这个方法close里边先尝试使用了wx.navigateBack然后进行退出。如果当前小程序页面栈里面它有页面调用会成功,反之如果是当前这个页面它是启动页或者是当前这个页面前面它没有页面了则会调用失败。在调用失败的时候也没有关系,可以再尝试使用setData改变数据属性visible,然后以此来关闭我们的一个面板。 现在我们在屏幕上看到的是这个组件的样式代码,这样式代码也没有什么稀奇的地方。主要是为了实现整个组件的一个外观,这个组件的外观它需要向我们用户提示,需要获取它的昵称、头像、性别等等这些信息。这些都是在微信小程序设计规范里边所规定的一些内容。我们需要按照这样的一个规范去设计我们的页面。现在我们在屏幕上看到的是我们组件在使用的时候,配置文件里面的一个配置信息以及在wxml里面的使用的信息,在wxml这个代码里面给了一个登录按钮,添加了一个tap事件监听,如我们现在看到的代码它是事件监听方法showLoginPanel的这样的一个代码。当单击我们登录按钮的时候,它会从底部然后打开我们登录面板。 下面我们进行实践一的代码演示如何使用页面容器及自定义组件。 首先我们需要在我们这个项目里面去实现一个自定义组件,在我们component 就是新的一个目录下,现在这个目录目前还不存在,我们看一下我们最终的,最终源码里边的它的一个实现情况,找到我们的2.3目录,然后这有一个components,这有一个目录,这个里面有一个login的组件。这个组件就是我们要实现的对吧,我们可以将这个目录拷贝一下,然后进到这个目录里面,进到资源管理器里面将整个目录给它拷贝一下,然后回到小程序项目里面,然后在这个地方再打开我们的资源管理器。 然后拷贝到这个地方,这个组件就已经进来了,我们看一下这个login组件我们主要做了什么事情?首先在wxml里面,外层我们使用了一个page-container这样的页面容器组件,然后它有一个show这样的一个属性,用visible然后去控制它的一个显示,后面position等于bottom它是控制它从底部进行滑出,这是它两个重要的属性。里边这些是我们实现的一个界面UI。这个界面UI是参照微信小程序设计规范而设计的,因为规范里面也有提到当我们想向用户去申请授权的时候,我们应该明确地告诉用户我们在申请什么样的一个权限,要获取他的什么样的信息 对吧,所以这个地方我们把这些信息都要给它设计上。 这是这样的一个。然后我们再看一下它的JS代码,这个地方是一个属性然后show属性默认等于false,这地方是一个属性监听。监听show属性如果它变化的时候,我们就设置visible等于它的新值,这是data对象里面visible控制组件的显示。method里面有两个方法一个是这个login,这个login我们是在我们这个里边有一个叫做确认地方。 这个button它有一个特殊的open-type等于getUserInfo,这是我们特殊的一个类型用于获取用户的信息,这个地方有一个事件监听就是catchgetuserinfo等于login绑定到我们这个方法上面,我们可以通过这个事件对象的detail这个信息去间接地去获取userInfo信息。当然这个里面它有其他的一些加密信息,加上这些信息,如果是我们一起传给后端的话那就可以进一步的解密得到 openId及unionId等等这些其他的一些信息了。这个地方我们是简单地将我们userInfo信息取出来,然后复制到我们globalData就是全局的对象上面去,然后以备后面使用,同时后面就调到close,close这个方法首先我们去尝试用navigateBack去调用了一下。 因为这个方法它是有客户端和版本限制的,它其实我们不能保证所有的情况下它都可以调用成功,所以这个地方我们要有一个判断。如果是它这种方式调用不成功的话,我们就用setData这种传统的方式去设置visible等于false来关闭这样的一个登录面板,这是它的一个主要代码。这个组件代码已经有了,至于另外的两个文件是样式,这些其实都没有什么好讲的,都是根据组件的需要然后去实现的。 下面在我们用户主页里面。在my这个文件里面开始去引入这个文件,用usingComponents,然后是login写它的地址,这个地址简单起见我们可以用拷贝大法对吧,先把相对路径拷贝过来然后再稍微地修改一下这样就可以了。引入以后就是使用,在我们wxml的文件的最下方,因为我们想放在页面的最上面所以我们要放在最下方,给它一个loginPanel这样的一个id。这个页面里面有一个登录的一个button,可以找一下在这个地方有登录的button ,所以这个地方给它加一个tap的事件的一个监听,然后在这个事件监听里面,就是我们想单击以后就是弹出我们的登录面板。这个地方加一个showLoginPanel,这是我们绑定的方法,把这个名字拷贝一下然后在我们的这个里边。这个方法我们应该还没有实现对吧,然后在这个里面去添加。 这个方法的一个实现,这个地方我们想去控制我们登录面板的显示一共是有两种方式: 第一种方式是使用数据绑定的这种方式。比如说我们现在搞一个showLogin这样的一个数据属性 默认等于false,然后我们将属性绑定在我们的组件上面,就放在这个地方,它有一个show的属性对不对,我们绑定在这个上面,在想弹出这个面板的时候,只需要用setData去设置showLogin等于true 这样就可以了。代码已经搞完了,现在我们单击编译看一下它的运行表现,然后普通编译我们可以选择我们的用户主页作为起始页面进行测试,然后单击登录,然后我们这个面板出来了对吧,可以取消,单击确认,可以发现我们userInfo信息也已经拿到了,这是一种显示我们面板的一种做法。 另外还有一种做法,更简单一种做法是我们不需要数据属性。不要这个数据属性,然后这个地方这个也不需要绑定。然后只需要拿id 给它一个id在这个地方,我们用页面的selectComponent这个方法,然后将id传给它以后我们就可以调用setData方法了。setData它里面我们还记得它有一个什么样的数据visible对吧,visible等于true 直接调到这个方法visible等于true,我们看一下这个表现,然后单击,也可以对不对。这种方式实现的效果跟我们刚才实现那种实现方式效果是一样的。但是我们这种方式不需要在这个页面里边去定义新的数据属性了,对不对?只需要然后查找这个组件然后调用它的方法就可以了,这种方式更简洁对不对?也不需要在wxml里面去绑、去做数据绑定了。 我们知道我们组件上其实它还有一个属性就是show属性,如果是我们直接去改变show属性,这种方式是不是也可以。比如说我们查到这个组件以后直接改了show等于true,然后上面这个先注掉,然后这样再测试一下,然后单击登录按钮。不行,这种方式不可以,所以我们就用上面这种方式就好了。这种方式是可以的,再测试一下,都可以,在我们面板里面这个取消与下面这个取消,其实它都绑定了close,因为我们用了page-container,因为我们知道它还可以响应什么,可以响应模拟操作对吧。我们比如说单击返回,这个不可以。 代码演示就到这里。 最后我们总结一下使用自定义组件的好处。page-container组件不仅可以响应iOS左滑这样的一个事件以及Android物理返回键这样的一个操作,它还默认实现了从页面的各个方向动画出场的一些效果 。对于开发者而言其实实在没有必要再造轮子实现各种浮窗组件了,类似的需求都可以基于page-container这样的一个页面容器组件然后以自定义组件的方式进行实现。 自定义组件是一种很好的封装页面视图功能的一种方式,它至少有以下优点,我们一起看一下: 第一点它可以复用,可以在各个页面里边使用,不需要重复定义; 第二点它可以将视图样式按组件进行划分,使样式代码更加清晰; 第三点就是它可以缩小setData的一个作用范围,当这个组件的数据发生变化的时候,重渲染机制它仅局限于当前组件内部发生作用,这在一定程度上也提高了视图的一个渲染的效率,现在我们在屏幕上看到的是这节课用到的一些文档地址。 这节课我们就讲到这里。 点击查看文档 这节课我们主要学习了容器组件如何使用,了解了自定义组件的好处我们自定义的login组件将负责接管所有页面的登录请求。这种组件化的开发方式它既让代码更加易于维护也避免了相似登录逻辑的一个重复编写。在一定程度上减少了代码包的一个体积,隐隐之中还提升了这个页面的渲染的效率,可以说是有百利而无一害。 下节课我们学习如何在用户主页页面里面优化动画效果。这里有一个问题请你思考一下:在HTML5页面开发里面有一种响应式设计方式。一种典型的页面效果是这样的:当用户向下滑动页面的时候页面上的元素它以不同的动画方式,从临时状态过渡到最终的目标状态,人类的眼睛对静态元素他感知是不灵敏的,但是对变化的元素的感知却是很灵敏的。使用响应式设计可以显著提升用户对这个页面上指定内容的一个关注度,在这个小程序里面我们怎么样去实现这种响应式的一个设计? 这个问题先留给你思考一下,下节课我们一起来深入探讨一下这个问题。
2022-07-13 - 使用虚拟dom,优化长列表显示(下)
[视频] 现在我们开始代码演示。使用recycle-view组件,首先需要在我们文档管理器里面选择miniprogram目录,然后选择它的在内建终端中打开,打开我们内建终端界面,在这个地方。 当前这个目录就是我们的小程序的所在的目录,第一步我们需要安装我们刚才提到的组件对吧,我们可以用yarn add然后去安装。它的名字是miniprogram-recycle-view这是它的名字,然后回车,安装以后我们可以看到一共是有一个文件,这个文件package.json生成了对吧,里面有一个dependencies里面多了一个组件,后面是它的一个版本号。然后在这个上面还多了一个node_moduels这样的一个目录。在这个目录下面我们可以看到也多出来了这样的一个组件,这是我们从外网下载下来的。通过这个指令下载的组件这是一步,这一步搞完以后我们还需要做个事情是什么? 在使用之前需要做的就是选择菜单工具,然后再选择这个构建npm。构建完成以后会再生成一个叫做miniprogram_npm这样的一个组件,这个组件才是我们这个小程序在运行的时候真正要加载模块的一个目录。它本质上代码是从这里加载的,它为啥要做这一步?是因为我们原来的默认加载下来的,下载下来组件的源码它有很多冗余的部分,有source代码、有目标代码。本质上我们运行时我们只需要这个目标代码,所以它就有了这一步。把这个目标代码拷贝到这个地方来,如果不这样做的话,这个代码包因为加了很多多余的一些其他代码,代码包体积会变大很多,这也是一个优化措施。 这一步,然后安装。安装完了以后接下来我们就可以去添加我们的组件了。在我们的主页里面我们看一下,首先是引用,要将我们的代码,我们打一个using,它会有提示的。我们可以使用这个具体这个代码的一个引用,就是引用我们已经放在目录下的组件,使用这个在我这里最终源码里面。我们2.2就是我们本节的最终源码,这个里面已经有写好的一个代码。我们将这个代码为了节省时间,我们直接把它给拷贝过来使用,实际上你在实践的时候自己亲自手写也是可以的,也没有问题。 这是组件的引用,接下来引用完成以后,我们要去改写我们的页面代码。页面代码首先是外围的recycle-view的一个使用,这是最外围,我们可以将使用它代替我们的这个地方,最外围的一个view,把这个给它代替。然后前后要对称,这个也给它改掉也是recycle-view。接下来就是里边我们的这两个slot也需要保留,一个是after一个是before。这个就是after,然后再往里面就是我们recycle-item的一个使用。这地方有一个block,将使用我们recycle-item代替block。原来循环的列表对象是allList,现在不需要了,现在要改成它里边我们用到的这些数据的一些绑定。这些都不需要变化,包括这个绑定也不需要变化,这是我们在单击某一项列表元素的时候去触发的,然后这个里面我们会有一个onScroll,这是我们新添加的一个事件监听,它是在我们这个列表滚动的时候触发的。 onScroll这个事件回调函数,它的源码很简单,我们目前只是让它有一个打印,告诉我们这个列表正在滚动,其他的我们暂时不需要。 这是关于wxml代码的一个修改,接下来我们开始改造我们JS代码。JS代码首先第一步是干嘛?需要引用、需要引入,将组件里边导出的这个方法引入。这个地方我们提一下,我们用require引入模块的时候,因为我们这个模块它本身就位于我们miniprogram_npm这个目录下面,所以我们这个地方可以直接使用这个名字去加载,不需要去写这个路径了。虽然我们当前这个页面和这个目录它俩相隔比较远。如果是我们用相对目录的话,这个地方可能还要写好几个点,好几个斜杠对吧,但现在因为它是一个模块,直接放在这个地方,所以不需要,我们直接这样写就可以了。 这是一个引入,引入完成以后,接下来这个地方是上下文的一个渲染对象,这个是必不可少的。然后再往下就是我们要有一个处理数据的函数dealWithListData。我们先把这个给它拷贝过来,这个我们原来就有,但不同的是我们后面这个地方,没有对长列表的一个设置,这个设置其实这个是setRecycleContext,我们把这个给它拷贝过来,然后这个方法在哪里调用?在我们拿到这个列表以后对吧,我们原来这个地方我们可以看到它是这样一种方式,它是通过allList写到data数据源对象里面然后进行渲染的对不对?我们现在就不采用这种方式了,当然allList的data我们还是需要保留,这个代码可以给它去掉,然后这个地方我们调用setRecycleContext,调用我们新写的方法。 在这个里面我们看一下干了什么事情,第一步我们需要判断ctx它是不是已经实例化了。所以这一步是实例化,在这个里面我们看到两个名称一个是recycleId,一个是recycleList。这两个名称在我们的wxml里面我们可以看到在这个里面这个地方有一个对吧,它跟这两个名字是对应的,它必须一致,不一致的话就会出现问题。然后下面会有一些关于itemSize的一个设置,itemSize这个里面用到了一个rpx2px,就是一种变量,一种单位的转换,转换我们方法是在这个地方写的。它本质上是拿这个 将rpx单位转成px这样的一个作用,然后给它转完以后将这个值给它传进来,这个值从哪来?这个值其实是我们运行的时候通过我们这个工具查看的。它本质上高是固定的,它每个单元元素的高是固定,所以直接写在这个地方就可以了,这样的一种方式。 然后这个地方其实也是,它是在哪个地方开始渲染的?最后然后去参数拼接完以后就创建这个对象,这个它只需要创建一次,第二次就不需要了。然后将我们newList,每次我们新加载的数据通过它的一个append方法,就是组件上的一个方法,然后去传递给它由组件然后负责进行渲染,这是我们的这样的一个主要的代码。 这个代码是写完了,现在我们尝试编译一下,把调试区也给它打开看看有没有什么问题。这个地方有问题,我们这个地方看到会有一个Error对吧,recycle-view correspond to this context is detached会有一个错误,我们就检查一下这个错误出现在什么地方呢?为什么会出现这样一个错误,我们引入的组件通过指令安装的组件就是recycle-view组件,它本身里边有一些代码是需要修改的,我在示例项目里面其实也做了修改,使用的并不是直接安装的模块。你在用的时候其实你可以使用我修改之后的代码,比如说我们现在这个地方,这是我们的一个目录对不对?我们可以打开这个目录将这个组件给它拷贝一下,然后到我们这个目录,一定要到node_modules下面,因为这个是源码目录,打开这个,这个给它删掉不要。然后拷贝过来,拷贝一个新的。拷贝了以后因为源码有修改,所以我们这个地方还需要再次调用这个构建npm。把这个目标源码给它构建一下。 接下来我们再次编译一下,看看它的一个运行效果。在这个地方,我们在调试区,发现有一个错误出现了,这个错误是怎么出现的呢?它其实源于在我们代码调用的时候,我们上面没有把loading等于false给它设置。因为目前我们看到这个页面上其实显示的还是骨架屏对不对?而我们骨架屏代码里面这个地方有一个Error对吧,也就是骨架屏在显示的时候它下面这些是不显示的,其中也包括我们这个长列表recycle-view它也是不显示的。在不显示的状态下,它不显示的状态下如果想去调用我们长列表组件然后去更新它的数据,这是不允许的 ,这种情况下会报这样的一个Error,解决办法也很简单,其实我们只需要在这个地方,我们把loading等于false给它设置,设置完以后,另外我们这个地方其实关于newList还需要再设置一下,需要把这个数据给它放在我们的数据源里面,稍后我们可能会用到。 但是列表渲染其实不靠它,列表渲染其实靠的还是我们调用这个方法,调用这个方法,这个调用上面这是一种调用方式,然后下面这也是一种方式。这两种方式都可以。下面这种方式是避免我们this对象丢失,我们可以先用上面这种方式去测试,代码修改完了 我们再测试一下。现在我们看到这个列表已经显示了对吧,但是列表显示有点不太正常,它把我们原来的轮播图,顶部的banner轮播图,还有导航区全部都给覆盖住了 对不对?这是怎么造成的呢? 一般情况下就是我们的这种wxml渲染没有问题,渲染没问题,而只是这个页面展示有问题。这种问题一般就是样式问题,我们要检查我们的样式是不是有些设置的不合适的地方,在我们这个里面其中有两个涉及到它一个是top-banner,另外是一个top-nav。这里面有一个position等于absolute,代表是绝对定位。我们需要将这两个给它去掉,不需要绝对定位,清一下编译缓存,然后再看一下它的表现,整体冷启动的一个表现。首先我们看到骨架屏对不对,然后看到渲染,整体上看起来,启动是不是感觉比原来的快了一点点对吧。而且我们看到system Launch Time这个地方启动的时间 冷启动的时间是2.9,大概是2922毫秒 2.9秒,比原来的7秒已经大大的有所优化了。 这就是我们关于recycle-view 长列表组件的一个使用,最后总结一下recycle-view组件有以下优点: 第一点它很好地实现虚拟DOM这样的一个优化思想; 第二点就是稍后我们还会看到结合这个滚动事件scrolltolower,它还可以实现主页加载与更新列表数据; 第三点它本身已经开启了throttle函数节流限制,稍后我们还会对这个函数节流做进一步的一个介绍。它本质上是为了减少不必要的 多余的CPU浪费 资源消耗; 第四点它预留了插槽。我们可以看到它有before以及after slot方便开发者添加个性的业务逻辑,所以对于长内容列表渲染 recycle-view组件它是一个不错的选择,没有必要再另造轮子了,对于组件中的一些些许不足我们只需要在源码上稍加修改就可以了。 现在我们在屏幕上看到的这几个网址是我们这节课用到的一些文档、一些网址。这节课我们就讲到这里。 这节课我们主要学习了recycle-view组件的使用,它是一个渲染长内容列表的利器,下节课我们学习使用页面容器。 这里有一个问题留给你思考一下:当用户在iOS设备上使用左滑手势或者是在Android设备上按一下返回键,这个返回键是物理的返回键,位于它这个屏幕的下方,用户的真实想法他可能只是关闭从底部滑出的半屏窗口而不是返回到上一个页面,这种情况下在小程序里面应该怎么优化、怎么处理呢? 下节课我们一起深入探讨一下这个问题。 点击查看开放文档: recycle-view scroll-view
2022-07-13 - 使用虚拟dom,优化长列表显示(上)
[视频] 你好,我是李艺。上节课我们学习了骨架屏技巧,在页面加载动态数据的时候给用户展示一个特定的骨架屏UI,以此提升用户的浏览体验。但页面需要渲染的内容并没有减少,用户需要等待的时间还是那么多,对于长列表页面,用户第一次仅需看到上面的首屏内容,下面需要滑动才能看到的内容是没有必要一次性渲染给用户的。 这节课我们就学习使用虚拟DOM,以此来优化长列表内容的一个显示。 关于虚拟DOM的组件实现有很多,官方文档上的recycle-view组件就是其中之一,这个组件它特别适用于长列表内容的一个渲染,它在渲染的时候仅会渲染用户当前在这个视图里面应该看到的内容,对于看不到的内容则不会渲染,使用这个组件不仅可以加速首屏渲染的一个速度,还可以让运行时的列表滑动浏览更加的流畅。 接下来我们首先先看一下recycle-view组件它是怎么实现的?它实现的原理是怎么样的? recycle-view组件是基于小程序的标准组件scroll-view组件去实现的,它是一个自定义组件,它的实现原理也很简单,一共包含两个部分:一个是recycle-view,一个是recycle-item。 首先我们在渲染的时候需要知道每个循环单元的一个高度,这是消费代码需要传递给组件的。有了这个高度再配上屏幕的一个窗口高度,长列表组件以此就可以算出完整渲染整个列表最少需要的子组件,也就是recycle-item的个数了。当用户在这个屏幕上下滑动这个列表的时候,实际上子组件实例它并没有改变,页面上永远是那些子组件,就是那一组子组件实例在负责渲染,改变只是传递给它们的一个数据。换言之,在recycle-view组件里面,用户滑动的是数据而不是组件本身。 现在我们在屏幕上看到一个示意图,这个图就是recycle-view组件运行时的一个示意图,至于滚动的高度是通过设置列表父组件的一个top属性,动态给它撑起来的。在列表滑动的时候使用子组件recycle-item循环渲染出来这个内容,相当于在一个长列表上的滑动,并且是可视范围一直是对准用户的视图窗口的这样的一种方式去渲染的。因为静态资源加载也需要时间,为了避免出现白屏,recycle-view组件会多渲染出前后屏幕、前后两屏的一个内容。它并不会只渲染当前我们这个屏幕看到的那些recycle-item,它会多渲染出两个屏幕的一些内容,以防备上下滑动的时候会出现一些白屏的地方看不到列表。 下面开始看实例演示。如何使用recycle-view组件? recycle-view组件它并不属于标准组件的一部分,在使用之前我们应该像使用第三方的类库一样,使用npm或者是yarn指令对其进行模块的依赖安装。现在我们在屏幕上看到的这就是两个指令:yarn和npm安装指令,这两个使用任何一个都可以。原来微信开发者工具还需要在这个项目设置里面选择启用npm,现在已经不需要了。现在甚至连这个项目目录下的webpack.json文件,也不需要使用这个初始化指令。也就是我们现在屏幕上看到的这两个指令yarn init -y或npm init -y这两个指令也不需要使用这两个初始化指令的任何一个手动去创建了。 当我们执行安装指令的时候,它模块安装的同时webpack.json这个文件也会自动被创建,安装以后选择菜单工具→构建npm这一步不能省略。为了减少打包的体积,小程序会把最少量的模块代码从node_modules目录下面拷贝到miniprogram_npm目录下面去,这个目录才是模块加载时真正查找以及用到的一个目录。 模块安装以后,接下来我们需要在使用recycle-view组件的页面配置文件里面,也就是json配置文件里面去添加一个配置。像我们现在屏幕上展示的配置,把这两个组件就是recycle-view以及recycle-item这两个组件引用都需要添加到usingComponents节点下面去,添加完成以后,我们就可以在wxml这个页面里面使用我们的recycle-view和子组件recycle-item了。我们现在在屏幕上看到的就是一个示例代码,外围是一个recycle-view。 它有一个batch,batch等于batchSetRecycleData这个字段是不可或缺并且是不能改变的,相当于是写死的一个字段。每次使用的时候直接给它写成这个字段就可以了,往下是一个id等于recycleId,这是一个id,这个id是我们在JS代码里面会用到,然后组件里面我们还绑定了一个滚动事件,catchscroll它等于onScroll,它这个列表在滚动的时候会触发这个事件回调函数,在里边有两个slot,前后各有一个,一个是before,下面是after。然后再往里面是一个recycle-item,这个recycle-item是我们这个列表里面一个单元,在这个上面我们绑定了wx:for,这个是列表的一个渲染语法。这个里面用到的变量名称recycleList基本上也是写死的,稍后我们在JS代码里面可以看到。 然后这个地方我们还绑定了一个type事件onTypeRecycleItem,它是在我们单击这个列表元素的时候会触发的一个回调事件,然后这个上面还有一个是dataItem以及dataId,这是我们自定义dataset数据属性,稍后在代码里面会用到它。这就是我们的在wxml里面的使用的一个代码了,recycleList在当前这个页面里面就我们刚才在那个代码里面看到的变量,它目前是不存在的,它是由recycle-view组件在其内部就是组件内部动态将我们当前的页面的data数据对象上动态去设置的。这样设计方便我们在使用这个组件的时候自定义子组件的一个监听事件,因为我们可以看到我们在使用这个组件的时候,子组件就是我们recycle-item里面的这些wxml这些节点完全是我们自己定义的,我们可以加自己的一些逻辑代码、业务代码,这方便我们控制子组件的一个渲染样式。 下面我们在屏幕上看到的是我们JS代码,主要的一个JS代码这个里面我们里面首先是引用,要有一个对组件里面有个createRecycleContext一个引用,引用以后这是一个方法,稍后我们会调用它。然后再往下是一个ctx,ctx是我们用到的一个上下文对象,这个对象也很重要。再往下就是有一个dealWithListData这样的一个函数,这个函数是我们专门去处理,我们拿到的后端的这些数据,需要将这个数据在我们的长列表组件上然后进行设置和渲染。 我们组件还用到一些wxss样式代码,这些代码就像我们在屏幕上看到的这些一样,很简单。这是两个组件样式一个是控制了它的背景颜色,另外是它的一个padding 一个外边距,一个内边距。稍后完成以后,渲染的最终的一个效果就是像我们现在在屏幕上看到的一样,是这样的一种效果。 在改造旧页面的时候,我们可能会遇到这样一个问题:列表它会浮在原来的内容的上面,我们新添加的recycle-view这个页面附在原来的内容上面,像原来的顶部的banner以及导航内容遮挡住,这是由于wxss样式造成的,怎么解决? 我们看一下屏幕上的这两个样式,我们只需要将两个类样式里面的position样式给它去掉,原来它是指定了它的一个绝对定位,我们只要把绝对定位的样式给它去掉,这个效果就可以正常渲染了。
2022-07-13 - 使用骨架屏
[视频] 你好,我是李艺。 上节课我们主要从整体上讲述了小程序的启动流程及双线程的运行机制,简述了小程序在启动流程中的一些优化要点以及运行时的视图渲染优化要点。 这节课我们从一个实战项目开始,正式开始优化技巧的学习与实践。 首先我们学习骨架屏技巧的一个使用。 骨架屏顾名思义是展示一个页面骨架而不含有实际的页面内容,给用户的感觉是数据正在紧张的加载,真实数据马上就可以呈现。从渲染效率上来讲,骨架屏它并不能使首屏渲染加快。由于骨架屏的一些使用又向用户渲染了额外的一些内容,这些内容是额外添加的、本来是不需要渲染的,它反而从整体上加长了首屏渲染的一个时长。但是骨架屏在这个页面白屏的时候,它给了用户及时的反馈,减缓了用户焦急等待的一个情绪,这是它的意义所在。 在小程序启动流程的第三阶段即首页渲染阶段,在使用真正的数据渲染页面之前也就是在这个Page.onLoad事件派发之后有一个空档期,这个时候这个视图页面它已经开始展示了,但因为动态数据还没有到,页面往往产生为白屏,动态数据的数据量加载越多,空档期它就会越长、白屏现象也会越严重。 在我们的实际的项目里面,主页在这个页面开始显现之前有明显的白屏现象,在主页里面有一个很长的列表,列表渲染出来之前,下方列表的位置也有轻微的一个白屏现象,这些现象稍后我们在演示的时候都会看到。 为了避免白屏现象的一个出现,我们可以这样优化:开发者可以在这个数据完成加载之前使用骨架屏和Loading提示,在这个数据完成之后将骨架屏和Loading做不渲染的一个处理,再展示真正的一个页面内容。 一般具体的做法是这样:当前这个页面里面,数据源对象里面我们加一个loading提示这样的变量初始值设置为true,数据加载完成以后再将这个变量通过setData方法设置为false,在WXML这个页面里面我们通过loading变量切换骨架屏内容以及Loading提示还有真正页面内容的一个显示,为主页添加上骨架屏以后,从用户的体验这个角度来看,他这个感觉已经好了很多。现在我们在屏幕上看到了,这个就是最终的一个骨架屏渲染的一个效果。 现在我们开始代码演示。首先看一下我们的项目,现在我们看到的微信开发者工具展示的就是我们的优化项目的初始的状态,然后为了更好地编辑这个项目,我同时还开了另外的一个编辑器,就是VSCode。VSCode下面在这个project下面有一个miniprogram,这个目录下也就是我们的小程序项目。我们同时会用这两个编辑器去编辑我们项目的一个代码,在这个里面我们看到模拟器里面已经展示了三个页面,默认展示的这是主页,然后前面有一个商品页,最后面一个我的是用户主页,一共这三个页面。稍后我们这个课程里面会逐渐对这个项目进行一步步的一个优化,让它结构以及性能都会一步一步地变好。 首先我们看一下我们这个项目首页在加载的时候它有什么样的一个问题。单击编译看一下它的一个表现效果,我们看到这个页面有长期的一个白屏并且上面这个图加载以后,下面列表区域在渲染之前与完全渲染之间也有一个很长时间白屏,可以明显地可以感觉到。 接下来我们就用骨架屏这个技巧对它进行一个优化,这个页面在完成渲染以后,我们在模拟器这个区域下方有三个点,单击这个菜单会有一个辅助菜单,上面选择生成骨架屏,然后它会有一个提示,单击确定,我们主页这个是在index目录下面,然后在这个地方,这个里面有一个子目录,这是我们的一个主页面。我们现在看到这个目录下面多出来的两个文件,以index开头了,然后中间加了一个skeleton,这个是骨架屏的一个意思。同时我们这个页面里面 这个代码也会有所修改,我们看一下这个代码,这两个文件是它默认生成的一个代码:一个是wxml,下面这个是它的样式代码,这两个代码我们再重新编译一下这个项目看一下它的现在的一个表现。 现在骨架屏代码已经生成了但是还没有在我们主页这个页面上,没有去应用上去,怎么去应用? 首先我们看样式代码,这里面有提示,我们需要将这个代码给它拷贝一下,在我们index.wxss这个页面里面,在这个上面,将这个代码给它拷贝在这个地方,这是添加了样式。 然后接下来在这个视图页面里面,还有把这个代码也给它拷贝一下放在我们主页的视图页面里面,放进来这个地方,这有一个loading,loading我们现在还没有,需要在我们js这个页面里面,在data数据对象里面加上loading,默认给了一个值是true 默认能显示。 另外我们还需要有一个loadingTip提示语,再加一个额外的提示语:页面正在加载,放在我们的页面里面。可以放在这个地方,然后用这个数据绑定的语法把它放在这个,就是小胡子语法,然后这个地方我们也要给它一个渲染的控制。拿了一个wx:if if loading等于true的时候进行渲染,这两个内容它会随着变量的一个改变,会同时显示或隐藏,loading开始是true 什么时候给它改成false。在我们这个代码里面我们要看一下有一个关于数据加载,就是列表数据加载,在onReady里面有一个 request的一个数据接口的一个调用,在success回调里面,最后面这个地方 我们在这个地方设置this.setData,然后loading等于false,这样我们这个代码已经全部写好了,骨架屏也已经安排好了。 现在我们重新编译,测试一下它的效果,我们看到在真正的数据加载之前这个地方是有一个骨架屏的效果的,但是我们这个效果好像有一点点不是很美观对吧,特别是在我们列表的数据真正的加载完成并渲染之前,我们会看到它上面会有一些重叠,对不对? 这个其实是骨架屏和我们真实的页面它的一个处理,我们可以这样来处理,下面这不有一个container,这是另外的一个地方,这个地方我们可以加一个else,它和上面是一个互斥的一个关系,然后我们再看一下它的一个表现。等到这个数据加载完成以后,然后再开始真实的一个页面的显示。 这是我们看到一个效果,改变以后整体上一个效果比原来稍好了一点。但是应该我们也注意到,本质上我们这个页面的整体的加载时间并没有减少,然后用户在看到骨架屏这个时间仍然是等了很长时间,骨架屏它只是缓解了我们用户等待的一个焦虑情绪,它其实并没有从根本上去将我们启动时间减少。 最后总结一下我们应该如何使用骨架屏以及在哪个时间节点使用骨架屏呢? 至少有三点需要注意: 第一点在data数据对象中默认设置loading等于true。 给骨架屏使用的loading变量,它默认就写到data数据变量这个里边,它在initDataSendTime这个时间节点它会发送到这个视图层,然后进行渲染,从后端接口动态加载的一个数据这个动作要放在Page.onLoad这个周期函数里面甚至更早的周期函数里面去执行。一等这个数据加载完成了立马就将这个数据塞到数据源对象里边,同时将loading设置为false,消除骨架屏的一个渲染,这是第一点。 第二点就是不要直接修改生成的骨架屏的一个代码。 现在我们在屏幕上看到一个网址,这个网址就是官方的骨架屏的一个文档。从这里面我们可以看到骨架屏功能它有loadingtext.color等配置节点,在这个我们项目里面,有一个就是project.config.json这个配置文件。这个文件里面我们可以看到如我们现在屏幕上展示的这些配置字段,它可以配置骨架屏的一个默认样式以及它的一个默认行为,这些行为其中就包括动画行为,其中global节点控制所有页面的一个骨架屏风格,pages节点下面还可以分别控制每个页面的一个个性设置,注意修改配置以后要重新生成骨架屏代码。 微信开发者工具在生成时它会读取这些配置,开发者不要直接去修改生成的骨架屏的代码,如果是有个性控制的一个需要,我们可以通过修改这个配置然后进行控制,这样方便我们在多个骨架屏页面保持统一的一个风格。还有就是后续如果我们这个页面,就是我们主页它的WXML这个代码甚至它的JS逻辑代码如果有修改的话,还需要重新去生成骨架屏代码,开发者工具它是不会帮助我们自动去更新这个页面的。 第三点就是不要过度去使用骨架屏。 既然这个骨架屏它可以提高用户体验,那么我们为每一个小程序页面都添加一个骨架屏的效果 ,这样不是可以吗?有人可能会这样想对吧,一般不这样做。 一般我们只给主页去添加骨架屏效果,骨架屏它是小程序提供的一种优化用户体验的一个机制,但其实任何的一个渲染都有消耗,骨架屏也是。骨架屏本质上它是一个模板,我们可以看到它本质上是一个WXML文件,在主页里面引用以后其实引用的是一个template的模板,它和我们开发者自定义这个模板本质上没有区别。如果是在骨架屏里边我们再写了复杂的一些节点以及动画效果的话,反而不利于我们整体上小程序首页的一个快速加载和渲染。 一般我们推荐的方案是这样的,仅使用微信开发者工具,它默认生成的骨架屏,如果要修改的话也仅是修改一下,通过我们配置修改一下它整体上的一个样式,这些代码它里面的动画效果一般也不要去使用。页面布局改动以后,再重新根据我们源的页面再生成一下骨架屏这个页面代码。 好,这节课我们就讲到这里。这节课我们主要学习了骨架屏的使用相对简单。在微信开发者工具的模拟器里面基于已经渲染的页面直接就可以快速生成骨架屏代码了。我们只需要在这个页面的data数据对象里面添加一个loading变量将其设置为true,并且在动态数据加载完成以后再将loading变量设置为false,这样就可以了。 但是我们也应该注意不能无节制地去使用骨架屏,还有给骨架屏组件使用的变量,我们在Page.onLoad这个事件派发之前要准备好,最好是直接写在data数据对象里面。下节课我们学习如何优化视图页面里面的长内容列表。 这里最后有一个问题留给你思考一下,对于传统的瀑布流页面,页面它几乎是无穷尽的。你越往下滑它DOM节点越多,而在小程序的页面里面,标准里面又说WXML这个节点,它建议不超过1000个。那么对于这种传统的瀑布流类型的这种页面我们到底应该怎么渲染呢? 这个问题先留给你思考一下,下节课我们一起来深入探讨一下这个问题。 点击查看文档
2022-07-13 - 了解小程序的启动流程(下)
[视频] 首先我们先了解一下微信小程序双线程运行机制。 微信小程序可以看作是由逻辑层、视图层两个线程协同完成运行的。 逻辑层负责执行JS代码,视图层负责渲染UI页面。逻辑层与视图层之间的事件触发以及数据传递,也就是setData的方法的一个调用全是由底层的Native层负责中转完成的。 我们以iOS Mac端为例,底层有一个叫做evaluateJavaScript函数,这个函数专门负责执行JS函数,每当这个逻辑层它有代码要执行的时候,这个代码它先转为字符串传递给这个函数,再由这个函数负责将这个代码传递给对应的WebView组件完成渲染、完成执行。 setData函数它用于更新视图数据,它的执行也不例外,底层的Native层,在逻辑层与视图层中间它像一座非常窄的 一个窄窄的一个独木桥,极大地限制了两个线程之间的一个通讯效率,按照微信小程序的性能评判标准,setData每次传递的数据大小不能超过256KB,超过这个限制 页面就容易卡顿,还有并不是低于256KB它便不会产生性能问题。在这个页面或者是列表组件scroll事件里面,如果我们频繁地调用setData视图层它来不及渲染也会出现明显的一个卡顿现象。 针对小程序的运行时特点有以下这些优化点,我们一起看一下。 首先是使用WXS的脚本,在这个视图层完成这个事件处理。 WXS脚本是微信自研的一种类似于JS的一个脚本语言,它完全可以在视图层线程里边独立完成执行,不需要由逻辑层进行插手。更无需底层进行中转,小程序使用重渲染机制。WXML节点越少、嵌套层次越浅、渲染效率越高,按照这个小程序的性能评判标准,每页总节点数要小于1000个,层次要低于30层,每个节点的子节点不能多于60个,针对JS脚本解析执行这样一个效率低下这样的一个特点。 小程序它支持WXWebAssembly这样的一个技术,可以将C、Go语言代码编译为wasm文件,在这个小程序中加载并直接调用,借用其他编译型语言的一个强大执行能力,来弥补解析性语言的性能上的一个不足。针对JS是单线程执行的一个特点,小程序还允许开发者另开一个Worker线程并发进行代码的一个运算,针对长列表页面setData单次传递的数据不能超过256KB限制可以采用分页渲染,采用虚拟DOM等优化技巧。这些技巧它不是小程序专用的,在HTML5 Web开发里面也用到了,但是Web开发它没有setData大小的限制,一般也不需要这么来做。 针对每次拉取动态数据需要时间可以使用LocalStorage接口,将这个数据缓存在本地,下次渲染的时候我们直接从本地去取。不过LocalStorage接口,无论是同步接口还是异步接口,它本质上其实都属于同步接口,在使用的时候我们要避免影响小程序的主体启动流程,小程序在进入后台状态以后它有5秒的一个挂起状态,在这种状态之下setData它已经没有必要执行了,这个时候执行其实就是对用户CPU资源的一个浪费。在这种情况下我们可以停止setData的代码的执行,在小程序与后台进行数据交互的时候可以启用Http2 Quic等协议,以提高网络通讯效率。 小程序的页面在跳转后,页面对象并没有立即销毁,通过getCurrentPages()接口仍然可以获取所有页面栈对象,在Page.onUnload周期函数里面,我们应该将定时器以及以wx.onXxx这样形式开头的全局监听、还有与全局对象有关的事件监听等等全部移除,避免GC不能及时进行垃圾内存的一个回收。 小程序里面还有一些原生组件例如像map video canvas等这些原生组件,它们都有原生的Context的节点,在JS代码中可以通过SelectorQuery查询到这个原生节点,然后再利用这个原生节点直接操作和更新视图,可以绕过底层Native层的一个数据中转,这个技巧可以提升原生组件的一个渲染效率。小程序中遇到的一些图片所有本地图片都可以传到云上转为网络图片,那图片还可以使用webp格式,小程序的image组件是支持这种图片格式的。 以上就是小程序在冷启动时以及运行时可以使用的性能优化技巧。微信开发者工具提供了静态代码分析、Performance面板、Memory面板、性能评分面板也就是Audits面板还有代码质量扫描工具等等这些工具可以对小程序代码进行分析,帮助开发者进行启动性能优化和运行时渲染性能优化。下面我们看项目诊断。 为小程序项目做项目诊断有很多工具,例如入口在这个项目详情面板里面的代码依赖分析性能报告,代码质量扫描以及调试区的Performance面板 Memory面板还有这个JavaScript Profiler面板和体验评分面板等等这些都是。下面我们依次看一下这些工具的运行效果,它们显示了我们的初始项目究竟有多么糟糕,这正是我们接下来课程里面要优化的一些地方。 首先看代码依赖分析。主包竟然有36MB,主包超过了2MB就无法上传了,主包必须进行优化,无依赖的文件总数也达到了124,最大的一张高清商品图片占用了11MB的一个大小,这是非常恐怖的,所有的商品图片都需要进行网络链接化不能放在本地,接下来我们看代码质量扫描。我们可以看到有多项指标都没有达标,每个红色提示部分都值得我们仔细对待、认真优化还有Performance面板中出现多个红三角,有的任务执行时间竟然超过了400ms 性能已经十分堪忧了。这是Memory面板的一个截图,小程序平稳运行的时候,貌似存在大量新增的内存数据,程序可能存在内存泄露的一个隐患,这些问题也是我们在优化的时候应该重点对待的。 接下来我们再看JavaScript Profiler面板的一个表现。我们可以看到个别脚本执行时间稍微长了一些,达到了200ms以上,这是性能评分面板的一个截图,综合体验评分整体上较差。最大节点数已经超过了1000个的最大限制,这是小程序在冷启动时调试区的一个截图,小程序的冷启动耗时竟然达到了7秒以上。这是在微信开发者工具上的一个耗时,在手机设备尤其是在低端机设备上可能这个时间还要更长,以上我们从诊断结果来看,目前小程序项目的启动效率和运行时性能都很不乐观。 下面我们看一下优化目标。小程序用户设备,按性能可以分为高、中、低三档,一般低端机相对高端机在启动性能上有2到3倍的一个差距,在优化的时候我们不仅要关注高端机的一个性能,还要更加关注低端机的一个表现。一个页面从单击到打开如果超过3秒,用户便很容易失去耐心,对于高端机这个启动要求还要更加放低一些, 开发的时候如果我们使用苹果iPhone8进行测试的话,启动的时候这个时间则要控制在2.5秒以内同时不能有明显的白屏现象,小程序页面操作不能有明显的卡顿现象,为了避免不必要的一些麻烦、达到一致的实践效果,你在安装软件的时候最好选择使用与我相同的软件版本,特别是对于这个Go语言如果这个版本不对的话,稍后我们在编译WebAssembly文件的时候,可能会遇到额外的一些麻烦,这节课我们就讲到这里。 这节课我们主要从整体上了解了小程序的启动流程及双线程的运行机制,简单阐述了小程序在启动流程中的一些优化要点以及运行时的一个渲染优化要点,所有的优化技巧都是建立在小程序的启动流程和运行机制这个基础之上的,熟练知晓这些启动流程和运行机制才能更好地理解和使用这些优化技巧。 从下节课开始我们就以我们糟糕的初始项目,以它为基础,然后开始我们优化技巧的一个学习与实践。 这里有一个问题请你思考一下: 骨架屏它是一个在首屏渲染阶段,可以使用的优化技巧,简单又容易操作。那么你知道小程序里边怎么样使用这个骨架屏技巧吗? 下节课我们就一起来深入探讨一下这个问题。
2022-07-13 - 了解小程序的启动流程(上)
[视频] *所有课程源码的链接:https://gitee.com/geektime-geekbang_admin/weapp_optimize 你好,我是李艺,是腾讯云TVP 、小程序从0到1的作者,极客时间 微信小程序全栈开发实战课程讲师。 小程序上线已经5年时间了,日活目前已经达到了4.5亿+,已然成为任何一家互联网企业都不能忽视的产品运营阵地。小程序的开发能力经过微信团队数年来的不断地努力,目前已经日臻完善,它早已经不是那个随随便便翻翻文档就可以完成开发的一个简易框架了,尤其是在性能优化方面,会优化与不会优化 ,对于产品的运行效果可以说有霄壤之别。 首先我们看第一部分,需要明确一下有哪些需要优化的现象。不知道你的小程序产品有没有遇到过这样的一些问题,点开小程序一直都是白屏什么也看不到、Loading加载提示转了好几圈页面还不显示,单击页面链接的时候页面跳转迟钝迟迟打不开、有的按钮单击了好几次一点反应也没有,长列表内容在滑动的时候越往下滑 页面越卡顿。你有没有因为这些问题而遭受过用户的抱怨,你会以为这些问题都是因为平台技术不完善而造成的吗? 那么为什么京东 滴滴等大厂的一些小程序,它们的功能那么繁杂。但使用起来却还是那么流畅呢,当我们抱怨框架不够给力的时候,我们对微信小程序的性能优化技巧又真正了解和使用多少?性能优化它是一个现人现地的活,讲究具体问题具体分析,需要有一个字节 一个字节去抠,一个毫秒 一个毫秒去节省的这样的一个细致精神。这个课程我们会演示相关的性能优化技巧。我准备了一个性能比较堪忧的项目,这个课程我们就一起诊断 优化这个项目,让它从体态臃肿的一个状态慢慢变得健步如飞,为了更好地理解和应用小程序优化技巧,在开始实践之前,我们十分有必要看一下小程序整体的运行环境以及启动的流程。小程序的运行环境大体可以分为三类,第一类是iOS端、Mac微信端,第二类是Android端 PC微信端,第三类就是我们开发者经常使用的微信开发者工具模拟器端。另外还有儿童手表上面也有微信,但是那个环境它没有小程序,所以不在我们的讨论范围之内。 三类的运行环境,虽然它们在底层是基于不同的技术实现的,但是它们的启动流程大体上是相似的,小程序的优化主要是指从小程序开始启动到首页完全渲染显示,也就是Page.onReady事件派发,这个过程之间的一个优化。这个过程主要包含了三个流程的节点,这一步包括小程序运行进程及运行环境的准备,这里面具体又包括拉取小程序基本信息,包括代码包版本 地址等信息,另外还有Native小程序进程和微信基本模块的一个初始化。例如在Android环境里面有Activity活动组件的一个初始化,再往下是代码包的下载 校验以及初始化,再往后是系统组件 WebView组件容器和原生组件的一个初始化,最后是JS引擎初始化以及域的创建。 下面我们看第二步关于代码注入,这一步主要包含两大部分:第一部分是框架及第三方基础代码的一个初始化,这里面又分为三个小部分: 第一部分是小程序基础库的注入; 第二小部分是扩展库,例如我们在配置文件里面通过使用useExtendedLib引入的WeUI以及kbone这样的一个类库的初始化; 第三小部分是插件、自定义组件 扩展库代码的一个注入。 第二部分是开发者代码的一个注入,这个里面主要分为两个小部分: 第一个小部分是开发者逻辑层代码的一个注入,这里会派发小程序里面的App.onLaunch还有App.onShow这些事件的一个派发,这些事件都是在这个阶段进行派发的; 第二小部分是开发者视图层代码的一个注入,包括公共代码以及页面代码的一个注入。 下面我们看第三部分关于首屏渲染,这个部分大致可以分为五个小部分: 第一小部分是页面的初始化,这个时间点是initDataSendTime这个时间点的一个触发时机,会有Page.onLoad一个事件的派发; 第二小部分是时间点走到viewLayerReaderStartTime这样的一个时间点的阶段,这个时候会有Page.onShow事件的一个派发; 第三小部分是开发者代码从后端拉取数据,准备data数据,这个时候也是一个阶段,是第三小部分; 第四小部分是页面的一个整体的渲染; 第五部分是当这个时间点走到viewLayerReaderEndTime这个时间点的时候,它会有一个Page.onReady事件的派发,这个时候就标志着我们首屏渲染的一个完成。 小程序采用逻辑层、视图层双线程运行机制,Native的工作准备它是先于这两个线程开始之前开始的,基础的执行环境准备好以后,逻辑层与视图层两个线程才开始工作,并且两个线程几乎是并发执行的。在视图层与逻辑层它代码完全注入以后,这个时间点它会对齐以后才会进入下一个阶段,也就是首屏渲染这个阶段的开始执行。 这些节点它并不是每一次小程序启动时都会经历的,有些会有,有些不会有。微信有运行环境预加载机制,如果小程序在启动时命中了预加载的环境,有关准备运行环境的节点就可以省略掉,这一部分的启动时间也可以节省了。对于开发者紧跟小程序框架的更新,及时使用用户覆盖率最广的基础库版本,让自己的小程序运行环境大众化、普通化,则有助于预加载环境的一个命中,终端类型不同经历的节点也不尽相同。 在Android上小程序启动的时候,微信它开启了新线程,在iOS上则没有 iOS上小程序它在启动的时候,它会复用与微信相同的一个进程,因此Android上有小程序进程与Activity初始化这样的一个节点,在iOS上则没有,再加上iOS的设备它普遍的性能是高于Android设备的,所以这使得iOS的设备的启动的效率普遍就高于Android。 对于相同版本的小程序在同一性能级别的设备上运行,iOS设备平均会比Android大概会少0.5的这样一个启动时间,另外还有启动方式对流程经历的节点也有影响。 下面我们看一下启动方式。小程序按启动方式不同分为冷启动和热启动两种方式。 什么是冷启动?什么是热启动?如果小程序在用户设备上是第一次打开或者是销毁后再次打开,这个时候的启动就是冷启动。热启动是相对冷启动而言的,热启动是小程序启动的一种优化机制,小程序进入后台30分钟以内再次进入前台,可以直接从后台状态然后恢复到前台,在这种热启动方式里面,像前面我们提到的小程序基本信息、拉取代码包的下载还有JS引擎初始化等等这些流程节点,甚至像App.onLaunch、Page.onLoad以及Page.onReady这些一次性的流程事件都不会有了。小程序第一次启动以及冷启动30分钟以后被系统回收重新再启动都是冷启动。 前面我们讲的启动流程的主要节点是冷启动流程的节点。我们说的小程序性能优化主要是指冷启动性能的一个优化以及运行时渲染性能的一个优化,在小程序冷启动流程里边涉及到一些程序以及页面事件。下面我们统一看一下这些事件。 在小程序中App与Page都有它们各自的一个生命周期函数,这些周期函数有一些与启动流程是密切相关的。我们先看一下App周期函数,这里面有三个事件需要我们注意: 第一个是onLaunch,它是监听小程序初始化的一个事件; 第二个是onShow 监听小程序启动或切前台这样一个事件; 第三个是onHide 监听小程序切后台这样的一个事件。 下面我们再看一下Page周期函数。这个里面有五个事件是与优化相关的: 第一个是onLoad它监听页面加载; 第二个是onShow监听页面显示; 第三个是onReady监听页面初次渲染完成; 第四个是onHide监听页面隐藏; 第五是onUnload监听页面卸载。 App.onShow事件和Page.onShow事件是视图界面开始显示时派发的,它们会重复派发与启动流程优化密切相关的一次性事件,主要有App.onLaunch Page.onLoad和Page.onReady这三个事件,在这三个事件节点恰当的安排执行合适的逻辑代码是优化的重要技巧一。 至于像App.onShow App.onHide以及Page.onHide Page.onUnload是与运行时性能优化十分相关的一些事件,下面我们根据小程序的冷启动流程以及与其相关的密切相关的一些八个生命周期函数大致讲一下有哪些节点是可以优化的。 第一条在这个环境准备阶段中,在拉取小程序基本信息阶段,这个阶段是有可能优化的。微信对用户设备上经常使用的小程序它会有轮询机制,在轮询的时候会自动拉取小程序的一个基本信息,正常情况下这个小程序的基本信息的一个拉取它是同步的,它会阻塞我们后续流程节点的一个执行,如果通过轮询节省了这样的一个过程,启动流程跳过这个节点时间是可以节约的,当然这个节点开发者基本上做不了什么事情,开发者并不能左右微信的轮询机制。但是越受用户欢迎的一个小程序因为它属于经常使用的小程序,它会命中轮询机制,启动的一个性能也会更好,而那些不被用户经常访问的小程序反而没有这个福利,这大概就是技术里面的一个马太效应,就是好的会更好然后坏的会越差。 针对环境准备阶段微信提供了环境预加载机制,微信客户端会根据用户设备的使用场景和设备资源的一个消耗情况,依据一定的一个策略,在小程序启动之前对运行环境进行部分的预加载,这个过程开发者基本也无法干涉,开发者能做的仅是紧跟小程序基础库的一个更新,积极使用最新的、最普遍的、最广泛的一个基础库版本以及提高预加载环境的一个命中率。 在代码注入阶段,逻辑层与视图层代码都需要注入,两个线程的代码都注入完成以后首屏渲染流程才能开始,Page.onLoad事件才能触发。我们可以想方设法减少代码的一个注入量和复杂度以期减少启动时间,小程序在这方面有分包、有独立分包、有按需注入、有用时加载和占位组件等等这些特性,这些都是这一阶段的一些优化技巧。这些技巧稍后我们在课程里面都会详细介绍。 在合适的生命周期函数节点执行合适的代码也可以优化启动性,Page.onReady事件派发于首屏渲染完成的时候,如果我们要从后端拉取数据并在首页上进行渲染,在这个事件函数里面执行拉取操作,势必会造成二次渲染的CPU资源浪费,但如果我们在Page.onLaunch这个事件触发的时候就开始数据拉取,又可能会阻塞小程序正常的一个启动流程,在这种情况下我们要怎么去做?我们可以使用异步转同步的编程范式以及使用并发复合命令,在多个文件里边对齐这个代码的执行点,这样的话就显得尤为重要了。具体的优化办法,稍后我们在课程里面会详细讲解。 从Page.onLoad事件派发页面开始渲染到Page.onReady这个事件派发首屏渲染完成,这中间涉及到的动态数据加载,其加载的数据量有多少、网络请求所需的时间有多少还有图片等静态资源它加载所需要的时间有多少,都会影响首屏渲染的一个效率,这个阶段使用骨架屏技巧包括压缩图片、提高服务器接口响应效率和数据传输效率等等,这些都可以优化首屏渲染的一个用户体验。针对小程序里面用到的一些数据,微信还提供了数据预加载周期性更新机制,不需要开发者自己去拉取微信就可以代为拉取,小程序在启动的时候,直接取用这些已经加载好的数据就可以了,这也是优化启动流程的一个技巧之一。 当然了这个技巧是微信团队特意为开发者而设计的,针对低端机首次渲染需要较长的一个时间,微信提供了初始渲染缓存机制,启用初始渲染缓存可以使视图层不需要等待逻辑层代码初始化完毕就可以直接提前将这个页面初始化的数据渲染的结果展示给用户。 以上就是针对启动流程中部分节点的一个性能优化技巧,稍后我们在课程里面都会详细地进行讲解在运行的时候针对小程序的双线程运行机制和视图重渲染机制也有相关的一些性能优化技巧。下面我们就再看一下这方面的一些技巧。
2022-07-29 - 使用断点调试功能,及Source Map和真机调试2.0介绍
[视频] 你好,我是李艺。 上节课我们学习使用了JavaScript Profiler面板以及静态依赖分析工具,这节课我们学习断点调试及关于Source Map和真机调试2.0功能的一些介绍。 首先看一下问题,即使对于有经验的程序员,在第一次面对某些奇怪的异常的时候,也不能完全凭直觉判断出来这个问题出现在哪里,这个时候便需要使用Debug需要断点调试,小程序它天生支持Source Map 不需要额外的配置,结合Sources面板就可以加断点进行调试了,并且结合真机调试功能,即使出问题的设备不在眼前也可以远端开启调试,这项技能对于任何小程序开发者,甚至前端开发者都是最有价值的技能之一,下面我们看项目实践。 首先看实践一:Source Map与本地断点调试。 小程序最终运行的代码与开发者编写的代码,它并不是同一套代码,因为原始代码它会经过开发工具的ES6转ES5编译 压缩和混淆,它会变成另外的一种格式,Source Map顾名思义,它是一种将编译后的代码,与原始代码映射起来的一种调试辅助技术,为了方便开发者读懂运行时出错信息,以及快速定位出出错的源码位置,小程序提供了Source Map功能,并且这项功能它是默认开启的,开发者不需要在这个项目里面做任何的一个配置,在编译之后的小程序JS源码文件里面,会有一条以sourceMappingURL开头的一个尾注,这是一条特殊的一个注释,它的内容就是用于Source Map映射的源码以及其他的一些必要的信息,我们将这个信息里面的base64后面的这一部分,使用Base64解码工具解开,会得到一个JS的字符串,在这个字符串里面它有一个叫做sourcesContent的字段,我们把这个字段拿出来以后,再使用正则法将双斜杠n转化为斜杠n,将斜杠双引号再转化为引号,这样就可以得到一个文本的源码。 这个源码便是我们在这个项目里面编写的原始的源码,json字符串在这里面还有其他信息,这些信息它属于编译前后代码的一个映射信息,它是方便开发工具实现源码定位的,Source Map信息虽然在小程序代码包里面它是默认包含的,但是它不计入我们代码包大小的一个计算,不会占用本来就不多的代码包的额度,而且在本地调试或者使用真机调试的时候,微信开发者工具它会自动解码Source Map信息并且还支持断点调试,这一点对我们十分有用,开发者可以在解码后的源码里面打断点,可以很清楚地看到当前执行上下文环境里面的变量的信息,在本地调试区里面,Sources面板里面,我们可以找到appContext的节点,它下面有一个叫做127.0.0.1这样开头的一个节点,继续往里面我们可以找到appservice,然后下面有一个app.js?[sm]这个节点,这个文件就是微信开发者工具帮我们复原出来的一个主文件源码,我们在这里可以打断点,可以步进,可以步入,可以步出还有可以跳过,进行这些常规的调试操作,当我们把鼠标放在变量上面的时候,还有小的浮动窗口可以显示这些变量的值,与app.js?[sm]文件并列的还有一个叫做app.js文件,这个文件是我们编译以后的真正的运行时执行的文件,我们打开这个文件可以看到里面有以sourceMappingURL开头的信息。 下面我们开始实践一的代码演示。 首先打开我们小程序项目,打开以后单击编译,打开我们调试区,在调试区里面 我们选Sources面板,这个里面我们有很多节点 我们可以选择top,top里面有appContext这个节点,再往里选择127开头的节点,再选择appservice,在这个里面有一个叫做app.js?[sm]这样的一个文件,我们选择这个文件,这个文件看起来有点熟悉,这就是我们的项目里面的一个主文件源码,我们在这个地方已取到了系统消息,这个地方打一个断点 ,打完断点以后 我们单击编译重新进行项目的启动,当再次执行到这个位置的时候,我们发现小程序它暂停了,停在了我们设置断点的地方,在这个地方我们可以进行步进 跳入 跳出以及跳过常规的一个调试操作,我们可以一步一步地来查看它每一个执行的状态,把鼠标放在上下文里面的这些变量上面的时候,它可以显示这些变量的一些状态以及它所包含的这些值。另外在这个地方还有一个Watch,我们还可以将我们这个里面的变量,可以使用添加监听的这种方式加到这个地方,然后以观测它们的一个变化也是可以的,如果我们不需要调试,可以单击这个让它结束 结束本来的调试,这样我们这个页面就渲染出来了。 再次运行一下,我们要看另外的一个文件,再次打开这个节点,与我们这个文件然后相并列的还有一个app.js文件,这个代码是我们编译以后运行时真正要运行的一个源码,在源码的底部它有一行注释,我们看一下这不有一行注释,注释很长 我们将这个给它拷贝一下,双击选择拷贝一下,拷贝以后我们找到一个Base64的一个解码工具,在线解码工具 把这个内容拷贝在这个地方,然后在这个里边,这个信息我们没有拷贝全,需要再重新拷贝一下,复制 然后粘到这个地方来,开头有些信息我们不需要。我们从base64逗号后面的给它断掉,只需要后面这部分就可以了,然后进行解码,解码以后这个就得到了一个json的字符串的信息,我们将这个信息给它拷贝一下放在我们的VSCode里面,新建一个页面,然后这个页面 这个文件我们选择json作为它的格式再进行格式化,看到没有,这个就是我们反解码出来的一个调试信息。 在这个地方有一个sourcesContent,我们将它里面的这个信息给它拷贝一下,可以选择自动换行让它换行一下,这样我们选择起来会比较好选,然后选择这个信息 选完这个信息以后,我们再打开一个文件,打开以后 然后我们这个地方也给它选择json,json作为文件的格式,再选择自动换行,这个里面目前没有办法格式化,我们需要做一些处理,打开正则替换,我们将两个杠的斜杠n转化为一个杠的斜杠n进行一个替换,还有一个是里面的引号,斜杠n引号转化为引号再进行替换,替换完了然后格式化,这个就是我们最终的一个源码了,源码是JS的,我们将文件的格式改一下,改成JavaScript,这就是我们最终的一个源码,当然这个地方好像没有替掉,我们再重新再替一下,现在就已经替掉了,这就是我们JS这个源码。 下面我们看实践二:真机调试2.0与远程断点调试。 微信开发者工具在调试区默认打开是本地调试,在这种调试模式下代码在本地执行调试面板也是在本地查看,另外还有一种真机调试,真机调试它是远程调试,在这种调试模式下,这个代码它是在远程的设备上运行,调试面板是在本地的开发者工具中查看,为了便于区别 真机调试模式打开的时候,微信开发者工具并没有复用调试区的面板,而是在弹出的新窗口里面打开相似的一个调试区窗口,真机调试又分1.0和2.0两个版本,2.0它是应用了新的调试模型,使得调试结果更接近于真机的表现,同时它还支持更多的高级的接口的一个调试,例如像Canvas画布类的接口,它的运行近似相当于真机运行,有一些在模拟器里面无法测试的一些内容,例如像流量主的视频激励广告等等,在真机调试里面都可以进行调试,1.0 2.0这个模式它在开启的时候可以相互的切换,我们在面板里面的左下角它有切换按钮,这里还有一个选项是局域网模式,局域网模式它是干什么用的呢,在开启真机调试的时候,设备与微信开发者工具的调试窗口之间保持了一个实时进行调试信息通讯的一个长连接,如果被调试的手机设备在远端,这个时候必须要经过一台服务器进行中转,而如果这个设备它在本地局域网里面,它就没有必要通过这个服务器进行中转了,这时候我们勾选这个选项,就可以在一定程度上提高我们调试的通讯效率。 在iOS设备的真机调试窗口里面,我们从运行截图可以看出来真机调试这个窗口它没有Performance面板,但它有这个Memory,JavaScript Profiler,还有Storage面板,这些面板的使用方法与我们微信开发者工具在默认的调试区里面打开的这些调试窗口用法是一样的,其中这个JavaScript的,这个Profiler面板在这个默认状态下可能是不显示的,这时候我们只需要选择右上角的更多按钮,然后选择more tools便可以进行手动开启了。 这里有一个问题,就是我们什么时候使用真机调试2.0版本,2.0真机调试推出来以后,1.0它其实并没有消失,它仍然有它的一个价值,有些小程序它可能在2.0下面无法正常运行,这个时候我们可以切到1.0模式进行调试,真机调试功能一般在这个产品上线之前使用,在内测期间,由于QA测试团队和研发团队可能不在一个办公区,这个时候我们就可以使用真机调试功能或者是用户的设备上出现了非常奇怪的一个问题,开发者仅凭这个线上日志他无法定位到这个错误,这个时候我们也可以使用远程真机调试功能,下面我们再看如何在真机2.0调试里面使用断点调试这样的功能。 在真机调试的调试窗口里面可以打开Sources面板,从左边的文档树里面可以找到webapp目录,然后在这个里面可以依次找到app.js?[sm]这个文件,这个文件名和我们在本地调试的时候看到的是一样的,与默认调试区的Sources面板的使用方法一样,在这里我们也可以使用断点调试功能,无疑这对于远程查看设备异常有莫大的一个便利。 下面我们进行实践二的代码演示。 打开我们小程序项目,然后在这个工具栏里面选择真机调试2.0,这个地方有一个切换真机调试2.0,切换真机调试1.0这样一个切换按钮,这个地方有一个局域网模式一个选项,我们选中,在进行手机上真机调试的时候还有一个事情必须要确认,就是我们的request.js里面,我们之前说过我们BASE_URL一定要使用我们本机的局域网的IP,不能再使用前面localhost了,如果用这个网址,我们在手机上无法访问我们这个接口,这个确认以后 接下来我们就可以进行真机调试了,然后选好以后我们单击启动调试按钮,如果是我们手机上已经打开过我们这个测试版本的话,当我们单击这个按钮以后,在手机上它会自动打开我们小程序,同时我们微信开发者工具里面真机调试2.0的调试窗口也会同步打开,这个窗口跟我们开发者工具里面默认调试区里面的窗口它不是一个窗口,这个是专门为真机调试来使用的,但它里面的这些面板与我们原来基本上的这个功能都是一模一样的。 在这个手机上面我们可以看到在右上角,它有一个已连接,展开这样的一个浮动的这样的面板,这个里面可以看到,我们电脑与我们手机现在已经建立一个连接,现在我们在开发者工具里面选择Sources面板接着选择weapp,weapp下面我们可以看到这里面仍然是有这个文件的,仍然是我们有这个文件,然后在这个文件里面我们仍然是可以找到,比如说我们先前已经使用了,已取到了系统消息在这个地方,然后打入一个断点,打入断点以后 我们可以重新进入我们小程序,我们还可以关掉,关掉然后再次打开,刚才是让这个面板记住我们这个调试信息,再次启动一下,现在我们可以看到我们这个断点已经执行到这个位置了,在这个地方我们仍然可以进行Step,还有Step into Step out和Step over的一些调试操作,同样在这个地方,我们还可以查看我们当前执行上下文里面的一些变量以及它变量所包含的这些值,在这个地方都可以看到这个功能与本地调试的时候,它这个功能是类似的,我们这个代码演示就说到这里。 最后我们总结一下,微信开发者工具拥有的这些调试工具各有各的用处,我们具体来看一下定位运行性能问题,可以使用Performance面板Memory面板和JavaScript Profiler面板,查看这个代码包的大小以及文件大小的占用情况,我们可以使用静态依赖分析工具定位JS代码错误,可以使用Sources面板以及断点调试功能,其中调试我们又分为本地模拟器调试和远端真机调试,这两种调试模式都有这个Sources面板,并且都支持这个断点调试功能,除了上面的这些工具以外,微信开发者工具还提供了一个运行时性能分析工具,这个工具我们从下节课开始进行介绍,这节课我们就讲到这里,上面的网址就是本课所涉及的一些文档地址。 点击查看相关文档: 真机调试2.0Source Map在浏览器中调试微信开发者工具下载的 sourcemaps 怎么用 这节课我们主要学习了断点调试及关于Source Map和真机调试2.0功能的一些介绍,下节课我们开始学习视图代码的一个优化技巧。 最后说一下思考题,这里有个问题请你思考一下,我们人类的视觉信息处理能力是有限的,一般低于200ms的变化基本上是感觉不出来的,还有对于某一些scroll事件触发的事件回调,它频率也非常之高,如果是任由它们触发和执行可能会对设备资源造成极大的一个浪费,在传统工业里面有一种叫做节流阀的东西可以限制液体的流动,让流体运行更加稳定和高效,那么在小程序开发里面也有类似的一个机制吗?我们在这个小程序开发里面可以怎么去处理这些问题,这个问题先留给你思考一下,下节课我们一起来深入探讨一下这个问题。
2022-07-14 - 视图代码优化技巧
[视频] 你好,我是李艺。 上节课我们主要介绍了微信开发者工具中一些调试工具的使用,这节课学习视图代码的优化技巧。 首先看一下问题,微信开发者工具里面的性能优化工具中,有一个非常重要的性能优化工具,也就是体验评分,我们还没有介绍,从调试区单击体验评分面板,也就是Audits面板,就可以看到微信开发者工具,对当前小程序项目的运行评判结果了。在这个面板里面会给出很多具体的优化建议,下面从8.1到8.7讲,我们会重点介绍体验评分面板里面所提到的优化事项,以及相关的优化技巧。下面看项目实践。 首先看实践一,在动态列表渲染里边优化wx:key的使用。 先看一张示意图,如我们现在屏幕上展示的这一张,假如有一个数据列表,它的起始数据内容是ABC,后来数据发生变化了,变成了AHBC,那么当这个列表数据变化的时候,它们在这个视图里面的列表渲染会引起什么样的一个变化,在小程序里面wx:for这个标签属性它用于列表渲染,wx:key用于指定列表渲染里边列表项的身份标识,如果这个列表中,它每一项都有一个唯一的key,当有一项新数据增加的时候,例如我们这个列表里面增加了一个H,在重渲染的时候,小程序它会创建一个新节点,并且把这个数据H更新给它,同时将这个BC节点在这个列表中向后移动,如果这个列表里边它每一项没有一个有效的key,小程序它就无法正确识别这个视图里边列表元素与这个列表项的一个对应关系,这个时候小程序同样会创建一个新节点,但是会依次将HBC更新给这三个列表项,与前面那种更新方式相比,也就是与有唯一wx:key更新的方式相比,后面这种更新方式小程序会重新渲染三个节点,而前面那种方式它只需要渲染一个节点,从这个列表数据的这个重渲染机制来看,对于列表数据的动态渲染,使用wx:key属性,并且给每一个列表项设置一个唯一的key是十分有必要的,具体怎么做呢? 在我们的这个页面标签代码里面如我们屏幕上看到的,我们可以将这个wx:key给它指定一个字段id,这个id它是唯一的,在主页的wxml文件里面,一共有三处使用了wx:key属性,在使用wx:key属性时,它的取值情况我们大致可以分为以下两种。第一,如果列表元素是单一的基本数据类型,并且是唯一的,这时候我们可以直接写成*this,这里这个*this,就代表当前数据列表里面的数据元素,如果列表元素它是一个对象,也就是Object的类型,我们可以填写成列表元素对象里面的一个字段名,这个字段要在这个数据列表里边是唯一的。如果这个列表它不是动态的,只渲染一次,wx:key设置与不设置,这个时候是没有差别的,此时wx:key属性可以设置为index,index是一个默认的下标变量名,这种设置虽然没有实际效果,但是却可以有效消除,微信开发者工具,在调试区所呈现的黄色警告,首先我们打开我们的主包index下面index.wxml文件,在这个文件我们有多处使用了wx:key这个标签属性,我们可以检索一下。检索一下上面的所有的用到的标签属性的地方,第一个,这是我们的滑块视图区,在这个里边我们用了一个wx:key这样的一个标签属性,它的这个值目前是*this,这种写法前面我们提到了也是可以的,但是对于我们目前的swipers列表数据,因为它是静态的,所以这个地方我们没有必要这样去写,这个地方我们直接写成index就可以了,接着我们再往下查看下面的使用这个标签的地方,这个地方是一个长列表内容区,这就是长列表内容区了,这地方我们用了这个recycle-item,在这个地方有一个wx:key绑定。注意这个列表它已经是一个动态数据列表了,然后我们从后台会不断拉取新的数据,然后填充进来,所以这个地方我们用index就不太合适了,我们可以用它这个数据列表里面,它本身所含有的,所包含的字段,比如说id,id就是它里面的一个唯一的字段,所以我们这个地方可以用id作为它的一个字段名,这样就可以了。 我们再看其他的,这两个地方都已经改了,另外我们还要看一下我们导航区在这个地方,这个地方我们也有一个wx:for列表渲染,我们这个页面里面一共有三个。这个地方也有,但是它目前还没有这个wx:key属性,导航区的列表,它是一个静态列表,它其实并不是动态的,所以我们这个地方简单起见,也可以使用index,给它写一个index就可以了,写完以后在我们调试区就不会出现一个黄色警告了,不然的话它会有一个警告提示,会让我们给它加一个这样的wx:key,加这样的一个标签属性,这个代码演示就到这里。 下面我们看实践二,绑定视图事件。 使用catch,代替bind,减少dataset的一个数据运输量,在很多教程示例里面,绑定事件,一般默认使用的是bind,但是大多数情况下,我们监听的事件,它其实并不需要冒泡。我们只是需要在某个特定的wxml节点上监听事件,这种情况下我们完全可以使用catch代替bind,使用catch绑定视图事件可以自动忽略冒泡阶段,避免因为不必要的事件触发,造成额外的逻辑层代码的一个执行的浪费,还有在添加事件监听属性的时候,一般我们需要回传一些额外的信息,这个信息可以使用以data-开头,这样的一种形式,去定义自定义属性,通过事件对象进行传递。但是在JS逻辑层,我们实际需要什么样的一个信息就传递什么信息就可以了,不要因为为了省事,将一个很大的数据对象整体传递过去,也不要传递多余的本来它就不需要的一些信息。例如在我们这个onTapRecycleItem,这个函数体代码里面,我们只需要传递进来一个index就可以了,在有了这个index索引数据以后,其他的一些信息像id,我们就可以直接从allList列表数据里面去获取,如我们现在屏幕上看到的代码截图所示,在主页JS文件onTapRecycleItem这个方法里面,id信息就是通过index索引间接取到的,每次事件回调函数的一个触发都涉及到一次,从视图层到逻辑层的数据传递,如果每次传递的数据量小于64KB,并且在30ms以内可以执行完成,这样的一种情况就属于一个正常操作,这种情况下一般都不会太影响页面的一个正常渲染。 在VSCode里面打开我们目前的代码,我们可以从这个项目的基本信息里面,然后确认我们本地代码它所在的位置,所在的一个本地目录的一个位置,这是我们当前代码的一个位置,然后在这个代码里面,在小程序的源码里面,我们进行一个全局的查找,比如说我们查找bindtap,查到它,查到以后我们可以看到,10个文件里面有14个结果,可以依次单击查看,这个地方有很多都是bindtap,刚才我们提到了大多数情况下,我们所有的关于tap这个事件监听,其实都不需要使用bind,我们使用catch就可以了,所以这个地方可以直接改成catch,可以改成它,改成它以后,我们然后单击全部替换,这样就可以了。改完以后我们可以在小程序这个项目里面,微信开发者工具里面,然后单击编译进行测试一下,看看有没有问题,一般情况下都是没有问题的,这是一个修改。其他的一些事件,其实我们还可以修改,查找,在我们这个里边有其他的一些绑定,事件绑定,所有以bind开头的其实都可以,这个地方我们还可以排除文件,也可以限制,可以排除js文件,还有json文件也可以排除。因为本质上我们需要查找的只有wxml文件,标签文件,当然这个查找它可能会查找出很多,查找以后 然后我们可以采用单个修改的方式,然后把它们给它替换了就可以了。 下面我们再看另外一个地方,就是在我们主页里边,我们有一个长内容列表,有这样的一个列表,这个列表里面在它列表项渲染的时候,我们传了通过data自定义数据属性传递了两个额外的信息,一个是id,一个是item,这两个信息传进来了,并且这个信息在在我们回调里面,在js里面我们是有使用的 我们可以看一眼,在这个地方,我们直接取了从dataset对象上取了id,item也取了,我们有取用,这个地方其实我们也可以修改,对于我们不需要的一些信息,我们没必要传递。并且这个信息量要尽量传的小一点,像item这个信息它其实是比较大的,因为它整体是一个Object的对象,整个对象的传递,它其实是非常浪费我们有限的设备资源的,这个地方我们传一个index,它其实当前列表里面索引,这两个就不需要,给它去掉,去掉以后,在我们js里面这个地方,我们就可以先取我们index相同的方法,把index这样取到,这个取到,然后下面这两个不能这样去取了 ,但是我们可以间接去取,因为我们这个地方这是用了两个对不对,我们可以间接来取,怎么样间接来取,怎么间接来取,看一下我们最终的源码。 第二个代码演示。 在这个地方先取到index,然后通过allList取到item,然后再从item上面取到的id,这样的话这两个信息都取到了,把这个代码我们拷贝 一下放在这里,然后allList我们可以看一下,它其实是在我们当前data数据对象底下,有这样的一个列表数据,而我们传进来的index其实是它的一个索引,在它里边的索引,我们的标签里面,我们知道我们这个地方,我们用了recycle, recycle-view组件,用了组件以后,我们内部的渲染的index,其实不是它的索引,是我们所有的allList,它本身所有数据的这样一个索引位置。所以在这个地方我们可以这样来使用,这个代码修改完以后,我们可以测试一下,然后单击刷新,单击某一项 ,没有输出,为啥没有输出,是我们没有触发吗,这个地方写错了,这是开始应该是catchtap,其实并不是catchbind,因为我们刚才替换的时候这地方是有问题的,刚才我们替换了14个,对不对。现在将这14个然后再重复再替换一下,这种替换我们要小心,刚才我们替换以后,其实我们也同样进行了单击测试,但是没有发现错误,像这种标签,绑定标签写错了,然后本身程序也不会发生错误,只有我们实际测试的时候,相关功能的时候,然后才会暴露出来,这个改完以后我们再单击编译进行二次测试,Ok。现在我们可以看到,我们单击哪一项,它现在所打印的列表项5,标题5,标题6,就是我们所单击的这一项,说明我们传递index的数据它本身是准确的,另外在我们打印代码,我们目前的这种方式,因为我们只传了index,相比我们原来的这种方式其实已经优化了一些了,这个代码演示就到这里。 下面我们看实践三,使用节流函数和防抖函数,防止按钮误点击与scroll事件函数频繁触发。 什么是节流,顾名思义就是控制某段JS代码的一个执行频率。举个例子,限制某类事件,一秒内只能触发一次函数执行或者是三秒发生一次,这样就是一个节流,在scroll input事件里面,如果触发网络请求或本地缓存读写这样的一个代码执行,由于此类事件它的派发比较频繁,所引发的操作又比较费时,这时候就很容易造成程序卡顿,这种场景便适合使用节流操作。下面我们再看一下什么是防抖,顾名思义就是防止抖动,避免把一次事件当做多次来处理,敲击键盘就是一个经常都会遇到的防抖操作场景,在这个网页里面我们监听用户单击键盘或者是某个按钮,为了防止用户误点了多次,这种场景下就适合使用函数防抖。什么时候应该节流,什么时候应该防抖,简单来说事件比所驱动的操作频率快,这种情况下我们用节流。事件与所驱动的操作频率都很快,这种情况下我们用防抖。在主页的JS代码里边,有两个防抖与节流的例子 我们一起来看一下,如我们现在屏幕上看到的截图所示,它所使用的throttle防抖函数,消弱的是列表项的单击回调函数的一个执行次数,对于这个onScroll,它使用的是debounce节流函数,消弱是列表的滚动事件,回调函数的一个执行次数,在测试项目的时候,我们从调试区可以清楚看到,防抖与节流的一个具体的打印信息,效果还是很明显的。 下面我们看代码实战。 在使用防抖和节流函数以前,首先我们需要实现这两个函数 我们看一下最终的一个源码,这个源码在8.1.3。我们找到在library下面 optimus下面 然后有一个throttle,另外还有一个debounce,这两个,一个是节流,一个是防抖。这两个我们把它拷贝一下,然后放在我们目前的项目下面,先拷贝过来,然后我们再看它的一个具体的实现,我们先看throttle,节流函数是这样的,我们要传进来一个method方法或者一个函数传进来,传进来以后,同时还要传进来一个,我们期待等待的一个时间,就是节流的时间,这个单位是ms,再下面我们会传进来以后,我们会返回一个闭包,其实它是一个匿名函数,是一个闭包,然后在每次我们这个闭包要执行的时候,我们首先要查一下,这个时间与我们前面的时间,它所相差的时间差,如果是大于我们设定的时间值,比如说50ms的话,我们就调用它,同时把前面的记录的时间值给它更新一下。否则的话我们就是打印一个节流少许这样的文本,然后就放弃执行了。这是主要的一个代码,接下来再看debounce它的一个执行,debounce也是要传进来一个方法,或者是函数传进来,然后还有一个时间wait的设置默认也是50ms,在这个地方我们要看一下,因为它触发执行,它可能是多次进来,所以我们也要返回,有这样的一个返回,返回以后,当返回的闭包被执行的时候,我们这里边会有一个setTimeout,就是设置一个延时定时器,当达到这个时间的时候,然后去调用它,然后去执行。因为它后面会可能有重复的事件触发时间点给它传进来,然后触发它的一个执行,所以这个地方,当后面的它进来的时候,我们要看一下它这个timer是不是为空的,如果它不为空,首先我们要清除,清除以后然后打印一个防抖少许,然后接着再去设置一个延时定时器,这样的一种方式,达到一种防抖的这样一种效果就是避免我们单击某一个按钮的时候,单击多次,然后执行多次的这样一种情况,因为用户他可能会出现误点的这种情况,我们需要将他多次单击然后处理成一次单击,然后这样去对待,这是我们两个方法。工具函数已经有了,接下来我们要再看怎么样去使用。 首先打开我们主页,在主页文件,使用之前,首先我们要引入代码,敲一下我们的引入代码,一个是debounce,然后是路径,library,optimus,然后是debounce.js,这是这一个。另外我们还有一个是throttle,这两个它的位置一样,其实只是它的名字不一样,以简单可以把它给拷贝过来,然后把这个名字给它改一下就可以了 ,下面我们开始应用,首先我们需要查看一下我们当前主页上,第一个我们先看一下我们最终的源码,看我们最终源码里边是怎么样去用的,打开首页,这是引入的先看throttle节流,这个地方,我们用到的onRecycleViewScrollToLower,就是我们滚动列表,它滚到底部的时候开始触发的,这样的一个事件回调函数,我们需要对它进行一个节流,进行节流。使用的方法就是我们直接在它后面这个方法,它这个方法的前面,然后给它加这样的一个throttle的调用,把它作为一个参数,它返回这个闭包,然后再给前面这个,这样的一种方式。我们先看一个使用。 在我们的代码里边,找到Recycle,也就这个方法,这是我们现在这个代码,然后我们要改写的话,只需要在它这个地方,加一个它,让它作为一个参数传给它,这样,这样就可以了,很简单。下面我们再看另外的一个使用,再看看还有没有其他的一个,还有onScroll,onScroll的时候我们也使用了throttle这个函数,所以这个地方我们还可以在它的下面这个地方,这个写法跟上面那个稍微有点不同对不对,但是我们可以改写,throttle然后传给它一个参数,然后这样传给它,然后这个地方加一个小括号,这样就可以了,这是关于throttle。然后另外还有debounce的使用我们来看一下,是用到了onTapRecycleItem这个方法上面,然后回到我们这个项目里面,先查找一下这个方法,在这个上面,用到它的上面,我们同样的一个方式,在它的后面加一个debounce,然后传给debounce的一个参数,一个匿名函数传递给它 作为它的一个参数,这个改写就完了。基本上就是我们在它原有的函数外面,然后多加一层 加一个方法的一个调用,代码写完了,然后我们单击编译做一个小的测试。 有问题了,有问题是正常的,然后我们看一下什么问题,现在我们看一下在我们的调试区,然后报出了异常,大概是在我们request.js这个位置,然后它触发了一个异常,我们可以在这个地方简单打一个断点看看它有什么样的一个,然后再刷新一下,此时这个参数是没问题的,可以把它放过去,因为对于同样一个方法的调用,它可能多个地方都调用到它了,并不一定每个地方都会发生问题,有可能只是某一个地方发生问题,这个时候我们可以看到我们args现在它是一个undefined,然后我们在undefined上面调它的url显然是有问题的,所以代码这个地方出现了问题,为什么这个参数会产生空的这样的一种情况呢,发生问题我们主要要往前梳理,看一下我们刚才主要修改了哪些代码。我们刚才在这个页面的首页,js文件这个顶部我们添加了两个函数,然后在这个地方引入的时候,我们这个地方关键字写错了,这两个长得很像 ,一个require 一个request,对不对,长得很像,我们给它改过来,都改成require,改完以后,然后我们再测试一下,现在这个问题就不再出现了。 我们接着测试,我们刚才新添加的防抖和节流这样的一个功能,对不对,首先是滚动,当我们往下滚动的时候,列表发生了滚动,这个代码是谁打印的,是我们里面的有一个叫做onScroll,是它,这个地方有一个列表发生滚动,是它打印的,但是我们这个地方为什么节流函数还没有添加,是我们前面加错了吗,我们检查一下我们,检查一下我们下面的方法,onScroll这个是有的,这里边有两个重复了,所以我们这个地方将这个给它拷贝一下,这种一般我们放在底部,因为它这里面页面函数如果你有重复的话,它会用你下面的,下面会把上面给它重写,然后你上面即使加了throttle,它也没有效果了,是这样的一种方式。并且这种错误也不容易发现,这个要靠我们细心,修改以后,然后我们再单击编译,再次测试,向下滚动,然后刚才我们看到的打印是列表发生了滚动,没有节流少许的一个打印对不对,现在我们再滚动的时候,我们可以发现它会有节流少许的一个打印信息,我们滚动得越快,打印信息越多,这样的一个方式,它减少的是对我们onScroll方法的一个调用,这是它,另外我们throttle,还有一个地方是在onRecycleViewScrollToLower事件回调上面,也加了throttle,这个是在我们滚动到底部的时候,它会发生,也就是在开始拉取新的数据之前,它会发生一个,然后这样的一个东西,我们会同样也是打印节流少许,因为节流少许它是在统一的throttle这个里边去打印的,打印数字是一样的,它跟我们滚动时候以及滚动到底部的时候打印的是一样的,这是两个。 接下来我们再看另外的一个防抖,防抖我们检索一下,debounce,在onTapRecycleItem,我们单击这个,然后它会出现这样的标题,当然我们刚才单击是比较温柔的一种单击。现在我们对它进行暴力单击,你看短时间内单击多次,然后这个时候我们防抖少许就已经有打印了,正常的单击一次其实是没问题的,一般情况下不会有问题,但是如果是我们单击多了,然后它这个地方就出现了,单击防抖少许,这是它的主要的一个作用。下面我们再看一下我们这两个工具函数的一个写法,基本上最简单的一个写法就是我们在用这样的一种方式,前面是名字,然后后面跟一个匿名函数,然后我们再把throttle或者是debounce,然后加在后面匿名函数的外面,然后对它重新包一层就可以了,这种情况下我们在这个里面如果是用了this的话,this它仍然不会丢失,它仍然会指向我们当前的页面,也就是它原来该是什么样子,现在还是什么样子,而不会受到影响,另外还有一种写法是什么写法。我们看一下,我们拿这个debounce来看一下,把function给它删掉,改成箭头函数,这样的一种方式,改完以后这是一个。另外我们还可以同时再改一下,我们的throttle,这个地方也可以给它改成,箭头函数的一种方式,然后下面还有一个这个,改完了,然后重新单击编译进行测试。首先是滚动没有问题,然后滚动到底部,滚动到底部的时候发生了问题,我们可以看一下,这个是我们在发生滚动到底部的时候,我们可以再测试一下,看到没有滚动到底部的时候,它其实是我们的,我们可以检索一下我们滚动到底部的函数,看一下,onRecycle在这个里边对不对,然后它提示我们不能访问空对象的this,也就是在这个地方它的this丢失了,它this丢失了,它是一个回调函数,这个地方this丢失了,所以在这个地方保守的一种方式,我们把它改回来,加上function这样一种方式,我们这个里面用this了看到没有,这个地方,这两个地方都用了this了,当我们改成箭头函数的时候,它其实不太好使,而用这种传统的写法,function这种匿名函数的写法,反而能够保留this它正确的一个指向,保留this正确的指向,然后我们再看另外一个地方。 这个地方倒不会出现问题 因为这个地方,我们即使对它改写成这样的一种,改写这样一种箭头函数的一种方式,因为它里面没有用this,所以它没有问题,所以这个地方无所谓,我们用,当然我们也可以改回来,如果是我们后续的代码里面,如果再增添代码,需要使用this对象的话,我们可能也需要给它改回来,这是一种方式。另外一个debounce,看一下debounce,debounce这个里面也用了this对不对,我们可以测试一下,这个是单击,把这个清一下,看到没有,它也会提示这样的一个错误,this没有找到解决方式的跟那个也是一样的,我们也可以加,把它改回来加上function,箭头函数给它去掉。这个地方有个问题我们思考一下,为什么我们把后面改成箭头函数的时候,它里面this为什么会丢失,为什么我们用function它不丢失,简单来讲就因为我们用箭头函数,因为箭头函数它本身,它是没有自己的,this对象的箭头函数是没有自己的一个this对象的,然后如果我们在箭头函数里面,然后用了this的话,它会取它上一个,就包含这个箭头函数的,所在的父函数里面的this对象,它这个父函数是谁,父函数是这个debounce或者throttle,而这个debounce和throttle我们知道其实,我们是在哪里定义的,我们是在我们另外的一个文件里面单独定义的。你像这样的一个文件,其实它是属于什么,它相当于是全局函数,你在这地方取this对象,取到的谁,取到的其实是全局对象。然后你在全局对象上再取它下面的data,你显然这种情况是取不到的,而另外一种方式写法,我们用function的写法,这种写法其实它就比较保险。它本身它作为一个就是最朴素的,经典的一种函数的写法,它本身是可以持有自己的this的,然后我们可以保留,我们原有代码里面的这种this对象的一个正确性,同时我们在throttle或debounce里面,我们也不需要将this给它传进来。因为我们在调用它的时候再把this对象传进来,反而增加了我们这个方法调用的一个复杂度,所以最简单的一种写法,就是我们在前面加上 外围加上一个debounce或者throttle,然后同时里面保持function的这样一种原始的写法,这样就可以了。这是我们在使用这两个工具函数的时候,我们需要注意的一点,不然的话可能会造成this对象丢失,给我们这个代码,给我们项目造成一些不易察觉的错误或者是异常。 下面我们看实践四,重渲染与使用wxml标签要克制。 前面我们曾经介绍过小程序有重渲染机制,这个机制在setData方法触发视图更新的时候会起作用,每次重渲染时小程序,将data与setData的一个新数据套用在wxml标签上,得到一个新节点树,然后将新节点树与当前这个节点树进行比较,这样就可以知道哪些节点以及哪些节点属性需要更新,还有知道哪些节点需要移除或者是添加,最后将setData新设置的数据合并到data数据对象上面去,并且使用新节点树替换原节点树,完成组件或者是页面的渲染工作。在重渲染过程中,我们可以看到有两个影响渲染效率的点,第一个点是wxml组件节点越多,嵌套的层次越深 越复杂,这时候渲染就越麻烦,渲染起来效率也会越慢。第二个就是我们通过setData传输的数据量,数据越多,渲染也就越慢,所以这个地方怎么优化就很明显了,一是减少wxml节点的数量,二是控制setData每次传递的数据量。需要更新哪些节点,我们只传这些节点需要的数据就可以了,有时候开发者他为了简单方便,他每次需要改变数据的时候,他先设一个data这样的临时对象,然后把需要更改的这些数据都写在了一个对象里面,最后再统一调用setData(data)这样的一种粗暴的方式,然后进行更新,这样的写法虽然简单,似乎还拥有一种暴力美学的一种美感,但是它的更新效率却是十分低下的。对于小程序页面的wxml节点,官方建议的优化标准是总页面节点树小于1000个,节点深度层级小于30层,子节点树不大于60个,在编写页面代码的时候,我们时刻应该保持一颗克制的心,要有初代程序员写代码时那种一个字节,一个字节去抠的一个细致精神,对于页面里边的节点,能用text标签就不要用view标签,能直接用文本就不要用text的标签,对于一些循环渲染的列表可以使用block标签的,我们就不要再浪费一层view标签。我们举个例子,对于stopwatch_nw组件,在它的wxml页面里边,我们可以看一下,如屏幕上展示的代码截图所示。在精简标签以后,只有这个slot插槽和这个text文本标签存在,而其他的子标签都不再需要了。 下面进行实践四的代码演示。 在我们的项目里面找到我们的秒表组件标签,我们以stopwatch_nw组件为例,看一下它这个标签代码,这是它这个标签代码,原始标签代码,对于这样的一个标签代码,其实我们是可以优化的。你比如像这个地方,它的slot插槽外面仍然有一个view标签,这个其实是可以去掉的 是不需要的,所以我们在这个地方可以怎么样修改,复制一份到下面把上面的给它注掉,然后这个地方view不需要了,去掉,然后这个text它本身其实也是可以作为一个节点,就是文本本身,它其实没有必要一定要放在一个text,或者是view这个节点下面,它本身也是可以存在的,所以这样写也是没有问题的,我们可以精简成这样一种状态。当然对于我们目前的组件,因为它标签不是特别复杂,我们这里只是演示怎么样尽量去减少页面以及组件的标签的节点的复杂度,以及嵌套的层级。尽量减少节点,尽量减少嵌套的层级,这样的话可以优化我们视图的一个渲染效率,关于这个代码演示我们就说到这里。 最后总结一下,关于视图代码优化的小技巧。第一点,使用wxml标签要克制,能不用容器标签就不用,能少用标签就少用标签。第二点,默认使用catch代替bind绑定事件,自定义的data数据属性里边永远只存储基本的数据类型,并且只存储小数据。第三点,在动态渲染的这个列表里边,一定要绑定一个唯一的wx:key,静态渲染的我们可以使用index。第四点,对于scroll高频事件,我们要节流 使用这个节流函数throttle。第五点,对于用户的单击事件,尤其是高频触发的一个单击事件,可以适当使用防抖函数debounce,视图代码主要就是wxml标签代码,优化视图代码,就是优化对wxml标签及其属性的一个使用,通过小程序的性能工具,我们可以发现启动过程中,有一个叫做Parse WXML的过程,如果页面足够复杂,标签嵌套足够深,那么Parse WXML的过程它的耗时就会很长,wxml和wxss它是经过解析后才能完成视图层代码注入的,如果这个过程很长的话,势必会影响冷启动的一个总时间。这节课我们就讲到这里,上面的网址是本课所涉及的文档地址。 点击查看开放文档: WXML 语法参考 /列表渲染 这节课我们主要学习了关于视图标签及其属性的一些优化技巧,下节课我们学习wxss优化技巧。 最后我们看一下思考题,这里有个问题请你思考一下,小程序项目在开发过程中样式代码会改来改去,总是会产生一些废弃的但是又没有及时删除的一些样式代码,这些代码如果也打包在最终的代码包里面,势必会造成对昂贵的代码包额度的一种浪费,那么有什么办法可以快速地将不再使用的样式代码删除吗?下节课我们就一起来深入探讨一下这个问题。
2022-07-15 - WXSS 优化技巧
[视频] 你好,我是李艺。 先看一下问题,wxss样式也会影响组件或页面的运行时性能,非常值得我们注意,整体上小程序的页面设计要符合微信小程序设计规范,风格尽量以简洁实用为主。开发者在优化wxss样式代码的时候,遇到的最多的一个问题是,如何删除项目里边无用的wxss样式代码,好在这个问题现在已经有gulp插件,可以帮助我们自动化解决了。下面看代码实践。 先看实践一,给滚动组件开启惯性滚动。 在安卓下默认有惯性滚动,而在iOS下面需要额外设置,例如在主页的长列表组件上,适合添加一个名称叫做-webkit-overflow-scrolling,这样的一个样式,将它的值设置为touch,便可以开启惯性滚动了。 下面进行实践一的代码演示。 首先我们看一下在我们主页的样式,主页的标签代码里边,我们用到了一个recycle-view这样的组件,它上面是有一个样式的,我们看一下它的样式文件里面的一个代码,相关的代码,这是它的属于标签样式,就是组件样式,在这个上面我们可以添加一个-webkit-overflow-scrolling,这样的一个样式,然后这个值我们可以设置为touch,我们打完前面的这些字符,后面它自动会帮我们完成,这个属性在安卓上我们不需要设置,然后在iOS上我们需要额外设置,但即使我们代码运行在安卓设备上也没有关系,多余的代码本身也不会造成额外的负面影响,这样就可以开启iOS设备,以及安卓设备的,同时的一个惯性滚动了,代码演示我们就说到这里。 下面我们看实践二,使用hover-class实现按钮的单击态。 在小程序开发里面使用:active伪类可以实现按钮的单击态效果,但是这种实现存在一个缺点,事件太容易触发,并且滚动态滑动的时候,单击态不容易消失,在一些旧的iOS设备上又容易失效,在MDN文档里面我们可以看到这样一段说明,如屏幕上这段文字所示,默认情况下iOS设备上的Safari浏览器,它不会使用:active状态,除非关联元素上,它有一个touchstart事件监听,小程序的运行环境虽然与Safari浏览器不同,但是在iOS设备上,视图渲染是需要靠引入iOS平台的,WKWebView组件来实现的,在单击态的实现上,特别是在旧的iOS设备上可能也受相关规则的影响。因此小程序官方建议我们使用内置的hover-class属性实现单击态,这个属性是小程序它自己定义的 ,在HTML5里边是不存在的,用这个属性代替原来的:active伪类来实现组件的单击态,如我们屏幕上现在看到的截图switch_btn组件,它的一个单击态实现,就是通过组件的,hover-class属性来实现的。 下面我们看代码演示。 我们需要实现一个自定义的单击,具有单击态的这样的一个按钮,我们先看一下最终的一个代码实现的结果,找到我们的代码,这里面有一个switch_btn,这就是我们要实现的自定义按钮的代码,然后我们将这个给它拷贝到我们现有的项目里面,好已经放进来了,这是它的一个标签代码,这是它的样式代码,分为两种,这是用伪类:active 这是伪类,用伪类实现了单击态它的一个样式代码。下面这个是hover,我们需要用到的一个样式类,给 hover-class属性使用的,我们先把下面这个给它注掉,用上面这个,同时在我们的这个地方,将属性的设置先给它删掉,组件定义好以后,接下来我们开始在主页上进行引入,引用和使用它。首先我们看一下最终的使用的代码,第一步是组件的一个引入,可以把它放在最上面,接下来是标签代码里面的一个对组件的使用,在stopwatch秒表组件下面,我们添加一个新的组件,放在这里,这个地方还涉及到一个class,一个样式,相关的样式我们可能还需要把它拷贝过来,主要是这两个,这里边已经有了,那么让它覆盖一下就可以了,这是我们新添加的标签代码,它所需要的一个样式,这个代码我们主要是把我们屏幕分成 从左到右分成三格,然后我们第一格里面放这样的一个组件,后面两格目前是空的,然后看一下我们样式,简单说一下,这个里面用了flex布局,然后这有一个justify-content设置是为了将我们,容器它的,使它的子元素平分,本身父容器的一个宽度,是为了实现这样的一个平分的效果,这个代码已经改写完了。我们单击编译,测试一下,我们将鼠标放上去,按下去的时候,现在这个单击态已经出现了 ,我们看到这个文本它变成了白色,同时按钮的背景色也有变化,这是它的一个单击态,我们现在单击态我们可以再次看一下我们实现的代码,目前我们是通过伪类:active,然后伪类去实现的,接下来我们换我们前面提到的,使用hover-class,这样的一种方式去实现它 ,这个class样式,这里面的具体的样式代码和上面这个是一样的,我们重新定义了这样的一个样式,然后将那个名字给它复制一下,在我们组件的标签代码里面,在组件上面我们加一个hover-class然后等于它这样就可以了,我们再刷新测试一下,基本上运行效果和之前是一致的,但因为我们在模拟器里面进行测试,所以显示不出它的差别来但是在处理自定义按钮的一些单击态的时候,我们应该优先选择,hover-class这样的一种方式,去实现单击态的效果,代码演示我们就说到这里。 下面看实践三,使用gulp工具删除无用的wxss样式代码。 首先我们需要安装gulp工具及其gulp-cleanwxss插件,指令如屏幕上现在显示的这些,安装以后,我们还需要创建一个gulpfile.js文件,这是gulp的配置文件,gulp指令的时候由它提供配置信息,在gulpfile.js文件里面,如我们屏幕上显示的这样,每一个task是一个任务,default是默认任务,default是不可缺少的,每次执行gulp指令的时候,如果我们没有指定任务名称,default任务将会被默认执行,当项目中有了gulpfile.js这个文件以后,我们在终端里边直接执行gulp指令就可以了,处理完成以后gulp会把处理后的文件,放在父目录的dist目录下,生成的文件不会覆盖原来的文件,我们需要将生成后的文件手动,然后拷贝到原来的位置,然后进行覆盖,这个工具它是怎么工作的呢,一般情况下小程序里面页面样式文件和页面标签文件,它都是放在同一个目录下的,gulp工具它先找到wxml文件,同时打开它们的wxss文件,然后读取wxml文件里面的标签代码并生成DOM树,接着用wxss文件里面的所有样式作为选择器,如果找不到元素,就把对应的样式给它删掉统一处理以后,最后将所得的结果再写到新文件里面,使用这个工具的时候要注意,对组件内的样式,要放在组件内,而不要放在组件外面的页面样式文件里面,否则容易被工具错误的清理掉,组件在定义的时候,组件本身的样式它就是隔离状态,保持这样的状态,也有利于保持组件样式的完备性和独立性。 下面我们看代码演示。 首先第一步我们需要在我们当前的项目目录下,创建一个新的tools这样的目录,我们将在这个目录下实现相关的清除样式代码的一些脚本,我们在目录下打开集成终端,首先我们添加一个Node.js的工具,这个地方我们可以全局添加使用gulp,yarn,global,add,gulp然后添加,添加完成以后,因为我这个地方原来已经装过了,所以它很快就没有了,接下来我们在我们当前目录下,添加它的本身以及它的一个插件,叫做cleanwxss这样的一个插件,然后再执行,安装指令它可能需要持续一段时间,趁这个时间我们可以先准备一下我们所需要的三个脚本,可以找到我们最终的这个源码 源码目录下,在这个tools目录下面我们会有几个文件,一个是gulp ,这是我们的gulp文件,将这个文件给它,这个文件我们应该可以自动生成 所以不需要拷贝,我们将这个gulpfile.js文件拷贝一下,放在我们新创建的那个tools目录下面,然后上面这个文件我们可以看到,就是package.json 这个文件,然后已经包含了我们刚才在本地添加的两个模块,重点我们要看一下gulpfile.js这个文件 gulp脚本,也就是它里边的所谓的task,我们可以执行的任务都是在这个文件里面进行定义的,首先这个文件我们要定义一个名字为default,这样的一个任务名称,然后后面就是它所要执行的代码了,所要执行的代码,我们需要去处理父目录下,也就是它下面有一个index pages,然后这样的样式文件 去处理它 处理完成以后,我们把处理的结果放在当前目录下的dist,放在这个目录下。然后第二个任务是goods,我们需要处理goods分包下面的页面,处理它里面的样式 ,还有是user,一共是三个,三个,现在我们已经装完了,模块已经装完了。 装完以后,接下来我们就可以尝试执行我们这个指令了,第一个我们可以执行default,当然我们不写也是可以的,不写的话它默认就会执行default这个任务,另外两个它不会执行,从这个地方我们可以看到,然后它会删除很多的一些样式,把这些都给它删掉了,然后我们看一下生成的文件,生成这些文件,骨架屏它也处理了,但是骨架屏我们不需要它处理了,我们只需要处理主页,样式代码,就需要处理,就可以了,处理以后我们可以看到它这个page也被保留了。虽然这个page,在我们wxml这个标签代码里面不存在,但是它也被保留了 这个是应该的,然后对于一些我们找不到的一些 它就删除了,当然这个地方有一点我们需要注意,以demo开头的这几个,这几个是我们代码里面动态使用的样式,动态使用的样式,所以它这几个还不能被删除,所以我们还要做一个事情,这个工具它本身其实并不是十分完善的,它只能做静态的甄别,动态的它是无法识别的,所以说这些代码我们其实是需要保留的,我们可以将这些代码给它拷贝一下,然后在这个文件里面,放进来以后代码总行数是208,然后原来的文件我们可以看一下,是236,它这个代码行数已经变少了,说明有些样式已经被删除了,这是第一个。 接下来我们再执行第二个任务是user,这个地方我们看一下,它会有一些提示 我们看它最终生成的,是否已经成功生成了,目标文件在这里,这是它生成的,然后这个文件是210 生成之后的是210,我们原来的我们可以再看一下它是多少,也是210,所以这个文件它其实基本上没有删除,然后我们再看goods分包,这个地方注意报错了,有了报错信息,为啥会报错,跟我们源码有关系,我们看一下我们这个里边源码,它里面有两个引用,因为这两个引用它所以导致报错了,我们可以将这两个引用先去掉,然后再执行,这样就没问题了,然后在我们生成的文件里边,我们再看一下我们dist的目录下,找到detail,生成这个目录里面,然后顶部我们再加上这两行引用代码就可以了,这个最终代码是75行,然后我们看一下原来的代码是,也是75行,基本上没有变化,存在多余样式代码的时候它会删除,如果不存在它基本上就不会删除了。 下面我们为了更进一步去对比处理前后文件的差异,我们可以在VSCode里边,同时打开我们的源文件,源样式文件,还有是我们生成之后的文件,我们可以按住control ,先选择一个,同时选中,然后选择右键菜单,选择将已选项进行比较,应该选这个,选这个以后,这个属于VSCode它本身的一个比较功能,它会在我们这一个table页里面然后同时打开两个文件,这种模式我们使用Git的同学很熟悉,它相当于是比较前后两个代码,这里面我们所有的有变动的地方,然后它会给我们进行高亮显示,我们可以看到这里面的一些注释,它帮我们删掉了,然后这个样式它没找到元素,它把我们删掉了,还有这个注释也把我们删掉了,这些,这个注释也把我们删了,这是我们的样式代码注释,它认为没用,它也给我们删掉了,这是它的一个主要的一个功能,然后处理完成以后,接下来还有一步,因为我们现在处理后的结果还在我们dist的目录下,我们可以将这个文件给它拷贝一下,放在我们目录下,粘过来,这是新的,这个是旧的,将旧的给它删掉,这个给它改一下名字,这样就可以了,然后所有修改完成以后,单击编译进行测试,样式看起来和原来也是一样的,没有什么差别,但是此时我们多余的样式已经被我们删掉了,被我们清除掉了,这个代码演示就说到这里。 下面我们看小结,删除无用的样式代码是为了提高wxss样式的命中率在小程序开发里面要尽量使样式代码物尽其用,如果某些样式只在分包里边使用,就不要在主包里面引入,如果只是在某一个子页面里边使用,就不要在全局的app.wxss文件里面去引用它,如果只是在某一个组件里面使用,就只在组件内进行定义就可以了,本质上这就是为了提高wxss样式代码的有效覆盖率。我们举一个例子,例如在商品详情页引入的有关colorui的样式,如我们屏幕上现在看到的,将它们放在app.wxss主文件中引入,便很不合适,甚至放在主包里边也不合适,因为它们只是在goods分包里面使用,减少公共样式的引入,不仅可以提高wxss样式的命中率,还有助于加快小程序的启动速度,这节课我们就讲到这里。 点击查看相关文档: 性能与体验 /体验评分 /评分方法与规则 /体验微信小程序设计指南Browser compatibility上面这些网址是本课涉及的文档地址。 这节课我们主要学习了一些wxss优化技巧,即如何使用gulp工具,清理无用的wxss样式代码,下节课我们学习UI交互方面的优化技巧。 最后看一下思考题,这里有个问题请你思考一下。程序启动慢,页面操作卡顿等问题我们需要优化,按钮不容易被选中,无法顺利执行按钮绑定的事件函数,这些问题也需要优化,那么你知道这样的问题应该怎么优化吗?下节课我们就一起来探讨一下这个问题。
2022-07-15 - UI 交互优化技巧
[视频] 你好,我是李艺。 上节课我们主要学习了一些WXSS优化技巧,及如何使用gulp工具,清理无用的WXSS样式代码,这节课我们学习UI交互方面的优化技巧。 首先看一下问题,根据小程序官方的微信小程序设计规范,可单击元素的单击区域宽高最低不能小于20px,小于这个大小一般人就很难点中了,开发者在进行组件设计的时候,必须考虑组件的交互操作对于大多数人是否易于完成,下面看项目实践。 首先看实践一,使用padding改变单击区域大小。 对于按钮类的组件可以通过改变它的内边距样式,也就是改变padding样式,改变它的可单击区域大小,样式代码及标签代码,如我们屏幕上看到的所示。 下面我们进行代码演示。 我们需要创建一个新的按钮组件,首先我们在最终的源码里面看一下,最终的代码是如何实现的。这里有一个switch_buttona这样的一个新组件,我们将这个组件给它拷贝一下,放在我们目前的项目的目录下,这里面有两个样式,我们先把它注释掉,接下来我们需要引入去使用,引入和使用这一个新组件,把它放在这个地方,这个名字和下边这个名字稍微有点不太一样,最后在这个标签页面里面进行使用它了,放在第二个格子里面,这个代码已经完成了。下面我们单击编译测试一下效果,这个地方有了问题,刚才我们在添加了对新组件的使用以后,在调试区看到一个错误,这个错误在我们在工具栏当中选择全面清除以后,选择全面清除或者是清除编译缓存也可以,然后再次单击编译,那么我们刚才看到错误便不复存在了,这样的问题我们也不需要去深究,清除缓存以后,只要可以正常运行就可以了,这可能也是我们在模拟器里面测试的时候,本身清除编译缓存按钮它需要存在的一个理由之一。接下来我们看一下新组件的一个表现,这是我们一个新的组件,然后switch文本下方有一个浅色的背景色,但是周围没有,我们可以看到我们其实是很难单击,然后很难击中 很难使它变成一个单击态,如果它变成一个单击态它这个背景我们可以看到,它会明显的会变成一个稍微深一点的颜色 ,但是我们当我们单击到旁边的时候,它其实不容易被选中了,这种情况我们应该怎么优化。我们可以使用样式,将我们刚才特意注释掉的这两个样式,一个是padding,这是设置它的内边距,另外一个是min-width,这是设置它的一个最小宽度,将这两个样式给它反注释,完成以后我们再单击编译按钮进行测试,这个时候我们看到这个按钮它现在更像一个按钮了,文本旁边它会有一个类似于方形的这样的浅色背景区域,我们单击,即使单击到按钮的边角的位置,它也会呈现一个选中的状态,代码演示就到这里。 下面我们看实践二,使用伪元素改变单击区域大小。 对于不方便使用padding改变单击区域大小的组件,我们可以通过伪元素扩大可单击区域样式代码,如我们现在在屏幕上看到的截图,在使用伪元素扩展按钮的单击区域以后,即使在按钮旁边的空白处单击 该按钮也能响应。 下面我们看代码演示。 首先我们在项目里面我们需要添加一个新的按钮组件,我们看一下最终的一个实现的代码,这是我们准备使用伪元素实现的新的按钮组件,这个里面我们用了after这样的一个伪元素,我们将组件目录直接复制一下,到我们的目前的项目的目录下,我们原来的组件名叫做switch_buttona,现在叫做switch_buttonb,它俩的主要的一个区别,在这个里面,其实没有什么区别,主要是样式上的一个区别,在样式里面主要是这个地方它用了一个伪元素,after这样的伪元素,在这个里边有两个信息,我们需要先给它处理一下,将这行代码给它注掉,注掉以后,然后我们需要在我们的主页面里边引入新的按钮组件,这是原来的组件,我们将这个给它复制一份,把后面这个地址改一下,前面这个名字改一下,然后在页面标签代码里面,将这个地方消费代码拷贝到下面,然后将这个标签的名称然后给它改掉,这样就可以了。完成以后单击编译按钮来进行测试。 我们在模拟器上看它的一个表现,最右边底色为黄色的,这个是新添加的按钮组件,然后当我们在按钮以外的空白处进行单击的时候,发现它可以轻松进入一个单击态,被单击的一个状态,即使离它很远,也可以触发这样的一个状态,这个就有问题,也就是说我们单击到任何一个地方都会触发它,而我们原来的这个地方当然也是可以触发,但其实这样是不对的 ,下面这个不可以 但上面这个我们发现单击以后,然后都会触发对它的单击,这是不对的,这个问题是怎么产生的,为什么会出现这个问题。样式,肯定是样式搞的鬼,我们可以使用调试区的选择工具,选择它,选它以后我们可以看到这个地方就是switch_buttonb了对不对,这就是我们自定义的组件,然后当我们选择的时候,我们往下单击的时候,其实它多了一个button_over,然后同时这个地方也有变化,但是这种属于待交互的这种样式,其实是很不方便在调试区直接进行查看的。我们目前的问题它其实很简单,关键就在于我们这个地方刚才已经注释掉的这样的一个样式,position等于relative,位置样式等于相对,而里边它里面after,也就是它挂载的伪元素,在它后面挂载的伪元素,它的位置样式position等于absolute是绝对的,当我们如果想使用伪元素,想给按钮添加一个附加的这样的一个可单击区域的时候,我们需要将样式设置为absolute,同时将它按钮本身也就是它的父元素,position设置为relative,这样的话才可以保证它样式的一个工作正常,我们修改完以后然后重新编译,再看一下它的一个运行效果。 单击,在其他地方单击没有效果,在这里单击有效果,当然了我们现在这个地方它其实还不太好,那是因为它比较小,如果我们愿意的话,其实还可以把它再改的更大一点 也是可以的,我们可以在这个地方进行调试 看一看,当我们选中after伪元素的时候,我们可以看到它所覆盖的区域,其实在这个背景色的外面对不对,所以我们在背景色外面单击的时候,这个按钮也会被触发单击,我们看到的背景,黄色背景它是怎么搞的,它其实是它决定的,如果我们想要背景色扩大一点,我们可以加在after上面给它加一个背景色,加完以后我们再单击测试一下 看它的表现,现在整体上都变成黄色了,对不对,但是这个效果也不是我们需要的效果,因为它这样的话,这样一来的话,它本身按钮的文本又不能显示了,所以这种效果它其实适合于什么,适合于我们不需要改变它本身的一个样式,我们前面这个,我们不需要给它加一个很大的背景,一个彩色背景,我们只需要扩大它的一个可单击区域,所以可以用这样的一种方式不影响它的UI,但是又能扩大可单击区域,使用这种方式是最恰当的了,这个代码演示就到这里。 点击查看开放文档: 性能与体验 /体验评分 /评分方法与规则 /体验这节课我们就讲到这里,上面的网址是本课涉及的文档地址。 这节课我们主要学习了如何通过padding样式或伪元素样式,扩大元素的可单击区域,下节课我们学习脚本优化技巧。 最后我们看一下思考题,这里有一个问题请你思考一下。我们知道使用JS开发项目不需要操心内存的管理,因为JS的宿主环境一般都带有一个垃圾内存回收器,俗称GC,GC的工作原理可以简单认为是引用计数,当一个对象不再被其他任何对象所引用的时候,那么这个对象便可以成为GC的回收对象,考虑到GC的这种工作机制,在项目里面我们应该如何避免不再使用的对象,因使用不当而被GC回收?如果有大内存对象不能及时被垃圾回收,同时还随着程序运行又被疯狂的创建,那么这种情况下是非常容易出现内存泄漏问题的,这种情况我们应该如何避免和优化?下节课我们就一起来深入探讨一下这个问题。
2022-07-15 - 脚本优化技巧
[视频] 你好,我是李艺。 上节课我们主要学习了如何通过padding样式,或伪元素样式扩大元素的可单击区域。这节课我们学习脚本优化技巧。 首先看一下问题,JS是一门支持异步编程的单线程语言,拥有垃圾内存回收机制,但如果对象被异步线程引用,或者是被全局对象引用,便可能造成对象一直无法被正常释放的局面。这种情况极有可能造成内存泄漏,在编程时候我们要特别注意。下面我们看项目实践。 首先看实践一。 定时器是异步线程里的东西,在离开页面的时候一定要记得销毁。传递给定时器的回调函数是主线程传递给异步线程的,如果定时器使用结束以后主线程不能将定时器及时停止和销毁,那么异步线程会一直持有对回调函数的一个引用。如果使用定时器的页面或者是组件是重复产生的,在运行时又会创建很多定时器,这种情况便极容易产生内存泄露现。 下面我们看一个实例,如我们屏幕上展示的,在stopwatch_nw组件里面,有一个stop方法,这个方法它负责停止和清扫定时器,当组件从页面节点树中移除的时候,或者是页面不显示的时候。例如在hide方法里边,这时候定时器已经没有执行的必要了,此时也需要调用stop方法将其停止和销毁。 下面进行实践一的代码演示。 首先我们看一下目前的一个组件的代码,stopwatch_nw这个组件,我们看它的js代码,在js代码里面,有一个stop方法,这个方法里面调用了clearInterval,进行了定时器的停止和清理,这是正确的一种做法。但是在这个地方我们还少了另外的一个操作,我们组件本身它是有生命周期的,生命周期一共是有两个,一个是pageLifetimes,在我们VSCode里面,它的提示性不是很好,可能不是很好,我们可以转到我们微信开发者工具里面,在这个里面一般情况下它会给我们正常的一个提示,我们写lift,当我们打lift以后会出现两个,一个是lifetimes,一个是pageLifetimes。其中这个地方,这个对象它是指我们组件的生命周期,另外一个是pageLifetimes,这个是我们组件它所在页面的生命周期,我们需要实现的效果是这样的,当我们组件在我们这个页面里边不再显示的时候,然后停止定时器,然后我们需要写在pageLifetimes,在这个里面,上面这个是组件本身的,所以我们把这个给它去掉,写在这个地方,这是我们要实现的一个代码,现在我们看一下最终代码的一个实现。找到我们的组件,然后调用,没有问题,这是组件所在页面的生命周期对象,跟我们之前这个代码是一样的,只是多了两行注释,这是stopwatch_nw组件。 另外我们还有两个类似的组件也使用了定时器,第一个是这个,就是我们后缀有wk的这样的一个组件,我们看一下它这个js代码,这个里面也有一个stop,在stop方法也有定时器清理的代码,但是在这个组件里面我们缺少相关的周期函数,所以我们可以将这一段代码可以拷贝过来,直接放在这个地方。另外还有stopwatch_go组件也有stop,但是它没有周期函数,所以我们把关于周期函数的一些代码放在这个地方,完成以后我们单击编译按钮进行测试。我们先看一下我们当前主页里边所用的秒表组件是哪一个,我们现在所用的是nw组件 ,我们现在单击让它开始计时,当我们尝试单击的时候,我们这个地方报了一个错误,报了一个接口错误,它说我们后端接口不存在,我们可以到VSCode里面看一下我们后端的代码,这个地方可能存在额外的一个问题需要我们处理。找到server controllers,然后是api home,这个是存在的,它本身是存在的,这个接口是存在的,我们可以在浏览器里面查看一下它的执行效果,不存在。 在刚才我们发现一个问题,我们的一个接口,我们发现无法去访问它,我们可以在浏览器里面再尝试看一下它的一个运行结果,运行结果,这个地方可能还没有启动,我们把它启动一下,然后在浏览器里边访问一下这个地址,因为这是一个GET接口,我们是可以直接在浏览器标签里面进行访问的,我们发现这个接口它是Not Found,然后不存在。但其实在我们的代码里边这个地方是有的,这种情况貌似之前也出现过一次,什么原因。而且我们启动脚本也在启动,原因在于我们目前启动的目录,跟我们的所用的项目目录不是一样的,因为我们每一讲都有一个示例源码,每一个示例源码里面都有前端后端的代码,所以这个server目录一定要搞对,搞不对的话,可能就会出现刚才那样的问题,我们现在启动目录是在这个目录下,而我们实际上在用的其实是这个,我们只需要在这个地方,打开集成终端窗口,然后再看一下目前,这是我们的实际在用的目录,然后再使用yarn dev启动我们的后端程序,启动以后再刷新一下,第一次它会编译,它会有问题,然后再刷新一下,这个地方接口少了一个api,现在可以了。接口正常以后,接下来我们回到小程序,微信开发者工具里面,然后再单击编译,重新测试,继续刚才我们的测试,启动以后,我们先单击组件让它运行,然后再单击用户主页,单击用户主页,然后再选择主页,这个时候我们会发现计时器它已经自动停止,为啥会自动停止。因为我们组件里面加了关于页面生命周期的代码 ,当这个页面隐藏以后,我们就调用定时器的清除代码,调用stop,然后停止定时器的一个执行,这是我们实现的效果,我们正要实现的一个效果,这个代码演示我们就说到这里。 下面我们看实践二。 使用wx.onXxx全局绑定一定要小心,有一个监听必然要有一个反监听,在主页的JS文件里面,如我们屏幕上显示的,有对系统主题改变事件的监听代码,所有像wx.onXxx这样形式的监听都属于全局监听,一般我们在页面的onLoad周期函数里边添加这样的监听,同时在onUnload周期函数里面移除监听,以免造成无效页面被异步线程引用,而无法被GC及时回收的情况,主题切换事件既可以在手机上触发,也可以在模拟器中模拟。但是在模拟器中可以模拟的前提是app.json文件中必须有一个名称为darkmode的配置节点,如我们屏幕上看到的这样。 下面我们看代码演示。 首先我们看一下最终源码的实现,找到主页,主页的js文件,在这个文件里面我们有一个这个,themeChangeHandler这样的一个监听,这个代码它可能经过格式化,它其实是这样的,它应该在这里,这样这是它正常的一个表示,然后与它相关的是一个onLoad onUnload,在onLoad里边我们添加了一个wx.onThemeChange这样的一个监听,这是一个全局监听。同时我们在onUnload的周期函数里边我们加了一个wx.offThemeChange,这样的一个反监听,微信小程序的全局接口一般有这样的一种规则,就是有一个on接口,它必然会有一个off接口与之对应,一个是监听,一个是反监听。接下来我们首先将这个代码给它拷贝一下,到我们的目前的小程序项目里面来,这是我们代码已经有了,但是我们是不是有一个onUnload这个方法。看一下,这个地方也有了,但是onUnload没有,我们可以将onUnload加到这个地方来,这是有一个反监听,然后在onUnload里边我们添加了这个,添加了它,然后同时我们回调也有了,这是我们回调,当我们主题发生改变的时候,然后这个代码会执行,代码添加完了,然后现在我们单击编译进行测试一下,启动以后我们选择模拟器上方这有一个模拟操作,然后我们切换一下主题 单击深色,但是没有效果,再单击浅色,也没有效果,为啥没有效果。因为刚才我们提到了,我们如果想在模拟器上进行测试的话,我们需要在app.json配置文件里面,添加一个darkmode这样的一个配置节点,把它设置为true,然后再选择模拟操作。现在这个地方有一个打印对不对,当前主题是dark,然后我们再切换过来,选择浅色,当前主题是light 现在已经可以模拟这个测试了,代码演示就到这里。 下面我们看实践三。 使用全局对象要小心,所有在global上或者在app上定义的全局数据,或者在上面添加的事件监听,在使用的时候我们都要小心,在相关代码的使命完成以后,要记得及时做清理工作,自定义的类对象如果可以的话,我们也要实现一个dispose方法,在释放对象的时候先调用dispose方法,再将其从全局对象上删除,在我们的stopwatch_go组件里面加载与初始化,WebAssembly文件的时候也使用了全局对象,如我们屏幕上展示的。在组件的生命周期函数,detached内部也应该做相应的清理工作。 下面我们看代码演示。 首先我们在VSCode里面,找到我们当前的项目,程序项目,然后看一下在library optimus里面有一个command这样的目录,在这个文件里面主要是关于命令类对象,Command是它的一个基类,在Command里面,其实我们实现了一个dispose,这个方法本身它的使命就是什么,释放,移除事件监听,做清理工作的,就是所有的清理工作我们都要在这里面来做,在这个地方。因为我们当前这个类,它继承于EventDispatcher,所以第一步我们要调用,父类的一个off方法,将所有的事件然后移除,同时将data,我们这里面可能会有一个临时数据仓对象data将它置为null,这是一个。另外重写类,重写对象它的一个子类,我们可以看一个 比如parallel_command,它里面也有一个dispose,本身它要先调用父类,然后再进行它自身的一个清扫工作,还有另外的一个是serial_command,这个里面同样也是,但是它不需要额外的清扫,所以这个地方它没有,这是我们关于dispose的方法。接下来我们要做一个事情,我们要检查一下,在当前我们这个项目里边,当前我们小程序项目里边,我们全局查找一下global全局对象,global是小程序的全局对象。在这个里面首先在这个app.json里边,我们知道这个地方有一个什么,它有一个asyncRetrieveSystemInfo,这样的一个 在全局对象上,然后注册的这样的对象,然后去执行,当它执行完以后,我们需要做一个什么操作,需要做及时的清理,对不对,它在什么时候完成的。我们看一下它的监听是在这个地方,这是它的一个监听onComplete对不对,然后在它完成以后,我们要注意一下,这个地方有一个global.retrieveHomeData.dispose,这有一个释放,然后同时还有一个删除,就是将用delete关键字,将这个对象从我们全局对象然后进行删除,这是一个清理工作,除了这样一个全局对象,另外我们这个里边,还有另外一个。来看一看有一个,再看我们另外的一个地方,retrieveHomeData这个地方它也是一个全局对象,在它的onComplete这个监听,这个回调里边,我们可以看到它也有对于dispose的一个调用,后面也是一个使用delete关键字,进行了一个全局对象引用的删除,这个地方也是有的,这是两个全局对象的一个清理。 另外我们再看我们的组件,我们有一个组件,有一个使用了WebAssembly的一个组件,这个组件位于我们的这个目录下,然后我们看一下它的JS代码,在这个里边我们也使用了global全局对象,然后在上面我们访问了它 对不对,在组件上负责清理的是谁负责清理的是我们stop,就停止的时候我们要对它进行一个清扫,对不对。首先我们看一下我们最终源码里面都有哪些清扫的动作,找一下我们最终的源码,这个地方还没有,我们可以往后找一找,奇怪,这个地方也没有,不管它了 我们自己写一下,找到我们组件,定位到它的JS源码,在这个地方我们可以看一下,这有一个detached,detached它在我们生命周期里边,它是一个反挂载,就是它从页面里面移除的时候,它所需要进行的一个动作,然后它与我们前面的ready它是相反的,我们在挂载的时候,组件准备好的时候,我们要进行初始化,然后组件从页面里边从视图,从DOM树里面进行移除的时候,我们要进行另外一个操作就是把console文档,然后删除,再删除这个全局对象,然后再删除这样的一个全局对象,这些全局对象其实是我们的,在我们源码里面有用到的,并且是由WXWebAssembly,由它帮助我们在全局对象上注册的一些子对象,所以我们在这个地方进行清除,这是关于它的一个清除方式。这个代码其实不太适合放在我们stop里边,stop是在我们 这个页面它隐藏的时候,我们需要停止计时器,停止它执行的时候,我们调stop,但是当这个页面它在显示的时候,我们还需要继续,然后有可能会继续执行这个定时器,所以这个时候我们去清扫不太合适,而只有当我们组件从DOM树里面移除,我们不再需要的时候,然后这个时候我们可以进行清扫了,这个时候是没有问题的,所以我们把清扫代码放在detached放在这样的一个生命周期函数里面去使用,代码演示我们就说到这里。 下面看实践四。 使用this对象要谨慎,this对象在JS中它是一个动态对象,对它的飘忽不定,很多程序员都深恶痛绝,在周期性发生的异步回调函数里面,使用this对象需要特别谨慎,在商品详情页面中对于animation2方法中的动态属性清理工作,如我们屏幕上展示的便使用了this关键字,这个地方的代码是可以优化的,因为绑定的this对象是当前页面,并不是一个简单的对象,优化以后会比优化前在执行时间上大概会少3ms的一个时间。 下面进行实践四的代码演示。 首先我们看一下目前的商品详情页的代码,在里面我们需要找到一个叫做animation2这样的一个方法,这是我们原来设置动画的代码,但是目前这个里面没有,为啥没有。因为可能是我们前面在这个示例演化的过程中给它去掉了,但是没关系,我们可以回到我们的,打开我们的2.4,我们动画是在2.4里面添加的,打开这个目录,这个目录下面我们可以看到这商品详情页,这个里边,它是有animation1,animation2这个代码都是存在的,我们可以将我们的目前的detail,把这个目录给它拷贝一下,然后到我们的目前目录下 到这个下面,然后粘过来,把这个给它删掉,将我们后拷贝的目录的名字给它改一改,改成detail,这就可以了。首先我们要找到animation,animation2这样的一个方法,在这个里边我们往下看,这个地方这是我们传给我们的目前的animate,它的一个参数,最后的一个参数是一个回调函数,然后在这个里面我们是用了一个什么,用了一个this对不对,this因为箭头函数它没有this,所以this它会向上找,找我们当前的页面对象,所以本质上我们就传递过去,传给异步线程的,然后异步线程又执行的这个函数,它其实是一个闭包,它里边裹挟了我们当前这个页面里面的this对象,接下来我们要做一个改进,什么改进,我们传递给我们这个方法,它的最后的参数,这个的参数,这都是它的参数,然后这个参数我们做一个改造,我们在这个上面加一个小括号,然后同时在这个地方,这个关系可能不太对,需要再重新理一下,在这个地方加一个,然后是这个地方加一个,这样就对了。同时在这个对象上面我们加一个bind调用,将this给它传进去,传完以后其实是在我们最后的一个参数上面,我们绑定了this对象给它,绑定this对象给它以后,然后它里面的这个函数它在执行的时候,它就会使用我们绑定的this对象,当然这个地方我们还需要将它改一下,不使用箭头函数,而改用这种普通的function的匿名函数的一个写法,然后传进来,这样的话this它其实取的是我们绑进去的对象了,不是原来我们刚才说的那种闭包的裹挟的那种方式,代码已经改完了,下面我们进行一个测试。 首先我们单击编译按钮,然后我们打开调试区的Performance面板,单击商品详情页,当然我们在这个地方也可以直接选择商品详情页作为启动页,这个地方它目前可能没有办法直接启动,我们可以换成普通编译,现在到了这样的一个页面,然后动画它现在已经执行了,我们现在在Performance面板里面打开录制,我们要看一下它的动画的一个执行表现,大概有一次执行就可以了,然后我们让它停止,我们目前是使用了bind方法对不对,使用了bind方法 所以我们看一下,它里面目前的,它的这个执行 动画的一个执行,大概是用了多长时间,这个地方会有一个Timer Fired,会有一个Timer Fired,然后一次触发,这个时间我们可以大致的看一下,它这个地方也有,这两个是类似的,然后这个时间大概是1.48ms,1.48的ms,现在我们再将我们这个代码给它做一下恢复,找到我们的商品详情页,找到这个位置,最后这一个,我们可以将,这是有两个,将这个往下放一点,然后这个地方我们可以把它拷贝一下,把这个代码给它注掉,然后下面这个代码,把我们刚才添加的再给它注掉,这个地方也拿掉,function也去掉,也变成原来的箭头函数这样的一种写法 ,代码已经恢复了,然后我们再单击编译再测试一下,把原来的结果给它清除一下,然后单击商品页面,动画开始执行了,我们现在单击录制,然后停止,最后我们这有一个触发,我们看一下最后的一次的Timer的一个触发,这个时间也非常短,前面这个时间稍微有点长,大概是2.04,再往前是2.88,整体上比我们平均来讲比我们刚才的时间,稍微有1到1.5的ms这样的一个差别,所以我们这个地方其实未来优化的话这个代码我们是可以将它删掉,还是使用我们这种绑定的方式改回来,然后使用这样的方式,然后再测试一下。 下面看一下小结,在JS编程里面一般很难产生内存泄漏问题,如果产生了多半是因为对引用类型的对象使用不当造成 的,JS中有引用类型也有值类型,对于值类型基本不用担心,我们需要担心的是引用类型,Object是最基本的引用类型,所有的组件,页面对象的实例,原生组件的引用都是引用类型,包括数组也是引用类型,这些类型在使用的时候我们都需要小心,在Memory面板里边的对比视图中,我们查看delta数据的时候也应该多关注引用类型的一个数据增长情况,要确保引用类型的变量,尤其是不断增长的临时变量,不被全局对象,异步线程对象,也就是定时器对象引用或间接引用,在使用完成以后要记得及时进行清理,这节课我们就讲到这里。 点击查看开放文档: 自定义组件 /组件生命周期基础 /小程序 /应用级事件 /wx.onThemeChange框架接口 /自定义组件 /Component 上面的网址是本课涉及的文档地址,这节课我们主要学习了脚本优化技巧,下节课学习setData方法在使用时注意的技巧。 最后看一下思考题,这里有个问题请你思考一下,对于列表数据的更新,setData方法的调用也有技巧,如果我们想更新列表中的某一条数据,其实不需要更新整个列表的数据,只需要结合JS对象的计算属性,按照索引更新方法,指定更新列表里边某一条数据就可以了,这种更新传递的数据大小会小很多,你知道这种索引更新的方式如何使用吗?下节课我们就一起来探讨一下这个问题。
2022-07-15 - setData 调用优化
[视频] 你好,我是李艺。 上节课我们主要学习了脚本优化技巧,这节课学习set Data方法在调用时的优化技巧。 首先看一下问题,在第6.6讲我们介绍过,当小程序切换至后台以后,setData便不需要调用了,这样可以节省逻辑层与视图层之间的调用消耗,还可以节省页面的渲染消耗。除了这个优化点以外,结合小程序的双线程运行机制和重渲染机制,还有其他的一些优化技巧,这节课我们就一起来看一下。 首先看实践一。 不要多次分开调用setData,尽量要合并调用,在主页的JS文件里,在dealWithListData这个方法里有三处调用setData的代码,这个代码是可以优化的,三处调用显然可以合并为一处,在一个函数内,如果调用一次setData可以达到目的就不需要分开多次调用,甚至在不同的if else分支语句里边调用了setData,适当用一些编程技巧也可以在if语句外面合并成一次调用,如我们屏幕上展示的是优化后的JS的调用代码,小程序页面没有一个beforeRender周期函数,如果有的话,所有需要setData的数据在不同地方的一个数据更新倒可以积攒起来,在这个周期函数里面集中进行更新。 下面我们看代码演示。 首先我们在主页的JS文件里面找到dealWithListData这个方法,这个方法里面我们看一下,目前我们有:这是第一个setData的调用,然后下面还有一个setData,在后面这个地方也有一个setData,一共有三处setData的调用,这三处调用它都处于同样的一个函数体代码里面是可以合并的。首先我们将这个给它移下来,移到这个位置,然后后面这个调用也可以移上来,把它放在一起,这个就删掉,同样我们后面还有一个newList,newList它其实不涉及到数据更新,所以我们是可以直接进行一个赋值的,这个地方它不需要调用setData,newList目前在我们这个里面,它其实目前是在data数据对象里面,它也不需要在data数据对象里面,它可以直接放在我们的Page对象下面,这样的话可以减少对这个视图更新的一个触发,位置改完以后我们还需要将所有的对它的调用代码,进行统一的一个查找,我们可以查this.data.newList,这个地方this.data.newList改成this.newList,再看一下其他地方,已经没有了 这个地方已经可以了,改完以后我们重新单击一下编译按钮,这个page我们再看一下,目前我们page是已经在我们Page对象下面,是没有问题的。page目前直接这样赋值也是可以的,它也不涉及到数据的视图的一个更新,所以直接这样赋值也是没有问题的,它和下面的newList一样的,也是可以通过这样的方式更新,项目已经刷新了,我们现在看一下表现,看一下我们的调试区没有错误,说明我们变动是可以的,没有问题,这个代码演示我们就说到这里。 下面看实践二。 不准备渲染的数据不要放在data数据对象里边,在主页的JS文件里边数据allList表示所有列表数据,目前它位于data数据对象里面,如我们屏幕上所示,我们可以将它直接放在当前的页面对象下,直接放在当前的页面对象上,也可以通过this关键字直接进行取用,一般方法里面this它就是指代我们当前的页面对象。 下面我们进行代码演示。 首先我们要查一下当前的allList目前位于我们data数据对象里面,前面说过所有的只有需要触发视图更新的数据才需要放在data对象里面,不需要触发的,完全可以放在外面,这是不影响的,所以我们将这个给它拷贝出来,另外我们在这个页面里面查一下所有的this.data.allList查一下这个引用,将相关的代码给它修改一下,把中间的data给它去掉,这个地方也是给它去掉,已经没有了 所有的都修改过来了,然后我们再单击编译按钮进行测试,调试区没有问题,然后看页面的表现也没有问题,这个代码演示就到这里。 下面我们看实践三,通过index局部更新长列表数据。 下面我们尝试给项目添加一个新的功能,在单击列表中的列表项标题的时候,在标题文本的末尾就压一个字符,每单击一次就追加一次,这个需求要求在主页JS文件里面的onTapRecycleItem这个方法里面进行实现,在列表数据中的某一项数据发生变化的时候,没有必要更新整个数据列表,只需要使用计算属性、使用索引局部更新的方法,更新列表中对应的某一条数据就可以了,要实现局部更新,对recycle-view组件有两种方法:第一种方法是如我们屏幕上所展示的,先改变data数据,再使用forceUpdate方法进行更新,在示例源码里面,我们在调用setData方法改变标题文本的时候,展示的便是更新某一条数据的方法,对于recycle-view recycleList是它当前真正渲染的数据,但是仅仅更新这个数据还不能完成页面的更新,还需要额外调用渲染上下文对象的forceUpdate方法,才能强制长列表进行重新渲染,对于recycle-view组件还有另外一种更新某一个列表项标题的方法,直接调用长列表组件上下文渲染对象的update方法,这种方法更简单,如我们屏幕上现在展示的,在示例源码中,item是引用对象,修改它的字段以后不需要再做其他的任何更新了,只需要再调用一下长列表组件渲染上下文对象的update方法就可以了。 下面我们看代码演示。 首先打开我们的最终的源码,在主页的JS文件里面我们找到onTapRecycleItem这个方法,在这个方法里面注意前面已经拿到了相关的一些数据,首先第一步,我们要用forceUpdate这种方法进行更新,将这个代码给它拷贝一下,然后在我们目前的项目里面找到onTapRecycleItem这个方法,前面的数据已经拿到了。我们将新代码给它粘进来,把这些代码给它反注释,我们看一下这个地方是直接将item它title属性直接在尾部加了一个加号,当然其他字符也可以,然后我们再拿到这个id,这个id其实前面已经有了,所以这个地方不再需要了,把这个给它去掉。然后我们再拿到recycleList 这是一个数据,在这个数据里面我们要找到id它所对应的需要更新的列表项,找到它以后,然后在这个地方我们用了计算属性,然后去更改它的title数据,这个数据就是item.title,就是我们前面已经更改的数据设置为这样的一种数据结果,最后再调用这个forceUpdate它其实是一个ctx,ctx是我们当前的recycle-view组件的一个上下文渲染对象,其实是它,然后在它上面调用forceUpdate方法,这个代码已经写完了,我们单击编译测试一下 看它的一个表现,单击可以看到标题后面它每次单击都会添加一个加号,这是第一种更新的方式,下面我们看第二种方式,第二种方式是更简单,直接用update这种方法将前面给它先注掉,然后使用后面这种方法,首先是改变数据属性,然后加一个加号,后面我们就直接调用渲染上下文对象的update方法,同时将index也就是我们当前的列表项里面索引,然后传给它,第二个参数是我们要更新的数据,我们需要更新哪一项数据就把这个数据传给它就可以了,注意这个地方其实它的参数类型它是一个数组,我们再单击测试看一下表现,我们看到标题后面加号仍然可以正常的追加,也没有问题,这个代码演示我们就说到这里。 下面看实践四,创建examples/pages/index页面。 按索引更新data数据,在扩展示例页面里边如我们屏幕上展示的,我们需要实现单击某一个组,将其他的组自动折叠起来这样的一个效果,折叠与否是通过一个名称为open的子属性进行控制的,在对这个属性的改变过程当中,我们就可以使用索引法进行局部更新,主要的代码如屏幕上所展示的这样,其中这个open是准备更新的数据属性,然后需要更新的数据准备好以后,就可以使用索引法调用setData方法统一进行更新了,这种更新方式具有统一性,在任何项目里面都可以使用,因为这个页面里边包含的数据量不大,修改带来的正面效果可能很小,但它也展示了如何使用索引进行局部更新的方法,当数据量很大的时候,这种更新方式的优势会更加明显 下面我们看代码演示。 首先我们看一下当前的源码在examples/pages目录下面,找到index.js文件,在这里面有一个kindToggle,这是这个方法它在我们单击每一组的时候会触发的,我们可以在wxml这个标签文件里面看到,它是有一个type属性type事件进行触发的,触发以后在js里面我们先取到id,然后这地方有一个循环,它会循环列表的每一项,同时进行检查,如果是这一项的id与我们当前传进来id相同,那就把它的open属性进行一个切换,同时其他的我们给它置为false,最后我们再通过setData重新设置一下list数据,这种方式在我们list数据很小的情况下其实没有什么问题,也看不出什么差别来,但是如果我们list数据量很大,这种方式其实它是非常耗费资源的,下面我们看我们要优化的代码是怎么写的。 首先找到我们最终的源码,找到同样的文件,也是kindToggle这个方法。第一步我们要创建一个tempData这样的一个临时对象,接下来是我们对for循环的一个改造,在这个里面我们要在属性的设置这个地方,我们要加这样一个代码,把它放在它的前面,另外还有一个这个地方也有一个,把这个代码也给它放在这个地方前面。注意这个地方,我们在往tempData数据对象里面添加这个计算属性的时候为什么要加if判断,这两个判断它本质上是为了让我们少一点往tempData对象里面,加一些不需要更新的数据。如果原来本质上它open属性没有变化的话,我们就没有必要更新,这种情况下我们就不需要去设置了。只有当它属性有变化的时候我们才需要去往里写,tempData准备好了以后,接下来我们就调set。这是set方法,通过set方法去设置,我们设置数据,这个就不要了,这个地方我们可以看一眼,打印我们可以把它放开,稍后我们会看到它的内部的数据表现。 这是原来的代码,把它注掉,然后现在是改用这样的一种方式,这个文件测试的主页目前我们还没有一个入口,下面我们给它加一个小入口,找到examples,这是一个独立的分包,然后分包里面这是它的一个主页,找到我们的app.json,找到关于分包的设置,这个地方已经存在了,所以我们也不需要添加了,接下来我们只需要改编译模式,将我们这个测试主页作为启动页面进行测试,已经启动了,现在我们单击任意组,单击以后我们可以看到这个地方,首先这个界面上它已经发生了切换,然后我们可以看一下tempData的打印情况,在这个里面我们看一下它里面它写法,是list[1].open,这是它的一个属性名称,后面这个true是它的一个值,然后上面这个也是list[0].open等于false,这是它的,而且数据更新量非常小,只有这一条数据 它这个数据量是非常小的,这是关于按索引法局部更新数据对象的一种写法,我们直接这样写也可以,但是我们如果是照着我们这个代码里面刚才看到的这种方式,就这样一种方式这样去写可能会更加清晰一点,先创建一个临时对象,然后往临时对象里边去准备我们要更新的数据属性、准备要更新的数据,所有的都准备好以后再统一调用setData,然后进行一个设置,这就是局部更新的方法,这个代码演示我们就说到这里。 在使用setData更新数据列表的时候,优先选择计算属性进行局部更新,如果有两条以上的数据需要更新,可以并排写多条计算属性,这节课我们就讲到这里。 点击查看:recycle-view 上面的网址是本课涉及的文档地址。 这节课我们主要学习了有关setData调用相关的优化技巧,下节课我们学习网络请求相关的优化技巧。 最后看一下思考题,这里有个问题请你思考一下,我们知道小程序为了保证整体上所有程序的流畅运行限制了网络请求的并发数最大为10,如果同时请求数达到了这个数字wx.request的请求将无法继续发出,后来在小程序基础库版本1.40更新以后,将这个地方进行优化了,超出的请求它不再被直接拒绝,而是放入了一个队列中排队,稍后等设备资源允许了以后,它会重新发起,只要我们使用的小程序基础库版本大于1.40便不会有10个最大限制这个问题,这样一来貌似关于并发限制这个问题便不是问题了。但是这里仍然有另外的一个问题,程序里面的并发请求它们的优先级往往并不是一样的,例如对后端数据接口的调用这类请求的优先级往往都比较高,而对于打点 日志的接口调用这类请求的优先级就比较低,那么有没有办法在网络请求发出的时候就给请求操作安排一个优先级,让高优先级的网络请求先执行呢?下节课我们就一起来深入探讨一下这个问题。
2022-07-15 - 网络请求优化之使用本地缓存
[视频] 你好,我是李艺。 上节课我们主要学习了有关setData调用相关的优化技巧,这节课我们学习网络请求相关的优化技巧。 首先我们看一下问题,针对网络请求的优化主要有以下三个方面: 一、减少不必要的网络请求,使用本地缓存的数据代替从后端接口拉取的数据 二、优化网络请求参数,提高网络请求的通讯效率 三、优化网络请求的并发数,让优先级高的请求先执行 其中第二项在第6.8讲我们已经讲过了,这节课我们重点看一三两项的优化,下面看项目实践 。 首先看实践一:在本地缓存数据。 在首页的JS文件里边有加载小程序导航数据的代码,我们可以在这里尝试使用本地缓存技术,如我们屏幕上看到的截图,首先从本地缓存中尝试取出缓存数据,如果取到了就先用上,然后向后端发起网络请求,拿到最新的导航数据以后再调用setData重新设置一下数据,并把本地数据也刷新一遍,避免本地缓存过时,运行以后如我们屏幕上看到的,在调试区的Storage面板里边可以看到本地缓存的导航数据,但是这个实现方案是有瑕疵的,有什么瑕疵,先从本地缓存,再从后端接口请求。这是一个顺序的并发过程,实际上这个过程还可以再优化一下,改用并发复合命令,让两个异步操作同时并行。或者我们更简单一些,改成两个异步函数,同时开始执行也可以,优化就不做演示了。留给你自己实践一下。 下面我们进行实践一的代码演示。 首先我们看一下我们最终实现的一个源码,找到主页的JS文件,在这个地方,我们有一个从主页中,从后端加载导航列表这样的一段代码。这段代码它的主要的一个作用,在这个地方。从这个地方开始,它的主要的一个作用就是从接口去拉取导航数据。拉取完成以后,然后我们再去设置我们的data数据对象里面的navs,这样的一个列表,同时设置完以后,我们还需要将我们本地缓存里边的这个navs这个列表然后进行一个更新。由于缓存里面它存储的是字符串,所以这个地方我们要拿json方法,进行序列化,更新一下。 在前面我们是先向本地,通过getStorage这个方法,然后去取了一下本地缓存的数据。当然这个地方有可能会取不到,所以我们会首先做一个判断。如果能取到的话,我们就将它进行设置,同时在设置之前我们还需要拿JSON.parse,然后进行一个解析。因为我们取到的数据它是一个字符串的数据,这就是我们主要的代码。首先我们将这部分代码给它拷贝一下,来到我们的小程序项目里面,找到我们首页的js文件,然后在这个里面我们先搜索一下。搜索navs 在这个位置,这是我们现有的代码,它一上就是从后端进行下载,我们添加一个从本地获取数据这样的一个代码,同时在这个地方这个代码已经有了,所以我们不需要再添加了。本质上在之前这个代码其实它是不需要的,因为我们如果前边没有消费代码的话,我们在这个地方去设置本地数据,它本质上它也是无用的。现在这个代码我们已经设置完了,单击编译按钮,然后进行测试,注意看我们的导航区。当然我们编译模式现在可以改一下了,改成我们的普通编译模式,注意看一下我们的调试区,这里面有一个已取到缓存的导航数据,这个代码是在这一行打印的。然后在下面还有一个也取到了后端的导航数据,这行代码是在这个地方打印的。也就是说我们这个代码它首先会从本地,然后取到缓存的数据,然后并且马上启用,同时它接着又向后端发起接口的请求,然后再获取数据,同时将本地的缓存的数据然后进行一个刷新。下面再看在我们这个调试区它有一个面板,一我们本地的这个Storage面板,这个里面navs这就是我们本地缓存的数据,这个代码演示我们就到这里。 下面我们看实践二。 打破网络请求的10个并发限制,并按优先级排序。由wx.request接口发出的网络请求,有最大10个的并发限制。为了破除这个限制,同时让高优先级的网络请求操作先执行,我们可以进一步改造我们的request工具函数,改造以后这个函数的代码如我们屏幕上显示的。首先我们引入了一个自带优先级的异步队列,叫做priority-async-queue。这个模块需要使用yarn或者npm安装,安装指令如屏幕上所示。安装以后在工具菜单栏,别忘记选择构建npm进行模块代码的构建。在使用自定义的request方法的时候,针对重要的网络请求只需要添加一个值等于urgent的一个priority参数即可,如我们屏幕上显示的这样。感觉是不是很简单。调用方法及其他参数都不需要修改,这是接口迭代进化中的向后兼容性,可以最大程度的保证旧代码在项目迭代中的一个持续使用,运行效果与之前一样。在网络环境顺畅的情况下基本上是无感知的。 下面我们看代码演示。 查看package.json这个文件,在这个文件里面我们可以看到多了一个模块的引用,这个模块叫做priority-async-queue,我们将这个给它复制一下,模块名给它复制一下,后面的版本是2.1.1,如果为了保证这个版本一致,稍后我们在安装的时候还可以将这个版本号给它加上。拷贝以后我们需要打开一个本地的终端窗口,终端窗口我们可以在VSCode里面操作,当然也可以在我们的微信小程序里面也是可以的。在我们miniprogram上面选择内建终端打开,选择以后这个地方我们使用yarn add,然后将我们刚才那个名字给它拷贝一下,还可以附带我们的版本号。添加,很快它就已经装上了。装上以后我们还可以顺带看一下这个目录下的package.json文件,确认一下 这个地方已经有这个模块了,这是第一步。第二步就是改造我们的request方法,目前我们request方法它是不支持优先级的,我们需要对它进行一个改造,打开我们已经修改好的代码,首先在最上面有一个对我们这个模块新安装模块的一个引入,同时下面有一个queue对象的创建,这个数字代表是我们最大允许的一个并发数字默认等于10。当然我们也可以传一个其他的数字都是可以的,然后将这个代码放在文件的最上面,再往下这个地方有一个关于优先级的定义,包括这个地方,它有一个导出,这个代码我们都需要拷贝过来,然后放在这里这些参数,还有这些参数其实都不需要修改,然后这个地方有一个关于默认的优先级的设置,如果它没有优先级的话我们就给它一个normal的这样的优先级。再往下在这个里边重点的代码在这个地方有一个queue.addTask,同时将priority优先级也给它传进去,后面是一个箭头函数,这个箭头函数它代表的是一个匿名的一个闭包。我们可以将这个代码给它拷贝一下,然后放在这个里面可以对比一下我们上下,它这个代码的一个区别。其实等于是我们将原来的代码,也就是这个代码放在了它的里边,同时将用addTask方法对它进行了一个封装。封装以后我们原来这个代码它是作为,然后由闭包函数然后封装一下,然后作为第二个参数,然后传进来的,是这样的一个改造方式,这样就可以了。 现在我们需要对我们原有的代码做一个改造。我们目前有一个是拉取首页数据的这样的一个代码我们修改一下它的优先级,在这个文件里面retrieve_home_data.js,然后这个里边,有对request的方法的一个调用,在这个地方,有一个调用,然后我们要在原来的这个位置,原来它是有一个url参数,我们再加另外的一个参数,就是priority,然后它的值我们让它等于urgent,等于这个,确认一下priority。这是一个priority,让它等一个urgent代表是最高的一个优先级,这样修改就可以了。修改完成以后我们单击编译按钮,然后进行测试,这个地方出现了一个错误。在调试区,大意是说module,然后这个module is not defined没有定义。这个模块没有定义,为啥没有定义,为什么没有定义?因为我们在本地刚才安装了第三方的模块以后我们没有选择,我们没有在这个菜单里面选择工具构建npm。这一步很重要,只有构建以后,我们新添加的模块它才会从我们的目录下面小程序这个目录下面有一个是node modules,从这个目录下面然后再转到我们的这个npm,就转到这个miniprogram_npm 转到这个目录下,转到这个目录下以后,然后我们才可以去加载和使用priority-async-queue这样的一个模块。现在这个目录下它已经有了,说明我们现在可以访问了,我们再次单击编译进行测试。现在代码错误已经不存在了,然后我们再看数据的表现,数据仍然可以加载,也没有问题,这个代码演示就到这里。 下面我们看一下小结,关于本地缓存接口,前面我们已经介绍过了,它们都是同步接口,即使像wx.setStorage、wx.getStorage这样不以Sync结尾的接口,由于某种历史原因,它们也是同步接口。那么至少目前是这样的,以后可能会修改,所以在使用这些接口的时候,我们一定要特别注意使用的时机,最好在Page.onReady周期函数中,或者是在之后的时机使用本地缓存接口,使用本地缓存。另外一个特别需要注意的点是一定要时刻铭记,本地缓存的数据是不可靠的,本地数据有可能因为各种原因缺失或者损坏。使用本地缓存的数据,但不能依赖本地数据。当获取本地缓存数据失败的时候一定要有后端接口可以顶上,或者是其他的方式可以顶上。对于小程序里边的wx.request接口可以管控起来,不仅因为网络请求在低版本的基础库版本中有最大10个并发限制,还处于优先级排序的需求,以及有可能存在的页面访问的权限控制要求。在实际的项目开发里面,某些后端接口是一定要用户实现鉴权以后才允许访问的。这类统一的鉴权访问控制就适合在request工具函数中统一实现,这部分内容不属于优化内容,但是对项目来说也十分必要,如果需要拓展的话都可以在request.js文件的基础之上然后进行修改。 点击查看相关文档: 数据缓存 /wx.setStoragepriority-async-queue这节课就讲到这里,上面的网址是本课涉及的文档地址。 这节课我们主要学习了如何使用本地缓存数据,即如何使用优先级队列优化网络请求。下节课我们学习图片优化技巧。 最后我们看一下思考题。这里有个问题请你思考一下,webp是Google在2010年推出的一种新的图片格式,它使用更优的图片压缩算法在相同的图片质量下能让图片保持更小的图片体积,Youtube的视频缩略图采用webp格式以后网络加载速度提升了10%左右,Google的Chrome网上应用商店采用webp的格式以后每天大概节省了几TB的一个带宽,现在小程序中的image组件也开始支持使用webp格式的图片了,但一般我们在团队开发中使用的图片多半是png,或者是jpg这样的格式,那么有什么办法可以快速将这些图片转化为webp格式,并且在小程序项目里边使用。下节课我们就一起来深入探讨一下这个问题。
2022-07-15 - 图片优化技巧
[视频] 你好,我是李艺。 上节课我们主要学习了如何使用本地缓存数据及如何优化网络请求的优先级顺序,这节课我们学习图片相关的优化技巧。 先看一下问题,随着网络的普及及用户带宽的提高,用户对图片清晰度的需求也越来越高,一张高分辨率的图片如果没有经过任何的压缩处理,动辄就是几MB 甚至十几MB的大小,当我们把这些图片用于小程序的本地图片时却是致命的,一个小程序的代码包最大也不过是2MB的大小,图片优化主要可以从以下这几个方面进行着手: 一 、尽量减少图片的请求次数 二、尽量压缩图片的大小 三、尽量使用带有CDN加速的网络图片链接 四、尽可能使用高压缩比的图片,例如webp格式的图片 下面我们看代码实践 。 首先看实践一,生成雪碧图。 这里的雪碧图与饮料没有一点关系,它们只是凑巧翻译自同一个英文单词Sprite,那么什么是雪碧图呢?雪碧图就是人为地将多张小图片拼接成一个大图片,在使用的时候一次性在客户端完成加载,利用CSS的background-image样式只显示图片的某一块区域的这样的一个技术,从而实现在多个元素上复用一张图片,减少图片的加载请求次数,miniprogram-slim是一个微信团队推出的小程序资源瘦身工具,下面我们就使用这个工具实现雪碧图的生成。首先我们需要安装这个模块,安装指令如屏幕上所示,在安装以后,我们就可以使用子指令sprite生成雪碧图了,生成的指令如屏幕上显示的这样,生成的sprite.png图片是由多张小图片自动排列而成的,如我们屏幕上展示的这样是它的一个生成的效果。虽然有时候看起来图片区域的利用率不是很高,但整体上来讲效果已经不错了,小图片的排列布局是自动生成的,本质上生成的图片效果是由参与合成的小图片它们来决定的,与雪碧图同时生成的还有一个sprite.css文件,这个文件包含了所有子图片的使用样式如屏幕上所示,在生成的样式文件里面各个图标的偏移量都已经写好了,在使用的时候直接拷贝这些样式,然后稍加修改就行了。如果我们愿意,可以将任意多张的图片拼接成一张图,不管原图它是大是小。 下面进行实践一的代码演示。 首先我们看一下最终写成的代码,这是我们最终的一个代码,在tools这个目录下面有一个static这样的静态资源目录,然后有一个generate_sprite.sh这样的一个脚本文件,在这个脚本文件我们使用miniprogram-slim,然后调用它的子指令sprite进行了一个雪碧图的生成,这是我们的一个代码,我们可以将这个代码先拷贝一下放在我们目前的这个项目目录下面,找到tools目录拷贝在这个地方,然后我们再看一下静态目录,静态目录我们不需要从这里拷贝,因为本身在我们小程序项目里边也有一个静态目录,可以切到我们项目里边,然后找到本地static静态目录,将这个目录拷贝到tools下面,现在就已经过来了,再看一下我们相关的模块,安装以后的模块在这个文件里面没有显示,本质上它是要有这个的,然后我们再往后看一下,打开这个,这个也没有,我们再往后找一个,这是另外的一个,这个我们可以进行全局安装,打开我们目前的tools,在这个目录下打开集成终端环境,然后使用yarn global add进行安装,如果我们知道版本号的话最好把版本号给它带上,现在我们就可以安装了,这个版本号稍后安装以后,我们就知道它大概的一个版本号信息了,另外我们还需要一个本质上这个模块工具,它还需要一个叫做libpng的第三方模块,这个也是需要提前安装,现在我们可以看到了我们这个版本是1.0.1,你在安装的时候最好把后面的版本号也给它带上,这样保证效果的一致性,下面我们再用brew安装另外的一个libpng这个模块,brew是Mac OS上面的一个包管理工具,本质上我们装的libpng它不是一个Node.js模块,所以我们拿这个来进行安装,安装以后这样就可以了,接下来我们就开始去调用我们先前已经生成的generate_sprite.sh这样的一个脚本了,执行脚本generate_sprite.sh,然后执行,脚本里面我们可以看一下,我们是以static也就是它当前目录下static里面的icons目录,它里边所有的这些图标文件作为输出的这些小图片,同时这一个-o它指定的是我们输出的目录,就是输出到我们当前这个目录下,现在这个指令已经执行完了,我们看一下在我们当前的tools目录下面多了两个文件,一个是sprite.png,这是我们生成的雪碧图,另外还有一个sprite.css文件,这个文件我们可以看一下它里面会有一些类样式 ,每个类样式都标注了背景图的一个偏移量,还有它的宽高,有这些信息以后,基本上我们在使用这张图片的时候就可以将这张图片作为背景,应用于不同的组件了,有它的起点以及区域所占的宽高大小就可以指定使用某一块区域了 ,它是这样的一个工作原理,代码演示我们就说到这里。 下面我们看实践二,图片压缩。 看完雪碧图的生成,下面我们看图片压缩,要想压缩图片,有现成的在线压缩网站,例如现在我们屏幕上看到的链接就可以使用,在本地无网络的状态情况之下也可以使用离线的压缩工具,我们将小程序项目下面的静态目录static拷贝至tools目录下面以后可以运行compress.sh这样一个脚本,就可以在dist目录下看到压缩之后的图片了,compress.sh脚本是使用miniprogram-slim模块的子指令imagemin完成这个图片压缩功能的,其脚本主要内容如我们屏幕上显示,子指令imagemin的运行依赖libpng模块,如果本地还没有安装这个模块则需要在执行压缩脚本之前进行安装,以Mac OS系统为例,我们可以使用包管理工具brew进行安装,安装指令如屏幕上所示,压缩并不是优化的终点,图片无论怎么压缩终究它还是会占用本地代码包的大小,小程序里面的图片的终极优化还是得依靠网络图片。 下面我们进行实践二的代码演示。 首先我们看一下最终的我们编写的压缩脚本,在我们的最终源码目录里我们可以找到,在这个tools子目录下有一个叫做compress这样的一个脚本,将这个脚本文件我们复制一下拷贝到我们当前的目录下面,看一下这个文件,这个文件里面的脚本是调用了miniprogram-slim它的一个子命令imagemin这样的一个子命令,然后我们以static目录下的文件,就是所有的文件 png的文件作为输入,同时指定压缩的图片的质量,然后将所有的文件然后输出到当前目录下的dist,这样的一个目录下是我们的写的这样的一个压缩的脚本,当然在使用这个脚本之前仍然需要确认在本地已经使用yarn global add这个模块,后面我们最好加上版本号1.0.1,这是我们目前在使用的版本号,这个模块安装以后,接下来我们还需要使用本地的包管理工具brew,然后install libpng 这个模块也需要安装,因为我本地已经安装了,这个指令就不需要重复执行了,这两个模块安装以后,接下来我们就可以执行compress指令了。 我们用bash执行它会显示一个结果,13个图片被处理,我们看一下这个目录下面会有很多的一些图片,包括icon图片,还有我们的程序里面用到的一些商品图片都包含在这个地方了,但是它这个里边它没有进行一个分组 没有分目录,所以需要我们自己去处理,这个文件搞完以后可以将这些程序用到的这些图片拷贝一下,然后到我们的当前的小程序的静态目录下,images 到这个目录下进行粘贴,因为粘贴它会重新起名,所以我们就原来的全部给它删掉,将这些图片先给它删除,然后再拷贝一下,把这些图片给它拷贝一下放在这个目录下,另外还有一些icon图片,这些图片我们也拷贝一下放在这个icons这个目录下面,这个拷贝的体验不是很好,拷贝过来了,另外还有两个,这两个其实应该在这个icons这个目录下面,我们把它移过来这样就可以了,全部修改完成以后,在我们微信开发者工具里面单击一下编译按钮,看一下最终的结果。 主要看我们这个图片的一个效果,没有问题,基本上没有问题,这个地方有问题,我们无法定位list,icons/list.png图片,这个图片可能有缺失,我们把它再补全一下,这个图片是有的,我们可以清一下编译缓存然后再刷新一下,这个地方它仍然会显示我们无法定位/static/icons/list.png,我们再查看一下/static/icons/list.png,这个里面还是少了一个 少了一个list.png图片,我们可以从最终的目录里边找到这个文件放在这里,然后再刷新一下,现在已经没问题了,这个代码演示就到这里。 下面我们看实践三,使用腾讯云cos存储本地图片。 下面使用腾讯云的cos对象存储服务将本地图片转化为网络图片,首先我们需要实现上传本地图片至腾讯云cos存储的方法,uploadImageToCos,最终这个代码如屏幕上所示,这个方法是基于官方的cos-sdk的Node.js脚本实现的,另外我们再创建一个upload.js文件,并且在这个文件里面实现一个uploadImageToCos函数,实现以后这个代码如屏幕上所示,upload.js文件主要用于遍历本地的static目录,将本地图片自动上传至腾讯云对象存储服务器,在使用upload.js脚本之前我们还需要做一些有关账号的准备工作,首先我们需要去腾讯的官方网站开通对象存储服务并创建一个默认的存储桶,这里我们不必担心花钱的问题,因为新用户他有一定的免费额度,上手是没有任何困难的,开通以后要到用户中心-访问管理-访问密钥,API密钥管理这个地方拿到一组SecretId和SecretKey信息,并把这两个信息写到本地的环境变量里面去,这一步是为了让源码中的环境变量读取代码可以生效,这个读取代码如屏幕上所示,稍后我们在代码演示的时候也会看到。 成功运行upload.js文件以后会得到一个cos_urls.json这样的一个文件,在这个文件里边存储了图片上传前的地址以及上传后的网络地址,还包括webp格式的网络地址,其内容结构如屏幕上所示,在拿到了图片的webp网络链接以后就可以到项目中进行使用了,将原来的本地图片链接都修改为线上的网络链接,例如在后端接口返回的数据当中就有不少的本地链接都是可以修改的,在给image组件换链接的时候,如果使用webp地址,我们还要注意要将image组件的webp属性打开,默认情况下这个属性它是关闭的,为了方便替换我们可以使用Node.js再写一个额外的处理脚本,脚本内容如屏幕上所示,replace.js文件是一个根据生成的cos_urls.json文件,批量将本地文件中的本地地址替换为webp云链接的一个脚本文件,在这个文件里边dealWithDir是要处理的目录,变量localToCloud控制是否是从本地向云端链接进行转换,还是反过来从云端链接向本地进行转换,找到我们最终源码的目录,在这个下面首先我们要看第一个文件是upload_img_to_cos.js这样的一个文件,这是一个js文件,这个文件里边我们用到了一个腾讯cos官方的一个cos-sdk,这是它的一个Node.js版本,目前这个版本是v5这样的一个版本,使用之前我们首先要在我们当前的项目目录下进行这个模块的安装,跟之前一样,我们安装的时候最好也要指定它的一个版本号,也就是2.11.6这样的一个版本号,我们看一下当前项目下的这个tools目录,安装指令就是yarn add,然后是这个模块名,后面跟版本号2.11.6,把这个给它装上,这一步装上以后还需要创建几个文件,第一个是这个文件,第二个是upload.js文件,第三个我们还需要有一个replace文件,这个文件也是需要的,将这三个文件给它拷贝一下放在我们当前的tools目录下。 首先我们看一下upload_img_to_cos.js这个文件,这个文件调用这个sdk,然后这个地方是从我们本地环境里面去取我们的两个环境变量,这两个环境变量现在我们还没有设置,稍后我们从腾讯的cos后台拿到这两个信息以后还要在终端里面进行设置,这是两个信息 这两个信息具备以后我们这个地方定义了一个uploadImageToCos的这样的一个函数,这个函数里面主要使用主要的一个功能就是调用前面引入的cos-sdk进行一个图片的上传,将我们本地的图片上传至腾讯云的cos对象存储,这里面有些信息,第一个地方这是存储桶的名字,这个名字稍后我们在cos后台可以看到,然后这个是存储桶所在的区域,这个区域也是我们创建存储桶的时候所选择的,在后台里面也可以看到,这个地方Key这个文件名 这是我们必须的设定的,是由我们自己来设定的,前面这个images是我们存储桶里边一个自定义的子目录的名字,然后这个上传模式我们选择标准模式,上传对象就是file,这是它主要的一个上传代码,上传以后它会进行返回,把这个信息进行一个拼接,在这个地方拿到这个上传之后的在线链接以后,它的webp格式本质上它其实是在我们原有链接之后,后面加了一些特殊的后缀将它变成了webp的这种格式,这是这样的一个代码,然后最后是将它导出。 下面我们再看upload.js这个文件,我们是去遍历我们本地的一个目录,这个目录我们是遍历我们上级目录下的static,也就是说我们真正的小程序本身遍历它静态目录 ,然后引入我们前面自己写的方法去循环读取我们这个目录,拿到下面的这些文件,拿到文件以后,当然这个文件我们还做了一些后缀的过滤,我们只允许上传这些后缀的文件,过滤以后我们就开始上传了,拿到以后开始有一个循环,将这文件循环,循环以后我就上传,上传以后得到一个上传的结果,拿了一个结果,这个结果再去写到本地的一个cos_urls.json这样的一个文件里面 ,这是我们上传代码。 下面我们到腾讯云的cos后台看一下我们需要准备的一些信息、密钥等等一些信息。打开腾讯云的网站cloud.tencent.com 以后,首先要找cos 就是对象存储,第一次打开的时候是没有存储桶的,所以我们要按照提示去创建一个存储桶,创建以后在存储桶列表里面会有我们默认的存储桶,其中这个地方我用的是cloud,然后连字符跟一串数字,这个存储桶我们单击进去会看到它相关的一些信息、基础配置,打开概览,这个是存储桶的一个名称,然后这个地方有一个ap-beijing,这是我们前面在那个代码里边看到的,跟这个是一样的,它所属的一个地域在代码里面也要写,这个地方存储桶的名称刚才我们的代码里面也看到了,这两个信息都是需要的,另外还有一个是我们这个文件列表,可以单击看一眼,这个地方它有一个images的子目录对不对,因为我们自定义的key在代码里面自定义key 前面加上这样的一个前缀,这样就可以将我们这个图片上传到这样的一个子目录下,这是一个很重要的信息,除了这些信息以外,接下来还有一个很重要的信息,就是我们要选择右上角头像这里有一个叫做安全设置,选择安全设置 ,然后再选择访问管理,然后再选择访问密钥,下面有一个API密钥管理,选择这个,这个地方默认它是没有密钥列表的,我们如果没有的话,第一步我们要选新建,新建以后,这个地方如果说你之前新建了还可以选择显示,SecretId和SecretKey 这两个信息拿到以后,接下来我们要去设置本地的环境变量,打开我们本地的,当然我这个系统是Mac OS,在其他系统上要设置,例如在Windows上面要设置系统的环境变量去设置,打开这个文件,在这个地方我们可以看到有一个export COS_SecretId等于它,下面这个是类似的COS_SecretKey等于它,这两个信息输出搞定以后,接下来我们就可以回到我们代码里面了,也就是在我们的上传代码,最上面这个地方,有了那两个环境变量以后,这个地方它就可以取到真实的密钥信息了。 为什么要把密钥信息放在我们环境变量里面,其实直接放在这里也是可以的,本地测试可以放在这里,我这里是为了防止密钥信息在这个仓库里面不小心外露去设置的放在这里的,这一步改完以后,接下来我们就可以执行我们upload.js文件了,这个文件它本身是一个js文件,我们可以拿node去执行,node upload.js运行以后,这个速度很快,因为这得益于我们的网速很快,所以它很快。拿到这个文件我们可以格式化一下,这是一个json文件,格式化以后我们可以看到这里面是一个列表,这个列表的每一项它包含四个信息path,这个path是我们本地的一个地址,然后是name 图片的名称,imgUrl是我们线上的地址,webpUrl是我们的webp格式的一个地址,如果单击这个链接的话,在浏览器里面是可以打开、查看图片的,后面这个也是一样,只不过是这个格式不一样。因为谷歌浏览器它本身支持这种格式,所以查看这种图片是没有问题的,这一步都搞完了,接下来我们接着再搞另外的一个文件,就是replace.js。本质上搞完前面那一步拿到这个结果以后我们就可以替换了,但是如果我们要替换的信息很多的话,这一步也比较累,所以我们再进一步,就写一个replace.js这样的一个脚本,这个脚本的作用它是自动将我们某一个目录下的,里边的本地链接,然后进行一个替换,这是我们主要的一个目的,我们替换的时候,其实它是有一个路径的比对的,有个路径的比对的。如果我们这个路径它不一样的话,它其实是很难去把它给替换掉的,比如说我们现在接下来想替换的一个是这个,我们导航组件,我们先打开我们导航组件,看一下我们这个目标文件,看一下它本身它有哪些本地图片链接,这个是它本地的链接对不对,这个是它本地链接,然后我们需要将这个,还有后面这些都给它替掉,都给它替掉,我们先执行一下,执行一下我们replace这个文件,先替换,看它的一个表现,node replace.js,然后会有一个输出,会有个输出,就是我们大概替换了哪些文件,替换的结果在哪里,替换结果在哪里。看一下我们这个文件,replace.js这个文件,看这个文件我们主要是怎么样去做的一个替换,读文件,这个是获取列表,然后获取所有的图片,然后这个是读取json的一个信息,创建目录,然后这个地方这是我们的主要的代码 在这个地方,然后我们这个地方是取里面的一些路径,取我们要替换的文件里面的一个路径,然后进行replace一个替换,本质上我们要查它里面是不是有这个地址,结果是已经处理了,我们这个地方会有一个打印,已经处理什么,一个path,可以看一下。这样一个信息它会有一个打印,这个地方好像,好像并没有打印,再次执行一下,好像没有,我们前面的这个打印,它其实是我们里边的console.log,这些信息都是有打印的。但是我们后面这个,它并没有打印对不对,并没有打印,它并没有打印,可能跟我们的文件里边,我们现在地方 这个信息跟它可能是有关系的,我们可以改写我们的代码也是可以的,因为把前面这个前缀给它去掉,当然我们在这个地方给它手动给它替掉也是可以的,然后这两项给它加上,然后全部替掉,现在就变成这样的一个链接了 。 我们再执行一下替换,这个地方就已经处理,处理完以后看一下目标文件,这个地方没有,在tools/dist目录下生成了一个文件,在这个下面有miniprogram。注意我们这个地方iconPath,selectedIconPath这两个都已经替掉了,这是我们替换之后的文件,我们可以将这个文件的内容给它拷贝一下覆盖到我们这个里面 ,这样就完成了导航组件的本地网址的替换。接下来我们再干第二件事情:替换另外一个目录,仍然是找到我们replace.js,把上面处理目录给它替掉,然后注释掉、打开,把它上面给它打开,下面这个给它注释掉,这个目录是我们后台,这个文件我们看一眼 ,在server controllers,api home这个文件里面,其实有不少的本地地址,我们可以搜一下static开头的,看有很多都是这些路径,现在我们要处理它,仍然执行这个,已经处理了,处理完成以后 然后在我们的 找一下tools,server这个目录下这有一个home.go这个文件,可以再查看一下这个地方所有的图片,这个地方看到没有,已经变成了线上的地址,已经变成网络链接了,我们将这个文件给它拷贝一下到我们这个服务器端这个程序目录下,将这个文件给它替掉,替掉以后,因为我们后端有热加载,原理上它会重新编译,我们打开这个地方再重新编译。 这一块处理完了,还有一个地方我们需要处理,在我们的小程序这个项目目录下我们有一个云函数,云开发目录下有一个叫做getHomeData这个文件,这个文件里面也有一些信息,看到这些、还有这些也需要处理,处理方法是一样的,找到我们的万能的replace.js文件,找到它拷贝一下,上面给它注掉,这个地址我们要变一变了,可以先拷贝一下它的相对路径粘到这个地方,然后这个地方我们要改写一下,当前的tools它的一个父目录下,父目录就是这个,所以把这个1给它去了,这样后面要加一个斜杠,代表它是目录,搞完了,然后再回到我们刚才执行脚本的终端再执行一下它,这个地方也已经处理了 对不对,我们查看一下最终的目录,它的文件这些已经替换了,把这个文件拷贝一下回到我们云开发,云函数 index.js ,覆盖完成了,所有的修改都完成了,现在我们单击编译按钮重新查看运行效果。 这是首次它编译所需要的出现的错误不用管它,再单击一下;怎么检查我们的图片是不是从网络加载的,可以看Network面板,Network面板里面它有一个Img标签,我们可以看这里边这些,这不都是有一些从网络上加载的地址,在这个地方我们还有两个地方需要修改:第一个是我们导航组件,我们这个里边不是用了image,我们要把它的什么 webp,webp属性给它打开,因为我们用的地址都是webp地址,这是一个。另外还有一个是在我们首页,看一下首页,这里,这个里面我们也用了,我们搜一下所有的图片,这个地方加上一个webp,然后再往下找,这个地方有了,这个也有了,确保所有的都有这个属性,这个代码演示我们就说到这里。虽然代码演示的内容有点多,但是并不复杂,相信你耐心进行操作的话也可以完成。 下面我们看小结,雪碧图在游戏开发里面使用较多,在小程序开发里面使用不是很普遍;一方面是由于每次修改图片以后 需要重新生成雪碧图,这样比较麻烦。另一方面小程序里面的本地图片,是在代码包里面整包下载的,它并不是单张一个一个分别下载的,这样一来生成雪碧图的意义就不是很大了。图片压缩是普遍需要的一个技巧,图片在上传腾讯云对象存储之前,还可以先进行一番无损压缩,这样多少还可以节省一些存储空间,图片上传以后,在使用webp格式的一个链接的时候,图片本身也存在压缩,这在一定程度上也切实减轻了小程序网络资源请求的压力,这节课我们就讲到这里。 点击查看文档: ● https://github.com/wechat-miniprogram/miniprogram-slim ● https://cloud.tencent.com/product/cos 上面的网址就是本课涉及到的文档地址,这节课我们主要学习了有关图片的优化技巧,下节课我们学习如何使用小程序助手。 最后我们看一下思考题,这里有个问题请你思考一下,我们知道在setData更新数据后。 触发的重渲染机制,会严重阻塞页面视图的即时渲染效率,但是有一类原生组件,它们是有原生的Context节点,可以使用标准的SelectorQuery查询得到,并且使用它们可以绕过底层的数据中转,直接更新视图节点,你知道这个技巧是怎么使用的吗? 下节课(对应9.2)我们就一起来深入探讨一下这个问题。
2022-08-05 - 使用“小程序助手”的性能分析,与实时日志提交
[视频] 你好,我是李艺。 上节课我们主要学习了有关图片的优化技巧,这节课学习使用小程序助手。 先看一下问题(背景),小程序助手是官方推出的一个小程序,它可以查看当前微信用户,也就是开发者有权限查看的小程序数据,包括但不限于性能分析,实时日志是从小程序代码里边 实时向微信服务器提交日志信息,并且可以从小程序后台查看提交的结果。虽然实时日志有上传大小,保留期限等等限制,但是在产品新版本上线的时候,管理员可以通过这个功能快速浏览新版本发布的情况,看看它是否正常,如果发现了大量异常,方便管理员进行版本的回滚。下面看项目实践。 首先看实践一,在小程序中查看性能分析。 如屏幕上所示,我们使用手机可以扫描小程序助手的小程序码就能打开小程序助手,打开以后依次选择当前的小程序账号,性能分析页面,如我们屏幕上所示。这里有概况,启动性能,运行性能和网络性能等标签。用户设备还分高档,中档和低档三个层次。已经上线的小程序,查看这个地方的数据,基本上就能知道性能优化的一个大致结果了。 下面我们进行实践一的代码演示。 使用自己的开发者微信账号点扫一扫,然后扫我们的小程序助手二维码,打开以后,我们在这个地方其实是可以进行切换的,然后选择我们当前在用的小程序账号,下面数据分析这一块有一个性能分析,打开以后可以看到有昨日概况,启动性能,运营性能,网络性能等很多标签,在这里面还有一些关于从启动耗时内存,网络等等相关的一些数据。这些数据对于我们团队里面不是开发者的人员来讲,使用小程序助手已经足够了。 下面我们看实践二,上传实时日志。 向微信后台发送实时日志是通过小程序提供的RealtimeLogManager这个管理器实现的。首先我们需要创建一个log.js文件,代码如屏幕上所示。在这个文件中实现管理器的获取,以及相关日志上传的代码逻辑,然后在app.js这个文件里面引入log.js文件模块,并截持console.log这个接口,代码如屏幕上所示。将原生的console.log,重定向日志上报接口,这会导致我们原来所有使用console.log输出的测试信息,都会上报至微信后台,如屏幕上所示。我们登录小程序后台以后,就可以在后台查看到上报的日志信息了。对实时日志的使用我们还是要克制一些,毕竟日志上报也是要占用设备流量的有,在研发的时候可以借助实时日志进行团队测试,开发者也可以查看测试人员的日志情况,在上线之前可以将console.log重定向代码注释掉,或者是仅上报错误信息。注意查看实时日志,不要求产品上线,但是对于开发版本来讲,在模拟器和客户端微信里面进行预览的时候也无法进行日志上传,只有在手机上预览时日志才会实时上报。 下面我们进行代码演示。 在library这个目录下面,我们有一个log.js文件,这个文件我们是需要的,我们将它拷贝到我们当前的项目目录下,放到library目录下面,这个文件的代码我们简单看一眼。首先上面我们是新建了一个rtlog这样的模块变量,它是通过wx.getRealtimeLogManager这样的一个平台接口,然后获取到这个实时日志管理器,得到这样的一个实例,然后在下面这个地方我们是保留了原来的console.log接口,保留是方便我们在重写这个接口以后仍然可以对它进行调用。下面就是相关的方法的一个实现了,我们重点看这个info接口。首先是我们将我们传进来的参数,同时然后传递给原来的console.log这个方法,这样可以在本地进行信息的一个打印,同时我们在后面去调用我们的管理器实例,它的info这样的一个接口,将这个信息传递给它,这样的话我们日志信息就会实时上报给微信的后台了。另外还有一件事我们需要做,关于我们把原来的console.log接口的一个截取,这个截取我们要放在我们的app.js这个里面来做。 首先这个地方我们要有一个引入,引入以后就是关于这个方法的一个重写了,我们将这个代码给它拷贝一下,然后到我们当前的小程序目录下,在最顶部的位置,然后放在这里,这样就可以了,然后我们原来的所有的关于console.log代码的调用都不需要修改,它会自动调用我们日志管理器里面的方法的,代码已经改完了,接下来我们可以进行测试,在测试的时候我们没有办法使用模拟器进行测试,在模拟器里面它还有一个叫做MAC端自动预览,或者是Windows端自动预览,这个也无法进行日志上报,但是我们可以通过手机端预览,然后可以实现日志上报,可以测试它的一个效果,上报以后,接下来我们就可以在后端进行查看了。下面我们登录我们小程序的后台,然后在左边的导航栏里面选择开发,开发管理,然后再选择运维中心下面实时日志,选择这个标签页,这个地方就会列出我们当前已经存在的一些实时的一个信息了,它还支持微信号OpenID的一个过滤,功能还是比较全面的。因为现在这个地方它没有,但是如果你在手机上预览以后,这个地方其实它是可以看到实时日志的,我们代码演示就到这里了。 下面我们看一下小结。小程序助手不仅可以查看小程序产品的基本性能状况,还能查看用户量,用户投诉等等信息,并且还不需要安装,只需要在后台添加相关的账号权限,在微信上扫码就可以使用了。非常适合团队中的非开发人员使用,上传实时日志对用户的带宽肯定是有消耗的,这一块还可以进一步的优化,一般情况下对于消息上传我们可以选择不上传,只上传那些关键的错误信息。另外还有当用户处于4G网络的时候也可以不上传 只有用户在WiFi网络下的时候才选择上传。 点击查看开放文档: 调试 /实时日志基础 /调试 /wx.getRealtimeLogManager 这节课就讲到这里,上面的网址是本课涉及的文档地址。这节课我们主要学习了如何查看小程序助手的性能分析,及如何使用日志管理器对象进行实时日志的上传。下节课我们学习如何使用小程序后台的自主测速功能。 最后我们看一下思考题,这里有个问题请你思考一下,小程序的性能分析数据不仅能通过小程序助手查看,还可以通过接口调用的方式,在我们自己的后台管理页面中以自定义数据的方式拉取和展示,在数据拉取的时候,可能会遇到一个47001 data format error 这样的一个错误,这个错误你以前遇到过吗?应该如何去尝试解决?下节课我们就一起来深入探讨一下这个问题。
2022-07-15 - 总结
[视频] 你好,我是李艺。 上节课我们主要学习了如何通过接口获取小程序的性能数据,感谢你一直坚持学习到了现在。下面我们以第1讲讲过的小程序启动流程为主线,一起梳理一下在这个流程之上都有哪些具体的性能优化技巧。下面我们分别从环境准备,代码注入,及首屏渲染三个部分查看冷启动流程里面具体涉及到了哪些优化节点。 首先看环境准备。在这个里边一共有四个节点,在第二个节点里边代码包下载,校验及初始化。下面一共涉及到三讲的优化。第4讲使用独立分包和分包预下载。第5.1讲,减少分包大小,在分包页面里边使用占位组件。第5.3讲,减少主包大小,JS分布异步化加载与执行及插件代码的分包异步化。 下面我们看第二部分。这个部分一共分为主要的两个节点,在第一个节点框架及第三方基础代码初始化这个节点下面,在插件自定义组件扩展库代码注入的时候有第4讲与它相关,使用独立分包和分包预下载,尽量减少全局组件,全局插件的使用。第二点,开发者代码注入,在这里面分为逻辑层开发者代码注入和视图层开发者代码注入,其中在逻辑层开发者代码注入的时候涉及到第6.5讲,延迟同步调用和加快启动流程。在视图层开发者代码注入的时候涉及到第3讲加快注入速度,代码按需注入与初始渲染缓存,这里包括静态缓存与动态缓存两种。 下面我们继续看首屏渲染,下面分为五个节点。 第一个节点逻辑层页面初始化,也就是initDataSendTime这个事件触发的时候,同时有Page.onLoad事件派发,在这个下面一共分为八个方面的优化技巧。第一个是5.4讲使用WXWebAssembly优化运算性能,第6.1讲使用异步转同步的编程范式,第6.3讲在worker中进行耗时运算,第6.4讲在后端使用Go语言异步执行逻辑运算代码,第6.5讲延迟同步操作,使用串发复合命令延迟同步调用。首屏只加载单页所需的数据,第6.6讲小程序转至后台状态,不再调用setData更新视图。第8.4讲脚本优化技巧,及时清除定时器,添加的全局监听要及时移除。小心使用全局对象,第8.5讲setData调用优化合并多次调用。减少data中数据的定义,使用index局部更新长列表数据。 下面看第二个节点视图层,这里涉及到viewLayerRenderStartTime这个节点事件。其中包括Page.onShow事件的派发,下面一共涉及到五点内容。第2.4讲优化视图页动画效果,第2.5讲重渲染与自定义组件优化,第8.1讲视图代码优化技巧在动态列表中使用wx:key属性,使用catch代替bind,使用防抖函数与节流函数,尽量减少wxml节点,降低页面重渲染的压力。第8.2讲wxss优化技巧。给滚动列表开启惯性滚动,使用hover-class实现按钮的单击态,清除无用的wxss样式代码。第8.3讲UI优化技巧,使用padding及伪元素扩展元素的可单击区域。 下面看第三点开发者代码,从后端拉取数据准备data这方面的一些优化技巧,这里一共涉及到四点。第6.2讲尽量提前开始数据拉取,使用并发复合命令对齐不同文件的代码执行点。第6.7讲优化数据拉取,使用数据预拉取与周期性更新创建云函数提供数据,使用并发复合命令的竞赛模式拉取数据。第6.8讲 优化后端接口,开启缓存,使用Http2与Quic协议。第8.6讲网络请求优化,使用缓存将网络请求按优先级排序。 下面看第四个节点页面渲染,在下面一共涉及到六讲相关的内容。第2.2讲优化长列表显示,使用recycle-view组件。第2.3讲使用页面容器。第5.2讲减少主包及主包大小,在主页中使用占位组件,使用封面页代替主页。第8.7讲,图片优化技巧,图片压缩与使用腾讯云cos转存本地图片。第9.2讲使用原生类型节点,绕过逻辑层直接更新视图内容。上面所有优化技巧都是基于小程序的冷启动流程而存在的,在学习与应用这些优化技巧的时候,有三点内容我们一定要烂熟于心。第一点,小程序的冷启动流程特点,这部分内容我们在第1讲已经详细介绍了。第二点小程序的双线程运行机制,这部分内容介绍也在第1讲里边。第三点小程序的重渲染机制,这部分内容详细介绍在第2.5讲。这三点关于冷启动流程双线程运行机制及重渲染的内容,它们是枝干,所有优化技巧都是在它们的基础之上展开的枝叶。 下面我们看推荐阅读,这里有一些文章链接,如屏幕上所示,它们的内容都很不错,为本课程的打造提供了不少的创作灵感,推荐给你阅读。 下面我们看致谢与课程总结,这个课程在打造过程当中参考了不少的优秀的开源组件和社区教程,在此我们一并感谢,这些教程以及开源组件库的链接,如屏幕上所示。这个课程在以下方面富有一些创造性,在这里请允许我总结一下。 第一点,使用异步转同步编程范式,结合立即执行函数,在保证代码清晰性,可阅读性的前提之下提高主线程的执行效率。 第二点,将优先级自动排序的列队应用于网络请求当中提高网络操作的执行效率。 第三点,使用串发复合指令,在多个文件里面延迟非重要同步代码的执行。 第四点,使用并发复合指令,在多个文件中对齐代码执行点,使用竞赛模式基于多种渠道竞相拉取主页数据。 前两项多多少少或许与其他课程在方法上有些重合,但第三项,第四项关于命令模式的优化在其他地方我还没有看到过,算是这门课程的创新点,如有雷同纯属巧合。在优化策略上,整体来讲大体可以分为两类。第一类以空间换时间,例如数据预拉取,周期性更新,初始渲染缓存,使用LocalStorage缓存接口等等这些技巧,都属于以空间换时间的技巧。第二类以时间换空间,例如使用虚拟DOM,使用长列表组件,这一类都是属于以时间换空间的优化技巧,无论怎样的优化技巧,本质上精神都是一致的。精神就是现人现地,立足小程序的启动流程,双线程运行机制和重渲染机制,具体项目具体分析,以一个字节,一个字节去抠,一个毫秒,一个毫秒去节省的细致精神,一点点进行优化。在这个地方抠下了一点,在那个地方省下了一点,整体上性能就提升了。如果你在这个地方凑合一下,在那个地方也马虎一样,那么整体上性能也就下降了。 2017年微信小程序刚上线,那一年还有不少企业絮絮叨叨在质疑,在思考小程序到底值不值得去做。2022年第一季度小程序日活已经达到了5亿以后,已经没有企业在思考这个问题了。其他平台类似的技术生态的跟风和学习,以及小程序日活数字的日益增长,已经证明了这个技术赛道是对的,选择小程序作为产品的运营阵地,尤其是作为第一个运营阵地,已经成为初创企业的一个共识,纵然现在虽然小程序还有这样与那样的不足,但是我们同时也应该看到它一直都在快速迭代和快速进化。目前小程序在性能方面已经具备了这么多的调试工具和优化技巧,开发者在遇到性能问题的时候,已经没有什么好抱怨的了。把原理学好,把JS语言学好,把优化技能学好,完全可以把产品的性能优化好,甚至在一定范围之内还可以媲美原生应用。 源码地址: https://gitee.com/geektime-geekbang_admin/weapp_optimize 最后我们说一下源码,本课涉及到所有示例源码都可以从屏幕上的这个位置进行下载,源码是分课程划分目录的,例如6.6对应就是第6.6讲的示例源码。以上就是本课程的全部内容了,限于我的能力有限,这个课程有不完善的地方,恳请读者朋友多多批评指正。
2022-07-15 - 使用小程序端性能接口及后台“小程序测速”功能
[视频] 你好,我是李艺。 上节课我们主要学习了查看小程序助手的性能分析,及如何使用实时日志管理器对象进行实时日志的上报。这节课我们主要学习如何使用小程序后台的自主测速功能。 首先看一下问题,小程序的性能数据不仅可以在小程序助手里面查看还可以通过小程序性能接口及后端Web端性能接口直接拉取。如果小程序默认收集的性能数据不能满足产品需求的话,小程序后台还提供了允许开发者自定义的小程序测速功能,允许开发者在代码中自定义上传的数据格式,待数据传送到微信服务器以后还可以通过接口从微信服务器以自定义的方式拉取出来并自定义显示。下面我们看代码实践。 首先看实践一,在小程序里边拉取性能数据与原生组件的高效视图更新。 小程序的性能接口是wx.getPerformance接口,它不像其他接口那样。该接口返回的是一个性能对象,在创建了对指定指标的监听以后才可以收取到相关的性能数据,其主要代码如屏幕上所示。相关示例位于examples/pages/performance目录下面。当取到性能数据及按钮单击的时候,我们在屏幕上绘制了文本,与以前不同的是文本的渲染不是通过数据绑定完成的,而是先查取原生组件的node节点,然后直接进行绘制的,其主要的实现代码如屏幕上所示。这是通过原生组件的Context节点直接完成的视图渲染。node的类型是CanvasContext,它是原生类型的节点。原生类型的节点在更新视图的时候,它不需要经过逻辑层的中转,可以直接更新视图,这会绕过重渲染机制,比传统方式效率会更高。缺点是只有部分原生组件支持,并且接口不是很容易操作,支持直接查询的原生节点类型,如屏幕上所示,一共有这6种类型。如果你的产品功能中涉及到这些原生节点类型,并且原生节点上也有相应的接口可以实现所需的功能,那就不妨试一下用它们直接更新视图,项目示例中还用到了系统信息管理器,如屏幕上所示。这两种获取设备像素比的方式都可以使用,由于在SystemInfoManager里面我们已经实现了对小程序接口的调用劫持,所以这两种方式在调用效果上,及结果上并没有本质的区别。 下面进行实践一的代码演示。 首先我们查看一下在我们当前的小程序项目目录下面有一个examples目录,它的下面有一个叫做performance一个页面。但是这个页面目前还没有任何的一个具体的实现下,它是一个空页面,接下来我们看一下我们最终的源码是怎么实现的。在我们这个源码里面首先我们看一下js,这是我们的主要的一个js代码,在这个地方这是我们主要的js代码。我们先将这个代码给它拷贝一下,然后放到我们的当前的项目里面,然后我们再看一下它的js文件没有任何的一个配置,然后接下来是它的标签代码,然后放在这里,最后是样式代码很简单。但是这个基本上是无用的,因为在我们标签里面其实没有用到,所以这个也不需要拷贝。 下面我们就看一下我们js文件它里边主要的一个实现。首先最上面这个地方,我们是引入了我们之前已经实现的一个系统信息管理器,然后引入了它,然后在onLoad这个周期函数里面,这个周期函数我们可以改一下写法,直接用简短的这样的一种方式。在这个里边首先是我们用wx.createSelectorQuery这样的一个接口,创建了节点查询对象,用这个对象我们可以查询我们当前这个页面上它所包含的节点。我们可以用我们的测试页面主页,然后以它作为主页,然后再跳转到我们performance面板,当然它没有跳转的话,我们可以检查一下它本身的页面代码,它是不是数据没有写全,这个是有可能的。这是performance,然后我们单击以后,它干了一个什么事情,它是先去引入router模块,然后用了它的navigateTo方法,然后page是从我们的组件上进行传过来的,来查一下我们组件上的绑定的属性。这是绑定的属性,然后里面它的page,page是我们的一个它里面的一个pages里面的字段。它里面字段我们看一下,在我们这个数据里边,也就是这个地方,也就这个本身也是它的名称,也是它的一个地址,是它的名称也是它的一个地址。但是在跳转的时候我们要做一些特别的处理,然后它才可以实现跳转,所以这个方法我们需要一个小小的修改,那么看一下我们最终的源码,在这个地方url我们要写成这样的一种形式,然后把这个给它替掉,然后再刷新一下,现在就可以跳转进来了 。 我们再看一下这个页面里面的js代码主要做了一个什么事情。首先这个地方我们通过SelectorQuery对象,它的实例去查询一个原生节点,这个节点它有一个id叫做canvas,这个id我们可以通过它的这个页面代码可以查看。也就是在这个地方,这是它的id。查到原生节点以后,然后这个地方我们还指定了我们查询的数据字段查询,然后执行。执行以后这个地方会有一个init,然后它查询以后它会去调用,然后我们的这个里边它会调用init方法,然后去取它的宽高信息,然后node节点,上下文渲染对象,然后取到以后在这个地方出现了我们刚才提到的获取屏幕分辨率的方法。这种方式是通过我们已经实现的systemInfoManager,然后间接去取用的。然后下面这种方式是直接通过小程序的接口,然后直接去获取的。因为下面这个接口已经被我们劫持了,所以上下这两种方式其实是一样的,没有差别他,然后取到以后这个地方我们去算它canvas的一个宽与高,因为它这个地方本质上是与分辨率是有关系的,所以这个地方要做一个它分辨率做一个转化,转化以后这个地方有一个scale,然后赋值,将我们引用赋值到我们当前页面的对象上。当然我们这个地方没有,其实它相当于是一个写入,然后这个地方有一个renderLoop,这是一个重复渲染的本地的一个函数,匿名函数变量renderLoop,然后这个地方我们调用了canvas原生对象,它上面有一个叫做requestAnimationFrame,属于帧频函数。通过这个方法我们要启动一个循环调用,循环渲染,然后这个地方我们调用了this.render,this.render是我们当前要在画布上,然后进行绘画内容的方法,实现绘画的一个方法。首先最上面是清扫,将我们画布上所有的旧内容进行清扫,然后接下来有两个临时变量,然后有一个for循环,这个for循环本质上是从我们entries这个列表里边去取它里面的信息,取到以后,然后调我们画布的上下文渲染对象里边的一个fillText,这个方法进行文本的一个绘制,这是它主要的一个代码。 在这个地方我们所需要用到的API,已经跟小游戏开发已经很类似的,因为在小游戏里面本质上主要的视图更新,通过画布的一个接口相关的接口进行实现的,接下来我们再看entries这个列表。这个列表是怎么来的,这个列表是在这个地方查询性能数据,这个是我们通过小程序内部,它本身我们通过wx.getPerformance接口,然后取到一个性能对象,这个对象它比较特殊。我们首先要去监听,拿这个observer去监听指定的信息类型,然后在这个地方还要给它传一个回调函数进去,然后如果监听到这些数据的时候,我们这个代码它就会被执行了。在这个地方我们拿到这些数据以后,然后就向entries里面去推送我们这个信息了,这是我们这个数据,它是从这个地方来的。这样的实现方式,这个地方还有一个打印,所以现在我们可以看一眼加载到的性能数据,加载的性能数据它其实是从这一行开始然后取到的,我们单击btn,我们看一下btn在单击的时候它干了一个什么事情。这是btn对不对,它会触发onTap,然后onTap里面干了什么事情,其实很简单,我们是手动模拟了entries列表的信息的一个增加,把这样的一个信息给它塞了进去。 当然了我们也可以塞其他的一些信息,它只是一个模拟单击按钮,我们可以看到上面canvas不停地在进行一个刷新,并且渲染它本身是没有任何阻塞的。为什么,因为它本身它不是通过我们经典的重渲染机制,然后进行视图更新的。它是通过本身我们这个视图上面就有一个canvas这样的原生节点,我们通过这个原生节点它本身的接口去实现这些文本的一个绘制的,是这样的一种方式去实现的,所以它效率它比我们之前以往所用的视图更新的效率都要高一些,最后再看一下我们js文件里面,有两个地方我们需要注意一下,一个是onUnload,在这个里面我们做了什么事情。首先是这个地方this.observer,我们调用了它的disconnect observer.disconnect这样的一个断开连接的方法,就是断开监听。这个地方是canvas要取消我们的帧频的这样一个回调,把这个取消掉,然后把这个canvas给它设为null,这是一个清理的操作,在我们这个页面不使用的时候将里边的这些对象做一个清理,这是它的一个主要的要做的一个事情。我们这个代码演示就说到这里。 下面看实践二,通过analysis.getPerformanceData接口,在Web端拉取性能数据。 小程序的性能数据本来就可以上传至微信服务器,如果再在小程序中调用相关的性能接口收集这些数据,多少有些画蛇添足,并且小程序本身设备资源紧张,有限的代码包实在不适合添加产品以外的功能,除了在小程序中调用接口拉取性能数据以外,在后台Web端我们还可以直接调用微信提供的HTTPS性能接口,直接拉取性能数据。要实现这个功能需要涉及调用两个接口,如屏幕上所示。第一个是拉取token的接口,第二个是性能数据拉取接口,第一个接口是获取接口调用凭证的,在开发者的服务器上,调用微信服务器的接口都需要先拉取这个token,没有它,这个接口是调用不了的。这一步在我们这个示例源码里面已经使用Go语言,已经实现了,直接使用就可以了。第二个接口才是我们真正拉取性能数据的接口,在后端的home.go这个文件里面已经实现了性能数据拉取。主要的Go语言代码如屏幕上所示,在这里有一点我们值得特别注意,就是在实践过程中,我们非常容易出现一个叫做47001的错误data format error如屏幕上所示,这个错误的产生是由于后台代码里边网络请求头设置不当引起的。我们只需要将请求类型改为raw就可以了,如屏幕上所示。此外还有起始时间,它的单位是秒,不是毫秒,也不是其他的时间单位,不能写错。如果以上一些顺利的话,基本上就可以看到一个正常的数据结果了,数据结构如屏幕上所示。每个小程序的开发者在调用的时候,这个数据可能会不大一样,但是这个数据结构是类似的, 下面看实践二的代码演示。 首先我们看一下接口文档在这个地方有一个analysis.getPerformanceData接口,在这个里边接口的调用我们需要先有一个ACCESS_TOKEN这样的一个信息,这个信息从哪里来,需要先调用另外的一个接口,就是接口调用凭证。从这个地方我们可以单击进来,然后这个地方,它是一个auth.getAccessToken这样的一个接口,它专门是获取token的服务器端接口地址,然后调用它的时候我们会传送一些基本的像appid和secret这样的基本的纪要信息,所以像这个接口一般我们是在服务器端进行调用的。目的就是拿到一个接口调用的一个凭证,然后基于这个凭证,我们才可以调用微信服务器上提供的其他的一些接口,这个拿到以后,然后接下来我们就可以传递相关的一些参数,然后进行调用了,在这个里面有一个很重要的参数就是module,这个module它是我们预定义的一些类型,在这个地方有些预定义类型。其中像10022它代表的是内存指标,每个数字都不一样,这是这两个接口已经了解完毕。 下面我们看我们最终的一个实现,首先看一下我们最终的源码在服务器端的home.go这个文件里面,在最下面这个地方server controllers api,然后home.go 这个地方最下面有一个接口叫做GetPerformanceBy这样的一个接口,它有一个参数是module,module就是我们刚才提到的文档里面的我们所获取的性能数据的它的一个类型,我们先将接口的一个实现代码给它拷贝一下,然后放在我们当前的server端,sever端home.go,放在这个文件里面,然后这个文件这个接口它本身在被调用的时候,它还会调用另外的一个接口叫做getAccessToken,这个方法在我们当前这个文件里面已经有了,它本质上是访问我们这个接口然后拿一些纪要信息,然后去查询到一个token,这个里面用到的APPID和APPSECRET,这些信息是在这个地方,它也是通过环境变量然后去取到的这些信息,这俩信息是从哪里来的。这是我们小程序的纪要信息,这个文件我们可以在我们的本地环境变量里面可以查看,也就是在这个地方,是在这里设置的。这两个信息从哪里查看,在我们小程序后台,小程序开发后台,开发管理里边,然后开发设置,在这个地方AppID,AppSecret是在这个地方查看的。这个地方是可以重置,重置以后我们把字符串给它记下来,然后记下来写到我们程序里面就可以了。这是关于这个信息,然后这个信息有了以后,接下来我们就可以调用我们这个接口了,我们看一下我们接口的一个地址,这个地方还有错误,它这个地方有波浪线代表有错误,因为我们这个地方还有两个原生的模块需要引入一个是strings,还有另外的一个bytes,那么这两个都是Go语言的原生内置模块。我们到页面的顶部,在引入的位置加上对它们引入,可以了。然后这个地方它开始自动编译了,下面我们看一下我们这个接口地址,准备在浏览器里面进行测试,这是相对接口地址,然后打开一个浏览器标签,这个地址然后刷新,然后再刷新一下,可以了。然后传入一个module,然后得到一些信息,在这个里边我们之前提到了有一个比较重要的信息,特别容易忽视导致异常的。其实我们req.Header,这个里面它属于是网络请求头,它的Content-Type,我们要把它有意设置为raw这样的一种方式。这种方式可以避免我们前面所提到的是47001这样的一种错误。如果你遇到这个错误的话,你就把这个请求头给它设置一下,然后就可以避免了。同时我们这个里边它还有一个是关于这个时间就是这个时间我们前面提到了它其实是秒,然后这个时间如果设置不对的话,它这个也不会正常的返回,基本上这两个对了以后接口就可以正常返回了,就可以取到我们想要的性能数据了,代码演示我们就到这里。 下面我们看实践三,新建监控ID,使用后台的小程序测速功能。 最后我们看一下小程序后台的测速功能,这是一个自定义提交埋点数据的功能,首先我们要登录小程序后台,在开发,开发管理,运维中心,小程序测速页面里边添加一个监控指标,如屏幕上所示。所谓的监控指标其实是一个数字有,它从1000开始向上递增,指标名称是我们自己自定义的,这个名称是给我们自己看的。在数据上传的时候其实并不会用到,添加监控指标以后,回到小程序前端代码里面,我们就可以添加打点代码了,主要代码如屏幕上所示。测试代码添加以后,在微信开发者工具的模拟器中就可以进行测试了,大概15分钟以后,在后台就可以看到我们自己提交的指标数据了。登录小程序后台,在测速页面中还可以看到指标上报的次数。 下面我们看代码演示。 首先我们看一下最终实现的代码。在我们library services下面有一个retrieve_home_data.js,在这个地方我们有一个关于演示代码的,关于一段演示代码,可以将这个代码可以给它拷贝一下,然后回到我们当前的实战的一个目录下面找到相关的文件。这里,然后在这里我们需要找一个合适的位置,这个地方,演示监控,测试,测速。监控数据上报,把下面这些代码我们可以反注释一下,这个地方首先我们是得到了一个差值。这里还有一个fetchStartingTime,起始的一个时间,这个时间现在还没有,我们在原来这个地方再看一眼,这个是在这个地方开始的时候,然后把它给定义,最上面这个方法被调用的时候,然后去拿到这样一个时间,然后这个地方我们去计算一个差值,它本身是我们这个数据拉取。在开始拉取到最终成功拉取所需要的一个时间,然后这个地方会有一个关于接口的安全测试,如果我们能使用这个接口的话,我们就用reportPerformance接口,然后进行数据上报,1001是我们在后台已经定义好的监控ID,ms是我们要上传数据,这个代码就已经写好了,然后接下来我们再看一下,我们后台里边,登录小程序管理后台 在左边导航栏里面找到开发,开发管理,开发管理下面我们找到运维中心,下面有一个小程序测速,在这个页面的最下面有一个管理监控指标,这个地方我们可以单击新建去创建一个新的测速类型,然后这个地方我们有一个,之前已经创建的,监控ID是1001。它的描述是首页数据拉取耗时,这样就可以了。然后我们就可以测试我们的代码,我们单击编译按钮进行相关代码的一个测试,这个地方有一个监控上传拉取首页数据耗时510ms,这是我们监控的一个数据。数据上报以后,在我们打开我们小程序后端,在小程序测速页面我们就可以看到我们上传的数据了。但是注意一点,这个地方它有延迟,大概有15分钟的一个延迟,所以马上上报的数据并不能及时看到。下面它还有一个统计,关于我们每一个指标,它被上报的一个次数,以及所在的地区都是有统计的,这是这个页面它的主要的一个功能,代码演示我们就说到这里。 下面看一下小结,在小程序里边通过wx.getPerformance相关接口收集性能数据,这种方式因为会跟小程序竞争有限的设备资源,所以不被提倡,至少不建议在生产环境里边使用,只能在研发环境里面做测试使用。如果对性能数据的需求不高,直接使用小程序助手的性能分析就已经足够了。如果对性能数据的自定义需求比较高的话,可以在小程序后台自定义创建相关的测速指标,在Web后台通过小程序性能接口自行拉取性能数据,然后以自己自定义的方式进行呈现。 点击查看开放文档: 基础 /性能 /wx.getPerformance数据分析 /getPerformanceData接口调用凭证 /getAccessToken性能与体验 /小程序测速这节课我们就讲到这里,上面这些网址是本课涉及的文档地址。 这节课我们主要学习了如何通过接口获取小程序的性能数据,到现在为止课程的主要内容已经讲完了。下节课是总结,从整体上概括一下这个课程讲到的所有性能优化技巧。
2022-07-15 - 小程序性能优化实践
小程序性能优化课程基于实际开发场景,由资深开发者分享小程序性能优化的各项能力及应用实践,提升小程序性能表现,满足用户体验。
10-09 - ColorUI-高颜值,高效率的小程序组件库
[图片] 简介 hi!开发者!ColorUI迎来了2.0的升级,相比之前的版本,2.0版本重构了基础代码,增加了更多的配色,这是一个全新的小程序UI解决方案。 ColorUI是一个Css类的UI组件库!不是一个Js框架。相比于同类小程序组件库,ColorUI更注重于视觉交互! 项目为个人开源项目,如果项目有帮到你,希望能支持下开发者。 [图片] 截图 [图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片][图片] 使用 浏览GitHub:https://github.com/weilanwl/ColorUI/
2018-12-25 - 小程序内用户帐号登录规范调整和优化建议
为更好地保护用户隐私信息,优化用户体验,平台将会对小程序内的帐号登录功能进行规范。本公告所称“帐号登录功能”是指开发者在小程序内提供帐号登录功能,包括但不限于进行的手机号登录,getuserinfo形式登录、邮箱登录等形式。具体规范要求如下: 1.服务范围开放的小程序 对于用户注册流程是对外开放、无需验证特定范围用户,且注册后即可提供线上服务的小程序,不得在用户清楚知悉、了解小程序的功能之前,要求用户进行帐号登录。 包括但不限于打开小程序后立即跳转提示登录或打开小程序后立即强制弹窗要求登录,都属于违反上述要求的情况; 以下反面示例,在用户打开小程序后立刻弹出授权登录页; [图片] 建议修改为如下正面示例形式:在体验小程序功能后,用户主动点击登录按钮后触发登录流程,且为用户提供暂不登录选项。 [图片] 2.服务范围特定的小程序 对于客观上服务范围特定、未完全开放用户注册,需通过更多方式完成身份验证后才能提供服务的小程序,可以直接引导用户进行帐号登录。例如为学校系统、员工系统、社保卡信息系统等提供服务的小程序; 下图案例为正面示例:校友管理系统,符合规范要求。 [图片] 3.仅提供注册功能小程序 对于线上仅提供注册功能,其他服务均需以其他方式提供的小程序,可在说明要求使用帐号登录功能的原因后,引导用户进行帐号注册或帐号登录。如ETC注册申请、信用卡申请; 如下反面示例,用户在进入时未获取任何信息,首页直接强制弹框要求登录注册ETC,这是不符合规范的。 [图片] 建议修改为如下正面示例所示形式:允许在首页说明注册功能后,提供登录或注册按钮供用户主动选择点击登录。 [图片] 4.提供可取消或拒绝登录选项 任何小程序调用帐号登录功能,应当为用户清晰提供可取消或拒绝的选项按钮,不得以任何方式强制用户进行帐号登录。 如下图所示反面示例,到需要登录环节直接跳转登录页面,用户只能选择点击登录或退出小程序,这不符合登录规范要求。 [图片] 建议修改为下图正面示例形式,在需帐号登录的环节,为用户主动点击登录,并提供可取消按钮,不强制登录。 [图片] 针对以上登录规范要求,平台希望开发者们能相应地调整小程序的帐号登录功能。如未满足登录规范要求,从2019年9月1日开始,平台将会在后续的代码审核环节进行规则提示和修改要求反馈。
2019-07-20