文章目录
一. 什么是微前端
-
“微前端架构”就是构建基于微服务的前端应用架构。
-
其思想是将前端应用切分为一系列可以单独部署的松耦合的应用,然后将这些应用组装起来创建单个面向用户的应用程序。
二. 微前端的优势
- 降低代码耦合
- 独立开发、独立部署
- 增量升级:微前端是一种非常好的实施渐进式重构的手段和策略
- 独立运行时,每个微应用之间状态隔离,运行时状态不共享
- 团队可以按照业务垂直拆分更高效
三. 微前端的多种实现
3.1 iframe
iframe
天然具备微前端的基因。我们只需将单体的前端应用,按照业务模块进行拆分,分别部署。最后通过 iframe
进行动态加载即可。
<html>
<head>
<title>微前端-ifame</title>
</head>
<body>
<h1>我是容器</h1>
<iframe id="mfeLoader"></iframe>
<script type="text/javascript">
const routes = {
'/': 'https://app.com/index.html',
'/app1': 'https://app1.com/index.html',
'/app2': 'https://app2.com/index.html',
};
const iframe = document.querySelector('#mfeLoader');
iframe.src = routes[window.location.pathname];
</script>
</body>
</html>
优点:
- 实现简单
- 天然具备隔离性
缺点:
- 主页面和 iframe 共享最大允许的 HTTP 链接数。
- iframe 阻塞主页面加载。
- 浏览器的后退按钮无效
iframe子窗口调用父窗口的方法
在iframe的页面中,通过JavaScript编写代码来调用父组件的方法。可以使用window.parent
或window.top
来引用父窗口对象,然后调用父窗口的方法。
例如,假设父组件中有一个名为"handleClick"的方法,可以在iframe的页面中使用以下代码来调用它:
window.parent.handleClick();
// 或者
window.top.handleClick();
iframe父窗口调用子窗口方法
在父窗口中,通过JavaScript获取iframe的引用,然后使用contentWindow
属性访问子窗口的对象
var iframe = document.getElementById('myIframe');
var childWindow = iframe.contentWindow;
childWindow.handleClick();
iframe子窗口向父窗口通信
postMessage
用于在不同的域之间发送消息。它允许你发送消息到父窗口,并接收来自父窗口的消息。
在 iframe 中:
window.parent.postMessage('Hello from iframe!', 'http://example.com');
在父窗口中:
window.addEventListener('message', function(event) {
if (event.origin !== 'http://example.com') return; // 验证消息来源
console.log('Received message from iframe:', event.data);
}, false);
iframe的父窗口传递参数给子窗口
方法一:父窗口可以使用window.postMessage
方法向子窗口发送消息
var iframe = document.getElementById('myIframe');
iframe.contentWindow.postMessage('Hello from parent!', '*');
在子窗口中,可以使用以下代码监听消息:
window.addEventListener('message', function(event) {
if (event.origin !== 'http://example.com') return; // 验证消息来源
console.log('Received message from parent:', event.data);
}, false);
方法二:使用URL查询参数:如果父窗口和子窗口处于同一域下,并且没有跨域限制,父窗口可以通过修改iframe的src
属性,将参数作为URL
查询参数传递给子窗口。在父窗口中,可以使用以下代码将参数作为URL查询参数传递给子窗口:
var iframe = document.getElementById('myIframe');
iframe.src = 'child.html?param1=value1¶m2=value2';
在子窗口中,可以通过JavaScript获取URL查询参数:
var param1 = getUrlParam('param1'); // 获取URL查询参数的方法,可以根据实际情况实现
var param2 = getUrlParam('param2'); // 获取URL查询参数的方法,可以根据实际情况实现
3.2 服务端模板组合
常见的实现方式是,服务端根据路由动态渲染特定页面的模板文件。架构图如下:
优点:
- 实现简单
- 技术栈独立
缺点:
- 需要额外配置 Nginx
- 前后端分离不彻底
3.3 微前端框架 single-spa
借助 single-spa
,开发者可以为不同的子应用使用不同的技术栈,比如子应用 A 使用 vue
开发,子应用 B 使用 react
开发,完全没有历史债务。
single-spa 的实现原理并不难,从架构上来讲可以分为两部分:子应用
和容器应用
。
子应用与传统的单页应用的区别在于:
- 不需要 HTML 入口文件,
- js 入口文件导出的模块,必须包括 bootstrap、mount 和 unmount 三个方法。
容器应用主要负责注册应用,当 url 命中子应用的路由时激活并挂载子应用,或者当子应用不处于激活状态时,将子应用从页面中移除卸载。其核心方法有两个:
registerApplication
注册并下载子应用start
启动处于激活状态的子应用。
容器应用代码
<html>
<body>
<script src="single-spa-config.js"></script>
</body>
</html>
single-spa-config.js
代码如下:
import * as singleSpa from 'single-spa';
const appName = 'app1';
const app1Url = 'http://app1.com/app1.js'
// loadJS 方法是伪代码,表示加载 app1.js。开发者需要自己实现,或者借助 systemJS 来实现。
singleSpa.registerApplication('app1',() => loadJS(app1Url), location => location.pathname.startsWith('/app1'))
singleSpa.start();
子应用代码:
//app1.js
let domEl;
export function bootstrap(props) {
return Promise
.resolve()
.then(() => {
domEl = document.createElement('div');
domEl.id = 'app1';
document.body.appendChild(domEl);
});
}
export function mount(props) {
return Promise
.resolve()
.then(() => {
domEl.textContent = 'App 1 is mounted!'
});
}
export function unmount(props) {
return Promise
.resolve()
.then(() => {
domEl.textContent = '';
})
}
优点:
- 纯前端解决方案
- 可以使用多种技术栈
- 完善的生态
缺点:
- 上手成本高
- 需要改造现有应用
- 跨应用的联调变得复杂
3.4 微前端框架 qiankun
qiankun
是一个基于 single-spa
的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
在主应用中注册微应用
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule',
},
{
name: 'vue app',
entry: { scripts: ['//localhost:7100/main.js'] },
container: '#yourContainer2',
activeRule: '/yourActiveRule2',
},
]);
start();
当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap
、mount
、unmount
三个生命周期钩子,以供主应用在适当的时机调用。
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
优点:
- 简单:qiankun 对于用户而言只是一个类似 jQuery 的库,你需要调用几个 qiankun 的 API 即可完成应用的微前端改造
- 解耦/技术栈无关
- 完善的生态
缺点:
- 上手成本高
- 需要改造现有应用
- 跨应用的联调变得复杂
3.5 微前端microApp
main.ts
中初始化micro-app
和相关配置
/**
* micro-app 微前端
*/
import microApp from '@micro-zoe/micro-app';
microApp.start({
'disable-memory-router': true, // 关闭虚拟路由系统
'disable-patch-request': true, // 关闭对子应用请求的拦截
lifeCycles: {
created() {
// console.log('created');
},
beforemount() {
// console.log('beforemount');
},
mounted() {
console.log('fst', performance.now().toFixed()); // 首屏时间
// console.log('mounted');
},
unmount() {
console.log('unmount');
},
error(e) {
Sentry.captureException(new Error('主站Error:生命周期错误'), {
level: 'error',
extra: {
...e
}
});
}
}
});
microApp.router.setBaseAppRouter(router);
microApp.router.beforeEach({
'micoro-app-homeweb-app': (to, from) => {
const toQuery = qs.parse(to.search, { ignoreQueryPrefix: true });
const fromQuery = qs.parse(from.search, { ignoreQueryPrefix: true });
if (
toQuery.type !== fromQuery.type ||
(from.pathname === '/search' && fromQuery.threadId && !toQuery.threadId && toQuery.type === 'chat')
) {
// 设置全局ref
savePageRef(to.fullPath);
} else if (to.pathname === '/search' && toQuery.type === 'chat' && toQuery.threadId) {
const BASE_URL = (import.meta as any).env.VITE_HOST;
sessionStorage.setItem('ref', BASE_URL + to.fullPath);
} else {
savePageRef(to.fullPath);
}
}
});
microApp.router.afterEach({
'micoro-app-homeweb-app': (to, from) => {
const names = {
'/': '首页',
'/search': '搜索',
'/g-star': 'g-star',
'/g-star/apply': 'g-star 申请',
'/explore': '搜索开源'
};
const toQuery = qs.parse(to.search, { ignoreQueryPrefix: true });
if (to.pathname === '/search' && toQuery.type !== 'repo') {
// 对话有threadid后上报
if (toQuery.threadId) useReport('pageview', {}, { 'homeweb-page-title': names[to.pathname] });
} else {
useReport('pageview', {}, { 'homeweb-page-title': names[to.pathname] });
}
}
});
- 路由中添加micorApp名称
{
path: '/',
component: DefaultLayout,
children: [
{
path: '',
name: 'home',
component: () => import('@/views/micro-page/proxy-homeweb.vue'),
meta: {
micorApp: ['micoro-app-homeweb-app'],
hasMobile: true,
hiddenFooter: true,
reportTitle: '首页',
className: 'w-full min-w-full',
headerClassName: ''
}
},
...
]
}
- Layout.vue中添加相关路由守卫和监听器
microApp.router.afterEach({
'micoro-app-homeweb-app': (to, from) => {
outOfSearch.value = to.pathname !== '/search';
}
});
const checkPage = (data: any) => {
if (data.type === 'checkPage') {
return 'ready';
}
};
onMounted(() => {
microApp.addDataListener('micoro-app-homeweb-app', checkPage, false);
});
onUnmounted(() => {
microApp.removeDataListener('micoro-app-homeweb-app', checkPage);
});
- 页面中加载微应用
<template>
<div id="homeweb-container"></div>
</template>
onMounted(() => {
toolbarContain.value = document.getElementById('toolbarBottom');
microApp.renderApp({
name: 'micoro-app-homeweb-app',
url: origin,
container: '#homeweb-container',
data: {
emitter,
toolbarContain,
$router: router
},
iframe: true,
baseroute: '/',
defaultPtah: route.path
});
});
onBeforeUnmount(() => {
microApp.unmountApp('micoro-app-homeweb-app');
});