diff --git a/flow/options.js b/flow/options.js index 60f6298045d..d70876fa50a 100644 --- a/flow/options.js +++ b/flow/options.js @@ -57,7 +57,9 @@ declare type ComponentOptions = { _propKeys?: Array; _parentVnode?: VNode; _parentListeners?: ?Object; - _renderChildren?: ?VNodeChildren + _renderChildren?: ?VNodeChildren; + _componentTag: ?string; + _scopeId: ?string; } declare type PropOptions = { diff --git a/flow/ssr.js b/flow/ssr.js new file mode 100644 index 00000000000..a3d5379cb00 --- /dev/null +++ b/flow/ssr.js @@ -0,0 +1,21 @@ +declare type ComponentWithCacheContext = { + type: 'ComponentWithCache'; + bufferIndex: number; + buffer: Array; + key: string; +} + +declare type ElementContext = { + type: 'Element'; + children: Array; + rendered: number; + endTag: string; + total: number; +} + +declare type ComponentContext = { + type: 'Component'; + prevActive: Component; +} + +declare type RenderState = ComponentContext | ComponentWithCacheContext | ElementContext diff --git a/package.json b/package.json index 794bcaa4b03..3238eebe8c2 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "eslint-loader": "^1.3.0", "eslint-plugin-flowtype": "^2.16.0", "eslint-plugin-html": "^1.5.2", - "flow-bin": "^0.32.0", + "flow-bin": "^0.33.0", "he": "^1.1.0", "http-server": "^0.9.0", "jasmine": "2.4.x", diff --git a/src/compiler/codegen/index.js b/src/compiler/codegen/index.js index fb8f9ef6ac0..d2572007d41 100644 --- a/src/compiler/codegen/index.js +++ b/src/compiler/codegen/index.js @@ -53,9 +53,10 @@ function genElement (el: ASTElement): string { // component or element let code if (el.component) { - code = genComponent(el) + code = genComponent(el.component, el) } else { - const data = genData(el) + const data = el.plain ? undefined : genData(el) + const children = el.inlineTemplate ? null : genChildren(el) code = `_h('${el.tag}'${ data ? `,${data}` : '' // data @@ -95,11 +96,7 @@ function genFor (el: any): string { '})' } -function genData (el: ASTElement): string | void { - if (el.plain) { - return - } - +function genData (el: ASTElement): string { let data = '{' // directives first. @@ -229,9 +226,10 @@ function genSlot (el: ASTElement): string { : `_t(${slotName})` } -function genComponent (el: any): string { +// componentName is el.component, take it as argument to shun flow's pessimistic refinement +function genComponent (componentName, el): string { const children = el.inlineTemplate ? null : genChildren(el) - return `_h(${el.component},${genData(el)}${ + return `_h(${componentName},${genData(el)}${ children ? `,${children}` : '' })` } diff --git a/src/core/util/props.js b/src/core/util/props.js index 5358ce2d1ab..bebd5cf8f62 100644 --- a/src/core/util/props.js +++ b/src/core/util/props.js @@ -126,7 +126,7 @@ function assertProp ( */ function assertType (value: any, type: Function): { valid: boolean, - expectedType: string + expectedType: ?string } { let valid let expectedType = getType(type) diff --git a/src/server/render-stream.js b/src/server/render-stream.js index 939c36a58f0..7488dd1d37f 100644 --- a/src/server/render-stream.js +++ b/src/server/render-stream.js @@ -13,9 +13,8 @@ import { createWriteFunction } from './write' export default class RenderStream extends stream.Readable { buffer: string; - render: Function; + render: (write: Function, done: Function) => void; expectedSize: number; - stackDepth: number; write: Function; next: Function; end: Function; @@ -26,7 +25,6 @@ export default class RenderStream extends stream.Readable { this.buffer = '' this.render = render this.expectedSize = 0 - this.stackDepth = 0 this.write = createWriteFunction((text, next) => { const n = this.expectedSize diff --git a/src/server/render.js b/src/server/render.js index 8b755da0743..0aec0bf8fea 100644 --- a/src/server/render.js +++ b/src/server/render.js @@ -3,6 +3,7 @@ import { escape } from 'he' import { compileToFunctions } from 'web/compiler/index' import { createComponentInstanceForVnode } from 'core/vdom/create-component' +import { noop } from 'shared/util' let warned = Object.create(null) const warnOnce = msg => { @@ -43,103 +44,184 @@ const normalizeRender = vm => { } } -export function createRenderFunction ( - modules: Array, - directives: Object, - isUnaryTag: Function, - cache: any -) { - if (cache && (!cache.get || !cache.set)) { - throw new Error('renderer cache must implement at least get & set.') +function renderNode (node, isRoot, context) { + const { write, next } = context + if (node.componentOptions) { + // check cache hit + const Ctor = node.componentOptions.Ctor + const getKey = Ctor.options.serverCacheKey + const name = Ctor.options.name + const cache = context.cache + if (getKey && cache && name) { + const key = name + '::' + getKey(node.componentOptions.propsData) + const { has, get } = context + if (has) { + has(key, hit => { + if (hit && get) { + get(key, res => write(res, next)) + } else { + renderComponentWithCache(node, isRoot, key, context) + } + }) + } else if (get) { + get(key, res => { + if (res) { + write(res, next) + } else { + renderComponentWithCache(node, isRoot, key, context) + } + }) + } + } else { + if (getKey && !cache) { + warnOnce( + `[vue-server-renderer] Component ${ + Ctor.options.name || '(anonymous)' + } implemented serverCacheKey, ` + + 'but no cache was provided to the renderer.' + ) + } + if (getKey && !name) { + warnOnce( + `[vue-server-renderer] Components that implement "serverCacheKey" ` + + `must also define a unique "name" option.` + ) + } + renderComponent(node, isRoot, context) + } + } else { + if (node.tag) { + renderElement(node, isRoot, context) + } else if (node.isComment) { + write(``, next) + } else { + write(node.raw ? node.text : escape(String(node.text)), next) + } } +} - const get = cache && normalizeAsync(cache, 'get') - const has = cache && normalizeAsync(cache, 'has') +function renderComponent (node, isRoot, context) { + const prevActive = context.activeInstance + const child = context.activeInstance = createComponentInstanceForVnode(node, context.activeInstance) + normalizeRender(child) + const childNode = child._render() + childNode.parent = node + context.renderStates.push({ + type: 'Component', + prevActive + }) + renderNode(childNode, isRoot, context) +} - // used to track and apply scope ids - let activeInstance: any +function renderComponentWithCache (node, isRoot, key, context) { + const write = context.write + write.caching = true + const buffer = write.cacheBuffer + const bufferIndex = buffer.push('') - 1 + context.renderStates.push({ + type: 'ComponentWithCache', + buffer, bufferIndex, key + }) + renderComponent(node, isRoot, context) +} - function renderNode ( - node: VNode, - write: Function, - next: Function, - isRoot: boolean - ) { - if (node.componentOptions) { - // check cache hit - const Ctor = node.componentOptions.Ctor - const getKey = Ctor.options.serverCacheKey - const name = Ctor.options.name - if (getKey && cache && name) { - const key = name + '::' + getKey(node.componentOptions.propsData) - if (has) { - has(key, hit => { - if (hit && get) { - get(key, res => write(res, next)) - } else { - renderComponentWithCache(node, write, next, isRoot, cache, key) - } - }) - } else if (get) { - get(key, res => { - if (res) { - write(res, next) - } else { - renderComponentWithCache(node, write, next, isRoot, cache, key) - } - }) - } - } else { - if (getKey && !cache) { - warnOnce( - `[vue-server-renderer] Component ${ - Ctor.options.name || '(anonymous)' - } implemented serverCacheKey, ` + - 'but no cache was provided to the renderer.' - ) - } - if (getKey && !name) { - warnOnce( - `[vue-server-renderer] Components that implement "serverCacheKey" ` + - `must also define a unique "name" option.` - ) +function renderElement (el, isRoot, context) { + if (isRoot) { + if (!el.data) el.data = {} + if (!el.data.attrs) el.data.attrs = {} + el.data.attrs['server-rendered'] = 'true' + } + const startTag = renderStartingTag(el, context) + const endTag = `` + const { write, next } = context + if (context.isUnaryTag(el.tag)) { + write(startTag, next) + } else if (!el.children || !el.children.length) { + write(startTag + endTag, next) + } else { + const children: Array = el.children + context.renderStates.push({ + type: 'Element', + rendered: 0, + total: children.length, + endTag, children + }) + write(startTag, next) + } +} + +function renderStartingTag (node: VNode, context) { + let markup = `<${node.tag}` + const { directives, modules } = context + if (node.data) { + // check directives + const dirs = node.data.directives + if (dirs) { + for (let i = 0; i < dirs.length; i++) { + const dirRenderer = directives[dirs[i].name] + if (dirRenderer) { + // directives mutate the node's data + // which then gets rendered by modules + dirRenderer(node, dirs[i]) } - renderComponent(node, write, next, isRoot) } - } else { - if (node.tag) { - renderElement(node, write, next, isRoot) - } else if (node.isComment) { - write(``, next) - } else { - write(node.raw ? node.text : escape(String(node.text)), next) + } + // apply other modules + for (let i = 0; i < modules.length; i++) { + const res = modules[i](node) + if (res) { + markup += res } } } - - function renderComponent (node, write, next, isRoot) { - const prevActive = activeInstance - const child = activeInstance = createComponentInstanceForVnode(node, activeInstance) - normalizeRender(child) - const childNode = child._render() - childNode.parent = node - renderNode(childNode, write, () => { - activeInstance = prevActive - next() - }, isRoot) + // attach scoped CSS ID + let scopeId + const activeInstance = context.activeInstance + if (activeInstance && + activeInstance !== node.context && + (scopeId = activeInstance.$options._scopeId)) { + markup += ` ${scopeId}` } + while (node) { + if ((scopeId = node.context.$options._scopeId)) { + markup += ` ${scopeId}` + } + node = node.parent + } + return markup + '>' +} - function renderComponentWithCache (node, write, next, isRoot, cache, key) { - write.caching = true - const buffer = write.cacheBuffer - const bufferIndex = buffer.push('') - 1 - renderComponent(node, write, () => { +const nextFactory = context => function next () { + const lastState = context.renderStates.pop() + if (!lastState) { + context.done() + // cleanup context, avoid leakage + context = (null: any) + return + } + switch (lastState.type) { + case 'Component': + context.activeInstance = lastState.prevActive + next() + break + case 'Element': + const { children, total } = lastState + const rendered = lastState.rendered++ + if (rendered < total) { + context.renderStates.push(lastState) + renderNode(children[rendered], false, context) + } else { + context.write(lastState.endTag, next) + } + break + case 'ComponentWithCache': + const { buffer, bufferIndex, key } = lastState const result = buffer[bufferIndex] - cache.set(key, result) + context.cache.set(key, result) if (bufferIndex === 0) { // this is a top-level cached component, // exit caching mode. - write.caching = false + context.write.caching = false } else { // parent component is also being cached, // merge self into parent's result @@ -147,81 +229,22 @@ export function createRenderFunction ( } buffer.length = bufferIndex next() - }, isRoot) + break } +} - function renderElement (el, write, next, isRoot) { - if (isRoot) { - if (!el.data) el.data = {} - if (!el.data.attrs) el.data.attrs = {} - el.data.attrs['server-rendered'] = 'true' - } - const startTag = renderStartingTag(el) - const endTag = `` - if (isUnaryTag(el.tag)) { - write(startTag, next) - } else if (!el.children || !el.children.length) { - write(startTag + endTag, next) - } else { - const children: Array = el.children || [] - write(startTag, () => { - const total = children.length - let rendered = 0 - - function renderChild (child: VNode) { - renderNode(child, write, () => { - rendered++ - if (rendered < total) { - renderChild(children[rendered]) - } else { - write(endTag, next) - } - }, false) - } - - renderChild(children[0]) - }) - } +export function createRenderFunction ( + modules: Array, + directives: Object, + isUnaryTag: Function, + cache: any +) { + if (cache && (!cache.get || !cache.set)) { + throw new Error('renderer cache must implement at least get & set.') } - function renderStartingTag (node: VNode) { - let markup = `<${node.tag}` - if (node.data) { - // check directives - const dirs = node.data.directives - if (dirs) { - for (let i = 0; i < dirs.length; i++) { - const dirRenderer = directives[dirs[i].name] - if (dirRenderer) { - // directives mutate the node's data - // which then gets rendered by modules - dirRenderer(node, dirs[i]) - } - } - } - // apply other modules - for (let i = 0; i < modules.length; i++) { - const res = modules[i](node) - if (res) { - markup += res - } - } - } - // attach scoped CSS ID - let scopeId - if (activeInstance && - activeInstance !== node.context && - (scopeId = activeInstance.$options._scopeId)) { - markup += ` ${scopeId}` - } - while (node) { - if ((scopeId = node.context.$options._scopeId)) { - markup += ` ${scopeId}` - } - node = node.parent - } - return markup + '>' - } + const get = cache && normalizeAsync(cache, 'get') + const has = cache && normalizeAsync(cache, 'has') return function render ( component: Component, @@ -229,8 +252,16 @@ export function createRenderFunction ( done: Function ) { warned = Object.create(null) - activeInstance = component + const context = { + activeInstance: component, + renderStates: [], + next: noop, // for flow + write, done, + isUnaryTag, modules, directives, + cache, get, has + } + context.next = nextFactory(context) normalizeRender(component) - renderNode(component._render(), write, done, true) + renderNode(component._render(), true, context) } } diff --git a/src/server/write.js b/src/server/write.js index 08721944806..90c53fc10ec 100644 --- a/src/server/write.js +++ b/src/server/write.js @@ -3,7 +3,7 @@ const MAX_STACK_DEPTH = 1000 export function createWriteFunction ( - write: Function, + write: (text: string, next: Function) => ?boolean, onError: Function ): Function { let stackDepth = 0 diff --git a/test/ssr/ssr-stream.spec.js b/test/ssr/ssr-stream.spec.js index 9717b540e51..26b07bcde64 100644 --- a/test/ssr/ssr-stream.spec.js +++ b/test/ssr/ssr-stream.spec.js @@ -76,4 +76,28 @@ describe('SSR: renderToStream', () => { }) stream.on('data', _ => _) }) + + it('should not mingle two components', done => { + const padding = (new Array(20000)).join('x') + const component1 = new Vue({ + template: `
${padding}
`, + _scopeId: '_component1' + }) + const component2 = new Vue({ + template: `
`, + _scopeId: '_component2' + }) + var stream1 = renderToStream(component1) + var stream2 = renderToStream(component2) + var res = '' + stream1.on('data', (text) => { + res += text.toString('utf-8').replace(/x/g, '') + }) + stream1.on('end', () => { + expect(res).not.toContain('_component2') + done() + }) + stream1.read(1) + stream2.read(1) + }) })