微前端进阶(一)

微前端进阶(一)

从架构设计到完整落地:手把手搭建一个生产可用的微前端 Portal 基座


目录

  1. 平台架构总览
  2. 基座应用项目初始化
  3. 统一布局框架
  4. 子应用注册中心
  5. 全局主题体系
  6. 国际化方案
  7. 错误边界与降级处理
  8. [全局 Loading 与过渡动画](#全局 Loading 与过渡动画)
  9. 基座应用发布与部署

一、平台架构总览

1.1 整体架构

复制代码
┌──────────────────────────────────────────────────────────────────┐
│                        接入层                                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────────────┐  │
│  │ Nginx    │  │ 统一认证 │  │ 权限中心 │  │ API 网关       │  │
│  │ 反向代理 │  │ SSO     │  │ RBAC    │  │ Kong/APISIX   │  │
│  └──────────┘  └──────────┘  └──────────┘  └────────────────┘  │
├──────────────────────────────────────────────────────────────────┤
│                     基座应用(Portal)                            │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  布局框架:Header + Sidebar + Content + Footer            │   │
│  │  全局功能:主题切换 / 国际化 / 用户菜单 / 消息通知        │   │
│  │  子应用注册中心:动态注册、懒加载、生命周期管理            │   │
│  │  基础设施:错误边界 / 全局 Loading / 路由守卫             │   │
│  └──────────────────────────────────────────────────────────┘   │
├──────────────────────────────────────────────────────────────────┤
│                     子应用集群                                    │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐          │
│  │ 子应用A │  │ 子应用B │  │ 子应用C │  │ 子应用D │  ...     │
│  │  React  │  │  Vue3   │  │ Angular │  │ 微前端  │          │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘          │
├──────────────────────────────────────────────────────────────────┤
│                     基础设施层                                    │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │ CI/CD    │  │ Sentry   │  │ Prometheus│  │ ELK     │       │
│  │ 流水线   │  │ 错误监控 │  │ 性能监控 │  │ 日志收集│       │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘       │
└──────────────────────────────────────────────────────────────────┘

1.2 技术选型

层级 技术栈 选型理由
基座框架 Vue 3 + TypeScript 生态成熟、组合式 API 灵活、与 qiankun 配合最佳
微前端方案 qiankun 国内主流、JS 沙箱 + 样式隔离开箱即用
构建工具 Vite 开发冷启动快、HMR 体验好
UI 组件 Element Plus Vue 3 生态首选,支持主题定制
状态管理 Pinia TypeScript 友好、简洁
路由 Vue Router 4 与 Vue 3 深度集成
HTTP 请求 Axios 拦截器机制完善,适合做统一鉴权

1.3 项目结构规划

复制代码
portal-platform/
├── src/
│   ├── main.ts                    # 入口:注册 qiankun + vue 实例
│   ├── App.vue                    # 根组件
│   ├── assets/
│   │   ├── styles/
│   │   │   ├── variables.scss     # 全局变量(主题色、间距等)
│   │   │   ├── reset.scss         # 样式重置
│   │   │   └── global.scss        # 全局样式
│   │   └── icons/                 # SVG 图标
│   ├── layouts/
│   │   ├── MainLayout.vue         # 主布局
│   │   ├── HeaderBar.vue          # 顶栏
│   │   ├── SidebarNav.vue         # 侧边导航
│   │   ├── ContentArea.vue        # 内容区(子应用挂载点)
│   │   └── FooterBar.vue          # 底栏
│   ├── micro/
│   │   ├── index.ts               # qiankun 初始化
│   │   ├── apps.ts                # 子应用注册配置
│   │   ├── loader.ts              # 自定义加载器
│   │   ├── lifecycles.ts          # 生命周期钩子
│   │   ├── sandbox.ts             # 沙箱增强
│   │   └── prefetch.ts            # 预加载策略
│   ├── router/
│   │   ├── index.ts               # 路由配置
│   │   ├── guards.ts              # 路由守卫(权限校验)
│   │   └── routes.ts              # 路由表
│   ├── stores/
│   │   ├── user.ts                # 用户状态
│   │   ├── theme.ts               # 主题状态
│   │   ├── micro.ts               # 子应用状态
│   │   └── permission.ts          # 权限状态
│   ├── composables/
│   │   ├── useAuth.ts             # 认证相关
│   │   ├── usePermission.ts       # 权限校验
│   │   ├── useMicroApp.ts         # 子应用操作
│   │   └── useTheme.ts            # 主题切换
│   ├── services/
│   │   ├── auth.ts                # 登录/登出 API
│   │   ├── user.ts                # 用户信息 API
│   │   ├── permission.ts          # 权限 API
│   │   └── micro.ts               # 子应用配置 API(动态获取)
│   ├── components/
│   │   ├── AppLoading.vue         # 全局 Loading
│   │   ├── ErrorBoundary.vue      # 错误边界
│   │   ├── MicroAppFallback.vue   # 子应用降级 UI
│   │   ├── AvatarMenu.vue         # 用户头像下拉
│   │   └── ThemeToggle.vue        # 主题切换按钮
│   └── utils/
│       ├── request.ts             # Axios 封装
│       ├── storage.ts             # 本地存储封装
│       └── helpers.ts             # 工具函数
├── public/
│   └── cdn/                       # 公共依赖 CDN 资源
├── package.json
├── tsconfig.json
├── vite.config.ts
└── .env.{development|production|staging}

二、基座应用项目初始化

2.1 创建项目

bash 复制代码
# 使用 Vite 创建 Vue3 + TypeScript 项目
pnpm create vite portal-platform --template vue-ts
cd portal-platform

# 安装核心依赖
pnpm add vue-router@4 pinia element-plus @element-plus/icons-vue
pnpm add qiankun axios
pnpm add -D sass unplugin-auto-import unplugin-vue-components

2.2 环境变量配置

typescript 复制代码
// .env.development
VITE_APP_TITLE=微前端平台(开发环境)
VITE_APP_BASE_URL=http://localhost:8080
VITE_APP_AUTH_URL=http://localhost:9090/auth
VITE_APP_API_BASE=http://localhost:9090/api
VITE_APP_SUB_APP_REACT=http://localhost:3001
VITE_APP_SUB_APP_VUE=http://localhost:3002
VITE_APP_ENV=development
typescript 复制代码
// .env.production
VITE_APP_TITLE=企业微前端平台
VITE_APP_BASE_URL=https://portal.company.com
VITE_APP_AUTH_URL=https://sso.company.com/auth
VITE_APP_API_BASE=https://api.company.com
VITE_APP_ENV=production
typescript 复制代码
// src/env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string;
  readonly VITE_APP_BASE_URL: string;
  readonly VITE_APP_AUTH_URL: string;
  readonly VITE_APP_API_BASE: string;
  readonly VITE_APP_SUB_APP_REACT: string;
  readonly VITE_APP_SUB_APP_VUE: string;
  readonly VITE_APP_ENV: 'development' | 'staging' | 'production';
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

2.3 Axios 封装(含统一鉴权)

typescript 复制代码
// src/utils/request.ts
import axios, { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
import { storage } from './storage';
import { ElMessage } from 'element-plus';
import router from '@/router';

const service = axios.create({
  baseURL: import.meta.env.VITE_APP_API_BASE,
  timeout: 15000,
  headers: { 'Content-Type': 'application/json' }
});

// 请求拦截器:注入 Token
service.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const token = storage.getToken();
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 响应拦截器:统一错误处理
service.interceptors.response.use(
  (response) => {
    const { code, data, message } = response.data;
    if (code === 0) return data;
    ElMessage.error(message || '请求失败');
    return Promise.reject(new Error(message));
  },
  (error) => {
    if (error.response?.status === 401) {
      storage.clearToken();
      router.push('/login');
      ElMessage.error('登录已过期,请重新登录');
    } else if (error.response?.status === 403) {
      ElMessage.error('无权限访问');
    } else if (error.code === 'ECONNABORTED') {
      ElMessage.error('请求超时,请稍后重试');
    } else {
      ElMessage.error(error.message || '网络错误');
    }
    return Promise.reject(error);
  }
);

export const request = {
  get<T = any>(url: string, config?: AxiosRequestConfig) {
    return service.get<T>(url, config).then(res => res.data);
  },
  post<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
    return service.post<T>(url, data, config).then(res => res.data);
  },
  put<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
    return service.put<T>(url, data, config).then(res => res.data);
  },
  delete<T = any>(url: string, config?: AxiosRequestConfig) {
    return service.delete<T>(url, config).then(res => res.data);
  }
};

