Skip to content

远程组件与微前端

本教程将教你如何使用 DVHA Pro 的远程组件功能,实现真正的微前端架构,让后端可以直接输出 Vue 组件代码。

📋 前置条件

🎯 目标效果

完成本教程后,你将能够:

  • 🚀 实现后端直接输出 Vue 组件
  • 🔄 动态加载和渲染远程组件
  • 📦 配置远程组件的依赖包管理
  • 🎨 结合 UnoCSS 实现样式的动态编译
  • 🌐 构建完整的微前端应用架构

💡 远程组件特点

DVHA Pro 的远程组件系统具有以下特点:

  • 后端渲染:后端直接输出 Vue 组件代码
  • 实时编译:使用 vue3-sfc-loader 实时编译 Vue 组件
  • 依赖管理:支持自定义包映射和依赖注入
  • 样式支持:与 UnoCSS 集成,支持动态样式编译
  • 国际化:支持组件级别的国际化配置
  • 错误处理:完善的错误处理和降级机制

🔧 第一步:配置远程组件系统

修改 src/main.ts,配置远程组件的基础设置:

typescript
import { createDux } from '@duxweb/dvha-core'
import { createApp } from 'vue'
import NaiveUI from 'naive-ui'
import * as DuxPro from '@duxweb/dvha-pro'
import App from './App.vue'
import { dataProvider } from './dataProvider'

const app = createApp(App)

const config = {
  defaultManage: 'admin',
  manages: [{
    name: 'admin',
    title: '管理后台',

        // 远程组件配置
    remote: {
      // 远程组件 API 配置
      apiMethod: 'POST',
      apiRoutePath: '/api/remote/component'
    },

    dataProvider,

    menus: [
      {
        name: 'dashboard',
        label: '仪表盘',
        path: 'dashboard',
        icon: 'i-tabler:dashboard',
        component: () => import('./pages/dashboard.vue')
      },
      {
        name: 'remote-demo',
        label: '远程组件演示',
        path: 'remote-demo',
        icon: 'i-tabler:components',
        loader: 'remote',  // 指定使用远程加载器
        meta: {
          path: '/demo/component'  // 远程组件路径
        }
      }
    ]
  }],
}

app.use(createDux(config))
app.mount('#app')

🌐 第二步:后端接口实现

创建后端接口,返回 Vue 组件代码。以下是不同后端语言的示例:

PHP 实现

php
<?php
// api/remote/component.php

header('Content-Type: application/json');

$path = $_POST['path'] ?? '';

switch ($path) {
    case '/demo/component':
        $response = [
            'code' => 200,
            'data' => [
                'content' => getComponentContent(),
                'type' => 'vue'
            ]
        ];
        break;

    default:
        $response = [
            'code' => 404,
            'message' => 'Component not found'
        ];
}

echo json_encode($response);

function getComponentContent() {
    return <<<'VUE'
<template>
  <div class="p-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl shadow-lg">
    <h2 class="text-2xl font-bold text-white mb-4">{{ title }}</h2>

    <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
      <div
        v-for="stat in stats"
        :key="stat.id"
        class="bg-white/20 backdrop-blur-sm rounded-lg p-4 hover:bg-white/30 transition-all cursor-pointer"
        @click="handleStatClick(stat)"
      >
        <div class="text-white/80 text-sm">{{ stat.label }}</div>
        <div class="text-2xl font-bold text-white">{{ stat.value }}</div>
        <div class="text-white/60 text-xs mt-1">{{ stat.change }}</div>
      </div>
    </div>

    <DuxCard title="数据列表" class="bg-white">
      <DuxList
        :data="listData"
        :loading="loading"
        @refresh="handleRefresh"
      >
        <template #item="{ item }">
          <div class="flex items-center justify-between p-4 border-b">
            <div>
              <div class="font-medium">{{ item.name }}</div>
              <div class="text-sm text-gray-500">{{ item.description }}</div>
            </div>
            <div class="text-right">
              <div class="text-lg font-bold text-primary">{{ item.value }}</div>
              <div class="text-xs text-gray-400">{{ formatDate(item.updatedAt) }}</div>
            </div>
          </div>
        </template>
      </DuxList>
    </DuxCard>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useList, useMessage } from '@duxweb/dvha-core'
import { DuxCard, DuxList } from '@duxweb/dvha-pro'
import dayjs from 'dayjs'

const message = useMessage()
const title = ref('远程组件演示')

// 统计数据
const stats = ref([
  { id: 1, label: '总用户数', value: '12,345', change: '+5.2%' },
  { id: 2, label: '今日访问', value: '1,234', change: '+12.1%' },
  { id: 3, label: '转化率', value: '3.45%', change: '-2.1%' }
])

// 列表数据
const { data: listData, loading, refetch } = useList({
  path: 'remote/data',
  pagination: { page: 1, pageSize: 10 }
})

const handleStatClick = (stat) => {
  message.success(`点击了 ${stat.label}: ${stat.value}`)
}

