vue3 ts + qiankun微前端+1Panel 实现多个 vue3 子项目,分别构建部署,主应用通过动态路由访问子应用和主应用自己的组件

一、安装命令

1.主应用安装命令
npm create vue@latest main-app
cd main-app

# 安装依赖
npm install

# 安装 qiankun
npm install qiankun --save
2.子应用安装命令
npm create vue@latest child-app
cd child-app


# 安装依赖
npm install


# (关键)为了兼容 qiankun,需要安装支持跨环境构建的插件:
npm install vite-plugin-qiankun --save-dev
✅ 3.推荐额外工具(可选)

这些工具在大型微前端项目中常用:

工具包作用说明
cross-env跨平台设置环境变量
piniaVue3 状态管理(替代 Vuex)
vue-router@4Vue3 路由
vite构建工具

安装命令:

npm install cross-env pinia vue-router@4

二、项目结构

1.主应用 
main-app/
├── src/
│   ├── views/
│   │   ├── Login.vue
│   │   ├── Home.vue
│   │   ├── AppStore.vue  <---主应用自己的组件
│   │   ├── SubAppView.vue  <-- 渲染子应用的视图容器
│   ├── router/
│   │   └── index.ts
│   ├── App.vue
│   ├── main.ts
2.子应用
child-app/
├── src/
│   ├── views/
│   │   ├── Home.vue
│   ├── router/
│   │   └── index.ts
│   ├── App.vue
│   ├── main.ts   <-------子应用暴露生命周期
├── vite.config.ts/
├── package.json/
├── index.html

三、具体实现代码

 1.主应用
//router/index.ts

import { createRouter, createWebHistory } from "vue-router";
import Login from "../views/Login.vue";
import Home from "../views/Home.vue";
import SubAppView from "../views/SubAppView.vue";

