Skip to content

Commit 7bc899c

Browse files
committed
feat(ssr): add shouldPrefetch option
close #5964
1 parent f881dd1 commit 7bc899c

File tree

3 files changed

+60
-27
lines changed

3 files changed

+60
-27
lines changed

src/server/create-renderer.js

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type RenderOptions = {
2727
inject?: boolean;
2828
basedir?: string;
2929
shouldPreload?: Function;
30+
shouldPrefetch?: Function;
3031
clientManifest?: ClientManifest;
3132
runInNewContext?: boolean | 'once';
3233
};
@@ -39,13 +40,15 @@ export function createRenderer ({
3940
inject,
4041
cache,
4142
shouldPreload,
43+
shouldPrefetch,
4244
clientManifest
4345
}: RenderOptions = {}): Renderer {
4446
const render = createRenderFunction(modules, directives, isUnaryTag, cache)
4547
const templateRenderer = new TemplateRenderer({
4648
template,
4749
inject,
4850
shouldPreload,
51+
shouldPrefetch,
4952
clientManifest
5053
})
5154

src/server/template-renderer/index.js

+33-26
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type TemplateRendererOptions = {
1515
inject?: boolean;
1616
clientManifest?: ClientManifest;
1717
shouldPreload?: (file: string, type: string) => boolean;
18+
shouldPrefetch?: (file: string, type: string) => boolean;
1819
};
1920

2021
export type ClientManifest = {
@@ -30,7 +31,7 @@ export type ClientManifest = {
3031
}
3132
};
3233

33-
type PreloadFile = {
34+
type Resource = {
3435
file: string;
3536
extension: string;
3637
fileWithoutQuery: string;
@@ -43,8 +44,8 @@ export default class TemplateRenderer {
4344
parsedTemplate: ParsedTemplate | null;
4445
publicPath: string;
4546
clientManifest: ClientManifest;
46-
preloadFiles: Array<string>;
47-
prefetchFiles: Array<string>;
47+
preloadFiles: Array<Resource>;
48+
prefetchFiles: Array<Resource>;
4849
mapFiles: AsyncFileMapper;
4950

5051
constructor (options: TemplateRendererOptions) {
@@ -61,8 +62,8 @@ export default class TemplateRenderer {
6162
const clientManifest = this.clientManifest = options.clientManifest
6263
this.publicPath = clientManifest.publicPath.replace(/\/$/, '')
6364
// preload/prefetch directives
64-
this.preloadFiles = clientManifest.initial
65-
this.prefetchFiles = clientManifest.async
65+
this.preloadFiles = (clientManifest.initial || []).map(normalizeFile)
66+
this.prefetchFiles = (clientManifest.async || []).map(normalizeFile)
6667
// initial async chunk mapping
6768
this.mapFiles = createMapper(clientManifest)
6869
}
@@ -125,30 +126,21 @@ export default class TemplateRenderer {
125126
return this.renderPreloadLinks(context) + this.renderPrefetchLinks(context)
126127
}
127128

128-
getPreloadFiles (context: Object): Array<PreloadFile> {
129+
getPreloadFiles (context: Object): Array<Resource> {
129130
const usedAsyncFiles = this.getUsedAsyncFiles(context)
130131
if (this.preloadFiles || usedAsyncFiles) {
131-
return (this.preloadFiles || []).concat(usedAsyncFiles || []).map(file => {
132-
const withoutQuery = file.replace(/\?.*/, '')
133-
const extension = path.extname(withoutQuery).slice(1)
134-
return {
135-
file,
136-
extension,
137-
fileWithoutQuery: withoutQuery,
138-
asType: getPreloadType(extension)
139-
}
140-
})
132+
return (this.preloadFiles || []).concat(usedAsyncFiles || [])
141133
} else {
142134
return []
143135
}
144136
}
145137

146138
renderPreloadLinks (context: Object): string {
147139
const files = this.getPreloadFiles(context)
140+
const shouldPreload = this.options.shouldPreload
148141
if (files.length) {
149142
return files.map(({ file, extension, fileWithoutQuery, asType }) => {
150143
let extra = ''
151-
const shouldPreload = this.options.shouldPreload
152144
// by default, we only preload scripts or css
153145
if (!shouldPreload && asType !== 'script' && asType !== 'style') {
154146
return ''
@@ -174,17 +166,20 @@ export default class TemplateRenderer {
174166
}
175167

176168
renderPrefetchLinks (context: Object): string {
169+
const shouldPrefetch = this.options.shouldPrefetch
177170
if (this.prefetchFiles) {
178171
const usedAsyncFiles = this.getUsedAsyncFiles(context)
179172
const alreadyRendered = file => {
180-
return usedAsyncFiles && usedAsyncFiles.some(f => f === file)
173+
return usedAsyncFiles && usedAsyncFiles.some(f => f.file === file)
181174
}
182-
return this.prefetchFiles.map(file => {
183-
if (!alreadyRendered(file)) {
184-
return `<link rel="prefetch" href="${this.publicPath}/${file}">`
185-
} else {
175+
return this.prefetchFiles.map(({ file, fileWithoutQuery, asType }) => {
176+
if (shouldPrefetch && !shouldPrefetch(fileWithoutQuery, asType)) {
177+
return ''
178+
}
179+
if (alreadyRendered(file)) {
186180
return ''
187181
}
182+
return `<link rel="prefetch" href="${this.publicPath}/${file}">`
188183
}).join('')
189184
} else {
190185
return ''
@@ -205,20 +200,21 @@ export default class TemplateRenderer {
205200

206201
renderScripts (context: Object): string {
207202
if (this.clientManifest) {
208-
const initial = this.clientManifest.initial
203+
const initial = this.preloadFiles
209204
const async = this.getUsedAsyncFiles(context)
210205
const needed = [initial[0]].concat(async || [], initial.slice(1))
211-
return needed.filter(isJS).map(file => {
206+
return needed.filter(({ file }) => isJS(file)).map(({ file }) => {
212207
return `<script src="${this.publicPath}/${file}" defer></script>`
213208
}).join('')
214209
} else {
215210
return ''
216211
}
217212
}
218213

219-
getUsedAsyncFiles (context: Object): ?Array<string> {
214+
getUsedAsyncFiles (context: Object): ?Array<Resource> {
220215
if (!context._mappedFiles && context._registeredComponents && this.mapFiles) {
221-
context._mappedFiles = this.mapFiles(Array.from(context._registeredComponents))
216+
const registered = Array.from(context._registeredComponents)
217+
context._mappedFiles = this.mapFiles(registered).map(normalizeFile)
222218
}
223219
return context._mappedFiles
224220
}
@@ -232,6 +228,17 @@ export default class TemplateRenderer {
232228
}
233229
}
234230

231+
function normalizeFile (file: string): Resource {
232+
const withoutQuery = file.replace(/\?.*/, '')
233+
const extension = path.extname(withoutQuery).slice(1)
234+
return {
235+
file,
236+
extension,
237+
fileWithoutQuery: withoutQuery,
238+
asType: getPreloadType(extension)
239+
}
240+
}
241+
235242
function getPreloadType (ext: string): string {
236243
if (ext === 'js') {
237244
return 'script'

test/ssr/ssr-template.spec.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ describe('SSR: template option', () => {
230230
(options.preloadOtherAssets ? `<link rel="preload" href="/test.png" as="image">` : ``) +
231231
(options.preloadOtherAssets ? `<link rel="preload" href="/test.woff2" as="font" type="font/woff2" crossorigin>` : ``) +
232232
// unused chunks should have prefetch
233-
`<link rel="prefetch" href="/1.js">` +
233+
(options.noPrefetch ? `` : `<link rel="prefetch" href="/1.js">`) +
234234
// css assets should be loaded
235235
`<link rel="stylesheet" href="/test.css">` +
236236
`</head><body>` +
@@ -281,6 +281,29 @@ describe('SSR: template option', () => {
281281
})
282282
})
283283

284+
it('bundleRenderer + renderToStream + clientManifest + shouldPrefetch', done => {
285+
createRendererWithManifest('split.js', {
286+
runInNewContext,
287+
shouldPrefetch: (file, type) => {
288+
if (type === 'script') {
289+
return false
290+
}
291+
}
292+
}, renderer => {
293+
const stream = renderer.renderToStream({ state: { a: 1 }})
294+
let res = ''
295+
stream.on('data', chunk => {
296+
res += chunk.toString()
297+
})
298+
stream.on('end', () => {
299+
expect(res).toContain(expectedHTMLWithManifest({
300+
noPrefetch: true
301+
}))
302+
done()
303+
})
304+
})
305+
})
306+
284307
it('bundleRenderer + renderToString + clientManifest + inject: false', done => {
285308
createRendererWithManifest('split.js', {
286309
runInNewContext,

0 commit comments

Comments
 (0)