Skip to content

局部安装方法与react工程化项目目录结构分析(Day4 2/4) ​

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

学习目标 ​

  • 能按局部安装流程完成 React 工程依赖安装,并说明安装结果与常见命令的作用。
  • 能分析 React 工程化项目目录结构,指出入口文件、组件目录、样式文件及资源目录的职责。
  • 能使用 CSS 模块化方式为同一元素应用多个类名,并通过数组与空格连接实现多个样式生效。
  • 能完成轮播图布局与交互功能,包括左右按钮切换、索引边界判断、数据驱动视图更新及图片数组化改造。
  • 能写出 React 17 的入口渲染代码,并对比说明其与 React 18 在导入方式和根节点创建方式上的差异。
  • 能结合组件出生、更新、销毁说明 React 生命周期的三个阶段,并列举各阶段会触发的特殊方法及执行时机。

知识点 ​

先明确局部安装的定位 ​

本节讲的是用“局部安装”的方式准备 create-react-app,再用它创建 React 项目。转写稿在回顾脚手架时提到:创建项目不只一种方式,前面介绍过 npm 的方式、yarn 的方式,也介绍了局部安装的方式。这里“y 的方式”应理解为 yarn 的方式,不是另一个陌生命令。局部安装的核心不是换框架,而是把创建工具装到当前项目里,避免依赖全局环境。下面按笔记顺序拆开讲。

1. 找一个非中文目录并打开终端 ​

为什么需要:项目路径里出现中文,容易让命令行工具、依赖解析或脚本执行出现不必要的问题;初学阶段先避开这类干扰。怎么想:不要急着敲安装命令,先确认“我在哪个目录”“这个目录能不能放项目”。怎么做:新建一个纯英文、尽量无空格的目录,在该目录中打开终端。易错点:在桌面或下载目录里随手执行,结果项目生成到意想不到的位置;路径含中文或空格后,后续命令定位困难。调试时先执行查看当前路径的命令,确认终端所在位置,再继续。

2. 执行 npm init -y ​

为什么需要:局部安装需要项目级的 package.json 来记录依赖和项目信息。怎么想:先把这个目录初始化成一个 npm 项目,后面的安装才有落点。怎么做:在终端执行:

shell
npm init -y

易错点:没看清当前目录就执行,导致在错误位置生成 package.json;目录里已有项目文件时,要先确认是否会覆盖或混用配置。若后续发现依赖没有写进当前项目,第一反应应是检查 package.json 是否在当前目录。

3. 局部安装 create-react-app ​

为什么需要:全局安装会把工具装到全局环境,多个项目之间可能互相影响;局部安装则把工具装进当前项目的 node_modules,依赖关系更清楚。怎么想:既然已经初始化了项目,就把创建工具作为当前项目的依赖安装。怎么做:执行:

shell
npm i create-react-app

易错点:安装完成后误以为可以直接输入 create-react-app 创建项目;局部安装的工具通常不在全局命令路径里,需要借助 npx 调用。另一个易错点是没确认安装是否成功就往下走,调试时可先看当前目录是否出现 node_modules,并检查 package.json 中是否记录了依赖。

4. 通过 npx 创建项目 ​

为什么需要:局部安装的 create-react-app 不在全局命令中,直接用原名可能找不到;npx 会优先使用当前项目本地安装的命令。怎么想:工具已经装在本地,就用 npx 去调用它,并给它一个项目名。怎么做:执行:

shell
npx create-react-app 项目名

易错点:项目名尽量使用英文小写,避免中文、空格和不合规字符;命令执行位置要在刚才初始化并安装依赖的目录中。若提示找不到命令,按“当前目录是否有 package.json、是否完成局部安装、是否使用了 npx”的顺序排查,而不是急着重装。

5. 进入项目目录并启动项目 ​

为什么需要:项目创建完成后,开发服务器要在项目目录内启动。怎么想:先 cd 进入刚生成的项目,再运行启动命令。怎么做:按笔记保留的命令执行:

shell
cd 项目名
npm start

易错点:还没进入项目目录就执行 npm start,会找不到启动脚本;启动后终端不要立即关闭,开发服务器需要保持运行。若启动失败,先确认是否在项目根目录、依赖是否安装完整,再检查端口是否被占用。局部安装的完整顺序就是:选目录 → 初始化 → 局部安装工具 → 用 npx 创建 → 进目录启动。

先看整体:根目录的边界 ​

为什么需要:React 工程化项目文件多,先分清“npm 包目录、静态资源、源码、配置文件”,后面找入口和排错才不会乱。
怎么想:按使用对象划分——node_modules 给 npm 用,public 给浏览器站点用,src 给开发者写业务用,package*.json 给依赖管理用。
怎么做:先保留本节目录树,读项目时按“public/index.html → src/index.js → src/App.js”找主链路。

shell
02-react-scaffold                          项目根目录
    |- node_modules                        npm包目录
    |— public                              静态资源目录
    |    |- favicon.ico                    站点图标
    |    |- index.html                     网站的入口html文件
    |    |- logo192.png                    移动端图片
    |    |- logo512.png                    移动端图片
    |    |- manifest.json                  移动端图标的配置文件
    |    |- robots.txt                     爬虫文件
    |- src                                 程序员开发目录
    |   |- App.css                         根组件的样式文件
    |   |- App.js                          根组件App
    |   |- App.test.js                     测试文件
    |   |- index.js                        js的入口文件
    |   |- index.css                       入口文件的样式文件
    |   |- logo.svg                        旋转的菊花图标
    |   |- reportWebVitals.js              google的报告文件
    |   |- setupTests.js                   测试启动文件
    |- package-lock.json                   包版本锁文件
    |- package.json                        包配置文件

易错点:不要改 node_modules;不要手改 package-lock.json;不要把业务代码写进 public。转写稿提到脚手架创建有 npm、yarn、局部安装等方式,创建后先认目录,不要急着堆业务。

public:静态资源与网站入口 ​

为什么需要:浏览器先拿到站点入口页,React 代码才有挂载位置。
怎么想:public 放站点级静态文件,index.html 是网站入口 html。
怎么做:favicon.ico 是站点图标;logo192.png、logo512.png 是移动端图片;manifest.json 是移动端图标配置;robots.txt 是爬虫文件。
易错点:不要以为 index.html 里要写业务组件;这些文件名不要随意改。转写稿里讲入口差异时提到 React18 从 react-dom/client 创建根节点,React17 用 react-dom 的 ReactDOM.render,这最终都挂在入口页提供的容器上。

src:程序员开发目录 ​

为什么需要:源码要集中管理,入口、根组件、样式、测试文件各自有位置。
怎么想:index.js 是 js 入口,App.js 是根组件,App.js 再组织组件树。
怎么做:index.js 负责入口挂载;App.js 是根组件;App.css 服务根组件;index.css 服务入口;logo.svg 是旋转菊花图标;App.test.js 是测试文件;reportWebVitals.js 是 Google 报告文件;setupTests.js 是测试启动文件。
易错点:不要把 App.js 当入口直接运行;不要在 index.js 里写大量业务;测试文件不要误当业务代码。转写稿演示过:18 项目写 17 的 ReactDOM.render 仍能运行,但会提示“no longer supported”,所以看 index.js 能判断项目写法。

包管理文件:package.json 与 package-lock.json ​

为什么需要:记录依赖和锁定版本,保证团队安装结果一致。
怎么想:package.json 是包配置,package-lock.json 是包版本锁文件。
怎么做:安装依赖看 package.json;提交代码保留 lock 文件;升级依赖用包管理器命令,不手写 lock。
易错点:手改 lock 会导致依赖树不一致;删 lock 后版本可能漂移;node_modules 不提交。

从脚手架到业务目录:components 与 assets ​

为什么需要:脚手架只给最小骨架,轮播图等业务组件要另建目录。
怎么想:按组件聚合,一个组件一个目录,入口 index.js,样式 index.css,图片放 assets/images。
怎么做:转写稿中在 src 下建 components/Swiper,Swiper 下建 index.js、index.css;图片放 assets/images;组件里导入样式和图片,图片路径用 require 加模板字符串拼接。
易错点:相对路径层级容易错,转写稿里从 Swiper 返回两层到 src 再进 assets/images;图片名不要写死为 1、2、3,转写稿后面把图片放进数组、用下标管理,说明资源命名与索引要分离。样式不生效时,第一反应是检查类名是否拼错并刷新。同音字纠正:Swiper 不是 swepper/swipper,render 不是 runner,index 不是 inice/inex,“轮模图/轮波图”应理解为轮播图。

读目录的调试路径 ​

为什么需要:出错时要知道先看哪一层。
怎么想:先看浏览器入口 public/index.html,再看 src/index.js 是否挂载成功,再看 App.js 组件树,最后看样式和资源路径。
怎么做:用 React 调试工具看组件结构和 state;改状态后视图重新渲染;样式问题看类名和样式文件导入;图片问题看 require 路径和文件名。
易错点:只看页面不查入口;状态直接赋值不会触发重新渲染,转写稿强调要用 setState;不要把“数据驱动视图”做成手动操作 DOM。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:局部安装 create-react-app 并启动项目 ​

shell
# 1. 找一个目录【不能是中文】,打开终端

# 2. 初始化 package.json
npm init -y

# 3. 局部安装 create-react-app
npm i create-react-app

# 4. 通过局部命令创建项目(用 npx 调 node_modules/.bin 里的命令)
npx create-react-app 02-react-scaffold

# 5. 进入项目目录并启动项目
cd 02-react-scaffold
npm start

关键行与易错点:

  • npm init -y 先生成 package.json,否则第 3 步装出来的包没有地方记录依赖,后面 npx 可能找不到。
  • 第 3 步是局部安装(不加 -g),所以第 4 步必须用 npx,不能直接写 create-react-app 项目名;npx 会优先到 node_modules/.bin 里找这个命令。
  • 项目目录不能是中文路径,否则安装和启动都容易报错。
  • 工程目录结构要记住这几个:public/index.html 是网站入口 html,src/index.js 是 js 入口,src/App.js 是根组件,node_modules 是 npm 包目录,package.json 是包配置文件,package-lock.json 是包版本锁文件。自己写的组件一律放在 src 下的 components 里,不要在根组件 App.js 里直接开发。

示例二:轮播图布局(结构 + 样式) ​

jsx
// src/components/Swiper/index.jsx
import React, { Component } from 'react'
import './index.css'

export default class Swiper extends Component {
  state = {
    index: 1
  }

  render() {
    const { index } = this.state
    return (
      <div className="wrapper">
        <img src={require(`../../assets/images/${index}.jpeg`)} alt="" />
        <span className="leftBtn">&lt;</span>
        <span className="rightBtn">&gt;</span>
      </div>
    )
  }
}
css
/* src/components/Swiper/index.css */
.wrapper {
  width: 600px;
  height: 350px;
  border: 3px solid red;
  margin: 50px auto;
  position: relative;   /* 子元素绝对定位的参照物 */
}

.wrapper img {
  width: 100%;
  height: 100%;
}

.wrapper span {
  display: block;       /* span 默认是行内元素,必须转块 */
  width: 30px;
  height: 60px;
  position: absolute;
  top: 145px;           /* (350 - 60) / 2 = 145,垂直居中 */
  background: #ccc;
  line-height: 60px;    /* 让箭头在按钮内垂直居中 */
  text-align: center;   /* 水平居中 */
  cursor: pointer;      /* 小手 */
  user-select: none;    /* 双击不被选中 */
}

.rightBtn {
  right: 0;
}

关键行与易错点:

  • 图片路径用 require(../../assets/images/${index}.jpeg) 拼模板字符串。当前文件在 src/components/Swiper/,所以要先退两层到 src,再进 assets/images。
  • &lt; &gt; 是 HTML 实体,直接写 < > 会被当成标签解析。
  • 左右按钮要定位,父级 .wrapper 必须写 position: relative,否则 top/right 会跑到整个页面上去——这是最容易漏的一行。
  • 样式没生效时,先检查类名有没有拼错,再刷新看一次(转写稿里就是类名写错导致“完全没作用”)。
  • 在 App.js 里用 <Swiper /> 引入后图片会“过于壮硕”,一定要用 .wrapper img { width:100%; height:100% } 控制尺寸。

