总篇50篇 2019年第24篇
在轻应用群雄混战的当下,小程序已经成为超级APP们角逐的焦点,汽车之家在这样的背景下推出了自己“小程序”,之家小程序采用自由组装组件的方式,为用户提供全面的「内容+数据+服务」的封装,满足个性化需求。言归正传本文将聊一聊如何在 SPA 应用中实现比拟原生 App 的转场效果。
目录
实现转场动效。
页面缓存策略。
滚动条位置记录。
转场动画性能优化。
页面间通信。
效果演示
实现转场动效
在 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
即可,实现转场动画。
这里面有一些细节需要说明:
转场动画
slide-in
、slide-out
是 transition 动画,因此 router 的直接子组件需要绝对定位。转入和转出需要根据具体效果设置
zIndex
层级,确保动画的组件在最上方。需要明确哪些行为是需要转场功能的,比如我们定义 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
:
当页面组件在路由历史记录中被移除时页面组件会被及时销毁,。
执行 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-view
的 activated
周期内恢复滚动条的位置。
示例代码
// 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-navigation
、 vue-router
、 keep-alive
、 eventBus
等组件/库实现了这些功能。未来我们将会持续打磨和优化代码,并更多的输出一些我们在项目开发过程中的经验。