跨应用数据共享最危险的误区,是把它理解成“另一个应用能访问我的数据库”。真正合理的设计,应该是:数据提供方只暴露明确的 URI 和能力边界,访问方只通过 DataShareHelper 完成受控的增删改查。本文从一套笔记共享 Demo 出发,把这条链拆开。

一、先把一个边界说清楚:DataShare 不是“数据库暴露器”

第一次接触 DataShare 时,很容易把它想成“把本地 RDB 给别的应用查”。

这种理解非常危险。

真正的工程结构应该分成两侧:

  • 提供方:决定哪些数据能共享、哪些操作能开放;
  • 访问方:通过 URI 建立连接,并调用受控的 query / insert / update / delete。

也就是说,外部应用并不会直接拿到你的数据库对象。

它拿到的是一个数据服务入口。

这件事非常重要,因为一旦把这个边界想清楚,后面的权限、字段过滤、写入校验、变更通知就都顺了。

另外还有一个必须提前说明的现实约束:DataShareExtensionAbility 等部分接口属于系统能力范围。三方应用在真实项目中必须先确认当前 HarmonyOS 版本、应用类型和权限开放范围,不能只看示例代码就默认可以直接上线使用。

本文重点放在数据共享机制和工程设计,而不是绕过平台权限边界。

二、我会先设计 URI,再写任何一行 CRUD

做 DataShare 时,我现在第一步不是建表,而是设计 URI。

比如一个笔记应用要对外提供:

  • 笔记列表;
  • 单条笔记;
  • 标签数据;
  • 最近更新数据。

我会先把路由规划成类似:

datashare:///com.example.notes/notes
datashare:///com.example.notes/notes/1001
datashare:///com.example.notes/tags
datashare:///com.example.notes/recent

URI 的价值不是“看起来规范”,而是它天然就是共享边界。

外部访问方看到哪条 URI,就只应该得到那条 URI 对应的数据能力。

如果所有东西都塞进一个入口,再靠参数猜业务,后面权限控制会越来越乱。

三、提供方最重要的事情,是把业务规则挡在边界层

提供方通常会围绕 DataShareExtensionAbility 去实现自己的服务能力。

核心思路不是把数据库 CRUD 原样透传,而是要在扩展能力层先做判断:

  • 当前 URI 是否支持;
  • 调用方有没有权限;
  • 写入字段是否合法;
  • 删除是否允许;
  • 查询是否需要限制列;
  • 是否需要记录访问日志。

一个典型的 provider 结构,我更喜欢这样组织:

import {
  DataShareExtensionAbility,
  dataShare,
  dataSharePredicates,
  relationalStore,
  DataShareResultSet
} from '@kit.ArkData'

export default class NoteDataShareExt extends DataShareExtensionAbility {
  private store: relationalStore.RdbStore | null = null

  onCreate() {
    // 初始化数据库、校验配置、注册日志
  }

  async query(
    uri: string,
    predicates: dataSharePredicates.DataSharePredicates,
    columns: string[]
  ): Promise<DataShareResultSet> {
    // 1. 解析 URI
    // 2. 校验允许访问的列
    // 3. 根据业务规则执行查询
    // 4. 返回受控结果集
    throw new Error('demo')
  }
}

真正的项目里,我不会让页面代码直接承担这些判断。

DataShare 的核心价值,恰恰是把“共享”收口成一个边界层。

四、访问方的关键,不是 query,而是正确创建 Helper

访问方通常先根据 URI 创建 DataShareHelper。

import { dataShare, dataSharePredicates } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'

async function createHelper(context: common.Context) {
  const uri = 'datashare:///com.example.notes/notes'
  const helper = await dataShare.createDataShareHelper(context, uri)
  return { uri, helper }
}

这一步我会额外做两件事:

  • helper 创建失败时,不直接让页面崩;
  • 把 URI 和 helper 封装到 repository,而不是到处 new。

因为一旦业务开始变复杂,页面里直接操作 DataShareHelper 会非常难维护。

我的建议是做一层 SharedNoteRepository,把对外共享数据的访问全部收起来。

五、查询时不要“SELECT * 思维”

DataShare 支持谓词查询,工程上最有价值的地方,是你可以明确限制条件和列。

async function queryRecentNotes(
  helper: dataShare.DataShareHelper,
  uri: string
) {
  const predicates = new dataSharePredicates.DataSharePredicates()
  predicates.equalTo('status', 'active')

  const columns = ['id', 'title', 'updateTime']
  const resultSet = await helper.query(uri, predicates, columns)
  return resultSet
}