const handleRefresh = () => {
  refetch()
  message.info('数据已刷新')
}

const formatDate = (date) => {
  return dayjs(date).format('YYYY-MM-DD HH:mm')
}

onMounted(() => {
  console.log('远程组件已加载')
})
</script>

<style scoped>
/* 组件级样式 */
.custom-gradient {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
</style>

<i18n>
{
  "zh-CN": {
    "title": "远程组件演示",
    "totalUsers": "总用户数",
    "todayVisits": "今日访问",
    "conversionRate": "转化率"
  },
  "en-US": {
    "title": "Remote Component Demo",
    "totalUsers": "Total Users",
    "todayVisits": "Today Visits",
    "conversionRate": "Conversion Rate"
  }
}
</i18n>
VUE;
}
?>

Node.js 实现

javascript
// api/remote/component.js
const express = require('express')
const router = express.Router()

router.post('/component', (req, res) => {
  const { path } = req.body

  switch (path) {
    case '/demo/component':
      res.json({
        code: 200,
        data: {
          content: getComponentContent(),
          type: 'vue'
        }
      })
      break

    case '/demo/chart':
      res.json({
        code: 200,
        data: {
          content: getChartComponent(),
          type: 'vue'
        }
      })
      break

    default:
      res.status(404).json({
        code: 404,
        message: 'Component not found'
      })
  }
})

function getComponentContent() {
  return `
<template>
  <div class="space-y-6">
    <DuxCard title="实时图表">
      <DuxChart
        type="line"
        :data="chartData"
        :options="chartOptions"
        height="300px"
      />
    </DuxCard>

    <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
      <DuxCard title="用户增长">
        <DuxStats
          :value="userGrowth.value"
          :change="userGrowth.change"
          :trend="userGrowth.trend"
          color="primary"
        />
      </DuxCard>

      <DuxCard title="收入统计">
        <DuxStats
          :value="revenue.value"
          :change="revenue.change"
          :trend="revenue.trend"
          color="success"
        />
      </DuxCard>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { DuxCard, DuxChart, DuxStats } from '@duxweb/dvha-pro'

const chartData = ref({
  labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
  datasets: [{
    label: '用户增长',
    data: [65, 59, 80, 81, 56, 55],
    borderColor: 'rgb(59, 130, 246)',
    backgroundColor: 'rgba(59, 130, 246, 0.1)',
    tension: 0.4
  }]
})

const chartOptions = ref({
  responsive: true,
  plugins: {
    legend: {
      position: 'top'
    }
  },
  scales: {
    y: {
      beginAtZero: true
    }
  }
})

const userGrowth = ref({
  value: '12,345',
  change: '+15.3%',
  trend: 'up'
})

const revenue = ref({
  value: '¥234,567',
  change: '+8.2%',
  trend: 'up'
})
</script>
`
}

function getChartComponent() {
  return `
<template>
  <div class="p-6">
    <h2 class="text-2xl font-bold mb-6">数据分析</h2>

    <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
      <DuxCard title="饼图统计">
        <DuxChart
          type="pie"
          :data="pieData"
          height="250px"
        />
      </DuxCard>

      <DuxCard title="柱状图对比">
        <DuxChart
          type="bar"
          :data="barData"
          height="250px"
        />
      </DuxCard>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { DuxCard, DuxChart } from '@duxweb/dvha-pro'

const pieData = ref({
  labels: ['桌面端', '移动端', '平板端'],
  datasets: [{
    data: [45, 35, 20],
    backgroundColor: [
      'rgba(59, 130, 246, 0.8)',
      'rgba(16, 185, 129, 0.8)',
      'rgba(245, 158, 11, 0.8)'
    ]
  }]
})

const barData = ref({
  labels: ['Q1', 'Q2', 'Q3', 'Q4'],
  datasets: [{
    label: '2023年',
    data: [65, 78, 90, 81],
    backgroundColor: 'rgba(59, 130, 246, 0.8)'
  }, {
    label: '2024年',
    data: [78, 85, 96, 89],
    backgroundColor: 'rgba(16, 185, 129, 0.8)'
  }]
})
</script>
`
}

module.exports = router

🎨 第三步:样式与主题集成

远程组件完全支持 UnoCSS 样式系统:

vue
<script setup>
import { useMessage } from '@duxweb/dvha-core'
import { ref } from 'vue'

const message = useMessage()

const cards = ref([
  {
    id: 1,
    title: '总收入',
    description: '本月总收入统计',
    value: '¥234,567',
    change: '+12.5%',
    trend: 'up',
    category: '财务',
    icon: 'i-tabler:currency-dollar',
    gradient: 'from-emerald-400 to-cyan-400'
  },
  {
    id: 2,
    title: '活跃用户',
    description: '本月活跃用户数量',
    value: '12,345',
    change: '+8.3%',
    trend: 'up',
    category: '用户',
    icon: 'i-tabler:users',
    gradient: 'from-blue-400 to-indigo-400'
  },
  {
    id: 3,
    title: '订单量',
    description: '本月订单总数',
    value: '1,234',
    change: '-2.1%',
    trend: 'down',
    category: '销售',
    icon: 'i-tabler:shopping-cart',
    gradient: 'from-purple-400 to-pink-400'
  }
])

