qiankun使用教程(从项目搭建到容器化部署)
一、框架搭建
1、使用lerna管理项目
jsx
npm install lerna -g
npx lerna init
创建packages文件夹,更新lerna.json和package.json

2、创建子应用1(Vue)
tsx
cd ./packages
# 创建主应用 app
npm create vite@latest
# Project name
sub-app1
# Select a framework
Vue
对vite.config.js与main.js进行配置
jsx
// vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun';
const useDevMode = process.env.NODE_ENV === 'development';
const host = '127.0.0.1';
const port = 8001;
const subAppName = 'subApp1'; // 统一路径格式与Nginx保持一致
const base = useDevMode
? `http://${host}:${port}/`
: `/${subAppName}`; // 这里 subAppName 对应 createBrowserRouter 的 basename
export default defineConfig({
base: useDevMode ? '' : `/${subAppName}`,
plugins: [
vue(),
qiankun(subAppName, {useDevMode, base, entry: 'src/main.js'}),
],
resolve: {
},
server: {
port: 8001, // 本地环境独立启动
host: 'localhost',
cors: true,// 作为子应用,需要配置跨域
open: true,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
}
})
jsx
// main.js
import './public-path'
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import {
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
let appInstance = null;
/** 渲染函数 */
const render = (props = {}) => {
const { container, routeType, baseUrl, initialQuery, userInfo } = props;
appInstance = createApp(App);
console.log('routeType', routeType);
console.log('baseUrl', baseUrl);
console.log('initialQuery', initialQuery);
console.log('userInfo', userInfo);
// 挂载到 qiankun 容器或默认 #app
appInstance.mount(container ? container.querySelector('#subApp1') : '#subApp1');
};
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {},
async mount(props) {
render(props);
},
update: () => {},
async unmount(props) {
},
});
};
// 检查是否在 Qiankun 环境中
console.log('qiankunWindow', qiankunWindow.__POWERED_BY_QIANKUN__);
if (qiankunWindow.__POWERED_BY_QIANKUN__) {
qiankun(); // 以子应用的方式启动
} else {
render(); // 独立运行
}
3、创建子应用2(React)
bash
# 从根目录进入 packages 目录
cd ./packages
# 创建主应用 app
npm create vite@latest
# Project name
main-app
# Select a framework
React
# Select a variant
TypeScript + SWC
jsx
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import qiankun from 'vite-plugin-qiankun';
const useDevMode = process.env.NODE_ENV === 'development';
const host = 'localhost';
const port = 8002;
const subAppName = 'subApp2'; // 修改为与主应用配置一致
// https://vitejs.dev/config/
export default defineConfig({
base: useDevMode ? `` : `/${subAppName}/`,
// 确保生产环境资源路径正确映射到子应用路由
plugins: [
react(),
qiankun(subAppName, {
useDevMode
})
],
resolve: {
alias: {}
},
server: {
port,
host: 'localhost',
cors: true,
open: false,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
rollupOptions: {},
}
})
jsx
//main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import {
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import type { QiankunProps } from 'vite-plugin-qiankun/dist/helper';
let root: any = null;
const render = (container?: HTMLElement) => {
const app = container || document.getElementById('root2') as HTMLDivElement;
console.log('subApp2 render target:', app);
if (root) {
root.unmount();
}
root = createRoot(app);
root.render(
<StrictMode>
<App/>
</StrictMode>,
)
}
/** Qiankun 生命周期钩子 */
const qiankun = () => {
renderWithQiankun({
bootstrap() {
console.log('subApp2 bootstrap');
},
async mount(props: QiankunProps) {
console.log('subApp2 mount', props);
render(props.container);
},
update: () => {
console.log('subApp2 update');
},
unmount: () => {
console.log('subApp2 unmount');
if (root) {
root.unmount();
root = null;
}
}
});
};
// 判断是否在qiankun环境中
if (qiankunWindow.__POWERED_BY_QIANKUN__) {
qiankun(); // 以子应用的方式启动
} else {
render(); // 独立启动
}
4、创建主应用
bash
# 从根目录进入 packages 目录
cd ./packages
# 创建主应用 app
npm create vite@latest
# Project name
main-app
# Select a framework
React
# Select a variant
TypeScript + SWC
在 packages/app/vite.config.ts 中配置 启动端口 和 应用名称
tsx
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {}
},
server: {
port: 8000,
host: 'localhost',
cors: true,
open: false,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api/, '')
}
}
},
build: {
rollupOptions: {}
}
})
jsx
// main.tsx
/* eslint-disable @typescript-eslint/no-explicit-any */
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { createStore } from 'redux';
import { BrowserRouter } from 'react-router-dom';
import './index.css'
import App from './App.tsx'
import { registerMicroApps, start } from 'qiankun';
import { microApps } from './microApps.ts'
import { SecureEventBus, EVENT_MESSAGE_NAME } from '@myqiankun/utils';
registerMicroApps(microApps, {
beforeLoad: async (app) => {
console.log(`%c before load: ${app.name}`, 'color: green');
},
beforeMount: async (app) => {
console.log(`%c before mount: ${app.name}`, 'color: green');
},
afterMount: async (app) => {
console.log(`%c after mount: ${app.name}`, 'color: yellow');
},
beforeUnmount: async (app) => {
console.log(`%c before unmount: ${app.name}`, 'color: red');
},
afterUnmount: async (app) => {
console.log(`%c after unmount: ${app.name}`, 'color: red');
},
}
);
// @ts-expect-error 注册事件总线
// window.mainEventBus = new MainEventBus();
window.mainEventBus = new SecureEventBus({
subApp1: [
EVENT_MESSAGE_NAME.SUB1_SEND_MESSAGE,
],
subApp2: [
EVENT_MESSAGE_NAME.SUB2_SEND_MESSAGE,
]
});
// 主应用创建共享库
// @ts-expect-error 共享数据
window.sharedLib = {
utils: {
formatDate: (date: string) => new Date(date).toLocaleDateString(),
currencyFormat: (num: number) => `¥${num.toFixed(2)}`},
services: {
api: {
getUser: () => fetch('/api/user'),
getProducts: () => fetch('/api/products')
}
},
constants: {
MAX_ITEMS: 100,
THEME_COLORS: ['#1890ff', '#52c41a', '#faad14']
}
};
const globalReducer = (state: any, action: any) => {
switch(action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'UPDATE_CONFIG':
return { ...state, config: { ...state.config, ...action.payload } };
default:
return state;
}
};
const globalStore = createStore(globalReducer)
// @ts-expect-error 共享数据
window.globalStore = globalStore;
start({singular: true });
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)
// mrcroApps.ts
const isDev = import.meta.env.MODE === 'development';
export const microApps = [
{
name: 'subApp1', // app name registered
entry: isDev ? `//${location.hostname}:8001/` : '/subApp1/',
container: '#subAppContainer',
activeRule: '/subApp1',
},
{
name: 'subApp2',
entry: isDev ? `//${location.hostname}:8002/${import.meta.env.VITE_SUB_APP_NAME}/` : '/subApp2/',
container: '#subAppContainer',
activeRule: '/subApp2',
},
]
二、应用通信
1、自定义事件通信(发布-订阅模式)
tsx
// 新建订阅发布类
interface EventHandler {
(eventName: string, ...args: unknown[]): void;
}
export class MainEventBus {
private events: Record<string, { callback: EventHandler; count: number }[]> = {};
$on(eventName: string, callback: EventHandler) {
// 订阅事件
if (!eventName || typeof callback !== "function") {
return;
}
if (!this.events[eventName]) {
// 尚未发布事件,压入事件队列
this.events[eventName] = [];
this.events[eventName].push({ callback, count: 0 });
} else {
const hasExist = this.events[eventName].some(
(item) => item.callback === callback
);
if (hasExist) {
return;
}
this.events[eventName].push({ callback, count: 0 });
}
}
$emit(eventName: string, ...args: unknown[]) {
// 发布事件
if (!this.events[eventName]) {
return;
}
this.events[eventName].forEach((item) => {
item.callback(eventName, ...args);
item.count++;
});
}
}
// 导出单例实例
export const emitter = new MainEventBus();
tsx
// 主应用发布
// main.ts
window.mainEventBus = new MainEventBus();
//发布处
eventBus.$emit(EVENT_MESSAGE_NAME.MAIN_SEND_MESSAGE, '主应用向子应用发送消息1');
//子应用监听
const eventBus = window.mainEventBus;
onMounted(() => {
eventBus.$on(EVENT_MESSAGE_NAME.MAIN_SEND_MESSAGE, (msg) => {
messages.value.push(msg);
});
});
2、 URL 参数通信
tsx
// 主应用
const getQueryParams = () => {
const searchParams = new URLSearchParams(window.location.search);
return Object.fromEntries(searchParams.entries());
};
microInstanceRef.current = loadMicroApp({
...microApp,
props: {
routeType: 'hash',
basePath: '/sub-app',
// 同时传递URL参数和props参数
initialQuery: getQueryParams(),
userInfo: currentUser
},
});
// 子应用入口文件
export async function mount(props) {
console.log('收到主应用参数:', props.initialQuery);
// 或通过全局变量
console.log('URL参数:', new URLSearchParams(window.location.search));
}
3、通信安全
jsx
// 主应用:安全通信包装器
export class SecureEventBus extends MainEventBus {
private ALLOWED_EVENTS: {[key: string]: Array<string>} = {}; // 每个app允许的事件
constructor(allowedEvents: {[key: string]: Array<string>}) {
super();
this.ALLOWED_EVENTS = allowedEvents;
}
$seOn(appName: string,eventName: string, callback: EventHandler): void {
if (!this.ALLOWED_EVENTS[appName].includes(eventName)) {
console.warn(`[Security] 应用 ${appName} 尝试发送未授权事件: ${eventName}`);
return;
}
super.$on(eventName, callback);
}
$seEmit(appName: string, eventName: string, ...args: unknown[]): void {
if (!this.ALLOWED_EVENTS[appName].includes(eventName)) {
console.warn(`[Security] 应用 ${appName} 尝试发送未授权事件: ${eventName}`);
return;
}
super.$emit(eventName, ...args);
}
}
window.mainEventBus = new SecureEventBus({
subApp1: [
EVENT_MESSAGE_NAME.SUB1_SEND_MESSAGE,
],
subApp2: [
EVENT_MESSAGE_NAME.SUB2_SEND_MESSAGE,
]
});
三、数据共享
1. 全局共享库
jsx
// 主应用创建共享库
window.sharedLib = {
utils: {
formatDate: (date) => new Date(date).toLocaleDateString(),
currencyFormat: (num) => `¥${num.toFixed(2)}`},
services: {
api: {
getUser: () => fetch('/api/user'),
getProducts: () => fetch('/api/products')
}
},
constants: {
MAX_ITEMS: 100,
THEME_COLORS: ['#1890ff', '#52c41a', '#faad14']
}
};
// 子应用使用
const formattedDate = window.sharedLib.utils.formatDate(new Date());
const products = await window.sharedLib.services.api.getProducts();
2. 基于 Redux 的共享状态(主应用与子应用都使用React的前提)
jsx
// 主应用创建全局 Redux store
import { createStore } from 'redux';
const globalReducer = (state = {}, action) => {
switch(action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'UPDATE_CONFIG':
return { ...state, config: { ...state.config, ...action.payload } };
default:
return state;
}
};
const globalStore = createStore(globalReducer);
window.globalStore = globalStore;
// 子应用连接全局 store
export async function mount(props) {
// 获取当前状态
const currentState = window.globalStore.getState();
// 订阅状态变化
const unsubscribe = window.globalStore.subscribe(() => {
const newState = window.globalStore.getState();
handleStateChange(newState);
});
// 派发 action
window.globalStore.dispatch({
type: 'SET_USER',
payload: { name: '王五', role: 'user' }
});
// 卸载时取消订阅
return () => unsubscribe();
}
3. 数据共享边界控制
jsx
// 数据沙箱代理
function createDataProxy(allowedKeys) {
return new Proxy(window.sharedData, {
get(target, key) {
if (allowedKeys.includes(key)) {
return target[key];
}
console.warn(`[Security] 尝试访问未授权数据: ${key}`);
return undefined;
},
set() {
console.error('[Security] 禁止直接修改共享数据');
return false;
}
});
}
// 子应用初始化时
export async function mount(props) {
// 只允许访问 user 和 config 数据
const safeData = createDataProxy(['user', 'config']);
// 使用安全数据
console.log('安全用户数据:', safeData.user);
}
四、数据持久化
1、LocalStorage 通信(简单场景)
jsx
// 主应用:创建持久化服务
class PersistenceService {
constructor(namespace = 'global') {
this.namespace = `qiankun:${namespace}:`;
}
setItem(key, value) {
localStorage.setItem(`${this.namespace}${key}`, JSON.stringify({
timestamp: Date.now(),
data: value
}));
}
getItem(key) {
const data = localStorage.getItem(`${this.namespace}${key}`);
return data ? JSON.parse(data).data : null;
}
// 添加数据过期机制
getItemWithExpiry(key, maxAge = 86400000 /* 1天 */) {
const item = localStorage.getItem(`${this.namespace}${key}`);
if (!item) return null;
const { timestamp, data } = JSON.parse(item);
if (Date.now() - timestamp > maxAge) {
this.removeItem(key);
return null;
}
return data;
}
removeItem(key) {
localStorage.removeItem(`${this.namespace}${key}`);
}
}
// 初始化全局持久化服务
window.persistence = new PersistenceService('main');
// 子应用使用
export async function mount(props) {
// 保存用户设置
window.persistence.setItem('user-settings', {
theme: 'dark',
fontSize: 16
});
// 获取数据
const settings = window.persistence.getItem('user-settings') || {};
applySettings(settings);
}
2、SessionStorage
jsx
// 安全封装 SessionStorage 操作
class SessionStorageService {
constructor(appName) {
this.prefix = `qiankun:${appName}:`;
}
// 设置带过期时间的值
set(key, value, ttl = 3600) {
const item = {
value,
expires: Date.now() + ttl * 1000
};
sessionStorage.setItem(`${this.prefix}${key}`, JSON.stringify(item));
}
// 获取值(自动处理过期)
get(key) {
const itemStr = sessionStorage.getItem(`${this.prefix}${key}`);
if (!itemStr) return null;
try {
const item = JSON.parse(itemStr);
if (Date.now() > item.expires) {
this.remove(key);
return null;
}
return item.value;
} catch (e) {
this.remove(key);
return null;
}
}
remove(key) {
sessionStorage.removeItem(`${this.prefix}${key}`);
}
}
// 主应用中初始化
window.sessionStorageService = new SessionStorageService('main-app');
// 子应用中使用
export function mount(props) {
// 存储表单草稿(1小时有效)
const formDraft = {
step: 3,
values: { name: '张三', phone: '13800138000' }
};
props.sessionStorage.set('order-form', formDraft, 3600);
// 读取数据
const draft = props.sessionStorage.get('order-form');
if (draft) {
restoreForm(draft);
}
// 离开页面时清理
window.addEventListener('beforeunload', () => {
props.sessionStorage.remove('order-form');
});
}
3、IndexedDB
jsx
// IndexedDB 封装类
class IDBService {
constructor(dbName, version = 1) {
this.dbName = `qiankun_${dbName}`;
this.version = version;
this.db = null;
}
async open(stores) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
stores.forEach(store => {
if (!db.objectStoreNames.contains(store.name)) {
const objectStore = db.createObjectStore(store.name, {
keyPath: store.keyPath || 'id',
autoIncrement: true
});
// 创建索引
(store.indexes || []).forEach(index => {
objectStore.createIndex(index.name, index.keyPath, index.options);
});
}
});
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = (event) => {
reject(`IndexedDB error: ${event.target.error}`);
};
});
}
async transaction(storeName, mode = 'readonly') {
if (!this.db) await this.open();
return this.db.transaction(storeName, mode).objectStore(storeName);
}
async add(storeName, data) {
const store = await this.transaction(storeName, 'readwrite');
return new Promise((resolve, reject) => {
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get(storeName, key) {
const store = await this.transaction(storeName);
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll(storeName) {
const store = await this.transaction(storeName);
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// 主应用中初始化
window.idbService = new IDBService('globalDB', 2);
// 子应用中使用
export async function mount(props) {
// 初始化数据库结构
await props.idbService.open([
{
name: 'products',
keyPath: 'sku',
indexes: [
{ name: 'category', keyPath: 'category', options: { unique: false } }
]
},
{ name: 'cart' }
]);
// 添加产品数据
await props.idbService.add('products', {
sku: 'P1001',
name: '无线耳机',
price: 299,
category: 'electronics'
});
// 查询所有电子产品
const electronics = (await props.idbService.getAll('products'))
.filter(p => p.category === 'electronics');
// 离线购物车功能
const addToCart = async (product) => {
await props.idbService.add('cart', {
...product,
quantity: 1,
addedAt: new Date()
});
updateCartCount();
};
// 同步购物车数据到服务器
const syncCart = async () => {
const cartItems = await props.idbService.getAll('cart');
if (cartItems.length > 0) {
await fetch('/api/cart/sync', {
method: 'POST',
body: JSON.stringify(cartItems)
});
// 清空本地购物车
const store = await props.idbService.transaction('cart', 'readwrite');
store.clear();
}
};
// 网络恢复时同步
window.addEventListener('online', syncCart);
}
4、Cookies
服务器端设置(Node.js/Express)
jsx
// 登录接口
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
// 验证用户(伪代码)
const user = authenticate(username, password);
if (user) {
// 生成JWT令牌
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1d' }
);
// 设置HttpOnly Cookie
res.cookie('auth_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000, // 1天
domain: '.example.com' // 主域共享
});
res.json({ success: true });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
// 受保护API
app.get('/api/user', (req, res) => {
const token = req.cookies.auth_token;
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
res.json({ user: decoded });
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
});
前端使用(主应用)
jsx
// 封装认证服务
class AuthService {
async login(credentials) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
credentials: 'include' // 包含cookies
});
if (!response.ok) {
throw new Error('Login failed');
}
return response.json();
}
async getUser() {
const response = await fetch('/api/user', {
credentials: 'include' // 包含cookies
});
if (response.status === 401) {
return null;
}
return response.json();
}
logout() {
// 清除cookie需要服务器端配合
document.cookie = 'auth_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.example.com;';
}
}
// 初始化认证服务
window.authService = new AuthService();
// 在子应用中注入
registerMicroApps([
{
name: 'dashboard',
entry: '//localhost:7101',
props: {
authService: window.authService
}
}
]);
子应用中使用
jsx
export async function mount(props) {
// 检查登录状态
const user = await props.authService.getUser();
if (!user) {
// 重定向到登录
window.location.href = '/login';
return;
}
// 根据用户角色显示内容
if (user.role === 'admin') {
showAdminPanel();
} else {
showUserDashboard();
}
// 登出按钮
document.getElementById('logout-btn').addEventListener('click', () => {
props.authService.logout();
window.location.reload();
});
}
3. 跨域安全策略
-
Cookie 配置:
arduino// 安全配置 { httpOnly: true, // 阻止JavaScript访问 secure: true, // 仅HTTPS传输 sameSite: 'Lax', // 防止CSRF攻击 domain: '.example.com', // 主域名共享 path: '/', // 全路径可用 maxAge: 86400 // 过期时间(秒) }
-
跨域请求处理:
jsx// 前端请求 fetch('https://api.example.com/data', { credentials: 'include' // 必须包含才能发送Cookie }); // 服务器响应 res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com'); res.setHeader('Access-Control-Allow-Credentials', 'true');
数据持久化建议:
- 用户偏好设置 → LocalStorage
- 敏感临时数据 → SessionStorage
- 大型应用数据 → IndexedDB
- 身份认证信息 → HttpOnly Cookies
五、容器化部署
采用的是独立部署方案
1、添加如下文件
- docker-compose.yml
jsx
version: '3.8'
services:
qiankun-app:
container_name: qiankun
build:
context: .
dockerfile: Dockerfile
ports:
- "${PORT:-8888}:${PORT:-8888}"
volumes:
- ./ssl:/etc/nginx/ssl
environment:
- PORT=${PORT:-8888}
- NGINX_SERVER_NAME=${NGINX_SERVER_NAME:-localhost}
restart: unless-stopped
- Dockerfile
jsx
# 构建阶段
FROM node:23.6.0 as builder
# 安装 pnpm
RUN npm install -g pnpm@8.15.5
# 设置工作目录
WORKDIR /app
# 复制 package.json 和 lock 文件
COPY package*.json ./
COPY pnpm-workspace.yaml ./
COPY lerna.json ./
COPY packages/main-app/package*.json ./packages/main-app/
COPY packages/sub-app1/package*.json ./packages/sub-app1/
COPY packages/sub-app2/package*.json ./packages/sub-app2/
COPY shared/utils/package*.json ./shared/utils/
# 安装依赖
RUN pnpm install
RUN pnpm add -w typescript
# 复制源代码
COPY . .
# 构建应用
RUN pnpm build
# 运行阶段
FROM nginx:alpine
# 安装 envsubst 和 curl (用于健康检查)
RUN apk add --no-cache gettext curl
# 复制 nginx 配置模板
COPY nginx.conf /etc/nginx/templates/default.conf.template
# 复制构建产物
COPY --from=builder /app/packages/main-app/dist /usr/share/nginx/html
COPY --from=builder /app/packages/sub-app1/dist /usr/share/nginx/html/sub-app1
COPY --from=builder /app/packages/sub-app2/dist /usr/share/nginx/html/sub-app2
# 设置默认环境变量
ENV PORT=80
ENV NGINX_SERVER_NAME=localhost
# 启动脚本
COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:${PORT:-80}/ || exit 1
EXPOSE 80
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
- nginx.conf
jsx
server {
listen 80;
server_name ${NGINX_SERVER_NAME};
root /usr/share/nginx/html;
index index.html;
# 主应用
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 '*';
if ($request_method = 'OPTIONS') {
return 204;
}
}
# 子应用1
location /sub-app1/ {
alias /usr/share/nginx/html/sub-app1/;
try_files $uri $uri/ /sub-app1/index.html;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers '*';
if ($request_method = 'OPTIONS') {
return 204;
}
}
# 子应用2
location /sub-app2/ {
alias /usr/share/nginx/html/sub-app2/;
try_files $uri $uri/ /sub-app2/index.html;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers '*';
if ($request_method = 'OPTIONS') {
return 204;
}
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, no-transform";
}
}
- .github/workflows/docker-deploy.yml
jsx
name: Docker Build & Deploy
on:
push:
branches:
- dev
pull_request:
branches:
- dev
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '23.6.0'
- name: Set up pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install
- name: Create .env file
run: |
echo "NGINX_SERVER_NAME=${{ secrets.NGINX_SERVER_NAME }}" >> .env
echo "PORT=${{ secrets.PORT }}" >> .env
echo "NODE_ENV=production" >> .env
- name: Build project
run: pnpm -r run build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKER_IMAGE_NAME }}
- name: Deploy to Server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
# 设置环境变量
export DOCKER_IMAGE_NAME="${{ secrets.DOCKER_IMAGE_NAME }}"
export NGINX_SERVER_NAME="${{ secrets.NGINX_SERVER_NAME }}"
export EXTERNAL_PORT="8888"
export INTERNAL_PORT="80"
# 停止并删除旧容器
echo "停止旧容器..."
docker stop qiankun-app 2>/dev/null || true
docker rm qiankun-app 2>/dev/null || true
# 检查端口占用
echo "检查端口占用情况..."
if lsof -Pi :8888 -sTCP:LISTEN -t >/dev/null 2>&1; then
echo "端口8888被占用,正在释放..."
lsof -ti:8888 | xargs kill -9 2>/dev/null || true
sleep 2
fi
# 拉取最新镜像
echo "拉取镜像..."
docker pull $DOCKER_IMAGE_NAME
# 启动新容器
echo "启动新容器..."
docker run -d \
--name qiankun-app \
-p 8888:80 \
-e PORT=80 \
-e NGINX_SERVER_NAME=$NGINX_SERVER_NAME \
--restart unless-stopped \
$DOCKER_IMAGE_NAME
# 等待容器启动
echo "等待容器启动..."
sleep 10
# 检查容器状态
if docker ps | grep -q qiankun-app; then
echo "✅ 容器启动成功!"
echo "容器信息:"
docker ps | grep qiankun-app
echo "访问地址: http://${{ secrets.SSH_HOST }}:8888"
else
echo "❌ 容器启动失败!"
echo "容器日志:"
docker logs qiankun-app --tail 20
exit 1
fi
# 健康检查
echo "执行健康检查..."
for i in {1..10}; do
if curl -f http://localhost:8888/ >/dev/null 2>&1; then
echo "✅ 健康检查通过!"
break
else
echo "等待服务启动... ($i/10)"
sleep 3
fi
done
2、github仓库中添加密钥
路径: 仓库 → settings → security → Actions → 添加仓库密钥
在这里添加你的运行时配置,主要是要与.env文件中的配置一样就好,还要添加docker hub中的帐号密码,这样构建完成后镜像就会推送到docker hub中

