TypeScript-React-Starter 详解:从 create-react-app 脚手架到 Redux 状态管理的 TypeScript + React 完整实战
TypeScript-React-Starter 详解:从 create-react-app 脚手架到 Redux 状态管理的 TypeScript + React 完整实战
本文以 README.md 这份 Quick Start 指南为主体,系统讲解如何在 React 项目中接入 TypeScript,并配套 TSLint 代码检查、Jest + Enzyme 组件测试与 Redux 状态管理。读完后你将能够从零搭建一个「类型安全、可测试、带全局状态」的 React 应用,并理解本仓库中 src/index.tsx、src/containers/Hello.tsx、src/reducers/index.tsx 等核心文件的真实实现。
需要先行说明的是:README 开篇就指出,该仓库所代表的"独立 TypeScript 脚手架"路线已经不再必要——Create React App、Next.js、Razzle 等工具如今都内置了 TypeScript 支持。因此 README 首先给出了现代化的起步方式:
# Creates an app called my-app
npx create-react-app my-app --typescript
cd my-app
# Adds the type definitions
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
echo "Good to go :tada:"
这条命令用 --typescript 模板直接生成带类型支持的项目,再补齐 @types/node、@types/react、@types/react-dom、@types/jest 四个声明包即可开工。本仓库的价值在于:它展示了一个更早期的完整方案——用 react-scripts-ts 把 TypeScript「混入」标准 create-react-app 流水线,并额外演示了生产构建、测试、Redux 状态管理这些官方脚手架 README 一笔带过的部分。下文将按原文档的骨架逐节展开,并对照仓库源码逐处印证。
一、搭建基础:安装脚手架并创建 TypeScript 项目
1. 安装 create-react-app
原文假设你已经在用 Node.js 与 npm(如需先熟悉 React 基础可参考 React 官方 Hello World 文档)。create-react-app 是一个命令行脚手架工具,它会为 React 项目预设一套「官方推荐」的工具链与默认配置,这正是本教程选择它的原因。安装命令:
npm install -g create-react-app
2. 创建新项目
创建一个名为 my-app 的新项目:
create-react-app my-app --scripts-version=react-scripts-ts
这里的关键是 --scripts-version=react-scripts-ts:react-scripts-ts 是对标准 create-react-app 项目流水线的一组改造,作用就是把 TypeScript 混入构建流程。本仓库 package.json 中的 "react-scripts-ts": "1.0.6"(devDependencies)正是这一方案的落点,四个脚本全部委托给它:
"scripts": {
"start": "react-scripts-ts start",
"build": "react-scripts-ts build",
"test": "react-scripts-ts test --env=jsdom",
"eject": "react-scripts-ts eject"
}
注意 test 脚本显式指定了 --env=jsdom,这为后文的组件测试提供了浏览器 DOM 模拟环境。
3. 项目结构解析
按照 README,脚手架生成后的项目布局如下:
my-app/
├─ .gitignore
├─ images.d.ts
├─ node_modules/
├─ public/
├─ src/
│ └─ ...
├─ package.json
├─ tsconfig.json
├─ tsconfig.prod.json
├─ tsconfig.test.json
└─ tslint.json
各文件的职责:
tsconfig.json:TypeScript 的项目级编译选项。原文档还提到会附带tsconfig.prod.json与tsconfig.test.json,用于分别微调生产构建与测试构建。从当前仓库看,只有 tsconfig.json 这一个文件,说明示例作者最终没有为生产/测试拆分配置。tslint.json:代码检查器 TSLint 的规则配置。package.json:依赖声明,以及测试、预览、部署等命令的快捷脚本。public:静态资源目录,存放即将部署的 HTML 页面(即 public/index.html)与图片;除index.html外可随意删除其他文件。index.html中有<div id="root"></div>,正是 React 的挂载点。src:TypeScript 与 CSS 源码目录。index.tsx是入口文件,必须存在。images.d.ts:告诉 TypeScript 某些图片文件类型可以被import——create-react-app 的 Webpack 流水线支持这种用法,例如 src/App.tsx 中的const logo = require('./logo.svg');。
tsconfig.json 的实际配置值得逐条对照理解:
{
"compilerOptions": {
"outDir": "build/dist",
"module": "commonjs",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react",
"moduleResolution": "node",
"rootDir": "src",
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true
},
"exclude": ["node_modules", "build", "scripts", "acceptance-tests", "webpack", "jest", "src/setupTests.ts"],
"types": ["typePatches"]
}
几个关键项:"jsx": "react" 开启经典 JSX 转换(对应 React.createElement 调用方式);"strictNullChecks": true 与 "noImplicitAny": true 是后文 document.getElementById('root') as HTMLElement 类型断言的根源——开启严格空检查后,getElementById 的返回类型是 HTMLElement | null,必须显式处理 null;"exclude" 中排除了 src/setupTests.ts,因为测试装配文件不参与应用编译。
二、初始化版本控制
README 强调:测试工具 Jest 期望项目里存在某种版本控制系统(Git 或 Mercurial),否则可能无法正常运行。因此新建项目后需要:
git init
git add .
git commit -m "Initial commit."
注意:如果你是从已有仓库克隆出来的项目(比如本仓库),则完全不需要执行以上步骤。
三、调整 TSLint 默认配置
原文档指出,react-scripts-ts 预置的 TSLint 配置"有些过于激进"(overzealous),需要手动放宽。具体修改是把 extends 清空、并将严重级别降为 warning:
{
- "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
+ "extends": [],
+ "defaultSeverity": "warning",
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts"
]
}
}
当前仓库的 tslint.json 恰好就是调整完成后的最终形态:
{
"extends": [],
"defaultSeverity": "warning",
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts"
]
}
}
也就是说,这个 Starter 最终选择了「只排除构建产物与 node_modules、不继承任何推荐规则集、违规仅警告不报错」的宽松策略。TSLint 规则体系的深入配置超出了本 Starter 的范围,但你可以在此基础上按团队偏好自由实验。
四、运行、测试与生产构建
1. 开发模式运行
npm run start
这会执行 package.json 中的 start 脚本,启动一个开发服务器。保存文件即自动热重载页面,服务通常运行在 http://localhost:3000 并自动打开浏览器。这条命令的价值在于「缩短迭代回路」——快速预览每一次改动。
2. 运行测试
npm run test
该命令启动 Jest,对后缀为 .test.ts 或 .spec.ts 的所有文件运行测试。与 start 一样,Jest 具备监听能力,检测到文件变化会自动重跑。一个实用的工作方式是让 npm run start 与 npm run test 并排运行,一边预览一边跑测试。
3. 生产构建
npm run start 产出的是开发构建,并没有做优化。我们希望最终交付给用户的代码尽可能快且小,而压缩(minification)这类优化往往耗时更长——这类构建即「production build」:
npm run build
产物中的优化后 JS 与 CSS 分别位于 ./build/static/js 与 ./build/static/css。日常开发很少需要跑生产构建,但在测量应用最终体积等场景下很有用。
五、编写组件:Hello 组件的 TypeScript 写法
这是本教程的核心实战环节。我们先约定组件行为:
- 写
<Hello name="Daniel" enthusiasmLevel={3} />时,组件应渲染出类似<div>Hello Daniel!!!</div>的内容; - 若不指定
enthusiasmLevel,默认显示 1 个感叹号; - 若
enthusiasmLevel为0或负数,组件应抛出错误。
1. 无状态函数组件(SFC)
在 src/components/Hello.tsx 中编写(与当前仓库 src/components/Hello.tsx 的初始形态一致):
// src/components/Hello.tsx
import * as React from 'react';
export interface Props {
name: string;
enthusiasmLevel?: number;
}
function Hello({ name, enthusiasmLevel = 1 }: Props) {
if (enthusiasmLevel <= 0) {
throw new Error('You could be a little more enthusiastic. :D');
}
return (
<div className="hello">
<div className="greeting">
Hello {name + getExclamationMarks(enthusiasmLevel)}
</div>
</div>
);
}
export default Hello;
// helpers
function getExclamationMarks(numChars: number) {
return Array(numChars + 1).join('!');
}
这里有几个 TypeScript 要点:
Props接口定义了组件接受的属性:name是必填的string;enthusiasmLevel是可选的number——名称后的?即"可选"标记。- SFC 写法:
Hello是一个接收Props对象、并对属性做「解构」的函数。解构时的enthusiasmLevel = 1是默认值,调用方不传就是1。 - 写函数组件是 React 允许的主要组件写法之一(另一种是类组件)。如果写成类:
class Hello extends React.Component<Props, object> {
render() {
const { name, enthusiasmLevel = 1 } = this.props;
if (enthusiasmLevel <= 0) {
throw new Error('You could be a little more enthusiastic. :D');
}
return (
<div className="hello">
<div className="greeting">
Hello {name + getExclamationMarks(enthusiasmLevel)}
</div>
</div>
);
}
}
类组件在组件实例需要持有状态或使用生命周期钩子时才有价值。本例并不需要状态,所以第二个泛型参数写成了 object,SFC 更合适——但知道如何写类组件同样重要。
注意类版本继承的是 React.Component<Props, object>。这里 TypeScript 特有的部分就是传给 React.Component 的两个类型实参:Props 是 this.props 的类型,object 是 this.state 的类型。组件状态稍后还会回来讲。
2. 在入口文件中渲染 Hello
现在打开 index.tsx,把渲染 <App /> 换成渲染 <Hello ... />。先在文件顶部导入:
import Hello from './components/Hello';
再修改 render 调用:
ReactDOM.render(
<Hello name="TypeScript" enthusiasmLevel={10} />,
document.getElementById('root') as HTMLElement
);
类型断言(type assertion)
上面这行里有个值得一提的写法:document.getElementById('root') as HTMLElement。这个语法叫类型断言(type assertion),有时也叫类型转换(cast)——它是你比类型检查器更了解实际类型时,告诉 TypeScript「这个表达式的真实类型是什么」的手段。
之所以必须断言,是因为 getElementById 的返回类型是 HTMLElement | null:找不到对应 id 的元素时它返回 null。我们假设它会成功找到,因此要用 as 语法"说服" TypeScript。TypeScript 还有一种尾部"感叹号"语法(!),它直接从表达式中剔除 null 和 undefined,所以也可以写成 document.getElementById('root')!——只是这里作者选择了更明确的 as HTMLElement 写法。
这个细节在当前仓库 src/index.tsx 中真实存在(document.getElementById('root') as HTMLElement),并且只有 tsconfig.json 中的 "strictNullChecks": true 才会让这行代码成为必要——类型系统与编译器配置在这里形成了一条完整的因果链。
有状态组件(Stateful Components)
前面说过 Hello 组件不需要状态。但如果我们希望组件能随用户交互而动态更新呢?这时状态就重要了。深入探讨 React 状态最佳实践超出本 Starter 的范围,但原文给出一个「有状态版」Hello 作为快速示范:渲染两个 <button>,用于增减 Hello 显示的感叹号数量。做法分三步:
- 为状态(即
this.state)定义类型; - 在构造函数中基于传入的 props 初始化
this.state; - 为两个按钮创建事件处理函数(
onIncrement与onDecrement)。
// src/components/StatefulHello.tsx
import * as React from "react";
export interface Props {
name: string;
enthusiasmLevel?: number;
}
interface State {
currentEnthusiasm: number;
}
class Hello extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentEnthusiasm: props.enthusiasmLevel || 1 };
}
onIncrement = () => this.updateEnthusiasm(this.state.currentEnthusiasm + 1);
onDecrement = () => this.updateEnthusiasm(this.state.currentEnthusiasm - 1);
render() {
const { name } = this.props;
if (this.state.currentEnthusiasm <= 0) {
throw new Error('You could be a little more enthusiastic. :D');
}
return (
<div className="hello">
<div className="greeting">
Hello {name + getExclamationMarks(this.state.currentEnthusiasm)}
</div>
<button onClick={this.onDecrement}>-</button>
<button onClick={this.onIncrement}>+</button>
</div>
);
}
updateEnthusiasm(currentEnthusiasm: number) {
this.setState({ currentEnthusiasm });
}
}
export default Hello;
function getExclamationMarks(numChars: number) {
return Array(numChars + 1).join('!');
}
注意三点:
- 与
Props一样,状态也需要一个新类型:State。 - React 中更新状态要用
this.setState——除了构造函数里,不要直接赋值。setState只接收需要更新的属性,组件会按需重新渲染。 - 事件处理函数使用了类属性初始化器 + 箭头函数(如
onIncrement = () => ...):- 声明为箭头函数可避免
this被孤儿化调用的问题; - 把它们设为实例属性意味着只在构造时创建一次——常见错误是在
render方法里初始化,那样每次render调用都会重新分配闭包。
- 声明为箭头函数可避免
原文说明:本 Starter 后面不会再使用这个有状态组件。有状态组件适合做「只专注展示内容」(而非处理核心应用状态)的组件;在某些场景下也可以用单一中心组件向下传递 setState 回调函数来管理整个应用状态,但对更大的应用而言,专用的状态管理器(如 Redux)往往更合适——这正是下一部分的主题。
六、添加样式
create-react-app 体系下给组件加样式很简单:在 src/components/Hello.css 创建 CSS 文件(当前仓库的 src/components/Hello.css 即此文件):
.hello {
text-align: center;
margin: 20px;
font-size: 48px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.hello button {
margin-left: 25px;
margin-right: 25px;
font-size: 40px;
min-width: 50px;
}
create-react-app 底层的 Webpack 与各 loader 允许你直接 import 所需样式表,构建时任何被导入的 .css 文件都会被拼接进输出文件。因此在 src/components/Hello.tsx 顶部加一行:
import './Hello.css';
仓库中的 src/components/Hello.tsx 第二行正是 import './Hello.css';,与文档完全吻合。
七、使用 Jest 与 Enzyme 编写测试
先重申组件的三条行为假设:
- 写
<Hello name="Daniel" enthusiasmLevel={3} />时,应渲染出类似<div>Hello Daniel!!!</div>的内容;- 不指定
enthusiasmLevel时,默认显示 1 个感叹号;enthusiasmLevel为0或负数时应抛出错误。
1. 安装 Enzyme
npm install -D enzyme @types/enzyme enzyme-adapter-react-16 @types/enzyme-adapter-react-16 react-test-renderer
这里要区分 enzyme 与 @types/enzyme:前者是实际被执行的 JavaScript 代码包,后者是声明文件包(.d.ts),让 TypeScript 理解如何使用 Enzyme。@types 包的机制可参见 TypeScript 官方的 Declaration Files 文档。
另外 enzyme-adapter-react-16 与 react-test-renderer 是 enzyme 明确要求安装的依赖。
Enzyme 的定位:应用默认自带 jsdom 库来模拟 DOM、无需浏览器即可测试运行时行为;Enzyme 类似,但构建在 jsdom 之上,提供了更便捷的组件查询能力。
2. 配置 Enzyme 适配器
写第一个测试前,必须让 Enzyme 使用 React 16 适配器。创建 src/setupTests.ts——该文件在运行测试时会被自动加载:
import * as enzyme from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
enzyme.configure({ adapter: new Adapter() });
(tsconfig.json 的 exclude 列表中包含 src/setupTests.ts,正是因为它只在测试环境被 Jest 加载,不参与应用编译。)
3. 编写测试文件
在 Hello.tsx 旁边创建 src/components/Hello.test.tsx。当前仓库的 src/components/Hello.test.tsx 就是原文档所述测试的落点,五条用例逐条覆盖了三条行为假设:
// src/components/Hello.test.tsx
import * as React from 'react';
import * as enzyme from 'enzyme';
import Hello from './Hello';
it('renders the correct text when no enthusiasm level is given', () => {
const hello = enzyme.shallow(<Hello name="Daniel" />);
expect(hello.find('.greeting').text()).toEqual('Hello Daniel!');
});
it('renders the correct text with an explicit enthusiasm of 1', () => {
const hello = enzyme.shallow(<Hello name="Daniel" enthusiasmLevel={1}/>);
expect(hello.find('.greeting').text()).toEqual('Hello Daniel!');
});
it('renders the correct text with an explicit enthusiasm level of 5', () => {
const hello = enzyme.shallow(<Hello name="Daniel" enthusiasmLevel={5} />);
expect(hello.find('.greeting').text()).toEqual('Hello Daniel!!!!!');
});
it('throws when the enthusiasm level is 0', () => {
expect(() => {
enzyme.shallow(<Hello name="Daniel" enthusiasmLevel={0} />);
}).toThrow();
});
it('throws when the enthusiasm level is negative', () => {
expect(() => {
enzyme.shallow(<Hello name="Daniel" enthusiasmLevel={-1} />);
}).toThrow();
});
测试很基础,但足以理解套路:enzyme.shallow 做浅渲染,find('.greeting') 定位节点,text() 取出文本后与期望值 toEqual 断言;抛错场景则用 expect(...).toThrow() 包裹渲染调用。
八、添加状态管理:Redux 接入全链路
如果 React 只用来"取一次数据再展示",到这里项目就基本完成了。但若是交互更复杂的 App,就需要状态管理。
1. 状态管理的一般性讨论
React 本身是构建可组合视图的有用库,但不规定在应用内同步数据的任何特定方式:就组件而言,数据只通过你在各元素上写的 props 向下流动;其中一些 props 可能是以某种方式更新状态的函数,但"具体怎么更新"是开放问题。因此社区涌现了 Redux、MobX 等库:
- Redux 依赖中心化、不可变的数据存储来同步数据,对数据的更新会触发应用重新渲染;状态通过发送显式的 action 消息、由称为 reducer 的函数处理,以不可变方式更新。由于这种显式性,往往更容易推断某个 action 对程序状态的影响。
- MobX 依赖函数式响应式模式:状态被包装为 observable 并作为 props 传递,只需将状态标记为 observable 即可保证各观察者的完全同步。另一个加分项是该库本身就用 TypeScript 编写。
两者各有取舍。总体来看 Redux 使用更广泛,所以本教程聚焦 Redux,但鼓励你也去探索 MobX。下文学习曲线可能较陡,原文强烈建议先通过 Redux 官方文档熟悉基本概念。
2. 为 action 铺路
给应用加 Redux 之前,先要有能触发状态变化的"动作源"——可以是定时器,也可以是按钮等 UI 元素。本例为 Hello 组件增加两个按钮,控制热情值。
3. 安装 Redux
npm install -S redux react-redux @types/react-redux
注意这里不需要安装 @types/redux,因为 Redux 官方包自带声明文件(.d.ts)。当前 package.json 中对应的依赖版本为 redux: ^3.6.0、react-redux: ^5.0.2、@types/react-redux: ^4.4.36。
4. 定义应用状态形状
创建 src/types/index.tsx,存放整个程序可能用到的类型定义(当前仓库 src/types/index.tsx 内容一致):
// src/types/index.tsx
export interface StoreState {
languageName: string;
enthusiasmLevel: number;
}
意图是:languageName 表示本应用使用的编程语言(TypeScript 或 JavaScript),enthusiasmLevel 则会被按钮改变。注意 store 状态与组件 props 刻意设计得不同(一个叫 languageName、一个叫 name),等写到 container 时会明白这是为了演示 mapStateToProps 的"整形"职责。
5. 添加 actions
先在 src/constants/index.tsx 中创建一组消息类型(与仓库 src/constants/index.tsx 一致):
// src/constants/index.tsx
export const INCREMENT_ENTHUSIASM = 'INCREMENT_ENTHUSIASM';
export type INCREMENT_ENTHUSIASM = typeof INCREMENT_ENTHUSIASM;
export const DECREMENT_ENTHUSIASM = 'DECREMENT_ENTHUSIASM';
export type DECREMENT_ENTHUSIASM = typeof DECREMENT_ENTHUSIASM;
这个 const/type 组合模式让我们能以易于访问且可重构的方式使用 TypeScript 字符串字面量类型——重命名常量时,类型引用会随之更新,避免了魔法字符串散落各处。
然后在 src/actions/index.tsx 中创建 action 类型与生成函数(与仓库 src/actions/index.tsx 一致):
import * as constants from '../constants';
export interface IncrementEnthusiasm {
type: constants.INCREMENT_ENTHUSIASM;
}
export interface DecrementEnthusiasm {
type: constants.DECREMENT_ENTHUSIASM;
}
export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm;
export function incrementEnthusiasm(): IncrementEnthusiasm {
return {
type: constants.INCREMENT_ENTHUSIASM
};
}
export function decrementEnthusiasm(): DecrementEnthusiasm {
return {
type: constants.DECREMENT_ENTHUSIASM
};
}
这里定义了描述增量/减量 action 应长什么样的两个类型;用联合类型 EnthusiasmAction 表示"可能是增也可能是减";最后两个函数负责实际"制造" action,省去手写冗长对象字面量。显然这里有样板代码,熟练后可以看看 redux-actions 这类库。
6. 编写 reducer
Reducer 就是"通过创建应用状态的修改副本来产生变化、且没有副作用"的函数——即纯函数。本例的 reducer 放在 src/reducers/index.tsx(仓库文件 src/reducers/index.tsx 与原文档一致,仅多了一个 default 分支):
// src/reducers/index.tsx
import { EnthusiasmAction } from '../actions';
import { StoreState } from '../types/index';
import { INCREMENT_ENTHUSIASM, DECREMENT_ENTHUSIASM } from '../constants/index';
export function enthusiasm(state: StoreState, action: EnthusiasmAction): StoreState {
switch (action.type) {
case INCREMENT_ENTHUSIASM:
return { ...state, enthusiasmLevel: state.enthusiasmLevel + 1 };
case DECREMENT_ENTHUSIASM:
return { ...state, enthusiasmLevel: Math.max(1, state.enthusiasmLevel - 1) };
default:
return state;
}
}
职责明确:增量把热情值加 1,减量减 1,且永不跌破 1(Math.max(1, ...) 兜底)。
两个关键细节:
- 使用了对象展开(
...state)创建状态的浅拷贝,同时替换enthusiasmLevel; enthusiasmLevel属性必须放在最后,否则会被旧状态里的同名属性覆盖。
由于 reducer 是纯函数,可以放心地传任意数据进去测试:对每个输入,只需断言它产出的新状态。建议为此写几个测试,用 Jest 的 toEqual 方法即可。
7. 制作 container(连接件)
写 Redux 时,除了组件(component)还会写容器(container):组件往往与数据无关、停留在展示层;容器则包裹组件,为它喂入显示与修改状态所需的数据。这个"展示组件/容器组件"的经典划分源自 Dan Abramov 的 Presentational and Container Components 一文。
第一步:让 Hello 组件接受回调。 给 Props 加两个可选回调属性 onIncrement 与 onDecrement(这正是当前 src/components/Hello.tsx 的最终形态):
export interface Props {
name: string;
enthusiasmLevel?: number;
onIncrement?: () => void;
onDecrement?: () => void;
}
然后把回调绑定到两个新按钮上:
function Hello({ name, enthusiasmLevel = 1, onIncrement, onDecrement }: Props) {
if (enthusiasmLevel <= 0) {
throw new Error('You could be a little more enthusiastic. :D');
}
return (
<div className="hello">
<div className="greeting">
Hello {name + getExclamationMarks(enthusiasmLevel)}
</div>
<div>
<button onClick={onDecrement}>-</button>
<button onClick={onIncrement}>+</button>
</div>
</div>
);
}
建议为「点击按钮会触发对应的 onIncrement/onDecrement」补写测试,借此练手组件测试。
第二步:用 connect 包裹成容器。 创建 src/containers/Hello.tsx(仓库文件 src/containers/Hello.tsx 即最终成品):
// src/containers/Hello.tsx
import Hello from '../components/Hello';
import * as actions from '../actions/';
import { StoreState } from '../types/index';
import { connect, Dispatch } from 'react-redux';
export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
return {
enthusiasmLevel,
name: languageName,
};
}
export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
return {
onIncrement: () => dispatch(actions.incrementEnthusiasm()),
onDecrement: () => dispatch(actions.decrementEnthusiasm()),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Hello);
connect 是这里的两把钥匙之一(另一把是原始 Hello 组件),它借助两个函数把组件变成容器:
mapStateToProps:把当前 store 中的数据"按摩"成组件所需 props 形状的一部分。回顾一下,store 状态有两个属性languageName与enthusiasmLevel,而Hello期望的是name与enthusiasmLevel——mapStateToProps负责取数据并在必要时调整形状(这里的name: languageName就是整形动作,也解释了第五节中为何状态与 props 刻意不同名)。注意它只提供了Hello期望的 4 个属性中的 2 个;mapDispatchToProps:接收一个 dispatcher 函数(可把 action 送入 store 触发更新),据此创建一对回调,在需要时调用 dispatcher。泛型Dispatch<actions.EnthusiasmAction>把可 dispatch 的 action 类型收紧为本应用的EnthusiasmAction联合类型。
最后调用 connect:它先接收 mapStateToProps 与 mapDispatchToProps,再返回一个可包裹组件的函数。一行代码 connect(mapStateToProps, mapDispatchToProps)(Hello) 即完成容器化。
第三步:创建 store 并接线。 回到 src/index.tsx。需要创建带初始状态的 store,并装配所有 reducer。当前 src/index.tsx 完整呈现了原文档所述的全部接线:
import { createStore } from 'redux';
import { enthusiasm } from './reducers/index';
import { StoreState } from './types/index';
const store = createStore<StoreState>(enthusiasm, {
enthusiasmLevel: 1,
languageName: 'TypeScript',
});
store 即应用全局状态的中心存储。泛型参数 <StoreState> 让 createStore 的状态类型与 src/types/index.tsx 中的定义对齐。
然后把对 ./src/components/Hello 的使用换成 ./src/containers/Hello,并用 react-redux 的 Provider 把 props 与容器接起来:
import Hello from './containers/Hello';
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<Hello />
</Provider>,
document.getElementById('root') as HTMLElement
);
注意此时 Hello 不再需要任何 props——connect 已经把应用状态适配为被包裹 Hello 组件所需的 props。数据流至此形成闭环:按钮点击 → mapDispatchToProps 生成的回调 → dispatch(action) → reducer enthusiasm 产出新状态 → mapStateToProps 重新映射 → 组件重渲染。
九、Eject:逃生通道
如果 create-react-app 的某些封装让你无法完成定制(例如想添加一个 Webpack 插件),可以随时选择退出、拿回全部配置选项:
npm run eject
重要提醒:eject 之前建议先提交所有工作。npm run eject 不可撤销——退出是永久性的,除非你能从执行 eject 之前的 commit 恢复。
十、后续学习方向
README 末尾给出的下一步建议:
- create-react-app 自带很多好东西,大部分都写在项目默认生成的
README.md里(本仓库对应 README-CRA.md),值得一读; - 想继续深入 Redux,看 Redux 官方文档;想探索 MobX,看 MobX 官方文档;
- 将来若打算 eject,需要补一些 Webpack 知识,可参考 TypeScript 官方的 React & Webpack 讲解;
- 某时你可能需要路由:在 Redux 项目中
react-router可能是最流行的选择,常与react-router-redux配合使用。
总结:从这份 Starter 中能带走什么
结合原文档与仓库源码,可以把整个技术栈的要点压缩成一张"文件—职责"对照:
| 文件 | 职责 |
|---|---|
| src/index.tsx | 应用入口:createStore<StoreState> 建库、Provider 注入、挂载到 #root |
| src/components/Hello.tsx | 展示组件:Props 接口约束属性,SFC 解构 + 默认值,CSS 按模块导入 |
| src/containers/Hello.tsx | 容器:connect + mapStateToProps/mapDispatchToProps 完成状态与 actions 的接线 |
| src/constants/index.tsx | const/type 模式定义字符串字面量类型的 action type |
| src/actions/index.tsx | action 接口、联合类型 EnthusiasmAction 与 action creator |
| src/reducers/index.tsx | 纯函数 reducer,对象展开做不可变更新,Math.max 守住下界 |
| src/types/index.tsx | 全局 StoreState 类型,贯穿 store、reducer 与 container |
| src/components/Hello.test.tsx | Jest + Enzyme 浅渲染测试,覆盖默认值、显式值与抛错三条规则 |
| tsconfig.json / tslint.json | 严格空检查、经典 JSX、宽松 lint 策略等工程化配置 |
这套方案诞生于 create-react-app 尚未原生支持 TypeScript 的年代,如今新项目可直接用 npx create-react-app my-app --typescript 起步;但 README 所沉淀的组件类型建模(Props/State 接口、类型断言)、Redux 四件套(constants → actions → reducers → container)以及 Jest + Enzyme 的测试手法,仍然适用于今天的 React + TypeScript 项目,而本仓库中每个文件都是可直接对照阅读的完整实现样本。