【无标题】

微前端项目搭建完整教程 - 基于 Qiankun

前言

微前端是一种将前端应用分解为更小、更简单的块的架构风格,每个块可以由不同的团队独立开发、测试和部署。本文将详细介绍如何从零开始搭建一个基于 Qiankun 的微前端项目。

目录

技术选型

本教程采用以下技术栈:

应用类型 技术栈 构建工具 端口
主应用 React 19 + TypeScript Vite 7000
子应用1 React + TypeScript Umi 4 7001
子应用2 Vue 2 Webpack (Vue CLI) 7002
子应用3 Vue 3 + TypeScript Vite 7003
微前端框架 Qiankun 2.x - -
包管理器 pnpm + workspace - -

项目架构

复制代码
monorepo/
├── packages/              # 所有应用目录
│   ├── main/             # 主应用 (React + Vite)
│   ├── react-app/        # 子应用1 (React + Umi4)
│   ├── vue2-app/         # 子应用2 (Vue2 + Webpack)
│   └── vue3-app/         # 子应用3 (Vue3 + Vite)
├── common/               # 公共工具库
├── scripts/              # 启动脚本
├── package.json          # 根目录配置
└── pnpm-workspace.yaml   # pnpm workspace 配置

环境准备

1. 安装 Node.js

确保已安装 Node.js (推荐 v18 或更高版本)

bash 复制代码
node -v
npm -v

2. 安装 pnpm

bash 复制代码
npm install -g pnpm
pnpm -v

项目初始化

1. 创建项目根目录

bash 复制代码
mkdir monorepo
cd monorepo

2. 初始化 package.json

bash 复制代码
pnpm init

修改 package.json

json 复制代码
{
  "name": "monorepo",
  "version": "1.0.0",
  "description": "微前端 monorepo 项目",
  "scripts": {
    "dev:main": "pnpm --filter main dev",
    "dev:react": "pnpm --filter react-app dev",
    "dev:vue2": "pnpm --filter vue2-app serve",
    "dev:vue3": "pnpm --filter vue3-app dev",
    "dev": "node ./scripts/start.js",
    "build:all": "pnpm --filter main build && pnpm --filter react-app build && pnpm --filter vue2-app build && pnpm --filter vue3-app build"
  }
}

3. 配置 pnpm workspace

创建 pnpm-workspace.yaml

yaml 复制代码
packages:
- 'packages/*'
- 'common'

主应用搭建

1. 创建主应用

bash 复制代码
cd packages
pnpm create vite main -- --template react-ts
cd main
pnpm install

2. 安装依赖

bash 复制代码
pnpm add qiankun react-router-dom axios
pnpm add -D @types/node sass

3. 配置 Vite

修改 vite.config.ts

typescript 复制代码
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: { '@': path.resolve(__dirname, './src') }
  },
  server: {
    port: 7000,
    cors: true
  },
  build: {
    outDir: 'dist',
    assetsDir: 'assets'
  }
})

4. 创建 public-path.ts

创建 src/public-path.ts

typescript 复制代码
if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

5. 修改主应用入口

修改 src/main.tsx

typescript 复制代码
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

6. 创建主应用组件

修改 src/App.tsx

typescript 复制代码
import { useEffect } from 'react'
import { BrowserRouter } from 'react-router-dom'
import { registerMicroApps, start } from 'qiankun'
import './App.scss'

