- 微信开发者工具下载的 sourcemaps 怎么用。
什么是 Sourcemaps uglifyjs、bable 等工具会对 源代码 进行编译处理生成编译后的代码(下称目标代码),而 sourcemaps 就是保留了目标代码在源代码中的 位置信息 --------- 大神分割线 --------- 如何解读 Sourcemaps Sourcemaps 是一个 json [代码]{ "version": 3, "sources": ["a.js", "b.js"], // 源文件列表,这个表示是由 a.js 和 b.js 合并生成 "names": ["myFn", "test"], // 如果开启了变量名混淆,这里会保留变量名在源文件中名字信息 "sourcesContent: [], // 可选项,保存源码信息,顺序与 sources 字段对应,chrome 的 sources 面板中源码使用了这个字段的内容进行展示 "sourceRoot": "", // 源文件所在的目录信息 "file": "dist.js", // 可选,编译后的文件名 "mappings": "" // 这个是重点,是目标代码和源文件的位置的映射关系 } [代码] mappings 目标文件"行"的信息 mappings 是使用 ; 分隔的,每个部分对应目标代码的行 如: “;AAAA;AAAA,BBBB;;” 本例子目标文件有 4 行 第 0 行和第 3 行没有源文件对应信息,所以这两行是编译过程中加入的代码 目标文件的"列"信息 如: “AAAA,CAEA,CAEA;” ‘,’ 表示行内的位置信息分隔符 本例表示目标文件的这一行有三个有效的位置信息。 位置信息的第一位表示目标文件的列的 偏移 信息 本例中,表示列的信息是 ‘A’、‘C’、‘C’,对应的数字为 0、+1、+1,(vlq 编码,在线编解码工具) 注意,这个是偏移信息; 列数从 0 开始,依次累加偏移值可以算出当前的位置信息对应的真正的列 所以本例中表示的是目标文件的第 n 行中的第 0 列,第 1 列,第 2 列(没错是第 2 列) 源文件的信息 如:‘AAAA;ACAA;ADAA;’ 位置信息的第二位表示源文件的信息,本例子中是 ‘A’、‘C’、‘D’,对应数字是 0、+1、-1 如果 sourcemaps 中的 sources 字段只有一个文件的话,那么位置信息中第二位一直是 A(不需要偏移) 假设 sourcemaps 中 sources: [‘a.js’, ‘b.js’] 本例的意思是 AAAA: 目标文件第 0 行第 0 列 对应 第 0 个文件 a.js ACAA; 目标文件第 1 行第 0 列 对应 第 1 个文件 b.js ADAA; 目标文件第 2 行第 0 列 对了 第 0 个文件 a.js (偏移是 -1 又回到了 a.js) 源文件的行信息 位置信息的第三位表示源文件中的行的信息, 理解了位置偏移的概念,我们很容易理解 如:‘AACA,CACA;AACA;‘ 那么 AACA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 1 行 CACA: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 1+1 行 AACA:目标文件的第 1 行第 0 列 对应 第 0 个文件的第 1 行 (注意:’;’ 后的行列偏移信息归 0) 源文件中的列信息 位置信息的第四位表示源文件中的列的信息 如:'AAAA,CAAC;' 那么 AAAA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 0 行第 0 列 CAAC: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 0 行第 0+1 列 位置信息的第五位 第五位表示变量的偏移,对应 sourcemaps 中的 names 字段,表示目标文件中的变量名对应域源文件中的变量 如:’AAAA,CAACC;AAAAD;' sourcemaps 中 names 字段是 [‘a’, ‘b’] 那么 AAAA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 0 行第 0 列,没有变量的信息 CAACC: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 0 行第 0+1 列,有变量信息,变量在源文件中是 ‘b’ (0+1=1) AAAAD: 目标文件的第 1 行第 0 列 对应 第 0 个文件的第 0 行第 0 列,有变量信息,变量在源文件中是 ‘a’ (1-1=0) --------- 大神分割线 --------- 怎么使用 Sourcemaps Q: 线上小程序报错,我怎么通过 sourcemaps 还原到源代码中? A: 如报错 appservice.js 1:15000, 表示目标文件第一行 第 15000 列位置报错。根据上文介绍的,通过 mappings 字段算。 Q: 不会。 A: 如果你会写代码的话,参考下边 [代码]import fs = require('fs') import {SourceMapConsumer} from 'source-map' async function originalPositionFor(line, column) { const sourceMapFilePath = '如果你不真的替换的成 sourcemaps 在硬盘中的位置,那你还是放弃自己写代码吧。 ' const sourceMapConsumer = await new SourceMapConsumer(JSON.parse(fs.readFileSync(sourceMapFilePath, 'utf8'))) return sourceMapConsumer.originalPositionFor({ line, column, }) } originalPositionFor(出错的行,出错的列) [代码] Q: 不会写代码 A: 下载最新版的开发者工具,菜单-设置-拓展设置-调试器插件 [图片] [图片] Q: 为啥都是 null? A: 每个小程序版本都应该对应一个sourcemap文件。 运营中心那里下载的 sourcemap 是对应线上最新的小程序版本。但运营中心的报错集合了多个小程序版本。拿旧小程序版本的报错信息,和最新版本的 sourcemap,是匹配不出的。开发者工具和ci 上传的时候,会提示下载对应版本的 sourcemap 信息,可以自助保存。 [图片] Q: 怎么确定有没有版本对应上 A: 下载的 sourcemap 中有个 wx 字段,标明了该 sourcemap 文件对应小程序版本号。 [图片] [图片] 前提 1.确保发生错误的小程序版本和下载回来的 sourcemap 版本是一致的。 a. 下载 sourceMap 文件,可在 mp 后台或开发者工具上传成功弹窗下载 2.确保 map 文件和发生错误的 js 文件是对应的。sourcemap 的目录和文件说明 a. APP 是主包,FULL 是整包(仅在不支持分包的低版本微信中使用),其他目录是分包 b. 每个分包下都有对应的 app-service.js.map 文件。 c. 如果是使用了按需注入特性(app.json中配置了lazyCodeLoading),那么每个分包下还会有 appservice.app.js.map(对应分包下非页面的js),和所有页面的 xxx.js.map 以上事情都确保正确之后,还是出现行列号匹配不出来的情况。那就需要进一步排查。 线上运行的小程序 sourcemap 文件是怎么生成的? 处理流程:源码 [ a.js a.js.map b.js b.js.map ] -> 开发者工具(JS转 ES5,压缩)-> 微信后台(合并 js 文件)[ appservice.app.js appservice.app.js.map]。 注意:如果源码在交给工具之前是经过了 webpack 等打包工具的处理,那源码这里需要有 map 文件。否则不需要存在 map 文件。 可以看出,map 文件经过三个步骤的处理,每个步骤都有可能导致出错,因此开发者需要先排查,是否是前两个步骤出错导致的 map 文件失效的。 如何排查前两个步骤产生的 map 文件是否有问题。 1.排查 a.js.map 文件是否有问题。 a. 可以在 a.js 的代码中写一下 throw new Error(‘test sourcemap’)。 b. 使用了 webpack 的情况下,要构建为生产环境的版本。 c. 在开发者工具模拟器中运行对应的页面,看看控制台中的报错,错误行列号是否能正常映射到源文件。 2.排查 开发者工具(JS转 ES5,压缩)步骤是否有问题。 在排查完第一步的基础上,点击预览,用微信上扫码预览,并打开调试 vConsole 功能,检查 vConsole 中是否有报错信息,检查报错信息中的行列号是否能正常映射到源文件。 如何排查 微信后台(合并 js 文件)是否有问题。 a. 一定要先排查完前两个步骤再来排查这一步,一般情况下,这一步是不会出错的。 b. 如果有问题,也只会导致 map 文件中的行号信息出现偏移。比如 Error 信息中显示报错地址是 100: 200,行号是 100。那么你可能直接用 100: 200 在 map 文件中搜索不出信息,但是如果 用 150: 200 就可以搜索出来,说明行号偏移了 50。那其他报错也可以偏移 50 后再进行搜索就找到结果。 c. 怎么排查偏移了多少?可以结合 error.message 的内容,初步判断大概错误的内容是什么。把对应的 map 文件放到这个网站上 source-map-visualization 进行搜索,找出哪些相同列号的地方。再结合 error.message 的内容进行判断。 d. 如果排查到是这一步导致的问题,请在社区上联系我们,我们会在后续版本进行修复。 依旧排查不出原因? 先整理一下按照上述步骤排查的结论,再在社区上联系我们协助
2023-02-10 - 一次假期故障引发的性能优化思考
在假期某个夜黑风高的晚上,商家正在直播间如火如荼的做着直播,突然间屏幕卡顿,随后屏幕上出现大大的“404”,紧接着大量的客诉、告警扑面而来。好在有赞教育的技术团队响应及时,再经过很短时间的问题分析后,迅速的恢复了系统,保障了商家直播顺利进行。这故障到底是怎么产生的呢?经排查是因为在流量高峰时,系统在性能、可用性方面存在不足导致的。那当时你们是怎么处理的呢?接下来,我会重点从性能优化这块出发,先普及下性能优化的基本概念,然后再简述下常用的性能优化手段,最后给出这个故障我们当时的应对之道。 一、什么是性能优化正如熵增定律描述的那样,在一个孤立的系统里,如果没有外力做工,其总混乱度(即熵)会不断增大,直至系统彻底变得无序。在软件服务领域亦是如此:从应用系统上线那一刻开始,随着用户量的增加、业务功能的持续迭代,系统会面临各种不同程度的挑战,如果不及时采取优化措施,我们会发现诸多问题,比如:系统怎么越来越慢了,流量一高系统就卡顿、甚至宕机等等。可以说,性能优化是贯穿在整个软件生命周期之中的。 1.1 性能衡量指标在衡量系统性能基线时,一般会从接口“响应时间”和“并发能力”两个维度考虑。 (1)响应时间(RT) 所谓响应时间,是指完成某一功能所需要的时间,一般可以通过“平均响应时间”、“百分位数”等指标来考量。 平均响应时间(AVG)该指标反映的是接口的平均处理能力,计算方式是:将接口请求所有的响应时间叠加起来,然后除以总的请求次数。举个例子:接口A总共发起了8次请求,其中,有1次3ms,3次5ms,4次6ms,那么此接口的平均响应时间就是 (1 * 3 + 3 * 5 + 4 * 6) / 8 = 5.25ms 。 百分位数(Top Percentile)一种统计学术语,反映的是超过n%的请求都在m时间内返回,一般用TPn=m来描述,比如:TP99=5,表示超过99%的请求都能在5ms内返回。它的计算方式是:将接口的响应时间按从小到大的顺序进行排列,取特定百分位的耗时,即为该接口的百分位数。举个例子:接口A总共发起了100次请求,响应时间依次是1、2、3、...、100,那么,TP95就是95ms。 一般而言,百分位数更能反映接口的整体响应情况,因为在高并发场景中,常常会出现一些长尾请求,如果采用平均响应时间去衡量,由于长尾请求会被大量低RT平均掉(此时很多用户的请求已经很慢了),进而无法及时感知真实业务状况。举个例子:接口A有100次请求,其中97次1ms,3次100ms,平均响应时间为 (1 * 97 + 3 * 100) / 100 = 3.97ms,此时3.97ms并不能真实反映接口的整体性能,因为其中97次请求RT才1ms。 (2)并发能力 并发能力一般用QPS或TPS来衡量。QPS指的是每秒请求数,TPS是指每秒事务数。一般在做性能评估时,TPS用的比较多。 1.2 性能优化本质在算法领域,评价一个算法的效率如何,主要会看它的时间复杂度和空间复杂度情况。同理,如果将“响应时间”比作时间维度的话,“并发能力”可类比为空间维度。那么,在做性能优化时,本质上也是从“优化时间”、“优化空间”、“时空互换(用时间换空间或用空间换时间)”三个方向去思考,然后在空间、时间上不停地做取舍。 举一个生活中的例子来说明下。图1-1是一条长度为5km的道路,道路的限速是50km/h,同时,规定在任何时刻,车道上有且仅有一辆汽车。那么,在1h内,从A点出发到达B点的汽车最多只有10辆。假设上级部门想提升这块路段的车流量,我们该怎么办? [图片] 图1-1 单车道限速50km/h 第一种方式,可以增加车道数(空间维度):将道路从单车道变为多车道,比如增加到6车道,那么,在1h内,从A点出发到达B点的汽车数可提量到60辆,见图1-2。 [图片] 图1-2 六车道限速50km/h 第二种方式,道路提速(时间维度):将道路限速从50km/h提升到100km/h,那么,在1h内,从A点出发到达B点的汽车数可提量到20辆,见图1-3。 [图片] 图1-3 单车道限速100km/h 二、怎么做性能优化 2.1 系统性思考性能优化点我们先来看下性能优化的落地过程,性能优化是由人来执行,然后服务于产品的,人和产品共同参与性能优化的落地,见图2-1。 [图片] 图2-1 性能优化落地过程 从人维度出发,性能优化是属于技术团队的,技术团队包括开发、测试和运维,其中,运维负责提供一些监控数据,测试负责提供一些压测数据,开发基于压测、监控数据,明确具体的优化点以及优化手段;从产品维度出发,性能优化是业务功能的一部分,是为了满足某些业务场景。于是,在做性能优化时,一般会考虑以下几点: (1)本次性能优化的业务场景是什么,有哪些场景需要优化; (2)这些场景的运维监控数据、测试压测数据是什么,要优化哪里; (3)这些数据里面反映的系统瓶颈在哪里,如何去优化; (4)重复(2)、(3)过程,直至满足优化目标。 结合性能优化的本质,整个优化过程其实就是:先从业务需求角度出发,思考待优化场景是否值得投入,比如:一个任务每次需要跑半小时,从技术层面,可以做下优化,但结合业务情况却发现,此任务的执行频次是每周一次,如果优化此场景需要耗费较大人力,那么,这个投入就是不值得的;然后再从技术实现角度出发,不停地去思考怎么优化时间、怎么优化空间、怎么牺牲空间换时间、怎么牺牲时间换空间等问题。总之,我们在做性能优化时,需要以一个更全面的视角去看待它,避免进入头痛医头脚痛医脚的误区。 2.2 常见性能优化方式在实际业务场景中,一个外部请求进入系统后,会先后经历多个软硬件节点,所有节点的处理时间加起来才是用户请求的处理时间,如果其中任意一个节点性能有问题,系统整体的性能就会上不去。而且,由于节点自身差异性,其性能提升的方法也会不一样,但总体概括起来,可以分为两大类:提升单个请求处理效率;并行处理多个请求。 2.2.1 提升单个请求处理效率这种方式,简单来说,就是一个外部请求进来后,让其在尽可能短的时间内处理完成。常见的方法有以下几种: (1)提升调用链上各节点的处理速度 从技术角度考虑:在数据库层面,可以考虑加索引、读写分离、分库分表等;在应用层层面,可以考虑加缓存(本地缓存,分布式缓存,或两者叠加)、复杂查询走ES索引;在代码编写时,可以考虑更高效的算法和数据结构,比如:读多写少用数组、写多读少用链表、取余采用位运算等。 从业务角度考虑:尽量避免重复查询;对于一些查询类操作,尽可能采用批量查询;上游调用方尽可能使用更合适的下游接口,比如:下游服务方有分别返回A、B、AB的三类接口,如果上游使用方仅需要A信息,应使用A接口;如果同时需要AB信息,应使用AB接口,而不是依次调用A、B接口,再在内存中做聚合。 (2)请求内部做并行化处理 这种思想,就是将单个请求拆分为多个子请求,各子请求并行处理,最后对子请求结果合并后返回。在实践中,我们基于 CompletableFuture 实现了一套并行处理框架,并成功运用到了商品详情页加载场景中。 (3)请求处理异步化 此思想,最典型的方法是采用消息队列,比如:下单操作时,除了扣减库存、生成订单外,还会给用户发送支付成功消息、赠送积分等后置操作。对于这些非核心的后置流程,可以采用消息队列做异步化处理,以此提升下单接口的性能。其他一些方法还有:在进程内,另开一个线程执行这些非核心流程;或者先将非核心操作数据暂存在某种介质(DB表、redis等)中,然后采用定时任务定期扫描并执行这些操作。 2.2.2 并行处理多个请求字面意思来看,就是当有多个外部请求进来时,可以让系统内部多个节点分别处理这些请求,或者节点内部做并行处理。比如:节点采用集群部署,并通过负载均衡策略,将用户请求分摊到不同的节点进行处理;节点内部采用线程池,通过另开线程来实现。 三、我们是怎么做的在具体讲述之前,先带大家一起熟悉下当时的业务场景:用户首先访问直播商品详情页,然后购买此商品,紧接着再次访问详情页面时,会出现直播间入口,在进入直播间之前,会做一次权限校验,校验通过后,方才可以进入直播间与讲师进行互动。详细的流程可见图3-1 。 [图片] 图3-1 直播间进入流程 从上述流程中,可清晰看出,主要涉及到三种外部请求:查询直播商品详情;商品下单;进入直播间前做用户权限校验。当时通过流量监控数据以及日志分析发现,性能瓶颈主要在“直播商品详情加载”这一环节。 直播商详这块,主要是因为上游服务的请求量超过了下游服务能承受的吞吐量,导致大量RPC调用超时。具体反应的问题点有: (1)依赖的部分非核心接口没有加缓存、做降级,导致整个请求失败; (2)依赖的部分核心接口性能较差,导致后续请求一直被阻塞,直至超时异常返回; (3)下游服务提供的查询接口比较重量级,但上游服务仅需要返参中的部分字段,导致单次查询RT一直下不去; (4)上游调用方使用了错误的下游接口,比如上游调用方本来可以调用一次详细信息查询接口,便能获取所有需要的信息,可实际中,却先后调用了两种查信息的接口,才拿到完整的信息; (5)无状态查询接口没有加缓存,导致了频繁的RPC调用。 针对上述这些问题点,我们当时主要从以下几点去做了优化: 优化前,我们重新梳理了整个调用链上,接口的强弱依赖关系,以及每个接口的RT情况(1) 针对弱依赖接口,从超时时间、缓存策略、降级策略三个层面进行了优化 RPC调用超时时间设置策略统计出弱依赖接口 TP99(RT较稳定的接口)/ TP95 (RT波动较大接口)的RT,设置它们的超时时间为 (1 + 50%) (TP99 或 TP95) 这里讲下为什么要这样设置超时时间:一般我们会设置超时时间为2s或3s,但每个接口的RT是不一样的,比如:接口A的RT稳定在100ms内,那么,如果超时时间是2s,假若接口A超时了,本次RT至少是2s,但如果超时时间设置为100ms,且我们加了1次重试,那么,本次请求的RT不会超过200ms,同时,重试时接口很大概率会正常返回结果。缓存策略给接口添加前置缓存。我们采用了公司自研的分布式缓存zanKV,缓存的更新策略是:采用了两个缓存,缓存A和缓存B(缓存A的失效时间为m分钟,缓存B为n分钟,且n>2m),首先从缓存A读数据,有则直接返回,没有则从B读数据,并在返回之前,异步启动一个更新线程,同时更新缓存A和缓存B。 降级策略接口接入熔断降级机制,并对异常做捕获,返回默认值。 (2)针对强依赖接口,从超时时间、重试策略、缓存策略三个层面做了优化 RPC调用超时时间设置策略统计出强依赖接口 TP99 的RT,设置它们的超时时间为 (1 + 50%) (TP99) 重试策略根据接口RT波动性,基于dubbo的重试机制,设置重试次数为2或3次。 缓存策略对于商品基础信息,考虑到“缓存预热”、“热点访问”等问题,接入了公司TMC(透明多级缓存),具体说明可见文档 https://mp.weixin.qq.com/s/BnWtbetNq076iRRZfnGRrw ;对于其他一些无状态查询信息,采用了本地缓存Guava。 (3)商品详情信息聚合操作并行化 商品详情页面是一个聚合类信息展示窗口,它除了商品基础信息外,还包括A、B、C等内容(出于商业保密性,这里泛化内容名称),且这里的A、B、C和商品基础信息四者间是没有任何前后依赖关系的。当时我们将商品详情加载拆分为了4个子任务,并采用教育后端团队自研的并行处理框架,对子任务做了并行化处理,并聚合返回,较大提升了接口RT性能。 (4)查询类接口能力收拢,下游服务方提供稳定的原子化接口 在问题点(3)、(4)中有提到,上游调用方使用了下游不太合适的接口。由于历史原因,当前下游服务方中有特别多的查询类接口,且很多查询类接口在功能上都是重叠的。本次我们针对查询类接口,按照其返参字段使用场景的不同,提供了三种不同粒度的通用类原子化接口,之后所有的查询类需求,都会强制要求上游调用方从这三类接口中选择。这三类接口如下: 粗粒度:返回最基本字段中粒度:返回经常使用的字段细粒度:返回详细信息四、总结产品功能是持续迭代的,性能优化也不是一蹴而就的事,大家在遇到性能问题时,可以参考本文提到的一些方法,做一些针对性的优化。同时,针对同一个节点,在不同的时刻,其优化点也可能不一样,比如:新功能刚上线时,查询性能的提升可能仅仅通过加索引的方式便能解决,但随着功能的不断叠加,后续的优化方向可能是“尽量走批量查询”、“加缓存”等方向。所以,性能优化还是要遵循“具体案例具体分析”这一基本原则。鉴于作者经验有限,我对性能优化的理解难免会有不足之处,欢迎大家共同探讨,共同提高。
2020-10-13 - 小程序架构设计(一)
在微信早期,我们内部就有这样的诉求,在微信打开的H5可以调用到微信原生一些能力,例如公众号文章里可以打开公众号的Profile页。所以早期微信提供了Webview到原生的通信机制,在Webview里注入JSBridge的接口,使得H5可以通过它调用到原生能力。 [图片] 我们可以通过JSBridge微信预览图片的功能: [代码]WeixinJSBridge.invoke('imagePreview', { current: https://img1.gtimg.com/1.jpg', urls: [ 'https://img1.gtimg.com/1.jpg', 'https://img1.gtimg.com/2.jpg', 'https://img1.gtimg.com/3.jpg' ] }) [代码] 早期微信官方是没有暴露这些接口的,都是腾讯内部业务在使用,很多外部开发者发现后,就依葫芦画瓢地使用了。 从另外一个角度看,JSBridge是微信和H5的通信协议,有一些能力可能要组合不同的能力才能完整调用。如果我们直接开放这套API,相当于所有开发者都要直接理解这样的接口协议,显然是很不合理的。 所以在2015年初的时候,微信就发布了JSSDK,其实就是隐藏了内部一些细节,包装了几十个API给到上层业务直接调用。 [图片] 前边的代码就变成了: [代码]wx.previewImage({ current: https://img1.gtimg.com/1.jpg', urls: [ 'https://img1.gtimg.com/1.jpg', 'https://img1.gtimg.com/2.jpg', 'https://img1.gtimg.com/3.jpg' ] }) [代码] 开发者可以用JSSDK来调用微信的能力,来完成一些以前H5做不到或者难以做到的事情。 能力上得到了更多的支持,但是微信里的H5体验却没有改善。 第一点是加载H5时的白屏。在微信里打开链接后会看到白屏,有一些H5的服务不稳定,这个白屏现象会更严重。 [图片] 第二点是在H5跳转到其他页面时,切换的效果也很不流畅,只能看到顶部绿色进度条在走。 [图片] 随着JSSDK的开放,还出现了更不好对付的问题。 微信上越来越多干坏事的人,有人做假红包,有人诱导分享,有伪造一些官方活动。他们会利用JSSDK的分享能力变相的去裂变分享到各个群或者朋友圈,由于JSSDK是根据域名来赋予api权限的,运营人员封了一个域名后,他们立马用别的域名又继续做坏,要知道注册一个新的域名的成本是很低的。 [图片] [图片] [图片] 龙哥在2016年微信公开课上提出了应用号的概念,我们要重新设计一个新的移动应用开发模式,同时我们要解决刚刚提到的一些问题。 至此,我们回顾一下目前移动应用开发的一些特点: Web开发的门槛比较低,而App开发门槛偏高而且需要考虑iOS和安卓多个平台; 刚刚说到H5会有白屏和页面切换不流畅的问题,原生App的体验就很好了; H5最大的优点是随时可以上线更新,但是App的更新就比较慢,需要审核上架,还需要用户主动安装更新。 我们更想要的一种开发模式应该是要满足一下几点: 像H5一样开发门槛低; 体验一定要好,要尽可能的接近原生App体验; 让开发者可以云端更新,而且我们平台要可以管控。 很多人可能会第一时间想到Facebook的React Native(下边简称RN),是不是RN就能解决这些问题呢? 是的,React Native貌似可以解决刚刚那些问题,我们也曾经想用RN来做。但是仔细分析了一下,我们发现了采用RN这个机制做开放平台还是存在一些问题。 RN只支持CSS的子集,作为一个开放的生态,我们还要告诉外边千千万万的开发者,哪些CSS属性能用,哪些不能用; RN本身存在一些问题,这些依赖RN的修复,同时这样就变成太过依赖客户端发版本去解决开发者那边的Bug,这样修复周期太长。 RN前阵子还搞出了一个Lisence问题,对我们来说也是存在隐患的。 [图片] 所以我们舍弃了这样的方案,我们改用了Hybrid的方式。简单点说,就是把H5所有代码打包,一次性Load到本地再打开。这样的好处是我们可以用一种近似Web的方式来开发,同时在体验上也可以做到不错的效果,并且也是可以做到云端更新的。 [图片] 现在留给我们的最后一个问题就是,平台的管控问题。 怎么理解呢?我们知道H5的界面结构是用HTML进行描述,浏览器进行一系列的解析最终绘制在界面上。 [图片] 同时浏览器提供了可以操作界面的DOM API,开发者可以用这些API进行一些界面上的变动,从而实现UI交互。 [图片] 既然我们要采用Web+离线包的方式,那我们要解决前边说过的安全问题,我们就要禁用掉很多危险的HTML标签,还要禁用掉一些API,我们要一直维护这样的白名单或者黑名单,实现成本太高了,而且未来浏览器内核一旦更新,对我们来说都是很大的安全隐患。 [图片] 这就是小程序一开始遇到的问题,在下篇文章《小程序架构设计(二)》,我们再详细展开一下小程序是如何解决以上这个问题的。
2019-02-26 - vConsole字段详解
[图片] request是代表什么的时间,接口请求吗? response是代表什么时间,接口响应时间吗? domComplete total 有js运行时间吗?
2019-07-02 - 【优化】解决swiper渲染很多图片时的卡顿
最近再用swiper,当有超过20个SwiperItem就会出现卡顿。 查看了各种资料 比如 https://developers.weixin.qq.com/community/develop/doc/000068ff25ccf0bae4e76eab156c04 这个方案不错,但是切换的时候会出现一个突然的跳跃,因为swiper的current会变。 经过我自己很多尝试,发现一个完美解决方案。 核心思想就是把没有显示出来的dom元素尽量简化 比如用空 <SwiperItem/> 代替 const ITEM_LIMIMT = 3; <Swiper current ={currentIndex} onChange={onChangeSwiper} displayMultipleItems={1} duration={300} > {items.map((item: any, index: number) => currentIndex < index + ITEM_LIMIMT && currentIndex > index - ITEM_LIMIMT ? ( <SwiperItem key={index}> <ComplexComponent item={item}/> </SwiperItem> ) : ( /* 没有显示出来的元素用空SwiperItem 代替*/ <SwiperItem key={index} /> ), )} </Swiper>
2020-05-28 - 关于 swiper 中 数据过多时 滑动卡顿掉帧的解决办法
[图片] 做小程序的时候 使用到了 swiper, 大概15个 <swiper-item> 每个<swiper-item>中是一个展示列表大概100条数据. 发现滑动的时候非常的卡顿掉帧, 苹果手机稍微好点但是也掉帧. 我的解决方式是: 定义2个数组 第一个数组存放所有数据 allList 第二个数组 存放 15个空数组 用于 <swiper-item>循环 list: [[],[],[],[]...] 然后获取当前页 下一页: 当前页+1, 上一页:当前页-1 如果当前页的索引为 0 (说明是第一页) 则上一页为最后一页 = list.length -1 如果当前页的索引为 list.length-1 (说明是最后一页) 则下一页为第一页 = 0 这样判断, 就可以拿到了 上一页 当前页 下一页的索引 然后每次切换页面 动态改变list { list [上一页索引] = allList [上一页索引] list [当前页索引] = allList [当前页索引] list [下一页索引] = allList [下一页索引] 可选: 可以选择清空之前的数据 例如 清空上上页的数据 这样就可以一直维持 只有3个数组中有数据 swiper就不会卡顿了, 也可以保留达到缓存的效果 } 这样就每次渲染其实只有3个数组中有数据 别的 <swiper-item>为空 就会流畅很多 如果表达不清晰 见谅
2020-06-15 - 微信小程序setData源码分析
背景 setData 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。详见官网描述 常见的 setData 操作错误 频繁的去 setData 每次 setData 都传递大量新数据 后台态页面进行 setData 针对第二点官网给出意见是,其中 key 可以以数据路径的形式给出,支持改变数组中的某一项或对象的某个属性,如 array[2].message,a.b.c.d,并且不需要在 this.data 中预先定义 下面通过源码深入分析的方式了解小程序是怎么针对数据路径进行组装和构造数据 小程序逻辑层框架源码 微信小程序运行在三端:iOS(iPhone/iPad)、Android 和 用于调试的开发者工具。在开发工具上,小程序逻辑层的 javascript 代码是运行在 NW.js 中,视图层是由 Chromium 60 Webview 来渲染的。这里简单点就直接通过开发者工具来查找源码。 在微信开发者工具中,编译运行你的小程序项目,然后打开控制台,输入 document 并回车,就可以看到小程序运行时,WebView 加载的完整的 WAPageFrame.html,如下图: [图片] 可以看到[代码]./__dev__/WAService.js[代码]这个库就小程序逻辑层基础库,提供逻辑层基础的 API 能力 查找WAService.js源码 在微信小程序 IDE 控制台输入 openVendor 命令,可以打开微信小程序开发工具的资源目录 [图片] 我们可以看到小程序各版本的运行时包 .wxvpkg。.wxvpkg 文件可以使用 wechat-app-unpack 解开,解开后里面就是[代码]WAService.js[代码] 和 [代码]WAWebView.js[代码] 等代码 [图片] 另外也可以只直接通过开发者工具的Sources面板查找到WAService.js的源码 [图片] 分析setData源码 在WAService.js中全局查找setData方法,找到定义此方法的地方,如下 [图片] 源代码使用了大量的逗号运算符,逗号运算符的优先级是最低的,比条件选择符还低 大量使用void 0 表示undefined setData函数定义中添加了关键的注释如下: [代码]function(c, e) { // 保存闭包内的this对象,即常用的that var u = this; // 官网定义 Page.prototype.setData(Object data, Function callback), // 即 c: Object对象,e: Function界面更新渲染完毕后的回调函数 try { // 返回 [object Object] 中的Object var t = v(c); if ("Object" !== t) return void E("类型错误", "setData accepts an Object rather than some " + t); Object.keys(c).forEach(function(e) { // e: 可枚举属性的键值, void 0 表示undefined (https://github.com/lessfish/underscore-analysis/issues/1) void 0 === c[e] && E("Page setData warning", 'Setting data field "' + e + '" to undefined is invalid.'); // t为包含子对象属性名的属性数组, u.data和u.__viewData__都是page.data的深拷贝副本 var t = N(e) , n = j(u.data, t) , r = n.obj , o = n.key; if (r && (r[o] = y(c[e])), void 0 !== c[e]) { var i = j(u.__viewData__, t) , a = i.obj , s = i.key; a && (a[s] = y(c[e])) } }), __appServiceSDK__.traceBeginEvent("Framework", "DataEmitter::emit"), this.__wxComponentInst__.setData(JSON.parse(JSON.stringify(c)), e), __appServiceSDK__.traceEndEvent() } catch (e) { k(e) } } [代码] 关键函数N(e),解析属性名(包含.和[]等数据路径符号),返回相应的层级数组,如 [代码]{abc: 1}中abc属性名 => [abc], {a.b.c: 1}中'a.b.c'属性 => [a,b,c], {"array[0].text": 1} => [array, 0, text][代码] 关键的注释如下 [代码]function N(e) { // 如果属性名不是String字符串就抛出异常 if ("String" !== v(e)) throw E("数据路径错误", "Path must be a string"), new M("Path must be a string"); for (var t = e.length, n = [], r = "", o = 0, i = !1, a = !1, s = 0; s < t; s++) { var c = e[s]; if ("\\" === c) // 如果属性名中包含\\. \\[ \\] 三个转义属性字符就将. [ ]三个字符单独拼接到字符串r中保存,否则就拼接\\ s + 1 < t && ("." === e[s + 1] || "[" === e[s + 1] || "]" === e[s + 1]) ? (r += e[s + 1], s++) : r += "\\"; else if ("." === c) // 遇到.字符并且r字符串非空时,就将r保存到n数组中并清空r; 目的是将{ a.b.c.d: 1 }中的链式属性名分开,保存到数组n中,如[a,b,c,] r && (n.push(r), r = ""); else if ("[" === c) { // 遇到[字符并且r字符串非空时,就将r保存到n数组中并清空r;目的是将{ array[11]: 1 }中的数组属性名保存到数组n中,如[array,] // 如果此时[为属性名的第一个字符就报错,也就是说属性名不能直接为访问器, 如{ [11]: 1} if (r && (n.push(r), r = ""), 0 === n.length) throw E("数据路径错误", "Path can not start with []: " + e), new M("Path can not start with []: " + e); // a赋值为true, i赋值为false i = !(a = !0) } else if ("]" === c) { if (!i) throw E("数据路径错误", "Must have number in []: " + e), new M("Must have number in []: " + e); // 遍历到{ array[11]: 1 }中的']'的时候,就将a赋值为false, 并将o保存到数组n中,如[array,11,] a = !1, n.push(o), o = 0 } else if (a) { if (c < "0" || "9" < c) throw E("数据路径错误", "Only number 0-9 could inside []: " + e), new M("Only number 0-9 could inside []: " + e); // 遍历到{ array[11]: 1 }中的'11'的时候,就将i赋值为true, 并将string类型的数字计算成Number类型保存到o中 i = !0, o = 10 * o + c.charCodeAt(0) - 48 } else r += c // 普通类型的字符就直接拼接到r中 } // 将普通的字符串属性名,.和]后面剩余的字符串保存到数组n中,如{abc: 1} => [abc], {a.b.c: 1} => [a,b,c], {array[0].text: 1} => [array, 0, text] if (r && n.push(r),0 === n.length) throw E("数据路径错误", "Path can not be empty"), new M("Path can not be empty"); return n } [代码] 关键函数j(e, t),解析出属性最终对应的子对象的属性名,以及对应的子对象 [代码]var x = Object.prototype.toString; function _(e) { return "[object Object]" === x.call(e) } function j(e, t) { // e: page.data的深拷贝副本, t为包含子对象属性名的属性数组 /* - 遍历属性数组[a,b], e={a: {b: 1}} 1. i=0, 此时o为Object类型时, n = a, r = {a: {b: 1}}, o = {b: 1}; 2. i=1, 此时o为Object类型时, n = b, r = {b: 1}, o = 1; retrun { obj: {b: 1}, key: b} - 遍历属性数组[a,0,b], e={a: [{b: 1}]} 1. i=0, 此时t[i]=a, o为Object类型时, n = a, r = {a: [{b: 1}]}, o = [{b: 1}]; 2. i=1, 此时t[i]=0, o为Array类型时, n = 0, r = [{b: 1}], o = {b: 1}; 3. i=2, 此时t[i]=b, o为Object类型时, n = b, r = {b: 1}, o = 1; retrun { obj: {b: 1}, key: b} */ for (var n, r = {}, o = e, i = 0; i < t.length; i++) Number(t[i]) === t[i] && t[i] % 1 == 0 ? // t[i]是否为有效的Number Array.isArray(o) || (r[n] = [], o = r[n]) : _(o) || (r[n] = {}, o = r[n]), n = t[i], o = (r = o)[t[i]]; //注意由于逗号分隔符的优先级是最低的,所以这一行会在前面的条件运算符执行完,再执行 return { obj: r, key: n } } [代码] 最后通过[代码]r && (r[o] = y(c[e]))[代码]的方式将新的值赋给匹配出的子对象的属性,这里j(e,t)函数内部是通过引用的方式向外传递出[代码]r[代码],所以这里改变[代码]r[o][代码]的值也会将[代码]u.data[代码]内部的值相应修改,完成局部刷新 由于不同的版本解包后,里面压缩之后的方法名称可能跟上面的对不上,但是大体的结构都是一样的 总结 官方提供的array[2].message,a.b.c.d方式就是通过解析成[array,2,message]和[a,b,c,d],找到相应的子结构进行复制操作,到达减少数据量的目的; 分页加载的时候,为了避免将整个list数据重新传输,就可以利用数据路径的方式只追加新的数据 [代码]假设原数组长度 length 为 10,新数组 newList 长度为 3 this.setData{ 'list[10]': newList[0], 'list[11]': newList[1], 'list[12]': newList[2], } [代码] 参考资料 微信小程序技术原理分析 小程序开发指南
2019-08-24 - recycle-view 商品长列表应用
recycle-view 在商品长列表中的应用 参考官方文档 WXML: [代码]<recycle-view class="list" batch="{{batchSetRecycleData}}" id="recycleId" bindscrolltolower="bindscrolltolower" scroll-y="true"> <recycle-item class="record" wx:for="{{recycleList}}" wx:key="id"> <view class="record_image" style="background:url( {{item.images[0] }}&x-oss-process=image/resize,s_320 )"></view> <view class="record_view">{{index}} . {{item.title}}</view> </recycle-item> <view slot="after">加载中...</view> </recycle-view> [代码] JS: [代码]const app = getApp() const createRecycleContext = require('miniprogram-recycle-view'); Page({ pageNum:1,//页码 listobj: Object,//RecycleContext对象 postflg:true,//是否可以加载列表,用户误触控制 windowWidth:0,//系统页面可视宽度 data: {}, onReady: function () { var than = this; //获取系统参数 wx.getSystemInfo({ success: function(res) { than.windowWidth = res.windowWidth; //创建RecycleContext对象来管理 recycle-view 定义的的数据 than.listobj = createRecycleContext({ id: 'recycleId', dataKey: 'recycleList', page: than, itemSize: than.itemSizeFunc, }) than.getlist();//请求接口 }, }) }, //设置item宽高信息,样式所设必须与之相同 itemSizeFunc: function (item, idx) { var than = this; return { width: than.windowWidth * 0.47, height: than.windowWidth * 0.61 } }, //滚动到底部监听,分页加载 bindscrolltolower(e) { console.log('滚动到底部----'); if(this.postflg){ this.postflg = false;//请求完成前不再更改页码请求接口 this.pageNum++; this.getlist(); } }, //数据请求 getlist(){ var than = this; wx.request({ url: 'https://w.taopaitang.com/api/discover?page=1&pagenum=10', data:{ page: than.pageNum, pagenum:10, }, method: 'get', success(res){ console.log('数据请求成功----' + than.pageNum +'---',res); if(res.data.message){ //append RecycleContext 对象提供的方法:在当前的长列表数据上追加list数据 than.listobj.append(res.data.data.items); than.postflg = true; } } }) } }) [代码] 全部代码 -------> 代码片段 <--------- 我想把此列表改成瀑布流展示,求路过的大神指点下!!!!!!
2019-10-07 - 一个奇葩思路实现的瀑布流双列布局
传统的瀑布流布局实现一般关键是去计算每一列的高度,从而判断下一个元素应该插入到哪一列(当然是最短的那列)。 这个奇葩思路没有任何计算,主要思路如下: 在瀑布流容器底部加入一根细线 利用微信小程序的IntersectionObserver,为每一列和细线添加监听 逐个加入要插入的item元素 根据监听相交变化结果判断下一个item应该插入哪一列(简单来说就是插入到当前不与细线相交的那一列,因为比较短) 这个思路实际上就是把计算高度换成了监听判断哪列更高,因此也不必知道每个元素的高度。 目前只能支持两列布局的情况,如果列数更多我没办法不通过计算来知道哪列最短,如果有思路或想法的童鞋欢迎交流~ 实现过程也比较简单,就分享个思路,不贴代码了(问就是懒!) 感兴趣的童鞋可以看代码片段,里面有完整的实现代码: https://developers.weixin.qq.com/s/nH5pg4mE78dG
2019-11-23