- 前端
- Web框架
- SSR
- 前端构建
- 插件系统
- 微前端
- 跨平台
【免费下载链接】ice
🚀 ice.js: The Progressive App Framework Based On React(基于 React 的渐进式应用框架)
导读
本文介绍 ice.js 官方兼容插件@ice/plugin-rax-compat,它用于将基于 rax-app 开发的存量项目迁移到 ice.js 渐进式应用框架。读完本文,你将掌握该插件的安装与配置方法、inlineStyle/cssModule/legacy三个核心选项的含义与适用场景,并从源码层面理解它在类型定义、模块别名、JSX 编译与样式处理四个维度上的兼容机制,以及如何利用仓库中的示例工程与集成测试验证迁移结果。
插件定位:为 rax-app 存量项目提供迁移通道
rax-app 是阿里巴巴推出的跨端应用框架,其组件库(rax-view、rax-text、rax-image等)与运行时 API(如createElement、createContext)在 API 形态上与 React 高度相似,但并非完全等价。当项目需要从 rax-app 迁移到基于 React 的 ice.js 时,会遇到三类典型障碍:
- 类型体系不匹配:Rax 的类型定义基于 React 16.8 之前的时代,与 React 18 的类型定义存在差异,直接迁移会出现大量类型报错;
- 模块路径不同:
rax-children、rax-clone-element等组成 Rax 核心逻辑的rax-*包在 React 生态中并不存在; - 样式模型不同:Rax 项目习惯使用
className="header"配合样式表内联(styleSheet)的写法,与 Web 端 CSS 文件外链的模型不同。
@ice/plugin-rax-compat正是为了解决这些问题而存在。它是一个标准的 ice.js 构建期插件(Plugin<RaxCompatPluginOptions>),其入口 packages/plugin-rax-compat/src/index.ts 在setup阶段依次挂载了四个服务:TypingsService(类型声明)、AliasService(模块别名)、JSXService(JSX 编译)、StyleService(样式处理),从插件 package.json 可以看到它依赖rax-compat、stylesheet-loader、babel-plugin-transform-jsx-stylesheet、@ice/bundles等包来完成这些工作。
安装与基本配置
首先安装插件依赖:
npm install @ice/plugin-rax-compat --save-dev # 或 pnpm add -D @ice/plugin-rax-compat然后在项目根目录的ice.config.mts中注册插件:
import { defineConfig } from 'ice'; import compatRax from '@ice/plugin-rax-compat'; export default defineConfig(() => ({ plugins: [compatRax({ /* options */ })], }));仓库中的 examples/rax-project/ice.config.mts 给出了一个最简示例:直接调用compatRax()(不传任何选项)即可获得默认行为,且可与其他插件如@ice/plugin-jsx-plus组合使用:
import { defineConfig } from '@ice/app'; import compatRax from '@ice/plugin-rax-compat'; import jsxPlus from '@ice/plugin-jsx-plus'; export default defineConfig(() => ({ publicPath: '/', plugins: [ compatRax(), jsxPlus(), ], }));插件选项详解
插件的完整选项类型定义在 packages/plugin-rax-compat/src/typings.ts 中,入口 src/index.ts 的normalizeOptions会为未传入的选项补齐默认值。下表汇总了三个选项的含义与默认值:
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
inlineStyle | boolean \| ((id: string) => boolean) | false | 启用 stylesheet loader,将命中的样式资源导入为内联的 styleSheet 对象 |
cssModule | boolean | true | 控制.module.css(less/scss)文件是否走 CSS Module 处理 |
legacy | boolean | false | 兼容 Rax v0.6.x 的命名空间导入方式,如import Rax from 'rax' |
inlineStyle:是否启用行内样式
默认false。开启后,插件会启用 stylesheet loader 来导入 CSS 文件,把样式编译为可供 JS 引用的 styleSheet 对象,从而支持 Rax 风格的内联样式写法。
值得注意的是,inlineStyle除了布尔值之外还支持函数形式。在 src/typings.ts 的类型定义中,它被声明为boolean | ((id: string) => boolean),即可以按文件路径精确控制哪些文件启用内联样式。这一点在 src/services/styles/index.ts 的StyleService.provide中有明确提示:当全量启用inlineStyle: true时,插件会输出一条警告,建议改用函数式写法控制内联样式的影响范围:
inlineStyle: (id) => id.includes('inline-style-module'),判断逻辑由 src/utils.ts 中的checkInlineStyleEnable实现:函数类型直接调用该函数并返回结果,布尔类型直接返回本身。
cssModule:控制 CSS Module 文件的处理方式
默认true。当inlineStyle启用、且cssModule被关闭时,.module.css(以及.module.less等)文件也会被交由 stylesheet-loader 处理,即把 CSS Module 文件也内联为样式对象——但原文档明确指出这种做法不推荐。原因是 CSS Module 的类名在编译后是局部化的哈希值,其本意是配合className={styles.xxx}使用,强行内联会破坏模块隔离语义。
legacy:兼容 Rax v0.6.x 的导入方式
默认false。启用后支持 Rax 老版本(v0.6.x)的默认导入 + 命名空间调用写法:
import Rax from 'rax'; Rax.createContext();四大兼容逻辑的实现原理
原文档明确了该插件处理的四类兼容逻辑:类型定义、别名、JSX、样式。下面结合源码逐一展开。
1. 类型定义:用 React 18 的类型补齐 Rax 命名空间
Rax 的类型定义停留在 React 16.8 时代,与 React 18 的类型定义存在差异。插件会向.ice目录渲染一份rax-compat.d.ts(模板见 packages/plugin-rax-compat/src/templates/rax-compat.d.ts),内部直接复用 React 的类型定义对Rax命名空间进行声明,涵盖FC、ForwardRefRenderFunction、RaxNode、PropsWithChildren、RaxFragment、RaxChildren等常用类型。
具体实现位于 src/services/typings.ts:TypingsService通过api.generator.addRenderFile把模板渲染到构建目录,再通过addExport以纯类型导入(type __UNUSED_TYPE_FOR_IMPORT_EFFECT_ONLY__)的方式引入,注释中解释了这样做的原因——避免值导入触发 Webpack 编译报错'Export assignment cannot be used when targeting ECMAScript modules.'(该 .d.ts 使用export =语法)。
2. 别名:把 rax-* 包映射到 rax-compat 内部实现
Rax 的核心逻辑由一组rax-*包组成(如rax-children、rax-clone-element),这些包在 React 生态中不存在。插件通过 Webpack/构建工具的 alias 机制将它们映射到rax-compat包的内部实现。完整的别名注册表定义在 src/services/alias.ts 的AliasRegistry中:
| 别名 | 映射目标 |
|---|---|
rax | rax-compat |
rax-children | rax-compat/children |
rax-clone-element | rax-compat/clone-element |
rax-create-class | rax-compat/create-class |
rax-create-factory | rax-compat/create-factory |
rax-create-portal | rax-compat/create-portal |
rax-find-dom-node | rax-compat/find-dom-node |
rax-is-valid-element | rax-compat/is-valid-element |
rax-unmount-component-at-node | rax-compat/unmount-component-at-node |
rax-compat/runtime/jsx-dev-runtime | rax-compat/runtime/jsx-dev-runtime |
rax-compat/runtime/jsx-runtime | rax-compat/runtime/jsx-runtime |
这些映射在api.onGetConfig阶段合并进构建配置的config.alias。当启用legacy模式时,AliasService还会做两件额外的事:
- 通过
api.generator.addRenderFile把 src/templates/rax-compat-legacy-exports.ts.template 渲染为.ice/rax-compat-legacy-exports.ts; - 将
rax的别名指向这个生成的文件。
从模板内容可以看到,该文件同时支持三种导入形态:import * as rax from 'rax-compat'的命名空间导入(v1.0)、export * from 'rax-compat'的具名导出、以及export default { ...rax }的默认导出(v0.6+),并额外补充了 Rax 时代的PropTypes对象(用空函数占位),从而让Rax.createContext()这类 v0.6.x 写法可以正常工作。启用legacy时插件会输出一条 warning:legacy 模式仅应用于兼容 rax v0.6.x。
3. JSX:基于源码内容动态调整 swc 编译配置
JSX 的编译行为由 src/services/jsx.ts 中的JSXService控制。它包裹了原有的swcOptions.compilationConfig,并针对每个源码文件做两种判断:
- 当源码中包含
@jsx createElement注解时,将jsc.transform.react.runtime设置为classic(经典运行时,由开发者显式提供createElement); - 当源码中存在
from 'rax'或require('rax')的导入语句时,将importSource设置为rax-compat/runtime(自动 JSX 运行时从该模块获取jsx/jsxs等函数,与 React 18 保持一致)。
判断依据分别是source.indexOf('@jsx createElement')和正则/(from|require\()\s*['"]rax['"]/。由于配置是函数式的、逐文件执行的,因此同一项目中不同文件的 JSX 编译策略可以不同,实现了按源码形态的精细切换。
4. 样式:inlineStyle 模式下的行内样式处理
当inlineStyle启用时,StyleService(src/services/styles/index.ts)会按顺序挂载三个处理环节:JSX 转换、客户端(Webpack)处理、服务端(esbuild)处理。
第一步:JSXClassNameTransformer 改写 className
babel-plugin-transform-jsx-stylesheet 接入,配置了retainClassName: true与forceEnableCSS: true)会把源码中的静态 className 字符串改写为 style 引用:
// 转换前 <div className="header" /> // 转换后 <div style={styleSheet.header} />需要特别留意三个限制条件(原文档明确强调):
- 只有项目源码内的代码才会被转换,
node_modules中的代码一律跳过(转换器入口直接return); className={'xxx'}这类表达式写法不会被转换(转换器只处理静态字符串字面量);import './x.module.css'这类仅引入样式的写法不会被转换(模块引入不等于 className 使用)。
此外,转换器只处理.jsx?/.tsx?/.mjs后缀的文件,TypeScript 文件会额外注入typescript与decorators-legacy解析插件;是否转换同样经过checkInlineStyleEnable过滤,即遵循inlineStyle的函数式作用域控制。
第二步:ClientSide——覆盖 Webpack Ruleset
客户端侧,插件通过configureWebpack注入处理器(见 applyClientSideProcessor.ts),为每种样式类型(css/less/sass/scss)重新组织 Webpack 规则为oneOf结构:
- 命中内联样式的文件:交由
stylesheet-loader处理,编译为导出 styleSheet 对象的 JS 模块;非 css 文件会先经过预处理器(less-loader、sass-loader)编译,其中 less 开启了javascriptEnabled: true; - 其余文件:保持原本的处理逻辑,即打入额外的 CSS 文件。
oneOf内部根据两个正则判断归属:当cssModule启用(默认)时,*.module.css与*.global.css走常规样式 loader;当cssModule禁用时,只有*.global.css走常规 loader,.module.*文件也交由 stylesheet-loader 内联处理。判断函数如下(简化自源码):
const commonStyleResourceMatcher = new RegExp( options.cssModule ? `(\\.module|global)\\.${styleKind}$` : `(\\.global)\\.${styleKind}$`, 'i', ); const useCommonStyleLoader = commonStyleResourceMatcher.test(id) || !inlineStyleEnabled;原文档还提示了两个限制:在--speedup模式下该逻辑无法生效;禁用cssModule后.module.css(less/...)文件也会被 stylesheet-loader 处理。
第三步:ServerSide——esbuild 处理
在 SSR/SSG 场景下,服务端构建由 esbuild 完成。插件会向服务端构建配置注入名为esbuild-inline-style的 onLoad 插件(见 applyServerSideProcessor.ts),对命中内联样式的.css/.sass/.scss/.less文件执行样式到 styleSheet 的转换,并把文件内容类型改为 JS(loader: 'js'),从而让服务端代码也能以 JS 模块方式引用样式对象。该处理仅当userConfig.ssr或userConfig.ssg开启时生效;注入时会移除esbuild-empty-css插件,并依据cssModule决定在esbuild-css-modules插件之后还是原位插入。
样式到 styleSheet 的核心转换
无论客户端还是服务端,样式转换的最终实现都汇聚在 src/lib/transform-styles.ts 的styleSheetLoader中。它的处理管线是:less/sass/scss 预处理 → postcss 插件rpx2vw(unitPrecision: 4,将 rpx 单位换算为 vw)→css.parse解析样式表 → 逐规则转换为 styleSheet 对象,并额外处理伪类(className:active合并为classNameActive键)、@media媒体查询(运行时通过window.matchMedia动态合并)、@font-face(通过FontFaceAPI 注册)以及prefers-color-scheme主题等场景。这就是className="xxx"最终能映射为style={styleSheet.xxx}的底层原因。
实战验证:示例工程与集成测试
仓库提供了两个直接相关的示例工程与对应的集成测试,可用于验证插件行为:
- examples/rax-project:基础 rax 项目迁移示例,
ice.config.mts中以默认选项注册插件,页面源码可直接使用rax、rax-view、rax-text等模块; - examples/rax-inline-style:行内样式示例,
ice.config.mts中启用inlineStyle: true(并配置server.bundle与server.format: 'cjs'以支撑 SSR 验证),页面 src/pages/index.jsx 中既使用className="homeContainer"的静态 className,也使用 CSS Module 的import styles from './index.module.less'写法,还引入了来自 node_modules 的组件,覆盖了多种边界场景。
集成测试 tests/integration/rax-inline-style.test.ts 对 build 与 devServer 两种模式分别断言了四个关键结果,可作为迁移正确性的验收标准:
img元素保留class属性(来自 CSS Module 的className={styles['logo']},未被转换);span元素保留class属性(CSS Module 场景,未被转换);span元素的style包含display:block(来自 node_modules 组件的内联 CSS,说明行内样式对依赖包同样生效);span元素的style包含color:rgb(85,85,85)(来自项目源码index.css的静态 className,说明className="xxx"被成功转换为内联 style)。
这些断言与 README 中“只有项目源码中className="xxx"写法才会被转换”的说明相互印证:CSS Module 的表达式写法与 class 保留共存,静态 className 则被内联。
使用注意事项与限制汇总
综合原文档与源码,使用该插件时需要注意以下事项:
inlineStyle建议按需开启:全量启用会输出警告,推荐使用函数式写法inlineStyle: (id) => ...限定到具体模块(如按目录或文件名匹配),插件内部会将该函数应用到每个源文件与样式文件;- 转换边界:只有项目源码、只有静态字符串
className="xxx"写法会被转换为style={styleSheet.xxx};className={styles.xxx}(CSS Module)、className={'xxx'}、import './x.module.css'均不会转换,node_modules代码也不会转换; --speedup限制:客户端样式规则覆盖在--speedup模式下无法生效;cssModule与内联的取舍:仅在确有需要时关闭cssModule,此时.module.*文件也会被内联,官方标注为不推荐;legacy仅用于 v0.6.x:启用后rax会映射到生成的rax-compat-legacy-exports.ts,以获得Rax.createContext()这类命名空间 API 与PropTypes导出;新代码应使用 v1.0 的具名/命名空间导入方式;- SSR/SSG:服务端内联样式处理仅在开启
ssr或ssg时生效,且会移除esbuild-empty-css插件、强制开启 tree shaking。
如需深入插件实现,可以继续阅读 packages/plugin-rax-compat/src/index.ts(插件入口与选项归一化)、src/services/alias.ts(别名注册)、src/services/jsx.ts(swc 配置)、src/services/styles/applyClientSideProcessor.ts(Webpack 规则覆盖)以及 src/lib/transform-styles.ts(styleSheet 转换核心)。
- 前端
- Web框架
- SSR
- 前端构建
- 插件系统
- 微前端
- 跨平台
【免费下载链接】ice
🚀 ice.js: The Progressive App Framework Based On React(基于 React 的渐进式应用框架)
相关推荐
在 ice.js 中平滑迁移 rax-app 项目:@ice/plugin-rax-compat 兼容插件全解析
在 ice.js 中平滑迁移 rax app 项目:@ice/plugin rax compat 兼容插件全解析 导读 @ice/plugin rax comp
前端Web框架SSR前端构建插件系统微前端跨平台ice.js 的 Rax 兼容层:rax-compat 运行时 polyfill 原理与迁移实践
ice.js 的 Rax 兼容层:rax compat 运行时 polyfill 原理与迁移实践 rax compat 是 ice.js 生态中用于"以 Rax
前端Web框架SSR前端构建插件系统微前端跨平台rax-compat 运行时兼容层:让 Rax 代码跑在真实 React 18 之上(ice.js 渐进式框架实践)
rax compat 运行时兼容层:让 Rax 代码跑在真实 React 18 之上(ice.js 渐进式框架实践) rax compat 是 ice.js 仓
前端Web框架SSR前端构建插件系统微前端跨平台
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考