静态渲染

编辑

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


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

设置

1

在项目的 app config 中启用 metro 打包器和静态渲染:

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

2

如果您的项目中有 metro.config.js 文件,请确保它扩展了 expo/metro-config,如下所示:

metro.config.js
const { getDefaultConfig } = require('expo/metro-config'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname, { // 其他功能... }); module.exports = config;

您还可以了解更多关于 自定义 Metro 的信息。

3

确保 Fast Refresh 已配置。Expo Router 至少需要 react-refresh@0.14.0。确保您在 package.json 有任何对 react-refresh 的覆盖或解析。

4

启动开发服务器:

Terminal
npx expo start

生产

要为生产打包您的静态网站,请运行通用导出命令:

Terminal
npx expo export --platform web

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

Terminal
npx serve dist

此项目几乎可以部署到任何托管服务。请注意,这不是一个单页面应用程序,也不包含自定义服务器 API。这意味着动态路由 (app/[id].tsx) 不一定能正常工作。您可能需要构建无服务器功能来处理动态路由。

动态路由

static 输出将为每个路由生成 HTML 文件。这意味着动态路由 (app/[id].tsx) 将无法开箱即用。您可以使用 generateStaticParams 函数提前生成已知路由。

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>Post {id}</Text>; }

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

dist
blog
  alpha.html
  beta.html

generateStaticParams

一个仅在构建时在 Node.js 环境中评估的服务器函数,由 Expo CLI 提供。这意味着它可以访问 __dirnameprocess.cwd()process.env 等等。它还可以访问过程中的每个环境变量。然而,带有前缀 EXPO_PUBLIC_.generateStaticParams 的值不会在浏览器环境中运行,因此无法访问浏览器 API,如 localStoragedocument。它也无法访问本机 Expo API,如 expo-cameraexpo-location

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

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

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

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

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(),它会给您提供项目正在编译的目录。

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

根 HTML

默认情况下,每个页面都用一些小的 HTML 样板包裹,这被称为 根 HTML

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

注意: 全局上下文提供程序应放在 根布局 组件中,而不是根 HTML 组件中。

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" /> {/* 禁用网页上的主体滚动。这使得 ScrollView 组件的工作方式更接近原生。 然而,对于移动网页,主体滚动通常是很好的。如果您希望启用它,请删除这一行。 */} <ScrollViewStyleReset /> {/* 添加任何想在网页上全局可用的额外 <head> 元素... */} </head> <body>{children}</body> </html> ); }
  • children prop 包含根 <div id="root" /> 标签。
  • JavaScript 脚本在静态渲染后追加。
  • React Native web 样式会自动静态注入。
  • 不应将全局 CSS 导入到此文件中。相反,应使用 根布局 组件。
  • window.location 这样的浏览器 API 在此组件中不可用,因为它仅在 Node.js 中的静态渲染期间运行。

expo-router/html

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

元标签

您可以使用来自 expo-router<Head /> 模块向您的页面添加元标签:

app/about.tsx
import Head from 'expo-router/head'; import { Text } from 'react-native'; export default function Page() { return ( <> <Head> <title>My Blog Website</title> <meta name="description" content="This is my blog." /> </Head> <Text>About my blog</Text> </> ); }

头部元素可以使用相同的 API 动态更新。然而,提前渲染静态头部元素对于 SEO 是有用的。

静态文件

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

public
favicon.ico
logo.png
.well-known
  apple-app-site-association

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

dist
index.html
favicon.ico
logo.png
.well-known
  apple-app-site-association
_expo
  static
   js
    index-xxx.js
   css
    index-xxx.css
仅限网页: 静态资产可以在运行时代码中使用相对路径访问。例如,logo.png 可以在 /logo.png 访问:
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 中,从而实现预加载、加快水合速度并减少布局位移。

以下代码片段将把 Inter 加载到命名空间中并在网页上进行静态优化:

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、延迟组件或异步函数内部加载。
  • 静态优化仅支持 Font.loadAsyncFont.useFonts 来自 expo-font。包装函数是被支持,只要包装器是同步的。

常见问题

如何添加自定义服务器?

没有单一的方式来添加自定义服务器。您可以使用任何服务器。然而,您需要自行处理动态路由。您可以使用 generateStaticParams 函数为已知路由生成静态 HTML 文件。

将来,会有一个服务器 API,以及一个新的 web.output 模式,这将生成一个项目,其中(除了其他事情)支持动态路由。

服务器端渲染

在请求时渲染(SSR)在 web.output: 'static' 中不支持。这可能会在将来的 Expo Router 版本中添加。

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

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

注意: 您无需向静态托管服务添加单页面应用样式重定向。静态网站并不是一个单页面应用。它是静态 HTML 文件的集合。