告别数据孤岛:Electric与Vue响应式数据同步实战指南

告别数据孤岛:Electric与Vue响应式数据同步实战指南

【免费下载链接】electric electric-sql/electric: 这是一个用于查询数据库的JavaScript库,支持多种数据库。适合用于需要使用JavaScript查询数据库的场景。特点:易于使用,支持多种数据库,具有灵活的查询构建和结果处理功能。 【免费下载链接】electric 项目地址: https://gitcode.com/GitHub_Trending/el/electric

你是否还在为Vue应用中的实时数据同步烦恼?客户端与服务端数据不一致、手动编写大量状态更新代码、复杂的WebSocket连接管理——这些问题不仅消耗开发精力,还会导致用户体验下降。本文将带你一步到位解决这些痛点,通过Electric与Vue的深度集成,实现高效、可靠的响应式数据同步。读完本文,你将掌握如何在Vue项目中无缝集成Electric,构建真正意义上的实时响应式应用。

核心概念与架构解析

Electric工作原理

Electric是一个用于查询数据库的JavaScript库,支持多种数据库,提供灵活的查询构建和结果处理功能。其核心优势在于能够实时同步数据库变更,使前端应用能够即时反映后端数据变化,无需手动刷新或轮询。

mermaid

Vue响应式系统与Electric集成点

Vue的响应式系统通过refreactive函数将数据转换为响应式对象,当数据变化时自动更新相关组件。Electric的订阅机制可以与Vue的响应式系统完美结合,实现数据变更的自动响应:

  1. 数据订阅:通过Electric订阅数据库表或查询结果的变更
  2. 响应式包装:将Electric返回的数据包装为Vue响应式对象
  3. 变更通知:当数据更新时,Electric触发回调,更新响应式对象
  4. UI更新:Vue自动检测响应式对象变化,更新组件视图

环境准备与安装

系统要求

  • Node.js 16.x或更高版本
  • Vue 3.x(Composition API)
  • npm或yarn包管理器
  • PostgreSQL数据库(Electric支持的版本)

安装依赖

# 创建Vue项目(如果尚未创建)
npm create vue@latest electric-vue-demo
cd electric-vue-demo
npm install

# 安装Electric客户端
npm install @electric-sql/client

配置Electric

首先,需要初始化Electric客户端。在项目根目录下创建src/electric.ts文件:

import { ElectricClient } from '@electric-sql/client'

// 初始化Electric客户端
export const electric = new ElectricClient({
  url: 'http://localhost:5133', // Electric Sync Service地址
  database: 'my-electric-db',   // 数据库名称
  auth: {
    token: 'your-auth-token'    // 身份验证令牌(实际项目中应动态获取)
  },
  // 配置数据同步选项
  sync: {
    autoStart: true,            // 自动启动同步
    retry: {
      initialDelay: 1000,       // 初始重试延迟
      maxDelay: 5000            // 最大重试延迟
    }
  }
})

// 等待客户端连接
export async function initElectric() {
  try {
    await electric.connect()
    console.log('Electric client connected successfully')
    return electric
  } catch (error) {
    console.error('Failed to connect Electric client:', error)
    throw error
  }
}

核心实现:Vue响应式数据同步

创建Electric-Vue桥接工具

创建src/composables/useElectric.ts文件,实现Electric与Vue的桥接:

import { ref, watch, Ref } from 'vue'
import { electric } from '../electric'
import type { Shape, ShapeChangedCallback } from '@electric-sql/client'

// 将Electric Shape包装为Vue响应式对象
export function useElectricShape<T>(shape: Shape<T>): {
  data: Ref<T | null>
  loading: Ref<boolean>
  error: Ref<Error | null>
  unsubscribe: () => void
} {
  const data = ref<T | null>(null)
  const loading = ref(true)
  const error = ref<Error | null>(null)
  
  // 初始加载数据
  shape.get().then(initialData => {
    data.value = initialData
    loading.value = false
  }).catch(err => {
    error.value = err
    loading.value = false
  })
  
  // 订阅数据变更
  const callback: ShapeChangedCallback<T> = ({ value, error: updateError }) => {
    if (updateError) {
      error.value = updateError
      return
    }
    data.value = value
  }
  
  const unsubscribe = shape.subscribe(callback)
  
  // 组件卸载时取消订阅
  watch(
    () => {},
    () => unsubscribe(),
    { once: true }
  )
  
  return { data, loading, error, unsubscribe }
}