三、统一布局框架

3.1 主布局组件

vue 复制代码
<!-- src/layouts/MainLayout.vue -->
<template>
  <div class="main-layout" :class="{ collapsed: sidebarCollapsed }">
    <HeaderBar
      :user="userStore.userInfo"
      :notifications="notificationStore.list"
      @toggle-sidebar="sidebarCollapsed = !sidebarCollapsed"
      @logout="handleLogout"
    />
    <div class="main-layout__body">
      <SidebarNav
        :menus="permissionStore.menus"
        :collapsed="sidebarCollapsed"
        :active-route="route.path"
        @menu-click="handleMenuClick"
      />
      <div class="main-layout__content">
        <ContentArea>
          <router-view v-slot="{ Component }">
            <transition name="fade" mode="out-in">
              <keep-alive :include="cachedViews">
                <component :is="Component" />
              </keep-alive>
            </transition>
          </router-view>
        </ContentArea>
        <FooterBar />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { usePermissionStore } from '@/stores/permission';
import { useNotificationStore } from '@/stores/notification';
import HeaderBar from './HeaderBar.vue';
import SidebarNav from './SidebarNav.vue';
import ContentArea from './ContentArea.vue';
import FooterBar from './FooterBar.vue';

const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const permissionStore = usePermissionStore();
const notificationStore = useNotificationStore();

const sidebarCollapsed = ref(false);
const cachedViews = ref<string[]>([]);

function handleMenuClick(menu: any) {
  if (menu.microApp) {
    router.push(menu.path);
  }
}

async function handleLogout() {
  await userStore.logout();
  router.push('/login');
}

onMounted(() => {
  userStore.fetchUserInfo();
  permissionStore.fetchMenus();
});
</script>

