主题
类组件与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 标签引入。
方式二:
- 安装样式库:
npm i bootstrap@3 - 在
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 对象取类名。
怎么做:
- CSS 文件名必须以
.module.css后缀结尾。 - 使用
import语法导入 CSS 模块,并存储成 JS 变量。 - 使用插值表达式给
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 使用映射后的类名。这样结构清楚,冲突也可控。
课堂画面





代码示例
示例一:组件内样式 + 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)时的样式冲突。
示例二:第三方样式库(bootstrap.css)—— 方式一:link 引入
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()。
练习
- 用类组件实现非受控登录表单:
username、password、sex三项,点击保存时通过ref获取最新值;其中sex用find取出选中项的值。 - 给一个类组件添加
index.module.css,要求同时应用模块内的box、app和全局类名container,并避免类名冲突。 - 给定
todoList,分别用filter、map、reduce完成:筛选未完成事项、把所有事项标记为已完成、统计已完成数量。 - 给定
goodsList,用findIndex找到id === 2的商品并改价;再用map写一个“返回新数组、不改原数组”的版本,比较两者差异。 - 用
indexOf和includes判断购物车中是否存在某商品,并写出各自正确的判断条件。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 非受控组件表单操作 | 14:13 |
| 2 | 金山打字通如何练习手速 | 03:30 |
| 3 | forEach-filter-map-reduce | 31:31 |
| 4 | find-findIndex-some-every | 10:08 |
| 5 | 改变原数组方法及5个特殊方法 | 20:50 |
小结
- 表单先明确“受控”还是“非受控”,不要在同一元素上混用两套思路。
- 非受控表单的难点不在绑定,而在提交时通过
ref稳定取到最新值。 - 多个
radio的ref要按数组处理,先定位选中项,再取值。 - 组件内样式优先用 CSS Modules;多类名最终必须拼成一个字符串。
- 数组方法先看需求形态:筛选、等长映射、累计、查找、判断存在,再选方法。
reduce要重点关注回调返回值、初始值和最终返回值三者关系。- 判断
indexOf结果不要直接看真假,索引0是常见坑。