From a8735646e8aae04d7091decc8c4fd54025ceb181 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 May 2020 22:43:43 -0400 Subject: [PATCH] feat: use hashed page file names --- lib/app/index.js | 23 +++++++++++++++++------ lib/shim.d.ts | 1 + src/build/build.ts | 27 +++++++++++++++++++++++++-- src/build/bundle.ts | 33 ++++++++++++++++++++++++--------- src/build/render.ts | 36 ++++++++++++++++++++++-------------- 5 files changed, 89 insertions(+), 31 deletions(-) diff --git a/lib/app/index.js b/lib/app/index.js index 7ff96f5e..b47f4339 100644 --- a/lib/app/index.js +++ b/lib/app/index.js @@ -56,12 +56,23 @@ export function createApp() { } else { // in production, each .md file is built into a .md.js file following // the path conversion scheme. - // /foo/bar.html -> /js/foo_bar.md.js - const useLeanBuild = isInitialPageLoad || initialPath === pagePath - pagePath = - (inBrowser ? __BASE__ + '_assets/' : './') + - pagePath.slice(inBrowser ? __BASE__.length : 1).replace(/\//g, '_') + - (useLeanBuild ? '.md.lean.js' : '.md.js') + // /foo/bar.html -> ./foo_bar.md + + if (inBrowser) { + pagePath = pagePath.slice(__BASE__.length).replace(/\//g, '_') + '.md' + // client production build needs to account for page hash, which is + // injected directly in the page's html + const pageHash = __VP_HASH_MAP__[pagePath] + // use lean build if this is the initial page load or navigating back + // to the initial loaded path (the static vnodes already adopted the + // static content on that load so no need to re-fetch the page) + const ext = + isInitialPageLoad || initialPath === pagePath ? 'lean.js' : 'js' + pagePath = `${__BASE__}_assets/${pagePath}.${pageHash}.${ext}` + } else { + // ssr build uses much simpler name mapping + pagePath = `./${pagePath.slice(1).replace(/\//g, '_')}.md.js` + } } if (inBrowser) { diff --git a/lib/shim.d.ts b/lib/shim.d.ts index 52bc0549..aadea8a8 100644 --- a/lib/shim.d.ts +++ b/lib/shim.d.ts @@ -1,5 +1,6 @@ declare const __DEV__: boolean declare const __BASE__: string +declare const __VP_HASH_MAP__: Record declare module '*.vue' { import { ComponentOptions } from 'vue' diff --git a/src/build/build.ts b/src/build/build.ts index 00610b9b..4a5828fb 100644 --- a/src/build/build.ts +++ b/src/build/build.ts @@ -3,6 +3,7 @@ import { bundle } from './bundle' import { BuildConfig as ViteBuildOptions } from 'vite' import { resolveConfig } from '../config' import { renderPage } from './render' +import { OutputChunk } from 'rollup' export type BuildOptions = Pick< ViteBuildOptions, @@ -17,10 +18,32 @@ export const ASSETS_DIR = '_assets/' export async function build(buildOptions: BuildOptions = {}) { const siteConfig = await resolveConfig(buildOptions.root) try { - const [clientResult] = await bundle(siteConfig, buildOptions) + const [clientResult, , pageToHashMap] = await bundle( + siteConfig, + buildOptions + ) console.log('rendering pages...') + + const indexChunk = clientResult.assets.find( + (chunk) => + chunk.type === 'chunk' && chunk.fileName.match(/^index\.\w+\.js$/) + ) as OutputChunk + + // We embed the hash map string into each page directly so that it doesn't + // alter the main chunk's hash on every build. It's also embedded as a + // string and JSON.parsed from the client because it's faster than embedding + // as JS object literal. + const hashMapStirng = JSON.stringify(JSON.stringify(pageToHashMap)) + for (const page of siteConfig.pages) { - await renderPage(siteConfig, page, clientResult) + await renderPage( + siteConfig, + page, + clientResult, + indexChunk, + pageToHashMap, + hashMapStirng + ) } } finally { await fs.remove(siteConfig.tempDir) diff --git a/src/build/bundle.ts b/src/build/bundle.ts index b0102f5d..558a2a47 100644 --- a/src/build/bundle.ts +++ b/src/build/bundle.ts @@ -31,12 +31,13 @@ const isPageChunk = ( export async function bundle( config: SiteConfig, options: BuildOptions -): Promise { +): Promise<[BuildResult, BuildResult, Record]> { const root = config.root const resolver = createResolver(config.themeDir) const markdownToVue = createMarkdownToVueRenderFn(root) let isClientBuild = true + const pageToHashMap = Object.create(null) const VitePressPlugin: Plugin = { name: 'vitepress', @@ -82,13 +83,18 @@ export async function bundle( const chunk = bundle[name] if (isPageChunk(chunk)) { // foo/bar.md -> foo_bar.md.js - chunk.fileName = - slash(path.relative(root, chunk.facadeModuleId)).replace( - /\//g, - '_' - ) + '.js' + const hash = isClientBuild + ? chunk.fileName.match(/\.(\w+)\.js$/)![1] + : `` + const pageName = slash( + path.relative(root, chunk.facadeModuleId) + ).replace(/\//g, '_') + chunk.fileName = `${pageName}${hash ? `.${hash}` : ``}.js` if (isClientBuild) { + // record page -> hash relations + pageToHashMap[pageName] = hash + // inject another chunk with the content stripped bundle[name + '-lean'] = { ...chunk, @@ -131,13 +137,22 @@ export async function bundle( preserveEntrySignatures: 'allow-extension', plugins: [VitePressPlugin, ...(rollupInputOptions.plugins || [])] }, - rollupOutputOptions, + rollupOutputOptions: { + ...rollupOutputOptions, + chunkFileNames: `common-[hash].js` + }, silent: !process.env.DEBUG, minify: !process.env.DEBUG } console.log('building client bundle...') - const clientResult = await build(viteOptions) + const clientResult = await build({ + ...viteOptions, + rollupOutputOptions: { + ...viteOptions.rollupOutputOptions, + entryFileNames: `[name].[hash].js` + } + }) console.log('building server bundle...') isClientBuild = false @@ -146,5 +161,5 @@ export async function bundle( outDir: config.tempDir }) - return [clientResult, serverResult] + return [clientResult, serverResult, pageToHashMap] } diff --git a/src/build/render.ts b/src/build/render.ts index b4c830e5..e9a643e1 100644 --- a/src/build/render.ts +++ b/src/build/render.ts @@ -11,7 +11,10 @@ const escape = require('escape-html') export async function renderPage( config: SiteConfig, page: string, // foo.md - result: BuildResult + result: BuildResult, + indexChunk: OutputChunk, + pageToHashMap: Record, + hashMapStirng: string ) { const { createApp } = require(path.join( config.tempDir, @@ -23,27 +26,30 @@ export async function renderPage( router.go(routePath) const content = await renderToString(app) - const pageJsFileName = page.replace(/\//g, '_') + '.js' + const pageName = page.replace(/\//g, '_') + // server build doesn't need hash + const pageServerJsFileName = pageName + '.js' + // for any initial page load, we only need the lean version of the page js + // since the static content is already on the page! + const pageHash = pageToHashMap[pageName] + const pageClientJsFileName = pageName + `.` + pageHash + '.lean.js' // resolve page data so we can render head tags const { __pageData } = require(path.join( config.tempDir, ASSETS_DIR, - pageJsFileName + pageServerJsFileName )) const pageData = JSON.parse(__pageData) const assetPath = `${config.site.base}${ASSETS_DIR}` - const preloadLinks = [ // resolve imports for index.js + page.md.js and inject script tags for // them as well so we fetch everything as early as possible without having // to wait for entry chunks to parse - ...resolvePageImports(config, page, result), - // for any initial page load, we only need the lean version of the page js - // since the static content is already on the page! - pageJsFileName.replace(/\.js$/, '.lean.js'), - 'index.js' + ...resolvePageImports(config, page, result, indexChunk), + pageClientJsFileName, + indexChunk.fileName ] .map((file) => { return `` @@ -64,7 +70,10 @@ export async function renderPage(
${content}
- + + `.trim() const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) @@ -75,13 +84,12 @@ export async function renderPage( function resolvePageImports( config: SiteConfig, page: string, - result: BuildResult + result: BuildResult, + indexChunk: OutputChunk ) { // find the page's js chunk and inject script tags for its imports so that // they are start fetching as early as possible - const indexChunk = result.assets.find( - (chunk) => chunk.type === 'chunk' && chunk.fileName === `index.js` - ) as OutputChunk + const srcPath = path.join(config.root, page) const pageChunk = result.assets.find( (chunk) => chunk.type === 'chunk' && chunk.facadeModuleId === srcPath