function AppContent() {
  
  const handleNavigate = (path: string) => {
    window.location.assign(path)
  }

  useEffect(() => {
    registerMicroApps(
      [
        {
          name: 'react-app',
          entry: 'http://localhost:7001',
          container: '#micro-app-container',
          activeRule: '/sub1',
        },
        {
          name: 'vue2-app',
          entry: 'http://localhost:7002',
          container: '#micro-app-container',
          activeRule: '/sub2',   
        },
        {
          name: 'vue3-app',
          entry: 'http://localhost:7003',
          container: '#micro-app-container',
          activeRule: '/sub3',
        },
      ],
    )

    start({
      sandbox: { experimentalStyleIsolation: true },
      singular: true,
    })
  }, [])

  const navItems = [
    { path: '/sub1', label: '子应用1: React+Umi4' },
    { path: '/sub2', label: '子应用2: Vue2+Webpack' },
    { path: '/sub3', label: '子应用3: Vue3+Vite' },
  ]

  return (
    <div className="main-app">
      <h1>主应用</h1>
      <nav className="main-nav">
        {navItems.map((item) => (
          <button
            key={item.path}
            onClick={() => handleNavigate(item.path)}
          >
            {item.label}
          </button>
        ))}
      </nav>
      <div id="micro-app-container" className="micro-container"></div>
    </div>
  )
}

function App() {
  return (
    <BrowserRouter basename="/">
      <AppContent />
    </BrowserRouter>
  )
}

export default App

子应用搭建

子应用1:React + Umi4

1. 创建项目
bash 复制代码
cd packages
pnpm create umi react-app
cd react-app
pnpm install
2. 安装依赖
bash 复制代码
pnpm add @umijs/plugins
3. 配置 Umi

修改 .umirc.ts

typescript 复制代码
import { defineConfig } from "umi";

export default defineConfig({
  base: '/sub1',
  routes: [
    { path: "/", component: "index" },
    { path: "/docs", component: "docs" },
  ],
  npmClient: 'pnpm',
  plugins: ["@umijs/plugins/dist/qiankun"],
  qiankun: {
    slave: {}
  },
});
4. 修改 package.json
json 复制代码
{
  "scripts": {
    "dev": "set PORT=7001 && umi dev",
    "build": "umi build"
  }
}

子应用2:Vue2 + Webpack

1. 创建项目
bash 复制代码
cd packages
vue create vue2-app
cd vue2-app
pnpm install
2. 创建 public-path.js

创建 public-path.js

javascript 复制代码
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
3. 修改入口文件

修改 src/main.js

javascript 复制代码
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import "../public-path.js";

Vue.config.productionTip = false;

let instance = null;

