feat(nuxt): allow configuring spa loading indicator (#21640)

This commit is contained in:
Daniel Roe 2023-06-20 19:55:20 +01:00 committed by GitHub
parent 343a46d5f9
commit c66c82e6a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 102 additions and 6 deletions

View File

@ -67,6 +67,11 @@ export default defineNuxtConfig({
}) })
``` ```
::alert{type=info}
If you do use `ssr: false`, you should also place an HTML file in `~/app/spa-loading-template.html` with some HTML you would like to use to render a loading screen that will be rendered until your app is hydrated.
:ReadMore{link="/docs/api/configuration/nuxt-config#spaloadingindicator"}
::
## Hybrid Rendering ## Hybrid Rendering
Hybrid rendering allows different caching rules per route using **Route Rules** and decides how the server should respond to a new request on a given URL. Hybrid rendering allows different caching rules per route using **Route Rules** and decides how the server should respond to a new request on a given URL.

View File

@ -1,4 +1,4 @@
import { existsSync, promises as fsp } from 'node:fs' import { existsSync, promises as fsp, readFileSync } from 'node:fs'
import { join, relative, resolve } from 'pathe' import { join, relative, resolve } from 'pathe'
import { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack' import { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack'
import type { Nitro, NitroConfig } from 'nitropack' import type { Nitro, NitroConfig } from 'nitropack'
@ -10,6 +10,8 @@ import { dynamicEventHandler } from 'h3'
import { createHeadCore } from '@unhead/vue' import { createHeadCore } from '@unhead/vue'
import { renderSSRHead } from '@unhead/ssr' import { renderSSRHead } from '@unhead/ssr'
import type { Nuxt } from 'nuxt/schema' import type { Nuxt } from 'nuxt/schema'
// @ts-expect-error TODO: add legacy type support for subpath imports
import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs'
import { distDir } from '../dirs' import { distDir } from '../dirs'
import { ImportProtectionPlugin } from './plugins/import-protection' import { ImportProtectionPlugin } from './plugins/import-protection'
@ -29,6 +31,13 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
? [new RegExp(`node_modules\\/(?!${excludePaths.join('|')})`)] ? [new RegExp(`node_modules\\/(?!${excludePaths.join('|')})`)]
: [/node_modules/] : [/node_modules/]
const spaLoadingTemplatePath = nuxt.options.spaLoadingTemplate ?? resolve(nuxt.options.srcDir, 'app/spa-loading-template.html')
if (spaLoadingTemplatePath !== false && !existsSync(spaLoadingTemplatePath)) {
if (nuxt.options.spaLoadingTemplate) {
console.warn(`[nuxt] Could not load custom \`spaLoadingTemplate\` path as it does not exist: \`${spaLoadingTemplatePath}\`.`)
}
}
const nitroConfig: NitroConfig = defu(_nitroConfig, <NitroConfig>{ const nitroConfig: NitroConfig = defu(_nitroConfig, <NitroConfig>{
debug: nuxt.options.debug, debug: nuxt.options.debug,
rootDir: nuxt.options.rootDir, rootDir: nuxt.options.rootDir,
@ -75,7 +84,15 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
devHandlers: [], devHandlers: [],
baseURL: nuxt.options.app.baseURL, baseURL: nuxt.options.app.baseURL,
virtual: { virtual: {
'#internal/nuxt.config.mjs': () => nuxt.vfs['#build/nuxt.config'] '#internal/nuxt.config.mjs': () => nuxt.vfs['#build/nuxt.config'],
'#spa-template': () => {
try {
if (spaLoadingTemplatePath) {
return `export const template = ${JSON.stringify(readFileSync(spaLoadingTemplatePath, 'utf-8'))}`
}
} catch {}
return `export const template = ${JSON.stringify(defaultSpaLoadingTemplate({}))}`
}
}, },
routeRules: { routeRules: {
'/__nuxt_error': { cache: false } '/__nuxt_error': { cache: false }

View File

@ -112,9 +112,12 @@ const getSSRRenderer = lazyCachedFunction(async () => {
const getSPARenderer = lazyCachedFunction(async () => { const getSPARenderer = lazyCachedFunction(async () => {
const manifest = await getClientManifest() const manifest = await getClientManifest()
// @ts-expect-error virtual file
const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '')
const options = { const options = {
manifest, manifest,
renderToString: () => `<${appRootTag} id="${appRootId}"></${appRootTag}>`, renderToString: () => `<${appRootTag} id="${appRootId}">${spaTemplate}</${appRootTag}>`,
buildAssetsURL buildAssetsURL
} }
// Create SPA renderer and cache the result for all requests // Create SPA renderer and cache the result for all requests

View File

@ -1,5 +1,6 @@
import { defineUntypedSchema } from 'untyped' import { defineUntypedSchema } from 'untyped'
import { defu } from 'defu' import { defu } from 'defu'
import { resolve } from 'pathe'
import type { AppHeadMetaObject } from '../types/head' import type { AppHeadMetaObject } from '../types/head'
export default defineUntypedSchema({ export default defineUntypedSchema({
@ -177,6 +178,65 @@ export default defineUntypedSchema({
rootTag: 'div', rootTag: 'div',
}, },
/** A path to an HTML file, the contents of which will be inserted into any HTML page
* rendered with `ssr: false`.
*
* By default Nuxt will look in `~/app/spa-loading-template.html` for this file.
*
* You can set this to `false` to disable any loading indicator.
*
* Some good sources for spinners are [SpinKit](https://github.com/tobiasahlin/SpinKit) or [SVG Spinners](https://icones.js.org/collection/svg-spinners).
*
* @example ~/app/spa-loading-template.html
* ```html
* <!-- https://github.com/barelyhuman/snips/blob/dev/pages/css-loader.md -->
* <div class="loader"></div>
* <style>
* .loader {
* display: block;
* position: fixed;
* z-index: 1031;
* top: 50%;
* left: 50%;
* transform: translate(-50%, -50%);
* width: 18px;
* height: 18px;
* box-sizing: border-box;
* border: solid 2px transparent;
* border-top-color: #000;
* border-left-color: #000;
* border-bottom-color: #efefef;
* border-right-color: #efefef;
* border-radius: 50%;
* -webkit-animation: loader 400ms linear infinite;
* animation: loader 400ms linear infinite;
* }
*
* \@-webkit-keyframes loader {
* 0% {
* -webkit-transform: rotate(0deg);
* }
* 100% {
* -webkit-transform: rotate(360deg);
* }
* }
* \@keyframes loader {
* 0% {
* transform: rotate(0deg);
* }
* 100% {
* transform: rotate(360deg);
* }
* }
* </style>
* ```
*
* @type {string | false}
*/
spaLoadingTemplate: {
$resolve: async (val, get) => typeof val === 'string' ? resolve(await get('srcDir'), val) : (val ?? null)
},
/** /**
* An array of nuxt app plugins. * An array of nuxt app plugins.
* *

View File

@ -5310,6 +5310,17 @@ packages:
slash: 3.0.0 slash: 3.0.0
dev: true dev: true
/globby@13.1.4:
resolution: {integrity: sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
dir-glob: 3.0.1
fast-glob: 3.2.12
ignore: 5.2.4
merge2: 1.4.1
slash: 4.0.0
dev: true
/globby@13.2.0: /globby@13.2.0:
resolution: {integrity: sha512-jWsQfayf13NvqKUIL3Ta+CIqMnvlaIDFveWE/dpOZ9+3AMEJozsxDvKA02zync9UuvOM8rOXzsD5GqKP4OnWPQ==} resolution: {integrity: sha512-jWsQfayf13NvqKUIL3Ta+CIqMnvlaIDFveWE/dpOZ9+3AMEJozsxDvKA02zync9UuvOM8rOXzsD5GqKP4OnWPQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -6425,7 +6436,7 @@ packages:
defu: 6.1.2 defu: 6.1.2
esbuild: 0.17.19 esbuild: 0.17.19
fs-extra: 11.1.1 fs-extra: 11.1.1
globby: 13.2.0 globby: 13.1.4
jiti: 1.18.2 jiti: 1.18.2
mlly: 1.3.0 mlly: 1.3.0
mri: 1.2.0 mri: 1.2.0
@ -8452,7 +8463,7 @@ packages:
consola: 3.1.0 consola: 3.1.0
defu: 6.1.2 defu: 6.1.2
esbuild: 0.17.19 esbuild: 0.17.19
globby: 13.2.0 globby: 13.1.4
hookable: 5.5.3 hookable: 5.5.3
jiti: 1.18.2 jiti: 1.18.2
magic-string: 0.30.0 magic-string: 0.30.0

View File

@ -35,7 +35,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
it('default server bundle size', async () => { it('default server bundle size', async () => {
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"61.3k"') expect.soft(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"62.1k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir) const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2295k"') expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2295k"')