- 小程序页面预加载技术
让小程序在跳转新页面前就预加载服务器数据,并且保持你项目各个页面的业务单一性。
2018-08-08 - 部分鸿蒙系统的手机无法打开蓝牙,errno:3,system permission denied
部分鸿蒙系统的手机调用wx.openBluetoothAdapter失败,报错为{"errno":3,"errMsg":"openBluetoothAdapter:fail:system permission denied"},询问华为那边的客服,说现在鸿蒙手机没有蓝牙的权限设置。scene=1089&brand=HUAWEI&model=ALN-AL80&version=8.0.50&system=Android 12&SDKVersion=3.4.9&bluetoothEnabled=true&locationEnabled=true。根据日志显示bluetoothEnabled也是true。
07-08 - 小程序性能优化实践
小程序性能优化课程基于实际开发场景,由资深开发者分享小程序性能优化的各项能力及应用实践,提升小程序性能表现,满足用户体验。
10-09 - 了解小程序的启动流程(上)
[视频] *所有课程源码的链接: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 - 使用异步转同步的编程范式
[视频] 你好,我是李艺。 上节课我们主要学习了如何在小程序里面使用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 - 微信支付通知,支付账单显示“商家电话”,如何配置?
微信支付通知,支付账单显示“商家电话”,如何配置?[图片] [图片]
05-16 - 低功耗蓝牙跨页面如何监听数据变化?
首页使用低功耗蓝牙模块连接硬件,然后设置了wx.onBLECharacteristicValueChange来监听数据变更。然后我需要跳转其他页面,其他页面也需要监听数据变更。这种需求下,我在首页跳转页面时调用了wx.offBLEConnectionStateChange,然后新页面使用了wx.onBLECharacteristicValueChange,然后问题就出现了,新页面无法监听一直发送的蓝牙数据,我返回了首页就发现可以接收到一直发送的蓝牙数据。总的来说就是,新页面无法接收一直发送的蓝牙数据,不知道是不是被首页抢了!最后请问跨页面进行蓝牙监听如何实现?
2022-10-13 - 收下这份指南,小程序蓝牙应用不再难
在移动互联网全民化发展的当下,蓝牙技术凭借着简便、安全、低成本的优势逐渐成为无线技术领域的中流砥柱。在共享单车领域,电子设备和单车锁通过蓝牙技术形成可靠且安全的连接,从而提升用户体验。 微信团队一直致力于 小程序蓝牙能力 的应用开发,提升小程序在无线技术领域的高效、安全、可靠应用。通过 滴滴青桔小程序 等项目落地验证,小程序蓝牙能力的合理应用能够实现搜索成功率、连接成功率、开锁成功率的全面提升。 [图片] 当前主流的蓝牙技术主要是经典蓝牙 (BR / EDR) 和低功耗蓝牙 (BLE),其中 BLE 凭借着与电子设备的简便互操作性,成为大部分共享单车蓝牙锁的重要技术选型。 在实际应用场景,小程序蓝牙技术与蓝牙锁之间进行以下技术交互,实现解锁。 Step 1:蓝牙扫描 扫描蓝牙广播,检查 Android 系统直连模式是否连接正常。 Step 2:生成通信帧 与锁建立 BLE 连接后,准备获取锁的 Token 通信帧,将传递信息 AES 加密并转化成 HexString 发送。 Step 3:获取锁的 Token 将传递信息 AES 解密后收到 Token 获取成功通信帧,同时截取相应位数的字节作为令牌 Token,准备开始解锁。 Step 4:等待解锁 发送解锁信息通信帧,利用 Password 还有 Token 同样将传递信息 AES 加密并转化成 HexString 以一定的规则发送,等待解锁。 Step 5:解锁成功 收到蓝牙返回的信息 AES 解密后得到解锁通信帧代表解锁成功。 [图片] 通过 滴滴青桔小程序 等项目的实践探索,合理应用小程序蓝牙技术能够实现解锁全链路的优化提升—— 提前搜索时机提升蓝牙搜索成功率 蓝牙技术在搜索耗时的原生限制导致单车锁-电子设备之间的搜索成功率偏低。因此在不增加用户功耗的前提下,适当提前搜索时机能够减少搜索设备的耗时,提升蓝牙搜索成功率。 [图片] 默认直连模式提升蓝牙连接成功率 大部分的 Android 系统手机均支持直连模式,建议针对 Android 系统,默认使用直连模式,加速蓝牙连接流程。如果直连模式失效,则重新搜索,连接附近设备。 [图片] 建立失败重试机制提升蓝牙开锁成功率 低功耗蓝牙的原生不稳定性要求开发者做好重连逻辑。建议建立失败重试机制,即在每个 API 回调失败后,允许失败重试 3 次,用于提升开锁成功率。 [图片] 在小程序蓝牙技术应用过程中,总结以下应用注意点,助力开发者更好地应用蓝牙能力。 连接前 开启 notify 功能:连接前必须调用 notifyBLECharacteristicValueChange 才能接收到设备推送的信息,否则只能发送数据,无法接收数据。注意 iOS 与 Android 的蓝牙应用区别[图片] 连接中 停止搜索以减少性能消耗:调用 onBluetoothDeviceFound 搜索设备成功,必须执行 stopBluetoothDevicesDiscovery 停止搜索,避免后台资源消耗性能。不使用 Android 1800 / 1801服务:在获取设备服务时,部分 Android 机型增加系统自带的 00001800 和 00001801 服务,请不要使用这 2 项服务。重装微信获取其他设备服务:部分 Android 机型通过 getBLEDeviceServices 仅得到系统自带的 00001800 和 00001801 服务,重装微信即可获取其他设备服务。 连接后 及时关闭连接及蓝牙设备:操作完成后,及时关闭连接及蓝牙设备,否则在 Android 系统下,出现设备搜索失败的情况。成对调用创建和关闭 BLE 连接:建议成对调用 createBLEConnection 和 closeBLEConnection 接口,否则在 Android 系统下,重复调用 createBLEConnection 接口,可能导致系统持有同一设备多个连接的实例,最终导致调用 closeBLEConnection 不能真正断开与设备的连接。 合理应用小程序蓝牙技术能够丰富小程序的应用场景,提升小程序性能表现,进一步优化用户体验。 如有其他小程序应用相关的问题,可在 微信开放社区小程序交流专区 发帖互动,技术专员将为大家解答及进行深度交流。
2022-07-12 - 微信小程序使用performance api 完成对项目性能的收集
微信小程序是一种轻量级的应用程序,需要在有限的资源下提供高效的性能。使用Performance API可以帮助开发者更好地了解小程序的性能状况,以便识别和解决性能问题。本文将介绍如何使用Performance API来收集小程序的性能数据,包括小程序启动时间、页面加载时间和报错日志等。 小程序启动时间的收集 小程序启动时间是指从小程序启动到显示第一个页面所需的时间。为了收集小程序启动时间,可以在app.js文件中使用Performance API的mark()方法,在小程序启动时设置一个标记,如下所示: [代码]App({ onLaunch: function() { performance.mark("startApp"); // ... } }) [代码] 然后在app.js中设置的onShow()方法中使用Performance API的measure()方法来测量时间差,如下所示: [代码]App({ onShow: function() { performance.mark("endApp"); performance.measure("startupTime", "startApp", "endApp"); const entry = performance.getEntriesByName("startupTime")[0]; console.log(`小程序启动时间为:${entry.duration}毫秒`); } }) [代码] 页面加载时间的收集 页面加载时间是指从用户点击打开页面到页面完全加载完成所需的时间。为了收集页面加载时间,可以在每个页面的onLoad()方法中使用Performance API的mark()方法,在页面加载时设置一个标记,如下所示: [代码]Page({ onLoad: function() { performance.mark("startPageLoad"); // ... } }) [代码] 然后在每个页面的onShow()方法中使用Performance API的measure()方法来测量时间差,如下所示: [代码]Page({ onShow: function() { performance.mark("endPageLoad"); performance.measure("pageLoadTime", "startPageLoad", "endPageLoad"); const entry = performance.getEntriesByName("pageLoadTime")[0]; console.log(`页面加载时间为:${entry.duration}毫秒`); } }) [代码] 报错日志的收集 小程序中的报错日志可以通过Performance API的report()方法进行收集。在app.js文件中,可以使用wx.onError()方法来捕获小程序中的错误,并通过Performance API的report()方法将错误信息发送到指定的接口,如下所示: [代码]App({ onLaunch: function() { wx.onError(function(error) { performance.mark("error"); performance.measure("errorTime", "startApp", "error"); const entry = performance.getEntriesByName("errorTime")[0]; const errorLog = { message: error.message, stack: error.stack, duration: entry.duration }; performance.clearMarks(); performance.clearMeasures(); performance.clearResourceTimings(); performance.report(errorLog); }); } }) [代码] 总结 通过使用Performance API,开发者可以更好地了解小程序的性能状况,以便识别和解决性能问题。
2023-05-17 - 小程序端性能监控(1)
微信小程序前端性能监控(1) 本文分为两篇,认识与使用 performance,阅读各大约10分钟 01 打点: performance 是前端性能监控的API,小程序也实现了它。 首先我们回到一个古老的监控方法:Date.now(),用它打点行么? [图片] 图示:W3C提供的一段代码示例 回答当然是可以,那它与 performance 的区别在哪,为什么现在不推荐这种方式了? 首先就是精度问题,我们知道时间是个无穷小数,时间原点及精度取舍是不同的。有兴趣了解时间起源等更深入知识的小伙伴可以到文章结尾获取链接。 [图片] 图示:获取精度示例 最重要的是它提供了抓取各时间节点的API,定义了专用于测试的时间原点,及浮点数达到微秒级别的精确度等。Date.now() 时间戳可以衡量获取资源所需的时间,但是它不能分解页面加载在各个阶段花费的时间。此外,脚本无法轻松衡量获取标记中描述的资源所花费的时间。 大多系统运行一个守护程序定期同步时间,通常15-20min调整几毫秒,这个速率大约10S间隔的值会有1%的误差。而performence是恒定的速率,它定义了navigationStart,performance.timing.navigationStart + performance.now() 约等于 Date.now()。 02 基础API的认识: 在控制台打印 performance 如下: [图片] 图示:performance API memory 内存相关 jsHeapSizeLimit 内存大小的限制。 totalJSHeapSize 总内存的大小。 usedJSHeapSize 可使用的内存的大小。 注:如果 usedJSHeapSize 大于 totalJSHeapSize的话,那么就会出现内存泄露的问题,因此是不允许大于该值的。 navigation 页面的来源信息 redirectCount:如果有重定向,页面通过几次重定向跳转而来,默认为0 type:该值的含义表示的页面打开的方式。默认为0. 可取值为0、1、2、255这个值在实际项目中蛮实用,它可以判断来源页的跳转方法例如:手机触发返回,它的值为2 0:正常进入该页面(非刷新、非重定向)。 1:通过 window.location.reload 刷新的页面。如果我现在刷新下页面后,再来看该值就变成1了。 2:通过浏览器的前进、后退按钮进入的页面。如果我此时先前进下页面,再后退返回到该页面后,查看打印的值,发现变成2了。 255:非以上的方式进入页面的。 onresourcetimingbufferfull 回调函数 浏览器的资源时间性能缓冲区满了执行的回调 timeOrigin 时间戳它是一系列时间点的基准点,精确到万分之一毫秒。该值是动态的,刷新下,该值是会发生改变的。 timing 各时间点的集合 connectEnd:HTTP完成建立连接的时间(完成握手)。如果是持久链接的话,该值则和fetchStart值相同,如果在传输层发生了错误且需要重新建立连接的话,那么在这里显示的是新建立的链接完成时间。 connectStart:HTTP 开始建立连接的时间,如果是持久链接的话,该值则和fetchStart值相同,如果在传输层发生了错误且需要重新建立连接的话,那么在这里显示的是新建立的链接开始时间。 domComplete:DOM树解析完成,且资源也准备就绪的时间。Document.readyState 变为 complete,并将抛出 readystatechange 相关事件。 domContentLoadedEventEnd:DOM解析完成后,网页内资源加载完成的时间。 domContentLoadedEventStart:DOM解析完成后,网页内资源加载开始的时间。 domInteractive:完成解析DOM树的时间(只是DOM树解析完成,并没有开始加载页面资源)。 domLoading:开始解析渲染DOM树的时间。 domainLookupEnd:DNS域名查询完成的时间,如果使用了本地缓存或持久链接,该值则与fetchStart值相同。 domainLookupStart:DNS域名查询开始的时间,如果使用了本地缓存或持久链接,该值则与fetchStart值相同。 fetchStart:准备好使用http请求抓取文档的时间(发生在检查本地缓存之前)。 loadEventEnd:load事件的回调函数执行完毕的时间,如果没有绑定load事件,该值为0 loadEventStart:load事件发送给文档。也即load回调函数开始执行的时间,如果没有绑定load事件,则该值为0 navigationStar:同一个浏览器上一个页面卸载结束时的时间戳。如果没有上一个页面的话,那么该值会和fetchStart的值相同 redirectEnd:最后一个HTTP重定向完成时的时间戳。如果没有重定向或者重定向到一个不同的源,该值也返回为0 redirectStart:该值的含义是第一个http重定向开始的时间戳,如果没有重定向或者重定向到一个不同源的话,那么该值返回为0 requestStart:HTTP请求读取真实文档开始的时间,包括从本地读取缓存,链接错误重连时。 responseEnd:HTTP响应全部接收完成时的时间(获取到最后一个字节)。包括从本地读取缓存。 responseStart:开始接收到响应的时间(获取到第一个字节的那个时候)。包括从本地读取缓存。 secureConnectionStart:HTTPS 连接开始的时间,如果不是安全连接,则值为 0 unloadEventStart:前一个页面unload的时间戳,如果没有前一个页面,那么该值为0 unloadEventEnd:和 unloadEventStart 相对应,返回是前一个网页unload事件绑定的回调函数执行完毕的时间戳。 拓展学习 W3C Web Performance:https://www.w3.org/TR/resource-timing-2/#cross-origin-resources Web Performance Working Group:https://www.w3.org/webperf/ Chrome Web Fundamentals:https://developers.google.cn/web/fundamentals/performance/navigation-and-resource-timing chrome.loadTimes() API:https://developers.google.cn/web/updates/2017/12/chrome-loadtimes-deprecated#requesttime MDN Performence API:https://developer.mozilla.org/zh-CN/docs/Web/API/Performance MDN Timing-Allow-Origin API:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin Unix time:https://en.wikipedia.org/wiki/Unix_time Performance Navigation Timing:https://w3c.github.io/navigation-timing/#dom-performancenavigationtiming Resource Timinghttps://www.w3.org/TR/resource-timing-2/#cross-origin-resources
2022-05-18 - onBluetoothDeviceFound在部分安卓手机,没办法回调
终端类型:微信安卓客户端 手机机型:华为STK-AL00 安卓系统版本:9 微信版本:7.0.14 基础库版本:2.11.0 用户微信:ceyn-2014 操作路径:openBluetoothAdapter执行成功->startBluetoothDevicesDiscovery执行成功->onBluetoothDeviceFound没有触发回调。 已采取措施:打开gps,打开位置权限,已上传日志,微信号:ceyn-2014 麻烦尽快回复问题,谢谢!
2020-05-15 - 安卓手机调用wx.onBluetoothDeviceFound无法执行回调?
wx.onBluetoothDeviceFound 没有回调 有时候需要用户关闭蓝牙然后开启蓝牙后才可以 日志已上传 微信号 d393911462 时间 2021年1月28日16:23 。 麻烦官方看一下是什么原因 微信版本号 8.0.0 库版本 2.14.1
2021-01-28 - 小程序用户头像昵称获取(含部分常见问题)
小程序用户头像昵称获取 引言 前段时间,小程序头像昵称获取的接口进行了调整,原公告《小程序用户头像昵称获取规则调整公告》。 简单来说,现在获取用户的微信头像和昵称要用户自己填写了。 下面和大家分享一下「头像昵称填写能力」的实操过程。 「头像昵称填写功能」实操 一、头像部分 WXML 首先,在wxml页面上添加一个[代码]button[代码]组件,设置[代码]open-type[代码]属性为[代码]chooseAvatar[代码],再添加一个触发事件[代码]bindchooseavatar[代码],代码如下: [代码] <button open-type="chooseAvatar" bindchooseavatar="bindchooseavatar"> <image src="{{avatarUrl}}"></image> </button> [代码] 我这里在[代码]button[代码]里面添加了[代码]image[代码]组件是为了实现点击头像(图片)就触发头像选择。 JS 在js部分,写好[代码]bindchooseavatar[代码]事件回调。 [代码] bindchooseavatar(e) { console.log("avatarUrl",e.detail.avatarUrl) } [代码] 当我们触发组件,选择好头像后,我们可以从事件回调中得到头像链接[代码]avatarUrl[代码]。 我这里获取到的链接是: [代码]http://tmp/bnMmEbfpqclVa77acadd216b18c692b3a2aa1d505353.jpeg[代码]。 [图片] 需要注意的是 这里获取到的是本地临时链接,只能在本地中读取与使用,随时会失效。 我们还需要将这个临时路径保存到服务器中,从而换取一个永久链接。 我们可以用API[代码]wx.uploadFile[代码]将图片上传到自己的服务器,改写后的[代码]bindchooseavatar[代码]事件回调代码如下: [代码] bindchooseavatar(e) { const avatarUrl = e.detail.avatarUrl console.log("avatarUrl", avatarUrl) wx.uploadFile({ url: 'https://www.hlxuan.top/upload', // 仅为示例,非真实的接口地址 filePath: avatarUrl, name: 'file', // 文件对应的 key,开发者在服务端可以通过这个 key 获取文件的二进制内容 formData: { 'user': 'test' }, // HTTP 请求中其他额外的 form data success (res){ const data = JSON.parse(res.data) // do something } }) } [代码] 如果你用的是小程序云开发,[代码]bindchooseavatar[代码]事件回调代码示例如下: [代码] bindchooseavatar(e) { const avatarUrl = e.detail.avatarUrl console.log("avatarUrl", avatarUrl) wx.cloud.uploadFile({ cloudPath: 'example.png', // 上传至云端的路径 filePath: avatarUrl, // 小程序临时文件路径 success: res => { // 返回文件 ID console.log(res.fileID) // do something }, fail: console.error }) } [代码] 二、昵称部分 WXML 组件自带安全检测功能,根据官方的文档说明,检测是在[代码]onBlur[代码]事件触发时异步进行的,也就是说,如果使用[代码]bindinput[代码]、[代码]bindblur[代码]、[代码]bindfocus[代码]、[代码]bindconfirm[代码]这些的回调去获取用户输入的昵称,就可能会获取到未通过安全检测的内容。 在基础库2.29.1中,新加入了回调 [代码]bindnicknamereview[代码],当安全检测完成后会进行回调 [代码]event.detail = { pass, timeout }[代码] 。 你可以用这个判断用户当前输入的昵称是否通过了安全检测,和检测是否超时。 文章内容更新于2022年12月29日 建议使用表单[代码]form[代码]来收集用户输入的昵称。 [图片] [代码] <form bindsubmit="formsubmit"> <input type="nickname" placeholder="请输入昵称" name="nickname" /> <button form-type="submit" type="primary">提交</button> </form> [代码] 我们需要给[代码]input[代码]设置一个[代码]name[代码]属性,这样才能在回调中获取到用户输入的昵称。 JS [代码] formsubmit(e){ const nickName = e.detail.value.nickname console.log("nickName", nickName) // do something }, [代码] 获取到用户输入的昵称后,你可以将其保存到数据库里面。 Q&A 下面是我在微信开放社区经常看到的一些问题,稍微整理了一下,希望能帮助大家。 1. 为什么「wx.getUserProfile」返回的是“灰色头像”和“微信用户”? 「wx.getUserProfile」接口有调整,参考公告《小程序用户头像昵称获取规则调整公告》。 2. 为什么有部分小程序仍能使用以前的接口获取头像? 公告中:“生效期前发布的小程序版本不受影响”。你看到接口能正常返回头像和昵称的小程序,基本上都是在生效期前发布的小程序,你可以去「更多资料」页面看看它的更新时间。 [图片] 针对下面这个问题,我这边测试了一下,生效期后发布的小程序,基础库低于2.27.1版本,是可以正常返回微信头像和昵称的。这个目前应该只能在 PC/macOS 平台上使用,手机上大部分使用不了的,因为 PC/macOS 平台目前的基础库版本低于2.27.1,所以可以正常返回微信头像和昵称。 [图片] 文章内容更新于2022年12月29日 3. 我调整了基础库版本后,工具上可以返回头像和昵称,真机就不行了? 用户客户端的基础库版本开发者是无法控制的,这个基础库版本会随着微信客户端的更新而更新,可以看下「基础库版本分布」。 在开发者工具上设置的基础库版本,仅用于开发者工具内的调试,所以不能调整移动设备的基础库版本。 [图片] 旁边的「推送」按钮只能将基础库版本推送到你登录开发者工具的微信号上(登录这个微信号的手机上),这并不能改变用户的基础库版本。 4. 我在小程序后台设置了基础库2.27.0或以下版本,为什么还是不能获取头像和昵称? 小程序后台里面设置的是「基础库最低版本设置」,当用户的基础库版本低于你设置的最低版本要求时,将无法正常使用小程序,并提示更新微信版本。 [图片] 5. 「头像昵称填写」有安全检测吗? 有的,组件在基础库2.24.4版本起,已经接入了内容安全服务端口。如果昵称或头像有异常时,页面会显示消息提示框,输入的昵称会被清空,头像也不会返回临时路径。请不要完全依赖内容安全服务。 6. 选择的头像会自动裁剪为1:1吗? 对于来自基础库2.28.1及以上版本,组件自带压缩和裁剪功能;对于来自基础库2.28.1以下版本,你可以做兼容处理。 经验分享:我在自己的小程序里面进行了判断,当基础库2.28.1以下版本时,调用「wx.cropImage」接口对用户选择的头像进行裁剪。 7. 为什么获取到的头像链接在浏览器上打不开? 通过「头像填写能力」获取到的链接为本地临时链接,只能在本地中读取与使用,随时会失效。 [图片]
2023-01-08 - iPhone4蓝牙状态获取不到
iPhone4,微信版本8.0.29,不论系统蓝牙是否开启,wx.openBluetoothAdapter获取的蓝牙状态始终不可用(state为3), wx.getSetting获取的权限状态始终为true, 最终蓝牙不可用,未授权,如果直接使用蓝牙会失败
2023-03-29 - writeBLECharacteristicValue在安卓上正常,ios上写入最后一帧为什无响应?
[图片][图片]如图所示,往蓝牙设备中通过分包写入1856块数据,索引从0开始,安卓设备可以正常写完所有数据,ios设备上最后一块的数据写入无响应,success/fail/complete回调中均无响应,有没有遇到过这种问题的?
2023-08-18 - 微信小程序答题页——swiper渲染优化及swiper分页实现
前言 swiper的加载太多问题,网上资料好像没有一个特别明确的,就拿这个答题页,来讲讲我的解决方案 这里实现了如下功能和细节: 保证swiper-item的数量固定,加载大量数据时,大大优化渲染效率记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验示例动图 [图片] 截图 [图片] [图片] 问题原因 当swiper-item数量很多的时候,会出现性能问题 我实现了一个答题小程序,在一次性加载100个swipe-item的时候,低端手机页面渲染时间达到了2000多ms 也就是说在进入答题页的时候,会卡顿2秒多去加载这100个swiper-item 思考问题 那我们能不能让他先加载一部分,然后滑动以后再去改变item的数据,让swiper一直保持一定量的swiper-item? 注意到官方文档有这么两个属性可以利用,我们可以开启衔接滑动,然后再bindchange方法中去修改data [图片] 1、保证swiper-item的数量固定,加载大量数据时,优化渲染效率 假设我们请求到的数据的为list,实际渲染的数据为swiperList 我们现在给他就固定3个swiper-item,前后滑动的时候去替换数据 正向滑动的时候去替换滑动后的下一页数据,反向滑动的时候去替换滑动后的上一页数据 当我们知道了要替换的条件,我们便可以去替换数据了 但是我们应该考虑到临界值的问题,如果当前页是list第一项和最后一项该怎么办,向左向右滑是不是得禁止啊 这边是判断没数据会让它再弹回去 2、记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页 有很多时候,我们是从某一项直接进来的,比如说上次答题答到了第五题,我这次进来要直接做第六题 那么我们需要去初始化这个swiperList,让它当前页、上一页、下一页都有数据 3、答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验 从答题卡选择index,那就不仅仅是滑动上下页了,它可以跳转到任何页,所以也采用类似初始化swiperList的方法 swiper切换动画我这边是默认250ms,但是发现有时候从答题卡点击回来,你在答题卡点击的下一项不知道会从左还是从右滑过来 体验真的很差,一开始不知道怎么禁掉动画,其实在跳转到答题卡页的时候把duration设为0就可以了 然后在答题卡页的unload方法中恢复 关键点: 在固定3个swiper-item的同时,要保证我们可以有办法来替代微信自带swiper的current属性和change方法 swiper-limited-load使用方法及说明: 将components中的swiper-limited-load复制到您的项目中在需要的页面引用此组件,并且创建自己的自定义组件item-view在初始化数据时,为你的list的每一项指定index属性具体可以参照项目目录start-swiper-limited-load中的用法说明:其它属性和swiper无异,你们可以自己单独添加你们需要的属性总结 一开始很头疼,为什么微信小程序提供的这个swiper,没去考虑这方面 然后在网上和社区找也没有一个特别好的解决方案。 后来想想,遇到需求就静下来解决吧。 项目地址:https://github.com/pengboboer/swiper-limited-load 如果错误,欢迎指出。 如有新的需求也可以提出来,如果有时间的话,我会帮你们完善。 如果能帮到你们,记得给一个star,谢谢。 ---补充 有很多朋友在评论区提到了分页的需求,抽时间写了一个分页的Demo和大家分享一下。 还是以答题为例,比如我们一共有500条数据,一页20条,可能需要如下功能,乍一看不就加了个分页,挺简单的,其实实现起来挺麻烦的,下面说一下思路和一些需要特别注意的点: 1、从其他页面跳转到答题页时,不光只能默认在第一题,可以是任意一题,比如第80题。 跳转到任意一题,那么需要我们根据index算出该数据在第几页,然后需要请求该页数据,最后显示对应的index。我的思路更注重用户体验,不可能是上滑或者下滑才开始去请求数据,一定是要用户滑动前提前请求好数据。所以起码要保证左右两侧在初始化那一刻都有数据。如果此题和它的上一题下一题都在同一页,那么我们只需要请求一页数据(第15题,那么只需请求第1页数据)。如果此题和它的上一题或者下一题不在同一页,那么我们可能需要请求两页数据。(第20题,那么需要请求第1页和第2页数据) 2、左滑、右滑没数据时,都可以加载新数据。直到滑到第一题或者最后一题。 如果我们初始化时是第24题,那么我们左滑到第21题时,就应该去请求第一页的数据。那么用户在看完21题时,再滑到20题,可能就根本不会感知到通过网络请求了数据。但是如果用户此刻滑动特别快:滑到21题时请求了网络,请求还没成功,就又向左滑了。那么我们需要限制用户的滑动,给用户一个提示:数据正在加载中。 3、从答题卡点击任意一题可以跳转到相应的题目,并且左右滑动显示正常数据 比如我们初始化是跳转到了第80题,不一会点击答题卡又要跳转到200题,一会又跳转到150题。各种无序操作,你也不知道用户要往哪里点。 一开始是想着维护一个主list,点到哪道题往list中添加这道题所在的当页的数据,但是还得判断这一页或者左滑右滑请求新一页的数据得往list的哪个位置添加。这来回来去乱七八糟的判断就很麻烦了,很容易出bug。而且list长度太长了以后insert的性能也不好。 后来就去想,要不答题卡点击任意一题都清空旧的list,然后请求新的数据,左右滑动没数据了再请求新的数据呗。但是这样很浪费资源,并且用户体验也不好,用户已经从第1题答到第200题了,这时用户从答题卡选择了一个25题,还得重新请求网络。而且200道题的数据都没了,那再选个26题,再重新请求网络?网络有延时不说,还浪费资源。 最后转念一想,这时候就需要弄一个缓存了。所以最终的解决方法就出来了:我们维护一个map,在网络请求成功后,在map中保存对应页的数据,同时我们维护一个主list来显示对应的题目。当我们在答题卡选择某一题目,就清空list,然后判断map中有没有该页的数据,如果有就直接拿来,没有就再去网络请求。这个处理方式,写法相对来说简单,不需要乱七八糟的判断,也不浪费资源,用户体验也很不错。 总结 以上就是一些思路和要注意的地方。这个Demo断断续续花了好几天时间写出来的。可能我说的比较啰嗦比较细,只是想让需要用到这个分页Demo的同学能理解我是如何实现的。 如果觉得能帮到你,记得给一个star,谢谢。同时如果这个demo有bug或者你们有新想法,欢迎提出来。
2021-01-07 - wx.getMenuButtonBoundingClientRect()获取的属性,top值不准确
[图片][图片][图片] 测试屏幕参数iphone6/7/8,在默认模式下,获取screenHeight为667px,windowHeight为603px,statusBarHeight为20px,计算顶部导航栏高度为44px,(44-32(胶囊高度))/2 = 6px (即胶囊距离状态栏底部的距离), 但是wx.getMenuButtonBoundingClientRect()获取的top值t为24px,24-20(状态栏高度) = 4px (即胶囊距离状态栏底部的距离) 真机和编辑器都出现问题 安卓 ios 都有 计算差值对应不起来
2021-10-14 - 小程序奇技淫巧之 -- 日志能力
日志与反馈 前端开发在进行某个问题定位的时候,日志是很重要的。因为机器兼容性问题、环境问题等,我们常常无法复现用户的一些bug。而微信官方也提供了较完整的日志能力,我们一起来看一下。 用户反馈 小程序官方提供了用户反馈携带日志的能力,大概流程是: 开发中日志打印,使用日志管理器实例 LogManager。 用户在使用过程中,可以在小程序的 profile 页面(【右上角胶囊】-【关于xxxx】),点击【投诉与反馈】-【功能异常】(旧版本还需要勾选上传日志),则可以上传日志。 在小程序管理后台,【管理】-【反馈管理】,就可以查看上传的日志(还包括了很详细的用户和机型版本等信息)。 这个入口可能对于用户来说过于深入(是的,官方也发现这个问题了,所以后面有了实时日志),我们小程序也可以通过[代码]button[代码]组件,设置[代码]openType[代码]为[代码]feedback[代码],然后用户点击按钮就可以直接拉起意见反馈页面了。利用这个能力,我们可以监听用户截屏的操作,然后弹出浮层引导用户主动进行反馈。 [代码]<view class="dialog" wx:if="{{isFeedbackShow}}"> <view>是否遇到问题?</view> <button open-type="feedback">点击反馈</button> </view> wx.onUserCaptureScreen(() => { // 设置弹窗出现 this.setData({isFeedbackShow: true}) }); [代码] LogManager 关于小程序的 LogManager,大概是非常实用又特别低调的一个能力了。它的使用方式其实和 console 很相似,提供了 log、info、debug、warn 等日志方式。 [代码]const logger = wx.getLogManager() logger.log({str: 'hello world'}, 'basic log', 100, [1, 2, 3]) logger.info({str: 'hello world'}, 'info log', 100, [1, 2, 3]) logger.debug({str: 'hello world'}, 'debug log', 100, [1, 2, 3]) logger.warn({str: 'hello world'}, 'warn log', 100, [1, 2, 3]) [代码] 打印的日志,从管理后台下载下来之后,也是很好懂: [代码]2019-6-25 22:11:6 [log] wx.setStorageSync api invoke 2019-6-25 22:11:6 [log] wx.setStorageSync return 2019-6-25 22:11:6 [log] wx.setStorageSync api invoke 2019-6-25 22:11:6 [log] wx.setStorageSync return 2019-6-25 22:11:6 [log] [v1.1.0] request begin 2019-6-25 22:11:6 [log] wx.request api invoke with seq 0 2019-6-25 22:11:6 [log] wx.request success callback with msg request:ok with seq 0 2019-6-25 22:11:6 [log] [v1.1.0] request done 2019-6-25 22:11:7 [log] wx.navigateTo api invoke 2019-6-25 22:11:7 [log] page packquery/pages/index/index onHide have been invoked 2019-6-25 22:11:7 [log] page packquery/pages/logs/logs onLoad have been invoked 2019-6-25 22:11:7 [log] [v1.1.0] logs | onShow | | [] 2019-6-25 22:11:7 [log] wx.setStorageSync api invoke 2019-6-25 22:11:7 [log] wx.setStorageSync return 2019-6-25 22:11:7 [log] wx.reportMonitor api invoke 2019-6-25 22:11:7 [log] page packquery/pages/logs/logs onShow have been invoked 2019-6-25 22:11:7 [log] wx.navigateTo success callback with msg navigateTo:ok [代码] LogManager 最多保存 5M 的日志内容,超过5M后,旧的日志内容会被删除。基础库默认会把 App、Page 的生命周期函数和 wx 命名空间下的函数调用写入日志,基础库的日志帮助我们定位具体哪些地方出了问题。 实时日志 小程序的 LogManager 有一个很大的痛点,就是必须依赖用户上报,入口又是右上角胶囊-【关于xxxx】-【投诉与反馈】-【功能异常】这么长的路径,甚至用户的反馈过程也会经常丢失日志,导致无法查问题。 为帮助小程序开发者快捷地排查小程序漏洞、定位问题,微信推出了实时日志功能。从基础库 2.7.1 开始,开发者可通过提供的接口打印日志,日志汇聚并实时上报到小程序后台。 使用方式如下: 使用 wx.getRealtimeLogManager 在代码⾥⾯打⽇志。 可从小程序管理后台【开发】-【运维中心】-【实时日志】进入日志查询页面,查看开发者打印的日志信息。 开发者可通过设置时间、微信号/OpenID、页面链接、FilterMsg内容(基础库2.7.3及以上支持setFilterMsg)等筛选条件查询指定用户的日志信息: [图片] 由于后台资源限制,实时日志使用规则如下: 为了定位问题方便,日志是按页面划分的,某一个页面,在onShow到onHide(切换到其它页面、右上角圆点退到后台)之间打的日志,会聚合成一条日志上报,并且在小程序管理后台上可以根据页面路径搜索出该条日志。 每个小程序账号每天限制500万条日志,日志会保留7天,建议遇到问题及时定位。 一条日志的上限是5KB,最多包含200次打印日志函数调用(info、warn、error调用都算),所以要谨慎打日志,避免在循环里面调用打日志接口,避免直接重写console.log的方式打日志。 意见反馈里面的日志,可根据OpenID搜索日志。 setFilterMsg 可以设置过滤的 Msg。这个接口的目的是提供某个场景的过滤能力,例如[代码]setFilterMsg('scene1')[代码],则在 MP 上可输入 scene1 查询得到该条日志。比如上线过程中,某个监控有问题,可以根据 FilterMsg 过滤这个场景下的具体的用户日志。FilterMsg 仅支持大小写字母。如果需要添加多个关键字,建议使用 addFilterMsg 替代 setFilterMsg。 日志开发技巧 既然官方提供了 LogManager 和实时日志,我们当然是两个都要用啦。 log.js 我们将所有日志的能力都封装在一起,暴露一个通用的接口给调用方使用: [代码]// log.js const VERSION = "0.0.1"; // 业务代码版本号,用户灰度过程中观察问题 const canIUseLogManage = wx.canIUse("getLogManager"); const logger = canIUseLogManage ? wx.getLogManager({level: 0}) : null; var realtimeLogger = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null; /** * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function DEBUG(file, ...args) { console.debug(file, " | ", ...args); if (canIUseLogManage) { logger!.debug(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.info(`[${VERSION}]`, file, " | ", ...args); } /** * * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function RUN(file, ...args) { console.log(file, " | ", ...args); if (canIUseLogManage) { logger!.log(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.info(`[${VERSION}]`, file, " | ", ...args); } /** * * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function ERROR(file, ...args) { console.error(file, " | ", ...args); if (canIUseLogManage) { logger!.warn(`[${VERSION}]`, file, " | ", ...args); } if (realtimeLogger) { realtimeLogger.error(`[${VERSION}]`, file, " | ", ...args); // 判断是否支持设置模糊搜索 // 错误的信息可记录到 FilterMsg,方便搜索定位 if (realtimeLogger.addFilterMsg) { try { realtimeLogger.addFilterMsg( `[${VERSION}] ${file} ${JSON.stringify(args)}` ); } catch (e) { realtimeLogger.addFilterMsg(`[${VERSION}] ${file}`); } } } } // 方便将页面名字自动打印 export function getLogger(fileName: string) { return { DEBUG: function(...args) { DEBUG(fileName, ...args); }, RUN: function(...args) { RUN(fileName, ...args); }, ERROR: function(...args) { ERROR(fileName, ...args); } }; } [代码] 通过这样的方式,我们在一个页面中使用日志的时候,可以这么使用: [代码]import { getLogger } from "./log"; const PAGE_MANE = "page_name"; const logger = getLogger(PAGE_MANE); [代码] autolog-behavior 现在有了日志组件,我们需要在足够多的地方记录日志,才能在问题出现的时候及时进行定位。一般来说,我们需要在每个方法在被调用的时候都打印一个日志,所以这里封装了一个 autolog-behavior 的方式,每个页面(需要是 Component 方式)中只需要引入这个 behavior,就可以在每个方法调用的时候,打印日志: [代码]// autolog-behavior.js import * as Log from "../utils/log"; /** * 本 Behavior 会在小程序 methods 中每个方法调用前添加一个 Log 说明 * 需要在 Component 的 data 属性中添加 PAGE_NAME,用于描述当前页面 */ export default Behavior({ definitionFilter(defFields) { // 获取定义的方法 Object.keys(defFields.methods || {}).forEach(methodName => { const originMethod = defFields.methods![methodName]; // 遍历更新每个方法 defFields.methods![methodName] = function(ev, ...args) { if (ev && ev.target && ev.currentTarget && ev.currentTarget.dataset) { // 如果是事件类型,则只需要记录 dataset 数据 Log.RUN(defFields.data.PAGE_NAME, `${methodName} invoke, event dataset = `, ev.currentTarget.dataset, "params = ", ...args); } else { // 其他情况下,则都记录日志 Log.RUN( defFields.data.PAGE_NAME, `${methodName} invoke, params = `, ev, ...args); } // 触发原有的方法 originMethod.call(this, ev, ...args); }; }); } }); [代码] 我们能看到,日志打印依赖了页面中定义了一个[代码]PAGE_NAME[代码]的 data 数据,所以我们在使用的时候可以这么处理: [代码]import { getLogger } from "../../utils/log"; import autologBehavior from "../../behaviors/autolog-behavior"; const PAGE_NAME = "page_name"; const logger = getLogger(PAGE_NAME); Component({ behaviors: [autologBehavior], data: { PAGE_NAME, // 其他数据 }, methods: { // 定义的方法会在调用的时候自动打印日志 } }); [代码] 页面如何使用 Behavior 看看官方文档:事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用[代码]Component[代码]构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应[代码]json[代码]文件中包含[代码]usingComponents[代码]定义段。 完整的项目可以参考wxapp-typescript-demo。 参考 LogManager 实时日志 Component构造器 behaviors 结束语 使用自定义组件的方式来写页面,有特别多好用的技巧,behavior 就是其中一个比较重要的能力,大家可以发挥自己的想象力来实现很多奇妙的功能。
2019-12-10 - TouchUI WX - 高质量的微信小程序UI框架
DEMO [图片] 简介 TouchUI-WX是一套完全免费的微信小程序开发框架,包含丰富的UI控件用于官方组件的补充,并扩展了小程序很多其他能力。 部分组件效果 [图片] 特点 1、组件扩充: 增加了30多种常用的组件用于官方组件的补充。 2、功能扩充:兼容阿里的iconfont图标库,海量矢量图标随意使用;补充了常用样式库、支持less语法、支持全局配置主题色等 3、开发体验改善:四文件方式改为单文件方式,通过VSCode编辑器+插件的方式开发,拥有web开发体验; 4、小程序转为H5应用:可以与H5开发框架TouchUI工程相互转换,发布成webApp。开发一套代码,拥有两套应用。 它与mpvue、wepy等最大的区别在于:组件完全是基于小程序官方的自定义组件机制实现,开发时直接输出为微信小程序工程源代码,而不是读不懂的编译代码。这样好处在于: 1、开发者迁移成本很小。可以轻松的将已有的小程序移植为TouchUI-WX工程,来使用它的扩展能力; 2、便于排查错误。当遇到问题时,开发者也可以随时查看输出的小程序原始代码来定位问题所在。不会搞不清楚到底是框架问题还是自己代码的问题; 3、按需编译由于小程序对体积有限制,在使用框架开发时,只有使用到的组件才会编译输出为小程序源码。没用到的不会输出。 4、不会对框架产生依赖。以后不想用了这套框架,可以直接对已经输出的小程序工程进行维护 Github地址 https://github.com/uileader/touchuiwx
2018-05-31 - 纵观JS对象的“简”与“繁”(下)
上篇文的最后,我们聊到了JS对象的一个重磅成员——原型模式,本以为迎来了对象领域的终极大boss,却发现它仍然存在局限性,这种局限就是: 不需要共享的也会被共享,导致出现期望之外的结果。 什么不需要共享?比如,如果我们这样操作: [代码]function Person(){ } Person.prototype.friends=["1","2","3","4"]; var person1 = new Person(); var person2 = new Person(); person1.friends.push("5"); alert(person1.friends); alert(person2.friends); [代码] 会输出什么?你应该猜对了,两个都是 [代码]1,2,3,4,5 [代码] person2需要friends么,不一定,他需要push进来一个新的“5”吗?也不一定,那么在完全被动的情况下,因为我们把friends定义在了原型里,且由于person1对其进行了操作,就同时影响到了person2,显然,这是不合适的。 怎么破? 当我们进行了代码的简化,作用域的优化之后,似乎仍有进一步改善的空间。要么是纯粹的私有,要么是纯粹的共享,有中和的方式吗? 构造函数和原型组合模式 看到这里,你脑海中是否闪过一个念头——“我早该想到的!”。 没错,既然他们一个那么自私,一个那么大方,把它们结合起来不就有所平衡了么? 看代码: [代码]function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.friends = ["lili","lucy"]; } Person.prototype = { constructor : Person; sayName : function(){ alert(this.name); } } [代码] 这段代码,既让每个实例都有自己的一份属性副本,同时又共享着对方法的引用,是现在使用最广泛的方式,最大限度地节省了内存。 其实这里,相比具体的方案,我们更应该重视一个思路,就是“组合”,我们常常面临方案的选择,选A,或者B,或者C,多数情况下,每种方案都有其优点和局限性,而组合使用不失为一种“两全”之策。 but,虽然功能上它是兼备的,并不能说它是完美的。 回想一下,前面我们看到过的,直接创建对象也好,对象字面量也好,或者构造函数、原型模式,我们都倾向于去使其具有封装性,而这里它们却是相互独立的,能否将其封装起来呢? 动态原型模式 所谓动态原型,似乎不太好理解,不论是书籍还是网上能够查到的文章,大都简单罗列,而没有解释得很清楚,我反复看过一些代码和短文,下面是我的理解。 其实这个模式是在“构造函数和原型模式”的基础上做了两个方面的改良,看代码: [代码] function Person(name,age,job){ this.name = name; this.age = age; this.job = job; if(typeof this.sayName != "function"){ Person.prototype.sayName = function(){ alert(this.name); } } } [代码] 一、封装,sayName没有单独放在函数的外部,而是内部。 二、创造私有实例的时候,去判断某个需要的共有方法是否已经存在,因为你可能已经在别的地方创建过了,而这种共有的方法只需要创建一次即可,如果有,忽略此段,没有,则对其初始化,避免时间和空间的浪费,动态体现在这里,从用意上来看,应该是一种预判,并不是共享方法这样写有什么好处。 需要注意的一点是:在这种模式下,不宜使用字面量重写原型,因为在已经创建了实例的情况下重写原型会切断现有实例和原型之间的联系。 到了这里,有关对象的高潮似乎已经过去,其实除此之外,还有两个“小”角色值得我们关注: 寄生构造函数模式 《高程》上说,在前面几种模式都不适用的情况下,使用这个模式,我觉得这么说有点不负责任,这样说等于没交代它的使用场景,有敷衍之嫌,不知道是原文的问题还是翻译的问题,或者是篇幅所限,暂且不管。 不妨顾名思义,分拆来看: 寄生:对某个东西有所依托 构造函数:用起来像构造函数 上代码: [代码]function Person(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); } return o; } var friends = new Person("alien",29,"teacher"); friends.sayName(); [代码] 你没看错,我也没写错,这里,除了创建实例的时候用了new操作符之外,它和工厂模式一模一样… 它看起来是个函数,但这个函数只起到封装作用,执行的结果是将在其中创建的对象给返回出来。 这种模式(就目前研究)主要是可以用来给一些内置构造函数增加新的方法,大家都知道,构造函数的属性和方法是可以改的,但直接改原生方法是不推荐的,那么寄生模式就派上用场,比如这样: [代码]function SpecialArray(){ var values = new Array(); values.push.apply(values, arguments); values.toPipedString = function(){ return this.join("|"); } return values; } var a = new SpecialArray(2,6,8,9,4); document.write(a.toPipedString()); [代码] 上面这段代码输出的值将会是: [代码]2|6|8|9|4 [代码] 而正常情况下,都会输出 [代码]2,6,8,9,4 [代码] 也就是说,根据个性化需要改变了数组的输出方式。 上面提过,“寄生”是一种依存关系,它是给已经存在的东西添加“功能”。 这么说你应该已经有大概的理解了,但到这一步我并不是很满意,感觉还可以挖掘出更多东西,如果你看到这篇文章,并且有不同的意见或者看法,欢迎交流。 稳妥构造函数模式 稳妥,听起来就很保守,也意味着安全。 先看代码: [代码]function Person(name,age,job){ // 创建要返回的对象 var o = new object(); //私有变量和方法 //添加方法 o.sayName = function(){ alert(name); } //返回 return o; } [代码] 稳妥构造函数的两个特点: 1、没有公共属性 2、方法不引用this的对象 这种模式是在某些禁用了this和new的环境下可使用的。 最后这两种模式,使用的场景较少,知道就好,重点还是前面那些方法的练习和运用。 总结 写这两篇文章,是因为“对象”这个东西一直都像是难啃的骨头,但其实任何显得复杂或者困难的东西,都是从简单慢慢演变而来的,如果循序渐进地加入一些有血有肉的思考,就能更容易地对其进行理解和记忆。 写文过程中,我尽量做到不生搬概念,加入个人的思考过程,但认知有限,不足在所难免,还望读者朋友不吝赐教。 后面还会继续跟大家一起啃硬骨头,下次见!
2019-03-11 - 纵观JS对象的“简”与“繁”(上)
JS这门语言,曾被不少开发者视为玩乐的语言,没有厚度和技术含量的语言,但发展到现在,想必没有人敢再这么说,它能做的事越来越多,所以,这门语言看似知识结构简单,但却在代码的行与行之间藏着很多细节和玄机。 “对象”在JS中是个很有意思的东西,它随处可见,说简单可以很简单,但也可以复杂到让人头皮发麻。 简单 如何简单? [代码]var person = new Object(); [代码] 这样就可以创建一个名为“person”的对象。 但事实上,没有人会在意它的简单,因为简单的东西往往承担不了重任。 所以,我们可以从另一个角度去理解它的复杂,就是强大——每一种形式,每一个特性,都为了解决更多问题而生。 复杂 像上面那样,我们可以轻松创建一个对象,进一步,为其添加一些属性和方法: [代码]person.name = "idea"; person.age = 18; person.run = function(){ alert("I can run!"); } [代码] 但其实这里我们就会发现需要写很多的“person”,代码不够简洁。 可以进行如下改进,也就是另外一种写法——“对象字面量”: 对象字面量 [代码]var person = { name : "idea"; age : 18; run : function(){ alert("I can run!"); } } [代码] 它有两个优点:简化代码、凸显封装性。 如果你对js不熟,但对css预处理还算熟悉,这就像是less或者sass的嵌套。(后面更深入的东西我还会拿css类比,以帮助理解。 由此,我们可以像这样创建对象: [代码]var person1 = { name : "tom"; age : 18; run : function(){ alert("I can run!"); } } var person2 = { name : "lili"; age : 16; run : function(){ alert("I can run!"); } } ... [代码] 但其实我们好像又发现一点不那么好的地方,这样以来,我们每创建一个新的实例,都要写这么一大块的代码,显然是冗余的,于是,有人发动脑筋,想了一种办法——“工厂模式”。 工厂模式 对于工厂模式,可以这么理解,我们需要制作100个同类的产品,但不需要为每件产品都弄一个模子,而是一个模子可以反复用,生产很多很多产品。 代码就像这样: [代码]function createPerson(name, age, job){ var o = new object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); } return o; } var person1 = createPerson("tom",18,"teacher"); var person2 = createPerson("lili",16,"doctor"); [代码] 这段代码的亮点在最后,我们可以只用一行代码就创建一个对象,并赋予其属性值。 看起来很不错,但它存在一个难以觉察的不足——我们好像无法获知这个对象的类型。 这么说其实并不准确,每个对象都有其类型,大不了是Object,但这个结果给不了我们更有价值的东西。所以,要引荐出一个对象世界里的重要角色——“构造函数”。 构造函数 先看代码: [代码]function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); } }; var person1 = new Person("tom",18,"teacher"); var person2 = new Person("lili",16,"doctor"); [代码] 从面上看,这段代码跟上面那段有这么几点不同: 没有显式创建对象 属性和方法直接给了this 没有返回 创建新实例的时候,使用了“new”关键字 名称首字母大写 为什么说它重要,上面我们只是创建了一个自定义的构造函数,其实js当中有很多内置的构造函数,我们会无数次地使用,比如Array、Date、String等等。 构造函数和普通函数有什么不同?好像只是首字母大写? 这么说也没错,它可以当做普通函数使用,就像这样: Person(“tom”,18,“teacher”); 但当它被这样使用了之后,就是作为构造函数: var person1 = new Person(“tom”,18,“teacher”); 会经历完全不同的过程。 构造函数看起来很好用,但它还有需要改进的地方吗?往下看。 上面那段代码里,有这么一行: this.sayName = function(){ alert(this.name); } 它会为新创建的实例新建一个sayName方法,别忘了,方法是赋予对象的函数,函数本身也是对象,所以,person1和person2是两个不同的对象实例,同样,它们的sayName方法也不同(虽然看起来是一个样子)。 这样以来,name、age和job都共用了,完成同样任务的方法却没有共用,每一个新的实例都会创建一次,显然,这不是最理想的,有办法解决吗? 我们可以做这样的尝试: [代码]function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = sayName; }; function sayName(){ alert(this.name); } var person1 = new Person("tom",18,"teacher"); var person2 = new Person("lili",16,"doctor"); [代码] 和之前的区别就在于,我们把sayName方法提到了函数体的外面,意味着,它是个全局的函数,而不属于某一个。 这个时候,你会发现一个有趣的现象,如果你写下这么一行代码: alert(person1.sayName() == person2.sayName()); 它会弹出三个值:“tom、lili、true”。 首先是里面两个分别执行,然后是外层,这说明,它们俩共用了同一个,不需要担心多余创建的那个函数实例。 终于皆大欢喜! 可是…慢着,好像哪里不对? 你可能记得,说对象字面量的时候,我们提到了封装性,你会发现,这里的sayName方法在函数体的外部,如果有很多个,就会有很多个方法散落在外部,这感觉很糟,这不是我们要的,so,必须找到一个办法解决它,就像上面做的那样。 原型模式 我们终于迎来了这个重磅的概念,关于这个概念,往往让人觉得是复杂的,抽象的,高深莫测的,其实并不,让我们来看看它具体的表现: [代码]function Person(){ } Person.prototype.name = "alien"; Person.prototype.age = "23"; Person.prototype.job = "teacher"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); var person2 = new Person(); [代码] 我们创建一个函数,但里面什么都没有。 接下来用到了“prototype”,其实每个函数都有一个prototype属性,这个属性是一个指针,所谓指针,就是建立A和B相关联的一个中介,prototype指向一个对象,这个对象,可以为我们提供所有同一类型实例能够共享的属性和方法,听起来是不是很熟悉?——它能为我们带来前面提到的所有。 但这么说似乎仍然不好理解,所以,你应该对CSS很熟悉,prototype,就相当于为所有可能的子元素,提供了一个父元素,这个父元素的所有属性,都能为子元素所共享,同时,它也不限制子元素的行为。 即,person1和person2具备Person.prototype所指对象的所有属性和方法,并且能够对其进行覆盖或者添加自身特有的新属性和方法。 看到这,我们是不是应该准备欢呼“大结局”了呢?的确,已经有不少内容了,可是,事情从来都不像我们想想的那么简单。 来思考一下,原型虽好,但它是否让我们走向了另一个极端?所有的属性和方法我们都需要共享吗?往往并不是,很多时候,对于某个属性,我们只需要私有即可,而不必共有,那么,什么样的属性私有更合适?既具备私有,又能共有的方案,存在吗? 想知道答案,且看下回分解~ 纵观JS对象的“简”与“繁”(下)
2019-03-11