Text组件的enableDataDetector如何让链接在App内打开,而不是跳转系统浏览器?
·
Text组件的enableDataDetector如何让链接在App内打开,而不是跳转系统浏览器?思路是利用dataDetectorConfig提供的onDetectResultUpdate回调拿到链接高亮信息,然后手动设置链接的点击事件。
实现代码:
import { promptAction } from '@kit.ArkUI'
const logTag = "[LinkHighlight]"
interface RecognizeResult {
code:number
entity:RecognizeResultEntity[]
}
interface RecognizeResultEntity {
start:number
end:number
entityContent:string
entityType:string
}
@Entry
@ComponentV2
struct Index {
@Local message: string = 'ArkUI 是一套构建分布式应用界面的声明式 UI 开发框架。它的官网是:https://developer.huawei.com/consumer/cn/arkui/。它使用简洁的 UI 信息语法、丰富的 UI 组件、以及实时界面预览工具,帮助你提升 HarmonyOS 应用界面开发效率。你只需使用一套 ArkTS API,就能在多个 HarmonyOS 设备上提供生动而流畅的用户界面体验。';
@Local
private isEnableDataDetector:boolean = true
@Local
private controller: TextController = new TextController()
build() {
RelativeContainer() {
// 关键点0:设置TextController
Text(this.message,{ controller:this.controller })
.enableDataDetector(this.isEnableDataDetector)
.dataDetectorConfig({
types: [TextDataDetectorType.URL],
onDetectResultUpdate: (resultJSONString:string) => {
console.debug(logTag,"收到dataDetectorConfig的onDetectResultUpdate回调")
// 不保证这个结构总是这样,所以要try-catch
try {
const structuralResult:RecognizeResult = JSON.parse(resultJSONString) as RecognizeResult
if (structuralResult.code === 0) {
console.debug(logTag,"识别成功")
structuralResult.entity.forEach((entity) => {
if (entity.entityType === "url") {
const start:number = entity.start
const length:number = entity.end-entity.start
const urlString:string = entity.entityContent
const urlStylePartA:StyleOptions = {
start:start,
length:length,
styledKey: StyledStringKey.FONT,
styledValue: new TextStyle({
fontColor: "#5CBD6C"
})
}
const urlStylePartB:StyleOptions = {
start:start,
length:length,
styledKey: StyledStringKey.DECORATION,
styledValue: new DecorationStyle({
type:TextDecorationType.Underline,
color: "#5CBD6C",
style:TextDecorationStyle.SOLID
})
}
// 关键点1:设置链接部分的文本可以被点击
const urlClickAction:StyleOptions = {
start:start,
length:length,
styledKey: StyledStringKey.GESTURE,
styledValue: new GestureStyle({
onClick:() => {
promptAction.showToast({ message:`用户点击了链接(${urlString}),您这时候可以推出App内的网页浏览页面` })
}
})
}
this.controller.setStyledString(new StyledString(this.message,[urlStylePartA,urlStylePartB,urlClickAction]))
}
})
} else {
console.debug(logTag,"链接识别失败")
}
} catch (error) {
console.debug(logTag,"链接识别出错")
}
// 因为我们已经自行处理了链接的点击事件,我们把DataDetector的默认链接点击处理(在系统浏览器打开)给禁用掉。
this.isEnableDataDetector = false
}
})
// 注意:目前的实现中不会响应文本内容的更变,需要响应文本内容的更变,请看本文中的下一个代码块
}
.height('100%')
.width('100%')
}
}
上面的实现支持了静态文本的链接高亮(不支持更新文本内容),下面的实现支持了更改文本内容后,自动刷新文本和重新计算链接高亮。
import { promptAction } from '@kit.ArkUI'
const logTag = "[LinkHighlight]"
interface RecognizeResult {
code: number
entity: RecognizeResultEntity[]
}
interface RecognizeResultEntity {
start: number
end: number
entityContent: string
entityType: string
}
@Entry
@ComponentV2
struct Index {
@Local message: string =
'ArkUI 是一套构建分布式应用界面的声明式 UI 开发框架。它的官网是:https://developer.huawei.com/consumer/cn/arkui/。它使用简洁的 UI 信息语法、丰富的 UI 组件、以及实时界面预览工具,帮助你提升 HarmonyOS 应用界面开发效率。你只需使用一套 ArkTS API,就能在多个 HarmonyOS 设备上提供生动而流畅的用户界面体验。';
@Local
private isEnableDataDetector: boolean = true
@Local
private controller: TextController = new TextController()
// 在文本变更的时候触发重新计算链接高亮
@Monitor("message")
private onMessageChange() {
// 因为刚才setStyledString了,为确保显示的文本正常刷新,应重新设置StyledString
this.controller.setStyledString(new StyledString(this.message))
// 重新打开dataDetector,它会重新计算链接高亮,并再次调用onDetectResultUpdate回调
this.isEnableDataDetector = true
}
build() {
RelativeContainer() {
// 关键点0:设置TextController
Text(this.message, { controller: this.controller })
.enableDataDetector(this.isEnableDataDetector)
.dataDetectorConfig({
types: [TextDataDetectorType.URL],
onDetectResultUpdate: (resultJSONString: string) => {
console.debug(logTag, "收到dataDetectorConfig的onDetectResultUpdate回调")
// 不保证这个结构总是这样,所以要try-catch
try {
const structuralResult: RecognizeResult = JSON.parse(resultJSONString) as RecognizeResult
if (structuralResult.code === 0) {
console.debug(logTag, "识别成功")
structuralResult.entity.forEach((entity) => {
if (entity.entityType === "url") {
const start: number = entity.start
const length: number = entity.end - entity.start
const urlString: string = entity.entityContent
const urlStylePartA: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.FONT,
styledValue: new TextStyle({
fontColor: "#5CBD6C"
})
}
const urlStylePartB: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.DECORATION,
styledValue: new DecorationStyle({
type: TextDecorationType.Underline,
color: "#5CBD6C",
style: TextDecorationStyle.SOLID
})
}
// 关键点1:设置链接部分的文本可以被点击
const urlClickAction: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.GESTURE,
styledValue: new GestureStyle({
onClick: () => {
promptAction.showToast({
message: `用户点击了链接(${urlString}),您这时候可以推出App内的网页浏览页面`
})
}
})
}
this.controller.setStyledString(new StyledString(this.message,
[urlStylePartA, urlStylePartB, urlClickAction]))
}
})
} else {
console.debug(logTag, "链接识别失败")
}
} catch (error) {
console.debug(logTag, "链接识别出错")
}
// 因为我们已经自行处理了链接的点击事件,我们把DataDetector的默认链接点击处理(在系统浏览器打开)给禁用掉。
this.isEnableDataDetector = false
}
})
}
.height('100%')
.width('100%')
.onAppear(() => {
// 模拟2.3秒后文本内容发生更变
setTimeout(() => {
console.debug(logTag, "已更改文本内容")
this.message =
"您可以点此查阅ArkUI的文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/application-dev-guide。感谢您的使用。"
}, 2300)
})
}
}
稍作改进,也可以实现:点击链接,先弹窗确认“您即将跳转到外部网站”,再拉起浏览器。
import { promptAction } from '@kit.ArkUI'
import { common, Want } from '@kit.AbilityKit'
const logTag = "[LinkHighlight]"
interface RecognizeResult {
code: number
entity: RecognizeResultEntity[]
}
interface RecognizeResultEntity {
start: number
end: number
entityContent: string
entityType: string
}
@Entry
@ComponentV2
struct Index {
@Local message: string =
'ArkUI 是一套构建分布式应用界面的声明式 UI 开发框架。它的官网是:https://developer.huawei.com/consumer/cn/arkui/。它使用简洁的 UI 信息语法、丰富的 UI 组件、以及实时界面预览工具,帮助你提升 HarmonyOS 应用界面开发效率。你只需使用一套 ArkTS API,就能在多个 HarmonyOS 设备上提供生动而流畅的用户界面体验。';
@Local
private isEnableDataDetector: boolean = true
@Local
private controller: TextController = new TextController()
// 在文本变更的时候触发重新计算链接高亮
@Monitor("message")
private onMessageChange() {
// 因为刚才setStyledString了,为确保显示的文本正常刷新,应重新设置StyledString
this.controller.setStyledString(new StyledString(this.message))
// 重新打开dataDetector,它会重新计算链接高亮,并再次调用onDetectResultUpdate回调
this.isEnableDataDetector = true
}
@Local
private selectedURL:string | null = null
@Local
private showConfirm:boolean = false
build() {
RelativeContainer() {
// 关键点0:设置TextController
Text(this.message, { controller: this.controller })
.enableDataDetector(this.isEnableDataDetector)
.dataDetectorConfig({
types: [TextDataDetectorType.URL],
onDetectResultUpdate: (resultJSONString: string) => {
console.debug(logTag, "收到dataDetectorConfig的onDetectResultUpdate回调")
// 不保证这个结构总是这样,所以要try-catch
try {
const structuralResult: RecognizeResult = JSON.parse(resultJSONString) as RecognizeResult
if (structuralResult.code === 0) {
console.debug(logTag, "识别成功")
structuralResult.entity.forEach((entity) => {
if (entity.entityType === "url") {
const start: number = entity.start
const length: number = entity.end - entity.start
const urlString: string = entity.entityContent
const urlStylePartA: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.FONT,
styledValue: new TextStyle({
fontColor: "#5CBD6C"
})
}
const urlStylePartB: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.DECORATION,
styledValue: new DecorationStyle({
type: TextDecorationType.Underline,
color: "#5CBD6C",
style: TextDecorationStyle.SOLID
})
}
// 关键点1:设置链接部分的文本可以被点击
const urlClickAction: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.GESTURE,
styledValue: new GestureStyle({
onClick: () => {
this.selectedURL = urlString
this.showConfirm = true
}
})
}
this.controller.setStyledString(new StyledString(this.message,
[urlStylePartA, urlStylePartB, urlClickAction]))
}
})
} else {
console.debug(logTag, "链接识别失败")
}
} catch (error) {
console.debug(logTag, "链接识别出错")
}
// 因为我们已经自行处理了链接的点击事件,我们把DataDetector的默认链接点击处理(在系统浏览器打开)给禁用掉。
this.isEnableDataDetector = false
}
})
}
.height('100%')
.width('100%')
.bindMenu(!!this.showConfirm,[{
value:"是",
action:() => {
// 拉起系统浏览器
let context = getContext(this) as common.UIAbilityContext;
let wantInfo: Want = {
action: 'ohos.want.action.viewData',
entities: ['entity.system.browsable'],
uri: this.selectedURL
}
context.startAbility(wantInfo).then(() => {
console.debug(logTag,"已打开")
}).catch((err:Error) => {
console.debug(logTag,"出错了")
})
}
},{
value:"否",
action:() => {}
}],{title:`是否跳转到外部链接${this.selectedURL}?`,onDisappear:() => { this.showConfirm = false }})
.onAppear(() => {
// 模拟2.3秒后文本内容发生更变
setTimeout(() => {
console.debug(logTag, "已更改文本内容")
this.message =
"我们可以点此查阅ArkUI的文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/application-dev-guide。感谢您的使用。"
}, 2300)
})
}
}
实现:点击部分链接可以直接在App内打开(比如来自“mydomain.cn”的链接),其它链接则在外部浏览器打开。
import { promptAction } from '@kit.ArkUI'
import { common, Want } from '@kit.AbilityKit'
const logTag = "[LinkHighlight]"
interface RecognizeResult {
code: number
entity: RecognizeResultEntity[]
}
interface RecognizeResultEntity {
start: number
end: number
entityContent: string
entityType: string
}
@Entry
@ComponentV2
struct Index {
@Local message: string =
'ArkUI 是一套构建分布式应用界面的声明式 UI 开发框架。它的官网是:https://developer.huawei.com/consumer/cn/arkui/。它使用简洁的 UI 信息语法、丰富的 UI 组件、以及实时界面预览工具,帮助你提升 HarmonyOS 应用界面开发效率。你只需使用一套 ArkTS API,就能在多个 HarmonyOS 设备上提供生动而流畅的用户界面体验。';
@Local
private isEnableDataDetector: boolean = true
@Local
private controller: TextController = new TextController()
// 在文本变更的时候触发重新计算链接高亮
@Monitor("message")
private onMessageChange() {
// 因为刚才setStyledString了,为确保显示的文本正常刷新,应重新设置StyledString
this.controller.setStyledString(new StyledString(this.message))
// 重新打开dataDetector,它会重新计算链接高亮,并再次调用onDetectResultUpdate回调
this.isEnableDataDetector = true
}
build() {
RelativeContainer() {
// 关键点0:设置TextController
Text(this.message, { controller: this.controller })
.enableDataDetector(this.isEnableDataDetector)
.dataDetectorConfig({
types: [TextDataDetectorType.URL],
onDetectResultUpdate: (resultJSONString: string) => {
console.debug(logTag, "收到dataDetectorConfig的onDetectResultUpdate回调")
// 不保证这个结构总是这样,所以要try-catch
try {
const structuralResult: RecognizeResult = JSON.parse(resultJSONString) as RecognizeResult
if (structuralResult.code === 0) {
console.debug(logTag, "识别成功")
structuralResult.entity.forEach((entity) => {
if (entity.entityType === "url") {
const start: number = entity.start
const length: number = entity.end - entity.start
const urlString: string = entity.entityContent
const urlStylePartA: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.FONT,
styledValue: new TextStyle({
fontColor: "#5CBD6C"
})
}
const urlStylePartB: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.DECORATION,
styledValue: new DecorationStyle({
type: TextDecorationType.Underline,
color: "#5CBD6C",
style: TextDecorationStyle.SOLID
})
}
// 关键点1:设置链接部分的文本可以被点击
const urlClickAction: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.GESTURE,
styledValue: new GestureStyle({
onClick: () => {
if (isMyDomain(urlString)) {
// 是本App域名,在App内打开
promptAction.showToast({ message:"用户点击了本App域名,在App内打开:"+urlString })
} else {
// 非本App域名,拉起系统浏览器
let context = getContext(this) as common.UIAbilityContext;
let wantInfo: Want = {
action: 'ohos.want.action.viewData',
entities: ['entity.system.browsable'],
uri: urlString
}
context.startAbility(wantInfo).then(() => {
console.debug(logTag,"已打开")
}).catch((err:Error) => {
console.debug(logTag,"出错了")
})
}
}
})
}
this.controller.setStyledString(new StyledString(this.message,
[urlStylePartA, urlStylePartB, urlClickAction]))
}
})
} else {
console.debug(logTag, "链接识别失败")
}
} catch (error) {
console.debug(logTag, "链接识别出错")
}
// 因为我们已经自行处理了链接的点击事件,我们把DataDetector的默认链接点击处理(在系统浏览器打开)给禁用掉。
this.isEnableDataDetector = false
}
})
}
.height('100%')
.width('100%')
.onAppear(() => {
// 模拟2.3秒后文本内容发生更变
setTimeout(() => {
console.debug(logTag, "已更改文本内容")
this.message =
"如果是本App的域名:https://mydomain.cn/hello-world。那我们就在App内打开。"
}, 2300)
})
}
}
// console.log(isMyDomain("https://mydomain.cn")); // true
// console.log(isMyDomain("http://www.mydomain.cn")); // true
// console.log(isMyDomain("api.mydomain.cn:8080")); // true
// console.log(isMyDomain("mydomain.cn/path/to/page")); // true
// console.log(isMyDomain("http://notmydomain.cn")); // false
// console.log(isMyDomain("http://fakemydomain.cn")); // false
// console.log(isMyDomain("http://mydomain.cn.evil.com")); // false
// console.log(isMyDomain("http://a.b.mydomain.cn")); // true
// console.log(isMyDomain("evil.mydomain.cn.evil.com")); // false
// console.log(isMyDomain("HTTP://WWW.MYDOMAIN.CN")); // true
function isMyDomain(rawUrl: string): boolean {
const domain = 'mydomain.cn';
// 去掉协议(http://, https://)
let url = rawUrl.trim().toLowerCase();
url = url.replace(/^https?:\/\//, '');
// 只取主机部分,去掉端口、路径、查询参数
// 找第一个 `/`,只保留前面
const slashIndex = url.indexOf('/');
if (slashIndex !== -1) {
url = url.substring(0, slashIndex);
}
// 去掉端口
const colonIndex = url.indexOf(':');
if (colonIndex !== -1) {
url = url.substring(0, colonIndex);
}
// 现在 url 应该就是个 hostname
// 先检查是不是 mydomain.cn 本身
if (url === domain) {
return true;
}
// 再检查是不是以 .mydomain.cn 结尾
if (url.endsWith('.' + domain)) {
// 额外防御:防止 evil.mydomain.cn.evil.com 这种
const rest = url.slice(0, url.length - domain.length - 1); // 去掉 .mydomain.cn
if (rest.indexOf('.') === -1) {
// rest 不能是空,但也不能包含更多的点(防止 a.b.mydomain.cn)
return true;
}
if (rest.length > 0) {
return true;
}
}
return false;
}
实现:只对白名单中的链接高亮。
import { promptAction } from '@kit.ArkUI'
import { common, Want } from '@kit.AbilityKit'
const logTag = "[LinkHighlight]"
interface RecognizeResult {
code: number
entity: RecognizeResultEntity[]
}
interface RecognizeResultEntity {
start: number
end: number
entityContent: string
entityType: string
}
@Entry
@ComponentV2
struct Index {
@Local message: string = "这是一个不可信域名:http://nottrusted.cn。我们不会高亮它。"
@Local
private isEnableDataDetector: boolean = true
@Local
private controller: TextController = new TextController()
// 在文本变更的时候触发重新计算链接高亮
@Monitor("message")
private onMessageChange() {
// 因为刚才setStyledString了,为确保显示的文本正常刷新,应重新设置StyledString
this.controller.setStyledString(new StyledString(this.message))
// 重新打开dataDetector,它会重新计算链接高亮,并再次调用onDetectResultUpdate回调
this.isEnableDataDetector = true
}
@Local
private trustedDomains:string[] = ['mydomain.cn', 'trusted.cn', 'secure.site']
build() {
RelativeContainer() {
// 关键点0:设置TextController
Text(this.message, { controller: this.controller })
.enableDataDetector(this.isEnableDataDetector)
.dataDetectorConfig({
types: [TextDataDetectorType.URL],
onDetectResultUpdate: (resultJSONString: string) => {
console.debug(logTag, "收到dataDetectorConfig的onDetectResultUpdate回调")
// 不保证这个结构总是这样,所以要try-catch
try {
const structuralResult: RecognizeResult = JSON.parse(resultJSONString) as RecognizeResult
if (structuralResult.code === 0) {
console.debug(logTag, "识别成功")
structuralResult.entity.forEach((entity) => {
if (entity.entityType === "url") {
const urlString: string = entity.entityContent
if (isTrustedDomain(urlString,this.trustedDomains)) {
// 可信域名,高亮
const start: number = entity.start
const length: number = entity.end - entity.start
const urlStylePartA: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.FONT,
styledValue: new TextStyle({
fontColor: "#5CBD6C"
})
}
const urlStylePartB: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.DECORATION,
styledValue: new DecorationStyle({
type: TextDecorationType.Underline,
color: "#5CBD6C",
style: TextDecorationStyle.SOLID
})
}
// 关键点1:设置链接部分的文本可以被点击
const urlClickAction: StyleOptions = {
start: start,
length: length,
styledKey: StyledStringKey.GESTURE,
styledValue: new GestureStyle({
onClick: () => {
// 是可信域名,拉起系统浏览器
let context = getContext(this) as common.UIAbilityContext;
let wantInfo: Want = {
action: 'ohos.want.action.viewData',
entities: ['entity.system.browsable'],
uri: urlString
}
context.startAbility(wantInfo).then(() => {
console.debug(logTag,"已打开")
}).catch((err:Error) => {
console.debug(logTag,"出错了")
})
}
})
}
this.controller.setStyledString(new StyledString(this.message,
[urlStylePartA, urlStylePartB, urlClickAction]))
} else {
// 不可信域名,不高亮
}
}
})
} else {
console.debug(logTag, "链接识别失败")
}
} catch (error) {
console.debug(logTag, "链接识别出错")
}
// 因为我们已经自行处理了链接的点击事件,我们把DataDetector的默认链接点击处理(在系统浏览器打开)给禁用掉。
this.isEnableDataDetector = false
}
})
}
.height('100%')
.width('100%')
.onAppear(() => {
// 模拟2.3秒后文本内容发生更变
setTimeout(() => {
console.debug(logTag, "已更改文本内容")
this.message =
"如果是可信域名:https://mydomain.cn/hello-world。那我们就高亮。"
}, 2300)
})
}
}
// const trustedDomains = ['mydomain.cn', 'trusted.cn', 'secure.site'];
//
// console.log(isTrustedDomain("https://mydomain.cn", trustedDomains)); // true
// console.log(isTrustedDomain("http://www.mydomain.cn", trustedDomains)); // true
// console.log(isTrustedDomain("api.trusted.cn:8080", trustedDomains)); // true
// console.log(isTrustedDomain("secure.site/path?query=123", trustedDomains)); // true
// console.log(isTrustedDomain("http://nottrusted.cn", trustedDomains)); // false
// console.log(isTrustedDomain("http://evil.mydomain.cn.evil.com", trustedDomains)); // false
// console.log(isTrustedDomain("evil.secure.site.evil.com", trustedDomains)); // false
// console.log(isTrustedDomain("HTTP://WWW.TRUSTED.CN", trustedDomains)); // true
function isTrustedDomain(rawUrl: string, trustedDomains: string[]): boolean {
// 预处理一下,把可信域名都转小写,方便比对
const lowerTrustedDomains = trustedDomains.map(d => d.toLowerCase());
let url = rawUrl.trim().toLowerCase();
url = url.replace(/^https?:\/\//, '');
// 只取主机部分,去掉端口、路径、查询参数
const slashIndex = url.indexOf('/');
if (slashIndex !== -1) {
url = url.substring(0, slashIndex);
}
const colonIndex = url.indexOf(':');
if (colonIndex !== -1) {
url = url.substring(0, colonIndex);
}
for (const domain of lowerTrustedDomains) {
if (url === domain) {
return true;
}
if (url.endsWith('.' + domain)) {
// 防止 evil.domain.com
const rest = url.slice(0, url.length - domain.length - 1);
if (rest.length > 0) {
return true;
}
}
}
return false;
}
更多推荐


所有评论(0)