从 Expo Webpack 迁移

编辑

学习如何将使用 Expo Webpack 的网站迁移到 Expo Router。


原始的 Expo for web 版本基于 Webpack 4,主要集中于构建单页面应用程序(SPAs)。这种方法基于 Create React App,使得使用 Expo SDK 和 React Native for web 构建简单的 web 应用程序成为可能。

Expo Router 是构建能够在 web 和原生上运行的强大通用应用的新方法。此指南将帮助您将现有网站迁移到 Expo Router。

React Navigation 和 Expo Router 都是 Expo 的路由和导航框架。Expo Router 是 React Navigation 的一个包装器,并且有许多共享的概念。

提示

@expo/webpack-config 已被废弃,不会再接收任何新特性更新。

Expo Router 支持 静态渲染在 web 上,这使得搜索引擎优化(SEO)、社交媒体预览和更快的加载时间成为可能,不同于 Expo Webpack。除了 React Navigation 的优势,它还支持自动深度链接、类型安全延迟打包模块化 HTML 模板静态渲染在 web 上 等等。

Expo Router 还旨在解决 Expo Webpack 的主要跨平台问题,通过在 web 和原生之间共享导航,而不影响功能或性能。

反提示

Expo Router 使用基于 Metro 的自定义打包器堆栈。这是 React Native 使用的相同打包器。这确保了最大代码重用,并解决了由于跨平台使用不同打包器而导致的许多分叉行为问题。这也意味着某些打包功能可能尚未在 Expo Router 中可用。

最终,作为一个完整的通用框架,Expo Router 是一种比 @expo/webpack-config 更强大的解决方案,后者只是一个打包器集成。它应该用于所有新的 Expo web 项目。

Expo CLI

@expo/webpack-config 不同,Expo Router 在 web 和原生中使用相同的 CLI 命令和功能。有关 Expo Router 和 @expo/webpack-config 之间差异的更多信息,请参阅下表。

特性Expo Router@expo/webpack-config
启动命令npx expo startnpx expo start
打包命令npx expo exportnpx expo export:web
输出目录distweb-build
静态目录publicweb
配置文件metro.config.jswebpack.config.js
默认配置@expo/metro-config@expo/webpack-config
打包拆分 (SDK 50 • web)
全局 CSS (SDK 50 • web)
CSS 模块 (SDK 50 • web)
静态字体优化 (SDK 50 • web)
API 路由 (SDK 50)
多平台
快速刷新
错误覆盖
延迟打包
静态生成
环境变量
tsconfig.json 路径
树摇晃 (部分支持)

HTML 模板

@expo/webpack-config 中,所有路由共享一个单一的 HTML 文件。该文件基于 web/index.html 中的模板,然后由 @expo/webpack-config 修改以包含必要的脚本和样式表。

在 Expo Router 中,有两种不同的渲染模式:

  • 推荐: web.output: "static",为应用中的每个路由输出一个新的 HTML 文件。这种方法允许您使用 app/+html.js 文件 动态生成整个 HTML 模板
  • 不推荐: web.output: "single",输出一个单页面应用程序。这种方法让您使用 public/index.html 作为模板 HTML 文件。

静态资源

@expo/webpack-config 中,您可以在 web 目录中托管静态文件,这些文件将从网站的根目录提供服务。例如,web/favicon.ico 是从 https://example.com/favicon.ico 提供的。

在 Expo Router 中,您可以使用 public 目录来托管静态文件。例如,public/favicon.ico 是从 https://example.com/favicon.ico 提供的。与 Webpack 不同,Expo Router 的托管在原生上也有效。在生产中使用前,请确保从服务器托管文件。

生产打包

@expo/webpack-config 中,您可以使用 npx expo export:web 来将网站打包为生产版本。这会将包输出到 web-build 目录。

在 Expo Router 中,使用 npx expo export --platform web 命令导出到 dist 目录。您可以使用 --dump-sourcemap 标志生成源映射。在构建时,public 目录的内容将被复制到 dist 目录。

Babel 配置

与之前一样,根 babel.config.js 文件用于 web 和原生。您可以通过在 API 调用者中使用 platform 属性来更改预设:

babel.config.js
module.exports = api => { // 从 API 调用者获取平台... const platform = api.caller(caller => caller && caller.platform); return { presets: ['babel-preset-expo'], plugins: [ // 添加一个仅限 Web 的插件... platform === 'web' && 'custom-web-only-plugin', ].filter(Boolean), }; };

开发服务器

在 Expo Router 中,所有平台都从同一开发服务器在同一端口托管。这方便了模拟应用的生产行为。所有日志和热更新也通过同一端口进行。

由于原生的限制,目前不支持使用假 HTTPS 托管。这个功能现在的重要性低于 2018 年,因为您可以使用 Chrome 等网络浏览器在 localhost 上测试摄像头和位置等安全特性。

Expo 常量

expo-constants 库可以用于访问应用内的 app.json。在幕后,这通过将 process.env.APP_MANIFEST 设置为 app.json 文件的字符串内容来实现。

在 Expo Router 中,通过使用 Babel 和 babel-preset-expo 来完成。如果您修改了 app.json,请使用 npx expo start --clear 重新启动 Babel 缓存以查看更新。

基路径和子路径托管

实验性功能。

@expo/webpack-config 中,您可以使用 PUBLIC_URL 环境变量或项目 package.json 中的 homepage 字段将网站打包为从子路径托管:

package.json
{ "homepage": "/evanbacon/my-website" }

在 Expo Router 中,您可以使用项目 app.json 中的实验性 baseUrl 字段:

app.json
{ "expo": { "experiments": { "baseUrl": "/evanbacon/my-website" } } }

与之前的系统不同,这还会更新路由以考虑基路径。例如,如果您有一个路由 /profile,并将基路径设置为 /evanbacon/my-website,则该路由将是 /evanbacon/my-website/profile

有关更多信息,请参见 使用子路径托管

快速刷新

@expo/webpack-config 中,您可以安装 @pmmmwh/react-refresh-webpack-plugin 并在 webpack.config.js 中添加以下内容:

webpack.config.js
const createExpoWebpackConfigAsync = require('@expo/webpack-config'); const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); module.exports = async function (env, argv) { const config = await createExpoWebpackConfigAsync(env, argv); // 在开发模式中使用 React 刷新插件 if (env.mode === 'development') { config.plugins.push(new ReactRefreshWebpackPlugin({ disableRefreshCheck: true })); } return config; };

在 Expo Router 中,快速刷新默认启用,使用 Meta 的官方快速刷新实现。

网站图标

@expo/webpack-config 一样,Expo Router 支持根据 app.json 中的 web.favicon 字段生成 favicon.ico 文件。

Service Workers

添加 service workers 时要小心,因为它们可能导致网络上出现意外行为。如果您不小心发布了一个过于激进缓存您网站的 service worker,则用户无法轻松请求更新。为了获得最佳的离线移动体验,请使用 Expo 创建一个原生应用。与带有 service workers 的网站不同,原生应用可以通过应用商店更新以清除缓存的体验。这类似于重置用户的原生浏览器(如果 service worker 过于激进的话,他们可能需要这样做)。有关更多信息,请参见 为什么 service workers 不理想

Expo Webpack 没有内置的 service worker 支持。但是,您可以通过使用 workbox-webpack-plugin 并将其添加到 webpack.config.js 中来自行添加。

Workbox 没有 Metro 集成,但因为 Workbox 不需要打包器的核心功能之一(转换、解析、序列化),它可以很容易地作为构建后的步骤使用。请遵循 使用 Workbox CLI 的指南,所有提到“构建脚本”的地方,使用 npx expo export -p web

例如,这是设置 Workbox 的可能流程。用以下命令创建新项目:

Terminal
npm create expo -t tabs my-app

cd my-app

接下来,为应用创建一个根 HTML 文件,并添加 service worker 注册脚本:

app/+html.tsx
import { ScrollViewStyleReset } from 'expo-router/html'; import type { PropsWithChildren } from 'react'; // 此文件仅限于 Web,用于配置每个 // 在静态渲染期间的 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" /> {/* 启动 service worker。 */} <script dangerouslySetInnerHTML={{ __html: sw }} /> {/* 在 web 上禁用 body 滚动。这使 ScrollView 组件的工作方式更接近于它们在原生中的工作方式。 然而,body 滚动通常在移动 Web 上是可以有的。如果您想启用它,请删除此行。 */} <ScrollViewStyleReset /> {/* 添加任何您希望在 web 上全局可用的额外 <head> 元素... */} </head> <body>{children}</body> </html> ); } const sw = ` if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js').then(registration => { console.log('Service Worker registered with scope:', registration.scope); }).catch(error => { console.error('Service Worker registration failed:', error); }); }); } `;

现在在运行向导之前构建应用:

Terminal
npx expo export -p web

运行向导命令,选择 dist 作为应用的根目录,其他所有默认值:

Terminal
npx workbox-cli wizard

? What is the root of your web app (that is which directory do you deploy)? dist/
? Which file types would you like to precache? js, html, ttf, ico, json
? Where would you like your service worker file to be saved? dist/sw.js
? Where would you like to save these configuration options? workbox-config.js
? Does your web app manifest include search parameter(s) in the 'start_url', other than 'utm_' or 'fbclid' (like '?source=pwa')? No

最后,运行 npx workbox-cli generateSW workbox-config.js 以生成 service worker 配置。以后,您可以在 package.json 中添加一个构建脚本,以正确的顺序运行这两个脚本:

package.json
{ "scripts": { "build:web": "expo export -p web && npx workbox-cli generateSW workbox-config.js" } }

PWA 清单

@expo/webpack-config 不同,Expo Router 不会自动尝试生成 PWA 清单配置。您可以在 public/manifest.json 中创建一个:

{ "short_name": "Expo App", "name": "Expo Router Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" }

您可以使用 link 标签在 HTML 文件中链接此文件:

app/+html.tsx
import { ScrollViewStyleReset } from 'expo-router/html'; import type { PropsWithChildren } from 'react'; // 此文件仅限于 Web,用于配置每个 // 在静态渲染期间的 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" /> {/* 链接 PWA 清单文件。 */} <link rel="manifest" href="/manifest.json" /> {/* 在 web 上禁用 body 滚动。这使 ScrollView 组件的工作方式更接近于它们在原生中的工作方式。 然而,body 滚动通常在移动 Web 上是可以有的。如果您想启用它,请删除此行。 */} <ScrollViewStyleReset /> {/* 添加任何您希望在 web 上全局可用的额外 <head> 元素... */} </head> <body>{children}</body> </html> ); }

打包器插件

如果您使用了自定义打包器插件,请参见 Expo Metro 配置 以向您的打包管道添加自定义功能。

导航

如果您在 @expo/webpack-config 中使用 React Navigation 在屏幕之间导航,请参见 React Navigation 的迁移指南

部署

查看 发布网站 了解如何将 Expo Router 网站部署到各种托管提供商。