前言
因后端项目要上微服务。为项目长远考虑也同时上微前端这种技术架构,从而有了改基于vite的微服务使用指南。
微前端的概念
借鉴了微服务的概念, 将多个项目融合唯一,减少项目之间的耦合,提升项目扩展项。有一个基座主应用来管理各个子应用的加载和卸载。所以微前端不关心子应用的依赖库与应用框架,不关心具体的工具只是一种架构模式。
项目概述
一、框架选型
1.调研框架
- qiankun 是一款基于 single-spa 封装的微前端框架
- MircoApp 京东开源, 一款基于 WebComponet 的思想的微前端框架
- vite-micro 基于vite 将模块联邦思想融入到微前端框架中 基于公司项目现有的项目多数为 vite+vue 的技术栈, 而qiankun框架对vite的支持并不完美(但3.0发布在即,等3.0发布后在试试)。所以本次为了项目扩展的便利性选择了MircoApp的这个微前端框架。但vite-micro的概念个人觉得是一个十分完美的方案,qiankun与mircoapp都是应用级别的模块, 都需要对现有的系统的鉴权与路由模块进行改造,但vite-micro基于模块联邦的思想既可以是微组件也可是微应用。
2. 场景概述
外层的 基座, 是微前端应用的重要平台。主要负责管理公共资源、依赖、规范的责任,主要有以下责任
- 子应用集成,给子应用提供渲染器
- 权限管理
- 会话管理
- 路由,菜单管理
- 主题管理
- 共享依赖
- 多语言管理
二、搭建项目
1. 项目采用monorepo项目结构管理, 使用turbo对项目进行脚本通道管理。
mkdir micro-demo
cd micro-demo
pnpm init
2. 项目根目录创建 pnpm-workspace.yaml, 管理monorepo项目目录。
// pnpm-workspace.yaml
packages:
# all packages in direct subdirs of packages/
- 'packages/*'
# all packages in subdirs of components/
- 'apps/**'
3. 创建项目目录如下:
├───📁 apps/
│ ├───📁 micro-project-a/
│ └───📁 micro-project-b/
├───📁 packages/
│ └───📁 micro-base/
├───📄 package.json
├───📄 pnpm-workspace.yaml
└───📄 turbo.json
4. 安装全局应用turbo:
pnpm install -w turbo
5. 添加配置 turbo文件
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"build": {
"dependsOn": [
"^build"
],
"outputs": [
"dist/**"
]
},
"stub": {},
"lint": {},
"clean": {
"cache": false
},
"dev": {
"dependsOn": [],
"cache": false,
"persistent": true
}
}
}
6. 创建基座应用与子应用
npm create vue
Vue.js - The Progressive JavaScript Framework
? 请输入项目名称: » vue-project
? 是否使用 TypeScript 语法? » 否 / 是
? 是否启用 JSX 支持? » 否 / 是
? 是否引入 Vue Router 进行单页面应用开发? » 否 / 是
? 是否引入 Pinia 用于状态管理? ... 否 / 是
? 是否引入 Vitest 用于单元测试? » 否 / 是
? 是否要引入一款端到端(End to End)测试工具? » - 使用箭头切换按Enter确认。
> 不需要
Cypress
Nightwatch
Playwright
? 是否引入 ESLint 用于代码质量检测? » 否 / 是
? 是否引入 Prettier 用于代码格式化? » 否 / 是
正在构建项目 micro-demo\apps\vue-project...
项目构建完成,可执行以下命令:
cd vue-project
npm install
npm run format
npm run dev
三、构建基座应用
1. 安装 `n@micro-zoe/micro-app ,并修改package.json的name值
npm i @micro-zoe/micro-app --save
// package.json
"name": "@base/micro-app",
2. 在main.ts 当中初始化 microapp
import microApp from '@micro-zoe/micro-app'
microApp.start({
lifeCycles: {
created() {
console.log('created 全局监听')
},
beforemount() {
console.log('beforemount 全局监听')
},
mounted() {
console.log('mounted 全局监听')
},
unmount() {
console.log('unmount 全局监听')
},
error() {
console.log('error 全局监听')
}
},
// 预加载
preFetchApps: [
//当子应用是vite时,除了name和url外,还要设置第三个参数iframe为true,开启iframe沙箱。
{ name: 'child-app-1', url: 'http://localhost:4002/child-app-1/', iframe: true, level: 3 }
],
plugins: { },
/**
* 自定义fetch
* @param url 静态资源地址
* @param options fetch请求配置项
* @returns Promise<string>
* (url: string, options: Record<string, unknown>, appName: string | null) => Promise<string>
*/
fetch(url: string, options: any, appName: string | null) {
return fetch(url, Object.assign(options, {})).then((res) => {
return res.text()
})
}
})
3. 维护基座应用与子应用的路由信息
- 主应用 route, history路由模式
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about2',
name: 'about',
component: () => import('../views/AboutView.vue')
},
{
name: 'child-app-1',
// 👇 非严格匹配,/child-app-1/* 都指向 vue3-vite 页面
path: '/child-app-1/:page*',
component: () => import('../views/AppOne.vue')
},
]
- 页面菜单路由 子应用路径应注意为 : 子应用名称/子应用页面路径
const pageRoutes = [
{
path: '/',
name: 'home',
app: 'main'
},
{
path: '/about2',
name: 'about',
app: 'main'
},
{
path: '/child-app-1',
name: '合同管理',
children: [
{
path: '/child-app-1/',
name: '收款',
app: 'child-app-1'
},
{
path: '/child-app-1/element-plus',
name: '付款',
app: 'child-app-1'
},
{
path: '/child-app-1/ant-design-vue',
name: '首页d',
app: 'child-app-1'
}
]
},
]
4. 创建基座应用页面
<template>
<a-layout>
<a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
<div class="logoc" />
<a-menu v-model:selectedKeys="selectedKeys" theme="dark" mode="inline">
<template v-for="item in pageRoutes">
<template v-if="item.children">
<a-sub-menu :key="item.path">
<template #title>
<span>
<user-outlined />
{{ item.name }}
</span>
</template>
<a-menu-item v-for="child in item.children" :key="child.path" @click="select(child)">
<user-outlined />
<span>{{ child.name }}</span>
</a-menu-item>
</a-sub-menu>
</template>
<template v-else>
<a-menu-item :key="item.path" @click="select(item)">
<user-outlined />
<span>{{ item.name }}</span>
</a-menu-item>
</template>
</template>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="background: #fff; padding: 0">
<menu-unfold-outlined v-if="collapsed" class="trigger" @click="() => (collapsed = !collapsed)" />
<menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
</a-layout-header>
<a-layout-content :style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '91vh' }">
<RouterView />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup lang="ts">
import { RouterView, useRouter } from 'vue-router'
import { ref } from 'vue';
import {
UserOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
} from '@ant-design/icons-vue';
import microApp, { getActiveApps } from '@micro-zoe/micro-app'
import { pageRoutes } from '@/router'
const router = useRouter()
const selectedKeys = ref<string[]>(['/']);
const collapsed = ref<boolean>(false);
/** 用户子菜单内部路由跳转回显主应用选中状态
* @param {object} to 即将要进入的路由
* @param {object} from 正要离开的路由
* @param {string} name 子应用的name
* @return cancel function 解绑路由监听函数
*/
microApp.router.beforeEach((to, from, name) => {
selectedKeys.value = [to.pathname]
})
// 用户点击菜单时控制基座应用跳转
function select(item: any) {
// 获取子应用appName
const appName = item.app
router.push(item.path)
if (item.app !== 'main') {
// 判断子应用是否存在
if (getActiveApps().includes(appName)) {
// 子应用存在,控制子应用跳转
microApp.router.replace({
name: appName,
path: item.path,
})
} else {
// 子应用不存在,设置defaultPage,控制子应用初次渲染时的默认页面
microApp.router.setDefaultPage({
name: appName, path: item.path
})
}
}
}
</script>
<style scoped>
.trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
.trigger:hover {
color: #1890ff;
}
.logoc {
height: 32px;
background: rgba(255, 255, 255, 0.3);
margin: 16px;
}
.site-layout .site-layout-background {
background: #fff;
}
</style>
5. 嵌入子应用
// views/AppOne.vue
<template>
<div>
<micro-app name='child-app-1' url='http://localhost:4002/child-app-1/' :data="data" inline iframe
@datachange='handleDataChange'></micro-app>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const data = ref({
type: '发送给子应用的数据'
})
function handleDataChange(e: CustomEvent): void {
console.log('来自子应用 child-vite 的数据:', e.detail.data)
}
</script>
<style scoped></style>
四、构建子应用
1. 安装 `@micro-zoe/micro-app ,并修改package.json的name值
npm i @micro-zoe/micro-app --save
// package.json
"name": "@app/micro-project-a",
2, 在main.ts 中注册加载
import './assets/main.css'
import { createApp, } from 'vue'
import { createRouter, createWebHistory, } from 'vue-router'
import { routes } from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
import App from './App.vue'
declare global {
interface Window {
microApp: any
mount: CallableFunction
unmount: CallableFunction
__MICRO_APP_ENVIRONMENT__: string
__MICRO_APP_BASE_ROUTE__: string
__MICRO_APP_NAME__: string
__MICRO_APP_PUBLIC_PATH__: string
__MICRO_APP_BASE_APPLICATION__: string
}
}
let app: any = null
let router: any = null
let history: any = null
function handleMicroData() {
console.log('child-vite getData:', window.microApp?.getData())
// 监听基座下发的数据变化
window.microApp?.addDataListener((data: any) => {
console.log('child-vite addDataListener:', data)
}, true)
// 向基座发送数据
setTimeout(() => {
window.microApp?.dispatch({ myname: 'child-vite' })
}, 3000)
}
// 将渲染操作放入 mount 函数
window.mount = (data: any) => {
history = createWebHistory(window.__MICRO_APP_BASE_ROUTE__ || import.meta.env.BASE_URL)
router = createRouter({
history,
routes,
})
app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.use(Antd)
app.mount('#vite-app')
handleMicroData()
}
// 将卸载操作放入 unmount 函数
window.unmount = () => {
app && app.unmount()
history && history.destroy()
app = null
router = null
history = null
console.log('微应用vite卸载了 -- UMD模式');
}
// 非微前端环境直接渲染
if (!window.__MICRO_APP_ENVIRONMENT__) {
window.mount()
}
3. 修改 vite.config.ts
// 添加base路径
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { writeFileSync } from 'node:fs'
import { join } from 'node:path'
export default defineConfig({
base: '/child-app-1/',
plugins: [
vue(),
vueJsx(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
port: 4002,
host: true,
headers: {
'Access-Control-Allow-Origin': '*'
},
// Allow services to be provided for non root directories of projects
fs: {
strict: false
},
}
})
4. 修改index.html, 修改根div的id为 vite-app, 为防止多个相同id造成microapp报错提示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="vite-app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
5. 监听路由变化, 根据基座应用跳转选中路由菜单
- 创建子应用路由
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Home from '../views/home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/element-plus',
name: 'element-plus',
component: () => import(/* webpackChunkName: "element-plus" */ '../views/element-plus.vue')
},
{
path: '/ant-design-vue',
name: 'ant-design-vue',
component: () => import(/* webpackChunkName: "ant-design-vue" */ '../views/ant-design-vue.vue'),
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
export {
routes
}
- 修改App.vue
<script setup lang="ts">
import { RouterView, useRouter } from 'vue-router'
import { watch, ref } from 'vue';
const router = useRouter()
const activeIndex = ref('/')
watch(() =>
router.currentRoute.value.path,
(toPath) => {
activeIndex.value = toPath
window.microApp.dispatch({ toPath })
}, { deep: true })
</script>
<template>
<div>
<div>
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" router>
<el-menu-item index="/">home</el-menu-item>
<el-menu-item index="/element-plus">element-plus</el-menu-item>
<el-menu-item index="/ant-design-vue">ant-design-vue</el-menu-item>
</el-menu>
</div>
<RouterView />
</div>
</template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style>
五、示例
六、参考资料:
- micro-app官网: zeroing.jd.com/docs.html#/
- 浅入深出的微前端MicroApp | 京东云技术团队
- 项目源码: github.com/1514100951/…