这里有一个很值得强调的习惯:共享接口返回的数据越少越好。

不是因为性能,而是因为边界。

对方只需要标题和更新时间,就不要顺手把正文、内部状态、同步标记一起带出去。

数据共享设计里,字段本身就是权限。

六、增删改查真正难的,是校验而不是调用

对于 insert / update / delete,调用代码往往不长。

真正难的是:你允许别人改什么?

比如一个共享笔记服务可以规定:

  • 访问方可以新增标题和正文;
  • 不允许写内部同步字段;
  • update 只能更新指定 ID;
  • delete 必须满足调用方身份和业务条件;
  • 某些字段只能提供方内部生成。

如果这些规则不放在 provider 里,DataShare 很容易变成一个过度开放的入口。

所以我会专门做字段白名单:

const writableFields = new Set(['title', 'content', 'tag'])

function sanitizeValues(values: Record<string, Object>) {
  const result: Record<string, Object> = {}
  Object.keys(values).forEach(key => {
    if (writableFields.has(key)) {
      result[key] = values[key]
    }
  })
  return result
}

这段代码技术含量不高,但工程价值非常高。

七、DevEco 联调时,我更关注“访问链”而不是页面

跨应用数据问题很容易陷入 UI 调试。

但这类问题真正要看的是访问链:

我会把日志分成四段:

  • helper 是否创建成功;
  • URI 是否匹配;
  • provider 是否收到请求;
  • 数据库操作是否完成。

如果还能再加一层,我会记录调用动作类型:query、insert、update、delete。

这样一旦出现“页面没有数据”,可以马上判断:

  • 是访问方根本没发起请求;
  • 还是 URI 不匹配;
  • 还是 provider 被权限挡住;
  • 还是数据库本身没查到。

八、为什么访问日志值得做成可视化页面

我很喜欢给这类能力做一个访问记录页。

页面可以直接展示:

  • 最近被读取的数据;
  • 哪个应用发起;
  • 操作类型;
  • 更新时间;
  • 成功或失败状态。

这样 DataShare 就不再是“后台黑盒”。

尤其是在调权限和共享范围时,可视化证据比单纯翻日志快很多。

九、外部应用数据页,应该把权限边界直接展示出来

第二个页面我通常会做成“外部数据访问”视角。

这个页面不要只展示结果,最好把来源和权限也显示出来:

  • 数据来自哪个应用;
  • 当前允许读取什么;
  • 哪些接口支持写;
  • 最近一次访问状态;
  • 是否订阅了数据变更。

因为真正的跨应用数据共享,不是“我能查到数据”这么简单,而是“我知道为什么能查到、查到什么、还能做什么”。

十、变更通知比轮询更重要

如果访问方要实时感知共享数据变化,最差的方式就是不断 query。

更合理的思路,是围绕 DataShare 的数据变更订阅机制建立更新链。

当提供方数据变化时,访问方收到事件,再刷新必要数据。

工程上这能减少:

  • 无意义查询;
  • 页面重复刷新;
  • 数据状态漂移;
  • 多端状态不同步的假象。

这里最重要的不是“有没有监听 API”,而是你的页面更新模型要支持事件驱动。

十一、几个必须提前想清楚的坑

1. DataShareExtensionAbility 的开放范围

部分 DataShare provider 能力属于系统接口。真实三方应用一定要先确认目标版本和权限范围。

2. URI 不能随便设计

URI 一旦对外发布,就相当于接口协议。后面修改路径可能直接影响调用方。

3. 不要返回无关字段

共享接口应该遵循最小暴露原则。

4. 写操作一定要做字段白名单

不能把客户端传来的 ValuesBucket 原样写入数据库。

5. ResultSet 要及时关闭

查询结果用完后要释放资源,避免长时间持有。

十二、本文小记

DataShare 真正有价值的地方,不是“跨应用能查表”,而是它强迫我们把数据访问边界设计清楚。

在我看来,一个稳定的数据共享模块至少要有四层:

  • URI 路由;
  • 权限和调用方校验;
  • 数据访问仓储;
  • 变更通知和日志。

如果只写一个 query() 能跑通,最多算 Demo。

把这四层补齐,才真正像一套可维护的数据共享服务。

Logo

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

更多推荐