news 2026/9/7 16:07:55

Material UI v4 到 v5 迁移实战:从 JSS 平滑过渡到 Emotion 的 styled、sx 与 tss-react 方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Material UI v4 到 v5 迁移实战:从 JSS 平滑过渡到 Emotion 的 styled、sx 与 tss-react 方案

Material UI v4 到 v5 迁移实战:从 JSS 平滑过渡到 Emotion 的 styled、sx 与 tss-react 方案

【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui

Material UI(现 MUI)v5 将默认样式引擎从 JSS 替换为 Emotion,这是 v4 → v5 升级中最核心的风格化架构变化。本文基于仓库中的迁移指南 migrating-from-jss.md 完整展开:两条官方迁移路线(styled/sxtss-react)的 codemod 命令、手动改写示例与限制条件全部保留,并深入 mui-codemod 包 的两个转换器源码,解释 PREFIX 推导、选择器重写、$嵌套规则替换等底层机制,帮助你把迁移风险降到最低。

背景:v5 为什么替换 JSS,以及迁移的灵活性

v5 中最大的变化之一,是用 Emotion(或 styled-components 作为替代引擎)取代 JSS 作为默认样式方案。两点关键事实需要明确:

  1. JSS 在 v5 中仍然可用。即使升级到 v5,你依然可以用makeStyleswithStyles添加组件覆盖样式。这意味着迁移可以渐进式进行:先把组件库升到 v5,再逐文件把 JSS 风格化代码重构为新引擎。
  2. SSR 场景有现成参考。如果项目使用 Next.js,且不确定如何让 SSR 同时兼容 Emotion 与 JSS,仓库中提供了专门的双引擎迁移示例工程 examples/material-ui-nextjs-ts-v4-v5-migration,其package.json中同时安装了@mui/styles与 Emotion 相关依赖,可直接对照其服务端渲染配置。

下面两条路线均可使用,官方认为第一条(styled / sx)是更优选择。

路线一:使用 styled 或 sx API

1.1 使用官方 codemod 自动转换

官方提供 codemod 将 JSS 样式迁移到styledAPI。注意其固有代价:转换结果会增加 CSS 特异性(specificity)——这是 codemod 能做到的最佳机械转换,后续可参考后文的示例自行精炼。

npx @mui/codemod@latest v5.0.0/jss-to-styled <path>

典型转换前后对比(makeStylesstyled+ 类名映射表):

import Typography from '@mui/material/Typography'; -import makeStyles from '@mui/styles/makeStyles'; +import { styled } from '@mui/material/styles'; -const useStyles = makeStyles((theme) => ({ - root: { - display: 'flex', - alignItems: 'center', - backgroundColor: theme.palette.primary.main - }, - cta: { - borderRadius: theme.shape.radius - }, - content: { - color: theme.palette.common.white, - fontSize: 16, - lineHeight: 1.7 - }, -})) +const PREFIX = 'MyCard'; +const classes = { + root: `${PREFIX}-root`, + cta: `${PREFIX}-cta`, + content: `${PREFIX}-content`, +} +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + display: 'flex', + alignItems: 'center', + backgroundColor: theme.palette.primary.main + }, + [`& .${classes.cta}`]: { + borderRadius: theme.shape.radius + }, + [`& .${classes.content}`]: { + color: theme.palette.common.white, + fontSize: 16, + lineHeight: 1.7 + }, +})) export const MyCard = () => { - const classes = useStyles(); return ( - <div className={classes.root}> + <Root className={classes.root}> {/* 这一方案的收益在于 Root 内部的 JSX 代码保持不变。 */} <Typography className={classes.content}>...</Typography> <Button className={classes.cta}>Go</Button> - </div> + </Root> ) }

官方建议:先对一小批文件运行该 codemod,检查变更再继续——转换并非覆盖所有情况,部分文件需要在转换后手工调整。

源码级解读:codemod 如何推导出 PREFIX 与选择器