const actions = ref([
  { id: 1, label: '刷新数据', icon: 'i-tabler:refresh' },
  { id: 2, label: '导出报告', icon: 'i-tabler:download' },
  { id: 3, label: '设置提醒', icon: 'i-tabler:bell' },
  { id: 4, label: '查看详情', icon: 'i-tabler:eye' }
])

function handleAction(action) {
  message.success(`执行操作: ${action.label}`)
}
</script>

<template>
  <!-- 远程组件中使用 UnoCSS 类名 -->
  <div class="min-h-screen bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500">
    <div class="container mx-auto px-4 py-8">
      <!-- 响应式网格布局 -->
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        <!-- 卡片组件 -->
        <div
          v-for="card in cards"
          :key="card.id"
          class="bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 hover:bg-white/20 transition-all duration-300 transform hover:scale-105"
        >
          <div class="flex items-center justify-between mb-4">
            <div :class="`w-12 h-12 rounded-full bg-gradient-to-r ${card.gradient} flex items-center justify-center`">
              <i :class="`${card.icon} text-white text-xl`" />
            </div>
            <span class="text-white/60 text-sm">{{ card.category }}</span>
          </div>

          <h3 class="text-white font-semibold text-lg mb-2">
            {{ card.title }}
          </h3>
          <p class="text-white/80 text-sm mb-4">
            {{ card.description }}
          </p>

          <div class="flex items-center justify-between">
            <span class="text-2xl font-bold text-white">{{ card.value }}</span>
            <span :class="`text-sm px-2 py-1 rounded-full ${card.trend === 'up' ? 'bg-green-500/20 text-green-300' : 'bg-red-500/20 text-red-300'}`">
              {{ card.change }}
            </span>
          </div>
        </div>
      </div>

      <!-- 交互式按钮组 -->
      <div class="mt-8 flex flex-wrap gap-4 justify-center">
        <button
          v-for="action in actions"
          :key="action.id"
          class="px-6 py-3 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-xl border border-white/20 text-white font-medium transition-all duration-300 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-white/30"
          @click="handleAction(action)"
        >
          <i :class="`${action.icon} mr-2`" />
          {{ action.label }}
        </button>
      </div>
    </div>
  </div>
</template>

🧪 第四步:测试远程组件

  1. 启动后端服务,确保远程组件 API 可访问
  2. 启动前端项目
  3. 访问远程组件页面,验证组件正常加载
  4. 测试组件交互功能
  5. 验证样式和主题是否正确应用
bash
# 启动开发服务器
npm run dev

# 访问远程组件页面
# http://localhost:3000/admin/remote-demo

🚀 第七步:生产环境部署

缓存优化

typescript
// 生产环境配置
const productionConfig = {
  remote: {
    // 启用组件缓存
    cache: {
      enabled: true,
      maxAge: 60 * 60 * 1000, // 1小时
      maxSize: 100 // 最多缓存100个组件
    },

    // API 配置
    apiMethod: 'POST',
    apiRoutePath: '/api/remote/component'
  }
}

安全配置

php
<?php
// 后端安全检查
function validateRemoteRequest($path, $user) {
    // 检查用户权限
    if (!$user || !$user['permissions']) {
        throw new Exception('Unauthorized');
    }

    // 检查路径白名单
    $allowedPaths = [
        '/demo/component',
        '/dashboard/stats',
        '/reports/chart'
    ];

    if (!in_array($path, $allowedPaths)) {
        throw new Exception('Path not allowed');
    }

    // 检查组件权限
    $requiredPermission = getRequiredPermission($path);
    if ($requiredPermission && !hasPermission($user, $requiredPermission)) {
        throw new Exception('Insufficient permissions');
    }

    return true;
}
?>

💡 最佳实践

  • 单一职责:每个远程组件只负责一个功能
  • 依赖最小化:尽量减少外部依赖
  • 错误边界:实现完善的错误处理
  • 性能优化:避免不必要的重新渲染

🎉 总结

通过本教程,你已经掌握了 DVHA Pro 远程组件系统的核心功能:

  • ✅ 配置远程组件加载器
  • ✅ 实现后端组件接口
  • ✅ 使用 JSON Schema 配置
  • ✅ 集成样式和主题系统
  • ✅ 处理错误和降级
  • ✅ 优化性能和安全

远程组件系统让你能够:

  • 🚀 实现真正的微前端架构
  • 🔄 动态更新前端组件而无需重新部署
  • 📦 灵活管理组件依赖和版本
  • 🎨 保持一致的设计系统和用户体验

这为构建大型、可扩展的企业级应用提供了强大的技术基础。