笔记 / 2026-05-03 10:30:00 CST

Flutter 工程师的 Vue 对比学习指南

以 Flutter/Dart 的概念为锚点,快速建立 Vue 3 Composition API 的心智模型。

以 Flutter/Dart 的概念为锚点,快速建立 Vue 3 (Composition API) 的心智模型。


1. 整体架构对比

维度FlutterVue
语言DartJavaScript / TypeScript
渲染自绘引擎 (Skia/Impeller)基于 DOM
构建单元WidgetComponent (.vue 单文件组件)
状态管理setState / Provider / Riverpod / Blocref / reactive / Pinia
路由Navigator / GoRouterVue Router
样式Widget 属性内联CSS / Scoped CSS / Tailwind
包管理pub (pubspec.yaml)npm / pnpm (package.json)
构建工具Flutter CLIVite

2. 项目结构对比

# Flutter                          # Vue (Vite 脚手架)
lib/                               src/
├── main.dart                      ├── main.ts          # 入口
├── app.dart                       ├── App.vue          # 根组件
├── models/                        ├── types/           # 类型定义
├── screens/                       ├── views/           # 页面组件
├── widgets/                       ├── components/      # 可复用组件
├── providers/                     ├── stores/          # Pinia 状态
├── services/                      ├── api/             # 网络请求
└── utils/                         ├── utils/
pubspec.yaml                       ├── router/          # 路由配置
                                   package.json

3. 组件 = Widget

3.1 基本组件结构

Flutter — StatelessWidget

class Greeting extends StatelessWidget {
  final String name;
  const Greeting({required this.name});

  @override
  Widget build(BuildContext context) {
    return Text('Hello, $name');
  }
}

Vue — 单文件组件 (SFC)

<template>
  <p>Hello, {{ name }}</p>
</template>

<script setup lang="ts">
defineProps<{ name: string }>()
</script>

对比要点:

  • Flutter 的 build() 方法 ≈ Vue 的 <template>
  • Flutter 的构造函数参数 ≈ Vue 的 props
  • Vue 用 {{ }} 做插值,Flutter 用 ${} 在 Dart 字符串里插值

3.2 有状态组件

Flutter — StatefulWidget

class Counter extends StatefulWidget {
  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Text('$count'),
      ElevatedButton(
        onPressed: () => setState(() => count++),
        child: Text('Add'),
      ),
    ]);
  }
}

Vue — Composition API

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="count++">Add</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
</script>

