feat: implement dynamic page loading and server components

This commit is contained in:
2026-04-14 10:09:00 +08:00
parent 11e75d0e11
commit f22899e424
5 changed files with 220 additions and 15 deletions

View File

@@ -9,6 +9,7 @@ import {
import type { ReactFormState } from 'react-dom/client'
import { Root } from '../root.tsx'
import { parseRenderRequest } from './request.tsx'
import type { ComponentType } from 'react'
// The schema of payload which is serialized into RSC stream on rsc environment
// and deserialized on ssr/client environments.
@@ -23,6 +24,47 @@ export type RscPayload = {
formState?: ReactFormState
}
// Dynamic page module loading - all pages are lazy loaded on demand
const pageModules = import.meta.glob('../pages/**/*.tsx', { eager: false })
// Find page component by pathname - dynamically resolves paths
async function findPage(pathname: string): Promise<ComponentType<any> | null> {
const segments = pathname.split('/').filter(Boolean)
// Build candidate paths to try
const candidates: string[] = []
if (segments.length === 0) {
// Root path: try index.tsx
candidates.push('../pages/index.tsx')
} else {
// Try original case path: /a -> pages/a/index.tsx, /blog -> pages/blog/index.tsx
const originalPath = `../pages/${segments.join('/')}/index.tsx`
candidates.push(originalPath)
// Try capitalized file: /pageA -> pages/PageA.tsx
const capitalizedPath = `../pages/${segments.map(s =>
s.charAt(0).toUpperCase() + s.slice(1)
).join('/')}.tsx`
candidates.push(capitalizedPath)
// Try capitalized index route: /a -> pages/A/index.tsx
const capitalizedIndexPath = `${capitalizedPath.replace('.tsx', '')}/index.tsx`
candidates.push(capitalizedIndexPath)
}
// Find first matching module
for (const candidate of candidates) {
const loader = pageModules[candidate]
if (loader) {
const mod = await loader() as { default: ComponentType<any> }
return mod.default
}
}
return null
}
// the plugin by default assumes `rsc` entry having default export of request handler.
// however, how server entries are executed can be customized by registering own server handler.
export default { fetch: handler }
@@ -73,12 +115,21 @@ async function handler(request: Request): Promise<Response> {
}
}
// Dynamic page resolution based on URL pathname
const pathname = renderRequest.url.pathname
const Page = await findPage(pathname)
// If no page found and it's not the root path, return 404
if (!Page && pathname !== '/') {
return new Response('Not Found: ' + pathname, { status: 404 })
}
// serialization from React VDOM tree to RSC stream.
// we render RSC stream after handling server function request
// so that new render reflects updated state from server function call
// to achieve single round trip to mutate and fetch from server.
const rscPayload: RscPayload = {
root: <Root url={renderRequest.url} />,
root: <Root url={renderRequest.url} page={Page ? <Page /> : undefined} />,
formState,
returnValue,
}

9
src/pages/a/index.tsx Normal file
View File

@@ -0,0 +1,9 @@
'use server';
export default async function List() {
return (
<div>
<h1>List A</h1>
</div>
);
}

10
src/pages/b/index.tsx Normal file
View File

@@ -0,0 +1,10 @@
'use server';
export default async function List() {
return (
<div>
<h1>List B</h1>
<div>123</div>
</div>
);
}

View File

@@ -5,7 +5,7 @@ import reactLogo from './assets/react.svg'
import { ClientCounter } from './client.tsx'
import List from './List'
export function Root(props: { url: URL }) {
export function Root(props: { url: URL; page?: React.ReactNode }) {
return (
<html lang="en">
<head>
@@ -21,7 +21,7 @@ export function Root(props: { url: URL }) {
)
}
function App(props: { url: URL }) {
function App(props: { url: URL; page?: React.ReactNode }) {
return (
<div id="root">
<div>
@@ -36,18 +36,8 @@ function App(props: { url: URL }) {
</a>
</div>
<h1>Vite + RSC</h1>
<div className="card">
<ClientCounter />
</div>
<div className="card">
<form action={updateServerCounter.bind(null, 1)}>
<button>Server Counter: {getServerCounter()}</button>
</form>
</div>
<div className="card">Request URL: {props.url?.href}</div>
<div className="card">
<List />
</div>
{/* Dynamic page content */}
{props.page || <DefaultHome />}
<ul className="read-the-docs">
<li>
Edit <code>src/client.tsx</code> to test client HMR.
@@ -73,3 +63,23 @@ function App(props: { url: URL }) {
</div>
)
}
// Default home page content when no dynamic page is provided
function DefaultHome() {
return (
<>
<div className="card">
<ClientCounter />
</div>
<div className="card">
<form action={updateServerCounter.bind(null, 1)}>
<button>Server Counter: {getServerCounter()}</button>
</form>
</div>
<div className="card">Request URL: Home</div>
<div className="card">
<List />
</div>
</>
)
}