首页指南参考教程

静态渲染

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


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

¥To enable Search Engine Optimization (SEO) on the web you must statically render your app. This guide will walk you through the process of statically rendering your Expo Router app.

设置

¥Setup

1

在项目的 应用配置 中启用 Metro 打包器和静态渲染:

¥Enable metro bundler and static rendering in the project's app config:

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

2

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

¥If you have a metro.config.js file in your project, ensure it extends expo/metro-config as shown below:

metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname, {
  // Additional features...
});

module.exports = config;

你还可以了解有关 定制 Metro 的更多信息。

¥You can also learn more about customizing Metro .

3

确保配置了快速刷新。

¥Ensure Fast Refresh is configured.

Expo 路由至少需要 react-refresh@0.14.0。这在 SDK 50 及以上版本中可用。确保你的 package.json 中没有对 react-refresh 进行任何覆盖或解析。

¥Expo Router requires at least react-refresh@0.14.0. This is available in SDK 50 and above. Ensure you do not have any overrides or resolutions for react-refresh in your package.json.

Expo 路由至少需要 react-refresh@0.14.0。截至 SDK 49 和 Expo Router v2,React Native 尚未升级,因此你需要通过设置 Yarn 解析或 npm 覆盖来强制升级你的 react-refresh 版本。

¥Expo Router requires at least react-refresh@0.14.0. React Native hasn't upgraded as of SDK 49 and Expo Router v2, so you need to force upgrade your react-refresh version by setting a Yarn resolution or npm override.

package.json
{
  %%placeholder-start%%... %%placeholder-end%%
  "overrides": {
    "react-refresh": "~0.14.0"
  }
}
package.json
{
  %%placeholder-start%%... %%placeholder-end%%
  "resolutions": {
    "react-refresh": "~0.14.0"
  }
}

4

启动开发服务器:

¥Start the development server:

Terminal
npx expo start

生产

¥Production

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

¥To bundle your static website for production, run the universal export command:

Terminal
npx expo export --platform web

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

¥This will create a dist directory with your statically rendered website. If you have files in a local public directory, these will be copied over as well. You can test the production build locally by running the following command and opening the linked URL in your browser:

Terminal
npx serve dist

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

¥This project can be deployed to almost every hosting service. Note that this is not a single-page application, nor does it contain a custom server API. This means dynamic routes (app/[id].tsx) will not arbitrarily work. You may need to build a serverless function to handle dynamic routes.

动态路由

¥Dynamic Routes

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

¥The static output will generate HTML files for each route. This means dynamic routes (app/[id].tsx) will not work out of the box. You can generate known routes ahead of time using the generateStaticParams function.

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();
  // Return an array of params to generate static HTML files for.
  // Each entry in the array will be a new page.
  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" }],则会生成以下文件:

¥This will output a file for each post in the dist directory. For example, if the generateStaticParams method returned [{ id: "alpha" }, { id: "beta" }], the following files would be generated:

dist
blog
  alpha.html
  beta.html

generateStaticParams

由 Expo CLI 在 Node.js 环境中构建时评估的仅服务器函数。这意味着它可以访问 __dirnameprocess.cwd()process.env 等。它还可以访问流程中可用的每个环境变量。但是,以 EXPO_PUBLIC_.generateStaticParams 为前缀的值不在浏览器环境中运行,因此它无法访问浏览器 API,例如 localStoragedocument。它也无法访问原生 Expo API,例如 expo-cameraexpo-location

¥A server-only function evaluated at build-time in a Node.js environment by Expo CLI. This means it has access to __dirname, process.cwd(), process.env, and more. It also has access to every environment variable that's available in the process. However, the values prefixed with EXPO_PUBLIC_.generateStaticParams do not run in a browser environment, so it cannot access browser APIs such as localStorage or document. It also cannot access native Expo APIs such as expo-camera or expo-location.

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

  return [];
}

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

¥generateStaticParams cascades from nested parents down to children. The cascading parameters are passed to every dynamic child route that exports generateStaticParams.

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

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

¥Now the dynamic child routes will be invoked twice, once with { id: 'one' } and once with { id: 'two' }. All variations must be accounted for.

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() 读取文件

¥Read files using process.cwd()

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

¥Since Expo Router compiles your code into a separate directory you cannot use __dirname to form a path as its value will be different than expected.

相反,使用 process.cwd(),它会为你提供正在编译项目的目录。

¥Instead, use process.cwd(), which gives you the directory where the project is being compiled.

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

¥Root HTML

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

¥By default, every page is wrapped with some small HTML boilerplate, this is known as the root HTML.

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

¥You can customize the root HTML file by creating an app/+html.tsx file in your project. This file exports a React component that only ever runs in Node.js, which means global CSS cannot be imported inside of it. The component will wrap all routes in the app directory. This is useful for adding global <head> elements or disabling body scrolling.

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

