前言: 微前端的概念是由ThoughtWorks在2016年提出的,它借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
它主要解决了两个问题:
- 随着项目迭代应用越来越庞大,难以维护。
- 跨团队或跨部门协作开发项目导致效率低下的问题
基于vite的实现: 首先创建一个主应用和一个子应用
yarn add @vitejs/app microapp_tool --template vue-ts
或
npm init @vitejs/app microapp_tool --template vue-ts
这是一个主应用
yarn add @vitejs/app spoon_tool --template vue-ts
或
npm init @vitejs/app spoon_tool --template vue-ts
这是一个子应用
子应用: router/index.ts
* @param {Function} ...
*/
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import cubetoolPath from "./modules/cube"
const routes: RouteRecordRaw[] = [
{ path: "/ceshi", name: "ceshi", component: () => import("@/views/output/ceshi.vue") },
{ path: '/host', name: "host", component: () => import("@/components/spoon/handsontable.vue") },
{ path: '/table', name: "table", component: () => import("@/components/spoon/EditableProTable.vue") },
{
path: '/cube', name: 'cube', component: () => import('@/views/output/cube.vue'),
children: [...cubetoolPath]
},
{path:"/",redirect:"/cube"}
]
const options = {
// 👇 设置基础路由,子应用可以通过window.__MICRO_APP_BASE_ROUTE__获取基座下发的baseroute,如果没有设置baseroute属性,则此值默认为空字符串
history: createWebHashHistory(),
routes,
}
const router = createRouter(options)
export default router
这里要注意的是:路由模式要用hash模式 即:history: createWebHashHistory()
vite.config.ts
import vue from '@vitejs/plugin-vue'
//import hook reactive ref....
import AutoImport from "unplugin-auto-import/vite"
//import ele-plus....
import Components from "unplugin-vue-components/vite"
//elementplus
import { ElementPlusResolver } from "unplugin-vue-components/resolvers"
//es6-modules 语法解析
import path from 'path'
import { resolve, join } from "path"
import { writeFileSync } from "fs"
export default (({ mode }) => {
//检查process.cwd()路径下.env.development....
loadEnv(mode, process.cwd())
const { VITE_APP_BASE_API, VITE_BASE_API, VITE_ENV } = loadEnv(mode, process.cwd());
// https://vitejs.dev/config/
return defineConfig({
base: `${VITE_ENV === 'production' ? 'http://my-site.com' : ''}/spoon_tool/`,//baseName 主应用所需
plugins: [
vue(),
//ele 按需导入
AutoImport({
//ref 、reactive.....
imports: ['vue', 'vue-router'],
dts: "src/auto-imports.d.ts",
// ele-plus
resolvers: [ElementPlusResolver()]
}),
Components({
// ele..
resolvers: [ElementPlusResolver()]
}),
// 自定义插件 micro-app微前端 子应用 配置
(function () {
let basePath = ''
return {
name: "vite:micro-app",
apply: 'build',
configResolved(config) {
basePath = `${config.base}${config.build.assetsDir}/`
},
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)
}
}
}
},
}
})(),
],
resolve: {
alias: {
"@": resolve(__dirname, "src"),
"@assets": resolve(__dirname, "src/assets"),
"@store": resolve(__dirname, "src/store"),
"@views": resolve(__dirname, "src/views")
}
},
css: {
preprocessorOptions: {
less: {
modifyVars: {
hack: `true; @import (reference) "${path.resolve("src/assets/css/base.less")}";`,
},
javascriptEnabled: true,
},
},
},
server: {
// port: 3000,
// proxy: {
// [VITE_BASE_API]: {
// target: VITE_APP_BASE_API, // 实际请求地址
// changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ""),
// },
// },
port: 8081,//主应用所挂载的url端口
headers: {
'Access-Control-Allow-Origin': '*',//子应用必须开启跨域,否则无法访问
}
},
})
})
这里主要注意的是跨域处理、端口号定义、自定义插件的应用
index.html
模板ID的修改,这里是为了不与主应用ID发生冲突,当然,对应的main.ts也得保持一致
app.mount('#my_vite_app')
主应用:
yarn add @micro-zoe/micro-app
router/index.ts
* @param {Function} ...
*/
import { createRouter,createWebHistory, Router, RouteRecordRaw } from 'vue-router'
import MyPage from '../views/spoon_tool.vue'
const routes: RouteRecordRaw[] = [
{
//严格模式
path: "/spoon_tool/:page*",
name: "spoon_tool",
component: MyPage
}
]
const options = {
// 👇 设置基础路由,子应用可以通过window.__MICRO_APP_BASE_ROUTE__获取基座下发的baseroute,如果没有设置baseroute属性,则此值默认为空字符串
history: createWebHistory(),
// base: process.env.BASE_URL,
routes,
}
const router: Router = createRouter(options)
export default router
这里要注意的是所需的path为子应用的baseName值,其路由模式要为history模式
views/spoon_tool.vue
<template>
<div class="base">
<!--
name(必传):应用名称
url(必传):应用地址,会被自动补全为http://localhost:3000/index.html
baseroute(可选):基座应用分配给子应用的基础路由,就是上面的 `/my-page`
-->
<micro-app class="micro-app" disableScopecss disableSandbox inline style="height: 100%;" name='spoon_tool' url='http://localhost:8081/spoon_tool/#/cube' baseroute='/my-page'></micro-app>
</div>
</template>
<style lang="less" scoped>
.base{
width: 100%;
height: 100%;
// background: red;
display: flex;
flex-direction: column;
h1{
background: pink;
}
.micro-app{
flex: 1;
}
}
</style>
这里的micro-app标签中对应子应用的url
main.ts
import './style.css'
import App from './App.vue'
import router from "./router/index"
// import "./public-path"
import ElementPlus from "element-plus"
import "element-plus/theme-chalk/index.css"
// 鼠标右键
import contextmenu from "v-contextmenu";
import "v-contextmenu/dist/themes/default.css";
//ele icon
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
//pinia
import pinia from "./store/index"
//i18n多语言配置
import i18n from "./lang/index"
//sql
// import Codemirror from "codemirror-editor-vue3"
// import Codemirror from "codemirror-editor-vue3";
import microApp from "@micro-zoe/micro-app"
microApp.start({
plugins: {
modules: {
// appName即应用的name值
'spoon_tool': [{
loader(code) {
if (import.meta.env.VITE_ENV === 'development') {
// 这里 basename 需要和子应用vite.config.js中base的配置保持一致
code = code.replace(/(from|import)(\s*['"])(\/spoon_tool\/)/g, all => {
return all.replace('/spoon_tool/', 'http://localhost:8081/spoon_tool/')
})
}
return code
}
}]
}
}
})
/**
* @params 解决Chrome报错 Added non-passive event listener to a scroll-blocking <some> event. Consider marking event handler as ‘passive’ to make the page more responsive. See <URL>
* @Desc 通过添加 passive,来阻止 touchstart 事件 提高滚动性能和防止滚动阻塞
*/
// import "default-passive-events"
const app = createApp(App)
//导入所有图标进行全局注册
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(router)
app.use(ElementPlus)
app.use(pinia)
app.use(i18n)
app.use(contextmenu)
// app.use(Codemirror)
app.mount('#app')
// 监听卸载操作
window.addEventListener('unmount', function () {
app.unmount()
})
效果: