diff --git a/package.json b/package.json index d909ed1b..21e860fe 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "eslint-plugin-vue": "^2.0.0", "file-loader": "^0.10.1", "flow-bin": "^0.39.0", + "hash-sum": "^1.0.2", "he": "^1.1.0", "http-server": "^0.9.0", "jasmine": "^2.5.2", diff --git a/src/server/bundle-renderer/create-bundle-renderer.js b/src/server/bundle-renderer/create-bundle-renderer.js index 51996236..18f60f7e 100644 --- a/src/server/bundle-renderer/create-bundle-renderer.js +++ b/src/server/bundle-renderer/create-bundle-renderer.js @@ -32,7 +32,7 @@ export function createBundleRendererCreator (createRenderer: () => Renderer) { bundle: string | RenderBundle, rendererOptions?: RenderOptions = {} ) { - let files, entry, maps, moduleMappings + let files, entry, maps let basedir = rendererOptions.basedir const direct = rendererOptions.directMode @@ -63,7 +63,6 @@ export function createBundleRendererCreator (createRenderer: () => Renderer) { files = bundle.files basedir = basedir || bundle.basedir maps = createSourceMapConsumers(bundle.maps) - moduleMappings = bundle.modules if (typeof entry !== 'string' || typeof files !== 'object') { throw new Error(INVALID_MSG) } @@ -75,13 +74,6 @@ export function createBundleRendererCreator (createRenderer: () => Renderer) { throw new Error(INVALID_MSG) } - if (moduleMappings) { - rendererOptions = Object.assign({}, rendererOptions, { - serverManifest: { - modules: moduleMappings - } - }) - } const renderer = createRenderer(rendererOptions) const run = createBundleRunner(entry, files, basedir, direct) diff --git a/src/server/bundle-renderer/create-bundle-runner.js b/src/server/bundle-renderer/create-bundle-runner.js index 7729afa3..2b4ad5d5 100644 --- a/src/server/bundle-renderer/create-bundle-runner.js +++ b/src/server/bundle-renderer/create-bundle-runner.js @@ -40,7 +40,7 @@ function compileModule (files, basedir) { return script } - function evaluateModule (filename, context, evaluatedFiles) { + function evaluateModule (filename, context, evaluatedFiles = {}) { if (evaluatedFiles[filename]) { return evaluatedFiles[filename] } @@ -93,9 +93,8 @@ export function createBundleRunner (entry, files, basedir, direct) { // on each render. Ensures entire application state is fresh for each // render, but incurs extra evaluation cost. return (_context = {}) => new Promise((resolve, reject) => { - const context = createContext(_context) - const evaluatedFiles = _context._evaluatedFiles = {} - const res = evaluate(entry, context, evaluatedFiles) + _context._registeredComponents = new Set() + const res = evaluate(entry, createContext(_context)) resolve(typeof res === 'function' ? res(_context) : res) }) } else { @@ -105,12 +104,13 @@ export function createBundleRunner (entry, files, basedir, direct) { // slightly differently. const initialExposedContext = {} const context = createContext(initialExposedContext) - const runner = evaluate(entry, context, {}) + const runner = evaluate(entry, context) if (typeof runner !== 'function') { throw new Error('direct mode expects bundle export to be a function.') } return (_context = {}) => { context.__VUE_SSR_CONTEXT__ = _context + _context._registeredComponents = new Set() // vue-style-loader styles imported outside of component lifecycle hooks if (initialExposedContext._styles) { _context._styles = deepClone(initialExposedContext._styles) diff --git a/src/server/create-renderer.js b/src/server/create-renderer.js index 9f40bfa7..fb0fbf44 100644 --- a/src/server/create-renderer.js +++ b/src/server/create-renderer.js @@ -4,7 +4,7 @@ import RenderStream from './render-stream' import TemplateRenderer from './template-renderer/index' import { createWriteFunction } from './write' import { createRenderFunction } from './render' -import type { ClientManifest, ServerManifest } from './template-renderer/index' +import type { ClientManifest } from './template-renderer/index' export type Renderer = { renderToString: (component: Component, cb: (err: ?Error, res: ?string) => void) => void; @@ -26,7 +26,6 @@ export type RenderOptions = { basedir?: string; directMode?: boolean; shouldPreload?: Function; - serverManifest?: ServerManifest; clientManifest?: ClientManifest; }; @@ -37,14 +36,12 @@ export function createRenderer ({ template, cache, shouldPreload, - serverManifest, clientManifest }: RenderOptions = {}): Renderer { const render = createRenderFunction(modules, directives, isUnaryTag, cache) const templateRenderer = new TemplateRenderer({ template, shouldPreload, - serverManifest, clientManifest }) diff --git a/src/server/template-renderer/create-async-file-mapper.js b/src/server/template-renderer/create-async-file-mapper.js index 8969e526..64c0647a 100644 --- a/src/server/template-renderer/create-async-file-mapper.js +++ b/src/server/template-renderer/create-async-file-mapper.js @@ -1,25 +1,24 @@ /* @flow */ /** - * Creates a mapper that maps files used during a server-side render + * Creates a mapper that maps components used during a server-side render * to async chunk files in the client-side build, so that we can inline them * directly in the rendered HTML to avoid waterfall requests. */ -import type { ServerManifest, ClientManifest } from './index' +import type { ClientManifest } from './index' export type AsyncFileMapper = (files: Array) => Array; export function createMapper ( - serverManifest: ServerManifest, clientManifest: ClientManifest ): AsyncFileMapper { - const fileMap = createFileMap(serverManifest, clientManifest) - - return function mapFiles (files: Array): Array { + const map = createMap(clientManifest) + // map server-side moduleIds to client-side files + return function mapper (moduleIds: Array): Array { const res = new Set() - for (let i = 0; i < files.length; i++) { - const mapped = fileMap.get(files[i]) + for (let i = 0; i < moduleIds.length; i++) { + const mapped = map.get(moduleIds[i]) if (mapped) { for (let j = 0; j < mapped.length; j++) { res.add(mapped[j]) @@ -30,27 +29,25 @@ export function createMapper ( } } -function createFileMap (serverManifest, clientManifest) { - const fileMap = new Map() - Object.keys(serverManifest.modules).forEach(file => { - fileMap.set(file, mapFile(serverManifest.modules[file], clientManifest)) +function createMap (clientManifest) { + const map = new Map() + Object.keys(clientManifest.modules).forEach(id => { + map.set(id, mapIdToFile(id, clientManifest)) }) - return fileMap + return map } -function mapFile (moduleIds, clientManifest) { - const files = new Set() - moduleIds.forEach(id => { - const fileIndices = clientManifest.modules[id] - if (fileIndices) { - fileIndices.forEach(index => { - const file = clientManifest.all[index] - // only include async files or non-js assets - if (clientManifest.async.indexOf(file) > -1 || !(/\.js($|\?)/.test(file))) { - files.add(file) - } - }) - } - }) - return Array.from(files) +function mapIdToFile (id, clientManifest) { + const files = [] + const fileIndices = clientManifest.modules[id] + if (fileIndices) { + fileIndices.forEach(index => { + const file = clientManifest.all[index] + // only include async files or non-js assets + if (clientManifest.async.indexOf(file) > -1 || !(/\.js($|\?)/.test(file))) { + files.push(file) + } + }) + } + return files } diff --git a/src/server/template-renderer/index.js b/src/server/template-renderer/index.js index 63d88c5c..f8962631 100644 --- a/src/server/template-renderer/index.js +++ b/src/server/template-renderer/index.js @@ -14,17 +14,10 @@ export const isJS = (file: string): boolean => JS_RE.test(file) type TemplateRendererOptions = { template: ?string; - serverManifest?: ServerManifest; clientManifest?: ClientManifest; shouldPreload?: (file: string, type: string) => boolean; }; -export type ServerManifest = { - modules: { - [file: string]: Array; - } -}; - export type ClientManifest = { publicPath: string; all: Array; @@ -42,7 +35,6 @@ export default class TemplateRenderer { options: TemplateRendererOptions; parsedTemplate: ParsedTemplate | null; publicPath: string; - serverManifest: ServerManifest; clientManifest: ClientManifest; preloadFiles: Array; prefetchFiles: Array; @@ -57,15 +49,14 @@ export default class TemplateRenderer { : null // extra functionality with client manifest - if (options.serverManifest && options.clientManifest) { - const serverManifest = this.serverManifest = options.serverManifest + if (options.clientManifest) { const clientManifest = this.clientManifest = options.clientManifest this.publicPath = clientManifest.publicPath.replace(/\/$/, '') // preload/prefetch drectives this.preloadFiles = clientManifest.initial this.prefetchFiles = clientManifest.async // initial async chunk mapping - this.mapFiles = createMapper(serverManifest, clientManifest) + this.mapFiles = createMapper(clientManifest) } } @@ -125,7 +116,7 @@ export default class TemplateRenderer { renderPrefetchLinks (context: Object): string { if (this.prefetchFiles) { - const usedAsyncFiles = this.getUsedAsyncFiles(context, true) + const usedAsyncFiles = this.getUsedAsyncFiles(context) const alreadyRendered = file => { return usedAsyncFiles && usedAsyncFiles.some(f => f === file) } @@ -162,25 +153,11 @@ export default class TemplateRenderer { } } - getUsedAsyncFiles (context: Object, raw?: boolean): ?Array { - if (!context._mappedfiles && context._evaluatedFiles && this.mapFiles) { - let mapped = this.mapFiles(Object.keys(context._evaluatedFiles)) - context._rawMappedFiles = mapped - // if a file has a no-css version (produced by vue-ssr-webpack-plugin), - // we should use that instead. - const noCssHash = this.clientManifest && this.clientManifest.hasNoCssVersion - if (noCssHash) { - mapped = mapped.map(file => { - return noCssHash[file] - ? file.replace(JS_RE, '.no-css.js') - : file - }) - } - context._mappedFiles = mapped + getUsedAsyncFiles (context: Object): ?Array { + if (!context._mappedfiles && context._registeredComponents && this.mapFiles) { + context._mappedFiles = this.mapFiles(Array.from(context._registeredComponents)) } - return raw - ? context._rawMappedFiles - : context._mappedFiles + return context._mappedFiles } // create a transform stream diff --git a/test/ssr/async-loader.js b/test/ssr/async-loader.js new file mode 100644 index 00000000..41b4a98a --- /dev/null +++ b/test/ssr/async-loader.js @@ -0,0 +1,6 @@ +const hash = require('hash-sum') + +module.exports = function (code) { + const id = hash(this.request) // simulating vue-loader module id injection + return code.replace('__MODULE_ID__', id) +} diff --git a/test/ssr/compile-with-webpack.js b/test/ssr/compile-with-webpack.js index 74297a61..42639f4f 100644 --- a/test/ssr/compile-with-webpack.js +++ b/test/ssr/compile-with-webpack.js @@ -11,6 +11,10 @@ export function compileWithWebpack (file, extraConfig, cb) { test: /\.js$/, loader: 'babel-loader' }, + { + test: /async-.*\.js$/, + loader: require.resolve('./async-loader') + }, { test: /\.(png|woff2)$/, loader: 'file-loader', diff --git a/test/ssr/fixtures/async-bar.js b/test/ssr/fixtures/async-bar.js index 825bd85a..1a334740 100644 --- a/test/ssr/fixtures/async-bar.js +++ b/test/ssr/fixtures/async-bar.js @@ -1,4 +1,9 @@ +/* globals __VUE_SSR_CONTEXT__ */ + module.exports = { + beforeCreate () { + __VUE_SSR_CONTEXT__._registeredComponents.add('__MODULE_ID__') + }, render (h) { return h('div', 'async bar') } diff --git a/test/ssr/fixtures/async-foo.js b/test/ssr/fixtures/async-foo.js index a3888d2f..dc40bf6c 100644 --- a/test/ssr/fixtures/async-foo.js +++ b/test/ssr/fixtures/async-foo.js @@ -1,8 +1,13 @@ +/* globals __VUE_SSR_CONTEXT__ */ + // import image and font import font from './test.woff2' import image from './test.png' module.exports = { + beforeCreate () { + __VUE_SSR_CONTEXT__._registeredComponents.add('__MODULE_ID__') + }, render (h) { return h('div', `async ${font} ${image}`) } diff --git a/test/ssr/fixtures/nested-cache.js b/test/ssr/fixtures/nested-cache.js index 94b39b6f..17dcc8df 100644 --- a/test/ssr/fixtures/nested-cache.js +++ b/test/ssr/fixtures/nested-cache.js @@ -1,7 +1,9 @@ +/* globals __VUE_SSR_CONTEXT__ */ + import Vue from '../../../dist/vue.runtime.common.js' function register (id, context) { - context = context || __VUE_SSR_CONTEXT__ // eslint-disable-line + context = context || __VUE_SSR_CONTEXT__ context.registered.push(id) }