原生滚动条滚轮不生效_如何在 SPA 应用中实现比拟原生 App 的转场效果

d13dc1e5fcabf514dbbc081932c038bf.png

总篇50篇 2019年第24篇

在轻应用群雄混战的当下,小程序已经成为超级APP们角逐的焦点,汽车之家在这样的背景下推出了自己“小程序”,之家小程序采用自由组装组件的方式,为用户提供全面的「内容+数据+服务」的封装,满足个性化需求。言归正传本文将聊一聊如何在 SPA 应用中实现比拟原生 App 的转场效果。

目录

  1. 实现转场动效。

  2. 页面缓存策略。

  3. 滚动条位置记录。

  4. 转场动画性能优化。

  5. 页面间通信。

效果演示

实现转场动效

在 SPA 单页应用中,页面的 “跳转” 是通过 router api 控制的,所以我们只需要监听 router 的相应行为就可以确定转场动画的形式(转入或是转出动画)。

这里我们引入 vue-navigation 组件,这个组件提供了对路由事件监听功能:

this.$navigation.on('forward', (to, from) => {})

this.$navigation.once('back', (to, from) => {})

this.$navigation.on('replace', (to, from) => {})

this.$navigation.off('refresh', (to, from) => {})

this.$navigation.on('reset', () => {})

当监听到路由的变化时,设置 transition 组件相应的 className 即可,实现转场动画。

这里面有一些细节需要说明:

  1. 转场动画 slide-in、 slide-out 是 transition 动画,因此 router 的直接子组件需要绝对定位

  2. 转入和转出需要根据具体效果设置 zIndex 层级,确保动画的组件在最上方。

  3. 需要明确哪些行为是需要转场功能的,比如我们定义 router 的 push 和 back 对应 slide-in 和 slide-out 动画, refresh、 reset、 relace 不做动画效果,但是可以接收一个 transition 参数用于确定使用哪种转场动画运动。

下面代码是 transition-router-view 组件,实现上面描述的细节:

实例代码:

:name="transitionName">

:style="{zIndex: styleZindexNum}">

export default {

name: 'TransitionRoterView',

data() {

return {

transitionName: '',

styleZindexNum: 100

};

},

created() {

this.$navigation.on('forward', (to, from) => {

if (!from.route.query.VNK) {

return;

}

++this.styleZindexNum;

this.transitionName = 'slide-in';

});

this.$navigation.on('back', (to, from) => {

--this.styleZindexNum;

this.transitionName = 'slide-out';

});

this.$navigation.on('replace', (to) => {

try {

if (to.route.query.transition === 'forward') {

++this.styleZindexNum;

this.transitionName = 'slide-in';

return;

}

if (to.route.query.transition === 'back') {

--this.styleZindexNum;

this.transitionName = 'slide-out';

return;

}

} catch (error) {

console.log(error);

}

this.transitionName = '';

});

this.$navigation.on('refresh', () => {

this.transitionName = '';

});

this.$navigation.on('reset', () => {

this.transitionName = '';

});

}

};

lang="scss" scoped>

.slide-in-enter {

transform: translate(100%, 0);

}

.slide-in-enter-active,

.slide-in-leave-active {

transition: transform .3s;

}


.slide-out-leave-active {

transform: translate(100%, 0);

}

.slide-out-enter-active,

.slide-out-leave-active {

transition: transform .3s;

}

上面提到的路由的直接子组件需绝对定位,这个我们会在下面的 page-view 中一同介绍。

页面缓存策略

说到组件缓存首先想到的是 keep-alive 组件,当在页面来回跳转时,需要保持这些页面的状态和数据,以避免重复渲染导致的性能问题。但在类似 webview 的场景下直接使用 keep-alive 并不容易控制,无法控制哪些组件在何时应该被销毁。在这里我们使用 vue-navigation 组件根据路由跳转规则实现页面缓存管理。

对比 keep-alive

  1. 当页面组件在路由历史记录中被移除时页面组件会被及时销毁,。

  2. 执行 router push/replace 新开一个渲染过的页面时,这个页面会被重新渲染并执行完整的生命周期而不是从缓存中恢复。

最终效果就是:

  • A前进到B,再前进到C;

  • C返回到B时,B会从缓存中恢复;

  • B再次前进到C,C会重新生成,不会从缓存中恢复;

  • C前进到A,A会生成,现在路由中包含2个A实例。

navigation 组件源码

import Routes from '../routes'

import { getKey, matches } from '../utils'

export default (keyName) => {

return {

name: 'navigation',

abstract: true,

props: {},

data: () => ({

routes: Routes

}),

computed: {},

watch: {

routes(val) {

for (const key in this.cache) {

if (!matches(val, key)) {

const vnode = this.cache[key]

vnode && vnode.componentInstance.$destroy()

delete this.cache[key]

}

}

},

},

created() {

this.cache = {}

},

destroyed() {

for (const key in this.cache) {

const vnode = this.cache[key]

vnode && vnode.componentInstance.$destroy()

}

},

render() {

const vnode = this.$slots.default ? this.$slots.default[0] : null

if (vnode) {

vnode.key = vnode.key || (vnode.isComment

? 'comment'

: vnode.tag)

// prevent vue-router reuse component

const key = getKey(this.$route, keyName)

if (vnode.key.indexOf(key) === -1) {

vnode.key = `__navigation-${key}-${vnode.key}`

}

if (this.cache[key]) {

if (vnode.key === this.cache[key].key) {

// restore vnode from cache

vnode.componentInstance = this.cache[key].componentInstance

} else {

// replace vnode to cache

this.cache[key].componentInstance.$destroy()

this.cache[key] = vnode

}

} else {

// cache new vnode

this.cache[key] = vnode

}

vnode.data.keepAlive = true

}

return vnode

}

}

}

