微前端MicroApp的学习(一):简单搭建项目

前言

最近公司框架升级,微前端选择了京东的MicroApp ,因为后续会参与开发。提前来学习一波。

推荐文章

极致简洁的微前端框架-京东MicroApp开源了

初探 MicroApp,一个极致简洁的微前端框架

官网
MicroApp官网

关于微前端的好处,前面两篇文章和官网都说了,这里就不提了。

准备

参考

参考官方示例

micro-app-demo

基于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 devnpm run dev是主应用package.json 里面的启动命令

本来打算是写个命令一下启动两个项目的,但是一个终端下不能启动两个,只能分别启动主应用和子应用

项目地址

简单demo

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无知的小菜鸡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值