跳到主要内容

配置

evjs 默认零配置。多数应用只需要在 ev.config.ts 中选择 SPA 或 MPA 文件路由,并配置服务端/runtime 能力。只有页面文件约定无法描述目标输出时,才使用更底层的 app 和 page 输出配置。

import { defineConfig } from "@evjs/ev";

export default defineConfig({
routing: {
mode: "spa",
},
});

默认值

配置默认值
entry./src/main.tsx
html./index.html
routing.modespa
dev.port3000
server.dev.port3001
server.basePath/__evjs
服务端函数端点${server.basePath}/fn

服务端函数端点从 server.basePath 派生,没有单独的公开函数端点配置。

顶层 config object 只接受 entryhtmldevservertransportapproutingremotesremotebundlerpluginspages。 生成的 app 声明、页面路由运行时接线、server-function endpoint 等框架 metadata 都由 evjs 派生,不需要也不能直接配置。

路由

src/pages 是主要客户端路由模型。SPA 模式会从页面文件构建一个 框架托管的应用:

export default defineConfig({
routing: {
mode: "spa",
dir: "./src/pages",
mount: "#app",
},
});

MPA 模式使用同一套文件,但每个路由输出独立页面,不引入客户端路由器:

export default defineConfig({
routing: {
mode: "mpa",
},
});

当项目存在 src/pages,且项目没有声明显式的 apppagesremote 配置时,SPA 路由会自动启用。 需要显式关闭文件路由发现时,设置 routing: false。 导出的 config 必须是 object。启用并配置选项时,routing 必须是 object;array 和 null 都会被拒绝。

SPA 模式可以使用根布局模块。默认情况下,evjs 会在路由目录旁边查找 layout/index.tsx,例如 src/pages 对应 src/layout/index.tsx。如果迁移应用的 外框在其他位置,或者使用 layout/index.jsx 这类非 TSX 布局模块,可以通过 routing.layout 显式指定模块路径。显式布局模块必须是源码模块,不能是声明文件、 测试/spec、Storybook、client-only 或 server-only 文件;如果 SPA 不需要框架根布局,可以设置为 false

export default defineConfig({
routing: {
mode: "spa",
layout: "./src/shell/AppLayout.tsx",
},
});

MPA 模式不支持 routing.layout。MPA 页面需要共享外框时,应像普通 React 代码一样组合共享组件; 如果只是文档外壳相同,可以复用页面 HTML 模板。

routing.mode 必须是 spampa。提供 routing.dirrouting.htmlrouting.mount 时,它们必须是非空字符串。routing.layout 必须是 false 或非空模块路径。

只有手动 bootstrap 单应用时,才使用顶层 entry / html。使用 src/pages 的应用不应该额外手写客户端 router 或 framework bootstrap:

export default defineConfig({
entry: "./src/main.tsx",
html: "./index.html",
});

提供顶层 entryhtml 时,它们必须是非空字符串。更底层的 app 声明中,字符串写法或 { source } 指向 lifecycle module;{ entry, html?, mount? } 指向自行 bootstrap 的浏览器入口:

export default defineConfig({
app: {
entry: "./src/main.tsx",
html: "./index.html",
mount: "#app",
},
});

app 必须是字符串模块路径或 object;null 和 array 会被拒绝。object 形式的 app 必须且只能指定 sourceentry 之一。提供 sourceentryhtmlmount 时,它们必须是非空字符串。object 形式的 app 只接受 sourceentryhtmlmount。 顶层 htmlapp.htmlrouting.htmlpages.*.html 中配置的 HTML 模板必须指向文件,并会在 bundler 运行之前被校验。配置对象声明了 mount 时,该 selector 必须能在对应 HTML 模板中匹配到元素。共享模板是允许的, 每个声明的 mount selector 都会独立校验。

页面

pages 是独立页面输出和非约定式路由的显式底层 API。当页面集合直接来自 src/pages 时,优先使用 routing: { mode: "mpa" }。字符串页面是 framework-managed React 组件模块的简写;只有页面需要自己控制 bootstrap 时才使用 { entry }

export default defineConfig({
pages: {
home: "./src/pages/Home.tsx",
about: {
entry: "./src/pages/about/main.tsx",
html: "./src/pages/about/index.html",
},
},
});

