前言:
微前端的概念是由ThoughtWorks在2016年提出的,它借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
它主要解决了两个问题:
- 1、随着项目迭代应用越来越庞大,难以维护。
- 2、跨团队或跨部门协作开发项目导致效率低下的问题
基于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 { defineConfig, loadEnv } from 'vite' 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
<div id="my_vite_app"></div>
模板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
<!-- my-page.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 { createApp } from 'vue' 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() })
microApp 的引入与使用
效果: