未验证 提交 83de3423 编写于 作者: T Tim Neutkens 提交者: GitHub

Convert incremental generation cache to a class (#14660)

We've been meaning to change this code for a while 👍

- Changed the name from spr to incremental
- Changed the code to be a class instead of using module scope variables
上级 14f1a7b2
......@@ -690,7 +690,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
...config,
initialPageRevalidationMap: {},
// Default map will be the collection of automatic statically exported
// pages and SPR pages.
// pages and incremental pages.
// n.b. we cannot handle this above in combinedPages because the dynamic
// page must be in the `pages` array, but not in the mapping.
exportPathMap: (defaultMap: any) => {
......
import { promises, readFileSync } from 'fs'
import LRUCache from 'next/dist/compiled/lru-cache'
import path from 'path'
import { PrerenderManifest } from '../../build'
import { PRERENDER_MANIFEST } from '../lib/constants'
import { normalizePagePath } from './normalize-page-path'
function toRoute(pathname: string): string {
return pathname.replace(/\/$/, '').replace(/\/index$/, '') || '/'
}
type IncrementalCacheValue = {
html: string
pageData: any
isStale?: boolean
curRevalidate?: number | false
// milliseconds to revalidate after
revalidateAfter: number | false
}
export class IncrementalCache {
incrementalOptions: {
flushToDisk?: boolean
pagesDir?: string
distDir?: string
dev?: boolean
}
prerenderManifest: PrerenderManifest
cache: LRUCache<string, IncrementalCacheValue>
constructor({
max,
dev,
distDir,
pagesDir,
flushToDisk,
}: {
dev: boolean
max?: number
distDir: string
pagesDir: string
flushToDisk?: boolean
}) {
this.incrementalOptions = {
dev,
distDir,
pagesDir,
flushToDisk:
!dev && (typeof flushToDisk !== 'undefined' ? flushToDisk : true),
}
if (dev) {
this.prerenderManifest = {
version: -1 as any, // letting us know this doesn't conform to spec
routes: {},
dynamicRoutes: {},
preview: null as any, // `preview` is special case read in next-dev-server
}
} else {
this.prerenderManifest = JSON.parse(
readFileSync(path.join(distDir, PRERENDER_MANIFEST), 'utf8')
)
}
this.cache = new LRUCache({
// default to 50MB limit
max: max || 50 * 1024 * 1024,
length(val) {
// rough estimate of size of cache value
return val.html.length + JSON.stringify(val.pageData).length
},
})
}
private getSeedPath(pathname: string, ext: string): string {
return path.join(this.incrementalOptions.pagesDir!, `${pathname}.${ext}`)
}
private calculateRevalidate(pathname: string): number | false {
pathname = toRoute(pathname)
// in development we don't have a prerender-manifest
// and default to always revalidating to allow easier debugging
const curTime = new Date().getTime()
if (this.incrementalOptions.dev) return curTime - 1000
const { initialRevalidateSeconds } = this.prerenderManifest.routes[
pathname
] || {
initialRevalidateSeconds: 1,
}
const revalidateAfter =
typeof initialRevalidateSeconds === 'number'
? initialRevalidateSeconds * 1000 + curTime
: initialRevalidateSeconds
return revalidateAfter
}
getFallback(page: string): Promise<string> {
page = normalizePagePath(page)
return promises.readFile(this.getSeedPath(page, 'html'), 'utf8')
}
// get data from cache if available
async get(pathname: string): Promise<IncrementalCacheValue | void> {
if (this.incrementalOptions.dev) return
pathname = normalizePagePath(pathname)
let data = this.cache.get(pathname)
// let's check the disk for seed data
if (!data) {
try {
const html = await promises.readFile(
this.getSeedPath(pathname, 'html'),
'utf8'
)
const pageData = JSON.parse(
await promises.readFile(this.getSeedPath(pathname, 'json'), 'utf8')
)
data = {
html,
pageData,
revalidateAfter: this.calculateRevalidate(pathname),
}
this.cache.set(pathname, data)
} catch (_) {
// unable to get data from disk
}
}
if (
data &&
data.revalidateAfter !== false &&
data.revalidateAfter < new Date().getTime()
) {
data.isStale = true
}
const manifestEntry = this.prerenderManifest.routes[pathname]
if (data && manifestEntry) {
data.curRevalidate = manifestEntry.initialRevalidateSeconds
}
return data
}
// populate the incremental cache with new data
async set(
pathname: string,
data: {
html: string
pageData: any
},
revalidateSeconds?: number | false
) {
if (this.incrementalOptions.dev) return
if (typeof revalidateSeconds !== 'undefined') {
// TODO: Update this to not mutate the manifest from the
// build.
this.prerenderManifest.routes[pathname] = {
dataRoute: path.posix.join(
'/_next/data',
`${normalizePagePath(pathname)}.json`
),
srcRoute: null, // FIXME: provide actual source route, however, when dynamically appending it doesn't really matter
initialRevalidateSeconds: revalidateSeconds,
}
}
pathname = normalizePagePath(pathname)
this.cache.set(pathname, {
...data,
revalidateAfter: this.calculateRevalidate(pathname),
})
// TODO: This option needs to cease to exist unless it stops mutating the
// `next build` output's manifest.
if (this.incrementalOptions.flushToDisk) {
try {
const seedPath = this.getSeedPath(pathname, 'html')
await promises.mkdir(path.dirname(seedPath), { recursive: true })
await promises.writeFile(seedPath, data.html, 'utf8')
await promises.writeFile(
this.getSeedPath(pathname, 'json'),
JSON.stringify(data.pageData),
'utf8'
)
} catch (error) {
// failed to flush to disk
console.warn('Failed to update prerender files for', pathname, error)
}
}
}
}
......@@ -55,12 +55,7 @@ import Router, {
import { sendHTML } from './send-html'
import { sendPayload } from './send-payload'
import { serveStatic } from './serve-static'
import {
getFallback,
getSprCache,
initializeSprCache,
setSprCache,
} from './spr-cache'
import { IncrementalCache } from './incremental-cache'
import { execOnce } from '../lib/utils'
import { isBlockedPage } from './utils'
import { compile as compilePathToRegex } from 'next/dist/compiled/path-to-regexp'
......@@ -128,6 +123,7 @@ export default class Server {
}
private compression?: Middleware
private onErrorMiddleware?: ({ err }: { err: Error }) => Promise<void>
private incrementalCache: IncrementalCache
router: Router
protected dynamicRoutes?: DynamicRoutes
protected customRoutes: CustomRoutes
......@@ -217,7 +213,7 @@ export default class Server {
initServer()
}
initializeSprCache({
this.incrementalCache = new IncrementalCache({
dev,
distDir: this.distDir,
pagesDir: join(
......@@ -994,7 +990,9 @@ export default class Server {
: `${urlPathname}${query.amp ? '.amp' : ''}`
// Complete the response with cached data if its present
const cachedData = ssgCacheKey ? await getSprCache(ssgCacheKey) : undefined
const cachedData = ssgCacheKey
? await this.incrementalCache.get(ssgCacheKey)
: undefined
if (cachedData) {
const data = isDataReq
......@@ -1117,7 +1115,7 @@ export default class Server {
// Production already emitted the fallback as static HTML.
if (isProduction) {
html = await getFallback(pathname)
html = await this.incrementalCache.getFallback(pathname)
}
// We need to generate the fallback on-demand for development.
else {
......@@ -1154,9 +1152,13 @@ export default class Server {
resHtml = null
}
// Update the SPR cache if the head request and cacheable
// Update the cache if the head request and cacheable
if (isOrigin && ssgCacheKey) {
await setSprCache(ssgCacheKey, { html: html!, pageData }, sprRevalidate)
await this.incrementalCache.set(
ssgCacheKey,
{ html: html!, pageData },
sprRevalidate
)
}
return resHtml
......
import { promises, readFileSync } from 'fs'
import LRUCache from 'next/dist/compiled/lru-cache'
import path from 'path'
import { PrerenderManifest } from '../../build'
import { PRERENDER_MANIFEST } from '../lib/constants'
import { normalizePagePath } from './normalize-page-path'
function toRoute(pathname: string): string {
return pathname.replace(/\/$/, '').replace(/\/index$/, '') || '/'
}
type SprCacheValue = {
html: string
pageData: any
isStale?: boolean
curRevalidate?: number | false
// milliseconds to revalidate after
revalidateAfter: number | false
}
let cache: LRUCache<string, SprCacheValue>
let prerenderManifest: PrerenderManifest
let sprOptions: {
flushToDisk?: boolean
pagesDir?: string
distDir?: string
dev?: boolean
} = {}
const getSeedPath = (pathname: string, ext: string): string => {
return path.join(sprOptions.pagesDir!, `${pathname}.${ext}`)
}
export const calculateRevalidate = (pathname: string): number | false => {
pathname = toRoute(pathname)
// in development we don't have a prerender-manifest
// and default to always revalidating to allow easier debugging
const curTime = new Date().getTime()
if (sprOptions.dev) return curTime - 1000
const { initialRevalidateSeconds } = prerenderManifest.routes[pathname] || {
initialRevalidateSeconds: 1,
}
const revalidateAfter =
typeof initialRevalidateSeconds === 'number'
? initialRevalidateSeconds * 1000 + curTime
: initialRevalidateSeconds
return revalidateAfter
}
// initialize the SPR cache
export function initializeSprCache({
max,
dev,
distDir,
pagesDir,
flushToDisk,
}: {
dev: boolean
max?: number
distDir: string
pagesDir: string
flushToDisk?: boolean
}) {
sprOptions = {
dev,
distDir,
pagesDir,
flushToDisk:
!dev && (typeof flushToDisk !== 'undefined' ? flushToDisk : true),
}
if (dev) {
prerenderManifest = {
version: -1 as any, // letting us know this doesn't conform to spec
routes: {},
dynamicRoutes: {},
preview: null as any, // `preview` is special case read in next-dev-server
}
} else {
prerenderManifest = JSON.parse(
readFileSync(path.join(distDir, PRERENDER_MANIFEST), 'utf8')
)
}
cache = new LRUCache({
// default to 50MB limit
max: max || 50 * 1024 * 1024,
length(val) {
// rough estimate of size of cache value
return val.html.length + JSON.stringify(val.pageData).length
},
})
}
export async function getFallback(page: string): Promise<string> {
page = normalizePagePath(page)
return promises.readFile(getSeedPath(page, 'html'), 'utf8')
}
// get data from SPR cache if available
export async function getSprCache(
pathname: string
): Promise<SprCacheValue | undefined> {
if (sprOptions.dev) return
pathname = normalizePagePath(pathname)
let data: SprCacheValue | undefined = cache.get(pathname)
// let's check the disk for seed data
if (!data) {
try {
const html = await promises.readFile(
getSeedPath(pathname, 'html'),
'utf8'
)
const pageData = JSON.parse(
await promises.readFile(getSeedPath(pathname, 'json'), 'utf8')
)
data = {
html,
pageData,
revalidateAfter: calculateRevalidate(pathname),
}
cache.set(pathname, data)
} catch (_) {
// unable to get data from disk
}
}
if (
data &&
data.revalidateAfter !== false &&
data.revalidateAfter < new Date().getTime()
) {
data.isStale = true
}
const manifestEntry = prerenderManifest.routes[pathname]
if (data && manifestEntry) {
data.curRevalidate = manifestEntry.initialRevalidateSeconds
}
return data
}
// populate the SPR cache with new data
export async function setSprCache(
pathname: string,
data: {
html: string
pageData: any
},
revalidateSeconds?: number | false
) {
if (sprOptions.dev) return
if (typeof revalidateSeconds !== 'undefined') {
// TODO: Update this to not mutate the manifest from the
// build.
prerenderManifest.routes[pathname] = {
dataRoute: path.posix.join(
'/_next/data',
`${normalizePagePath(pathname)}.json`
),
srcRoute: null, // FIXME: provide actual source route, however, when dynamically appending it doesn't really matter
initialRevalidateSeconds: revalidateSeconds,
}
}
pathname = normalizePagePath(pathname)
cache.set(pathname, {
...data,
revalidateAfter: calculateRevalidate(pathname),
})
// TODO: This option needs to cease to exist unless it stops mutating the
// `next build` output's manifest.
if (sprOptions.flushToDisk) {
try {
const seedPath = getSeedPath(pathname, 'html')
await promises.mkdir(path.dirname(seedPath), { recursive: true })
await promises.writeFile(seedPath, data.html, 'utf8')
await promises.writeFile(
getSeedPath(pathname, 'json'),
JSON.stringify(data.pageData),
'utf8'
)
} catch (error) {
// failed to flush to disk
console.warn('Failed to update prerender files for', pathname, error)
}
}
}
......@@ -345,7 +345,7 @@ const runTests = (dev = false, isEmulatedServerless = false) => {
expect(html).toMatch(/hello.*?world/)
})
it('should SSR SPR page correctly', async () => {
it('should SSR incremental page correctly', async () => {
const html = await renderViaHTTP(appPort, '/blog/post-1')
const $ = cheerio.load(html)
......@@ -1601,7 +1601,7 @@ describe('SSG Prerender', () => {
exportTrailingSlash: true,
exportPathMap: function(defaultPathMap) {
if (defaultPathMap['/blog/[post]']) {
throw new Error('Found SPR page in the default export path map')
throw new Error('Found Incremental page in the default export path map')
}
return defaultPathMap
},
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册