Browse Source

ssr: handle link rel=preload for non-js assets too

dev
Evan You 8 years ago
parent
commit
f4f4c126f2
  1. 3
      src/server/create-renderer.js
  2. 4
      src/server/template-renderer/create-async-file-mapper.js
  3. 50
      src/server/template-renderer/index.js
  4. 14
      test/ssr/compile-with-webpack.js
  5. 6
      test/ssr/fixtures/async-foo.js
  6. 0
      test/ssr/fixtures/test.png
  7. 0
      test/ssr/fixtures/test.woff2
  8. 4
      test/ssr/ssr-bundle-render.spec.js
  9. 21
      test/ssr/ssr-template.spec.js
  10. 12
      yarn.lock

3
src/server/create-renderer.js

@ -24,6 +24,7 @@ export type RenderOptions = {
cache?: RenderCache;
template?: string;
basedir?: string;
shouldPreload?: Function;
serverManifest?: ServerManifest;
clientManifest?: ClientManifest;
};
@ -34,12 +35,14 @@ export function createRenderer ({
isUnaryTag = (() => false),
template,
cache,
shouldPreload,
serverManifest,
clientManifest
}: RenderOptions = {}): Renderer {
const render = createRenderFunction(modules, directives, isUnaryTag, cache)
const templateRenderer = template && new TemplateRenderer({
template,
shouldPreload,
serverManifest,
clientManifest
})

4
src/server/template-renderer/create-async-file-mapper.js

@ -45,8 +45,8 @@ function mapFile (moduleIds, clientManifest) {
if (fileIndices) {
fileIndices.forEach(index => {
const file = clientManifest.all[index]
// only include async files
if (clientManifest.async.indexOf(file) > -1) {
// only include async files or non-js assets
if (clientManifest.async.indexOf(file) > -1 || !(/\.js($|\?)/.test(file))) {
files.add(file)
}
})

50
src/server/template-renderer/index.js

@ -1,5 +1,6 @@
/* @flow */
const path = require('path')
const serialize = require('serialize-javascript')
import TemplateStream from './template-stream'
@ -8,10 +9,14 @@ import { createMapper } from './create-async-file-mapper'
import type { ParsedTemplate } from './parse-template'
import type { AsyncFileMapper } from './create-async-file-mapper'
const JS_RE = /\.js($|\?)/
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 = {
@ -34,6 +39,7 @@ export type ClientManifest = {
};
export default class TemplateRenderer {
options: TemplateRendererOptions;
template: ParsedTemplate;
publicPath: string;
serverManifest: ServerManifest;
@ -43,6 +49,7 @@ export default class TemplateRenderer {
mapFiles: AsyncFileMapper;
constructor (options: TemplateRendererOptions) {
this.options = options
this.template = parseTemplate(options.template)
// extra functionality with client manifest
@ -80,7 +87,29 @@ export default class TemplateRenderer {
const usedAsyncFiles = this.getUsedAsyncFiles(context)
if (this.preloadFiles || usedAsyncFiles) {
return (this.preloadFiles || []).concat(usedAsyncFiles || []).map(file => {
return `<link rel="preload" href="${this.publicPath}/${file}" as="script">`
let extra = ''
const withoutQuery = file.replace(/\?.*/, '')
const ext = path.extname(withoutQuery).slice(1)
const type = getPreloadType(ext)
const shouldPreload = this.options.shouldPreload
// by default, we only preload scripts and fonts
if (!shouldPreload && type !== 'script' && type !== 'font') {
return ''
}
// user wants to explicitly control what to preload
if (shouldPreload && !shouldPreload(withoutQuery, type)) {
return ''
}
if (type === 'font') {
extra = ` type="font/${ext}" crossorigin`
}
return `<link rel="preload" href="${
this.publicPath}/${file
}"${
type !== '' ? ` as="${type}"` : ''
}${
extra
}>`
}).join('')
} else {
return ''
@ -118,7 +147,7 @@ export default class TemplateRenderer {
const initial = this.clientManifest.initial
const async = this.getUsedAsyncFiles(context)
const needed = [initial[0]].concat(async || [], initial.slice(1))
return needed.map(file => {
return needed.filter(isJS).map(file => {
return `<script src="${this.publicPath}/${file}"></script>`
}).join('')
} else {
@ -136,7 +165,7 @@ export default class TemplateRenderer {
if (noCssHash) {
mapped = mapped.map(file => {
return noCssHash[file]
? file.replace(/\.js$/, '.no-css.js')
? file.replace(JS_RE, '.no-css.js')
: file
})
}
@ -152,3 +181,18 @@ export default class TemplateRenderer {
return new TemplateStream(this, context || {})
}
}
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 possbilities here, but above covers common cases
return ''
}
}

14
test/ssr/compile-with-webpack.js

@ -6,7 +6,19 @@ export function compileWithWebpack (file, extraConfig, cb) {
const config = Object.assign({
entry: path.resolve(__dirname, 'fixtures', file),
module: {
rules: [{ test: /\.js$/, loader: 'babel-loader' }]
rules: [
{
test: /\.js$/,
loader: 'babel-loader'
},
{
test: /\.(png|woff2)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]'
}
}
]
}
}, extraConfig)

6
test/ssr/fixtures/async-foo.js

@ -1,5 +1,9 @@
// import image and font
import font from './test.woff2'
import image from './test.png'
module.exports = {
render (h) {
return h('div', 'async')
return h('div', `async ${font} ${image}`)
}
}

0
test/ssr/fixtures/test.png

0
test/ssr/fixtures/test.woff2

4
test/ssr/ssr-bundle-render.spec.js

@ -168,7 +168,7 @@ describe('SSR: bundle renderer', () => {
const context = { url: '/test' }
renderer.renderToString(context, (err, res) => {
expect(err).toBeNull()
expect(res).toBe('<div data-server-rendered="true">/test<div>async</div></div>')
expect(res).toBe('<div data-server-rendered="true">/test<div>async test.woff2 test.png</div></div>')
done()
})
})
@ -183,7 +183,7 @@ describe('SSR: bundle renderer', () => {
res += chunk.toString()
})
stream.on('end', () => {
expect(res).toBe('<div data-server-rendered="true">/test<div>async</div></div>')
expect(res).toBe('<div data-server-rendered="true">/test<div>async test.woff2 test.png</div></div>')
done()
})
})

21
test/ssr/ssr-template.spec.js

@ -25,12 +25,13 @@ function generateClientManifest (file, cb) {
})
}
function createRendererWithManifest (file, cb) {
function createRendererWithManifest (file, cb, shouldPreload) {
generateClientManifest(file, clientManifest => {
createBundleRenderer(file, {
asBundle: true,
template: defaultTemplate,
clientManifest
clientManifest,
shouldPreload
}, cb)
})
}
@ -145,16 +146,20 @@ describe('SSR: template option', () => {
})
})
const expectedHTMLWithManifest =
const expectedHTMLWithManifest = preloadImage =>
`<html><head>` +
// used chunks should have preload
`<link rel="preload" href="/manifest.js" as="script">` +
`<link rel="preload" href="/main.js" as="script">` +
`<link rel="preload" href="/0.js" as="script">` +
// images are only preloaded when explicitly asked for
(preloadImage ? `<link rel="preload" href="/test.png" as="image">` : ``) +
// critical assets like fonts are preloaded by default
`<link rel="preload" href="/test.woff2" as="font" type="font/woff2" crossorigin>` +
// unused chunks should have prefetch
`<link rel="prefetch" href="/1.js" as="script">` +
`</head><body>` +
`<div data-server-rendered="true"><div>async</div></div>` +
`<div data-server-rendered="true"><div>async test.woff2 test.png</div></div>` +
// manifest chunk should be first
`<script src="/manifest.js"></script>` +
// async chunks should be before main chunk
@ -166,7 +171,7 @@ describe('SSR: template option', () => {
createRendererWithManifest('split.js', renderer => {
renderer.renderToString({}, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(expectedHTMLWithManifest)
expect(res).toContain(expectedHTMLWithManifest(false))
done()
})
})
@ -180,9 +185,13 @@ describe('SSR: template option', () => {
res += chunk.toString()
})
stream.on('end', () => {
expect(res).toContain(expectedHTMLWithManifest)
expect(res).toContain(expectedHTMLWithManifest(true))
done()
})
}, (file, type) => {
if (type === 'image' || type === 'script' || type === 'font') {
return true
}
})
})
})

12
yarn.lock

@ -2076,6 +2076,12 @@ file-entry-cache@^2.0.0:
flat-cache "^1.2.1"
object-assign "^4.0.1"
file-loader@^0.10.1:
version "0.10.1"
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.10.1.tgz#815034119891fc6441fb5a64c11bc93c22ddd842"
dependencies:
loader-utils "^1.0.2"
file-uri-to-path@0:
version "0.0.2"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-0.0.2.tgz#37cdd1b5b905404b3f05e1b23645be694ff70f82"
@ -3411,7 +3417,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
dependencies:
brace-expansion "^1.0.0"
minimist@0.0.8:
minimist@0.0.8, minimist@~0.0.1:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@ -3419,10 +3425,6 @@ minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
minimist@~0.0.1:
version "0.0.10"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
mkdirp@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12"

Loading…
Cancel
Save