Skip to content

医院排班与医院上线(Day15 3/3) ​

本页对应 React + TypeScript + 医通项目,共 6 段视频,约 2 小时 38 分。

学习目标 ​

  • 能够执行 npm run build 生成 build 产物,说明交付后端的目录内容,并在模拟 Express 服务中完成静态资源目录、history fallback、接口代理三项配置,验证页面刷新与登录请求正常。
  • 能够使用 i18next 与 react-i18next 创建中英文语言包和初始化配置,在组件中通过 useTranslation 的 t 函数渲染文案,并使用 changeLanguage 完成语言切换。
  • 能够集成 antd 国际化语言包,使用 ConfigProvider 的 locale 控制 Table 分页等组件内置文案,并与自定义国际化状态联动切换。
  • 能够解释 React diff 的同层比较与最小量更新原则,对比列表 key 使用唯一 id 与索引的差异,分析向前或中间追加删除时的错误复用与输入框串位问题,并给出正确 key 选择规则。
  • 能够应用 React 核心复习点完成代码说明或实现,包括函数/类组件的 state、props、事件对象与 this、生命周期与 useEffect、受控/非受控组件、ref、路由参数及 Redux Toolkit 基本读写流程。
  • 能够结合入行前三个月、半年市场复盘、学历与技术积累、目标大厂等阶段,制定个人 3–5 年职业发展计划,并列出各阶段可执行动作与检验节点。

知识点 ​

本节说明 ​

本节笔记标题为“医院排班”,但转写稿实际围绕项目上线部署、React 国际化、antd 国际化、diff 算法与 key、React 复习及职业规划展开。下面按转写稿整理,不补造医院排班业务细节。

打包上线部署:交给后端的是 build 目录 ​

为什么需要:开发目录里有 src、public、node_modules 等,后端并不懂前端工程,不能直接把源码目录丢给后端。
怎么想:前端先完成构建,把可部署的静态资源目录交给后端;后端再按服务器类型部署。
怎么做:执行 npm run build,得到 build 目录,里面通常有 index.html、压缩后的 JS/CSS。可以用 Express 模拟服务器:初始化 package.json、安装 express,配置静态资源目录、history fallback 和代理。

bash
npm run build
npm init -y
npm i express
npm i http-proxy-middleware
npm i connect-history-api-fallback
js
const express = require('express');
const app = express();

app.use(express.static(__dirname));
// 404 时回退到 index.html,解决刷新前端路由 404
app.use(require('connect-history-api-fallback')());
app.listen(5000);

易错点:public/index.html 里资源路径最好用 / 开头,表示最终 build 根目录;用 ./ 容易随地址变化找错目录。部署时要解决三件事:history 重定向、静态资源目录、代理跨域。刷新后白屏,本质是服务器按前端路由路径找文件找不到,不是 React 路由本身坏了。

i18n 国际化:语言包 + T 函数 + 切换语言 ​

为什么需要:页面文字不能写死,否则中英文切换要改所有组件。
怎么想:把文案按组件放到语言包 JSON 中,组件里用 t('hello') 读当前语言的值;切换语言时换语言包。
怎么做:安装 i18next、react-i18next,在 src/locales 下建 en-US.json、zh-CN.json,再建 i18n.ts 初始化。组件里用 useTranslation() 取 t 和 i18n,用 i18n.changeLanguage('en') 或 'zh' 切换。

js
const { t, i18n } = useTranslation();
// 显示
t('hello');
// 切换
i18n.changeLanguage('en');

易错点:语言包字段名要对应;默认语言在初始化配置里指定;切换语言后组件内仍用同一个 t 读不同语言包。转写稿提到 XSS 与 escapeValue,实际配置需以 i18next 文档为准,不要机械照搬。

antd 国际化:ConfigProvider 管组件内部文案 ​

为什么需要:Table 分页、快速跳转等文案在 antd 组件内部,不受自己写的 t 函数控制。
怎么想:自己写的文字用 i18n;antd 组件内部文字用 antd 内置语言包。
怎么做:安装 antd,从 antd/locale/zh_CN、antd/locale/en_US 导入语言包,用 ConfigProvider 包裹区域并设置 locale。

jsx
<ConfigProvider locale={flag ? zhCN : enUS}>
  <Table />
</ConfigProvider>

易错点:只切 i18n 不会自动切 antd 分页文案;locale 要跟随同一个状态切换。

diff 算法与 key:列表 key 优先用唯一 ID ​

为什么需要:setState 后 React 会比对新旧虚拟 DOM,尽量复用真实 DOM,实现最小量更新。
怎么想:同层比较;标签不同直接重建;组件不同也直接重建。列表比较靠 key 判断是否同一节点。
怎么做:列表项优先用唯一 ID:key={item.id}。只有纯展示、不涉及顺序变化时,才可退而用索引。
易错点:向前追加、删除、中间插入时用索引会导致错误复用,出现输入框内容串位;key 重复会报错。医院编码这类业务唯一值可以作唯一标识,但业务校验仍要单独做。

React 复习与职业规划要点 ​

为什么需要:上线部署、国际化、diff 都是项目收尾能力,面试和入职还会追问基础。
怎么想:把知识按“状态、props、事件、生命周期、路由、仓库、受控/非受控”归类复习。
怎么做:类组件关注 this 指向、生命周期;函数组件关注 useState、useEffect;受控组件通过状态和 onChange 获取值,非受控通过 ref 取真实 DOM 值。
易错点:函数组件没有 this 指向问题;useEffect 返回函数对应卸载清理;不要把所有知识混成一团。职业规划部分转写稿强调心态、前三个月坚持、定期面试了解行情和学历规划,属于经验建议,不是技术结论。

先明确交付物:给后端的是 build 目录 ​

为什么需要:项目写完不能把源码目录直接丢给后端。后端通常不熟悉前端工程,拿到 src、public、node_modules 这类目录也不知道怎么运行。

怎么想:前端开发时用 webpack 等工具启动开发服务器;上线时要先构建,把源码编译、压缩成浏览器可直接加载的静态文件。

怎么做:在项目根目录执行:

bash
npm run build

构建完成后会生成 build 目录。打开可以看到 index.html,里面引用了打包后的 JS、CSS。最终交付给后端的是这个 build 目录,而不是源码目录。

易错点:不要给后端 src、public、node_modules;不要以为 npm run dev 能直接上线。构建产物才是交付物。

用 Express 模拟服务器部署 ​

为什么需要:后端部署方式取决于公司服务器,可能是 Nginx、Apache 等,但核心操作相同。我们可以用 Express 模拟,提前发现上线问题。

怎么想:把 build 目录当成网站根目录,启动一个本地服务器,访问 http://localhost:5000 看页面是否正常。

怎么做:进入 build 目录初始化并安装依赖:

bash
npm init -y
npm i express

新建 server.js:

js
const express = require('express');
const app = express();

app.listen(5000, () => {
  console.log('server run at 5000');
});

运行:

bash
node server.js

易错点:命令要在 build 目录下执行;端口不要和已有服务冲突;server.js 是模拟文件,不是前端源码的一部分。

配置静态资源目录 ​

为什么需要:访问服务器时,浏览器要能加载 index.html、JS、CSS、图片等静态资源,否则页面空白。

怎么想:Express 需要把当前 build 目录设置为静态资源目录。

怎么做:

js
const express = require('express');
const app = express();

app.use(express.static(__dirname));

app.listen(5000, () => {
  console.log('server run at 5000');
});

易错点:express.static 的目录要指向 build 根目录;如果路径写错,页面会 404。public/index.html 里资源路径最好用绝对路径 /,不要用 ./,否则刷新或进入子路由后相对路径容易失效。

配置代理解决接口跨域 ​

为什么需要:开发阶段跨域代理由 webpack devServer 的 proxy 完成;打包上线后 webpack 不生效,生产环境请求前缀可能变成 /prod/api,需要服务器单独代理。

怎么想:把以前 devServer 里的代理配置迁移到 Express,用 http-proxy-middleware 做中间件。

怎么做:安装:

bash
npm i http-proxy-middleware

在 server.js 中配置:

js
const { createProxyMiddleware } = require('http-proxy-middleware');

app.use('/prod/api', createProxyMiddleware({
  target: 'http://后端服务器地址',
  changeOrigin: true,
  pathRewrite: { '^/prod/api': '' }
}));

易错点:target 要换成真实后端地址;pathRewrite 要按实际接口前缀调整;代理前缀必须和生产环境请求前缀一致。调试时看 Network:请求是否 200、地址和参数是否正确。

刷新 404 与 history fallback ​

为什么需要:前端路由在地址栏刷新时,服务器会按路径找文件,例如 /hospital/hospitalSet,但服务器上没有这个文件,于是 404。

怎么想:访问不存在的路径时,用 index.html 替代 404 响应,让前端路由接管。

怎么做:安装:

bash
npm i connect-history-api-fallback

在 server.js 中,必须放在所有中间件最前面:

js
const history = require('connect-history-api-fallback');
app.use(history());

完整顺序建议:

js
app.use(history());
app.use('/prod/api', createProxyMiddleware({ /* ... */ }));
app.use(express.static(__dirname));

易错点:history() 不放在最前面,可能导致静态资源或代理不生效;刷新 404 不是路由写错,而是服务器找不到对应文件。

上线部署要交代给后端的三件事 ​

为什么需要:后端不一定熟悉前端构建产物,最好明确告诉他 build 目录要处理什么。

怎么想:部署服务器不同,配置方式不同,但操作目标一致。

怎么做:交付 build 目录,并说明:1)配置静态资源目录;2)配置代理服务器;3)配置 404 重定向到 index.html。

易错点:不要只丢一个压缩包不说明;不要假设后端会自动处理 history 路由;代理和重定向顺序要检查。按这三件事排查,页面、接口、刷新问题基本都能定位。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

代码1:Express 模拟部署 build 目录(JS/Node)

bash
# 把 npm run build 生成的 build 目录交给后端
# 进入 build 目录后执行:
npm init -y
npm i express http-proxy-middleware connect-history-api-fallback
js
// server.js 放在 build 根目录,与 index.html 同级
const express = require('express');
const history = require('connect-history-api-fallback');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// 1. 所有 404 响应回退到 index.html,解决前端路由刷新 404
// 必须放在所有配置最前面
app.use(history());

// 2. 当前 build 目录作为静态资源目录
app.use(express.static(__dirname));

// 3. 打包后代理要单独配置:/prod/api -> 后端接口地址
app.use(
  '/prod/api',
  createProxyMiddleware({
    target: 'http://localhost:8080', // 换成实际后端接口地址
    changeOrigin: true,
    pathRewrite: {
      '^/prod/api': ''
    }
  })
);

app.listen(5000, () => {
  console.log('server run on 5000');
});

要点解释:

  • 给后端的不是 src/public/node_modules,而是 npm run build 生成的 build 目录。
  • app.use(history()) 解决刷新后找不到 index.html 下子路径的问题,转写稿强调它要放在所有配置最前面。
  • express.static(__dirname) 把当前 build 目录设为静态资源目录。
  • pathRewrite: { '^/prod/api': '' } 用于去掉请求前缀,和开发环境 proxy 配置对应。
  • 易错点:index.html 里资源路径建议用 / 开头;target 必须替换成真实后端地址;开发环境的 proxy 打包后不生效,需要单独配置。

代码2:React + i18next + antd 国际化(TSX)

bash
npm i i18next react-i18next antd
json
// src/locales/en_US.json
{
  "hello": "hello",
  "name": "Name",
  "age": "Age",
  "address": "Address"
}
json
// src/locales/zh_CN.json
{
  "hello": "你好",
  "name": "姓名",
  "age": "年龄",
  "address": "地址"
}
ts
// src/locales/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import enUS from './en_US.json';
import zhCN from './zh_CN.json';

i18n.use(initReactI18next).init({
  resources: {
    en: { translation: enUS },
    zh: { translation: zhCN }
  },
  lng: 'en',
  interpolation: {
    escapeValue: false
  }
});

export default i18n;
tsx
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './locales/i18n';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
tsx
// src/App.tsx
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ConfigProvider, Table } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import enUS from 'antd/locale/en_US';

export default function App() {
  const { t, i18n } = useTranslation();
  const [flag, setFlag] = useState(false);
  const [current, setCurrent] = useState(1);
  const [pageSize, setPageSize] = useState(3);
  const [total] = useState(10);

  const columns = [
    { title: t('name'), dataIndex: 'name' },
    { title: t('age'), dataIndex: 'age' },
    { title: t('address'), dataIndex: 'address' }
  ];

  const dataSource = [
    { key: '1', name: '张三', age: 18, address: '上海' },
    { key: '2', name: '李四', age: 20, address: '北京' }
  ];

  const toggleLang = () => {
    setFlag(!flag);
    i18n.changeLanguage(flag ? 'en' : 'zh');
  };

  return (
    <div>
      <h1>{t('hello')}</h1>
      <button onClick={toggleLang}>{flag ? '中文' : '英文'}</button>

      <ConfigProvider locale={flag ? zhCN : enUS}>
        <Table
          columns={columns}
          dataSource={dataSource}
          pagination={{
            current,
            pageSize,
            total,
            showQuickJumper: true,
            showSizeChanger: true,
            pageSizeOptions: [3, 10, 20],
            onChange: (page, size) => {
              setCurrent(page);
              setPageSize(size);
            }
          }}
        />
      </ConfigProvider>
    </div>
  );
}

要点解释:

  • i18n.ts 中 resources 配置语言包映射,lng 是默认语言。
  • 组件里用 useTranslation() 解构出 t 和 i18n,t('hello') 读取语言包中的 hello 字段。
  • 切换语言用 i18n.changeLanguage(...),界面中仍然只写 t('hello'),不需要改显示代码。
  • antd 组件内部文案不走 t,要用 ConfigProvider + antd/locale/zh_CN、antd/locale/en_US。
  • 易错点:i18n.ts 要在入口文件导入;语言包 key 要和 t() 参数一致;escapeValue:false 是转写稿中的配置点,React 默认会转义文本,不要直接 dangerouslySetInnerHTML。

代码3:React diff 算法 key 示例(JSX)

jsx
// App.jsx
import { useState } from 'react';

export default function App() {
  const [todos, setTodos] = useState([
    { id: 1, title: '吃饭', isDone: false },
    { id: 2, title: '睡觉', isDone: false }
  ]);

  const addFront = () => {
    setTodos([{ id: 99, title: '钱', isDone: false }, ...todos]);
  };

  const addBack = () => {
    setTodos([...todos, { id: 100, title: '后', isDone: false }]);
  };

  return (
    <div>
      <button onClick={addFront}>向前追加</button>
      <button onClick={addBack}>向后追加</button>

      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo.title}
            <input />
          </li>
        ))}
      </ul>
    </div>
  );
}

要点解释:

  • 上面用 key={index} 是为了复现转写稿中的问题:在 input 里输入内容后点“向前追加”,会出现值串位。
  • 原因是 diff 按同级比较,并用标签名 + key 判断复用;索引在向前追加、中间追加、删除时会变化,导致错误复用。
  • 修复方式是把 key={index} 改成唯一标识:key={todo.id}。
  • 易错点:key 不要重复;纯列表展示可用索引;涉及向前追加、中间追加、删除时不要用索引;向后追加用索引影响较小,但仍推荐唯一 id。

