主题
自定义主题与下载工具包(Day12 1/3)
本页对应 React + TypeScript + 医通项目,共 7 段视频,约 1 小时 4 分。
学习目标
- 能正确使用 Table 组件的
rowKey、columns、dataSource配置表格,并通过render完成自定义列渲染。 - 能配置 Pagination 的
current、pageSize、total、showQuickJumper、showSizeChanger、pageSizeOptions、showTotal等属性并验证效果。 - 能使用状态与
onChange回调动态更新当前页和每页条数,实现可点击切换的分页功能。 - 能解释 TS 编译期检查与 Ajax 运行时数据之间的矛盾,并按“查接口、定义类型、封装 API 函数传泛型、调用并设置状态类型”的步骤发送请求。
- 能为请求参数和响应数据定义类型,正确使用 Axios 泛型传递响应体类型,并将请求结果渲染到 Table 中实现数据分页。
- 能完成自定义主题、路径别名和 nrm 的下载与配置,并验证配置生效。
知识点
本节要解决的问题
本节笔记只有两个关键问题:第一,项目中出现“当前环境不支持 less”的警告错误;第二,antd 默认的主题颜色是蓝色,而我们的应用需要使用绿色。笔记给出的解决方向是:使用 craco 及其插件来解决。
这里先纠正一个同音字:笔记里的“主体颜色”应理解为“主题颜色”。也就是说,这不是普通页面样式问题,而是 antd 组件库的主题变量问题。
需要说明的是,本节转写稿主要讲的是 Table 组件复习、pagination 分页配置、动态分页、TS 中发送 Ajax 请求以及 axios 结合 Table 实现分页,并没有展开“自定义主题”的具体安装命令和配置代码。因此下面只依据本节笔记给出的问题与解决方向扩写,不补造笔记和转写稿中没有出现的命令、插件名或配置项。
为什么需要自定义主题
antd 默认主题色是蓝色。如果我们的应用整体视觉是绿色,那么直接使用 antd 默认样式会出现两个明显问题:一是组件颜色与业务页面不一致,页面看起来像“拼”出来的;二是如果只用普通 CSS 去覆盖按钮、链接、选中态、悬浮态等颜色,覆盖面会很大,而且容易漏掉状态样式。
所以这里的“需要”不是单纯把某个按钮改绿,而是要让 antd 在编译阶段就按我们的主题变量生成样式。这样组件在不同状态下才能保持统一。与此同时,项目又出现了“当前环境不支持 less”的警告,这说明当前构建环境对 Less 的支持没有配置好。antd 的主题定制通常与 Less 变量相关,因此这个警告不能简单忽略,否则后续主题配置可能不生效。
怎么想:为什么选 craco 及插件
Create React App 默认把 webpack 等构建配置封装起来了,直接改构建配置并不方便。craco 的作用就是让我们在不执行 eject 的情况下扩展 CRA 配置。笔记中给出的方案是“使用 craco 及其插件来解决”,这个思路的核心是:不去改 node_modules 里的 antd 源码,也不用手写大量 CSS 去硬覆盖,而是通过构建层配置让 Less 支持和主题变量生效。
可以把它理解成两层:第一层,解决“当前环境不支持 less”的警告,让构建环境具备处理 Less 的能力;第二层,在构建配置中把 antd 的主题颜色指向应用需要的绿色。笔记没有展开具体插件名和配置字段,所以学习时不要把注意力放在背某个未出现的配置上,而要先明确:这个问题属于构建配置问题,不是单个组件样式问题。
怎么做:按笔记锁定处理路径
按照本节笔记,处理路径可以概括为:引入 craco,再配合其插件处理 Less 支持与 antd 主题定制。具体操作时,应先确认项目当前是否已经通过 craco 启动,而不是仍然使用默认的 CRA 启动方式;然后确认插件已经接入构建配置;最后再验证 antd 组件的主题色是否变为绿色,以及“不支持 less”的警告是否消失。
由于本节笔记和转写稿没有给出具体安装命令、插件名称和配置代码,这里不补写这些内容。实际练习时应以课程后续给出的配置为准。验证时不要只看某一个按钮,要同时看主按钮、链接、选中态、悬浮态等是否统一变成绿色;也不要只看控制台是否还有警告,还要看页面样式是否真正生效。
易错点
第一,把“当前环境不支持 less”的警告当成无关提示。它往往说明构建环境还没有正确处理 Less,后面主题配置可能失败。第二,只改普通 CSS,不去处理 antd 主题变量。这样容易造成状态样式遗漏,比如按钮悬浮、选中、禁用状态仍然是蓝色。第三,直接修改 node_modules 中的 antd 文件。这样做短期看似有效,但依赖重装后会丢失,不可维护。第四,随意 eject。笔记选择 craco 就是为了避免直接暴露和改动 CRA 配置。第五,接入 craco 后仍用原启动方式运行,导致配置没有生效。第六,改完配置不重启开发服务,误以为没有生效。第七,只验证一个组件,没有检查全局主题是否统一。
本节小结
本节要记住的不是一段未给出的配置代码,而是问题定位:antd 默认蓝色与应用绿色不一致,同时环境出现不支持 Less 的警告;解决方向是使用 craco 及其插件扩展构建配置,让 Less 支持和 antd 主题定制生效。学习时先分清“样式覆盖”和“主题配置”的区别,再按课程给出的具体配置落地,避免改源码、乱 eject、只改单个组件等易错做法。
本节要解决什么:先装工具包,再谈配置
本节笔记的主题很明确:下载并安装 @craco/craco 与 craco-less。为什么需要先做这一步?因为后续如果要在 CRA 项目里使用 Less、扩展 webpack 配置,通常不希望直接 eject,而是借助 craco 这类工具接管配置。工具包没装,后面的配置和导入都无从谈起。怎么想?先把“构建期依赖”准备好,再写业务代码;不要一边写页面一边猜为什么 Less 不生效。怎么做?按笔记原样执行安装命令:
shell
yarn add @craco/craco
yarn add craco-less -D
npm i @craco/craco -f
npm i craco-less -D -f # --save-dev易错点:-D 和 --save-dev 是同一类含义,表示开发依赖;-f 是 --force,不是“开发依赖”的缩写。不要把两个参数混用。转写稿里有一句很典型的调试顺序:“这个我没装啊,所以我们现在得装一下。”这提醒我们:代码里出现 import 报错时,先确认依赖是否真的安装,而不是先怀疑代码逻辑。
安装 @craco/craco:为什么先装它
为什么需要?craco 用来在不 eject 的情况下扩展 CRA 配置,属于项目构建工具链的一环。怎么想?先把它作为项目依赖安装,再在后续配置中引用;如果安装失败,先看网络、包名、lockfile 和 npm 依赖树,不要直接改业务代码。怎么做?执行:
shell
yarn add @craco/craco易错点:包名是 @craco/craco,有作用域前缀,不能写成 craco;安装后要检查 package.json 是否出现对应依赖。转写稿里安装 axios 时也是先“安装一下”,再在代码里导入使用;如果没安装就直接写导入,编译阶段就会报错。这个“先安装、再导入验证”的顺序同样适用于本节。
安装 craco-less:为什么放到开发依赖
为什么需要?craco-less 是配合 craco 处理 Less 的插件,主要在构建/开发阶段起作用,所以笔记把它放在开发依赖里。怎么想?运行时业务代码不一定直接依赖它,但构建配置需要它;因此用 -D 或 --save-dev 更符合依赖分类。怎么做?执行:
shell
yarn add craco-less -D易错点:不要漏掉包名中的 craco-;不要把它和 less 本身混淆——本节笔记只给了 craco-less 的安装命令,没有展开 less 的安装与配置,所以这里不编造额外步骤。安装后同样要检查 package.json 的 devDependencies 是否出现它。
npm 报错时再用 --force:调试顺序不能反
为什么需要?npm 安装可能因为依赖树冲突、缓存、版本不匹配等失败。怎么想?先看报错信息,确认是不是“没装”“版本冲突”“网络失败”,再决定是否强制安装;--force 是应急手段,不是首选。怎么做?按笔记执行:
shell
npm i @craco/craco -f
npm i craco-less -D -f # --save-dev易错点:-f 可能掩盖真实冲突,团队项目里强制安装后要同步 lockfile,避免别人安装结果不一致;-D -f 的顺序不要写反。转写稿里“npm 安装如果报错,加 --force 强制安装”对应的就是这种调试思路:先确认没装,再安装;安装失败再考虑强制,而不是一上来就强制。
安装后的验证:装了不等于生效
为什么需要?只把包下载下来,不代表项目已经接入 craco 配置。怎么想?先做最小验证:看 package.json 是否有依赖、node_modules 是否出现包、代码里能否正常导入。怎么做?可以检查 package.json,再在后续配置中按项目约定接入;本节笔记没有给出 craco.config.js 的具体内容,所以不在这里编造配置细节。易错点:只安装不配置,craco 不会自动接管;如果项目启用了 TypeScript,后续导入 Less 等资源时若类型检查报错,要按项目约定补类型声明,而不是误以为安装命令写错了。
转写稿里关于 TS“编译阶段检查”的讨论也说明一个道理:依赖和类型没准备好,编译阶段就会拦你;先把安装和验证做扎实,再进入 Table、分页、axios 等后续内容,调试会清晰很多。
本节配置要解决什么
默认 CRA 项目里,start/build/test 直接调用 react-scripts,不方便插入自定义 webpack/less 配置;而 Ant Design 默认主题色又需要通过 less 变量覆盖。整体思路是:用 craco 接管脚本入口,用 craco-less 让 less 变量覆盖生效,再把样式入口从 antd.css 换成 antd.less,最后重启脚手架验证。转写稿里反复强调“先写死看效果,再换成动态值,并通过控制台/页面刷新验证”,配置调试也应遵循这个顺序:先改入口,再改配置,再换样式入口,最后重启看主色是否变化。
修改 package.json 的 scripts
为什么需要:CRA 默认脚本直接执行 react-scripts,不会读取 craco.config.js;只有把启动、构建、测试入口换成 craco,后面的 less 插件配置才有机会生效。
怎么想:不改变 start/build/test 三个脚本的语义,只替换命令执行者,让它们都走 craco。
怎么做:把原来的三条命令替换为:
js
"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
+ "start": "craco start",
+ "build": "craco build",
+ "test": "craco test",
}易错点:不要只改 start 漏掉 build/test;脚本名要保持不变;如果 craco 相关依赖或配置文件缺失,启动会直接报错。改完后不要继续用旧进程,要重启。
新建 craco.config.js 并接入 craco-less
为什么需要:Ant Design 的主题变量需要通过 less 编译期覆盖,而 CRA 默认不暴露 less loader 配置,craco-less 负责把 less 能力接入进来。
怎么想:在 plugins 中注册 CracoLessPlugin,在 lessLoaderOptions.lessOptions 里用 modifyVars 覆盖 @primary-color;javascriptEnabled 用于让 less 处理 antd 中的 JS 表达式。
怎么做:新建 craco.config.js:
js
const CracoLessPlugin = require('craco-less');
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
/*
* 覆盖内置的样式变量
* https://ant-design.antgroup.com/docs/react/customize-theme-cn
*/
lessOptions: {
modifyVars: { '@primary-color': '#1DA57A' },
javascriptEnabled: true,
},
},
},
},
],
};易错点:modifyVars 的键名要带 @;javascriptEnabled 要放在 lessOptions 里;require('craco-less') 若报找不到模块,先检查课程依赖是否已准备好,不要随意改成别的插件名。
把样式入口换成 antd.less
为什么需要:antd.css 是预编译好的 CSS,modifyVars 不会对它生效;只有引入 antd.less,变量覆盖才会参与编译。
怎么想:只替换样式入口,不动业务代码。
怎么做:
js
// import 'antd/dist/antd.css'
import 'antd/dist/antd.less'易错点:不要两行同时保留;路径大小写要正确;改完样式入口后必须重启,热更新不一定重新读取配置。
重启脚手架并验证
为什么需要:craco.config.js、package.json 脚本和 less 入口通常在启动阶段读取,不重启可能仍走旧配置。
怎么想:先停掉当前开发服务,再重新执行启动命令;观察终端是否报 craco/less 相关错误,再看页面主色是否变成 #1DA57A。
怎么做:保存以上文件,停止旧进程,重新启动脚手架。
易错点:不要只保存不重启;不要忽略终端第一条报错;若主色没变,按“脚本入口 → 配置文件 → less 入口 → 重启”顺序排查。转写稿中出现的 rowKey、render、泛型、axios、pageSize、total、showQuickJumper、showSizeChanger、pageSizeOptions、showTotal 等同音字,写代码时必须按官方 API 拼写,不能把同音字带进配置或代码里。
本节要解决的问题
本节笔记只有两个核心问题:一是自定义模块引入时相对路径太深,出现多个 ../;二是使用路径别名后,TypeScript 检查会报错。转写稿主要讲 Table、分页和 axios,未包含路径别名的具体操作记录,因此下面不把无关内容硬塞进本节,只依据笔记中的两个问题展开推导与调试思路,不虚构命令输出。
问题1:相对路径太麻烦,用模块路径别名
为什么需要:在项目里引入自定义模块时,如果文件层级较深,常见写法会变成 ../../../../components/xxx。路径越长,越容易数错层级;文件移动后还要批量改路径,维护成本高。
怎么想:笔记给出的方向是“使用模块路径别名来处理”,并明确这是 webpack 的技术;在 CRA + craco 项目里,需要通过 craco 的 craco-alias 插件实现。也就是说,别名不是只写一个字符串,而是要让构建工具知道“某个别名指向哪个目录”。
怎么做:项目已经使用 craco 的前提下,安装 craco-alias 插件;在项目根目录的 craco.config.js 中注册插件并配置 alias。配置结构示意如下,实际要合并进项目已有配置,不能直接覆盖:
js
// craco.config.js 结构示意
const CracoAlias = require('craco-alias')
module.exports = {
plugins: [
{
plugin: CracoAlias,
options: {
source: 'tsconfig',
baseUrl: './src',
tsConfigPath: './tsconfig.json'
}
}
]
}易错点:不要只改 tsconfig.json 就以为构建一定成功;webpack 侧和 TS 侧是两套解析。别名键名、目标目录、baseUrl 要一致;配置后通常需要重启开发服务。最容易忽略的是笔记特别强调的:一定要打开项目根目录。如果编辑器只打开了 src 子目录,tsconfig.json、craco.config.js 可能不在编辑器识别的项目根上,TS 路径映射和插件配置就容易失效。
问题2:使用别名引入模块时 TS 检查报错
为什么需要:webpack 配好别名后,代码可能能构建,但 TypeScript 仍按自己的规则解析模块。TS 不认识这个别名,就会在编辑器或类型检查阶段报“找不到模块”。这不是别名“没生效”,而是 TS 侧还没被告知同样的路径映射。
怎么想:要把“构建解析”和“类型检查解析”分开看。webpack 负责打包时找文件,TS 负责编译检查和编辑器提示。两边都配置,才能既运行通过又不报类型错误。
怎么做:在项目根目录的 tsconfig.json 中配置 compilerOptions.baseUrl 和 compilerOptions.paths,让 TS 知道别名指向哪里。结构示意:
json
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["*"]
}
}
}配置后重启编辑器 TS 服务或开发服务,再用别名导入验证:构建是否通过、TS 是否还报错、跳转是否能定位到目标文件。
易错点:paths 的键和值要与 webpack alias 对齐;不要漏掉 baseUrl;通配符 /* 的位置要写对;修改后不重启可能仍看到旧报错;不要覆盖项目原有 tsconfig.json 配置,应合并。若出现“运行正常但 TS 报红”,优先检查 TS 侧;若“TS 正常但构建失败”,优先检查 craco/webpack 侧。
小结
本节的关键不是死记某个配置,而是建立“双端对齐”的意识:craco-alias 解决 webpack 构建时的别名解析,tsconfig.json 解决 TypeScript 的路径识别。操作时始终打开项目根目录,配置后重启并分别验证构建与 TS 检查,才能避免“只配一半”的典型问题。
本节笔记的范围说明
本节笔记标题是“4-1. 下载”,正文只给出一条安装命令:
shell
yarn add craco-alias -D需要先说明:提供的转写稿主要讲的是 Table 组件复习、pagination 分页配置、TS 中发送 axios 请求、定义响应类型以及 axios 结合 Table 实现分页,并没有出现 craco-alias 的安装推导、调试过程或相关配置。因此,本节不能把转写稿里的 Table、分页、axios 内容硬套到“下载 craco-alias”上,否则就是编造。下面只围绕这条笔记命令本身,讲清楚“为什么需要 → 怎么想 → 怎么做 → 易错点”,并明确哪些内容本节没有给出、不能擅自补写。
为什么需要这条安装命令
为什么需要:项目里要使用某个第三方包时,必须先把它安装到项目依赖中,否则代码里无法正确解析该包。本节命令安装的是 craco-alias,并且通过 -D 把它作为开发依赖安装。
怎么想:先判断这个包是运行时依赖还是开发期工具。craco-alias 从名称和常见用途看,是配合 CRACO 做别名配置的工具,通常属于开发构建阶段的依赖,所以笔记里使用了 -D。但要注意,本节笔记没有给出后续 craco.config.js 的配置代码,也没有说明别名规则,因此不能在这里补写具体配置。
怎么做:在项目根目录执行笔记中的命令:
shell
yarn add craco-alias -D执行后应检查 package.json 中是否出现 devDependencies 下的 craco-alias,同时确认 yarn.lock 是否更新。
易错点:不要把 -D 漏掉;不要在错误目录执行;不要 npm 和 yarn 混用导致锁文件混乱;包名不要拼错。安装成功不等于配置生效,若项目还需要 CRACO 配置,必须等后续笔记给出配置步骤后再做。
命令拆解:yarn add 与 -D
为什么需要:理解命令结构,才能知道它改动了什么文件、出了问题该查哪里。
怎么想:yarn add 表示向当前项目添加依赖;craco-alias 是包名;-D 是 --dev 的简写,表示写入开发依赖。开发依赖通常不参与生产运行,但参与开发、构建或工具链。
怎么做:执行命令后,重点看两个结果:一是终端是否提示成功;二是 package.json 的 devDependencies 是否新增 craco-alias。如果项目使用 yarn,还应看到 yarn.lock 更新。
易错点:-D 的位置一般放在包名后即可;不要把开发依赖误装成普通依赖;如果终端报网络错误,应先检查 registry 和网络,而不是反复改命令;如果报权限错误,不要直接滥用管理员权限,应先确认项目目录和缓存权限。
安装后的验证与边界
为什么需要:安装命令只是第一步,验证能避免“以为装好了,实际没写入依赖”的问题。
怎么想:验证分三层:终端是否成功、package.json 是否记录、node_modules 是否存在对应包。三层都确认后,才说明安装动作完成。
怎么做:打开 package.json 查看 devDependencies;必要时查看 yarn.lock 中是否有对应条目;不要只看终端一行输出就结束。
易错点:本节没有给出 craco-alias 的使用代码和别名配置,所以不能根据本节笔记推导出“安装后立即生效”。转写稿里出现的“安基体站”应理解为 Ant Design,“excus/exus”应理解为 axios,“饭型/发型”应理解为泛型,“详应”应理解为响应,“收 quick jumper”应理解为 showQuickJumper,但这些都属于转写稿中 Table 与 axios 部分,不是本节 craco-alias 安装的内容。学习本节时,应把范围限定在“下载依赖”这一动作上,后续配置等笔记明确给出后再继续。
先想清楚:这两个配置解决的是同一件事的两半
在 src 目录一深,import Table from "../../../components/Table" 这种路径就开始满天飞。改一次目录结构,几十个文件的相对路径全要跟着改,这是最典型的维护成本。于是我们想用 @/components/Table 这种“从 src 出发”的绝对写法。
但 React + TS 的项目里,一条 @/xxx 要同时被两套工具认识:TS 的类型检查器要靠它找到模块、推断类型;webpack(CRA 底层) 要靠它真正打包到文件。少配一边,结果就是要么编辑器飘红、要么编译报错。所以本节的做法是:把别名规则单独写进 tsconfig.extend.json,让 TS 和 craco 插件共用同一份声明,避免两处维护、两处不一致。
tsconfig.extend.json:别名的“唯一事实来源”
在项目根目录下新建 tsconfig.extend.json:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@api/*": ["./src/api/*"],
"@assets/*": ["./src/assets/*"],
"@comps/*": ["./src/components/*"],
"@utils/*": ["./src/utils/*"],
"@pages/*": ["./src/pages/*"]
}
}
}baseUrl 是解析基准,写 "." 表示以本文件所在目录(项目根)为基准;paths 里每个 key 都是“带 /* 的前缀”,值是一个数组,用 ["./src/*"] 把通配部分接回去。为什么要拆出 @api、@comps、@pages 这些细分前缀,而不是只用 @/*?因为转写稿里讲到发请求那部分时明确提过:src/api 目录专门放封装请求的函数,里面还有 model 目录用来定义请求参数和响应数据的类型。这类目录引用频率高、层级也深,单独给一个短前缀,写起来更短、读起来也更快知道这是“接口层”还是“组件层”。
易错点:baseUrl 不要写成 "./src",否则 paths 里的 ./src/* 就变成了 ./src/src/*;paths 的 key 末尾的 /* 不能漏,漏了就只能匹配完全相等的模块名。
craco.config.js:让 webpack 也认这套别名
TS 认识 @/ 之后,代码不再飘红,但一运行仍然会报“模块找不到”——因为 webpack 不知道这回事。这里用 craco-alias 读我们刚写的 tsconfig 文件,把规则同步给 webpack:
js
const CracoLessPlugin = require("craco-less");
const CracoAlias = require("craco-alias");
module.exports = {
plugins: [
// 自定义主题
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { "@primary-color": "#1DA57A" },
javascriptEnabled: true,
},
},
},
},
// 路径别名
{
plugin: CracoAlias,
options: {
source: "tsconfig",
baseUrl: "./",
tsConfigPath: "./tsconfig.extend.json",
},
},
],
// 开发服务器配置
devServer: {
// 激活代理服务器
proxy: {
// 将来以/dev-api开头的请求,就会被开发服务器转发到目标服务器去。
"/dev-api": {
// 需要转发的请求前缀
target: "http://syt-api.atguigu.cn", // 目标服务器地址
changeOrigin: true, // 允许跨域
pathRewrite: {
// 路径重写
"^/dev-api": "",
},
},
},
},
};三个 options 各有分工:source: "tsconfig" 告诉插件“规则来源是 tsconfig 文件”而不是别的格式;baseUrl: "./" 是插件自己解析路径的基准;tsConfigPath 指向我们那份扩展文件。易错点:source 的值必须是 "tsconfig" 这个字符串,写成 "ts" 或 "typescript" 都不生效;tsConfigPath 建议带 ./,且改完配置一定要重启 dev server,热更新不会重读 craco 配置。
同一个文件里还挂了 CracoLessPlugin。因为项目用 antd,默认主色是蓝的,想换主题色不能去改 node_modules 里的样式,而是通过 modifyVars 覆盖 less 变量——注意 key 要写成 "@primary-color",@ 不能丢;javascriptEnabled: true 是因为 less 3.x 之后默认关闭了 JS 表达式,antd 的 less 源码需要它。
proxy:绕开开发阶段的跨域
前端跑在 localhost:3000,接口在 http://syt-api.atguigu.cn,浏览器同源策略直接拦。开发阶段最省事的办法是让 dev server 做中转:页面里统一以 /dev-api 开头发请求,dev server 看到这个前缀就替你转发到 target,浏览器眼里请求始终是同源的,跨域问题消失。
changeOrigin: true 的作用是把转发请求的 Host 头改成目标服务器的域名,否则有些后端会按 Host 做校验而拒绝。pathRewrite 用正则 "^/dev-api" 把前缀抹掉——因为真实接口地址里没有这一段。易错点:changeOrigin 是 Origin 不是 Origins,拼错不报错但代理行为会不对;pathRewrite 的 key 是正则字符串,写成 "/dev-api" 也能匹配,但容易误伤路径中间出现的同名片段,用 ^ 锚定开头更稳;另外代理只在 npm start 的开发服务器生效,打包后的静态资源没有这层中转,上线要靠 Nginx 之类的反向代理。
tsconfig.json:用 extends 把配置接回去
js
{
"extends":"./tsconfig.extend.json"
}CRA 生成的 tsconfig.json 是 TS 默认读取的文件,我们不想把别名规则塞进去破坏它原有的结构,就用 extends 继承。extends 的语义是合并:父文件的 compilerOptions 会被并进来,子文件里同名项优先。所以别名规则写在扩展文件里、由主文件继承,两边都能用。
易错点:extends 的路径是相对当前文件的,"./tsconfig.extend.json" 的扩展名不要漏;继承之后如果 paths 不生效,先确认你改的是被 extends 的那份文件,而不是又往主文件里补了一遍互相冲突的规则。
到这里,一条 import { getUsers } from "@api/getUser" 才能在编辑器里不飘红、在浏览器里也能真的加载到文件。后面写接口层、写页面时,就再也不用数 ../ 的层数了。
Table组件与columns配置
为什么需要:表格要把后端列表数据按列展示,还要能唯一标识每一行。怎么想:Table 的 rowKey 指定唯一字段,columns 配列,dataSource 配数据源。怎么做:注意 rowKey、columns、dataSource 是 Table 的属性,不要塞到标签内容里,否则会报错且难定位。
tsx
<Table
rowKey="id"
columns={columns}
dataSource={users}
/>columns 每列重点掌握 title、dataIndex、render:title 是列名,dataIndex 决定取数据源中哪个字段;需要自定义渲染时用 render,它返回什么,这一列就渲染什么。render 第一个参数受 dataIndex 影响:配了 dataIndex 时 value 是该字段值;没配时 value 就是当前行对象 row。第二、三个参数是当前行和索引。因为用了 TS,列配置要写类型,且泛型传“数组每一项的对象类型”,不是数组类型。
tsx
const columns: ColumnsType<IUserItem> = [
{ title: 'ID', dataIndex: 'id' },
{ title: '用户名', dataIndex: 'login' },
{
title: '头像',
render: (row: IUserItem) => (
<a href={row.html_url}>
<img src={row.avatar_url} style={{ width: 100 }} />
</a>
),
},
];易错点:rowKey 必须选不重复字段;columns 类型泛型别传成数组;头像这类要放图片时不能只写 dataIndex,要用 render 返回 img。
分页配置与动态分页
为什么需要:列表数据多时要分页。怎么想:先看 Table 的 pagination 配置,false 不显示分页器,对象形式才可配置。怎么做:常用 current 当前页、pageSize 每页条数、total 总条数;showQuickJumper 显示快速跳页,showSizeChanger 显示每页条数下拉框,pageSizeOptions 配下拉选项,showTotal 自定义总条数描述。注意 pageSizeOptions 建议和 pageSize 保持一致。
tsx
pagination={{
current,
pageSize,
total,
showQuickJumper: true,
showSizeChanger: true,
pageSizeOptions: [2, 3, 5, 10],
onChange: (page, size) => {
setCurrent(page);
setPageSize(size);
},
}}只写静态值时分页点不动,因为状态没变。要把 current、pageSize、total 做成 useState,并在 onChange 中拿到最新页码和每页条数去更新状态。易错点:onChange 不更新状态;total 不跟接口数据长度同步;showSizeChanger 与 pageSizeOptions 不配合。
TS中发送Ajax:先定义响应类型
为什么需要:TS 在编译阶段检查,而 Ajax 数据只有执行后才知道结构,所以会先报“res 上不存在 items”。怎么想:先打印确认 items 确实在响应里,然后提前定义响应类型,告诉 TS 未来返回什么。怎么做:在 model 目录定义类型,在 API 函数里通过 get 的第二个泛型传响应体类型。
ts
export interface IUserItem {
id: number;
login: string;
avatar_url: string;
html_url: string;
}
export type IUserList = IUserItem[];
export interface IUserResponse {
items: IUserList;
}ts
export function getUsers(keyword: string) {
return request.get<any, IUserResponse>('/search/users', {
params: { q: keyword },
});
}组件里设置状态也要传泛型:useState<IUserList>([])。易错点:把 IUserList 传给响应泛型而不是 IUserResponse,导致 res.items 仍报错;接口属性名不能随便改,要和真实数据一致。
axios结合Table实现分页
为什么需要:真实项目是“请求数据→渲染表格→分页”。怎么做:axios.create 配 baseURL、timeout,响应拦截器简化数据;API 层封装请求函数;组件挂载后请求,setUsers(res.items),同时 setTotal(res.items.length);最后把 users 给 dataSource,把分页状态和 onChange 给 pagination。易错点:total 不要写死;分页变化只改状态不重新请求时要明确当前是前端分页;接口类型、状态类型、列类型要串起来,否则 TS 会连续报错。
需要先说明:本节标题是“nrm使用”,但转写稿实际讲的是 Table 组件分页、TS 中发送 axios 请求,以及 axios 结合 Table 渲染分页,全文没有出现 nrm 的安装、源切换、命令等内容。为避免编造,下面按转写稿实际内容整理,不补写 nrm 用法;转写稿中也没有可保留的图片,因此不插入图片。
Table 组件的基本配置
为什么需要:表格要渲染列表数据,必须把“行唯一标识、列配置、数据源”正确传给 Table 组件。怎么想:先明确 Table 是组件调用,不是把配置塞到标签内容里;再分别配置 rowKey、columns、dataSource。怎么做:rowKey 指定数据中唯一不重复字段,转写稿用 id;columns 配列名和取数字段;dataSource 放列表数据。列配置至少掌握 title、dataIndex、render。render 的第一个参数受 dataIndex 影响:配了 dataIndex,value 是该字段值;没配时 value 是当前行对象 row。易错点:把 rowKey、columns、dataSource 写到标签内容里会报错;TS 下 columns 要传泛型,泛型是“数组每一项的对象类型”,不是数组类型。
tsx
<Table
rowKey="id"
columns={columns}
dataSource={users}
/>
const columns: ColumnsType<IUserItem> = [
{ title: 'ID', dataIndex: 'id' },
{ title: '用户名', dataIndex: 'login' },
{
title: '头像',
render: (row: IUserItem) => (
<a href={row.html_url}>
<img src={row.avatar_url} style={{ width: 100 }} />
</a>
)
}
]pagination 分页配置
为什么需要:列表数据多时要分页显示。怎么想:先看 Table 的 pagination 配置项,它可以是对象,也可以是 false。false 表示不显示分页器;对象形式才配置页码、每页条数、总条数。怎么做:current 是当前页,pageSize 是每页条数,total 是总条数;showQuickJumper 显示快速跳页输入框;showSizeChanger 显示每页条数下拉框,并配合 pageSizeOptions 配选项。易错点:pageSizeOptions 的第一项建议与 pageSize 保持一致;只写死配置时,分页器能显示但点不动,因为状态没有变化。
tsx
<Table
pagination={{
current,
pageSize,
total,
showQuickJumper: true,
showSizeChanger: true,
pageSizeOptions: [2, 3, 5, 10],
onChange: (page, size) => {
setCurrent(page)
setPageSize(size)
}
}}
/>动态分页与 onChange
为什么需要:静态配置只能展示,不能真正切页。怎么想:把 current、pageSize、total 变成状态;分页变化时用 onChange 拿到最新页码和每页条数,再更新状态。怎么做:用 useState<number> 定义三个状态,onChange 的两个参数都是数字,TS 中标注 number。易错点:只配 pagination 不写 onChange,页码和每页条数不会更新;total 应根据请求回来的列表长度设置,而不是永远写死。
tsx
const [current, setCurrent] = useState<number>(1)
const [pageSize, setPageSize] = useState<number>(3)
const [total, setTotal] = useState<number>(0)TS 中发送 axios 请求与定义类型
为什么需要:TS 在编译阶段检查类型,而 axios 响应数据只有执行后才回来,两者存在矛盾。怎么想:先看接口,确定请求方式、URL、参数、响应数据结构;再在 model 目录定义响应类型;最后通过 axios 泛型把响应类型传给请求方法。怎么做:定义 IUserItem、IUserList、IUserResponse,请求方法第二个泛型传响应体类型。易错点:不定义类型就访问 res.items 会报“属性不存在”;泛型传错会把数组类型和响应对象类型混淆;状态也要传泛型,如 useState<IUserList>([])。
ts
interface IUserItem {
id: number
login: string
avatar_url: string
html_url: string
}
type IUserList = IUserItem[]
interface IUserResponse {
items: IUserList
}
export const getUsers = (keyword: string) =>
request.get<any, IUserResponse>('/search/users', {
params: { q: keyword }
})axios 封装与请求调用
为什么需要:统一基础地址、超时和响应处理,避免每个请求重复写。怎么想:一般建 request 或 http 文件,创建 axios 实例,配响应拦截器简化返回数据;API 目录封装请求函数;组件在 useEffect 中调用。怎么做:request.interceptors.response.use(res => res.data),组件里 await getUsers('aa') 后 setUsers(res.items),并把 res.items.length 设置给 total。易错点:请求拦截器不是本节必需;响应拦截器要返回简化后的数据;接口返回属性多但类型只定义用到的字段不会报错,因为 TS 只在编译阶段检查,执行后多出的属性已不再检查。
课堂画面






代码示例
示例一:用 craco 自定义 antd 主题(解决 less 警告 + 主色改绿)
package.json(只改 scripts 部分)
json
{
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test"
}
}craco.config.js(项目根目录新建)
js
const CracoLessPlugin = require('craco-less');
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
/*
* 覆盖内置的样式变量
* https://ant-design.antgroup.com/docs/react/customize-theme-cn
*/
lessOptions: {
modifyVars: { '@primary-color': '#1DA57A' },
javascriptEnabled: true,
},
},
},
},
],
};src/index.tsx
tsx
// import 'antd/dist/antd.css' // 注释掉:css 里变量是写死的,改不动主题
import 'antd/dist/antd.less' // 改成 less,才能让 modifyVars 生效安装命令
shell
yarn add @craco/craco
yarn add craco-less -D
# npm 安装报错时加 --force 强制安装
npm i @craco/craco -f
npm i craco-less -D -f关键行 / 易错点
- 三个脚本必须全部换成
craco xxx,只要漏一个,用react-scripts起服务就还是原配置,主题不会变。 index.tsx里antd.css必须注释掉换成antd.less,两行同时存在时 css 会覆盖 less 的变量结果。javascriptEnabled: true不能少,less 编译 antd 源码时需要它。- 改完配置必须重启脚手架,热更新不会重新读
craco.config.js。
示例二:路径别名(craco-alias + tsconfig.extend.json)
tsconfig.extend.json(项目根目录新建)
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@api/*": ["./src/api/*"],
"@assets/*": ["./src/assets/*"],
"@comps/*": ["./src/components/*"],
"@utils/*": ["./src/utils/*"],
"@pages/*": ["./src/pages/*"]
}
}
}craco.config.js(完整版:主题 + 别名 + 代理)
js
const CracoLessPlugin = require("craco-less");
const CracoAlias = require("craco-alias");
module.exports = {
plugins: [
// 自定义主题
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { "@primary-color": "#1DA57A" },
javascriptEnabled: true,
},
},
},
},
// 路径别名
{
plugin: CracoAlias,
options: {
source: "tsconfig",
baseUrl: "./",
tsConfigPath: "./tsconfig.extend.json",
},
},
],
// 开发服务器配置
devServer: {
proxy: {
"/dev-api": {
target: "http://syt-api.atguigu.cn", // 目标服务器地址
changeOrigin: true, // 允许跨域
pathRewrite: { "^/dev-api": "" }, // 路径重写
},
},
},
};tsconfig.json
json
{
"extends": "./tsconfig.extend.json"
}安装命令
shell
yarn add craco-alias -D关键行 / 易错点
- 别名是两份配置:webpack 端靠
craco-alias(source: "tsconfig"+tsConfigPath),TS 检查端靠tsconfig.json的extends。只配一边,要么运行时报找不到模块,要么编辑器飘红。 tsconfig.extend.json必须在项目根目录,tsConfigPath和baseUrl都是相对根目录写的./。paths里的键必须以/*结尾、值必须是数组形式["./src/*"],写错一个字符别名就失效。tsconfig.json里用extends继承,不要直接把paths抄一遍,否则两份配置容易不同步。
示例三:TS 中发 ajax 请求 + Table 分页(类型定义是关键)
src/utils/request.ts
ts
import axios from 'axios'
const request = axios.create({
baseURL: 'https://api.github.com',
timeout: 20000,
})
// 请求拦截器
request.interceptors.request.use((config) => {
return config
})
// 响应拦截器:简化服务器响应回来的数据
request.interceptors.response.use((response) => {
return response.data
})
export default requestsrc/api/model/github.types.ts
ts
// 数组里每一项(对象)的类型
export interface IUserItem {
id: number
login: string
avatar_url: string
html_url: string
}
// 用户列表(数组)类型
export type IUserList = IUserItem[]
// 获取用户列表的响应结果类型
export interface IUserResponse {
items: IUserList
total_count: number
}src/api/github.ts
ts
import request from '@/utils/request'
import type { IUserResponse } from './model/github.types'
export const getUsers = (keyword: string) => {
// 第二个泛型 = 响应体类型
return request.get<any, IUserResponse>('/search/users', {
params: { q: keyword },
})
}src/pages/AxiosTable.tsx
tsx
import { useEffect, useState } from 'react'
import { Table, Image } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { getUsers } from '@/api/github'
import type { IUserItem } from '@/api/model/github.types'
const AxiosTable = () => {
// 表格数据 + 分页相关状态
const [users, setUsers] = useState<IUserItem[]>([])
const [current, setCurrent] = useState<number>(1)
const [pageSize, setPageSize] = useState<number>(3)
const [total, setTotal] = useState<number>(0)
// 泛型传的是数组里每一项(对象)的类型,不是数组类型
const columns: ColumnsType<IUserItem> = [
{ title: 'ID', dataIndex: 'id' },
{ title: '用户名', dataIndex: 'login' },
{
title: '头像',
// 没有配 dataIndex 时,render 第一个参数就是当前行 row
render: (row) => (
<a href={row.html_url} target="_blank" rel="noreferrer">
<Image src={row.avatar_url} width={100} />
</a>
),
},
]
useEffect(() => {
const load = async () => {
const res = await getUsers('aa')
setUsers(res.items) // res 现在有 items 属性,TS 不报错
setTotal(res.items.length) // 总条数
}
load()
}, [])
return (
<Table
rowKey="id"
columns={columns}
dataSource={users}
pagination={{
current,
pageSize,
total,
showQuickJumper: true,
showSizeChanger: true,
pageSizeOptions: [2, 3, 5, 10], // 建议第一项与 pageSize 保持一致
onChange: (page: number, size: number) => {
setCurrent(page)
setPageSize(size)
},
}}
/>
)
}
export default AxiosTable关键行 / 易错点
request.get<any, IUserResponse>:泛型第二个参数才是响应体类型,第一个必须占位(原来默认是any,就写any);不传泛型,res.items一定会报“不存在属性 items”。- TS 是编译期检查、数据是运行期才回来,两者天然矛盾,所以必须先定义
IUserResponse告诉 TS“回来的东西长这样”。接口里只定义你要用的字段即可(id / login / avatar_url / html_url)。 ColumnsType<IUserItem>的泛型是数组每一项的对象类型,不是IUserItem[],传错类型 TS 会立刻飘红。useState<IUserItem[]>([])必须给泛型,否则setUsers(res.items)会报“不能将数组分配给 never[]”。Table的rowKey、columns、dataSource是属性,写到<Table>标签体里会各种报错还找不到原因。- 分页要“能点”,必须配
onChange拿到最新page / size去更新状态;只写current/pageSize/total是死的,点不动。 pagination传false就不显示分页器;传对象才生效。
常见错误
1. 启动后终端提示 less 相关警告,主题色仍是默认蓝
- 现象:终端出现 less / Inline JavaScript 警告,页面按钮颜色没变。
- 原因:入口仍然
import 'antd/dist/antd.css'(编译后的 CSS 里颜色已写死),或lessOptions里漏了javascriptEnabled。 - 修法:改为
import 'antd/dist/antd.less',并在lessOptions中加javascriptEnabled: true,然后重启脚手架。
2. 改完 craco.config.js 毫无反应
- 现象:
modifyVars里的颜色怎么改都不生效,别名怎么加都找不到。 - 原因:craco 配置只在 dev server 启动时读取一次,热更新不会重新加载配置文件。
- 修法:
Ctrl+C停掉进程,重新yarn start。
3. npm i @craco/craco 报 ERESOLVE 依赖冲突
- 现象:安装中断,提示 peer dependency 冲突。
- 原因:npm 7+ 严格校验 peer 依赖,与 CRA 内置的 react-scripts 版本对不上。
- 修法:
npm i @craco/craco -f(或改用yarn add),装完确认package.json的 scripts 已换成craco start/build/test。
4. 别名 @/xxx 运行时 Module not found
- 现象:TS 编辑器不报红,但浏览器/终端编译报找不到模块。
- 原因:只在
tsconfig.extend.json里写了paths,那只影响 TS 类型检查,webpack 并不认识。 - 修法:在
craco.config.js里加CracoAlias插件,source: 'tsconfig'、tsConfigPath: './tsconfig.extend.json',并在tsconfig.json里extends它。
5. 别名能跑通,但编辑器一直红线"找不到模块"
- 现象:页面正常渲染,但每个
@/引入都有红色波浪线。 - 原因:反过来——只配了 craco-alias,没给 TS 声明
baseUrl+paths。 - 修法:补
tsconfig.extend.json的compilerOptions.baseUrl/paths,并确认tsconfig.json已extends(注意文件要放在项目根目录)。
6. Table 把 columns、dataSource 写在 <Table>...</Table> 标签中间
- 现象:表格空白,控制台报属性不存在 / 各种莫名错误。
- 原因:
rowKey、columns、dataSource是 Table 的 props,不是子节点。 - 修法:写成
<Table rowKey="id" columns={columns} dataSource={list} />自闭合标签。
7. TS 中 res.items 报"类型上不存在属性 items"
- 现象:请求明明成功,
console.log(res)能看到 items,一写res.items就报错。 - 原因:TS 在编译期检查,此时还没有真实响应数据;axios 的返回值类型没告诉它响应体长什么样。
- 修法:在
src/api/model下定义响应体 interface,调用时把响应体类型作为 第二个泛型 传给请求方法:request.get<any, IUserResponse>(url, { params })。
8. useState([]) 存列表数据后赋值报 never[]
- 现象:
setUsers(res.items)报"不能将 X 分配给 never[]"。 - 原因:空数组初始化时 TS 推断为
never[],未显式声明元素类型。 - 修法:
useState<IUserItem[]>([])。
9. 分页器能显示但点击无反应
- 现象:页码、每页条数都渲染出来了,点了不变。
- 原因:
current/pageSize是写死的字面量,且没配onChange去更新 state。 - 修法:
current、pageSize用 state 驱动,并配onChange={(page, size) => { setCurrent(page); setPageSize(size); }}。
练习
主题验证:把主色从
#1DA57A改成#722ED1,改完先不重启观察现象,再重启观察现象,用一句话解释差异原因。别名双向验证:新增
@hooks/*别名,在某个组件里用import useXxx from '@hooks/useXxx',要求同时满足"浏览器不报 Module not found"和"编辑器不报红",写清你需要动哪几个文件。表格列渲染:给用户列表加三列——ID、登录名(点击新窗口跳转
html_url)、头像(用render返回<Image>,宽度限制 100)。要求columns的泛型传对,且头像列不写dataIndex。请求 + 类型 + 分页串联:请求用户列表接口,定义
IUserItem/IUserResponse两个类型,把响应体类型通过泛型传给 axios,用useState<IUserItem[]>([])存数据,再接入 Table 的受控分页(current/pageSize/total全部动态,total取自res.items.length)。排错题:下面的分页为什么点了不动?至少指出两处问题并给出修法。
tsx
<Table
rowKey="id"
columns={columns}
dataSource={users}
pagination={{ current: 1, pageSize: 3, total: 30 }}
/>原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | Table组件复习 | 03:24 |
| 2 | pagination分页配置介绍 | 09:28 |
| 3 | 动态实现分页功能 | 09:32 |
| 4 | ts中发送ajax请求-语法检查错误演示 | 12:51 |
| 5 | ts发送ajax请求定义类型及请求泛型传递 | 15:21 |
| 6 | ts中发送ajax请求步骤总结[重要] | 03:22 |
| 7 | axios结合table实现数据分页 | 10:56 |
小结
- 主题定制失败的 90% 是两件事:入口没换成
antd.less、配置改完没重启。 - craco 只做两件事——改 webpack(less 变量、别名、proxy),而 TS 的类型检查需要单独在
tsconfig里对齐,两边缺一不可。 - 别名"能跑但报红"和"不报红但跑不起来"是两种相反的错误,看到现象先判断是 webpack 层还是 TS 层的问题。
- Table 的三个核心 props 是
rowKey/columns/dataSource,columns的泛型是行对象类型而非数组类型。 - 分页组件"显示"和"能用"是两回事:显示靠
current/pageSize/total,能用靠onChange回写 state。 - TS 发请求与 JS 的唯一实质差异是先定义类型再发请求——因为响应数据结构只有运行时才有,而 TS 在编译期就要检查。
- 类型定义写进
src/api/model,请求泛型传响应体类型(第二个泛型),useState传元素类型,这三处类型保持一致就不会连环报错。 - TS 的类型只是编译期契约,运行时多出的字段不会被拦截——所以接口定义"够用即可",不必把返回字段全列出来。