Twitter Clone frontend notes

Twitter Clone backend notes

Project URL

https://github.com/merikbest/twitter-spring-reactjs

涉及的主要技术

通过分析项目的 package.json 文件,该项目前端主要使用了以下技术:

  • React (v16.14.0):一个用于构建用户界面的 JavaScript 库。
  • TypeScript:JavaScript 的一个超集,添加了静态类型定义。
  • Redux:一个用于 JavaScript 应用程序的可预测状态容器,常与 React 配合使用,本项目中还使用了 react-redux 进行绑定,以及 redux-saga 处理副作用。
  • Material-UI (MUI):一个流行的 React UI 框架,提供了丰富的预构建组件,包括核心组件 (@material-ui/core)、图标 (@material-ui/icons) 和实验性组件 (@material-ui/lab)。
  • React Router:用于处理 React 应用中的路由。
  • Axios:一个基于 Promise 的 HTTP 客户端,用于发送网络请求。
  • i18next:一个强大的国际化框架,用于实现多语言支持。
  • Node-sass:允许在项目中使用 Sass/SCSS 进行 CSS 预处理。
  • Jest & Enzyme:用于 JavaScript 和 React 应用的测试框架和工具库。

项目本身是使用 Create React App 引导创建的。

实践和阅读代码:

  • 尝试运行项目,并修改一些简单的组件,观察变化。
  • 阅读项目中的代码,特别是组件 (src/components) 和页面 (src/pages) 部分,理解它们是如何组织和工作的。
  • 重点关注 src/store 目录,理解 Redux 的状态管理是如何在本项目的具体实践的。
  • 查看 src/services/api 目录,了解与后端 API 的交互方式。

文件目录结构

好的,我们来详细了解一下这个前端项目的目录结构和关键文件。这个项目看起来是一个标准的 Create React App (CRA) 项目结构,并在此基础上进行了一些常见的组织。

project-root/
├── src/ # 源代码目录
│ ├── components/ # 可复用的 React 组件
│ ├── pages/ # 页面级组件
│ ├── store/ # Redux 状态管理相关文件
│ ├── services/ # API 服务和接口调用
│ ├── hook/ # 自定义 React Hooks
│ ├── hoc/ # 高阶组件
│ ├── core/ # 核心功能和配置
│ ├── util/ # 工具函数
│ ├── constants/ # 常量定义
│ ├── types/ # TypeScript 类型定义
│ ├── i18n.ts # 国际化配置
│ ├── theme.ts # 主题配置
│ ├── App.tsx # 应用程序主组件
│ └── index.tsx # 应用程序入口文件
├── public/ # 静态资源目录
├── package.json # 项目依赖配置和脚本
├── tsconfig.json # TypeScript 配置
├── jest.config.js # Jest 测试配置
└── babel.config.js # Babel 转译配置

根目录 (frontend/)

这是前端项目的顶层目录。

  • package.json
    • 作用:定义了项目的元数据,包括项目名称、版本、依赖项 (dependencies 和 devDependencies) 以及可执行脚本 (如 start, build, test)。
    • 关键信息:通过此文件可以了解项目使用了哪些第三方库,例如 React, Redux, Material-UI, Axios 等。
  • README.md
    • 作用:项目的说明文档,通常包含如何启动项目、运行测试、构建项目等信息。
  • tsconfig.json
    • 作用:TypeScript 编译器的配置文件,定义了编译选项,如目标 JavaScript 版本、模块系统、JSX 处理方式、路径别名等。
  • jest.config.jssetupTests.js
    • 作用:Jest 测试框架的配置文件。jest.config.js 用于配置 Jest 的行为,如测试环境、模块名映射、覆盖率报告等。setupTests.js (通常在 src 目录下) 用于在每个测试文件运行之前执行一些全局设置,例如配置 Enzyme Adapter。
  • babel.config.js
    • 作用:Babel 的配置文件,用于将较新版本的 JavaScript (和 TypeScript/JSX) 代码转换为向后兼容的版本。
  • node_modules/ (通常在 .gitignore 中,不会上传到版本库)
    • 作用:存放项目所有依赖的第三方库的地方。
  • build/ (通常在 .gitignore 中,执行 yarn buildnpm run build 后生成)
    • 作用:存放生产环境构建后的静态文件,这些文件最终会被部署到服务器。
  • public/
    • index.html:应用的 HTML 入口文件。React 应用通常会挂载到这个文件中的某个 DOM 元素上(例如 <div id="root"></div>)。
    • favicon.ico, logo192.png, logo512.png, manifest.json, robots.txt 等:这些是应用的图标、PWA (Progressive Web App) 配置文件以及搜索引擎爬虫规则文件。
  • src/:这是我们最关心的目录,包含了应用所有的源代码。

