feat: use hashed page file names

pull/18/head
Evan You 5 years ago
parent b61e2398fc
commit a8735646e8

@ -56,12 +56,23 @@ export function createApp() {
} else { } else {
// in production, each .md file is built into a .md.js file following // in production, each .md file is built into a .md.js file following
// the path conversion scheme. // the path conversion scheme.
// /foo/bar.html -> /js/foo_bar.md.js // /foo/bar.html -> ./foo_bar.md
const useLeanBuild = isInitialPageLoad || initialPath === pagePath
pagePath = if (inBrowser) {
(inBrowser ? __BASE__ + '_assets/' : './') + pagePath = pagePath.slice(__BASE__.length).replace(/\//g, '_') + '.md'
pagePath.slice(inBrowser ? __BASE__.length : 1).replace(/\//g, '_') + // client production build needs to account for page hash, which is
(useLeanBuild ? '.md.lean.js' : '.md.js') // 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) { if (inBrowser) {

1
lib/shim.d.ts vendored

@ -1,5 +1,6 @@
declare const __DEV__: boolean declare const __DEV__: boolean
declare const __BASE__: string declare const __BASE__: string
declare const __VP_HASH_MAP__: Record<string, string>
declare module '*.vue' { declare module '*.vue' {
import { ComponentOptions } from 'vue' import { ComponentOptions } from 'vue'

@ -3,6 +3,7 @@ import { bundle } from './bundle'
import { BuildConfig as ViteBuildOptions } from 'vite' import { BuildConfig as ViteBuildOptions } from 'vite'
import { resolveConfig } from '../config' import { resolveConfig } from '../config'
import { renderPage } from './render' import { renderPage } from './render'
import { OutputChunk } from 'rollup'
export type BuildOptions = Pick< export type BuildOptions = Pick<
ViteBuildOptions, ViteBuildOptions,
@ -17,10 +18,32 @@ export const ASSETS_DIR = '_assets/'
export async function build(buildOptions: BuildOptions = {}) { export async function build(buildOptions: BuildOptions = {}) {
const siteConfig = await resolveConfig(buildOptions.root) const siteConfig = await resolveConfig(buildOptions.root)
try { try {
const [clientResult] = await bundle(siteConfig, buildOptions) const [clientResult, , pageToHashMap] = await bundle(
siteConfig,
buildOptions
)
console.log('rendering pages...') 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) { for (const page of siteConfig.pages) {
await renderPage(siteConfig, page, clientResult) await renderPage(
siteConfig,
page,
clientResult,
indexChunk,
pageToHashMap,
hashMapStirng
)
} }
} finally { } finally {
await fs.remove(siteConfig.tempDir) await fs.remove(siteConfig.tempDir)

@ -31,12 +31,13 @@ const isPageChunk = (
export async function bundle( export async function bundle(
config: SiteConfig, config: SiteConfig,
options: BuildOptions options: BuildOptions
): Promise<BuildResult[]> { ): Promise<[BuildResult, BuildResult, Record<string, string>]> {
const root = config.root const root = config.root
const resolver = createResolver(config.themeDir) const resolver = createResolver(config.themeDir)
const markdownToVue = createMarkdownToVueRenderFn(root) const markdownToVue = createMarkdownToVueRenderFn(root)
let isClientBuild = true let isClientBuild = true
const pageToHashMap = Object.create(null)
const VitePressPlugin: Plugin = { const VitePressPlugin: Plugin = {
name: 'vitepress', name: 'vitepress',
@ -82,13 +83,18 @@ export async function bundle(
const chunk = bundle[name] const chunk = bundle[name]
if (isPageChunk(chunk)) { if (isPageChunk(chunk)) {
// foo/bar.md -> foo_bar.md.js // foo/bar.md -> foo_bar.md.js
chunk.fileName = const hash = isClientBuild
slash(path.relative(root, chunk.facadeModuleId)).replace( ? chunk.fileName.match(/\.(\w+)\.js$/)![1]
/\//g, : ``
'_' const pageName = slash(
) + '.js' path.relative(root, chunk.facadeModuleId)
).replace(/\//g, '_')
chunk.fileName = `${pageName}${hash ? `.${hash}` : ``}.js`
if (isClientBuild) { if (isClientBuild) {
// record page -> hash relations
pageToHashMap[pageName] = hash
// inject another chunk with the content stripped // inject another chunk with the content stripped
bundle[name + '-lean'] = { bundle[name + '-lean'] = {
...chunk, ...chunk,
@ -131,13 +137,22 @@ export async function bundle(
preserveEntrySignatures: 'allow-extension', preserveEntrySignatures: 'allow-extension',
plugins: [VitePressPlugin, ...(rollupInputOptions.plugins || [])] plugins: [VitePressPlugin, ...(rollupInputOptions.plugins || [])]
}, },
rollupOutputOptions, rollupOutputOptions: {
...rollupOutputOptions,
chunkFileNames: `common-[hash].js`
},
silent: !process.env.DEBUG, silent: !process.env.DEBUG,
minify: !process.env.DEBUG minify: !process.env.DEBUG
} }
console.log('building client bundle...') 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...') console.log('building server bundle...')
isClientBuild = false isClientBuild = false
@ -146,5 +161,5 @@ export async function bundle(
outDir: config.tempDir outDir: config.tempDir
}) })
return [clientResult, serverResult] return [clientResult, serverResult, pageToHashMap]
} }

@ -11,7 +11,10 @@ const escape = require('escape-html')
export async function renderPage( export async function renderPage(
config: SiteConfig, config: SiteConfig,
page: string, // foo.md page: string, // foo.md
result: BuildResult result: BuildResult,
indexChunk: OutputChunk,
pageToHashMap: Record<string, string>,
hashMapStirng: string
) { ) {
const { createApp } = require(path.join( const { createApp } = require(path.join(
config.tempDir, config.tempDir,
@ -23,27 +26,30 @@ export async function renderPage(
router.go(routePath) router.go(routePath)
const content = await renderToString(app) 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 // resolve page data so we can render head tags
const { __pageData } = require(path.join( const { __pageData } = require(path.join(
config.tempDir, config.tempDir,
ASSETS_DIR, ASSETS_DIR,
pageJsFileName pageServerJsFileName
)) ))
const pageData = JSON.parse(__pageData) const pageData = JSON.parse(__pageData)
const assetPath = `${config.site.base}${ASSETS_DIR}` const assetPath = `${config.site.base}${ASSETS_DIR}`
const preloadLinks = [ const preloadLinks = [
// resolve imports for index.js + page.md.js and inject script tags for // 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 // them as well so we fetch everything as early as possible without having
// to wait for entry chunks to parse // to wait for entry chunks to parse
...resolvePageImports(config, page, result), ...resolvePageImports(config, page, result, indexChunk),
// for any initial page load, we only need the lean version of the page js pageClientJsFileName,
// since the static content is already on the page! indexChunk.fileName
pageJsFileName.replace(/\.js$/, '.lean.js'),
'index.js'
] ]
.map((file) => { .map((file) => {
return `<link rel="modulepreload" href="${assetPath}${file}">` return `<link rel="modulepreload" href="${assetPath}${file}">`
@ -64,7 +70,10 @@ export async function renderPage(
</head> </head>
<body> <body>
<div id="app">${content}</div> <div id="app">${content}</div>
<script type="module" async src="${assetPath}index.js"></script> <script>__VP_HASH_MAP__ = JSON.parse(${hashMapStirng})</script>
<script type="module" async src="${assetPath}${
indexChunk.fileName
}"></script>
</body> </body>
</html>`.trim() </html>`.trim()
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))
@ -75,13 +84,12 @@ export async function renderPage(
function resolvePageImports( function resolvePageImports(
config: SiteConfig, config: SiteConfig,
page: string, page: string,
result: BuildResult result: BuildResult,
indexChunk: OutputChunk
) { ) {
// find the page's js chunk and inject script tags for its imports so that // find the page's js chunk and inject script tags for its imports so that
// they are start fetching as early as possible // 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 srcPath = path.join(config.root, page)
const pageChunk = result.assets.find( const pageChunk = result.assets.find(
(chunk) => chunk.type === 'chunk' && chunk.facadeModuleId === srcPath (chunk) => chunk.type === 'chunk' && chunk.facadeModuleId === srcPath

Loading…
Cancel
Save