示例三:轮播图功能实现(数组版本,完整可运行) ​

jsx
// src/components/Swiper/index.jsx
import React, { Component } from 'react'
import './index.css'

export default class Swiper extends Component {
  state = {
    index: 0,                                   // 下标从 0 开始
    imgArr: ['1.jpeg', '2.jpeg', '3.jpeg']      // 图片名随便起,改数组就行
  }

  // 右侧按钮:下一张
  rightBtnClick = () => {
    let index = this.state.index
    index++
    if (index > this.state.imgArr.length - 1) { // 越界就回到第一张
      index = 0
    }
    this.setState({ index })
  }

  // 左侧按钮:上一张
  leftBtnClick = () => {
    let index = this.state.index
    index--
    if (index < 0) {                            // 越界就回到最后一张
      index = this.state.imgArr.length - 1
    }
    this.setState({ index })
  }

  render() {
    const { index, imgArr } = this.state
    return (
      <div className="wrapper">
        <img src={require(`../../assets/images/${imgArr[index]}`)} alt="" />
        <span className="leftBtn" onClick={this.leftBtnClick}>&lt;</span>
        <span className="rightBtn" onClick={this.rightBtnClick}>&gt;</span>
      </div>
    )
  }
}
jsx
// src/App.js
import React, { Component } from 'react'
import Swiper from './components/Swiper'

export default class App extends Component {
  render() {
    return (
      <div>
        <Swiper />
      </div>
    )
  }
}

关键行与易错点:

  • 事件回调必须写成箭头函数(rightBtnClick = () => {}),这样函数内部没有自己的 this,会使用 render 里的 this,即当前组件实例;写成普通函数 function 再 onClick={this.rightBtnClick},this 就是 undefined,this.setState 直接报错。
  • onClick 后面只能放函数的引用地址,不能写 onClick={this.rightBtnClick()},那样一渲染就立刻调用。
  • 修改状态一定用 this.setState({ index })。直接 this.state.index = xxx 能改数据但不会重新渲染——React 是数据驱动视图,改完数据视图才会跟着变。
  • 边界判断是核心:length - 1 才是最后一个下标,> length - 1 回 0;< 0 回 length - 1。写成 > length 或 > 3 硬编码,一旦图片增删就会错位。
  • 换成数组后下标从 1 变 0,index 初始值和两个边界值都要同步改,漏改一处就会白屏。
  • 补充(多类名):CSS 模块化时 className 只能给一个值,多个类名要用数组加空格拼接: className={[styles.box, styles.app, 'global'].join(' ')}。

常见错误 ​

  1. npx create-react-app 报“command not found”或版本对不上 现象:局部装完了 create-react-app,npx 却提示找不到命令 / 拉起了另一个旧版本。 原因:没在 npm init -y 的那个目录里执行;或本机全局也装过 create-react-app,npx 优先命中了全局那份。 修法:确认当前路径下有 package.json 与 node_modules/.bin,在同一目录执行 npx create-react-app 项目名;必要时 npm uninstall -g create-react-app 后再试。

  2. 创建项目直接失败,提示项目名不合法 现象:Could not create a project called "我的项目",或名字含大写、空格报错。 原因:脚手架要求项目名是小写、无中文、无空格的 npm 合法包名;路径里带中文也会让部分依赖安装异常。 修法:项目名改成 my-app、react-demo 这类;父目录也换成纯英文路径(如 D:/code),再重新执行创建命令。

  3. CSS 模块化只写一个类名,多类名拼不出来 现象:元素要同时挂 styles.box、styles.app 和全局类,直接写 className={styles.box styles.app} 或字符串相加,结果类名变成 [object Object] 或样式全丢。 原因:CSS Modules 导出的是对象,多个类名需要先收集再拼成一个字符串。 修法:放进数组再 join(' '),例如 className={[styles.box, styles.app, 'globalCls'].join(' ')};全局类不要走 styles。

  4. 轮播图点到最后一张再点就白屏 / 图片 404 现象:点“下一张”到第 3 张后再点,图裂了或报 Cannot find module。 原因:索引只做了自增没做回绕;同时图片路径把文件名和索引硬绑在一起(${index}.jpeg),文件名一改就崩。 修法:自增后判边界,超出末位下标就回到 0;图片改用数组存放,渲染时取 imgs[index],索引只当下标用。

  5. setState 改了值但页面不动 现象:this.state.index = 2 之后 console 里值变了,视图没变。 原因:直接赋值只改了对象属性,没有触发 React 的重新渲染流程。 修法:一律走 this.setState({ index }),且先用局部变量算好边界再一次性 set。

  6. 事件里 this 是 undefined,报“Cannot read properties of undefined (reading 'setState')” 现象:点击按钮报错,this 打印出来是 undefined。 原因:类组件方法默认不绑定 this,作为回调传入时丢失了调用上下文。 修法:统一用箭头函数定义方法,或在 JSX 中包一层箭头函数调用,或在构造器里 bind。

  7. onClick={this.handleClick()} 一进页面就执行了 现象:还没点按钮,状态就变了 / 直接进入死循环。 原因:花括号里写的是函数调用,渲染时立即执行,把返回值交给了 onClick。 修法:写引用 onClick={this.handleClick},需要传参时再包一层 onClick={() => this.handleClick(id)}。

  8. React 17 写法混进 18 项目,控制台一片 warning 现象:ReactDOM.render 仍能跑,但提示 “no longer supported”。 原因:装的是 18 的包,入口却沿用了 17 的 react-dom 直接渲染写法。 修法:18 用 createRoot(document.getElementById('root')).render(<App />);若刻意演示 17,就别把这段留在正式入口里。

练习 ​

  1. 从零走一遍局部安装流程:新建纯英文目录 → npm init -y → 局部装 create-react-app → 用 npx 创建项目 → 启动。中途故意把项目名写成中文,记录报错信息并说明原因。

  2. 对照目录结构,说出 public/index.html 与 src/index.js 各自的作用;然后把 src 下你认为本项目用不到的文件删掉,验证项目还能否正常启动,并解释 reportWebVitals.js、setupTests.js 删掉后有什么影响。

  3. 在轮播图组件里加一个“跳转到指定张”的按钮组(1/2/3),要求索引仍受边界约束;再验证:把图片文件名全部改成 a.jpeg、b.jpeg、c.jpeg,代码需要改动几处?

  4. 给轮播图写多类名样式:根容器同时应用 CSS Modules 的 wrapper、wrapperDark 和一个全局类 center,用一行 className 完成,并说明为什么不能直接字符串相加。

  5. 用一句话分别描述挂载、更新、卸载三个阶段各自对应的一次“程序员可介入的时机”,并举出本项目里会触发“更新”阶段的两个具体操作。

原视频对照 ​

#视频时长
1css模块化多个样式类名应用的补充说明02:06
2轮播图练习之布局08:25
3轮播图功能实现04:36
4轮播图-图片是数组元素版本03:12
5react基础课件内容概览-react17语法介绍12:46
6react生命周期的理解07:32

小结 ​

  1. 局部安装的核心是“在哪装、在哪用”:package.json 所在目录决定 npx 能不能找到本地命令,路径和项目名保持纯英文小写能省掉一大半玄学报错。
  2. 目录结构只需记住两条主线:public 是静态资源与页面入口,src 是开发目录,index.js 是 JS 入口、App.js 是根组件。
  3. CSS Modules 导出的是对象,多类名必须自己拼成字符串——数组 + join(' ') 是最省心的写法,全局类不要混进 styles。
  4. 轮播图这类需求,布局只做一次,功能全靠“改数据”;把索引当作唯一状态源,边界判断在 setState 之前完成。
  5. 图片路径与索引解耦(数组版)是这一节最有价值的重构:新增一张图只需往数组里加一项,逻辑代码零改动。
  6. 类组件里 this 和 setState 是两大坑源:方法用箭头函数定义,状态只通过 setState 改,直接赋值必然视图不更新。
  7. 生命周期可以先用“出生—更新—死亡”的人生存节点来类比,先建立“到点执行特定方法”的直觉,再去看具体钩子名。
  8. React 17 与 18 的入口差别集中在 createRoot:包版本和写法要对齐,否则功能能跑但 warning 不断。