一、什么是微前端
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
微前端架构具备以下几个核心价值:
- 技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权
- 独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
- 增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
- 独立运行时
每个微应用之间状态隔离,运行时状态不共享
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。
二、qiankun
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台,在经过一批线上应用的充分检验及打磨后,我们将其微前端内核抽取出来并开源,希望能同时帮助社区有类似需求的系统更方便的构建自己的微前端系统,同时也希望通过社区的帮助将 qiankun 打磨的更加成熟完善。
目前 qiankun 已在蚂蚁内部服务了超过 2000+ 线上应用,在易用性及完备性上,绝对是值得信赖的。
三、qiankun 的核心设计理念
🥄 简单
由于主应用微应用都能做到技术栈无关,qiankun 对于用户而言只是一个类似 jQuery 的库,你需要调用几个 qiankun 的 API 即可完成应用的微前端改造。同时由于 qiankun 的 HTML entry 及沙箱的设计,使得微应用的接入像使用 iframe 一样简单。
🍡 解耦/技术栈无关
微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,而 qiankun 的诸多设计均是秉持这一原则,如 HTML entry、沙箱、应用间通信等。这样才能确保微应用真正具备 独立开发、独立运行 的能力。
四、qiankun特性
📦 基于 single-spa 封装,提供了更加开箱即用的 API。
📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
🛡 样式隔离,确保微应用之间样式互相不干扰。
🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
五、创建两个项目
- qiankun-base 基座
- qiankun-vue 子应用
基座基础配置
App.vue中建立一个element-ui的菜单用来跳转:
<template>
<div>
<el-menu router mode="horizontal">
<!-- 基座中可以放自己的路由 -->
<el-menu-item index="/">Home</el-menu-item>
<!-- 引用其他子应用 -->
<el-menu-item index="/vue">vue应用</el-menu-item>
</el-menu>
<router-view></router-view>
<!-- 挂载vue应用的地方 -->
<div id="vue"></div>
</div>
</template>
安装qiankun
npm i qiankun -S
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'
// 定义一个列表 写入要注册的app
const apps = [{
name: 'vueApp', //微应用名称,在微应用的打包配置文件中library的名称,微应用之间必须确保唯一
entry: 'http://localhost:8081', //微应用地址,子应用必须支持跨域 fetch
container: '#vue', //微应用挂载的容器节点
activeRule: '/vue', //微应用的激活规则:访问到/vue的时候跳转子应用
props:{token:'gaiery-token-xxxx'} //主应用需要传递给微应用的数据
}]
// 注册app
registerMicroApps(apps)
// 开启
start()
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
router/index.js:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
子应用基础配置
因为在基座的main.js中配置了访问到/vue的时候就挂载子应用,所以就要修改子应用路由的根路径 router/index.js:
const router = new VueRouter({
mode: 'history',
base: '/vue',
routes
})
然后我们要把子应用打包成一个类库,在main.js中配置:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
//定义一个变量接收vue的实例
let instance = null
function render() {
// 这里是挂载到自己的html上
// 基座会拿到挂载后的html 插入到#vue这个容器中
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
}
//如果是基座访问子应用 (qiankun访问子应用)
if (window.__POWERED_BY_QIANKUN__) {
//就动态配置子应用的自身的根路径 ( 避免使用到基座的根路径 )
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
//如果子应用是独立运行的话 就直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 子组件的协议(固定) 导出三个promise函数
export async function bootstrap(props) {
//应用初始化的时候调用
}
export async function mount(props) {
//每次进入子应用的时候调用
//子应用渲染的时候挂载 也可以把props传入子应用进行通讯
render(props)
}
export async function unmount(props) {
//切出 / 卸载的时候会调用
// 子应用卸载的时候用destory把实例卸载了
// instance.$destory()
}
配置完成后需要打包成类库,所以还得配置一下打包vue.config.js,因为子应用嵌套在父应用下,所以还需要配置跨域,还要配置打包后的结果:
module.exports = {
devServer: {
port: 8081, // 配置端口号
headers: {
// 配置跨域 表示所有的人都可以访问我
'Access-Control-Allow-Origin': '*'
}
},
configureWebpack: {
output: {
library: "vueApp",
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
}
}
基本的接入已经完成
注意:基座和子应用的router mode相同,都为“history”
const router = new VueRouter({
mode: 'history',
routes
})
六、主微应用之间简易传递数据方法
使用props传递数据
在主应用的main.js的registerMicroApps(apps)中 props:{token:'gaiery-token-xxxx'} //主应用需要传递给微应用的数据
在微应用的main.js的mount方法中
export async function mount(props){
console.log(props) //props中携带主应用传递过来的token:gaiery-token-xxxx
render(props) //渲染
}
七、基于vue3+cli+webpack5+qiankun实现微前端
主应用改造(又称基座改造)
1、在主应用中安装qiankun:
npm i qiankun -S
{
"name": "qiankun_parent",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"qiankun": "^2.10.16",
"vue": "^3.2.13",
"vue-router": "^4.4.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}
2、改造vue.config.js,允许跨域访问子应用页面
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port: 8000, // 基座所在端口
headers: {
// 重点1: 允许跨域访问子应用页面
'Access-Control-Allow-Origin': '*',
},
},
})
3、改造main.js
import { createApp } from "vue";
import router from "@/router";
import App from "./App.vue";
import { registerMicroApps, start } from "qiankun";
const app = createApp(App);
app.use(router);
app.mount("#app-base"); // 此处为/public/index.html中的div id
// 注册子应用,一个对象就是一个子应用的配置
registerMicroApps(
[
{
name: "vueChildOne",
entry: "//localhost:3001", // 此处为子应用的访问地址
container: "#child-vue3-one-content", // 此处为基座中加载子应用的容器id
activeRule: "/vue/map", // 此处为子应用的访问路由
},
],
{
//还有一些生命周期 如果需要可以根据官网文档按需加入
beforeMount(app) {
console.log("挂载前", app);
},
afterMount(app) {
console.log("卸载后", app);
},
}
);
// 启动 qiankin
console.log("hello 主应用");
// 启动子应用
start({
prefetch: false, //取消预加载
sandbox: { experimentalStyleIsolation: true }, //沙盒模式
});
注意:
{
name: "vueChildOne",
entry: "//localhost:3001", // 此处为子应用的访问地址
container: "#child-vue3-one-content", // 此处为基座中加载子应用的容器id
activeRule: "/vue/map", // 此处为子应用的访问路由
}
// entry+activeRule就能访问子应用
原理:当访问基座入口+ activeRule时会在基座的container中加载对应子应用(子应用entry+activeRule)。
4、在App.vue中写响应跳转子应用(根据自己的项目找对应位置写,不局限于App.vue)
<template>
<router-view/>
<div id="child-vue3-one-content"></div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: left;
color: #2c3e50;
margin-top: 60px;
}
#child-vue3-one-content{
height: 300px;
}
</style>
注意:main.js中的container要和此处的"child-vue3-one-content"对应。
子应用改造
1、在src下新建public-path.js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
如果报错,这是 eslint 的问题, webpack_public_path 不是全局变量所导致的。
解决方式:
在子应用 package.json文件中eslintConfig配置全局变量后重起服务。
"eslintConfig": {
...,
"globals": {
"__webpack_public_path__": true
}
}
2、改造main.js
import { createApp } from "vue";
import router from "@/router";
import "./public-path.js"; // 引入public-path.js
import App from "./App.vue";
// const app = createApp(App);
// app.use(router);
// app.mount('#app');
let instance = null;
function render(props = {}) {
if (instance) return;
const { container } = props;
console.log(container);
instance = createApp(App)
.use(router)
.mount(
container ? container.querySelector("#app-child-one") : "#app-child-one"
);
}
// 此处"app-child-one"为子应用/public/index.html中的div id
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
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 = null;
}
3、改造vue.config.js
const { defineConfig } = require("@vue/cli-service");
// 获取 package.json 的 name,需要保持一致
const { name } = require("./package.json");
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port: 3001, // 子应用所在端口
headers: {
"Access-Control-Allow-Origin": "*", //开发时增加跨域,表示所有人都可以访问我的服务器
},
},
configureWebpack: {
output: {
// 必须打包出一个库文件
library: `${name}-[name]`,
// 库格式必须是 umd
libraryTarget: "umd", // 把子应用打包成 umd 库格式
// jsonpFunction: `webpackJsonp_${name}`,
chunkLoadingGlobal: `webpackJsonp_${name}`,
},
},
});
4、/router/index.js
import { createRouter, createWebHistory } from "vue-router";
const routes = [
{
path: "/vue",
component: () => import("../HomeComp.vue"),
},
{
path: "/vue/map",
component: () => import("../MapComp.vue"),
},
{
path: "/vue/login",
component: () => import("../LoginComp.vue"),
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
此处的路由:/vue/map和基座main.js中的activeRule对应。
/src/MapComp.vue
<template>
<div>Child Map</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
5、访问效果: