Evan You 9 лет назад
Родитель
Сommit
4e40666d53

+ 1 - 0
package.json

@@ -93,6 +93,7 @@
     "karma-sourcemap-loader": "^0.3.0",
     "karma-webpack": "^2.0.1",
     "lodash": "^4.17.1",
+    "lru-cache": "^4.0.2",
     "nightwatch": "^0.9.9",
     "nightwatch-helpers": "^1.2.0",
     "phantomjs-prebuilt": "^2.1.1",

+ 2 - 2
src/server/create-renderer.js

@@ -63,7 +63,7 @@ export function createRenderer ({
         return false
       }, done)
       try {
-        render(component, write, () => {
+        render(component, write, context, () => {
           if (template) {
             result = templateRenderer.renderSync(result, context)
           }
@@ -79,7 +79,7 @@ export function createRenderer ({
       context?: ?Object
     ): stream$Readable {
       const renderStream = new RenderStream((write, done) => {
-        render(component, write, done)
+        render(component, write, context, done)
       })
       if (!template) {
         if (context && clientManifest) {

+ 12 - 3
src/server/render-context.js

@@ -15,10 +15,12 @@ type RenderState = {
   type: 'ComponentWithCache';
   buffer: Array<string>;
   bufferIndex: number;
+  componentBuffer: Array<Set<Class<Component>>>;
   key: string;
 };
 
 export class RenderContext {
+  userContext: ?Object;
   activeInstance: Component;
   renderStates: Array<RenderState>;
   write: (text: string, next: Function) => void;
@@ -35,6 +37,7 @@ export class RenderContext {
   has: ?(key: string, cb: Function) => void;
 
   constructor (options: Object) {
+    this.userContext = options.userContext
     this.activeInstance = options.activeInstance
     this.renderStates = []
 
@@ -80,8 +83,11 @@ export class RenderContext {
         break
       case 'ComponentWithCache':
         this.renderStates.pop()
-        const { buffer, bufferIndex, key } = lastState
-        const result = buffer[bufferIndex]
+        const { buffer, bufferIndex, componentBuffer, key } = lastState
+        const result = {
+          html: buffer[bufferIndex],
+          components: componentBuffer[bufferIndex]
+        }
         this.cache.set(key, result)
         if (bufferIndex === 0) {
           // this is a top-level cached component,
@@ -90,9 +96,12 @@ export class RenderContext {
         } else {
           // parent component is also being cached,
           // merge self into parent's result
-          buffer[bufferIndex - 1] += result
+          buffer[bufferIndex - 1] += result.html
+          const prev = componentBuffer[bufferIndex - 1]
+          result.components.forEach(c => prev.add(c))
         }
         buffer.length = bufferIndex
+        componentBuffer.length = bufferIndex
         this.next()
         break
     }

+ 25 - 8
src/server/render.js

@@ -37,15 +37,20 @@ const normalizeRender = vm => {
 }
 
 function renderNode (node, isRoot, context) {
-  const { write, next } = context
+  const { write, next, userContext } = context
   if (isDef(node.componentOptions)) {
     // check cache hit
     const Ctor = node.componentOptions.Ctor
     const getKey = Ctor.options.serverCacheKey
     const name = Ctor.options.name
+
     // exposed by vue-loader, need to call this if cache hit because
     // component lifecycle hooks will not be called.
-    const injectStyles = Ctor.options._injectStyles
+    const registerComponent = Ctor.options._ssrRegister
+    if (write.caching && isDef(registerComponent)) {
+      write.componentBuffer[write.componentBuffer.length - 1].add(registerComponent)
+    }
+
     const cache = context.cache
     if (isDef(getKey) && isDef(cache) && isDef(name)) {
       const key = name + '::' + getKey(node.componentOptions.propsData)
@@ -54,8 +59,9 @@ function renderNode (node, isRoot, context) {
         (has: any)(key, hit => {
           if (hit === true && isDef(get)) {
             (get: any)(key, res => {
-              injectStyles && injectStyles.call({})
-              write(res, next)
+              registerComponent && registerComponent(userContext)
+              res.components.forEach(register => register(userContext))
+              write(res.html, next)
             })
           } else {
             renderComponentWithCache(node, isRoot, key, context)
@@ -64,8 +70,9 @@ function renderNode (node, isRoot, context) {
       } else if (isDef(get)) {
         (get: any)(key, res => {
           if (isDef(res)) {
-            injectStyles && injectStyles.call({})
-            write(res, next)
+            registerComponent && registerComponent(userContext)
+            res.components.forEach(register => register(userContext))
+            write(res.html, next)
           } else {
             renderComponentWithCache(node, isRoot, key, context)
           }
@@ -101,7 +108,10 @@ function renderNode (node, isRoot, context) {
 
 function renderComponent (node, isRoot, context) {
   const prevActive = context.activeInstance
-  const child = context.activeInstance = createComponentInstanceForVnode(node, context.activeInstance)
+  const child = context.activeInstance = createComponentInstanceForVnode(
+    node,
+    context.activeInstance
+  )
   normalizeRender(child)
   const childNode = child._render()
   childNode.parent = node
@@ -117,9 +127,14 @@ function renderComponentWithCache (node, isRoot, key, context) {
   write.caching = true
   const buffer = write.cacheBuffer
   const bufferIndex = buffer.push('') - 1
+  const componentBuffer = write.componentBuffer
+  componentBuffer.push(new Set())
   context.renderStates.push({
     type: 'ComponentWithCache',
-    buffer, bufferIndex, key
+    key,
+    buffer,
+    bufferIndex,
+    componentBuffer
   })
   renderComponent(node, isRoot, context)
 }
@@ -234,11 +249,13 @@ export function createRenderFunction (
   return function render (
     component: Component,
     write: (text: string, next: Function) => void,
+    userContext: ?Object,
     done: Function
   ) {
     warned = Object.create(null)
     const context = new RenderContext({
       activeInstance: component,
+      userContext,
       write, done, renderNode,
       isUnaryTag, modules, directives,
       cache

+ 1 - 0
src/server/write.js

@@ -28,5 +28,6 @@ export function createWriteFunction (
   }
   cachedWrite.caching = false
   cachedWrite.cacheBuffer = []
+  cachedWrite.componentBuffer = []
   return cachedWrite
 }

+ 57 - 0
test/ssr/fixtures/nested-cache.js

@@ -0,0 +1,57 @@
+import Vue from '../../../dist/vue.runtime.common.js'
+
+function register (id, context) {
+  context = context || __VUE_SSR_CONTEXT__ // eslint-disable-line
+  context.registered.push(id)
+}
+
+const grandchild = {
+  name: 'grandchild',
+  props: ['id'],
+  _ssrRegister: context => {
+    register('grandchild', context)
+  },
+  beforeCreate () {
+    register('grandchild')
+  },
+  serverCacheKey: props => props.id,
+  render (h) {
+    return h('div', '/test')
+  }
+}
+
+const child = {
+  name: 'child',
+  props: ['id'],
+  _ssrRegister: context => {
+    register('child', context)
+  },
+  beforeCreate () {
+    register('child')
+  },
+  serverCacheKey: props => props.id,
+  render (h) {
+    return h(grandchild, { props: { id: this.id }})
+  }
+}
+
+const app = {
+  name: 'app',
+  props: ['id'],
+  _ssrRegister: context => {
+    register('app', context)
+  },
+  beforeCreate () {
+    register('app')
+  },
+  serverCacheKey: props => props.id,
+  render (h) {
+    return h(child, { props: { id: this.id }})
+  }
+}
+
+export default () => {
+  return Promise.resolve(new Vue({
+    render: h => h(app, { props: { id: 1 }})
+  }))
+}

+ 44 - 4
test/ssr/ssr-bundle-render.spec.js

@@ -1,3 +1,4 @@
+import LRU from 'lru-cache'
 import { VueSSRServerPlugin } from 'vue-ssr-webpack-plugin'
 import { compileWithWebpack } from './compile-with-webpack'
 import { createBundleRenderer } from '../../packages/vue-server-renderer'
@@ -105,8 +106,10 @@ describe('SSR: bundle renderer', () => {
         expect(err).toBeNull()
         expect(res).toBe(expected)
         expect(get).toHaveBeenCalledWith(key)
-        expect(set).toHaveBeenCalledWith(key, expected)
-        expect(cache[key]).toBe(expected)
+        const setArgs = set.calls.argsFor(0)
+        expect(setArgs[0]).toBe(key)
+        expect(setArgs[1].html).toBe(expected)
+        expect(cache[key].html).toBe(expected)
         renderer.renderToString((err, res) => {
           expect(err).toBeNull()
           expect(res).toBe(expected)
@@ -149,8 +152,10 @@ describe('SSR: bundle renderer', () => {
         expect(res).toBe(expected)
         expect(has).toHaveBeenCalledWith(key)
         expect(get).not.toHaveBeenCalled()
-        expect(set).toHaveBeenCalledWith(key, expected)
-        expect(cache[key]).toBe(expected)
+        const setArgs = set.calls.argsFor(0)
+        expect(setArgs[0]).toBe(key)
+        expect(setArgs[1].html).toBe(expected)
+        expect(cache[key].html).toBe(expected)
         renderer.renderToString((err, res) => {
           expect(err).toBeNull()
           expect(res).toBe(expected)
@@ -163,6 +168,41 @@ describe('SSR: bundle renderer', () => {
     })
   })
 
+  it('render with cache (nested)', done => {
+    const cache = LRU({ maxAge: Infinity })
+    spyOn(cache, 'get').and.callThrough()
+    spyOn(cache, 'set').and.callThrough()
+    const options = { cache }
+    createRenderer('nested-cache.js', options, renderer => {
+      const expected = '<div data-server-rendered="true">/test</div>'
+      const key = 'app::1'
+      const context1 = { registered: [] }
+      const context2 = { registered: [] }
+      renderer.renderToString(context1, (err, res) => {
+        expect(err).toBeNull()
+        expect(res).toBe(expected)
+        expect(cache.set.calls.count()).toBe(3) // 3 nested components cached
+        const cached = cache.get(key)
+        expect(cached.html).toBe(expected)
+        expect(cache.get.calls.count()).toBe(1)
+
+        // assert component usage registration for nested children
+        expect(context1.registered).toEqual(['app', 'child', 'grandchild'])
+
+        renderer.renderToString(context2, (err, res) => {
+          expect(err).toBeNull()
+          expect(res).toBe(expected)
+          expect(cache.set.calls.count()).toBe(3) // no new cache sets
+          expect(cache.get.calls.count()).toBe(2) // 1 get for root
+
+          console.log(context1)
+          console.log(context2)
+          done()
+        })
+      })
+    })
+  })
+
   it('renderToString (bundle format with code split)', done => {
     createRenderer('split.js', { asBundle: true }, renderer => {
       const context = { url: '/test' }

+ 1 - 1
yarn.lock

@@ -3297,7 +3297,7 @@ lru-cache@2.2.x:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
 
-lru-cache@^4.0.1:
+lru-cache@^4.0.1, lru-cache@^4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e"
   dependencies: