Ember路由过渡与动画 #
一、路由过渡概述 #
路由过渡是指从一个路由切换到另一个路由的过程。Ember提供了多种方式来处理和增强这个过程。
1.1 过渡生命周期 #
text
当前路由 deactivate
↓
新路由 beforeModel
↓
新路由 model
↓
新路由 afterModel
↓
新路由 setupController
↓
新路由 activate
二、过渡处理 #
2.1 阻止过渡 #
javascript
import Route from '@ember/routing/route';
import { action } from '@ember/object';
export default class EditRoute extends Route {
@action
willTransition(transition) {
if (this.controller.hasUnsavedChanges) {
const confirmed = confirm('有未保存的更改,确定离开吗?');
if (!confirmed) {
transition.abort();
}
}
}
}
2.2 重试过渡 #
javascript
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ProtectedRoute extends Route {
@service session;
beforeModel(transition) {
if (!this.session.isAuthenticated) {
// 保存目标过渡
this.session.set('attemptedTransition', transition);
this.transitionTo('login');
}
}
}
javascript
// 登录成功后重试
@action
loginSuccess() {
const transition = this.session.attemptedTransition;
if (transition) {
transition.retry();
} else {
this.router.transitionTo('home');
}
}
2.3 过渡目标信息 #
javascript
import Route from '@ember/routing/route';
export default class ApplicationRoute extends Route {
@action
willTransition(transition) {
console.log('从:', transition.from?.name);
console.log('到:', transition.to.name);
console.log('参数:', transition.to.params);
console.log('查询参数:', transition.to.queryParams);
}
}
三、加载状态 #
3.1 loading子状态 #
javascript
// app/routes/posts.js
import Route from '@ember/routing/route';
export default class PostsRoute extends Route {
async model() {
// 模拟慢速加载
await new Promise((resolve) => setTimeout(resolve, 2000));
return this.store.findAll('post');
}
}
handlebars
{{! app/templates/posts-loading.hbs}}
<div class="loading-state">
<div class="spinner"></div>
<p>加载中...</p>
</div>
3.2 error子状态 #
javascript
// app/routes/posts.js
import Route from '@ember/routing/route';
export default class PostsRoute extends Route {
async model() {
const response = await fetch('/api/posts');
if (!response.ok) {
throw new Error('加载失败');
}
return response.json();
}
}
handlebars
{{! app/templates/posts-error.hbs}}
<div class="error-state">
<h2>出错了</h2>
<p>{{@model.message}}</p>
<button {{on "click" this.retry}}>重试</button>
</div>
四、页面过渡动画 #
4.1 使用CSS动画 #
css
/* app/styles/app.css */
.page-enter {
opacity: 0;
transform: translateX(20px);
}
.page-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms, transform 300ms;
}
.page-exit {
opacity: 1;
transform: translateX(0);
}
.page-exit-active {
opacity: 0;
transform: translateX(-20px);
transition: opacity 300ms, transform 300ms;
}
4.2 使用ember-animated #
bash
ember install ember-animated
javascript
// app/routes/posts.js
import Route from '@ember/routing/route';
import AnimatedRoute from 'ember-animated/mixins/animated-route';
import { fadeSlide } from '../animations/transition';
export default class PostsRoute extends Route.extend(AnimatedRoute) {
model() {
return this.store.findAll('post');
}
animation = fadeSlide;
}
javascript
// app/animations/transition.js
import { fadeOut, fadeIn } from 'ember-animated/motions/opacity';
import { move } from 'ember-animated/motions/move';
export function* fadeSlide({ insertedSprites, keptSprites, removedSprites }) {
for (let sprite of removedSprites) {
yield fadeOut(sprite);
}
for (let sprite of insertedSprites) {
sprite.startAtPixel({ x: 20 });
yield fadeIn(sprite);
}
for (let sprite of keptSprites) {
yield move(sprite);
}
}
4.3 组件级动画 #
javascript
// app/components/animated-list.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import Animated from 'ember-animated/mixins/animated-component';
import { fadeSlide } from '../animations/transition';
export default class AnimatedListComponent extends Component {
animation = fadeSlide;
@tracked items = [];
@action
addItem() {
this.items = [...this.items, { id: Date.now() }];
}
}
handlebars
{{! app/components/animated-list.hbs}}
<button {{on "click" this.addItem}}>添加</button>
<AnimatedContainer>
{{#animated-each this.items use=this.animation as |item|}}
<div class="item">{{item.id}}</div>
{{/animated-each}}
</AnimatedContainer>
五、过渡钩子 #
5.1 应用级过渡 #
javascript
// app/routes/application.js
import Route from '@ember/routing/route';
import { action } from '@ember/object';
export default class ApplicationRoute extends Route {
@action
willTransition(transition) {
// 所有路由过渡前执行
console.log('即将过渡');
}
@action
didTransition() {
// 过渡完成后执行
console.log('过渡完成');
window.scrollTo(0, 0);
}
}
5.2 路由级过渡 #
javascript
// app/routes/posts.js
import Route from '@ember/routing/route';
import { action } from '@ember/object';
export default class PostsRoute extends Route {
@action
willTransition(transition) {
// 离开posts路由时执行
}
@action
didTransition() {
// 进入posts路由后执行
}
}
六、进度指示器 #
6.1 全局加载指示器 #
javascript
// app/services/loading.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class LoadingService extends Service {
@tracked isLoading = false;
@tracked progress = 0;
start() {
this.isLoading = true;
this.progress = 0;
}
update(value) {
this.progress = value;
}
complete() {
this.progress = 100;
setTimeout(() => {
this.isLoading = false;
}, 300);
}
}
javascript
// app/routes/application.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class ApplicationRoute extends Route {
@service loading;
@action
willTransition() {
this.loading.start();
}
@action
didTransition() {
this.loading.complete();
}
}
handlebars
{{! app/templates/application.hbs}}
{{#if this.loading.isLoading}}
<div class="loading-bar" style="width: {{this.loading.progress}}%"></div>
{{/if}}
{{outlet}}
七、页面标题管理 #
7.1 使用ember-page-title #
bash
ember install ember-page-title
handlebars
{{! app/templates/posts/show.hbs}}
<PageTitle>{{@model.title}} - 我的博客</PageTitle>
<h1>{{@model.title}}</h1>
7.2 动态标题 #
javascript
// app/routes/posts/show.js
import Route from '@ember/routing/route';
export default class PostsShowRoute extends Route {
afterModel(model) {
document.title = `${model.title} - 我的博客`;
}
}
八、滚动行为 #
8.1 滚动到顶部 #
javascript
// app/routes/application.js
import Route from '@ember/routing/route';
import { action } from '@ember/object';
export default class ApplicationRoute extends Route {
@action
didTransition() {
window.scrollTo(0, 0);
}
}
8.2 保持滚动位置 #
javascript
// app/services/scroll-position.js
import Service from '@ember/service';
export default class ScrollPositionService extends Service {
positions = {};
save(routeName) {
this.positions[routeName] = window.scrollY;
}
restore(routeName) {
const position = this.positions[routeName] || 0;
window.scrollTo(0, position);
}
}
九、最佳实践 #
9.1 优雅的加载状态 #
handlebars
{{! 好的做法 - 提供有意义的加载状态}}
<div class="loading-state">
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
</div>
{{! 避免 - 简单的加载文字}}
<div>加载中...</div>
9.2 平滑过渡 #
javascript
// 使用CSS过渡
.page-container {
transition: opacity 0.3s ease;
}
.page-container.loading {
opacity: 0.5;
}
9.3 避免闪烁 #
javascript
// 添加最小加载时间
async model() {
const [data] = await Promise.all([
this.store.findAll('post'),
new Promise((resolve) => setTimeout(resolve, 300)),
]);
return data;
}
十、总结 #
路由过渡与动画要点:
| 技术 | 用途 |
|---|---|
| loading子状态 | 加载中UI |
| error子状态 | 错误处理UI |
| ember-animated | 动画效果 |
| 过渡钩子 | 过渡控制 |
| 页面标题 | SEO优化 |
良好的过渡体验能提升用户满意度。
最后更新:2026-03-28