静态渲染

编辑页面

了解如何使用 Expo Router 将路由渲染为静态 HTML 和 CSS 文件。


For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

要在网页上启用搜索引擎优化(SEO),你必须对应用进行静态渲染。本指南将带你完成 Expo Router 应用的静态渲染过程。

使用静态渲染时,data loaders 会在构建过程中执行,并将其结果嵌入到输出的 HTML 文件中。

设置

1

在你项目的 app config 中启用静态渲染:

app.json
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "web": { "output": "static" } } }

2

启动开发服务器:

Terminal
npx expo start

生产环境

要为生产环境打包你的静态网站,请运行导出命令:

Terminal
npx expo export --platform web

这将创建一个包含静态渲染网站的 dist 目录。如果你在本地 public 目录中有文件,这些文件也会一并被复制过去。 你可以在本地测试生产构建,方法是运行以下命令并在浏览器中打开链接的 URL:

Terminal
npx serve dist

这个项目可以部署到几乎所有托管服务。请注意,这既不是单页应用,也不包含自定义服务器 API。这意味着动态路由(例如 src/app/[id].tsx)不会按预期正常工作。你可能需要构建一个无服务器函数来处理动态路由。

动态路由

static 输出会为每个路由生成 HTML 文件。这意味着动态路由(src/app/[id].tsx)开箱即用并不起作用。你可以使用 generateStaticParams 函数提前生成已知路由。

