Skip to content

跨端对比:HarmonyOS / Flutter / Vue

来源:https://juejin.cn/post/7362788635123220495 作者:我女儿是张果果呀 | 2024-04-29

本篇从语言、框架和 UI 设计理念等方面对比 HarmonyOS(ArkTS)、Flutter(Dart)、Vue 三者的基础组件与方法,便于对应技术栈的小伙伴进行对比和学习。

三者都采用声明式编程风格:

  • ArkTSFlutter 通过组件进行布局,布局和逻辑交叉在一起
  • Vue 明确区分了 HTML、JavaScript、CSS 等模块
  • Flutter 设计理念为万物皆为 Widget,设置属性(width/height/margin/padding/onClick)都需要使用对应 Widget
  • ArkTS 跟 Flutter 类似,通过组件嵌套布局,但通过抽取公共属性方式直接设置属性,比 Flutter 方便很多

HarmonyOS 开发语言

  • 开发语言为 ArkTS,基于 TypeScript 演进而来(熟悉 TypeScript 是必备的)
  • UI 通过方舟开发框架 ArkUI 实现,分为 Stage 模型和 FA 模型
  • 主要应用开发采用 Stage 模型

学习步骤

  1. 项目结构
  2. 资源引用
  3. 页面、组件生命周期
  4. 路由跳转与路由数据获取
  5. 页面、组件间通信及响应式 UI
  6. 状态管理
  7. 布局
  8. 网络请求
  9. 数据持久化存储

1. 项目结构

Vue

my-vue-project/
├── public/
│   ├── index.html         # 主HTML文件
├── src/
│   ├── assets/            # 静态资源(图片、样式等)
│   ├── components/        # 组件文件夹
│   ├── views/             # 视图文件夹
│   ├── App.vue            # 根组件
│   └── main.js            # 入口文件
├── babel.config.js        # Babel配置文件
├── package.json           # 项目配置文件
├── vue.config.js          # Vue CLI 配置文件

Flutter

flutter_project/
├── android/               # Android项目文件夹
├── ios/                   # iOS项目文件夹
├── lib/                   # Dart代码文件夹
│   ├── main.dart          # 应用程序入口文件
├── test/                  # 测试文件夹
├── assets/                # 静态资源文件夹
├── pubspec.yaml           # 依赖管理和项目配置文件

HarmonyOS

harmony_project/
├── app.json5                   # 应用全局配置信息
├── entry/                      # 工程模块,编译构建生成HAP包
│   └── src/
│       ├── main/
│       │   ├── ets/            # ArkTS源码
│       │   │   ├── entryability/   # 应用/服务入口
│       │   │   └── pages/          # 页面
│       │   └── resources/      # 资源文件
│       └── module.json5        # Stage模型模块配置
├── build-profile.json5         # 模块信息、编译配置
├── hvigorfile.ts               # 模块级编译构建任务脚本
├── oh_modules/                 # 三方库依赖
└── build-profile.json5         # 应用级配置

2. 设备图片适配

Vue

资源目录:/src/assets/images/...

html
<img src="../assets/logo.png" alt="Logo">

多分辨率适配:

/src/assets/images/user_bg@2x.png
/src/assets/images/user_bg@3x.png
scss
/* mixin.scss - 根据设备像素密度选择图片 */
@mixin bg-image($url) {
  background-image: url($url + "@2x.png");
  @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3) {
    background-image: url($url + "@3x.png");
  }
}

/* 组件中使用 */
.brand {
  width: 30px; height: 30px;
  background-size: 30px 30px;
  background-repeat: no-repeat;
  @include bg-image('brand');
}

Flutter

资源目录:/assets/images/...,在 pubspec.yaml 中注册。

dart
Image.asset("assets/images/user_bg.png", width: 100, height: 100)

多分辨率适配:

/assets/images/user_bg.png          # 默认
/assets/images/3x/user_bg.png       # 3x
/assets/images/4x/user_bg.png       # 4x

Flutter 根据设备像素密度自动选择合适图片。

HarmonyOS

资源目录:/entry/src/main/resources

typescript
Image($r('app.media.user_bg'))  // media资源的$r引用

