feat(nuxt): `app:rendered` and `app:response` hooks (#6042)

This commit is contained in:
pooya parsa 2022-07-27 11:53:53 +02:00 committed by GitHub
parent 2c6e50124b
commit f58aa8114f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 107 additions and 73 deletions

View File

@ -6,6 +6,8 @@ import type { RuntimeConfig } from '@nuxt/schema'
import { getContext } from 'unctx' import { getContext } from 'unctx'
import type { SSRContext } from 'vue-bundle-renderer' import type { SSRContext } from 'vue-bundle-renderer'
import type { CompatibilityEvent } from 'h3' 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') const nuxtAppCtx = getContext<NuxtApp>('nuxt-app')
@ -23,7 +25,7 @@ export interface RuntimeNuxtHooks {
'app:created': (app: App<Element>) => HookResult 'app:created': (app: App<Element>) => HookResult
'app:beforeMount': (app: App<Element>) => HookResult 'app:beforeMount': (app: App<Element>) => HookResult
'app:mounted': (app: App<Element>) => HookResult 'app:mounted': (app: App<Element>) => HookResult
'app:rendered': () => HookResult 'app:rendered': (ctx: NuxtRenderContext) => HookResult
'app:redirected': () => HookResult 'app:redirected': () => HookResult
'app:suspense:resolve': (Component?: VNode) => HookResult 'app:suspense:resolve': (Component?: VNode) => HookResult
'app:error': (err: any) => HookResult 'app:error': (err: any) => HookResult

View File

@ -2,31 +2,32 @@ import { createRenderer } from 'vue-bundle-renderer'
import { eventHandler, useQuery } from 'h3' import { eventHandler, useQuery } from 'h3'
import devalue from '@nuxt/devalue' import devalue from '@nuxt/devalue'
import { renderToString as _renderToString } from 'vue/server-renderer' import { renderToString as _renderToString } from 'vue/server-renderer'
import type { NuxtApp } from '#app' import type { NuxtApp } from '#app'
// @ts-ignore // @ts-ignore
import { useRuntimeConfig } from '#internal/nitro' import { useRuntimeConfig, useNitroApp } from '#internal/nitro'
// @ts-ignore // @ts-ignore
import { buildAssetsURL } from '#paths' 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 { export interface NuxtRenderContext {
html: any ssrContext: NuxtSSRContext
renderResourceHints: () => string html: {
renderStyles: () => string htmlAttrs: string[]
renderScripts: () => string head: string[]
meta?: Partial<{ bodyAttrs: string[]
htmlAttrs?: string, bodyPreprend: string[]
bodyAttrs: string, body: string[]
headAttrs: string, bodyAppend: string[]
headTags: string, }
bodyScriptsPrepend : string, }
bodyScripts : string
}> export interface NuxtRenderResponse {
body: string,
statusCode: number,
statusMessage?: string,
headers: Record<string, string>
} }
// @ts-ignore // @ts-ignore
@ -132,50 +133,81 @@ export default eventHandler(async (event) => {
// Render app // Render app
const renderer = (process.env.NUXT_NO_SSR || ssrContext.noSSR) ? await getSPARenderer() : await getSSRRenderer() const renderer = (process.env.NUXT_NO_SSR || ssrContext.noSSR) ? await getSPARenderer() : await getSSRRenderer()
const rendered = await renderer.renderToString(ssrContext).catch((e) => { const _rendered = await renderer.renderToString(ssrContext).catch((err) => {
if (!ssrError) { throw e } if (!ssrError) { throw err }
}) 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
}
// Handle errors // Handle errors
if (!_rendered) {
return
}
if (ssrContext.error && !ssrError) { if (ssrContext.error && !ssrError) {
throw ssrContext.error throw ssrContext.error
} }
if (ssrContext.nuxt?.hooks) { // Render meta
await ssrContext.nuxt.hooks.callHook('app:rendered') 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) // Allow hooking into the rendered result
event.res.setHeader('Content-Type', 'text/html;charset=UTF-8') const nitroApp = useNitroApp()
return html 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> { function lazyCachedFunction <T> (fn: () => Promise<T>): () => Promise<T> {
let res: Promise<T> | null = null let res: Promise<T> | null = null
return () => { return () => {
@ -185,3 +217,23 @@ function lazyCachedFunction <T> (fn: () => Promise<T>): () => Promise<T> {
return res 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>`
}

View File

@ -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 = { export const pluginsDeclaration = {
filename: 'types/plugins.d.ts', filename: 'types/plugins.d.ts',
getContents: (ctx: TemplateContext) => { getContents: (ctx: TemplateContext) => {