]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(server-renderer): support on-the-fly template compilation (#707)
authorDmitry Sharshakov <d3dx12.xx@gmail.com>
Mon, 10 Feb 2020 19:37:35 +0000 (22:37 +0300)
committerGitHub <noreply@github.com>
Mon, 10 Feb 2020 19:37:35 +0000 (14:37 -0500)
packages/server-renderer/__tests__/renderToString.spec.ts
packages/server-renderer/package.json
packages/server-renderer/src/renderToString.ts

index c7a7cfb980c45b4c76207bd7acdb392b6c95a09d..051f59b4f030a63b32c632a3e951ccc61b8a0b53 100644 (file)
@@ -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,6 +58,31 @@ describe('ssr: renderToString', () => {
       ).toBe(`<div>hello</div>`)
     })
 
+    describe('template components', () => {
+      test('render', async () => {
+        expect(
+          await renderToString(
+            createApp({
+              data() {
+                return { msg: 'hello' }
+              },
+              template: `<div>{{ msg }}</div>`
+            })
+          )
+        ).toBe(`<div>hello</div>`)
+      })
+
+      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 () => {
       const Child = {
         props: ['msg'],
@@ -96,7 +123,22 @@ describe('ssr: renderToString', () => {
       ).toBe(`<div>parent<div>hello</div></div>`)
     })
 
-    test('mixing optimized / vnode components', async () => {
+    test('nested template components', async () => {
+      const Child = {
+        props: ['msg'],
+        template: `<div>{{ msg }}</div>`
+      }
+      const app = createApp({
+        template: `<div>parent<Child msg="hello" /></div>`
+      })
+      app.component('Child', Child)
+
+      expect(await renderToString(app)).toBe(
+        `<div>parent<div>hello</div></div>`
+      )
+    })
+
+    test('mixing optimized / vnode / template components', async () => {
       const OptimizedChild = {
         props: ['msg'],
         ssrRender(ctx: any, push: any) {
@@ -111,6 +153,11 @@ describe('ssr: renderToString', () => {
         }
       }
 
+      const TemplateChild = {
+        props: ['msg'],
+        template: `<div>{{ msg }}</div>`
+      }
+
       expect(
         await renderToString(
           createApp({
@@ -120,11 +167,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(`</div>`)
             }
           })
         )
-      ).toBe(`<div>parent<div>opt</div><div>vnode</div></div>`)
+      ).toBe(
+        `<div>parent<div>opt</div><div>vnode</div><div>template</div></div>`
+      )
     })
 
     test('nested components with optimized slots', async () => {
@@ -236,6 +293,50 @@ describe('ssr: renderToString', () => {
       )
     })
 
+    test('nested components with template slots', async () => {
+      const Child = {
+        props: ['msg'],
+        template: `<div class="child"><slot msg="from slot"></slot></div>`
+      }
+
+      const app = createApp({
+        template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
+      })
+      app.component('Child', Child)
+
+      expect(await renderToString(app)).toBe(
+        `<div>parent<div class="child">` +
+          `<!----><span>from slot</span><!---->` +
+          `</div></div>`
+      )
+    })
+
+    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' })
+          )
+        }
+      }
+
+      const app = createApp({
+        template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
+      })
+      app.component('Child', Child)
+
+      expect(await renderToString(app)).toBe(
+        `<div>parent<div class="child">` +
+          `<span>from slot</span>` +
+          `</div></div>`
+      )
+    })
+
     test('async components', async () => {
       const Child = {
         // should wait for resovled render context from setup()
index b8b47c549fd1a8eee20144d2e1a4c26b64568994..2d30373e0b4c4e89201e8cbe8431445d541b3984 100644 (file)
@@ -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"
   }
 }
index 1b0eff4cc3fdb1df8676f6f3e63b1aa38d8ce517..074d98b0423f2de314ea21f9968c00798e34ac5b 100644 (file)
@@ -11,7 +11,8 @@ import {
   Portal,
   ShapeFlags,
   ssrUtils,
-  Slots
+  Slots,
+  warn
 } from 'vue'
 import {
   isString,
@@ -19,10 +20,14 @@ import {
   isArray,
   isFunction,
   isVoidTag,
-  escapeHtml
+  escapeHtml,
+  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,
@@ -126,6 +131,44 @@ function renderComponentVNode(
   }
 }
 
+type SSRRenderFunction = (
+  ctx: any,
+  push: (item: any) => void,
+  parentInstance: ComponentInternalInstance
+) => void
+const compileCache: Record<string, SSRRenderFunction> = 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<ResolvedSSRBuffer> {
@@ -134,6 +177,10 @@ function renderComponentSubTree(
   if (isFunction(comp)) {
     renderVNode(push, renderComponentRoot(instance), instance)
   } else {
+    if (!comp.ssrRender && !comp.render && isString(comp.template)) {
+      comp.ssrRender = ssrCompile(comp.template, instance)
+    }
+
     if (comp.ssrRender) {
       // optimized
       // set current rendering instance for asset resolution
@@ -143,11 +190,10 @@ function renderComponentSubTree(
     } else if (comp.render) {
       renderVNode(push, renderComponentRoot(instance), instance)
     } else {
-      // TODO on the fly template compilation support
       throw new Error(
         `Component ${
           comp.name ? `${comp.name} ` : ``
-        } is missing render function.`
+        } is missing template or render function.`
       )
     }
   }