微前端
最近项目基本都脱离webpack而采用vite,对比之前的微前端方案,需要有一些调整。这里使用的方案是qiankun,对比其他一些方案,主要优势是qiankun相对成熟,也有过经验。但是官网对于vite的文档似乎没有更新。趁着这次机会,从头梳理一遍微前端项目的搭建。
主应用
技术栈:vite + vue3
应用改造
省略创建应用的工程,先贴上项目结构。
1. 安装qiankun
npm i qiankun -S
2. 子应用管理
新建文件夹qiankun,集中管理,新建config.js
,管理所有子应用。
// src/qiankun/config.js
export const subApps = [
{
name: import.meta.env.VITE_API_NAME_REACT, // 子应用名称
entry: import.meta.env.VITE_API_ENTRY_REACT, // 子应用入口,本地环境下指定端口
container: '#subApp', // 挂载子应用的dom
activeRule: import.meta.env.VITE_API_ACTIVERULE_REACT, // 路由匹配规则
props: {} // 主应用与子应用通信传值
},
{
name: import.meta.env.VITE_API_NAME_VUE, // 子应用名称
entry: import.meta.env.VITE_API_ENTRY_VUE, // 子应用入口,本地环境下指定端口
container: '#subApp', // 挂载子应用的dom
activeRule: import.meta.env.VITE_API_ACTIVERULE_VUE, // 路由匹配规则
props: {} // 主应用与子应用通信传值
}
]
这里使用了环境变量,主要是为了方便本地开发和线上部署。
同域部署
# react子应用
VITE_API_NAME_REACT = microReact
VITE_API_ENTRY_REACT = http://localhost:9527/micro-react/
VITE_API_ACTIVERULE_REACT = /main/micro-react
# vue子应用
VITE_API_NAME_VUE = microVUE
VITE_API_ENTRY_LY = http://localhost:9527/micro-vue/
VITE_API_ACTIVERULE_LY = /main/micro-vue
异域部署
# react子应用
VITE_API_NAME_REACT = microReact
VITE_API_ENTRY_REACT = http://localhost:9528/
VITE_API_ACTIVERULE_REACT = /micro-react
# vue子应用
VITE_API_NAME_VUE = microVUE
VITE_API_ENTRY_LY = http://localhost:9529/
VITE_API_ACTIVERULE_LY = /micro-vue
3. 子应用注册
这里我使用了vue3的hooks,没什么用,可以使用普通的export/import方式来使用。
// src/qiankun/qiankunHooks.js
import { registerMicroApps, start } from 'qiankun'
import { subApps } from './config'
const useQiankunHooks = () => {
const registerApps = () => {
try {
registerMicroApps(subApps, {
beforeLoad: [
app => {
console.log('before load app.name====>>>>>', app.name)
}
],
beforeMount: [
app => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name)
}
],
afterMount: [
app => {
console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name)
}
]
})
} catch (e) {
console.log(e)
}
}
const initQiankun = () => {
if (!window.qiankunStarted) {
registerApps()
start({
prefetch: 'all',
sandbox: {
experimentalStyleIsolation: true // 样式隔离(非严格模式)
},
excludeAssetFilter: assetUrl => { // 排除子应用引用资源对主应用的影响,例如QQ地图之类的第三方cdn
const witeList = ['map.qq.com']
if (witeList.some(el => assetUrl.includes(el))) {
return true
}
}
})
}
}
return { initQiankun }
}
export default useQiankunHooks
4. layout改造
在layout文件夹里新建一个EmptyComponent.vue
文件,作为子应用的component路径,里面放空即可。
// eslint-disable-next-line vue/valid-template-root
<template></template>
在main.vue
文件改造如下
<template>
<el-scrollbar ref="scrollbarRef">
<div class="g-app-main">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
<div id="subApp"></div>
</div>
</el-scrollbar>
</template>
<script setup>
import useQiankunHooks from '@/qiankun/qiankunHooks'
const { initQiankun } = useQiankunHooks()
onBeforeMount(() => {
initQiankun()
})
</script>
5. 路由使用
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'layout',
redirect: '/login',
component: () => import('@/layout/Index.vue'),
children: [
{
path: '/welcome',
name: 'welcome',
component: () => import('@/views/Welcome/Index.vue')
},
// 子应用链接
{
path: '/micro-react',
name: 'react子应用',
meta: {
title: 'react子应用',
icon: 'iconfont icon-dashebord'
},
children: [
{
path: '/micro-react/home',
name: 'react子应用首页',
component: () => import('@/layout/EmptyComponent.vue'),
meta: {
title: 'react首页'
}
},
{
path: '/micro-react/list',
name: 'react子应用列表',
component: () => import('@/layout/EmptyComponent.vue'),
meta: {
title: 'react列表'
}
},
{
path: '/micro-react/123',
name: 'react子应用404',
component: () => import('@/layout/EmptyComponent.vue'),
meta: {
title: 'react无权限'
}
}
]
},
// 子应用链接
{
path: '/micro-vue',
name: 'vue子应用',
meta: {
title: 'vue子应用',
icon: 'iconfont icon-dashebord'
},
children: [
{
path: '/micro-vue/home',
name: 'vue子应用首页',
component: () => import('@/layout/EmptyComponent.vue'),
meta: {
title: 'vue首页'
}
},
{
path: '/micro-vue/list',
name: 'vue子应用列表',
component: () => import('@/layout/EmptyComponent.vue'),
meta: {
title: 'vue列表'
}
}
]
},
{
path: '/redirect/:pathMatch(.*)*',
name: 'redirect',
component: () => import('@/views/Redirect/Index.vue')
},
{
path: '/404',
name: 'notfound',
component: () => import('@/views/Error/NotFound.vue')
}
]
},
{
path: '/login',
name: 'login',
component: () => import('@/views/Login/Index.vue')
}
]
const router = createRouter({
history: createWebHistory('/main/'), // 主应用同域名部署使用main作为二级域名
routes
})
export default router
6.应用子应用代理
一般情况下,这里是不需要配置,但是如果涉及到子应用在开发环境中使用qiankun,就需要考虑代理。需要把子应用的代理也加入到主应用里面
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [
vue()
],
server: {
host: '0.0.0.0',
cors: true,
port: 9527,
headers: {
'Access-Control-Allow-Origin': '*'
},
proxy: {
'^/reactApi': {
target: env.VITE_API_ENTRY_REACT,
rewrite: path => path.replace(/^\/reactApi/, ''),
changeOrigin: true
},
'^/vueApi': {
target: env.VITE_API_ENTRY_VUE,
rewrite: path => path.replace(/^\/vueApi/, ''),
changeOrigin: true
}
}
}
}
})
服务器部署
1. 同域部署
server
listen 9527;
server_name microApp;
location /main {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
alias /usr/local/nginx/www/micro_main;
index index.html index.htm;
try_files $uri $uri/ /main/index.html;
}
2. 异域部署
server
listen 9527;
server_name microApp;
location /main {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
alias /usr/local/nginx/www/micro_main;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
子应用改造
这里展示的应用都是基于vite
,所以使用vite-plugin-qiankun
插件,安装插件。
npm i vite-plugin-qiankun
React子应用
1. 修改vite.config.ts配置
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// 1. vite环境下安装qiankun插件
import qiankun from "vite-plugin-qiankun";
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const isDev = mode === 'development'
return {
plugins: [
// 2. 引用qiankun插件,需要注意,这里的microReact是子应用名称,需要和主应用中注册的子应用名称一致
qiankun("microReact", {
useDevMode: true,
}),
// 3. react()插件会跟vite-plugin-qiankun插件冲突,所以需要判断是否是开发环境
!isDev && react(),
],
// 4. 同域配置二级域名,异域配置不需要可修改为:isDev ? "/" : 'http://xxx.com/'
base: isDev ? "/micro-react/" : 'http://xxx.com/',
server: {
host: "0.0.0.0",
port: 9528,
headers: {
'Access-Control-Allow-Origin': '*'
},
origin: "http://localhost:9528",
proxy: {
'^/reactApi': {
target: 'http://localhost:9528/',
rewrite: path => path.replace(/^\/reactApi/, ''),
changeOrigin: true
}
}
},
}
})
2. 在src/main.tsx
中添加registerMicroApps
相关配置
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
// 1. 引用vite-plugin-qiankun
import { renderWithQiankun, qiankunWindow, QiankunProps } from 'vite-plugin-qiankun/dist/helper'
let root: ReactDOM.Root | null = null
// 2. qiankun渲染函数
function render(props: QiankunProps) {
const { container } = props
const root = ReactDOM.createRoot(
(container
? container.querySelector('#root')
: document.querySelector('#root')) as HTMLElement
)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)
return root
}
renderWithQiankun({
mount(props) {
root = render(props)
},
bootstrap() {
console.log('bootstrap')
},
unmount() {
root?.unmount()
},
update() {
},
})
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
root = render({})
}
3. 在src/router/index.tsx
中添加registerMicroApps
相关配置
// src/router/index.tsx
import React, { Suspense } from 'react'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
// 路由懒加载
const Home = React.lazy(() => import('../views/home'))
const List = React.lazy(() => import('../views/list'))
const NotFound = React.lazy(() => import('../views/notFound'))
export default function Router(){
return(
// 同域:判断是否为qiankun引用,给出不同路由;异域分别修改为 '/micro-react' : '/'
<BrowserRouter basename={qiankunWindow.__POWERED_BY_QIANKUN__ ? "/main/micro-react" : "/micro-react"}>
<Routes>
<Route path='/' element={<Suspense><Home/></Suspense>}></Route>
<Route path='/home' element={<Suspense><Home/></Suspense>}></Route>
<Route path='/list' element={<Suspense><List/></Suspense>}></Route>
{/* 定义404路由*/}
<Route path='/404' element={<Suspense><NotFound/></Suspense>}></Route>
{/* 未匹配的路由使用Navigate重定向到此页面 这里即notFound.jsx */}
<Route path='/*' element={<Navigate to='/404' />}></Route>
</Routes>
</BrowserRouter>
)
}
4. 引用路由
此处可以跟router.tsx合并,我这里做项目结构习惯了,所以分开。
// src/App.tsx
import Router from './router/index.tsx'
import './App.css'
function App() {
return (
<>
<Router></Router>
</>
)
}
export default App
5. typescript类型
需要类型的话,参考下面。
// src/qiankun.d.ts
declare global {
interface Window {
__POWERED_BY_QIANKUN__?: boolean;
__INJECTED_PUBLIC_PATH_BY_QIANKUN__?: string;
}
// 添加对 __webpack_public_path__ 的声明
const __webpack_public_path__: string;
}
export {};
Vue子应用
1. 修改vite.config.ts配置
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
// 1. vite环境下安装qiankun插件
import qiankun from 'vite-plugin-qiankun'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const isDev = mode === 'development'
return {
plugins: [
vue(),
// 2. 引用qiankun插件,需要注意,这里的microReact是子应用名称,需要和主应用中注册的子应用名称一致
qiankun('microVue', {
useDevMode: true
})
],
// 3. 同域配置二级域名,异域配置不需要可修改为:isDev ? "/" : 'http://xxx.com/'
base: isDev ? '/micro-vue/' : 'http://xxx.com/',
server: {
host: '0.0.0.0',
port: 9529,
headers: {
'Access-Control-Allow-Origin': '*'
},
origin: 'http://localhost:9529',
proxy: {
'^/vueApi': {
target: 'http://localhost:9529/',
rewrite: path => path.replace(/^\/vueApi/, ''),
changeOrigin: true
}
}
}
}
})
2. 在src/main.js
中添加registerMicroApps
相关配置
import { createApp } from 'vue'
import pinia from './pinia'
import router from './router'
import App from './App.vue'
import './permission'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
let app
const render = container => {
app = createApp(App)
app
.use(pinia)
.use(router)
.mount(container ? container.querySelector('#app') : '#app')
}
const initQianKun = () => {
renderWithQiankun({
mount(props) {
const { container } = props
render(container)
},
bootstrap() {},
unmount() {
app.unmount()
}
})
}
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render()
3. 在src/router/index.js
中添加registerMicroApps
相关配置
import { createRouter, createWebHistory } from 'vue-router'
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
const routes = [
{
path: '/',
name: 'layout',
redirect: '/login',
// 这里注意是如果作为子应用打开,就不使用layout布局,只渲染main主体部分即可
component: qiankunWindow.__POWERED_BY_QIANKUN__ ? () => import('@/layout/NullLayout.vue') : () => import('@/layout/Index.vue'),
children: [
{
path: '/welcome',
name: 'welcome',
component: () => import('@/views/Welcome/Index.vue')
},
{
path: '/redirect/:pathMatch(.*)*',
name: 'redirect',
component: () => import('@/views/Redirect/Index.vue')
},
{
path: '/404',
name: 'notfound',
component: () => import('@/views/Error/NotFound.vue')
}
]
},
{
path: '/login',
name: 'login',
component: () => import('@/views/Login/Index.vue')
}
]
const router = createRouter({
// 同域:判断是否为qiankun引用,给出不同路由;异域分别修改为 '/micro-vue' : '/'
history: createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? '/main/micro-vue/' : '/micro-vue/'),
routes
})
export default router
4. NullLayout.vue
<template>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</template>
服务器部署
- 同域nginx配置示例
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 9527;
server_name microApp;
location /main {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
alias /usr/local/nginx/www/micro_main;
index index.html index.htm;
try_files $uri $uri/ /main/index.html;
}
location /micro-react {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
alias /usr/local/nginx/www/micro_react;
index index.html index.htm;
try_files $uri $uri/ /micro-react/index.html;
}
location /micro-vue {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
alias /usr/local/nginx/www/micro_vue;
index index.html index.htm;
try_files $uri $uri/ /micro-vue/index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
2. 异域nginx配置
直接参考主应用配置即可。