<style lang="scss" scoped>
.main-layout {
  height: 100vh;
  display: flex;
  flex-direction: column;

  &__body {
    flex: 1;
    display: flex;
    overflow: hidden;
  }

  &__content {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: auto;
    background: var(--bg-color-page, #f5f7fa);
    transition: margin-left 0.3s;
  }
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

3.2 顶栏组件

vue 复制代码
<!-- src/layouts/HeaderBar.vue -->
<template>
  <header class="header-bar">
    <div class="header-bar__left">
      <el-button
        text
        class="collapse-btn"
        @click="$emit('toggle-sidebar')"
      >
        <el-icon :size="20">
          <Fold v-if="!collapsed" />
          <Expand v-else />
        </el-icon>
      </el-button>
      <div class="header-bar__brand">
        <img src="/logo.svg" alt="logo" class="logo" />
        <span class="title">{{ appTitle }}</span>
      </div>
    </div>

    <div class="header-bar__center">
      <el-breadcrumb>
        <el-breadcrumb-item
          v-for="item in breadcrumbs"
          :key="item.path"
          :to="item.path"
        >
          {{ item.title }}
        </el-breadcrumb-item>
      </el-breadcrumb>
    </div>

    <div class="header-bar__right">
      <!-- 主题切换 -->
      <ThemeToggle />

      <!-- 国际化切换 -->
      <el-dropdown trigger="click" @command="handleLangChange">
        <el-button text>
          <el-icon><Globe /></el-icon>
          {{ currentLang.label }}
        </el-button>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item
              v-for="lang in languages"
              :key="lang.value"
              :command="lang.value"
            >
              {{ lang.label }}
            </el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>

      <!-- 消息通知 -->
      <el-badge :value="unreadCount" :hidden="unreadCount === 0">
        <el-button text @click="handleNotification">
          <el-icon :size="20"><Bell /></el-icon>
        </el-button>
      </el-badge>

      <!-- 用户菜单 -->
      <el-dropdown trigger="click" @command="handleUserCommand">
        <span class="user-profile">
          <el-avatar :src="user?.avatar" :size="32">
            {{ user?.name?.charAt(0) }}
          </el-avatar>
          <span class="user-name">{{ user?.name }}</span>
        </span>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item command="profile">
              <el-icon><User /></el-icon>个人中心
            </el-dropdown-item>
            <el-dropdown-item command="settings">
              <el-icon><Setting /></el-icon>系统设置
            </el-dropdown-item>
            <el-dropdown-item divided command="logout">
              <el-icon><SwitchButton /></el-icon>退出登录
            </el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
  </header>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from '@/composables/useI18n';
import ThemeToggle from '@/components/ThemeToggle.vue';

defineProps<{
  user?: { name: string; avatar?: string };
  notifications?: any[];
}>();

defineEmits<{
  'toggle-sidebar': [];
  logout: [];
}>();

const route = useRoute();
const {
  currentLang,
  languages,
  changeLang,
  appTitle
} = useI18n();

const breadcrumbs = computed(() => {
  return route.matched.map(r => ({
    path: r.path,
    title: (r.meta?.title as string) || r.name as string
  }));
});

const unreadCount = computed(() => 0);

function handleLangChange(lang: string) {
  changeLang(lang);
}

function handleNotification() {
  // 打开通知面板
}

function handleUserCommand(command: string) {
  // 处理用户菜单命令
}
</script>

3.3 侧边导航

vue 复制代码
<!-- src/layouts/SidebarNav.vue -->
<template>
  <aside class="sidebar" :class="{ collapsed }">
    <el-menu
      :default-active="activeRoute"
      :collapse="collapsed"
      :collapse-transition="false"
      :router="true"
      background-color="transparent"
      @select="handleSelect"
    >
      <template v-for="menu in menus" :key="menu.path">
        <!-- 有子菜单 -->
        <el-sub-menu v-if="menu.children?.length" :index="menu.path">
          <template #title>
            <el-icon v-if="menu.icon">
              <component :is="menu.icon" />
            </el-icon>
            <span>{{ menu.title }}</span>
          </template>
          <el-menu-item
            v-for="child in menu.children"
            :key="child.path"
            :index="child.path"
          >
            <el-icon v-if="child.icon">
              <component :is="child.icon" />
            </el-icon>
            <span>{{ child.title }}</span>
          </el-menu-item>
        </el-sub-menu>

        <!-- 无子菜单 -->
        <el-menu-item v-else :index="menu.path">
          <el-icon v-if="menu.icon">
            <component :is="menu.icon" />
          </el-icon>
          <span>{{ menu.title }}</span>
        </el-menu-item>
      </template>
    </el-menu>
  </aside>
</template>

<script setup lang="ts">
import type { MenuItem } from '@/types/menu';

defineProps<{
  menus: MenuItem[];
  collapsed: boolean;
  activeRoute: string;
}>();

const emit = defineEmits<{
  'menu-click': [menu: MenuItem];
}>();

function handleSelect(index: string) {
  // 如果菜单项标记为微应用,可以触发额外逻辑
}
</script>

<style lang="scss" scoped>
.sidebar {
  width: var(--sidebar-width, 220px);
  height: 100%;
  background: var(--bg-color-sidebar, #001529);
  transition: width 0.3s;
  overflow-y: auto;
  overflow-x: hidden;

  &.collapsed {
    width: var(--sidebar-collapsed-width, 64px);
  }

  :deep(.el-menu) {
    border-right: none;
  }

  :deep(.el-menu-item),
  :deep(.el-sub-menu__title) {
    color: rgba(255, 255, 255, 0.7);

    &:hover {
      color: #fff;
      background-color: rgba(255, 255, 255, 0.08);
    }

    &.is-active {
      color: #fff;
      background-color: var(--el-color-primary);
    }
  }
}
</style>

3.4 内容区(子应用挂载点)

vue 复制代码
<!-- src/layouts/ContentArea.vue -->
<template>
  <main class="content-area">
    <!-- 子应用挂载容器 -->
    <div
      v-if="isMicroRoute"
      id="micro-app-container"
      class="content-area__micro"
      v-loading="microLoading"
    >
      <MicroAppFallback
        v-if="microError"
        :error="microError"
        @retry="handleRetry"
      />
    </div>

    <!-- 基座自身路由内容 -->
    <slot v-else />
  </main>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useMicroStore } from '@/stores/micro';
import MicroAppFallback from '@/components/MicroAppFallback.vue';

const route = useRoute();
const microStore = useMicroStore();

const isMicroRoute = computed(() => microStore.isMicroRoute(route.path));
const microLoading = computed(() => microStore.loading);
const microError = computed(() => microStore.error);

function handleRetry() {
  microStore.retryLoad(route.path);
}
</script>

<style lang="scss" scoped>
.content-area {
  flex: 1;
  padding: 20px;
  min-height: 0;

  &__micro {
    height: 100%;
    min-height: calc(100vh - 120px);
    position: relative;
  }
}
</style>

3.5 路由配置

typescript 复制代码
// src/router/routes.ts
import type { RouteRecordRaw } from 'vue-router';

export const routes: RouteRecordRaw[] = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { title: '登录', noAuth: true }
  },
  {
    path: '/',
    component: () => import('@/layouts/MainLayout.vue'),
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('@/views/Dashboard.vue'),
        meta: { title: '工作台', icon: 'Odometer' }
      },
      // 本地模块路由
      {
        path: 'settings',
        name: 'Settings',
        component: () => import('@/views/Settings.vue'),
        meta: { title: '系统设置', icon: 'Setting' }
      },
      // 微前端子应用路由(占位路由,实际由 qiankun 接管)
      {
        path: 'react/:pathMatch(.*)*',
        name: 'MicroReact',
        component: () => import('@/views/MicroPlaceholder.vue'),
        meta: { title: 'React 应用', microApp: 'app-react' }
      },
      {
        path: 'vue/:pathMatch(.*)*',
        name: 'MicroVue',
        component: () => import('@/views/MicroPlaceholder.vue'),
        meta: { title: 'Vue 应用', microApp: 'app-vue' }
      }
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue'),
    meta: { title: '404' }
  }
];
typescript 复制代码
// src/router/guards.ts
import type { Router } from 'vue-router';
import { storage } from '@/utils/storage';
import { ElMessage } from 'element-plus';