src/app/blog/[id].tsx
import { Text } from 'react-native'; import { useLocalSearchParams } from 'expo-router'; export async function generateStaticParams(): Promise<Record<string, string>[]> { const posts = await getPosts(); // 返回一个参数数组,用于生成静态 HTML 文件。 // 数组中的每一项都会成为一个新页面。 return posts.map(post => ({ id: post.id })); } export default function Page() { const { id } = useLocalSearchParams(); return <Text>文章 {id}</Text>; }

这会在 dist 目录中为每篇文章输出一个文件。例如,如果 generateStaticParams 方法返回 [{ id: "alpha" }, { id: "beta" }],则会生成以下文件:

dist
blog
  alpha.html
  beta.html

generateStaticParams

由 Expo CLI 在 Node.js 环境中于构建时求值的仅服务器函数。这意味着它可以访问 __dirnameprocess.cwd()process.env 等更多内容。它还可以访问进程中可用的每个环境变量。不过,前缀为 EXPO_PUBLIC_ 的值不会在浏览器环境中运行,因此它无法访问诸如 localStoragedocument 之类的浏览器 API。它也无法访问诸如 expo-cameraexpo-location 之类的原生 Expo API。

src/app/[id].tsx
export async function generateStaticParams(): Promise<Record<string, string>[]> { console.log(process.cwd()); return []; }

generateStaticParams 会从嵌套父级向子级级联传递。级联参数会传递给每一个导出 generateStaticParams 的动态子路由。

src/app/[id]/_layout.tsx
export async function generateStaticParams(): Promise<Record<string, string>[]> { return [{ id: 'one' }, { id: 'two' }]; }

现在,动态子路由将被调用两次,一次使用 { id: 'one' },另一次使用 { id: 'two' }。所有组合都必须被考虑到。

src/app/[id]/[comment].tsx
export async function generateStaticParams(params: { id: 'one' | 'two'; }): Promise<Record<string, string>[]> { const comments = await getComments(params.id); return comments.map(comment => ({ ...params, comment: comment.id, })); }

使用 process.cwd() 读取文件

由于 Expo Router 会将你的代码编译到一个单独的目录中,因此你不能使用 __dirname 来构造路径,因为它的值会与预期不同。

相反,请使用 process.cwd(),它会返回项目正在编译的目录。

src/app/[category].tsx
import fs from 'node:fs/promises'; import path from 'node:path'; export async function generateStaticParams(params: { id: string; }): Promise<Record<string, string>[]> { const directory = await fs.readdir(path.join(process.cwd(), './posts/')); const posts = directory.filter(fileOrSubDirectory => return path.extname(fileOrSubDirectory) === '.md') return [{ id, posts, }]; }

根 HTML

默认情况下,每个页面都会被一些简短的 HTML 脚手架包裹,这称为 根 HTML

你可以在项目中创建 src/app/+html.tsx 文件来自定义根 HTML 文件。该文件导出一个 React 组件,它只会在 Node.js 中运行,这意味着其中不能导入全局 CSS。该组件会包裹 app 目录中的所有路由。这对于添加全局 <head> 元素或禁用页面滚动非常有用。

Note: 全局上下文提供者应放在 Root Layout 组件中,而不是 Root HTML 组件中。

src/app/+html.tsx
import { ScrollViewStyleReset } from 'expo-router/html'; import { type PropsWithChildren } from 'react'; // 此文件仅用于 Web,并用于为每个 // 在静态渲染期间的网页配置根 HTML。 // 此函数的内容仅在 Node.js 环境中运行, // 无法访问 DOM 或浏览器 API。 export default function Root({ children }: PropsWithChildren) { return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta httpEquiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> {/* 禁用 Web 上的 body 滚动。这会使 ScrollView 组件的行为更接近原生端。 不过,在移动端 Web 上,body 滚动通常也很有用。如果你想启用它,请删除这一行。 */} <ScrollViewStyleReset /> {/* 添加你希望在 Web 上全局可用的任何额外 <head> 元素... */} </head> <body>{children}</body> </html> ); }
  • children 属性会包含根 <div id="root" /> 标签。
  • JavaScript 脚本会在静态渲染之后附加。
  • React Native Web 样式会自动进行静态注入。
  • 不要将全局 CSS 导入到这个文件中。应改为使用 Root Layout。Expo Router 会从根布局开始遍历依赖图,因此在其他地方导入 CSS 可能会导致意外的加载顺序,使 node_modules 中的 CSS 优先于你的自定义样式。
  • 诸如 window.location 之类的浏览器 API 在此组件中不可用,因为它只会在静态渲染期间于 Node.js 中运行。

expo-router/html

来自 expo-router/html 的导出与 Root HTML 组件相关。

  • ScrollViewStyleReset: 对于带有根 <ScrollView /> 的全屏 React Native web apps,应使用根样式重置,以确保与原生端保持一致。

Meta 标签

你可以使用 expo-router 中的 <Head /> 模块为页面添加 meta 标签:

src/app/about.tsx
import Head from 'expo-router/head'; import { Text } from 'react-native'; export default function Page() { return ( <> <Head> <title>我的博客网站</title> <meta name="description" content="这是我的博客。" /> </Head> <Text>关于我的博客</Text> </> ); }

可以使用相同的 API 动态更新 head 元素。不过,为了 SEO,提前静态渲染 head 元素会很有用。

静态文件

Expo CLI 支持一个根 public 目录,在静态渲染期间会被复制到 dist 目录。这对于添加图片、字体和其他资源等静态文件非常有用。

public
favicon.ico
logo.png
.well-known
  apple-app-site-association
某些路径(例如 /assets)是 Metro 预留的。请避免将文件放在 public/assets/ 或其他预留路径中。完整列表请参见 Reserved paths

这些文件会在静态渲染期间被复制到 dist 目录:

dist
index.html
favicon.ico
logo.png
.well-known
  apple-app-site-association
_expo
  static
   js
    index-xxx.js
   css
    index-xxx.css
仅限 Web:静态资源可以在运行时代码中通过相对路径访问。例如,logo.png 可以通过 /logo.png 访问:
src/app/index.tsx
import { Image } from 'react-native'; export default function Page() { return <Image source={{ uri: '/logo.png' }} />; }

字体

Expo Font 在 Expo Router 中对字体加载提供自动静态优化。当你使用 expo-font 加载字体时,Expo CLI 会自动提取字体资源并将其嵌入页面的 HTML 中,从而实现预加载、更快的 hydration 以及更少的布局偏移。

下面的代码片段会将 Inter 加载到命名空间中,并在 Web 上进行静态优化:

src/app/home.tsx
import { Text } from 'react-native'; import { useFonts } from 'expo-font'; export default function App() { const [isLoaded] = useFonts({ inter: require('@/assets/inter.ttf'), }); if (!isLoaded) { return null; } return <Text style={{ fontFamily: 'inter' }}>Hello Universe</Text>; }

这会生成以下静态 HTML:

dist/home.html
/* @info 在 JavaScript 加载之前预加载字体。 */ <link rel="preload" href="/assets/inter.ttf" as="font" crossorigin /> /* @end */ <style id="expo-generated-fonts" type="text/css"> @font-face { font-family: inter; src: url(/assets/inter.ttf); font-display: auto; } </style>
  • 静态字体优化要求字体同步加载。如果字体没有被静态优化,可能是因为它是在 useEffect、延迟组件或异步函数中加载的。
  • 静态优化仅支持 expo-font 中的 Font.loadAsyncFont.useFonts。只要包装函数是同步的,就支持包装函数。

常见问题

如何添加自定义服务器?

添加自定义服务器没有规定的方式。你可以使用任何你想要的服务器。不过,你需要自己处理动态路由。你可以使用 generateStaticParams 函数为已知路由生成静态 HTML 文件。

将来会提供一个服务器 API,以及一种新的 web.output 模式,它将生成一个项目,并且(在其他功能中)支持动态路由。

服务器端渲染

web.output: 'static' 时,不支持在请求时进行渲染。要在每次请求时动态渲染页面,请改用 服务器渲染,并将 web.output 设置为 'server'

我可以将静态渲染的网站部署到哪里?

你可以将静态渲染的网站部署到任何静态托管服务。以下是一些常见的选项:

注意: 你不需要在静态托管服务上添加单页应用风格的重定向。静态网站不是单页应用。它是由静态 HTML 文件组成的集合。