const routes = [
  {
    path: "/",
    redirect: "/login",
  },
  {
    path: "/login",
    name: "Login",
    component: Login,
  },
  {
    path: "/home",
    name: "Home",
    component: Home,
    meta: { requiresAuth: true }, //该路由需要登录认证,
    children: [
      {
        path: "",
        redirect: "/home/app-store", // 👈 默认跳转到“应用商店”
      },
      {
        path: ":appName",
        name: "SubAppView",
        component: SubAppView,
        meta: { requiresAuth: true },
      },
    ],
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

router.beforeEach((to, _, next) => {
  const token = localStorage.getItem("token");
  if (to.meta.requiresAuth && !token) {
    next("/login");
  } else {
    next();
  }
});

export default router;
//App.vue

<template>
  <router-view />
</template>

<script setup lang="ts">
</script>
//main.ts

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')

//Home.vue

<template>
  <div class="layout">
    <!-- 左侧菜单 -->
    <aside class="menu">
      <h3>子应用菜单</h3>
      <ul>
        <li @click="openSubApp('child-app')">子应用</li>
        <!-- 可以添加更多子应用菜单 -->
      </ul>
      <button @click="logout">退出登录</button>
    </aside>

    <!-- 右侧展示子应用 -->
    <main  class="content">
      <router-view /> <!-- 显示子应用内容 -->
    </main >
  </div>
</template>

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

const router = useRouter()

function openSubApp(appName: string) {
    router.push(`/home/${appName}`).then(() => {
      window.location.reload();
    });
}
  

function logout() {
  localStorage.removeItem("token");
  router.push("/login").then(() => {
    window.location.reload();
  });
}

</script>

<style scoped>
.layout {
  display: flex;
  height: 100vh;
}
.menu {
  width: 200px;
  background: #f3f3f3;
  padding: 20px;
}
.content {
  flex: 1;
  padding: 20px;
  border-left: 1px solid #ccc;
}
</style>

//SubAppView.vue

<template>
  <div>
    <!-- 如果是子应用路由,就挂载 qiankun 微前端 -->
    <div v-if="isMicroAppRoute" id="sub-container" style="min-height: 500px" />
    <!-- 否则显示主应用自己的组件 -->
    <component v-else :is="appComponent"  />
  </div>
</template>

<script setup lang="ts">
import { onBeforeUnmount, nextTick,computed,watch,shallowRef  } from "vue";
import { useRoute } from "vue-router";
import { loadMicroApp } from "qiankun";
import type { MicroApp } from "qiankun";

let microApp: MicroApp | null = null;
const route = useRoute();

//子应用配置
const appsMap: Record<string, { name: string; entry: string }> = {
  "child-app": {
    name: "child-app",
    entry: "http://*.*.*.*:8200/", // 子应用地址
  },
};

// 主应用本地页面映射
const localAppComponents: Record<string, any> = {
  'app-store': () => import('../views/AppStore.vue'),//应用管理
}

// 当前路由是否为子应用
const isMicroAppRoute = computed(() => {
  const appName = route.params.appName as string;
  return Boolean(appsMap[appName]);
});


// 动态导入主应用页面组件
const appComponent = shallowRef<any>(null)

const loadLocalComponent = async (name: string) => {
  const loader = localAppComponents[name]
  if (loader) {
    const module = await loader();
    appComponent.value = module.default ?? module; // ✅ 注意 .default
  } else {
    appComponent.value = {
      template: `<div style="padding: 20px;">页面 "${name}" 不存在</div>`
    }
  }
}
// 启动微应用
const startMicroApp = async () => {
  const name = route.params.appName as string
  const config = appsMap[name]
  if (!config) return

  await nextTick()
  const container = document.querySelector('#sub-container')
  if (!container) return
  // 加载子应用
  microApp = loadMicroApp({
    name: config.name,
    entry: config.entry,
    container: '#sub-container',
    props: {},
  })
}

// 清理微应用
const cleanup = () => {
  microApp?.unmount()
  microApp = null
}
// 初始化
watch(
  () => route.params.appName,
  async (appName) => {
    cleanup()
    if (isMicroAppRoute.value) {
      appComponent.value = null; // 清空上一个本地组件
      await startMicroApp()
    } else {
      await loadLocalComponent(appName as string)
    }
  },
  { immediate: true }
)
// 离开当前子应用路由时卸载
onBeforeUnmount(() => {
  cleanup()
})
</script>

2.子应用
//main.ts

// ⚠️ 一定要放在最顶行,所有 import 之前
declare const window: any
if (window.__POWERED_BY_QIANKUN__) {
  // @ts-ignore
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
import { createApp, type App as VueApp } from 'vue'
import App from './App.vue'
import router from './router'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
import { createPinia } from 'pinia'

// 👇 添加此类型定义
export const qiankunWindow = window as typeof window & {
  __POWERED_BY_QIANKUN__: boolean
}

let app: VueApp<Element> | null = null

function render(props: any = {}) {
  const { container } = props
  const mountPoint = container
    ? container.querySelector('#childBox') || '#childBox'
    : '#childBox'

  app = createApp(App)
  app.use(router)
  app.use(createPinia())
  app.use(Antd)
  app.mount(mountPoint)
}

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render()
}

export async function bootstrap() {}
export async function mount(props: any) {
  render(props)
}
export async function unmount() {
  app?.unmount()
  app = null
}

//index.html

<!DOCTYPE html>
<html lang="">

<head>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Can file download</title>
</head>

<body>
  <div id="childBox"></div>👈
  <script type="module" src="/src/main.ts"></script>
</body>

</html>
//App.vue 

<template>
  <div id="childBox">👈
    <router-view />
  </div>
</template>

<script setup lang="ts">
</script>

//package.json

{
  "name": "child-app",👈
   ....省略
}

//router/index.ts

import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'

declare global {
  interface Window {
    __POWERED_BY_QIANKUN__?: boolean
  }
}


const routes = [
  {
    path: '/',
    redirect: '/home/child-app',
  },
  {
    path: '/home/child-app',
    name: 'Home',
    component: Home,
  },
]

const router = createRouter({
  history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/home/child-app' : '/'),
  routes,
})

export default router

//vite.config.ts

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import qiankun from 'vite-plugin-qiankun'

// 获取是否在 qiankun 环境中
const isQiankun = process.env.__POWERED_BY_QIANKUN__ === 'true'

export default defineConfig({
  base: isQiankun
    ? 'http://*.*.*.*:8200/home/child-app/'
    : 'http://*.*.*.*:8200/',
  server: {
    port: 8200,
    cors: true, // 开启 CORS
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
    
  },
  plugins: [
    vue(),
    vueJsx(),
    vueDevTools(),
    qiankun('can-file-download', {
      useDevMode: true, // ✅ dev 模式只在开发环境使用,部署必须为 false
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
 
  build: {
    outDir: 'dist',
    assetsDir: 'static',
    sourcemap: true, // 建议开启调试
  },
})

四、1Panel配置

1.主应用(main-app) 

 

//nginx.conf

worker_processes 1;

events {
    worker_connections 1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen 80;
        server_name localhost;

        # ✅ 只保留一个 location /
        location / {
            root /usr/share/nginx/html;
            index index.html index.htm;

            try_files $uri $uri/ /index.html;

            # 跨域头(可选)
            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';
            add_header Access-Control-Expose-Headers 'Content-Length,Content-Range';
        }
    }
}
2.子应用(child-app)

 

//nginx.conf

worker_processes 1;

events {
    worker_connections 1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen 80;
        server_name localhost;

        root /usr/share/nginx/html;
        index index.html;

        # ✅ 支持 Vue 前端路由 history 模式,刷新不报 404
        location / {
            try_files $uri $uri/ /index.html;

            # ✅ 跨域支持(供主应用加载)
            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';
            add_header Access-Control-Expose-Headers 'Content-Length,Content-Range';
        }

        # ✅ 静态资源缓存策略
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff|woff2|ttf|otf|eot|svg|json)$ {
            expires 6M;
            access_log off;
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小九今天不码代码

感谢支持,一起进步~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

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

抵扣说明:

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

余额充值