对比要点:

  • setState() ≈ 直接修改 ref.value(模板中自动解包,不用写 .value
  • Flutter 需要 StatefulWidget + State 两个类,Vue 只需 ref() 一行
  • Vue 的响应式是自动追踪依赖的,不需要手动调用 setState

4. 响应式系统对比

FlutterVue说明
setState(() { })自动(修改 ref/reactive 即触发)Vue 无需手动通知
ValueNotifier<T>ref<T>()单值响应式
ChangeNotifierreactive({})对象级响应式
Provider.of<T>(context)inject() / Pinia store跨组件共享状态
StreamBuilderwatch() / watchEffect()监听变化并执行副作用
FutureBuilderonMounted + async 或 Suspense异步数据加载

ref vs reactive

<script setup lang="ts">
import { ref, reactive } from 'vue'

// ref — 用于基本类型(类似 ValueNotifier)
const count = ref(0)
count.value++  // 脚本中需要 .value

// reactive — 用于对象(类似 ChangeNotifier)
const user = reactive({ name: 'Alice', age: 25 })
user.age++     // 直接修改属性,不需要 .value
</script>

计算属性 = 派生状态

Flutter

// 每次 build 都重新计算
String get fullName => '${firstName} ${lastName}';

Vue

<script setup lang="ts">
import { ref, computed } from 'vue'

const firstName = ref('Alice')
const lastName = ref('Smith')

// 自动缓存,只在依赖变化时重新计算
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>

侦听器 = 监听变化

Flutter

// 用 didUpdateWidget 或 addListener
@override
void didUpdateWidget(oldWidget) {
  if (widget.id != oldWidget.id) fetchData(widget.id);
}

Vue

<script setup lang="ts">
import { ref, watch } from 'vue'

const id = ref(1)

watch(id, (newVal, oldVal) => {
  fetchData(newVal)
})
</script>

5. 生命周期对比

Flutter (State)Vue 3 (Composition API)时机
initState()onMounted()组件挂载/初始化
didUpdateWidget()onUpdated()更新后
dispose()onUnmounted()销毁/卸载
didChangeDependencies()watch()依赖变化
onBeforeMount()挂载前
onBeforeUpdate()更新前
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'

// ≈ initState
onMounted(() => {
  console.log('组件已挂载')
  window.addEventListener('resize', onResize)
})

// ≈ dispose
onUnmounted(() => {
  window.removeEventListener('resize', onResize)
})
</script>

6. 模板语法 = Widget 树

6.1 条件渲染

Flutter

Column(children: [
  if (isLoggedIn) Text('Welcome'),
  if (!isLoggedIn) TextButton(onPressed: login, child: Text('Login')),
])

Vue

<template>
  <p v-if="isLoggedIn">Welcome</p>
  <button v-else @click="login">Login</button>
</template>

6.2 列表渲染

Flutter

ListView.builder(
  itemCount: items.length,
  itemBuilder: (ctx, i) => ListTile(
    key: ValueKey(items[i].id),
    title: Text(items[i].name),
  ),
)

Vue

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
</template>

6.3 事件绑定

FlutterVue说明
onPressed: () => {}@click="handler"点击
onChanged: (v) => {}@input="handler"输入
onSubmitted: (v) => {}@submit.prevent="handler"表单提交
GestureDetector@mousedown @touchstart手势

6.4 属性绑定

<template>
  <!-- 静态属性 -->
  <img src="/logo.png" />

  <!-- 动态绑定(v-bind 缩写为 :) -->
  <img :src="imageUrl" />

  <!-- class 绑定 -->
  <div :class="{ active: isActive, disabled: isDisabled }"></div>

  <!-- style 绑定 -->
  <div :style="{ color: textColor, fontSize: size + 'px' }"></div>
</template>

7. 组件通信对比

场景FlutterVue
父→子构造函数参数props
子→父回调函数 onChangedemit 事件
跨层级InheritedWidget / Providerprovide / inject
全局状态Riverpod / BlocPinia

Props 传递(父→子)

Flutter

// 父组件
UserCard(name: userName, onTap: () => goToProfile())

// 子组件
class UserCard extends StatelessWidget {
  final String name;
  final VoidCallback onTap;
  // ...
}

Vue

<!-- 父组件 -->
<UserCard :name="userName" @tap="goToProfile" />

<!-- 子组件 UserCard.vue -->
<script setup lang="ts">
defineProps<{ name: string }>()
const emit = defineEmits<{ tap: [] }>()
</script>

<template>
  <div @click="emit('tap')">{{ name }}</div>
</template>

插槽 = child / builder

Flutter

Card(child: Text('内容'))

// builder 模式
MyWidget(builder: (context) => Text('动态内容'))

Vue

<!-- 默认插槽 ≈ child -->
<Card>
  <p>内容</p>
</Card>

<!-- 作用域插槽 ≈ builder -->
<MyList :items="items">
  <template #default="{ item }">
    <span>{{ item.name }}</span>
  </template>
</MyList>

8. 路由对比

Flutter — GoRouter

GoRouter(routes: [
  GoRoute(path: '/', builder: (ctx, state) => HomePage()),
  GoRoute(path: '/user/:id', builder: (ctx, state) {
    final id = state.pathParameters['id']!;
    return UserPage(id: id);
  }),
])

// 导航
context.go('/user/42');

Vue — Vue Router

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: () => import('@/views/Home.vue') },
    { path: '/user/:id', component: () => import('@/views/User.vue') },
  ],
})
<!-- 使用路由 -->
<template>
  <router-link to="/user/42">Go to User</router-link>
  <router-view />  <!-- ≈ Navigator 的显示区域 -->
</template>

<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()     // 读取当前路由信息
const router = useRouter()   // 编程式导航

console.log(route.params.id) // '42'
router.push('/user/42')      // ≈ context.go()
</script>

路由守卫 ≈ Navigator Observer

router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    return '/login'  // 重定向
  }
})

9. 状态管理对比

Pinia ≈ Riverpod / Provider

Flutter — Riverpod

