微前端之vite + qiankun + vue3 + react18

1,187 阅读7分钟

微前端

最近项目基本都脱离webpack而采用vite,对比之前的微前端方案,需要有一些调整。这里使用的方案是qiankun,对比其他一些方案,主要优势是qiankun相对成熟,也有过经验。但是官网对于vite的文档似乎没有更新。趁着这次机会,从头梳理一遍微前端项目的搭建。

image.png

主应用

技术栈:vite + vue3

应用改造

省略创建应用的工程,先贴上项目结构。

image.png

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>

服务器部署

  1. 同域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配置

直接参考主应用配置即可。