
模块 4.4:让数据驱动界面
- 零到全栈
- June 26, 2026
Table of Contents
React 最大的变化,就是把“操作页面”变成了“改变数据/值”。页面根据数据自动长成应该有的样子,这就是 React 的另一个核心思想——数据驱动界面。
前言
这一节我们继续优化我们上一节课的那个用 React 实现的项目。首先我们先回顾并关注上一节的两个组件细节。
首先是 PageHeading 组件,它把标题 title 和副标题 subtitle 空出来,留给调用它的组件去往其中喂内容,以此来实现复用。
然后是 App 组件,它有两个职责,一个是把现有的两个 Page 组件收纳其中,另一个是控制什么时候该呈现 HomePage,什么时候该呈现 TextLabPage。
它们目前已经可以正常工作,但是我们今天要基于这两个组件的行为,做进一步的思考——现在这样的实现有什么问题吗?还可以更好吗?
数据驱动界面
我们先看 PageHeading。它把标题那个位置空了出来——你用它的时候,喂给它什么字,它就显示什么字。个人主页喂“关于我”,文字实验室喂“文字实验室”,于是同一个组件,在两页显示成了不同的标题。
一句话:界面长什么样,不写死在组件里,而是照着“喂进去的值”显示。 喂的值变了,显示就变。
如果用这个视角来看,那么 App 又何尝不是在照着一个“值”决定显示哪一页呢?只不过这个值的来路不太一样——它不是哪个外部组件喂给它的,而是 App 自己揣在手里的:用户点了导航栏中的“个人主页”,这个值就成了“home”,点了“文字实验室”,它就成了“textlab”;App 再照着这个值,决定当下给用户显示哪一个 Page。(这种“组件自己揣着、还会变”的值,待会儿我们会专门讲。)
界面,就是照着一些“值”在显示,就是所谓的数据驱动界面。
数据与界面分离
一个项目在建设完成之后,不会一直不变。总会有需要修改的时候,尤其是数据的部分。比如说我们的 subtitle,或者作品列表,它们发生变化是一个太正常不过的事情了。
相对来说,界面的结构变化一般不如数据的变化那么频繁。如果是这样的话,那么大家就摸索出来了一种工程管理的思想——把数据和界面分离。
先看一个现象:假设现在我们想把首页那句“关于我”改成别的,得进入 HomePage 的组件代码里,在一堆标签中间找到它、再改。内容和代码搅在一起,这就是数据和界面没有分离的状态。
数据和界面分离,就是设想把网站要显示的这些文字,集中到一个地方,专门来管理。
比如说像这样存数据:
export const home = {
heroTitle: "关于我",
heroSubtitle: "项目,创意,灵感,心得,我的作品",
featuredWork: {
kicker: "作品",
title: "文字实验室",
copy: "拼音和情绪,挖掘中文里的细节",
linkLabel: "打开作品",
},
identity: {
motto: "已识乾坤大,尤怜草木青",
learning: "零到全栈",
},
};
export const textLab = {
heroTitle: "文字实验室",
heroSubtitle: "拼音和情绪,挖掘中文里的细节",
};
它基本就是一张纯内容清单——网站上要显示的字,都在这儿。
有些同学可能会把这个和我们前面学过的那个 json 格式联系起来。它们很相似,但有点不一样,这个大括号里最后一项的结尾是可以有逗号的。
而组件那边(HomePage)呢,不再写死任何文案了,它只需要 到这张表里,把 heroTitle 取出来显示,至于 heroTitle 写的是“关于我”还是“About Me”,它就不关心了。
我们现在可以下载这一节的 demo,看一看(目前 demos 中的 zero-to-tech-4-4 就已经把页面展示的数据抽取到了 src/data/site.js 中了)。
git clone https://github.com/joylibo/zero-to-tech-demos
cd zero-to-tech-demos/zero-to-tech-4-4
npm install
npm run dev
看现象,最直接:
打开
src/data/site.js,把heroTitle改成别的字,保存——页面上的大标题当场就变了。而组件代码HomePage.jsx和PageHeading.jsx,你一个字都没碰。
这就是数据和界面分开:
- 组件只管“怎么显示”(排版、样式、结构);
site.js只管“显示什么内容”。
两边各管各的,谁也不烦谁。
而且这么一分,好处马上就来。举一个最直观的:哪天你想给网站做一个英文版,怎么办?你会发现,组件一个都不用碰——只要再备一份英文的内容表(把 site.js 里的字都译成英文),让组件根据“当前选的是中文还是英文”去读对应那张表就行。排版、结构、样式,一个字都不用改。“做多语言”这种听起来挺折腾的事,就因为数据和界面早早分了家,一下就变简单了。
这里还悄悄埋了一颗模块 5 的种子:现在这张表是写死在文件里的。等模块 5 引入后端 API 的概念,
site.js中的这些内容可以改成从网络接口实时取。
状态驱动 UI
上面看到的示例,都是数据驱动 UI——但它们有个共同点:组件显示的值是从外面喂进来的。哪怕内容集中进了 site.js,也还得由父组件读出来、再传给子组件去显示。这些值还有一个特点:它们都是定死的,你不主动去改 site.js,它就不会变。
但有些数据,需要在用户用着用着的时候自己变——它不是谁喂进来的,而是组件在被操作时自己产生、自己揣着、自己管的数据。
这种“会变、且一变显示就跟”的值,在 React 中一般就会被称为组件的状态(state)。
想象一个最纯粹的组件,它身上揣着一个值:
- 这个值是它自己的一部分(不是外面喂的,是它自己揣着的);
- 它有个默认值(一出生就带着);
- 组件显示什么,全照着这个值;
- 这个值允许被改;而且一改,显示当场就跟着变。
在 demo-4-4 中,我们已经在一个组件上实现了“状态驱动 UI”的效果。
cd zero-to-tech-demos/zero-to-tech-4-4
npm run dev
打开 demo 的文字实验室那页,盯住输入框下面那行“已输入 N 字”:
你往框里多敲几个字、删几个——那行数字当场跟着跳。
这就是上述我说的状态驱动界面:框里“当前的文字”是个会变的值(state),你打字的过程就是在改它,下面的字数照着它算——一改,立刻变。
你完全不用看这是怎么写的。React 内部替你盯着这个值、值一变就自动刷新界面——这套机制就是它最核心的引擎。你只要认得这个现象:有的值会变,界面会自动跟着它走。
其实,这个机制我们在上一节已经见过一次了:在导航栏中点击,切换页面,靠的就是一个这样的值(“现在在哪一页”)。所以 App 组件之所以能够控制什么时候该呈现 HomePage,什么时候该呈现 TextLabPage,靠的也就是这个 state。
用 URL 管理路由
我们再看一下,上一节课的这个 App 管理 Page 的效果:
cd ~/zero-to-tech
npm run dev
鼠标点击顶部导航栏,的确能够实现页面的切换。
这个就叫做路由。
但是,现在用 state 来管理路由,是不是有一点问题?如果,现在选中“文字实验室”,虽然页面呈现的是 TextLabPage 这个页面组件的内容,但是,URL 始终都没有变化,一直是 /,这个 / 代表网站的首页(根路径)。此时如果刷新一下呢?另外,如果我们想要分享这个文字实验室的页面给别人,别人打开之后,看到的是文字实验室,还是个人主页呢?
答案很显然,尽管我们选中了文字实验室,但是一旦页面刷新,就又回到了个人主页。另外,如果我们在文字实验室页面复制 URL,发送给其他人,其他人打开之后看到的也会是个人主页这个首页,而不是文字实验室页面。
这个就是用 state 来管理路由的时候,会出现的问题。
这是因为 state 是被存到了浏览器的内存里的。浏览器的内存一刷新就清空——所以刷新之后,“当前选中了哪个页面”这条信息就丢失了,地址栏也不知道你在哪页。
但是,如果我们观察 demo-4-4,就会发现它已经解决了这个问题:
cd zero-to-tech-demos/zero-to-tech-4-4
npm run dev
先看现象。打开网站,点一下导航的“文字实验室”——盯住浏览器最上面的地址栏:它从 …/ 变成了 …/text-lab。
现在,在这一页按一下刷新。
它还稳稳停在文字实验室。
再点浏览器的后退键:回到了首页。
把地址栏里 …/text-lab 这个网址复制下来发给朋友(或者我们换一个浏览器打开,无中生“友”一下):朋友一打开,直接就是文字实验室那页。
为什么 demo-4-4 可以做到呢?差别只在一处——demo-4-4 把它写进了浏览器的网址(URL) 里。网址不会因为刷新消失,还能整条复制——所以刷新还在、链接能发、前进后退也能用。
把“在哪一页”从“记在内存里”,换成了“写在网址里”。 网址天生就刷新不丢、能复制、能前进后退——它天生就适合用作页面路由。
这背后其实是有个叫“路由”的小文件在盯着网址、在你点导航时帮你改网址。你不用看它怎么写——看懂上面那个“值存哪儿”的区别,就够了。
顺带认个名字:这个“小路由”是我们自己手搓的极简版。真实项目里大家往往不手搓,而是用一个现成的库,叫 react-router。你不用学它怎么写,认得这个名字、了解这个概念就行——以后看 React 项目、看 AI 写的代码,可能会碰见它。
这其实也是数据驱动界面的一个案例,不同的是它的数据是存在于 URL 里面,而不是在内存里。
关于从外部获取数据
你大概也注意到了——文字实验室“结果区”里显示的拼音、情感分数,目前是写死的假数据;点“开始分析”也不会真发生什么。
考虑到我们马上要讲后端了,这部分内容我们就留给后端来解决,我们后续会接入一个小小的算法模型,帮我们把这个情感分数给算出来。
由后端模型把数据算出来再给到前端,其实也是一种数据驱动界面的具体体现,只不过数据是由后端网络接口提供的。这个我们这一节先不演示,你知道这个意思就行。
升级我们的项目
这一节重点是看懂上面那几件事。我们目前已经知道了 demo-4-4 的代码实现是优化后的版本,那么我们就让自己那个 zero-to-tech 项目跟着长到这一步,不用大动——在我们现有项目的基础上,从 demo 里拷这几个文件就行:
src/data/site.js(新增——那张内容表)src/router/useRoute.js(新增——盯网址的小路由)- 用 demo 里更新过的版本覆盖这几个:
src/App.jsxsrc/components/HomePage.jsxsrc/components/TextLabPage.jsxsrc/components/InputCard.jsxsrc/css/lab.css(InputCard 那行“已输入 N 字”用到一个新样式.lab-count,就加在这里)
拷完 npm run dev,对着上面那些现象挨个试一遍:地址栏会变、刷新还在、输入框打字字数当场跳、改 site.js 页面当场变。一致,就说明你接对了。
demo 仓库里
zero-to-tech-4-4/README.md有更细的分步操作,也可以跟着它的描述走。
把它发布到公网
到这儿,我们的 zero-to-tech 已经是个像样的双页面 React 项目了——可它还只在你电脑的 localhost:5173 上跑,外面没人看得见。我们现在就把这个改进后的 React 版送上线,让它可以在互联网上被访问。
我们上一次做发布是在 4.1 那一节课,当时我们的项目还是 vanilla 实现,我们把它发布到了 Ubuntu 服务器,那个发布分三步:
- 本地
git push到 GitHub; - SSH 到服务器
git pull把代码拉下来; - Nginx 的
root指向那个文件夹,reload,搞定。
那次的项目特别“原生”——一堆手写的 .html / .css / .js,Git 拉到哪儿、Nginx 对哪儿,直接就能服务。
可现在不一样了。这个项目是个 Vite + React 工程:浏览器不认识 .jsx,得先 npm run build 把它构建成普通的 .html / .css / .js(4.2 讲过)。而且 4.2 还立了条规矩——构建产物 dist/ 进 .gitignore,不提交。
于是这次比 4.1 多绕一道弯:我们 push 上去的是源代码,
dist/不在里头。那 Nginx 要服务的dist/从哪来?——服务器要先npm install把依赖准备好,再自己npm run build一次,把源码变成dist/。因此,我们的服务器第一次要请来一个新角色:Node。它负责把源代码 build 成静态文件;Nginx 还是那个 Nginx,负责把文件送给访问者。
第一步:给服务器装 Node。 SSH 远程登录上去,然后执行下面的命令(这些是来自 Node.js 官网提供的在 Linux 安装 Node.js 的命令):
# 下载并安装 nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# 代替重启 shell
\. "$HOME/.nvm/nvm.sh"
# 下载并安装 Node.js:
nvm install 24
# 验证 Node.js 版本:
node -v # Should print "v24.17.0".
# 验证 npm 版本:
npm -v # Should print "11.13.0".
第二步:推代码、拉代码(4.1 的老动作)。 本地 git push;服务器上把代码拉下来:
cd ~/zero-to-tech
git pull
第三步:在服务器上安装依赖并 build。
cd ~/zero-to-tech
npm install
npm run build
build 完,项目里多了个 dist/——这就是要交给 Nginx 的那一坨,跟 4.1 那堆手写 html 同性质:都是浏览器能直接跑的死文件。
dist/
index.html ← 页面外壳
assets/
index-[hash].js ← 打包压缩过的 JS
index-[hash].css ← 打包压缩过的 CSS
第四步:让 Nginx 指向 dist/。
sudo vim /etc/nginx/sites-enabled/default
仅修改 root 这一行:
server {
listen 80 default_server;
server_name _;
root /home/ubuntu/zero-to-tech/dist; # ← 指向 build 产物
index index.html;
}
和 4.1 那一节相比,实质就改了一处:root 从手写 html 的目录,挪到了 build 产物 dist/。测试 + reload,还是 4.1 用过的老命令:
sudo nginx -t
sudo systemctl reload nginx
见证一下。 浏览器打开 http://你的服务器IP/:
个人主页稳稳出现——卡片飞入、分数滚动,跟你本地看到的一模一样。你亲手做的这个 React 网站,现在全世界都访问得到了。 🎉
以后每次改完代码:本地
git push→ 服务器git pull→npm install→npm run build。跟 4.1 比,工程化项目不再是“拉下来就能直接给 Nginx”,中间多了一个准备依赖、再构建产物的过程——这就是“工程化项目”要付的成本,换来的是组件复用、构建优化那些好处。
核心概念回顾
界面,说到底就是照着一些“值”在显示。这一节,我们见识了这些“值”的三种玩法:
- 值能从外面喂进去,还能集中起来管。 组件空出一个位置,使用它的时候喂它什么、它就显示什么;而要显示的内容,可以集中记在一个文件里——改文案、加作品,只动这个文件,组件一个字不用碰。(这就是 props + 数据分离)
- 值能自己变,界面自动跟。 组件可以有一些自己控制的值,当用户与组件交互(打字、点一下),组件自己揣着的那个值就变——它一变,界面当场跟着变。(这就是 state)
- 值还能存进网址里。 把“现在在哪一页”写进 URL,于是刷新不丢、链接能发、前进后退都好使。(这就是路由)
这一节我们还顺手做了件大事:把项目发布到了公网——push 源码、服务器 npm install 准备依赖、npm run build 生成 dist/,再让 Nginx 指向 dist/。与 4.1 的发布流程相比,关键变化是:Nginx 服务的不再是源码目录,而是服务器上装好依赖后构建出来的产物。
下一节,我们再细看这个刚上线的网站,并且我们要在下一节正式请 Next.js 出场。