mirror of
https://gitee.com/vuejs/vue.git
synced 2024-12-02 20:17:52 +08:00
SSR: allow user to provide own (possibly async) cache implementation
This commit is contained in:
parent
6cf19291be
commit
ba3bec824d
@ -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'
|
||||
}
|
||||
]
|
||||
|
@ -13,8 +13,7 @@ module.exports = {
|
||||
alias: alias
|
||||
},
|
||||
externals: {
|
||||
'entities': true,
|
||||
'lru-cache': true
|
||||
'entities': true
|
||||
},
|
||||
module: {
|
||||
noParse: /run-in-vm/,
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export function createRenderer (options?: Object = {}): {
|
||||
isUnaryTag,
|
||||
modules,
|
||||
directives,
|
||||
cache: options.cache || {}
|
||||
cache: options.cache
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 = {}
|
||||
|
17
test/ssr/fixtures/cache.js
Normal file
17
test/ssr/fixtures/cache.js
Normal 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 }})
|
||||
}))
|
||||
}
|
@ -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">/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">/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)
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user