// 便捷的表查询组合式函数
export function useElectricTable<T>(tableName: string) {
  // 创建表查询Shape
  const shape = electric.db[tableName].watch()
  
  // 使用前面定义的hook包装为响应式数据
  return useElectricShape<T>(shape)
}

实现响应式数据查询

在Vue组件中使用上述组合式函数,实现响应式数据查询:

<template>
  <div class="electric-demo">
    <h2>任务列表</h2>
    
    <!-- 加载状态 -->
    <div v-if="loading" class="loading">加载中...</div>
    
    <!-- 错误处理 -->
    <div v-else-if="error" class="error">
      错误: {{ error.message }}
    </div>
    
    <!-- 数据展示 -->
    <div v-else class="task-list">
      <div v-for="task in data" :key="task.id" class="task-item">
        <input 
          type="checkbox" 
          v-model="task.completed" 
          @change="handleTaskToggle(task)"
        >
        <span :class="{ completed: task.completed }">{{ task.title }}</span>
        <button @click="handleTaskDelete(task.id)">删除</button>
      </div>
    </div>
    
    <!-- 添加任务 -->
    <div class="add-task">
      <input v-model="newTaskTitle" placeholder="输入新任务">
      <button @click="handleAddTask">添加</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useElectricTable } from '../composables/useElectric'
import { electric } from '../electric'

// 定义任务类型
interface Task {
  id: string
  title: string
  completed: boolean
  created_at: string
}

// 使用Electric查询任务表
const { data, loading, error } = useElectricTable<Task>('tasks')

// 新任务标题
const newTaskTitle = ref('')

// 添加任务
const handleAddTask = async () => {
  if (!newTaskTitle.value.trim()) return
  
  try {
    await electric.db.tasks.insert({
      title: newTaskTitle.value,
      completed: false,
      created_at: new Date().toISOString()
    })
    newTaskTitle.value = '' // 清空输入框
  } catch (err) {
    console.error('添加任务失败:', err)
  }
}

// 切换任务完成状态
const handleTaskToggle = async (task: Task) => {
  try {
    await electric.db.tasks.update({
      id: task.id,
      completed: !task.completed
    })
  } catch (err) {
    console.error('更新任务失败:', err)
    // 回滚UI状态
    task.completed = !task.completed
  }
}

// 删除任务
const handleTaskDelete = async (taskId: string) => {
  try {
    await electric.db.tasks.delete({ id: taskId })
  } catch (err) {
    console.error('删除任务失败:', err)
  }
}
</script>

<style scoped>
.task-list {
  margin: 20px 0;
}

.task-item {
  display: flex;
  align-items: center;
  margin: 8px 0;
  padding: 8px;
  border: 1px solid #eee;
  border-radius: 4px;
}

.task-item.completed {
  text-decoration: line-through;
  color: #888;
}

.task-item input {
  margin-right: 10px;
}

.task-item button {
  margin-left: auto;
  background: #ff4444;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 4px 8px;
  cursor: pointer;
}

.add-task {
  margin-top: 20px;
  display: flex;
  gap: 10px;
}

.add-task input {
  flex: 1;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.add-task button {
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 8px 16px;
  cursor: pointer;
}

.loading {
  color: #666;
  padding: 20px;
  text-align: center;
}

.error {
  color: #dc3545;
  padding: 20px;
  border: 1px solid #f5c6cb;
  border-radius: 4px;
  background-color: #f8d7da;
}
</style>

高级功能实现

条件查询与过滤

Electric支持复杂的条件查询,结合Vue的响应式特性,可以实现动态过滤:

// 在组合式函数中添加条件查询支持
export function useFilteredElectricTable<T>(
  tableName: string, 
  filter: Record<string, any> = {}
) {
  // 创建带过滤条件的Shape
  const shape = electric.db[tableName].watch(filter)
  
  return useElectricShape<T>(shape)
}

// 在组件中使用
const searchQuery = ref('')
const showCompleted = ref(false)

// 响应式过滤条件
const filter = computed(() => {
  const conditions: Record<string, any> = {}
  
  // 文本搜索过滤
  if (searchQuery.value) {
    conditions.title = { contains: searchQuery.value }
  }
  
  // 状态过滤
  if (showCompleted.value !== undefined) {
    conditions.completed = showCompleted.value
  }
  
  return conditions
})

// 使用响应式过滤条件
const { data: filteredTasks } = useFilteredElectricTable<Task>('tasks', filter.value)

实时多用户协作

Electric的实时同步特性使得多用户协作变得简单。以下是一个多用户在线状态指示实现:

<template>
  <div class="user-presence">
    <h3>在线用户</h3>
    <div class="user-list">
      <div v-for="user in onlineUsers" :key="user.id" class="user-item">
        <div class="avatar" :style="{ backgroundColor: user.color }">
          {{ user.name.charAt(0) }}
        </div>
        <span>{{ user.name }}</span>
        <div class="status-indicator online"></div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useElectricTable } from '../composables/useElectric'

