qiankun使用教程(从项目搭建到容器化部署)

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

五、容器化部署

采用的是独立部署方案

graph TD A[Nginx 网关] --> B[主应用容器] A --> C[子应用1容器] A --> D[子应用2容器] B -->|加载子应用| C B -->|加载子应用| D

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私钥复制

    bash 复制代码
     cat **~/.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...

相关推荐
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端