Skip to content

类组件与css样式文件处理(Day5 1/3) ​

本页对应 React + TypeScript + 医通项目,共 5 段视频,约 1 小时 20 分。

学习目标 ​

  • 能使用 defaultValue、defaultChecked 和 ref 实现非受控表单,并提交时获取用户名、密码及单选按钮的最新值。
  • 能针对 radio 的 ref 数组,使用 find 或 filter 定位选中项并提取其 value。
  • 能配置金山打字通自定义课程并导入常用单词 txt 文件,完成一次手速练习并记录成绩。
  • 能根据业务需求区分并选用 forEach、filter、map、reduce 完成遍历修改、过滤、映射和累加操作。
  • 能使用 find、findIndex、some、every 完成查找元素、查找索引及条件判断,并说明返回值差异。
  • 能列举 7 个改变原数组的方法和 5 个特殊方法,并使用 splice、slice、concat、indexOf、includes 完成增删改查、复制数组或类数组转换。

知识点 ​

本节要解决什么问题 ​

类组件里做表单,最容易写成“受控组件”:把 state 直接赋给 value,再配 onChange 实时 setState。但有些场景不想每输入一个字符就更新状态,只想在提交时拿到用户最新输入,这时可用“非受控组件”:状态只提供初始值,DOM 自己维护输入,提交时通过 ref 读取。转写稿里的“数控组件”应纠正为“受控组件”,“onub mate”是 onSubmit,“sve”是 save。

为什么需要:非受控写法简单,不需要为每个字段写 onChange。
怎么想:输入框不要给 value,改给 defaultValue;单选/复选框不要给 checked,改给 defaultChecked;提交时用 ref 拿 DOM 当前值。
怎么做:类组件里用 React.createRef() 创建 ref,绑定到表单元素,在 save 中读取。
易错点:给 value 却不给 onChange,字段会变成只读;非受控不是“不绑状态”,而是状态只做默认值;提交函数忘记 e.preventDefault() 会刷新页面。

从受控到非受控:value 改成 defaultValue ​

先保留状态作为初始值:

jsx
state = {
  username: '上硅谷',
  password: '123123',
  sex: 1
};

如果写成 <input value={this.state.username} /> 又没有 onChange,控制台会提示字段只读。改成非受控:

jsx
<input
  type="text"
  defaultValue={this.state.username}
  ref={this.usernameRef}
/>
<input
  type="password"
  defaultValue={this.state.password}
  ref={this.passwordRef}
/>
<input
  type="radio"
  name="sex"
  value="1"
  defaultChecked={this.state.sex === 1}
  ref={this.sexRef[0]}
/>男
<input
  type="radio"
  name="sex"
  value="0"
  defaultChecked={this.state.sex === 0}
  ref={this.sexRef[1]}
/>女

为什么需要:defaultValue/defaultChecked 只决定首次渲染的默认值,之后用户输入由 DOM 管理。
怎么想:把“状态控制输入”改成“状态只给初值”。
怎么做:文本用 defaultValue,单选/复选用 defaultChecked。
易错点:defaultChecked 后续不会随 state 变化;受控与非受控不要混用,同一个字段不要同时写 value 和 defaultValue。

创建 ref 并绑定表单元素 ​

转写稿的调试过程是:先创建 ref,再绑定,最后在 save 里读。文本和密码各一个 ref,单选因为有两个 input,所以 ref 用数组:

jsx
usernameRef = React.createRef();
passwordRef = React.createRef();
sexRef = [React.createRef(), React.createRef()];

绑定后,this.usernameRef.current 就是真实 input DOM。
为什么需要:非受控组件的值不在 state 里,提交时只能从 DOM 取。
怎么想:一个表单元素对应一个 ref;一组单选对应一组 ref。
怎么做:ref={this.usernameRef}、ref={this.sexRef[0]}。
易错点:sexRef 是数组,不能写 this.sexRef.current;ref 未绑定或组件未挂载时 current 可能为 null。

提交时读取最新值:radio 的 find 推导 ​

提交函数先阻止默认行为,再读取:

jsx
save = (e) => {
  e.preventDefault();
  const username = this.usernameRef.current.value;
  const password = this.passwordRef.current.value;
  const sex = this.sexRef
    .find(item => item.current.checked)
    .current.value;
  console.log(username, password, sex);
};

转写稿中调试 sexRef 时可以看到:数组里每一项是 ref 对象,item.current 是 input DOM,DOM 上有 checked 和 value。选中男时第一个 checked 为 true,选中女时第二个为 true。所以先用 find 找到 checked 为真的那一项,再取 current.value。用 filter 也可以,但返回数组,还要再取 [0]。

为什么需要:radio 是多个元素共享一个值,不能直接取某一个 input 的 value。
怎么想:先找“被选中的那个”,再取它的 value。
怎么做:find(item => item.current.checked).current.value。
易错点:忘记 preventDefault 会刷新页面,看不到打印;name 不一致会导致单选不互斥;直接读 state.sex 在非受控下不会随用户输入更新。

非受控组件的适用边界 ​

为什么需要:不是所有表单都适合受控。文件上传、简单提交、第三方 DOM 插件常需要非受控。
怎么想:需要实时校验、联动、禁用按钮,用受控;只需要提交时取值,用非受控。
怎么做:非受控 = defaultValue/defaultChecked + ref;受控 = value/checked + onChange。
易错点:非受控组件提交前要确保读取的是 DOM 最新值;若后续要重置表单,不能只改 state,需要操作 DOM 或重新挂载。转写稿后半复习数组方法,radio 取选中项用到的 find 正是这里的关键推导。

本节说明 ​

本节笔记主题是“CSS 样式文件处理”。给定的转写稿主要讲非受控组件、打字练习和数组方法,并没有对应的 CSS 样式处理推导与调试过程。为避免编造,下面只依据本节笔记扩写;转写稿中与本节无关的内容不强行套用。

为什么要先给样式文件分类 ​

为什么需要:React 项目里既有第三方样式库,又有重置样式、全局通用样式和组件内样式。如果不先约定位置和引入方式,容易出现样式不生效、引入顺序混乱、组件类名冲突等问题。

怎么想:先按“作用范围”分层:第三方库、重置样式、全局通用样式、组件内样式;再按“谁使用谁引入”的原则处理。全局性的放在入口或根组件,组件自己的样式放在组件目录。

怎么做:笔记给出四类位置与引入方式,后面分别展开。

易错点:不要把所有 CSS 都塞进入口文件;组件样式不要随意放到全局,否则类名容易互相影响。

第三方 CSS 库:bootstrap.css 的两种方式 ​

为什么需要:使用 Bootstrap 这类第三方样式库,可以快速获得按钮、栅格、表单等基础样式,减少重复编写。

怎么想:笔记给出两种思路:一种把现成 CSS 文件放到 public 下,通过 HTML 的 link 引入;另一种通过 npm 安装,再在 JS 入口用 import 引入。

怎么做:

方式一:位置为 public/css/bootstrap.css,在 public/index.html 中通过 link 标签引入。

方式二:

  1. 安装样式库:npm i bootstrap@3
  2. 在 src/index.js 中使用 import 语法导入样式。

易错点:方式一要注意 public/index.html 中的路径是否正确;方式二要注意安装的是笔记指定的 bootstrap@3,不要随意混用不同大版本。两种方式选一种即可,不要重复引入造成样式覆盖混乱。

重置样式:src/index.css ​

为什么需要:浏览器对 margin、padding、盒模型等有默认样式,不同浏览器表现可能不一致。重置样式用于统一基础表现。

怎么想:重置样式影响整个页面,属于全局基础层,因此放在 src/index.css,由入口 src/index.js 引入。

怎么做:位置为 src/index.css,在 src/index.js 中通过 import 导入。

易错点:重置样式应在业务样式之前生效;不要把它误放到某个组件目录里,否则只在该组件引入时才生效,失去全局意义。

全局通用样式:src/App.css ​

为什么需要:项目里会有多个组件共用的样式,例如通用布局、公共类名等。放在 App.css 中便于统一维护。

怎么想:它属于应用级通用样式,不是某一个小组件的私有样式,所以放在 src/App.css,由根组件 src/App.jsx 引入。

怎么做:位置为 src/App.css,在 src/App.jsx 中通过 import 导入。

易错点:全局通用样式里的类名要尽量有明确语义,避免和组件内普通 CSS 类名撞车;如果冲突明显,应考虑 CSS 模块化。

组件内样式:组件目录中的 CSS ​

为什么需要:组件自己的样式应跟随组件存放,便于定位、复用和维护。

怎么想:哪个组件使用,就放在哪个组件目录中,并由该组件通过 import 导入。

怎么做:位置为组件目录的 CSS 中;导入方式是在组件中使用 import 导入。

易错点:普通 CSS 文件即使写在组件目录里,类名仍然是全局的,并不是“放在组件目录就自动隔离”。真正的隔离要靠 CSS 模块化。

CSS 模块化:把 CSS 变成 JS 模块处理 ​

为什么需要:不同组件可能都写 .box、.app 这类类名,普通 CSS 会发生同类名样式冲突。CSS 模块化会把类名映射成带哈希的名字,降低冲突概率。

