Forráskód Böngészése

ssr component-level caching + context injection

Evan You 9 éve
szülő
commit
c902e1f9ab

+ 1 - 1
build/build.js

@@ -72,7 +72,7 @@ var builds = [
   {
     entry: 'src/entries/web-server-renderer.js',
     format: 'cjs',
-    external: ['stream', 'entities'],
+    external: ['stream', 'entities', 'lru-cache'],
     out: 'packages/vue-server-renderer/create-renderer.js'
   }
 ]

+ 2 - 1
build/webpack.ssr.dev.config.js

@@ -13,7 +13,8 @@ module.exports = {
     alias: alias
   },
   externals: {
-    'entities': true
+    'entities': true,
+    'lru-cache': true
   },
   module: {
     loaders: [

+ 6 - 0
flow/modules.js

@@ -10,3 +10,9 @@ declare module 'source-map' {
     toString(): string;
   }
 }
+
+declare module 'lru-cache' {
+  declare var exports: {
+    (): any
+  }
+}

+ 1 - 0
package.json

@@ -73,6 +73,7 @@
     "karma-sourcemap-loader": "^0.3.0",
     "karma-webpack": "^1.7.0",
     "lodash": "^4.13.1",
+    "lru-cache": "^4.0.1",
     "nightwatch": "^0.9.0",
     "phantomjs-prebuilt": "^2.1.1",
     "rollup": "^0.26.3",

+ 25 - 14
packages/vue-server-renderer/index.js

@@ -1,28 +1,39 @@
-var createRenderer = require('./create-renderer')
-var Module = require('./module')
+'use strict'
 
-function runAsNewModule (code) {
-  var path = '__app__'
-  var m = new Module(path, null, true /* isBundle */)
+const createRenderer = require('./create-renderer')
+const Module = require('./module')
+const stream = require('stream')
+
+function runAsNewModule (code, context) {
+  const path = '__app__'
+  const m = new Module(path, null, context)
   m.load(path)
   m._compile(code, path)
-  return Object.prototype.hasOwnProperty.call(m.exports, 'default')
+  const res = Object.prototype.hasOwnProperty.call(m.exports, 'default')
     ? m.exports.default
     : m.exports
+  if (typeof res.then !== 'function') {
+    throw new Error('SSR bundle should export a Promise.')
+  }
+  return res
 }
 
 exports.createRenderer = createRenderer
 
-exports.createBundleRenderer = function (code, options) {
-  var renderer = createRenderer(options)
+exports.createBundleRenderer = function (code, rendererOptions) {
+  const renderer = createRenderer(rendererOptions)
   return {
-    renderToString: function (cb) {
-      var app = runAsNewModule(code)
-      renderer.renderToString(app, cb)
+    renderToString: (context, cb) => {
+      runAsNewModule(code, context).then(app => {
+        renderer.renderToString(app, cb)
+      })
     },
-    renderToStream: function () {
-      var app = runAsNewModule(code)
-      return renderer.renderToStream(app)
+    renderToStream: (context) => {
+      const res = new stream.PassThrough()
+      runAsNewModule(code, context).then(app => {
+        renderer.renderToStream(app).pipe(res)
+      })
+      return res
     }
   }
 }

+ 27 - 30
packages/vue-server-renderer/module.js

@@ -1,21 +1,22 @@
 // thanks to airbnb/hypernova
+'use strict'
 
-var NativeModule = require('module')
-var path = require('path')
-var assert = require('assert')
-var vm = require('vm')
+const NativeModule = require('module')
+const path = require('path')
+const assert = require('assert')
+const vm = require('vm')
 
-var NativeModules = process.binding('natives')
+const NativeModules = process.binding('natives')
 
-var moduleExtensions = Object.assign({}, NativeModule._extensions)
+const moduleExtensions = Object.assign({}, NativeModule._extensions)
 
 function isNativeModule (id) {
   return Object.prototype.hasOwnProperty.call(NativeModules, id)
 }
 
 // Creates a sandbox so we don't share globals across different runs.
-function createContext () {
-  var sandbox = {
+function createContext (context) {
+  const sandbox = {
     Buffer,
     clearImmediate,
     clearInterval,
@@ -24,22 +25,22 @@ function createContext () {
     setInterval,
     setTimeout,
     console,
-    process
+    process,
+    __VUE_SSR_CONTEXT__: context || {}
   }
   sandbox.global = sandbox
   return sandbox
 }
 
-function Module (id, parent, isBundle) {
-  var cache = parent ? parent.cache : null
+function Module (id, parent, context) {
+  const cache = parent ? parent.cache : null
   this.id = id
   this.exports = {}
   this.cache = cache || {}
   this.parent = parent
   this.filename = null
   this.loaded = false
-  this.context = parent ? parent.context : createContext()
-  this.isBundle = isBundle
+  this.context = parent ? parent.context : createContext(context)
 }
 
 Module.prototype.load = function (filename) {
@@ -49,8 +50,8 @@ Module.prototype.load = function (filename) {
 }
 
 Module.prototype.run = function (filename) {
-  var ext = path.extname(filename)
-  var extension = moduleExtensions[ext] ? ext : '.js'
+  const ext = path.extname(filename)
+  const extension = moduleExtensions[ext] ? ext : '.js'
   moduleExtensions[extension](this, filename)
   this.loaded = true
 }
@@ -61,32 +62,28 @@ Module.prototype.require = function (filePath) {
 }
 
 Module.prototype._compile = function (content, filename) {
-  var self = this
-
-  function r (filePath) {
-    return self.require(filePath)
-  }
+  const r = filePath => this.require(filePath)
   r.resolve = request => NativeModule._resolveFilename(request, this)
   r.main = process.mainModule
   r.extensions = moduleExtensions
   r.cache = this.cache
 
-  var dirname = path.dirname(filename)
+  const dirname = path.dirname(filename)
 
   // create wrapper function
-  var wrapper = NativeModule.wrap(content)
+  const wrapper = NativeModule.wrap(content)
 
-  var options = {
+  const options = {
     filename,
     displayErrors: true
   }
 
-  var compiledWrapper = vm.runInNewContext(wrapper, this.context, options)
+  const compiledWrapper = vm.runInNewContext(wrapper, this.context, options)
   return compiledWrapper.call(this.exports, this.exports, r, this, filename, dirname)
 }
 
 Module.load = function (id, filename) {
-  var m = new Module(id)
+  const m = new Module(id)
   filename = filename || id
   m.load(filename)
   m.run(filename)
@@ -94,22 +91,22 @@ Module.load = function (id, filename) {
 }
 
 Module.loadFile = function (file, parent) {
-  var filename = NativeModule._resolveFilename(file, parent)
+  const filename = NativeModule._resolveFilename(file, parent)
 
   if (parent) {
-    var cachedModule = parent.cache[filename]
+    const cachedModule = parent.cache[filename]
     if (cachedModule) return cachedModule.exports
   }
 
-  if (parent.isBundle || isNativeModule(filename)) {
+  if (isNativeModule(filename)) {
     return require(filename)
   }
 
-  var m = new Module(filename, parent)
+  const m = new Module(filename, parent)
 
   m.cache[filename] = m
 
-  var hadException = true
+  let hadException = true
 
   try {
     m.load(filename)

+ 2 - 1
src/entries/web-server-renderer.js

@@ -15,6 +15,7 @@ export default function publicCreateRenderer (options?: Object = {}): {
   return createRenderer({
     isUnaryTag,
     modules,
-    directives
+    directives,
+    cache: options.cache || {}
   })
 }

+ 5 - 3
src/server/create-renderer.js

@@ -9,11 +9,13 @@ export const MAX_STACK_DEPTH = 1000
 export function createRenderer ({
   modules = [],
   directives = {},
-  isUnaryTag = (() => false)
+  isUnaryTag = (() => false),
+  cache = {}
 }: {
   modules: Array<Function>,
   directives: Object,
-  isUnaryTag: Function
+  isUnaryTag: Function,
+  cache: Object
 } = {}): {
   renderToString: Function,
   renderToStream: Function
@@ -25,7 +27,7 @@ export function createRenderer ({
       'by turning data observation off.'
     )
   }
-  const render = createRenderFunction(modules, directives, isUnaryTag)
+  const render = createRenderFunction(modules, directives, isUnaryTag, cache)
 
   return {
     renderToString (

+ 34 - 6
src/server/render.js

@@ -2,15 +2,22 @@
 
 import { cached } from 'shared/util'
 import { encodeHTML } from 'entities'
+import LRU from 'lru-cache'
 import { createComponentInstanceForVnode } from 'core/vdom/create-component'
 
 const encodeHTMLCached = cached(encodeHTML)
+const defaultOptions = {
+  max: 5000
+}
 
 export function createRenderFunction (
   modules: Array<Function>,
   directives: Object,
-  isUnaryTag: Function
+  isUnaryTag: Function,
+  cacheOptions: Object
 ) {
+  const cache = LRU(Object.assign({}, defaultOptions, cacheOptions))
+
   function renderNode (
     node: VNode,
     write: Function,
@@ -18,7 +25,9 @@ export function createRenderFunction (
     isRoot: boolean
   ) {
     if (node.componentOptions) {
-      const child = createComponentInstanceForVnode(node)._render()
+      const child =
+        getCachedComponent(node) ||
+        createComponentInstanceForVnode(node)._render()
       child.parent = node
       renderNode(child, write, next, isRoot)
     } else {
@@ -30,6 +39,21 @@ export function createRenderFunction (
     }
   }
 
+  function getCachedComponent (node) {
+    const Ctor = node.componentOptions.Ctor
+    const getKey = Ctor.options.server && Ctor.options.server.getCacheKey
+    if (getKey) {
+      const key = Ctor.cid + '::' + getKey(node.componentOptions.propsData)
+      if (cache.has(key)) {
+        return cache.get(key)
+      } else {
+        const res = createComponentInstanceForVnode(node)._render()
+        cache.set(key, res)
+        return res
+      }
+    }
+  }
+
   function renderElement (
     el: VNode,
     write: Function,
@@ -70,6 +94,9 @@ export function createRenderFunction (
   }
 
   function renderStartingTag (node: VNode) {
+    if (node._rendered) {
+      return node._rendered
+    }
     let markup = `<${node.tag}`
     if (node.data) {
       // check directives
@@ -97,13 +124,14 @@ export function createRenderFunction (
     if (node.host && (scopeId = node.host.$options._scopeId)) {
       markup += ` ${scopeId}`
     }
-    while (node) {
-      if ((scopeId = node.context.$options._scopeId)) {
+    let _node = node
+    while (_node) {
+      if ((scopeId = _node.context.$options._scopeId)) {
         markup += ` ${scopeId}`
       }
-      node = node.parent
+      _node = _node.parent
     }
-    return markup + '>'
+    return (node._rendered = markup + '>')
   }
 
   return function render (