常见错误 ​

  1. 现象:把包含 src/public/node_modules 的源码目录直接交给后端,后端无法部署。原因:交付物错误,后端需要的是构建产物。修法:本地执行 npm run build,只交付 build/dist 目录,并说明入口是 index.html。
  2. 现象:部署后首页能打开,刷新任意前端路由直接 404。原因:服务端未做 history fallback,按 URL 找不到真实文件。修法:服务端将非静态资源/404 回退到 index.html;Express 中用 connect-history-api-fallback,且放在静态资源和代理中间件之前。
  3. 现象:部署后接口请求打到当前域名 /prod/api/... 失败或跨域。原因:开发期 devServer proxy 只在开发环境生效,生产环境未配置代理。修法:生产服务器配置反向代理和路径重写;Express 中用 http-proxy-middleware 配 /prod 转发并 pathRewrite。
  4. 现象:静态资源 404、页面白屏或样式丢失。原因:index.html 中资源引用用了相对路径,部署到子路径或刷新后按当前 URL 解析。修法:资源引用改为根路径 / 开头(按部署基路径配置),并把静态资源目录指向 build/dist。
  5. 现象:自己写的文案能中英切换,antd Table 分页、快速跳转等组件内文案不跟着变。原因:只用了 i18n 的 t,未给 antd 组件注入 locale。修法:从 antd/locale 引入 zhCN/enUS,用 ConfigProvider locale 包裹,并让 locale 与全局语言状态同步。
  6. 现象:列表向前插入后,输入框内容串位。原因:key 用了数组索引,diff 错误复用节点。修法:用稳定唯一 id 作 key;索引仅限纯展示、不重排、不插入删除时谨慎使用。
  7. 现象:受控输入框无法输入或表现为只读。原因:只绑了 value,没绑 onChange。修法:补 onChange,用最新输入更新对应 state。
  8. 现象:类组件事件处理函数里 this 指向不对。原因:方法未绑定且作为回调传递。修法:构造函数 bind、箭头函数定义,或内联箭头调用。

练习 ​

  1. 一个 React 项目要交付后端部署,请列出构建命令、交付目录,以及后端部署必须处理的三类问题。
  2. 部署后刷新 /hospital/set 出现 404,请写出排查顺序,并说明 Express 中修复中间件应放在什么位置。
  3. 页面登录请求为 http://localhost:5000/prod/api/admin/login 且失败,请说明生产代理要改哪里,pathRewrite 应如何处理。
  4. antd Table 的分页文案不随语言切换,请给出修复方案,并说明需要与哪个全局状态同步。
  5. 列表用索引作 key,向前插入后 input 内容串位。请说明复用过程错在哪里,并给出修复方案。

原视频对照 ​

#视频时长
1打包上线部署27:36
2i18n国际化22:00
3antd中的国际化11:46
4react-diff算法-key39:15
5react复习34:24
6未来规划23:29

小结 ​

  1. 前端交付物是 build/dist,不是源码目录;交付时要说清入口和静态资源目录。
  2. 生产部署至少盯三件事:history fallback、静态资源目录、接口代理。
  3. history fallback 必须尽早生效,否则依赖前端资源加载的配置都可能失效。
  4. 国际化要分两层处理:业务文案走 t,组件库文案走 ConfigProvider locale。
  5. 列表 key 优先用稳定唯一 id;索引只在纯展示、尾部追加等安全场景谨慎使用。
  6. 排错顺序先看 Network:请求地址、参数、响应码,再定位代理、路由和静态资源。
  7. 上线后“能点不能刷”不是玄学,优先查 fallback 和资源路径。
  8. 工作前三个月重点不是证明自己多强,而是稳住心态、补齐排错和沟通能力。