Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit 46ac631

Browse files
committed
feat: support ISR (#135)
1 parent 306b77b commit 46ac631

File tree

12 files changed

+99
-57
lines changed

12 files changed

+99
-57
lines changed

framework/core/style.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function recoverCSS(id: string) {
2222
}
2323

2424
export function applyCSS(id: string, css: string) {
25-
if (util.inDeno()) {
25+
if (util.inDeno) {
2626
serverStyles.set(id, css)
2727
} else {
2828
const { document } = window as any

framework/react/bootstrap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ComponentType, createElement } from 'https://esm.sh/react'
22
import { hydrate, render } from 'https://esm.sh/react-dom'
33
import { importModule, trimModuleExt } from '../core/module.ts'
44
import { RouteModule, Routing, RoutingOptions } from '../core/routing.ts'
5-
import { loadPageDataFromTag } from './helper.ts'
5+
import { loadPageDataFromTag } from './pagedata.ts'
66
import { createPageProps, PageRoute } from './pageprops.ts'
77
import Router from './router.ts'
88

framework/react/head.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default function Head(props: PropsWithChildren<{}>) {
1919
const renderer = useContext(SSRContext)
2020
const [els, forwardNodes] = useMemo(() => parse(props.children), [props.children])
2121

22-
if (util.inDeno()) {
22+
if (util.inDeno) {
2323
els.forEach(({ type, props }, key) => renderer.headElements.set(key, { type, props }))
2424
}
2525

framework/react/helper.ts

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { createContext } from 'https://esm.sh/react'
22
import util from '../../shared/util.ts'
3-
import type { RouterURL } from '../../types.ts'
43

5-
const symbolFor = typeof Symbol === 'function' && Symbol.for
6-
const REACT_FORWARD_REF_TYPE = symbolFor ? Symbol.for('react.forward_ref') : 0xead0
7-
const REACT_MEMO_TYPE = symbolFor ? Symbol.for('react.memo') : 0xead3
4+
const REACT_FORWARD_REF_TYPE = util.supportSymbolFor ? Symbol.for('react.forward_ref') : 0xead0
5+
const REACT_MEMO_TYPE = util.supportSymbolFor ? Symbol.for('react.memo') : 0xead3
86

97
export function isLikelyReactComponent(type: any): Boolean {
108
switch (typeof type) {
@@ -41,31 +39,6 @@ export function isLikelyReactComponent(type: any): Boolean {
4139
}
4240
}
4341

44-
export async function loadPageData({ pathname }: RouterURL): Promise<void> {
45-
const url = `/_aleph/data${pathname === '/' ? '/index' : pathname}.json`
46-
const data = await fetch(url).then(resp => resp.json())
47-
if (util.isPlainObject(data)) {
48-
for (const key in data) {
49-
Object.assign(window, { [`data://${pathname}#${key}`]: data[key] })
50-
}
51-
}
52-
}
53-
54-
export async function loadPageDataFromTag(url: RouterURL) {
55-
const { document } = window as any
56-
const ssrDataEl = document.getElementById('ssr-data')
57-
if (ssrDataEl) {
58-
try {
59-
const ssrData = JSON.parse(ssrDataEl.innerText)
60-
for (const key in ssrData) {
61-
Object.assign(window, { [`data://${url.pathname}#${key}`]: ssrData[key] })
62-
}
63-
return
64-
} catch (e) { }
65-
}
66-
await loadPageData(url)
67-
}
68-
6942
export function createNamedContext<T>(defaultValue: T, name: string) {
7043
const ctx = createContext<T>(defaultValue)
7144
ctx.displayName = name // show in devTools

framework/react/hooks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ export function useDeno<T = any>(callback: () => (T | Promise<T>), revalidate?:
3737
const { pathname } = useRouter()
3838
return useMemo(() => {
3939
const global = globalThis as any
40-
const dataUrl = 'data://' + pathname
40+
const dataUrl = 'pagedata://' + pathname
4141
const eventName = 'useDeno-' + dataUrl
4242
const key = dataUrl + '#' + id
4343
const expires = revalidate ? Date.now() + revalidate * 1000 : 0
4444
const renderingDataCache = global['rendering-' + dataUrl]
4545
if (renderingDataCache && key in renderingDataCache) {
4646
return renderingDataCache[key] // 2+ pass
47-
} else if (util.inDeno()) {
47+
} else if (util.inDeno) {
4848
const v = callback()
4949
if (v instanceof Promise) {
5050
events.emit(eventName, id, v.then(value => {

framework/react/pagedata.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import util from '../../shared/util.ts'
2+
import type { RouterURL } from '../../types.ts'
3+
4+
const global = window as any
5+
6+
export async function loadPageData({ pathname }: RouterURL): Promise<void> {
7+
if (`pagedata://${pathname}` in global) {
8+
const { expires, keys } = global[`pagedata://${pathname}`]
9+
if (expires === 0 || Date.now() < expires) {
10+
return
11+
}
12+
delete global[`pagedata://${pathname}`]
13+
keys.forEach((key: string) => {
14+
delete global[`pagedata://${pathname}#key`]
15+
})
16+
}
17+
const url = `/_aleph/data${pathname === '/' ? '/index' : pathname}.json`
18+
const data = await fetch(url).then(resp => resp.json())
19+
if (util.isPlainObject(data)) {
20+
storeData(data, pathname)
21+
}
22+
}
23+
24+
export async function loadPageDataFromTag(url: RouterURL) {
25+
const ssrDataEl = global.document.getElementById('ssr-data')
26+
if (ssrDataEl) {
27+
try {
28+
const ssrData = JSON.parse(ssrDataEl.innerText)
29+
if (util.isPlainObject(ssrData)) {
30+
storeData(ssrData, url.pathname)
31+
return
32+
}
33+
} catch (e) { }
34+
}
35+
await loadPageData(url)
36+
}
37+
38+
function storeData(data: any, pathname: string) {
39+
let expires = 0
40+
for (const key in data) {
41+
const { expires: _expires } = data[key]
42+
if (expires === 0 || (_expires > 0 && _expires < expires)) {
43+
expires = _expires
44+
}
45+
global[`pagedata://${pathname}#${key}`] = data[key]
46+
}
47+
global[`pagedata://${pathname}`] = { expires, keys: Object.keys(data) }
48+
}

framework/react/renderer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,20 @@ export async function render(
3131
headElements: new Map(),
3232
scriptElements: new Map(),
3333
}
34-
const dataUrl = 'data://' + url.pathname
34+
const pagedataUrl = 'pagedata://' + url.pathname
3535
const asyncCalls: Array<Promise<any>> = []
3636
const data: Record<string, any> = {}
3737
const pageProps = createPageProps(nestedPageComponents)
3838
const defer = () => {
39-
delete global['rendering-' + dataUrl]
40-
events.removeAllListeners('useDeno-' + dataUrl)
39+
delete global['rendering-' + pagedataUrl]
40+
events.removeAllListeners('useDeno-' + pagedataUrl)
4141
}
4242

4343
// rendering data cache
44-
global['rendering-' + dataUrl] = {}
44+
global['rendering-' + pagedataUrl] = {}
4545

4646
// listen `useDeno-*` events to get hooks callback result.
47-
events.on('useDeno-' + dataUrl, (id: string, v: any) => {
47+
events.on('useDeno-' + pagedataUrl, (id: string, v: any) => {
4848
if (v instanceof Promise) {
4949
asyncCalls.push(v)
5050
} else {

framework/react/router.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { importModule } from '../core/module.ts'
1010
import { RouteModule, Routing } from '../core/routing.ts'
1111
import { RouterContext } from './context.ts'
1212
import { E400MissingComponent, E404Page, ErrorBoundary } from './error.ts'
13-
import { isLikelyReactComponent, loadPageData } from './helper.ts'
13+
import { isLikelyReactComponent } from './helper.ts'
14+
import { loadPageData } from './pagedata.ts'
1415
import type { PageRoute } from './pageprops.ts'
1516
import { createPageProps } from './pageprops.ts'
1617

framework/react/script.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { SSRContext } from './context.ts'
99
export default function Script(props: PropsWithChildren<ScriptHTMLAttributes<{}>>) {
1010
const { scriptElements } = useContext(SSRContext)
1111

12-
if (util.inDeno()) {
12+
if (util.inDeno) {
1313
const key = 'script-' + (scriptElements.size + 1)
1414
scriptElements.set(key, { props })
1515
}

server/ssr.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,22 @@ import util from '../shared/util.ts'
55
import type { RouterURL } from '../types.ts'
66
import type { Application } from './app.ts'
77

8+
export type SSRData = {
9+
expires: number
10+
value: any
11+
}
12+
13+
export type SSROutput = {
14+
html: string
15+
data: Record<string, SSRData> | null
16+
}
17+
818
/** The framework render result of SSR. */
919
export type FrameworkRenderResult = {
1020
head: string[]
1121
body: string
1222
scripts: Record<string, any>[]
13-
data: Record<string, string> | null
23+
data: Record<string, SSRData> | null
1424
}
1525

1626
/** The framework renderer for SSR. */
@@ -26,7 +36,7 @@ export type FrameworkRenderer = {
2636
export class Renderer {
2737
#app: Application
2838
#renderer: FrameworkRenderer
29-
#cache: Map<string, Map<string, [string, any]>>
39+
#cache: Map<string, Map<string, SSROutput>>
3040

3141
constructor(app: Application) {
3242
this.#app = app
@@ -41,25 +51,36 @@ export class Renderer {
4151
async useCache(
4252
namespace: string,
4353
key: string,
44-
render: () => Promise<[string, any]>
54+
render: () => Promise<[string, Record<string, SSRData> | null]>
4555
): Promise<[string, any]> {
4656
let cache = this.#cache.get(namespace)
4757
if (cache === undefined) {
4858
cache = new Map()
4959
this.#cache.set(namespace, cache)
5060
}
51-
const cached = cache.get(key)
52-
if (cached !== undefined) {
53-
return cached
61+
if (cache.has(key)) {
62+
const { html, data } = cache.get(key)!
63+
let expires = 0
64+
if (data !== null) {
65+
Object.values(data).forEach(({ expires: _expires }) => {
66+
if (expires === 0 || (_expires > 0 && _expires < expires)) {
67+
expires = _expires
68+
}
69+
})
70+
}
71+
if (expires === 0 || Date.now() < expires) {
72+
return [html, data]
73+
}
74+
cache.delete(key)
5475
}
55-
const ret = await render()
76+
let [html, data] = await render()
5677
if (namespace !== '-') {
5778
this.#app.getCodeInjects('ssr')?.forEach(transform => {
58-
ret[0] = transform(key, ret[0])
79+
html = transform(key, html)
5980
})
6081
}
61-
cache.set(key, ret)
62-
return ret
82+
cache.set(key, { html, data })
83+
return [html, data]
6384
}
6485

6586
clearCache(url?: string) {
@@ -71,7 +92,7 @@ export class Renderer {
7192
}
7293

7394
/** render page base the given location. */
74-
async renderPage(url: RouterURL, nestedModules: RouteModule[]): Promise<[string, any]> {
95+
async renderPage(url: RouterURL, nestedModules: RouteModule[]): Promise<[string, Record<string, SSRData> | null]> {
7596
const start = performance.now()
7697
const isDev = this.#app.isDev
7798
const appModule = this.#app.findModuleByName('app')

0 commit comments

Comments
 (0)