Skip to content

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。父组件传的不是数据,而是一个“回调方法”;子组件在合适时机调用它,并把要传的数据作为实参传回去。

怎么做:按转写稿的五步整理:

  1. 父组件定义方法,并设置一个或多个形参;
  2. 把方法的 this 指向当前父组件实例;
  3. 通过标签属性把方法传给子组件;
  4. 子组件通过 props 接收:类组件用 this.props.方法名,函数组件用 props.方法名;
  5. 子组件调用该方法,并把数据作为实参传入。

示意代码:

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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例 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。

练习 ​

  1. 子传父最小闭环:父组件维护 count,子组件放两个按钮,分别调用父组件传下来的方法把 count 加 5、减 3。要求子组件内不出现任何对 props 的赋值,并说明 bind(this) 去掉后会发生什么。

  2. props 校验三件套:给一个子组件加校验——name 必须是字符串且必填,age 必须是数字且默认 18。先用字符串 '19' 传 age,观察警告;再改成 {19} 确认警告消失;最后去掉 name,观察必填警告。

  3. children 封装:写一个 Card 组件,标题固定、内容区用 props.children 渲染,父组件分别放入一段文字、一个按钮、一个列表,验证同一个组件能承载不同内容。

  4. 静态结构拆分:把课件里的 todolist 静态页面拆成 Header / Main / Item / Footer。要求记录顺序:先整体搬进 to-do-list 组件 → 全局替换 className → 再逐块剪切拆分 → 最后按块拆 CSS。每完成一步刷新一次,说明为什么不能先拆再替换。

  5. 综合改造:在题 4 基础上,给 Item 组件加 propTypes 限定 todo 为对象且必填;在 Footer 里放「清空」按钮,通过子传父方式调用父组件的清空方法。

原视频对照 ​

#视频时长
1props外部数据不可修改02:45
2props组件通信-子传父10:30
3props-children属性04:40
4限定props类型-必填-默认值12:42
5todolist静态结构布局14:58

小结 ​

  • props 是单向只读的外部数据,子组件永远没有修改权,只能通过父组件提供的方法回调去改数据源。
  • 子传父的本质是「父传函数、子调用函数」,五步套路固定,类组件与函数组件的差别只在接收语法(this.props vs 解构参数)。
  • 方法作为 props 传递时 this 会丢失,绑定动作必须在父组件完成,否则错误只能在子组件调用时才暴露。
  • children 是让组件具备「内容可插拔」能力的入口,只有双标签调用才有子元素,单标签场景下它必然为空。
  • propTypes 是开发期提示而非运行时拦截:它只打警告、不阻断渲染,所以不能靠它做业务校验。
  • isRequired 与 defaultProps 语义互斥,同时存在时默认值永远没有生效时机。
  • 静态页面转组件要「先整体搬入、先批量替换、再逐块拆分」,每步刷新验证,比一次拆完再调试成本低得多。
  • 组件拆分的边界是结构 + 样式 + 行为一起走,CSS 必须跟着组件文件 import,否则拆分后样式必然丢失。