多分辨率适配:使用屏幕密度限定符 sdpi/mdpi/ldpi/xldpi/xxldpi/xxxldpi

/entry/src/main/resources/xxldpi/media/user_bg.png
/entry/src/main/resources/xxxldpi/media/user_bg.png

HarmonyOS 根据设备像素密度自动选择合适图片。


3. 页面、组件及生命周期

Vue

页面与组件生命周期:

钩子说明
beforeCreate实例初始化之后,数据观测和事件配置之前
created实例创建完成,数据观测和事件已初始化
beforeMount挂载开始之前,render 函数首次被调用
mountedel 被挂载到实例上
beforeUpdate数据更新时,虚拟 DOM 重新渲染前
updated虚拟 DOM 重新渲染和打补丁后
beforeDestroy实例销毁之前,实例仍然完全可用
destroyedVue 实例销毁后,所有指令解绑定,事件监听移除

页面特有生命周期(Vue Router):

钩子说明
beforeRouteEnter路由进入页面前调用
beforeRouteUpdate路由更新页面时调用
beforeRouteLeave路由离开页面时调用

Flutter

Widget 生命周期:

方法说明
initState()Widget 被创建时调用,初始化状态或数据
didChangeDependencies()依赖关系变化时调用
build()构建 Widget 的主要方法
didUpdateWidget()Widget 重新构建时调用,处理属性变化
setState()标记状态变化,触发 build() 重新构建
dispose()Widget 被销毁时调用,释放资源

其他生命周期:

方法说明
didChangeAppLifecycleState()应用生命周期变化(进入后台/返回前台)
reassemble()开发模式下热重载时调用
deactivate()Widget 从树中移除时调用(后续可能重新插入)
mountedWidget 是否已插入树中的标志

HarmonyOS

组件生命周期:

钩子说明
aboutToAppear组件即将出现,在 build() 之前执行
aboutToDisappear组件析构销毁之前(不允许改变状态变量)
build构建组件 UI

页面特有生命周期:

钩子说明
onPageShow页面每次显示时触发(路由、应用进入前台)
onPageHide页面每次隐藏时触发(路由、应用进入后台)
onBackPress用户点击返回按钮时触发

4. 路由跳转及获取路由数据

Vue

路由注册:

js
import { createRouter, createWebHistory } from 'vue-router';
import HomePage from '../components/HomePage.vue';

const routes = [{ path: '/', component: HomePage }];
const router = createRouter({ history: createWebHistory(), routes });
export default router;

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
createApp(App).use(router).mount('#app');

导航方式:

js
// 声明式
<router-link to="/about">About</router-link>

// 编程式
this.$router.push('about')

获取路由数据:

js
this.$route.query    // query参数
this.$route.params   // params参数
// props

Flutter

路由配置:

dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        '/': (context) => HomePage(),
      },
      initialRoute: '/',
    );
  }
}

导航方式:

dart
Navigator.pushNamed(context, '/about', arguments: 'arguments');

获取路由数据:

dart
final String data = ModalRoute.of(context).settings.arguments as String;

HarmonyOS

页面定义:

typescript
@Entry
@Component
struct TestPage { }  // 一个页面有且只有一个@Entry

路由注册:entry/src/main/resources/base/profile/main_pages.json 中注册(DevStudio 创建页面时自动注册)

导航方式:

typescript
import router from '@ohos.router';

router.pushUrl({ url: "pages/AboutPage", params: { "id": "1" } })  // 打开新页面
router.replaceUrl()  // 关闭当前页面,打开新页面

页面打开模式:

模式说明
standard标准模式(默认),每次跳转都添加新路由到栈
single单实例模式,栈内已有同一路由则将其置为栈顶

获取路由数据:

typescript
const params = router.getParams();
console.log("router=aboutToAppear==", params["id"])

组件定义(与路由无关):

typescript
// @Component 注解的 UI 类
@Component
struct TestComponent { }

// 自定义组件方法(只能在当前类中使用)
@Builder _buildImageSwiper() {
  Swiper(this.swiperController) {
    ForEach(this.imageList, (item) => {
      Image(item).width('100%').height('100%')
    })
  }.height(500).width("100%").loop(true)
}

