一、安装命令
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 | 跨平台设置环境变量 |
pinia | Vue3 状态管理(替代 Vuex) |
vue-router@4 | Vue3 路由 |
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;
}
}
}