diff --git a/build/build.js b/build/build.js index 954707a0..4959ad6d 100644 --- a/build/build.js +++ b/build/build.js @@ -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' } ] diff --git a/build/webpack.ssr.dev.config.js b/build/webpack.ssr.dev.config.js index 17bb35bc..a61b41e4 100644 --- a/build/webpack.ssr.dev.config.js +++ b/build/webpack.ssr.dev.config.js @@ -13,8 +13,7 @@ module.exports = { alias: alias }, externals: { - 'entities': true, - 'lru-cache': true + 'entities': true }, module: { noParse: /run-in-vm/, diff --git a/package.json b/package.json index c1199942..6f15142b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/vue-server-renderer/package.json b/packages/vue-server-renderer/package.json index 13209cc5..be59ece1 100644 --- a/packages/vue-server-renderer/package.json +++ b/packages/vue-server-renderer/package.json @@ -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" } diff --git a/src/entries/web-server-renderer.js b/src/entries/web-server-renderer.js index e81cd187..88878e75 100644 --- a/src/entries/web-server-renderer.js +++ b/src/entries/web-server-renderer.js @@ -17,7 +17,7 @@ export function createRenderer (options?: Object = {}): { isUnaryTag, modules, directives, - cache: options.cache || {} + cache: options.cache }) } diff --git a/src/server/create-renderer.js b/src/server/create-renderer.js index 369c4751..7a7e2c8e 100644 --- a/src/server/create-renderer.js +++ b/src/server/create-renderer.js @@ -9,12 +9,12 @@ export function createRenderer ({ modules = [], directives = {}, isUnaryTag = (() => false), - cache = {} + cache }: { modules: Array, directives: Object, isUnaryTag: Function, - cache: Object + cache: ?Object } = {}): { renderToString: Function, renderToStream: Function diff --git a/src/server/render.js b/src/server/render.js index 72c30b8b..4b106551 100644 --- a/src/server/render.js +++ b/src/server/render.js @@ -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, 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,35 +40,34 @@ 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) - } else { - write.caching = true - const buffer = write.cacheBuffer - const bufferIndex = buffer.push('') - 1 - const _next = next - next = () => { - const result = buffer[bufferIndex] - cache.set(key, result) - if (bufferIndex === 0) { - // this is a top-level cached component, - // exit caching mode. - write.caching = false + if (has) { + has(key, hit => { + if (hit) { + get(key, res => write(res, next)) } else { - // parent component is also being cached, - // merge self into parent's result - buffer[bufferIndex - 1] += result + renderComponentWithCache(node, write, next, isRoot, cache, key) } - buffer.length = bufferIndex - _next() - } + }) + } 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) } - const child = createComponentInstanceForVnode(node)._render() - child.parent = node - renderNode(child, write, next, isRoot) } else { if (node.tag) { renderElement(node, write, next, isRoot) @@ -66,12 +77,34 @@ export function createRenderFunction ( } } - function renderElement ( - el: VNode, - write: Function, - next: Function, - isRoot: boolean - ) { + 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 + renderComponent(node, write, () => { + const result = buffer[bufferIndex] + cache.set(key, result) + if (bufferIndex === 0) { + // this is a top-level cached component, + // exit caching mode. + write.caching = false + } else { + // parent component is also being cached, + // merge self into parent's result + buffer[bufferIndex - 1] += result + } + buffer.length = bufferIndex + next() + }, isRoot) + } + + function renderElement (el, write, next, isRoot) { if (isRoot) { if (!el.data) el.data = {} if (!el.data.attrs) el.data.attrs = {} diff --git a/test/ssr/fixtures/cache.js b/test/ssr/fixtures/cache.js new file mode 100644 index 00000000..910b84a7 --- /dev/null +++ b/test/ssr/fixtures/cache.js @@ -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 }}) + })) +} diff --git a/test/ssr/ssr-bundle-render.spec.js b/test/ssr/ssr-bundle-render.spec.js index 735dc7e1..175ccd96 100644 --- a/test/ssr/ssr-bundle-render.spec.js +++ b/test/ssr/ssr-bundle-render.spec.js @@ -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 = '
/test
' + 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 = '
/test
' + 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) + }) })