- 微信小程序怎么区分是热启动进入的页面还是从当前小程序内别的页面跳转过来的?
微信小程序怎么区分是热启动进入的页面还是从当前小程序内别的页面跳转过来的? 前提是在一个小程序里
2023-05-22 - 微信开发者工具1.06.2401020 Stable 更新说明
下载地址Windows 64、Window 32、MacOS、MacOS(M1) 当前版本基于 1.06.2312061 修复了以下bug: [代码]F[代码] 修复 win11 预览版系统工具启动卡死的问题[代码]U[代码] 优化 编辑器打开较大 JS文件解析的时间[代码]U[代码] 优化 不应该允许开启调试器设置 Selected Context Only[代码]F[代码] 修复 异步分包出现重定义的问题 新增功能特性:1、wx.request 支持处理重定向请求 文档地址工具对齐真机,支持调试 wx.request 重定向请求拦截能力。 2、工具多端插件模式本版本完善了系列工具配置多端能力,同时为方便开发者实现在多端应用中引入第三方 SDK 或实现原生扩展的一些功能,新增支持开发者通过「多端应用插件」的方式以实现与 native 的连接与扩展。更多文档详情[图片] 3、上传预览展示优化针对上传、预览因为文件大小超过限制的情况,增加返回编译后的文件列表,和对应的大小,方便用户排查体积问题 [图片] [图片] 4、Mock 调试支持 getFuzzyLocation 接口[图片] 5、基础库推送增加桌面端微信客户端[图片] 6、小程序渲染调试能力优化增加完善 glass-easel、skyline 渲染能力的工具适配支持 具体所有特性:[代码]A[代码] 新增 多端 Android 支持打包 aab 格式[代码]A[代码] 新增 [代码]wx.getSystemInfo[代码] 对齐客户端的内容[代码]A[代码] wx.request新增支持redirect参数传值manual[代码]A[代码] 多端安卓云构建展示专业版构建横幅[代码]A[代码] 优化了多端的部分交互[代码]A[代码] 新增 工具 skyline 内核更新[代码]A[代码] 新增 多端支持查看 apk 文件签名与公钥[代码]A[代码] 新增 支持多端应用插件模式详情[代码]A[代码] 新增 windows 32位版本的工具使用 skyline 功能时提示不支持[代码]A[代码] 新增 工具模拟器 webview 渲染时支持 glass-easel 组件框架[代码]A[代码] iOS多端模拟器支持显示iPad[代码]A[代码] 新增 支持多端iOS配置RequireFullScreen[代码]A[代码] 新增多端第三方框架兼容,自动恢复 app.miniapp.json[代码]A[代码] 新增多端第三方框架兼容,自动补齐丢失字段[代码]A[代码] 新增 代码片段分享时支持不设置最低基础库版本[代码]U[代码] 优化 多端插件开发流程[代码]U[代码] 优化 [代码]project.miniapp.json[代码] 可视化配置 iOS 端支持开启 Skyline 扩展模块[代码]F[代码] 修复 微搭项目窗口工具栏错位问题[代码]F[代码] 修复 公众号右上角更多按钮点击失效[代码]F[代码] 修复 代码质量面板为空的问题[代码]F[代码] 修复 windows 编译出现 wcc.exe ENOENT的问题[代码]F[代码] 修复多端 ios 水印移除后的黑屏问题[代码]F[代码] 修复 偶现新建项目后出现 [代码]a.getState is not a function[代码] 的错误[代码]F[代码] 修复 部分情况下,代码包大小超出之后,上传报错面板没有展示文件列表的 bug[代码]F[代码] 修复 [代码]wx.switchTab[代码] 到当前页面回调与客户端不一致的问题[代码]F[代码] 修复 [代码]wx.chooseAddress[代码] 无法取消的问题[代码]F[代码] 修复 自定义机型浮窗样式[代码]F[代码] 修复 2.11.0 基础库模拟器白屏[代码]F[代码] 修复 第二次编译的时候报错的问题[代码]F[代码] 修复 路由事件未完成前调用 [代码]wx.reLaunch[代码] api 表现与客户端不一致的问题[代码]F[代码] 修复 redirectTo 不应该有动画效果[代码]F[代码] 修复 APIMock 配置在下次启动时丢失的问题[代码]F[代码] 修复 自定义编译条件搜索过滤失效的问题[代码]F[代码] 修复 多端插件插件解压错误[代码]F[代码] 修复 工具中 skyline 渲染模式下某些情况会 crash 的 bug[代码]F[代码] 修复 首次 cmd+Q 弹窗报错的问题[代码]F[代码] 修复 信任项目的弹窗被挡住的问题[代码]F[代码] 修复 当调试器在模拟器下方/右方时,终止模拟器时调试器区域没有提示的问题[代码]F[代码] 修复 启动时会连续编译两次的问题[代码]F[代码] 修复 项目启动时调试器会被刷新一次的问题[代码]F[代码] 修复 某些情况下会一直编译的问题[代码]F[代码] 修复 工具 1.06.2310182 版本引入的 WXML 编译报错问题[代码]F[代码] 修复 工具 onPageNotFound 事件监听不触发的 bug[代码]F[代码] 修复 worker 脚本执行同个 API 多次回调只有一个的问题[代码]U[代码] 更新 工具模拟器 wxml 编译模块的版本[代码]F[代码] 修复 skyline 渲染模式下,wxml 调试面板可能出现空白的 bug[代码]F[代码] 修复 [代码]picker[代码] 的 level 属性为非 [代码]sub-district[代码] 时与客户端不一致的问题 如果使用过程中有相关问题和建议反馈,可以加入下面的反馈群 (左)windows vip 反馈群 (右)mac vip 反馈群 [图片][图片]
02-18 - 运用小程序Skyline技术构建无缝用户体验 —— 同程旅行酒店最佳实践分享
[图片] 动效衔接设计与小程序渲染框架 1、什么是动效衔接设计? 随着互联网技术和设计理念的不断发展,动效设计成为现代 UI 设计中不可或缺的一部分。其中,动效衔接是非常重要的一环。动效衔接设计是指通过巧妙的动效设计,将不同的 UI 元素在动画过程中自然、流畅地衔接起来,从而增强用户的交互体验和视觉感受。在实际应用中,动效衔接设计主要应用于界面转场、信息提示、状态变化等方面,通过顺畅的衔接,降低用户因白屏等待而产生的焦虑。 2、动效衔接设计的意义 (1)极大提高用户体验,让用户感受到界面的流畅和自然,从而增加用户对产品的好感度。 (2)降低用户的操作认知成本,帮助用户更好地理解执行操作后所带来的结果,从而减少用户对产品的困惑。 (3)强化视觉层,让用户更好地区分不同的信息和元素,从而增强视觉层次感。 (4)增加界面的美感度,让界面更加生动有趣,从而提升整体的美感和设计价值。 (5)提升品牌的认知度,让产品更加具有特色和独特性,从而提高品牌的认知度和市场竞争力。 3、什么是小程序渲染框架Skyline? 为了进一步优化小程序性能,小程序在原 webview 渲染引擎之外最新推出 小程序渲染框架Skyline,其使用更精简高效的渲染管线,并拥有诸多增强特性,让它拥有更接近原生渲染的性能体验。新的增强特性有 worklet 动画系统、手势系统、自定义路由、共享元素动画,而且许多常用的组件如 scroll-view、swiper 都有了更高性能的实现。 [图片] [图片] 实践理念和场景拆解 1、动效衔接设计的核心原则 简单而清晰的动效设计,需要遵守以下几个原则: (1)一致性:动效衔接应该与整体设计风格保持一致。包括颜色、字体、动画速度等方面。 (2)可预测性:用户能够感知动画元素的变化关联性,从而增加用户对产品的掌控感和对界面的理解。 (3)反馈性:动效衔接与用户操作相响应,从而帮助用户理解他们的操作所带来的结果。 (4)视觉层次:动效衔接遵循视觉层次原则,让用户区分页面中的上下关系以及三维物理世界的关系层次,给用户清晰的层级区分感知,提高用户体验。 (5)自然性:动效衔接符合物理规律,例如重力、加速度等,从而增强动画的真实感和用户体验。 2、理念孵化与使用场景拆解 以提炼的动态感受为出发点,理性的层面给予了我们大致的产品体验感知,为我们动效理念的建成提供了框架。对此我们将继续从感性层面出发,找寻可传递真实感受的运动现象并加以组合提炼。 本次 同程旅行小程序 以酒店预订链路中核心的相册页面进行应用场景,在用户操作图片的过程中运用小程序渲染框架承接。 (1)将整个过程进行了拆解,首先为了退出时行动的路径更加清晰,做了一个响应设计,当界面向右滑动退出的过程中,相册图片进行缩小,在缩放过程中,会根据位移距离控制缩放的比例,同时蒙层的透明度以及毛玻璃效果也跟随手指移动变化,和相册列表页在视觉上呈现 XY 轴以及上下层级的空间关系,在缩小到一定的比例时,触发震动效果,松手退出到相册列表页。 (2)在交互结束时,图片退回相册列表页原始位置,在返回路径的过程中,根据交互结束时的定位点,来判断运动的方向和距离,计算运动加速度,以及模拟运动加速度带来的惯性回弹的方向和角度变化,加强与模拟真实物理世界的运动定律,和视觉上的动态感知。 结合自然世界的运动规律来看,把页面进入的元素比作是行驶的汽车,用户当作是正在斑马线上行驶的人,将马路作为页面空间。若汽车采用的是缓入运动(加速)的话,马路上的行人则看到的是一辆不断加速向他行驶过来的车辆。因为担心车辆高速的逼近导致刹车不及时的情况,行人便会本能的作出躲闪的反应。其实页面也是一个道理,进入的元素使用加速运动出现过冲的运动感知会让用户体验时产生不适。 [图片] 小程序渲染框架技术开发实践过程剖析 1、开发自定义路由实现此交互,需要 自定义路由动画,因为小程序渲染框架的页面支持自定义跳转动画。当使用自定义路由后,页面跳转时指定路由类型,就会触发自定义路由动画,而不再是默认的从右往左的动画,此处的实现可以使得页面跳转时,没有默认的路由动画,页面将直接以透明的方式渲染在屏幕上,由开发者自己控制页面内元素的动画展示方式,具体实现如下: (1)在图片查看页面配置文件 index.json 中声明 { "backgroundColor": "#00000000", "backgroundColorContent": "#00000000", // 设置客户端页面背景为透明 "navigationStyle": "custom", "renderer": "skyline", // skyline渲染引擎 "disableScroll": true, "usingComponents": { } } (2)在 wxss 中,设置图片查看页面的 page 节点为透明背景 page { background: transparent; } (3)在 js 中,使用 wx.router.addRouteBuilder(routeType, fn) 来声明自定义路由动画 wx.router.addRouteBuilder('myCustomRoute', function (params) { const handlePrimaryAnimation = () => { 'worklet'; return { // 可在此处,根据 params.primaryAnimation.value 的值,来设置页面的动画效果 backgroundColor: `rgba(0,0,0,${ params.primaryAnimation.value })` }; }; return { opaque: false, handlePrimaryAnimation, barrierColor: '', barrierDismissible: false, transitionDuration: 320, reverseTransitionDuration: 250, canTransitionTo: true, canTransitionFrom: false }; }) (4)在图片列表页面中,使用 x.navigateTo 来跳转页面,并且设置 routeType 为 myCustomRoute wx.navigateTo({ url: '/pages/skyline-image-viewer/index?index=0', routeType: 'myCustomRoute' }) [图片] 需配置页面的渲染引擎为 Skyline,并且在跳转时使用 routeType 就可以实现让页面在跳转时没有默认的路由动画。 2、共享元素穿越在连续的页面跳转时,页面间 key 相同的 share-element 节点将产生飞跃特效,还可自定义插值方式和动画曲线,通常作用于图片。为保证动画效果,前后页面的 share-element 子节点结构应该尽量保持一致 <share-element key="share-key"> <view> you code here </view> <!-- 需要注意,share-element 内要求只有一个根节点 --> </share-element> [图片] 这时,界面的表现像上面视频一样,是一个连续的动画状态,这完全是由 share-element 来控制的,share-element 的动画原理如下图所示: [图片] 3、接入手势组件,实现图片放大、缩小、平移在图片查看页面有如下结构: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <share-element key="{{ shareKey }}" class="current-item"> <image src="{{ src }}"/> </share-element> </scale-gesture-handle> 这里,我们使用小程序渲染框架提供的 手势组件 <scale-gesture-handle>,来实现图片的放大、缩小、平移等手势交互。 注意,所有声明为 worklet 指令的方法它们运行在UI线程,不要在方法中修改普通的变量,因为跨线程的关系,只能修改使用 wx.worklet.shared 声明的变量。 const GestureState = { POSSIBLE: 0, // 此时手势未识别 BEGIN: 1, // 手势已识别 ACTIVE: 2, // 连续手势活跃状态 END: 3, // 手势终止 CANCELLED: 4 // 手势取消 }; Component({ attached() { this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.sharScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.sharScale.value})` } }); // 页面所需的数据,需要在 attached 事件里初始化完毕,使其可以参与首帧渲染 this.setData({ src: '...', shareKey: '...' }); }, methods: { // 当手势组件识别到手势时,触发此回调 onScaleGestureHandle(e) { 'worklet'; const { state } = e; // 在worklet函数里,不要使用 const {} = this 对this解构 const shareX = this.shareX; const shareY = this.shareY; const sharScale = this.sharScale; if (state === GestureState.BEGIN) { // 手势已经识别,此时,可以获取到手势的初始值 } else if (state === GestureState.ACTIVE) { // 手势活跃状态,此时,可以获取到手势的变化值,如平移的距离、缩放的比例等 // 将当前变化的值,设置到 `shared` 变量,就可以改变元素的样式,类似于vue3的数据驱动 shareX.value += e.focalDeltaX; shareY.value += e.focalDeltaY; sharScale.value = e.scale; } else if (state === GestureState.END || state === GestureState.CANCELLED) { // 手势终止或取消,此时,可以获取到手势的最终值 } } } }) [图片] 4、手势协商(解决手势冲突) 上面的 demo 简单演示如何使用手势组件来做图片交互,但是在图片查看页面中,我们还有其他的手势交互,如图片的左右滑动切换等,一般我们会使用 <swiper> 组件来实现,但是 <swiper>组件的内部实现和 <scale-gesture-handle> 组件,都会监听手势事件,手势组件的事件不支持冒泡的,就会导致下面结构横时: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </scale-gesture-handle> 使用手势横向滑动时,会优先触发 swiper 的横向切换事件,而无法触发 <scale-gesture-handle> 的手势事件了,这在图片放大时的图片横向移动产生了冲突。此时就需要使用手势协商来解决手势冲突。 什么是手势协商? 手势协商指的是:当页面同时有多个手势交互时,需通过一定的约定来决定哪些手势事件应该被执行,哪些需要被忽略。 小程序渲染框架解决手势冲突的方式,主要是通过手势组件的 tag、simultaneous-handlers、native-view 和 should-response-on-move 来实现 tag:手势组件的标识,用于区分不同的手势组件simultaneous-handlers:手势组件的协商者,表示需要同时触发事件的手势组件的标识should-response-on-move:参与手势时间的派发过程,返回 false时,表示该手势时间不会继续派发native-view:用当前手势组件来代理原生组件内部的手势事件,如<swiper>组件内部的手势事件<swiper> 的内部也是使用了 <horizontal-drag-gesture-handler>手势组件,但是我们不能直接在<swiper>上设置tag来使其参与手势协商,需要用相同的手势组件通过native-view=swiper将其内部的事件代理出来,使其可以参与协商<!-- <scale-gesture-handle> 缩放手势 --> <!-- <horizontal-drag-gesture-handler> 横向拖动手势 --> <!-- 通过 simultaneous-handlers=tag 来声明多个手势应该同时触发 --> <scale-gesture-handle tag="scale" simultaneous-handlers="{{['swiper']}}" worklet:ongesture="onScaleGestureHandle"> <!-- 此处使用 native-view=swiper 代理内部的手势组件 --> <!-- 通过 should-response-on-move=fn 来参与`事件派发`过程,决定手势的事件是否应该派发 --> <horizontal-drag-gesture-handler tag="swiper" native-view="swiper" simultaneous-handlers="{{['scale']}}" worklet:should-response-on-move="shouldResponseOnMove"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </horizontal-drag-gesture-handler> </scale-gesture-handle> const GuestureMode = { INIT: 0, SCALE: 1, SWIPE: 2, MOVE: 3 // ... }; Component({ attached() { this.GuestureModeShared = wx.worklet.shared(GuestureMode.INIT); this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.shareScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.shareScale.value})` } }); // ... }, methods: { onScaleGestureHandle(e) { 'worklet'; const { state } = e; if (state === GestureState.BEGIN) { this.GuestureModeShared.value = GuestureMode.INIT; } else if (state === GestureState.ACTIVE) { if(this.GuestureModeShared.value === GuestureMode.INIT) { this.gestureBefore(e); // 手势类型未知时,判断手势类型 } else { this.gestureHandle(e); // 手势类型已知时,处理手势事件 } } else if (state === GestureState.END || state === GestureState.CANCELLED) { this.GuestureModeShared.value = GuestureMode.INIT; } }, // 判断手势类型 gestureBefore(e) { 'worklet'; const { focalDeltaX, focalDeltaY, scale } = e; if (Math.abs(focalDeltaX) > Math.abs(focalDeltaY)) { this.GuestureModeShared.value = GuestureMode.SWIPE; } else if (scale > 1) { this.GuestureModeShared.value = GuestureMode.SCALE; } else { this.GuestureModeShared.value = GuestureMode.MOVE; } }, // 处理手势事件 gestureHandle(e) { 'worklet'; if (this.GuestureModeShared.value === GuestureMode.SCALE) { this.shareScale.value = e.scale; } else if (this.GuestureModeShared.value === GuestureMode.SWIPE) { // swiper 切换模式时,这里什么都不用做 } else if (this.GuestureModeShared.value === GuestureMode.MOVE) { this.shareX.value += e.focalDeltaX; this.shareY.value += e.focalDeltaY; } }, // 用于判断手势事件是否应该派发 shouldResponseOnMove(e) { 'worklet'; return this.GuestureModeShared.value === GuestureMode.SWIPE; // 当模式为SWIPE时,才响应手势事件 } } }) [图片] 通过上面的代码,我们实现了手势协商,当用户在图片上进行滑动的操作时,总是会触发 <scale-gesture-handler> 的手势事件,通过对图片当前状态的判断来决定应该触发哪种手势,我们通过此种协商让 <horizontal-drag-gesture-handle> 手势在合适的时机触发,以此避免手势冲突。 5、使用小程序渲染框架时需要注意的一些地方作为一款新的渲染优化方式,开发者使用小程序渲染框架需要注意以下内容,以保证渲染的效果和性能。 (1)自定义路由时首帧渲染&首帧性能优化 小程序渲染框架的首帧渲染对共享元素动画非常重要,若共享元素节点的key 错过首帧设置的话,可能会丢失飞跃动画,所以在使用小程序渲染框架时,共享元素的 key 应该尽量在 attached 中或之前设置到页面,并且在首帧渲染时,应尽可能的减少 UI 层的渲染工作 如下: 1)所需要的数据应尽可能使用提前计算好,避免构建页面时等待太久影响响应速度 2)首次设置的数据应该尽可能的少,避免首次渲染时,页面上的元素过多,导致首帧渲染时间过长,导致动画卡顿(如:不要同时初始化太多的 <swiper-item>) 3)确保首帧渲染时,共享元素的 key 正确的设置,避免在首帧渲染时,由于找不到对应的共享元素,导致动画丢失,看不到飞跃动画 4)由于手势事件触发频繁,应尽量避免大量需要的计算的逻辑高频执行,容易导致机器发烫,或者导致动画卡顿 **worklet 函数的使用** worklet 函数的使用有一些限制,主要是由于它是在 UI 线程执行的,所以 worklet 函数中的 this 并非是页面的 this 实例, 里面所使用到的变量也是通过特殊的 babel 插件转换到UI线程的,需要与逻辑层共用的变量都需要用 wx.worklet.shared 将它声明成共享变量,在 UI 线程调用逻辑层的函数需要使用 wx.worklet.runOnJS (2)与 web 规范的差异 虽然小程序渲染框架尽可能的与 web 规范保持一致,但是由底层渲染引擎的限制,还是有一些差异,如: 1)display: flex 的默认朝向是 column,而不是 row,这需要开发者注意,官方后续会支持 block 布局方式 2)暂不支持 css 伪元素,如 ::after、::before,官方正在支持中 3)position 仅支持 absolute、relative,不支持 sticky,实现滚动吸附的效果需用 sticky-* 组件来配合 scroll-view 实现 ** <share-element> 在非小程序渲染框架运行环境里的表现是什么** 在非小程序渲染框架的运行环境内,<share-element> 组件会被视为一个 <view> 组件,需要做好布局的兼容 6、何时使用小程序渲染框架开发时,请确保小程序开发者工具版本是 最新版 nightly,sdk 版本在 2.30.2+,具体限制可参考 文档。 这些新特性的引入,使得小程序渲染框架在小程序开发中的优势更加明显,开发者可以更加便捷地实现各种复杂的交互效果,并且达到接近原生APP的体验。 [图片] 未来展望 1、个性化产品形态:将会根据不同的用户需求和场景,设计出更加符合用户喜好和习惯的动效衔接,进行组件化调用。 2、更加自然和真实的动效衔接:动效衔接将会更加贴近自然规律和真实物理效应,从而增强动画的真实感和用户体验。 3、更加智能化和自适应的动效衔接:动效衔接将会根据用户的操作行为和使用习惯,自适应调整动画效果,从而提高用户体验和产品效果。 4、扩大产品、设计与开发的协作效应:设计对动效的把控、产品对用户的洞察以及开发对新技术的应用,才可以发挥最大化的协作效应。 附1:本文作者 同程旅行研发工程师 同程旅行体验设计师 同程旅行产品经理 附2:代码片段 相册小程序代码片段(请使用 PC 端浏览器打开):https://developers.weixin.qq.com/s/E979jCmP7oHG 附3:UE标注 [图片] 附4:AB 实验效果 AB 实验显著win0.23% [图片]
2023-04-28 - swiper签到scroll-view上下滑动出现卡顿,基础库推送下又好了为什么一天三次了?
我的小程序首页是swiper 嵌套scroll-view 今天出现三次上下滑动会很卡,其他页面上下滑动也是用scroll-view 的不会,而且不仅是开发版卡,是开发版卡之后体验版跟线上的版本都跟着卡,接着如果我推送下基础库又好了,但是今天出现了三次这种情况都是推送完基础库就又好了是怎么回事
2020-07-02 - 组件attached生命周期被触发两次
在页面第一次渲染(onLoad生命周期触发之前)的过程中,如果A组件通过triggerEvent通知页面,通过setData改变状态,渲染一个组件B,则B组件的attached生命周期会被触发两次。 该问题在开发者工具或真机上均可复现。 [图片]
2021-03-01 - 小程序仿instagram交互效果实现(附长列表优化处理)
需求 最近几天在忙着搞公司项目的一个新的需求,原因是这样的:公司准备开发一个偏向于社交娱乐项的小程序,其中首页是可以看到用户发的话题帖子之类的,每个帖子都至少包含一张图片或者一个视频, 然后产品那边希望首页可以实现instagram的交互效果,效果图如下(本来应该是显示图片,奈何我的gif图片大小超过两兆,不能上传,所以我就用表情包替换了): [图片] 嗯,大致上这个就是需求的背景,然后就是每个帖子的高度是不确定的,高度大概在500~600px之间。 实现思路 一开始接到这个需求,其实我心里还是有点慌的,毕竟有一段时间不怎么接触小程序,也不知道小程序更新到什么程度,文档更新到什么程度。仔细分析一下项目需求,大致上可以归类为两个:交互 和 性能优化。 性能优化 因为首页是一个长列表,众所周知,页面一旦渲染的节点过多,就会卡顿,更何况是小程序,并且小程序是分为逻辑层和渲染层,两者通过setData链接,所以处理的时候需要注意两点: setData的数据量不能太大,记得好像是有个大小限制,,忘了是多少,也懒得找:clown_face:,你们可以自己在官方文档上找一下; 页面能够渲染的帖子数量是有限的,在这里,我是控制为最多渲染25个帖子。 处理 针对于长列表的优化,官方也有相应的组件-recycle-view,但是貌似并不符合项目需求,所以被我pass掉了。 虽然没用官方的组件,但是在组件的文档里面把对于长列表得性能优化解释一遍,这里摘抄一下重点: 核心的思路就是只渲染显示在屏幕的数据,基本实现就是监听 scroll 事件,并且重新计算需要渲染的数据,不需要渲染的数据留一个空的 div 占位元素。 其实也就是设置一个变量控制该数据是否可以渲染,如果是不能够渲染得话,那我们就用一个空的view取代它,需要注意的失败的是:空的框架高度需要设置为帖子的高度,这样子才不会闪屏。 针对这种思路,我们就可以确定其中一种长列表的性能优化的解决思路: 将数据分为二维数组,这样子就可以限制每次setData的数据量,等待数据渲染完成之后,获取每组数据所占用的总高度,这里的高度是为了在改组数据不渲染时设置占位框的高度。 [代码]/** * 获取 有渲染,但是高度还没获取到的分组 的高度 */ _getGroupListHeight() { this.data.list.forEach((item, index) => { if (item.show && !item.height) { const id = 'XXXXXXXX' // 组的id let query = wx.createSelectorQuery() query.select(id).boundingClientRect(rect => { this.data.list[index].height = rect.height }).exec(); this.getTopicHeight(item.data, index) // 获取列表中每个话题的高度,用于计算滑动时要滚动的距离 } }) } [代码] 上面就是一个简单获取每组的高度的代码实例,当该组数据有被渲染但是高度不明的情况下,就会去获取,加一步判断是为了防止重获获取组的数据,造成不必要的浪费。在获取每组数据的高度时,还会对应去 获取该组的每个帖子的高度,这样子是为了后面实现 仿instagram 交互做准备。 获取到了每组数据的高度,接下来,我们就可以监听页面滚动的高度,从而控制需要渲染的数据,需要注意的一点是,我们需要在该组的数据基础上,多渲染上两组和下两组数据,目的是防止用户快速滑动的时候出现白屏的不友好体验。当然也可以根据自己的需要多渲染几组。 [代码]/** * 页面滚动 * @param e */ onPageScroll(e) { // android页面滑动处理(非仿instagram版本) if (!this.data.isIos && !this.data.scrollBoxInfo.canUseScrollBox) { // 1. 处理当前页面正在播放的视频 if (this.data.currentPlayingId && Math.abs(e.scrollTop - this.data.scrollTop) > 100) { this.selectComponent(this.data.currentPlayingId).pauseVideo() this.data.currentPlayingId = '' } // 2. 处理 Andorid 渲染的分组数据 this._dealAndroidScroll(e) // 3. 处理视频自动播放 if (this.timer) { clearTimeout(this.timer) } this.timer = setTimeout(() => { this.data.scrollTop = e.scrollTop // 记录下当前的滚动距离,滑动暂停视频播放的时候需要用到 this.handleAutoPlay(e) // 视频自动播放 }, 300) } } /** * Android 监听滚动,动态设置分组 * @param {Object} e */ _dealAndroidScroll(e) { let max_height = 0 // 最大高度 for (let i = 0; i <= this.data.topicScroll.show_index; i++) { max_height += this.data.list[i].height } let min_height = max_height - this.data.list[this.data.topicScroll.show_index].height // 最小高度 // 超过,+1 if (e.scrollTop > max_height && this.data.topicScroll.show_index < this.data.list.length - 1) { ++this.data.topicScroll.show_index this._dealListShow(this.data.topicScroll.show_index) } // 小于,-1 if (e.scrollTop < min_height) { --this.data.topicScroll.show_index this._dealListShow(this.data.topicScroll.show_index) } } [代码] 这里没有对代码进行过滤,代码实现的效果就是监听滚动到的位置,并且设置渲染分组数据,因为有些是视频帖子,所以需要在滚动完成之后实现自动播放视频的功能。这段代码只是处理anroid平滑滚动的情况,因为代码涉及到 instagram 交互的实现。 对于长列表性能优化的思路大致上就这样:数据分为二位数组,渲染指定分组的数据,减少渲染的数据量和需要渲染的节点数量, 不需要渲染的数据就用指定高度的空的view替代,指定高度是为了防止闪屏。 仿 instagram 交互实现 从上面的 instagram 交互视频可以看出来,我们需要监听用户的手势滑动从而控制帖子的切换,并且每次切换只是切换一个帖子。知道了交互的详情,我们就可以展开想象了,大致上可以给个基本的实现思路: 监听手指点击和手指离开的事件,记录下手指点击的高度 && 手指离开的高度,用来判断用户滑动的距离和方向;记录下手指点击 和 手指离开的时间,可以粗略用来判断用户当前的滑动行为是快滑还是慢滑。根据上面的得到的信息,我们基本上实现滑动切换帖子的操作: [代码]/** * 监听手指点击操作 * @param e */ touchStart(e) { this.data.topicScroll.startTimeStamp = new Date().getTime() // 记录下当前手指点击事件 this.data.topicScroll.startPosition = e.changedTouches[0].clientY // 记录下手指开始点击的位置 }, /** * 手指离开屏幕 * @param e */ touchEnd(e) { const diffTime = new Date().getTime() - this.data.topicScroll.startTimeStamp // 手指离开的时候的时间戳 const clientY = e.changedTouches[0].clientY // 手指离开屏幕的位置 const diffY = Math.abs(clientY - this.data.topicScroll.startPosition) // 手指滑动的距离 const direction = this.data.topicScroll.startPosition - clientY > 0 // 手指滑动的方向,true为向上滑,false为向下滑 const scrollInfo = this.data.topicScroll // 1. 第一个节点手指向下滑动 && 最后一个节点手指向上滑动 不做操作 if (!scrollInfo.parent_index && !scrollInfo.child_index && !direction) { return } // 第一个节点向下滑动不做操作 if (scrollInfo.parent_index === (this.data.list.length - 1) && scrollInfo.child_index === (this.data.list[scrollInfo.parent_index].data.length - 1) && direction) { return } // 最后一个节点向上滑动不做操作 // 2. 根据滑动的方向,判断需要滚动到哪个节点下 const can_move = (diffTime < 100 && diffY > 50) || (diffTime >= 100 && diffY > 80) // 是否可以滑动,手势滑动判断依据 if (can_move) { if (direction) { if (scrollInfo.child_index === 4) { ++scrollInfo.parent_index scrollInfo.child_index = 0 } else { ++scrollInfo.child_index } } else { if (scrollInfo.child_index === 0) { --scrollInfo.parent_index scrollInfo.child_index = 4 } else { --scrollInfo.child_index } } } // 3. 处理滚动 this._dealScroll(can_move ? pausePlayId : '', can_move) } [代码] 根据监听到信息,可以判断是否切换以及切换到那个帖子的操作,所以我们只需要根据面已经获取到的帖子高度就可以计算出来要滚动的高度。 2. 根据第一点的操作,我们就可以实现 仿Instagram 交互效果,但是忽略了致命的一点:页面的惯性滚动。也正是因为这个所谓的“惯性滚动”,我多花了几天的时间去研究交互的实现💥 众所周知,为了使得页面的滑动更加流畅,当我们滑动停止的时候,页面就像会产生惯性一般,自动的滑动一定距离才停下。 安卓下默认有惯性滚动,而在 iOS 下需要额外设置-webkit-overflow-scrolling: touch的样式 而第一点方案实现的大前提是页面不能拥有惯性滚动,否则的话页面无法准确滚动到指定位置。 解决方案 ios的解决方案比较简单,我们只需要设置 -webkit-overflow-scrolling: auto 的样式即可。 比较麻烦的是android的实现,一开始我是上网找了不少的资料去实现取消页面的惯性滚动,毕竟页面滚动的性能是最好的。不过很可惜,我没有找到可行的方案,所以我选择退而求其次,模拟手指的滑动。经过对小程序文档的浏览,我选择可两种比较可能实现的方案:①使用wxs实现手指滑动的效果;②使用scroll-view的fast-deceleration 属性。显而易见,使用wxs方案实现比较复杂,所以我一开始选择scroll-view这个方案。 android实现思路 scroll-view方案实现的思路与ios类似,比较不同的是页面的滚动,因为ios是可以直接使用页面滚动的,即wx.pageScrollTo,而scroll-view则是使用scroll-into-view 或者 scroll-top,为了减少操作,我是直接使用了scroll-top这个属性。正当我信心满满的时候,做出来的效果却是打了我的脸:每次滚动到指定位置的时候都会抖动一下,虽然没有仔细去查明原因,不过 我猜想是页面滚动依然存在一定的惯性滚动,当我们设置了scroll-top的时候,惯性滚动的存在会使得列表偏移位置,最后在偏移回来,这样子看起来就像是抖动了一下。 正当我准备放弃scroll-view这个方案的时候,无意中让我发现了scroll-view的api接口,本着想试一下新接口的心态,我尝试一下使用官方的api接口控制scroll-view滚动,神奇的是,成了!!!! (此处为效果图) 既然scroll-view做出来的效果勉强还可以,我就直接pass后面的wxs方案,毕竟那一块的逻辑应该会比较复杂。 [代码] 因为时使用了scroll-view的api接口,支持的版本库比较高,是2.14.4,虽然大部分的用户可以支持,但还是要兼容少部分的用户,所以android部分我是搞了一个scroll-view版本和平滑滚动版本 fast-deceleration这个特性在开发者工具那里貌似不生效,但是手机预览却是可以,不知道是开发工具的问题还是说只是兼容了部分手机(我这边测试了小米和华为这两款,没更多的手机测试了:clown_face:) [代码] demo 说了这么多,也不一定有人看得下去,demo呈上 结尾 在调试这个效果的时候,遇到不少的坑,其中有一个坑印象巨深,在此记录一下: touch 期间 touchstart 的目标节点被移除,则对应的 touchend 事件会因为没有目标节点而缺失。 遇到这个坑是因为小程序同个页面不允许存在多个视频,所以需要将没有播放的视频使用图片替换,需要播放的视频的时候就替换回来,这样子,就会出现上面的情况,导致无法监听到touchend事件,整个列表停在原地,没有滚动到指定位置。 原文链接 传送门
2021-11-14 - v-for 内 有 插槽时 会出现警告
slot "" duplication is found under a single shadow root , 调试基础库大于2.18.0的版本都会出现这个问题 , 导致偶尔会出现 slot 内不渲染 这个bug已经出现了4个月多了
2021-12-24 - 基于Proxy的小程序状态管理
作者:wwayne 原文:基于Proxy的小程序状态管理 Fundebug经授权转载,版权归原作者所有。 微信小程序的市场在进一步的扩大,而背后的技术社区仍在摸索着最好的实践方案。我在帮助Nike,沃尔玛以及一些创业公司开发小程序后,依旧认为使用小程序原生框架是一个更高效,稳定的选择,而使用原生框架唯独缺少一个好的状态管理库,如果不引入状态管理则会让我们在模块化,项目结构以及单元测试上都有些捉襟见肘。 目前相对比较稳健的做法是针对redux或者mobx做一个adaptor应用到小程序中,但这样需要自己想办法打包引入外部库,还要想怎么去写这个adaptor,总显得有些麻烦。于是我迸发出一个想法去写一个专用于小程序的状态管理库,它使用起来足够简单并且可以通过小程序自己的npm机制安装。 目前我已经用这个开源库开发了两个电商小程序,在提高我开发效率的同时亦保证了程序的性能,所以接下来我想谈谈这背后的理念以启发更多开发者尝试新的解决方案。 基于Proxy的状态管理实现 Proxy在小程序中已经得到了足够好的支持,目前并没有发现在任何iPhone或者Android上不能使用Proxy的情况。而基于Proxy的状态管理其实也就是订阅监听的模式,一方面监听数据的变化,另一方面将这些变化传达给订阅的小程序页面。 举一个比较常见的例子,当一个用户从自己的主页进入用户编辑页面,然后更改了自己的用户名点击保存后,用户主页和用户编辑页上的用户名这时候都应该被更新。这背后的程序逻辑则是:更新这个行为将触发Proxy去通知状态管理库,然后状态管理库负责检查此时还在页面栈中的所有页面,更新订阅了用户名这个数据的页面,如下图: [图片] Part1: 监听数据变化 监听数据变化其实就是监听各个Store的属性变化,实现上就是在各个Store前面加了一层Proxy,用更直观的图片来表示就是这样: [图片] 当一个Store被观察以后,它的属性就都变成了Proxy实例,当这个属性值是Object或者Array的时候,它内部的值也会被包装成Proxy实例,这样无论多深层的数据变动都能被监听到。 而在Proxy的后面,Store的属性其实是被另一套数据(紫色部分)所维护,这套数据不负责监听,它就是纯数据,针对属性的任何变动最后都会应用到这套数据上来,它的作用是维护和返回最新的数据。 实现细节: https://github.com/wwayne/min… Part2: 页面数据绑定 因为小程序每个页面的js都是向Page中传递一个对象,这就让我们有机会包装这个对象,从而实现: 进入页面后,将页面保存在页面栈中 将来自状态管理库的数据映射到这个页面的data上来 页面退出时,将页面从页面栈中移除 实现细节: https://github.com/wwayne/min… Part3: 页面订阅更新 当数据被监听到变化后,我们需要依次做两件事,先是找到所有存储在页面栈里的页面,然后根据各个页面订阅的数据来检查变化,如果有变化就通知这些页面,从而让它们去触发setData更新页面。 实现细节:https://github.com/wwayne/min… 使用状态管理的例子 有了状态管理库,现在我们就来实现一开始举例的更新用户信息的操作,我们的文件路径如下: [代码]stores/ user.js pages/ userEdit/ index.js index.wxml [代码] 1. 首先我们创建一个Store保存用户的信息,并且监听它的变化: [代码]// stores/user.js import { observe } from 'minii' Class UserStore { constructor () { this.name = 'bob' } changeName (name) { this.name = name } } export default observe(new UserStore(), 'user') [代码] 2. 接着在我们的小程序页面订阅Store的信息 [代码]// pages/userEdit/index.js import { mapToData } from 'minii' import userStore from '../../stores/user' const connect = mapToData(state => (({ myName: state.user.name })) Page(connect({ updateNameToJames () { userStore. changeName('james') } })) [代码] 3. 完成,现在可以在页面中使用和更新数据了 [代码]// pages/userEdit/index.wxml <text>{{ myName }}</text> <button bindtap="updateNameToJames">update name to James</button> [代码] 最后 小程序因为有体积的限制,所以我希望在代码量上也尽量做到轻量和便捷,所以目前这个状态管理库并没有太多很复杂的功能,在小程序打包后所占用的体积也不到1kb,颇有点够用就好的意思。 我也已经用它开发了两款小程序,在经历了一段时间的用户使用后,我也更有信心说这个方案在小程序中是可行的。如果你有任何想法和建议,都欢迎告诉我。 项目Github: https://github.com/wwayne/minii 关于作者 Hi, 我是wwayne,是一名居住在上海的独立软件工程师,我正在开发我的新产品 talk-to-kim, 你可以在Github 或者专栏 一个人写代码找到我
2019-06-20 - scrollview this.animate()在页面中可正常驱动动画,在自定义组件中无效?
代码片段https://developers.weixin.qq.com/s/CR9LcRm37Ym5
2020-12-25 - IOS14.5 部分动画失效
在升级IOS14.5系统后,小程序的部分动画效果失效,具体效果可以参考京东、拼多多小程序商品详情页面点击购买按钮后,弹出规格选择面板的动画。并不是100%复现,但很大概率会出现。 IOS14.4.x系统下表现正常。
2021-04-28 - 用小程序Canvas 2D自定义生成转发图片
前言 小程序原先转发给别人,要不是一张图片,要不是就是截屏,不是很自定义化。 我们要每个用户在不同的页面,转发的内容都不一样,这当然可以直接从服务端实时生成这样 5:4 的图片做转发,毕竟好处是避免了兼容性问题 本示例将会用另外一种思路,从客户端实时生成分享图片,并进行转发。 技术调查 微信小程序中 以 [代码]wx.createCanvasContext[代码] 为代表的 [代码]CanvasContext[代码] (v1) 都从基础库 [代码]2.9.0[代码] 开始,停止维护了 所以我们就使用更加贴近 mdn 上 [代码]Canvas[代码] 来代替 (v2) 这里简称停止维护的版本为 v1, [代码]Canvas[代码] 版本为 v2 值得注意的是 v2 版本中还有一个 [代码]wx.createOffscreenCanvas[代码] 它也可以使用 [代码]OffscreenCanvas.getContext('2d')[代码] 这样的 api 不过它的最低版本为 [代码]2.16.1[代码] 到今天 (2021/05/05) 大部分人用的版本还是 [代码]2.16.1[代码] ([代码]2.17.0[代码] 还在灰度) 所以说,从兼容性的角度出发, 使用 [代码]OffscreenCanvas[代码] 是有很大风险的 而且我出于好奇实验使用了一下,bug 也是一大堆 issue 开始吧 ps: canvas api 不熟的 [代码]mdn[代码] 上看看 绘图前言 rpx 这种单位换算问题 , [代码]wx.getSystemInfoSync[代码] 获取一下屏幕宽,做个换算就行 小程序中的 canvas 2d 不能把它设置 style:display:none ,不然转化成图片时会空白 , 我们可以把它移出可视界面。 层级问题,canvas 和 svg 一样都是后面的覆盖前面的,所以我们可以使用 对[代码]zIndex[代码] 进行排序,来决定每个 [代码]function[代码] 的执行顺序 绘图 ing 圆角矩形可以使用 [代码]arc[代码] api 来解决 圆角图片可以使用 [代码]clip[代码] + [代码]drawImage[代码] 比如在这里因为我们需要使用微信的头像 所以需要把 图片的域名 在后台配置一下 这里贴一段 uni 下,在 ctx 中绘制圆角头像的代码 [代码]async addAvatar() { // 下载到本地 const [err, res] = await uni.getImageInfo({ src: avatarUrl }) const { path, type } = res const img = canvas.createImage() img.src = path // 直接设置 width height 是不生效 const offsetX = 20 * dpr const offsetY = 20 * dpr const r = (50 * dpr) / 2 const circle = { x: offsetX + r, y: offsetY + r, r: r } // 这样写是因为最后绘制的时候是执行一个 ()=> Promise<any> // 这样可以确保生成的时候,图片已经 onload了 await new Promise((resolve, reject) => { img.onload = () => { ctx.save() // 切圆角 ctx.beginPath() ctx.arc(circle.x, circle.y, circle.r, Math.PI * 2, false) ctx.clip() // 画成指定的长和高 ctx.drawImage(img, offsetX, offsetY, 50 * dpr, 50 * dpr) ctx.restore() resolve() } img.onerror = event => { reject(event) } }) } [代码] 图片的丰富度 -> svg 的引入 在上面,我们已经把图片的,渐变,排版,文字,依赖的图片全部绘制上去了 然而这时候我们想加一些丰富度,但是不想使用本地图片去那种,缝合的事情,毕竟这样也有失水准。 这时候我们自然而然的想到了 [代码]SVG-to-canvas parser[代码] 目前在 [代码]npm[代码] 主要有 [代码]fabric[代码] / [代码]canvg[代码] 这些 这里我选用的是 基于 [代码]canvg[代码] 的 [代码]svg2canvas[代码] 这样我们在 [代码]iconfont[代码] 就可以挑选一些 [代码]svg[代码] 下载下来,然后通过 [代码]converter[代码] 就可以直接转换成 [代码]canvas js code[代码] 使用了。 当然,转化实际上是不完美的,定位和大小,我们可以改造绘制的 [代码]draw function[代码] 在其中加入 [代码]ctx.translate(x, y) ctx.scale(dpr / 2, dpr / 2) [代码] 颜色什么当然都可以作为参数 这样,我们的 [代码]svg[代码] 就顺利的加了上去 onShareAppMessage 转发 [代码]onShareAppMessage[代码] 是支持 [代码]promise[代码] 的,不过 [代码]promise[代码] 三秒内不 resolve , 会自动 [代码]fallback[代码] 见 onShareAppMessage 文档 这时候,我们就可以写出下列代码 [代码]onShareAppMessage() { const createPromise = async () => { try { this.shareBtnLoading = true // 获得图片的临时路径 const loaclPath = await aWayToGetCanvasTempFilePath() return { title: 'hello, i am icebreaker', path: 'resolve path', imageUrl: loaclPath } } catch (e) { console.warn(e) } finally { this.shareBtnLoading = false } } return { title: 'fallback title', promise: createPromise(), path: 'fallback path', imageUrl: 'fallback imageUrl' } } [代码] 获得图片的临时路径,可以使用 [代码]wx.canvasToTempFilePath[代码] api 这个api是 画布 v1 版本 和 v2版本通用的 不过 v1 版本传入的是 [代码]canvas-id[代码] v2 则是 [代码]canvas[代码] 组件实例 生成速度 在不同的 dpr 下,时间都很快 真机在 [代码]300ms[代码] 左右 远小于 [代码]3000ms[代码] 自动 fallback 的限制 效果视频 在知乎上, 链接地址
2021-05-05 - 高阶性能渲染-wxs
我们永远没有资格说放弃,因为这是属于我们的年华,应该开出耀眼的繁花。 这里主要讲解两点使用方式 wxs --> 数据处理使用 wxs --> 拖拽使用 wxs介绍 1.先看看官方如何介绍的 ,如下 点我可以查看更多官方文档地址 WXS 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行。 WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。 WXS 的运行环境和其他 JavaScript 代码是隔离的,WXS 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的API。 WXS 函数不能作为组件的事件回调。 由于运行环境的差异,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备上二者运行效率无差异。 总的来说,我主要看中它快和方便 (有种在wxml写函数的感觉),并且处理函数内容后渲染不需要去调用setData,可以节省在请求数据返回后写大量的逻辑函数或者遍历方法去处理事务,所以我才拿出来讲解。 wxs数据处理使用 使用我习惯现在utils文件夹里创建一个common.wxs,然后在使用的wxml引入该文件。具体使用也很简单,如下 在你定义的common.wxs文件里,写你需要处理的函数,然后使用export导出 [代码] var getMax = function(array) { var max = undefined; for (var i = 0; i < array.length; ++i) { max = max === undefined ? array[i] : (max >= array[i] ? max : array[i]); } return max; } module.exports = { getMax: getMax }; [代码] 在.wxml文件需要的地方引入该.wxs文件,其中<wxs>标签包含2个重要属性,[代码]module[代码]和[代码]src[代码]<br> module表示你要导出wxs的方法集合,在wxml里面可以使用该方法名去使用具体的函数<br> src表示你.wxs路径位置引入,代码如下 [代码]<wxs module="common" src="../../utils/common.wxs"></wxs> <view> <text>{{common.getMax(arrNumber)}}</text> </view> [代码] 上面代码里[代码]arrNumber[代码]是你页面.js文件里面定义的数据 [代码]data: { arrNumber: [1,2,9,2,1,5,7] } [代码] 就是如此简单,你已经学会了使用wxs处理数据了,接下来我们来点难的,使用wxs做一个可以随便拖动又不影响小程序性能的手势拖拽事件 wxs拖拽使用 你是否还在使用setData控制元素?为什么不能频繁使用setData,点我查看详情然后通过输出手势坐标来移动元素具体位置?如果是建议你该换个方法了,推荐使用wxs,让你小程序性能上一个档次。 解决思路如下: 我们先在页面的.wxml文件书写初始化的样式内容; 给[代码]view[代码]标签绑定手势事件,其中[代码]touchmove[代码]我使用了阻止冒泡事件绑定是为了防止在苹果机中,元素移动会带着屏幕一起移动,导致滑动手势突然中断不流畅, [代码]clickLive[代码]事件我也用了阻止冒泡事件是为了触发子元素的事件防止点击被父元素拦截而使用,代码如下: [代码]<wxs module="comm" src="../../utils/common.wxs"></wxs> <view class="enterLive liveMove" data-maxWidth="{{windowWidth}}" data-maxHeight="{{windowHeight}}" bind:touchstart="{{comm.liveTouchmove}}" catch:touchmove="{{comm.liveTouchmove}}" bind:touchend="{{comm.liveTouchmove}}"> <view class="live-block" catch:tap="{{comm.clickLive}}"> <image class="liveEnter-img" mode="aspectFit" src="https://img0.baidu.com/it/u=401284325,23907343&fm=26&fmt=auto&gp=0.jpg" /> </view> </view> [代码] 我们发现上面的代码有[代码]windowWidth,windowHeight[代码]两个参数,这是在页面的.js中,是计算当前页面的宽高,用于防止滑块滑出视野范围内,导致无法滑动回来,其中下面代码[代码]getApp().globalData.systemInfo[代码]是读取全局文件[代码]app.js[代码]里面拿值的,那里我有获取系统参数设为全局保存, 而[代码]bindEnter[代码]事件是用于待会在[代码].wxs[代码]里面可以触发.js文件内函数示例 [代码]data: { windowWidth: 375, windowHeight: 667, }, ready: function() { let systemInfo = getApp().globalData.systemInfo; let width = 150 * systemInfo.windowWidth / 750; let height = 150 * systemInfo.windowWidth / 750; if (systemInfo) { this.setData({ windowWidth: systemInfo.windowWidth - parseInt(width), windowHeight: systemInfo.windowHeight - parseInt(height) }) } }, bindEnter(e) { wx.showToast({ title: '你点击到我啦~', icon: 'none' }) }, [代码] 接下来我们将书写.wxs文件,处理手势输出内容,使页面的滑块可以平静的滑动; 认真看的同学会发现页面的[代码]touchstart,touchmove,touchend[代码]事件我都是绑定同一个函数[代码]liveTouchmove[代码]这里是我想节约函数名称,统一使用一个函数,然后在输出的回调里面判断当前是什么手势 这里的滑块滑动逻辑是:当输出是touchstart手势时,我们记住当前滑块的[代码]starLeft,starTop[代码]值及坐标[代码]starX,starY[代码]值 当我们判断当前是[代码]touchmove[代码]手势时,我们就用当前的x,y坐标值减去开始starX和starY坐标值,得到一个差值[代码]diffX,diffY[代码] 然后我们使用这个[代码]diffX,diffY[代码]差值分别加上滑块刚开始的[代码]starLeft,starTop[代码]值,即可得到我们滑块当前移动的位置 最后我们通过条件判断当前是否超出屏幕即可,我这里做多一步,就是滑块永远停留左右两边,所以要多算一次滑块停留是偏离那一边,在通过[代码]ownerInstance.selectComponent('.liveMove').setStyle[代码]可以给页面元素添加行内样式。 滑动滑动逻辑已经讲完了,就是这么简单 如果你想通过[代码].wxs[代码]函数触发[代码].js[代码]文件内的函数,这里有提供[代码]ownerInstance.callMethod("bindEnter")[代码]方法给你触发.js里面的函数,其中[代码]ownerInstance[代码]是绑定页面函数的第二个回调参数,[代码]callMethod[代码]是官方提供的一个方法,[代码]bindEnter[代码]是.js文件里自己命名的函数方法名称,整体代码如下:更多wxs内置方法,请查看官方文档 [代码]/** * 滑块计算位置 */ var starLeft = 0; var starTop = 0; var starX = 0; var starY = 0; function liveTouchmove(event, ownerInstance) { // console.log(JSON.stringify(event)) // console.log(JSON.stringify(ownerInstance)) var left = 0, top = 0; var diffX = 0, diffY = 0; if(event.type === 'touchstart') { starLeft = event.currentTarget.offsetLeft; starTop = event.currentTarget.offsetTop; starX = event.changedTouches[0].clientX; starY = event.changedTouches[0].clientY; left = starLeft; top = starTop; } else { diffX = event.changedTouches[0].clientX - starX; diffY = event.changedTouches[0].clientY - starY; left = starLeft + diffX; top = starTop + diffY; var maxWidth = event.currentTarget.dataset.maxwidth; var maxHeight = event.currentTarget.dataset.maxheight; if (left > maxWidth) { left = maxWidth; } if (top > maxHeight) { top = maxHeight; } if (left <= 0) { left = 0; } if (top <= 0) { top = 0; } } if (event.type === 'touchend') { if (maxWidth / 2 < left) { left = "calc(100% - 3% - 152rpx)"; } if (maxWidth / 2 > left) { left = '3%'; } } else { left = left + 'px'; } var instance = ownerInstance.selectComponent('.liveMove'); // 返回组件的实例 instance.setStyle({ left: left, top: top +'px', }); } /** * 点击直播入口 * @param */ function clickLive (event, ownerInstance) { ownerInstance.callMethod("bindEnter"); } module.exports = { liveTouchmove: liveTouchmove, clickLive: clickLive, }; [代码] 总结 wxs确实可以解决我们一些性能问题,和wxml函数调用方便,但是我们也要注意几个问题 目前还不支持原生组件的事件、input和textarea组件的 bindinput 事件 1.02.1901170及以后版本的开发者工具上支持交互动画,最低版本基础库是2.4.4 目前在WXS函数里面仅支持console.log方式打日志定位问题,注意连续的重复日志会被过滤掉。 wxs有自己的语法,我理解为不支持ES6及以上语法更多wxs语法可以查看官方文档 以上内容,包括之前写的文章内容,最终会上传到我的gitee里面,请留意。 文章创作不易,喜欢的记得点赞 本文同步掘金号文章 喜欢的记得去点个赞哦
2021-07-11 - 页面局部滑动的方法推荐 详解【scroll-view】【swiper】【overflow:scroll属性】区别
前言: 初期开发小程序的时候 我们可能遇到一个问题 一个scroll-view组件内的滑动与页面的滑动冲突 造成 scroll-view动画到边界后 页面才跟着滑动另外 有时可能遇到这样的需求 一个页面中有多处局部滑动 且 他们之间不能互相干扰 互相影响 而且大多数情况下 页面整体不能滑动 这时候我们通过文档 获取到的第一印象是使用scroll-view组件 或 使用swiper组件满足需求 而在实际的工程搭建上又会因为经验不足遇到许多坑 下面我们使用不同方法分别创建一个竖向的局部滑动区域 横向的参考竖向即可 多个滑动区域的使用三种方法创建对应组件 三种方法区别较大 需要根据实际场景选择 第一种方式 使用scroll-view方式创建一个滑动区域 我们创建一个长度为10的数组 在scroll-view内部 wxml: [图片] wxss: [图片] js: [图片] 注意 1.Page元素是该页面整体 类似于H5中的body 给其设置height:100%;变相屏蔽了页面的滑动事件 2.这里scroll-view组件除了设置scroll-y属性外 需要给list一个固定高度 才能生效 第二种方式 使用css属性 overflow-y:scroll 这里wxml和wxss稍作修改 其他的和上面相同 [图片] [图片] 其实使用css属性只是模拟了scroll-view组件的效果 但毕竟scroll-view组件提供了许多其他的属性和方法可供开发者操作 例如监控组件的滑动等 如果使用css的话 只有局部区域的滑动效果而已 第三种方式 使用swiper组件实现竖向滑动 wxml [图片] js [图片] wxss: [图片] 这里还是比较复杂的,其中swiper组件需要设置vertical属性为true 另外margin-next指 swiper-item与后面子元素的距离 介于我们要设置swiper内尽可能显示全部的子元素 margin-next应设置为swiper高度 减 swiper-item的高度 计算方法如下 使用 wx.createSelectorQuery() 方法获取swiper和swiper-item元素的属性 [图片] 预览图: [图片] 注意: 1.swiper的margin-next属性不支持 calc()动态计算高 因此只能使用在页面渲染完成后 获取滑动区域总体的高 - 元素内容的高 setdata 来渲染 2.swiper无论如何滑动 都会最终停在某个元素的顶端/左端 而上面两种方式可以scrollTop值可以停留在任何地方 3.swiper滑动到最后几个元素的时候 下方/右方还会留下空白位置 而上面两种方式scrollTop滑动到最后几个元素 滑动条不再滑动 下面/右方没有空白位置 4.swiper组件有 bindanimationfinish 这个方法监控 滑动动画的结束事件 而scroll-view只能通过 bindtouchend 拿到滑动手势的结束事件 swiper的这种特性可以满足特殊场景下的需求 比如无论如何滑动区域后 自动对焦 5.经过测试 swiper组件 在渲染子元素变多的情况下 性能会远小于 scroll-view 以5000个子元素为例 swiper 在安卓/IOS手机上的渲染速度已经严重影响用户体验 而scroll-view组件渲染可以在2秒内完成 并且实现滑动 因此在渲染数量在极限状态下 推荐使用scroll-view 6.如果想使用 scroll-view 模拟swiper的效果 可以使用 遮罩浮层盖住scroll-view主体 + 监听scroll-into-view事件 的方法达到效果 不过如果快速滑动 会造成高频使用 setdata 触发 scroll-into-view 事件造成页面撕裂 不同高度设备的适配: 但进一步想 现在设备五花八门 不同设备的宽高是不一样的 比如新出的安卓手机和iphone x/10/12屏幕都比较长,需要我们对滑动区域父元素进行高的计算 需要 height:calc()/width:calc() 和 createSelectorQuery 动态计算,一个页面顶部有一处固定广告 下面的区域是滑动区域 那么滑动区域的高为 height:calc(100% - 300rpx) 相关代码如下 另附一个可以多处区域滑动的页面 https://developers.weixin.qq.com/s/jgK6DNmR77hJ 作者:陶路 其他相关阅读: 1.swiper组件 https://developers.weixin.qq.com/miniprogram/dev/component/swiper.html 2..scroll-view组件 https://developers.weixin.qq.com/miniprogram/dev/component/scroll-view.html 3.动态获取页面元素方法 wx.createSelectorQuery https://developers.weixin.qq.com/miniprogram/dev/api/wxml/wx.createSelectorQuery.html
2020-06-02 - 线上下载的source-map为什么是空的?
{"version": 3,"file": "", "sourceroot": "", "sources": [], "names": [], "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;", "sourcesContent": []}
2020-06-11 - 请教官方一下小程序自动化测试发展方向?
目前小程序开发文档中的自动化部分已经比较完善,用原生的语法加Jest框架编写用例,基本的能力已经满足。但是看到官方同时还在做自动化框架Python版和JS版(Python版功能更多些),功能和文档中的自动化能力多数重合,请问接下来官方的重点发展方向到底是在哪一个? ps:下一个开发者工具正式版发布大概什么时间?
2020-03-03 - sticky在自定义组件无效。page有效?
sticky在自定义组件下,苹果真机无效。安卓有效。 在page下使用都是有效的。 看了文档:苹果运行在iOS8、iOS9、iOS10。can i use 上显示IOS6.1就可以用这个属性了 代码片段:https://developers.weixin.qq.com/s/dCbeMimt7baL
2019-08-01 - 开发版本0421,开发者工具骨架屏功能建议?
这边尝试使用骨架屏后在本地生成了两个文件。 [图片] 这两个文件能否新建一个文件夹 并且使用template标签包起来。我们这边直接用template标签就好了 [图片] 引用的时候就好了 [图片]
2020-04-22 - 微信SDK海外用户的云服务器是否部署在中国地区?
您好: 我是来自安徽华米科技的客户,我们的小米运动/Amazfit App使用了贵公司的微信登录、微信分享、微信支付的Android、iOS服务。小米运动/Amazfit App面向全球用户提供服务,为了保护用户数据安全, 我们需要贵公司确认所提供的微信登录、微信分享、微信支付服务是否存在以下的行为: 1. 收集不必要的数据并存储至云服务器。 2. 海外用户网络接口请求(包含正常的接口请求)的云服务器部署在中国地区。 如果存在以上的行为请如实告知,我们将对海外用户使用相应服务进行合理的规避。 感谢支持! Hi : I am a customer from Anhui Huami Information Technology Co.,Ltd. Our Mi Fit/Amazfit App uses your company's XXX Android and iOS services. Mi Fit/Amazfit App provides services to users around the world. In order to protect user data security, we need to confirm whether the sdk have the following behaviors: 1. Collect unnecessary data and store it on the cloud server. 2. Cloud servers requested by users in non-China regions are deployed in China. If there are any of the above behaviors, please tell the truth, we will reasonably circumvent the use of corresponding services by non-China users. Thanks for the support!
2020-08-13 - 给属性 visibility 动态设置值(hidden=>visible)后,子长文本元素表现诡异
问题描述:给父级的 visibility 属性动态设置值(hidden: visible)后,子元素中如果有下面代码wxss示例中的 `.long-sentence`,那就会出现省略号不生效的情况。此问题在真机和开发者工具中都为必现。 截图:[图片] 期望:[图片] 复现代码: <!--index.wxml--> <view style="visibility: {{ visibility }};"> <view class="long-sentence">超长文案超长文案超长文案超长文案超长文案超长文案超长文案超长文案超长文案超长文案</view> </view> <!--index.js--> Page({ data: { visibility: 'hidden' }, onLoad: function () { this.setData({ visibility: 'visible' }) } }) <!--index.wxss--> .long-sentence { display: -webkit-box; overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: 1; -webkit-box-orient: vertical; }
2020-03-20 - setData 学问多
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ vm, newData, }) => { newState = { ...newState, ...newData, }; clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, }); newState = null }, 0); }; [代码] 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) }) [代码] 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); [代码] 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList }); [代码] 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList }) [代码] 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) }); [代码] 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList }); [代码] 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() } module.exports = { reverseArr: reverseArr } </wxs> <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 问题解决! 参考资料 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
2019-04-11 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 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 - 官方文档介绍wxml属性驼峰写法有误
* 链接:https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html * 这里官方解释是「指定属性值时对应使用连字符写法」,描述不全,验证了下指定属性值时驼峰写法也是支持的。[图片]
2019-06-17 - (3)强制更新
背景 此前有开发者反馈小程序发布新版本后,新版本覆盖率比较慢,因为小程序的更新机制是异步的,部分用户不会马上应用上新版本。 小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。 假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。 小程序的异步更新发生在冷启动过程,当发现新版本后,会异步下载新版本的代码包,但不会马上应用上最新版本,需要等小程序下一次冷启动,才会应用上新版本。 解决思路为了解决这个问题,我们内部也经历了数个方案的讨论,这里简单介绍下: 1. 同步检查更新(放弃):可能是最直接的解决思路,但主要问题是会影响小程序的启动速度,当下小程序的更新迭代是非常频繁的,部分用户可能每次启动都命中更新,如果需要同步检查更新+同步下载新的版本,那将会影响这部分用户的启动体验。 2. 模块热替换(放弃):从技术上来说,这是最好的方案,小程序运行起来后,在打开新页面时,马上应用新版本里的页面,但这就会存在新旧逻辑、页面共存问题,对于开发者来说,反而更不好处理,特别是涉及到全局变量时,情况会更复杂,对于我们已有的框架来说,也是一个大挑战,不过这个也是我们之后努力的方向。 3. 定时 check 新版本(目前方案):6.6.3 及以上版本的客户端,会定时 check 最近使用过的小程序是否有发布新版本;如果有,下次打开的时候会同步更新新版本再打开。这可以保证在新版本发布 24 小时后,所有小程序都能使用最新版本。(这部分是微信客户端自身优化,开发者无需关心) 4. 异步更新 + 强制更新(目前方案):同步检查更新与模块热替换两者之间的折衷方案,即还是维持异步更新机制,在异步下载完小程序代码包后,提供重启小程序的能力,这样在遇到紧急问题时可以马上解决。 异步更新 + 强制更新方案介绍从基础库 1.9.90 开始,我们提供了 wx.getUpdateManager 接口,使用该接口,可以获知是否有新版本小程序、新版本是否下载好以及应用新版本的能力。 当小程序冷启动时,会自动向微信后台请求新版本信息,如果有新版本,会马上触发新版本的下载。开发者可以通过 wx.getUpdateManager,获知当前更新的状态。 wx.getUpdateManager 接口会返回一个 UpdateManager 实例,UpdateManager 包含了三个回调: 1. onCheckForUpdate:当小程序向后台请求完新版本信息,会通知这个版本告知检查结果 2. onUpdateReady:当新版本下载完成,会回调这个事件 3. onUpdateFailed: 当新版本下载失败,会回调这个事件 还有重启应用新版本的接口: 1. applyUpdate:当新版本下载完成(onUpdateReady),调用该方法会强制当前小程序应用上新版本并重启 具体示例: [代码]// wx.getUpdateManager 在 1.9.90 才可用,请注意兼容[代码] [代码]const updateManager = wx.getUpdateManager()[代码] [代码]updateManager.onCheckForUpdate(function[代码] [代码](res) {[代码] [代码] // 请求完新版本信息的回调[代码] [代码] console.log(res.hasUpdate)[代码] [代码]})[代码] [代码]updateManager.onUpdateReady(function[代码] [代码]() {[代码] [代码] wx.showModal({[代码] [代码] title: '更新提示',[代码] [代码] content: '新版本已经准备好,是否马上重启小程序?',[代码] [代码] success: function[代码] [代码](res) {[代码] [代码] if[代码] [代码](res.confirm) {[代码] [代码] // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启[代码] [代码] updateManager.applyUpdate()[代码] [代码] }[代码] [代码] }[代码] [代码] })[代码] [代码]})[代码] [代码]updateManager.onUpdateFailed(function[代码] [代码]() {[代码] [代码] // 新的版本下载失败[代码] [代码]})[代码] 更详细信息可以参考 UpdateManager 的详细文档 最佳实践从用户体验上来说,我们还是建议只在非常必要时才强制用户重启更新,例如出现线上紧急 BUG。通常情况下,可以选通过 wx.showModal 弹出选择框让用户选择是否重启更新(实现请参考示例代码)。 如何调试最新版本的微信开发者工具提供了强制更新的调试能力,通过编译模式 - 编辑编译模式 - 勾上「下次编译时模拟更新」即可在开发者工具上调试强制更新功能。 最新开发者工具下载链接 点我。
2022-08-08 - CSS3 Animation动画的十二原则
作为前端的设计师和工程师,我们用 CSS 去做样式、定位并创建出好看的网站。我们经常用 CSS 去添加页面的运动过渡效果甚至动画,但我们经常做的不过如此。 [代码] 动效是一个有助于访客和用户理解我们设计的强有力工具。这里有些原则能最大限度地应用在我们的工作中。 迪士尼经过基础工作练习的长时间累积,在 1981 年出版的 The Illusion of Life: Disney Animation 一书中发表了动画的十二个原则 ([] (https://en.wikipedia.org/wiki/12_basic_principles_of_animation)) 。这些原则描述了动画能怎样用于让观众相信自己沉浸在现实世界中。 [代码] 在本文中,我会逐个介绍这十二个原则,并讨论它们怎样运用在网页中。你能在 Codepen 找到它们[] (https://codepen.io/collection/AxKOdY/)。 挤压和拉伸 (Squash and stretch) [图片] 这是物体存在质量且运动时质量保持不变的概念。当一个球在弹跳时,碰击到地面会变扁,恢复的时间会越来越短。 [代码] 创建对象的时候最有用的方法是参照实物,比如人、时钟和弹性球。 当它和网页元件一起工作时可能会忽略这个原则。DOM 对象不一定和实物相关,它会按需要在屏幕上缩放。例如,一个按钮会变大并变成一个信息框,或者错误信息会出现和消失。 尽管如此,挤压和伸缩效果可以为一个对象增加实物的感觉。甚至一些形状上的小变化就可以创造出细微但抢眼的效果。 HTML [代码] [代码] <h1>Principle 1: Squash and stretch</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle one"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].one .shape { animation: one 4s infinite ease-out; } .one .surface { background: #000; height: 10em; width: 1em; position: absolute; top: calc(50% - 4em); left: calc(50% + 10em); } @keyframes one { 0%, 15% { opacity: 0; } 15%, 25% { transform: none; animation-timing-function: cubic-bezier(1,-1.92,.95,.89); width: 4em; height: 4em; top: calc(50% - 2em); left: calc(50% - 2em); opacity: 1; } 35%, 45% { transform: translateX(8em); height: 6em; width: 2em; top: calc(50% - 3em); animation-timing-function: linear; opacity: 1; } 70%, 100% { transform: translateX(8em) translateY(5em); height: 6em; width: 2em; top: calc(50% - 3em); opacity: 0; } } body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 预备动作 (Anticipation) [图片] 运动不倾向于突然发生。在现实生活中,无论是一个球在掉到桌子前就开始滚动,或是一个人屈膝准备起跳,运动通常有着某种事先的累积。 [代码] 我们能用它去让我们的过渡动画显得更逼真。预备动作可以是一个细微的反弹,帮人们理解什么对象将在屏幕中发生变化并留下痕迹。 例如,悬停在一个元件上时可以在它变大前稍微缩小,在初始列表中添加额外的条目来介绍其它条目的移除方法。 [代码] HTML [代码]<h1>Principle 2: Anticipation</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle two"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].two .shape { animation: two 5s infinite ease-out; transform-origin: 50% 7em; } .two .surface { background: #000; width: 8em; height: 1em; position: absolute; top: calc(50% + 4em); left: calc(50% - 3em); } @keyframes two { 0%, 15% { opacity: 0; transform: none; } 15%, 25% { opacity: 1; transform: none; animation-timing-function: cubic-bezier(.5,.05,.91,.47); } 28%, 38% { transform: translateX(-2em); } 40%, 45% { transform: translateX(-4em); } 50%, 52% { transform: translateX(-4em) rotateZ(-20deg); } 70%, 75% { transform: translateX(-4em) rotateZ(-10deg); } 78% { transform: translateX(-4em) rotateZ(-24deg); opacity: 1; } 86%, 100% { transform: translateX(-6em) translateY(4em) rotateZ(-90deg); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 演出布局 (Staging) [图片] 演出布局是确保对象在场景中得以聚焦,让场景中的其它对象和视觉在主动画发生的地方让位。这意味着要么把主动画放到突出的位置,要么模糊其它元件来让用户专注于看他们需要看的东西。 [代码] 在网页方面,一种方法是用 model 覆盖在某些内容上。在现有页面添加一个遮罩并把那些主要关注的内容前置展示。 另一种方法是用动作。当很多对象在运动,你很难知道哪些值得关注。如果其它所有的动作停止,只留一个在运动,即使动得很微弱,这都可以让对象更容易被察觉。 [代码] 还有一种方法是做一个晃动和闪烁的按钮来简单地建议用户比如他们可能要保存文档。屏幕保持静态,所以再细微的动作也会突显出来。 HTML [代码]<h1>Principle 3: Staging</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle three"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].three .shape.a { transform: translateX(-12em); } .three .shape.c { transform: translateX(12em); } .three .shape.b { animation: three 5s infinite ease-out; transform-origin: 0 6em; } .three .shape.a, .three .shape.c { animation: threeb 5s infinite linear; } @keyframes three { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 26%, 30% { transform: rotateZ(-40deg); } 32.5% { transform: rotateZ(-38deg); } 35% { transform: rotateZ(-42deg); } 37.5% { transform: rotateZ(-38deg); } 40% { transform: rotateZ(-40deg); } 42.5% { transform: rotateZ(-38deg); } 45% { transform: rotateZ(-42deg); } 47.5% { transform: rotateZ(-38deg); animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 58%, 100% { transform: none; } } @keyframes threeb { 0%, 20% { filter: none; } 40%, 50% { filter: blur(5px); } 65%, 100% { filter: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 连续运动和姿态对应 (Straight-Ahead Action and Pose-to-Pose) [图片] 连续运动是绘制动画的每一帧,姿态对应是通常由一个 assistant 在定义一系列关键帧后填充间隔。 [代码] 大多数网页动画用的是姿态对应:关键帧之间的过渡可以通过浏览器在每个关键帧之间的插入尽可能多的帧使动画流畅。 [代码] 有一个例外是定时功能step。通过这个功能,浏览器 “steps” 可以把尽可能多的无序帧串清晰。你可以用这种方式绘制一系列图片并让浏览器按顺序显示出来,这开创了一种逐帧动画的风格。 HTML [代码]<h1>Principle 4: Straight Ahead Action and Pose to Pose</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle four"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].four .shape.a { left: calc(50% - 8em); animation: four 6s infinite cubic-bezier(.57,-0.5,.43,1.53); } .four .shape.b { left: calc(50% + 8em); animation: four 6s infinite steps(1); } @keyframes four { 0%, 10% { transform: none; } 26%, 30% { transform: rotateZ(-45deg) scale(1.25); } 40% { transform: rotateZ(-45deg) translate(2em, -2em) scale(1.8); } 50%, 75% { transform: rotateZ(-45deg) scale(1.1); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 跟随和重叠动作 (Follow Through and Overlapping Action) [图片] 事情并不总在同一时间发生。当一辆车从急刹到停下,车子会向前倾、有烟从轮胎冒出来、车里的司机继续向前冲。 [代码] 这些细节是跟随和重叠动作的例子。它们在网页中能被用作帮助强调什么东西被停止,并不会被遗忘。例如一个条目可能在滑动时稍滑微远了些,但它自己会纠正到正确位置。 要创造一个重叠动作的感觉,我们可以让元件以稍微不同的速度移动到每处。这是一种在 iOS 系统的视窗 (View) 过渡中被运用得很好的方法。一些按钮和元件以不同速率运动,整体效果会比全部东西以相同速率运动要更逼真,并留出时间让访客去适当理解变化。 [代码] 在网页方面,这可能意味着让过渡或动画的效果以不同速度来运行。 HTML [代码]<h1>Principle 5: Follow Through and Overlapping Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle five"> <div class="shape-container"> <div class="shape"></div> </div> </article> [代码] CSS [代码].five .shape { animation: five 4s infinite cubic-bezier(.64,-0.36,.1,1); position: relative; left: auto; top: auto; } .five .shape-container { animation: five-container 4s infinite cubic-bezier(.64,-0.36,.1,2); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } @keyframes five { 0%, 15% { opacity: 0; transform: translateX(-12em); } 15%, 25% { transform: translateX(-12em); opacity: 1; } 85%, 90% { transform: translateX(12em); opacity: 1; } 100% { transform: translateX(12em); opacity: 0; } } @keyframes five-container { 0%, 35% { transform: none; } 50%, 60% { transform: skewX(20deg); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 缓入缓出 (Slow In and Slow Out) [图片] 对象很少从静止状态一下子加速到最大速度,它们往往是逐步加速并在停止前变慢。没有加速和减速,动画感觉就像机器人。 [代码] 在 CSS 方面,缓入缓出很容易被理解,在一个动画过程中计时功能是一种描述变化速率的方式。 [代码] 使用计时功能,动画可以由慢加速 (ease-in)、由快减速 (ease-out),或者用贝塞尔曲线做出更复杂的效果。 HTML [代码]<h1>Principle 6: Slow in and Slow out</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle six"> <div class="shape a"></div> </article> [代码] CSS [代码].six .shape { animation: six 3s infinite cubic-bezier(0.5,0,0.5,1); } @keyframes six { 0%, 5% { transform: translate(-12em); } 45%, 55% { transform: translate(12em); } 95%, 100% { transform: translate(-12em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 弧线运动 (Arc) [图片] 虽然对象是更逼真了,当它们遵循「缓入缓出」的时候它们很少沿直线运动——它们倾向于沿弧线运动。 我们有几种 CSS 的方式来实现弧线运动。一种是结合多个动画,比如在弹力球动画里,可以让球上下移动的同时让它右移,这时候球的显示效果就是沿弧线运动。 HTML [代码]<h1>Principle 7: Arc (1)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevena"> <div class="shape-container"> <div class="shape a"></div> </div> </article> [代码] CSS [代码].sevena .shape-container { animation: move-right 6s infinite cubic-bezier(.37,.55,.49,.67); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } .sevena .shape { animation: bounce 6s infinite linear; border-radius: 50%; position: relative; left: auto; top: auto; } @keyframes move-right { 0% { transform: translateX(-20em); opacity: 1; } 80% { opacity: 1; } 90%, 100% { transform: translateX(20em); opacity: 0; } } @keyframes bounce { 0% { transform: translateY(-8em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 15% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 25% { transform: translateY(-4em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 32.5% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 40% { transform: translateY(0em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 45% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 50% { transform: translateY(3em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 56% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 60% { transform: translateY(6em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 64% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 66% { transform: translateY(7.5em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 70%, 100% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] [图片] 另外一种是旋转元件,我们可以设置一个在对象之外的原点来作为它的旋转中心。当我们旋转这个对象,它看上去就是沿着弧线运动。 HTML [代码]<h1>Principle 7: Arc (2)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevenb"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].sevenb .shape.a { animation: sevenb 3s infinite linear; top: calc(50% - 2em); left: calc(50% - 9em); transform-origin: 10em 50%; } .sevenb .shape.b { animation: sevenb 6s infinite linear reverse; background-color: yellow; width: 2em; height: 2em; left: calc(50% - 1em); top: calc(50% - 1em); } @keyframes sevenb { 100% { transform: rotateZ(360deg); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 次要动作 (Secondary Action) [图片] 虽然主动画正在发生,次要动作可以增强它的效果。这就好比某人在走路的时候摆动手臂和倾斜脑袋,或者弹性球弹起的时候扬起一些灰尘。 在网页方面,当主要焦点出现的时候就可以开始执行次要动作,比如拖拽一个条目到列表中间。 HTML [代码]<h1>Principle 8: Secondary Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eight"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].eight .shape.a { transform: translateX(-6em); animation: eight-shape-a 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } .eight .shape.b { top: calc(50% + 6em); opacity: 0; animation: eight-shape-b 4s linear infinite; } .eight .shape.c { transform: translateX(6em); animation: eight-shape-c 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } @keyframes eight-shape-a { 0%, 50% { transform: translateX(-5.5em); } 70%, 100% { transform: translateX(-10em); } } @keyframes eight-shape-b { 0% { transform: none; } 20%, 30% { transform: translateY(-1.5em); opacity: 1; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 32% { transform: translateY(-1.25em); opacity: 1; } 34% { transform: translateY(-1.75em); opacity: 1; } 36%, 38% { transform: translateY(-1.25em); opacity: 1; } 42%, 60% { transform: translateY(-1.5em); opacity: 1; } 75%, 100% { transform: translateY(-8em); opacity: 1; } } @keyframes eight-shape-c { 0%, 50% { transform: translateX(5.5em); } 70%, 100% { transform: translateX(10em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 时间节奏 (Timing) [图片] 动画的时间节奏是需要多久去完成,它可以被用来让看起来很重的对象做很重的动画,或者用在添加字符的动画中。 [代码] 这在网页上可能只要简单调整 animation-duration 或 transition-duration 值。 [代码] 这很容易让动画消耗更多时间,但调整时间节奏可以帮动画的内容和交互方式变得更出众。 HTML [代码]<h1>Principle 9: Timing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle nine"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].nine .shape.a { animation: nine 4s infinite cubic-bezier(.93,0,.67,1.21); left: calc(50% - 12em); transform-origin: 100% 6em; } .nine .shape.b { animation: nine 2s infinite cubic-bezier(1,-0.97,.23,1.84); left: calc(50% + 2em); transform-origin: 100% 100%; } @keyframes nine { 0%, 10% { transform: translateX(0); } 40%, 60% { transform: rotateZ(90deg); } 90%, 100% { transform: translateX(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 夸张手法 (Exaggeration) [图片] 夸张手法在漫画中是最常用来为某些动作刻画吸引力和增加戏剧性的,比如一只狼试图把自己的喉咙张得更开地去咬东西可能会表现出更恐怖或者幽默的效果。 在网页中,对象可以通过上下滑动去强调和刻画吸引力,比如在填充表单的时候生动部分会比收缩和变淡的部分更突出。 HTML [代码]<h1>Principle 10: Exaggeration</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle ten"> <div class="shape"></div> </article> [代码] CSS [代码].ten .shape { animation: ten 4s infinite linear; transform-origin: 50% 8em; top: calc(50% - 6em); } @keyframes ten { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.87,-1.05,.66,1.31); } 40% { transform: rotateZ(-45deg) scale(2); animation-timing-function: cubic-bezier(.16,.54,0,1.38); } 70%, 100% { transform: rotateZ(360deg) scale(1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 扎实的描绘 (Solid drawing) [图片] 当动画对象在三维中应该加倍注意确保它们遵循透视原则。因为人们习惯了生活在三维世界里,如果对象表现得与实际不符,会让它看起来很糟糕。 如今浏览器对三维变换的支持已经不错,这意味着我们可以在场景里旋转和放置三维对象,浏览器能自动控制它们的转换。 HTML [代码]<h1>Principle 11: Solid drawing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eleven"> <div class="shape"> <div class="container"> <span class="front"></span> <span class="back"></span> <span class="left"></span> <span class="right"></span> <span class="top"></span> <span class="bottom"></span> </div> </div> </article> [代码] CSS [代码].eleven .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .eleven .shape .container { animation: eleven 4s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; } .eleven .shape span { display: block; position: absolute; opacity: 1; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; } .eleven .shape span.front { transform: translateZ(3em); } .eleven .shape span.back { transform: translateZ(-3em); } .eleven .shape span.left { transform: rotateY(-90deg) translateZ(-3em); } .eleven .shape span.right { transform: rotateY(-90deg) translateZ(3em); } .eleven .shape span.top { transform: rotateX(-90deg) translateZ(-3em); } .eleven .shape span.bottom { transform: rotateX(-90deg) translateZ(3em); } @keyframes eleven { 0% { opacity: 0; } 10%, 40% { transform: none; opacity: 1; } 60%, 75% { transform: rotateX(-20deg) rotateY(-45deg) translateY(4em); animation-timing-function: cubic-bezier(1,-0.05,.43,-0.16); opacity: 1; } 100% { transform: translateZ(-180em) translateX(20em); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 吸引力 (Appeal) [图片] 吸引力是艺术作品的特质,让我们与艺术家的想法连接起来。就像一个演员身上的魅力,是注重细节和动作相结合而打造吸引性的结果。 [代码] 精心制作网页上的动画可以打造出吸引力,例如 Stripe 这样的公司用了大量的动画去增加它们结账流程的可靠性。 [代码] HTML [代码]<h1>Principle 12: Appeal</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle twelve"> <div class="shape"> <div class="container"> <span class="item one"></span> <span class="item two"></span> <span class="item three"></span> <span class="item four"></span> </div> </div> </article> [代码] CSS [代码].twelve .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .twelve .shape .container { animation: show-container 8s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; position: relative; } .twelve .item { background-color: #1f7bb6; position: absolute; } .twelve .item.one { animation: show-text 8s 0.1s infinite ease-out; height: 6%; width: 30%; top: 15%; left: 25%; } .twelve .item.two { animation: show-text 8s 0.2s infinite ease-out; height: 6%; width: 20%; top: 30%; left: 25%; } .twelve .item.three { animation: show-text 8s 0.3s infinite ease-out; height: 6%; width: 50%; top: 45%; left: 25%; } .twelve .item.four { animation: show-button 8s infinite cubic-bezier(.64,-0.36,.1,1.43); height: 20%; width: 40%; top: 65%; left: 30%; } @keyframes show-container { 0% { opacity: 0; transform: rotateX(-90deg); } 10% { opacity: 1; transform: none; width: 4em; height: 4em; } 15%, 90% { width: 12em; height: 12em; transform: translate(-4em, -4em); opacity: 1; } 100% { opacity: 0; transform: rotateX(-90deg); width: 4em; height: 4em; } } @keyframes show-text { 0%, 15% { transform: translateY(1em); opacity: 0; } 20%, 85% { opacity: 1; transform: none; } 88%, 100% { opacity: 0; transform: translateY(-1em); animation-timing-function: cubic-bezier(.64,-0.36,.1,1.43); } } @keyframes show-button { 0%, 25% { transform: scale(0); opacity: 0; } 35%, 80% { transform: none; opacity: 1; } 90%, 100% { opacity: 0; transform: scale(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码]
2019-03-21 - iconfont硬核,支持多色彩、支持自定义颜色
目前市面上很多教程叫我们如何把iconfont的字体整到小程序中,基本千篇一律,都有一个特点,就是需要用字体文件。 但是用字体文件意味着只能设置一种颜色了(单色)。这是个硬伤~~~ 所以,今天笔者花了一天时间,做了一个支持多色彩、支持自定义颜色的iconfont开源库。你一定会喜欢 [代码]<iconfont name="alipay" /> <iconfont name="alipay" color="{{['red', 'orange']}}" size="300" /> [代码] [图片] 特性: 1、纯组件 2、不依赖字体文件 3、支持px和rpx两种格式 4、原样渲染多色彩图标 4、图标颜色可定制 地址:https://github.com/iconfont-cli/mini-program-iconfont-cli 喜欢的小伙伴记得给个star呦。
2019-09-26 - 建议image组件css支持fill
- 需求的场景描述(希望解决的问题) svg图标比png图标的优势在于: 1.同一图标可指定不同尺寸的样式复用,不失真,移动端显示效果好. 2.压缩效果好,占空间小,这样程序整体加载快,用户体验好. 现image组件中可以显示svg图标,但不足的是,同一图标无法在不同使用场景下指定不同颜色. - 希望提供的能力 望早日支持svg的fill样式定义.
2018-10-23 - 微信 7.0.5 黑屏
就去就直接黑屏 小程序内->更多按钮->关于XXX->反馈与投诉->功能异常->提交 通过此途径有看到日志,并没有发现报错信息
2019-07-31 - setStorageSync:fail write DB data
只有我的手机有问题,突然发生这种现象。删除小程序,清缓存,重启微信,重启手机都试过了,就差重装微信没试过。 其他同事都是可以正常使用的。正式版,体验版,开发版现象一致。 功能是将接口带回的token存储,后续其他接口调用都会用到。 APPID:wxf9c391aa2a80c106 [图片]
2019-08-01 - web性能优化之渲染性能优化
引子: 笔者在业务开发过程中,需要一个vue版的无限滚动组件,从github上找了一些组件后发现效果都不太好(主要是卡顿),最后自己查阅一些渲染性能优化的文章后,基于iScroll二次开发了一个组件,自己觉得效果还不错,主要是利用了硬件渲染加速和dom元素的复用,有同样需求的朋友可以试一下。(https://github.com/zuolei828/vue-virtual-infinite-scroll) 针对这次组件的优化,记录一下渲染性能优化的比较系统的知识,个人能力所限,很多方面理解的可能不对,欢迎大家指正! 一个web页面的性能优化,包括加载(loading)性能优化以及渲染(rendering)性能优化,关于加载性能的优化在另一篇文章中讨论(加载优化),这里来整理一下渲染性能优化的相关知识。 浏览器多进程模型 为了方便后面优化知识的阐述,这里先简单介绍下浏览器的多进程模型(以chrome为例)。 [图片] 主要进程如图所示: Browser进程:浏览器的主进程,负责浏览器界面的显示,各个页面的管理,其他各种进程的管理; Renderer进程:页面的渲染进程,负责页面的渲染工作,Blink的工作主要在这个进程中完成(主要分成render主线程和合成器线程); NPAPI插件进程:每种类型的插件只会有一个进程,每个插件进程可以被多个Render进程共享; GPU进程:最多只有一个,当且仅当GPU硬件加速打开的时候才会被创建,主要用于对3D加速调用的实现; Pepper插件进程:同NPAPI插件进程,不同的是为Pepper插件而创建的进程 页面渲染过程 页面渲染中每一帧的渲染最多进行了如下五个步骤。 [图片] JavaScript:通常我们会使用 JavaScript 来实现页面视觉变化的效果。比如做一个动画或者往页面里添加一些 DOM 元素等。 Style:计算样式,这个过程是根据 CSS 选择器,对每个 DOM 元素匹配对应的 CSS 样式。 Layout:在知道对一个元素应用哪些样式之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,例如 body 元素的宽度一般会影响其子元素的宽度以及树中各处的节点,因此对于浏览器来说,布局过程是经常发生的。 Paint:绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个层(Layer)上完成的。 Composite:由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。 换成这个图来看渲染引擎的处理流程 [图片] 这个过程比较复杂,详细的留在后面介绍Composite优化的时候再阐述。先简单说一下中间步骤,DOM树构建完成后,等待JS和CSS一起合成了Render树,每一个DOM节点对应一个Render Object,根据RenderObject的样式属性,可能将多个或者单个的object转换成RenderLayer,通常,渲染引擎的软件渲染到这就结束了,在开启硬件加速后,某些RenderLayer才会被转换成GraphicsLayer,最后利用GPU来进行合成和最终呈现。 如何检测render性能 上面说的渲染的五个步骤中的每一个都有可能造成卡顿,当然根据css属性的不同,可能会跳过layout或者paint阶段(具体每个css属性影响哪些阶段,请查看css触发器,注意chrome现在用的是blink内核),那么如何知道页面runtime中触发了哪些步骤以及各自性能了,最好的方法就是使用chrome devtool中的performance来记录分析。 打开chrome开发者工具,切换到performance tab,点击record按钮,这时你对页面的操作就会被记录下来,点击stop后就能看到性能火焰图等信息了,点击Frames中的一帧,下方的Main区域就会集中到这一帧的运行过程,如下图所示。(红圈区域即为选中一帧) [图片] 黄色为JS,紫色为Style和Layout,绿色为Paint和Composite部分,选中每个部分会显示各自的花费时间等信息,可以看出这个图片中JS运行的时间太长。目前的显示设备一般刷新率是60FPS,所以理想中每帧的时间最好为16毫秒,利用performance就能很直观的看出渲染中哪一步骤出现问题,下面介绍如何对每个步骤进行优化。 优化JS执行 JS 经常会触发视觉变化。有时是直接通过样式操作,有时是会产生视觉变化的计算,例如搜索数据或将其排序。时机不当或长时间运行的 JS 可能是导致性能问题的常见原因。通常可以通过以下几个方法来优化JS的执行。 对于动画效果的实现,避免使用 setTimeout 或 setInterval,请使用 requestAnimationFrame。 将长时间运行的 JavaScript 从主线程移到 Web Worker。 使用微任务来执行对多个帧的 DOM 更改。 使用requestAnimationFrame来执行视觉变化 先看一张图 [图片] 为了避免显示撕裂,开启垂直同步后,显示器每16ms(假设为60HZ)会发出一个VSync信号,浏览器收到信号后开启一帧的渲染,中间过程可能只用CPU完成软件渲染,也可能利用GPU硬件渲染,最终将渲染结果绘制到帧缓冲区,在下一个VSync信号到来时,显示器显示最新的渲染结果,并通知开启下一帧渲染。 在16ms的间隔中,如果一帧没有渲染完,那么这一帧就会被丢弃,显示器还是显示之前的画面,就会造成掉帧;同时如果16ms内如果完成多次渲染,显示器也只会更新一次画面,多次的渲染就会造成CPU和GPU的资源浪费。所以最理想的情况就是每16ms只渲染一次,一些老的框架会使用setTimeout来实现出这个间隔,但是会出现下图的问题。 [图片] 由于不能保证renderer主线程的运行时间,有可能setTimeout的回调会正好在间隔的中间被执行,如果渲染不能在下次间隔前完成,还是会造成卡帧。为了保证每次渲染都在一帧的开始来执行,requestAnimationFrame是唯一正确的方法,但是在使用时候也要注意一点,在requestAnimationFrame的回调执行之前,如果多次调用requestAnimationFrame,也会导致下一帧开始时多次执行这个回调,造成结果的不正确,所以需要加一下类似下面代码的控制。 [代码]function onScroll (evt) { // Store the scroll value for laterz. lastScrollY = window.scrollY; // Prevent multiple rAF callbacks. if (scheduledAnimationFrame) return; scheduledAnimationFrame = true; requestAnimationFrame(readAndUpdatePage); } window.addEventListener('scroll', onScroll); [代码] 分割长时间的JS的执行 由于长时间的JS执行会阻塞渲染,要尽量缩减一帧中JS的执行时间,不需要DOM权限的操作可以移到web worker中,但是通常我们的JS代码都会造成视觉变化,所以可以将一个耗时任务拆分成若干微任务,并利用requestAnimationFrame来执行,如下代码所示。 [代码]var taskList = breakBigTaskIntoMicroTasks(monsterTaskList); requestAnimationFrame(processTaskList); function processTaskList(taskStartTime) { var taskFinishTime; do { // Assume the next task is pushed onto a stack. var nextTask = taskList.pop(); // Process nextTask. processTask(nextTask); // Go again if there’s enough time to do the next task. taskFinishTime = window.performance.now(); } while (taskFinishTime - taskStartTime < 3 && taskList.length > 0); if (taskList.length > 0) requestAnimationFrame(processTaskList); } [代码] 优化样式的计算过程 通过添加和删除元素,更改属性、类或通过动画来更改 DOM,全都会导致浏览器重新计算元素样式。计算样式通过两个阶段来完成,首先浏览器计算出给指定元素应用哪些类、伪选择器和 ID,然后从匹配选择器中获取所有样式规则,并计算出此元素的最终样式。在Chrome的Performance记录区域,可以看到每一帧的渲染中,都有一个recalculate style的紫色矩形,记录的就是此次重新计算的耗时及影响到的元素数量等信息。通常采用下述两个方法来优化计算过程: 降低选择器的复杂性 减少必须计算其样式的元素数量 有时候我们喜欢用p:nth-of-type(2),:nth-child(n)等选择器来书写css内容,因为这样方便我们在一个父元素的所有子元素中找出一个特例来修改样式,但是这样会增加计算的复杂度,浏览器要知道其它所有子元素的情形,通常还是建议给元素一个明确的类选择器,例如BEM。 优化布局 布局是浏览器计算各元素几何信息的过程:元素的大小以及在页面中的位置。如何优化需要做到以下几点。 尽可能避免触发布局 因为布局几乎总是作用到整个文档。 如果有大量元素,将需要很长时间来算出所有元素的位置和尺寸。修改元素的几何属性(大小,位置等)都会导致整个文档重新布局,这个时候可以利用tranform的位移,放大缩小等操作来避免重新布局(前提是开启了硬件加速),这部分会在后面的composite优化部分详细描述,下面看两个demo demo1(更改top属性导致重新布局) demo2(利用translate不会导致重新布局) [图片] [图片] 利用performance分析能看出demo2没有触发layout 使用flex布局而不是浮动 早些年因为兼容性的问题,喜欢用float来实现布局,现在请使用flexbox,布局的性能会得到显著提升,看一下两个demo demo1(使用float布局) demo2(使用flex布局) 利用performance来分析,为了模拟手机上的效果,请将cpu 4x down降速 [图片] float是26.77ms [图片] flex是13.43ms 提升了一倍,看下flexbox目前的兼容性 [图片] 非IE的情况下,大家请安心使用吧(吐槽下,为啥还有人用IE),再贴一张最近一年桌面浏览器占有率 [图片] 避免强制同步布局 回忆下帧的渲染步骤,JS先运行,然后计算样式,再来布局,然而,JS可以强制布局提前,这被称为强制同步布局,看下代码。 [代码]// Schedule our function to run at the start of the frame. requestAnimationFrame(logBoxHeight); function logBoxHeight() { box.classList.add('super-big'); // Gets the height of the box in pixels and logs it out. console.log(box.offsetHeight); } [代码] JS运行时,来自上一帧的浏览器的布局信息是已知的,但是例子中的回调方法先增加了一个类,这个时候浏览器必须先应用样式修改,再重新布局,然后才能输出高度信息。通常上一帧的布局信息已经够用,这种强制同步布局会造成性能浪费。 4. 避免布局抖动 有一种情况会频繁的强制同步布局,看一下代码。 [代码] function resizeAllParagraphsToMatchBlockWidth() { // Puts the browser into a read-write-read-write cycle. for (var i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = box.offsetWidth + 'px'; } } [代码] 此代码循环处理一组段落,并设置每个段落的宽度以匹配一个称为“box”的元素的宽度。这看起来没有害处,但问题是循环的每次迭代读取一个样式值 (box.offsetWidth),然后立即使用此值来更新段落的宽度 (paragraphs[i].style.width)。在循环的下次迭代时,浏览器必须考虑样式已更改这一事实,因为 offsetWidth 是上次请求的(在上一次迭代中),因此它必须应用样式更改,然后运行布局。每次迭代都将出现此问题! [代码]此示例的修正方法还是先读取值,然后写入值: [代码] [代码]// Read. var width = box.offsetWidth; function resizeAllParagraphsToMatchBlockWidth() { for (var i = 0; i < paragraphs.length; i++) { // Now write. paragraphs[i].style.width = width + 'px'; } } [代码] 优化绘制与合成 绘制是填充像素的过程,像素最终合成到用户的屏幕上。 它往往是渲染过程中运行时间最长的任务,应尽可能避免此任务。合成是将页面的已绘制部分放在一起以在屏幕上显示的过程。这两个过程通常需要放在一起优化,而且是渲染过程中最需要关注的优化点,所以一起来详细阐述下。在介绍优化之前,我们要了解一下Blink的渲染基础知识,再来回顾一下之前放的一张图。 [图片] 这张图展示了Blink从最初的DOM树如何转换到最终的用于合成的Graphics Layer树,具体是如下步骤: Nodes 和 DOM树 网页内容在Blink内部以Node为节点的树形结构存储,称为DOM树。网页中的每一个HTML 元素,包括元素之间的text都和一个Node相关联。DOM tree的最顶层Node 永远是Document Node. From Nodes to RenderObjects DOM树中每一个可视化的Node 节点都对应着一个RenderObject。RenderObject 也存储在一棵对应的树结构中,称为Render树。 RenderObject 知道如何在一个显示设备上绘制(paint) Node 节点的内容。它通过调用GraphicsContext提供的绘制接口来完成绘制过程。GraphicsContext最终负责将像素写入一块bitmap,这块bitmap会被显示在屏幕上。在Chrome中,GraphicsContext 封装了Skia( 2D图形库)。 之前对GraphicsContext的大多数调用都转变成对SkCanvas或SkPlatformCanvas的接口调用。不过为了把绘制的实际过程移出主线程(后面会详细讲),现在这些调用命令被替换成记录到SkPicture。SkPicture是一个能够记录command,最后可以replay这些command的有序数据结构,类似于display list。 From RenderObjects to RenderLayers 每一个RenderObject 都关联着RenderLayer。这种关联是通过祖先RenderObject 节点直接或间接地建立的。分享同一坐标系的RenderObject(比如被同一CSS transform属性影响的元素)必然位于同一RenderLayer。 正是由于RenderLayer的存在,网页上的元素才可以按照正确的顺序合成,从而恰当的显示有交叠的内容,和半透明元素等效果。通常来讲,满足下列条件之一时,RenderObject就会创建RenderLayer: 根节点 有明确的CSS定位属性(relative, absolute) 透明的(opacity 小于 1) 有overflow, an alpha mask or reflection 有CSS filter 有2D加速Context或者3D(webGL)context的 canvas 元素对应的 有video元素的 需要注意的是RenderObject和RenderLayer之间并不是一一对应的。 RenderObject 或者与它所创建的RenderLayer相关联(如果它创建了的话),或者与它的第一个拥有RenderLayer的祖先RenderObject创建的RenderLayer相关联。 RenderLayer 也会形成一个树型层次结构。这个树结构的根节点是与网页的根元素相对应的RenderLayer。每一个RenderLayer 节点的后代都是包含在父亲RenderLayer内的可视化的RenderLayer. 每一个RenderLayer的子节点都被存储在两个按升序排列的有序表中。negZOrderList 有序表中存储的子节点是z-index值为负的子RenderLayer,所以这些RenderLayer在当前RenderLayer的下面;posZOrderList有序表中存储的子节点是z-index值为正的RenderLayer,所以这些RenderLayer在当前RenderLayer的上面。 事实上,在老版本的chrome里(15年之前),有一个软件渲染路径的概念,就是不需要硬件加速的情况下,渲染到这里结束了,放一张图来简单了解一下。 [图片] 所有的RenderLayer构建完成后,浏览器渲染进程调用Skia递归的将layer树绘制到共享内存中的单个位图,然后通过IPC传递到Browser Process,最终由Browser Process负责将位图drawing到屏幕。 4. From RenderLayers to GraphicsLayers 为了有效利用GPU硬件加速渲染,Blink又引入了一个新的GraphicsLayer,并且专门独立了一个专门的Compositor(合成器) Thread来管理GraphicsLayer以及协调帧的生命周期(后面会专门介绍这个合成器)。作为一个前端开发,你会经常听到用transform: translateZ(0)来开启所谓的硬件加速,实质上就是提升成了GraphicsLayer。 每一个RenderLayer或者拥有自己的GraphicsLayer(如果这个RenderLayer是compositing Layer的话),或者是使用它的第一个拥有GraphicsLayer的祖先节点的GraphicsLayer. RenderLayer与GraphicsLayer的关系类似于RenderObject与RenderLayer之间的关系。每个GraphicsLayer都拥有一个GraphicsContext,与这个GraphicsLayer相对应的每个RenderLayer都绘制到这个GraphicsContext上。合成器会负责将多个的GraphicsContext输出的位图最终合成一个最终的image。 理论上讲,每一个RenderLayer都可以将自己绘制到一个单独的backing surface上以避免不必要的重绘。但是在实际中,这种做法会导致内存的大量浪费(尤其是VRAM)。在当前的Blink实现中,只有满足以下条件之一,RenderLayer才会拥有它自己的compositing layer。 layer 有3D或者perspective transform 属性值 layer是硬解码的video 元素使用的 layer是拥有3D context或2D加速context的Canvas标签使用的 layer是一个合成的插件使用的 layer使用了动画表示它的透明度,或者使用了动画形式的webkit 变换 layer 使用了加速的CSS 滤镜 拥有compositing layer后代的layer 渲染在compositing layer之上的layer(overlap) 最后一个overlap为啥会产生合成层了?看一个例子。 [图片] 图中蓝色矩形覆盖在绿色矩形之上,同时它们的父元素是一个GraphicsLayer,假设绿色矩形也是一个GraphicsLayer,如果蓝色不是,那么它将和父元素公用一个合成层,既变成如下图情形。 [图片] 绿色矩形覆盖了蓝色矩形,渲染的顺序就发生了错误,所以为了保证正确,overlap也必须提升为合成层。 5. Layer Squashing overlap引起的合成层提升经常出现,就会导致有很多的合成层,岂不是会造成内存大量浪费,所以Blink专门有Layer Squashing(层压缩)的处理。看一下demo(层压缩)。 打开chrome的Performance工具来分析,选中一帧后,会看到下方工具栏出现一个layer tab,选中这个tab就能看到页面对应的合成层信息。 [图片] 红色圈中部分是显示有几个合成层,右侧绿色圈中部分显示这个合成层形成的原因和大小等信息。很明显,中间可视区域的深蓝色的矩形因为开启3D加速的原因被提升为合成层,绿色,红色,浅蓝三个矩形因为overlap的原因被提升成了合成层。 当我们把鼠标移到绿色矩形上,对应的CSS属性也修改成3Dtransform,所以绿色矩形也被提升为合成层,剩下的红色和浅蓝还是因为overlap被提升为另一个合成层,如下图所示。 [图片] 每一个GraphicsLayer都有对应的Composite Layer,这样Chrome的合成器才知道如何对这个GraphicsLayer进行处理,下面我们就来阐述下什么是合成器。 合成器(Compositor) Chrome的合成器是一个用来管理GraphicsLayer树和协调帧的生命周期的软件库。最初合成器也是被设计在渲染进程的主线程中的,现在合成器被拆成了两部分,一半在主线程里面,负责绘制(painting),主要工作就是把layer树的信息记录到SkPicture中,并没有实际上产生像素;另一半变成了单独的Compositor Thread(简称为cc),也被称为impl thread,这部分是真正的drawing,负责将painting中记录的layer信息经过光栅,合成等操作,最终显示到屏幕。下面分步骤来详细阐述合成器的工作。 Recording: Painting from Blink’s Perspective 兴趣区域(interest area)是要被记录到SkPicture中的viewport附近的区域。每当DOM元素改变,Blink会把兴趣区域中失效的部分layer树信息记录到 SkPicture-backed GraphicsContext。记住,这一步并没有真正的绘制像素,只是记录了可以replay出像素的命令的一个display list。 The Commit: Handoff to the Compositor Thread 合成器线程的一个关键特性就是它维护了主线程状态的一个复制,因此可以根据这个复制来生成帧而不用去询问主线程。主线程的状态信息就是一个LayerChromiumtree,对应的合成器线程复制的是CCLayerImpltree,这两棵树理论上是彼此独立的,这就意味着合成器线程可以在主线程阻塞的情况下使用当前的复制信息执行drawing内容到屏幕。 而当主线程产生了新的兴趣区域,合成器线程如何知道去修改它所维持的树的状态了?合成器线程有一个专门的调度器,使用commit来定期同步两棵树的状态。commit会将主线程更新过的LayerChromiumtree的状态以及新的SkPicture命令传给合成器线程,并同时block主线程来达成同步。这也是主线程在一个帧的生成过程中的最后一步。由于合成器线程独立于主线程,而且专门负责实际的drawing,所以浏览器传来的用户输入都是直接传到合成器线程的,一些不需要主线程参与的交互,例如用户键盘输入等,合成器线程可以直接处理完成页面的更新,但是如果主线程注册了事件的回调,这时候合成器线程就必须将更新的CCLayerImpltree状态以及一些额外任务反向commit给主线程。 Tree Activation 当合成器线程通过主线程的commit同步到更新后的layer tree信息后,会检查哪些layer是失效的并且重新光栅化这些layer。这时active tree是合成器线程保留的上一帧的layer tree信息,而新光栅化的layer tree信息被称为pending tree。为了保持展示内容的一致性,只有当pending tree已经完全光栅化后才会转换成新的active tree,从pending到active的过程被称为tree activation。 需要注意的非常重要的一点是有可能屏幕会滚动到当前的active tree之外,因为主线程只记录viewport周围的兴趣区域。这个时候合成器线程就会询问主线程去记录和commit新区域的信息,但是如果新的pending tree没能及时激活,用户就会滚动到一个所谓的 checkerboard zone。 为了减轻checkerboard zone,chrome将pending tree的光栅化分成低分辨率的部分和高分辨率的部分,当要出现checkerboard zone的时候优先光栅化低分辨率的部分并激活用来展现,这也就是为什么有时候有些页面在快速滚动时候会变模糊(例如google地图)。这部分工作是一个专门的tile manager来管理的(下一节的内容)。 Tiling 光栅化整个页面的layer tree是非常浪费CPU和内存的,所以合成器线程将layer tree分割成多个小的tile,设定好各个tile的优先级(根据离viewport的远近等因素来设置),并且专门创建了tile worker线程(一个或者多个)来执行这些tile的光栅化。在chrome的performance分析中能看到页面的tile,如图所示,勾选rending选项中的红色区域,就能看到页面中绿色border的tile。 [图片] Rasterization: Painting from cc/Skia’s perspective 主线程记录的SkPicture的display list,合成器线程通过两种方式来转变成最终上传到GPU的纹理(texture)。一种是基于CPU、使用Skia库的Software Rasterization,首先绘制进位图里,然后再作为纹理上传至GPU。这一方式中,Compositor Thread会创建出一个或多个Compositor Tile Worker Thread,然后多线程并行执行SkPicture records中的绘画操作,以之前介绍的Graphics Layer为单位,绘制Graphics Layer里的Render Object。同时这一过程是将Layer拆分为多个小tile进行光栅化后写入进tile对应的位图中的。另一种则是基于GPU的Hardware Rasterization,也是基于tile worker线程,也是分tile进行,但是这个过程不是像Software Rasterization那样在CPU里绘制到位图里,然后再上传到GPU中作为纹理。而是借助Skia’s OpenGL backend (Ganesh) 直接在GPU中的纹理中进行绘画和光栅化,填充像素。 Drawing on the GPU 一旦所有的纹理已经被填充,GPU进程就能使用深度优先遍历来遍历layer树的信息,然后调用GL/D3D命令来draw每个layer到帧的缓冲池,当然实际上每个layer的drawing还是分成tiles来进行的。下面这张图展示了GPU进程如何进行drawing。 [图片] 好了,到这里整个Compositor的部分阐述完了,我们也就知道了如何对帧渲染步骤中的绘制和合成来进行优化了–将页面频繁变化的部分提升到合成层,通常使用transform: translateZ(0),利用GPU渲染加速来进行合成。总结下,主要有以下几个优点。 合成层的位图,会交由 GPU 合成,比 CPU 处理要快 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层 对于 transform 和 opacity 效果,不会触发 layout 和 paint 当然,不能盲目的增加合成层数量,因为增加一个合成层就意味着更多的内存分配(特别是GPU内存)和更复杂的合成管理。我们应该专注于那些频繁变化的区域来进行优化。 帧的整个渲染步骤的优化都阐述完了,下面贴一张完整的流程图来总结一下。 [图片] 注意并不是每一帧中这些步骤都会发生,最多的步骤如下: Frame Start. 合成器线程收到来自浏览器的Vsync信号和Input data,一帧开始。 Input event handlers. Input data被合成器线程传给了主线程,注册的事件回调被执行,注意这里合成器线程做了优化,保证一帧中最多只会触发一次event handler,所以自带了requestAnimationFrame的节流效果。 requestAnimationFrame. 如果之前注册了raf回调,会在这里执行,这是最完美的执行更新视觉的地方。唯一要注意的就是避免发生强制布局,即导致样式计算和布局提前(红线所示)。 Parse HTML. 新增的html会在这里被解析,生成对应DOM元素。大部分你会在page load和appendChild之类操作后见到它。 Recalc Styles. 如果你在JS执行过程中修改了样式或者改动了DOM,那么便会执行这一步,重新计算指定元素及其子元素的样式。 Layout. 如果有涉及元素位置信息的DOM改动或者样式改动,那么浏览器会重新计算所有元素的位置、尺寸信息。 Update Layer Tree. 这一步实际上是更新Render Layer的层叠顺序关系,保证层叠的正确。 Paint. paint操作实际上有两步,第一步是主进程将layer tree的相关信息记录到SkPicture中,类似一个display list;第二部是合成器线程replay这个记录list来光栅化和填充上传纹理。主线程的paint只是第一步。 Composite. 这里其实也分两步,主线程这里计算出每个Graphics Layers的合成时所需要的data,包括位移(Translation)、缩放(Scale)、旋转(Rotation)、Alpha 混合等操作的参数,然后就是图中我们看到的第一个commit,主线程通知合成器线程去同步layer tree的信息。然后主线程此时会去执行requestIdleCallback。这一步并没有真正对Graphics Layers完成位图的composite。 Raster Scheduled and Rasterize。 第8步生成的SkPicture records在这个阶段被执行。合成器线程创建出若干个Compositor Tile Worker Thread,利用CPU软件光栅化或者GPU的硬件光栅化,最终将纹理写入了GPU内存中。 Frame End. 合成器线程已经完成paint和composite的工作,这时会发送一个commit给GPU进程,告诉他可以进行draw了,同时会传达主线程一个commit done,如果一个帧中视觉的变化没有主线程参与,这里合成器线程也会同步更新后的合成器layer tree信息给主线程。 draw. GPU进程按照深度优先遍历将最后的纹理draw到帧缓冲区,等待显示器的下一个Vsync到来时去显示。 结语 整个浏览器页面渲染的过程以及优化都阐述完了,性能优化是一门艺术,本文也只是很浅显的探讨了其中的一些基本概念和设计思想,如果想深入理解具体的架构和实现过程,还是要去阅读一下chrome的内核源码。对于我们前端开发来说,切忌的是为了优化而优化,实际开发过程中碰到了页面卡顿的情况,利用performance来分析找出卡顿的原因,针对卡顿的步骤不断进行改进测试,才是正确的优化方法。 参考引用 GPU Accelerated Compositing in Chrome compositor-thread-architecture rendering-performance the-anatomy-of-a-frame 无线性能优化:Composite
2019-03-15 - 小程序支持 http 304吗?
想通过 etag和last-modifiied来 缓存 json数据。浏览器本身是支持 304的,请问小程序能支持 etag和last-modified吗?支持304 status吗?
2019-07-01 - position:sticky
position: sticky; position: -webkit-sticky; 在安卓6.6.7上无效 (线下无效 线上未知)
2018-06-25 - 全自动、全面化收集 formid 思路,开启脑洞时刻
formid 基础知识 formid 是啥 小程序给用户发消息的唯一途径,所以前端要尽量多收集 formid 保存到服务端; formid 怎么收集 小程序要求必须用户提交表单才能得到formid; form 增加 report-submit 属性,并通过 button submit,前端 formSubmit 事件就能获得 formid ,如下方代码所示: [代码]<form bindsubmit="formSubmit" report-submit class="layout"> <button formType="submit" class="button" hover-class="none"> 。。。 </button> </form> [代码] formid 组件封装(初期思路) 我们将form与button 封装成组件(fomids),通过slot,可以显示任意元素,模拟form submit; 把页面上需要点击的元素用fomids包起来,fomids 将收集的 formid 放入cache,然后通过网络请求将 fomid 传给服务端; 大数小程序都是这么做的,这样做也没有问题,就是略微麻烦,form 与button 组合的样式有时不太好控制 formid 组件封装(进阶思路) 此思路是上面思路的延申,我们把 formids 这个组件做成layout级,把整个页面包在里面,利用事件自动冒泡的特性,只要有点击事件,就能收集到 formid 新建组件layout [代码]<!--components/layout.wxml--> <form bindsubmit="formSubmit" report-submit class="layout"> <button formType="submit" class="button" hover-class="none"> <view class="fixed"><slot></slot></view> </button> </form> [代码] [代码]/* components/layout.wxss */ .layout { display: inline-block; padding-left: 0; padding-right: 0; box-sizing: border-box; font-size: inherit; text-align: left; text-decoration: none; line-height: inherit; -webkit-tap-highlight-color: transparent; color: inherit; width: 100%; position: relative; } .layout .button{ display: inline-block; padding-left: 0; padding-right: 0; box-sizing: border-box; font-size: inherit; text-align: left; text-decoration: none; line-height: inherit; -webkit-tap-highlight-color: #000; color: inherit; width: 100%; position: relative; } .layout .button .fixed{ position:relative; z-index: 9999; width: 100%; } .layout .button:before,.layout .button:after{ border-width: 0; } [代码] [代码]// components/layout.js Component({ methods:{ formSubmit: function(e) { console.log('layout.formids',e.detail.formId) if("the formId is a mock one"!=e.detail.formId){ let formids=wx.getStorageSync('formids') || []; formids.push(e.detail.formId); formids=[...new Set(formids)]; wx.setStorage({key:'formids',data:formids}); } }, }, }) [代码] 将 layout 添加为全局组件 app.json 中增加 [代码]"usingComponents":{ "layout":"/components/layout" }, [代码] 在页面wxml中使用 [代码]<layout> <view class="pages">...</view> </layout> [代码] 怎么将 formid 提给服务端 首先你必须将wx.requxest 进行封装为myRequxest,页面上都是用myRequxest 进行网络请求; myRequxest 中 header 增加formids,从cache中获得放到header中;并删除formid缓存;(需要服务端从header中获得formids并存起来) [代码]function myRequxest(....){ let formids = wx.getStorageSync('formids'); if(formids){ wx.removeStorage({key:'formids'}); } let openid='';//用户openid wx.request({ ... header: { formids:formids.toString(), openid:openid, }, ... }) } [代码] 延申思考 从 写 form 收集 -> 封装 formids 组件 -> 封装 layout(formids) 组件,跳出思维固化,可以做更多可能; 希望这个思路能给你一些触发
2019-07-31 - 升级了7.0.5 用户反应出现黑屏
几乎必现的黑屏 用户升级了7.0.5 ,当编辑上传图片的时候,就会出现黑屏, 用户用的是vivoX21,以前没有这个问题。用户的另外一部手机三星,试了下也没有这个问题 录屏1: http://qxbimg.yiqujing.com/contvideo/20190718/0532/upload_6rg12mqofhezdpdp0rf35bkq1o60ej9r.mp4 录屏2: http://qxbimg.yiqujing.com/contvideo/20190718/0532/upload_yfehn9rmds0ugvw42t4hjym2cw4ze3fg.mp4 测试了我们几个小程序,大都是在编辑图片的地方会就容易黑屏。 =======补充=========== 最新反馈的: 里面没有日志 [图片] 之前反馈的,有日志 [图片]
2019-07-18 - this.setData更新movable-view数据,组件位置重置
Bug * Bug 表现是什么?预期表现是什么? this.setData更新movable-view数据,movable-view组件位置重置(并没有修改x,y值);预期表示是this.setData更新movable-view数据,组件的位置不会改变 * 如何复现? .wxml <movable-area style="height: 1000rpx; width: 100%; background: red;" scale-area> <block wx:for="{{imgsk}}" wx:key> <movable-view style="width:{{item.width}};height:{{item.height}};background:{{item.bgcolor}};"x="{{item.x}}" y="{{item.y}}" direction="all" damping="99999999999" data-ind="{{item.ind}}"> movable-view> block> movable-area> <view style="width:120rpx;height:60rpx;background:yellow" catchtap="more"> <text>点我text> view> .js Page({ data:{ imgsk: [ { img_src: '/images/yizi.png', x: 200, y: 120, width: '80rpx', height: '80rpx', bgcolor: 'blue' }, { img_src: '/images/yizi.png', x: 100, y: 400, width: '80rpx', height: '80rpx', bgcolor: 'green' } ] }, more:function(){ // var inde = this.data.imgsk.length // this.data.imgsk.push({ // img_src: '/images/yizi.png', // x: 10, // y: 10, // width: '80rpx', // height: '80rpx', // bgcolor: 'black' // }); var imgskc = this.data.imgsk; this.setData({ imgsk: imgskc }); } }) * 解决方式 修改.wxml为以下代码 <movable-area style="height: 1000rpx; width: 100%; background: red;" scale-area> <block wx:for="{{imgsk}}" wx:key> <movable-view style="width:{{item.width}};height:{{item.height}};background:{{item.bgcolor}};transform: translateX({{item.x+'px'}}) translateY({{item.y+'px'}}) translateZ(0px) scale(1); transform-origin: center center 0px;"x="{{item.x}}" y="{{item.y}}" direction="all" damping="99999999999" data-ind="{{item.ind}}"> movable-view> block> movable-area> <view style="width:120rpx;height:60rpx;background:yellow" catchtap="more"> <text>点我text> view> 说明: 位置信息最终是通过transform: translateX({{item.x+'px'}}) translateY({{item.y+'px'}}) translateZ(0px) scale(1); transform-origin: center center 0px;来确定的,因此添加这些属性且设置对应值就能解决之前的问题
2018-03-16 - 自定义导航栏所有机型的适配方案
写在前面的话 大家看到这个文章时一定会感觉这是在炒剩饭,社区中已经有那么多分享自定义导航适配的文章了,为什么我还要再写一个呢? 主要原因就是,社区中大部分的适配方案中给出的大小是不精确的,并不能完美适配各种场景。 社区中大部分文章给到的值是 iOS -> 44px , Android -> 48px 思路 正常来讲,iOS和Android下的胶囊按钮的位置以及大小都是相同且不变的,我们可以通过胶囊按钮的位置和大小再配合 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 来计算出导航栏的位置和大小。 小程序提供了一个获取菜单按钮(右上角胶囊按钮)的布局位置信息的API,可以通过这个API获取到胶囊按钮的位置信息,但是经过实际测试,这个接口目前存在BUG,得到的值经常是错误的(通过特殊手段可以偶尔拿到正确的值),这个接口目前是无法使用的,等待官方修复吧。 下面是我经过实际测试得到的准确数据: 真机和开发者工具模拟器上的胶囊按钮不一样 [代码]# iOS top 4px right 7px width 87px height 32px # Android top 8px right 10px width 95px height 32px # 开发者工具模拟器(iOS) top 6px right 10px width 87px height 32px # 开发者工具模拟器(Android) top 8px right 10px width 87px height 32px [代码] [代码]top[代码] 的值是从 [代码]statusBarHeight[代码] 作为原点开始计算的。 使用上面数据中胶囊按钮的高度加 [代码]top[代码] * 2 上再加上 [代码]statusBarHeight[代码] 的高度就可以得到整个导航栏的高度了。 为什么 [代码]top[代码] * 2 ?因为胶囊按钮是垂直居中在 title 那一栏中的,上下都要有边距。 扩展 通过胶囊按钮的 [代码]right[代码] 可以准确的算出自定义导航的 [代码]左边距[代码]。 通过胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]右边距[代码] 。 通过 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]windowWidth[代码] - 胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]width[代码] 。 再扩展 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 每个机型都不一样,刘海屏得到的数据也是准确的。 如果是自定义整个页面,iPhone X系列的刘海屏,底部要留 [代码]68px[代码] ,不要问我为什么! 代码片段 https://developers.weixin.qq.com/s/Q79g6kmo7w5J
2019-02-25 - 有赞前端质量保障体系
前言 最近一年多一直在做前端的一些测试,从小程序到店铺装修,基本都是纯前端的工作,刚开始从后端测试转为前端测试的时候,对前端东西茫然无感,而且团队内没有人做过纯前端的测试工作,只能一边踩坑一边总结经验,然后将容易出现问题的点形成体系、不断总结摸索,最终形成了目前的一套前端测试解决方案。在此,将有赞的前端质量保障体系进行总结,希望和大家一起交流。 先来全局看下有赞前端的技术架构和针对每个不同的层次,主要做了哪些保障质量的事情: [图片] [图片] 有赞的 Node 技术架构分为业务层、基础框架层、通用组件和基础服务层,我们日常比较关注的是基础框架、通用组件和业务层代码。Node 业务层做了两件事情,一是提供页面渲染的 client 层,用于和 C 端用户交互,包括样式、行为 js 等;二是提供数据服务的 server 层,用于组装后台提供的各种接口,完成面向 C 端的接口封装。 对于每个不同的层,我们都做了一些事情来保障质量,包括: 针对整个业务层的 UI 自动化、核心接口|页面拨测; 针对 client 层的 sentry 报警; 针对 server 层的接口测试、业务报警; 针对基础框架和通用组件的单元测试; 针对通用组件变更的版本变更报警; 针对线上发布的流程规范、用例维护等。 下面就来分别讲一下这几个维度的质量保障工作。 一、UI 自动化 很多人会认为,UI 自动化维护成本高、性价比低,但是为什么在有赞的前端质量保证体系中放在了最前面呢? 前端重用户交互,单纯的接口测试、单元测试不能真实反映用户的操作路径,并且从以往的经验中总结得出,因为各种不可控因素导致的发布 A 功能而 B 功能无法使用,特别是核心简单场景的不可用时有出现,所以每次发布一个应用前,都会将此应用提供的核心功能执行一遍,那随着业务的不断积累,需要回归的测试场景也越来越多,导致回归的工作量巨大。为了降低人力成本,我们亟需通过自动化手段释放劳动力,所以将核心流程回归的 UI 自动化提到了最核心地位。 当然,UI 自动化的最大痛点确实是维护成本,为降低维护成本,我们将页面分为组件维度、页面维度,并提供统一的包来处理公用组件、特殊页面的通用逻辑,封装通用方法等,例如初始化浏览器信息、环境选择、登录、多网点切换、点击、输入、获取元素内容等等,业务回归用例只需要关注自己的用例操作步骤即可。 1、框架选择 – puppeteer[1],它是由 Chrome 维护的 Node 库,基于 DevTools 协议来驱动 chrome 或者 chromium 浏览器运行,支持 headless 和 non-headless 两种方式。官网提供了非常丰富的文档,简单易学。 UI 自动化框架有很多种,包括 selenium、phantom;对比后发现 puppeteer 比较轻量,只需要增加一个 npm 包即可使用;它是基于事件驱动的方式,比 selenium 的等待轮询更稳当、性能更佳;另外,它是 chrome 原生支持,能提供所有 chrome 支持的 api,同时我们的业务场景只需要覆盖 chrome,所以它是最好的选择。 – mocha[2] + mochawesome[3],mocha 是比较主流的测试框架,支持 beforeEach、before、afterEach、after 等钩子函数,assert 断言,测试套件,用例编排等。 mochawesome 是 mocha 测试框架的第三方插件,支持生成漂亮的 html/css 报告。 js 测试框架同样有很多可以选择,mocha、ava、Jtest 等等,选择 mocha 是因为它更灵活,很多配置可以结合第三方库,比如 report 就是结合了 mochawesome 来生成好看的 html 报告;断言可以用 powser-assert 替代。 2、脚本编写 封装基础库 封装 pc 端、h5 端浏览器的初始化过程 封装 pc 端、h5 端登录统一处理 封装页面模型和组件模型 封装上传组件、日期组件、select 组件等的统一操作方法 封装 input、click、hover、tap、scrollTo、hover、isElementShow、isElementExist、getElementVariable 等方法 提供根据 “html 标签>>页面文字” 形式获取页面元素及操作方法的统一支持 封装 baseTest,增加用例开始、结束后的统一操作 封装 assert,增加断言日志记录 业务用例 安装基础库 编排业务用例 3、执行逻辑 分环境执行 增加预上线环境代码变更触发、线上环境自动执行 监控源码变更 增加 gitlab webhook,监控开发源码合并 master 时自动在预上线环境执行 增加 gitlab webhook,监控测试用例变更时自动在生产环境执行 每日定时执行 增加 crontab,每日定时执行线上环境 [图片] [图片] [图片] [图片] 二、接口测试 接口测试主要针对于 Node 的 server 层,根据我们的开发规范,Node 不做复杂的业务逻辑,但是需要将服务化应用提供 dubbo 接口进行一次转换,或将多个 dubbo 接口组合起来,提供一个可供 h5/小程序渲染数据的 http 接口,转化过程就带来了各种数据的获取、组合、转换,形成了新的端到端接口。这个时候单单靠服务化接口的自动化已经不能保障对上层接口的全覆盖,所以我们针对 Node 接口也进行自动化测试。为了使用测试内部统一的测试框架,我们通过 java 去请求 Node 提供的 http 接口,那么当用例都写好之后,该如何评判接口测试的质量?是否完全覆盖了全部业务逻辑呢?此时就需要一个行之有效的方法来获取到测试的覆盖情况,以检查有哪些场景是接口测试中未覆盖的,做到更好的查漏补缺。 – istanbul[4] 是业界比较易用的 js 覆盖率工具,它利用模块加载的钩子计算语句、行、方法和分支覆盖率,以便在执行测试用例时透明的增加覆盖率。它支持所有类型的 js 覆盖率,包括单元测试、服务端功能测试以及浏览器测试。 但是,我们的接口用例写在 Java 代码中,通过 Http 请求的方式到达 Node 服务器,非 js 单测,也非浏览器功能测试,如何才能获取到 Node 接口的覆盖率呢? 解决办法是增加 cover 参数:–handle-sigint,通过增加 --handle-sigint 参数启动服务,当服务接收到一个 SIGINT 信号(linux 中 SIGINT 关联了 Ctrl+C),会通知 istanbul 生成覆盖率。这个命令非常适合我们,并且因此形成了我们接口覆盖率的一个模型: [代码]1. istanbule --handle-sigint 启动服务 2. 执行测试用例 3. 发送 SIGINT结束istanbule,得到覆盖率 [代码] 最终,解决了我们的 Node 接口覆盖率问题,并通过 jenkins 持续集成来自动构建 [图片] [图片] [图片] 当然,在获取覆盖率的时候有需求文件是不需要统计的,可以通过在根路径下增加 .istanbule.yml 文件的方式,来排除或者指定需要统计覆盖率的文件 [代码]verbose: false instrumentation: root: . extensions: - .js default-excludes: true excludes:['**/common/**','**/app/constants/**','**/lib/**'] embed-source: false variable: __coverage__ compact: true preserve-comments: false complete-copy: false save-baseline: false baseline-file: ./coverage/coverage-baseline.json include-all-sources: false include-pid: false es-modules: false reporting: print: summary reports: - lcov dir: ./coverage watermarks: statements: [50, 80] lines: [50, 80] functions: [50, 80] branches: [50, 80] report-config: clover: {file: clover.xml} cobertura: {file: cobertura-coverage.xml} json: {file: coverage-final.json} json-summary: {file: coverage-summary.json} lcovonly: {file: lcov.info} teamcity: {file: null, blockName: Code Coverage Summary} text: {file: null, maxCols: 0} text-lcov: {file: lcov.info} text-summary: {file: null} hooks: hook-run-in-context: false post-require-hook: null handle-sigint: false check: global: statements: 0 lines: 0 branches: 0 functions: 0 excludes: [] each: statements: 0 lines: 0 branches: 0 functions: 0 excludes: [] [代码] 三、单元测试 单元测试在测试分层中处于金字塔最底层的位置,单元测试做的比较到位的情况下,能过滤掉大部分的问题,并且提早发现 bug,也可以降低 bug 成本。推行一段时间的单测后发现,在有赞的 Node 框架中,业务层的 server 端只做接口组装,client 端面向浏览器,都不太适合做单元测试,所以我们只针对基础框架和通用组件进行单测,保障基础服务可以通过单测排除大部分的问题。比如基础框架中店铺通用信息服务,单测检查店铺信息获取;比如页面级商品组件,单测检查商品组件渲染的 html 是否和原来一致。 单测方案试行了两个框架: Jest[5] ava[6] 比较推荐的是 Jest 方案,它支持 Matchers 方式断言;支持 Snapshot Testing,可测试组件类代码渲染的 html 是否正确;支持多种 mock,包括 mock 方法实现、mock 定时器、mock 依赖的 module 等;支持 istanbule,可以方便的获取覆盖率。 总之,前端的单测方案也越来越成熟,需要前端开发人员更加关注 js 单测,将 bug 扼杀在摇篮中。 四、基础库变更报警 上面我们已经对基础服务和基础组件进行了单元测试,但是单测也不能完全保证基础库的变更完全没有问题,伴随着业务层引入新版本的基础库,bug 会进一步带入到业务层,最终影响 C 端用户的正常使用。那如何保障每次业务层引入新版本的基础库之后能做到全面的回归?如何让业务测试同学对基础库变更更加敏感呢?针对这种情况,我们着手做了一个基础库版本变更的小工具。实现思路如下: [代码]1. 对比一次 master 代码的提交或 merge 请求,判断 package.json 中是否有特定基础库版本变更 2. 将对应基础库的前后两个版本的代码对比发送到测试负责人 3. 根据 changelog 判断此次回归的用例范围 4. 增加 gitlab webhook,只有合并到合并发布分支或者 master 分支的代码才触发检查 [代码] 这个小工具的引入能及时通知测试人员针对什么需求改动了基础组件,以及这次基础组件的升级主要影响了哪些方面,这样能避免相对黑盒的测试。 第一版实现了最简功能,后续再深挖需求,可以做到前端代码变更的精准测试。 [图片] 五、sentry 报警 在刚接触前端测试的时候,js 的报错没有任何追踪,对于排查问题和定位问题有很大困扰。因此我们着手引入了 sentry 报警监控,用于监控线上环境 js 的运行情况。 – sentry[7] 是一款开源的错误追踪工具,它可以帮助开发者实时监控和修复崩溃。 开始我们接入的方式比较简单粗暴,直接全局接入,带来的问题是报警信息非常庞大,全局上报后 info、warn 信息都会打出来。 更改后,使用 sentry 的姿势是: sentry 的全局信息上报,并进行筛选 错误类型: TypeError 或者 ReferenceError 错误出现用户 > 1k 错误出现在 js 文件中 出现错误的店铺 > 2家 增加核心业务异常流程的主动上报 最终将筛选后的错误信息通过邮件的形式发送给告警接收人,在固定的时间集中修复。 [图片] [图片] 六、业务报警 除了 sentry 监控报警,Node 接口层的业务报警同样是必不可少的一部分,它能及时发现 Node 提供的接口中存在的业务异常。这部分是开发和运维同学做的,包括在 Node 框架底层接入日志系统;在业务层正确的上报错误级别、错误内容、错误堆栈信息;在日志系统增加合理的告警策略,超过阈值之后短信、电话告警,以便于及时发现问题、排查问题。 业务告警是最能快速反应生产环境问题的一环,如果某次发布之后发生告警,我们第一时间选择回滚,以保证线上的稳定性。 七、约定规范 除了上述的一些测试和告警手段之外,我们也做了一些流程规范、用例维护等基础建设,包括: 发布规范 多个日常分支合并发布 限制发布时间 规范发布流程 整理自测核心检查要点 基线用例库 不同业务 P0 核心用例定期更新 项目用例定期更新到业务回归用例库 线上问题场景及时更新到回归用例库 目前有赞的前端测试套路基本就是这样,当然有些平时的努力没有完全展开,例如接口测试中增加返回值结构体对比;增加线上接口或页面的拨测[8];给开发进行自测用例设计培训等等。也还有很多新功能探索中,如接入流量对比引擎,将线上流量导到预上线环境,在代码上线前进行对比测试;增加UI自动化的截图对比;探索小程序的UI自动化等等。 参考链接 [1] https://github.com/GoogleChrome/puppeteer [2] https://www.npmjs.com/package/mocha [3] https://www.npmjs.com/package/mochawesome [4] https://github.com/gotwarlost/istanbul [5] https://github.com/facebook/jest [6] https://github.com/avajs/ava [7] https://docs.sentry.io [8] https://tech.youzan.com/youzan-online-active-testing/
2019-04-25 - 小程序多端框架全面测评
最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一些疑惑:这些框架都有什么优缺点?到底应该用哪个? 作为 Taro 开发团队一员,笔者想在本文尽量站在一个客观公正的角度去评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待,权当抛砖引玉。 那么,当我们在讨论多端框架时,我们在谈论什么: 多端 笔者以为,现在流行的多端框架可以大致分为三类: 1. 全包型 这类框架最大的特点就是从底层的渲染引擎、布局引擎,到中层的 DSL,再到上层的框架全部由自己开发,代表框架是 Qt 和 Flutter。这类框架优点非常明显:性能(的上限)高;各平台渲染结果一致。缺点也非常明显:需要完全重新学习 DSL(QML/Dart),以及难以适配中国特色的端:小程序。 这类框架是最原始也是最纯正的的多端开发框架,由于底层到上层每个环节都掌握在自己手里,也能最大可能地去保证开发和跨端体验一致。但它们的框架研发成本巨大,渲染引擎、布局引擎、DSL、上层框架每个部分都需要大量人力开发维护。 2. Web 技术型 这类框架把 Web 技术(JavaScript,CSS)带到移动开发中,自研布局引擎处理 CSS,使用 JavaScript 写业务逻辑,使用流行的前端框架作为 DSL,各端分别使用各自的原生组件渲染。代表框架是 React Native 和 Weex,这样做的优点有: 开发迅速 复用前端生态 易于学习上手,不管前端后端移动端,多多少少都会一点 JS、CSS 缺点有: 交互复杂时难以写出高性能的代码,这类框架的设计就必然导致 [代码]JS[代码] 和 [代码]Native[代码] 之间需要通信,类似于手势操作这样频繁地触发通信就很可能使得 UI 无法在 16ms 内及时绘制。React Native 有一些声明式的组件可以避免这个问题,但声明式的写法很难满足复杂交互的需求。 由于没有渲染引擎,使用各端的原生组件渲染,相同代码渲染的一致性没有第一种高。 3. JavaScript 编译型 这类框架就是我们这篇文章的主角们:[代码]Taro[代码]、[代码]WePY[代码] 、[代码]uni-app[代码] 、 [代码]mpvue[代码] 、 [代码]chameleon[代码],它们的原理也都大同小异:先以 JavaScript 作为基础选定一个 DSL 框架,以这个 DSL 框架为标准在各端分别编译为不同的代码,各端分别有一个运行时框架或兼容组件库保证代码正确运行。 这类框架最大优点和创造的最大原因就是小程序,因为第一第二种框架其实除了可以跨系统平台之外,也都能编译运行在浏览器中。(Qt 有 Qt for WebAssembly, Flutter 有 Hummingbird,React Native 有 [代码]react-native-web[代码], Weex 原生支持) 另外一个优点是在移动端一般会编译到 React Native/Weex,所以它们也都拥有 Web 技术型框架的优点。这看起来很美好,但实际上 React Native/Weex 的缺点编译型框架也无法避免。除此之外,编译型框架的抽象也不是免费的:当 bug 出现时,问题的根源可能出在运行时、编译时、组件库以及三者依赖的库等等各个方面。在 Taro 开源的过程中,我们就遇到过 Babel 的 bug,React Native 的 bug,JavaScript 引擎的 bug,当然也少不了 Taro 本身的 bug。相信其它原理相同的框架也无法避免这一问题。 但这并不意味着这类为了小程序而设计的多端框架就都不堪大用。首先现在各巨头超级 App 的小程序百花齐放,框架会为了抹平小程序做了许多工作,这些工作在大部分情况下是不需要开发者关心的。其次是许多业务类型并不需要复杂的逻辑和交互,没那么容易触发到框架底层依赖的 bug。 那么当你的业务适合选择编译型框架时,在笔者看来首先要考虑的就是选择 DSL 的起点。因为有多端需求业务通常都希望能快速开发,一个能够快速适应团队开发节奏的 DSL 就至关重要。不管是 React 还是 Vue(或者类 Vue)都有它们的优缺点,大家可以根据团队技术栈和偏好自行选择。 如果不管什么 DSL 都能接受,那就可以进入下一个环节: 生态 以下内容均以各框架现在(2019 年 3 月 11日)已发布稳定版为标准进行讨论。 开发工具 就开发工具而言 [代码]uni-app[代码] 应该是一骑绝尘,它的文档内容最为翔实丰富,还自带了 IDE 图形化开发工具,鼠标点点点就能编译测试发布。 其它的框架都是使用 CLI 命令行工具,但值得注意的是 [代码]chameleon[代码] 有独立的语法检查工具,[代码]Taro[代码] 则单独写了 ESLint 规则和规则集。 在语法支持方面,[代码]mpvue[代码]、[代码]uni-app[代码]、[代码]Taro[代码] 、[代码]WePY[代码] 均支持 TypeScript,四者也都能通过 [代码]typing[代码] 实现编辑器自动补全。除了 API 补全之外,得益于 TypeScript 对于 JSX 的良好支持,Taro 也能对组件进行自动补全。 CSS 方面,所有框架均支持 [代码]SASS[代码]、[代码]LESS[代码]、[代码]Stylus[代码],Taro 则多一个 [代码]CSS Modules[代码] 的支持。 所以这一轮比拼的结果应该是: [代码]uni-app[代码] > [代码]Taro[代码] > [代码]chameleon[代码] > [代码]WePY[代码]、[代码]mpvue[代码] [图片] 多端支持度 只从支持端的数量来看,[代码]Taro[代码] 和 [代码]uni-app[代码] 以六端略微领先(移动端、H5、微信小程序、百度小程序、支付宝小程序、头条小程序),[代码]chameleon[代码] 少了头条小程序紧随其后。 但值得一提的是 [代码]chameleon[代码] 有一套自研多态协议,编写多端代码的体验会好许多,可以说是一个能戳到多端开发痛点的功能。[代码]uni-app[代码] 则有一套独立的条件编译语法,这套语法能同时作用于 [代码]js[代码]、样式和模板文件。[代码]Taro[代码] 可以在业务逻辑中根据环境变量使用条件编译,也可以直接使用条件编译文件(类似 React Native 的方式)。 在移动端方面,[代码]uni-app[代码] 基于 [代码]weex[代码] 定制了一套 [代码]nvue[代码] 方案 弥补 [代码]weex[代码] API 的不足;[代码]Taro[代码] 则是暂时基于 [代码]expo[代码] 达到同样的效果;[代码]chameleon[代码] 在移动端则有一套 SDK 配合多端协议与原生语言通信。 H5 方面,[代码]chameleon[代码] 同样是由多态协议实现支持,[代码]uni-app[代码] 和 [代码]Taro[代码] 则是都在 H5 实现了一套兼容的组件库和 API。 [代码]mpvue[代码] 和 [代码]WePY[代码] 都提供了转换各端小程序的功能,但都没有 h5 和移动端的支持。 所以最后一轮对比的结果是: [代码]chameleon[代码] > [代码]Taro[代码]、[代码]uni-app[代码] > [代码]mpvue[代码]、[代码]WePY[代码] [图片] 组件库/工具库/demo 作为开源时间最长的框架,[代码]WePY[代码] 不管从 Demo,组件库数量 ,工具库来看都占有一定优势。 [代码]uni-app[代码] 则有自己的插件市场和 UI 库,如果算上收费的框架和插件比起 [代码]WePy[代码] 也是完全不遑多让的。 [代码]Taro[代码] 也有官方维护的跨端 UI 库 [代码]taro-ui[代码] ,另外在状态管理工具上也有非常丰富的选择(Redux、MobX、dva),但 demo 的数量不如前两个。但 [代码]Taro[代码] 有一个转换微信小程序代码为 Taro 代码的工具,可以弥补这一问题。 而 [代码]mpvue[代码] 没有官方维护的 UI 库,[代码]chameleon[代码] 第三方的 demo 和工具库也还基本没有。 所以这轮的排序是: [代码]WePY[代码] > [代码]uni-app[代码] 、[代码]taro[代码] > [代码]mpvue[代码] > [代码]chameleon[代码] [图片] 接入成本 接入成本有两个方面: 第一是框架接入原有微信小程序生态。由于目前微信小程序已呈一家独大之势,开源的组件和库(例如 [代码]wxparse[代码]、[代码]echart[代码]、[代码]zan-ui[代码] 等)多是基于原生微信小程序框架语法写成的。目前看来 [代码]uni-app[代码] 、[代码]Taro[代码]、[代码]mpvue[代码] 均有文档或 demo 在框架中直接使用原生小程序组件/库,[代码]WePY[代码] 由于运行机制的问题,很多情况需要小改一下目标库的源码,[代码]chameleon[代码] 则是提供了一个按步骤大改目标库源码的迁移方式。 第二是原有微信小程序项目部分接入框架重构。在这个方面 Taro 在京东购物小程序上进行了大胆的实践,具体可以查看文章《Taro 在京东购物小程序上的实践》。其它框架则没有提到相关内容。 而对于两种接入方式 Taro 都提供了 [代码]taro convert[代码] 功能,既可以将原有微信小程序项目转换为 Taro 多端代码,也可以将微信小程序生态的组件转换为 Taro 组件。 所以这轮的排序是: [代码]Taro[代码] > [代码]mpvue[代码] 、 [代码]uni-app[代码] > [代码]WePY[代码] > [代码]chameleon[代码] 流行度 从 GitHub 的 star 来看,[代码]mpvue[代码] 、[代码]Taro[代码]、[代码]WePY[代码] 的差距非常小。从 NPM 和 CNPM 的 CLI 工具下载量来看,是 Taro(3k/week)> mpvue (2k/w) > WePY (1k/w)。但发布时间也刚好反过来。笔者估计三家的流行程度和案例都差不太多。 [代码]uni-app[代码] 则号称有上万案例,但不像其它框架一样有一些大厂应用案例。另外从开发者的数量来看也是 [代码]uni-app[代码] 领先,它拥有 20+ 个 QQ 交流群(最大人数 2000)。 所以从流行程度来看应该是: [代码]uni-app[代码] > [代码]Taro[代码]、[代码]WePY[代码]、[代码]mpvue[代码] > [代码]chameleon[代码] [图片] 开源建设 一个开源作品能走多远是由框架维护团队和第三方开发者共同决定的。虽然开源建设不能具体地量化,但依然是衡量一个框架/库生命力的非常重要的标准。 从第三方贡献者数量来看,[代码]Taro[代码] 在这一方面领先,并且 [代码]Taro[代码] 的一些核心包/功能(MobX、CSS Modules、alias)也是由第三方开发者贡献的。除此之外,腾讯开源的 [代码]omi[代码] 框架小程序部分也是基于 Taro 完成的。 [代码]WePY[代码] 在腾讯开源计划的加持下在这一方面也有不错的表现;[代码]mpvue[代码] 由于停滞开发了很久就比较落后了;可能是产品策略的原因,[代码]uni-app[代码] 在开源建设上并不热心,甚至有些部分代码都没有开源;[代码]chameleon[代码] 刚刚开源不久,但它的代码和测试用例都非常规范,以后或许会有不错的表现。 那么这一轮的对比结果是: [代码]Taro[代码] > [代码]WePY[代码] > [代码]mpvue[代码] > [代码]chameleon[代码] > [代码]uni-app[代码] 最后补一个总的生态对比图表: [图片] 未来 从各框架已经公布的规划来看: [代码]WePY[代码] 已经发布了 [代码]v2.0.alpha[代码] 版本,虽然没有公开的文档可以查阅到 [代码]2.0[代码] 版本有什么新功能/特性,但据其作者介绍,[代码]WePY 2.0[代码] 会放大招,是一个「对得起开发者」的版本。笔者也非常期待 2.0 正式发布后 [代码]WePY[代码] 的表现。 [代码]mpvue[代码] 已经发布了 [代码]2.0[代码] 的版本,主要是更新了其它端小程序的支持。但从代码提交, issue 的回复/解决率来看,[代码]mpvue[代码] 要想在未来有作为首先要打消社区对于 [代码]mpvue[代码] 不管不顾不更新的质疑。 [代码]uni-app[代码] 已经在生态上建设得很好了,应该会在此基础之上继续稳步发展。如果 [代码]uni-app[代码] 能加强开源开放,再加强与大厂的合作,相信未来还能更上一层楼。 [代码]chameleon[代码] 的规划比较宏大,虽然是最后发的框架,但已经在规划或正在实现的功能有: 快应用和端拓展协议 通用组件库和垂直类组件库 面向研发的图形化开发工具 面向非研发的图形化页面搭建工具 如果 [代码]chameleon[代码] 把这些功能都做出来的话,再继续完善生态,争取更多第三方开发者,那么在未来 [代码]chameleon[代码] 将大有可为。 [代码]Taro[代码] 的未来也一样值得憧憬。Taro 即将要发布的 [代码]1.3[代码] 版本就会支持以下功能: 快应用支持 Taro Doctor,自动化检查项目配置和代码合法性 更多的 JSX 语法支持,1.3 之后限制生产力的语法只有 [代码]只能用 map 创造循环组件[代码] 一条 H5 打包体积大幅精简 同时 [代码]Taro[代码] 也正在对移动端进行大规模重构;开发图形化开发工具;开发组件/物料平台以及图形化页面搭建工具。 结语 那说了那么多,到底用哪个呢? 如果不介意尝鲜和学习 DSL 的话,完全可以尝试 [代码]WePY[代码] 2.0 和 [代码]chameleon[代码] ,一个是酝酿了很久的 2.0 全新升级,一个有专门针对多端开发的多态协议。 [代码]uni-app[代码] 和 [代码]Taro[代码] 相比起来就更像是「水桶型」框架,从工具、UI 库,开发体验、多端支持等各方面来看都没有明显的短板。而 [代码]mpvue[代码] 由于开发一度停滞,现在看来各个方面都不如在小程序端基于它的 [代码]uni-app[代码] 。 当然,Talk is cheap。如果对这个话题有更多兴趣的同学可以去 GitHub 另行研究,有空看代码,没空看提交: chameleon: https://github.com/didi/chameleon mpvue: https://github.com/Meituan-Dianping/mpvue Taro: https://github.com/NervJS/taro uni-app: https://github.com/dcloudio/uni-app WePY: https://github.com/Tencent/wepy (按字母顺序排序)
2019-03-19 - 【canvas.drawImage】的严重BUG。
在安卓上面通过canvas.drawImage绘制图片的时候,如果图片的尺寸宽和高超过2804*2804,(形如2805*2805),canvas就会绘制不出来,在IOS里面则能够正常显示,希望官方给个说法
2017-09-20 - 安卓上,wxs事件touchmove向下滑动后,之后的touchmove受限
- 当前 Bug 的表现(可附上截图) 在安卓机器上,touchmove在数次deltaY为正数(下滑)的时候,touchmove的触发频率会被限制,导致动画卡顿 通过在touchmove收集时间戳并输出到控制台,发现touchmove的触发频率在数次后被限制在约200ms左右 - 预期表现 不限制touchmove的触发频率 - 复现路径 如果滑块一开始往上拖动,则动画流畅 如果滑块一开始往下拖动,则往后的拖动十分卡顿 - 提供一个最简复现 Demo https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html 官方例子wxs事件的movable案例
2019-02-14 - 小程序Android版本touchmove 事件侦听
- 当前 Bug 的表现(可附上截图) 在安卓端绑小程序定一个view 的touchmove 事件时,纵向触摸滑动每秒只有5次事件回调 但是用网页端复现的时候表现正常 - 预期表现 - 复现路径 - 提供一个最简复现 Demo 小程序代码: - index.wxml <view class="layer" bindtouchmove="layerMove"> <text>{{touchMovesPerSecond}}</text> </view> - index.js const app = getApp() Page({ data: { touchMovesPerSecond: 0 }, touchMovesArray: [], layerMove: function(event) { this.touchMovesArray.push(new Date()) const now = new Date() this.touchMovesArray = this.touchMovesArray.filter(time => time > now - 1000) this.setData({ touchMovesPerSecond: this.touchMovesArray.length }) } }) --------------------------------------------分界线------------------------------------------ Web 端代码: <!DOCTYPE html> <html> <head> <title></title> <style type="text/css"> body, html { background-color: #4e4d4d; margin: 0px; padding: 0px; height: 100%;} #container { background-color: red; height: 100%; } </style> </head> <body> <div id="container"> <p id="count"></p> </div> <script> const touchMovesArray = [] const handleMove = function() { touchMovesArray: [], touchMovesArray.push(new Date()) const now = new Date() touchMovesArray = touchMovesArray.filter(time => time > now - 1000) document.querySelector('#count').innerHTML = touchMovesArray.length } var el = document.querySelector('#container') el.addEventListener("touchmove", handleMove, false); </script> </body> </html>
2018-10-03 - 安卓,canvas上导出的图片保存到相册只有图片一部分
现象: canvas上画了一张2k以上的图,通过canvasToTempFilePath导出文件路径,然后通过saveImageToPhotosAlbum保存到相册。相册里的图是只有原图的一部分(非必现)。 分析原因: 怀疑是canvasToTempFilePath成功回调时,图片文件并没有完全生成。因此在用saveImageToPhotosAlbum保存到相册时,就保存了一个没有完全写入的图片文件。 理由; 1、用性能较好的手机,比如:小米8做实验。发现有时能保存成功,有时保存为黑的。但是如果canvasToTempFilePath成功回调以后,延迟几秒钟再调用saveImageToPhotosAlbum就能百分之百成功。 2、选择性能较差的手机,发现百分百失败,保存为黑的。但是如果canvasToTempFilePath成功回调以后,延迟十几秒再调用saveImageToPhotosAlbum也能百分百成功。 目标: 希望在canvasToTempFilePath成功回调时能保证临时图片已经完全生成。
2019-03-11 - 获取用户信息方案介绍
背景 小程序一个比较重要的能力就是获取用户信息,也就是使用 [代码]wx.getUserInfo[代码] 接口。我们发现几乎所有的小程序都会调用这个接口。虽然我们在设计文档上有提出最好的设计是在真正要用户信息的情况下才去获取用户信息,不过很多开发者并没有按照我们的期望去做,导致用户在使用的时候有很多困扰。 归结起来有几点: 开发者在首页直接调用 [代码]wx.getUserInfo[代码] 进行授权,弹框有会使得一部分用户放弃小程序的使用。 开发者没有处理用户拒绝弹框的情况,有部分小程序强制要求用户授权头像昵称等信息才能继续使用小程序。 用户没有很好的方式重新授权,虽然在前几个版本我们增加了[代码]设置[代码]页面可以让用户选择重新授权,但是操作还是不够便捷。 开发者希望进到首页就获取到用户的[代码]unionId[代码],以便和之前已经关注了公众号的用户画像关联起来。 开发者默认将 [代码]wx.login[代码] 和 [代码]wx.getUserInfo[代码] 绑定使用,这个是由于我们一开始的设计缺陷和实例代码导致: [代码]getUserInfo[代码]必须通过[代码]wx.login[代码] 在后台生成[代码]session_key[代码] 后才能调用。 为了解决以上几点,我们更新了三个能力: 使用组件来获取用户信息,用户拒绝授权后也可以重新弹窗再次授权 若用户满足一定条件(下文有详细介绍),则可以用[代码]wx.login[代码] 获取到的code直接换到[代码]unionId[代码] [代码]wx.getUserInfo[代码] 不依赖 [代码]wx.login[代码] 就能调用得到数据。 获取用户信息组件介绍[代码][代码] 组件变化: [代码]open-type[代码] 属性增加 [代码]getUserInfo[代码] :用户点击时候会触发 [代码]bindgetuserinfo[代码] 事件。 新增事件 [代码]bindgetuserinfo[代码] :当 [代码]open-type[代码] 为 [代码]getUserInfo[代码] 时,用户点击会触发。可以从事件返回参数的[代码]detail[代码]字段中获取到和[代码]wx.getUserInfo[代码] 返回参数相同的数据。 示例: [代码]<[代码][代码]button[代码] [代码]open-type[代码][代码]=[代码][代码]"getUserInfo"[代码] [代码]bindgetuserinfo[代码][代码]=[代码][代码]"userInfoHandler"[代码][代码]> Click me </[代码][代码]button[代码][代码]>[代码]和 [代码]wx.getUserInfo[代码] 不同之处在于: API [代码]wx.getUserInfo[代码] 只会弹一次框,用户拒绝授权之后,再次调用将不会弹框 组件 [代码][代码][代码][代码] 由于是用户主动触发,不受弹框次数限制,只要用户没有授权,都会再次弹框 直接获取unionId考虑很多场景下,业务方申请userinfo授权主要为了获取unionid。我们鼓励开发者在不骚扰用户的情况下合理获得unionid,而仅在必要时才向用户弹窗申请使用昵称头像。为此,凡使用“获取用户信息组件”获取用户昵称头像的小程序,在满足以下全部条件时,将可以静默获得unionid。 在微信开放平台下存在同主体的App、公众号、小程序。 用户关注了某个相同主体公众号,或曾经在某个相同主体App、公众号上进行过微信登录授权。 getUserInfo 和 login很多开发者会把login和getUserInfo捆绑调用当成登录使用,其实login已经可以完成登录,可以建立账号体系了,getUserInfo只是获取额外的用户信息。 在login获取到code,然后发送到开发者后端,开发者后端再通过接口去微信后端换取到openid和sessionKey(并且现在会将unionid也一并返回)之后,然后把3rd_session返回给前端,就已经完成登录行为。而login行为是静默,不必授权的,不会对用户造成骚扰。 getUserInfo只是为了提供更优质的服务而存在,比如展示头像昵称,判断性别,通过unionId和其他公众号上已有的用户画像结合起来提供历史数据。所以不必在刚刚进入小程序的时候就强制要求授权。 推荐使用方法调用[代码]wx.login[代码] 获取[代码]code[代码],然后从微信后端换取到[代码]sessionKey[代码],用于解密[代码]getUserInfo[代码]返回的敏感数据。 使用[代码]wx.getSetting[代码] 获取用户的授权情况 如果用户已经授权,直接调用 API [代码]wx.getUserInfo[代码] 获取用户最新的信息 用户未授权,在界面中显示一个按钮提示用户登入,当用户点击并授权后就获取到用户的最新信息。 获取到用户数据后可以进行展示或者发送给自己的后端。 文档中的quickStart已经更新 特别注意为了给用户提供更好的小程序环境,我们约定在一段时间后(具体时间会做通知),若还出现以下情况(包括但不限于),将无法通过审核 初次打开小程序就弹框授权用户信息 未处理用户拒绝授权的情况 强制要求用户授权 已经上线的小程序不会受到影响。 FAQ Q: 除了 UserInfo 呢,比如说位置信息 --- ’风の诺言 . A: 其他授权信息不像用户信息那么高频繁,也基本是在使用时候才申请授权,所以没有同 UserInfo 一起给出。我们会先看看 UserInfo 的使用情况再结合具体场景我们会给出相应的方案 Q: 后台要维护用户信息 --- Azleal 我们的小程序业务是功能都需要授权才能使用的(也就是必须拿到unionid获取用户信息) --- elemeNT 我在小程序与服务号的数据需要互通,通过unionId来确定用户的唯一性,如果在用户进入小程序后不强制他授权,单凭一个openid来存储他的用户数据,在用户下次从服务号进入时。不就会产生重复数据吗?就没做到数据互通了 --- ﺭ并向你吐了趴口水ﺭ五年. 另外看到官方提到 要强制推行,我想说我们目前所有用户是通过unionid注册的。那么这些用户就不得不使用 openid重新登录 、注册一遍。更重要的是,之前他们的相关数据都会对应不上(因为你们也不允许强制用户登录授权) --- 羊毛 现在这种方案,不能满足我们的需求,我们的小程序,必须一进入就要获取他的信息,然后加载他的数据; --- 韩文 A: 调用`wx.login`已经可以获取到用户的登录态,已经可以做用户账号的管理。 UserInfo 中带的 UnionId 是额外的信息,没有它完全可以完成登录 对于需要和开发平台绑定的业务进行数据互通的情况,一个新用户进来没有互通数据的情况下也是可以体验到所有业务,那么对于没有授权unionId的用户,可以将其当成是新用户,当真正授权unionId之后再做绑定完全是可以的 Q: 我需要确保用户的唯一性,这样就必须取unionID,否则用户删除了小程序,或者换了设备, 下次再进来这个小程序,该用什么来区分是上次来过的用户呢?? --- WEI+ A: 如果你本身没有其他公众号、App、小程序,那么也就没必要拿到unionid,因为unionid是打通你在开放平台下所有应用的标识 如果只有一个小程序,用 openid 足以, openid 是一个用户对于一个小程序的标识,永远不变 Q: wx.getUserInfo 是网络请求,如果使用了 open-type = "getUserInfo",是否每次点击都会调接口? --- SouthernBox A: 是的,open-type="getUserInfo" 的作用以及内部实现基本和 wx.getUserInfo 一样 区别是一个开发者主动(拒绝一次不再弹窗),一个是用户主动(拒绝任意次都可以重新弹窗) Q: 比如有一个创建按钮,用户点击一次授权了,我已经获取到用户信息,再次点击就没必要再调用 getUserInfo 去网络请求了。 --- SouthernBox A: 可以参考文中 quickStart 的做法,如果已经授权了,那就可以把按钮隐藏,之后的授权直接用API wx.getUserInfo 调用(因为已经授权,所以也不会弹窗),用户也不会再点了 Q: 小程序是不是必须要用微信自带的授权才可以登录 ,能否不使用授权方式登录,用自己系统的api接口数据实现?这个会不会涉及到审核不过的问题??麻烦解答一下 谢谢了。 --- WEI+ A: 自己做登录不会涉及到审核问题。 不过不建议在没有原有账号体系的情况下让用户在小程序内注册,过重的行为会损失用户。 Q: 在小程序中有一个"我的"页面,这是属于会员页,如果用户要进入这个页面就必须授权。交互方式就是在用户未授权情况下整个页面只显示一个授权获取用户信息的button 按钮,这个需要用户自己去触发,算不算强制授权? --- ﺭ并向你吐了趴口水ﺭ五年. A: 强制授权是说如果用户如果不授权基本信息,连最基础的浏览功能都不提供(当然这个也是要分具体的业务场景,不会限制得太死板) 可以有更好的交互,参考下主流App,在未登录的时候点击【我的】页面,也不会直接要求登录,而是展示了一定的页面结构,同时给一个登录按钮(例如【携程】【京东】等),之后再在这个页面做操作的话可以弹一个登录页面或按钮提示用户登录是完全可以的。 上述所说的登录只是用户感知上的登录,从业务逻辑上用户其实在 wx.login 的时候已经完成登录了。 Q: 看了很多评论,有些人还是不知道为什么官方要这样做,我作为一个商家角度来说下。 --- Mr.J 1. 比如我们要做一些户外推广的二唯码,用户只看到了你的图片宣传单,扫描二唯码一打开就提示“需要获取你的个人信息,您是否允许”,你不要当自己是开发者当自己是一个正常人,看到这个提示我相信很多人的第一反应就是拒绝。如果第一步已经把你拒之门外,谈何营销? 2. 没有小程序之前,我们在公众号有很多用户,都绑定了unionid,有小程序之后我们考虑怎么让用户接受小程序,可以静默登录我觉得非常好,从公众号过来的用户可以直接就登录了,没有任何提示,完美的对接,这是一个很好的体验。 A: 说得很好,我们的这些改造不仅是为了开发者,同时也是为了这个生态下的用户考虑。希望开发者们也能站在用户的角度去思考怎么做一个产品。 Q: 我不明白为什么login 给多个unionid 为什么不行? unionid也不能算是个人信息吧,给多个unionid可以更方便开发者,而且很多情况下就不用调用getUserInfo了 --- candyTong 我们提个建议,能否直接开放unionid呢?这样也许会有许多小程序不需要再弹窗了。既一定程度保障了用户体验,也照顾到了我们开发者的体验。 --- 羊毛 A: 如果直接开放了unionid,就会出现这种情况:当你作为一个用户进入一个小程序,这个小程序并没要求你授权就直接把你的头像昵称显示出来(它之前把unionId对应的头像昵称都存了下来),但是这个小程序主体(open平台主体和公众平台主体并不相同)相关的任何一个应用你从来没用过,你会不会觉得很奇怪并且很不舒服,觉得自己在微信内的用户信息没有丝毫的保障? Q: 那有推荐的比较好的例子么?对于必须使用用户头像、昵称这些信息的小程序而言 --- 亚里士朱德 A: 首先,没有什么逻辑是一定要使用用户的头像、昵称才能work的。对于这个case,完全可以先用默认头像、匿名昵称先做替代,用户点击默认头像后就可以弹出授权信息,非常的水到渠成。 Q: 之前看了这个帖子一直在思考,如果是一进去需要回去用户的地理位置信息显示到地图上的呢?这样算不算是一进去就弹窗授权获取用户信息? --- 吴俊绩🤔 A: 地图的情况和获取用户信息不同,我们目前还没对地图的授权请求有所调整。当前不受上述策略的影响 Q: 对于开发者而言,小程序与公众号是同级的,只是不同的入口 但是这样的设计,公众号与小程序成了主从关系咯 --- log琥珀① A: 并无什么主从关系,只是多一个渠道让开发者可以更方便的获取到已经是该主体下用户的unionId
2017-08-07