主题
react脚手架构建工程化开发环境与全局安装脚手架并创建项目(Day3 3/3)
本页对应 React + TypeScript + 医通项目,共 5 段视频,约 45 分钟。
学习目标
- 能通过代码验证 props 外部数据在子组件中只读,演示直接修改 props 会报错,并说明应修改父组件数据源。
- 能按“父定义方法并绑定 this、通过属性传递、子组件接收并调用传参”的步骤实现子传父通信,并在类组件和函数组件中分别验证。
- 能使用 props.children 获取双标签调用中的子元素并渲染,区分单标签与双标签调用场景。
- 能为类组件和函数组件配置 propTypes、isRequired、defaultProps,并验证类型校验、必填警告和默认值生效。
- 能全局安装 React 脚手架并创建、启动项目,确认工程化开发环境可正常运行。
- 能完成 Todolist 静态结构拆分:创建组件及样式文件、导入样式、替换 className、拆分 Header/Main/Item/Footer,并刷新验证页面结构和样式无异常。
知识点
先明确本节在工程化环境中的位置
这一节是在已经搭好的 React 脚手架项目里,练习组件间通信、props 约束,以及把一个静态页面拆成工程化组件。脚手架提供了目录组织、模块导入、样式拆分和调试能力;真正要掌握的是:组件的数据从哪里来、能不能改、父子组件如何协作、组件对外暴露的 props 如何约束,以及页面结构如何拆分成可维护的组件。
props 是外部数据,只能读不能改
为什么需要:子组件拿到的 props 来自父组件。如果子组件直接修改它,就会和父组件的数据源不一致。转写稿里在类组件中尝试写 this.props.count = ...,运行时直接报错,核心信息是只读属性不能被重新赋值,类似 Cannot assign to read only property。
怎么想:props 是“别人传给我的数据”,不是子组件自己的状态。子组件想改数据,不能改 props,而应通知父组件改父组件的 state,再通过 props 把新值传下来。
怎么做:不要在子组件里写:
jsx
this.props.count = 100; // 错误:props 只读易错点:把 props 当成自己的 state 随意赋值;或者误以为“改完能生效”。调试时看控制台报错,就能确认这是 React 的只读约束。
子传父:借助 props 传一个方法
为什么需要:子组件要修改父组件的数据源,不能直接改 props,只能调用父组件传下来的方法,让父组件自己更新 state。
怎么想:通信方向反过来,但媒介仍是 props。父组件传的不是数据,而是一个“回调方法”;子组件在合适时机调用它,并把要传的数据作为实参传回去。
怎么做:按转写稿的五步整理:
- 父组件定义方法,并设置一个或多个形参;
- 把方法的
this指向当前父组件实例; - 通过标签属性把方法传给子组件;
- 子组件通过
props接收:类组件用this.props.方法名,函数组件用props.方法名; - 子组件调用该方法,并把数据作为实参传入。
示意代码:
jsx
// 父组件
decCount = (num) => {
this.setState({ count: this.state.count - num });
};
<Child decCount={this.decCount.bind(this)} />jsx
// 类子组件
const { decCount } = this.props;
<button onClick={() => decCount(5)}>减5</button>jsx
// 函数子组件
function Child(props) {
const { decCount } = props;
return <button onClick={() => decCount(7)}>减7</button>;
}调试过程:转写稿中点击按钮后,父组件的 count 每点一次减 5;函数组件传 7 时每点一次减 7。父组件 state 变化后重新 render,子组件也重新渲染,所以界面数字同步变化。
易错点:忘记处理 this,导致方法里的 this 不可控;子组件接收方法名写错;调用时忘记传实参。
children 属性:对标签中的子元素
为什么需要:同一个 Button 组件,有时显示“保存”,有时显示“取消”“提交”。如果文字写死在组件里,就无法复用。children 可以获取组件对标签之间的子元素。
怎么想:组件既可用单标签,也可用对标签。只有对标签内部才可能有子元素;需要读取标签中间内容时,就用 children。
怎么做:
jsx
<Button>保存</Button>
<Button>取消</Button>
<Button>提交</Button>类组件中:
jsx
const { children } = this.props;
return <button>{children}</button>;调试过程:转写稿中先打印 this.props,能看到 children 属性;不同调用位置的值不同。若组件未引入,会出现 Button is not defined,引入后三个按钮文字才能正确显示。
易错点:单标签没有子元素;children 是 props 里的特殊属性,不要另起变量名混淆。
限定 props 类型、必填与默认值
为什么需要:组件使用者可能把数字传成字符串。子组件若要做算术运算,就需要约束类型,并给出清晰警告。
怎么想:React 脚手架项目默认已有 prop-types 包,直接导入即可。类组件用静态属性 propTypes、defaultProps;函数组件把函数当作对象,给它添加 propTypes、defaultProps。
怎么做:
jsx
import PropTypes from 'prop-types';
class TestClass extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
};
static defaultProps = {
age: 100
};
}函数组件:
jsx
TestFun.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
};
TestFun.defaultProps = {
age: 100
};调试过程:把 age 传成字符串 "19",控制台提示类型无效,期望 number;改成 {19} 后警告消失。不传 name 时提示 name is marked as required。不传 age 时使用默认值。
易错点:PropTypes 的 P 是大写,propTypes 的 p 是小写;isRequired 与默认值不要同时用于同一属性,因为必填就没有“不传时使用默认值”的场景。
TodoList 静态结构拆分:先整体再拆分
为什么需要:工程化开发不能把整个页面堆在一个文件里。要先保留整体结构,再拆成 Header、Main、Footer、Item 等组件。
怎么想:先把静态页面的根节点内部结构整体拷到 TodoList,统一把 HTML 的 class 替换成 JSX 的 className,再逐步剪切到各组件。这样避免每拆一个文件就单独替换一次。
怎么做:创建 components 目录及组件文件;把结构整体放入 TodoList;用查找替换把 class 改成 className,注意不要误改 class 关键字;再把 header、ul、footer、li 分别拆到对应组件。样式也先整体放入 index.css,再拆出 base 到全局 index.css,App.css 放 App 样式,各组件样式放各自 CSS 并导入。
调试过程:每拆一步刷新页面,确认样式没有乱。若出现 Button is not defined,检查是否导入;若出现 checkProp 相关警告,转写稿说明先不用管,因为涉及后续“受控组件”知识。
易错点:先拆再统一替换 className 效率低;剪切后忘记在原位置导入组件;样式拆分后忘记在组件中 import 对应 CSS。
本节要解决的问题
本节笔记的主题很明确:使用 create-react-app 脚手架快速构建一个工程化的 React 开发环境。它只解决两件事:第一,把脚手架安装到全局;第二,用脚手架命令创建 React 项目。
需要先说明:你给的转写稿主要内容是 props 只读、子传父、children、props 类型校验、todolist 静态结构拆分等,属于后续章节,并不是“13-1 全局安装脚手架并创建项目”的推导过程。为避免编造,下面不把无关内容硬塞进本节;转写稿中的同音字可纠正为:props、父组件、子传父、this、count、button、children、undefined、string、marked、index,但这些不作为本节脚手架步骤展开。
脚手架的作用:为什么需要 create-react-app
为什么需要:如果从零手写 React 工程,需要自己处理构建工具、模块解析、开发服务器、目录规范等配置,初学阶段容易被工程化细节拖住。
怎么想:create-react-app 的定位不是 React 语法本身,而是“帮你快速构建一个工程化的开发 React 的环境”。先把环境跑起来,再学组件、props、状态等内容。
怎么做:把 create-react-app 当作一个全局命令工具使用,用它生成项目骨架。
易错点:不要把脚手架当成 React 本身;它只是创建和运行工程的工具。初学阶段先按笔记命令执行,不要同时混用多套构建方案。
全局安装脚手架:为什么装到全局
为什么需要:笔记要求“全局安装脚手架”,目的是让 create-react-app 命令可以在任意目录下被调用,而不是只能在某个项目目录里使用。
怎么想:全局安装的是“命令工具”,不是某个具体项目的业务依赖。
怎么做:保留笔记中的两组命令,npm 和 yarn 二选一即可:
shell
npm i create-react-app -g
yarn add global create-react-app易错点:第一,npm 与 yarn 不要在同一环境里反复混装,选一个执行;第二,全局安装可能遇到权限、网络或全局路径问题,先看命令是否报错;第三,-g 与 yarn 的 global 写法不同,不要照抄错位。
使用脚手架创建 React 项目
为什么需要:安装脚手架后,最终目的是创建 React 项目,而不是只装一个命令。
怎么想:先进入你希望存放项目的父目录,再执行创建命令;项目名应避免和已有目录冲突。
怎么做:笔记写的是“使用 create-react-app 命令创建 react 项目”,但创建命令代码块在笔记中未给出完整内容。按脚手架命令形式可写为:
shell
# 笔记此处未给出完整创建命令,仅记录:
# 使用 create-react-app 命令创建 react 项目
create-react-app 你的项目名执行时以你本地安装版本的提示为准;创建完成后按脚手架终端提示进入项目并继续。
易错点:不要在没有确认父目录的情况下直接执行;项目名不要与已有文件夹重名;创建过程中不要随意中断依赖安装;如果命令不存在,先回到上一节检查全局安装是否成功。
验证与调试边界
为什么需要:安装和创建都属于命令行操作,必须确认“命令可用”和“项目已生成”。
怎么想:先验证 create-react-app 是否能被调用,再验证目标目录是否出现项目文件。
怎么做:如果命令报“找不到”,优先检查全局安装是否完成、终端是否需重开、npm/yarn 全局路径是否正常。本节笔记没有展开启动命令和目录结构讲解,因此不额外编造。
易错点:不要用后续 props 章节的报错来解释本节脚手架问题;两者不是同一个知识点。转写稿中没有本节脚手架的推导和调试过程,所以本节只基于笔记保留安装代码与创建项目这一目标,不虚构调试细节。
课堂画面





代码示例
示例 1:props 只读 + 子传父(类组件 / 函数组件)
jsx
// src/App.js
import React, { Component } from 'react'
import ChildClass from './components/ChildClass'
import ChildFunc from './components/ChildFunc'
export default class App extends Component {
state = { count: 100 }
// 父组件定义方法:子组件调用时把要减的数字作为实参传回来
decCount = (num) => {
this.setState({ count: this.state.count - num })
}
render() {
return (
<div>
<h3>父组件 count:{this.state.count}</h3>
{/* 把方法通过标签属性传给子组件 */}
<ChildClass count={this.state.count} decCount={this.decCount} />
<ChildFunc decCount={this.decCount} />
</div>
)
}
}
// src/components/ChildClass.js
import React, { Component } from 'react'
export default class ChildClass extends Component {
changeProps = () => {
// 下面这行会报错:Cannot assign to read only property 'count'
// this.props.count = 0
console.log('props 是只读的,不能修改:', this.props.count)
}
render() {
const { count, decCount } = this.props
return (
<div>
<p>类子组件收到 count:{count}</p>
{/* 子组件调用父组件传过来的方法,并把 5 作为实参传回去 */}
<button onClick={() => decCount(5)}>类子组件减 5</button>
<button onClick={this.changeProps}>检查 props 只读</button>
</div>
)
}
}
// src/components/ChildFunc.js
import React from 'react'
export default function ChildFunc({ decCount }) {
return <button onClick={() => decCount(7)}>函数子组件减 7</button>
}要点解释与易错点:
props是外部数据,只能读不能改;this.props.count = 0会报只读属性错误。- 子传父的核心套路:父组件定义方法 → 处理
this指向 → 通过标签属性传给子组件 → 子组件通过props接收 → 在合适时机调用并传实参。 - 类组件里普通方法如果直接传给子组件,容易丢失
this,本节用箭头函数decCount = (num) => {}保证this指向父组件实例。 - 函数组件通过参数解构接收方法:
function ChildFunc({ decCount }) {}。 - 子组件不能直接改父组件数据,本质是要调用父组件方法,让父组件自己
setState修改数据源。
示例 2:children 属性 + PropTypes 类型校验 + 必填 + 默认值
jsx
// src/App.js
import React, { Component } from 'react'
import Button from './components/Button'
import TestClass from './components/TestClass'
import TestFunc from './components/TestFunc'
export default class App extends Component {
render() {
return (
<div>
{/* 双标签调用,标签中间的“保存/取消/提交”就是 children */}
<Button>保存</Button>
<Button>取消</Button>
<Button>提交</Button>
{/* age 传数字要用 {19},不能写成 "19" */}
<TestClass name="艾特硅谷" age={19} />
{/* 不传 age,使用 defaultProps 默认值 */}
<TestFunc name="艾特硅谷" />
</div>
)
}
}
// src/components/Button.js
import React, { Component } from 'react'
export default class Button extends Component {
render() {
const { children } = this.props
return <button>{children}</button>
}
}
// src/components/TestClass.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class TestClass extends Component {
static propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
}
static defaultProps = {
age: 100
}
render() {
const { name, age } = this.props
return <p>类组件:{name},{age}</p>
}
}
// src/components/TestFunc.js
import React from 'react'
import PropTypes from 'prop-types'
function TestFunc(props) {
const { name, age } = props
return <p>函数组件:{name},{age}</p>
}
TestFunc.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
}
TestFunc.defaultProps = {
age: 100
}
export default TestFunc要点解释与易错点:
children用来获取双标签组件中间的子元素;单标签<Button />没有children。- 类组件限定 props:写
static propTypes和static defaultProps。 - 函数组件限定 props:把函数组件当成对象,写
TestFunc.propTypes和TestFunc.defaultProps。 - 注意包名和属性名大小写:
import PropTypes from 'prop-types',类组件里是static propTypes。 PropTypes.string.isRequired表示字符串且必填;不传会警告。age: PropTypes.number表示必须是数字;如果写age="19"会警告类型不符合。defaultProps只在没传值时生效;同一个属性不要同时写isRequired和默认值,因为必填后默认值基本没有生效时机。- 本节按转写稿说明:
prop-types在工程化项目中直接导入使用;不要把导入包写成PropTypes去导入。
常见错误
现象:子组件里
this.props.count = 10,控制台直接报Cannot assign to read only property 'count'。 原因:props 是外部传入的只读数据,数据源不在当前组件。 修法:父组件定义修改 state 的方法,通过 props 传给子组件,由子组件调用该方法间接修改。现象:子组件点击按钮调用父组件方法时报
this.setState is not a function或Cannot read properties of undefined。 原因:方法以普通函数形式传递,调用者是子组件,this不再指向父组件实例。 修法:父组件在传递时this.decCount = this.decCount.bind(this)后传this.decCount,或直接用箭头函数定义/包裹传递。现象:页面一渲染就执行了减数逻辑,或者子组件传的数字变成了事件对象。 原因:
onClick={this.props.decCount(5)}是立即调用,onClick={this.props.decCount}拿到的是 event。 修法:统一写成onClick={() => this.props.decCount(5)}。现象:写了 propTypes 但控制台没有任何类型警告,或报
Cannot read properties of undefined (reading 'string')。 原因:propTypes大小写写错(应为小写 p 开头)、校验包引入名写错,或函数组件忘了以「函数对象属性」方式挂载。 修法:import PropTypes from 'prop-types';类组件用static propTypes = {...},函数组件用TestFun.propTypes = {...},两者都用大写PropTypes.string。现象:给 age 设了默认值,但控制台仍提示 age 未传;或默认值永远不出现。 原因:同时写了
isRequired和defaultProps,语义互相冲突。 修法:二选一——要么必填不给默认值,要么给默认值不加isRequired。现象:组件标签里写了内容,子组件里
props.children却是undefined。 原因:调用时用了单标签自闭合写法。 修法:改成双标签<Button>保存</Button>,子组件内解构children并渲染。现象:从静态页面拷贝的 todolist 结构样式全丢、控制台报语法错误。 原因:JSX 里仍是 HTML 的
class;全局替换class→className时把class关键字也一起替换了。 修法:全局替换后回头检查并修复被误改的class关键字,再逐块刷新验证。现象:样式拆分后某个组件样式不生效,但 CSS 文件明明已存在。 原因:只在 to-do-list 的 index.css 里留了样式,没在对应组件里
import './xxx.css'。 修法:样式跟着组件走,每个组件文件顶部导入自己的 CSS。
练习
子传父最小闭环:父组件维护
count,子组件放两个按钮,分别调用父组件传下来的方法把 count 加 5、减 3。要求子组件内不出现任何对 props 的赋值,并说明bind(this)去掉后会发生什么。props 校验三件套:给一个子组件加校验——
name必须是字符串且必填,age必须是数字且默认 18。先用字符串'19'传 age,观察警告;再改成{19}确认警告消失;最后去掉 name,观察必填警告。children 封装:写一个
Card组件,标题固定、内容区用props.children渲染,父组件分别放入一段文字、一个按钮、一个列表,验证同一个组件能承载不同内容。静态结构拆分:把课件里的 todolist 静态页面拆成 Header / Main / Item / Footer。要求记录顺序:先整体搬进 to-do-list 组件 → 全局替换 className → 再逐块剪切拆分 → 最后按块拆 CSS。每完成一步刷新一次,说明为什么不能先拆再替换。
综合改造:在题 4 基础上,给 Item 组件加 propTypes 限定 todo 为对象且必填;在 Footer 里放「清空」按钮,通过子传父方式调用父组件的清空方法。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | props外部数据不可修改 | 02:45 |
| 2 | props组件通信-子传父 | 10:30 |
| 3 | props-children属性 | 04:40 |
| 4 | 限定props类型-必填-默认值 | 12:42 |
| 5 | todolist静态结构布局 | 14:58 |
小结
- props 是单向只读的外部数据,子组件永远没有修改权,只能通过父组件提供的方法回调去改数据源。
- 子传父的本质是「父传函数、子调用函数」,五步套路固定,类组件与函数组件的差别只在接收语法(
this.propsvs 解构参数)。 - 方法作为 props 传递时 this 会丢失,绑定动作必须在父组件完成,否则错误只能在子组件调用时才暴露。
children是让组件具备「内容可插拔」能力的入口,只有双标签调用才有子元素,单标签场景下它必然为空。- propTypes 是开发期提示而非运行时拦截:它只打警告、不阻断渲染,所以不能靠它做业务校验。
isRequired与defaultProps语义互斥,同时存在时默认值永远没有生效时机。- 静态页面转组件要「先整体搬入、先批量替换、再逐块拆分」,每步刷新验证,比一次拆完再调试成本低得多。
- 组件拆分的边界是结构 + 样式 + 行为一起走,CSS 必须跟着组件文件 import,否则拆分后样式必然丢失。