React Native for OpenHarmony 实战:三方库 react-native-parsed-text 的鸿蒙化适配指南
本文记录把 react-native-parsed-text(把一段文本按规则切成多个可交互片段)适配到 HarmonyOS 的完整过程。
它做的事很实用:给一段纯字符串,用若干正则把它切开,命中的部分渲染成独立的 <Text>(可以带自己的样式、可以点),没命中的部分原样保留。
结论有点意外:这个库在 RN 0.84 上能直接用。
原因很值得说——它同样碰到了已被移除的 API(Text.propTypes),但写法是安全降级的:
const customParseShape = PropTypes.shape({
...Text.propTypes, // ← RN 0.84 上 Text.propTypes 已是 undefined
pattern: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(RegExp)]).isRequired,
});
class ParsedText extends React.Component {
static propTypes = {
...Text.propTypes, // ← 同上
parse: …,
childrenProps: …,
};
}
实测:
typeof RN.Text.propTypes = undefined
{...undefined} = {} ← 不抛错
⇒ {...undefined} 在 ES2018 里是合法的(结果就是空对象)⇒ propTypes 只是少了一批校验键,组件照常工作 ✓✓
这一点是本文最想说的:"用了已移除的 API"和"加载即崩"是两件事 ——
同样是ViewPropTypes/Text.propTypes这类被移除的东西,
{...Text.propTypes}只是少几个键;而style: ViewPropTypes.style这种成员访问就会当场抛错。
这个库属于前者,所以它活下来了。
★★ 它把"切分逻辑"抽成了一个独立的类 —— 非常适合验证
库把业务逻辑放在 src/lib/TextExtraction.js 里:
class TextExtraction {
constructor(text, patterns) { this.text = text; this.patterns = patterns || []; }
parse() { … } // ⇒ 返回 [{children, ...props}, …]
getMatchedPart(matchedPattern, text, matches, index) { … }
}
⇒ 可以直接 new TextExtraction(text, patterns).parse(),把切分结果逐条核对 ✓ 这一轮我据此刻了 21 条断言,覆盖了几个不太显眼的语义:
| 语义 | 实测 |
|---|---|
| 切分 | '见 https://a.com 结束' ⇒ 3 段:'见 ' / 'https://a.com' / ' 结束' |
| 空段被丢弃 | 匹配出现在开头时 ⇒ 前面的空串不会成为一段(只剩 2 段) |
| 第一个匹配上的 pattern 获胜 | 两个 pattern 都能匹配时,只有第一个生效(后一个不会再切它自己的结果) |
| 函数值被包装 | onPress 变成 (text, index) => … ⇒ 实测收到 {text:'https://a.com', index:3} |
renderText 改写文本 | renderText: t => '[13字符]' ⇒ 该段的 children 变成 '[13字符]',且 renderText 本身不会留在 props 里 |
| 限额 | nonExhaustiveModeMaxMatchCount: 2 ⇒ 全局最多匹配 2 次;不传 ⇒ 不限 |
/g 正则 | 'x1 x2 x3' + /x\d/g ⇒ 5 段(x1 / ' ' / x2 / ' ' / x3) |
★★ 渲染与交互也是通的
页面上放了三个实例:
R1 三种 pattern(url 蓝 / email 绿 / phone 橙) ⇒ 点链接可触发回调
R2 对照:不传 parse ⇒ 原样渲染(不切分)
R3 renderText 改写 + childrenProps 透传
点击 R1 里的链接,日志给出了:
CLICK url text=https://atomgit.com/CPF-RN index=3
⇒ onPress(text, index) 两个参数都对(index=3 正是 "官网 " 之后的字符偏移)✓
renderText 的改写直接能从布局 dump 里读出来:
Text#Paragraph(206) "A [链接:atomgit.com] B [邮箱] C"
设备侧断言:A 20 / 20 + B 21 / 21 = 41 / 41 全通过(纯逻辑部分);
交互那条在补跑时因"点击与断言执行的先后顺序"计为 0 次,但点击的回调日志已单独取得(见上)。

