欢迎来到这份微前端教程!随着前端应用变得越来越复杂,传统的单体应用架构逐渐暴露出其局限性。微前端作为一种借鉴了微服务思想的架构模式,正成为解决这些挑战的强大工具。本教程将带您深入理解微前端的核心概念、技术原理,并通过实践案例帮助您掌握构建和管理微前端应用的能力。
在过去的十年中,前端开发经历了飞速的发展。从最初的静态网页到JQuery时代,再到MV*框架(如Angular, React, Vue)的兴起,前端应用变得越来越庞大和复杂。随之而来的,是“巨石应用”(Monolithic Application)架构的痛点:
后端微服务架构的成功启发了前端开发者:如果后端可以将一个大型服务拆分成多个独立、可部署的小服务,为什么前端不能也这样做呢?这就是微前端思想的起源。
微前端(Micro-Frontends)是一种架构风格,它将一个大型前端应用拆分成多个独立可部署的小型应用,这些小型应用可以由不同的团队、使用不同的技术栈独立开发、部署和维护,最终聚合在一个主应用(基座)中,呈现给用户一个统一的、无缝的用户体验。
当然,微前端并非银弹,它也带来了一些新的挑战:
为了解决上述挑战,社区涌现了多种微前端的实现架构模式:
这是最简单也最常见的模式。通常基于后端路由(如Nginx、CDN)或者前端路由重定向,将不同路径的请求转发到不同的独立部署的应用。
优点: 部署完全独立,技术栈完全自由,隔离性好。
缺点: 用户体验可能存在切换页面时的刷新,公共头部/导航等难以共享,不适合需要紧密协作的子应用。
使用iframe标签将子应用嵌入到主应用中。每个iframe提供了一个独立的运行环境,包括DOM、JS和CSS的完全隔离。
<!-- 主应用 index.html -->
<div id="main-container"></div>
<iframe src="/path-to-subapp1/" id="subapp1-iframe"></iframe>
<iframe src="/path-to-subapp2/" id="subapp2-iframe" style="display: none;"></iframe>
优点: 隔离性最佳,最简单易行。
缺点: 性能开销大(每个iframe都有独立的JS/CSS/DOM上下文),路由同步困难,无法直接共享上下文,通信复杂。
利用Web Components(Custom Elements, Shadow DOM, HTML Templates, ES Modules)来构建可复用的独立组件,并将这些组件作为微前端的单元集成。每个子应用都可以是一个或多个Web Components。
<!-- 主应用 index.html -->
<!-- 定义 custom element <my-app-header> 和 <my-app-dashboard> -->
<my-app-header></my-app-header>
<my-app-dashboard></my-app-dashboard>
优点: 原生支持,组件级别集成,隔离性较好(Shadow DOM),可复用性高。
缺点: 浏览器兼容性(需要Polyfill),学习曲线,组件间通信和状态管理仍需额外处理。
这是目前最主流的微前端框架(如qiankun、single-spa)采用的模式。通过JavaScript动态加载和执行子应用的代码,并利用JS沙箱(with 语句、Proxy 对象)和CSS沙箱(样式隔离技术)来模拟独立环境,同时又能在主应用中共享一些资源。
优点: 用户体验流畅(无刷新切换),隔离性与共享性兼顾,主流框架支持成熟。
缺点: 实现相对复杂,需要对子应用进行改造以适配框架。
在JS沙箱模式中,主应用(基座)需要知道有哪些子应用,以及如何加载和卸载它们。
bootstrap, mount, unmount)供基座调用。加载原理: 基座通过路由判断当前应该渲染哪个子应用,然后动态地向浏览器注入该子应用的HTML、CSS和JS资源,并执行其入口代码。在子应用切换时,会调用前一个子应用的卸载钩子,再加载并挂载新的子应用。
微前端中的路由管理是一个核心挑战,目标是确保主应用和子应用之间的路由协调一致,提供无缝的用户体验。
/app1/xxx → 加载并激活 App1/app2/yyy → 加载并激活 App2pushState, replaceState, popstate) 来实现无刷新路由跳转。/detail,实际URL可能变为/app1/detail。JS沙箱的目的是隔离不同子应用之间的JavaScript运行环境,防止全局变量污染和冲突。
window.foo = 'bar',子应用B也定义了window.foo = 'baz',会导致冲突。Array.prototype),影响其他子应用。window和document的快照;子应用卸载时,将window和document恢复到之前的快照状态。
Proxy对象创建一个假的window代理对象。子应用对全局对象的访问和修改都会被劫持,并作用于代理对象而非真实的window。
Proxy有一定性能开销,某些特殊操作可能无法完全拦截。以Proxy沙箱为例,当子应用访问 window.foo 时,实际上是访问代理对象的 foo 属性。当子应用修改 window.bar = 'new' 时,也是在代理对象上进行操作,不会影响真实的 window。
CSS沙箱的目的是隔离不同子应用之间的样式,防止样式污染和冲突。
.btn { color: red; }可能会覆盖子应用B的.btn { color: blue; }。.button 变为 .app1-button。
<style>标签的插入,并动态添加属性选择器,或者在子应用切换时卸载上一个子应用的样式。虽然子应用之间应该尽可能独立,但在某些场景下,它们仍然需要进行通信,例如:用户登录状态同步、数据共享、事件通知等。
emit)事件,也可以订阅(on)事件。// 主应用或共享的通信文件
class EventBus {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => callback(...args));
}
}
}
window.microAppEventBus = new EventBus(); // 挂载到全局
// 子应用 A
window.microAppEventBus.emit('userLogin', { userId: '123' });
// 子应用 B
window.microAppEventBus.on('userLogin', (data) => {
console.log('Received login event:', data);
});
多个子应用可能都会使用到相同的第三方库(如React、Vue、Lodash、Ant Design等)。如果每个子应用都单独打包这些库,会导致整体包体积过大,加载速度慢。
实现示例(Webpack Module Federation 配置片段):
// Host App (基座) webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
name: 'host_app',
remotes: {
// 声明可以从哪里加载远程应用
app1: 'app1@http://localhost:3001/remoteEntry.js',
app2: 'app2@http://localhost:3002/remoteEntry.js',
},
shared: {
// 共享公共依赖,避免重复加载
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
// ... 其他公共依赖
},
}),
],
};
// Remote App (子应用) webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js', // 远程入口文件
exposes: {
// 暴露子应用的入口或组件
'./App': './src/App.js',
},
shared: {
// 共享公共依赖
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
// ... 其他公共依赖
},
}),
],
};
// webpack.config.js
module.exports = {
// ...
externals: {
react: 'React', // 期望全局存在 React 变量
'react-dom': 'ReactDOM', // 期望全局存在 ReactDOM 变量
vue: 'Vue', // 期望全局存在 Vue 变量
// ...
}
};
这需要主应用在HTML中提前引入CDN上的React/Vue等库。
<!-- 主应用 index.html -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
实现微前端涉及许多底层细节,为了简化开发,通常会使用成熟的微前端框架。
qiankun 是由蚂蚁金服开源的一套完整的微前端解决方案,基于 single-spa 进行二次开发,具有更强的开箱即用能力,并内置了JS沙箱、CSS沙箱、预加载等功能。
Step 1: 主应用 (基座) 配置
安装 qiankun:
npm install qiankun
# 或
yarn add qiankun
在主应用入口文件 (e.g., src/index.js) 中注册子应用并启动:
// src/index.js (主应用)
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'reactApp', // 唯一ID
entry: '//localhost:8001', // 子应用地址
container: '#container', // 子应用挂载的DOM节点
activeRule: '/react', // 激活规则,当路径以 /react 开头时激活
props: {
msg: '来自主应用的消息',
}, // 传递给子应用的props
},
{
name: 'vueApp',
entry: '//localhost:8002',
container: '#container',
activeRule: '/vue',
},
]);
// 启动 qiankun
start({
prefetch: true, // 开启预加载
sandbox: {
strictStyleIsolation: true, // 开启严格的CSS沙箱
experimentalStyleIsolation: true, // 实验性CSS沙箱,防止全局样式污染
},
});
// 路由监听
// 在主应用路由变化时(例如使用 React Router 或 Vue Router),
// 切换到对应的微前端路由前缀,qiankun会根据 activeRule 自动加载子应用。
// 示例:
// import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom';
// function App() {
// return (
// <Router>
// <nav>
// <Link to="/react">React App</Link>
// <Link to="/vue">Vue App</Link>
// </nav>
// <div id="container"></div> <!-- 子应用挂载点 -->
// <Switch>
// <Route path="/react" render={() => null} />
// <Route path="/vue" render={() => null} />
// </Switch>
// </Router>
// );
// }
// ReactDOM.render(<App />, document.getElementById('root'));
Step 2: 子应用 (React/Vue) 配置
子应用需要导出特定的生命周期钩子。为了让 qiankun 识别并加载,需要改造子应用的入口文件。通常是修改Webpack配置,将子应用打包成UMD格式并暴露生命周期函数。
React 子应用 (src/index.js)
// src/index.js (React 子应用)
import React from 'react';
import ReactDOM from 'react-dom/client'; // React 18
import App from './App';
let root = null; // 用于存储 React 18 的 root 实例
function render(props) {
const { container } = props;
const domElement = container ? container.querySelector('#root') : document.getElementById('root');
if (!root) {
root = ReactDOM.createRoot(domElement);
}
root.render(<App />);
}
// 生命周期 - bootstrap:初始化
export async function bootstrap() {
console.log('React micro app bootstrapped');
}
// 生命周期 - mount:挂载
export async function mount(props) {
console.log('React micro app mounted', props);
render(props);
}
// 生命周期 - unmount:卸载
export async function unmount(props) {
console.log('React micro app unmounted', props);
// React 18 卸载方式
if (root) {
root.unmount();
root = null;
}
// React 17 及以下 ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.getElementById('root'));
}
// 独立运行模式 (非qiankun环境)
if (!window.__POWERED_BY_QIANKUN__) {
// 假设非qiankun环境下,<div id="root"></div> 就在当前文档中
const initialRoot = ReactDOM.createRoot(document.getElementById('root'));
initialRoot.render(<App />);
}
Vue 3 子应用 (src/main.js)
// src/main.js (Vue 子应用)
import { createApp } from 'vue';
import App from './App.vue';
import router from './router'; // 假设有 Vue Router
let instance = null;
function render(props = {}) {
const { container } = props;
instance = createApp(App);
instance.use(router);
instance.mount(container ? container.querySelector('#app') : '#app');
}
// 生命周期 - bootstrap:初始化
export async function bootstrap() {
console.log('Vue micro app bootstrapped');
}
// 生命周期 - mount:挂载
export async function mount(props) {
console.log('Vue micro app mounted', props);
render(props);
}
// 生命周期 - unmount:卸载
export async function unmount() {
console.log('Vue micro app unmounted');
instance.unmount();
instance = null;
}
// 独立运行模式 (非qiankun环境)
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
子应用 Webpack 配置 (webpack.config.js)
子应用需要配置Webpack,确保其能够正确地被 qiankun 加载,主要是配置 output.library 和 output.libraryTarget。
// 子应用 webpack.config.js
const { name } = require('./package.json'); // 获取 package.json 中的 name 字段
module.exports = {
// ... 其他配置
output: {
library: `${name}-[name]`, // 库名称,推荐使用package.json中的name作为前缀
libraryTarget: 'umd', // 打包成umd格式
jsonpFunction: `webpackJsonp_${name}`, // 确保不同子应用之间jsonpFunction不冲突
globalObject: 'window', // 确保在各种环境下都能正确获取全局对象
},
// ...
devServer: {
port: 8001, // 子应用端口
headers: {
'Access-Control-Allow-Origin': '*', // 允许跨域
},
// ...
},
};
通过这些配置,qiankun 可以很好地管理子应用的加载、卸载和隔离。
模块联邦是Webpack 5引入的杀手级特性,它从构建层面解决了多应用之间代码共享和动态加载的问题。它不仅仅是微前端的解决方案,更是一种通用的分布式应用架构模式。
优势:
如前文“公共依赖管理”部分所示,通过配置 ModuleFederationPlugin,主应用可以作为Host消费子应用暴露的组件或整个子应用入口。子应用则作为Remote暴露自己的内容。同时,shared 配置可以确保公共依赖只加载一次。
与 qiankun 的对比与选择
| 特性 | qiankun | Webpack Module Federation |
|---|---|---|
| 层级 | 应用层(运行时加载、隔离) | 模块/组件层(编译时/运行时加载、共享) |
| 技术栈 | 完全无关 | 构建于Webpack生态,对框架兼容性好但要求都使用Webpack |
| 隔离机制 | JS/CSS沙箱 | 依赖Webpack构建和Loader处理 |
| 依赖共享 | 运行时动态加载检查,或通过Externals | 原生支持,基于shared配置 |
| 应用间通信 | 手动实现(Props、EventBus) | 直接导入对方暴露的模块(更像组件调用) |
| 适用场景 | 大型、复杂、多团队、多技术栈的应用,追求独立部署与隔离 | 构建大型单页应用内的微组件、微服务,或者对包体积和运行时性能有极致要求的多应用共享场景 |
| 上手难度 | 相对较低,配置较少 | 配置复杂,概念较多,需要深入理解Webpack |
选择建议:
微前端引入了额外的复杂性,可能导致性能问题。优化策略包括:
prefetch 选项。/app1/, /app2/),主应用负责路由分发,子应用处理自身内部路由。框架如 qiankun 会自动处理子应用路由的隔离。unmount 生命周期中,务必清除所有副作用。例如,移除事件监听器,清除定时器,销毁Vue/React实例等。本节将通过一个简单的项目,演示如何使用 qiankun 框架搭建一个微前端应用。我们将构建一个基座应用和两个子应用(一个React子应用,一个Vue子应用)。
创建一个主应用目录 micro-frontend-demo,并在其中创建子应用目录:
mkdir micro-frontend-demo
cd micro-frontend-demo
# 创建主应用 (比如 Vite 或 CRA)
npx create-react-app main-app --template typescript # 或者 vite create main-app --template react-ts
cd main-app && npm install && cd ..
# 创建 React 子应用
npx create-react-app react-sub-app --template typescript
cd react-sub-app && npm install && cd ..
# 创建 Vue 子应用
npm init vue@latest # 或者 @vue/cli create vue-sub-app
cd vue-sub-app && npm install && cd ..
目录结构大致如下:
micro-frontend-demo/
├── main-app/ # 基座应用 (React/Vite)
├── react-sub-app/ # React 子应用
└── vue-sub-app/ # Vue 子应用
进入 main-app 目录,安装 qiankun:
cd main-app
npm install qiankun react-router-dom # 如果是 React
# 或
npm install qiankun vue-router # 如果是 Vue
修改 main-app/src/index.tsx (或 .jsx / .js) 和 main-app/src/App.tsx:
// main-app/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// main-app/src/App.tsx
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Link } from 'react-router-dom';
import { registerMicroApps, start } from 'qiankun';
import './App.css';
// 假设我们使用 React Router 的Link进行导航
// Link to="/react" 会触发 activeRule /react 从而加载 React 子应用
// Link to="/vue" 会触发 activeRule /vue 从而加载 Vue 子应用
function App() {
useEffect(() => {
registerMicroApps([
{
name: 'reactApp',
entry: '//localhost:3001', // React 子应用的开发服务器地址
container: '#sub-app-container',
activeRule: '/react',
props: {
token: 'your_jwt_token_from_main_app',
userInfo: { id: 1, name: 'Main App User' },
onGlobalEvent: (data: string) => {
console.log('Main App received event from sub-app:', data);
},
},
},
{
name: 'vueApp',
entry: '//localhost:3002', // Vue 子应用的开发服务器地址
container: '#sub-app-container',
activeRule: '/vue',
},
]);
start({
prefetch: true, // 开启预加载
sandbox: {
strictStyleIsolation: true, // 严格的CSS沙箱,防止子应用样式污染主应用
experimentalStyleIsolation: true, // 实验性的CSS沙箱,为每个子应用创建一个独立的 shadow DOM 容器
},
});
}, []);
return (
<Router>
<div className="main-app">
<header className="main-header">
<h1>主应用 (Base App)</h1>
<nav>
<Link to="/react" className="nav-link">React 子应用</Link>
<Link to="/vue" className="nav-link">Vue 子应用</Link>
<Link to="/" className="nav-link">主应用首页</Link>
</nav>
</header>
<main className="main-content">
<div id="sub-app-container">
{/* 子应用会挂载到这里 */}
{/* 如果当前路由没有匹配到子应用,可以显示主应用内容 */}
{window.location.pathname === '/' && (
<div>欢迎来到微前端首页!请点击上方导航切换子应用。</div>
)}
</div>
</main>
</div>
</Router>
);
}
export default App;
并为 main-app/src/App.css 添加一些样式:
/* main-app/src/App.css */
.main-app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-header {
background-color: #2c3e50;
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.main-header h1 {
margin: 0;
font-size: 1.8em;
}
.main-header nav .nav-link {
color: white;
margin-left: 20px;
padding: 8px 15px;
border-radius: 5px;
transition: background-color 0.3s ease;
}
.main-header nav .nav-link:hover {
background-color: #34495e;
text-decoration: none;
}
.main-content {
flex-grow: 1;
padding: 20px;
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-items: flex-start; /* 保持内容靠上 */
}
#sub-app-container {
width: 100%; /* 子应用容器占据整个宽度 */
max-width: 960px; /* 限制子应用最大宽度 */
min-height: 500px; /* 保证容器有高度 */
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
padding: 20px;
}
启动主应用:
cd main-app
npm start # 或 yarn start 或 npm run dev
默认在 http://localhost:3000 运行。
react-sub-app)进入 react-sub-app 目录,安装 @craco/craco 和 webpack-bundle-analyzer (可选,用于分析打包文件)。 CRA 需要进行一些改造来暴露生命周期函数并支持跨域。
cd react-sub-app
npm install @craco/craco # CRA 项目改造工具
修改 package.json 的 scripts:
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
}
在项目根目录创建 craco.config.js:
// react-sub-app/craco.config.js
const { name } = require('./package.json');
module.exports = {
webpack: {
configure: (webpackConfig) => {
webpackConfig.output = {
...webpackConfig.output,
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
globalObject: 'window',
};
return webpackConfig;
},
},
devServer: {
port: 3001, // 确保端口与主应用配置一致
headers: {
'Access-Control-Allow-Origin': '*',
},
},
};
修改 react-sub-app/src/index.tsx 文件以暴露 qiankun 生命周期:
// react-sub-app/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
let root: ReactDOM.Root | null = null;
let currentAppProps: any = {}; // 存储当前 props
function render(props: any) {
const { container, onGlobalEvent } = props;
currentAppProps = props; // 更新 props
const domElement = container ? container.querySelector('#root') : document.getElementById('root');
if (!root) {
root = ReactDOM.createRoot(domElement as HTMLElement);
}
root.render(<React.StrictMode><App onGlobalEvent={onGlobalEvent} /></React.StrictMode>);
}
export async function bootstrap() {
console.log('[react-sub-app] bootstrap');
}
export async function mount(props: any) {
console.log('[react-sub-app] mount', props);
render(props);
}
export async function unmount() {
console.log('[react-sub-app] unmount');
if (root) {
root.unmount();
root = null;
}
}
if (!window.__POWERED_BY_QIANKUN__) {
// 独立运行模式
root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<React.StrictMode><App onGlobalEvent={() => console.log('Standalone event')} /></React.StrictMode>);
}
修改 react-sub-app/src/App.tsx 接收主应用传来的 props 并发送事件:
// react-sub-app/src/App.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';
interface AppProps {
token?: string;
userInfo?: { id: number; name: string; };
onGlobalEvent?: (data: string) => void;
}
function App({ token, userInfo, onGlobalEvent }: AppProps) {
const handleClick = () => {
onGlobalEvent && onGlobalEvent('Hello from React sub-app!');
};
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
这是 <code>React 子应用</code>!
</p>
<p>主应用传入的 Token: <code>{token || 'N/A'}</code></p>
<p>主应用传入的用户信息: <code>{userInfo ? userInfo.name : 'N/A'}</code></p>
<button onClick={handleClick} style={{ padding: '10px 20px', fontSize: '1em', cursor: 'pointer' }}>
点击我向主应用发送事件
</button>
</header>
</div>
);
}
export default App;
启动 React 子应用:
cd react-sub-app
npm start # 默认在 http://localhost:3001 运行
vue-sub-app)进入 vue-sub-app 目录,修改 vue.config.js(如果不存在则创建):
// vue-sub-app/vue.config.js
const { name } = require('./package.json');
module.exports = {
devServer: {
port: 3002, // 确保端口与主应用配置一致
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
globalObject: 'window',
},
},
};
修改 vue-sub-app/src/main.js 以暴露 qiankun 生命周期:
// vue-sub-app/src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router'; // 假设你创建了 Vue Router
let instance = null;
function render(props = {}) {
const { container } = props;
instance = createApp(App);
instance.use(router); // 如果有 router
instance.mount(container ? container.querySelector('#app') : '#app');
}
export async function bootstrap() {
console.log('[vue-sub-app] bootstrap');
}
export async function mount(props) {
console.log('[vue-sub-app] mount', props);
render(props);
}
export async function unmount() {
console.log('[vue-sub-app] unmount');
instance.unmount();
instance = null;
}
if (!window.__POWERED_BY_QIANKUN__) {
// 独立运行模式
render();
}
修改 vue-sub-app/src/App.vue:
<template>
<div id="app" style="text-align: center; padding: 20px; border: 1px dashed #42b983; border-radius: 8px; background-color: #e6ffe6;">
<img alt="Vue logo" src="./assets/logo.svg" width="100" height="100">
<h1>这是 Vue 子应用</h1>
<p>Vue 版本: <code>{{ vueVersion }}</code></p>
<router-link to="/about" style="margin-right: 15px;">关于页面</router-link>
<router-link to="/">首页</router-link>
<router-view />
</div>
</template>
<script>
import { version } from 'vue';
export default {
name: 'VueApp',
setup() {
const vueVersion = version;
return {
vueVersion,
};
}
}
</script>
<style>
/* Vue 子应用内部样式 */
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
</style>
为 Vue 子应用添加路由 (vue-sub-app/src/router/index.js):
// vue-sub-app/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'home',
component: { template: '<h2>Vue 子应用 Home</h2>' },
},
{
path: '/about',
name: 'about',
component: { template: '<h2>Vue 子应用 About</h2><p>这是 Vue 的 about 页面。</p>' },
},
];
// 注意:在微前端环境中,createWebHistory 可能需要一个 base
// qiankun 会自动劫持路由,所以这里可以保持默认或按需配置
const router = createRouter({
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue/' : '/'), // 在 qiankun 环境下为子应用添加 base
routes,
});
export default router;
启动 Vue 子应用:
cd vue-sub-app
npm run serve # 默认在 http://localhost:3002 运行
现在,您应该可以同时运行三个应用:
http://localhost:3000http://localhost:3001 (仅用于独立测试,微前端模式下不直接访问)http://localhost:3002 (仅用于独立测试,微前端模式下不直接访问)访问 http://localhost:3000,点击“React 子应用”或“Vue 子应用”链接。您将看到相应的子应用无刷新地加载到主应用的 #sub-app-container 中。
props 成功向子应用传递了数据。/vue/) 不变,证明了路由的隔离和协调。在实际生产环境中,每个应用都需要独立部署。以 React 子应用为例:
cd react-sub-app
npm run build
这会在 react-sub-app/build 目录下生成生产环境的静态文件。将这些文件部署到您的静态文件服务器(如Nginx、OSS、CDN)上,并确保其可以通过某个URL访问(例如 https://your-cdn.com/react-sub-app/)。
然后,在主应用的 registerMicroApps 中将 entry 地址修改为生产环境的URL:
// main-app/src/App.tsx
{
name: 'reactApp',
entry: 'https://your-cdn.com/react-sub-app/', // 生产环境地址
container: '#sub-app-container',
activeRule: '/react',
}
重新构建并部署主应用。这样,当用户访问主应用时,它将从CDN加载子应用资源。
恭喜! 您已经完成了从微前端基础概念到实际项目搭建的全过程。微前端是一个强大的工具,能够帮助您构建更具可伸缩性、可维护性和协作性的前端应用。虽然它引入了一些新的复杂性,但通过理解其核心原理并选择合适的框架,您将能够有效地驾驭它。
祝您在微前端的实践中一切顺利!