src/ 目录

这是前端应用的核心代码所在地。

  • index.tsx
    • 作用:应用的 JavaScript/TypeScript 入口文件。它负责将根组件 (通常是 App) 渲染到 public/index.html 中的根 DOM 节点上。
    • 关键操作
      • 引入全局样式。
      • 设置 Redux Store Provider,使整个应用都能访问到 store。
      • 设置 React Router (BrowserRouter),启用客户端路由。
      • 初始化 i18next 以进行国际化。
      • <App /> 组件渲染到 DOM。
  • App.tsx
    • 作用:应用的根组件。它通常包含主要的布局结构(如侧边栏、主内容区)和路由配置。
    • 关键内容
      • 定义应用的主题 (Theme) 和颜色方案切换逻辑。
      • 通过 react-router-domSwitchRoute 组件来定义不同 URL 路径对应的页面组件。
      • 可能会包含一些全局的 Modal 或 Notification 组件。
  • routes.ts (或类似名称的文件,如 router.ts)
    • 作用:通常用于集中管理应用的路由配置。它会导出一个路由配置数组或对象,供 App.tsx 使用。
    • 关键内容:定义了每个路由的路径 (path)、对应的组件 (component)、是否精确匹配 (exact) 以及可能的子路由。
  • theme.ts (或 styles/theme.ts)
    • 作用:定义了 Material-UI (或其他 UI 库) 的主题配置。
    • 关键内容:包括颜色、字体、间距、组件覆盖样式等,用于统一应用的视觉风格。此项目定义了多种主题如 dimTheme, lightsOutTheme, defaultTheme 以及不同的颜色方案。
  • i18n.ts
    • 作用:i18next 国际化库的配置文件。
    • 关键内容:初始化 i18next,配置语言检测器、后端 (用于加载翻译文件) 以及默认语言等。
  • store/:存放 Redux 状态管理相关代码。
    • store.ts:创建和配置 Redux store 的地方。它会组合所有的 reducers (来自 ducks 目录),并应用中间件 (如 redux-saga)。
    • rootReducer.ts:组合所有 “ducks” 中的 reducer 成一个根 reducer。
    • saga.ts (或 rootSaga.ts):组合所有 “ducks” 中的 sagas 成一个根 saga。
    • ducks/:按照功能模块组织 Redux 的 action, reducer, saga, selector 等。具体结构已在上一问中详细解释。
  • components/:存放可复用的 UI 组件。
    • 作用:这些组件通常是无状态的 (stateless) 或展示型的 (presentational),专注于 UI 的渲染。
    • 示例SideMenu/SideMenu.tsx, TweetComponent/TweetComponent.tsx, AddTweetForm/AddTweetForm.tsx 等。
    • 每个组件文件夹内通常包含组件本身 (.tsx) 和其样式文件 (.module.css, .module.scss, 或 styled-components 定义)。
  • pages/ (或 views/, screens/):存放页面级别的组件。
    • 作用:这些组件通常对应应用中的一个完整页面或视图,它们会组合多个基础组件,并可能包含页面特有的业务逻辑和状态管理。
    • 示例Home/Home.tsx, Login/Login.tsx, Settings/Settings.tsx, UserPage/UserPage.tsx 等。
  • services/ (或 api/):存放与后端 API 交互的逻辑。
    • 作用:封装 API 请求,使得组件或 saga 可以更方便地调用。
    • 示例api/tweet-service/tweetApi.ts 封装了与推文相关的 API 请求。
    • 可能包含不同服务(如 user-service, tweet-service)的子目录。
  • core/:存放项目核心的、基础的工具或配置。
    • axios.ts:配置 Axios 实例,例如设置基础 URL、请求拦截器(如添加认证 token)等。
  • hooks/:存放自定义 React Hooks。
    • 作用:用于封装可复用的有状态逻辑,使组件逻辑更清晰。
    • 示例useDebounce.ts (防抖 hook), useModalWindow.ts (控制模态框显示的 hook)。
  • constants/:存放应用中使用的常量。
    • 作用:将硬编码的值提取出来,便于管理和修改。
    • 示例
      • path-constants.ts:定义路由路径常量。
      • endpoint-constants.ts:定义 API 端点常量。
      • common-constants.ts:定义一些通用的常量。
  • types/:存放 TypeScript 类型定义。
    • 作用:定义应用中数据结构的类型和接口,增强代码的健壮性和可维护性。
    • 示例tweet.ts, user.ts, common.ts 等。
    • types.d.tsreact-app-env.d.ts:用于声明全局类型或第三方库的类型。
  • util/ (或 utils/):存放一些工具函数或辅助模块。
    • 作用:提供一些通用的辅助函数,如日期格式化、文本处理、测试工具等。
    • 示例format-date-helper.ts, text-formatter.tsx, test-utils/ 等。
  • assets/ (或 images/, styles/):存放静态资源,如图片、全局样式表 (如果未使用 CSS Modules 或 styled-components) 等。本项目中,图标似乎直接在 icons.tsx 中定义为 SVG 组件。
  • hoc/: 存放高阶组件 (Higher-Order Components)。
    • 作用: 用于复用组件逻辑,向组件注入额外的 props 或行为。
    • 示例: withDocumentTitle.tsx 用于动态修改页面标题。