export function setupRouterGuards(router: Router) {
  // 白名单:不需要登录即可访问
  const WHITE_LIST = ['/login'];

  router.beforeEach(async (to, from, next) => {
    // 设置页面标题
    const title = to.meta?.title;
    if (title) {
      document.title = `${title} - ${import.meta.env.VITE_APP_TITLE}`;
    }

    // 检查是否需要认证
    if (to.meta?.noAuth) {
      next();
      return;
    }

    const token = storage.getToken();

    if (!token) {
      // 未登录,跳转登录页
      if (!WHITE_LIST.includes(to.path)) {
        ElMessage.warning('请先登录');
        next({ path: '/login', query: { redirect: to.fullPath } });
        return;
      }
      next();
      return;
    }

    // 已登录但去了登录页,重定向到首页
    if (to.path === '/login') {
      next('/');
      return;
    }

    next();
  });

  router.afterEach((to) => {
    // 触发微应用激活
    const microApp = to.meta?.microApp as string | undefined;
    if (microApp) {
      window.dispatchEvent(new CustomEvent('micro:route-change', {
        detail: { appName: microApp, path: to.fullPath }
      }));
    }
  });
}
typescript 复制代码
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import { routes } from './routes';
import { setupRouterGuards } from './guards';

const router = createRouter({
  history: createWebHistory(import.meta.env.VITE_APP_BASE_URL),
  routes,
  scrollBehavior: () => ({ top: 0 })
});

setupRouterGuards(router);

export default router;

四、子应用注册中心

4.1 子应用配置

typescript 复制代码
// src/micro/apps.ts
import type { MicroAppConfig } from '@/types/micro';

// 子应用配置清单
export const microAppConfigs: MicroAppConfig[] = [
  {
    name: 'app-react',
    entry: import.meta.env.VITE_APP_SUB_APP_REACT,
    container: '#micro-app-container',
    activeRule: '/react',
    props: {
      appName: 'app-react',
      platform: 'react'
    }
  },
  {
    name: 'app-vue',
    entry: import.meta.env.VITE_APP_SUB_APP_VUE,
    container: '#micro-app-container',
    activeRule: '/vue',
    props: {
      appName: 'app-vue',
      platform: 'vue'
    }
  },
  {
    name: 'app-angular',
    entry: import.meta.env.VITE_APP_SUB_APP_ANGULAR,
    container: '#micro-app-container',
    activeRule: '/angular',
    props: {
      appName: 'app-angular',
      platform: 'angular'
    }
  }
];

// 从服务端动态获取子应用配置
export async function fetchMicroAppConfigs(): Promise<MicroAppConfig[]> {
  try {
    const response = await fetch('/api/micro-apps/configs');
    const configs = await response.json();
    return configs.map((cfg: any) => ({
      name: cfg.name,
      entry: cfg.entry,
      container: '#micro-app-container',
      activeRule: cfg.activeRule,
      props: {
        appName: cfg.name,
        platform: cfg.platform,
        permissions: cfg.permissions,
        features: cfg.features
      }
    }));
  } catch {
    // 动态获取失败,fallback 到本地配置
    console.warn('[Micro] 动态获取子应用配置失败,使用本地配置');
    return microAppConfigs;
  }
}

4.2 qiankun 初始化

typescript 复制代码
// src/micro/index.ts
import {
  registerMicroApps,
  start,
  addGlobalUncaughtErrorHandler,
  removeGlobalUncaughtErrorHandler
} from 'qiankun';
import { microAppConfigs, fetchMicroAppConfigs } from './apps';
import { microLifecycles } from './lifecycles';
import { useMicroStore } from '@/stores/micro';
import { prefetchStrategy } from './prefetch';

let isStarted = false;

// 启动微前端
export async function bootstrapMicroFrontend() {
  const microStore = useMicroStore();

  // 1. 获取子应用配置
  const configs = await fetchMicroAppConfigs();

  // 2. 注册子应用
  registerMicroApps(configs, {
    beforeLoad: async (app) => {
      microStore.setLoading(app.name, true);
      console.log(`[Micro] beforeLoad: ${app.name}`, app);
      return microLifecycles.beforeLoad(app);
    },
    afterMount: async (app) => {
      microStore.setLoading(app.name, false);
      microStore.setMounted(app.name, true);
      console.log(`[Micro] afterMount: ${app.name}`, app);
      return microLifecycles.afterMount(app);
    },
    afterUnmount: async (app) => {
      microStore.setMounted(app.name, false);
      microStore.clearError(app.name);
      console.log(`[Micro] afterUnmount: ${app.name}`, app);
      return microLifecycles.afterUnmount(app);
    }
  });

  // 3. 全局错误处理
  addGlobalUncaughtErrorHandler((event) => {
    const appName = (event as any)?.appOrParcelName;
    const error = (event as any)?.error || event;
    microStore.setError(appName || 'unknown', error);
    console.error(`[Micro] 全局错误: ${appName}`, error);
  });

  // 4. 启动
  if (!isStarted) {
    start({
      prefetch: prefetchStrategy,
      sandbox: {
        strictStyleIsolation: false,
        experimentalStyleIsolation: true
      },
      singular: false
    });
    isStarted = true;
  }
}

