- 1小时实战入门小程序开发,历史上的今天案例讲解
我们前面学了这么多的小程序基础知识,一直没有用一个实际的案例来把前面的知识点串起来,今天我们就来开发一款简单的《历史上的今天》,来把我们前面的知识点完整的串起来。 老规矩,先看效果图 [图片] 可以看到我们实现了如下功能 1,列表页 2,列表跳转详情页 3,视频播放(其实是假的,后面给大家讲这个视频播放) 4,网络请求 5,列表到详情数据携带 好了,话不多说,我们来直接看代码实现。 一,网络数据的获取 网络数据获取我们用来官方提供的wx.request方法。 下面红色框里就是我们的网络数据获取的代码 [图片] 是不是感觉就这几段代码,很简单,事实上,石头哥写这段代码费老劲了。。。 不是说代码难写。主要是因为我这里用到的是一个三方提供的api,刚开始是想着用豆瓣电影的api,可是。。。 豆瓣之前的免费api好像停了,再去找一些api吧,基本上都收费了。这个api也是找了好久,才找到了百度提供的一个“历史上的今天百科” api,接口很简单如下图: [图片] 我们只需要用这个api,简单的做下get请求,就能请求下来一大堆数据。 [图片] 而这一大堆数据也正是我们所需要的列表数据。 二,首页列表数据的解析 上面第一步已经获取到了我们所需要的数据,但是那么一大坨,我们该怎么使用呢,所以,使用之前我们要对数据做一个简单的解析。这样我们才可以显示到我们的桌面上。话不多说,我们直接写代码来获取数据。 核心代码就是我们下图红色框,框起来的这部分。 [图片] 再来看下我们请求到的数据。通过下图可以看到,小程序使用wx.request在请求数据的同时,已经把数据给我们解析好了。 [图片] 但是这里有个问题,我们请求的数据一下子把整个12月历史上的今天,都返回了。我们只想取到今天的数据,也就是12月14日的数据。该怎么取呢。 因为这里对象里的属性值不是我们传统的name,age。。。。这样的字母样式的,而是用一个数字,比如1201来作为对象里的一个属性。这样我们取值的时候就不能用传统的 object.name 这样的方式了。 当然直接用res.data.12会报错的。如下图 [图片] 所以呢我们就换种方式,比如我们先通过 res.data[‘12’]先把所有12月的数据都取到。 [图片] 然后再通过 res.data[‘12’][‘1214’]来取12月14日的数据。如下图 [图片] 这样我们就成功的取到了历史上的12月14日的16条数据,我们接下来要做的就是把这16条数据,展示到页面上。 三,首页数据的展示 其实列表的展示,我之前写过好多文章讲解的,大家可以去翻下我之前的文章,也可以看下我之前录的讲解视频 《10小时零基础快速入门小程序开发》 我这里直接把关键代码贴出来给大家。 1,index.wxml [图片] 2,index.js [图片] 3,index.wxss [图片] 这样我们的首页展示就实现了,接下来看我们的详情页 四,详情页 [图片] 可以看出我们的详情页很简单,就一个webview,但是功能确很丰富。 [图片] 当然这一切都拜webview这个强大的组件所赐。至于如何实现这个视频功能的,我视频里有说的。偷笑。。。。。 《10小时零基础快速入门小程序开发》 还是接着讲我们的这个详情页,首先我们要实现的是首页列表点击,跳转到详情页。这里还要贴出首页的代码了 [图片] 上图的bindtap用来实现点击事件,data-link用来在点击的时候传递值。 [图片] 看上图的点击事件的实现,可以看出,我们是在点击的时候拿到一个link值,然后把这个值传递到详情页,而这个值,就是我们webview用来展示网页的链接。 [图片] 这个时候我们的详情页,其实就相当于一个浏览器了,你往里面传递不同的网址,我们就能显示不同的内容。 其实到这里我们就基本上实现了我们的功能了。 下面把index.js的完整代码贴给大家。 [代码]Page({ data: { dataList: [], yueRi: '' }, onLoad() { let month = this.getMonth() let monthDay = this.getTime() let yueRi = this.getFullTime() let that = this wx.request({ url: `https://baike.baidu.com/cms/home/eventsOnHistory/${month}.json`, success(res) { console.log("请求成功", res.data['12']['1214']) that.setData({ dataList: res.data[month][monthDay], yueRi }) }, fail(res) { console.log("请求失败", res) } }) }, //跳转到详情页 goDetail(event) { let link = event.currentTarget.dataset.link console.log(link) wx.navigateTo({ url: '/pages/detail/detail?link=' + link, }) }, //获取月日 getTime() { let date = new Date() let month = date.getMonth() + 1 if (month < 10) { month = '0' + month } let day = date.getDate() if (day < 10) { day = '0' + day } let monthDay = '' + month + day console.log(monthDay) return monthDay }, //获取月份呢 getMonth() { let date = new Date() let month = date.getMonth() + 1 if (month < 10) { month = '0' + month } return month }, //获取标准的月日 getFullTime() { let date = new Date() let month = date.getMonth() + 1 if (month < 10) { month = '0' + month } let day = date.getDate() if (day < 10) { day = '0' + day } let monthDay = month + '月' + day + '日' console.log(monthDay) return monthDay }, }) [代码] 好了,今天就到这里了,后面会分享给大家更多的关于小程序实战入门的案例,敬请期待。 我这里也有把这个案例录制一套视频出来,感兴趣的同学可以去看下 https://study.163.com/course/courseMain.htm?courseId=1209460834
2019-12-16 - PHP获取小程序openid,10行代码快速获取小程序openid
上一节教大家如何在本地运行php后台项目,并可以被小程序访问到,这一节就来给大家讲一个实际工作中常用的需求,微信小程序openid的获取。当然了,还是用我们的php做为后台。 老规矩,先看效果图 [图片] 通过上图我们可以看到我们成功的获取到了小程序的openid,而这里获取openid是借助php后台获取的。 我们的php核心代码,只有下面10行 [图片] 下面就来讲讲具体的实现步骤。 一,首先要搭建一个可以本地访问的php后台 这里我在上一节的文章已经讲过了,还不知道怎么配置本地php项目的同学可以去翻看我上篇文章:phpstorm运行本地PHP服务器,实现小程序可以访问的PHP后台 二,编写php代码 只需要创建一个如下图所示的简单的php文件就可以了。 [图片] 把代码给大家贴出来。要记得把下面的appid和appSecret换成你自己的。至于如果获取,我之前的文章也有讲过,去翻下我零基础入门小程序开发的文章即可。 [代码]<?php /** * 2019/9/21 20:34 * author: 编程小石头 * wechat:2501902696 * desc: 获取小程序用户openid */ getOpenid(); function getOpenid() { $code = $_GET['code'];//小程序传来的code值 $appid = 'wx4b55bb240aec2ee3';//小程序的appid $appSecret = '1f6f68884c1add6293cfa9b86e1f6bfd';// 小程序的$appSecret $wxUrl = 'https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code'; $getUrl = sprintf($wxUrl, $appid, $appSecret, $code);//把appid,appsecret,code拼接到url里 $result = curl_get($getUrl);//请求拼接好的url $wxResult = json_decode($result, true); if (empty($wxResult)) { echo '获取openid时异常,微信内部错误'; } else { $loginFail = array_key_exists('errcode', $wxResult); if ($loginFail) {//请求失败 var_dump($wxResult); } else {//请求成功 $openid = $wxResult['openid']; echo "获取openid成功成功:" . $openid; } } } //php请求网络的方法 function curl_get($url, &$httpCode = 0) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); //不做证书校验,部署在linux环境下请改为true curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); $file_contents = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return $file_contents; } [代码] 代码就这么多,我们编写好以后,运行下项目。 [图片] 三,编写小程序代码 小程序的代码很简单,就是一个按钮,点击按钮的时候我们请求上面定义好的php后台项目。 小程序代码也很简单 [图片] [图片] 代码就上图这么点,我把code.js里的代码贴出来给大家 [代码]Page({ getCode() { let that = this; wx.login({ success(res) { console.log('code', res.code) that.getOpenid(res.code) } }) }, getOpenid(wxCode) { wx.request({ url: 'http://localhost:8080/Demo.php', data: { code: wxCode }, success(res) { console.log('获取成功', res) }, fail(res) { console.log('获取失败', res) } }) } }) [代码] 给大家解释下上面的代码,我们通过wx.login来获取code,然后通过wx.request调用我们定义的php后台。url里的 Demo.php必须和你的php文件名保持一致。 [图片] 效果 点击按钮以后,就可以成功的获取到我们所需的小程序openid了。效果图如下 [图片] 到这里我们就成功的实现了PHP获取小程序openid的功能了,是不是很简单。 后面我会写更多关于php和小程序的文章,敬请关注。
2019-09-21 - phpstorm运行本地PHP服务器,实现小程序可以访问的PHP后台
今天就来正式给大家讲解PHP相关的知识了,本着使用优先的原则,今天还是先给大家运行一个小程序可以访问的PHP后台项目。 老规矩先看效果图 [图片] 通过效果图我们可以看出来,我们的小程序可以成功的访问到我们的后台数据,而这个后台就是用PHP搭建的一个本地服务器。 准备工作 1,安装phpstorm(php开发者工具) 2,安装PHP(这里以PHP5.6为例) 一,创建PHP项目 1,创建项目 [图片] 2,设置项目目录 [图片] 3,创建一个PHP文件 [图片] 4,简单的写一句PHP代码 [图片] 来简单解释下,所有的PHP代码必须以 <?php 作为开头。 echo 是用来输出一段话用的。 二,配置本地服务器 1,进入设置页,然后找到 Languages&Frameworks下的PHP,安装箭头所示,点击 … [图片] 2,这里点击+号,选择PHP安装包 [图片] 3,这里使用的是php5.6安装包 [图片] 4,可以看到 CLI Interpreter里使用了PHP5.6 [图片] 5,点击下面箭头所示进入服务器信息配置页 [图片] 6,选择 PHP Built-in Web Serve [图片] 7,配置服务器信息 [图片] 三,启动PHP本地服务器 1,点击下图所示绿色三角 [图片] 2,只要不报红,不报错,就代表启动成功 [图片] 四,访问PHP服务器 1,浏览器里访问,可以看到已经成功访问到了 [图片] 2,代码里动态修改,然后刷新浏览器,可以看到不用重启服务器就可以动态刷新数据了。 [图片] 3,小程序里访问PHP后台数据 [图片] 可以看到我们小程序也可以成功的访问到PHP后台数据了。 是不是很简单,后期我会写出一系列实战入门PHP和小程序开发的系列文章,敬请关注。
2019-09-03 - 从微信小程序到Taro,不完全指南
Taro介绍简介Taro 是一套遵循React语法规范的多端统一开发 框架。使用Taro,我们可以只书写一套代码,再通过 Taro 的编译工具,将源代码分别编译出可以在不同端(微信/百度/支付宝/字节跳动/QQ小程序、快应用、H5、React-Native 等)运行的代码。 特性React 语法风格Taro 遵循 React 语法规范,它采用与 React 一致的组件化思想,组件生命周期与 React 保持一致,同时支持使用 JSX 语法,让代码具有更丰富的表现力,使用 Taro 进行开发可以获得和 React 一致的开发体验。 快速开发微信小程序Taro 立足于微信小程序开发,众所周知小程序的开发体验并不是非常友好,比如小程序中无法使用 npm 来进行第三方库的管理,无法使用一些比较新的 ES 规范等等,针对小程序端的开发弊端,Taro 具有以下的优秀特性 支持使用 npm/yarn 安装管理第三方依赖支持使用 ES7/ES8 甚至更新的 ES 规范,一切都可自行配置支持使用 CSS 预编译器,例如 Sass 等支持使用 Redux 进行状态管理小程序 API 优化,异步 API Promise 化支持多端开发转化Taro 方案的初心就是为了打造一个多端开发的解决方案。目前 Taro 代码可以支持转换到 微信/百度/支付宝/字节跳动/QQ小程序 、快应用、 H5 端 以及 移动端(React Native)。 安装及使用安装node 环境(>=8.0.0);Taro开发工具@tarojs/cli# 使用 npm 安装 CLI $ npm install -g @tarojs/cli 使用使用命令创建模板项目 $ taro init myApp 项目结构 ├── config 配置目录 | ├── dev.js 开发时配置 | ├── index.js 默认配置 | └── prod.js 打包时配置 ├── src 源码目录 | ├── components 公共组件目录 | ├── pages 页面文件目录 | | ├── index index 页面目录 | | | ├── banner 页面 index 私有组件 | | | ├── index.js index 页面逻辑 | | | └── index.css index 页面样式 | ├── utils 公共方法库 | ├── app.css 项目总通用样式 | └── app.js 项目入口文件 └── package.json 各端dev环境启动 $ npm run dev:h5 web $ npm run dev:weapp 微信小程序 $ npm run dev:swan 百度小程序 $ npm run dev:alipay 支付宝小程序 $ npm run dev:tt 字节跳动小程序 $ npm run dev:qq QQ小程序 $ npm run dev:rn 生命周期React框架中的生命周期函数被保留,新增支持小程序的几个生命周期 componentWillMount() 页面加载时触发,一个页面只会调用一次,此时页面 DOM 尚未准备好,还不能和视图层进行交互 componentDidMount() 页面初次渲染完成时触发,一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互 shouldComponentUpdate(nextProps, nextState) 页面是否需要更新,返回 false 不继续更新,否则继续走更新流程 componentWillUpdate(nextProps, nextState) 页面即将更新 componentDidUpdate(prevProps, prevState) 页面更新完毕 componentWillUnmount() 页面卸载时触发,如 redirectTo 或 navigateBack 到其他页面时 componentDidShow() 页面显示/切入前台时触发 componentDidHide() 页面隐藏/切入后台时触发, 如 navigateTo 或底部 tab 切换到其他页面,小程序切入后台等 页面事件处理函数增加在小程序中的专属事件处理函数,如下: onPullDownRefresh(): 监听用户下拉刷新事件onReachBottom():监听用户上拉触底事件onPageScroll(Object):监听用户滑动页面事件onShareAppMessage(Object): 监听用户点击页面内转发按钮(Button 组件 openType='share')或右上角菜单“转发”按钮的行为,并自定义转发内容。onTabItemTap(Object):点击 tab 时触发设计稿及尺寸单位在 Taro 中尺寸单位建议使用 px、 百分比 %,Taro 默认会对所有单位进行转换。在 Taro 中书写尺寸按照 1:1 的关系来进行书写,即从设计稿上量的长度 100px,那么尺寸书写就是 100px,当转成微信小程序的时候,尺寸将默认转换为 100rpx,当转成 H5 时将默认转换为以 rem 为单位的值。 如果你希望部分 px 单位不被转换成 rpx 或者 rem ,最简单的做法就是在 px 单位中增加一个大写字母,例如 Px 或者 PX 这样,则会被转换插件忽略。 Taro 默认以 750px作为换算尺寸标准,如果设计稿不是以 750px 为标准,则需要在项目配置 config/index.js 中进行设置,例如设计稿尺寸是 640px,则需要修改项目配置 config/index.js 中的 designWidth 配置为 640: const config = { projectName: 'myProject', date: '2018-4-18', designWidth: 640, .... } 目前 Taro 支持 750、 640 、 828 三种尺寸设计稿,他们的换算规则如下: const DEVICE_RATIO = { '640': 2.34 / 2, '750': 1, '828': 1.81 / 2 } 路由功能在 Taro 中,路由功能是默认自带的,不需要开发者进行额外的路由配置。 我们只需要在入口文件的 config 配置中指定好 pages,然后就可以在代码中通过 Taro 提供的 API 来跳转到目的页面,例如: // 跳转到目的页面,打开新页面 Taro.navigateTo({ url: '/pages/page/path/name' }) 路由传参我们可以通过在所有跳转的 url 后面添加查询字符串参数进行跳转传参,例如 // 传入参数 id=2&type=test Taro.navigateTo({ url: '/pages/page/path/name?id=2&type=test' }) 这样的话,在跳转成功的目标页的生命周期方法里就能通过 this.$router.params 获取到传入的参数,例如上述跳转,在目标页的 componentWillMount 生命周期里获取入参 class C extends Taro.Component { componentWillMount () { console.log(this.$router.params) // 输出 { id: 2, type: 'test' }// 输出 { id: 2, type: 'test' } } } state状态管理&Propsstate状态更新一定是异步的,同步更新需要传入callBack 这是 Taro 和 React 另一个不同的地方:React 的 setState 不一定总是异步的,而对于 Taro 而言,setState 之后,你提供的对象会被加入一个数组,然后在执行下一个 eventloop 的时候合并它们。 例如: // 假设我们之前设置了 this.state.counter = 0 updateCounter () { this.setState({ counter: 1 }) console.log(this.state.counter) // 这里 counter 还是 0 } 正确的做法是这样,在 setState 的第二个参数传入一个 callback: // 假设我们之前设置了 this.state.counter = 0 updateCounter () { this.setState({ counter: 1 }, () => { // 在这个函数内你可以拿到 setState 之后的值 }) }Ï 任何组件的事件传递都要以 on 开头但在 Taro 中,只要当 JSX 组件传入的参数(props)是函数,参数名就必须以 on 开头: const element =
const element2 = const element3 = 环境变量 process.env 的使用不要以解构的方式来获取通过 env 配置的 process.env 环境变量,请直接以完整书写的方式 process.env.NODE_ENV 来进行使用 // 错误写法,不支持 const { NODE_ENV = 'development' } = process.env if (NODE_ENV === 'development') { ... } // 正确写法 if (process.env.NODE_ENV === 'development') { } 环境判断Taro 在编译时提供了一些内置的环境变量来帮助用户做一些特殊处理。用于判断当前编译类型,可以通过这个变量来书写对应一些不同环境下的代码,在编译时会将不属于当前编译类型的代码去掉,只保留当前编译类型下的代码。 Taro.ENV_TYPEENV_TYPE.WEAPP 微信小程序环境 ENV_TYPE.SWAN 百度小程序环境 ENV_TYPE.ALIPAY 支付宝小程序环境 ENV_TYPE.TT 字节跳动小程序环境 ENV_TYPE.WEB WEB(H5)环境 ENV_TYPE.RN ReactNative 环境 关于 JSX 支持程度补充说明由于 JSX 中的写法千变万化,我们不能支持到所有的 JSX 写法,同时由于微信小程序端的限制,也有部分 JSX 的优秀用法暂时不能得到很好地支持,特在此补充说明一下对于 JSX 的支持程度: 不能使用 Array#map 之外的方法操作 JSX 数组Taro 在小程序端实际上把 JSX 转换成了字符串模板,而一个原生 JSX 表达式实际上是一个 React/Nerv 元素(react-element)的构造器,因此在原生 JSX 中你可以随意地一组 React 元素进行操作。但在 Taro 中你只能使用 map 方法,Taro 转换成小程序中 wx:for。 规则详情以下代码会被 ESLint 提示警告,同时在 Taro(小程序端)也不会有效: test.push( ) numbers.forEach(numbers => { if (someCase) { a = } }) test.shift( ) components.find(component => { return component === }) components.some(component => component.constructor.__proto__ === .constructor) 以下代码不会被警告,也应当在 Taro 任意端中能够运行: numbers.filter(Boolean).map((number) => { const element = return }) 暂不支持在 render() 之外的方法定义 JSX由于微信小程序的 template 不能动态传值和传入函数,Taro 暂时也没办法支持在类方法中定义 JSX。 规则详情以下代码会被 ESLint 提示警告,同时在 Taro(小程序端)也不会有效: class App extends Component { _render() { return } } class App extends Component { renderHeader(showHeader) { return showHeader && } } class App extends Component { renderHeader = (showHeader) => { return showHeader& & } } 解决方案在 render 方法中定义。 class App extends Component { render () { const { showHeader, showMain } = this.state const header = showHeader && const main = showMain && return ( {header} {main} /View> ) } } 不能在 JSX 参数中使用对象展开符微信小程序组件要求每一个传入组件的参数都必须预先设定好,而对象展开符则是动态传入不固定数量的参数。所以 Taro 没有办法支持该功能。 规则详情以下代码会被 ESLint 提示警告,同时在 Taro(小程序端)也不会有效: 以下代码不会被警告,也应当在 Taro 任意端中能够运行: const { id, ...rest } = obj const [ head, ...tail] = array const obj = { id, ...rest } 异步编程Taro 支持使用 async functions 来让开发者获得不错的异步编程体验,开启 async functions 支持需要安装包 @tarojs/async-await $ npm install --save @tarojs/async-await 随后在项目入口文件 app.jsx 中直接 import ,就可以开始使用 async functions 功能了 // src/app.jsx import '@tarojs/async-await' 使用Redux在 Taro 中可以自由地使用 React 生态中非常流行的数据流管理工具 Redux 来解决复杂项目的数据管理问题。而为了更方便地使用 Redux ,Taro 提供了与 react-redux API 几乎一致的包 @tarojs/redux 来让开发人员获得更加良好的开发体验。 首先请安装 redux 、 @tarojs/redux 和 @tarojs/redux-h5,以及一些需要用到的 redux 中间件 $ npm install --save redux @tarojs/redux @tarojs/redux-h5 redux-thunk redux-logger 随后可以在项目 src 目录下新增一个 store 目录,在目录下增加 index.js 文件用来配置 store,按自己喜好设置 redux 的中间件,例如下面例子中使用 redux-thunk 和 redux-logger 这两个中间件 // src/store/index.js import { createStore, applyMiddleware } from 'redux' import thunkMiddleware from 'redux-thunk' import { createLogger } from 'redux-logger' import rootReducer from '../reducers' const middlewares = [ thunkMiddleware, createLogger() ] export default function configStore () { const store = createStore(rootReducer, applyMiddleware(...middlewares)) return store } 接下来在项目入口文件 app.js 中使用 @tarojs/redux 中提供的 Provider 组件将前面写好的 store 接入应用中 // src/app.js import Taro, { Component } from '@tarojs/taro' import { Provider } from '@tarojs/redux' import configStore from './store' import Index from './pages/index' import './app.scss' const store = configStore() class App extends Component { config = { pages: [ 'pages/index/index' ], window: { navigationBarTitleText: 'Test' } } render() { return ( /Provider> ) } } Taro.render( , document.getElementById('app')) 然后就可以开始使用了。如 redux 推荐的那样,可以增加 constants 目录,用来放置所有的 action type 常量actions 目录,用来放置所有的 actionsreducers 目录,用来放置所有的 reducers例如我们要开发一个简单的加、减计数器功能 新增 action type // src/constants/counter.js export const ADD = 'ADD' export const MINUS = 'MINUS' 新增 reducer 处理 // src/reducers/counter.js import { ADD, MINUS } from '../constants/counter' const INITIAL_STATE = { num: 0 } export default function counter (state = INITIAL_STATE, action) { switch (action.type) { case ADD: return { ...state, num: state.num + 1 } case MINUS: return { ...state, num: state.num - 1 } default: return state } } // src/reducers/index.js import { combineReducers } from 'redux' import counter from './counter' export default combineReducers({ counter }) 新增 action 处理 // src/actions/counter.js import { ADD, MINUS } from '../constants/counter' export const add = () => { return { type: ADD } } export const minus = () => { return { type: MINUS } } 最后,我们可以在页面(或者组件)中进行使用,我们将通过 tarojs/redux 提供的 connect 方法将 redux 与我们的页面进行连接 // src/pages/index/index.js import Taro, { Component } from '@tarojs/taro' import { View, Text } from '@tarojs/components' import { connect } from '@tarojs/redux' import './index.scss' import { add, minus } from '../../actions/counter' @connect(({ counter }) => ({ counter }), (dispatch) => ({ add () { dispatch(add()) }, dec () { dispatch(minus()) } })) class Index extends Component { config = { navigationBarTitleText: '首页' } render () { return ( 2020-08-28 - wx.request 重复向服务器发送请求问题
- 当前 Bug 的表现(可附上截图) - 预期表现 - 复现路径 - 提供一个最简复现 Demo 今天发现问题,调用一次wx.request会重复向自己的服务器发送请求(每请求一次会在数据库中存一条带时间的记录,所以发现了问题),一般3次,一秒以内完成,排除误触,有时候间隔一段时间会有第4次。不通过wx.request直接调用没有重复的问题,应该是wx.request本身的机制或bug。有明白人能指点一下吗?
2019-06-06 - 微信小程序Page间数据传递解决方案分析
内容概要 利用微信小程序的路由堆栈信息解决小程序内Page间的数据(或事件)传递的问题。通过对各种方案的对比、分析、总结,得出我们的升华版解决方案,满足你的不满足。 场景再现 工作中我们经常会遇到B页面需要A页面内的部分或全部数据;C页面内的一个函数执行完之后需要改变B页面内的显示样式;也或者是A和B两个页面用到了同样的网络数据,在其中一个页面做出修改后另一个页面也要随之改变以保证回传服务器时数据的准确性,等等诸如此类的页面间数据传递的问题。 现有方案梳理 当然针对上面场景中各种问题我们可以有很多种的解决方案。例如利用微信API中reLaunch、redirectTo、navigateTo 函数的url路径携带参数到目标页面;利用EventChannel信道实现打开页面与被打开页面通信;利用全局文件共享数据;利用路由堆栈获取目标页面实例等等,我目前用过的方案就以上四种,如果你用过其他方案也可以提出来我们一起讨论学习。接下来我们就针对以上四种方案进行一个简略的分析。 方案一:reLaunch、redirectTo、navigateTo 函数url路径携带参数到目标页面 这种方案大家应该都用过,在页面跳转过程中传递一些简单的数据还是十分方便的,但是它的缺点也很明显,在五个路由函数中只有三个可以在跳转路径上携带参数,而且参数不可以是对象类型,在遇到携带"?"等特殊字符的参数还需要进行转码操作。对于跨多个页面的数据传递比较繁琐。归纳如下: 优点:操作简单方便。 缺点:单项传递;复杂数据传递局限性较大;跨多页面数据传递繁琐;只能在reLaunch、redirectTo、navigateTo 三个函数中使用。 适用情况:页面间简单数据的单项传递。 方案二:EventChannel信道实现打开页面与被打开页面通信 从基础库2.7.3开始支持EventChannel。开发者可以通过navigateTo路由函数在页面跳转切换过程中自定义两个页面的数据交互函数。至于EventChannel自定义规则可参考微信API中的 navigateTo 函数。该方案解决了方案一中的复杂数据传递的局限性。可是它仅限于navigateTo函数中使用,并且在跨多页面传递时操作起来复杂性仍然很高。归纳如下: 优点:使用灵活性较高;可传递较复杂数据;可双向传递; 缺点:跨多页面数据传递繁琐;只能在navigateTo一个函数中使用。 适用情况:由navigateTo控制路由的两个页面间数据传递。 方案三:全局文件共享数据 全局共享数据无非就是定义一份谁都可以使用和修改的数据。这个方案很简单,而且很轻松的解决了方案一和方案二中的跨多个页面的数据传递问题。可是数据处理的及时性却大打折扣,只能期待各个页面自己触发自身的事件。归纳如下: 优点:实现简单,可跨多页面。 缺点:及时性欠缺,灵活度不够。 适用情况:不要求事件的及时性和功能比较集中的模块。 方案四:利用路由堆栈获取目标页面实例 兵法中常说“擒贼先擒王”,如果我们拿到了某个页面的实例索引,那就相当于是在战场上控制了敌方的将领,我们说要粮草他就得给粮草,我们说要兵器他就得乖乖的给兵器。所以该方案我们也可以戏称为“擒王方案”。如果我们给“擒王方案”做一个归纳的话,应该是这样的: 优点:灵活性/及时性高;数据类型不限;可跨多页面使用; 缺点:代码重复性较高;不在堆栈内的页面无法进行操作; 适用情况: 确定页面实例在堆栈内的交互性比较强多页面。 提炼升华 在上一节中我们对四个方案进行了一下简单的梳理,每个方案也各有优缺点,上述四种方案可能已经满足了我们工作中的使用,可是作为程序员的我们不应该停下追逐更优更好的脚步。我们尽量把上述方案的优点集中起来,并且规避掉缺点。整理出一个相对完善的方案。首先自定义一个跨页面(当然也可在页面内使用)的事件处理类,暂时命名其为funbus。具体的处理逻辑如下: 一:定义全局事件缓存Map。 [代码]// 事件缓存 const events = {}; [代码] 二:根据 getCurrentPages() 函数获取被操作页面实例。 [代码]/** * 同步执行,会立即执行并拿到被执行函数的返回结果 * * pagePath 页面名称或路径 * method 执行的方法名 * params 方法参数 */ function callFun(pagePath, method, params) { let pages = getCurrentPages(); let page = null; for (let i = 0; i < pages.length; i++) { if (pages[i].route.indexOf(pagePath) > -1) { page = pages[i]; break; } } if (page) { try { return page[method](...params); } catch (err) { console.error('FunBus Error: ', err); return null; } } return null; } [代码] 三:当然我们也需要处理如果被操作的页面不在堆栈内的情况。 1.事件注册和解绑。 [代码]/** * 注册事件 * * key 值命名规则 页面名称-描述 (如: index-refresh) * event 是一个对象: {path: '被注册事件发生的页面路径', method: '被注册的方法名称', params: [...被注册的方法需要的参数]} * * 调用范例: subscribe("index-refresh", {path:"pages/index", method:"refreshPage", params: [1,2,3]}); */ function subscribe(key, event) { events[key] = event; } /** * 解除绑定 */ function unSubscribe(key) { delete events[key]; } [代码] 2.之前未在堆栈内的页面加入到堆栈时(即展示到前台时)在适当时机触发之前缓存的事件。 [代码]/** * 唤醒/执行之前订阅的事件 */ function notifyEvent(key, remove) { let event = events[key]; if (event) { // 只有remove的值是布尔类型的false时才不会移除当前事件,其他任何值该事件都会被移除 if (!remove && remove !== false) { remove = true; } remove && delete events[key]; return callFun(event.path, event.method, event.params) } return null; } [代码] 四:工具类的使用 1. 直接使用 [代码]let result = funbus.callFun('a/a', 'returnBpageData', [1, 2]); this.setData({ astring: result, }); [代码] 2. 页面未在堆栈内的使用 [代码]// C页面内 // 由于B页面跳转C是重定向 redirectTo 跳过来的,所以B页面不在路由堆栈内,我们要先注册,然后再B页面里适当的时机出发该函数。 funbus.subscribe('b-changebgc', { path: 'b/b', method: 'changeBgColor', params:['yellow']}); // B页面内 // 触发在C页面注册的函数 funbus.notifyEvent('b-changebgc'); [代码] 五:传送门 Demo 下载 总结 本文大致可以分为两个重点部分。前一部分我们把页面之间进行数据传递的四种常用的方案做了一个简要的分析总结。后一部分主要是根据前一部分的优缺点整理得出一个通用的数据传递和事件处理的工具类,并对其实现和使用进行了简要说明。在文末也提供了Demo的github地址,若使用中有什么不合理不完善的地方还请不吝指出。
2019-11-14 - 【微信小程序】性能优化
为什么要做性能优化? 一切性能优化都是为了体验优化 1. 使用小程序时,是否会经常遇到如下问题? 打开是一直白屏 打开是loading态,转好几圈 我的页面点了怎么跳转这么慢? 我的列表怎么越滑越卡? 2. 我们优化的方向有哪些? 启动加载性能 渲染性能 3. 启动加载性能 1. 首次加载 你是否见过小程序首次加载时是这样的图? [图片] 这张图中的三种状态对应的都是什么呢? 小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。此时,微信会在背后完成几项工作:[代码]下载小程序代码包[代码]、[代码]加载小程序代码包[代码]、[代码]初始化小程序首页[代码]。下载到的小程序代码包不是小程序的源代码,而是编译、压缩、打包之后的代码包。 2. 加载顺序 小程序加载的顺序是如何? 微信会在小程序启动前为小程序准备好通用的运行环境。这个运行环境包括几个供小程序使用的线程,并在其中完成小程序基础库的初始化,预先执行通用逻辑,尽可能做好小程序的启动准备。这样可以显著减少小程序的启动时间。 [图片] 通过2,我们知道了,问题1中第一张图是[代码]资源准备[代码](代码包下载);第二张图是[代码]业务代码的注入以及落地页首次渲染[代码];第三张图是[代码]落地页数据请求时的loading态[代码](部分小程序存在) 3. 控制包大小 提升体验最直接的方法是控制小程序包的大小,这是最显而易见的 勾选开发者工具中“上传代码时,压缩代码”选项; 及时清理无用的代码和资源文件(包括无用的日志代码) 减少资源包中的图片等资源的数量和大小(理论上除了小icon,其他图片资源从网络下载),图片资源压缩率有限 从开发者的角度看,控制代码包大小有助于减少小程序的启动时间。对低于1MB的代码包,其下载时间可以控制在929ms(iOS)、1500ms(Android)内。 4. 采用分包加载机制 根据业务场景,将用户访问率高的页面放在主包里,将访问率低的页面放入子包里,按需加载; [图片] 使用分包时需要注意代码和资源文件目录的划分。启动时需要访问的页面及其依赖的资源文件应放在主包中。 5 采用分包预加载技术 在4的基础上,当用户点击到子包的目录时,还是有一个代码包下载的过程,这会感觉到明显的卡顿,所以子包也不建议拆的太大,当然我们可以采用子包预加载技术,并不需要等到用户点击到子包页面后在下载子包,而是可以根据后期数据,做子包预加载,将用户在当先页可能点击的子包页面先加载,当用户点击后直接跳转; [图片] 这种基于配置的子包预加载技术,是可以根据用户网络类型来判断的,当用户处于网络条件好时才预加载;是灵活可控的 6. 采用独立分包技术 目前很多小程序[代码]主包+子包[代码](2M+6M)的方式,但是在做很多运营活动时,我们会发现活动(红包)是在子包里,但是运营、产品投放的落地页链接是子包链接,这是的用户在直达落地时,必须先下载主包内容(一般比较大),在下载子包内容(相对主包,较小),这使得在用户停留时间比较短的小程序场景中,用户体验不是很好,而且浪费了很大部分流量; [图片] 可以采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源; 7. 首屏加载的优化建议 7.1 提前请求 异步请求可以在页面onLoad就加载,不需要等页面ready后在异步请求数据;当然,如果能在前置页面点击跳转时预请求当前页的核心异步请求,效果会更好; 7.2 利用缓存 利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新,这不仅优化了性能,在无网环境下,用户也能很顺畅的使用到关键服务; 7.3 避免白屏 可以在前置页面将一些有用的字段带到当前页,进行首次渲染(列表页的某些数据–> 详情页),没有数据的模块可以进行骨架屏的占位,使用户不会等待的很焦虑,甚至走了; 7.4 及时反馈 及时的对需要用户等待的交互操作进行反馈,避免用户以为小程序卡了,无响应 渲染性能优化 1. 小程序渲染原理 双线程下的界面渲染,小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。 [图片] 分析这个流程不难得知:页面初始化的时间大致由页面初始数据通信时间和初始渲染时间两部分构成。其中,数据通信的时间指数据从逻辑层开始组织数据到视图层完全接收完毕的时间,数据量小于64KB时总时长可以控制在30ms内。传输时间与数据量大体上呈现正相关关系,传输过大的数据将使这一时间显著增加。因而减少传输数据量是降低数据传输时间的有效方式。 [图片] 2. 避免使用不当setData 在数据传输时,逻辑层会执行一次[代码]JSON.stringify[代码]来去除掉[代码]setData[代码]数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将[代码]setData[代码]所设置的数据字段与[代码]data[代码]合并,使开发者可以用[代码]this.data[代码]读取到变更后的数据。因此,为了提升数据更新的性能,开发者在执行[代码]setData[代码]调用时,最好遵循以下原则: 2.1 不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用; [图片] 2.2 数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用[代码]setData[代码]来设置这些数据; [图片] 2.3 与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下 [图片] 提升数据更新性能方式的代码示例 [代码]Page({ onShow: function() { // 不要频繁调用setData this.setData({ a: 1 }) this.setData({ b: 2 }) // 绝大多数时候可优化为 this.setData({ a: 1, b: 2 }) // 不要设置不在界面渲染时使用的数据,并将界面无关的数据放在data外 this.setData({ myData: { a: '这个字符串在WXML中用到了', b: '这个字符串未在WXML中用到,而且它很长…………………………' } }) // 可以优化为 this.setData({ 'myData.a': '这个字符串在WXML中用到了' }) this._myData = { b: '这个字符串未在WXML中用到,而且它很长…………………………' } } }) [代码] 利用setData进行列表局部刷新 在一个列表中,有[代码]n[代码]条数据,采用上拉加载更多的方式,假如这个时候想对其中某一个数据进行点赞操作,还能及时看到点赞的效果 解决方法 1、可以采用setData全局刷新,点赞完成之后,重新获取数据,再次进行全局重新渲染,这样做的优点是:方便,快捷!缺点是:用户体验极其不好,当用户刷量100多条数据后,重新渲染量大会出现空白期(没有渲染过来) 2、说到重点了,就是利用[代码]setData[代码]局部刷新 [代码]> a.将点赞的`id`传过去,知道点的是那一条数据, 将点赞的`id`传过去,知道点的是那一条数据 [代码] [代码]<view wx:if="{{!item.status}}" class="btn" data-id="{{index}}" bindtap="couponTap">立即领取</view> [代码] [代码]> b.重新获取数据,查找相对应id的那条数据的下标(`index`是不会改变的) > c.用setData进行局部刷新 [代码] [代码]this.setData({ list[index] = newList[index] }) [代码] 其实这个小操作对刚刚接触到微信小程序的人来说应该是不容易发现的,不理解setData还有这样的写法。 2.4 切勿在后台页面进行setData 在一些页面会进行一些操作,而到页面跳转后,代码逻辑还在执行,此时多个[代码]webview[代码]是共享一个js进程;后台的[代码]setData[代码]操作会抢占前台页面的渲染资源; [图片] [图片] 3. 用户事件使用不当 视图层将事件反馈给逻辑层时,同样需要一个通信过程,通信的方向是从视图层到逻辑层。因为这个通信过程是异步的,会产生一定的延迟,延迟时间同样与传输的数据量正相关,数据量小于64KB时在30ms内。降低延迟时间的方法主要有两个。 1.去掉不必要的事件绑定(WXML中的[代码]bind[代码]和[代码]catch[代码]),从而减少通信的数据量和次数; 2.事件绑定时需要传输[代码]target[代码]和[代码]currentTarget[代码]的[代码]dataset[代码],因而不要在节点的[代码]data[代码]前缀属性中放置过大的数据。 [图片] 4. 视图层渲染原理 4.1首次渲染 初始渲染发生在页面刚刚创建时。初始渲染时,将初始数据套用在对应的WXML片段上生成节点树。节点树也就是在开发者工具WXML面板中看到的页面树结构,它包含页面内所有组件节点的名称、属性值和事件回调函数等信息。最后根据节点树包含的各个节点,在界面上依次创建出各个组件。 [图片] 在这整个流程中,时间开销大体上与节点树中节点的总量成正比例关系。因而减少WXML中节点的数量可以有效降低初始渲染和重渲染的时间开销,提升渲染性能。 简化WXML代码的例子 [代码]<view data-my-data="{{myData}}"> <!-- 这个 view 和下一行的 view 可以合并 --> <view class="my-class" data-my-data="{{myData}}" bindtap="onTap"> <text> <!-- 这个 text 通常是没必要的 --> {{myText}} </text> </view> </view> <!-- 可以简化为 --> <view class="my-class" data-my-data="{{myData}}" bindtap="onTap"> {{myText}} </view> [代码] 4.2 重渲染 初始渲染完毕后,视图层可以多次应用[代码]setData[代码]的数据。每次应用[代码]setData[代码]数据时,都会执行重渲染来更新界面。初始渲染中得到的data和当前节点树会保留下来用于重渲染。每次重渲染时,将[代码]data[代码]和[代码]setData[代码]数据套用在WXML片段上,得到一个新节点树。然后将新节点树与当前节点树进行比较,这样可以得到哪些节点的哪些属性需要更新、哪些节点需要添加或移除。最后,将[代码]setData[代码]数据合并到[代码]data[代码]中,并用新节点树替换旧节点树,用于下一次重渲染。 [图片] 在进行当前节点树与新节点树的比较时,会着重比较[代码]setData[代码]数据影响到的节点属性。因而,去掉不必要设置的数据、减少[代码]setData[代码]的数据量也有助于提升这一个步骤的性能。 5. 使用自定义组件 自定义组件的更新只在组件内部进行,不受页面其他不能分内容的影响;比如一些运营活动的定时模块可以单独抽出来,做成一个定时组件,定时组件的更新并不会影响页面上其他元素的更新;各个组件也将具有各自独立的逻辑空间。每个组件都分别拥有自己的独立的数据、setData调用。 [图片] 6. 避免不当的使用onPageScroll 每一次事件监听都是一次视图到逻辑的通信过程,所以只在必要的时候监听pageSrcoll [图片] 总结 小程序启动加载性能 控制代码包的大小 分包加载 首屏体验(预请求,利用缓存,避免白屏,及时反馈 小程序渲染性能 避免不当的使用setData 合理利用事件通信 避免不当的使用onPageScroll 优化视图节点 使用自定义组件
2019-03-07 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21