Browse Source

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

Evan You 9 years ago
parent
commit
f4f4c126f2

+ 3 - 0
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
   })

+ 2 - 2
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)
         }
       })

+ 47 - 3
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 ''
+  }
+}

+ 13 - 1
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)
 

+ 5 - 1
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 - 0
test/ssr/fixtures/test.png


+ 0 - 0
test/ssr/fixtures/test.woff2


+ 2 - 2
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()
       })
     })

+ 15 - 6
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
+      }
     })
   })
 })

+ 7 - 5
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"