// 定义用户类型
interface User {
  id: string
  name: string
  color: string
  last_active: string
  online: boolean
}

// 订阅用户在线状态表
const { data: onlineUsers } = useElectricTable<User>('user_presence', {
  online: true
})
</script>

<style scoped>
/* 样式实现省略 */
</style>

离线数据访问与同步

Electric支持离线数据访问,当网络恢复后自动同步本地变更:

// 增强useElectricShape以支持离线状态
export function useElectricShapeWithOffline<T>(shape: Shape<T>) {
  const { data, loading, error, unsubscribe } = useElectricShape<T>(shape)
  const isOffline = ref(false)
  
  // 监听连接状态
  const connectionUnsubscribe = electric.connectionState.subscribe(state => {
    isOffline.value = state.status !== 'connected'
  })
  
  // 重写unsubscribe方法,包含连接状态订阅的取消
  const enhancedUnsubscribe = () => {
    unsubscribe()
    connectionUnsubscribe()
  }
  
  return { data, loading, error, isOffline, unsubscribe: enhancedUnsubscribe }
}

性能优化策略

数据订阅管理

合理管理数据订阅是优化性能的关键。以下是一些最佳实践:

策略适用场景优势潜在风险
组件级订阅小型应用、简单数据实现简单,自动清理可能导致重复订阅
全局状态订阅大型应用、共享数据集中管理,减少重复需要手动管理订阅生命周期
路由级订阅页面级数据按需加载,路由切换时清理复杂路由场景需额外处理
按需订阅大型数据集减少初始加载时间用户体验可能受影响

批量操作优化

对于大量数据操作,使用批量API可以显著提升性能:

// 批量更新任务状态
async function batchUpdateTasks(taskIds: string[], completed: boolean) {
  // 使用事务确保操作原子性
  await electric.transaction(async (tx) => {
    for (const id of taskIds) {
      await tx.tasks.update({ id, completed })
    }
  })
}

索引优化建议

为常用查询字段创建索引,可以提高Electric的查询性能:

-- 在PostgreSQL中创建索引
CREATE INDEX idx_tasks_title ON tasks(title);
CREATE INDEX idx_tasks_completed ON tasks(completed);
CREATE INDEX idx_tasks_created_at ON tasks(created_at);

常见问题与解决方案

数据同步冲突处理

当多个用户同时修改同一数据时,可能会发生冲突。Electric提供了冲突检测机制:

// 冲突处理示例
const handleTaskToggle = async (task: Task) => {
  try {
    await electric.db.tasks.update({
      id: task.id,
      completed: !task.completed,
      // 添加乐观锁字段
      version: task.version
    })
  } catch (err) {
    if (err instanceof ConflictError) {
      // 处理冲突:重新获取最新数据并提示用户
      alert('数据已被其他用户修改,请刷新后重试')
      // 重新获取数据
      shape.refresh()
    }
  }
}

连接状态管理

实现健壮的连接状态管理,提升用户体验:

<template>
  <div class="connection-status" :class="statusClass">
    <span class="status-indicator" :class="status"></span>
    <span class="status-text">{{ statusText }}</span>
  </div>
</template>

<script setup lang="ts">
import { ref, onUnmounted } from 'vue'
import { electric } from '../electric'

const status = ref<'connected' | 'connecting' | 'disconnected'>('connecting')
const statusText = ref('连接中...')
const statusClass = ref('connecting')

// 订阅连接状态
const unsubscribe = electric.connectionState.subscribe(state => {
  switch (state.status) {
    case 'connected':
      status.value = 'connected'
      statusText.value = '已连接'
      statusClass.value = 'connected'
      break
    case 'connecting':
      status.value = 'connecting'
      statusText.value = '连接中...'
      statusClass.value = 'connecting'
      break
    case 'disconnected':
      status.value = 'disconnected'
      statusText.value = '已断开连接'
      statusClass.value = 'disconnected'
      break
  }
})

