1***@qq.com
1***@qq.com
  • 发布:2025-11-06 14:37
  • 更新:2025-11-07 13:21
  • 阅读:1077

从Web到鸿蒙:uni-app x 开发踩坑与成长实录

分类:鸿蒙Next

一、起因

去年年底,我决定做一个冰箱食材管理的 App,主要功能是记录食材、推荐菜谱,顺便接入 AI 生成个性化建议。

技术选型的时候,我面临一个选择:是分别写 iOS、Android 和鸿蒙三套代码,还是用跨平台框架?作为一个前端开发者,我自然选择了后者,最终决定用 uni-app x。

选择 uni-app x 的原因很简单:官方说支持鸿蒙,而且可以用类似 TypeScript 的语法写代码。我当时想,这不就是 Web 开发的思路吗?写一套代码,三个平台都能跑,多省事。

结果没想到,光是适配鸿蒙就踩了无数的坑。项目做到现在,我最深的体会是:跨平台开发不是"一次编写,处处运行",而是"一次编写,处处适配"

这篇文章记录了我在开发过程中遇到的几个典型问题,希望能帮到后来者。

二、UTS 不是 TypeScript

uni-app x 使用 UTS 语言,官方文档说它"类似 TypeScript"。我一开始以为只是换了个名字,代码照写就行了。

结果第一天就被打脸了。

1. Map.forEach 不支持

我在统计功能里用了 Map 来记录食材出现次数,写了这样的代码:

const ingredientMap = new Map<string, number>()  
ingredientMap.set('鸡蛋', 3)  
ingredientMap.set('番茄', 2)  

const stats: IngredientStat[] = []  
ingredientMap.forEach((count: number, name: string) => {  
    stats.push({ name, count })  
})

在 iOS 上运行正常,一到鸿蒙就报错:undefined is not callable

调试了半天才发现,鸿蒙平台的 Map 实现不支持 forEach 方法。这让我很惊讶,因为 forEach 在 JavaScript 里是最基础的 API 之一。

解决办法是改用 for...of 循环:

const stats: IngredientStat[] = []  
for (const [name, count] of ingredientMap.entries()) {  
    stats.push({ name: name, count: count } as IngredientStat)  
}

这个改动不难,但问题是我没想到会有这样的限制。这让我意识到,UTS 虽然语法像 TypeScript,但底层编译到 Kotlin/Swift 时,会受到原生平台的约束。

2. 类型隐式转换被禁止

还有一个问题,是条件判断。我习惯这样写:

if (data) {  
    // 处理数据  
}

这在 Web 开发里很常见,但在 UTS 里会报错。因为 UTS 是强类型语言,条件表达式必须是布尔类型,不能用"真值/假值"的概念。

正确的写法是:

if (data != null) {  
    // 处理数据  
}

这种严格的类型检查一开始让我很不适应,但后来发现它确实能避免很多隐藏的 Bug。毕竟 JavaScript 的类型转换规则太复杂了,0''nullundefined 在条件判断里的行为都不一样。

3. onLoad 参数的陷阱

页面加载时,我需要从 URL 参数里获取数据。我看官方文档说 onLoad 的参数是 Map 类型,就写了这样的代码:

onLoad((options: Map<string, any | null>) => {  
    const id = options.get('id')  
})

结果又报错了:undefined is not callable

后来才知道,虽然文档说是 Map 类型,但在鸿蒙平台上,options 实际上是个普通对象。正确的写法是:

onLoad((options: any) => {  
    if (options !== null && options.id !== null) {  
        const id = options.id as string  
    }  
})

这个问题让我明白,文档和实际实现之间可能会有差异,不能完全依赖类型标注,要以实际运行结果为准。

三、最难查的 Bug:数据库查询的平台差异

说起来,前面这些坑虽然麻烦,但至少报错信息还算明确。最让我头疼的是一个"看不见的 Bug"。

统计页面在 iOS 上显示正常,数据都对。但一到鸿蒙上,所有统计数字全是 0。界面正常,没有报错,就是数据不对。

问题排查

我先检查了数据库操作,发现查询 SQL 没问题,也能正常执行。然后加了一堆 console.log,发现查询结果确实返回了:

const result = db.query('SELECT COUNT(*) as count FROM saved_recipes')  
console.log('查询结果:', JSON.stringify(result.maps))  
// iOS: [{"count": 5}]  
// 鸿蒙: [{}]  // 看起来像空对象?

但是取值的时候就出问题了:

const count = result.maps[0]['count']  
console.log('统计数量:', count)  
// iOS: 5  
// 鸿蒙: undefined

我当时就懵了,明明查询结果里有数据,为什么取不到?

问题根源

查了半天资料,最后在一个 issue 里看到有人提到:鸿蒙平台的查询结果不是普通对象数组,而是 Map<string, any>[]

这就解释了为什么 JSON.stringify 输出是空对象——Map 对象序列化后本来就是空的。而在 iOS/Android 上,查询结果是普通对象,所以 item['count'] 可以正常取值,但在鸿蒙上必须用 item.get('count')

这个差异太隐蔽了,因为:

  1. 没有报错信息
  2. result.maps 的长度是正确的
  3. 直接打印对象看不出问题

解决方案

我在基础 Service 类里封装了一个通用方法:

protected static getFieldValue(item: any, field: string, defaultValue: any = null): any {  
    if (item instanceof Map) {  
        // 鸿蒙平台:Map 对象  
        return (item as Map<string, any>).get(field) ?? defaultValue  
    } else {  
        // 其他平台:普通对象  
        return item[field] ?? defaultValue  
    }  
}

