From 63e1a7f28116057a21ecef381b71e34f6e57d83a Mon Sep 17 00:00:00 2001 From: Dmitry Sharshakov Date: Sat, 8 Feb 2020 21:45:03 +0300 Subject: [PATCH 1/7] chore: start working --- packages/server-renderer/src/renderToString.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index 1b0eff4cc3f..6a99095df06 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -143,7 +143,7 @@ function renderComponentSubTree( } else if (comp.render) { renderVNode(push, renderComponentRoot(instance), instance) } else { - // TODO on the fly template compilation support + // TODO on the fly template compilation support throw new Error( `Component ${ comp.name ? `${comp.name} ` : `` From 29e8a3ddcebfc5963183c3599b947a82d1dd59a2 Mon Sep 17 00:00:00 2001 From: Dmitry Sharshakov Date: Sun, 9 Feb 2020 14:29:01 +0300 Subject: [PATCH 2/7] feat(server-renderer): compile templates on-the-fly --- .../__tests__/renderToString.spec.ts | 76 ++++++++++++++++++- packages/server-renderer/package.json | 3 + .../server-renderer/src/renderToString.ts | 17 ++++- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts index c7a7cfb980c..47127eb7dfa 100644 --- a/packages/server-renderer/__tests__/renderToString.spec.ts +++ b/packages/server-renderer/__tests__/renderToString.spec.ts @@ -56,6 +56,19 @@ describe('ssr: renderToString', () => { ).toBe(`
hello
`) }) + test('template components', async () => { + expect( + await renderToString( + createApp({ + data() { + return { msg: 'hello' } + }, + template: `
{{ msg }}
` + }) + ) + ).toBe(`
hello
`) + }) + test('nested vnode components', async () => { const Child = { props: ['msg'], @@ -96,7 +109,22 @@ describe('ssr: renderToString', () => { ).toBe(`
parent
hello
`) }) - test('mixing optimized / vnode components', async () => { + test('nested template components', async () => { + const Child = { + props: ['msg'], + template: `
{{ msg }}
` + } + const app = createApp({ + template: `
parent
` + }) + app.component('Child', Child) + + expect(await renderToString(app)).toBe( + `
parent
hello
` + ) + }) + + test('mixing optimized / vnode / template components', async () => { const OptimizedChild = { props: ['msg'], ssrRender(ctx: any, push: any) { @@ -111,6 +139,11 @@ describe('ssr: renderToString', () => { } } + const TemplateChild = { + props: ['msg'], + template: `
{{ msg }}
` + } + expect( await renderToString( createApp({ @@ -120,11 +153,21 @@ describe('ssr: renderToString', () => { renderComponent(OptimizedChild, { msg: 'opt' }, null, parent) ) push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent)) + push( + renderComponent( + TemplateChild, + { msg: 'template' }, + null, + parent + ) + ) push(``) } }) ) - ).toBe(`
parent
opt
vnode
`) + ).toBe( + `
parent
opt
vnode
template
` + ) }) test('nested components with optimized slots', async () => { @@ -236,6 +279,35 @@ describe('ssr: renderToString', () => { ) }) + test('nested components with template slots', async () => { + const Child = { + props: ['msg'], + ssrRender(ctx: any, push: any, parent: any) { + push(`
`) + ssrRenderSlot( + ctx.$slots, + 'default', + { msg: 'from slot' }, + null, + push, + parent + ) + push(`
`) + } + } + + const app = createApp({ + template: `
parent{{ msg }}
` + }) + app.component('Child', Child) + + expect(await renderToString(app)).toBe( + `
parent
` + + `from slot` + + `
` + ) + }) + test('async components', async () => { const Child = { // should wait for resovled render context from setup() diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index b8b47c549fd..2d30373e0b4 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -28,5 +28,8 @@ "homepage": "https://github.com/vuejs/vue/tree/dev/packages/server-renderer#readme", "peerDependencies": { "vue": "3.0.0-alpha.4" + }, + "dependencies": { + "@vue/compiler-ssr": "3.0.0-alpha.4" } } diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index 6a99095df06..a82f84a871c 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -19,8 +19,10 @@ import { isArray, isFunction, isVoidTag, - escapeHtml + escapeHtml, + NO } from '@vue/shared' +import { compile } from '@vue/compiler-ssr' import { ssrRenderAttrs } from './helpers/ssrRenderAttrs' import { SSRSlots } from './helpers/ssrRenderSlot' @@ -142,12 +144,21 @@ function renderComponentSubTree( setCurrentRenderingInstance(null) } else if (comp.render) { renderVNode(push, renderComponentRoot(instance), instance) + } else if (comp.template && isString(comp.template)) { + const compileResult = compile(comp.template, { + isCustomElement: instance.appContext.config.isCustomElement || NO + }) + const ssrRender = Function(compileResult.code)() + + // set current rendering instance for asset resolution + setCurrentRenderingInstance(instance) + ssrRender(instance.proxy, push, instance) + setCurrentRenderingInstance(null) } else { - // TODO on the fly template compilation support throw new Error( `Component ${ comp.name ? `${comp.name} ` : `` - } is missing render function.` + } doesn't provide template or render function.` ) } } From e9cab0c8b2d2bdb1bac452f84cf4f58053677a99 Mon Sep 17 00:00:00 2001 From: Dmitry Sharshakov Date: Mon, 10 Feb 2020 17:25:31 +0300 Subject: [PATCH 3/7] Update packages/server-renderer/src/renderToString.ts Co-Authored-By: Evan You --- packages/server-renderer/src/renderToString.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index a82f84a871c..eb94dad4133 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -158,7 +158,7 @@ function renderComponentSubTree( throw new Error( `Component ${ comp.name ? `${comp.name} ` : `` - } doesn't provide template or render function.` + } is missing template or render function.` ) } } From cc02b8946ae652267c1bddfe872c3816d64b3bc2 Mon Sep 17 00:00:00 2001 From: Dmitry Sharshakov Date: Mon, 10 Feb 2020 18:28:18 +0300 Subject: [PATCH 4/7] test: update tests --- .../__tests__/renderToString.spec.ts | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts index 47127eb7dfa..feef1b5010c 100644 --- a/packages/server-renderer/__tests__/renderToString.spec.ts +++ b/packages/server-renderer/__tests__/renderToString.spec.ts @@ -282,17 +282,32 @@ describe('ssr: renderToString', () => { test('nested components with template slots', async () => { const Child = { props: ['msg'], - ssrRender(ctx: any, push: any, parent: any) { - push(`
`) - ssrRenderSlot( - ctx.$slots, - 'default', - { msg: 'from slot' }, - null, - push, - parent + template: `
` + } + + const app = createApp({ + template: `
parent{{ msg }}
` + }) + app.component('Child', Child) + + expect(await renderToString(app)).toBe( + `
parent
` + + `from slot` + + `
` + ) + }) + + test('nested render fn components with template slots', async () => { + const Child = { + props: ['msg'], + render(this: any) { + return h( + 'div', + { + class: 'child' + }, + this.$slots.default({ msg: 'from slot' }) ) - push(`
`) } } @@ -303,7 +318,7 @@ describe('ssr: renderToString', () => { expect(await renderToString(app)).toBe( `
parent
` + - `from slot` + + `from slot` + `
` ) }) From 2f5f40f0ba39d7eec066496835d9e2aec5ac968a Mon Sep 17 00:00:00 2001 From: Dmitry Sharshakov Date: Mon, 10 Feb 2020 18:29:36 +0300 Subject: [PATCH 5/7] refactor: DRY up the code --- packages/server-renderer/src/renderToString.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index eb94dad4133..a49e3d9b051 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -136,6 +136,13 @@ function renderComponentSubTree( if (isFunction(comp)) { renderVNode(push, renderComponentRoot(instance), instance) } else { + if (!comp.ssrRender && !comp.render && isString(comp.template)) { + const compileResult = compile(comp.template, { + isCustomElement: instance.appContext.config.isCustomElement || NO + }) + comp.ssrRender = Function(compileResult.code)() + } + if (comp.ssrRender) { // optimized // set current rendering instance for asset resolution @@ -144,16 +151,6 @@ function renderComponentSubTree( setCurrentRenderingInstance(null) } else if (comp.render) { renderVNode(push, renderComponentRoot(instance), instance) - } else if (comp.template && isString(comp.template)) { - const compileResult = compile(comp.template, { - isCustomElement: instance.appContext.config.isCustomElement || NO - }) - const ssrRender = Function(compileResult.code)() - - // set current rendering instance for asset resolution - setCurrentRenderingInstance(instance) - ssrRender(instance.proxy, push, instance) - setCurrentRenderingInstance(null) } else { throw new Error( `Component ${ From 68dd5d4612ae3c2f2b8de7e5ba8302b04eade1f4 Mon Sep 17 00:00:00 2001 From: Dmitry Sharshakov Date: Mon, 10 Feb 2020 18:46:27 +0300 Subject: [PATCH 6/7] feat(ssr): handle template compiling errors --- .../__tests__/renderToString.spec.ts | 38 +++++++++++++------ .../server-renderer/src/renderToString.ts | 31 ++++++++++++--- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts index feef1b5010c..051f59b4f03 100644 --- a/packages/server-renderer/__tests__/renderToString.spec.ts +++ b/packages/server-renderer/__tests__/renderToString.spec.ts @@ -6,10 +6,12 @@ import { resolveComponent, ComponentOptions } from 'vue' -import { escapeHtml } from '@vue/shared' +import { escapeHtml, mockWarn } from '@vue/shared' import { renderToString, renderComponent } from '../src/renderToString' import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot' +mockWarn() + describe('ssr: renderToString', () => { test('should apply app context', async () => { const app = createApp({ @@ -56,17 +58,29 @@ describe('ssr: renderToString', () => { ).toBe(`
hello
`) }) - test('template components', async () => { - expect( - await renderToString( - createApp({ - data() { - return { msg: 'hello' } - }, - template: `
{{ msg }}
` - }) - ) - ).toBe(`
hello
`) + describe('template components', () => { + test('render', async () => { + expect( + await renderToString( + createApp({ + data() { + return { msg: 'hello' } + }, + template: `
{{ msg }}
` + }) + ) + ).toBe(`
hello
`) + }) + + test('handle compiler errors', async () => { + await renderToString(createApp({ template: `<` })) + + expect( + '[Vue warn]: Template compilation error: Unexpected EOF in tag.\n' + + '1 | <\n' + + ' | ^' + ).toHaveBeenWarned() + }) }) test('nested vnode components', async () => { diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index a49e3d9b051..c340054e082 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -11,7 +11,8 @@ import { Portal, ShapeFlags, ssrUtils, - Slots + Slots, + warn } from 'vue' import { isString, @@ -20,11 +21,13 @@ import { isFunction, isVoidTag, escapeHtml, - NO + NO, + generateCodeFrame } from '@vue/shared' import { compile } from '@vue/compiler-ssr' import { ssrRenderAttrs } from './helpers/ssrRenderAttrs' import { SSRSlots } from './helpers/ssrRenderSlot' +import { CompilerError } from '@vue/compiler-dom' const { isVNode, @@ -136,11 +139,27 @@ function renderComponentSubTree( if (isFunction(comp)) { renderVNode(push, renderComponentRoot(instance), instance) } else { - if (!comp.ssrRender && !comp.render && isString(comp.template)) { - const compileResult = compile(comp.template, { - isCustomElement: instance.appContext.config.isCustomElement || NO + const { template } = comp + if (!comp.ssrRender && !comp.render && isString(template)) { + const { code } = compile(template, { + isCustomElement: instance.appContext.config.isCustomElement || NO, + onError(err: CompilerError) { + if (__DEV__) { + const message = `Template compilation error: ${err.message}` + const codeFrame = + err.loc && + generateCodeFrame( + template as string, + err.loc.start.offset, + err.loc.end.offset + ) + warn(codeFrame ? `${message}\n${codeFrame}` : message) + } else { + throw err + } + } }) - comp.ssrRender = Function(compileResult.code)() + comp.ssrRender = Function(code)() } if (comp.ssrRender) { From 6337eb750b2cd71de37efe2fb2c47c75a36dd85f Mon Sep 17 00:00:00 2001 From: Dmitry Sharshakov Date: Mon, 10 Feb 2020 19:05:46 +0300 Subject: [PATCH 7/7] feat(ssr): support compile cache --- .../server-renderer/src/renderToString.ts | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index c340054e082..074d98b0423 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -131,6 +131,44 @@ function renderComponentVNode( } } +type SSRRenderFunction = ( + ctx: any, + push: (item: any) => void, + parentInstance: ComponentInternalInstance +) => void +const compileCache: Record = Object.create(null) + +function ssrCompile( + template: string, + instance: ComponentInternalInstance +): SSRRenderFunction { + const cached = compileCache[template] + if (cached) { + return cached + } + + const { code } = compile(template, { + isCustomElement: instance.appContext.config.isCustomElement || NO, + isNativeTag: instance.appContext.config.isNativeTag || NO, + onError(err: CompilerError) { + if (__DEV__) { + const message = `Template compilation error: ${err.message}` + const codeFrame = + err.loc && + generateCodeFrame( + template as string, + err.loc.start.offset, + err.loc.end.offset + ) + warn(codeFrame ? `${message}\n${codeFrame}` : message) + } else { + throw err + } + } + }) + return (compileCache[template] = Function(code)()) +} + function renderComponentSubTree( instance: ComponentInternalInstance ): ResolvedSSRBuffer | Promise { @@ -139,27 +177,8 @@ function renderComponentSubTree( if (isFunction(comp)) { renderVNode(push, renderComponentRoot(instance), instance) } else { - const { template } = comp - if (!comp.ssrRender && !comp.render && isString(template)) { - const { code } = compile(template, { - isCustomElement: instance.appContext.config.isCustomElement || NO, - onError(err: CompilerError) { - if (__DEV__) { - const message = `Template compilation error: ${err.message}` - const codeFrame = - err.loc && - generateCodeFrame( - template as string, - err.loc.start.offset, - err.loc.end.offset - ) - warn(codeFrame ? `${message}\n${codeFrame}` : message) - } else { - throw err - } - } - }) - comp.ssrRender = Function(code)() + if (!comp.ssrRender && !comp.render && isString(comp.template)) { + comp.ssrRender = ssrCompile(comp.template, instance) } if (comp.ssrRender) {