final counterProvider = StateNotifierProvider<CounterNotifier, int>(
  (ref) => CounterNotifier(),
);

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);
  void increment() => state++;
}

// 使用
final count = ref.watch(counterProvider);
ref.read(counterProvider.notifier).increment();

Vue — Pinia

// stores/counter.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() { count.value++ }
  return { count, increment }
})
<!-- 使用 -->
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
</script>

<template>
  <p>{{ counter.count }}</p>
  <button @click="counter.increment()">+1</button>
</template>

10. 样式对比

Flutter — 所有样式通过 Widget 属性内联

Container(
  padding: EdgeInsets.all(16),
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(8),
  ),
  child: Text('Hello', style: TextStyle(fontSize: 18, color: Colors.white)),
)

Vue — CSS (Scoped)

<template>
  <div class="card">
    <p class="card-text">Hello</p>
  </div>
</template>

<style scoped>
.card {
  padding: 16px;
  background-color: blue;
  border-radius: 8px;
}
.card-text {
  font-size: 18px;
  color: white;
}
</style>

常用 CSS 布局 ≈ Flutter 布局 Widget

FlutterCSS说明
Columndisplay: flex; flex-direction: column纵向排列
Rowdisplay: flex; flex-direction: row横向排列
Stackposition: relative + position: absolute层叠
Expanded(flex: 1)flex: 1弹性占比
SizedBox(width: 100)width: 100px固定尺寸
Paddingpadding: 16px内边距
Centerdisplay: flex; justify-content: center; align-items: center居中
ListViewoverflow-y: auto滚动列表
GridViewdisplay: grid; grid-template-columns: ...网格
Wrapdisplay: flex; flex-wrap: wrap自动换行

11. 网络请求对比

Flutter — dio

final dio = Dio();
final response = await dio.get('/api/users');
final users = response.data;

Vue — axios / fetch

import axios from 'axios'

// 在 composable 中封装(≈ Flutter 的 Repository)
export function useUsers() {
  const users = ref([])
  const loading = ref(false)

  async function fetchUsers() {
    loading.value = true
    try {
      const { data } = await axios.get('/api/users')
      users.value = data
    } finally {
      loading.value = false
    }
  }

  onMounted(fetchUsers)
  return { users, loading }
}

12. 组合式函数 (Composables) ≈ Mixin / Hook

Vue 的 Composable 是复用逻辑的核心方式,类似 Flutter 中的 Mixin 或 Riverpod 的自定义 Provider。

// composables/useMouse.ts(≈ Flutter 的 mixin)
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(e: MouseEvent) {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}
<!-- 使用 -->
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>

<template>
  <p>Mouse: {{ x }}, {{ y }}</p>
</template>

13. 开发工具链对比

用途FlutterVue
创建项目flutter createnpm create vue@latest
开发服务器flutter runnpm run dev (Vite)
热重载内置 Hot ReloadVite HMR
调试工具Flutter DevToolsVue DevTools (浏览器插件)
构建flutter buildnpm run build
代码检查dart analyzeESLint
格式化dart formatPrettier
测试flutter testVitest
组件测试Widget Test@vue/test-utils
E2E 测试Integration TestCypress / Playwright

14. 快速上手路径

  1. 环境搭建:安装 Node.js → npm create vue@latest(选 TypeScript + Router + Pinia)
  2. 先跑通:看懂 App.vuemain.ts、路由配置
  3. 写组件:把你熟悉的 Flutter Widget 用 Vue SFC 重写一遍
  4. 学响应式:重点掌握 refreactivecomputedwatch
  5. 学路由:Vue Router 的配置式路由和 GoRouter 非常像
  6. 学状态管理:Pinia 比 Riverpod 简单,先用再深入
  7. 学 CSS:这是 Flutter 工程师最大的新领域,重点学 Flexbox 和 Grid

15. 核心思维转换

Flutter 思维Vue 思维
一切皆 Widget一切皆组件
Widget 树是不可变的,rebuild 整棵树模板 + 响应式数据,只更新变化的 DOM
样式是 Widget 的属性样式和结构分离 (CSS)
BuildContext 访问上层数据inject() / Pinia 访问共享数据
Key 控制 Widget 复用:key 控制 DOM 元素复用
const Widget 优化性能Vue 编译器自动优化
手动 setState 触发重建修改响应式数据自动触发更新