Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

Commit

Permalink
feat(nuxt): app:rendered and app:response hooks (#6042)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Jul 27, 2022
1 parent 2c6e501 commit f58aa81
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 72 deletions.
4 changes: 3 additions & 1 deletion packages/nuxt/src/app/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -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
Expand Down
154 changes: 103 additions & 51 deletions packages/nuxt/src/core/runtime/nitro/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']

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 type NuxtSSRContext = NuxtApp['ssrContext']

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
Expand Down Expand Up @@ -132,49 +133,80 @@ 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 }
const _rendered = await renderer.renderToString(ssrContext).catch((err) => {
if (!ssrError) { throw err }
})

if (event.res.writableEnded) {
// Handle errors
if (!_rendered) {
return
}

// Handle errors
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'
}
}

async function renderHTML (payload: any, rendered: RenderResult, ssrContext: NuxtSSRContext) {
const state = `<script>window.__NUXT__=${devalue(payload)}</script>`
// Allow extending the response
await nitroApp.hooks.callHook('nuxt:app:response', { response })

rendered.meta = rendered.meta || {}
if (ssrContext.renderMeta) {
Object.assign(rendered.meta, await ssrContext.renderMeta())
// 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
}

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 || '')
})
}
if (!event.res.writableEnded) {
event.res.end(response.body)
}
})

function lazyCachedFunction <T> (fn: () => Promise<T>): () => Promise<T> {
let res: Promise<T> | null = null
Expand All @@ -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>`
}
20 changes: 0 additions & 20 deletions packages/nuxt/src/core/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down

0 comments on commit f58aa81

Please sign in to comment.