{ component } 对象写法等价于字符串简写;当页面需要 pathhtmlmount 时使用对象写法:

export default defineConfig({
pages: {
dashboard: {
path: "/dashboard",
component: "./src/pages/dashboard/Page.tsx",
html: "./src/pages/public.html",
mount: "#app",
},
},
});

Component page 对象也可以直接声明 framework render metadata:

export default defineConfig({
pages: {
dashboard: {
path: "/dashboard",
component: "./src/pages/dashboard/Page.tsx",
render: "ssr",
hydrate: "visible",
},
},
});

pages 必须是 object map。Page id 必须是非空 build identifier:只能使用字母、 数字、下划线或连字符,不能包含路径分隔符。每个 page value 必须是字符串模块路径或 page object,并且每个页面必须且只能指定一种模块契约:entrycomponentapp。这些模块路径必须是非空字符串。Page object 只接受 pathentrycomponentapphtmlmountrenderhydrateprerenderrsc。 提供 path 时必须以 / 开头,并且不能和其他显式页面重复。动态参数名不会产生不同的 URL shape,所以 /users/:id/users/:userId 会冲突。它是 URL pathname,因此不能包含空白字符、 query string 或 hash。htmlmount 在提供时也必须是非空字符串。Page id 会参与生成 build entry name 和 HTML 文件名,因此不能与 app entry 或其他页面输出冲突。

// src/pages/dashboard/Page.tsx
export const render = "ssr";
export const hydrate = "load";

export default function DashboardPage() {
return <main>Dashboard</main>;
}

配置了 path 时,该页面也会贡献 framework route。SSR、SSG、PPR 等由框架服务端处理的页面应把 URL 和 component 放在配置里。 Rendering metadata 可以放在 component page config 中,也可以作为组件模块的 static exports。配置里显式声明的字段优先;未声明的字段会继续从 static exports 中补齐。未配置 path 时,页面会输出为 campaign.html 这样的 HTML 文档。 Route-derived page id 必须唯一;例如 /admin/panel/admin_panel 都会派生出 admin_panel 时,evjs 会报告冲突。生成的 id 会冲突时,请重命名其中一个 route 文件,或改用显式 pages 配置并提供唯一 page id。

Page Module 静态导出

当对应的 component page config 字段没有声明时,evjs 会从 framework-managed page module 中读取以下 named static exports。请使用字面量值,这样 graph analysis 不需要执行用户代码也能解析。 无效的字面量值会在 app graph analysis 阶段报错,并且发生在 bundling 之前。 PPR 不是独立的 render 值;请使用 render = "ssr" 搭配 prerender = { partial: true }prerender 对象只能包含 partialdeliveryrevalidate,并且至少要声明 其中一个属性。revalidate 必须是 false 或表示秒数的正整数。没有选项的 full prerendering 请使用 true。 Analyzer 支持直接的 export const 声明,也支持本地 export specifier, 例如 const mode = "ssr"; export { mode as render };。Page metadata 不会跟随 来自其他模块的 re-export;把 metadata 名称从其他模块重新导出会被报告为无效。 运行时 metadata export 必须是带静态 initializer 的本地变量; export let render; 这类未初始化声明、function export 和 class export 都是无效的。 export type { mode as render } 这类 type-only export 以及 export declare const render: "ssr" 这类 ambient declaration 都会被忽略, 因为它们不会产生运行时值。每个 metadata 名称只能导出一次;重复的 renderhydrateprerenderrsc 导出是 graph-analysis 错误,而不是 last-write-wins。

