静态渲染
了解如何使用 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.
信息 使用静态渲染时,数据加载器 会在构建过程中执行,其结果会嵌入到输出的 HTML 文件中。
设置
🌐 Setup
1
在你的项目应用配置中启用静态渲染:
🌐 Enable static rendering in your project's app config:
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "web": { "output": "static" } } }
2
启动开发服务器:
🌐 Start the development server:
- npx expo start生产
🌐 Production
要将你的静态网站打包用于生产环境,请运行导出命令:
🌐 To bundle your static website for production, run the export command:
- npx expo export --platform web这将创建一个包含你静态渲染网站的 dist 目录。如果你在本地 public 目录中有文件,这些文件也会被复制过去。 你可以通过运行以下命令在本地测试生产环境构建,并在浏览器中打开链接的 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:
- 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 (for example, 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.
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:
distblogalpha.htmlbeta.htmlgenerateStaticParams
一个仅服务器端的函数,在由 Expo CLI 提供的 Node.js 环境中于构建时执行。这意味着它可以访问 __dirname、process.cwd()、process.env 等。它还可以访问进程中可用的每个环境变量。然而,前缀为 EXPO_PUBLIC_ 的值不能在浏览器环境中运行,因此无法访问诸如 localStorage 或 document 之类的浏览器 API。它也无法访问诸如 expo-camera 或 expo-location 之类的原生 Expo API。
🌐 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_ 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.
export async function generateStaticParams(): Promise<Record<string, string>[]> { console.log(process.cwd()); return []; }
generateStaticParams 从嵌套的父级向下传递到子级。级联参数会传递给每个导出 generateStaticParams 的动态子路由。
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.
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.
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
🌐 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。该组件将封装 app 目录下的所有路由。这对于添加全局的 <head> 元素或禁用 body 滚动非常有用。
🌐 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 Layout 组件中,而不是 Root HTML 组件中。
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" />标签。- JavaScript 脚本会在静态渲染之后附加。
- React Native Web 样式会自动以静态方式注入。
- 全局 CSS 不应导入到此文件中。相反,请使用 Root Layout 组件。
- 像
window.location这样的浏览器 API 在此组件中不可用,因为它只在静态渲染期间的 Node.js 中运行。
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 />的根样式重置应使用以下样式以确保与原生一致。
元标签
🌐 Meta tags
你可以使用来自 expo-router 的 <Head /> 模块向你的页面添加元标签:
🌐 You can add meta tags to your pages with the <Head /> module from expo-router:
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,预先渲染静态头部元素是有用的。
🌐 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 支持一个根 public 目录,该目录在静态渲染过程中会被复制到 dist 目录。这对于添加静态文件,如图片、字体和其他资源,非常有用。
🌐 Expo CLI supports a root public directory that gets copied to the dist directory during static rendering. This is useful for adding static files like images, fonts, and other assets.
publicfavicon.icologo.png.well-knownapple-app-site-association在静态渲染期间,这些文件将被复制到 dist 目录:
🌐 These files will be copied to the dist directory during static rendering:
distindex.htmlfavicon.icologo.png.well-knownapple-app-site-association_expostaticjsindex-xxx.jscssindex-xxx.css信息 仅限网页:可以在运行时代码中使用相对路径访问静态资源。例如,logo.png 可以通过
/logo.png访问:
import { Image } from 'react-native'; export default function Page() { return <Image source={{ uri: '/logo.png' }} />; }
字体
🌐 Fonts
Expo 字体在 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 加载到命名空间中,并在网页上进行静态优化:
🌐 The following snippet will load Inter into the namespace and statically optimize on web:
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:
/* @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、延迟组件或异步函数中加载的。 - 静态优化仅支持来自
expo-font的Font.loadAsync和Font.useFonts。只要封装函数是同步的,就支持封装函数。
常见问题
🌐 Common questions
我如何添加自定义服务器?
🌐 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 (among other things) support dynamic routes.
服务器端渲染
🌐 Server-side Rendering
web.output: 'static' 不支持在请求时进行渲染。要在每次请求时动态渲染页面,请改用搭配 web.output: 'server' 的服务器渲染。
🌐 Rendering at request-time is not supported with web.output: 'static'. To render pages dynamically on each request, use server rendering with web.output: 'server' instead.
我可以在哪里部署静态渲染的网站?
🌐 Where can I deploy statically rendered websites?
你可以将你的静态渲染网站部署到任何静态托管服务。以下是一些常见的选项:
🌐 You can deploy your statically rendered website to any static hosting service. Here are some popular options:
注意: 你无需向静态托管服务添加单页应用样式的重定向。该静态网站不是单页应用,它是由静态 HTML 文件组成的集合。