function render(props = {}) {
  const { container } = props;
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount(container ? container.querySelector("#app") : "#app");
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap(props) {
  console.log("vue2应用启动", props);
}

export async function mount(props) {
  console.log("vue2子应用:接收主应用参数", props);
  render(props);
}

export async function unmount(props) {
  console.log("vue2应用卸载", props);
  if (instance) {
    instance.$destroy();
    instance.$el.innerHTML = "";
    instance = null;
  }
}
4. 配置 Vue CLI

修改 vue.config.js

javascript 复制代码
const { name } = require("./package.json");

module.exports = {
  devServer: {
    port: 7002,
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: "umd",
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};

子应用3:Vue3 + Vite

1. 创建项目
bash 复制代码
cd packages
pnpm create vite vue3-app -- --template vue-ts
cd vue3-app
pnpm install
2. 安装依赖
bash 复制代码
pnpm add vue-router pinia
pnpm add -D vite-plugin-qiankun
3. 创建 public-path.js

创建 public-path.js

javascript 复制代码
if (window.__POWERED_BY_QIANKUN__) {
}
4. 修改入口文件

修改 src/main.ts

typescript 复制代码
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
import '../public-path.js'
import { renderWithQiankun} from "vite-plugin-qiankun/dist/helper"

let app = null

function render(props = {}) {
  const { container } = props
  if (app) {
    app.unmount()
    app = null
  }
  app = createApp(App)
  app.use(router)
  app.mount(container ? container.querySelector('#app') : '#app')
}

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

renderWithQiankun({
  bootstrap() {
    console.log("vue3-vite子应用启动")
  },
  mount(props) {
    console.log("vue3-vite子应用挂载", props)
    render(props)
  },
  update(props) {
    console.log("vue3-vite子应用更新", props)
  },
  unmount() {
    console.log("vue3-vite子应用卸载")
    app.unmount()
    app = null
  }
})
5. 配置 Vite

修改 vite.config.ts

typescript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun'

export default defineConfig({
  plugins: [vue(), qiankun('vue3-app', { useDevMode: true })],
  base: '/sub3',
  server: {
    port: 7003,
    cors: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
})

公共工具库

1. 创建公共库

bash 复制代码
cd ..
mkdir common
cd common
pnpm init

2. 创建工具函数

创建 utils.js

javascript 复制代码
export function setStorage(key, value) {
  localStorage.setItem(key, JSON.stringify(value));
}

export function getStorage(key) {
  const value = localStorage.getItem(key);
  return value ? JSON.parse(value) : null;
}

export function removeStorage(key) {
  localStorage.removeItem(key);
}

创建 format.js

javascript 复制代码
export function formatDate(date) {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  const hours = String(date.getHours()).padStart(2, '0');
  const minutes = String(date.getMinutes()).padStart(2, '0');
  const seconds = String(date.getSeconds()).padStart(2, '0');
  
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

3. 创建入口文件

创建 index.js

javascript 复制代码
export * from "./utils.js";
export * from "./format.js";

4. 在各应用中使用

在各应用的 package.json 中添加依赖:

json 复制代码
{
  "dependencies": {
    "common": "workspace:*"
  }
}

在代码中导入使用:

typescript 复制代码
import { formatDate, setStorage } from 'common';

const currentDate = formatDate(new Date());
setStorage('data', { timestamp: Date.now() });

项目启动脚本

创建 scripts/start.js

javascript 复制代码
const { spawn } = require("child_process");
const { join } = require("path");

const rootDir = join(__dirname, "..");

const projects = [
  {
    name: "主应用 (main)",
    cwd: join(rootDir, "packages", "main"),
    command: "pnpm",
    args: ["dev"],
    port: 7000,
    color: "\x1b[36m",
  },
  {
    name: "React 子应用 (react-app)",
    cwd: join(rootDir, "packages", "react-app"),
    command: "pnpm",
    args: ["dev"],
    port: 7001,
    color: "\x1b[33m",
  },
  {
    name: "Vue2 子应用 (vue2-app)",
    cwd: join(rootDir, "packages", "vue2-app"),
    command: "pnpm",
    args: ["serve"],
    port: 7002,
    color: "\x1b[32m",
  },
  {
    name: "Vue3 子应用 (vue3-app)",
    cwd: join(rootDir, "packages", "vue3-app"),
    command: "pnpm",
    args: ["dev"],
    port: 7003,
    color: "\x1b[35m",
  },
];

const reset = "\x1b[0m";
const green = "\x1b[32m";

console.log(`${green}正在启动所有微前端应用...${reset}\n`);

const processes = projects.map((project) => {
  const { name, cwd, command, args, port, color } = project;

  console.log(`${color}[${name}]${reset} 正在启动 (端口: ${port})...`);

  const proc = spawn(command, args, {
    cwd,
    stdio: "inherit",
    shell: true,
  });

  proc.on("error", (error) => {
    console.error(`${color}[${name}]${reset} 启动失败:`, error.message);
  });

  proc.on("exit", (code) => {
    if (code !== 0 && code !== null) {
      console.error(`${color}[${name}]${reset} 进程退出,代码: ${code}`);
    }
  });

  return { name, port, color, process: proc };
});

setTimeout(() => {
  console.log(`\n${green}所有应用已启动!${reset}`);
  processes.forEach(({ name, port, color }) => {
    console.log(`${color}${name}: http://localhost:${port}${reset}`);
  });
  console.log("\n按 Ctrl+C 停止所有服务\n");
}, 2000);

process.on("SIGINT", () => {
  console.log(`\n${green}正在停止所有服务...${reset}`);
  processes.forEach(({ name, color, process: proc }) => {
    console.log(`${color}[${name}]${reset} 正在停止...`);
    proc.kill("SIGINT");
  });
  setTimeout(() => {
    process.exit(0);
  }, 1000);
});

process.on("SIGTERM", () => {
  processes.forEach(({ process: proc }) => {
    proc.kill("SIGTERM");
  });
  process.exit(0);
});

运行项目

1. 安装所有依赖

在项目根目录执行:

bash 复制代码
pnpm install

2. 启动所有应用

bash 复制代码
pnpm dev

3. 访问应用

打开浏览器访问:http://localhost:7000

点击导航按钮切换不同的子应用:

4. 单独启动某个应用

bash 复制代码
pnpm dev:main    # 只启动主应用
pnpm dev:react   # 只启动 React 子应用
pnpm dev:vue2    # 只启动 Vue2 子应用
pnpm dev:vue3    # 只启动 Vue3 子应用

5. 构建所有应用

bash 复制代码
pnpm build:all

常见问题

1. 跨域问题

确保所有子应用的开发服务器都配置了 CORS:

javascript 复制代码
headers: {
  "Access-Control-Allow-Origin": "*",
}

2. 静态资源路径问题

使用 public-path.js 解决静态资源加载问题:

javascript 复制代码
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

3. 样式隔离

在主应用中开启样式隔离:

typescript 复制代码
start({
  sandbox: { experimentalStyleIsolation: true }
})

4. 应用间通信

Qiankun 提供了多种通信方式:

props 传递:

typescript 复制代码
registerMicroApps([
  {
    name: 'sub-app',
    entry: '//localhost:port',
    container: '#container',
    activeRule: '/sub',
    props: { data: 'from main' }
  }
])

全局状态管理:

typescript 复制代码
import { initGlobalState } from 'qiankun'

const actions = initGlobalState(state)

actions.onGlobalStateChange((state, prev) => {
  console.log(state, prev)
})

actions.setGlobalState(state)

5. 路由模式

确保主应用和子应用的路由模式一致(都使用 history 模式)。

总结

本教程详细介绍了如何从零开始搭建一个基于 Qiankun 的微前端项目,包括:

  1. 使用 pnpm workspace 管理 monorepo 项目
  2. 搭建 React + Vite 主应用
  3. 搭建三个不同技术栈的子应用(React + Umi4、Vue2 + Webpack、Vue3 + Vite)
  4. 配置 Qiankun 微前端框架
  5. 创建公共工具库实现代码复用
  6. 编写启动脚本简化开发流程

微前端架构的优势:

  • 技术栈无关:可以使用不同的框架和技术栈
  • 独立开发部署:各团队可以独立开发和部署
  • 代码复用:通过公共库实现代码共享
  • 渐进式升级:可以逐步迁移和升级应用

希望本教程对你有所帮助!

相关推荐
网络点点滴2 小时前
组件通信-provide和inject
javascript·vue.js·ecmascript
rudy_zhou2 小时前
DOM手搓一个渲染树形卡片的画布(二)
typescript·dom·stylus·pug
江沉晚呤时2 小时前
RabbitMQ 延迟队列实战指南:C# 版订单超时与定时任务解决方案
开发语言·后端·ruby
早點睡3902 小时前
ReactNative项目OpenHarmony三方库集成实战:@react-native-oh-tpl/masked-view
javascript·react native·react.js
We་ct2 小时前
LeetCode 427. 建立四叉树:递归思想的经典应用
前端·算法·leetcode·typescript·dfs·深度优先遍历·分治
摸鱼的春哥2 小时前
Agent教程20:更适合编程工具的记忆方案——情景摘要
前端·javascript·后端
架构师李肯3 小时前
TypeScript与React全栈实战:从架构搭建到项目部署,避开常见陷阱
react.js·架构·typescript
Hamm11 小时前
不想花一分钱玩 OpenClaw?来,一起折腾这个!
javascript·人工智能·agent
Setsuna_F_Seiei11 小时前
AI 对话应用之 JS 的流式接口数据处理
前端·javascript·ai编程