前言
最近公司框架升级,微前端选择了京东的MicroApp
,因为后续会参与开发。提前来学习一波。
推荐文章
官网
MicroApp官网
关于微前端的好处,前面两篇文章和官网都说了,这里就不提了。
准备
参考
参考官方示例
基于vite和vue3创建一个主应用和一个子应用
1、 创建一个app文件,在app文件夹里创建一个main-apps
文件夹放主应用,创建一个child-apps
放子应用
2、 初始化app文件夹 npm initall
生成一个packgae.json
文件
3、 在main-apps
文件夹下创建一个主应用main-vue
,在child-apps
文件夹下创建一个子应用child-vue
在这里项目只安装路由,如果需要其他依赖自行安装。每一个项目里都要单独安装依赖
项目具体创建过程见:使用Vite搭建Vue3 + Ts项目
4、 修改主应用和子应用的vite.config.ts
配置文件,这里直接把官方示例的配置文件内容拷贝过来就行
main-vue/vite.config.ts
import { defineConfig, searchForWorkspaceRoot } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => /^micro-app/.test(tag),
},
},
}),
],
server: {
port: 3000,
open: true,
fs: {
allow: [
searchForWorkspaceRoot(process.cwd()),
"/mygit/micro-zoe/micro-app/",
],
},
},
resolve: {
alias: {},
},
base: "/main-vite/",
build: {
outDir: "main-vite",
},
});
child-vue/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { join } from "path";
import { writeFileSync } from "fs";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
(function () {
let basePath = "";
return {
name: "vite:micro-app",
apply: "build",
configResolved(config) {
basePath = `${config.base}${config.build.assetsDir}/`;
},
// writeBundle 钩子可以拿到完整处理后的文件,但已经无法修改
writeBundle(options, bundle) {
for (const chunkName in bundle) {
if (Object.prototype.hasOwnProperty.call(bundle, chunkName)) {
const chunk = bundle[chunkName];
if (chunk.fileName && chunk.fileName.endsWith(".js")) {
chunk.code = chunk.code.replace(
/(from|import\()(\s*['"])(\.\.?\/)/g,
(all, $1, $2, $3) => {
return all.replace($3, new URL($3, basePath));
}
);
const fullPath = join(options.dir, chunk.fileName);
writeFileSync(fullPath, chunk.code);
}
}
}
},
};
})() as any,
],
server: {
port: 4007,
},
base: `${
process.env.NODE_ENV === "production" ? "http://www.micro-zoe.com" : ""
}/child/vite/`,
build: {
outDir: "vite",
},
});
5、 简单配置主应用和子应用的路由
以主应用为例
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
const history = createWebHistory();
const routes: Array<RouteRecordRaw> = [
{
path: "/",
redirect: "/main-vite",
},
{
path: "/main-vite",
name: "home",
component: () => import("../view/home/index.vue"),
},
];
const router = createRouter({
history,
routes,
});
export default router;
注: 路由的根路径要与你的vite.config.ts
设置的一致
6、 启动两个项目看一下是否可以正常运行
基座应用
安装依赖
主应用和子应用都需要安装
npm i @micro-zoe/micro-app --save
在入口中引入
复制官方示例中的代码,官方处理了一些问题
main-vue/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import microApp from "@micro-zoe/micro-app";
microApp.start({
plugins: {
modules: {
"appname-vite": [
{
loader(code) {
if (process.env.NODE_ENV === "development") {
// 这里 /basename/ 需要和子应用vite.config.js中base的配置保持一致
code = code.replace(
/(from|import)(\s*['"])(\/child\/vite\/)/g,
(all) => {
return all.replace(
"/child/vite/",
"http://localhost:4007/child/vite/"
);
}
);
}
return code;
},
},
],
},
},
});
createApp(App).use(router).mount("#app");
这里报了个错:
解决:
npm i --save-dev @types/node
child-vue
import { createApp, App as AppInstance } from "vue";
import {
createRouter,
createWebHashHistory,
RouterHistory,
Router,
} from "vue-router";
import App from "./App.vue";
import routes from "./router";
declare global {
interface Window {
eventCenterForAppNameVite: any;
__MICRO_APP_NAME__: string;
__MICRO_APP_ENVIRONMENT__: string;
__MICRO_APP_BASE_APPLICATION__: string;
}
}
// 与基座进行数据交互
function handleMicroData(router: Router) {
// eventCenterForAppNameVite 是基座添加到window的数据通信对象
if (window.eventCenterForAppNameVite) {
// 主动获取基座下发的数据
console.log(
"child-vite getData:",
window.eventCenterForAppNameVite.getData()
);
// 监听基座下发的数据变化
window.eventCenterForAppNameVite.addDataListener(
(data: Record<string, unknown>) => {
console.log("child-vite addDataListener:", data);
if (data.path && typeof data.path === "string") {
data.path = data.path.replace(/^#/, "");
// 当基座下发path时进行跳转
if (data.path && data.path !== router.currentRoute.value.path) {
router.push(data.path as string);
}
}
}
);
// 向基座发送数据
setTimeout(() => {
window.eventCenterForAppNameVite.dispatch({ myname: "child-vite" });
}, 3000);
}
}
/**
* 用于解决主应用和子应用都是vue-router4时相互冲突,导致点击浏览器返回按钮,路由错误的问题。
* 相关issue:https://github.com/micro-zoe/micro-app/issues/155
*/
function fixBugForVueRouter4(router: Router) {
// 判断主应用是main-vue3或main-vite,因为这这两个主应用是 vue-router4
//这里根据实际情况改一下,不要直接复制过来
if (window.location.href.includes("/main-vue")) {
/**
* 重要说明:
* 1、这里主应用下发的基础路由为:`/main-xxx/app-vite`,其中 `/main-xxx` 是主应用的基础路由,需要去掉,我们只取`/app-vite`,不同项目根据实际情况调整
*
* 2、因为vite关闭了沙箱,又是hash路由,我们这里写死realBaseRoute为:/app-vite#
*/
const realBaseRoute = "/app-vite#";
router.beforeEach(() => {
if (typeof window.history.state?.current === "string") {
window.history.state.current = window.history.state.current.replace(
new RegExp(realBaseRoute, "g"),
""
);
}
});
router.afterEach(() => {
if (typeof window.history.state === "object") {
window.history.state.current =
realBaseRoute + (window.history.state.current || "");
}
});
}
}
// ----------分割线---默认模式------两种模式任选其一-----放开注释即可运行------- //
const router = createRouter({
history: createWebHashHistory(),
routes,
});
const app = createApp(App);
app.use(router);
app.mount("#vite-app");
console.log("微应用child-vite渲染了");
handleMicroData(router);
fixBugForVueRouter4(router);
// 监听卸载操作
window.addEventListener("unmount", function () {
app.unmount();
// 卸载所有数据监听函数
window.eventCenterForAppNameVite?.clearDataListener();
console.log("微应用child-vite卸载了");
});
// ----------分割线---umd模式------两种模式任选其一-------------- //
// let app: AppInstance | null = null;
// let router: Router | null = null;
// let history: RouterHistory | null = null;
// // 将渲染操作放入 mount 函数
// function mount() {
// history = createWebHashHistory();
// router = createRouter({
// history,
// routes,
// });
// app = createApp(App);
// app.use(router);
// app.mount("#vite-app");
// console.log("微应用child-vite渲染了");
// handleMicroData(router);
// // fixBugForVueRouter4(router)
// }
// // 将卸载操作放入 unmount 函数
// function unmount() {
// app?.unmount();
// history?.destroy();
// // 卸载所有数据监听函数
// window.eventCenterForAppNameVite?.clearDataListener();
// app = null;
// router = null;
// history = null;
// console.log("微应用child-vite卸载了");
// }
// // 微前端环境下,注册mount和unmount方法
// if (window.__MICRO_APP_BASE_APPLICATION__) {
// // @ts-ignore
// window["micro-app-appname-vite"] = { mount, unmount };
// } else {
// // 非微前端环境直接渲染
// mount();
// }
有两个注意的点:
- // 判断主应用是main-vue3或main-vite,因为这这两个主应用是 vue-router4 。找到这块注释,if判断根据你自己的实际情况来,需要进行修改
- 路由
const router = createRouter({
history: createWebHashHistory(),
routes,
});
我前面那篇搭建项目的文章里,创建路由对象是在路由里做的,但是这里是在main.ts
里做的。要么修改路由,要么修改main.ts
我这里是修改的路由
刷新一下自己的两个应用看看有没有问题
我这里主应用没问题,子应用的路由出了问题
排查了一下子应用绑定的id不对,这里推荐改一下index.html
里面的id,并且不要跟主应用里的id一致
app.mount("#vite-app");
分配一个路由给子应用
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
const history = createWebHistory(import.meta.env.BASE_URL);
console.log("import.meta.env.BASE_URL:", import.meta.env.BASE_URL);
const routes: Array<RouteRecordRaw> = [
{
path: "/",
redirect: "/main-vite",
},
{
path: "/main-vite",
name: "home",
component: () => import("../view/home/index.vue"),
},
{
path: "/app-vite:page*",
name: "vite",
component: () => import("../view/vite.vue"),
},
];
const router = createRouter({
history,
routes,
});
export default router;
看看自己的主应用有没有问题,没问题继续。
这里其实可以理解为子应用也是主应用下的一个路由页面,只是这个路由页面由于是micro-app
,因此可以访问子应用中的页面。通过将每个子应用封装在一个独立的micro-app中,可以实现更好的代码隔离和模块化,同时也可以更方便地进行版本控制和升级。
一个micro-app
标签代表一个微应用,并不是每一个模块都需要建立一个微应用。只有当多个微应用都需要存在时才需要建立多个,因为每次只会显示一个模块,所有共用一个微应用即可。比如:a模块需要使用系统模块的审批功能时,这两个需要同事存在就需要两个微应用
在页面中嵌入子应用
上面给子应用分配一个路由时,新建了一个vite.vue
。这里复制一下官方示例中的内容,修改一下url就好
// vite.vue
<template>
<div>
<micro-app
name="appname-vite"
:url="url"
inline
disablesandbox
:data="microAppData"
@created="handleCreate"
@beforemount="handleBeforeMount"
@mounted="handleMount"
@unmount="handleUnmount"
@error="handleError"
@datachange="handleDataChange"
></micro-app>
</div>
</template>
<script lang="ts">
import { EventCenterForMicroApp } from "@micro-zoe/micro-app";
// @ts-ignore 因为vite子应用关闭了沙箱,我们需要为子应用appname-vite创建EventCenterForMicroApp对象来实现数据通信
window.eventCenterForAppNameVite = new EventCenterForMicroApp("appname-vite");
export default {
name: "vite",
data() {
return {
url: `http://localhost:4007/child/vite/`,
microAppData: { msg: "来自基座的数据" },
};
},
methods: {
handleCreate(): void {
console.log("child-vite 创建了");
},
handleBeforeMount(): void {
console.log("child-vite 即将被渲染");
},
handleMount(): void {
console.log("child-vite 已经渲染完成");
setTimeout(() => {
// @ts-ignore
this.microAppData = { msg: "来自基座的新数据" };
}, 2000);
},
handleUnmount(): void {
console.log("child-vite 卸载了");
},
handleError(): void {
console.log("child-vite 加载出错了");
},
handleDataChange(e: CustomEvent): void {
console.log("来自子应用 child-vite 的数据:", e.detail.data);
},
},
};
</script>
<style>
</style>
修改 main-vue/src/view/home/index.vue
就是添加一个路由,可以跳转到vite.vue
<template>
<div>
<h3>主应用</h3>
<p @click="go('/app-vite')">跳转到子应用</p>
<div>
<router-view></router-view>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { useRouter } from "vue-router";
export default defineComponent({
setup() {
let router = useRouter()
let go = (url: string) => {
router.push(url)
}
return {
go
}
}
});
</script>
<style scoped>
p {
cursor: pointer;
}
</style>
效果
package.json
根据官方示例来修改最外层的package.json
编写相应的命令
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"main-vue":"cd main-apps/main-vue && npm run dev",
"child-vue":"cd child-apps/child-vue && npm run dev",
"install:main-vue": "cd main-apps/main-vue && npm install",
"install:child-vue": "cd child-apps/child-vue && npm install",
"bootstrap": "npm run install:main-vue && npm run install:child-vue",
},
命令简单介绍
"cd main-apps/main-vue && npm run dev"
切换到main-vue
下运行npm run dev
,npm run dev
是主应用package.json
里面的启动命令
本来打算是写个命令一下启动两个项目的,但是一个终端下不能启动两个,只能分别启动主应用和子应用