导出可选值含义
render"csr"客户端渲染页面。页面在浏览器中 mount,不生成 server document renderer。省略 render 时默认是该模式。
render"ssr"服务端渲染 document。框架服务端为请求生成 HTML,然后浏览器按 hydrate 策略 hydration。需要启用 server
render"ssg"静态 document 意图。manifest 会把页面标记为 fully prerendered/static,默认 hydration mode 是 none。不需要动态服务端能力时,deployment adapter 可以把它作为静态 HTML 服务。
hydrate"none"不对整页做浏览器 hydration。适合静态页面、RSC document,或通过显式 islands/regions 建模交互的 PPR shell。
hydrate"load"页面 runtime 加载后 hydration。非 SSG 的 server-rendered 页面默认是该模式。
hydrate"visible"声明 mount point 可见后再 hydration。不支持 visibility scheduling 的 runtime/adapter 可以回退到 load
hydrate"idle"声明浏览器空闲时再 hydration。不支持 idle scheduling 的 runtime/adapter 可以回退到 load
prerendertrue标记非 CSR 页面可 full prerender,但不启用 partial prerendering。manifest 会输出 rendering.prerender = "full";如果初始 HTML 需要静态交付,请使用 render = "ssg"
prerender{ partial: true }启用 PPR。框架会从页面树中的 Suspense + lazy(() => import(...)) boundary 推导动态 region。
prerender.delivery"merge"非流式 PPR delivery。服务端解析 shell 和 regions 后,返回一个完整 HTML response。partial prerendering 默认使用该模式。
prerender.delivery"stream"流式 PPR delivery。服务端可以先 flush shell,再把已完成的 regions patch 到同一个 response 中。
prerender.revalidate正整数声明 prerendered output 的 revalidation 间隔,单位是秒。
prerender.revalidatefalse声明 prerendered output 不自动 revalidate。
rsctrue启用 RSC 页面路径。需要和 render = "ssr" 一起使用。RSC document 默认使用 hydrate = "none";显式声明 loadvisibleidle hydration 会被拒绝。需要当前 bundler/server adapter 支持 server.rsc

rsc = false 会作为兼容性的 no-op 被接受,但会产生 warning。除非页面要改成 rsc = true 的 RSC 页面,否则请删除它。

渲染支持契约

页面渲染模式刻意保持收敛。不支持的组合会在 bundling 之前失败,因此 deployment adapter 可以信任 manifest:

能力必需页面契约SPA document 输出MPA document 输出Server/runtime 要求不支持的组合
CSR省略 render,或导出 render = "csr"App HTML fallback每个页面一个 HTML document不需要 server
SSRrender = "ssr"route-owned server documentroute-owned server document,不输出静态 HTML 文件Framework server document routeserver: false
SSGrender = "ssg"App HTML fallback,并为 route page 记录 static metadata独立静态 HTML document生成和 manifest linking 阶段需要 server buildserver: false
PPRcomponent page 上声明 render = "ssr" + prerender = { partial: true }带服务端合成 regions 的 route-owned server document带服务端合成 regions 的 route-owned server documentFramework server document route,另有可选 runtime.server.ppr direct/debug endpoint同页 RSC、整页 hydration entry、server: false
RSCcomponent page 上声明 render = "ssr" + rsc = trueroute-owned server document 加 RSC Flight endpointroute-owned server document 加 RSC Flight endpointFramework server document route 加 runtime.server.rsc同页 PPR、hydrate 不是 "none"server: false
Remote app buildremote config 配置一个或多个 entries不是 page render mode不是 page render modeRemote manifest/assets;只有 remote 同时声明 server-rendered pages 时才需要 serverremote 当作 host remotes

如果同一个页面同时需要 RSC 数据流和 partial prerendered regions,当前请拆成不同 page routes。单个 component page 必须在 rsc = trueprerender = { partial: true } 之间二选一。

server: false 只适用于 CSR 页面、MPA client entries、remote-only build 和静态资源。 SSR、SSG、PPR、RSC、server functions 和 server routes 都要求构建时启用 framework server。

PPR 页面推荐在页面组件树中声明动态 region:

export default defineConfig({
pages: {
campaign: {
path: "/campaign",
component: "./src/pages/campaign/Page.tsx",
},
},
});
import { lazy, Suspense } from "react";

export const render = "ssr";
export const hydrate = "none";
export const prerender = {
partial: true,
delivery: "stream",
} as const;

const OfferRegion = lazy(() => import("./Offer.region"));

export default function CampaignPage() {
return (
<Suspense fallback={<p>Loading</p>}>
<OfferRegion />
</Suspense>
);
}
// ./Offer.region.tsx
export const cache = { revalidate: 60 } as const;
export const hydrate = "none";

export default function OfferRegion() {
return <section>Live offer inventory</section>;
}

