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...

相关推荐
chao_78938 分钟前
frame 与新窗口切换操作【selenium 】
前端·javascript·css·selenium·测试工具·自动化·html
天蓝色的鱼鱼1 小时前
从零实现浏览器摄像头控制与视频录制:基于原生 JavaScript 的完整指南
前端·javascript
三原1 小时前
7000块帮朋友做了2个小程序加一个后台管理系统,值不值?
前端·vue.js·微信小程序
popoxf1 小时前
在新版本的微信开发者工具中使用npm包
前端·npm·node.js
爱编程的喵2 小时前
React Router Dom 初步:从传统路由到现代前端导航
前端·react.js
每天吃饭的羊2 小时前
react中为啥使用剪头函数
前端·javascript·react.js
Nicholas683 小时前
Flutter帧定义与60-120FPS机制
前端
多啦C梦a3 小时前
【适合小白篇】什么是 SPA?前端路由到底在路由个啥?我来给你聊透!
前端·javascript·架构
薛定谔的算法3 小时前
《长安的荔枝·事件流版》——一颗荔枝引发的“冒泡惨案”
前端·javascript·编程语言
中微子3 小时前
CSS 的 position 你真的理解了吗?
前端·css