目录
什么是微前端
目前现有的微前端
好处
使用
子应用的页面在主应用里显示
什么是微前端
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
我的理解就是将一个大型的前端应用拆分成多个模块,每个微前端模块可以由不同的团队进行管理,并可以自主选择框架,并且有自己的仓库,可以独立部署上线。
目前现有的微前端方案
iframe
通过iframe实现的话就是每个子应用通过iframe标签来嵌入到父应用中,iframe具有天然的隔离属性,各个子应用之间以及子应用和父应用之间都可以做到互不影响。
但是iframe也有很多缺点:
- url不同步,如果刷新页面,iframe中的页面的路由会丢失。
- 全局上下文完全隔离,内存变量不共享。
- UI不同步,比如iframe中的页面如果有带遮罩层的弹窗组件,则遮罩就不能覆盖整个浏览器,只能在iframe中生效。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程
single-spa
single-spa是最早的微前端框架,可以兼容很多技术栈。
single-spa首先在基座中注册所有子应用的路由,当URL改变时就会去进行匹配,匹配到哪个子应用就会去加载对应的那个子应用。
相对于iframe的实现方案,single-spa中基座和各个子应用之间共享着一个全局上下文,并且不存在URL不同步和UI不同步的情况,但是single-spa也有以下的缺点:
- 没有实现js隔离和css隔离
- 需要修改大量的配置,包括基座和子应用的,不能开箱即用
qiankun
基于single-spa二次开发,封装了开箱即用的api
资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
实现了样式隔离
基于qiankun的微前端实战
准备两个空项目
- qiankun-base 主应用
- qiankun-child vue 子应用
创建基座项目qiankun-base和qiankun-child-vue
创建一个vue3+vite+tsx项目详情见 创建一个vue3+vite+ts项目
vue子应用 qiankun-child-vue
修改.env
VITE_APP_NAME = qiankun-child-vue
修改根节点挂载id
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>qiankun-child-vue</title>
</head>
<body>
<div id="qiankun-child-vue"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
main.ts
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import router from "./router";
import antv from "ant-design-vue";
const app = createApp(App);
app.use(router).use(antv).mount("#qiankun-child-vue");
配置子应用菜单
/views/index.tsx
import { defineComponent, h, reactive, ref } from "vue";
import { Menu, SubMenu, MenuItem, ItemType } from "ant-design-vue";
import "./index.css";
import { RouterView, useRouter } from "vue-router";
// 展平数组
const flattenMenu = (list) => {
const res: any = [];
if (!list) return;
list.forEach((item) => {
res.push(item);
if (item.children) res.push(...flattenMenu(item.children));
});
return res;
};
const getMenuKey = (menuList, key) => {
const allList = flattenMenu(JSON.parse(JSON.stringify(menuList)));
const cur = allList.find((item) => item.key == key);
return cur ? cur : {};
};
export default defineComponent({
setup() {
const router = useRouter();
const menuList = ref([
{
key: "1",
label: "子应用菜单",
url: "/qiankun-child-vue",
children: [
{ label: "设置", key: "2", url: "/qiankun-child-vue/setting" },
],
},
]);
// 找到点击的菜单对象
const handleMenuSelect = (params) => {
const menu = getMenuKey(menuList.value, params.key);
router.push(menu.url);
};
return () => (
<a-layout class="layout">
<a-layout-sider>
<Menu
onSelect={handleMenuSelect}
style="height:100%"
mode="inline"
items={menuList.value}
></Menu>
</a-layout-sider>
<a-layout-content>
<RouterView></RouterView>
</a-layout-content>
</a-layout>
);
},
});
新建/views/setting.tsx
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
return () => <div>设置</div>;
},
});
配置路由 /router/index.ts
import { createRouter, createWebHashHistory } from "vue-router";
import Index from "@/views/index";
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: "/",
component: Index,
children: [
{
path: "/qiankun-child-vue/setting",
name: "setting",
component: () => import("@/views/setting"),
},
],
},
],
});
export default router;
基本的页面就搭建好了
基座 qiankun-base
同样在/views/index.tsx 写好基本的菜单
配置路由 /router/index.ts
import { createRouter, createWebHashHistory } from "vue-router";
import Index from "@/views";
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: "/:afterUser(.*)", // 正则匹配url 跳转不会报错
component: Index,
},
],
});
export default router;
配置vite.config.js 根路径base
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import { resolve } from "path";
export const pathResolve = (dir: string) => resolve(process.cwd(), ".", dir);
export default defineConfig(({ mode }) => {
return {
base: "/qiankun-base/",
plugins: [vue(), vueJsx()],
server: {
host: "0.0.0.0",
port: 1000,
},
resolve: {
alias: {
"@": pathResolve("src"),
},
},
};
});
修改dom根节点 和主应用一样改为子应用的项目名称 改这个是为了主应用和子应用的挂载在根节点的id不会重复,如果一样的话会导致主应用页面渲染不出来子应用(这里不在展示细节)
qiankun配置步骤(上面还没开始)
主应用qiankun-base中下载qiankun
yarn add qiankun
在main.ts中开启
注意:在挂载之前开启
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import { start } from "qiankun";
import router from "./router";
import antv from "ant-design-vue";
start({
sandbox: {
// strictStyleIsolation: true, // 开启严格的样式隔离模式
experimentalStyleIsolation: true, // 开启后所有样式都会加上一个类名 .app-main {}
===> div[data-qiankun-react16] .app-main {}
},
singular: false, // 单一时间只渲染一个微应用,默认为true
});
createApp(App).use(router).use(antv).mount("#qiankun-base");
vue子应用 qiankun-child-vue
下载vite-plugin-qiankun插件
yarn add vite-plugin-qiankun
配置vite.config.js 使用vite-plugin-qiankun
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import qiankun from "vite-plugin-qiankun";
import { resolve } from "path";
export const pathResolve = (dir: string) => resolve(process.cwd(), ".", dir);
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd());
return {
base: mode == "production" ? `/${env.VITE_APP_NAME}/` : "",
plugins: [
vue(),
vueJsx(),
qiankun(env.VITE_APP_NAME, { useDevMode: true }),
],
server: {
host: "0.0.0.0",
port: 2000,
},
resolve: {
alias: {
"@": pathResolve("src"),
},
},
};
});
修改main.ts 判断是在主应用还是子应用中
import "./style.css";
import App from "./App.vue";
import router from "./router";
import antv from "ant-design-vue";
import { render } from "@/hooks/microApp";
render(App, "#qiankun-child-vue", (app, props) => {
app.use(router).use(antv);
});
/hooks/microApp.ts
import renderWithQiankun, {
QiankunProps,
qiankunWindow,
} from "vite-plugin-qiankun/dist/helper";
import { App, Component, createApp } from "vue";
const isMicroApp = qiankunWindow.__POWERED_BY_QIANKUN__;
export const render = (
AppRoot: Component,
domId,
configApp: (app: App, props?: QiankunProps) => any
) => {
let app: App;
const _render = (props: QiankunProps = {}) => {
const { container } = props;
const root: string | Element = container
? container.querySelector(domId)!
: domId; // 避免 id 重复导致微应用挂载失败
app = createApp(AppRoot);
// 回调配置app的函数 让调用的地方 可以使用app
configApp(app, props);
app.mount(root);
};
const initQiankun = () => {
renderWithQiankun({
bootstrap() {
// console.log("微应用:bootstrap");
},
mount(props) {
// 获取主应用传入数据
// console.log("微应用:mount", props);
_render(props);
},
unmount(props) {
// console.log("微应用:unmount", props);
app.unmount();
},
update(props) {
// console.log("微应用:update", props);
},
});
};
isMicroApp ? initQiankun() : _render();
};
在views/index.tsx 增加判断逻辑 是在主应用中还是在子应用中
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
return () => {
// 判断如果在主应用中
if (qiankunWindow.__POWERED_BY_QIANKUN__) {
return <router-view></router-view>;
}
// 在子应用中
return (
<a-layout class="layout">
<a-layout-sider>
<Menu
onSelect={handleMenuSelect}
style="height:100%"
mode="inline"
items={menuList.value}
></Menu>
</a-layout-sider>
<a-layout-content>
<RouterView></RouterView>
</a-layout-content>
</a-layout>
);
};
易错点
1.主应用和子应用挂载在根节点的domid是同一个
2.主应用配置路径和子应用路径不一致