- 从源码看微信小程序启动过程
一、写作背景 接触小程序一年多,真实体验就是小程序开发门槛相对而言确实比较低。不过小程序的开发方式,一直是开发者吐槽的,如习惯了 Vue,React 开发的开发者经常会吐槽小程序一个 Page 必须由多个文件组成,组件化支持不完善或者说不能非常愉快的开发组件。在以前小项目中没太大感觉,从加入有赞,参与有赞微商城小程序的开发,是真切的体会到对于大型小程序项目开发的复杂性。 有赞从微信小程序内测就开始开发小程序,在不支持自定义组件的时代,只能通过 import 的形式拆分模块或实现组件。在业务复杂的页面,可能会 import 非常多的模块,而相应的 wxss 也需要 import 样式,除了操作繁琐,有时候也难免遗漏。 作为开发者,我们当然希望可以让工作更简单,更愉快,也希望改善我们的开发方式。所以希望能够更了解微信小程序框架,减少不必要的试错,于是有了一次对小程序框架的 debug 之旅。(基础库 1.9.93) 通过三周空余时间的 debug,也算对小程序框架有了一些浅显的认识,达到了最初的目的;对小程序启动,实例,运行等有了真切的体会。这篇文章记录了小程序框架的基本代码结构,启动流程,以及程序实例化过程。 本文的目的是希望把我看到的分享给对小程序感兴趣或者正在开发小程序的读者,主要解答“框架对传入的对象等到底做了什么”。 二、从启动流程一窥小程序框架细节 在开发者工具中使用 help() 方法,可以查看一些指令和方法。使用其中的 openVendor 方法可以打开微信开发者工具在小程序框架所在目录。其中以包括以基础库命名的目录和其他帮助文件,如其中有两个工具 wcc,wcsc。wcc 可把 wxml 转换为对应的 JS 函数 —— $gwx(path, global),wcsc 可将 wxss 转换为 css。而基础库目录包括 WAService.js 和 WAWebview.js 文件。小程序框架在开发者工具中以 WAService.js 命名(WAWebview.js 不知其作用,听说在真机环境使用该文件)。 在开发中工具命令行使用 document.head 可以查看到小程序的启动流程大致如下: [图片] 以小节的方式分别介绍这些流程,小程序是如何处理的(小节编号与图中编号相同)。 1、初始化全局变量 下图是小程序启动是初始化的一些全局的变量: [图片] 那些使用“__”开头,未在文档中提及可使用变量是不建议使用的,wxAppCode 在开发者工具中分为两类值,json 类型和 wxml 类型。以 .json 结尾的,其 key 值为开发者代码中对应的 json 文件的内容,.wxml 结尾的,其 key 值为通过调用 $gwx(’./pages/example/index.wxml’) 将得到一个可执行函数,通过调用这个函数可得到一个标识节点关系的 JSON 树。 [图片] 2、加载框架(WAService.js) 使用工具对 WAService.js 进行格式化后进行 debug。可以发现小程序框架大致由: WeixinJSBridge、 NativeBuffer、 wxConsole、 WeixinWorker、 JavaScript兼容(这部分为猜测)、 Reporter、 wx、 exparser、 virtualDOM、 appServiceEngine 几部分组成。 其中除了 wx 和 WeixinJSBridge 这两个基础 API 集合, exparser, virtualDOM, appServiceEngine 这三部分作为框架的核心, appServiceEngine 提供了框架最基本的接口如 App,Page,Component; exparser 提供了框架底层的能力,如实例化组件,数据变化监听,view 层与逻辑层的交互等;而 virtualDOM 则起着链接 appServiceEngine 和 exparser 的作用,如对开发者传入 Page 方法的对象进行格式化再传入 exparser 的对应方法处理。 框架对外暴露了以下API:Behavior,App,Page,Component,getApp,getCurrentPages,definePlugin,requirePlugin,wx。 3、业务代码的加载 在小程序中,开发者的 JavaScript 代码会被打包为 [代码]define('xxx.js', function(require, module, exports, window, document, frames, self, location, navigator, localStorage, history, Caches, screen, alert, confirm, prompt, fetch, XMLHttpRequest, WebSocket, webkit, WeixinJSCore, Reporter, print, WeixinJSBridge) { 'use strict'; // your code }) [代码] 这里的 define 是在框架中定义的方法,在框架中提供了两个方法:require 和 define 用来定义和使用业务代码。其方式有些像 AMD 规范接口,通过 define 定义一个模块,使用 require 来应用一个模块。但是也有很大区别,首先 define 限制了模块可使用的其他模块,如 window,document;其次 require 在使用模块时只会传入 require 和 module,也就是说参数中的其他模块在定义的模块中都是 undefined,这也是不能在开发者工具中获取一些浏览器环境对象的原因。 在小程序中,JavaScript 代码的加载方式和在浏览器中也有些不同,其加载顺序是首先加载项目中其他 js 文件(非注册程序和注册页面的 js 文件),其次是注册程序的 app.js,然后是自定义组件 js 文件,最后才是注册页面的 js 代码。而且小程序对于在 app.js 以及注册页面的 js 代码都会加载完成后立即使用 require 方法执行模块中的程序。其他的代码则需要在程序中使用 require 方法才会被执行。 下面详细介绍了 app.js,自定义组件,页面 js 代码的处理流程。 4、加载 app.js 与注册程序 在 app.js 加载完成后,小程序会使用 require(‘app.js’) 注册程序,即对 App 方法进行调用,App 方法是对 appServiceEngine.App 方法的引用。 下图是框架对于 App 方法调用时的处理流程: [图片] App 方法根据传入的对象实例化一个 app 实例,其生命周期函数 onLaunch 和 onShow 因为使用不同的方式获取 options的参数。在有些需要根据场景值来实现需求的,或许使用 onShow 中的场景值更合适。 在实际开发过程中发现,在微信顶部唤起小程序和在小程序列表唤起的 options 也是不一样的。在该案例中通过点击分享的小程序进入后,关闭小程序,再通过不同方式进入小程序,通过顶部唤起的还是 options 的 path 属性还是分享出来的 path,但是通过列表中打开直接回到了首页,这里 App 中的 onShow 就会获取到不同的 options。 5、加载自定义组件代码以及注册自定义组件 自定义组件在 app.js 之后被加载,小程序会在这个过程中加载完所有的自定义组件(分包中自定义组件没有有测试过),并且是加载完成后自动注册,只有注册完成后才会加载下一个自定义组件的代码。 下图是框架对于 Component 方法处理流程: [图片] 图中介绍了框架如何对传入 Component 方法的对象的处理,其后面还有很多深入的对于组件实例化的步骤没有在图中表示出来,具体可以在文章最后的附件中查看。 自定义组件在小程序中越来越完善,其拥有的能力也比 Page 更强大,而后面会提到在使用自定义组件的 Page 中,Page 实例也会使用和自定义组件一样的实例化方式,也就是说,他拥有和自定义组件一样的能力。 6、加载页面代码和注册页面 加载页面代码的处理流程和加载自定义组件一样,都是加载完成后先注册页面,然后才会加载下一个页面。 下图是注册一个页面时框架对于 Page 方法的处理流程: [图片] Page 方法会根据是否使用自定义组件做不同的处理。使用自定义组件的 page 对象会被处理为和自定义组件的结构,并在页面实例化时使用不同的处理流程进行实例化。当然对于开发而言没任何不同。 从图中可以发现 Page 传入的(生命周期)代码并不会在这里被执行,可以通过下面小节了解 Page 实例化的详细过程。 7、等待页面 Ready 和 Page 实例化 还记得上面介绍的启动流程中最后一步等待页面 Ready?严格来讲是等待浏览器 Ready,小程序虽然有部分原生的组件,不过本质上还是一个 web 程序。 在小程序中切换页面或打开页面时会触发 onAppRoute 事件,小程序框架通过 wx.onAppRoute 注册页面切换的处理程序,在所有程序就绪后,以 entryPagePath 作为入口使用 appLaunch 的方式进入页面。 下图是处理导航的程序流程: [图片] 从图中可以看出页面的实例化是在进入页面时进行,下图是具体的实例化过程: [图片] 下图是最终可得到 Page 实例: [图片] 可以发现其中多了 onRouteEnd API,实际该接口不会被调用。其中以 component 标记的表示只有在使用了自定义组件时才会有的方法和属性。在前面第 5 小节提到了对于使用自定义组件的页面会按照自定义组件方式解析,这些属性和方法与自定义组件表现一致。 8、关于 setData 小程序框架是一个以数据驱动的框架,当然不能少了对他如何实现数据绑定的探索,下图是 Page 实例的 setData 执行流程: [图片] 其中 component:setData 表示使用自定义组件的 Page 实例的 setData 方法。 三、写在最后 这是一次不完全的小程序框架探索,是在微信开发工具中 debug 的结果。虽然对于实际开发没有什么太大的帮助,但是对框架如何对开发的 js 代码进行处理有了一个很明确的认识,在使用一些 js 特性时可以有明确的感知。如果你还疑惑“小程序框架对传入的对象等到底做了什么”那一定是我表达能力太差,说声对不起。 通过这一次 debug ,也给我引入了新的问题,还希望能够有更多的讨论: · 自定义组件太多启动时会耗时处理自定义组件 · 文件太多会耗时读文件 · 合理的设计分包很重要 当然最后对于框架中已有的能力,还是非常希望微信可以开放更多稳定的接口,并在文档中告知开发者,让开发变得简单一些。
2019-03-05 - 一次在微信小程序里跑 h5 页面的尝试
前言 标题看起来有点唬人,在微信小程序里跑 h5 页面,不会又是说使用 web-view 组件来搞吧?确实,使用 web-view 组件可以达到跑 h5 页面的要求,但是 web-view 组件在使用上还是有一些限制:不支持个人类型与海外类型的小程序、不支持全屏、页面与小程序通信不方便、很多小程序接口无法直接调用等。 那么,还有其他的法子么?理论上来说还是有的。 运行环境 h5 页面是运行在 web 环境下,小程序本身也是基于 web 的,那为什么一直没有办法让 h5 在小程序里直接运行呢?原因在于小程序特有的运行环境。 以一个小程序的页面为例,通常一个小程序的页面至少包含三个文件:wxss 文件、 wxml 文件和 js 文件。其中 wxml 文件和 wxss 文件组成了页面的视图层,js 文件则属于页面的逻辑层,在小程序中,视图层和逻辑层是在不同的线程中执行的。小程序里所有页面的逻辑层都在一个 js 线程中运行,而视图层则分别在不同的 view 线程中。通常一个页面对应一个 view 线程,为了对性能的控制,不会允许用户无节制的启动 view 线程,所以也就有了页面栈数量的限制(目前最多允许打开十层页面)。 [图片] 在 view 线程中是有类似浏览器一样的环境,但是只有页面的视图层在上面跑,页面的渲染完全基于另一个 js 线程传输过来的数据。js 线程是一个纯净的 js 环境,那些你想要调的 document.getElementById、location.href 等 dom/bom 接口通通都没有,你只能在这里执行 js 代码,调用官方提供的接口,而页面的逻辑层就是在这样的线程中跑。这样问题就出来了,页面会渲染成什么样子,完全基于初始模板和数据,你想调接口来修改页面结构,门都没有~ 方案制定 小程序的运行环境如此特殊,以至于它的开发模式也比较另类,但是还是有很多人希望能够将开发过程大一统,一份代码各端运行。那么在限制如此之大的情况下,要怎么做的? 现在市面上有一些基于 react 或 vue 搞出来的工具,它们要求你使用 react 或者 vue 来写页面,由构建工具来编译到各个环境上可运行的目标代码,因为 react 和 vue 本身是基于数据来驱动的组件化框架,在一定程度上限制开发者直接调用 dom/bom 接口,所以可以比较方便的实现代码的编译转化。 PS:因为小程序本身的限制,react 和 vue 的小部分功能也是无法做到完全兼容的,不过大部分的实现都是 OK 的。 不过这不是我想要的,因为它们虽然实现得很漂亮,但是仍有不小的开发局限,你必须选择 vue、react 等框架的其中一种来使用。我想要的是更原生、更底层的大一统方案,底层到除了 vue、react 之外,你甚至可以在小程序中使用 jQuery。 目标太大会扯着蛋,至少得先有一个思路才行。 回到先前提到,小程序的逻辑层是在一个纯净的 js 线程中跑,那里没有 dom/bom 接口,只能运行页面逻辑层的代码,那么我们想办法在逻辑层建一棵 dom 树,把基本的 dom/bom 接口都模拟出来不就行了么? [图片] 乍一想好像可以,但是这里隐藏着一个问题:逻辑层中 dom 树的变更要如何转变成数据并更新到视图层呢? 这里很重要的一点:小程序提供了自定义组件,并且支持递归引用。也就是说,我们可以将 h5 中的 div、span、ul 等 dom 节点转成自定义组件,逻辑层里的每个 dom 节点都会对应一个视图层中的自定义组件实例,节点更新了,我们找到对应的自定义组件实例进行更新即可。 开始实施 方案很简单,但是实现起来的话细节和坑特别多,下面且听我挑几个关键的 trick 来介绍: dom 树的模拟 想要在小程序的逻辑层模拟出一个 dom 树,本质上和建立一棵虚拟树类似,因为它并不是真实的 dom 树。整个流程简单来说是这样的: [图片] 不管是页面中的静态 html 内容还是使用 innerHTML 等接口动态插入的 html 内容都可以走上面的流程来进行 dom 树的创建。dom 树创建比较简单,只是细节比较多,此处的问题是建立好的 dom 树要如何渲染到视图层呢?上面提到了一个关键点:将 dom 节点转成小程序的自定义组件。 此处再重复一次表强调:小程序的自定义组件支持递归引用! 什么叫递归引用?感觉用文字表达不如直接用代码说明来的快,我们先直接编写一个名为 element 的自定义组件: [代码]// element.js Component({ data: { children: ['div', 'span'], } }) [代码] [代码]<!-- element.wxml --> <block wx:for="{{children}}"> <div wx:if="{{item === 'div'}}"></div> <span wx:elif="{{item === 'span'}}"></span> </block> [代码] [代码]{ component: true, usingComponents: { "div": "./element", "span": "./element" } } [代码] 看代码应该很容易就能明白,所谓递归引用就是自己可以作为自己的一个子组件使用,这样可以很方便地将 dom 树给渲染在逻辑层,并且也不需要我们为每种节点标签给编写一个自定义组件,如上述例子,只需要编写一个 element 自定义组件基本可以覆盖所有标签。 同样,根据上述例子可以很明显的看出,每个节点只负责渲染自己的子节点,其他如孙子节点等后代节点一概不管,这样就避免了在更新时需要将整棵树结构传递给视图层的麻烦问题。 对于从逻辑层向视图层传递数据,小程序里有个数据包大小的限制,此处若同步整棵树结构,一来可能爆了这个限制,二来会传递很多无用数据,增大更新开销。 建立联系 创建好 dom 树后,下一步就是要想办法将小程序创建的自定义组件实例和我们创建的 dom 节点给一一联系起来。首先,我们得给每个 dom 节点生成一个节点 id 用于区分,根节点的节点 id 可以在构建过程中直接生成,假定根节点就是 body 节点,那么生成的页面的 wxml 和 json 中大概是这样的: [代码]<!-- page.wxml --> <body data-private-node-id="node-id-xxxxxx"></body> [代码] [代码]{ usingComponents: { "body": "../components/element", } } [代码] body 节点同样是用上面例子中的 element 自定义组件来渲染,修改下 element 自定义组件的 js 文件: [代码]// element.js Component({ data: { children: []. }, attached() { const nodeId = this.dataset.privateNodeId // 节点 id } }) [代码] 如上述代码所述,在自定义组件的 attached 生命周期中即可拿到节点 id,然后根据此节点 id 从 dom 树中找到对应的 dom 节点,两者之间即可建立关系。 找到根节点后,后续节点就好办了。根节点对应的自定义组件实例在和 dom 节点建立联系后,就可以通过 dom 节点拿到子节点列表,进而开始渲染子节点。 由上可知,每个节点只负责渲染自己的子节点,每个节点的渲染流程都和根节点一样: 拿到节点 id 和 dom 节点建立联系 通过 dom 节点拿到子节点列表 渲染子节点 根据这个逻辑修改一下上述例子中的 element 自定义组件的代码: [代码]// element.js Component({ data: { children: []. }, attached() { const nodeId = this.dataset.privateNodeId // 节点 id const domNode = getDomNode(nodeId) // 根据节点 id 找到 dom 节点 const children = domNode.children // 拿到子节点列表 // 渲染子节点 this.setData({ children, }) } }) [代码] [代码]<!-- element.wxml --> <block wx:for="{{children}}"> <div wx:if="{{item.tagName === 'DIV'}}" data-private-node-id="{{item.nodeId}}"></div> <span wx:elif="{{item.tagName === 'SPAN'}}" data-private-node-id="{{item.nodeId}}"></span> </block> [代码] 如此递归下去,即可将自定义组件实例和 dom 节点一一联系起来,同时将整棵 dom 树渲染出来。 渲染信息的同步 假若逻辑层想要获取视图层的渲染信息,比如调 [代码]getComputedStyle[代码] 接口获取节点样式、使用 [代码]clientWidth[代码] 或取节点宽度等,因为逻辑层本身只是模拟了 dom 树,并没有实现布局和渲染的模拟(理论上可以实现,只是开销巨大,而且很难与小程序本身环境的渲染结果对齐),所以这块实现起来很头疼。 目前的方案上使用小程序的 [代码]selectorQuery[代码] 接口来拉取渲染信息,因为此接口只能异步拉取,所以没法完整模拟渲染信息的即时同步。为了尽可能做到相对同步,在初始渲染完成后尝试拉取一次渲染信息,之后在每次触发节点更新后再异步拉取渲染信息,同时提供一个异步接口给某些需要即时拉取渲染信息的场景中使用。 全局对象的处理 上面提到小程序的逻辑层是跑在一个 js 线程中,这个 js 线程是一个纯净的 js 线程,别说那些 bom、dom 接口了,连一个正经的全局对象都没有。 小程序中其实有 global 对象,但是和我们熟悉的 nodejs 中的 global 对象实现不一样,所以此处按下不提。 做前端开发的同学们应该都知道,h5 环境中声明在全局的变量/函数会挂在 window 下,在页面的其他地方是可以使用或者是通过 [代码]window.xxx[代码] 的方式来访问的。在小程序的运行环境中这些是不存在的,那就意味着会有下述这些问题: [代码]// a.js function xxx() {} window.yyy = function() {} // b.js window.xxx() // 报 xxx 不存在 yyy() // 报 yyy 不存在 [代码] 这个问题很严重,要知道我们很多时候访问一些对象都会忽略掉 [代码]window.[代码] 这个前缀,比如使用 location 对象的时候。也就是说,必须对这些 js 做一遍后处理,强制将全局函数挂在 window 下,强制通过使用 [代码]window.xxx[代码] 的方式访问全局变量/函数。经过后处理后,上述例子会变成下面这样: [代码]// a.js function xxx() {} window.yyy = function() {} window.xxx = xxx // 强制挂在 window 下 // b.js window.xxx() // 正常执行 window.yyy() // 强制补充 window 前缀,正常执行 [代码] 嗯,完美解决! 其他 上面提到的是整体的实现框架,也简单的提到几个坑。当然,这只不过是冰山一角,事实上需要兼容的坑、无法按照标准实现的接口和完全无法实现的特性犹如满天星,数都数不清,幸运的是我们并不完全依赖到所有的接口,有些时候我们仅仅需要用到像 querySelector、事件系统、异步请求等接口就可以构造出大部分 h5 页面,而这些大多是能做到兼容的,所以对于某些不太复杂的场景来说还是可行的。 工具试用 按照上面的思路实现了一个差不多的构建工具,暂且命名为 [代码]h5-to-miniprogram[代码],只看名字就知道这是个想干嘛的工具,不过这终究是个玩票性质的尝试,有兴趣的同学也可以点击此处翻看源码~
2019-02-27 - 小程序性能和体验优化方法
[图片] 小程序应避免出现任何 JavaScript 异常 出现 JavaScript 异常可能导致小程序的交互无法进行下去,我们应当追求零异常,保证小程序的高鲁棒性和高可用性 小程序所有请求应响应正常 请求失败可能导致小程序的交互无法进行下去,应当保证所有请求都能成功 所有请求的耗时不应太久 请求的耗时太长会让用户一直等待甚至离开,应当优化好服务器处理时间、减小回包大小,让请求快速响应 避免短时间内发起太多的图片请求 短时间内发起太多图片请求会触发浏览器并行加载的限制,可能导致图片加载慢,用户一直处理等待。应该合理控制数量,可考虑使用雪碧图技术或在屏幕外的图片使用懒加载 避免短时间内发起太多的请求 短时间内发起太多请求会触发小程序并行请求数量的限制,同时太多请求也可能导致加载慢等问题,应合理控制请求数量,甚至做请求的合并等 避免 setData 的数据过大 setData工作原理 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。 由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间 常见的 setData 操作错误 频繁的去 setData Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层 染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时 每次 setData 都传递大量新数据 由setData的底层实现可知,数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程 后台态页面进行 setData 当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行 避免 setData 的调用过于频繁 setData接口的调用涉及逻辑层与渲染层间的线程通过,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用 避免将未绑定在 WXML 的变量传入 setData setData操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入setData会造成不必要的性能消耗 合理设置可点击元素的响应区域大小 我们应该合理地设置好可点击元素的响应区域大小,如果过小会导致用户很难点中,体验很差 避免渲染界面的耗时过长 渲染界面的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要校验下是否同时渲染的区域太大 避免执行脚本的耗时过长 执行脚本的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要确认并优化脚本的逻辑 对网络请求做必要的缓存以避免多余的请求 发起网络请求总会让用户等待,可能造成不好的体验,应尽量避免多余的请求,比如对同样的请求进行缓存 wxss 覆盖率较高,较少或没有引入未被使用的样式 按需引入 wxss 资源,如果小程序中存在大量未使用的样式,会增加小程序包体积大小,从而在一定程度上影响加载速度 文字颜色与背景色搭配较好,适宜的颜色对比度更方便用户阅读 文字颜色与背景色需要搭配得当,适宜的颜色对比度可以让用户更好地阅读,提升小程序的用户体验 所有资源请求都建议使用 HTTPS 使用 HTTPS,可以让你的小程序更加安全,而 HTTP 是明文传输的,存在可能被篡改内容的风险 不使用废弃接口 使用即将废弃或已废弃接口,可能导致小程序运行不正常。一般而言,接口不会立即去掉,但保险起见,建议不要使用,避免后续小程序突然运行异常 避免过大的 WXML 节点数目 建议一个页面使用少于 1000 个 WXML 节点,节点树深度少于 30 层,子节点数不大于 60 个。一个太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长 避免将不可能被访问到的页面打包在小程序包里 小程序的包大小会影响加载时间,应该尽量控制包体积大小,避免将不会被使用的文件打包进去 及时回收定时器 定时器是全局的,并不是跟页面绑定的,当页面因后退被销毁时,定时器应注意手动回收 避免使用 css ‘:active’ 伪类来实现点击态 使用 css ‘:active’ 伪类来实现点击态,很容易触发,并且滚动或滑动时点击态不会消失,体验较差 建议使用小程序内置组件的 ‘hover-*’ 属性来实现 滚动区域可开启惯性滚动以增强体验 惯性滚动会使滚动比较顺畅,在安卓下默认有惯性滚动,而在 iOS 下需要额外设置 [代码]-webkit-overflow-scrolling: touch[代码] 的样式
2019-03-15