diff options
| author | ruki <waruqi@gmail.com> | 2018-11-08 00:38:48 +0800 |
|---|---|---|
| committer | ruki <waruqi@gmail.com> | 2018-11-07 21:53:09 +0800 |
| commit | 26105034da4fcce7ac883c899d781f016559310d (patch) | |
| tree | c459a5dc4e3aa0972d9919033ece511ce76dd129 /node_modules/vue/src/server/template-renderer | |
| parent | 2c77f00f1a7ecb6c8192f9c16d3b2001b254a107 (diff) | |
| download | xmake-docs-26105034da4fcce7ac883c899d781f016559310d.tar.gz xmake-docs-26105034da4fcce7ac883c899d781f016559310d.zip | |
switch to vuepress
Diffstat (limited to 'node_modules/vue/src/server/template-renderer')
4 files changed, 434 insertions, 0 deletions
diff --git a/node_modules/vue/src/server/template-renderer/create-async-file-mapper.js b/node_modules/vue/src/server/template-renderer/create-async-file-mapper.js new file mode 100644 index 00000000..64c0647a --- /dev/null +++ b/node_modules/vue/src/server/template-renderer/create-async-file-mapper.js @@ -0,0 +1,53 @@ +/* @flow */ + +/** + * 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 { ClientManifest } from './index' + +export type AsyncFileMapper = (files: Array<string>) => Array<string>; + +export function createMapper ( + clientManifest: ClientManifest +): AsyncFileMapper { + const map = createMap(clientManifest) + // map server-side moduleIds to client-side files + return function mapper (moduleIds: Array<string>): Array<string> { + const res = new Set() + 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]) + } + } + } + return Array.from(res) + } +} + +function createMap (clientManifest) { + const map = new Map() + Object.keys(clientManifest.modules).forEach(id => { + map.set(id, mapIdToFile(id, clientManifest)) + }) + return map +} + +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/node_modules/vue/src/server/template-renderer/index.js b/node_modules/vue/src/server/template-renderer/index.js new file mode 100644 index 00000000..dabc08a1 --- /dev/null +++ b/node_modules/vue/src/server/template-renderer/index.js @@ -0,0 +1,257 @@ +/* @flow */ + +const path = require('path') +const serialize = require('serialize-javascript') + +import { isJS, isCSS } from '../util' +import TemplateStream from './template-stream' +import { parseTemplate } from './parse-template' +import { createMapper } from './create-async-file-mapper' +import type { ParsedTemplate } from './parse-template' +import type { AsyncFileMapper } from './create-async-file-mapper' + +type TemplateRendererOptions = { + template: ?string; + inject?: boolean; + clientManifest?: ClientManifest; + shouldPreload?: (file: string, type: string) => boolean; + shouldPrefetch?: (file: string, type: string) => boolean; +}; + +export type ClientManifest = { + publicPath: string; + all: Array<string>; + initial: Array<string>; + async: Array<string>; + modules: { + [id: string]: Array<number>; + }, + hasNoCssVersion?: { + [file: string]: boolean; + } +}; + +type Resource = { + file: string; + extension: string; + fileWithoutQuery: string; + asType: string; +}; + +export default class TemplateRenderer { + options: TemplateRendererOptions; + inject: boolean; + parsedTemplate: ParsedTemplate | null; + publicPath: string; + clientManifest: ClientManifest; + preloadFiles: Array<Resource>; + prefetchFiles: Array<Resource>; + mapFiles: AsyncFileMapper; + + constructor (options: TemplateRendererOptions) { + this.options = options + this.inject = options.inject !== false + // if no template option is provided, the renderer is created + // as a utility object for rendering assets like preload links and scripts. + this.parsedTemplate = options.template + ? parseTemplate(options.template) + : null + + // extra functionality with client manifest + if (options.clientManifest) { + const clientManifest = this.clientManifest = options.clientManifest + this.publicPath = clientManifest.publicPath.replace(/\/$/, '') + // preload/prefetch directives + this.preloadFiles = (clientManifest.initial || []).map(normalizeFile) + this.prefetchFiles = (clientManifest.async || []).map(normalizeFile) + // initial async chunk mapping + this.mapFiles = createMapper(clientManifest) + } + } + + bindRenderFns (context: Object) { + const renderer: any = this + ;['ResourceHints', 'State', 'Scripts', 'Styles'].forEach(type => { + context[`render${type}`] = renderer[`render${type}`].bind(renderer, context) + }) + // also expose getPreloadFiles, useful for HTTP/2 push + context.getPreloadFiles = renderer.getPreloadFiles.bind(renderer, context) + } + + // render synchronously given rendered app content and render context + renderSync (content: string, context: ?Object) { + const template = this.parsedTemplate + if (!template) { + throw new Error('renderSync cannot be called without a template.') + } + context = context || {} + if (this.inject) { + return ( + template.head(context) + + (context.head || '') + + this.renderResourceHints(context) + + this.renderStyles(context) + + template.neck(context) + + content + + this.renderState(context) + + this.renderScripts(context) + + template.tail(context) + ) + } else { + return ( + template.head(context) + + template.neck(context) + + content + + template.tail(context) + ) + } + } + + renderStyles (context: Object): string { + const cssFiles = this.clientManifest + ? this.clientManifest.all.filter(isCSS) + : [] + return ( + // render links for css files + (cssFiles.length + ? cssFiles.map(file => `<link rel="stylesheet" href="${this.publicPath}/${file}">`).join('') + : '') + + // context.styles is a getter exposed by vue-style-loader which contains + // the inline component styles collected during SSR + (context.styles || '') + ) + } + + renderResourceHints (context: Object): string { + return this.renderPreloadLinks(context) + this.renderPrefetchLinks(context) + } + + getPreloadFiles (context: Object): Array<Resource> { + const usedAsyncFiles = this.getUsedAsyncFiles(context) + if (this.preloadFiles || usedAsyncFiles) { + return (this.preloadFiles || []).concat(usedAsyncFiles || []) + } else { + return [] + } + } + + renderPreloadLinks (context: Object): string { + const files = this.getPreloadFiles(context) + const shouldPreload = this.options.shouldPreload + if (files.length) { + return files.map(({ file, extension, fileWithoutQuery, asType }) => { + let extra = '' + // by default, we only preload scripts or css + if (!shouldPreload && asType !== 'script' && asType !== 'style') { + return '' + } + // user wants to explicitly control what to preload + if (shouldPreload && !shouldPreload(fileWithoutQuery, asType)) { + return '' + } + if (asType === 'font') { + extra = ` type="font/${extension}" crossorigin` + } + return `<link rel="preload" href="${ + this.publicPath}/${file + }"${ + asType !== '' ? ` as="${asType}"` : '' + }${ + extra + }>` + }).join('') + } else { + return '' + } + } + + renderPrefetchLinks (context: Object): string { + const shouldPrefetch = this.options.shouldPrefetch + if (this.prefetchFiles) { + const usedAsyncFiles = this.getUsedAsyncFiles(context) + const alreadyRendered = file => { + return usedAsyncFiles && usedAsyncFiles.some(f => f.file === file) + } + return this.prefetchFiles.map(({ file, fileWithoutQuery, asType }) => { + if (shouldPrefetch && !shouldPrefetch(fileWithoutQuery, asType)) { + return '' + } + if (alreadyRendered(file)) { + return '' + } + return `<link rel="prefetch" href="${this.publicPath}/${file}">` + }).join('') + } else { + return '' + } + } + + renderState (context: Object, options?: Object): string { + const { + contextKey = 'state', + windowKey = '__INITIAL_STATE__' + } = options || {} + const state = serialize(context[contextKey], { isJSON: true }) + const autoRemove = process.env.NODE_ENV === 'production' + ? ';(function(){var s;(s=document.currentScript||document.scripts[document.scripts.length-1]).parentNode.removeChild(s);}());' + : '' + return context[contextKey] + ? `<script>window.${windowKey}=${state}${autoRemove}</script>` + : '' + } + + renderScripts (context: Object): string { + if (this.clientManifest) { + const initial = this.preloadFiles + const async = this.getUsedAsyncFiles(context) + const needed = [initial[0]].concat(async || [], initial.slice(1)) + return needed.filter(({ file }) => isJS(file)).map(({ file }) => { + return `<script src="${this.publicPath}/${file}" defer></script>` + }).join('') + } else { + return '' + } + } + + getUsedAsyncFiles (context: Object): ?Array<Resource> { + if (!context._mappedFiles && context._registeredComponents && this.mapFiles) { + const registered = Array.from(context._registeredComponents) + context._mappedFiles = this.mapFiles(registered).map(normalizeFile) + } + return context._mappedFiles + } + + // create a transform stream + createStream (context: ?Object): TemplateStream { + if (!this.parsedTemplate) { + throw new Error('createStream cannot be called without a template.') + } + return new TemplateStream(this, this.parsedTemplate, context || {}) + } +} + +function normalizeFile (file: string): Resource { + const withoutQuery = file.replace(/\?.*/, '') + const extension = path.extname(withoutQuery).slice(1) + return { + file, + extension, + fileWithoutQuery: withoutQuery, + asType: getPreloadType(extension) + } +} + +function getPreloadType (ext: string): string { + if (ext === 'js') { + return 'script' + } else if (ext === 'css') { + return 'style' + } else if (/jpe?g|png|svg|gif|webp|ico/.test(ext)) { + return 'image' + } else if (/woff2?|ttf|otf|eot/.test(ext)) { + return 'font' + } else { + // not exhausting all possibilities here, but above covers common cases + return '' + } +} diff --git a/node_modules/vue/src/server/template-renderer/parse-template.js b/node_modules/vue/src/server/template-renderer/parse-template.js new file mode 100644 index 00000000..1ccfe899 --- /dev/null +++ b/node_modules/vue/src/server/template-renderer/parse-template.js @@ -0,0 +1,42 @@ +/* @flow */ + +const compile = require('lodash.template') +const compileOptions = { + escape: /{{([^{][\s\S]+?[^}])}}/g, + interpolate: /{{{([\s\S]+?)}}}/g +} + +export type ParsedTemplate = { + head: (data: any) => string; + neck: (data: any) => string; + tail: (data: any) => string; +}; + +export function parseTemplate ( + template: string, + contentPlaceholder?: string = '<!--vue-ssr-outlet-->' +): ParsedTemplate { + if (typeof template === 'object') { + return template + } + + let i = template.indexOf('</head>') + const j = template.indexOf(contentPlaceholder) + + if (j < 0) { + throw new Error(`Content placeholder not found in template.`) + } + + if (i < 0) { + i = template.indexOf('<body>') + if (i < 0) { + i = j + } + } + + return { + head: compile(template.slice(0, i), compileOptions), + neck: compile(template.slice(i, j), compileOptions), + tail: compile(template.slice(j + contentPlaceholder.length), compileOptions) + } +} diff --git a/node_modules/vue/src/server/template-renderer/template-stream.js b/node_modules/vue/src/server/template-renderer/template-stream.js new file mode 100644 index 00000000..ed4db78f --- /dev/null +++ b/node_modules/vue/src/server/template-renderer/template-stream.js @@ -0,0 +1,82 @@ +/* @flow */ + +const Transform = require('stream').Transform +import type TemplateRenderer from './index' +import type { ParsedTemplate } from './parse-template' + +export default class TemplateStream extends Transform { + started: boolean; + renderer: TemplateRenderer; + template: ParsedTemplate; + context: Object; + inject: boolean; + + constructor ( + renderer: TemplateRenderer, + template: ParsedTemplate, + context: Object + ) { + super() + this.started = false + this.renderer = renderer + this.template = template + this.context = context || {} + this.inject = renderer.inject + } + + _transform (data: Buffer | string, encoding: string, done: Function) { + if (!this.started) { + this.emit('beforeStart') + this.start() + } + this.push(data) + done() + } + + start () { + this.started = true + this.push(this.template.head(this.context)) + + if (this.inject) { + // inline server-rendered head meta information + if (this.context.head) { + this.push(this.context.head) + } + + // inline preload/prefetch directives for initial/async chunks + const links = this.renderer.renderResourceHints(this.context) + if (links) { + this.push(links) + } + + // CSS files and inline server-rendered CSS collected by vue-style-loader + const styles = this.renderer.renderStyles(this.context) + if (styles) { + this.push(styles) + } + } + + this.push(this.template.neck(this.context)) + } + + _flush (done: Function) { + this.emit('beforeEnd') + + if (this.inject) { + // inline initial store state + const state = this.renderer.renderState(this.context) + if (state) { + this.push(state) + } + + // embed scripts needed + const scripts = this.renderer.renderScripts(this.context) + if (scripts) { + this.push(scripts) + } + } + + this.push(this.template.tail(this.context)) + done() + } +} |
