mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 08:33:53 +00:00
feat(nuxt): app:rendered
and app:response
hooks (#6042)
This commit is contained in:
parent
2c6e50124b
commit
f58aa8114f
@ -6,6 +6,8 @@ import type { RuntimeConfig } from '@nuxt/schema'
|
||||
import { getContext } from 'unctx'
|
||||
import type { SSRContext } from 'vue-bundle-renderer'
|
||||
import type { CompatibilityEvent } from 'h3'
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import type { NuxtRenderContext } from '../core/runtime/nitro/renderer'
|
||||
|
||||
const nuxtAppCtx = getContext<NuxtApp>('nuxt-app')
|
||||
|
||||
@ -23,7 +25,7 @@ export interface RuntimeNuxtHooks {
|
||||
'app:created': (app: App<Element>) => HookResult
|
||||
'app:beforeMount': (app: App<Element>) => HookResult
|
||||
'app:mounted': (app: App<Element>) => HookResult
|
||||
'app:rendered': () => HookResult
|
||||
'app:rendered': (ctx: NuxtRenderContext) => HookResult
|
||||
'app:redirected': () => HookResult
|
||||
'app:suspense:resolve': (Component?: VNode) => HookResult
|
||||
'app:error': (err: any) => HookResult
|
||||
|
@ -2,31 +2,32 @@ import { createRenderer } from 'vue-bundle-renderer'
|
||||
import { eventHandler, useQuery } from 'h3'
|
||||
import devalue from '@nuxt/devalue'
|
||||
import { renderToString as _renderToString } from 'vue/server-renderer'
|
||||
|
||||
import type { NuxtApp } from '#app'
|
||||
|
||||
// @ts-ignore
|
||||
import { useRuntimeConfig } from '#internal/nitro'
|
||||
import { useRuntimeConfig, useNitroApp } from '#internal/nitro'
|
||||
// @ts-ignore
|
||||
import { buildAssetsURL } from '#paths'
|
||||
// @ts-ignore
|
||||
import htmlTemplate from '#build/views/document.template.mjs'
|
||||
|
||||
type NuxtSSRContext = NuxtApp['ssrContext']
|
||||
export type NuxtSSRContext = NuxtApp['ssrContext']
|
||||
|
||||
interface RenderResult {
|
||||
html: any
|
||||
renderResourceHints: () => string
|
||||
renderStyles: () => string
|
||||
renderScripts: () => string
|
||||
meta?: Partial<{
|
||||
htmlAttrs?: string,
|
||||
bodyAttrs: string,
|
||||
headAttrs: string,
|
||||
headTags: string,
|
||||
bodyScriptsPrepend : string,
|
||||
bodyScripts : string
|
||||
}>
|
||||
export interface NuxtRenderContext {
|
||||
ssrContext: NuxtSSRContext
|
||||
html: {
|
||||
htmlAttrs: string[]
|
||||
head: string[]
|
||||
bodyAttrs: string[]
|
||||
bodyPreprend: string[]
|
||||
body: string[]
|
||||
bodyAppend: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface NuxtRenderResponse {
|
||||
body: string,
|
||||
statusCode: number,
|
||||
statusMessage?: string,
|
||||
headers: Record<string, string>
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
@ -132,50 +133,81 @@ export default eventHandler(async (event) => {
|
||||
|
||||
// Render app
|
||||
const renderer = (process.env.NUXT_NO_SSR || ssrContext.noSSR) ? await getSPARenderer() : await getSSRRenderer()
|
||||
const rendered = await renderer.renderToString(ssrContext).catch((e) => {
|
||||
if (!ssrError) { throw e }
|
||||
}) as RenderResult
|
||||
|
||||
// If we error on rendering error page, we bail out and directly return to the error handler
|
||||
if (!rendered) { return }
|
||||
|
||||
if (event.res.writableEnded) {
|
||||
return
|
||||
}
|
||||
const _rendered = await renderer.renderToString(ssrContext).catch((err) => {
|
||||
if (!ssrError) { throw err }
|
||||
})
|
||||
|
||||
// Handle errors
|
||||
if (!_rendered) {
|
||||
return
|
||||
}
|
||||
if (ssrContext.error && !ssrError) {
|
||||
throw ssrContext.error
|
||||
}
|
||||
|
||||
if (ssrContext.nuxt?.hooks) {
|
||||
await ssrContext.nuxt.hooks.callHook('app:rendered')
|
||||
// Render meta
|
||||
const renderedMeta = await ssrContext.renderMeta()
|
||||
|
||||
// Create render conrtext
|
||||
const rendered: NuxtRenderContext = {
|
||||
ssrContext,
|
||||
html: {
|
||||
htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]),
|
||||
head: normalizeChunks([
|
||||
renderedMeta.headTags,
|
||||
_rendered.renderResourceHints(),
|
||||
_rendered.renderStyles(),
|
||||
ssrContext.styles
|
||||
]),
|
||||
bodyAttrs: normalizeChunks([renderedMeta.bodyAttrs]),
|
||||
bodyPreprend: normalizeChunks([
|
||||
renderedMeta.bodyScriptsPrepend,
|
||||
ssrContext.teleports?.body
|
||||
]),
|
||||
body: [
|
||||
// TODO: Rename to _rendered.body in next vue-bundle-renderer
|
||||
_rendered.html
|
||||
],
|
||||
bodyAppend: normalizeChunks([
|
||||
`<script>window.__NUXT__=${devalue(ssrContext.payload)}</script>`,
|
||||
_rendered.renderScripts(),
|
||||
renderedMeta.bodyScripts
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const html = await renderHTML(ssrContext.payload, rendered, ssrContext)
|
||||
event.res.setHeader('Content-Type', 'text/html;charset=UTF-8')
|
||||
return html
|
||||
// Allow hooking into the rendered result
|
||||
const nitroApp = useNitroApp()
|
||||
await ssrContext.nuxt.hooks.callHook('app:rendered', rendered)
|
||||
await nitroApp.hooks.callHook('nuxt:app:rendered', rendered)
|
||||
|
||||
// Construct HTML response
|
||||
const response: NuxtRenderResponse = {
|
||||
body: renderHTMLDocument(rendered),
|
||||
statusCode: event.res.statusCode,
|
||||
statusMessage: event.res.statusMessage,
|
||||
headers: {
|
||||
'Content-Type': 'text/html;charset=UTF-8',
|
||||
'X-Powered-By': 'Nuxt'
|
||||
}
|
||||
}
|
||||
|
||||
// Allow extending the response
|
||||
await nitroApp.hooks.callHook('nuxt:app:response', { response })
|
||||
|
||||
// Send HTML response
|
||||
if (!event.res.headersSent) {
|
||||
for (const header in response.headers) {
|
||||
event.res.setHeader(header, response.headers[header])
|
||||
}
|
||||
event.res.statusCode = response.statusCode
|
||||
event.res.statusMessage = response.statusMessage
|
||||
}
|
||||
if (!event.res.writableEnded) {
|
||||
event.res.end(response.body)
|
||||
}
|
||||
})
|
||||
|
||||
async function renderHTML (payload: any, rendered: RenderResult, ssrContext: NuxtSSRContext) {
|
||||
const state = `<script>window.__NUXT__=${devalue(payload)}</script>`
|
||||
|
||||
rendered.meta = rendered.meta || {}
|
||||
if (ssrContext.renderMeta) {
|
||||
Object.assign(rendered.meta, await ssrContext.renderMeta())
|
||||
}
|
||||
|
||||
return htmlTemplate({
|
||||
HTML_ATTRS: (rendered.meta.htmlAttrs || ''),
|
||||
HEAD_ATTRS: (rendered.meta.headAttrs || ''),
|
||||
HEAD: (rendered.meta.headTags || '') +
|
||||
rendered.renderResourceHints() + rendered.renderStyles() + (ssrContext.styles || ''),
|
||||
BODY_ATTRS: (rendered.meta.bodyAttrs || ''),
|
||||
BODY_PREPEND: (ssrContext.teleports?.body || ''),
|
||||
APP: (rendered.meta.bodyScriptsPrepend || '') + rendered.html + state + rendered.renderScripts() + (rendered.meta.bodyScripts || '')
|
||||
})
|
||||
}
|
||||
|
||||
function lazyCachedFunction <T> (fn: () => Promise<T>): () => Promise<T> {
|
||||
let res: Promise<T> | null = null
|
||||
return () => {
|
||||
@ -185,3 +217,23 @@ function lazyCachedFunction <T> (fn: () => Promise<T>): () => Promise<T> {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeChunks (chunks: string[]) {
|
||||
return chunks.filter(Boolean).map(i => i.trim())
|
||||
}
|
||||
|
||||
function joinTags (tags: string[]) {
|
||||
return tags.join('')
|
||||
}
|
||||
|
||||
function joinAttrs (chunks: string[]) {
|
||||
return chunks.join(' ')
|
||||
}
|
||||
|
||||
function renderHTMLDocument (rendered: NuxtRenderContext) {
|
||||
return `<!DOCTYPE html>
|
||||
<html ${joinAttrs(rendered.html.htmlAttrs)}>
|
||||
<head>${joinTags(rendered.html.head)}</head>
|
||||
<body ${joinAttrs(rendered.html.bodyAttrs)}>${joinTags(rendered.html.bodyPreprend)}${joinTags(rendered.html.body)}${joinTags(rendered.html.bodyAppend)}</body>
|
||||
</html>`
|
||||
}
|
||||
|
@ -74,26 +74,6 @@ export const serverPluginTemplate = {
|
||||
}
|
||||
}
|
||||
|
||||
export const appViewTemplate = {
|
||||
filename: 'views/document.template.mjs',
|
||||
write: true,
|
||||
getContents () {
|
||||
return `export default (params) => \`<!DOCTYPE html>
|
||||
<html \${params.HTML_ATTRS}>
|
||||
|
||||
<head \${params.HEAD_ATTRS}>
|
||||
\${params.HEAD}
|
||||
</head>
|
||||
|
||||
<body \${params.BODY_ATTRS}>\${params.BODY_PREPEND}
|
||||
\${params.APP}
|
||||
</body>
|
||||
|
||||
</html>\`
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
export const pluginsDeclaration = {
|
||||
filename: 'types/plugins.d.ts',
|
||||
getContents: (ctx: TemplateContext) => {
|
||||
|
Loading…
Reference in New Issue
Block a user