SSR: allow user to provide own (possibly async) cache implementation

This commit is contained in:
Evan You 2016-06-29 16:45:01 -04:00
parent 6cf19291be
commit ba3bec824d
9 changed files with 178 additions and 47 deletions

View File

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

View File

@ -13,8 +13,7 @@ module.exports = {
alias: alias
},
externals: {
'entities': true,
'lru-cache': true
'entities': true
},
module: {
noParse: /run-in-vm/,

View File

@ -74,7 +74,6 @@
"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.33.0",

View File

@ -18,8 +18,7 @@
"url": "https://github.com/vuejs/vue/issues"
},
"dependencies": {
"entities": "^1.1.1",
"lru-cache": "^4.0.1"
"entities": "^1.1.1"
},
"homepage": "https://github.com/vuejs/vue#readme"
}

View File

@ -17,7 +17,7 @@ export function createRenderer (options?: Object = {}): {
isUnaryTag,
modules,
directives,
cache: options.cache || {}
cache: options.cache
})
}

View File

@ -9,12 +9,12 @@ export function createRenderer ({
modules = [],
directives = {},
isUnaryTag = (() => false),
cache = {}
cache
}: {
modules: Array<Function>,
directives: Object,
isUnaryTag: Function,
cache: Object
cache: ?Object
} = {}): {
renderToString: Function,
renderToStream: Function

View File

@ -2,21 +2,33 @@
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
const normalizeAsync = (cache, method) => {
const fn = cache[method]
if (!fn) {
return
} else if (fn.length > 1) {
return (key, cb) => fn.call(cache, key, cb)
} else {
return (key, cb) => cb(fn.call(cache, key))
}
}
export function createRenderFunction (
modules: Array<Function>,
directives: Object,
isUnaryTag: Function,
cacheOptions: Object
cache: any
) {
const cache = LRU(Object.assign({}, defaultOptions, cacheOptions))
if (cache && (!cache.get || !cache.set)) {
throw new Error('renderer cache must implement at least get & set.')
}
const get = cache && normalizeAsync(cache, 'get')
const has = cache && normalizeAsync(cache, 'has')
function renderNode (
node: VNode,
@ -28,16 +40,54 @@ export function createRenderFunction (
// check cache hit
const Ctor = node.componentOptions.Ctor
const getKey = Ctor.options.server && Ctor.options.server.getCacheKey
if (getKey) {
if (getKey && cache) {
const key = Ctor.cid + '::' + getKey(node.componentOptions.propsData)
if (cache.has(key)) {
return write(cache.get(key), next)
if (has) {
has(key, hit => {
if (hit) {
get(key, res => write(res, next))
} else {
renderComponentWithCache(node, write, next, isRoot, cache, key)
}
})
} else {
get(key, res => {
if (res) {
write(res, next)
} else {
renderComponentWithCache(node, write, next, isRoot, cache, key)
}
})
}
} else {
if (getKey) {
console.error(
'Component implemented server.getCacheKey, ' +
'but no cache was provided to the renderer.'
)
}
renderComponent(node, write, next, isRoot)
}
} else {
if (node.tag) {
renderElement(node, write, next, isRoot)
} else {
write(node.raw ? node.text : encodeHTMLCached(node.text), next)
}
}
}
function renderComponent (node, write, next, isRoot) {
const child = createComponentInstanceForVnode(node)._render()
child.parent = node
renderNode(child, write, next, isRoot)
}
function renderComponentWithCache (node, write, next, isRoot, cache, key) {
write.caching = true
const buffer = write.cacheBuffer
const bufferIndex = buffer.push('') - 1
const _next = next
next = () => {
renderComponent(node, write, () => {
const result = buffer[bufferIndex]
cache.set(key, result)
if (bufferIndex === 0) {
@ -50,28 +100,11 @@ export function createRenderFunction (
buffer[bufferIndex - 1] += result
}
buffer.length = bufferIndex
_next()
}
}
}
const child = createComponentInstanceForVnode(node)._render()
child.parent = node
renderNode(child, write, next, isRoot)
} else {
if (node.tag) {
renderElement(node, write, next, isRoot)
} else {
write(node.raw ? node.text : encodeHTMLCached(node.text), next)
}
}
next()
}, isRoot)
}
function renderElement (
el: VNode,
write: Function,
next: Function,
isRoot: boolean
) {
function renderElement (el, write, next, isRoot) {
if (isRoot) {
if (!el.data) el.data = {}
if (!el.data.attrs) el.data.attrs = {}

View File

@ -0,0 +1,17 @@
import Vue from '../../../dist/vue.common.js'
const app = {
props: ['id'],
server: {
getCacheKey: props => props.id
},
render (h) {
return h('div', '/test')
}
}
export default () => {
return Promise.resolve(new Vue({
render: h => h(app, { props: { id: 1 }})
}))
}

View File

@ -4,8 +4,8 @@ import MemoeryFS from 'memory-fs'
import { createBundleRenderer } from '../../packages/vue-server-renderer'
const rendererCache = {}
function createRenderer (file, cb) {
if (rendererCache[file]) {
function createRenderer (file, cb, options) {
if (!options && rendererCache[file]) {
return cb(rendererCache[file])
}
const compiler = webpack({
@ -26,7 +26,7 @@ function createRenderer (file, cb) {
expect(err).toBeFalsy()
expect(stats.errors).toBeFalsy()
const code = fs.readFileSync('/bundle.js', 'utf-8')
const renderer = rendererCache[file] = createBundleRenderer(code)
const renderer = rendererCache[file] = createBundleRenderer(code, options)
cb(renderer)
})
}
@ -78,4 +78,88 @@ describe('SSR: bundle renderer', () => {
})
})
})
it('render with cache (get/set)', done => {
const cache = {}
const get = jasmine.createSpy('get')
const set = jasmine.createSpy('set')
const options = {
cache: {
// async
get: (key, cb) => {
setTimeout(() => {
get(key)
cb(cache[key])
}, 0)
},
set: (key, val) => {
set(key, val)
cache[key] = val
}
}
}
createRenderer('cache.js', renderer => {
const expected = '<div server-rendered="true">&sol;test</div>'
const key = '1::1'
renderer.renderToString((err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(get).toHaveBeenCalledWith(key)
expect(set).toHaveBeenCalledWith(key, expected)
expect(cache[key]).toBe(expected)
renderer.renderToString((err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(get.calls.count()).toBe(2)
expect(set.calls.count()).toBe(1)
done()
})
})
}, options)
})
it('render with cache (get/set/has)', done => {
const cache = {}
const has = jasmine.createSpy('has')
const get = jasmine.createSpy('get')
const set = jasmine.createSpy('set')
const options = {
cache: {
// async
has: (key, cb) => {
has(key)
cb(!!cache[key])
},
// sync
get: key => {
get(key)
return cache[key]
},
set: (key, val) => {
set(key, val)
cache[key] = val
}
}
}
createRenderer('cache.js', renderer => {
const expected = '<div server-rendered="true">&sol;test</div>'
const key = '1::1'
renderer.renderToString((err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(has).toHaveBeenCalledWith(key)
expect(get).not.toHaveBeenCalled()
expect(set).toHaveBeenCalledWith(key, expected)
expect(cache[key]).toBe(expected)
renderer.renderToString((err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(has.calls.count()).toBe(2)
expect(get.calls.count()).toBe(1)
expect(set.calls.count()).toBe(1)
done()
})
})
}, options)
})
})