滚动条位置记录

同上面描述的例子类似,A前进到B,B滑动滚动条到页面中部,再前进到C;C返回到B时,B会保持跳转前的滚动条位置;因为转场动画需要 page-view 组件是绝对定位的元素,这导致无法使用 vue-rotuer 自带的滚动位置记录功能,这里我们需要在 router.beforeEach 中获取滚动条的位置并记录下来,在 page-viewactivated 周期内恢复滚动条的位置。

示例代码

// router.js

router.beforeEach((to, from, next) => {

const wrapScroll = document.querySelector('#contentScrollWrap');

if (wrapScroll) {

window._VUE_PAGE_VIEW_SCROLL_[from.query.VNK] = wrapScroll.scrollTop; // 根据 VNK 记录滚动条位置

}

if (!window._VUE_ROUTER_INDEX_) {

window._VUE_ROUTER_INDEX_ = to.query.VNK || from.query.VNK; // 标记入口页面

}

next();

});

// page-view

activated() {

const scrollTop = window._VUE_PAGE_VIEW_SCROLL_[this.$route.query.VNK];

if (scrollTop) {

this.$refs.contentScrollWrap.scrollTop = scrollTop;

}

}

转场动画性能优化

在转场阶段转场动画时长 300ms,如果页面接口请求在 300ms 内完成并渲染 DOM 到页面,将会影响转场性能。解决方法是:接口在 300ms 内返回并准备渲染DOM时,延迟等待动画完成后继续渲染,在延迟渲染的时间内用 loading 组件填充页面。

示例代码:

class="page-view">

class="page-view__content" ref="contentScrollWrap" id="contentScrollWrap">

v-if="allowRender">

:fullscreen="fullscreen" v-if="loadingState"/>

import Loading from '@/components/loading';

export default {

name: 'PageView',

props: {

delayTime: {

type: Number,

default: 0

},

loading: {

type: Boolean

},

fullscreen: {

type: Boolean,

default: false

}

},

data() {

return {

transitionEnd: false

};

},

computed: {

allowRender() {

return this.transitionEnd;

},

loadingState() {

return this.loading || !this.transitionEnd;

}

},

components: {

Loading

},

mounted() {

this.$emit('complete', this.$refs.contentScrollWrap);

},

activated() {

const scrollTop = window._VUE_PAGE_VIEW_SCROLL_[this.$route.query.VNK];

if (scrollTop) {

this.$refs.contentScrollWrap.scrollTop = scrollTop;

}

},

created() {

setTimeout(() => {

this.transitionEnd = true;

}, this.delayTime);

}

};

页面间通信

页面间通信可分为,路由前进 ( push, replace) 和 路由后退 ( back) 通信。两种行为都可以通过 store 实现通信,当存在一对多的通信关系时复杂度高,无法解耦。

解决方法:路由前进 ( push, replace) 的场景中使用 query 传递参数。路由后退 ( back) 的场景中使用 EventBus 模式通知来源页面更新状态(这个模式需要 keep-alive 模式支持)。

全局挂载 $eventBus

// event-bus.js

import Vue from 'vue';

export const EventBus = new Vue();

let _Vue;

export default {

install(Vue) {

if (this.installed && _Vue === Vue) return;

this.installed = true;

Vue.prototype.$eventBus = new Vue();

_Vue = Vue;

}

};

// main.js

import { EventBus } from '@/global';

Vue.use(EventBus);

假设有以下场景,A组件跳转到B组件,B组件选择参数后,返回到A组件,A组件获取B组件返回值。

// A组件

created() {

this.$eventBus.$on(`updateSpec-${this.VNK}`, (item) => { // this.VNK 唯一id

this.specid = item.id;

this.getSpecInfo();

});

},

beforeDestroy() {

this.$eventBus.$off(`updateSpec-${this.VNK}`); // 注意销毁监听器

},

handleClick() {

this.$router.push({

name: 'SelectSpec',

query: {

seriesid: this.seriesid,

onSell: 1,

emitName: `updateSpec-${this.VNK}`

}

});

},

// B组件

handleItemClick(item) {

if (this.$route.query && this.$route.query.emitName) {

this.$eventBus.$emit(this.$route.query.emitName, item);

}

this.$router.back();

}

总结

回顾本文,我们通过 动画实现、页面状态、缓存、性能优化、以及页面间通信等几个方面介绍了模拟了 webview 转场功能的实现,应用 vue-navigationvue-routerkeep-aliveeventBus 等组件/库实现了这些功能。未来我们将会持续打磨和优化代码,并更多的输出一些我们在项目开发过程中的经验。

77b39c3f69a901d12dd9bec01f166ee6.png

d24dfa63f2276928058981a320716011.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值