14. 示例 [nuxt-11]:自定义加载图片
默认情况下,[nuxt] 的加载图片是一个进度条。示例 [nuxt-11] 展示了如何将其替换为自定义的加载图片:

该 [nuxt-11] 示例还展示了如何处理加载错误。

[nuxt-11] 示例最初源自 [nuxt-10] 示例:

在 [1] 中,我们将为客户端添加一个插件,该插件将负责管理组件之间的事件。
14.1. [event-bus] 插件
[event-bus] 插件将在客户端和服务器端同时执行,但我们会发现它在服务器端无法正常工作。其代码如下:
// on crée un bus d'événements entre les vues
import Vue from 'vue'
export default (context, inject) => {
// le bus d'événements
const eventBus = new Vue()
// injection d'une fonction [eventBus] dans le contexte
inject('eventBus', () => eventBus)
}
- 第 5 行:事件总线是 [Vue] 类的实例。该类提供了用于处理事件的方法:
- [$emit]:用于触发事件;
- [$on]:监听特定事件;
该事件总线仅处理一个事件 [loading],页面将利用该事件在等待异步函数完成时启动/停止加载动画;
- 第 7 行:我们创建了一个名为 [$eventBus] 的函数(第一个参数),其作用是返回我们刚刚创建的 [eventBus] 对象(第二个参数)。该函数被注入到上下文中,以便在页面的 [context.app] 和 [this] 对象中可用;
14.2. [default.vue] 布局
[default.vue] 布局的演变如下:
<template>
<div class="container">
<b-card>
<!-- un message -->
<b-alert show variant="success" align="center">
<h4>[nuxt-11] : personnalisation de l'attente, gestion des erreurs</h4>
</b-alert>
<!-- la vue courante du routage -->
<nuxt />
<!-- loading -->
<b-alert v-if="showLoading" show variant="light">
<strong>Requête au serveur de données en cours...</strong>
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
</b-alert>
<!-- erreur de chargement -->
<b-alert v-if="showErrorLoading" show variant="danger">
<strong>La requête au serveur de données a échoué : {{ errorLoadingMessage }}</strong>
</b-alert>
</b-card>
</div>
</template>
<script>
/* eslint-disable no-console */
export default {
name: 'App',
data() {
return {
showLoading: false,
showErrorLoading: false
}
},
// life cycle
beforeCreate() {
console.log('[default beforeCreate]')
},
created() {
console.log('[default created]')
// listen to the evt [loading]
this.$eventBus().$on('loading', this.mShowLoading)
// and the [errorLoadingMessage] event
this.$eventBus().$on('errorLoading', this.mShowErrorLoading)
},
beforeMount() {
console.log('[default beforeMount]')
},
mounted() {
console.log('[default mounted]')
},
methods: {
// load management
mShowLoading(value) {
console.log('[default mShowLoading], showLoading=', value)
this.showLoading = value
},
// loading error
mShowErrorLoading(value, errorLoadingMessage) {
console.log('[default mShowErrorLoading], showErrorLoading=', value, 'errorLoadingMessage=', errorLoadingMessage)
this.showErrorLoading = value
this.errorLoadingMessage = errorLoadingMessage
}
}
}
</script>
- 第 11–14 行:加载动画。仅当 [showLoading] 属性为 true(第 29 行)时才会显示;
- 第 16–18 行:加载错误提示。仅当 [showErrorLoading] 属性(第 30 行)为 true 时才会显示;
- 第 29–30 行:组件初始加载时,加载动画和错误消息均被隐藏;
- 第 37–43 行:页面创建时,会监听插件创建的事件总线上的 [loading] 事件(第一个参数)。接收到该事件后,将执行第 52–55 行中的 [mShowLoading] 方法(第二个参数);
- 第 52–55 行:[mShowLoading] 方法接收的参数为布尔值(true/false),用于显示或隐藏加载提示;
- 第 41–42 行:页面创建时,会监听插件创建的事件总线上的 [errorLoading] 事件(第一个参数)。接收到该事件后,将执行第 57–61 行中的 [mShowErrorLoading] 方法(第二个参数);
- 第 57 行:[mShowErrorLoading] 方法接受两个参数:
- 第一个参数是一个布尔值(true/false),用于控制是否显示错误提示;
- 第二个参数仅在发生错误时存在,它表示要显示的错误消息;
- 第 53 行和第 58 行的日志将显示,[showLoading] 和 [showErrorLoading] 方法在服务器端并未被执行;
14.3. [page1] 页面
[page1] 页面的代码如下所示:
<!-- vue n° 1 -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message-->
<b-alert slot="right" show variant="primary"> Page 1 -- result={{ result }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page1',
// components used
components: {
Layout,
Navigation
},
// asynchronous data
asyncData(context) {
// log
console.log('[page1 asyncData started]')
// start waiting
context.app.$eventBus().$emit('loading', true)
// no error
context.app.$eventBus().$emit('errorLoading', false)
// we make a promise
return new Promise(function(resolve, reject) {
// we simulate an asynchronous function
setTimeout(function() {
// end waiting
context.app.$eventBus().$emit('loading', false)
// log
console.log('[page1 asyncData finished]')
// we make the result asynchronous - a random number here
resolve({ result: Math.floor(Math.random() * Math.floor(100)) })
}, 5000)
})
},
// life cycle
beforeCreate() {
console.log('[page1 beforeCreate]')
},
created() {
console.log('[page1 created]')
},
beforeMount() {
console.log('[page1 beforeMount]')
},
mounted() {
console.log('[page1 mounted]')
}
}
</script>
- 更改发生在第 26–47 行的 [asyncData] 函数中;
- 第 29–30 行:在异步函数开始之前,会向应用程序的其他页面发出 [loading] 事件。请注意,在 [asyncData] 中,我们尚无法访问 [this] 对象,因为它尚未被创建。因此,我们使用作为参数传递给 [asyncData] 函数的上下文(第 26 行);
- 第 30 行:使用事件总线来指示加载即将开始;
- 第 38 行:我们使用事件总线来指示加载已完成;
注意:在运行时,当向服务器请求 [page1] 页面时,加载图标不会显示。在日志中,我们可以看到服务器端并未调用 [default.mShowLoading] 方法。 无论如何,在向服务器请求页面时显示加载图标毫无意义。只有当 [asyncData] 函数执行完毕后,服务器才会将页面发送给客户端浏览器。因此,加载图标是多余的。对于应用程序中所有直接从服务器请求的页面,情况均是如此。
14.4. [index] 页面
[index] 页面的代码如下:
<!-- page principale -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message-->
<b-alert slot="right" show variant="warning">
Home
</b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Home',
// components used
components: {
Layout,
Navigation
},
// asynchronous data
asyncData(context) {
// log
console.log('[page1 asyncData started]')
// start waiting
context.app.$eventBus().$emit('loading', true)
// no error
context.app.$eventBus().$emit('errorLoading', false)
// we make a promise
return new Promise(function(resolve, reject) {
// we simulate an asynchronous function
setTimeout(function() {
// end waiting
context.app.$eventBus().$emit('loading', false)
// log
console.log('[page1 asyncData finished]')
// we return an error
reject(new Error("le serveur n'a pas répondu assez vite"))
}, 5000)
}).catch((e) => context.error({ statusCode: 500, message: e.message }))
},
// life cycle
beforeCreate() {
console.log('[home beforeCreate]')
},
created() {
console.log('[home created]')
},
beforeMount() {
console.log('[home beforeMount]')
},
mounted() {
console.log('[home mounted]')
// no error
this.$eventBus().$emit('errorLoading', false)
}
}
</script>
- 第 30–49 行:[asyncData] 函数与 [page1] 页面上的完全相同,只有一个例外:在第 46 行,异步函数在失败时会被终止(使用 [reject] 方法);
- 第 46 行:[reject] 函数的参数是 [Error] 类的实例。而 [Error] 构造函数的参数则是错误消息;
- 第 48 行:此错误由 [Promise] 的 [catch] 方法捕获,该方法将错误作为参数接收。随后我们使用 [context.error] 函数报告错误。此处 [context.error] 函数的参数是一个具有两个属性的对象:
- [statusCode]:HTTP 错误代码;
- [message]:错误消息;
无论 [asyncData] 是在客户端还是服务器端执行,一旦发生 [context.error] 错误,[nuxt] 都会显示 [layouts/error.vue] 页面:

尽管它是一个页面,但 [error.vue] 页面是在 [layouts] 文件夹中查找的(可能是为了防止它被包含在应用程序的路由中?)。这里,[error.vue] 页面如下所示:
<!-- définition HTML de la vue -->
<template>
<!-- mise en page -->
<Layout :left="true" :right="true">
<!-- alerte dans la colonne de droite -->
<template slot="right">
<!-- message sur fond jaune -->
<b-alert show variant="danger" align="center">
<h4>L'erreur suivante s'est produite : {{ JSON.stringify(error) }}</h4>
</b-alert>
</template>
<!-- menu de navigation dans la colonne de gauche -->
<Navigation slot="left" />
</Layout>
</template>
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
export default {
name: 'Error',
// components used
components: {
Layout,
Navigation
},
// property [props]
props: { error: { type: Object, default: () => 'waiting ...' } },
// life cycle
beforeCreate() {
// client and server
console.log('[error beforeCreate]')
},
created() {
// client and server
console.log('[error created, error=]', this.error)
},
beforeMount() {
// customer only
console.log('[error beforeMount]')
},
mounted() {
// customer only
console.log('[error mounted]')
}
}
</script>
当 [nuxt] 渲染 [error.vue] 页面时,会将发生的错误作为 [props] 属性传递给该页面(第 33 行)。如果错误是由 [context.error(object1)] 引起的,那么 [error.vue] 页面的 [props] 属性将取值为 [object1]。 [nuxt] 文档指出,[object1] 必须至少包含 [statusCode, message] 这两个属性。第 9 行显示了接收到的 [object1] 对象的 JSON 字符串。
14.5. [page2] 页面
[page2] 页面展示了另一种处理错误的方式:
- 在 [page1] 中,错误会在独立页面 [error.vue] 中显示;
- 而在 [page2] 中,错误将显示在引发该错误的 [page2] 页面上;
[page2] 的代码如下:
<!-- vue n° 2 -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message -->
<b-alert slot="right" show variant="secondary">
Page 2
</b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page2',
// components used
components: {
Layout,
Navigation
},
// asynchronous data
asyncData(context) {
// log
console.log('[page2 asyncData started]')
// start waiting
context.app.$eventBus().$emit('loading', true)
// no error
context.app.$eventBus().$emit('errorLoading', false)
// we make a promise
return new Promise(function(resolve, reject) {
// we simulate an asynchronous function
setTimeout(function() {
// end waiting
context.app.$eventBus().$emit('loading', false)
// arbitrarily generate an error
const errorLoadingMessage = "le serveur n'a pas répondu assez vite"
// successful completion
resolve({ showErrorLoading: true, errorLoadingMessage })
// log
console.log('[page2 asyncData finished]')
}, 5000)
})
},
// life cycle
beforeCreate() {
console.log('[page2 beforeCreate]')
},
created() {
console.log('[page2 created]')
},
beforeMount() {
console.log('[page2 beforeMount]')
},
mounted() {
console.log('[page2 mounted]')
// customer
if (this.showErrorLoading) {
console.log('[page2 mounted, showErrorLoading=true]')
this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
}
}
}
</script>
我们再次将 [asyncData] 函数插入到页面代码中,与 [index] 类似,[page2] 也会引发一个错误,不过这次我们将采用不同的方式来处理它。
- 第 44 行:服务器和客户端都通过返回结果 [{ showErrorLoading: true, errorLoadingMessage }] 成功解析了该 Promise。我们知道这会将 [showErrorLoading] 和 [errorLoadingMessage] 这两个属性添加到页面的 [data] 属性中,并且客户端将收到这些属性;
- 第 60–67 行:我们知道 [mounted] 函数仅由客户端执行;
- 第 63 行:客户端检查 [showErrorLoading] 属性是否已被设置(由服务器或客户端根据情况设置)。如果是,则触发 [‘errorLoading’] 事件(第 65 行),以便 [default] 页面显示错误消息 [this.errorLoadingMessage]。最终,服务器发送的页面上不会显示错误消息。 错误消息由客户端在页面“挂载”的最后一刻显示;
14.6. 执行
14.6.1. [nuxt.config]
[nuxt.config.js] 运行时文件如下:
export default {
mode: 'universal',
/*
** Headers of the page
*/
head: {
title: 'Introduction à [nuxt.js]',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: 'ssr routing loading asyncdata middleware plugins store'
}
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
},
/*
** Customize the progress-bar color
*/
loading: false,
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [{ src: '@/plugins/event-bus' }],
/*
** Nuxt.js dev-modules
*/
buildModules: [
// Doc: https://github.com/nuxt-community/eslint-module
'@nuxtjs/eslint-module'
],
/*
** Nuxt.js modules
*/
modules: [
// Doc: https://bootstrap-vue.js.org
'bootstrap-vue/nuxt',
// Doc: https://axios.nuxtjs.org/usage
'@nuxtjs/axios'
],
/*
** Axios module configuration
** See https://axios.nuxtjs.org/options
*/
axios: {},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {}
},
// source code directory
srcDir: 'nuxt-11',
// router
router: {
// application URL root
base: '/nuxt-11/'
},
// server
server: {
// service port, default 3000
port: 81,
// network addresses listened to, default localhost: 127.0.0.1
// 0.0.0.0 = all the machine's network addresses
host: 'localhost'
}
}
- 第 22 行:将 [loading] 属性设置为 [false],以便 [nuxt] 不使用其默认加载图片;
- 第 31 行:定义事件总线的插件;
14.6.2. 服务器提供的 [index] 页面
让我们向服务器请求 [index] 页面(手动输入 URL [http://localhost:81/nuxt-11/])。客户端浏览器显示的页面如下:

日志内容如下:

- 在 [3] 中,我们可以看到服务器发送了 [error.vue] 页面;
- 在 [4] 中,我们看到客户端也显示了 [error] 页面,且错误信息与服务器端一致;
- 我们可以看到,尽管 [index] 页面已触发等待,但服务器端并未调用 [default] 页面的 [mShowLoading] 方法。该方法是在接收到事件时被调用的,显然服务器端未实现事件处理;
让我们检查客户端浏览器接收到的页面源代码:
<!doctype html>
<html data-n-head-ssr>
<head>
<title>Introduction à [nuxt.js]</title>
<meta data-n-head="ssr" charset="utf-8">
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
<meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
<base href="/nuxt-11/">
<link rel="preload" href="/nuxt-11/_nuxt/runtime.js" as="script">
<link rel="preload" href="/nuxt-11/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/nuxt-11/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/nuxt-11/_nuxt/app.js" as="script">
...
</head>
<body>
<div data-server-rendered="true" id="__nuxt">
<div id="__layout">
<div class="container">
<div class="card">
<div class="card-body">
<div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
<h4>[nuxt-11] : personnalisation de l'attente, gestion des erreurs</h4>
</div>
<div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-11/" target="_self" class="nav-link active nuxt-link-active">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-11/page1" target="_self" class="nav-link">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-11/page2" target="_self" class="nav-link">
Page 2
</a>
</li>
</ul>
</div> <div class="col-10"><div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-danger">
<h4>L'erreur suivante s'est produite : {"statusCode":500,"message":"le serveur n'a pas répondu assez vite"}</h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>window.__NUXT__ = (function (a, b, c, d) {
d.statusCode = 500; d.message = "le serveur n'a pas répondu assez vite";
return {
layout: "default", data: [d], error: d, serverRendered: true,
logs: [
{ date: new Date(1575047424168), args: ["[event-bus créé]"], type: a, level: b, tag: c },
{ date: new Date(1575047424175), args: ["[page1 asyncData started]"], type: a, level: b, tag: c },
{ date: new Date(1575047429455), args: ["[page1 asyncData finished]"], type: a, level: b, tag: c },
{ date: new Date(1575047429515), args: ["[default beforeCreate]"], type: a, level: b, tag: c },
{ date: new Date(1575047429675), args: ["[default created]"], type: a, level: b, tag: c },
{ date: new Date(1575047430157), args: ["[error beforeCreate]"], type: a, level: b, tag: c },
{ date: new Date(1575047430246), args: ["[error created, error=]", "{ statusCode: 500,\n message: 'le serveur n\\'a pas répondu assez vite' }"], type: a, level: b, tag: c }]
}
}("log", 2, "", {}));</script>
<script src="/nuxt-11/_nuxt/runtime.js" defer></script>
<script src="/nuxt-11/_nuxt/commons.app.js" defer></script>
<script src="/nuxt-11/_nuxt/vendors.app.js" defer></script>
<script src="/nuxt-11/_nuxt/app.js" defer></script>
</body>
</html>
- 第 57 行:我们看到服务器发送了一个对象 [d],该对象代表了服务器端发生的错误;
- 第 59 行:我们看到一个名为 [error] 的属性,其值为对象 [d]。我们可以推断,正是服务器发送的页面中存在 [error] 属性,导致客户端脚本显示包含 [error] 错误的 [error.vue] 页面;
14.6.3. 由服务器执行的 [page1] 页面
我们手动输入 URL [http://localhost:81/nuxt-11/page1]。5 秒后,浏览器显示以下页面:

显示的日志如下:

- 在 [1] 中,是服务器日志。请注意,[default] 页面的 [mShowLoading] 方法未被调用;
- 在 [2] 中,客户端日志;
14.6.4. 由服务器执行的 [page2] 页面
我们手动输入 URL [http://localhost:81/nuxt-11/page2]。5 秒后,浏览器显示以下页面:

让我们查看浏览器中显示的日志:

- 在 [1] 中,是服务器日志。回想一下,服务器在发送给客户端浏览器的页面中包含了 [showErrorLoading, errorLoadingMessage] 属性。我们知道,这些属性随后会被包含在客户端显示的页面的 [data] 中
- 在 [3] 中,当 [page2] 页面挂载时,它发现 [showErrorLoading] 属性被设置为 true。随后它向 [default] 页面发送了一个事件,使其显示服务器发送的错误消息 [4];
14.6.5. 客户端执行的 [index] 页面
现在我们使用导航链接来显示这三个页面。客户端显示的所有页面与服务器显示的完全一致。唯一的区别在于每次都会显示 5 秒的加载图片。
我们从 [index] 页面开始。随后显示加载图片:

5秒后,将显示以下页面:

因此,最终页面与服务器端生成的页面完全一致。

回顾 [index] 页面上的 [asyncData] 函数:
asyncData(context) {
// log
console.log('[page1 asyncData started]')
// start waiting
context.app.$eventBus().$emit('loading', true)
// no error
context.app.$eventBus().$emit('errorLoading', false)
// we make a promise
return new Promise(function(resolve, reject) {
// we simulate an asynchronous function
setTimeout(function() {
// end waiting
context.app.$eventBus().$emit('loading', false)
// log
console.log('[page1 asyncData finished]')
// we return an error
reject(new Error("le serveur n'a pas répondu assez vite"))
}, 5000)
}).catch((e) => context.error({ statusCode: 500, message: e.message }))
}
客户端日志如下:

- 在 [1] 中,[asyncData] 函数开始执行;
- 在 [2],显示加载图片;
- 在 [2-3] 中,我们可以看到 [default] 页面已收到由 [index] 页面的 [asyncData] 函数发送的 [loading, true] [2] 和 [errorLoading, false] 事件(第 5 行和第 7 行);
- 在 [4],等待结束。[default] 页面已收到由 [index] 页面发送的 [loading, false] 事件(第 13 行);
- 在 [5] 中,[asyncData] 函数已完成其工作;
- 由于 [asyncData] 函数触发了 [context.error] 错误(第 19 行),因此显示 [error] 页面 [6];
14.6.6. 客户端执行的 [page1] 页面
等待 5 秒后,客户端显示以下页面:

让我们回顾一下 [page1] 中 [asyncData] 函数的代码:
asyncData(context) {
// log
console.log('[page1 asyncData started]')
// start waiting
context.app.$eventBus().$emit('loading', true)
// no error
context.app.$eventBus().$emit('errorLoading', false)
// we make a promise
return new Promise(function(resolve, reject) {
// we simulate an asynchronous function
setTimeout(function() {
// end waiting
context.app.$eventBus().$emit('loading', false)
// log
console.log('[page1 asyncData finished]')
// we make the result asynchronous - a random number here
resolve({ result: Math.floor(Math.random() * Math.floor(100)) })
}, 5000)
})
},
日志如下:

14.6.7. 客户端执行的 [page2] 页面
等待 5 秒后,客户端显示以下页面:

让我们回顾一下 [page2] 中 [asyncData] 和 [mounted] 函数的代码:
asyncData(context) {
// log
console.log('[page2 asyncData started]')
// start waiting
context.app.$eventBus().$emit('loading', true)
// no error
context.app.$eventBus().$emit('errorLoading', false)
// we make a promise
return new Promise(function(resolve, reject) {
// we simulate an asynchronous function
setTimeout(function() {
// end waiting
context.app.$eventBus().$emit('loading', false)
// arbitrarily generate an error
const errorLoadingMessage = "le serveur n'a pas répondu assez vite"
// successful completion
resolve({ showErrorLoading: true, errorLoadingMessage })
// log
console.log('[page2 asyncData finished]')
}, 5000)
})
}
mounted() {
console.log('[page2 mounted]')
// customer
if (this.showErrorLoading) {
console.log('[page2 mounted, showErrorLoading=true]')
this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
}
}
日志如下:

- 在 [1] 中,[default] 页面接收到了由 [page2] 发送的 [showErrorLoading, true] 事件(第 29 行),该事件指示其显示错误消息;