From 561447d278da26e95c488ea75856823557b66c5e Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 25 Jan 2017 12:14:33 -0500 Subject: [PATCH] feat: support multi-chunk bundles in ssr bundle renderer --- src/server/create-bundle-renderer.js | 43 +++++++++++++++++++++---- src/server/create-renderer.js | 22 +++++++------ src/server/render-stream.js | 3 +- src/server/run-in-vm.js | 47 ++++++++++++++++++++-------- 4 files changed, 86 insertions(+), 29 deletions(-) diff --git a/src/server/create-bundle-renderer.js b/src/server/create-bundle-renderer.js index 94629a51d1..6cd1cd7311 100644 --- a/src/server/create-bundle-renderer.js +++ b/src/server/create-bundle-renderer.js @@ -1,22 +1,53 @@ +/* @flow */ + import runInVm from './run-in-vm' import { PassThrough } from 'stream' +import type { Renderer, RenderOptions } from './create-renderer' + +const INVALID_MSG = + 'Invalid server-rendering bundle format. Should be a string of bundled code ' + + 'or an Object of type { entry: string; chunks: { [filename: string]: string }}.' + +// The render bundle can either be a string (single bundled file) +// or an object containing a chunks hash of filename:code pairs with the +// name of the entry file. The object format is used in conjunction with +// Webpack's compilation output so that code-split chunks can also be loaded. +type RenderBundle = string | { + entry: string; + chunks: { + [filename: string]: string; + }; +}; -export function createBundleRendererCreator (createRenderer) { - return (code, rendererOptions) => { +export function createBundleRendererCreator (createRenderer: () => Renderer) { + return (bundle: RenderBundle, rendererOptions?: RenderOptions) => { const renderer = createRenderer(rendererOptions) + let chunks, entry + if (typeof bundle === 'object') { + entry = bundle.entry + chunks = bundle.chunks + if (typeof entry !== 'string' || typeof chunks !== 'object') { + throw new Error(INVALID_MSG) + } + } else if (typeof bundle === 'string') { + entry = '__vue_ssr_bundle__' + chunks = { '__vue_ssr_bundle__': bundle } + } else { + throw new Error(INVALID_MSG) + } return { - renderToString: (context, cb) => { + renderToString: (context?: Object, cb: (err: ?Error, res: ?string) => void) => { if (typeof context === 'function') { cb = context context = {} } - runInVm(code, context).then(app => { + runInVm(entry, chunks, context).then(app => { renderer.renderToString(app, cb) }).catch(cb) }, - renderToStream: (context) => { + renderToStream: (context?: Object) => { const res = new PassThrough() - runInVm(code, context).then(app => { + runInVm(entry, chunks, context).then(app => { const renderStream = renderer.renderToStream(app) renderStream.on('error', err => { res.emit('error', err) diff --git a/src/server/create-renderer.js b/src/server/create-renderer.js index eb66640bae..9ea218ef63 100644 --- a/src/server/create-renderer.js +++ b/src/server/create-renderer.js @@ -4,20 +4,24 @@ import RenderStream from './render-stream' import { createWriteFunction } from './write' import { createRenderFunction } from './render' +export type Renderer = { + renderToString: (component: Component, cb: (err: ?Error, res: ?string) => void) => void; + renderToStream: (component: Component) => RenderStream; +}; + +export type RenderOptions = { + modules: Array, + directives: Object, + isUnaryTag: Function, + cache: ?Object +}; + export function createRenderer ({ modules = [], directives = {}, isUnaryTag = (() => false), cache -}: { - modules: Array, - directives: Object, - isUnaryTag: Function, - cache: ?Object -} = {}): { - renderToString: Function, - renderToStream: Function -} { +}: RenderOptions = {}): Renderer { const render = createRenderFunction(modules, directives, isUnaryTag, cache) return { diff --git a/src/server/render-stream.js b/src/server/render-stream.js index 397e2b0e49..25834ca6b9 100644 --- a/src/server/render-stream.js +++ b/src/server/render-stream.js @@ -8,7 +8,8 @@ * Modified by Evan You (@yyx990803) */ -import stream from 'stream' +const stream = require('stream') + import { createWriteFunction } from './write' export default class RenderStream extends stream.Readable { diff --git a/src/server/run-in-vm.js b/src/server/run-in-vm.js index 530939b950..b83692d689 100644 --- a/src/server/run-in-vm.js +++ b/src/server/run-in-vm.js @@ -1,5 +1,6 @@ -import NativeModule from 'module' -import vm from 'vm' +const NativeModule = require('module') +const vm = require('vm') +const path = require('path') function createContext (context) { const sandbox = { @@ -18,19 +19,39 @@ function createContext (context) { return sandbox } -export default function runInVm (code, _context = {}) { +function evaluateModule (filename, chunks, context, evaluatedModules) { + if (evaluatedModules[filename]) { + return evaluatedModules[filename] + } + + const code = chunks[filename] + const wrapper = NativeModule.wrap(code) + const compiledWrapper = vm.runInNewContext(wrapper, context, { + filename, + displayErrors: true + }) + const m = { exports: {}} + const r = file => { + file = path.join('.', file) + if (chunks[file]) { + return evaluateModule(file, chunks, context, evaluatedModules) + } else { + return require(file) + } + } + compiledWrapper.call(m.exports, m.exports, r, m) + + const res = Object.prototype.hasOwnProperty.call(m.exports, 'default') + ? m.exports.default + : m.exports + evaluatedModules[filename] = res + return res +} + +export default function runInVm (entry, chunks, _context = {}) { return new Promise((resolve, reject) => { - const wrapper = NativeModule.wrap(code) const context = createContext(_context) - const compiledWrapper = vm.runInNewContext(wrapper, context, { - filename: '__vue_ssr_bundle__', - displayErrors: true - }) - const m = { exports: {}} - compiledWrapper.call(m.exports, m.exports, require, m) - const res = Object.prototype.hasOwnProperty.call(m.exports, 'default') - ? m.exports.default - : m.exports + const res = evaluateModule(entry, chunks, context, {}) resolve(typeof res === 'function' ? res(_context) : res) }) }