框架会分析 page module,并把 Suspense lazy boundary 转成内部 region renderer。 Region id 会从 lazy 组件名派生,因此 OfferRegion 会变成 offer

Region module 可以声明以下静态导出:

导出可选值含义
cache"no-store"每次都动态渲染 region。适合请求相关或用户相关数据。
cache{ revalidate: 正整数 }缓存 region output,并在给定秒数后 revalidate。
hydrate"none"不在浏览器中 hydrate region。server-only region 默认使用该模式。
hydrate"load"region client runtime 加载后 hydration。
hydrate"visible"声明 region 可见后 hydration。不支持 visibility scheduling 的 runtime 可以回退到 load
hydrate"idle"声明 region 在浏览器空闲时 hydration。不支持 idle scheduling 的 runtime 可以回退到 load

无效的 region 静态导出字面量会在 graph analysis 阶段、bundling 之前报错, 与 page module metadata 的校验保持一致。 Region metadata 也遵循和 page metadata 相同的 runtime export 规则: runtime export 必须是带静态 initializer 的本地变量。重新导出的 metadata、 function export 和 class export 都是无效的;type-only export 和 ambient declare 声明会被忽略。每个 region metadata 名称只能导出一次;重复的 cachehydrate 导出是 graph-analysis 错误。

框架合成 PPR page response 时,会根据 region cache 策略派生默认 Cache-Control。只要任意 region 是 "no-store" 或省略 cache,page response 默认使用 no-store;如果所有 regions 都声明 { revalidate },page response 默认使用 最小 region s-maxage。如果 shell renderer 自己返回了 Cache-Control,框架会保留它。 服务端 adapter 可以在运行时设置 framework.ppr.staleWhileRevalidate。设置后,可缓存的 PPR region response 和合成后的 page response 会包含 stale-while-revalidate, 过期但仍在 stale 窗口内的 region entry 会先返回给请求,同时由框架在后台刷新缓存。

prerender.delivery 控制初始 document response。"merge" 是默认非流式模式: 框架服务端先渲染 shell 和 regions,再返回完整 HTML。"stream" 会先发送 shell, 再在同一个 HTML response 中把已完成的 regions patch 到页面里。两种模式的首屏 导航都不要求浏览器主动请求 /__evjs/ppr

PPR 页面由服务端合成,不会生成整页客户端 hydration entry。需要交互能力的 PPR 页面应显式建模为 client islands 或 region-level hydration,而不是 hydrate 整个 page shell。

RSC 页面使用 SSR document render mode,并通过 rsc = true 显式开启 RSC:

export default defineConfig({
pages: {
insights: {
path: "/insights",
component: "./src/pages/Insights.tsx",
},
},
server: {
rsc: true,
},
});
// src/pages/Insights.tsx
export const render = "ssr";
export const rsc = true;
export const hydrate = "none";

export default function InsightsPage() {
return <main>Insights</main>;
}

RSC 页面可以省略 hydrate = "none",因为这是 RSC document 的默认值。 如果显式声明 hydrate,则必须是 "none";RSC document 不支持整页浏览器 hydration 模式。 RSC Flight response 默认使用 Cache-Control: no-store,因为它可能依赖 request 状态和服务端数据。RSC renderer 显式返回的 Cache-Control 会被保留。 RSC 页面不能同时声明 partial prerendering;在组合 runtime contract 可用之前, 请把 RSC 和 PPR 行为拆到不同 page routes。

当前 webpack validation adapter 已经覆盖完整 RSC 请求链路。默认 Utoopack adapter 仍需要补齐等价的 client/server reference metadata 后,才能运行同样路径。

react-server-dom-webpack 是 evjs client 和 server runtime 的可选 peer dependency。直接使用 RSC 的应用需要安装它,或者使用提供 RSC runtime path 的 bundler/server adapter。

服务端渲染的 RSC document 会包含一个很小的 __EVJS_RSC_BOOTSTRAP__ payload, 用于告诉 client runtime Flight endpoint、page id、mount selector、public path、页面 assets 和可选 page route metadata。client runtime 会在请求 Flight 数据前校验该 payload;JSON 格式错误、非法 build/page identifier、非法 public path、格式错误的页面 assets 或缺少必填字段都会报告为启动错误。自定义 runtime 调用 startReactRscPageRuntime({ document }) 时,会用这个 document 同时查找 bootstrap 和解析 mount selector。

