|
|
@@ -15,6 +15,7 @@ type TemplateRendererOptions = {
|
|
|
inject?: boolean;
|
|
|
clientManifest?: ClientManifest;
|
|
|
shouldPreload?: (file: string, type: string) => boolean;
|
|
|
+ shouldPrefetch?: (file: string, type: string) => boolean;
|
|
|
};
|
|
|
|
|
|
export type ClientManifest = {
|
|
|
@@ -30,7 +31,7 @@ export type ClientManifest = {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-type PreloadFile = {
|
|
|
+type Resource = {
|
|
|
file: string;
|
|
|
extension: string;
|
|
|
fileWithoutQuery: string;
|
|
|
@@ -43,8 +44,8 @@ export default class TemplateRenderer {
|
|
|
parsedTemplate: ParsedTemplate | null;
|
|
|
publicPath: string;
|
|
|
clientManifest: ClientManifest;
|
|
|
- preloadFiles: Array<string>;
|
|
|
- prefetchFiles: Array<string>;
|
|
|
+ preloadFiles: Array<Resource>;
|
|
|
+ prefetchFiles: Array<Resource>;
|
|
|
mapFiles: AsyncFileMapper;
|
|
|
|
|
|
constructor (options: TemplateRendererOptions) {
|
|
|
@@ -61,8 +62,8 @@ export default class TemplateRenderer {
|
|
|
const clientManifest = this.clientManifest = options.clientManifest
|
|
|
this.publicPath = clientManifest.publicPath.replace(/\/$/, '')
|
|
|
// preload/prefetch directives
|
|
|
- this.preloadFiles = clientManifest.initial
|
|
|
- this.prefetchFiles = clientManifest.async
|
|
|
+ this.preloadFiles = (clientManifest.initial || []).map(normalizeFile)
|
|
|
+ this.prefetchFiles = (clientManifest.async || []).map(normalizeFile)
|
|
|
// initial async chunk mapping
|
|
|
this.mapFiles = createMapper(clientManifest)
|
|
|
}
|
|
|
@@ -125,19 +126,10 @@ export default class TemplateRenderer {
|
|
|
return this.renderPreloadLinks(context) + this.renderPrefetchLinks(context)
|
|
|
}
|
|
|
|
|
|
- getPreloadFiles (context: Object): Array<PreloadFile> {
|
|
|
+ getPreloadFiles (context: Object): Array<Resource> {
|
|
|
const usedAsyncFiles = this.getUsedAsyncFiles(context)
|
|
|
if (this.preloadFiles || usedAsyncFiles) {
|
|
|
- return (this.preloadFiles || []).concat(usedAsyncFiles || []).map(file => {
|
|
|
- const withoutQuery = file.replace(/\?.*/, '')
|
|
|
- const extension = path.extname(withoutQuery).slice(1)
|
|
|
- return {
|
|
|
- file,
|
|
|
- extension,
|
|
|
- fileWithoutQuery: withoutQuery,
|
|
|
- asType: getPreloadType(extension)
|
|
|
- }
|
|
|
- })
|
|
|
+ return (this.preloadFiles || []).concat(usedAsyncFiles || [])
|
|
|
} else {
|
|
|
return []
|
|
|
}
|
|
|
@@ -145,10 +137,10 @@ export default class TemplateRenderer {
|
|
|
|
|
|
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 = ''
|
|
|
- const shouldPreload = this.options.shouldPreload
|
|
|
// by default, we only preload scripts or css
|
|
|
if (!shouldPreload && asType !== 'script' && asType !== 'style') {
|
|
|
return ''
|
|
|
@@ -174,17 +166,20 @@ export default class TemplateRenderer {
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
+ return usedAsyncFiles && usedAsyncFiles.some(f => f.file === file)
|
|
|
}
|
|
|
- return this.prefetchFiles.map(file => {
|
|
|
- if (!alreadyRendered(file)) {
|
|
|
- return `<link rel="prefetch" href="${this.publicPath}/${file}">`
|
|
|
- } else {
|
|
|
+ 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 ''
|
|
|
@@ -205,10 +200,10 @@ export default class TemplateRenderer {
|
|
|
|
|
|
renderScripts (context: Object): string {
|
|
|
if (this.clientManifest) {
|
|
|
- const initial = this.clientManifest.initial
|
|
|
+ const initial = this.preloadFiles
|
|
|
const async = this.getUsedAsyncFiles(context)
|
|
|
const needed = [initial[0]].concat(async || [], initial.slice(1))
|
|
|
- return needed.filter(isJS).map(file => {
|
|
|
+ return needed.filter(({ file }) => isJS(file)).map(({ file }) => {
|
|
|
return `<script src="${this.publicPath}/${file}" defer></script>`
|
|
|
}).join('')
|
|
|
} else {
|
|
|
@@ -216,9 +211,10 @@ export default class TemplateRenderer {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- getUsedAsyncFiles (context: Object): ?Array<string> {
|
|
|
+ getUsedAsyncFiles (context: Object): ?Array<Resource> {
|
|
|
if (!context._mappedFiles && context._registeredComponents && this.mapFiles) {
|
|
|
- context._mappedFiles = this.mapFiles(Array.from(context._registeredComponents))
|
|
|
+ const registered = Array.from(context._registeredComponents)
|
|
|
+ context._mappedFiles = this.mapFiles(registered).map(normalizeFile)
|
|
|
}
|
|
|
return context._mappedFiles
|
|
|
}
|
|
|
@@ -232,6 +228,17 @@ export default class TemplateRenderer {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+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'
|