从 Expo Webpack 迁移

了解如何使用 Expo Webpack 将网站迁移到 Expo Router。


原始的 Expo for web 版本基于 Webpack 4,主要专注于构建单页应用(SPA)。这种方法基于 Create React App,并能够使用 Expo SDK 和 React Native for Web 构建简单的 web 应用。

🌐 The original Expo for web version was based on Webpack 4 and focused primarily on building single-page applications (SPAs). This approach was based on Create React App and enabled building simple web apps with Expo SDK and React Native for web.

Expo Router 是构建能够在网页和原生环境中运行的强大通用应用的新方法。本指南将帮助你将现有网站迁移到 Expo Router。

🌐 Expo Router is the new approach to building powerful universal apps that run on web and native. This guide will help you migrate your existing website to Expo Router.

React Navigation 和 Expo Router 都是用于路由和导航的 Expo 框架。Expo Router 是 React Navigation 的一个封装,并且有许多共同的概念。

🌐 Both React Navigation and Expo Router are Expo frameworks for routing and navigation. Expo Router is a wrapper around React Navigation and has many shared concepts.

投入

🌐 Pitch

警告 @expo/webpack-config 已被弃用,不会再接收任何新功能更新。

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

🌐 Expo Router supports static rendering on web, which enables search engine optimization (SEO), social media previews, and faster loading times, unlike Expo Webpack. Along with the benefits of React Navigation, it enables automatic deep linking, type safety, deferred bundling, modular HTML templates, static rendering on web, and more.

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

🌐 Expo Router is also designed to fix the main cross-platform issue with Expo Webpack by sharing navigation between web and native without compromising functionality or performance.

反投入

🌐 Anti-pitch

Expo Router 使用了基于 Metro 的自定义打包器堆栈。这与 React Native 使用的打包器相同。这对于确保最大程度的代码可复用性非常有用,并解决了在不同平台使用不同打包器时产生的许多分叉行为问题。这也意味着某些打包功能在 Expo Router 中可能尚不可用。

🌐 Expo Router uses a custom bundler stack based on Metro. It is the same bundler used by React Native. This is great for ensuring maximum code reusability and solves many forked behavior issues from using different bundlers across platforms. This also means certain bundling features may not be available in Expo Router yet.

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

🌐 Ultimately as a full universal framework, Expo Router is a substantially more robust solution than @expo/webpack-config, which is a bundler integration. It should be used for all new Expo web projects.

Expo CLI

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

🌐 Unlike @expo/webpack-config, Expo Router uses the same CLI commands and features for web and native. Refer to the table below for more information on the differences between Expo Router and @expo/webpack-config.

FeatureExpo Router@expo/webpack-config
Start commandnpx expo startnpx expo start
Bundle commandnpx expo exportnpx expo export:web
Output directorydistweb-build
Static directorypublicweb
Config filemetro.config.jswebpack.config.js
Default config@expo/metro-config@expo/webpack-config
Bundle Splitting (SDK 50 • web)
Global CSS (SDK 50 • web)
CSS Modules (SDK 50 • web)
Static Font Optimization (SDK 50 • web)
API Routes (SDK 50)
Multi-platform
Fast Refresh
Error Overlay
Lazy bundling
Static Generation
Environment Variables
tsconfig.json paths
Tree Shaking (Partial support)

HTML 模板

🌐 HTML template

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

🌐 In @expo/webpack-config all routes shared a single HTML file. This file was based on the template in web/index.html which was then modified by the @expo/webpack-config to include the necessary scripts and stylesheets.

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

🌐 In Expo Router, there are two different rendering patterns:

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

静态资源

🌐 Static resources

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

🌐 In @expo/webpack-config, you could host static files in the web directory, which would be served from the website's root. For example, web/favicon.ico was served from https://example.com/favicon.ico.

在 Expo Router 中,你可以使用 public 目录来托管静态文件。例如,public/favicon.ico 可以从 https://example.com/favicon.ico 访问。与 Webpack 不同,Expo Router 的托管在原生环境中也可用。确保在生产环境中使用这些文件之前先将它们托管在服务器上。

🌐 In Expo Router, you can use the public directory to host static files. For example, public/favicon.ico is served from https://example.com/favicon.ico. Unlike Webpack, Expo Router's hosting works on native too. Make sure to host the files from a server before using them in production.

打包用于生产

🌐 Bundling for production

@expo/webpack-config 中,你可以使用 npx expo export:web 将你的网站打包用于生产环境。这会将打包输出到 web-build 目录。

🌐 In @expo/webpack-config, you could bundle your website for production using npx expo export:web. This would output a bundle to the web-build directory.

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

🌐 In Expo Router, use the npx expo export --platform web command to export to the dist directory. You can generate sourcemaps with the --dump-sourcemap flag. On build, the contents of the public directory will be copied to the dist directory.

Babel 配置

🌐 Babel configuration

和以前一样,根目录下的 babel.config.js 文件同时用于 web 和原生。你可以通过在 API 调用中使用 platform 属性来更改预设:

🌐 Like before, the root babel.config.js file is used for both web and native. You can change the preset by using the platform property in the API caller:

