背景
优势
-
无技术栈限制:主框架不限制接入应用的技术栈,不论是 React/Vue/Angular/JQuery 还是其他等框架。
-
子应用具备完全自主权
-
独立开发,独立部署,子应用的仓库独立,部署完成后主框架自动完成同步更新
-
独立运行时,每个子应用之间状态隔离,运行时状态不共享
- CSS 样式隔离
- Javascript 沙箱隔离
-
-
重复依赖,不同应用之间依赖的包存在很多重复,由于各应用独立开发、编译和发布,难免会存在重复依赖的情况。导致不同应用之间需要重复下载依赖,额外再增加了流量和服务端压力
-
增量升级,在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
-
更快交付客户价值,有助于持续集成、持续部署以及持续交付;
-
维护和 bugfix 非常简单,每个团队都熟悉所维护特定的区域;
场景
- 单实例:即同一时刻,只要一个子应用被展示,子应用具备一个完整的应用生命周期,通常基于url的变化来做子应用的切换
- 多实例:同一时刻可展示多个子应用。通常使用web components方案来做子应用封装,子应用更像是一个业务组件而不是应用
区别
-
iframe的优缺点
-
优点
- 完全隔离了css和js,避免了各个系统之间的样式和js污染
- 可以在子系统完全不修改的情况下嵌入进来
-
缺点
-
页面加载问题: 影响主页面加载,阻塞onload事件,本身加载也很慢,页面缓存过多会导致电脑卡顿。(无法解决)
-
布局问题:
- 出现多个滚动条,用户体验不佳。
- 弹窗及遮罩层问题:只能在iframe范围内垂直水平居中,没法在整个页面垂直水平居中。
-
浏览器前进/后退问题:iframe和主页面共用一个浏览历史
- 刷新页面,iframe会重置(比如说从列表页跳转到详情页,然后刷新,会返回到列表页),因为iframe的地址没有变化
-
跨域
解决方法:
后端代理:通过服务器端设置代理,请求目标网页内容,然后将内容返回给前端,实现跨域访问。 CORS:如果你控制了被嵌入页面的服务器,可以通过设置CORS(跨源资源共享)策略来允许特定的跨域请求。 Window.postMessage:使用window.postMessage方法进行跨域通信。这是一种安全的方式来实现跨源通信。
-
不同源的系统之间的通讯需要通过postMessage,存在一定的安全性
-
搜索引擎的检索程序无法解读这种页面,不利于
SEO
-
-
-
single-spa 是一个用于前端微服务化的 javascript 前端解决方案,实现了路由的劫持和应用加载
-
缺点
- 无通信机制
- 不支持 Javascript 沙箱
- 样式冲突
- 无法预加载
-
-
qiankun
-
优点
- 📦基于 single-spa 封装,提供了更加开箱即用的 API。
- 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
- 🛡 样式隔离,确保微应用之间样式互相不干扰。
- 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
- ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
- 🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
- 🔥 社区较为活跃,维护者也较多,有问题会及时得到响应;
-
qiankun
cloud.tencent.com/developer/a…
nginx部署配置
目的:多项目背景下,在根目录创建qiankun文件夹
└── html/ # 根文件夹
|
├── qiankun/ # 存放所有qiankun的项目文件夹
| |── child/ # 存放所有微应用的文件夹
| | ├── vue-hash/ # 存放微应用 vue-hash 的文件夹
| | ├── vue-history/ # 存放微应用 vue-history 的文件夹
| | ├── react-hash/ # 存放微应用 react-hash 的文件夹
| | ├── react-history/ # 存放微应用 react-history 的文件夹
| | ├── angular-hash/ # 存放微应用 angular-hash 的文件夹
| | ├── angular-history/ # 存放微应用 angular-history 的文件夹
| |── index.html # 主应用的index.html
| |── css/ # 主应用的css文件夹
| |── js/ # 主应用的js文件夹
使用history路由模式下的nginx配置
location / {
root html;
index index.html index.htm;
try_files $uri $uri/ /qiankun/index.html; //资源重定向
}
location /qiankun {
try_files $uri $uri/ /qiankun/index.html;//qiankun项目
}
location /vue2-mobile {
try_files $uri $uri/ /qiankun/child/vue2-mobile/index.html;//这里将qiankun里的微应用作为单一项目
}
搭建
主应用(react)
-
下载qiankun
-
react主应用创建:通过create-react-app脚手架创建模板
-
注册微应用
//main.js registerMicroApps( [ { name: "vue2-mobile", entry: isDev ? "//localhost:8001" : '/qiankun/child/vue2-mobile/',//配置微应用访问入口,注意微应用的 entry 路径最后面的 / 不可省略,否则 publicPath 会设置错误 container: "#vue2-mobile", //容器 activeRule: "/qiankun/vue2-mobile", //访问路径 // activeRule: getActiveRule('#/vue2-mobile'), }, ], { beforeLoad: (app) => { console.log("before load app.name====>>>>>", app.name); }, beforeMount: [ (app) => { console.log("[LifeCycle] before mount %c%s", "color: green;", app.name); }, ], afterMount: [ (app) => { console.log("[LifeCycle] after mount %c%s", "color: green;", app.name); }, ], afterUnmount: [ (app) => { console.log( "[LifeCycle] after unmount %c%s", "color: green;", app.name ); }, ], } ); //当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑, //所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。 start();
-
路由
//main.js const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <React.StrictMode> <RouterProvider router={router}> <App /> </RouterProvider> </React.StrictMode> ); //router import { createBrowserRouter, RouterProvider } from "react-router-dom"; import Web1 from "../page/web1.jsx"; export const router = createBrowserRouter([ { path: "/qiankun", element: <Home></Home>, children: [ { path: "vue2-mobile", element: <Web1></Web1>, children: [ { path: "learn", }, { path: "user", }, { path: "login", }, ], }, { path: "vue2-pc", element: <Web2></Web2>, children: [ { path: "login", }, { path: "user", }, ], }, ] }, // { // path: '/:catchAll(.*)*', // // name: 'error', // // meta: { // // name: '404', // // }, // component: () => import('../page/404.jsx'), // } ]); //page/Home //TODO:这里容器必须设置在父路径上 function Home() { return ( <Outlet></Outlet> ); } export default Home; //page/web1.jsx function Web1() { return ( <div> <div id="vue2-mobile" style={{ width: "100%", height: "100%" }}></div> </div> ); } export default Web1;
微应用
vue2
-
vue.config.js
devServer: { headers: { "Access-Control-Allow-Origin": "*", }, }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: "umd", // 把微应用打包成 umd 库格式 jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal }, },
-
public-path.js
if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
-
main.js
import './public-path'; let instance = null; function render(props = {}) { const { container } = props; instance = new Vue({ router, store, render: (h) => h(App), }).$mount(container ? container.querySelector("#app") : "#app"); console.log(instance); } // 独立运行时 if (!window.__POWERED_BY_QIANKUN__) { render(); } /** * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。 */ export async function bootstrap() { console.log("bootstrap"); } /** * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法 */ export async function mount(props) { console.log("[vue] props from main framework", props); render(props); } /** * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例 */ export async function unmount() { console.log("bootstrap unmount"); instance.$destroy(); }
-
路由
const router = new VueRouter({ mode: "history", base: window.__POWERED_BY_QIANKUN__ ? "/qiankun/vue2-mobile/" //配置子应用的路由根路径 : isProd ? "/vue2-mobile/"//单一项目下的访问路径 : process.env.BASE_URL, routes, scrollBehavior(to, from, savedPosition) { if (to.hash) { return { selector: to.hash, }; } }, });
vue3+vite
使用vite-plugin-qiankun插件
-
路由
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper' const isProd = process.env.NODE_ENV === 'production' export const history = createWebHistory( qiankunWindow.__POWERED_BY_QIANKUN__ ? '/qiankun/vue3' //配置子应用的路由根路径 : isProd ? '/vue3/' //单一项目下的访问路径 : process.env.BASE_URL )
-
main.ts
import './assets/main.css' import './public-path' import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' import router from './router' import { history } from './router' import { renderWithQiankun, qiankunWindow, type QiankunProps } from 'vite-plugin-qiankun/dist/helper' let instance: any = null function render(props: any = {}) { const { container } = props instance = createApp(App) instance.use(createPinia()) instance.use(router) instance.use(router) instance.mount(container ? container.querySelector('#vue3-page') : '#vue3-page') } // 独立运行时 if (!qiankunWindow.__POWERED_BY_QIANKUN__) { render() } else { renderWithQiankun({ mount(props) { console.log('viteapp mount') render(props) }, bootstrap() { console.log('bootstrap') }, unmount(props) { console.log('vite被卸载了') instance.unmount(document.querySelector('#vue3-page')) instance._container.innerHTML = '' history.destroy() // 不卸载 router 会导致其他应用路由失败 instance = null }, update: function (props: QiankunProps): void | Promise<void> { throw new Error('Function not implemented.') } }) }
打包部署
注意事项
部署之后注意三点:
activeRule
不能和微应用的真实访问路径一样,否则在主应用页面刷新会直接变成微应用页面。- 微应用的真实访问路径就是微应用的
entry
,entry
可以为相对路径。 - 微应用的
entry
路径最后面的/
不可省略,否则publicPath
会设置错误,例如子项的访问路径是http://localhost:8080/app1
,那么entry
就是http://localhost:8080/app1/
。
打包
主应用
-
main.js
{ name: "vue2-mobile", entry: isDev ? "//localhost:8001" : '/qiankun/child/vue2-mobile/',//配置微应用访问入口,注意微应用的 entry 路径最后面的 / 不可省略,否则 publicPath 会设置错误 container: "#vue2-mobile", //容器 activeRule: "/qiankun/vue2-mobile", //访问路径 // activeRule: getActiveRule('#/vue2-mobile'), },
-
webpack
output: { path: path.resolve(__dirname, "./dist"), filename: "js/[name].[hash].js", //输出文件名 publicPath: isDev ? '/' : '/qiankun', //资源访问根路径 },
微应用
-
vue2
- publicPath: isProd ? "/qiankun/child/vue2-mobile/" : "/",
问题
[qiankun]: Target container with #app not existed while app1 loading!
需要在主应用 配置 子应用的路由页面,配置子应用容器id为web1
{
name: "Web1", // app name registered
entry: "//localhost:8088",
container: "#web1",
activeRule: "/Web1",
},
You need to export lifecycle functions in Web1 entry
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: "umd", // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
},
},
接口请求404
主应用代理子应用ip
proxy: [
{
context: ['/vue2-mobile/api'],
target: 'http://localhost:8001',
// changeOrigin: true,
// secure: false,
// xfwd: false,
// pathRewrite: { '^/api': '' } //重点:重写资源访问路径,避免转发请求 404问题
}, {
context: ['/vue2-pc/api'],
target: 'http://localhost:8002',
}
]
子应用代理后端接口
proxy: {
'/vue2-px/api': {
target: 'http://lm-web.top:3600/',
changeOrigin: true,
secure: false,
xfwd: false,
},
},
api
umd
cloud.tencent.com/developer/a…
UMD
的全称是Universal Module Definition
,也就是通用模块标准,它的目标是使一个模块能运行在各种环境下,不论是CommonJS
、AMD
、ES6 Module
或者非模块化环境都能正确引入到库文件
registerMicroApps
参数解析: registerMicroApps(apps, lifeCycles?)
apps
-Array
- 微应用的注册信息
name
-String
- 微应用的名称,必须确保唯一 - 【必选】entry
-String
- 微应用的入口 - 【必选】container
-String | HTMLElement
- 微应用的容器节点的选择器或者Element
实例 - 【必选】acticeRule
-String | (location:Location) => boolean | Array | (location:Location) => boolean
- 微应用的激活规则 - 【必选】
loadMicroApp 手动加载微应用
import { loadMicroApp } from 'qiankun';
loadMicroApp({
name: 'micro-clouds',
entry: '//localhost:7000',
activeRule: '/micro-clouds',
container: '#subapp2', // 子应用挂载的div
// 传递给子应用的参数
props: {
routerBase: '/micro-clouds',
}
});