什么是微前端
微前端
,听这名字感觉很超前,其实已经有很多公司都已经用上了该技术,例如我的项目组也用上了,跟后端的微服务很类似,用作解耦。现在的网站开发日渐复杂,通常会有很多独立的模块组成,分成不同的业务组,在之前用的是iframe
进行嵌套。随着技术的发展现在出现了微前端的概念,目前微前端框架有很多,如single-spa
qiankun(阿里)
Micro App(京东)
wujie(腾讯)
擎天(vivo)
等
在微前端的架构下,需要一个主应用,也可以叫基座应用
,其次每一个独立的模块都可以叫做子应用
,子应用和主应用不受框架的限制,可以用任何框架任何技术。
举个栗子
在这张图我们可以把菜单和头部看做是一个主应用
,然后菜单中的每一个模块都可以看做一个子应用(也就是每一个单独的项目)
,这样就可以就行解耦,并且应用之间是可以进行通讯的。
这一章我们主要体验一下micro-app
支持一下自家产品。他的原理也很简单,就是把子应用做成一个webComponents
进行沙箱隔离,应用之间的通讯用的是CustomEvent
开整
在开整之前得吐槽一下,在github的issues提的很多问题,得不到及时回复,平时忙也能理解,如果使用vite
将变的比较复杂,做好准备。
tips:当子应用是vite应用时需要做特别的适配,适配vite的代价是巨大的,我们必须关闭沙箱功能,因为沙箱在module script
下不支持,这导致大部分功能失效,包括:环境变量、样式隔离、元素隔离、资源路径补全、baseroute 等。
1.框架我这边主子应用
使用vite+vue3
,大家可以随意选择框架。
2.目录结构mfe->主应用,web放子应用,子应用可以有多个第一个是main
3.主子应用安装依赖npm install 然后 启动主应用,和子应用使用 npm run dev。启动完成之后我这边主应用端口是5173,子应用是5174,当然也可以自己配置不要冲突就行。
4.主应用
安装micro-app
npm install @micro-zoe/micro-app
5.主应用
在views新建一个文件如views/page.vue
,子应用修改vite.config.ts
主子应用不要搞错, 子应用的vite.config.ts 增加代码
tips:如果是production记得换成服务器的地址,如果是本地增加一个自定义前缀随便写我这儿写的basename
export default defineConfig({ base: `${process.env.NODE_ENV === 'production' ? 'http://xxxxx.com' : ''}/basename/`, plugins:[vue()] })
6.在刚才主应用新建的page.vue 加入以下代码
<template> <div> <h1>子应用</h1> <micro-app disable-sandbox disable-scopecss name='main' :url='url'></micro-app> </div> </template> <script lang="ts" setup> import { ref, reactive } from "vue" let url = ref('') if (import.meta.env.MODE == "development") { url.value = "http://localhost:5174/basename" } else { url.value = "http://xxxxxxxx/basename" } </script> <style lang="less" scoped> </style>
解释一下 micro-app 是个自定义组件可以直接用,如果报错可以配置以下代码忽略警告
主应用的vite.config.ts 修改
vue({ template: { compilerOptions: { isCustomElement: tag => /^micro-app/.test(tag) } } })
其次是必须要设置 disable-scopecss 这个属性取消沙箱,不然打包之后会有乱码,也就是必须得放弃沙箱。
url 就是子应用的地址,本地的地址和服务器地址做个切换。
7.主应用
配置路由 主应用使用history模式,子应用换成hash模式createWebHashHistory
import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/vue3:page*', //自定义地址 component: ()=> import('@/views/page.vue') //刚才主应用创建的page.vue }, ] }) export default router
8.主应用加载子应用 main.ts
import { createApp } from 'vue' import App from './App.vue' import router from './router' import microApp from '@micro-zoe/micro-app' const app = createApp(App) app.use(router) app.mount('#mfe') microApp.start({ plugins:{ modules:{ main: [{ loader(code) { if (import.meta.env.MODE === 'development') { // 这里 basename 需要和子应用vite.config.js中base的配置保持一致 code = code.replace(/(from|import)(\s*['"])(\/basename\/)/g, all => { return all.replace('/basename/', 'http://localhost:5174/basename/') }) } return code } }] } } })
固定写法,有额外的子应用只需要改一下code.replace
正则和all.replace
第一个参数,以及第二个参数对应的地址就好,其他不用动
tips:注意modules下面的名字需要跟micro-app 的name对应 我这儿都是main
tips:主子应用不能使用同一个#App名字 我这儿主应用
的名称换成了mfe,app.mount('#mfe')
<!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>微前端</title> </head> <body> <!--名字要变--> <div id="mfe"></div> <script type="module" src="/src/main.ts"></script> </body> </html>
访问主应用你会发现子应用已经出来了
tips:注意我得路由写的是/vue3
坑来了:你的图片如果加载不出来,加载静态资源的方式变了
//js获取 const logo = new URL('./assets/logo.png',import.meta.url).href //template使用 <img alt="Vue logo" class="logo" :src="logo" width="125" height="125" />
如果还出不来,我也遇到了 需要把vite的版本降级为3.0.0方可好使
上线
打包主应用npm run build 记得换成服务器地址之前提到的
子应用需要修改vite.config.ts
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import {join} from 'path' import {writeFileSync} from 'fs' // https://vitejs.dev/config/ export default defineConfig({ base: `${process.env.NODE_ENV === 'production' ? '上线地址' : ''}/basename/`, plugins: [vue(), vueJsx(),(function () { let basePath = '' return { name: "vite:micro-app", apply: 'build', configResolved(config:any) { basePath = `${config.base}${config.build.assetsDir}/` }, writeBundle (options:any, bundle:any) { 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:any, $1:any, $2:any, $3:any) => { return all.replace($3, new URL($3, basePath)) }) const fullPath = join(options.dir, chunk.fileName) writeFileSync(fullPath, chunk.code) } } } }, } })() as any], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, build:{ outDir:"../basename" } })
解释一下 这是micro-app写的一个vite插件主要是子应用支持微前端做的直接贴进去就行了
outDir打包之后我输出到上层目录这个随意
子应用然后进行打包 npm run build
服务器我用的是宝塔方便
目录结构 主应用放到最外面,子应用直接把文件夹拖进去我得是basename
修改nginx代理
location /{ 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/ /index.html; } location /basename { 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/ /basename/index.html; }
上线预览