模块 4.4:让数据驱动界面

模块 4.4:让数据驱动界面

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.jsxPageHeading.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.jsx
    • src/components/HomePage.jsx
    • src/components/TextLabPage.jsx
    • src/components/InputCard.jsx
    • src/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 服务器,那个发布分三步:

  1. 本地 git push 到 GitHub;
  2. SSH 到服务器 git pull 把代码拉下来;
  3. 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 pullnpm installnpm run build。跟 4.1 比,工程化项目不再是“拉下来就能直接给 Nginx”,中间多了一个准备依赖、再构建产物的过程——这就是“工程化项目”要付的成本,换来的是组件复用、构建优化那些好处。


核心概念回顾

界面,说到底就是照着一些“值”在显示。这一节,我们见识了这些“值”的三种玩法:

  1. 值能从外面喂进去,还能集中起来管。 组件空出一个位置,使用它的时候喂它什么、它就显示什么;而要显示的内容,可以集中记在一个文件里——改文案、加作品,只动这个文件,组件一个字不用碰。(这就是 props + 数据分离)
  2. 值能自己变,界面自动跟。 组件可以有一些自己控制的值,当用户与组件交互(打字、点一下),组件自己揣着的那个值就变——它一变,界面当场跟着变(这就是 state)
  3. 值还能存进网址里。 把“现在在哪一页”写进 URL,于是刷新不丢、链接能发、前进后退都好使(这就是路由)

这一节我们还顺手做了件大事:把项目发布到了公网——push 源码、服务器 npm install 准备依赖、npm run build 生成 dist/,再让 Nginx 指向 dist/。与 4.1 的发布流程相比,关键变化是:Nginx 服务的不再是源码目录,而是服务器上装好依赖后构建出来的产物。

下一节,我们再细看这个刚上线的网站,并且我们要在下一节正式请 Next.js 出场。


← 上一节:模块 4.3 React 登场 | 下一节:模块 4.5 Next.js →