本文记录把react-native-render-html(把 HTML 渲染成 RN 组件树)适配到 HarmonyOS 的完整过程。
它做的事很专一:给一段 HTML 字符串,渲染成原生组件—— 支持p/h1~h6/ul/ol/li/a/b/img/table等等,还能用tagsStyles逐标签改样式、用renderers换掉某个标签的实现。
结论:可以用,而且验证方式特别"硬" —— HTML 渲染出来的文本、以及 dump 里的标签名,都能直接核对。
它有两个值得记下来的特点:依赖链有 22 个包(但全是纯 JS)、contentWidth并不是布局约束。
★★★★★ 一、依赖:file:接入不会装库自己的依赖(这次又踩到,但立刻处理了)
库里声明了9 个dependencies:
"ramda":"^0.27.2","urijs":"^1.19.6","prop-types":"^15.5.7","@types/ramda":"^0.27.40","@types/urijs":"^1.19.15","stringify-entities":"^3.1.0","@jsamr/counter-style":"^2.0.1","@jsamr/react-native-li":"^2.3.0","@native-html/transient-render-engine":"11.2.3"← ★ 精确锁定版本,它是核心渲染引擎第一次npm install的结果:
added 1 package in 2s ← 只加了 file: 接入的那一个 ramda / urijs / stringify-entities / @jsamr/* / @native-html/transient-render-engine 全部 存在=False⇒file:方式接入时,npm 不会安装库自己的dependencies✓(在另外两个库上也遇到过同一件事)
⇒修法:把库的dependencies原样合并进宿主的package.json,再npm install:
新增 8 个依赖:@jsamr/counter-style@^2.0.1 / @jsamr/react-native-li@^2.3.0 / @native-html/transient-render-engine@11.2.3 / @types/ramda@^0.27.40 / @types/urijs@^1.19.15 / ramda@^0.27.2 / stringify-entities@^3.1.0 / urijs@^1.19.6 npm install ⇒ added 22 packages in 32s ramda 0.27.2 ✓ urijs 1.19.11 ✓ stringify-entities 3.1.0 ✓ prop-types 15.8.1 ✓ @jsamr/counter-style 2.0.2 ✓ @jsamr/react-native-li 2.3.1 ✓ @native-html/transient-render-engine 11.2.3 ✓(精确版本命中)★★★ 好消息:整条依赖链全是纯 JS
核心引擎@native-html/transient-render-engine@11.2.3自己的依赖:
"@native-html/css-processor":"1.11.0","csstype":"^3.0.9","domelementtype":"^2.2.0","domhandler":"^4.2.2","domutils":"^2.8.0","htmlparser2":"^7.1.2","ramda":"^0.27.2"⇒htmlparser2/domhandler/domutils/csstype/ramda—— 都是纯 JS✓
⇒没有harmony/目录需求、没有原生模块✓ ⇒这条链不需要任何 HAR✓
⚠️一个背景事实:上游仓库(
meliorence/react-native-render-html)已经归档(archived: true)。
好在这个库零原生依赖、依赖链也全在 npm 上,所以仍然可以直接用 ✓
★★★★★ 二、文本级判据:HTML 渲染出来的内容可以直接核对
这是这个库最好验证的地方:它渲染出的文本会长在布局 dump 里,于是"HTML 有没有被正确解析"变成了可以直接读的证据。
实测(5 段不同的 HTML):
| 实例 | HTML | dump 里读到的文本 | 尺寸 |
|---|---|---|---|
| E1 | <p>RHTML 段落一</p> | ✅「RHTML 段落一」 | w=1188 h=52 |
| E2 | <h3>RHTML 标题</h3><p>RHTML 正文</p> | ✅「RHTML 标题」+「RHTML 正文」 | h=94(标题)/h=94(同层) |
| E3 | <ul><li>甲</li><li>乙</li><li>丙</li></ul> | ✅「甲」「乙」「丙」 | h=52 |
| E4 | <p><b>粗体</b> 与 <a href="…">链接</a></p> | ✅「粗体」「链接」 | h=52 |
| E5 | <p>RHTML 红字段落</p>+tagsStyles={{p:{fontSize:18}}} | ✅「RHTML 红字段落」 | h=45(字号 18 生效) |
⇒五种结构全部渲染成功✓✓
⇒h3的行高(94)明显大于p的行高(52)⇒标题与正文的字号差异被正确应用(结构被正确解析的定量证据)✓
★★★★★ 额外发现:dump 里的Custom#<标签名>直接暴露 HTML 结构
在扫几何的时候发现,渲染出的容器以 HTML 标签名命名:
Custom#p [54,726,1266,775] ← <p> 段落 Custom#html [54,932,1266,1097] ← <html> Custom#body [54,932,1266,1097] ← <body> Custom#ul [54,932,1266,1097] ← <ul> Custom#marked-list-item [54,932,1266,987] ← 列表项(带项目符号) Custom#marked-list-item [54,987,1266,1042]⇒html → body → ul → marked-list-item的层级完整保留✓✓
⇒⇒ 这是验证"HTML 结构有没有被正确解析"最直观的观测点:不仅能看到文本,还能看到标签层级✓
★★★★ 三、defaultFallbackFonts三条精确命中 —— "Platform.select带default"的运行时证据
库里有三处字体族的平台判断(TRenderEngineProvider.js):
constdefaultFallbackFonts={'sans-serif':Platform.select({ios:'system',default:'sans-serif'}),monospace:Platform.select({ios:'Menlo',default:'monospace'}),serif:Platform.select({ios:'Times New Roman',default:'serif'}),};⇒三处【全都带了default】⇒鸿蒙上应该分别取到'sans-serif'/'monospace'/'serif'✓
实测(页面上直接读这个导出的常量):
NOTE | defaultFallbackFonts = {"sans-serif":"sans-serif","monospace":"monospace","serif":"serif"} PASS | ★★★ fallback 字体 sans-serif = "sans-serif"(鸿蒙取 default) PASS | ★★★ fallback 字体 monospace = "monospace" PASS | ★★★ fallback 字体 serif = "serif"⇒三条精确命中✓✓ ——这是"Platform.select写上default后在鸿蒙上安然落地"的运行时证据✓
⇒正面样本:Platform.select({ios: …, default: …})是该怎么写的写法 ✓
(另外两处平台分支:BRRenderer.js的isWeb = Platform.OS === 'web'、GenericPressable.js的if (Platform.OS === 'android')—— 鸿蒙上分别取false与走else分支,都不崩✓)
★★★★ 四、一个我预言错了的点:contentWidth不是布局约束
我原本以为"contentWidth={300}⇒ 渲染容器宽 300vp = 900px" ——实测不是:
✗ 未匹配到宽 900px 的块 实测:Custom#p 的宽度是 1212px(= 我这一层的容器宽)⇒contentWidth是"计算图片等尺寸时用的参考宽度",并不会把布局收窄到那个值✓
⇒实际用法:通常把它设为可用的内容宽度(例如屏宽减去两侧 padding),而不是"期望的容器宽度" ✓
⇒教训:prop 的语义要看文档/源码,不能按名字猜—— 我把"content width"想成了"容器 width" ✗
★★★ 五、其余预检结论(都很干净)
| 预检项 | 结果 |
|---|---|
| 原生痕迹 | ✅ 无原生源文件;NativeModules/requireNativeComponent/TurboModule全空 |
| 老写法九项 | ✅✅全部为空(createClass/React.PropTypes/ 从 react 具名导入 /ViewPropTypes/ 字符串ref/this.refs./mixins/componentWillReceiveProps/componentWillMount) |
deprecated-react-native-prop-types | ✅没有(这个库历史上打过该补丁,本轮预检专门查了 ⇒ 没有 ✓) |
measure()/measureInWindow/onLayout/findNodeHandle | ✅全空 |
useNativeDriver | ✅全空(这个库不做动画) |
| 平台分支 | ✅ 3 处Platform.select全带default+ 2 处二元判断 ⇒ 都不崩 |
| peerDependencies | ✅ 只有react/react-native⇒无原生 peer |
| 脚本 | ✅无prepare(有prepack,但npm install不跑它) |
具名导出与常量(反射实测):
NOTE | 具名导出个数 = 45 NOTE | 默认导出的 typeof = function PASS | ★★ 11 个关键具名导出全部存在 | 缺失=(无) NOTE | HTMLContentModel = {"block":"block","textual":"textual","mixed":"mixed","none":"none"} NOTE | defaultHTMLElementModels 的键数 = 117 ← ★★★ 117 个 HTML 标签模型 PASS | ★★ defaultHTMLElementModels 含 p/h1/ul/li/a/b/br NOTE | isDomElement({type:"tag"}) = true / isDomText({type:"text"}) = true⇒45 个具名导出、117 个标签模型、HTMLContentModel4 个取值 —— 都是可精确断言的事实 ✓
⇒isDomElement/isDomText传{type:'tag'}/{type:'text'}分别返回true(与domhandler的约定一致)✓
六、接入与预检(要点汇总)
# ① 装库本身(file: 接入)npminstall"git+https://atomgit.com/oh-react-native/react-native-render-html.git#6.3.4-ohos-1.0.0"--ignore-scripts# ② ★ 必须补装它自己的 9 个 dependencies(file: 接入不会自动装)npminstall--ignore-scripts\ramda@^0.27.2 urijs@^1.19.6 prop-types@^15.5.7 stringify-entities@^3.1.0\@jsamr/counter-style@^2.0.1 @jsamr/react-native-li@^2.3.0\@native-html/transient-render-engine@11.2.3\@types/ramda@^0.27.40 @types/urijs@^1.19.15更稳的做法:把库的
dependencies原样合并进你工程的package.json,再一次npm install—— 这样版本完全一致。
七、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | src/(TS 源码)+lib/(commonjs + typescript,94 个 js 文件)⇒ 源码可读 ✓;427 文件 |
spec.json | ⚠️缺upstreamCommit;⚠️ 上游是 monorepo(directory: packages/render-html) |
validation | ⚠️"build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0"+rnoh: "0.84.4"(verification: evidence/batch50/…)⇒两字段自相矛盾,且既不是当前宿主(0.84.1/0.84.3)也不是权威 0.88 基线 |
| ⚠️ 上游状态 | 仓库已归档(archived: true) |
关于基线:本次验证在RN0.84.1+ RNOH0.84.3上完成。
这个库没有任何已移除的写法(九项全空、连deprecated-react-native-prop-types都没有),
行为依赖htmlparser2系的纯 JS 解析 + RN 基础组件 ⇒结论跨基线成立✓
八、已知限制与本次验证边界
8.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
★★★★file:接入不会装它自己的 9 个依赖 | 必须显式补装(含核心引擎@native-html/transient-render-engine@11.2.3) |
★★★★contentWidth不是布局约束 | 它用于计算图片等尺寸;容器宽度仍由你的父容器决定(实测渲染块宽 = 容器宽) |
★★★defaultFallbackFonts在鸿蒙上是sans-serif/monospace/serif | 因为三处Platform.select都带了default✓ |
★★★ dump 里能看到Custom#<标签名> | Custom#html/Custom#body/Custom#ul/Custom#marked-list-item⇒ 很好验证结构 |
| ★★ 具名导出 45 个 | 含TRenderEngineProvider/RenderHTMLSource/TNodeRenderer/defaultHTMLElementModels(117 个标签模型)等 |
| ★★ 上游已归档 | meliorence/react-native-render-html已archived;好在零原生依赖、依赖链都在 npm 上 |
| ★ 图片/表格/自定义渲染器 | 支持,但本轮未验证(见下) |
8.2 本次验证的边界
- ★ 未验证
<img>(图片加载、contentWidth对图片尺寸的影响、imagesMaxWidth等); - ★ 未验证
<table>/<ol>/<blockquote>/<code>/<pre>等更多标签; - ★ 未验证
renderers自定义渲染器; - ★ 未验证
<a>的点击回调(onLinkPress); - 未验证
baseStyle/classesStyles/idsStyles; - 未验证
TRenderEngineProvider/RenderHTMLSource的直接用法(只断言了它们存在); - 未验证
domNodeToHTMLString的实际输出(只断言它是函数); - 未验证
defaultSystemFonts/defaultListStyleSpecs的内容; - 未验证
useNormalizedUrl/useIMGElementState等 hook; - 未验证中文换行、超长 HTML、畸形 HTML 的容错;
- 未在
RN 0.88.0/RNOH 0.84.4上复验(交付包声明的基线); - 未测其它屏幕宽度;未做跨平台对照。
未改动库代码。
九、常见问题
Q1:它在 RN 0.84 上能跑吗?
能。五种 HTML 结构(段落 / 标题+正文 / 无序列表 / 粗体+链接 /tagsStyles覆盖)渲染出的文本全部能在布局 dump 里读到,
契约与常量判据全部通过,LogBox无通知。
Q2:装完就报Unable to resolve module @native-html/transient-render-engine?
因为file:接入不会安装库自己的dependencies。把库的 9 个依赖显式补装(见第六节),
或者直接把它们合并进你的package.json再npm install。
Q3:contentWidth我该传多少?
传你实际可用的内容宽度(例如屏宽减两侧 padding)。⚠️ 它不会把布局收窄成那个值 ——
实测:我传contentWidth={300},但渲染出的Custom#p宽度仍是1212px(= 容器宽)。
Q4:怎么确认 HTML 有没有被正确解析?
两条路:
- 读渲染文本:布局 dump 里能搜到渲染出的文字(本次就是用这招核对 5 种结构的);
- 看容器名:dump 里能看到
Custom#html→Custom#body→Custom#ul→Custom#marked-list-item这样的标签层级✓
Q5:字体在鸿蒙上会取到什么?
defaultFallbackFonts实测是:
{'sans-serif':'sans-serif',monospace:'monospace',serif:'serif'}(因为三处Platform.select都写了default)✓ 如果你想用系统字体,可以在systemFonts里追加。
Q6:可以只渲染一小段 HTML 吗?
可以。最基本用法:
<RenderHTML contentWidth={width} source={{html: '<p>你好</p>'}} />source也支持{uri: 'https://…'}或直接传 DOM 节点(配合RenderHTMLSource)。
Q7:这个库需要鸿蒙侧做什么特殊处理吗?
不需要。纯 JS、零原生依赖、依赖链全在 npm 上;平台分支都带有default或安全落到else。
小结
react-native-render-html是把 HTML 渲染成 RN 组件树的成熟库。
它零原生依赖(9 个直接依赖 + 13 个传递依赖共 22 个包,全是纯 JS),老写法九项全空,
五种 HTML 结构渲染出的文本全部能在布局 dump 里核对,契约与常量判据全部通过,LogBox 无通知。
最值得记下来的是这几条:
★★★★★
file:接入不会装库自己的依赖 —— 这次是 9 个(含核心渲染引擎)。
第一次npm install只added 1 package;补装后added 22 packages⇒@native-html/transient-render-engine@11.2.3(精确锁定版本)必须显式装上✓★★★★★ 文本级判据让"HTML 是否被正确解析"变得可直接核对。
段落 / 标题 / 正文 / 列表三项 / 粗体 / 链接 /tagsStyles覆盖 ——7 处文本全部在 dump 里读到✓
而且h3的行高(94)明显大于p(52)⇒ 结构差异被正确应用 ✓★★★★★ dump 里的
Custom#<标签名>是额外的结构观测点。Custom#html → Custom#body → Custom#ul → Custom#marked-list-item⇒层级完整保留、可数可验✓★★★★
defaultFallbackFonts三条精确命中 —— "Platform.select带default"的运行时证据。
实测{'sans-serif':'sans-serif', monospace:'monospace', serif:'serif'}✓
⇒Platform.select({ios: …, default: …})是"第四平台"上最稳的写法✓★★★★ 我预言错了一个 prop 的语义:
contentWidth不是布局约束。
我按名字猜成"容器宽度 300vp = 900px",实测渲染块宽1212px(= 容器宽)✗
⇒prop 的语义要看文档/源码,别按名字猜;它的实际用途是"计算图片等尺寸的参考宽度"。★★★ 量化事实很值钱:45 个具名导出、117 个 HTML 标签模型、
HTMLContentModel4 个取值 ⇒
这些都是可精确断言的契约✓★★ 老写法九项全空,连
deprecated-react-native-prop-types都没有。
(这个库历史上打过该补丁,所以本轮预检专门查了这一项 ⇒ 现在没有 ✓)★ 一个背景事实:上游已归档(
archived: true)。
好在它零原生依赖、依赖链都在 npm 上 ⇒仍然可用;但要有"上游不再更新"的心理准备 ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-render-html(上游6.3.4的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-render-html |
| 适配 TAG | 6.3.4-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(9 个直接依赖 + 13 个传递依赖全是纯 JS) |
| 库类型 | HTML 渲染器(HTML → RN 组件树) |
| 运行时依赖 | ⚠️9 个直接依赖必须显式补装(含@native-html/transient-render-engine@11.2.3);无原生依赖 |
prepare/postinstall | ✅无(有prepack,npm install不跑) |
| 许可证 | BSD-2-Clause |
| 上游仓库 | https://github.com/meliorence/react-native-render-html(★3660,⚠️已归档) |
| 宿主工程 | RNOH084Demo(测试页rnAppKey = RenderHtmlTestApp) |
# ① 装库本身npminstall"git+https://atomgit.com/oh-react-native/react-native-render-html.git#6.3.4-ohos-1.0.0"--ignore-scripts# ② ★ 必须补装它自己的 9 个 dependencies(file: 接入不会自动装)npminstall--ignore-scripts ramda@^0.27.2 urijs@^1.19.6 prop-types@^15.5.7\stringify-entities@^3.1.0 @jsamr/counter-style@^2.0.1 @jsamr/react-native-li@^2.3.0\@native-html/transient-render-engine@11.2.3 @types/ramda@^0.27.40 @types/urijs@^1.19.15import RenderHTML from 'react-native-render-html'; // ① 基本用法(contentWidth 是"参考宽度",不是布局约束) <RenderHTML contentWidth={width} // 通常传可用内容宽度(如屏宽减 padding) source={{html: '<p>你好,鸿蒙</p>'}} /> // ② 逐标签改样式 <RenderHTML contentWidth={width} source={{html: '<h3>标题</h3><p>正文</p>'}} tagsStyles={{p: {color: '#D0021B', fontSize: 18}, h3: {fontSize: 22}}} /> // ③ 列表 / 行内标签都支持 <RenderHTML contentWidth={width} source={{html: '<ul><li>甲</li><li>乙</li><li>丙</li></ul><p><b>粗体</b> 与 <a href="https://example.com">链接</a></p>'}} /> // ⚠️ file: 接入时不装它自己的依赖 ⇒ 必须补装 9 个(否则 Unable to resolve module) // ⚠️ contentWidth 不是布局约束(实测渲染块宽 = 容器宽) // ⚠️ defaultFallbackFonts 在鸿蒙上是 sans-serif / monospace / serif(三处 Platform.select 都带 default) // ⚠️ 上游仓库已归档(archived: true)# 换页启动测试页 —— force-stop 不能省hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey RenderHtmlTestApp判据的复现方式
# ① 契约 / 常量:逐条 PASS/FAIL 日志hdc shell"hilog -x | grep rhtml-test"|Select-String'PASS|FAIL|SUMMARY|NOTE'# ⇒ 具名导出 45 个、defaultHTMLElementModels 117 个键、# defaultFallbackFonts = {"sans-serif":"sans-serif","monospace":"monospace","serif":"serif"}# ② 渲染文本:在布局 dump 里直接搜文本devecocli ui layout|Select-String'RHTML'# ⇒ 「RHTML 段落一」「RHTML 标题」「RHTML 正文」「RHTML 红字段落」# ③ 渲染结构:dump 里的标签名devecocli ui layout|Select-String'Custom#(html|body|ul|p|marked-list-item)'# ④ 注意:contentWidth 不改变布局宽度(渲染块宽 = 容器宽)环境搭建
本手册的验证环境是React Native for OpenHarmony(RNOH)。从零准备开发环境(DevEco Studio、HarmonyOS SDK API 26、Node.js、ohpm、hvigorw、模拟器与hdc调试)请按 CPF-RN 开发者指南的「环境初始化」操作:
- CPF-RN 开发者指南 · 02-搭建准备 · 环境初始化:https://atomgit.com/CPF-RN/docs/blob/main/开发者指南/02-搭建准备/环境初始化.md
环境就绪后,本次验证使用的具体版本组合见文末「验证环境」一节。
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony/@rnoh/react-native-openharmony0.84.3 |
| Node.js / npm | v24.14.0/11.9.0 |
| DevEco Studio | 26.0.0.621 |
| HarmonyOS SDK | API 26(26.0.0.32) |
| 设备 | HarmonyOS 7.0.0(26.0.0) Beta2 模拟器Pura X View(ohos-x64),1320×2232,density 3 |
| 本机状态栏高度 | 117px = 39vp |
| 宿主依赖 | ⚠️需补装 9 个(合并进宿主 package.json 后共added 22 packages) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap3 分 58 秒(首次)+4 分 01 秒(接线后)⇒ 均一次成功 |
| 验证规模 | 契约/常量判据全 PASS(45 个具名导出、11 个关键导出、3 条 fallback 字体、4 个 content model、117 个标签模型、2 个类型守卫);7 处渲染文本全部在 dump 里读到;Custom#<标签>层级可见;LogBox 零通知;4 张截图互异 |
⚠️基线说明:交付包
spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
本库无任何已移除写法、依赖全为纯 JS ⇒结论跨基线成立。
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| React Native 鸿蒙官方社区(CPF-RN) | https://atomgit.com/CPF-RN |
| RNOH 框架仓库 | https://atomgit.com/openharmony-RN/ohos_react_native |
| RNOH 三方库使用文档 | https://atomgit.com/OpenHarmony-RN/usage-docs |
| React Native for OpenHarmony 组织 | https://atomgit.com/oh-react-native |
| RN 三方库鸿蒙适配清单 | https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview |
欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN
React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native
RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview