微前端进阶(一)

微前端进阶(一)

从架构设计到完整落地:手把手搭建一个生产可用的微前端 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 与过渡动画
  • 发布部署配置
相关推荐
卸任1 天前
Vibe coding工具使用心得
前端·ai编程
brien91 天前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance1 天前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M1 天前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写1 天前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李1 天前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易1 天前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马1 天前
前沿视界
前端
kyriewen1 天前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易1 天前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员