前端">一. 什么是微前端
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
微前端架构具备以下几个核心价值:
-
技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权 -
独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 -
增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
-
独立运行时
每个微应用之间状态隔离,运行时状态不共享
二. qiankun框架
文档地址 https://qiankun.umijs.org/zh
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统 。
qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台,在经过一批线上应用的充分检验及打磨后,我们将其微前端内核抽取出来并开源,希望能同时帮助社区有类似需求的系统更方便的构建自己的微前端系统,同时也希望通过社区的帮助将 qiankun 打磨的更加成熟完善。
特性
- 📦 基于 single-spa 封装,提供了更加开箱即用的 API。
- 📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
- 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
- 🛡 样式隔离,确保微应用之间样式互相不干扰。
- 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
- ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
- 🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
三. qiankun框架实例
准备建立三个项目实际操作,主要用的vue和react技术栈(不限技术栈),qiankun-base充当主应用(主应用为vue),qiankun-vue 和 qiankun-react 充当子应用。
1. 创建应用
1.创建基座
vue create qiankun-base
2.创建子应用1
vue create qiankun-vue
3.创建子应用2
create-react-app qiankun-react
2.项目配置
1、基座qiankun-base配置
项目创建好后我们首先进行主应用qiankun-base的配置,进入man.js文件进行配置,在main.js中加入以下代码,要注意的是,
entry 是我们两个子项目的域名和端口,我们必须确保两个子项目运行在这两个端口上面,container 就是我们的容器名,就是我们子应用挂载的节点,相当于Vue项目里面的app节点,activeRule 就是我们的激活路径,根据路径来显示不同的子应用
(1) 引入qiankun插件
npm i qiankun -S
(2) main.js配置
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.config.productionTip = false
Vue.use(ElementUI);
// 引入qiankun
import { registerMicroApps, start } from 'qiankun';
const apps = [
{
name: 'vueApp', // 应用的名字
entry: '//localhost:8081', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
container: '#vue', // 容器名(此项目页面中定义的容器id,用于把对应的子应用放到此容器中)
activeRule: '/vue', // 激活的路径
props: { a: 1 } // 传递的值(可选)
},
{
name: 'reactApp',
entry: '//localhost:8082', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
container: '#react',
activeRule: '/react',
}
]
registerMicroApps(apps); // 注册应用
start({
prefetch: false // 取消预加载
});// 开启
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
(3) 配置完之后,写一个导航栏引用其他子应用到基座中,这里我直接写在App.vue文件中
<template>
<div id="app">
<el-menu :router="true" mode="horizontal">
<!--基座中可以放自己的路由-->
<el-menu-item index="/">Home</el-menu-item>
<el-menu-item index="/about">About</el-menu-item>
<!--引用其他子应用-->
<el-menu-item index="/vue">vue应用</el-menu-item>
<el-menu-item index="/react">react应用</el-menu-item>
</el-menu>
<router-view></router-view>
<div id="vue"></div>
<div id="react"></div>
</div>
</template>
<style lang="less">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>
2、子应用qiankun-vue配置
(1) . main.js配置
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';
Vue.config.productionTip = false;
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/vue/' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
if (window.__POWERED_BY_QIANKUN__) { // 动态添加publicPath
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) { // 默认独立运行
render();
}
// 父应用加载子应用,子应用必须暴露三个接口:bootstrap、mount、unmount
// 子组件的协议就ok了
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
console.log('[vue] props from main framework', props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
(2). router.js的配置
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
***ponent: HomeView
},
{
path: '/about',
name: 'about',
***ponent: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",//基础路径
routes
})
export default routes
(3). vue.config.js的配置
const { name } = require('./package');
module.exports = {
lintOnSave: false,
devServer: {
port: 8081,
headers: {
'A***ess-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
//jsonpFunction: `webpackJsonp_${name}`,// 取决于webpack的版本(可不要,但小于webpack5.x则需要加上)
},
}
};
3、子应用qiankun-react配置
(1) .在 src 目录下新建 public-path.js 配置资源公共路径,在 index.js 中导入(否则按需加载或外部加载的资源在主应用中加载时,会以主应用为基础路径)
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
/**
* 坑
* 直接放index.js import 下面:微应用中静态资源(图片)不显示
* 直接放index.js 最上方 运行报错
*/
坑点
- webpack_public_path 不添加 eslint 全局注释,会报错
- 将代码直接放在 index.js 中,无论在哪个位置都不生效
(2). 加载react-app-rewired
npm i react-app-rewired -D
项目根目录新增config-overrides.js
module.exports = {
webpack: (config) => {
config.output.library = `reactApp`;
config.output.libraryTarget = "umd";
// config.jsonpFunction = `webpackJsonp_reactApp1`;
config.output.globalObject = "window";
return config
},
devServer: (_) => {
const config = _;
config.headers = {
"A***ess-Control-Allow-Origin": "*",
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
return config;
}
}
(3).将子应用src里面更改为index.js更改为
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import './public-path'
import reportWebVitals from './reportWebVitals';
import { BrowserRouter as Router } from 'react-router-dom';
import App from './App';
let root = null;
function render(props) {
root = ReactDOM.createRoot(props.container ? props.container.querySelector('#root') : document.getElementById('root'));
root.render(
<React.StrictMode>
<Router basename={window.__POWERED_BY_QIANKUN__ ? '/react' : '/'}>
<App />
</Router>
</React.StrictMode>
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('[react18] react app bootstraped');
}
export async function mount(props) {
console.log('[react18] props from main framework', props);
render(props);
}
export async function unmount(props) {
//const {container} = props;
root.unmount()
//ReactDOM.unmount***ponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
reportWebVitals();
坑点:
1. react18.0以上版本不在支持 ReactDOM.render()
2. createRoot()方法创建的react组件不能被ReactDOM.unmount***ponentAtNode()方法直接卸载
看下改造效果
四. 主应用与子应用之间通信
这里使用qiankun提供的api- initGlobalState 进行通信
1. 主应用:
在src目录下创建 action.js 文件
// 此action文件为定义微应用之间全局状态
// 引入qiankun的应用间通信方法initGlobalState
import { initGlobalState } from 'qiankun'
const initialState = {
// 这里可以写初始化数据
name:'123456',
msg:'base'
}
const actions = initGlobalState(initialState) //初始化state
// 监听actions全局公共状态数据的变化
actions.onGlobalStateChange((state, prevState) => {
console.log("主应用变更前:", prevState);
console.log("主应用变更后:", state);
//this.$store.***mit("setState", state) //将获取的最新的公共状态保存到vuex中
})
//actions.setGlobalState(initialState);
//actions.offGlobalStateChange();
export default actions
在main.js文件中引入action.js ,props中传入 actions
import store from './store'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import actions from './action.js'
Vue.config.productionTip = false
Vue.use(ElementUI);
// 引入qiankun
import { registerMicroApps, start } from 'qiankun';
const apps = [
{
name: 'vueApp', // 应用的名字
entry: '//localhost:8081', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
container: '#vue', // 容器名(此项目页面中定义的容器id,用于把对应的子应用放到此容器中)
activeRule: '/vue', // 激活的路径
props: { actions } // 传递的值(可选)
},
{
name: 'reactApp',
entry: '//localhost:3000', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
container: '#react',
activeRule: '/react',
props: { actions} // 传递的值(可选)
}
]
registerMicroApps(apps); // 注册应用
start({
prefetch: false // 取消预加载
});// 开启
找一个vue页面测试值得变化
<template>
<div class="about">
<h1>This is an about page</h1>
<button @click="handle1">点击向子应用vue发送消息</button>
<button @click="handle2">点击向子应用react发送消息</button>
</div>
</template>
<script>
import actions from '../action.js'
export default{
name:'AboutView',
data() {
return {
mes1: {
name: "vue-1",
},
mes2: {
name: "react-2",
},
};
},
methods: {
handle1() {
actions.setGlobalState(this.mes1); //修改全局的actions
this.$router.push("/vue"); //跳转到vue子应用中
},
handle2() {
actions.setGlobalState(this.mes2); //修改全局的actions
this.$router.push("/react"); //跳转到vue子应用中
},
},
}
</script>
2.子应用:
在src目录下新建actions.js文件
function emptyAction() {
console.warn("Current execute action is empty!");
}
class Actions {
actions = {
onGlobalStateChange: emptyAction,
setGlobalState: emptyAction
}
constructor() {}
setActions(actions) {
this.actions = actions
}
onGlobalStateChange(...args) {
return this.actions.onGlobalStateChange(...args)
}
setGlobalState(...args) {
return this.actions.setGlobalState(...args)
}
}
const actions = new Actions()
export default actions
在main.js文件中引入action.js ,并且在render方法中加入判断
if(props){
actions.setActions(props)
}
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';
import actions from './actions.js'
Vue.config.productionTip = false;
let router = null;
let instance = null;
function render(props = {}) {
if(props){
actions.setActions(props)
}
const { container } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/vue/' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
if (window.__POWERED_BY_QIANKUN__) { // 动态添加publicPath
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) { // 默认独立运行
render();
}
// 父应用加载子应用,子应用必须暴露三个接口:bootstrap、mount、unmount
// 子组件的协议就ok了
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
console.log('[vue] props from main framework', props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
这里也找个vue页面向主应用传值测试
<template>
<div class="about">
<h1>This is an about page</h1>
<button @click="handle">点击向主应用base发送消息</button>
</div>
</template>
<script>
import actions from "../actions"
export default {
name: 'AboutView',
mounted() {
actions.onGlobalStateChange((state) => {
console.log("我是子应用,我检测到数据了:", state);
}, true); //onGlobalStateChange的第二个参数设置为true,则会立即触发一次观察者函数
},
methods:{
handle(){
actions.setGlobalState({ name: '1234' }); //修改全局的actions
}
}
}
</script>
控制台打印