前言
基于 microapp
的微前端实操
安装
microapp
需要基座项目+若干子项目这里以如下三个项目为例子
1 2 3
| vue create base vue create first_child vue create second_child
|
Cd 到基座项目 安装 microapp
1
| pnpm install @micro-zoe/micro-app --save
|
在基座中的 main.ts
中引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { createApp } from "vue"; import App from "./App.vue"; import router from "./router"; import microApp from "@micro-zoe/micro-app"; microApp.start({ lifeCycles: { created(e) { console.log("created"); }, beforemount(e) { console.log("beforemount"); }, mounted(e) { console.log("mounted"); }, unmount(e) { console.log("unmount"); }, error(e) { console.log("error", e); }, }, });
createApp(App).use(router).mount("#app");
|
模式
microapp
支持如下的路由模式
基座应用是 hash
的话 子应用必须是 hash
基座应用是 history
的话 子应用是 history
或者 hash
我这边的配置是基座 histroy
子引用 hash
所以先改一下基座的配置
基座 history 模式的配置
router.ts
中修改
1 2 3 4 5
| const router = createRouter({ history: createWebHistory("/base"), routes, });
|
vue.config.js
中修改
1 2 3 4 5 6 7 8 9 10
| module.exports = { publicPath: "/base", devServer: { host: "localhost", port: 3000, headers: { "Access-Control-Allow-Origin": "*", }, }, };
|
子项目由于 vue
初始化的时候就是默认的 hash
所以就不改了
基座路由配置
router.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { createRouter, createWebHistory, RouteRecordRaw, } from "vue-router"; import HomeView from "../views/HomeView.vue";
const routes: Array<RouteRecordRaw> = [ { path: "/", name: "home", component: HomeView, }, { path: "/micro-app1/*", name: "child1", component: () => import("@/views/app_first.vue"), }, { path: "/micro-app2/*", name: "child2", component: () => import("@/views/app_second.vue"), }, ];
const router = createRouter({ history: createWebHistory("/base"), routes, });
export default router;
|
创建子项目 vue app_first.vue
保证 name
和 baseroute
对应 且其中的 url
是子项目启动地址
(由于基座是 history
子项目是 hash
所以组件不需要设置 baseroute)
url
属性 html
地址 作用是加载 html
资源
1 2 3 4 5
| <template> <micro-app name="child1" url="http://localhost:1111/"></micro-app> </template>
<script setup lang="ts"></script>
|
app_second.vue
1 2 3 4 5
| <template> <micro-app name="child2" url="http://localhost:1112/"></micro-app> </template>
<script setup lang="ts"></script>
|
基座 app.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <template> <div> <app_first></app_first> <div class="app_second"> <app_second></app_second> </div> </div> </template> <script setup lang="ts"> import app_first from "@/views/app_first.vue"; import app_second from "@/views/app_second.vue"; </script> <style lang="scss"> #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; } } } .app_second { width: 100px; height: 400px; } </style>
|
子项目配置
配置跨域 和 port
子项目 1 端口是 1111 子项目 2 端口我写了 1112
1 2 3 4 5 6 7 8 9 10
| module.exports = { devServer: { host: "localhost", port: 1111, headers: { "Access-Control-Allow-Origin": "*", }, }, };
|
app.vue
子项目随便在 app.vue
中写点东西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <template> <nav>这个是第一个app</nav> </template>
<style lang="scss"> #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>
|
第二个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <template> <nav>这个是第二个app</nav> </template>
<style lang="scss"> #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>
|
启动
如果不是模块空间的话,就手动启动三个项目吧
出现的界面如下 则成功
应用间跳转
基座控制子应用跳转
原理是
注意事项:如果两个子应用的 path
是一样的 虽然 setdata
会触发相应的页面跳转,但是直接在网址上跳转的话两个子应用都会被触发
子项目 1 配置
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <div>这个是第一个app</div> <RouterView></RouterView> </template> <script setup lang="ts"> import { useRouter } from "vue-router";
const router = useRouter(); // 监听基座下发的数据变化 if (window?.microApp?.addDataListener) { window.microApp.addDataListener((data: any) => { console.log(data.path, "datapath"); // 当基座下发跳转指令时进行跳转 if (data.path) { router.push(data.path); } }); } </script>
|
子项目 1 的测试跳转页面为自带的 about
页
子项目 2 配置
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <div>这个是第二个app</div> <RouterView></RouterView> </template> <script setup lang="ts"> import { useRouter } from "vue-router";
const router = useRouter(); // 监听基座下发的数据变化 if (window?.microApp?.addDataListener) { window.microApp.addDataListener((data: any) => { console.log(data.path, "datapath"); // 当基座下发跳转指令时进行跳转 if (data.path) { router.push(data.path); } }); } </script>
|
子项目 2 的测试跳转页面同样为自带的 about
页 但是 path
要写不一样
主项目 base 配置
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <template> <div> <app_first></app_first> <div class="app_second"> <app_second></app_second> </div> <button @click="routeToProject('child1')">跳转项目1</button> <button @click="routeToProject('child2')">跳转项目2</button> </div> </template> <script setup lang="ts"> import microApp from "@micro-zoe/micro-app"; import app_first from "@/views/app_first.vue"; import app_second from "@/views/app_second.vue"; type routerType = "child1" | "child2"; const routeToProject = (type: routerType) => { console.log(type, "type"); const typeMapper = { child1: "/about/", child2: "/about2/", }; microApp.setData(type, { path: typeMapper[type] }); }; </script> <style lang="scss"> #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; } } } .app_second { width: 100px; height: 400px; } </style>
|
测试
跳转项目 1
跳转项目 2
注意:我们发现跳转的时候 url
变了,但是跳转的过程中并没有重置之前项目 1 的状态,也就是项目 1 的状态仍然被保留了下来
我们试着刷新页面
发现此时子项目 1 的状态才被重置。
解决方法也很简单 给 routerToProject
方法提供一个还原其他页面状态的操作就 ok 了 这里就不写了
子应用控制基座跳转
如果有基座控制子应用跳转 那么自然有子应用控制基座跳转
原理是利用了回调函数的写法 给子项目传递一个回调函数去触发
bug 探讨
在使用了上述方法 流程是 base
项目触发跳转子项目 1,子项目 1 提供按钮回退到 base
项目
此时拿不到 pushState
或者说是 window.mircoApp.getData
的数据被刷新了
但是初始化的时候是存在这个内容的
所以我们得通过监听下发通知的方式去触发跳转,或者直接通过状态管理去存储初次的状态 后续就直接调用
pinia
代码 /store/useMicro.ts
1 2 3 4 5 6 7 8
| import { defineStore } from "pinia"; import { ref } from "vue"; export const useMicro = defineStore("useMicro", () => { const microData = ref<any>(null); return { microData, }; });
|
子项目 1 的 App.vue
此时就收集到了基座项目的 pushState
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <div>这个是第一个app</div> <RouterView></RouterView> </template> <script setup lang="ts"> import { useRouter } from "vue-router"; import { useMicro } from "@/store/useMicro"; import { storeToRefs } from "pinia"; const { microData } = storeToRefs(useMicro()); const router = useRouter(); // 监听基座下发的数据变化 if (window?.microApp?.addDataListener) { window.microApp.addDataListener((data: any) => { console.log(data.path, "datapath"); // 当基座下发跳转指令时进行跳转 if (data.path) { router.push(data.path); } }); microData.value = window.microApp.getData(); console.log(microData.value, "window.microApp.getData()"); } </script>
|
对应页面回调出去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <div class="about"> <h1>This is an about page child1</h1> <button @click="routeToBaseProject">基座跳转回首页</button> </div> </template>
<script lang="ts" setup> // import { nextTick } from "vue"; import { useMicro } from "@/store/useMicro"; import { storeToRefs } from "pinia"; const { microData } = storeToRefs(useMicro()); const routeToBaseProject = () => { // nextTick(() => { if (window?.microApp?.getData) { console.log(window.microApp.getData(), "window.microApp"); // window.microApp.getData().pushState("/"); } console.log(microData.value, "microData.value"); microData.value.pushState("/baseTest"); // }); }; </script>
|
多项目启动
一般来说 npm
不支持平行启动多个项目,参考了 microapp
仓库,他的 packagejson
里面有如下的东西
将子项目和主项目分开 然后通过一个 run-all
的命令去全部执行
于是找到了对应的 npm
仓库
1
| pnpm install npm-run-all --save-dev
|
从项目根目录下初始化一个 packagejson
1 2 3 4 5 6 7
| "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev-child:child1": "cd first_child && pnpm run serve", "dev-child:child2": "cd second_child && pnpm run serve", "dev-main:base": "cd base && pnpm run serve", "dev-main": "npm-run-all --parallel dev-main:base dev-child:_" },
|
结果:
多项目打包
1 2 3 4 5 6
| { "build-main:base":"cd base && pnpm run build", "build-child:child1":"cd first_child && pnpm run build", "build-child:child2":"cd second_child && pnpm run build", "build-main":"npm-run-all --parallel build-main:base build-child:_" }
|
部署项目(本地 docker
先修改之前 base
项目的 router
,因为这样方便我们后续操作,将原来叫 micro_app1
和 2 的路径分别改为子项目的名字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import { createRouter, createWebHistory, RouteRecordRaw, } from "vue-router"; import HomeView from "../views/HomeView.vue";
const routes: Array<RouteRecordRaw> = [ { path: "/", name: "home", component: HomeView, }, { path: "/baseTest", name: "baseTest", component: () => import("@/views/AboutView.vue"), }, { path: "/first_child/*", name: "child1", component: () => import("@/views/app_first.vue"), }, { path: "/second_child/*", name: "child2", component: () => import("@/views/app_second.vue"), }, ];
const router = createRouter({ history: createWebHistory("/base"), routes, });
export default router;
|
Output dir 配置打包路径(可以不写,参见 Dockerfile)
我们继续编写各个 vueconfigjs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| module.exports = { publicPath: "/base", devServer: { host: "localhost", port: 3000, headers: { "Access-Control-Allow-Origin": "_", }, }, outputDir: "base", chainWebpack: (config) => { config.module .rule("vue") .use("vue-loader") .tap((options) => { options.compilerOptions = { ...(options.compilerOptions || {}), isCustomElement: (tag) => /^micro-app/.test(tag), }; return options; }); }, };
module.exports = { devServer: { host: "localhost", port: 1111, headers: { "Access-Control-Allow-Origin": "_", }, }, outputDir: "first_child", };
module.exports = { devServer: { host: "localhost", port: 1112, headers: { "Access-Control-Allow-Origin": "*", }, }, outputDir: "second_child", };
|
编写 default.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| server { listen 80; server_name localhost;
location / { root /usr/share/nginx/html; index index.php index.html index.htm; # add_header Cache-Control; add_header Access-Control-Allow-Origin _; if ( $request_uri ~_ ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){ add_header Cache-Control max-age=7776000; add_header Access-Control-Allow-Origin \*; } }
# 主应用 base
location /base { root /usr/share/nginx/html; add_header Access-Control-Allow-Origin _; if ( $request_uri ~_ ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){ add_header Cache-Control max-age=7776000; add_header Access-Control-Allow-Origin \*; } try_files $uri $uri/ /base/index.html; }
# 子应用 first_child
location /first_child { root /usr/share/nginx/html; add_header Access-Control-Allow-Origin _; if ( $request_uri ~_ ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){ add_header Cache-Control max-age=7776000; add_header Access-Control-Allow-Origin \*; } try_files $uri $uri/ /first_child/index.html; }
# 子应用 second_child
location /second_child { root /usr/share/nginx/html; add_header Access-Control-Allow-Origin _; if ( $request_uri ~_ ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){ add_header Cache-Control max-age=7776000; add_header Access-Control-Allow-Origin \*; } try_files $uri $uri/ /second_child/index.html; }
error_page 404 /404.html; location = /40x.html { }
error_page 500 502 503 504 /50x.html; location = /50x.html { } }
|
Dockerfile
注意在 window
下 mkdir
会产生一个额外的目录 不过问题不大
1 2 3 4 5 6 7
| FROM nginx RUN mkdir -p /usr/share/nginx/html/{base,first_child,second_child} COPY /base/base/ /usr/share/nginx/html/base COPY /first_child/first_child/ /usr/share/nginx/html/first_child COPY /second_child/second_child/ /usr/share/nginx/html/second_child
COPY default.conf /etc/nginx/conf.d/default.conf
|
如果在上述 outputdir
里面没有编写打包名称的话 问题也不大 道理是一样的 可以这样写
1 2 3 4 5 6 7
| FROM nginx RUN mkdir -p /usr/share/nginx/html/{base,first_child,second_child} COPY /base/dist/ /usr/share/nginx/html/base COPY /first_child/dist/ /usr/share/nginx/html/first_child COPY /second_child/dist/ /usr/share/nginx/html/second_child
COPY default.conf /etc/nginx/conf.d/default.conf
|
运行 docker
1 2 3 4 5 6 7 8 9 10
| docker pull nginx//拉 nginx docker images// 查看镜像 docker build -t docker_micro_app . --no-cache//构建镜像 记得加. dokcer run -d -p 5555:80 --name micro_app docker_micro_app // 运行容器 docker exec -it 容器 id bash // 进入容器 cd /usr/share/nginx/html // 进入 nginx 内部的 html ls //查看所有文件 如下图 说明已经有对应的文件了 exit //退出 // 如果不对 请删除镜像再操作 docker rm -f xxxID
|
在地址栏查看 localhost:5555/base
如果有内容则成功
运行到本地 ip
1
| dokcer run -d -p 你的 ip:5555:80 --name micro_app docker_micro_app // 运行镜像
|
常见问题
基座应用警报
1
| vue3: [Vue warn]: Failed to resolve component: micro-app
|
解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| module.exports = { chainWebpack: (config) => { config.module .rule("vue") .use("vue-loader") .tap((options) => { options.compilerOptions = { ...(options.compilerOptions || {}), isCustomElement: (tag) => /^micro-app/.test(tag), }; return options; }); }, };
|