Material UI v4 到 v5 迁移实战:从 JSS 平滑过渡到 Emotion 的 styled、sx 与 tss-react 方案 📅 发布时间:2026/9/7 16:08:21 👁 浏览次数: Material UI v4 到 v5 迁移实战从 JSS 平滑过渡到 Emotion 的 styled、sx 与 tss-react 方案【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Googles Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-uiMaterial UI现 MUIv5 将默认样式引擎从 JSS 替换为 Emotion这是 v4 → v5 升级中最核心的风格化架构变化。本文基于仓库中的迁移指南 migrating-from-jss.md 完整展开两条官方迁移路线styled/sx与tss-react的 codemod 命令、手动改写示例与限制条件全部保留并深入 mui-codemod 包 的两个转换器源码解释 PREFIX 推导、选择器重写、$嵌套规则替换等底层机制帮助你把迁移风险降到最低。背景v5 为什么替换 JSS以及迁移的灵活性v5 中最大的变化之一是用 Emotion或 styled-components 作为替代引擎取代 JSS 作为默认样式方案。两点关键事实需要明确JSS 在 v5 中仍然可用。即使升级到 v5你依然可以用makeStyles、withStyles添加组件覆盖样式。这意味着迁移可以渐进式进行先把组件库升到 v5再逐文件把 JSS 风格化代码重构为新引擎。SSR 场景有现成参考。如果项目使用 Next.js且不确定如何让 SSR 同时兼容 Emotion 与 JSS仓库中提供了专门的双引擎迁移示例工程 examples/material-ui-nextjs-ts-v4-v5-migration其package.json中同时安装了mui/styles与 Emotion 相关依赖可直接对照其服务端渲染配置。下面两条路线均可使用官方认为第一条styled / sx是更优选择。路线一使用 styled 或 sx API1.1 使用官方 codemod 自动转换官方提供 codemod 将 JSS 样式迁移到styledAPI。注意其固有代价转换结果会增加 CSS 特异性specificity——这是 codemod 能做到的最佳机械转换后续可参考后文的示例自行精炼。npx mui/codemodlatest v5.0.0/jss-to-styled path典型转换前后对比makeStyles→styled 类名映射表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 原版。重复类键自动去重。createClasses与convertToStyledArg中的计数逻辑会把重名键追加数字后缀root、root2避免类名冲突。Fragment 根与 Suspense 的特殊处理。若组件根是React.Fragmentcodemod 会替换为div并插入TODO jss-to-styled codemod注释见createStyledComponent中的注释分支提醒你检查标签是否合适若根是Suspense则直接放弃转换isTagNameSuspense检查后原样返回。withStyles也会被一并转换withStyles(styles)(Button)形式会被解包样式并入Root并在 JSX 使用处自动补回classespropwithStylesComponents收集逻辑。导入清理转换器把material-ui/core/styles/mui/styles中makeStyles、withStyles、createStyles的具名导入剥离并在缺失时插入import { styled } from mui/material/styles结尾还有三条正则替换做useStyles()残留与classes解构的收尾清理。对应的行为测试集中在 jss-to-styled.test.jsjss-to-styled.test/ 目录下有多组 before/expected 快照multipleWithStyles、ninth、tenth等可对照验证各类边界写法。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} labelChip / - /div Box sx{{ display: flex }} Chip labelChip sx{{ py: 1, px: 1.5, boxShadow: 1 }} / /Box ); }注意sx中py: 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-reacttss-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-reacttss-react 并非由 MUI 官方维护。SSRNext.js配置、theme对象定制等问题应查阅 tss-react 官方文档Bug 与需求也应在其仓库中提交。2.1 使用官方 codemodnpx mui/codemodlatest v5.0.0/jss-to-tss-react path基础转换makeStyles()变为柯里化调用返回值解构出classesimport * 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} tohttps://support.mui.com className{classes.apply} Apply now /Button /div ); } export default Apply;使用$嵌套选择器语法与clsx组合类名时转换结果会引入 TypeScript 泛型与cximport * 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 makeStylesvoid, 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。转换器检测文件中的clsx或classnames默认导入把调用点改写为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 中开箱即用。操作要点按 v5 风格破坏性变更文档 中 Restructure component definitions 一节重构组件定义为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底层引擎Emotionemotion/react与 v4 API 的相似度低需改为 styled 组件或 sx 系统属性高makeStyles形态几乎不变TypeScript 支持良好显著优于 v4makeStyles自动化工具npx mui/codemodlatest v5.0.0/jss-to-styled pathnpx mui/codemodlatest 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 Googles Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考