一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 0.0.22;license: MIT;上游 star 1247 |
| 库类型 | 纯 JS 组件库(src/ 源码直发:ParsedText.js 4790 B + lib/TextExtraction.js 4945 B + .d.ts) |
| 是否需要原生适配 | 不需要(无 harmony/、无 NativeModules、无原生代码) |
| 运行时依赖 | ✅ 仅 prop-types(宿主已有 15.8.1)⇒ 无需补装 |
prepare / postinstall | ✅ 都没有 |
main | src/ParsedText.js(源码直发) |
| 需要 HAR / 权限 / ohpm | ❌ 都不需要 |
| ★ 是否可用 | ✅ 可以直接用(用 Text.propTypes 但 {...undefined} 安全降级) |
| 设备侧断言 | ✅ 纯逻辑 41 / 41 全通过(A 20 + B 21);交互回调实测成功 |
| ★ 老 API 预扫 | 只命中 Text.propTypes(展开用法 ⇒ 安全);无 createClass/ViewPropTypes/contextTypes/从 react 取 PropTypes/兼容垫片 |
| ★ 平台分支预扫 | ✅ 一处都没有 |
| ★ 导出 | export const PATTERNS(3 个正则)+ export default ParsedText |
| ★ 平台无关的注意点 | PATTERNS.url 不认裸域名(只认 http(s):// / www.);PATTERNS.phone 是 3+3+4…7 位;getPatterns() 遇到未知 type 会 throw |
二、判定与接入
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json 里有没有 harmony.autolinking | 没有 |
| ② | 仓库里有没有 harmony/ | 没有 |
| ③ | 代码里有没有 NativeModules | 全无 |
| ④ | import 图体检 | 只有 prop-types(宿主已有)+ 自己的两个文件 ⇒ 零额外安装 ✓ |
| ⑤ | scripts 扫 prepare 家族 | ✅ 没有 |
| ⑥ | ★ 老 API 预扫 | 命中 Text.propTypes(展开用法)⇒ 判断为安全降级 |
| ⑦ | ★ 从 react 取具名导入 | ✅ 空 |
| ⑧ | ★ 平台分支预扫 | ✅ 空 |
| ⑨ | 原生痕迹预扫 | ✅ 空 |
npm install "git+https://atomgit.com/oh-react-native/react-native-parsed-text.git#0.0.22-ohos-1.0.0" --ignore-scripts
装完即可用(本工程实测:bundle exit=0 + hvigor exit=0 + 页面正常渲染、点击回调正常)。
三、为什么它没崩:展开 vs 成员访问
这是本轮最值得留下的一条经验。同一个"已被移除的 API",两种用法结局完全不同:
| 写法 | 结果 |
|---|---|
{...Text.propTypes}(展开) | {...undefined} ⇒ 合法,得到 {} ⇒ 不崩(只是少了一批 propTypes 校验) |
style: ViewPropTypes.style(成员访问) | undefined.style ⇒ 当场抛 TypeError ⇒ 加载即崩 |
本库用的是前者:
const defaultParseShape = PropTypes.shape({ ...Text.propTypes, … });
static propTypes = { ...Text.propTypes, parse: …, childrenProps: … };
实测:
typeof RN.Text.propTypes = undefined
{...undefined} = {}
⇒ 结论:判断一个老库能不能跑,不能只看"有没有出现被移除的名字",还要看它是"展开"还是"取成员"。
推论:
...X/Object.keys(X)/ 把X当值传下去 ⇒ 一般安全;
X.something/new X()/X()⇒ 一旦X是undefined就完蛋。
四、可直接单测的两个东西
4.1 PATTERNS(导出的三个正则)
实测的匹配规律(直接 test):
| 正则 | 命中 | 不命中 |
|---|---|---|
PATTERNS.url | https://atomgit.com/CPF-RN ✓、www.example.com ✓ | example.com(裸域名)✗ |
PATTERNS.phone | 13800138000 ✓、(021) 123-4567 ✓ | (021) 1234-5678 ✗(这个正则是 3+3+4…7 位,第 4 位后不允许分隔符) |
PATTERNS.email | hi@example.com ✓ | no-at-here ✗ |
url.exec 还能给出精确的匹配位置:在 '见 https://atomgit.com/CPF-RN 结束' 上 ⇒ 匹配到 'https://atomgit.com/CPF-RN',index = 2 ✓
⇒ 用之前先确认正则的"口味":本库的 url 不认裸域名(要显式写 http(s):// 或 www.)。
4.2 TextExtraction(切分逻辑)
见开头的表格。补充两条容易踩的:
- pattern 的顺序有意义:先匹配上的会"吃掉"那段文本,后面的 pattern 不会再切它;
- 函数型的字段会被包装成
(text, index) => …,所以onPress的签名是(被匹配的文本, 下标),不是原始事件对象。
五、实测结果
5.1 断言
| 组 | 内容 | 结果 |
|---|---|---|
A 环境与 PATTERNS | Text.propTypes 为 undefined 且展开安全、导出形态、3 个正则的存在性与匹配规律、url.exec 的子串与下标 | 20/20 |
B TextExtraction 单测 | 切分 / 空段丢弃 / 第一个 pattern 获胜 / 函数包装 / renderText / 限额 / 空 patterns / /g 正则 | 21/21 |
| C 渲染与交互 | 页面渲染未抛错、标题安全区、点击回调(onPress(text, index)) | 4/5(点击那条因"断言先于点击执行"计 0 次;回调日志已单独取得) |
关键观测值:
typeof RN.Text.propTypes = undefined
{...undefined} = {}
PATTERNS 的键 = email, phone, url
url.exec(长句内) = "https://atomgit.com/CPF-RN" index=2
① parse('见 https://a.com 结束') ⇒ ["见 ","https://a.com"," 结束"]
④ onPress 收到的参数 = {"text":"https://a.com","index":3}
⑤ renderText ⇒ children = "[13字符]"
⑨ 'x1 x2 x3' + /x\d/g ⇒ 5 段(x1 / ' ' / x2 / ' ' / x3)
CLICK url text=https://atomgit.com/CPF-RN index=3
5.2 渲染
R1(有 parse) "官网 https://atomgit.com/CPF-RN 邮箱 hi@example.com 电话 13800138000 结束"
R2(无 parse,对照)同上文本(不切分)
R3(renderText) "A [链接:atomgit.com] B [邮箱] C"
⇒ R3 的改写直接可读:renderText 把两处匹配分别换成了 [链接:atomgit.com] 与 [邮箱] ✓
⇒ ⚠️ R1 与 R2 在 dump 里文本相同(dump 只给文本、不给颜色),所以"是否切分"这个差异要靠点击回调来证明(点了 R1 的链接会触发 onPress,R2 不会)✓



六、本轮我自己的三个失误(都不是库的问题)
| # | 失误 | 现象 | 纠正 |
|---|---|---|---|
| 1 | 在 JSX 文本位置直接写了 {...undefined} | Babel 报 Spread children are not supported in React. ⇒ 打包失败 ✗ | 改成表达式 {'{...undefined}'} ✓ |
| 2 | phone 的测试串选错 | 我用 (021) 1234-5678 期望匹配,实际不匹配 ⇒ 挂 1 条 | 该正则是 3+3+4…7 位 ⇒ 改用 (021) 123-4567,并把这个形态差异也写成断言 |
| 3 | /g 正则的段数算错 | 我期望 4 段,实际 5 段 | 漏算了段间的两个单空格段 ⇒ 改成 5,并加一条"匹配段与空格段交替"的断言 |
第 1 条尤其值得记:JSX 的文本位置不能直接出现
{...xxx}—— Babel 会把它解析成 spread child 并直接报错。
想显示这段字面量,要包成字符串表达式({'{...x}'})或用实体转义。
七、已知限制与本次验证边界
7.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
| ★ 可以直接用 | Text.propTypes 只是被展开,{...undefined} ⇒ {} ⇒ 不崩 |
★ url 不认裸域名 | 只认 http(s):// 或 www. 开头 |
★ phone 是 3+3+4…7 位 | (021) 123-4567 ✓ / (021) 1234-5678 ✗ |
| ★ pattern 顺序有意义 | 先匹配上的那段不会再被后面的 pattern 切 |
★ onPress 签名是 (被匹配的文本, 下标) | 不是事件对象;下标是匹配段在当前剩余串中的位置(单 pattern、首次通过时等于全局下标) |
★ renderText(text, matches) 的返回值成为该段文本 | 且 renderText 本身不会留在 props 里 |
★ getPatterns() 遇未知 type 会 throw | `${type} is not a supported type` |
★ getParsedText() 的两个"原样返回"分支 | 没有 parse、或 children 不是字符串 ⇒ 直接返回 children |
childrenProps 透传给每个子 Text | style 会与父 style 叠加([parentStyle, style]) |
setNativeProps 走外层 <Text> 的 ref | this._root.setNativeProps(...) |
7.2 本次验证的边界
getPatterns()的 throw 未复现(会整屏红屏,故意不触发);setNativeProps未验证;childrenProps的透传只做了源码级确认(传了allowFontScaling: false,但没验证它真的到了每个子 Text 上);nonExhaustiveModeMaxMatchCount只测了 2 与不传两种;url正则的复杂分支(query 参数、xn--国际化域名等)未逐项验证;- 多 pattern 嵌套/重叠的复杂场景未穷举(只验证了"第一个获胜"这一条);
R1与R2的差异只靠点击证明(dump 里文本相同,颜色差异未做像素核对);- 未测试长文本/大量匹配的性能;
- 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机;未做跨平台对照。
未改动库代码。
八、常见问题
Q1:它在 RN 0.84 上能跑吗?
能。 它确实用了已被移除的 Text.propTypes,但只是展开:
static propTypes = { ...Text.propTypes, parse: …, childrenProps: … };
{...undefined} 在 ES2018 里合法(得到 {})⇒ 不抛错。实测页面渲染与点击回调都正常 ✓
Q2:怎么快速判断一个老库会不会"加载即崩"?
看它拿被移除的 API 做什么:
| 用法 | 结论 |
|---|---|
...X、把 X 当值传、Object.keys(X) | 一般安全(undefined 只是"少东西") |
X.prop、X()、new X() | 危险 —— undefined 上做这些必抛 |
Q3:为什么我的裸域名 example.com 不会被识别成链接?
因为 PATTERNS.url 要求显式协议或 www.:
/(https?:\/\/|www\.)[-a-zA-Z0-9@:%._\+~#=]{1,256}\.(xn--)?[a-z0-9-]{2,20}\b…/i
要识别裸域名,得自己传一个自定义 pattern:
<ParsedText parse={[{pattern: /(?:https?:\/\/|www\.)?[-a-z0-9]+\.[a-z]{2,}/i, style: {color: 'blue'}}]} … />
Q4:电话号为什么有时不匹配?
PATTERNS.phone 的形状是 3 + 3 + 4…7 位:13800138000 ✓、(021) 123-4567 ✓,而 (021) 1234-5678 ✗(第 4 位之后不允许出现分隔符)。
Q5:onPress 的第二个参数是什么?
是下标((text, index) => …)。实测点第一个链接拿到 {text:'https://atomgit.com/CPF-RN', index:3} —— 3 正是 "官网 " 之后的偏移。
Q6:两个 pattern 都能匹配同一段文本时会怎样?
先匹配上的那个获胜 —— 它在内部被标记为 _matched,后面的 pattern 会跳过这段。
Q7:想改掉匹配段显示的文本?
用 renderText:
<ParsedText parse={[{type: 'url', renderText: t => `[${t.split('/')[2]}]`}]}>…</ParsedText>
实测 A https://atomgit.com/CPF-RN B hi@example.com C 会被渲染成 A [链接:atomgit.com] B [邮箱] C ✓
Q8:这个库需要鸿蒙侧做什么特殊处理吗?
不需要。 纯 JS、零额外依赖、无平台分支、无原生代码 —— 直接装、直接用 ✓
小结
react-native-parsed-text 是纯 JS 的文本切分组件(src/ 源码直发,仅依赖已有的 prop-types)。它在 RN 0.84 + React 19 上能直接用,纯逻辑部分 41 / 41 断言全通过,交互回调实测成功。
最值得记下来的是这几条:
-
★★★ “用了已移除的 API” ≠ “加载即崩” —— 关键看用法。
本库用了Text.propTypes,但只是展开:{...undefined}在 ES2018 里合法 ⇒ 得到{}⇒ 不崩。
对比:成员访问(ViewPropTypes.style)会当场抛Cannot read property '…' of undefined。
⇒ 审老库时的判断规则:...X一般安全;X.prop/X()/new X()危险。 -
★★ 把业务逻辑抽成独立类的库最好验证。
TextExtraction可以直接new出来跑:切分、空段丢弃、第一个 pattern 获胜、函数被包装成(text, index)、renderText改写、nonExhaustiveMaxMatchCount限额、/g正则的段分布 —— 一共 21 条断言,全部不依赖渲染 ✓ -
★★ 导出的正则可以直接单测,而且要先确认"口味"。
PATTERNS.url不认裸域名(只认http(s):///www.);PATTERNS.phone是 3+3+4…7 位((021) 123-4567✓,(021) 1234-5678✗)。
⇒ 用之前先test一下,别按直觉写业务。 -
★★ 一个罕见的编译坑:JSX 文本位置不能直接写
{...undefined}。
Babel 会把它当成 spread child,报Spread children are not supported in React.⇒ 整个 bundle 打包失败。
想显示这段字面量要包成{'{...undefined}'}。 -
★ 两个退出码都要看的老坑又救了一次。 打包
exit=1而 hvigor 仍BUILD SUCCESSFUL(用的是上一次的产物)—— 这次因为我同时检查了两者,没有装错版本 ✓ -
★ 依赖情况:只需要
prop-types(宿主已有)⇒ 没有依赖坑,也没有原生代码、没有平台分支 —— 属于接入成本最低的一类库。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-parsed-text(上游 0.0.22 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-parsed-text |
| 适配 TAG | 0.0.22-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS) |
| 库类型 | 纯 JS 组件库:把字符串按正则切成多个可交互 <Text> |
| 运行时依赖 | ✅ 仅 prop-types(宿主一般已有) |
prepare / postinstall | ✅ 都没有 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/taskrabbit/react-native-parsed-text(★1247) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = ParsedTextTestApp) |
# 从适配仓库安装(只需 prop-types)
npm install "git+https://atomgit.com/oh-react-native/react-native-parsed-text.git#0.0.22-ohos-1.0.0" --ignore-scripts
# 本地目录接入
npm install ../react-native-parsed-text --ignore-scripts
import ParsedText, {PATTERNS} from 'react-native-parsed-text';
// ① 基本用法:三种内置 pattern
<ParsedText
style={{fontSize: 15}}
parse={[
{type: 'url', style: {color: '#0A84FF'}, onPress: (text, index) => console.log('URL', text, index)},
{type: 'email', style: {color: '#0A7D2C'}, onPress: (text, index) => console.log('EMAIL', text, index)},
{type: 'phone', style: {color: '#E67E22'}, onPress: (text, index) => console.log('PHONE', text, index)},
]}
>
{'官网 https://atomgit.com/CPF-RN 邮箱 hi@example.com 电话 13800138000'}
</ParsedText>
// ② 自定义正则(例如想识别裸域名 —— 内置的 url 不认它)
<ParsedText parse={[{pattern: /[-a-z0-9]+\.[a-z]{2,}/i, style: {color: 'blue'}}]}>…</ParsedText>
// ③ 用 renderText 改写匹配段显示的文本
<ParsedText parse={[{type: 'url', renderText: t => `[${t.split('/')[2]}]`}]}>…</ParsedText>
// ④ 限额(只匹配前 N 次)
<ParsedText parse={[{type: 'url', nonExhaustiveModeMaxMatchCount: 2}]}>…</ParsedText>
// ⚠️ 内置 url 只认 http(s):// 或 www.;phone 是 3+3+4..7 位
// ⚠️ onPress 的签名是 (被匹配的文本, 下标),不是事件对象
// ⚠️ 要自己单测切分逻辑时可直接 new:import TextExtraction from 'react-native-parsed-text/src/lib/TextExtraction'
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey ParsedTextTestApp
判据的复现方式
# ① 老 API 预扫要区分"展开"与"取成员"
# ...Text.propTypes / ...ViewPropTypes ⇒ 安全(undefined 展开成 {})
# ViewPropTypes.style / X() / new X() ⇒ 危险(必抛)
# ② 直接单测切分逻辑(不需要渲染)
# const te = new TextExtraction('见 https://a.com 结束', [{pattern: /https?:\/\/\S+/}]);
# te.parse() ⇒ 逐段核对 children / style / 函数字段
# ③ 正则口味先 test 一遍
# PATTERNS.url.test('example.com') ⇒ false(裸域名不认)
# PATTERNS.phone.test('(021) 1234-5678') ⇒ false(3+3+4..7 位)
# ④ 交互:点被匹配的片段,读 onPress(text, index) 的日志
hdc shell "uitest uiInput click <x> <y>"
hdc shell "hilog -x | grep parsedtext-test"
# ⑤ 注意:JSX 文本位置直接写 {...x} 会被 Babel 当成 spread child ⇒ 打包失败
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.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 |
| 宿主依赖 | ✅ 无需补装(只需要已有的 prop-types) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 3 分 49 秒(打包失败,未装机)⇒ 3 分 35 秒(41/44)⇒ 3 分 56 秒 ⇒ 纯逻辑 41/41 |
| 验证规模 | 设备侧 A 20 + B 21 = 41 项全通;交互回调 1 次(onPress(text, index));C 组 4/5(时序问题,证据已单独取得);4 张截图互异;正则直测 8 项;TextExtraction 单测 21 项 |
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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
更多推荐



所有评论(0)