本文记录把 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):

实例HTMLdump 里读到的文本尺寸
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):

const defaultFallbackFonts = {
  '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 个标签模型、HTMLContentModel 4 个取值 —— 都是可精确断言的事实 ✓
⇒ isDomElement / isDomText 传 {type:'tag'} / {type:'text'} 分别返回 true(与 domhandler 的约定一致)✓

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


六、接入与预检(要点汇总)

# ① 装库本身(file: 接入)
npm install "git+https://atomgit.com/oh-react-native/react-native-render-html.git#6.3.4-ohos-1.0.0" --ignore-scripts
# ② ★ 必须补装它自己的 9 个 dependencies(file: 接入不会自动装)
npm install --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)

关于基线:本次验证在 RN 0.84.1 + RNOH 0.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 本次验证的边界

  1. ★ 未验证 <img>(图片加载、contentWidth 对图片尺寸的影响、imagesMaxWidth 等);
  2. ★ 未验证 <table> / <ol> / <blockquote> / <code> / <pre> 等更多标签;
  3. ★ 未验证 renderers 自定义渲染器;
  4. ★ 未验证 <a> 的点击回调(onLinkPress);
  5. 未验证 baseStyle / classesStyles / idsStyles;
  6. 未验证 TRenderEngineProvider / RenderHTMLSource 的直接用法(只断言了它们存在);
  7. 未验证 domNodeToHTMLString 的实际输出(只断言它是函数);
  8. 未验证 defaultSystemFonts / defaultListStyleSpecs 的内容;
  9. 未验证 useNormalizedUrl / useIMGElementState 等 hook;
  10. 未验证 中文换行、超长 HTML、畸形 HTML 的容错;
  11. 未在 RN 0.88.0 / RNOH 0.84.4 上复验(交付包声明的基线);
  12. 未测其它屏幕宽度;未做跨平台对照。

未改动库代码。


九、常见问题

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 无通知。

最值得记下来的是这几条:

  1. ★★★★★ file: 接入不会装库自己的依赖 —— 这次是 9 个(含核心渲染引擎)。
    第一次 npm install 只 added 1 package;补装后 added 22 packages ⇒
    @native-html/transient-render-engine@11.2.3(精确锁定版本)必须显式装上 ✓

  2. ★★★★★ 文本级判据让"HTML 是否被正确解析"变得可直接核对。
    段落 / 标题 / 正文 / 列表三项 / 粗体 / 链接 / tagsStyles 覆盖 —— 7 处文本全部在 dump 里读到 ✓
    而且 h3 的行高(94)明显大于 p(52) ⇒ 结构差异被正确应用 ✓

  3. ★★★★★ dump 里的 Custom#<标签名> 是额外的结构观测点。
    Custom#html → Custom#body → Custom#ul → Custom#marked-list-item ⇒ 层级完整保留、可数可验 ✓

  4. ★★★★ defaultFallbackFonts 三条精确命中 —— "Platform.select 带 default"的运行时证据。
    实测 {'sans-serif':'sans-serif', monospace:'monospace', serif:'serif'} ✓
    ⇒ Platform.select({ios: …, default: …}) 是"第四平台"上最稳的写法 ✓

  5. ★★★★ 我预言错了一个 prop 的语义:contentWidth 不是布局约束。
    我按名字猜成"容器宽度 300vp = 900px",实测渲染块宽 1212px(= 容器宽) ✗
    ⇒ prop 的语义要看文档/源码,别按名字猜;它的实际用途是"计算图片等尺寸的参考宽度"。

  6. ★★★ 量化事实很值钱:45 个具名导出、117 个 HTML 标签模型、HTMLContentModel 4 个取值 ⇒
    这些都是可精确断言的契约 ✓

  7. ★★ 老写法九项全空,连 deprecated-react-native-prop-types 都没有。
    (这个库历史上打过该补丁,所以本轮预检专门查了这一项 ⇒ 现在没有 ✓)

  8. ★ 一个背景事实:上游已归档(archived: true)。
    好在它零原生依赖、依赖链都在 npm 上 ⇒ 仍然可用;但要有"上游不再更新"的心理准备 ✓


本篇用到的库

项内容
三方库react-native-render-html(上游 6.3.4 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-render-html
适配 TAG6.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)
# ① 装库本身
npm install "git+https://atomgit.com/oh-react-native/react-native-render-html.git#6.3.4-ohos-1.0.0" --ignore-scripts
# ② ★ 必须补装它自己的 9 个 dependencies(file: 接入不会自动装)
npm install --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
import 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 -b com.rnoh084.demo -a EntryAbility --ps rnAppKey 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 Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3
Node.js / npmv24.14.0 / 11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 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
本次构建assembleHap 3 分 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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