怎么想:笔记明确:CSS 模块化是将 CSS 文件变为 JS 模块进行处理,作用是解决组件同类名的样式冲突问题。使用时不再直接写字符串类名,而是通过导入得到的 styles 对象取类名。

怎么做:

  1. CSS 文件名必须以 .module.css 后缀结尾。
  2. 使用 import 语法导入 CSS 模块,并存储成 JS 变量。
  3. 使用插值表达式给 className 赋值,值为 styles 中的类名。

示例:

js
#例如:
import styles from './index.module.css'
console.log('styles: ', styles); // {box: 'B_box__-Qmig'}

组件示例:

jsx
import React, { Component } from 'react'
// 2 导入css模块并存储成js变量
import styles from './index.module.css'
console.log('styles: ', styles);
export default class B extends Component {
    render() {
        return (
            <>
            	<div className={styles.box}>B</div>
            	<div className={[styles.box, styles.app, 'container'].join(' ')}>B多个类名</div>
            </>
        )
    }
}

调试过程:先 console.log(styles),确认打印结果是从类名到哈希类名的映射,例如 {box: 'B_box__-Qmig'};再在 JSX 中用 styles.box 取映射后的类名。多个类名可用数组加 join(' ') 拼接,普通全局类名如 'container' 可直接写在数组里。

易错点:文件名不是 .module.css 时,导入结果通常不是类名映射对象;不要写成 className="box" 或 className={styles['不存在的类名']};多个类名不要直接相加漏空格;styles.box 是变量取值,不是字符串 'styles.box'。

小结 ​

本节的核心不是“会写几个 import”,而是建立样式分层:第三方库、重置样式、全局通用样式、组件内样式各归其位;当组件类名可能冲突时,再用 .module.css 把 CSS 变成 JS 模块,通过 styles.xxx 使用映射后的类名。这样结构清楚,冲突也可控。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:组件内样式 + CSS 模块化(解决组件同类名冲突) ​

css
/* src/components/B/index.module.css  —— 注意后缀必须是 .module.css */
.box {
  width: 200px;
  height: 200px;
  background-color: pink;
  color: #fff;
}
.app {
  border: 2px solid #333;
}
jsx
// src/components/B/index.jsx
import React, { Component } from 'react'
// 2 导入 css 模块并存储成 js 变量
import styles from './index.module.css'
console.log('styles: ', styles) // {box: 'B_box__-Qmig', app: 'B_app__xxxx'}

export default class B extends Component {
  render() {
    return (
      <>
        <div className={styles.box}>B</div>
        <div className={[styles.box, styles.app, 'container'].join(' ')}>B多个类名</div>
      </>
    )
  }
}

关键行与易错点

  • index.module.css:文件名必须以 .module.css 结尾,否则 import styles 拿不到对象,只会被当作普通样式文件全局生效。
  • import styles from './index.module.css':CSS 模块化会把 css 变成 js 模块,styles 是一个对象,styles.box 才是编译后带哈希的真实类名。
  • className={styles.box}:必须用插值表达式,不能写成 className="styles.box",那样只会得到字符串 "styles.box"。
  • 多类名用 [styles.box, styles.app, 'container'].join(' '):普通类名(如 container)不能加 styles. 前缀,只有 css 模块文件里的类名才从 styles 上取。
  • 作用:解决不同组件出现同名类(都叫 .box)时的样式冲突。

html
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>React App</title>
    <!-- 方式一:把 bootstrap.css 放到 public/css/ 下,通过 link 标签引入 -->
    <link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css" />
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
jsx
// src/App.jsx  —— 此时直接用 bootstrap 的类名即可
import React, { Component } from 'react'
import './App.css'
import B from './components/B'

export default class App extends Component {
  render() {
    return (
      <div className="container">
        <button className="btn btn-primary">bootstrap 按钮</button>
        <B />
      </div>
    )
  }
}

关键行与易错点

  • 文件位置:public/css/bootstrap.css,放在 public 下不会被 webpack 处理,直接原样拷贝到根目录。
  • 引入位置:public/index.html 的 <head> 里用 <link> 标签引入,注意 public 目录在访问时是根路径,写 css/bootstrap.css 而不是 public/css/bootstrap.css。
  • 顺序问题:link 要写在 <body> 之前(放 head 里),否则可能先渲染出无样式页面再闪一下。
  • className="btn btn-primary":bootstrap 是普通全局 css,类名直接写字符串,不要加 styles.。

示例三:方式二 import 引入样式 + 重置样式 + 全局通用样式 ​

js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

// 2. 安装样式库:npm i bootstrap@3,使用 import 语法导入样式
import 'bootstrap/dist/css/bootstrap.css'
// 2. 重置样式:位置 src/index.css
import './index.css'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)
jsx
// src/App.jsx
import React, { Component } from 'react'
// 3. 全局通用样式:位置 src/App.css
import './App.css'
import B from './components/B'