3、推送镜像到服务器
准备服务器环境,需要服务器防火墙放开22端口
-
将当前服务器的ssh私钥复制
bashcat **~/.ssh/authorized_keys**
-
将公钥
github-actions.pub
添加到服务器的~/.ssh/authorized_keys
-
将私钥
github-actions
保存为 GitHub 仓库的 Secret(命名为SSH_PRIVATE_KEY
)
配置 GitHub Secrets(与4.2一样)
Secret 名称 | 值 |
---|---|
SSH_PRIVATE_KEY |
服务器生成的私钥内容 |
SSH_HOST |
服务器 IP 或域名 |
SSH_USER |
SSH 登录用户名(如 root ) |
DOCKER_IMAGE_NAME |
镜像名(如 my-app:latest ) |
4、服务器给容器配置域名(此处为腾讯云服务器下搭建了宝塔面板)
登录到宝塔面板,切换到docker/容器,给容器添加反向代理,配置完成就可以了,这个端口记得要跟nginx.conf中配置监听到端口一致

六、常见问题
1、react子应用切换时,报错application 'subApp1' died in status LOADING_SOURCE_CODE: [qiankun]: Target container with #subAppContainer1 not existed while subApp1_1 loading!
问题分析:首次渲染时容器存在,二次切换时容器消失或未及时渲染,这是典型的"容器卸载时机"问题。
问题解决方案:在同一个容器内的,使用固定容器节点
2、本地docker构建时,报错 load metadata for docker.io/library/ngi...
解决办法:
先手动拉取nginx和node的镜像(具体版本以docker.depoly.yml为准)
jsx
docker pull nginx:alpine
docker pull node:23.6.0
docker login -u yourname
再执行构建就可以了
3、共享库无法加载
运行时报错
jsx
Uncaught TypeError: Failed to resolve module specifier "@myqiankun/utils". Relative references must start with either "/", "./", or "../".
解决方案:
1、确保共享库的构建在其他应用之前,修改dockerfile与.github/workflows/docker-depoly.yml
jsx
// Dockerfile
# 先构建共享库
RUN cd shared/utils && pnpm build
# 构建所有应用
RUN pnpm build
// .github/workflows/docker-depoly.yml
- name: Build shared utils
run: cd shared/utils && pnpm build
- name: Build app project
run: pnpm -r run build
参考demo
演示:点击这里查看演示
github仓库:github.com/bigkrys/qia...