Skip to content

Commit 6d10a6c

Browse files
authored
feat(server-renderer): support on-the-fly template compilation (#707)
1 parent cfadb98 commit 6d10a6c

File tree

3 files changed

+157
-7
lines changed

3 files changed

+157
-7
lines changed

packages/server-renderer/__tests__/renderToString.spec.ts

+104-3
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import {
66
resolveComponent,
77
ComponentOptions
88
} from 'vue'
9-
import { escapeHtml } from '@vue/shared'
9+
import { escapeHtml, mockWarn } from '@vue/shared'
1010
import { renderToString, renderComponent } from '../src/renderToString'
1111
import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
1212

13+
mockWarn()
14+
1315
describe('ssr: renderToString', () => {
1416
test('should apply app context', async () => {
1517
const app = createApp({
@@ -56,6 +58,31 @@ describe('ssr: renderToString', () => {
5658
).toBe(`<div>hello</div>`)
5759
})
5860

61+
describe('template components', () => {
62+
test('render', async () => {
63+
expect(
64+
await renderToString(
65+
createApp({
66+
data() {
67+
return { msg: 'hello' }
68+
},
69+
template: `<div>{{ msg }}</div>`
70+
})
71+
)
72+
).toBe(`<div>hello</div>`)
73+
})
74+
75+
test('handle compiler errors', async () => {
76+
await renderToString(createApp({ template: `<` }))
77+
78+
expect(
79+
'[Vue warn]: Template compilation error: Unexpected EOF in tag.\n' +
80+
'1 | <\n' +
81+
' | ^'
82+
).toHaveBeenWarned()
83+
})
84+
})
85+
5986
test('nested vnode components', async () => {
6087
const Child = {
6188
props: ['msg'],
@@ -96,7 +123,22 @@ describe('ssr: renderToString', () => {
96123
).toBe(`<div>parent<div>hello</div></div>`)
97124
})
98125

99-
test('mixing optimized / vnode components', async () => {
126+
test('nested template components', async () => {
127+
const Child = {
128+
props: ['msg'],
129+
template: `<div>{{ msg }}</div>`
130+
}
131+
const app = createApp({
132+
template: `<div>parent<Child msg="hello" /></div>`
133+
})
134+
app.component('Child', Child)
135+
136+
expect(await renderToString(app)).toBe(
137+
`<div>parent<div>hello</div></div>`
138+
)
139+
})
140+
141+
test('mixing optimized / vnode / template components', async () => {
100142
const OptimizedChild = {
101143
props: ['msg'],
102144
ssrRender(ctx: any, push: any) {
@@ -111,6 +153,11 @@ describe('ssr: renderToString', () => {
111153
}
112154
}
113155

156+
const TemplateChild = {
157+
props: ['msg'],
158+
template: `<div>{{ msg }}</div>`
159+
}
160+
114161
expect(
115162
await renderToString(
116163
createApp({
@@ -120,11 +167,21 @@ describe('ssr: renderToString', () => {
120167
renderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
121168
)
122169
push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent))
170+
push(
171+
renderComponent(
172+
TemplateChild,
173+
{ msg: 'template' },
174+
null,
175+
parent
176+
)
177+
)
123178
push(`</div>`)
124179
}
125180
})
126181
)
127-
).toBe(`<div>parent<div>opt</div><div>vnode</div></div>`)
182+
).toBe(
183+
`<div>parent<div>opt</div><div>vnode</div><div>template</div></div>`
184+
)
128185
})
129186

130187
test('nested components with optimized slots', async () => {
@@ -236,6 +293,50 @@ describe('ssr: renderToString', () => {
236293
)
237294
})
238295

296+
test('nested components with template slots', async () => {
297+
const Child = {
298+
props: ['msg'],
299+
template: `<div class="child"><slot msg="from slot"></slot></div>`
300+
}
301+
302+
const app = createApp({
303+
template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
304+
})
305+
app.component('Child', Child)
306+
307+
expect(await renderToString(app)).toBe(
308+
`<div>parent<div class="child">` +
309+
`<!----><span>from slot</span><!---->` +
310+
`</div></div>`
311+
)
312+
})
313+
314+
test('nested render fn components with template slots', async () => {
315+
const Child = {
316+
props: ['msg'],
317+
render(this: any) {
318+
return h(
319+
'div',
320+
{
321+
class: 'child'
322+
},
323+
this.$slots.default({ msg: 'from slot' })
324+
)
325+
}
326+
}
327+
328+
const app = createApp({
329+
template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
330+
})
331+
app.component('Child', Child)
332+
333+
expect(await renderToString(app)).toBe(
334+
`<div>parent<div class="child">` +
335+
`<span>from slot</span>` +
336+
`</div></div>`
337+
)
338+
})
339+
239340
test('async components', async () => {
240341
const Child = {
241342
// should wait for resovled render context from setup()

packages/server-renderer/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,8 @@
2828
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/server-renderer#readme",
2929
"peerDependencies": {
3030
"vue": "3.0.0-alpha.4"
31+
},
32+
"dependencies": {
33+
"@vue/compiler-ssr": "3.0.0-alpha.4"
3134
}
3235
}

packages/server-renderer/src/renderToString.ts

+50-4
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,23 @@ import {
1111
Portal,
1212
ShapeFlags,
1313
ssrUtils,
14-
Slots
14+
Slots,
15+
warn
1516
} from 'vue'
1617
import {
1718
isString,
1819
isPromise,
1920
isArray,
2021
isFunction,
2122
isVoidTag,
22-
escapeHtml
23+
escapeHtml,
24+
NO,
25+
generateCodeFrame
2326
} from '@vue/shared'
27+
import { compile } from '@vue/compiler-ssr'
2428
import { ssrRenderAttrs } from './helpers/ssrRenderAttrs'
2529
import { SSRSlots } from './helpers/ssrRenderSlot'
30+
import { CompilerError } from '@vue/compiler-dom'
2631

2732
const {
2833
isVNode,
@@ -126,6 +131,44 @@ function renderComponentVNode(
126131
}
127132
}
128133

134+
type SSRRenderFunction = (
135+
ctx: any,
136+
push: (item: any) => void,
137+
parentInstance: ComponentInternalInstance
138+
) => void
139+
const compileCache: Record<string, SSRRenderFunction> = Object.create(null)
140+
141+
function ssrCompile(
142+
template: string,
143+
instance: ComponentInternalInstance
144+
): SSRRenderFunction {
145+
const cached = compileCache[template]
146+
if (cached) {
147+
return cached
148+
}
149+
150+
const { code } = compile(template, {
151+
isCustomElement: instance.appContext.config.isCustomElement || NO,
152+
isNativeTag: instance.appContext.config.isNativeTag || NO,
153+
onError(err: CompilerError) {
154+
if (__DEV__) {
155+
const message = `Template compilation error: ${err.message}`
156+
const codeFrame =
157+
err.loc &&
158+
generateCodeFrame(
159+
template as string,
160+
err.loc.start.offset,
161+
err.loc.end.offset
162+
)
163+
warn(codeFrame ? `${message}\n${codeFrame}` : message)
164+
} else {
165+
throw err
166+
}
167+
}
168+
})
169+
return (compileCache[template] = Function(code)())
170+
}
171+
129172
function renderComponentSubTree(
130173
instance: ComponentInternalInstance
131174
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
@@ -134,6 +177,10 @@ function renderComponentSubTree(
134177
if (isFunction(comp)) {
135178
renderVNode(push, renderComponentRoot(instance), instance)
136179
} else {
180+
if (!comp.ssrRender && !comp.render && isString(comp.template)) {
181+
comp.ssrRender = ssrCompile(comp.template, instance)
182+
}
183+
137184
if (comp.ssrRender) {
138185
// optimized
139186
// set current rendering instance for asset resolution
@@ -143,11 +190,10 @@ function renderComponentSubTree(
143190
} else if (comp.render) {
144191
renderVNode(push, renderComponentRoot(instance), instance)
145192
} else {
146-
// TODO on the fly template compilation support
147193
throw new Error(
148194
`Component ${
149195
comp.name ? `${comp.name} ` : ``
150-
} is missing render function.`
196+
} is missing template or render function.`
151197
)
152198
}
153199
}

0 commit comments

Comments
 (0)