create-react-app 部署实战:从 build 产物到客户端路由回退与多平台发布
【免费下载链接】create-react-appSet up a modern web app by running one command.项目地址: https://gitcode.com/gh_mirrors/cr/create-react-app
本文围绕 create-react-app 仓库中的官方部署文档 deployment.md 展开,系统讲解npm run build之后如何将build产物部署到静态服务器、集成到现有服务端应用,以及如何正确处理 HTML5pushState客户端路由的回退问题。文中同时结合 react-scripts 构建脚本 与 webpack 配置 的源码,解释homepage字段如何决定资源路径,帮助读者独立完成从 GitHub Pages 到 Firebase、Netlify、Vercel 等多种发布方案。
1. 部署的对象:npm run build产出了什么
npm run build会创建一个build目录,其中包含应用的生产构建产物。部署的本质是让任意 HTTP 服务器做到两件事:
- 访问者打开站点时,返回
index.html; - 对
/static/js/main.<hash>.js这类静态路径的请求,返回对应文件的实际内容。
理解这一点后,再来看构建脚本的源码,可以确认build目录是如何被组装出来的。在 build.js 中,构建流程依次是:
- 校验必需文件存在:
checkRequiredFiles([paths.appHtml, paths.appIndexJs]),即public/index.html与src/index(build.js 第 50 行); - 清空旧的
build目录(保留目录本身,防止你正在该目录内时整个目录被移入回收站); - 调用
copyPublicFolder(),把整个public目录合并进build; - 运行 webpack 生产构建,最后通过
printHostingInstructions()打印部署提示。
其中copyPublicFolder()的实现值得注意(build.js 第 220-225 行):
function copyPublicFolder() { fs.copySync(paths.appPublic, paths.appBuild, { dereference: true, filter: file => file !== paths.appHtml, }); }public目录里的所有文件(favicon、manifest.json、robots.txt,以及后文要讲的.htaccess、_redirects)都会被原样拷入build,唯独public/index.html被过滤掉——因为它只作为 HTML 模板,最终由 webpack 的HtmlWebpackPlugin生成带资源引用的版本写入build。这也解释了为什么所有“把某个文件放进public/”的平台技巧(Apache 的.htaccess、Netlify 的_redirects、GitHub Pages 的CNAME)都能自动进入构建产物。
构建完成后,printHostingInstructions.js 会根据你的配置打印不同的提示:如果homepage指向*.github.io且尚未配置deploy脚本,会给出gh-pages的安装与脚本配置示例;如果publicPath不是/,会提示“构建假设站点托管在某个子路径”;否则打印serve -s build这样的静态服务器启动建议。这就是文档中“运行npm run build后能看到一张部署 cheat sheet”的由来。
2. 用静态服务器运行生产构建
对于 Node 环境,最简单的方式是全局安装 Vercel 出品的静态服务器serve:
npm install -g serve serve -s build最后一条命令会把静态站点部署在3000端口上。与serve的许多内置设置类似,端口可以通过-l或--listen参数调整:
serve -s build -l 4000运行下面命令可以查看完整的可选项列表:
serve -h这里的-s(single-page app 模式)会让serve把未知路径都回退到index.html,因此它天然支持客户端路由场景。
3. 集成到现有服务端应用
运行一个 create-react-app 项目并非必须依赖独立静态服务器,它同样可以很好地嵌入已有的服务端应用。下面是一个基于 Node 和 Express 的程序化示例:
const express = require('express'); const path = require('path'); const app = express(); app.use(express.static(path.join(__dirname, 'build'))); app.get('/', function (req, res) { res.sendFile(path.join(__dirname, 'build', 'index.html')); }); app.listen(9000);服务器软件本身的选择并不重要:create-react-app 是完全平台无关的,没有必要显式使用 Node。build目录就是 create-react-app 输出的唯一产物,任何能把静态文件映射到 URL 的服务器(nginx、Apache、Caddy、S3……)都可以承接它。
但上面的配置对于使用客户端路由的应用来说还“差一点”。如果你希望在单页应用中支持/todos/42这类 URL,请继续看下一节。
4. 支持客户端路由(HTML5 pushState 回退)
如果你的路由基于 HTML5 的pushStatehistory API 实现(例如使用browserHistory的 React Router),很多静态文件服务器会直接失败。以 React Router 中配置了/todos/42路由为例:开发服务器能正确响应localhost:3000/todos/42,但按上一节方式配置的生产 Express 服务器则不行。
原因很直白:当用户首次(fresh load)打开/todos/42时,服务器会去寻找文件系统里真实的build/todos/42,自然找不到。服务器需要被配置为:对/todos/42的请求返回index.html。修改上面的 Express 示例,对任意未知路径都返回index.html即可:
app.use(express.static(path.join(__dirname, 'build'))); -app.get('/', function (req, res) { +app.get('/*', function (req, res) { res.sendFile(path.join(__dirname, 'build', 'index.html')); });Apache HTTP Server:使用 .htaccess
如果你使用 Apache HTTP Server,需要在public文件夹中创建一个如下内容的.htaccess文件:
Options -MultiViews RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.html [QSA,L]运行npm run build时,它会随public目录一起被拷入build文件夹(这正是第 1 节中copyPublicFolder()过滤逻辑要覆盖的场景)。
Apache Tomcat
如果你使用 Apache Tomcat,可以按社区中针对 Tomcat 回退配置的常见 Stack Overflow 方案处理(通过 Tomcat 的 404 重定向机制把未知路径指向index.html)。
完成以上任一配置后,对/todos/42的请求在开发和生产环境都会被正确处理。
Service Worker 的导航回退(PWA 场景)
在生产构建中,如果应用已经按 making-a-progressive-web-app.md 文档选择启用 PWA,service worker 会自动处理所有导航请求(例如/todos/42),方式是直接返回缓存的index.html副本,相当于在浏览器层完成回退。文档指出,可以通过eject后修改SWPrecachePlugin配置中的navigateFallback与navigateFallbackWhitelist选项来定制或关闭这一导航回退。
需要补充的是当前仓库的源码现状:从 webpack.config.js 第 705-717 行 看,react-scripts 目前已改用workbox-webpack-plugin的InjectManifest来生成 service worker,且仅当项目存在src/service-worker.js(paths.swSrc)时才生效;而默认模板 cra-template/template 并不携带该文件,即只有采用 PWA 模板(opt-in)的项目才会生成 service worker。因此,对当前版本而言,定制导航回退更直接的做法是修改自己编写的 service worker 源码,或在 eject 后调整InjectManifest相关配置。
修正 Web App Manifest 的 start_url
当用户把应用安装到设备主屏时,默认配置会让快捷方式指向/index.html,这对期望应用从/开始提供服务的客户端路由器可能无效。请编辑public/manifest.json,把start_url改为所需的 URL 方案,例如:
"start_url": ".",值得一提的是,当前仓库的默认模板 manifest.json 已经将"start_url"设为"."(相对路径),新创建的项目在子路径部署时天然规避了这一问题。
5. 相对路径构建与 homepage 字段
默认情况下,create-react-app 的构建假设应用托管在服务器根路径。要覆盖这个假设,在package.json中指定homepage,例如:
"homepage": "http://mywebsite.com/relativepath",这样 create-react-app 就能正确推断生成 HTML 文件时应使用的根路径。
注意:如果你使用react-router@^4,可以通过给任意<Router>传递basename属性来让<Link>相对它生成链接:
<BrowserRouter basename="/calendar"/> <Link to="/today"/> // renders <a href="/calendar/today">homepage 是如何参与构建的(源码级解析)
homepage的读取入口在 config/paths.js 第 26-30 行:
const publicUrlOrPath = getPublicUrlOrPath( process.env.NODE_ENV === 'development', require(resolveApp('package.json')).homepage, process.env.PUBLIC_URL );它把三个输入交给 getPublicUrlOrPath,其解析规则可以归纳为:
PUBLIC_URL环境变量优先级最高:如果设置了PUBLIC_URL,直接使用它(末尾自动补/);homepage次之:如果是完整 URL,则只取它的 pathname 部分(例如http://mywebsite.com/relativepath得到/relativepath);- 以
.开头的值有特殊待遇:在生产模式下原样保留(如"."就是相对路径);在开发模式下统一归一化为/,因为开发服务器必须使用绝对路径; - 都没有时返回默认值
/。
paths.js 第 20-25 行 的注释解释了为什么这一步必不可少:webpack 必须知道应用被服务的根路径,才能在 HTML 里写入正确的<script>href。不能简单用相对路径,否则在把index.html作为/todos/42等嵌套 URL 的响应时,浏览器会错误地去加载/todos/42/static/js/bundle.js。
这个值随后在 webpack.config.js 中成为output.publicPath:
output: { // ... // We inferred the "public path" (such as / or /my-project) from homepage. publicPath: paths.publicUrlOrPath,同时它被注入为应用中的%PUBLIC_URL%(index.html)与process.env.PUBLIC_URL(JavaScript,见 env.js 的 getClientEnvironment)。一个容易忽略的细节是:当publicUrlOrPath以.开头(相对路径)时,MiniCssExtractPlugin 的 publicPath 会被设为../../,因为生产构建的 CSS 位于static/css目录下,需要两级../才能定位到index.html所在目录。
同一份构建部署到不同路径
该能力自
react-scripts@0.9.0起可用。
如果你没有使用 HTML5pushStatehistory API,甚至完全不用客户端路由,就没有必要在package.json里写明应用将被服务的 URL。取而代之,可以这样写:
"homepage": ".",这会让所有资源路径相对于index.html。之后你可以把应用从http://mywebsite.com搬到http://mywebsite.com/relativepath,甚至http://mywebsite.com/relative/path,而无需重新构建。这正是上文源码分析中“生产模式下.被原样保留”这条规则的用途。
6. 为任意构建环境定制环境变量
你可以通过创建自定义.env文件并借助env-cmd来构建任意构建环境。以 staging 环境为例:
创建名为
.env.staging的文件;像普通
.env文件一样设置变量(例如REACT_APP_API_URL=http://api-staging.example.com);安装
env-cmd:$ npm install env-cmd --save $ # 或者 $ yarn add env-cmd在
package.json中新增一个使用该环境构建的脚本:{ "scripts": { "build:staging": "env-cmd -f .env.staging npm run build" } }
现在运行npm run build:staging即可使用 staging 环境配置进行构建,其他环境可依葫芦画瓢。
.env.production中的变量会作为回退生效,因为构建时NODE_ENV恒为production(build.js 第 11-13 行 在最开始就把NODE_ENV设为production)。环境变量文件的加载顺序可以在 env.js 第 26-34 行 得到印证:.env.production.local>.env.local(非 test 环境)>.env.production>.env,且 dotenv 永不覆盖已存在的环境变量——所以env-cmd提前注入的变量会优先于这些文件生效。另外注意 build.js 第 181-200 行:当检测到CI环境变量为真时,构建 warning 会被当作 error 处理(source map 解析类 warning 除外),这对在 CI 中执行上述构建脚本的团队是一个重要的质量闸门。
7. 主流云平台部署方案
AWS Amplify
AWS Amplify Console 为现代 Web 应用(单页应用与静态站点生成器)提供持续部署与托管,附带 serverless 后端能力,包括全球 CDN、自定义域名、特性分支部署和口令保护。
- 登录 Amplify Console 控制台;
- 关联你的 create-react-app 仓库并选择分支(也可以选用社区提供的 create-react-app + Amplify 鉴权 starter 快速起步);
- Amplify Console 会自动识别构建配置,选择 Next;
- 选择Save and deploy。
构建成功后,应用即部署并托管在 amplifyapp.com 域名的全球 CDN 上,后续每次向 Git 仓库提交代码都会触发前端或后端的持续部署。
Azure Static Web Apps
Azure Static Web Apps 基于 GitHub Actions 为 React 应用创建自动化的构建与部署流水线,应用默认地理分布、多接入点,PR 会自动构建出 staging 环境预览。
- 在 Azure 门户创建新的 Static Web App;
- 填写信息并关联你的 GitHub 仓库;
- 确认 “build” 选项卡中构建文件夹配置正确,然后创建资源。
Azure Static Web Apps 会在你的仓库中自动配置好 GitHub Action 并开始部署;路由、API、认证与授权、自定义域名等更多能力可查阅其官方文档。
Firebase Hosting
如果尚未安装 Firebase CLI,先运行npm install -g firebase-tools。注册 Firebase 账号并创建一个新项目,然后运行firebase login登录。
在项目根目录运行firebase init:选择Hosting: Configure and deploy Firebase Hosting sites,选择刚创建的 Firebase 项目,同意生成database.rules.json,把build选为 public directory,并在询问Configure as a single-page app时回复y。典型的交互输出如下:
=== Project Setup First, let's associate this project directory with a Firebase project. You can create multiple project aliases by running firebase use --add, but for now we'll set up a default project. ? What Firebase project do you want to associate as default? Example app (example-app-fd690) === Database Setup Firebase Realtime Database Rules allow you to define how your data should be structured and when your data can be read from and written to. ? What file should be used for Database Rules? database.rules.json ✔ Database Rules for example-app-fd690 have been downloaded to database.rules.json. Future modifications to database.rules.json will update Database Rules when you run firebase deploy. === Hosting Setup Your public directory is the folder (relative to your project directory) that will contain Hosting assets to uploaded with firebase deploy. If you have a build process for your assets, use your build's output directory. ? What do you want to use as your public directory? build ? Configure as a single-page app (rewrite all urls to /index.html)? Yes ✔ Wrote build/index.html i Writing configuration info to firebase.json... i Writing project information to .firebaserc... ✔ Firebase initialization complete!重要:你需要在firebase.json中为service-worker.js文件设置正确的 HTTP 缓存头,否则首次部署之后你将看不到任何变更(对应 create-react-app 仓库的历史 issue #2440)。在"hosting"键内添加:
{ "hosting": { ... "headers": [ {"source": "/service-worker.js", "headers": [{"key": "Cache-Control", "value": "no-cache"}]} ] ... } }之后创建生产构建npm run build,再运行firebase deploy即可部署:
=== Deploying to 'example-app-fd690'... i deploying database, hosting ✔ database: rules ready to deploy. i hosting: preparing build directory for upload... Uploading: [============================== ] 75%✔ hosting: build folder uploaded successfully ✔ hosting: 8 files uploaded successfully i starting release process (may take several minutes)... ✔ Deploy complete!GitHub Pages
该能力自
react-scripts@0.2.0起可用。
第 1 步:在 package.json 中添加 homepage
这一步至关重要!如果跳过,应用将无法正确部署。
打开package.json,为项目添加homepage字段。项目页(project page):
"homepage": "https://myusername.github.io/my-app",GitHub 用户页(user page):
"homepage": "https://myusername.github.io",自定义域名页:
"homepage": "https://mywebsite.com",create-react-app 使用homepage字段来确定构建后 HTML 文件中的根 URL(见第 5 节的源码解析)。
第 2 步:安装 gh-pages 并添加 deploy 脚本
安装之后,每次运行npm run build都会看到一份如何部署到 GitHub Pages 的 cheat sheet(即 printHostingInstructions.js 检测homepage含.github.io/后打印的分支)。
要发布到https://myusername.github.io/my-app,先安装:
npm install --save gh-pages或者使用 yarn:
yarn add gh-pages在package.json中添加以下脚本:
"scripts": { + "predeploy": "npm run build", + "deploy": "gh-pages -d build", "start": "react-scripts start", "build": "react-scripts build",predeploy脚本会在deploy运行前自动执行。
如果要部署到GitHub 用户页而非项目页,还需额外修改:把package.json脚本调整为推送部署到main分支:
"scripts": { "predeploy": "npm run build", - "deploy": "gh-pages -d build", + "deploy": "gh-pages -b main -d build",第 3 步:运行 npm run deploy 部署站点
npm run deploy第 4 步:项目页需确认仓库设置使用 gh-pages 分支
最后,确保 GitHub 仓库设置中的GitHub Pages选项配置为从gh-pages分支提供服务。
第 5 步(可选):配置自定义域名
你可以向public/文件夹添加一个CNAME文件来为 GitHub Pages 配置自定义域名,内容形如:
mywebsite.com关于客户端路由的说明
GitHub Pages 不支持使用 HTML5pushStatehistory API 的路由器(例如使用browserHistory的 React Router)。因为当http://user.github.io/todomvc/todos/42这样含前端路由的 URL 发生首次页面加载时,GitHub Pages 服务器不认识/todos/42,会返回 404。如果你要在托管于 GitHub Pages 的项目中加入路由器,有几种解决思路:
- 从 HTML5 history API 切换为基于 hash 的路由。如果使用 React Router,可以改用
hashHistory,但 URL 会更长、更啰嗦(例如http://user.github.io/todomvc/#/todos/42?_k=yknaj)。 - 或者使用一种技巧,让 GitHub Pages 把 404 重定向到带自定义 redirect 参数的
index.html:在部署前向build文件夹添加一个含重定向代码的404.html,并在index.html中加入处理该 redirect 参数的代码。社区中有一篇名为 “spa-github-pages” 的指南详细解释了该技巧。
故障排查
“/dev/tty: No such a device or address”
如果部署时出现/dev/tty: No such a device or address或类似错误,尝试:
- 创建一个新的 GitHub Personal Access Token;
- 运行
git remote set-url origin https://<user>:<token>@github.com/<user>/<repo>; - 再次尝试
npm run deploy。
“Cannot read property 'email' of null”
如果部署时出现Cannot read property 'email' of null,尝试:
git config --global user.name '<your_name>'git config --global user.email '<your_email>'- 再次尝试
npm run deploy。
Heroku
使用面向 create-react-app 的 Heroku Buildpack(基于 Node.js Buildpack 的零配置方案),按官方博客 “Deploying React with Zero Configuration” 的步骤操作即可。
Heroku 部署错误排查
有时npm run build本地能成功,却在 Heroku 部署时失败,以下是最常见的两类情况。
“Module not found: Error: Cannot resolve 'file' or 'directory'”
如果看到类似:
remote: Failed to create a production build. Reason: remote: Module not found: Error: Cannot resolve 'file' or 'directory' MyDirectory in /tmp/build_1234/src意味着你需要确保import的文件或目录大小写与文件系统(或 GitHub 仓库)中实际一致。这一点很关键,因为 Heroku 使用的 Linux 是大小写敏感的:MyDirectory与mydirectory是两个不同的目录——即使本地能构建成功,大小写不一致也会破坏 Heroku 远程构建中的import语句。
“Could not find a required file.”
如果你把必要文件排除或忽略在包之外,会看到类似错误:
remote: Could not find a required file. remote: Name: `index.html` remote: Searched in: /tmp/build_a2875fc163b209225122d68916f1d4df/public remote: remote: npm ERR! Linux 3.13.0-105-generic remote: npm ERR! argv "/tmp/build_a2875fc163b209225122d68916f1d4df/.heroku/node/bin/node" "/tmp/build_a2875fc163b209225122d68916f1d4df/.heroku/node/bin/npm" "run" "build"此时请确保该文件以正确的大小写存在,且没有被本地.gitignore或~/.gitignore_global忽略。这也与第 1 节中构建前的checkRequiredFiles([paths.appHtml, paths.appIndexJs])校验相呼应:public/index.html与src/index.js缺失都会直接让构建失败。
Netlify
手动部署到 Netlify CDN:
npm install netlify-cli -g netlify deploy选择build作为部署路径。
配置持续交付:
这样配置后,Netlify 会在你推送 git 或打开 pull request 时自动构建并部署:
- 创建新的 Netlify 项目;
- 选择你的 Git 托管服务并选中仓库;
- 点击
Build your site。
客户端路由支持:
要支持pushState,请创建public/_redirects文件并写入如下重写规则:
/* /index.html 200构建项目时,create-react-app 会把public文件夹的内容放入构建输出(与 Apache.htaccess方案同一机制)。
Vercel
Vercel 是一个 Jamstack 云托管平台:即时部署、自动扩缩容、零配置,提供全球边缘网络、SSL 加密、资源压缩、缓存失效等能力。
第 1 步:部署你的 React 项目。使用 Vercel 的 Git 集成时,确保项目已推送到 Git 仓库;通过 Import Flow 将项目导入 Vercel,导入过程中所有相关构建选项都会为你预配置好,也可按需修改。项目导入后,所有后续推送到各分支都会生成 Preview Deployment,而对 Production Branch(通常是main或master)的改动会生成 Production Deployment。部署完成后你会得到一个在线 URL。
第 2 步(可选):使用自定义域名。在 Vercel 账号的 Domain 设置中添加或转移域名:进入 Dashboard 中的项目页,点击 Settings 选项卡下的Domains菜单项,填入希望绑定的域名,随后按提示选择 DNS 配置方式。
对于全新的 React 项目,还可以直接通过 Vercel 提供的 Deploy Button 一键部署,Git 仓库会自动帮你建好。
Render
Render 提供免费静态站点托管,含完全托管的 SSL、全球 CDN 以及来自 GitHub 的持续自动部署,按官方的 create-react-app 部署指南操作即可在几分钟内完成部署。
S3 与 CloudFront
将 React 应用部署到 AWS S3 + CloudFront 的通行做法是:npm run build后把build目录内容上传到 S3 桶并开启静态网站托管,再由 CloudFront 作为 CDN 前置分发;为支持客户端路由,需要在 CloudFront 配置中把 404 错误页指向index.html。若还要附加自定义域名、HTTPS 与持续部署,可以在此基础上配置 ACME/证书与 CI 钩子。社区中有两篇经典教程分别覆盖了“S3/CloudFront 基础部署”和“S3 + HTTPS + 自定义域名 + CDN 完整指南”两种深度。
Surge
如果尚未安装 Surge CLI,运行npm install -g surge。执行surge命令并登录(或注册新账号)。询问项目路径时,务必指定build文件夹,例如:
project path: /path/to/project/build注意:为了支持使用 HTML5pushStateAPI 的路由器,建议部署前把build文件夹中的index.html重命名为200.html——这样可以确保所有 URL 都回退到该文件(Surge 原生支持200.html作为 SPA 回退页)。
8. 把组件发布到 npm
create-react-app 本身不提供把组件发布到 npm 的内置能力。当你准备从项目中抽出一个组件供他人使用时,官方建议是:把它移出项目、放到一个独立的目录中,然后使用如nwb之类的工具来准备发布。换言之,create-react-app 的职责止步于“应用”的构建与部署,组件库的封装应交给专门的工具链。
小结
build目录是唯一的部署产物:public/全量拷贝(index.html除外)+ webpack 编译输出,serve -s build或任意静态服务器即可承接;- 客户端路由的关键是让服务器对未知路径回退
index.html(Express 通配路由、Apache.htaccess、Netlify_redirects、Surge200.html都是同一思想的变体); homepage字段通过getPublicUrlOrPath决定output.publicPath与%PUBLIC_URL%,设为"."可获得跨路径免重构建的相对路径构建;- 多环境构建用
.env.staging+env-cmd,环境变量文件按env.js的固定顺序加载且不可覆盖已存在变量,CI 下 warning 会被提升为 error; - 各云平台方案(GitHub Pages、Firebase、Netlify、Vercel、AWS、Azure、Heroku、Render、S3/CloudFront、Surge)本质上都只是“构建目录 + 路由回退 + 缓存策略”三要素的不同组合。
【免费下载链接】create-react-appSet up a modern web app by running one command.项目地址: https://gitcode.com/gh_mirrors/cr/create-react-app
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考