本文记录把 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✅ 都没有
mainsrc/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.urlhttps://atomgit.com/CPF-RN ✓、www.example.com ✓example.com(裸域名)✗
PATTERNS.phone13800138000 ✓、(021) 123-4567 ✓(021) 1234-5678 ✗(这个正则是 3+3+4…7 位,第 4 位后不允许分隔符)
PATTERNS.emailhi@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 环境与 PATTERNSText.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}'} ✓
2phone 的测试串选错我用 (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 透传给每个子 Textstyle 会与父 style 叠加([parentStyle, style])
setNativeProps 走外层 <Text> 的 refthis._root.setNativeProps(...)

7.2 本次验证的边界

  1. getPatterns() 的 throw 未复现(会整屏红屏,故意不触发);
  2. setNativeProps 未验证;
  3. childrenProps 的透传只做了源码级确认(传了 allowFontScaling: false,但没验证它真的到了每个子 Text 上);
  4. nonExhaustiveModeMaxMatchCount 只测了 2 与不传两种;
  5. url 正则的复杂分支(query 参数、xn-- 国际化域名等)未逐项验证;
  6. 多 pattern 嵌套/重叠的复杂场景未穷举(只验证了"第一个获胜"这一条);
  7. R1 与 R2 的差异只靠点击证明(dump 里文本相同,颜色差异未做像素核对);
  8. 未测试长文本/大量匹配的性能;
  9. 只在一台模拟器上验证(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 断言全通过,交互回调实测成功。

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

  1. ★★★ “用了已移除的 API” ≠ “加载即崩” —— 关键看用法。
    本库用了 Text.propTypes,但只是展开:{...undefined} 在 ES2018 里合法 ⇒ 得到 {} ⇒ 不崩。
    对比:成员访问(ViewPropTypes.style)会当场抛 Cannot read property '…' of undefined。
    ⇒ 审老库时的判断规则:...X 一般安全;X.prop / X() / new X() 危险。

  2. ★★ 把业务逻辑抽成独立类的库最好验证。
    TextExtraction 可以直接 new 出来跑:切分、空段丢弃、第一个 pattern 获胜、函数被包装成 (text, index)、renderText 改写、nonExhaustiveMaxMatchCount 限额、/g 正则的段分布 —— 一共 21 条断言,全部不依赖渲染 ✓

  3. ★★ 导出的正则可以直接单测,而且要先确认"口味"。
    PATTERNS.url 不认裸域名(只认 http(s):// / www.);PATTERNS.phone 是 3+3+4…7 位((021) 123-4567 ✓,(021) 1234-5678 ✗)。
    ⇒ 用之前先 test 一下,别按直觉写业务。

  4. ★★ 一个罕见的编译坑:JSX 文本位置不能直接写 {...undefined}。
    Babel 会把它当成 spread child,报 Spread children are not supported in React. ⇒ 整个 bundle 打包失败。
    想显示这段字面量要包成 {'{...undefined}'}。

  5. ★ 两个退出码都要看的老坑又救了一次。 打包 exit=1 而 hvigor 仍 BUILD SUCCESSFUL(用的是上一次的产物)—— 这次因为我同时检查了两者,没有装错版本 ✓

  6. ★ 依赖情况:只需要 prop-types(宿主已有)⇒ 没有依赖坑,也没有原生代码、没有平台分支 —— 属于接入成本最低的一类库。


本篇用到的库

项内容
三方库react-native-parsed-text(上游 0.0.22 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-parsed-text
适配 TAG0.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 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
宿主依赖✅ 无需补装(只需要已有的 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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