这种结构使得项目代码清晰、模块化,便于团队协作和长期维护。了解每个文件夹和文件的职责是理解整个前端应用的关键。

package.json 和 package-lock.json的区别

package.jsonpackage-lock.json 都用于管理 Node.js 项目的依赖,但它们的职责不同,配合使用能保证依赖声明清晰安装结果一致

package.json 的作用(声明依赖)

  • 手动编辑的文件,用于声明项目所需的包及版本范围
  • 比如:
{
"dependencies": {
"express": "^4.18.0"
}
}
  • ^4.18.0 表示允许安装 4.x.x 中最新的版本(不含 5.0.0)。
  • 同时也包含项目名称、脚本命令、作者等元信息。

package-lock.json 的作用(锁定依赖)

  • 自动生成(首次执行 npm install 后),用于锁定具体安装的版本,包括所有子依赖的确切版本。
  • 确保在任何机器上执行 npm install 都会装出完全相同的依赖树

例如:

"express": {
"version": "4.18.2", // 实际安装的版本
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-..."
}

关键区别总结

项目 package.json package-lock.json
是否手动编辑 ✅ 是 ❌ 自动生成
是否提交到版本控制 ✅ 是 ✅ 是(必须提交)
是否定义依赖的版本范围 ✅ 是(用 ^, ~ 等) ❌ 否(精确版本)
是否控制依赖树一致性 ❌ 否 ✅ 是(完全锁定安装结果)
是否包含子依赖信息 ❌ 否 ✅ 是

建议:

  • 永远提交 package-lock.json 到 Git,避免团队成员之间或 CI 环境中的依赖差异。
  • 修改依赖时,只改 package.json,让 npm 自动更新 lock 文件。

有了package.json还需要package-lock.json吗

需要,绝对需要package.jsonpackage-lock.json 解决的是两个完全不同层面的问题

  • package.json 和 package-lock.json 都用于管理 Node.js 项目的依赖,但它们的职责不同,配合使用能保证依赖声明清晰且安装结果一致。
  • package.json 像是 菜单 —— 你告诉厨房:“我要吃宫保鸡丁,不辣,份量正常。”
  • package-lock.json厨房实际做出来的菜谱和食材清单 —— 精确到“用了哪家厂的鸡腿、哪瓶酱油、几克盐”。

package.json 只声明版本范围,不够精确

"lodash": "^4.17.0"

这个意思是:安装 4.x.x 中的最新版本,但每个人、每台机器、每次安装时点不同,就可能装出:

  • A 开发机装了 4.17.21
  • B CI 环境装了 4.17.15
  • C 同事昨天装了 4.17.19

这种不一致会导致线上 bug 无法复现构建失败行为不一致


package-lock.json 锁定了实际安装版本

它会记录下:

  • lodash@4.17.21
  • 以及 lodash 的子依赖、孙依赖的具体版本
  • 所有依赖的下载地址和校验 hash(integrity 字段)

确保:

“任何人在任何时间、任何机器上 npm install 都会装出一模一样的依赖树。”


如果只保留 package.json 会发生什么?

  • 安装出来的依赖版本会随时间波动
  • 同一个项目,成员之间、测试环境、线上环境可能装出不同的依赖
  • 你看到的 bug,别人可能复现不了

这是灾难,特别是生产级项目。

结论:

问题 只用 package.json package.json + lock
能声明需要哪些依赖
能精确控制版本一致性
能保证 CI/CD、开发环境一致
会不会随机踩到依赖更新的坑 ✅ 有可能 ❌ 锁死版本,避免问题

所以,lock 文件是保证你项目稳定性、可复现性、安全性的重要保障。

不管是 npmpackage-lock.json,还是 yarn.lock,都必须提交并维护。

项目中和jest有关的代码在哪

基于项目中使用的依赖和搜索结果,Jest相关的文件通常位于以下位置:

  1. 配置文件

    • jest.config.js 通常在项目根目录下
    • src/setupTests.ts 用于 Jest 的全局设置
  2. 测试文件的常见位置:

    • src/__tests__/ 目录下 [3]
    • 与源文件同目录下的 *.test.ts*.spec.ts 文件
    • 组件测试通常与组件文件放在同一目录
  3. 测试辅助文件

    /src
    /utils
    /__tests__/
    test-utils.ts // 测试工具函数
    mocks/ // mock 数据
    /components
    /__tests__/
    Component.test.ts // 组件测试
    setupTests.ts // 全局测试设置
  4. 相关的包依赖(从项目配置中可以看到):

    {
    "jest": "26.6.0",
    "@testing-library/react": "11.1.0",
    "enzyme-adapter-react-16": "1.15.6",
    "enzyme-to-json": "3.6.2",
    "jest-websocket-mock": "2.3.0"
    }

要找到具体的测试文件,你可以:

  1. 在项目根目录查找 jest.config.js
  2. 查找 src/setupTests.tssrc/setupTests.js
  3. 搜索 .test.ts, .test.tsx, .spec.ts, .spec.tsx 文件
  4. 查找 __tests__ 目录

Enzyme

Enzyme 是一个由 Airbnb 开发的 React 测试工具库,用于更方便地对 React 组件进行单元测试。

虽然 Enzyme 在 React 16/17 时代非常流行,但现在已经基本被 React Testing Library 取代,因为后者更符合用户行为驱动(“以用户的方式测试”)的理念,且官方更推荐。

不过,如果你维护的是旧项目,理解 Enzyme 的用法仍然有价值。


核心概念

Enzyme 提供三种渲染方式:

API 用途 特点
shallow() 浅渲染,只渲染当前组件 快速、隔离子组件
mount() 全渲染,包括子组件 适合集成测试,依赖 DOM 环境
render() 使用 cheerio 渲染成 HTML 快速、不可交互

示例