阅读 jss-to-styled.js 可以弄清上面转换结果中每个细节的由来,这对你复核 codemod 输出非常有用:

  • PREFIX 推导是四级回退getPrefix()按顺序尝试:①withStyles(styles, { name })第二参数中的name选项;② 文件的export default声明名;③ 首个大写开头的具名导出变量名;④ 最终回退到文件 basename(去掉扩展名,见computePrefixFromPath)。所以上例中PREFIX = 'MyCard'来自组件导出名/文件名。
  • &.& .的选择器区分getRootClassKeys()会扫描 JSX 根元素上出现的className={classes.xxx}(支持直接成员表达式与clsx(classes.xxx)调用参数),凡是根元素自身使用的类键生成[`&.${classes.root}`](自身匹配),其余类键生成[`& .${classes.cta}`](后代选择器)。这就是为何转换结果的特异性高于 JSS 原版。
  • 重复类键自动去重createClassesconvertToStyledArg中的计数逻辑会把重名键追加数字后缀(rootroot2),避免类名冲突。
  • Fragment 根与 Suspense 的特殊处理。若组件根是React.Fragment,codemod 会替换为div并插入TODO jss-to-styled codemod注释(见createStyledComponent中的注释分支),提醒你检查标签是否合适;若根是Suspense则直接放弃转换(isTagNameSuspense检查后原样返回)。
  • withStyles也会被一并转换withStyles(styles)(Button)形式会被解包,样式并入Root,并在 JSX 使用处自动补回classesprop(withStylesComponents收集逻辑)。
  • 导入清理:转换器把@material-ui/core/styles/@mui/stylesmakeStyleswithStylescreateStyles的具名导入剥离,并在缺失时插入import { styled } from '@mui/material/styles';结尾还有三条正则替换做useStyles()残留与classes解构的收尾清理。

对应的行为测试集中在 jss-to-styled.test.js,jss-to-styled.test/ 目录下有多组 before/expected 快照(multipleWithStylesninthtenth等),可对照验证各类边界写法。

1.2 手动改写:优先推荐 sx

对于响应式样式或小的 CSS 覆盖,官方推荐sx而非styledsx的系统式属性详见 sx 属性文档):

import Chip from '@mui/material/Chip'; -import makeStyles from '@mui/styles/makeStyles'; +import Box from '@mui/material/Box'; -const useStyles = makeStyles((theme) => ({ - wrapper: { - display: 'flex', - }, - chip: { - padding: theme.spacing(1, 1.5), - boxShadow: theme.shadows[1], - } -})); function App() { - const classes = useStyles(); return ( - <div className={classes.wrapper}> - <Chip className={classes.chip} label="Chip" /> - </div> + <Box sx={{ display: 'flex' }}> + <Chip label="Chip" sx={{ py: 1, px: 1.5, boxShadow: 1 }} /> + </Box> ); }

注意sxpy: 1, px: 1.5直接对应theme.spacing(1, 1.5)boxShadow: 1对应theme.shadows[1]——sx 会按主题系统自动解析这些简写值。

1.3 手动改写:拆分为多个 styled 组件

若不想增加 CSS 特异性,可以把一组类拆成多个 styled 组件,每个样式只挂在自己对应的元素上:

-import makeStyles from '@mui/styles/makeStyles'; +import { styled } from '@mui/material/styles'; -const useStyles = makeStyles((theme) => ({ - root: { - display: 'flex', - alignItems: 'center', - borderRadius: 20, - background: theme.palette.grey[50], - }, - label: { - color: theme.palette.primary.main, - } -})) +const Root = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + borderRadius: 20, + background: theme.palette.grey[50], +})) +const Label = styled('span')(({ theme }) => ({ + color: theme.palette.primary.main, +})) function Status({ label }) { - const classes = useStyles(); return ( - <div className={classes.root}> - {icon} - <span className={classes.label}>{label}</span> - </div> + <Root> + {icon} + <Label>{label}</Label> + </Root> ) }

社区还有一个非 MUI 维护的转换工具,能把 JSS 直接转成多个 styled 组件而不增加特异性,适合上述拆分场景。

路线二:使用 tss-react

tss-react的 API 与 v4 的makeStyles非常接近,但底层运行在@emotion/react之上,且 TypeScript 支持远好于 v4 的makeStyles限制条件:如果你以 styled-components 替代 Emotion 作为底层引擎(参见 互操作性文档 中 styled-components 一节),tss-react 不可用。

安装依赖:

# npm npm install tss-react # yarn yarn add tss-react

tss-react 并非由 MUI 官方维护。SSR(Next.js)配置、theme对象定制等问题应查阅 tss-react 官方文档;Bug 与需求也应在其仓库中提交。

2.1 使用官方 codemod

npx @mui/codemod@latest v5.0.0/jss-to-tss-react <path>

基础转换(makeStyles()变为柯里化调用,返回值解构出classes):

import * as React from 'react'; -import makeStyles from '@material-ui/styles/makeStyles'; +import { makeStyles } from 'tss-react/mui'; import Button from '@mui/material/Button'; import Link from '@mui/material/Link'; -const useStyles = makeStyles((theme) => { +const useStyles = makeStyles()((theme) => { return { root: { color: theme.palette.primary.main, }, apply: { marginRight: theme.spacing(2), }, }; }); function Apply() { - const classes = useStyles(); + const { classes } = useStyles(); return ( <div className={classes.root}> <Button component={Link} to="https://support.mui.com" className={classes.apply}> Apply now </Button> </div> ); } export default Apply;

使用$嵌套选择器语法与clsx组合类名时,转换结果会引入 TypeScript 泛型与cx

import * as React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import clsx from 'clsx'; +import { makeStyles } from 'tss-react/mui'; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles<void, 'child' | 'small'>()((theme, _params, classes) => ({ parent: { padding: 30, - '&:hover $child': { + [`&:hover .${classes.child}`]: { backgroundColor: 'red', }, }, small: {}, child: { backgroundColor: 'blue', height: 50, - '&$small': { + [`&.${classes.small}`]: { backgroundColor: 'lightblue', height: 30 } }, })); function App() { - const classes = useStyles(); + const { classes, cx } = useStyles(); return ( <div className={classes.parent}> <div className={classes.child}> Background turns red when the mouse hovers over the parent. </div> - <div className={clsx(classes.child, classes.small)}> + <div className={cx(classes.child, classes.small)}> Background turns red when the mouse hovers over the parent. I am smaller than the other child. </div> </div> ); } export default App;

使用 JavaScript(而非 TypeScript)的项目,请删掉<void, 'child' | 'small'>泛型参数。

2.2 综合示例:$语法 + 参数 + classes prop + 显式命名

以下完整示例同时覆盖四种高级特性:$语法、useStyles()参数、合并classesprop、为样式表提供显式name

-import clsx from 'clsx'; -import { makeStyles, createStyles } from '@material-ui/core/styles'; +import { makeStyles } from 'tss-react/mui'; -const useStyles = makeStyles((theme) => createStyles< - 'root' | 'small' | 'child', {color: 'primary' | 'secondary', padding: number} -> -({ - root: ({color, padding}) => ({ +const useStyles = makeStyles<{color: 'primary' | 'secondary', padding: number}, 'child' | 'small'>({name: 'App'})((theme, { color, padding }, classes) => ({ + root: { padding: padding, - '&:hover $child': { + [`&:hover .${classes.child}`]: { backgroundColor: theme.palette[color].main, } - }), + }, small: {}, child: { border: '1px solid black', height: 50, - '&$small': { + [`&.${classes.small}`]: { height: 30 } } -}), {name: 'App'}); +})); function App({classes: classesProp}: {classes?: any}) { - const classes = useStyles({color: 'primary', padding: 30, classes: classesProp}); + const { classes, cx } = useStyles({ + color: 'primary', + padding: 30 + }, { + props: { + classes: classesProp + } + }); return ( <div className={classes.root}> <div className={classes.child}> The Background take the primary theme color when the mouse hovers the parent. </div> - <div className={clsx(classes.child, classes.small)}> + <div className={cx(classes.child, classes.small)}> The Background take the primary theme color when the mouse hovers the parent. I am smaller than the other child. </div> </div> ); } export default App;
源码级解读:tss-react codemod 的转换规则与 TODO 标记

jss-to-tss-react.js 实现了上述全部转换,理解它能帮你预判哪些写法会失败:

  • 导入重定向。转换器把来自@material-ui/core/styles@material-ui/core@mui/styles@material-ui/styles/makeStyles等路径的makeStyles/withStyles具名导入统一改写为tss-react/muicreateStyles的包裹会被剥掉,但会先提取其中的第二个类型参数作为 params 类型(即上例<{color: ...}, 'child' | 'small'>中第一个泛型的来源)。
  • $嵌套规则的正则替换transformNestedKeys收集样式对象顶层所有规则名,构造形如(\$child|\$small)的正则,在嵌套规则键中定位$xxx片段并替换为模板字符串`.${classes.child}`(父级嵌套用后代选择器.,自身组合规则如'&$small'替换为`&.${classes.small}`),同时把这些规则名收集进nestedKeys生成第二个泛型。
  • clsx/classnames自动替换为cx。转换器检测文件中的clsxclassnames默认导入,把调用点改写为cx并从解构中补上cx,同时移除原导入。
  • classesprop 的二元参数迁移useStyles({ ..., classes })中的classes会被抽出来,重组为第二参数{ props: { classes } }(源码中对hookArg.properties的拆分逻辑),与 tss-react 的 API 契约对齐。
  • withStyles参数重排。tss-react 的withStyles(Component, styles)把组件放在第一个参数,转换器执行withStylesCall.arguments.unshift(component)完成参数换位,这也解释了 2.4 节 diff 中Button从外层移到内层的原因。
  • 无法可靠处理的场景会留下TODO jss-to-tss-react codemod注释,包括:CSS prop 值中嵌套了 ArrowFunctionExpression、@global规则(tss-react 不支持,注释中附替代方案指引)、createStyles具名 hook 被跨文件导出使用(外部文件中的调用不会被转换)、箭头函数参数形态不符合预期等。

运行 codemod 后的必做检查:全局搜索TODO jss-to-tss-react codemod定位未转换的写法。此外,即使没有 TODO 注释,仍存在 codemod 未完全处理的情况——特别是当样式定义在函数体内部且使用了$语法或useStyles参数时,这些样式不会被正确迁移。相关边界场景见 jss-to-tss-react.test.js 与 expected-todo-comments.js 快照。

2.3cx取代clsx的取舍

官方明确要求丢弃clsx、改用 Emotion 的cx。其核心优势是:cx能识别 Emotion 生成的类名,从而保证样式按正确顺序被覆盖。注意一个细节:JSS 与 tss-react 对多个 CSS 类的默认优先级不同,cx参数的顺序可能需要手工重排——迁移后若出现样式覆盖不符合预期,这是首要排查点。

2.4withStyles()的类型安全替代

tss-react 提供 v4withStyles()的类型安全实现,$语法在其withStyles()中同样受支持:

-import Button from '@material-ui/core/Button'; +import Button from '@mui/material/Button'; -import withStyles from '@material-ui/styles/withStyles'; +import { withStyles } from 'tss-react/mui'; const MyCustomButton = withStyles( + Button, (theme) => ({ root: { minHeight: '30px', }, textPrimary: { color: theme.palette.text.primary, }, '@media (min-width: 960px)': { textPrimary: { fontWeight: 'bold', }, }, }), -)(Button); +); export default MyCustomButton;

2.5 主题样式覆盖(Theme style overrides)

全局主题覆盖(components主题键)在 TSS 中开箱即用。操作要点:

  1. 按 v5 风格破坏性变更文档 中 "Restructure component definitions" 一节重构组件定义;
  2. makeStyles提供name(如上文综合示例的{name: 'App'}),使类名包含组件真实名称,主题覆盖才能正确匹配。

v5 中样式覆盖还支持回调形式。默认情况下 TSS 只向覆盖回调提供theme;若还需要 props 与ownerState,需按 tss-react 的 "MUI global style overrides" 文档额外配置。

2.6 类名中包含组件真实名称

为了让类名始终包含组件实际名称(便于调试与主题覆盖匹配),可以把name作为隐式命名键传入:name: { App }。这与 jss-to-styled codemod 的 PREFIX 推导目的相同——区别在于前者由 codemod 静态推导,后者由你在makeStyles选项中显式声明。

2.7 ESLint 提示

当你从 hook 返回值解构多个变量(const { classes, cx } = useStyles())时,可能触发eslint(prefer-const-destructuring)之类的告警;在常规项目中可以直接对该规则关闭,这是官方给出的处理方式。

完成迁移:移除 @mui/styles

全部样式迁移完成后,卸载不再需要的 JSS 包:

# npm npm uninstall @mui/styles # yarn yarn remove @mui/styles

重要@emotion/styled@mui/material的 peer dependency。即使你的代码从不显式引用它,也必须保留在依赖中,否则物料组件内部样式无法工作。

迁移路径小结

维度styled / sxtss-react
底层引擎Emotion@emotion/react
与 v4 API 的相似度低(需改为 styled 组件或 sx 系统属性)高(makeStyles形态几乎不变)
TypeScript 支持良好显著优于 v4makeStyles
自动化工具npx @mui/codemod@latest v5.0.0/jss-to-styled <path>npx @mui/codemod@latest v5.0.0/jss-to-tss-react <path>
主要代价机械转换会增加 CSS 特异性多类优先级可能与 JSS 不同,cx参数顺序需复核
维护方MUI 官方 codemod第三方(非 MUI 维护)
兼容性限制不能与 styled-components 引擎混用

无论选择哪条路线,都建议:小批量先行 → 检查 codemod 输出与 TODO 标记 → 全量执行 → 搜索验证 → 卸载@mui/styles并确认@emotion/styled仍保留在依赖中。

【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 16:06:36

放大器频率补偿实战:从相位裕度到密勒补偿

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 16:06:01

家装公司AI智能体落地指南:从售前咨询到测试集设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 16:05:38

React Native美食日记应用开发实战:从数据库设计到性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 16:04:09

离线OCR+本地大模型:Windows环境下的OCR-10结构化字段提取方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 16:01:56

日常通勤Momenta和华为选哪个?前者遇加塞平顺不急刹

在日常通勤与高频泊车场景中&#xff0c;Momenta&#xff08;06880.HK&#xff09;与华为乾崑智驾展现出不同的产品哲学。Momenta依托R7世界模型物理AI基座、新芯航途X7专用SoC芯片与QNX安全操作系统构建的软硬一体架构&#xff0c;已通过TV莱茵ISO 26262 ASIL D最高功能安全认…

作者头像 李华