// 注销微前端(清理用)
export function destroyMicroFrontend() {
  if (isStarted) {
    removeGlobalUncaughtErrorHandler(() => {});
    isStarted = false;
  }
}

4.3 生命周期钩子

typescript 复制代码
// src/micro/lifecycles.ts
import type { AppGlobalState } from '@/types/micro';
import { useUserStore } from '@/stores/user';
import { useThemeStore } from '@/stores/theme';

export const microLifecycles = {
  async beforeLoad(app: any) {
    // 注入全局 Props
    const userStore = useUserStore();
    const themeStore = useThemeStore();

    app.props = {
      ...app.props,
      // 用户信息
      user: userStore.userInfo,
      // 主题
      theme: themeStore.currentTheme,
      // 权限
      permissions: userStore.permissions,
      // 通信 API
      globalState: {
        get: () => ({
          user: userStore.userInfo,
          theme: themeStore.currentTheme,
          token: userStore.token
        }),
        set: (state: Partial<AppGlobalState>) => {
          if (state.theme) themeStore.setTheme(state.theme);
          if (state.user) userStore.setUserInfo(state.user);
        },
        subscribe: (callback: (state: AppGlobalState) => void) => {
          return userStore.$subscribe(() => {
            callback({
              user: userStore.userInfo,
              theme: themeStore.currentTheme,
              token: userStore.token
            });
          });
        }
      }
    };
  },

  async afterMount(app: any) {
    // 子应用挂载后的操作,如埋点
    console.log(`[Lifecycle] 子应用 ${app.name} 挂载完成`);
  },

  async afterUnmount(app: any) {
    // 子应用卸载后的清理
    console.log(`[Lifecycle] 子应用 ${app.name} 已卸载`);
  }
};

4.4 预加载策略

typescript 复制代码
// src/micro/prefetch.ts
import type { App } from 'qiankun';

// 预加载优先级:根据用户行为预测
const PRIORITY_RULES: Record<string, {
  level: 'critical' | 'high' | 'normal' | 'low';
}> = {
  'app-dashboard': { level: 'critical' },
  'app-react': { level: 'high' },
  'app-vue': { level: 'high' },
  'app-angular': { level: 'normal' },
  'app-report': { level: 'low' }
};

export function prefetchStrategy(app: App) {
  const rule = PRIORITY_RULES[app.name];
  if (!rule) return false;

  switch (rule.level) {
    case 'critical':
      return 'immediate'; // 立即预加载
    case 'high':
      return true; // 首个子应用挂载后预加载
    case 'normal':
      return 'idle'; // 空闲时预加载
    case 'low':
      return false; // 不预加载,访问时加载
    default:
      return false;
  }
}

// 用户行为预测:鼠标悬停时预加载
export function setupPrefetchByHover() {
  const navLinks = document.querySelectorAll('[data-micro-app]');
  navLinks.forEach((link) => {
    let prefetchTimer: ReturnType<typeof setTimeout> | null = null;

    link.addEventListener('mouseenter', () => {
      const appName = link.getAttribute('data-micro-app');
      if (appName) {
        prefetchTimer = setTimeout(() => {
          import('qiankun').then(({ loadMicroApp }) => {
            // 手动触发预加载
          });
        }, 300);
      }
    });

    link.addEventListener('mouseleave', () => {
      if (prefetchTimer) {
        clearTimeout(prefetchTimer);
        prefetchTimer = null;
      }
    });
  });
}

五、全局主题体系

5.1 主题 Store

typescript 复制代码
// src/stores/theme.ts
import { defineStore } from 'pinia';
import { storage } from '@/utils/storage';

export type ThemeMode = 'light' | 'dark';
export type ThemeColor =
  | 'blue'
  | 'green'
  | 'orange'
  | 'purple'
  | 'red';

interface ThemeState {
  mode: ThemeMode;
  color: ThemeColor;
}

export const useThemeStore = defineStore('theme', {
  state: (): ThemeState => ({
    mode: storage.get('theme-mode') || 'light',
    color: storage.get('theme-color') || 'blue'
  }),

  getters: {
    currentTheme: (state) => state,
    isDark: (state) => state.mode === 'dark'
  },

  actions: {
    setMode(mode: ThemeMode) {
      this.mode = mode;
      storage.set('theme-mode', mode);
      this.applyTheme();
    },

    setColor(color: ThemeColor) {
      this.color = color;
      storage.set('theme-color', color);
      this.applyTheme();
    },

    toggleMode() {
      this.setMode(this.mode === 'light' ? 'dark' : 'light');
    },

    applyTheme() {
      const root = document.documentElement;

      // 切换暗黑模式
      if (this.mode === 'dark') {
        root.classList.add('dark');
        document.body.setAttribute('arco-theme', 'dark');
      } else {
        root.classList.remove('dark');
        document.body.removeAttribute('arco-theme');
      }

      // 切换主题色
      root.setAttribute('data-theme-color', this.color);

      // 通知所有已挂载的子应用
      window.dispatchEvent(new CustomEvent('micro:theme-change', {
        detail: { mode: this.mode, color: this.color }
      }));
    },

    // 初始化主题:从子应用同步
    initTheme() {
      this.applyTheme();
    }
  }
});

5.2 CSS 变量体系

scss 复制代码
// src/assets/styles/variables.scss

// 亮色主题 - 蓝色(默认)
:root,
[data-theme-color='blue'] {
  --color-primary: #1890ff;
  --color-primary-hover: #40a9ff;
  --color-primary-active: #096dd9;
  --color-primary-light: #e6f7ff;
  --color-primary-bg: #bae7ff;

  --color-success: #52c41a;
  --color-warning: #faad14;
  --color-danger: #ff4d4f;
  --color-info: #1890ff;

  --bg-color-page: #f5f7fa;
  --bg-color-container: #ffffff;
  --bg-color-sidebar: #001529;
  --bg-color-header: #ffffff;

  --text-color-primary: #303133;
  --text-color-regular: #606266;
  --text-color-secondary: #909399;
  --text-color-placeholder: #c0c4cc;

  --border-color-base: #dcdfe6;
  --border-color-light: #e4e7ed;
  --border-color-lighter: #ebeef5;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);

  --sidebar-width: 220px;
  --sidebar-collapsed-width: 64px;
  --header-height: 56px;
}

