一、微前端基本概念
概念:
微前端借鉴了微服务的架构理念,将一个庞大的前端应用拆分为多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用联合为一个完整的应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
核心目标:
微前端的核心目标是将巨石应用拆解成若干可自治的松耦合微应用。
应用场景:
商业产品设计,一个大的商业产品,开发过程中拆分微前端架构来设计,可以将大系统拆分微独立的各种子模块。在进行产品销售过程中可以根据用户的需求来选中哪些业务打包组合。
一个庞大的业务,可能分包给不同的团队开发,技术栈不统一的情况下,我们可以采用微前端来进行项目融合。保证项目业务是完善的。
微前端架构里面可以包含很多业务、H5端、PC端业务、数据可视化平台等等。
因为微前端理念是不限制技术,不限制框架。
二、微前端特点
- 技术栈无关 主框架不限制接入应用的技术栈,子应用可自主选择技术栈
- 独立开发/部署 各个团队之间仓库独立,单独部署,互不依赖
- 增量升级 当一个应用庞大之后,技术升级或重构相当麻烦,而微应用具备渐进式升级的特性
- 独立运行时 微应用之间运行时互不依赖,有独立的状态管理
- 提升效率 应用越庞大,越难以维护,协作效率越低下。微应用可以很好拆分,提升效率
三、目前微前端主流方案
一般大型互联网公司都有一套自己的微前端解决方案。
我们不针对哪家公司的架构讨论,我们主要说一下主流方案
基于iframe
完全隔离的方案
iframe在一个应用中可以独立运行另一个应用,这种方案我们在html中可以使用。因为他是两个独立的容器。
特点:
- 用法简单,一个标签就可以解决问题
- 完美隔离,JS、CSS 都是独立的运行环境
- 不限制使用,页面上可以放多个
iframe
来组合业务
缺点:
- 无法保持路由状态,刷新后路由状态就丢失
- 完全的隔离导致与子应用的交互变得极其困难,想要各个应用进行通信,需要提出很多其他解决方案
- 整个应用全量资源加载,加载太慢,进入程序就要加载所有的应用。
single-spa
路由劫持方案
Single SPA 推荐 动态模块加载 的方式来搭建微前端架构,主应用作为一个纯净的“加载器”,仅提供一个基础 HTML 页面,配合 SystemJS 的 importMap 特性来加载微应用,所有微应用的注册以及版本管理通过 import-map.json 文件来控制。
其实我们Vue和React开发思想就是基于single-spa的方式,结合H5的history来进行路由映射和访问
京东micro-app
方案
京东 micro-app
并没有沿袭 single-spa
的思路,而是借鉴了 WebComponent
的思想,通过 CustomElement
结合自定义的 ShadowDom
,将微前端封装成一个类 webComponents
组件,从而实现微前端的组件化渲染。
QianKun
蚂蚁开源的微前端框架。真正意义上的单页微前端框架,基于single-spa封装
umi内置了qiankun框架。
四、项目实战
项目源码地址
gitee:weiqianduan-qiankun1
创建一个Vue2项目
不同于single-spa来实现微前端,我们用qiankun来搭建整个架构的时候,主应用(基座)是一个完整的项目。你可以选中用vue来搭建一个基座项目,也可以使用react或者umi等等。
再基座项目中,我们所有主页面、路由、权限都可以实现。子应用只负责对应自己的业务板块。
接下来我创建一个vue2的项目来作为我们基座应用。使用vue2来搭建相对比较简单,容易上手。
(1)搭建完成的效果
(2)创建项目base
vue create base
(3)下载elementUI插件
vue add element
下载elementUI的目的是为了在项目中构建导航和布局方便一些,当然你可以不用这个UI组件库。
(4)App.vue中完成主页面搭建
<el-container style="height: 100vh; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(84, 92, 100)">
<div style="padding:10px">
logo区域
</div>
<el-menu>
左侧菜单。。。。
</el-menu>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 12px;">
头部区域代码。。。
</el-header>
<el-main>
<!-- 主应用的渲染区域,用于挂载主应用触发的组件 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
其中el-main标签中我们要设置路由渲染出口。
(5)修改项目端口
在vue.config.js配置文件中修改一下主应用的启动端口。项目默认启动端口微8080,但是我们后续会创建vue的子应用,为了防止端口冲突,所以我们尽量修改一下主应用端口
module.exports = {
devServer: {
open: true,
port: 8888,
}
}
搭建项目基座
将上面步骤的Vue2项目作为我们整个微前端项目的基座
(1)主应用下载插件
yarn add qiankun # 或者 npm i qiankun -S
下载后的依赖
"dependencies": {
"core-js": "^3.6.5",
"element-ui": "^2.4.5",
"nprogress": "^0.2.0",
"qiankun": "^2.7.4",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"vuex": "^3.4.0"
},
我们先在主应用中创建微应用的承载容器,这个容器规定了微应用的显示区域,微应用将在该容器内渲染并显示。
(2)主应用中配置
base/src/qiankun/index.js
/**
* 用于存放我们各个微应用的数组。
* 以后有多少个微应用,这个数组中就会存放多少个对象
*/
const apps = [
];
import { message } from "element-ui";
import {
registerMicroApps,
addGlobalUncaughtErrorHandler,
start,
} from "qiankun";
/**
* 注册微应用
* 第一个参数 - 微应用的注册信息
* 第二个参数 - 全局生命周期钩子
*/
registerMicroApps(apps, {
// qiankun 生命周期钩子 - 微应用加载前
beforeLoad: (app) => {
console.log("before load", app.name);
return Promise.resolve();
},
// qiankun 生命周期钩子 - 微应用挂载后
afterMount: (app) => {
console.log("after mount", app.name);
return Promise.resolve();
},
});
/**
* 添加全局的未捕获异常处理器
*/
addGlobalUncaughtErrorHandler((event) => {
console.error(event);
const { message: msg } = event;
// 加载失败时提示
if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
message.error("微应用加载失败,请检查应用是否可运行");
}
});
// 导出 qiankun 的启动函数
export default start;
(3)加载qiankun的配置
src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import startQiankun from "./qiankun/index";
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
startQiankun()
执行start就是在开启乾坤架构。
(4)配置微应用容器
我们先在主应用中创建微应用的承载容器,这个容器规定了微应用的显示区域,微应用将在该容器内渲染并显示。
要修改的代码在App.js
文件中
<el-main>
<!-- 主应用的渲染区域,用于挂载主应用触发的组件 -->
<router-view v-show="$route.name"></router-view>
<!-- 子应用渲染的区域,用于挂载Vue或者React子应用节点 -->
<div v-show="!$route.name" id="container"></div>
</el-main>
先保证自己的主应用访问能够正常进行。
主应用的路由如下:
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
- 如果当前访问的路径是主应用的路径,我们可以利用
$route.name
判断是否渲染 - 如果获取不了
$route.name
,说明访问的不是主应用路由,对应就是加载子应用。单独在container容器中加载我们的子应用
总结:如果你在浏览器输入的地址:http://127.0.0.1:8888/或者http://127.0.0.1:8888/about那一定代表访问主应用程序,$route.name获取到结果为true。
反之,如果访问http://127.0.0.1:8888/vue,无法获取路由映射这个时候,默认访问微应用,渲染放在#container容器中加载
搭建Vue微应用
基座已经搭建好了。接下来我们就开始搭建Vue微应用程序
(1) 创建一个vue项目
vue create vuedemo
(2)安装elementUI
vue add element
(3)配置路由
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
和页面渲染出口
<el-menu :router="true" :default-active="activeIndex2" class="el-menu-demo" mode="horizontal" @select="handleSelect"
background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
<el-menu-item index="/">处理中心</el-menu-item>
<el-menu-item index="/about">订单管理</el-menu-item>
</el-menu>
<div>
<router-view></router-view>
</div>
在vue微应用中我们也配置了两个页面,等会可以实现页面切换效果
点击不同菜单可以实现切换效果。
单独启动这个微应用,我们正常跑是没有问题的。
(4)配置微应用
在创建好了 Vue
微应用后,我们可以开始我们的接入工作了。首先我们需要在主应用中注册该微应用的信息
base/src/qiankun/index.js
const apps = [
/**
* name: 微应用名称 - 具有唯一性
* entry: 微应用入口 - 通过该地址加载微应用,保证微应用地址和端口正确
* container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
* activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
*/
{
name: "VueApp",
entry: "//localhost:8080",
container: "#container",
activeRule: "/vue",
},
];
通过上面的代码,我们就在主应用中注册了我们的 Vue
微应用,进入 /vue
路由时将加载我们的 Vue
微应用。
(5)配置微应用的启动方案
在主应用注册好了微应用后,我们还需要对微应用进行一系列的配置。首先,我们在 Vue
的入口文件 main.js
中,导出 qiankun
主应用所需要的三个生命周期钩子函数。
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import store from './store'
import routes from "./router"
import './plugins/element.js'
Vue.config.productionTip = false
Vue.use(VueRouter);
if (window.__POWERED_BY_QIANKUN__) {
// 动态设置 webpack publicPath,防止资源加载出错
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
let instance = null;
let router = null;
/**
* 渲染函数
* 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
*/
function render() {
// 在 render 中创建 VueRouter,可以保证在卸载微应用时,移除 location 事件监听,防止事件污染
router = new VueRouter({
// 运行在主应用中时,添加路由命名空间 /vue
base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
mode: "history",
routes,
});
// 挂载应用
instance = new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
}
// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log("VueApp bootstraped");
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log("VueApp mount", props);
render(props);
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
console.log("VueApp unmount");
instance.$destroy();
instance = null;
router = null;
}
路由文件修改
vuedemo/src/router/index.js
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
export default routes
说明:
我们微应用的加载有两种方案:
- 自己启动微应用来访问这个系统
- 通过主应用来调用微应用。
为了区分这种方案,我们需要设置一些变量来控制
window.__POWERED_BY_QIANKUN__
用于判断window全局对象中是否有挂载一个属性,如果有这个属性说明我们是采用qiankun来加载这个微应用function render()
这个方法中就是在进行判断,那种场景运行项目。路径/vue
和/
代表不同访问方式bootstrap
,mount
,unmount
三个生命周期函数,代表主应用加载这个子应用的时候执行的流程
(6)微应用的配置
在配置好了入口文件 main.js
后,我们还需要配置 webpack
,使 main.js
导出的生命周期钩子函数可以被 qiankun
识别获取.
vue.config.js配置如下:
const path = require("path");
module.exports = {
devServer: {
// 监听端口
port: 8080,
// 关闭主机检查,使微应用可以被 fetch
disableHostCheck: true,
// 配置跨域请求头,解决开发环境的跨域问题
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
output: {
// 微应用的包名,这里与主应用中注册的微应用名称一致
library: "VueApp",
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: "umd",
// 按需加载相关,设置为 webpackJsonp_VueApp 即可
jsonpFunction: `webpackJsonp_VueApp`,
},
},
};
我们需要重点关注一下 output
选项,当我们把 libraryTarget
设置为 umd
后,我们的 library
就暴露为所有的模块定义下都可运行的方式了,主应用就可以获取到微应用的生命周期钩子函数了
在 vue.config.js
修改完成后,我们重新启动 Vue
微应用,然后打开主应用基座 http://localhost:8888
。我们点击左侧菜单切换到微应用,此时我们的 Vue
微应用被正确加载啦!
(6)异常处理
如果你遇到一下的问题
Uncaught Error: application ‘vueApp’ died in status NOT_MOUNTED: [qiankun]: Target container with #container not existed after vueApp mounted!
原因:
App.vue内的#container未能读取到
解决方案:
目录base/src/App.vue
去除外层div的id="app"整个标签即可
<div id="app"></div>
搭建React微应用
搭建React微应用我们选中React17(webpack4)+antd4+v6路由
(1)创建react应用
npx create-react-app reactdemo
(2)引入antd组件库
yarn add antd@4
(3)配置项目启动接口
根目录下添加 .env
文件,设置项目监听的端口,代码实现如下
PORT=8000
BROWSER=none
(4)设置路由和页面
下载路由默认是V6版本路由
yarn add react-router-dom
在App.jsx页面中
import { Menu } from 'antd';
import { MailOutlined, AppstoreOutlined } from '@ant-design/icons';
import List from './pages/List';
import Category from './pages/Category';
import { Link, BrowserRouter,Routes,Route } from "react-router-dom"
const App = () => (
<BrowserRouter>
<Menu mode="horizontal" defaultSelectedKeys={['list']}>
<Menu.Item key="list" icon={<MailOutlined />}>
<Link to="/list">商品列表</Link>
</Menu.Item>
<Menu.Item key="category" icon={<AppstoreOutlined />}>
<Link to="/category">商品分类</Link>
</Menu.Item>
</Menu>
<Routes>
<Route path='/' element={<List />}></Route>
<Route path='/category' element={<Category />}></Route>
</Routes>
</BrowserRouter>
);
export default App;
单独启动项目访问:点击菜单可以实现切换
(6)注册微应用
base/src/qiankun/index.js
配置文件中注册微应用
const apps = [
/**
* name: 微应用名称 - 具有唯一性
* entry: 微应用入口 - 通过该地址加载微应用
* container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
* activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
*/
{
name: "VueApp",
entry: "//localhost:8080",
container: "#container",
activeRule: "/vue",
},
{
name: "ReactApp",
entry: "//localhost:8000",
container: "#container",
activeRule: "/react",
}
];
之前注册的vue微应用正常配置。
reactdemo/src/main.js
配置文件中设置如下代码
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(
// <App />
// );
if (window.__POWERED_BY_QIANKUN__) {
// 动态设置 webpack publicPath,防止资源加载出错
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
/**
* 渲染函数
* 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
*/
function render() {
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
}
// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log("ReactApp bootstraped");
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log("ReactApp mount", props);
render(props);
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
console.log("ReactApp unmount");
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}
跟Vue是一样的效果,我们需要判断项目启动是独立运行还是基于qiankun来进行调用
App.jsx
中配置路由命名空间
import { Menu } from 'antd';
import { MailOutlined, AppstoreOutlined } from '@ant-design/icons';
import List from './pages/List';
import Category from './pages/Category';
import { Link, BrowserRouter,Routes,Route } from "react-router-dom"
//引入路由的命名空间
const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
const App = () => (
<BrowserRouter basename={BASE_NAME}>
<Routes>
<Route path='/' element={<List />}></Route>
<Route path='/category' element={<Category />}></Route>
</Routes>
</BrowserRouter>
);
export default App;
(7)配置webpack
默认react项目打包后,不是采用umd格式暴露,那qiankun加载过程中找不到这个包的,
所有我们需要在配置react项目的webpack,配置 webpack
,使 index.js
导出的生命周期钩子函数可以被 qiankun
识别获取。
react中要修改webpack的配置,我们可以借助于第三方的一些工具
需要借助 react-app-rewired
来帮助我们修改 webpack
的配置
npm install react-app-rewired -D
yarn add react-app-rewired -d
在 react-app-rewired
安装完成后,我们还需要修改 package.json
的 scripts
选项,修改为由 react-app-rewired
启动应用
目录:reactdemo/package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
在reactdemod
项目下面创建一个config-overrides.js
,一定要注意,这个文件不要放在src目录中去了。
const path = require("path");
module.exports = {
webpack: (config) => {
// 微应用的包名,这里与主应用中注册的微应用名称一致
config.output.library = `ReactApp`;
// 将你的 library 暴露为所有的模块定义下都可运行的方式
config.output.libraryTarget = "umd";
// 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
config.output.jsonpFunction = `webpackJsonp_ReactApp`;
config.resolve.alias = {
...config.resolve.alias,
"@": path.resolve(__dirname, "src"),
};
return config;
},
devServer: function (configFunction) {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
// 关闭主机检查,使微应用可以被 fetch
config.disableHostCheck = true;
// 配置跨域请求头,解决开发环境的跨域问题
config.headers = {
"Access-Control-Allow-Origin": "*",
};
// 配置 history 模式
config.historyApiFallback = true;
return config;
};
}
};
(8)启动项目运行
单独启动reactdemo项目,看是否能正常运行。
在启动base基座项目。运行看效果
能看到这个页面说明访问成功。
(9)异常处理
如果你的react-scripts
版本为4,那无法支持craco来启动项目。默认我们采用react-app-rewired
如果你的React
版本为18,默认采用webpack5来打包,上述的webpack配置就失效无法使用。
搭建H5项目
基于H5技术来搭建的项目,跟框架无关。
纯HTML+CSS+JS+jQuery项目也可以接入到qiankun中。
搭建Vue3+Vite微应用
vite+vue3的项目默认在qiankun里面不支持,主要是不支持vite
因为目前qiankun的所有配置都是webpack的配置。我们需要安装对应的插件
(1)创建项目
npm create vite@latest
创建项目,选项Vue版本。
(2)配置路由
npm i vue-router@next
关于项目中路由的搭建这里就不一步步搭建了。
在路由中需要设置一个basename名字,让qiankun来进行路由通信
const router = createRouter({
routes,
// 路由模式
history: createWebHistory("/vue3")
})
export default router;
其中createWebHistory
中写的/vue3
代表当前这个子应用的路由运行之前,基础地址就是/vue3
(3)下载插件
vite-plugin-qiankun
:这个插件可以帮助我们将vite接入到qiankun
npm install vite-plugin-qiankun
(4)vite.config.js配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from "vite-plugin-qiankun"
export default defineConfig({
plugins: [
vue(),
qiankun('Vue3App', {
useDevMode: true
})
],
server:{
host: '127.0.0.1',
port: 8088,
headers: {
'Access-Control-Allow-Origin': '*'
}
}
})
核心的配置包含两块:
- 引入qiankun并配置到plugins里面。
Vue3App
这个名字就是在base基座中配置的名字 - server配置中host代表主机地址,port配置端口号,headers配置允许跨域
(5)在main.ts文件中
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
import {
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
let app: any;
function render(props: any) {
const { container, parentActions } = props;
app = createApp(App);
app.use(router).mount(container instanceof Element
? (container.querySelector("#app"))
: (container)
);
}
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render({ container: "#app" });
} else {
renderWithQiankun({
mount(props) {
// 每次挂载都重新执行render方法
render(props)
},
bootstrap() {
console.log("Vue3App正在加载");
},
update() {
console.log("Vue3App正在更新");
},
unmount() {
console.log("Vue3App正在卸载");
// 卸载时同时卸载子应用实例
app.unmount();
}
});
}
// const app = createApp(App)
// app.use(router);
// app.mount("#app")
引入qiankunWindow和renderWithQiankun
重写render方法
单独启动项目保证没有问题。
基于qiankun来加载项目也没有问题
(6)主应用中配置vue3
在主应用中添加一个新的菜单。
在主应用的app模块中,我们注册Vue3项目
base/src/qiankun/index.js
中配置如下
// 此时我们还没有微应用,所以 apps 为空
const apps = [
/**
* name: 微应用名称 - 具有唯一性
* entry: 微应用入口 - 通过该地址加载微应用
* container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
* activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
*/
{
name: "VueApp", // name不是随便写的,在子应用中定义好
entry: "//localhost:8080", // 子应用部署的地址
container: "#container", // 子应用加载容器,在主应用中定义的id = container,
activeRule: "/vue", // 触发子应用的规则
},
{
name: "ReactApp",
entry: "//localhost:8001",
container: "#container",
activeRule: "/react",
},
{
name: "Vue3App",
entry: "//localhost:8088",
container: "#container",
activeRule: "/vue3",
},
{
name: "UmiApp",
entry: "//localhost:8800",
container: "#container",
activeRule: "/umi",
},
];
// 一个进度条插件
// import NProgress from "nprogress";
// import "nprogress/nprogress.css";
import { message } from "element-ui";
import {
registerMicroApps,
addGlobalUncaughtErrorHandler,
start,
} from "qiankun";
/**
* 注册微应用
* 第一个参数 - 微应用的注册信息
* 第二个参数 - 全局生命周期钩子
*/
registerMicroApps(apps, {
// qiankun 生命周期钩子 - 微应用加载前
beforeLoad: (app) => {
// 加载微应用前,加载进度条
// NProgress.start();
console.log("before load", app.name);
return Promise.resolve();
},
// qiankun 生命周期钩子 - 微应用挂载后
afterMount: (app) => {
// 加载微应用前,进度条加载完成
// NProgress.done();
console.log("after mount", app.name);
return Promise.resolve();
},
});
/**
* 添加全局的未捕获异常处理器
*/
addGlobalUncaughtErrorHandler((event) => {
console.error(event);
const { message: msg } = event;
// 加载失败时提示
if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
message.error("微应用加载失败,请检查应用是否可运行");
}
});
// 导出 qiankun 的启动函数
export default start;
(7)启动项目看效果
在基座项目中访问/vue3模块。
搭建umijs微应用
搭建umi项目,umi相对来说比较简单一点,因为umi已经封装了qiankun模块
(1) 创建项目
yarn create @umijs/umi-app
(2)修改项目的启动端口
我们可以在umi项目下面添加一个.env 文件
主要是用于设置项目的端口
PORT=8800
BABEL_CACHE=none
(3)下载qiankun插件
接下来我们需要在项目中下载qiankun插件,并引入到我们的项目中
yarn add @umijs/plugin-qiankun -D
配置 qiankun
开启。
在umirc.ts文件中配置qiankun
import { defineConfig } from 'umi';
export default defineConfig({
....其他配置
qiankun: {
slave: {},
},
});
(4)构建umi项目子应用的路由
export default defineConfig({
...其他配置
routes: [
{ path: '/umi', component: '@/pages/index' },
]
});
/umi默认访问 index.tsx这个组件。
因为在主应用中我们要通过http://127.0.0.1:8800/umi来访问umi这个子应用。
所以默认在umi中设置访问的路径为/umi
(5)配置项目打包后名字
在umijs这个项目中,我们找到package.json文件
{
"private": true,
"name":"UmiApp",
}
一定要加上name属性,因为打包的时候我们要给项目命名。这样主应用中才能找到这个打包后项目
(6)配置运行时生命周期钩子
插件会自动为你创建好 qiankun 子应用需要的生命周期钩子,但是如果你想在生命周期期间加一些自定义逻辑,可以在子应用的 src/app.ts
里导出 qiankun
对象,并实现每一个生命周期钩子,其中钩子函数的入参 props
由主应用自动注入
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
console.log('UmiApp bootstrap', props);
},
// 应用 render 之前触发
async mount(props) {
console.log('UmiApp mount', props);
},
// 应用卸载之后触发
async unmount(props) {
console.log('UmiApp unmount', props);
},
};
配置了这个钩子函数后,我们项目被加载的时候,可以默认执行这三个钩子函数
(7)主应用中配置umi子应用
子应用配置完成后,启动测试没有问题。
接下来我们进入主应用base
.开始接入子应用
在base/qiankun/index.js
中加入下面的配置
const apps = [
/**
* name: 微应用名称 - 具有唯一性
* entry: 微应用入口 - 通过该地址加载微应用
* container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
* activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
*/
...其他的子应用
{
name: "UmiApp",
entry: "//localhost:8800",
container: "#container",
activeRule: "/umi",
},
];
UmiApp:这个名字一定要和umi项目中package.json中配置name属性一致
activeRule:这个路径就是默认主应用要寻找的子应用路径。
效果如下:
五、服务通信
通信基本流程图
涉及到父子应用、兄弟应用之间的参数传递。
兄弟之间的参数传递,也是通过父应用来完成的,类似于框架里面组件值的传递,流程都是一样的。
(1)父应用中的代码
在父应用中找到Home组件
<template>
<div class="home">
主应用(主页)
<h2 class="testText">测试文本</h2>
</div>
</template>
<script>
import { initGlobalState } from 'qiankun';
// 初始化 state
const actions = initGlobalState({ id: 100 });
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
})
export default {
name: 'Home'
}
</script>
<style scoped>
.testText {
color: red;
}
</style>
initGlobalState可以初始化父应用中的数据,这个数据是全局数据,所有子应用都可以获取到。
onGlobalStateChange:用于检测数据是否发生变化
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
},true)
如果设置为true,代表立即监听,可以马上输出结果,当子应用修改了state数据后,父应用也能执行console.log
主应用:
子应用:
(2)在vue-demo子应用中
找到vue-demo子应用,在src目录下面创建qiankun/actions.js文件
class Actions {
// 默认值为空 Action
actions = {
onGlobalStateChange: null,
setGlobalState: null,
};
/**
* 设置 actions
*/
setActions(actions) {
this.actions = actions;
}
/**
* 映射
*/
onGlobalStateChange() {
return this.actions.onGlobalStateChange(...arguments);
}
/**
* 映射
*/
setGlobalState() {
return this.actions.setGlobalState(...arguments);
}
}
const actions = new Actions();
export default actions;
创建一个actions对象,并暴露出去,这个目的是子应用可以将接受到的onGlobalStateChange和setGlobalState保存起来,并在任何一个组件中触发使用。
在main.js文件中
import actions from "./qiankun/actions"
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log("VueApp mount", props);
actions.setActions(props)
// props.setGlobalState({id:1999});
render(props);
}
mount表表子应用挂载,接受到的props对象中包含了onGlobalStateChange,setGlobalState这个两个函数,我们要将这两个函数保存起来。并在组件中使用。
actions.setActions(props)
保存props对象到actions中
在组件中使用
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<p>父组件传递过来的值:{{msg}}</p>
<button @click="checkParent">点击操作父应用</button>
</div>
</template>
<script>
import actions from '../qiankun/actions';
export default {
name: 'Home',
data(){
return{
msg:""
}
},
mounted() {
actions.onGlobalStateChange((state) => {
console.log("vuedemo-state",state);
this.msg = state.id
}, true);
},
methods: {
checkParent(){
actions.setGlobalState({id:1999})
}
},
}
</script>
通过引入actions对象,并调用onGlobalStateChange可以获取到父应用传递过来onGlobalStateChange,并获取数据。
如果要进行数据修改,调用actions.setGlobalState({id:1999})
来进行修改
六、样式隔离
在qiankun中我们涉及到父应用和子应用。
思考两个问题:
- 父应用的样式会不会影响子应用。
- 子应用的样式会不会相互影响
如果会出现以上的两种情况,那我们在代码设计的时候要解决,不然架构设计出来后,开发的业务一定会有样式的重叠,以后定位并且修改是很复杂的一件事,除非严格约束变量的命名。
接下来就实验一下样式会不会相互冲突影响
(1)父子应用的影响
在父应用中base/App.vue设计一个css样式
<template>
<span class="testText">蜗牛学苑</span>
</template>
<style>
.testText{
color:red
}
</style>
在子应用中vuedemo\App.vue中
我们新增一个h2标签,新增一个class=“testText”属性
<template>
<div id="app">
<h2 class="testText">测试文本</h2>
</div>
</template>
//没有scoped
<style>
</style>
在子应用中reactdemo\App.jsx中
新增一个文本标签,并给标签新增一个className=“testText”
import React, { useState } from 'react'
export default function App() {
const [current, setCurrent] = useState('list');
const onClick = (e) => {
console.log('click ', e);
setCurrent(e.key);
};
return (
<BrowserRouter basename={BASE_NAME}>
<div>
<h3 className="testText">React测试文本</h3>
</div>
</BrowserRouter>
)
}
效果如下:
路由切换为:http://localhost:8888/vue
默认访问vue子应用,我们能同时看到父应用中的文本样式和子应用的文本样式
通过上图,可以发现文本都是红色,说明父应用样式影响了子应用。
在qiankun中加载子应用实际就是将你的整个vue子应用项目打包后的html文档加载到一起最终渲染在一起。所以样式肯定会受到影响。
(2)子应用的相互影响
如果vuedemo子应用中设计一个样式,这个样式会不会影响reactdemo这个子应用呢。
在子应用vuedemo\App.vue文件中
添加了一个h3的标签,并且class="application"
<template>
<div id="app">
<h2 class="testText">测试文本</h2>
<h3 class="application">vuedemo-子应用</h3>
</div>
</template>
<style>
.application{
color: tomato;
}
</style>
在子应用reactdemo\App.jsx文件中
也给h2标签添加className="application" 属性,但是并没有设置对应的css样式
import React, { useState,useEffect } from 'react'
export default function List() {
return (
<div>
<h2 className="testText">测试文本</h2>
<h2 className='application'>Reactdemo子应用</h2>
</div>
)
}
效果如下:
结论:
子应用之间默认情况下,并不会相互影响。
原因:因为qiankun内部已经默认开启了样式隔离,在qiankun的文档中有一下的这样一段配置
sandbox - boolean | { strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean } - 可选,是否开启沙箱,默认为 true
设置sandbox的值目的是为了开启沙箱,可以隔离各个子应用之间的样式影响。
默认情况下为true,如果我们设置为false,看看情况
(3)关闭沙箱环境
在父级应用base\main.js中
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import startQiankun from "./qiankun/index";
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
//设置sandbox为false
startQiankun({prefetch:true,sandbox:false})
startQiankun这个函数启动的时候,可以传递参数进去。sandbox就是设置环境是否开启。
这个时候,你会发现ReactDemo子应用的文本变成了tomato
这个颜色,说明vuedemo的样式影响了reactdemo中的文本。
开启了沙箱隔离后,默认情况下就可以解决这个问题。
但是父应用的文本样式还是影响了子应用。默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。
(4)scoped css解决样式隔离
在vue开发的时候,我们为了不让样式穿透,采用了scoped来设计样式
同样的,在react开发的时候,我们采用了css modules来控制样式的作用访问。
这种思路同样可以应用在qiankun架构中,scoped css是vue-loader实现的样式隔离方案,如果采用了这个来修饰样式,他会给标签动态新增data-v的属性,这个属性有唯一性,可以将样式控制在当前这个模块中。
在父应用base\App.vue 中
<style lang="scss" scoped>
.testText{
color: red;
}
.el-header {
background-color: #B3C0D1;
color: #333;
line-height: 60px;
}
<style>
编译后的代码:默认新增了唯一标志符data-v属性
此刻子组件的样式效果
你可以看到vuedemo子应用中,这个样式已经不受影响了。说明我们隔离已经成功。
并且如果你在子应用中也这样设计样式,子应用相互之间也不会受到影响。
(5)沙箱和scoped对比
既然我们可以直接用scoped来实现子组件样式的隔离,那为什么qiankun还会有沙箱环境呢。
在微前端架构中,子应用可以采用各种的技术栈
vue全家桶默认提供了scoped来处理、react也提供的css modules来隔离样式。那如果是jquery、h5这种类型的项目,我们没有提供scoped的情况,就需要用到qiankun默认沙箱来进行隔离。
一般情况下,优先选择用scoped来进行样式隔离。
沙箱实例:
七、子应用之间跳转
八、公共依赖加载
原文链接:https://juejin.cn/post/7262982840018665532,如有侵权,请联系删除,感谢!