Appearance
跨端对比:HarmonyOS / Flutter / Vue
来源:https://juejin.cn/post/7362788635123220495 作者:我女儿是张果果呀 | 2024-04-29
本篇从语言、框架和 UI 设计理念等方面对比 HarmonyOS(ArkTS)、Flutter(Dart)、Vue 三者的基础组件与方法,便于对应技术栈的小伙伴进行对比和学习。
三者都采用声明式编程风格:
- ArkTS 和 Flutter 通过组件进行布局,布局和逻辑交叉在一起
- Vue 明确区分了 HTML、JavaScript、CSS 等模块
- Flutter 设计理念为万物皆为 Widget,设置属性(width/height/margin/padding/onClick)都需要使用对应 Widget
- ArkTS 跟 Flutter 类似,通过组件嵌套布局,但通过抽取公共属性方式直接设置属性,比 Flutter 方便很多
HarmonyOS 开发语言
- 开发语言为 ArkTS,基于 TypeScript 演进而来(熟悉 TypeScript 是必备的)
- UI 通过方舟开发框架 ArkUI 实现,分为 Stage 模型和 FA 模型
- 主要应用开发采用 Stage 模型
学习步骤
- 项目结构
- 资源引用
- 页面、组件生命周期
- 路由跳转与路由数据获取
- 页面、组件间通信及响应式 UI
- 状态管理
- 布局
- 网络请求
- 数据持久化存储
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.pngscss
/* 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 # 4xFlutter 根据设备像素密度自动选择合适图片。
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.pngHarmonyOS 根据设备像素密度自动选择合适图片。
3. 页面、组件及生命周期
Vue
页面与组件生命周期:
| 钩子 | 说明 |
|---|---|
beforeCreate | 实例初始化之后,数据观测和事件配置之前 |
created | 实例创建完成,数据观测和事件已初始化 |
beforeMount | 挂载开始之前,render 函数首次被调用 |
mounted | el 被挂载到实例上 |
beforeUpdate | 数据更新时,虚拟 DOM 重新渲染前 |
updated | 虚拟 DOM 重新渲染和打补丁后 |
beforeDestroy | 实例销毁之前,实例仍然完全可用 |
destroyed | Vue 实例销毁后,所有指令解绑定,事件监听移除 |
页面特有生命周期(Vue Router):
| 钩子 | 说明 |
|---|---|
beforeRouteEnter | 路由进入页面前调用 |
beforeRouteUpdate | 路由更新页面时调用 |
beforeRouteLeave | 路由离开页面时调用 |
Flutter
Widget 生命周期:
| 方法 | 说明 |
|---|---|
initState() | Widget 被创建时调用,初始化状态或数据 |
didChangeDependencies() | 依赖关系变化时调用 |
build() | 构建 Widget 的主要方法 |
didUpdateWidget() | Widget 重新构建时调用,处理属性变化 |
setState() | 标记状态变化,触发 build() 重新构建 |
dispose() | Widget 被销毁时调用,释放资源 |
其他生命周期:
| 方法 | 说明 |
|---|---|
didChangeAppLifecycleState() | 应用生命周期变化(进入后台/返回前台) |
reassemble() | 开发模式下热重载时调用 |
deactivate() | Widget 从树中移除时调用(后续可能重新插入) |
mounted | Widget 是否已插入树中的标志 |
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参数
// propsFlutter
路由配置:
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}`);
}
}
}