// 亮色主题 - 绿色
[data-theme-color='green'] {
  --color-primary: #52c41a;
  --color-primary-hover: #73d13d;
  --color-primary-active: #389e0d;
  --color-primary-light: #f6ffed;
  --color-primary-bg: #d9f7be;
}

// 亮色主题 - 橙色
[data-theme-color='orange'] {
  --color-primary: #fa8c16;
  --color-primary-hover: #ffa940;
  --color-primary-active: #d46b08;
  --color-primary-light: #fff7e6;
  --color-primary-bg: #ffe7ba;
}

// 暗黑主题覆盖
.dark {
  --color-primary: #1890ff;
  --color-primary-hover: #40a9ff;
  --color-primary-active: #096dd9;
  --color-primary-light: #111d2c;
  --color-primary-bg: #153450;

  --bg-color-page: #0f1117;
  --bg-color-container: #1a1c23;
  --bg-color-sidebar: #14161c;
  --bg-color-header: #1a1c23;

  --text-color-primary: #e5e5e5;
  --text-color-regular: #b0b0b0;
  --text-color-secondary: #808080;
  --text-color-placeholder: #505050;

  --border-color-base: #333;
  --border-color-light: #3a3a3a;
  --border-color-lighter: #444;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
}

5.3 主题切换组件

vue 复制代码
<!-- src/components/ThemeToggle.vue -->
<template>
  <el-dropdown trigger="click" @command="handleCommand">
    <el-button text>
      <el-icon :size="18">
        <Sunny v-if="!themeStore.isDark" />
        <Moon v-else />
      </el-icon>
    </el-button>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item command="mode">
          <el-icon>
            <Sunny v-if="themeStore.isDark" />
            <Moon v-else />
          </el-icon>
          {{ themeStore.isDark ? '切换亮色' : '切换暗黑' }}
        </el-dropdown-item>
        <el-dropdown-item divided>
          <span>主题色</span>
        </el-dropdown-item>
        <el-dropdown-item
          v-for="color in themeColors"
          :key="color.value"
          :command="`color-${color.value}`"
        >
          <span
            class="theme-dot"
            :style="{ background: color.hex }"
          />
          {{ color.label }}
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script setup lang="ts">
import { useThemeStore, type ThemeColor } from '@/stores/theme';

const themeStore = useThemeStore();

const themeColors: { label: string; value: ThemeColor; hex: string }[] = [
  { label: '默认蓝', value: 'blue', hex: '#1890ff' },
  { label: '极客绿', value: 'green', hex: '#52c41a' },
  { label: '活力橙', value: 'orange', hex: '#fa8c16' },
  { label: '优雅紫', value: 'purple', hex: '#722ed1' },
  { label: '烈焰红', value: 'red', hex: '#f5222d' }
];

function handleCommand(command: string) {
  if (command === 'mode') {
    themeStore.toggleMode();
  } else if (command.startsWith('color-')) {
    const color = command.replace('color-', '') as ThemeColor;
    themeStore.setColor(color);
  }
}
</script>

<style lang="scss" scoped>
.theme-dot {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: middle;
}
</style>

六、国际化方案

6.1 国际化组合式函数

typescript 复制代码
// src/composables/useI18n.ts
import { ref, computed } from 'vue';
import { storage } from '@/utils/storage';

type Lang = 'zh-CN' | 'en-US';

interface LangOption {
  value: Lang;
  label: string;
}

const messages: Record<Lang, Record<string, string>> = {
  'zh-CN': {
    'nav.dashboard': '工作台',
    'nav.settings': '系统设置',
    'nav.reactApp': 'React 应用',
    'nav.vueApp': 'Vue 应用',
    'user.profile': '个人中心',
    'user.settings': '系统设置',
    'user.logout': '退出登录',
    'theme.light': '亮色模式',
    'theme.dark': '暗黑模式',
    'app.title': '微前端平台',
    'micro.loading': '子应用加载中...',
    'micro.error': '子应用加载失败',
    'micro.retry': '点击重试',
    'error.boundary': '页面出错了',
    'error.boundary.desc': '请刷新页面或联系技术支持'
  },
  'en-US': {
    'nav.dashboard': 'Dashboard',
    'nav.settings': 'Settings',
    'nav.reactApp': 'React App',
    'nav.vueApp': 'Vue App',
    'user.profile': 'Profile',
    'user.settings': 'Settings',
    'user.logout': 'Logout',
    'theme.light': 'Light Mode',
    'theme.dark': 'Dark Mode',
    'app.title': 'Micro Frontend Platform',
    'micro.loading': 'Loading micro app...',
    'micro.error': 'Micro app failed to load',
    'micro.retry': 'Click to retry',
    'error.boundary': 'Something went wrong',
    'error.boundary.desc': 'Please refresh or contact support'
  }
};

const currentLang = ref<Lang>(storage.get('lang') || 'zh-CN');
const languages: LangOption[] = [
  { value: 'zh-CN', label: '简体中文' },
  { value: 'en-US', label: 'English' }
];

export function useI18n() {
  const t = (key: string): string => {
    return messages[currentLang.value]?.[key] || key;
  };

  const appTitle = computed(() => t('app.title'));

  function changeLang(lang: Lang) {
    currentLang.value = lang;
    storage.set('lang', lang);
    // 通知子应用
    window.dispatchEvent(new CustomEvent('micro:lang-change', {
      detail: { lang }
    }));
  }

  return {
    currentLang,
    languages,
    t,
    appTitle,
    changeLang
  };
}