export default class App extends Component {
  render() {
    return (
      <div className="container">
        <B />
      </div>
    )
  }
}

关键行与易错点

  • 方式二与方式一的区别:方式一靠 public/index.html 的 <link>,方式二靠 import,import 'bootstrap/dist/css/bootstrap.css' 是副作用导入,没有变量接收,只是把样式打包进来。
  • import './index.css'(重置样式)和 import './App.css'(全局通用样式)同理,都是无变量导入,写在文件顶部。
  • 引入顺序决定覆盖关系:index.css(重置)一般放在 bootstrap 之后、App.css 之前,否则重置样式会把 bootstrap 的样式覆盖掉。
  • 四种位置要分清:第三方库(bootstrap)→ public 或 index.js;重置样式 → src/index.css 由 src/index.js 引入;全局通用样式 → src/App.css 由 src/App.jsx 引入;组件内样式 → 组件目录下的 css 由组件自己 import。
  • 组件内样式若不想污染全局,一律用 .module.css,不要图省事直接 import './index.css'。

常见错误 ​

  • 现象:给 input 写了 value 却没写 onChange,控制台警告且输入框只读。→ 原因:把受控组件和非受控组件混用。→ 修法:非受控改用 defaultValue / defaultChecked;要受控就补 onChange。
  • 现象:radio 的 ref 直接取 current.value 报错或拿到 undefined。→ 原因:多个 radio 对应的是 ref 数组,不是单个 DOM 对象。→ 修法:先 find/filter 出 checked === true 的项,再取 current.value。
  • 现象:className={styles.box} 得到 undefined,样式不生效。→ 原因:CSS 文件名没有 .module.css 后缀,或没有按模块导入。→ 修法:改为 index.module.css,import styles from './index.module.css' 后再用 styles.box。
  • 现象:多个类名写成 className={styles.box styles.app},编译报错或类名丢失。→ 原因:className 需要字符串,不能直接并列写多个表达式。→ 修法:用 [styles.box, styles.app, 'container'].join(' ') 或模板字符串拼接。
  • 现象:想筛出部分数据却用了 map,结果长度没变;想做全量映射却用了 filter,结果条数变少。→ 原因:没有先判断需求是“筛选”还是“等长映射”。→ 修法:筛选用 filter,等长转换用 map,累计用 reduce。
  • 现象:reduce 累加结果出现 NaN 或 undefined。→ 原因:回调没有 return 上一次累计值,或初始值不符合业务。→ 修法:明确 return pre + cur.xxx,并给出正确初始值。
  • 现象:indexOf 查找第一个元素时判断失败。→ 原因:返回索引 0 被当成假值。→ 修法:判断 !== -1,或改用返回布尔值的 includes。
  • 现象:表单点击保存后页面刷新,拿不到最新值。→ 原因:提交事件默认行为未阻止。→ 修法:在提交回调开头调用 e.preventDefault()。

练习 ​

  1. 用类组件实现非受控登录表单:username、password、sex 三项,点击保存时通过 ref 获取最新值;其中 sex 用 find 取出选中项的值。
  2. 给一个类组件添加 index.module.css,要求同时应用模块内的 box、app 和全局类名 container,并避免类名冲突。
  3. 给定 todoList,分别用 filter、map、reduce 完成:筛选未完成事项、把所有事项标记为已完成、统计已完成数量。
  4. 给定 goodsList,用 findIndex 找到 id === 2 的商品并改价;再用 map 写一个“返回新数组、不改原数组”的版本,比较两者差异。
  5. 用 indexOf 和 includes 判断购物车中是否存在某商品,并写出各自正确的判断条件。

原视频对照 ​

#视频时长
1非受控组件表单操作14:13
2金山打字通如何练习手速03:30
3forEach-filter-map-reduce31:31
4find-findIndex-some-every10:08
5改变原数组方法及5个特殊方法20:50

小结 ​

  • 表单先明确“受控”还是“非受控”,不要在同一元素上混用两套思路。
  • 非受控表单的难点不在绑定,而在提交时通过 ref 稳定取到最新值。
  • 多个 radio 的 ref 要按数组处理,先定位选中项,再取值。
  • 组件内样式优先用 CSS Modules;多类名最终必须拼成一个字符串。
  • 数组方法先看需求形态:筛选、等长映射、累计、查找、判断存在,再选方法。
  • reduce 要重点关注回调返回值、初始值和最终返回值三者关系。
  • 判断 indexOf 结果不要直接看真假,索引 0 是常见坑。