然后在所有查询结果处理的地方都改用这个方法:

const result = db.query('SELECT COUNT(*) as count FROM saved_recipes')  
const row = result.maps[0]  
const count = this.getFieldValue(row, 'count', 0) as number

这样就兼容所有平台了。

深层思考

这个 Bug 让我对跨平台开发有了新的认识。

在 Web 开发里,JavaScript 的数据结构在各个浏览器上基本是一致的。但在跨平台开发里,即使是数组、对象这样的基础数据结构,在不同平台上的实现也可能不同。

鸿蒙之所以返回 Map 对象,可能是因为 UTS 编译到 Kotlin 后,用了 Kotlin 的 Map 类型。而 iOS/Android 可能用的是字典或者其他数据结构,反射到 UTS 层面就是普通对象。

这种差异不是框架的 Bug,而是跨平台开发的本质特征。框架只能保证 API 层面的一致性,底层数据结构的差异还是要开发者自己处理。

四、页面滚动的迷惑行为

还有一个坑,让我调试了整整一个下午。

AI 生成菜谱的结果页面,内容很长,需要滚动查看。我一开始用了 Web 开发的常规思路:

<template>  
  <view class="page">  
    <x-navbar title="菜谱详情" />  
    <scroll-view class="content" scroll-y>  
      <!-- 菜谱内容 -->  
    </scroll-view>  
  </view>  
</template>  

<style>  
.page {  
  height: 100vh;  
}  
.content {  
  flex: 1;  
}  
</style>

结果页面完全不滚动。

连环踩坑

我开始排查问题:

第一步:去掉 scroll-view,想让页面自然滚动。不行。

第二步:调用 uni.pageScrollTo 滚动到底部。报错:selector invalid

第三步:检查了半天 CSS,发现鸿蒙不支持 vh 单位。改成 100%,还是不行。

最后我仔细看了一遍页面结构,才发现问题:

<template>  
  <scroll-view class="content">  
    <!-- 内容 -->  
  </scroll-view>  
  <x-snackbar />  <!-- 提示组件 -->  
</template>

我把 Snackbar 组件放在了 scroll-view 外面,导致 <template> 下有两个根节点。这样一来,scroll-view 就不是真正的页面滚动容器了。

正确方案

正确的做法是,scroll-view 必须是 <template> 下的唯一根标签,所有其他组件都要放在里面:

<template>  
  <scroll-view class="page">  
    <x-navbar title="菜谱详情" />  
    <!-- 内容 -->  
    <x-snackbar />  <!-- 也要放里面 -->  
  </scroll-view>  
</template>  

<style>  
.page {  
  width: 100%;  
  height: 100%;  /* 用 100% 而不是 100vh */  
  padding-bottom: calc(24rpx + env(safe-area-inset-bottom));  /* 安全距离 */  
}  
</style>

改完之后,滚动立刻正常了,uni.pageScrollTo 也能用了。

为什么这么设计?

我后来想明白了,这是因为 uni-app x 的页面模型和 Web 不同。

在 Web 里,<body> 标签天然就是滚动容器,你写多少内容都会自动滚动。但在原生应用里,页面不会自动滚动,必须明确指定一个滚动容器。

鸿蒙的 CSS 支持也比 Web 少得多,没有 vh/vw 单位,没有 position: sticky,很多 Web 开发的技巧都用不了。这逼着我用原生应用的思路来写布局。

五、收获

踩了这么多坑,我最大的感受是:跨平台开发的难点不在于语法,而在于平台差异

uni-app x 已经做得很好了,它把三个平台的 API 统一了,让我可以用一套代码来写。但是,底层数据结构、CSS 支持、API 实现细节上的差异,还是需要开发者自己去适配。

给后来者的几点建议:

  1. 仔细阅读 UTS 与 TypeScript 的差异文档。不要想当然地认为它们是一样的。

  2. 善用 instanceof 和类型判断。在处理查询结果、事件参数等可能有平台差异的地方,先判断类型再操作。

  3. 详细的日志是最好的调试工具。把关键数据都打印出来,包括类型信息,能快速定位问题。

  4. 页面布局用原生思维。不要用 Web 那套 vh/vwposition: sticky,老老实实用 scroll-viewflex 布局。

  5. 记录踩坑过程。我把每个问题都写成了文档,方便以后查阅,也能分享给团队其他人。

最后想说的是,虽然踩了很多坑,但我并不后悔选择 uni-app x。相比分别写三套原生代码,它已经节省了我大量时间。而且踩坑的过程也让我对跨平台开发有了更深的理解。

从 Web 思维到原生思维的转变,是一个痛苦但必要的过程。这可能就是成长的代价吧。


22 关注 分享
tmui Isaacedvr DCloud_CHB 绛珠 阿岳 潇辰风 青衫行者 用户2920722 DCloud_云服务_JRP 唐家三少 DCloud_UNI_yuhe raise verify 希语 DCloud_uniCloud_JSON DCloud_UNI_Anne 用户2919468 威龙 WstWrld CodeCrafter 兔兔兔兔子 DCloud_uniCloud_CRL

要回复文章请先登录注册

用户2920722

用户2920722

数据库查询那段,没看懂,前段查询的 sqlite?
2025-11-07 13:21
青衫行者

青衫行者

你这属于入坑比较早的,25 年 4 月,Map.forEach 在鸿蒙平台就支持了,对应HBuilderX 版本是 4.61
2025-11-07 13:03
潇辰风

潇辰风

原来有这么多坑啊,666
2025-11-06 15:10