服务端

纯 CSR 可以禁用服务端:

export default defineConfig({ server: false });

server: false 时:

  • 构建输出为扁平 dist/
  • "use server" 模块会成为构建错误;
  • dev 模式不会配置框架服务端代理。

框架服务端边界默认是 /__evjs。只有部署平台要求固定路径时,才需要配置 server.basePath

export default defineConfig({
server: {
entry: "./src/server.ts",
dev: {
port: 3001,
https: false,
},
},
});

提供 devserverserver.devtransport 时,它们都必须是 object;使用 server: false 关闭框架服务端。提供 server.entry 时,它必须是非空模块路径; evjs 会在 app graph analysis 阶段、bundler 运行之前校验 server.entry 等已配置的 source path。server.basePath 必须是以 / 开头的非空 URL pathname,不能包含空白字符、query string 或 hash;尾部 / 会被归一化移除。如果 server.rsc 配置为 object, server.rsc.endpoint 也遵循同样的 URL pathname 规则。dev.httpsserver.dev.https 中的 key/cert 值必须是非空字符串,HTTPS object config 不能是 null 或 array。dev.portserver.dev.port 必须是 165535 之间的 TCP 端口整数。

派生路径:

/__evjs/fn 服务端函数
/__evjs/ppr 存在 PPR 页面时的 region direct/debug endpoint
/__evjs/rsc 启用 server.rsc 时的 Flight endpoint

PPR 页面首屏不会要求浏览器调用 /__evjs/ppr;框架服务端在服务 page route 时解析 declared regions。direct/debug region 调用必须精确使用 GET /__evjs/ppr/<pageId>/<regionId>pageIdregionId 使用 build identifier 规则,多余 path segment 不会匹配。 成功的 RSC page model 响应必须使用 Content-Type: text/x-component, 可以附带 content-type 参数。 客户端 RSC debug JSON helper 只会解析以 Content-Type: application/json 返回的响应,可以附带 content-type 参数。 Debug payload 必须使用 version: 1type: "evjs.rsc",包含符合 build identifier 规则的 buildId,并提供结构正确的 asset 列表,loadRscDebugPage() 才会挂载诊断 HTML。

只有当浏览器需要调用另一个 origin 上的框架服务端时,才配置 transport.baseUrl

export default defineConfig({
transport: {
baseUrl: "https://api.example.com",
},
});

提供 transport 时它必须是 object。提供 transport.baseUrl 时,它必须是 absolute HTTP(S) URL,且不能包含首尾空白字符。

启用 framework server 时,用户配置的 dev.proxy 规则会排在框架代理之前。每条规则都 必须是 object,包含非空的 context pathname pattern 数组,以及作为 absolute HTTP(S) URL 的 targetnull 和 array entry 会被拒绝。Context pattern 必须以 / 开头,不能包含空白字符、query string 或 hash,并且同一条规则内不能重复。Target 不能包含首尾空白字符。可选的 changeOriginsecure 必须是布尔值。

远程应用

远程应用通过 manifest 加载。Host 应用声明复数 remotes;当前包要输出 remote manifest 时声明单数 remote

export default defineConfig({
remotes: {
crm: {
manifest: "https://assets.example.com/crm/evjs-remote.json",
activeWhen: ["/app/crm/*"],
},
},
});

remotes 必须是从 remote id 到 remote declaration object 的 object map。Remote id 必须是非空 build identifier:只能使用字母、数字、下划线或连字符。Remote declaration object 只接受 manifestactiveWhen。manifest URL 必须是非空 HTTP(S) URL 或 path,且不能包含首尾空白字符。 activeWhen 是 pathname pattern:每个条目都必须非空、以 / 开头,且不能包含空白字符、query string 或 hash。Host remote 之间不能声明完全重复的 pattern。Host runtime 会用 activeWhen 在浏览器导航时选择远程应用;当多个 pattern 同时匹配当前 URL pathname 时,最具体的 pattern 优先。