6.2 与子应用的国际化同步

typescript 复制代码
// src/micro/i18n-sync.ts
import { useI18n } from '@/composables/useI18n';

// 监听子应用语言切换请求
window.addEventListener('micro:lang-change-request', (e: CustomEvent) => {
  const { lang } = e.detail;
  const { changeLang } = useI18n();
  changeLang(lang);
});

// 向子应用广播当前语言
export function syncI18nToMicroApps() {
  const { currentLang } = useI18n();
  window.dispatchEvent(new CustomEvent('micro:lang-change', {
    detail: { lang: currentLang.value }
  }));
}

// 在子应用加载时同步
export function setupI18nSync() {
  // 子应用挂载后自动同步
  window.addEventListener('micro:app-mounted', () => {
    syncI18nToMicroApps();
  });
}

七、错误边界与降级处理

7.1 错误边界组件

vue 复制代码
<!-- src/components/ErrorBoundary.vue -->
<template>
  <div v-if="hasError" class="error-boundary">
    <div class="error-boundary__content">
      <el-result
        icon="error"
        :title="t('error.boundary')"
        :sub-title="errorMessage"
      >
        <template #extra>
          <el-button type="primary" @click="handleRefresh">
            {{ t('micro.retry') }}
          </el-button>
          <el-button @click="handleGoHome">
            返回首页
          </el-button>
        </template>
      </el-result>

      <details v-if="isDev" class="error-boundary__details">
        <summary>错误详情(开发环境可见)</summary>
        <pre>{{ errorDetail }}</pre>
      </details>
    </div>
  </div>
  <slot v-else />
</template>

<script setup lang="ts">
import { ref, computed, onErrorCaptured } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from '@/composables/useI18n';

const props = defineProps<{
  fallback?: boolean;
}>();

const router = useRouter();
const { t } = useI18n();

const hasError = ref(false);
const errorMessage = ref('');
const errorDetail = ref('');

const isDev = computed(() =>
  import.meta.env.VITE_APP_ENV === 'development'
);

onErrorCaptured((err, instance, info) => {
  hasError.value = true;
  errorMessage.value = err instanceof Error
    ? err.message
    : '未知错误';
  errorDetail.value = `
错误: ${err}
组件: ${instance?.$options?.name || '未知'}
信息: ${info}
时间: ${new Date().toISOString()}
  `.trim();
  return false;
});

function handleRefresh() {
  hasError.value = false;
  errorMessage.value = '';
  errorDetail.value = '';
  window.location.reload();
}

function handleGoHome() {
  hasError.value = false;
  router.push('/');
}
</script>

<style lang="scss" scoped>
.error-boundary {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  min-height: 400px;

  &__content {
    text-align: center;
  }

  &__details {
    margin-top: 20px;
    text-align: left;
    pre {
      background: #f5f5f5;
      padding: 12px;
      border-radius: 4px;
      font-size: 12px;
      max-height: 300px;
      overflow: auto;
    }
  }
}
</style>

7.2 子应用降级 UI

vue 复制代码
<!-- src/components/MicroAppFallback.vue -->
<template>
  <div class="micro-fallback">
    <el-result
      icon="warning"
      :title="t('micro.error')"
      :sub-title="errorMessage"
    >
      <template #extra>
        <el-button type="primary" @click="$emit('retry')">
          {{ t('micro.retry') }}
        </el-button>
        <el-button @click="handleSkip">
          跳过此应用
        </el-button>
      </template>
    </el-result>

    <!-- 可选:显示降级后的静态内容 -->
    <div v-if="degradedContent" class="micro-fallback__degraded">
      <h3>降级内容</h3>
      <p>{{ degradedContent }}</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from '@/composables/useI18n';

const props = defineProps<{
  error: Error | string | null;
  appName?: string;
}>();

defineEmits<{
  retry: [];
}>();

const router = useRouter();
const { t } = useI18n();

const errorMessage = computed(() => {
  if (!props.error) return t('micro.error');
  if (typeof props.error === 'string') return props.error;
  return props.error.message || t('micro.error');
});

const degradedContent = computed(() => {
  // 根据子应用名称返回降级内容
  const degradedMap: Record<string, string> = {
    'app-dashboard': '仪表盘暂不可用,请稍后重试',
    'app-report': '报表系统维护中'
  };
  return props.appName ? degradedMap[props.appName] : '';
});

function handleSkip() {
  router.push('/');
}
</script>

<style lang="scss" scoped>
.micro-fallback {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  padding: 40px;

  &__degraded {
    margin-top: 24px;
    padding: 20px;
    background: var(--bg-color-container);
    border-radius: 8px;
    border: 1px dashed var(--border-color-base);
    max-width: 480px;
  }
}
</style>

7.3 全局错误处理

typescript 复制代码
// src/utils/errorHandler.ts
import type { App } from 'vue';

export function setupGlobalErrorHandler(app: App) {
  // Vue 全局错误
  app.config.errorHandler = (err, instance, info) => {
    console.error('[GlobalError] Vue 错误:', err);
    console.error('[GlobalError] 组件:', instance?.$options?.name);
    console.error('[GlobalError] 信息:', info);

    // 上报监控平台
    reportError({
      type: 'vue',
      message: err instanceof Error ? err.message : String(err),
      stack: err instanceof Error ? err.stack : '',
      component: instance?.$options?.name,
      info
    });
  };

  // 未捕获的 Promise 错误
  window.addEventListener('unhandledrejection', (event) => {
    console.error('[GlobalError] 未捕获的 Promise 错误:', event.reason);
    reportError({
      type: 'promise',
      message: event.reason?.message || String(event.reason),
      stack: event.reason?.stack || ''
    });
  });

  // 运行时错误
  window.addEventListener('error', (event) => {
    if (event.target !== window) return; // 只处理全局错误
    console.error('[GlobalError] 运行时错误:', event.error);
    reportError({
      type: 'runtime',
      message: event.error?.message || String(event.error),
      stack: event.error?.stack || ''
    });
  });
}