5. 组件间通信及响应式 UI

Vue

响应式 UI(MVVM):

vue
<template>
  <div>
    <input v-model="messageRef" />
    <p>{{ messageRef }}</p>
    <input v-model="state.name" />
    <p>{{ state.name }}</p>
  </div>
</template>

<script>
import { ref, reactive } from 'vue';
export default {
  setup() {
    const messageRef = ref('Hello, Vue!');
    const state = reactive({ name: 'Reactive State' });
    return { messageRef, state };
  }
};
</script>

组件间通信:

场景方式
父→子props
子→父emit
兄弟/父孙事件总线(Event Bus)或 Vuex
跨多层级事件总线/Vuex,或 provide/inject
vue
<!-- 父组件 -->
<template>
  <Child :message="parentMessage" @notify="handleNotify" />
</template>
<script>
setup() {
  const parentMessage = ref('父组件的消息');
  const handleNotify = (message) => { childMessage.value = message; };
  return { parentMessage, handleNotify };
}
</script>

<!-- 子组件 -->
<script>
export default defineComponent({
  props: { message: String },
  setup(props, { emit }) {
    const sendMessage = () => { emit('notify', '子组件的消息'); };
    return { sendMessage };
  }
});
</script>

Flutter

响应式 UI: 使用 StatefulWidget + setState()

dart
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() { _counter++; });  // 更新状态并重新构建UI
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text('$_counter')));
  }
}

Widget 间通信:

场景方式
父→子构造函数传递数据
子→父回调函数
全局Provider、Riverpod、GetX
Widget 树共享InheritedWidget
dart
// 父组件
ChildWidget(
  initialMessage: '来自父亲的关怀',
  onMessageReceived: (msg) { setState(() { messageFromChild = msg; }); },
)

// 子组件
class ChildWidget extends StatefulWidget {
  final String initialMessage;
  final Function(String) onMessageReceived;
  ChildWidget({required this.initialMessage, required this.onMessageReceived});
}

HarmonyOS

组件间通信:

场景方式
父→子(单向)@Prop - 拷贝一份给子组件,子组件不可修改
父→子(双向)@Link - 传递引用,子组件可直接修改父组件变量
对象双向同步@ObjectLink + @Observed - 嵌套对象/数组场景
父孙通信@Provide + @Consume(变量名需相同或定义别名)
子→父回调函数
typescript
// 父组件
@Entry
@Component
struct Index {
  @State parentText: string = '我是 parent'
  @State childText: string = 'child-text'

  build() {
    Column() {
      ChildComponent({ parentText: this.parentText, childText: $childText })
      Text("child text=" + this.childText)
    }
  }
}

// 子组件
@Component
struct ChildComponent {
  @Prop parentText: string    // 单向:只读
  @Link childText: string     // 双向:可修改父组件值

  build() {
    Text(this.parentText + "点击传递值给parent")
      .onClick(() => { this.childText = "child changed" })
  }
}

6. 状态管理

Vue(provide + inject 实现全局状态)

js
// useUserStore.js
import { reactive, ref } from 'vue';
export const useUserStore = () => {
  const state = reactive({ user: ref(null) });
  const setUser = (newUser) => { state.user = ref(newUser); };
  const clearUser = () => { state.user = ref(null); };
  return { provideUserStore: () => ({ user: state.user, setUser, clearUser }) };
};

// main.js
const userStore = useUserStore();
app.provide('userStore', userStore.provideUserStore());

// Login.vue - 保存用户信息
const { user, setUser, clearUser } = inject('userStore');
const login = () => { setUser(credentials.value); };

// User.vue - 读取用户信息
const { user } = inject('userStore');

Flutter(Provider 实现全局状态)

dart
// UserInfoModel.dart
class UserInfoModel with ChangeNotifier {
  User? _currentUser;
  User? get currentUser => _currentUser;
  Future<void> login(String name, String password) async {
    await Future.delayed(Duration(seconds: 1));
    _currentUser = User(name, password);
    notifyListeners();
  }
  void logout() { _currentUser = null; notifyListeners(); }
}

// main.dart - 注册全局 Provider
MultiProvider(
  providers: [ChangeNotifierProvider.value(value: UserInfoModel())],
  child: MaterialApp(home: MyHomePage()),
)

// Login.dart - 保存数据
UserInfoModel userModel = context.watch<UserInfoModel>();
userModel.login(username, password);

// User.dart - 读取数据
UserInfoModel userModel = context.watch<UserInfoModel>();
Text(userInfoModel.name)

HarmonyOS(PersistentStorage + AppStorage)

  • AppStorage:全局状态存储,退出 App 后数据被清除
  • PersistentStorage:持久化保存 AppStorage 的对象,退出 App 后复原
typescript
// UserInfoModel.ets
export class UserInfoModel {
  static init() {
    PersistentStorage.PersistProp('userInfo', { "isLogin": false });
  }
  static login(loginInfo) {
    AppStorage.SetOrCreate('userInfo', loginInfo)
  }
  static logout() {
    PersistentStorage.DeleteProp('userInfo');
  }
}

// 初始化(根组件 aboutToAppear)
aboutToAppear() { UserInfoModel.init() }

// 登录保存
UserInfoModel.login(userInfo);

// 读取全局状态
@StorageProp("userInfo") userInfo: string = ""

// 退出登录
UserInfoModel.logout();

7. 布局

Vue(HTML + CSS)

Flex 布局:

html
<div class="flex-container">
  <div class="item">Item 1</div>
  <div class="item">Item 2</div>
</div>
css
.flex-container {
  display: flex;
  flex-direction: row;        /* row横向/column纵向 */
  justify-content: center;    /* 主轴对齐 */
  align-items: center;        /* 交叉轴对齐 */
}

Position 定位:

css
.stack-item {
  position: absolute;
  top: 10px;   /* 从左上计算偏移 */
  left: 10px;
  /* 同一方向只能一个值 */
  /* top+left=左上 / top+right=右上 / bottom+left=左下 / bottom+right=右下 */
}

Flutter(Widget 嵌套)

Flex 布局:

dart
// Row - 水平方向
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,  // 主轴对齐
  crossAxisAlignment: CrossAxisAlignment.center,       // 交叉轴对齐
  children: [
    Container(width: 50, height: 50, color: Colors.red),
    Container(width: 50, height: 50, color: Colors.green),
  ],
)

// Column - 垂直方向
Column(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [ /* ... */ ],
)

Stack 堆叠布局:

dart
Stack(
  children: [
    Container(width: double.infinity, height: double.infinity, color: Colors.white),
    Align(alignment: Alignment.center, child: Container(width: 200, height: 200, color: Colors.blue)),
    Align(alignment: Alignment.topRight, child: Container(width: 100, height: 100, color: Colors.yellow)),
    Positioned(top: 50, left: 50, child: Container(width: 100, height: 100, color: Colors.red)),
    Positioned(bottom: 50, right: 50, child: Container(width: 100, height: 100, color: Colors.green)),
  ],
)

HarmonyOS(ArkUI)

9 种布局方式:

布局说明适用场景
线性布局(Row/Column)简单线性排列线性排列子元素时优先使用
层叠布局(Stack)堆叠效果弹出面板等需要覆盖的场景
弹性布局(Flex)支持压缩/拉伸需要计算拉伸压缩比例时
相对布局(RelativeContainer)锚点规则定位元素分布复杂、自由布局
栅格布局(GridRow/GridCol)跨设备灵活布局手机/大屏/平板适配
媒体查询(@ohos.mediaquery)设备状态修改样式动态更新页面布局
列表(List)结构化可滚动图片/文本列表
网格(Grid)均分能力强计算器/相册/日历
轮播(Swiper)广告轮播/预览广告轮播、图片预览

定位方式:

类型方式说明
绝对定位position相对于父容器左上角偏移,适应性差
相对定位offset相对于自身偏移,不脱离文档流

常用组件:

组件说明
Button按钮(胶囊/圆形/普通),可作为容器
Radio单选框
Toggle状态切换(勾选框/开关/按钮)
Progress进度条
Text / Span文本显示
TextInput / TextArea文本输入
CustomDialog自定义弹窗
Video视频播放
Image图片显示

布局实战示例(Stack 层叠):

typescript
build() {
  Stack({ alignContent: Alignment.BottomStart }) {   // 底部沉底
    Stack({ alignContent: Alignment.TopStart }) {     // 顶部置顶
      this._buildUgcContent()                         // 中间内容
      UgcDetailHeader()                               // 顶部功能区
    }.width('100%').height("100%")
    UgcDetailBottom()                                 // 底部功能区
  }
}

8. 网络请求

Vue(Axios)

js
import axios from 'axios';

const fetchGetUser = async () => {
  const response = await axios.get('https://api.example.com/users/1');
  user.value = response.data;
};

const fetchPostUsers = async (data) => {
  const response = await axios.post('https://api.example.com/users', data);
};

Flutter(Dio)

dart
import 'package:dio/dio.dart';

Future<void> _getRequest() async {
  Dio dio = Dio();
  Response response = await dio.get('https://api.example.com/data');
  if (response.statusCode == 200) { var data = response.data; }
}

Future<void> _postRequest() async {
  Dio dio = Dio();
  Map<String, dynamic> data = { 'name': '张三', 'email': 'zhangsan@example.com' };
  Response response = await dio.post('https://api.example.com/submit', data: data);
}

HarmonyOS(Axios)

typescript
@Entry
@Component
struct AxiosPage {
  build() {
    Column() {
      Button("发送GET请求").onClick(() => { this._getRequest() })
      Button("发送POST请求").onClick(() => { this._postRequest({ "userId": "1" }) })
    }
  }

  async _getRequest() {
    const response: AxiosResponse<BaseResponseData<LoginInfoBean>> =
      await axios.get<BaseResponseData<LoginInfoBean>>("https://api.example.com/data")
    console.log("_getRequest===", JSON.stringify(response.data.data))
  }

  async _postRequest(data) {
    const response: AxiosResponse<BaseResponseData<LoginInfoBean>> =
      await axios.post<BaseResponseData<LoginInfoBean>>("https://api.example.com/data", data, {
        "headers": { "message": "custom header" }
      })
  }
}

9. 数据持久化存储

Vue(localStorage)

js
localStorage.setItem('myKey', message.value);
retrievedMessage.value = localStorage.getItem('myKey') || '';

Flutter(SharedPreferences)

dart
// 读取
final SharedPreferences prefs = await SharedPreferences.getInstance();
String? savedValue = prefs.getString('myKey');

// 保存
await prefs.setString('myKey', valueToSave);

其他方式:文件存储(path_provider)、SQLite 数据库(sqflite)

HarmonyOS

三种存储方式:

方式说明适用场景
Preferences(用户首选项)文本保存配置,全量加载到内存,速度快少量简单键值对
KV-Store(键值型数据库)非关系型,键值对存储,跨设备兼容好分布式场景、少量数据关系
RelationalStore(关系型数据库)行列表存储,支持增删改查和 SQL复杂业务数据处理

Preferences 使用示例:

typescript
import dataPreferences from '@ohos.data.preferences';

@Entry
@Component
struct PreferencesPage {
  @State inputMessage: string = ''

  // 页面加载时读取
  aboutToAppear() {
    dataPreferences.getPreferences(getContext(), 'xxPreferences', async (err, preferences) => {
      const text = await preferences.get("myKey", "默认值")
      this.inputMessage = text.toString()
    })
  }

  build() {
    Column() {
      Text(this.inputMessage)
      TextInput({ placeholder: "Enter some text" })
        .onChange((value: string) => { this.inputMessage = value })
      Button("save").onClick(() => { this.saveTextToPreferences(this.inputMessage) })
    }
  }

  // 保存数据
  async saveTextToPreferences(text: string) {
    try {
      dataPreferences.getPreferences(getContext(), 'xxPreferences', async (err, preferences) => {
        await preferences.put("myKey", text)
      })
    } catch (err) {
      console.error(`Failed to get preferences. Code:${err.code},message:${err.message}`);
    }
  }
}