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

Commit ffe5545

Browse files
author
Wenjie Xia
committed
refactor(framework): improve react framework
1 parent 9a209f2 commit ffe5545

File tree

11 files changed

+240
-332
lines changed

11 files changed

+240
-332
lines changed

framework/react/anchor.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ import { useRouter } from './hooks.ts'
77

88
const prefetchedPages = new Set<string>()
99

10-
interface AnchorDataProps {
10+
type AnchorProps = PropsWithChildren<AnchorHTMLAttributes<{}> & {
1111
'data-active-className'?: string
1212
'data-active-style'?: CSSProperties
13-
}
14-
15-
type AnchorProps = PropsWithChildren<AnchorHTMLAttributes<{}> & AnchorDataProps>
13+
}>
1614

1715
/**
1816
* Anchor Component to link between pages.

framework/react/bootstrap.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,27 @@ import { createElement } from 'https://esm.sh/react'
33
import { hydrate, render } from 'https://esm.sh/react-dom'
44
import { reModuleExt } from '../../shared/constants.ts'
55
import { Route, RouteModule, Routing } from '../core/routing.ts'
6-
import AlephAppRoot from './root.ts'
6+
import Router from './router.ts'
77
import { importModule } from './util.ts'
88

9-
type BootstrapConfig = {
9+
type BootstrapOptions = {
1010
baseUrl: string
1111
defaultLocale: string
1212
locales: string[]
1313
routes: Route[]
14-
preloadModules: RouteModule[],
14+
sharedModules: RouteModule[],
1515
renderMode: 'ssr' | 'spa'
1616
}
1717

18-
export default async function bootstrap({ baseUrl, defaultLocale, locales, routes, preloadModules, renderMode }: BootstrapConfig) {
18+
export default async function bootstrap({ baseUrl, defaultLocale, locales, routes, sharedModules, renderMode }: BootstrapOptions) {
1919
const { document } = window as any
2020
const ssrDataEl = document.querySelector('#ssr-data')
2121
const routing = new Routing(routes, baseUrl, defaultLocale, locales)
2222
const [url, pageModuleTree] = routing.createRouter()
23-
const customComponents: Record<string, ComponentType> = {}
2423
const pageComponentTree: { url: string, Component?: ComponentType }[] = pageModuleTree.map(({ url }) => ({ url }))
24+
const customComponents: Record<string, ComponentType> = {}
2525

26-
await Promise.all([...preloadModules, ...pageModuleTree].map(async mod => {
26+
await Promise.all([...sharedModules, ...pageModuleTree].map(async mod => {
2727
const { default: C } = await importModule(baseUrl, mod)
2828
switch (mod.url.replace(reModuleExt, '')) {
2929
case '/404':
@@ -44,12 +44,12 @@ export default async function bootstrap({ baseUrl, defaultLocale, locales, route
4444
if (ssrDataEl) {
4545
const ssrData = JSON.parse(ssrDataEl.innerText)
4646
for (const key in ssrData) {
47-
Object.assign(window, { [`useDeno://${url.pathname}#${key}`]: ssrData[key] })
47+
Object.assign(window, { [`data://${url.pathname}#${key}`]: ssrData[key] })
4848
}
4949
}
5050

5151
const rootEl = createElement(
52-
AlephAppRoot,
52+
Router,
5353
{
5454
url,
5555
routing,

framework/react/context.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ export const RouterContext = createContext<RouterURL>({
1010
})
1111
RouterContext.displayName = 'RouterContext'
1212

13-
interface RendererCache {
13+
interface RenderStorage {
1414
headElements: Map<string, { type: string, props: Record<string, any> }>
1515
scriptsElements: Map<string, { type: string, props: Record<string, any> }>
1616
}
1717

18-
export const RendererContext = createContext<{ cache: RendererCache }>({
19-
cache: {
18+
export const RendererContext = createContext<{ storage: RenderStorage }>({
19+
storage: {
2020
headElements: new Map(),
2121
scriptsElements: new Map()
2222
}

framework/react/head.ts

Lines changed: 24 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { PropsWithChildren, ReactElement, ReactNode } from 'https://esm.sh/react'
2-
import { Children, createElement, Fragment, isValidElement, useContext, useEffect } from 'https://esm.sh/react'
1+
import type { PropsWithChildren, ReactNode } from 'https://esm.sh/react'
2+
import { Children, Fragment, isValidElement, useContext, useEffect } from 'https://esm.sh/react'
33
import util from '../../shared/util.ts'
44
import { RendererContext } from './context.ts'
55
import Script from './script.ts'
@@ -8,7 +8,7 @@ export default function Head(props: PropsWithChildren<{}>) {
88
const renderer = useContext(RendererContext)
99

1010
if (window.Deno) {
11-
parse(props.children).forEach(({ type, props }, key) => renderer.cache.headElements.set(key, { type, props }))
11+
parse(props.children).forEach(({ type, props }, key) => renderer.storage.headElements.set(key, { type, props }))
1212
}
1313

1414
useEffect(() => {
@@ -62,64 +62,6 @@ export default function Head(props: PropsWithChildren<{}>) {
6262
return null
6363
}
6464

65-
interface SEOProps {
66-
title?: string
67-
description?: string
68-
keywords?: string | string[]
69-
url?: string
70-
image?: string
71-
twitter?: {
72-
card?: 'summary' | 'summary_large_image' | 'app' | 'player'
73-
site?: string
74-
creator?: string
75-
}
76-
}
77-
78-
export function SEO(props: SEOProps) {
79-
const { title, description, keywords, url, image, twitter } = props
80-
return createElement(
81-
Head,
82-
undefined,
83-
title && createElement('title', undefined, title),
84-
description && createElement('meta', { name: 'description', content: description }),
85-
keywords && createElement('meta', { name: 'keywords', content: util.isArray(keywords) ? keywords.join(',') : keywords }),
86-
title && createElement('meta', { name: 'og:title', content: title }),
87-
description && createElement('meta', { name: 'og:description', content: description }),
88-
title && createElement('meta', { name: 'twitter:title', content: title }),
89-
description && createElement('meta', { name: 'twitter:description', content: description }),
90-
url && createElement('meta', { name: 'og:url', content: url }),
91-
image && createElement('meta', { name: 'og:image', content: image }),
92-
image && createElement('meta', { name: 'twitter:image', content: image }),
93-
image && createElement('meta', { name: 'twitter:card', content: twitter?.card || 'summary_large_image' }),
94-
twitter?.site && createElement('meta', { name: 'twitter:site', content: twitter.site }),
95-
twitter?.creator && createElement('meta', { name: 'twitter:creator', content: twitter.creator }),
96-
)
97-
}
98-
99-
interface ViewportProps {
100-
width?: number | 'device-width'
101-
height?: number | 'device-height'
102-
initialScale?: number
103-
minimumScale?: number
104-
maximumScale?: number
105-
userScalable?: 'yes' | 'no'
106-
targetDensitydpi?: number | 'device-dpi' | 'low-dpi' | 'medium-dpi' | 'high-dpi'
107-
}
108-
109-
export function Viewport(props: ViewportProps) {
110-
const content = Object.entries(props)
111-
.map(([key, value]) => {
112-
key = key.replace(/[A-Z]/g, c => '-' + c.toLowerCase())
113-
return `${key}=${value}`
114-
})
115-
.join(',')
116-
return createElement(
117-
Head,
118-
undefined,
119-
content && createElement('meta', { name: 'viewport', content })
120-
)
121-
}
122-
12365
function parse(node: ReactNode, els: Map<string, { type: string, props: Record<string, any> }> = new Map()) {
12466
Children.forEach(node, child => {
12567
if (!isValidElement(child)) {
@@ -131,10 +73,6 @@ function parse(node: ReactNode, els: Map<string, { type: string, props: Record<s
13173
case Fragment:
13274
parse(props.children, els)
13375
break
134-
case SEO:
135-
case Viewport:
136-
parse((type(props) as ReactElement).props.children, els)
137-
break
13876
case Script:
13977
type = "script"
14078
case 'base':
@@ -144,32 +82,30 @@ function parse(node: ReactNode, els: Map<string, { type: string, props: Record<s
14482
case 'style':
14583
case 'script':
14684
case 'no-script':
147-
{
148-
let key = type
149-
if (type === 'meta') {
150-
const propKeys = Object.keys(props).map(k => k.toLowerCase())
151-
if (propKeys.includes('charset')) {
152-
return // ignore charset, always use utf-8
153-
}
154-
if (propKeys.includes('name')) {
155-
key += `[name=${JSON.stringify(props['name'])}]`
156-
} else if (propKeys.includes('property')) {
157-
key += `[property=${JSON.stringify(props['property'])}]`
158-
} else if (propKeys.includes('http-equiv')) {
159-
key += `[http-equiv=${JSON.stringify(props['http-equiv'])}]`
160-
} else {
161-
key += Object.keys(props).filter(k => !(/^content|children$/i.test(k))).map(k => `[${k.toLowerCase()}=${JSON.stringify(props[k])}]`).join('')
162-
}
163-
} else if (type !== 'title') {
164-
key += '-' + (els.size + 1)
85+
let key = type
86+
if (type === 'meta') {
87+
const propKeys = Object.keys(props).map(k => k.toLowerCase())
88+
if (propKeys.includes('charset')) {
89+
return // ignore charset, always use utf-8
16590
}
166-
// remove the children prop of base/meta/link
167-
if (['base', 'meta', 'link'].includes(type) && 'children' in props) {
168-
const { children, ...rest } = props
169-
els.set(key, { type, props: rest })
91+
if (propKeys.includes('name')) {
92+
key += `[name=${JSON.stringify(props['name'])}]`
93+
} else if (propKeys.includes('property')) {
94+
key += `[property=${JSON.stringify(props['property'])}]`
95+
} else if (propKeys.includes('http-equiv')) {
96+
key += `[http-equiv=${JSON.stringify(props['http-equiv'])}]`
17097
} else {
171-
els.set(key, { type, props })
98+
key += Object.keys(props).filter(k => !(/^content|children$/i.test(k))).map(k => `[${k.toLowerCase()}=${JSON.stringify(props[k])}]`).join('')
17299
}
100+
} else if (type !== 'title') {
101+
key += '-' + (els.size + 1)
102+
}
103+
// remove the children prop of base/meta/link
104+
if (['base', 'meta', 'link'].includes(type) && 'children' in props) {
105+
const { children, ...rest } = props
106+
els.set(key, { type, props: rest })
107+
} else {
108+
els.set(key, { type, props })
173109
}
174110
break
175111
}

framework/react/hooks.ts

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { AsyncUseDenoError } from './error.ts'
88
/**
99
* `useRouter` allows you to use `RouterURL` obeject of routing
1010
*
11-
* ```javascript
11+
* ```tsx
1212
* export default function App() {
1313
* const { locale, pathname, pagePath, params, query } = useRouter()
1414
* return <p>{pathname}</p>
@@ -22,10 +22,10 @@ export function useRouter(): RouterURL {
2222
/**
2323
* `withRouter` allows you to use `useRouter` hook with class component.
2424
*
25-
* ```javascript
25+
* ```tsx
2626
* class MyComponent extends React.Component {
2727
* render() {
28-
* return <p>{this.props.version.deno}</p>
28+
* return <p>{this.props.router.pathname}</p>
2929
* }
3030
* }
3131
* export default withRouter(MyComponent)
@@ -34,72 +34,78 @@ export function useRouter(): RouterURL {
3434
export function withRouter<P>(Component: ComponentType<P>) {
3535
return function WithRouter(props: P) {
3636
const router = useRouter()
37-
return createElement(Component, { ...props, ...router })
37+
return createElement(Component, { ...props, router })
3838
}
3939
}
4040

4141
/**
4242
* `useDeno` allows you to use Deno runtime in build time(SSR).
4343
*
44-
* ```javascript
44+
* ```tsx
4545
* export default function App() {
4646
* const version = useDeno(() => Deno.version)
4747
* return <p>{version.deno}</p>
4848
* }
4949
* ```
50+
*
51+
* @param {Function} callback - hook callback.
52+
* @param {number} revalidate - revalidate duration in seconds.
5053
*/
51-
export function useDeno<T = any>(callback: () => (T | Promise<T>)): T {
52-
const id = arguments[1] // generated by compiler
54+
export function useDeno<T = any>(callback: () => (T | Promise<T>), revalidate?: number): T {
55+
const id = arguments[2] // generated by compiler
5356
const { pathname } = useRouter()
5457
return useMemo(() => {
5558
const global = window as any
56-
const useDenoUrl = `useDeno://${pathname}`
57-
const { [`__asyncData_${useDenoUrl}`]: asyncData } = global
58-
const key = `${useDenoUrl}#${id}`
59-
if (asyncData && key in asyncData) {
60-
return asyncData[key]
59+
const dataUrl = 'data://' + pathname
60+
const eventName = 'useDeno-' + dataUrl
61+
const { ['rendering-' + dataUrl]: renderingData } = global
62+
const key = dataUrl + '#' + id
63+
const expires = revalidate ? Date.now() + revalidate * 1000 : 0
64+
if (renderingData && key in renderingData) {
65+
return renderingData[key]
6166
} else if (typeof Deno !== 'undefined' && Deno.version.deno) {
62-
const ret = callback()
63-
if (ret instanceof Promise) {
64-
events.emit(useDenoUrl, id, ret.then(data => {
65-
if (asyncData) {
66-
asyncData[key] = data
67+
const v = callback()
68+
if (v instanceof Promise) {
69+
events.emit(eventName, id, v.then(value => {
70+
if (renderingData) {
71+
renderingData[key] = value
6772
}
68-
events.emit(useDenoUrl, id, data)
69-
}), true)
70-
throw new AsyncUseDenoError('async useDeno')
73+
events.emit(eventName, id, { value, expires })
74+
}))
75+
// thow an `AsyncUseDenoError` to break current rendering, then re-render
76+
throw new AsyncUseDenoError()
7177
} else {
72-
if (asyncData) {
73-
asyncData[key] = ret
78+
if (renderingData) {
79+
renderingData[key] = v
7480
}
75-
events.emit(useDenoUrl, id, ret)
76-
return ret
81+
events.emit(eventName, id, { value: v, expires })
82+
return v
7783
}
7884
}
79-
return global[key] || null
85+
return global[key].value || null
8086
}, [pathname])
8187
}
8288

8389
/**
8490
* `withDeno` allows you to use `useDeno` hook with class component.
8591
*
86-
* ```javascript
92+
* ```tsx
8793
* class MyComponent extends React.Component {
8894
* render() {
89-
* return <p>{this.props.version.deno}</p>
95+
* return <p>{this.props.deno.version}</p>
9096
* }
9197
* }
92-
* export default withDeno(() => Deno.version)(MyComponent)
98+
* export default withDeno(() => ({ version: Deno.version.deno }))(MyComponent)
9399
* ```
100+
*
101+
* @param {Function} callback - hook callback.
102+
* @param {number} revalidate - revalidate duration in seconds.
94103
*/
95-
export function withDeno<T>(callback: () => (T | Promise<T>)) {
104+
export function withDeno<T>(callback: () => (T | Promise<T>), revalidate?: number) {
96105
return function <P extends T>(Component: ComponentType<P>): ComponentType<Exclude<P, keyof T>> {
97106
return function WithDeno(props: Exclude<P, keyof T>) {
98-
const denoProps = useDeno(callback)
99-
if (typeof denoProps === 'object') {
100-
return createElement(Component, { ...props, ...denoProps })
101-
}
102-
return createElement(Component, props)
107+
const deno = useDeno<T>(callback, revalidate)
108+
return createElement(Component, { ...props, deno })
103109
}
104110
}
105111
}

0 commit comments

Comments
 (0)