¥Note: Global context providers should go in the Root Layout component, not the Root HTML component.

app/+html.tsx
import { ScrollViewStyleReset } from 'expo-router/html';
import { type PropsWithChildren } from 'react';

// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
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" />

        {/*
          Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
          However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
        */}
        <ScrollViewStyleReset />

        

{/* Add any additional <head> elements that you want globally available on web... */}


      </head>
      <body>{children}</body>
    </html>
  );
}
  • children 属性附带了根 <div id="root" /> 标签。

    ¥The children prop comes with the root <div id="root" /> tag included inside.

  • JavaScript 脚本附加在静态渲染之后。

    ¥The JavaScript scripts are appended after the static render.

  • React Native Web 样式是自动静态注入的。

    ¥React Native web styles are statically injected automatically.

  • 全局 CSS 不应导入到此文件中。请改用 根布局 组件。

    ¥Global CSS should not be imported into this file. Instead, use the Root Layout component.

  • window.location 这样的浏览器 API 在此组件中不可用,因为它仅在静态渲染期间在 Node.js 中运行。

    ¥Browser APIs like window.location are unavailable in this component as it only runs in Node.js during static rendering.

expo-router/html

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

¥The exports from expo-router/html are related to the Root HTML component.

  • ScrollViewStyleReset:全屏 React Native Web 应用 与根 <ScrollView /> 的根样式重置应使用以下样式以确保原生奇偶校验。

    ¥ScrollViewStyleReset: Root style-reset for full-screen React Native web apps with a root <ScrollView /> should use the following styles to ensure native parity.

元标签

¥Meta tags

你可以使用 expo-router 中的 <Head /> 模块将元标记添加到你的页面:

¥You can add meta tags to your pages with the <Head /> module from expo-router:

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 动态更新 head 元素。然而,提前渲染静态头部元素对于 SEO 很有用。

¥The head elements can be updated dynamically using the same API. However, it's useful for SEO to have static head elements rendered ahead of time.

静态文件

¥Static Files

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

¥Expo CLI supports a root public directory that gets copied to the dist folder during static rendering. This is useful for adding static files like images, fonts, and other assets.

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

这些文件在静态渲染时会被复制到 dist 文件夹中:

¥These files will be copied to the dist folder during static rendering:

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' }} />;
}

字体

¥Fonts

SDK 50 及更高版本中提供字体优化。

¥Font optimization is available in SDK 50 and above.

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

¥Expo Font has automatic static optimization for font loading in Expo Router. When you load a font with expo-font, Expo CLI will automatically extract the font resource and embed it in the page's HTML, enabling preloading, faster hydration, and reduced layout shift.

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

¥The following snippet will load Inter into the namespace and statically optimize on web:

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:

¥This generates the following static HTML:

dist/home.html
/* @info preload the font before the JavaScript loads. */
<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、延迟组件或异步函数中加载的。

    ¥Static font optimization requires the font to be loaded synchronously. If the font isn't statically optimized, it could be because it was loaded inside a useEffect, deferred component, or async function.

  • expo-font 中的 Font.loadAsyncFont.useFonts 支持静态优化。只要封装器是同步的,就支持封装器函数。

    ¥Static optimization is only supported with Font.loadAsync and Font.useFonts from expo-font. Wrapper functions are supported as long as the wrappers are synchronous.

常见问题

¥FAQ

如何添加自定义服务器?

¥How do I add a custom server?

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

¥There is no prescriptive way to add a custom server. You can use any server you want. However, you will need to handle dynamic routes yourself. You can use the generateStaticParams function to generate static HTML files for known routes.

将来,将有一个服务器 API 和一个新的 web.output 模式,它将生成一个支持动态路由的项目(除其他外)。

¥In the future, there will be a server API, and a new web.output mode which will generate a project that will (amongst other things) support dynamic routes.

服务端渲染

¥Server-side Rendering

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

¥Rendering at request-time (SSR) is not supported in web.output: 'static'. This will likely be added in a future version of Expo Router.

在哪里可以部署静态渲染的网站?

¥Where can I deploy statically rendered websites?

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

¥You can deploy your statically rendered website to any static hosting service. Here are some popular options:

  • Netlify

  • Cloudflare 页面

    ¥Cloudflare Pages

  • AWS 放大

    ¥AWS Amplify

  • Vercel

  • GitHub 页面

    ¥GitHub Pages

  • 使成为

    ¥Render

  • ¥Surge

注意:你不需要将单页应用样式的重定向添加到静态托管服务。静态网站不是单页应用。它是静态 HTML 文件的集合。

¥Note: You don't need to add Single-Page Application styled redirects to your static hosting service. The static website is not a single-page application. It is a collection of static HTML files.

Expo 中文网 - 粤ICP备13048890号