babel.config.js
module.exports = api => { // Get the platform from the API caller... const platform = api.caller(caller => caller && caller.platform); return { presets: ['babel-preset-expo'], plugins: [ // Add a web-only plugin... platform === 'web' && 'custom-web-only-plugin', ].filter(Boolean), }; };

开发服务器

🌐 Dev server

在 Expo Router 中,所有平台都在同一个端口的同一个开发服务器上托管。这对于模拟应用的生产行为非常方便。所有日志和热模块重载也都通过同一个端口进行。

🌐 In Expo Router, all platforms are hosted from the same dev server on the same port. This is convenient for emulating the production behavior of the app. All logs and hot module reloading go through the same port as well.

由于本地环境的限制,目前不支持使用假的 HTTPS 托管。相比 2018 年,这个功能现在不那么重要,因为你可以在本地主机上使用像 Chrome 这样的网页浏览器测试摄像头和定位等安全功能。

🌐 Due to limitations on native, hosting with fake HTTPS is not currently supported. This feature is less important now than in 2018, as you can test secure features such as camera and location on localhost using a web browser like Chrome.

Expo 常量

🌐 Expo constants

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

🌐 The expo-constants library can be used to access the app.json in-app. Behind the scenes, this is accomplished by setting process.env.APP_MANIFEST with the stringified contents of the app.json file.

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

🌐 In Expo Router, this is done using Babel with the babel-preset-expo. If you modify the app.json, restart the Babel cache with npx expo start --clear to see the updates.

基本路径和子路径托管

🌐 Base path and subpath hosting

重要 实验功能。

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

🌐 In @expo/webpack-config, you could bundle your website to be hosted from a subpath by using the PUBLIC_URL environment variable or the homepage field in the project's package.json:

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

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

🌐 In Expo Router, you can use the experimental baseUrl field in the project's app.json:

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

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

🌐 Unlike the previous system, this will also update the routing to account for the base path. For example, if you have a route /profile and you set the base path to /evanbacon/my-website, then the route will be /evanbacon/my-website/profile.

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

🌐 See hosting with sub-paths for more information.

快速刷新

🌐 Fast refresh

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

🌐 In @expo/webpack-config you could install @pmmmwh/react-refresh-webpack-plugin and add the following to the 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); // Use the React refresh plugin in development mode if (env.mode === 'development') { config.plugins.push(new ReactRefreshWebpackPlugin({ disableRefreshCheck: true })); } return config; };

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

🌐 In Expo Router, Fast Refresh is enabled by default using the official Fast Refresh implementation by Meta.

网站图标

🌐 Favicons

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

🌐 Like @expo/webpack-config, Expo Router supports generating the favicon.ico file based on the web.favicon field in the app.json.

服务工作线程

🌐 Service workers

警告 添加服务工作者时请小心,因为它们已知会在网页上引起意外行为。如果你不小心部署了一个会积极缓存你网站的服务工作者,用户将很难请求更新。为了获得最佳的离线移动体验,请使用 Expo 创建原生应用。与具有服务工作者的网站不同,原生应用可以通过应用商店更新,以清除缓存体验。这类似于重置用户的原生浏览器(如果服务工作者足够激进,用户可能不得不这样做)。更多信息请参见 为什么服务工作者不理想

Expo Webpack 没有内置的服务工作者支持。不过,你可以通过使用 workbox-webpack-plugin 并将其添加到 webpack.config.js 来自己添加。

🌐 Expo Webpack didn't have built-in service worker support. However, you could add it yourself by using the workbox-webpack-plugin and adding it to the webpack.config.js.

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

🌐 Workbox doesn't have a Metro integration, but because Workbox doesn't require one of the core features of a bundler (transformation, resolution, serialization), it can easily be used as a post-build step. Follow the guide for using Workbox CLI, and wherever it refers to a "build script" use npx expo export -p web instead.

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

🌐 For example, here's a possible flow for setting up Workbox. Create a new project with the following command:

Terminal
npm create expo -t tabs my-app

cd my-app

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

🌐 Next, create a root HTML file for the app and add the service worker registration script:

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" /> {/* Bootstrap the service worker. */} <script dangerouslySetInnerHTML={{ __html: sw }} /> {/* 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> ); } 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); }); }); } `;

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

🌐 Now build the app before running the wizard:

Terminal
npx expo export -p web

运行向导命令,将 dist 选择为应用的根目录,其余选项使用默认设置:

🌐 Run the wizard command, select dist as the root of the app, and the defaults for everything else:

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 来生成服务工作者配置。接下来,你可以在 package.json 中添加一个构建脚本,以正确顺序运行这两个脚本:

🌐 Finally, run npx workbox-cli generateSW workbox-config.js to generate the service worker config. Going forward, you can add a build script in package.json to run both scripts in the correct order:

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

PWA 清单

🌐 PWA manifests

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

🌐 Unlike @expo/webpack-config, Expo Router does not automatically attempt to generate the PWA manifest configuration. You can create one in 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" }

你可以在 HTML 文件中使用 link 标签来链接它:

🌐 You can link this in your HTML file using the link tag:

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" /> {/* Link the PWA manifest file. */} <link rel="manifest" href="/manifest.json" /> {/* 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> ); }

打包器插件

🌐 Bundler plugins

如果你正在使用自定义打包器插件,请参阅 Expo Metro 配置 以向你的打包器流水线添加自定义功能。

🌐 If you were using custom bundler plugins, see Expo Metro config for adding custom functionality to your bundler pipeline.

导航

🌐 Navigation

如果你在 @expo/webpack-config 中使用 React Navigation 在屏幕之间进行导航,请参阅 React Navigation 迁移指南

🌐 If you used React Navigation for navigating between screens in @expo/webpack-config, see the migration guide for React Navigation.

部署

🌐 Deployment

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

🌐 Check out Publishing websites on how to deploy Expo Router websites to various hosting providers.