interface ErrorReport {
  type: string;
  message: string;
  stack?: string;
  component?: string;
  info?: string;
  timestamp?: number;
}

function reportError(report: ErrorReport) {
  report.timestamp = Date.now();

  // 发送到监控平台
  if (window.__monitor__) {
    window.__monitor__.reportError(report);
  }

  // 开发环境也打印到控制台
  if (import.meta.env.DEV) {
    console.table(report);
  }
}

八、全局 Loading 与过渡动画

8.1 全局 Loading 组件

vue 复制代码
<!-- src/components/AppLoading.vue -->
<template>
  <transition name="loading-fade">
    <div v-if="visible" class="app-loading">
      <div class="app-loading__spinner">
        <svg class="app-loading__icon" viewBox="0 0 50 50">
          <circle
            class="path"
            cx="25" cy="25" r="20"
            fill="none"
            stroke="currentColor"
            stroke-width="4"
          />
        </svg>
        <p class="app-loading__text">{{ text }}</p>
      </div>
    </div>
  </transition>
</template>

<script setup lang="ts">
defineProps<{
  visible: boolean;
  text?: string;
}>();
</script>

<style lang="scss" scoped>
.app-loading {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  justify-content: center;
  align-items: center;
  background: var(--bg-color-page);

  &__spinner {
    text-align: center;
  }

  &__icon {
    width: 40px;
    height: 40px;
    animation: rotate 2s linear infinite;
    color: var(--color-primary);

    .path {
      stroke-dasharray: 90, 150;
      stroke-dashoffset: 0;
      stroke-linecap: round;
      animation: dash 1.5s ease-in-out infinite;
    }
  }

  &__text {
    margin-top: 16px;
    color: var(--text-color-secondary);
    font-size: 14px;
  }
}

@keyframes rotate {
  100% { transform: rotate(360deg); }
}

@keyframes dash {
  0% {
    stroke-dasharray: 1, 150;
    stroke-dashoffset: 0;
  }
  50% {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -35;
  }
  100% {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -124;
  }
}

.loading-fade-enter-active,
.loading-fade-leave-active {
  transition: opacity 0.3s ease;
}
.loading-fade-enter-from,
.loading-fade-leave-to {
  opacity: 0;
}
</style>

8.2 子应用切换过渡动画

typescript 复制代码
// src/micro/transitions.ts

// 子应用切出前等待动画完成
const TRANSITION_DURATION = 300;

export function withTransition(
  action: () => Promise<void>,
  container: HTMLElement
): Promise<void> {
  return new Promise((resolve) => {
    container.classList.add('micro-transition-exit');
    container.style.opacity = '0';
    container.style.transform = 'translateX(20px)';

    setTimeout(async () => {
      await action();
      container.classList.remove('micro-transition-exit');
      container.classList.add('micro-transition-enter');
      container.style.opacity = '1';
      container.style.transform = 'translateX(0)';

      setTimeout(() => {
        container.classList.remove('micro-transition-enter');
        resolve();
      }, TRANSITION_DURATION);
    }, TRANSITION_DURATION);
  });
}
scss 复制代码
// 全局样式补充
.micro-transition-exit {
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.micro-transition-enter {
  transition: opacity 0.3s ease, transform 0.3s ease;
}

九、基座应用发布与部署

9.1 Nginx 配置

nginx 复制代码
# nginx/portal.conf
upstream portal_api {
    server api.company.com:9090;
}

server {
    listen 80;
    server_name portal.company.com;

    # 基座应用静态资源
    location / {
        root /data/www/portal/dist;
        index index.html;
        try_files $uri $uri/ /index.html;

        # 强缓存:带 hash 的资源
        location ~* \.(js|css|png|jpg|svg|woff2?)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }

    # API 代理
    location /api/ {
        proxy_pass http://portal_api;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 子应用入口(CORS 配置)
    location /micro/ {
        proxy_pass http://portal_api;
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
        add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization";
    }
}

9.2 Docker 部署

dockerfile 复制代码
# Dockerfile
FROM node:20-alpine AS builder

WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:stable-alpine AS runner

COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx/portal.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

9.3 构建脚本

json 复制代码
// package.json scripts
{
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "build:staging": "vite build --mode staging",
    "build:prod": "vite build --mode production",
    "preview": "vite preview",
    "docker:build": "docker build -t portal-platform:latest .",
    "docker:run": "docker run -d -p 80:80 portal-platform:latest",
    "analyze": "vite build --analyze"
  }
}

通过本文,你已经搭建了一个生产可用的微前端 Portal 基座,包含:

  • 完整的布局框架(Header + Sidebar + Content)
  • 基于 qiankun 的子应用注册中心
  • 动态加载与预加载策略
  • 全局主题系统(暗黑模式 + 多主题色)
  • 国际化方案
  • 错误边界与降级处理
  • 全局 Loading 与过渡动画
  • 发布部署配置
相关推荐
ZC跨境爬虫1 小时前
跟着 MDN 学CSS day_34:(CSS 布局全面解析)
前端·css·ui·html·tensorflow
万少1 小时前
湖南卫视的秘密武器曝光!芒果灵创,专业AI影视创作平台
前端·javascript·后端
边界条件╝1 小时前
微前端进阶(三)
前端
红辣椒...1 小时前
codex+第三方模型
java·服务器·前端
木子雨廷1 小时前
Flutter 使用 flutter_flavorizr 多渠道打包
前端·flutter
环境工程笔记1 小时前
浏览器自动化跑成功了,为什么结果还是不对?
前端
东风破_1 小时前
一文搞懂 JavaScript 变量声明:var、let、const 到底有什么区别?
前端·javascript
问心无愧05131 小时前
ctf show web入门261
android·前端·笔记
触底反弹1 小时前
你真的理解 JavaScript 变量提升(Hoisting)吗?从 V8 引擎编译原理深入剖析
前端·面试