// 组件卸载时取消订阅
onUnmounted(unsubscribe)
</script>

<style scoped>
/* 样式实现省略 */
</style>

项目实战:构建实时协作任务管理应用

项目结构设计

electric-vue-task-app/
├── src/
│   ├── assets/           # 静态资源
│   ├── components/       # 通用组件
│   │   ├── ConnectionStatus.vue
│   │   ├── TaskItem.vue
│   │   └── UserPresence.vue
│   ├── composables/      # 组合式函数
│   │   ├── useElectric.ts
│   │   └── useTasks.ts
│   ├── electric.ts       # Electric配置
│   ├── main.ts           # 应用入口
│   ├── router/           # 路由配置
│   ├── views/            # 页面组件
│   │   ├── Dashboard.vue
│   │   ├── TaskDetail.vue
│   │   └── UserProfile.vue
│   └── types/            # 类型定义
├── public/               # 公共资源
└── package.json          # 项目依赖

数据库设计

-- 任务表
CREATE TABLE tasks (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  title TEXT NOT NULL,
  description TEXT,
  completed BOOLEAN DEFAULT false,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  user_id UUID NOT NULL REFERENCES users(id),
  version INTEGER DEFAULT 1
);

-- 用户表
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  avatar_url TEXT,
  created_at TIMESTAMP DEFAULT NOW()
);

-- 用户在线状态表
CREATE TABLE user_presence (
  user_id UUID PRIMARY KEY REFERENCES users(id),
  last_active TIMESTAMP DEFAULT NOW(),
  online BOOLEAN DEFAULT false,
  color TEXT DEFAULT '#007bff'
);

-- 创建必要的索引
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
CREATE INDEX idx_tasks_completed ON tasks(completed);

核心功能实现

完整实现代码请参考以下文件:

  1. 任务列表与管理views/Dashboard.vue
  2. 任务详情views/TaskDetail.vue
  3. 用户资料与设置views/UserProfile.vue

部署与监控

部署架构

mermaid

部署步骤

# 1. 克隆仓库
git clone https://gitcode.com/GitHub_Trending/el/electric.git
cd electric

# 2. 安装依赖
npm install

# 3. 配置环境变量
cp .env.example .env
# 编辑.env文件设置数据库连接等信息

# 4. 构建应用
npm run build

# 5. 启动服务
npm run start

性能监控

Electric提供了内置的性能监控功能:

// 监控查询性能
electric.metrics.subscribe(metrics => {
  console.log('查询性能:', metrics.queryPerformance)
  console.log('同步延迟:', metrics.syncLatency)
  console.log('连接状态:', metrics.connectionState)
})

总结与展望

通过本文介绍的方法,我们实现了Electric与Vue的深度集成,构建了一个功能完善的实时响应式应用。主要收获包括:

  1. 响应式数据同步:利用Electric的订阅机制和Vue的响应式系统,实现了数据的实时同步
  2. 离线数据访问:支持离线状态下的数据操作,网络恢复后自动同步
  3. 多用户协作:通过实时数据同步,实现了多用户协作功能
  4. 性能优化:学习了多种优化策略,确保应用在大数据量下仍保持良好性能

未来发展方向

  1. 更深入的Vue集成:开发专门的Vue插件,提供更简洁的API
  2. 自动代码生成:基于数据库模式自动生成TypeScript类型和API
  3. 更智能的冲突解决:实现基于业务规则的自动冲突解决
  4. 性能进一步优化:利用Web Workers处理复杂数据转换,避免主线程阻塞

学习资源推荐

  • 官方文档:Electric SQL官方文档提供了详细的API参考和概念解释
  • 示例项目:本文配套的示例项目可作为实际开发的参考
  • 社区论坛:Electric社区论坛是解决问题和分享经验的好地方

【免费下载链接】electric electric-sql/electric: 这是一个用于查询数据库的JavaScript库,支持多种数据库。适合用于需要使用JavaScript查询数据库的场景。特点:易于使用,支持多种数据库,具有灵活的查询构建和结果处理功能。 【免费下载链接】electric 项目地址: https://gitcode.com/GitHub_Trending/el/electric

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值