Counter.jsx
// Counter.jsx
import React, { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
Counter.test.js
// Counter.test.js
import React from 'react';
import { shallow } from 'enzyme';
import { Counter } from './Counter';

describe('Counter', () => {
it('renders count and increments on click', () => {
const wrapper = shallow(<Counter />);
expect(wrapper.find('p').text()).toContain('0');

wrapper.find('button').simulate('click');
expect(wrapper.find('p').text()).toContain('1'); // ❌ 注意:shallow 不会更新 useState 的值
});
});

✅ 实际上你需要用 mount 来测试 useState 行为:

import { mount } from 'enzyme';

const wrapper = mount(<Counter />);
wrapper.find('button').simulate('click');
expect(wrapper.find('p').text()).toContain('1');

配置

Enzyme 需要配合适配器使用(依赖 React 版本):

npm install --save enzyme enzyme-adapter-react-16

然后在测试配置中添加:

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

总结

优点:

  • 支持组件内部结构和方法测试
  • 提供类似 jQuery 的 API(如 .find(), .prop()

缺点:

  • 对 React 新版本支持差(如 hooks、concurrent mode)
  • 不符合用户行为驱动的测试理念

建议

如果是新项目,请使用 React Testing Library,它更稳定、更现代、更贴近真实用户行为,且社区主流也已经转向。

本项目中的实例-登录页面测试

这个测试文件主要测试了登录组件的四个方面:

  1. 基本渲染测试:确保组件正确渲染所有必要的 UI 元素
  2. 表单提交测试:验证表单输入和提交功能是否正常工作
  3. 错误状态测试:确保在发生错误时正确显示错误信息
  4. 卸载行为测试:验证组件在卸载时是否正确清理状态

测试使用了以下关键工具:

  • Jest 作为测试框架
  • Enzyme 用于组件渲染和交互测试
  • Redux mock store 用于模拟状态管理
  • Memory History 用于路由测试

每个测试用例都通过模拟用户交互和验证组件行为来确保组件的可靠性。

Login.test.tsx
// 导入必要的依赖
import React from "react";
import { createMemoryHistory } from "history"; // 用于创建内存路由历史
import { Button } from "@material-ui/core"; // Material-UI 按钮组件

// 导入本地组件和工具
import Login from "../Login";
import { createMockRootState, mockDispatch, mountWithStore } from "../../../util/test-utils/test-helper";
import { LoginTextField } from "../LoginInputField";
import { UserActionsType } from "../../../store/ducks/user/contracts/actionTypes";
import { LoadingStatus } from "../../../types/common";

// 登录组件的测试套件
describe("Login", () => {
// 创建一个加载完成状态的模拟 Redux store
const mockStore = createMockRootState(LoadingStatus.LOADED);
// 声明模拟 dispatch 函数
let mockDispatchFn: jest.Mock;

// 在每个测试用例之前执行
beforeEach(() => {
// 重置模拟 dispatch 函数
mockDispatchFn = mockDispatch();
});

// 测试组件渲染是否正确
// `it` 是定义单个测试用例的函数。语义是 "It should...",即“它应该……”。
// test 和 it 在 Jest 中是完全等价的,官方推荐使用 test,但 it 也很常见。
it("should render correctly", () => {
// 使用模拟 store 挂载登录组件
const wrapper = mountWithStore(<Login />, mockStore);

// 验证必要的文本内容是否存在
expect(wrapper.text().includes("Log in to Twitter")).toBe(true);
expect(wrapper.find(Button).text().includes("Login")).toBe(true);
expect(wrapper.text().includes("Forgot password?")).toBe(true);
expect(wrapper.text().includes("Sign up for Twitter")).toBe(true);
});

// 测试登录表单提交
it("should submit Login form", () => {
// 创建路由历史对象
const history = createMemoryHistory();
// 模拟的用户输入数据
const mockEmail = "testemail@test.test";
const mockPassword = "testpassword";
// 挂载组件,传入 store 和 history
const wrapper = mountWithStore(<Login />, mockStore, history);

// 查找输入框元素
const inputEmail = wrapper.find(LoginTextField).at(0).find("input").at(0);
const inputPassword = wrapper.find(LoginTextField).at(1).find("input").at(0);

// 模拟用户输入
inputEmail.simulate("change", { target: { value: mockEmail } });
inputPassword.simulate("change", { target: { value: mockPassword } });
// 模拟表单提交
wrapper.find(Button).at(0).simulate("submit");

// 验证 dispatch 是否被正确调用
expect(mockDispatchFn).nthCalledWith(1, {
payload: {
email: mockEmail,
password: mockPassword,
history: history
},
type: UserActionsType.FETCH_SIGN_IN
});
});

// 测试错误状态的渲染
it("should render error", () => {
// 使用错误状态创建 store 并挂载组件
const wrapper = mountWithStore(<Login />, createMockRootState(LoadingStatus.ERROR));

// 验证错误消息是否显示
expect(wrapper.text().includes("The username and password you entered did not match our records. " +
"Please double-check and try again.")).toBe(true);
});

// 测试组件卸载时的行为
it("should component unmount", () => {
// 挂载组件
const wrapper = mountWithStore(<Login />, mockStore);
// 卸载组件
wrapper.unmount();

// 验证卸载时是否正确设置了加载状态
expect(mockDispatchFn).nthCalledWith(1, {
payload: LoadingStatus.LOADING,
type: UserActionsType.SET_USER_LOADING_STATE
});
});
});