export default defineConfig({
server: false,
remote: {
name: "crm",
baseUrl: "https://assets.example.com/crm/",
entries: {
customers: {
app: "./src/remote.tsx",
activeWhen: ["/app/crm/*"],
mount: "#app",
},
},
},
});

Remote build 必须声明非空 build-identifier name,并至少包含一个 entry。remote 值必须是 object,且只接受 namebaseUrlsharedentriesremote.entries 必须是从 entry id 到 remote entry object 的 object map。Remote entry object 只接受 appactiveWhenmount。提供 remote.baseUrl 时,它必须是非空 HTTP(S) URL 或 path,且不能包含首尾空白字符, 因为该值会写入 evjs-remote.json。Remote entry id 也使用 build identifier 规则。每个 entry 都需要 非空 app 模块路径;可选的 activeWhen 是 pathname pattern,必须以 / 开头, 且不能包含空白字符、query string 或 hash;可选的 mount 在提供时也必须非空,且 不能包含首尾空白字符。Remote entry 之间不能声明完全重复的 activeWhen pattern,避免 activation 依赖对象顺序;重叠 pattern 会按最具体 pathname 匹配解析。Remote-only build 可以使用 server: false,因为 remote entry 是浏览器运行时模块,不是页面 render mode。每个 remote entry 会由默认 Utoopack adapter 包装成 generated lifecycle entry;当 remote build 还需要更底层的 SSR/PPR/RSC 行为验证时,webpack 仍可作为验证 adapter。每个 remote entry 都必须产出客户端 JavaScript 资产;如果 bundler 无法关联到对应资产, manifest emission 会失败,而不是写出不可用的 evjs-remote.jsonremote.shared 必须是 object map;key 必须非空,并且每个依赖值都必须是 object。Shared dependency object 只接受 shareKeyrequiredVersionsingletonstrictVersioneager。可选的 shareKeyrequiredVersion 必须是非空字符串,且不能包含首尾空白字符;提供 singletonstrictVersioneager 时,它们必须是布尔值。requiredVersion 接受 runtime negotiation 支持的 range 形式:精确或部分 semver version、*、caret 和 tilde range、>=18 <20 这样的 comparison range,以及 ^18 || ^19 这样的 OR range。 浏览器 runtime 会在 activation 前校验默认 fetch loader 或自定义 loadRemoteManifest 返回的 remote manifest,并在 response media type、name、entry id、module、asset metadata 无效时报告对应 manifest URL。默认 fetch loader 只接受成功的 Content-Type: application/json manifest response,允许附带可选 content-type 参数。 获取到的 manifest 字符串字段,例如 baseUrl、module href、asset href、mountshareKeyrequiredVersion,都不能包含首尾空白。baseUrl 可以是绝对 URL, 也可以相对 remote manifest URL;module 和 asset href 必须能从该 base URL 解析, 这样错误的 CDN 路径会在 activation 前失败。

插件

export default defineConfig({
plugins: [
{
name: "build-timer",
setup() {
const start = Date.now();
return {
buildEnd({ output }) {
console.log("Build", output.buildId, Date.now() - start);
},
};
},
},
],
});

plugins 必须是 plugin object 数组。每个 plugin 都需要非空 name,且不能包含首尾空白。 提供 dependenciesoptionalDependencies 时,它们必须是非空 plugin name 数组; enforce 必须是 prenormalpost。Plugin object 只接受 namedependenciesoptionalDependenciesenforceconfigsetup

更多 hook 签名、单 HTML 文档上下文和 bundler 辅助函数见 插件指南

Bundler

CLI 默认使用 Utoopack。也可以显式传入 adapter:

import { defineConfig } from "@evjs/ev";
import { utoopackAdapter } from "@evjs/bundler-utoopack";

export default defineConfig({
bundler: utoopackAdapter,
});

bundler 必须是 adapter object,并且包含非空 name 以及 build / dev 函数,且只接受这三个 key。null、array、未知 key 和不完整的 adapter object 会在 config resolution 阶段被拒绝,不会等到命令启动后才报错。

@evjs/bundler-webpack 主要用于框架验证,等待 Utoopack 底层 API 补齐时兜底。 Utoopack 仍是默认运行路径。