模块 4.6:把前端项目发布到公网

模块 4.6:把前端项目发布到公网

Table of Contents

前端部分最后一站,把我们一手带起来的现代前端项目发布到服务器上去。


这一节我们要干什么

到目前为止,我们手上有一个功能完整的双页面 Next.js 项目——就是上一节已经提交的那个 zero-to-tech,可它还只在 localhost:3000 上跑。这一节,我们把它部署上线

但动手之前,先有道选择题:一个 Next 项目,“怎么上线”其实有两种方式。我们先把这两种讲清楚、选定一种,再照着干。

这一节没有配套 demo——就在我们已有的项目上动手(上一节往 Next 的迁移要先做了)。


Next 项目的两种上线方式

上一节我们提到 next build 默认产出的 .next/server/app/ 里,虽然也躺着 index.htmltext-lab.html 这些真实文件,但不能像之前 Vite 的 dist/ 那样直接交给 Nginx。为什么?

因为 .next/ 不是一个“网站文件夹”,而是 Next 自己的一堆构建半成品——它是做给一台跑着的 Next 服务吃的(使用 npm run start 可以开启这个服务),不是给 Nginx 直接发的:

  • 那些 .html 里引用的 CSS / JS,散在 .next/ 的别处.next/static/),凑不成“一个干净的网站根目录”;
  • /text-lab 这种没后缀的网址该回哪个文件、404 怎么兜、缓存头怎么设——这些是那台 Next 服务运行时现办的;
  • 里头还混着一堆只有服务器看得懂的东西(.rsc 数据、各种 manifest、服务端 JS)。

为什么 Next.js 搞了这么个 npm run start 服务呢?这要说回到上一节我们简单提过的一个概念——服务端组件(Server Component)

Next.js 把 React 组件分成两类:客户端组件服务端组件

客户端组件,是要在浏览器里“跑起来”的那种——它得响应点击、改状态、播动画(4.3、4.4 我们写的那些组件都是这类),所以它会被构建工具翻译成 js、送到浏览器里去执行。

服务端组件,则不需要在浏览器里跑,纯粹是把界面画出来给人看——它在服务端(或者构建时)就能把界面画成 HTML,浏览器直接拿到画好的成品,连 js 都不用送。

这里要点破一个关键、也最容易误会的点:在 Next 里,这两类组件最后都会被“预渲染”成 HTML(这正是 4.5 说它对 SEO 友好的原因——查看源代码,页面内容实打实都在)。区别只在于:客户端组件除了那份 HTML,还会额外送一份 js 到浏览器,好让它“活”过来、能交互;服务端组件就只给 HTML、不送 js。所以 "use client" 不是“不预渲染”,而是“还要在浏览器里再跑一遍”。

那么 .next/server/app/ 里这些 .html,是不是 build 时就画好了呢?——对我们这个项目,是的。数据全写死在前端,Next 在 build 时就能把每一页画成 HTML 存这儿(build 日志里那排 ○ (Static) 就是它)。

可它们画好了,也还是不能直接交给 Nginx——原因就是上面那三条:整包 .next/ 是按“喂给 next start 那台服务”的结构摆的,不是一个能直接发的干净静态站。

那 Next 默认为什么非要你跑这么一台服务?因为它还能干一件静态文件干不了的事——对动态页面,按每个请求当场现画(每次的数据可能都不一样)。我们这个项目没有动态页、用不上这本事;但 Next 默认冲着“通用”去,就默认给你备好那台服务。

好在 Next 也给了另一条路:output: "export"——等于告诉它“我每一页 build 时都能画死,别给我留服务了,直接打包成一个干净的静态 out/ 文件夹”。

所以,一个 Next 项目上线,就有了两条路可选

第一条路 A:跑一个常驻的 Next 服务(用 npm run start

在服务器上启动 Next 自带的 Node 服务(next start),由它接住每一个用户请求,服务整个网站。.next/ 那堆半成品,正是喂给它的。

  • 能耐最大:它能在服务器上、按每个请求,当场把最新数据现画进发出去的 HTML——这是“静态导出”做不到的(静态文件 build 时就定死了)。也正因如此,它是 Next 默认的、主流的部署方式(交给 Vercel 这类平台托管,走的也是这条)。
  • 代价:服务器上得有一个 Node 进程 7×24 常驻。网站不再是“一堆静态文件”,而成了“一个一直跑着的程序”——更重、也更费服务端计算资源。

第二条路 B:静态导出一个 out/(用 output: "export"

build 环节直接吐出一个干净的静态网站文件夹 out/——每页一个 .html,里头两类组件的界面都已经预渲染好了;其中客户端组件再额外配上一份 js,好让它到浏览器里能交互。整体就跟我们之前见过的 dist/ 一个样,以一个文件夹(out/)的形式 交给 Nginx 做转发,服务器上不用常驻任何东西

  • 代价:它相比第一条路 A 而言,无法做到在请求环节灵活调整页面内容,更适合内容和结构在 build 时就能被确定的网站。如果需要动态内容,可以在浏览器再次发起请求获取。

怎么选?划清两条路的“地盘”

  • 走 B(静态):页面在 build 时就能定下来的——纯展示站、博客、文档,以及“静态前端 + 后端 API、数据靠浏览器现取”的那一大类
  • 走 A(常驻服务):页面的 HTML 本身必须在服务器上、按每个请求当场拼出来才行——典型是海量又时时变、还要被搜索引擎收录的电商 / 新闻 / 社媒等“重内容”的站点。

看了这个示意图可能就会更容易理解两种的差异了:

部署的两种方式:传统前端+后端 API vs RSC 动态渲染

有个特别容易误读的点先点破“内容是动态的” ≠ “必须走 A”。 登录/注册、读数据库、实时数值展示这些动态功能,绝大多数都是“静态前端 + 后端 API”做的——页面还是那套静态文件,数据由浏览器事后找接口现取,照样走 B。

我们这个 zero-to-tech,数据全硬编码在前端,就算后面讲到后端部分,接了后端接口之后也计划用“静态前端 + 后端 API”的方式,所以我们会选择 B 的方式。那就没必要去养一台常驻服务器。所以我们会按照 B 的部署方式来进行下一步:build 一次、出一堆静态文件、通过 Nginx 转发,又轻又稳,还跟 3.5 / 4.1 / 4.4 这几节课是同一套发布方式。


静态导出发布

选定 B,剩下的就跟我们在 4.4 那一节部署 Vite 版几乎一样了。只有两处不同

  • 差别一:build 产物从 dist/ 变成 out/——靠项目里加一行 output: "export"
  • 差别二:Nginx 的 root 指向 out/,并在 location 里加一句 try_files

下面把这两处落地,其余全是 4.4 的肌肉记忆。

差别一:加一行 output: "export"

因为 Next.js 默认的发布方式是走 next start,所以如果要构建静态文件,需要做一个配置。打开项目里的 next.config.mjs,加一行:

// 改之前
const nextConfig = {};

// 改之后
const nextConfig = {
  output: "export",       // ← 就加这一行
};

它告诉 Next:build 完别留给那台常驻服务了,直接把每一页预渲染成静态 HTML、连同资源一起塞进一个干净的 out/ 里。

改完之后,本地 npm run build 跑一次,项目里就多出个 out/

out/
  index.html            ← / 这一页
  text-lab.html         ← /text-lab 这一页   ← ← ← 看这个,它真实存在!
  404.html
  _next/static/         ← 打包压缩过的 CSS、JS……

注意,out/ 是构建产物,不进 Git(要在 .gitignore 里把 out/.next/ 忽略)。

差别二:Nginx 指向 out/ + 一句 try_files

部署的命令和我们此前运行过的一样,几乎一字不差,快速走一遍:

# 1. 本地:推代码(output:export 那行也一起提交了)
git push

# 2. 服务器:确认 Node 还在 → 拉代码 → 装依赖 → build
ssh ubuntu@你的服务器IP
node -v                       # 4.4 用 nvm 装过,能打印版本号就行
cd ~/zero-to-tech
git pull
npm install
npm run build                 # 服务器上也出一个 out/

但是 Nginx 配置要改一下——root 指向 out/,并加上一句 try_files

sudo vim /etc/nginx/sites-enabled/default
server {
    listen 80 default_server;
    server_name _;

    root /home/ubuntu/zero-to-tech/out;   # ← 4.4 是 .../dist,这次是 .../out
    index index.html;

    location / {
        # /text-lab 这种没后缀的 URL,自动追个 .html 去找
        try_files $uri $uri.html $uri/ =404;
    }
}
sudo nginx -t                 # 检查语法
sudo systemctl reload nginx

和 4.4 那节课中的 Nginx 配置相比,就两处不同rootdist 改成 out,外加这句 try_files

这句 try_files 的意思是:根据 url 的内容去找文件,或者根据 url 的内容加上 .html 后缀去找文件,否则就报 404。 当 URL 是 /text-lab 时,Nginx 先找同名文件 text-lab,找不到的话 就自动补个 .html 再找——而 out/text-lab.html 这次真的在那儿等着,所以不再 404。


见证上线

浏览器直接敲 http://服务器IP/text-lab——直达文字实验室,不会先进入首页、也不会报 404。上一节课开头那三个毛病,全部都没有了:

  • 404 没了out/text-lab.html 真实存在,Nginx 找得到、直接送出;
  • 白屏等待没了:服务器送来的就是已经画好text-lab.html,访客一来就看到内容,不用再等 JS 现画;
  • SEO 好了:右键“查看网页源代码”——HTML 里实打实就有页面内容(标题、文案都在),不再是 4.5 那个空壳,爬虫不跑 JS 也能读到。

这条路是怎么一步步走过来的:

  • 4.4 我们把 React 版部署上线,F12 一看——页面是浏览器临时画的;
  • 4.5 Next.js 把每一页预渲染成真实 HTML,从根上拔掉病根,但尚未部署;
  • 4.6 一行 output: "export" 把这些真实 HTML 导成 out/、塞上服务器,Nginx 找得到、送得出。

4.4 的痛、4.5 的解药、4.6 的落地——三节合成一个完整闭环

以后改代码之后的发布流程:本地 git commitgit push → 服务器 git pullnpm run build。(工程上可以把这套“pull + build”写成脚本、交给 CI/CD 自动跑——一推代码就自动上线,但那是后话。比自动化 CI/CD 流程更有价值的是你心里有这条链路:明白源码在哪、产物在哪、谁 build、谁服务。)


我们没走的那条路——A

刚才的两条路,A 和 B,我们走了 B,主线任务就此收工,我们这门课的前端部分也要结束了。但临走前,值得回头看一眼那条没走的路 A——因为它通向的地方,恰好就是这门课的终点关键词:全栈

A 的核心,是服务器上有一台跑着的 Next 服务。这意味着 Next 不再只是“前端框架”——它能在服务器上、按每个请求当场把页面拼出来,于是:服务端组件可以直接连数据库、读文件、用上只有服务器才有的密钥,你甚至能在同一个 Next 项目里直接写后端接口app/api/...)。前端、后端,一个项目全包了——这就是大家说的“Next 全栈”。

但我们不打算用 Next 的这种方式来做后端。 我们这门课走的是另一条更经典、也更通用的分工:前端归前端(就是这个静态 out/,走 B),后端归后端(我们会用 Python 单独起一个后端服务,专门计算情感分数和拼音),两边靠 API 通话、各自独立部署。这不是“Next 全栈”,而是“静态前端 + 独立后端”。

两种都成立,我们挑了解耦的这种——它跟你前端用什么、后端用什么语言都无关,替换、迁移、各自扩容都更自由。

那为什么不顺势教 Next 全栈呢?三个原因:

  1. 它把前后端绑在同一个框架里,尽管这在 AI 编程时代有独特的优势(统一的上下文),但是对初学者而言,不利于建立工程化思维。把它们分开,对于初学者而言更有助于建立“前后端是两个独立角色”的清晰心智;
  2. Python 做后端的生态更为成熟,尤其是做一些算法模型服务或数据分析服务,Python 比 Node 有更丰富的生态;
  3. 更现实的——Next.js 全栈技术还很年轻,还需要成长。 服务端组件这套“请求时现画”的机制,就在不久前(2025 年 12 月)爆出了一个满分级(CVSS 10.0)的远程代码执行漏洞:攻击者只要一个不用登录的 HTTP 请求喂给那台常驻服务,就能让它跑任意代码,这个漏洞披露后很快被大规模利用,当时我自己也是受害者之一,攻击者直接把勒索信发到了我服务器的家目录里(所幸那只是一台“玩具”服务器,所以并未造成实质损失,我重装了系统,升级了 Next.js 的安全版本)。

新技术好用,但“够不够稳”得自己掂量。生态确实在往服务端组件(RSC)/ 全栈这个方向卷,值得我们认得它、知道它在解决什么;但我们这门课学习的全栈技术,到“纯静态 out/ + 后端 API”为止——已经足够做出可靠、能上线的真东西了。


收尾

我们整个现代前端课程,到这儿就结束了。现在我们拥有了:

  • 一个真正可访问的双页面现代前端——挂在自己的云主机上,IP 一访问,世界看得见;
  • 整个现代前端工程的心智地图:依赖怎么管、构建在做什么、npm run dev/build 各干啥、React 在哪一层、Next.js 又叠了什么、最后这堆东西怎么变成服务器上一坨文件;
  • 一份给后端留好钩子的前端:data/site.js 里的硬编码值、文字实验室那个“开始分析”按钮——下一步全都对接后端 API。

回头瞥一眼模块 4 走过的路—— 4.1 手写到工程化 → 4.2 构建 → 4.3 React 组件 → 4.4 数据驱动 + 路由 + 上线 → 4.5 Next.js → 4.6 静态导出收尾。 这正是任何一个“严肃前端项目”过去十年走过的真实历史。你没死记任何一个名词,而是被痛一路推着走完了一遍。

模块 5,我们让后端登场,把这些硬编码换成真接口。


← 上一节:模块 4.5 Next.js | 下一节:进入下一模块 →