qiankun+vue3+vite+ts+lerna实现微前端

709 阅读3分钟

新项目需要主应用包含多个子系统,我使用的是qiankun+vue3+vite+ts+lerna, 并使用pnpm。

参考了github一位大佬的代码。 github.com/kakajun/qia…

首先安装lerna

npm install -g lerna

1.创建一个空文件夹 my-project

cd my-project

初始化 Lerna 项目:

lerna init

3.在 packages 目录下创建主应用程序和子应用程序

进入packages,初始化 Vue 3 +vite + ts项目:

cd packages 
npx create-vite@latest main
npx create-vite@latest sub-app1

4.在根目录的 lerna.json 文件写:

{

  "version": "0.0.0",

  "packages": ["packages/*"],

  "useWorkspaces": true,

  "npmClient": "pnpm"

}

5.根目录和主项目安装qiankun,子项目需要安装vite-plugin-qiankun。

6.根目录安装npm-run-all来启动所有应用。 以下是根目录package.json配置

{
  "name": "root",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "dev": " npm run start",
    "start": "npm-run-all --parallel start:*",
    "start:sub-app1": "cd packages/sub-app1 && npm start",
    "start:main": "cd packages/main && npm start",
    "build": "npm-run-all build:* && bash ./bundle.sh",
    "build:sub-app1": "cd packages/sub-app1 && npm run build",
    "build:main": "cd packages/main && npm run build"
  },
  "devDependencies": {
    "lerna": "^6.6.2"
  },
  "dependencies": {
    "npm-run-all": "^4.1.5",
    "qiankun": "^2.10.11"
  }
}

以下是关于主应用的编写

主应用程序中注册子应用程序:新建micro-app.ts

const microApps = [
  {
    name: "sub-app1",
    entry: process.env.NODE_ENV === 'development'?"//localhost:7200/":"测试环境地址",
    activeRule: "/sub-app1",
  }
];

const apps = microApps.map((item) => {
  return {
    ...item,
    container: "#cnbi-viewport", // 子应用挂载的div
    props: {
      routerBase: item.activeRule, // 下发基础路由
    },
  };
});

export default apps;

在main.ts中引入。 main.ts代码:

import { createApp } from 'vue'
import App from './App.vue'
import  '@/style/index.scss';
import { registerMicroApps, start } from 'qiankun';
import microApps from "./micro-app";
import router from "./router/index.ts";
const instance= createApp(App).use(router).mount('#app')


function loader(loading: any) {
  if (instance.isLoading) {
    instance.isLoading = loading
  }
  }
// 给子应用配置加上loader方法
const apps = microApps.map(item => {
    return {
      ...item,
      loader
    };
  });
registerMicroApps(apps, {
    beforeLoad: (app: any) => {
      console.log("before load app.name====>>>>>", app.name);
    },
    beforeMount: [
      (app: any) => {
        console.log("[LifeCycle] before mount %c%s", "color: green;", app.name);
      }
    ],
    afterMount: [
      (app: any) => {
        console.log("[LifeCycle] after mount %c%s", "color: green;", app.name);
      }
    ],
    afterUnmount: [
      (app: any) => {
        console.log("[LifeCycle] after unmount %c%s", "color: green;", app.name);
      }
    ]
  });
  start();

我的项目是从登录页进入,到主页面,主页面有子系统的入口,进入子系统。

image.png

image.png

image.png

APP.vue代码

<template>
  <router-view />
  <div id="cnbi-viewport"></div>
</template>

登录页代码

<template>
  <div class="login">
    <button class="btn" @click="login">登录</button>
  </div>
</template>
<script setup lang="ts">
function login() {  history.pushState(null, '/home','/home');}

</script>
<style scoped>
</style>

Home页代码



<template>
  <div class="mainapp">
    <div class="mainapp-main">
      <div class="footer">
        <div class="app-item pointer" @click="push('/sub-app1')">系统1</div>
        <div class="app-item pointer" @click="push('/sub-app2')">系统2</div>
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
function push(subapp: any) { history.pushState(null, subapp, subapp); }

</script>
<style scoped>
</style>

router.ts代码

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'


const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    redirect: '/login'
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import(/* webpackChunkName: "home" */ '../views/MainLogin.vue'),
    meta: {
      title: '登录页'
    }
  },
  {
    path: '/home',
    name: 'Home',
    component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue'),
    meta: {
      title: '首页'
    }
  },
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

注意:路由用history模式,别用hash,hash模式从子应用点击退回箭头,主应用会显示空白。(子应用也要使用history)。

以下是关于子应用sub-app1的编写

main.ts


import { createApp } from 'vue'
import App from './App.vue'
import routes from './router/index.ts';
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';
import { createRouter, createWebHistory } from 'vue-router';
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

function setDomain() {
  window.ISNK = document.domain.indexOf('172') > -1 // 如果是172客户的域名,那就拿客户地址,自动判断,这里搞个全局判断标志
  window.ORIGIN =
    process.env.NODE_ENV === 'development'
      ? process.env.VITE_ORIGIN_DEV
      : window.ISNK
        ? process.env.VITE_ORIGIN_PRO
        : process.env.VITE_ORIGIN_PRO_TEST;
  console.log(window.ORIGIN, "window.ORIGIN");
}
//  设置主域名,但不跟随基座端口变化而变化
setDomain()
let router = null;
let instance = null;
let history = null;
function render(props = {}) {
  const { container } = props;
  history = createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? '/sub-app1' : '/');
  //   console.log(history,qiankunWindow.__POWERED_BY_QIANKUN__ ,"history");
  router = createRouter({
    history,
    routes,
  });

  instance = createApp(App).use(ElementPlus);
  instance.use(router);
  //   instance.use(store);
  instance.mount(container ? container.querySelector('#app') : document.getElementById("app"));
  if (qiankunWindow.__POWERED_BY_QIANKUN__) {
    console.log('我正在作为子应用运行')
  }
}

// some code
renderWithQiankun({
  mount(props) {
    console.log("viteapp mount");
    render(props);
    // console.log(instance.config.globalProperties.$route,"444444444");
  },
  bootstrap() {
    console.log('bootstrap');
  },
  unmount(props) {
    console.log("vite被卸载了");
    instance.unmount();
    instance._container.innerHTML = '';
    history.destroy();// 不卸载  router 会导致其他应用路由失败
    router = null;
    instance = null;
  },
});

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render();
}

一开始我使用npm下载element-plus,element-plus一直报错,后删除了node_modules并重新用pnpm安装就可以使用。

vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun';
import { resolve } from "path";
import { loadEnv } from 'vite'
// useDevMode 开启时与热更新插件冲突
const useDevMode = true     // 如果是在主应用中加载子应用vite,必须打开这个,否则vite加载不成功, 单独运行没影响

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd())
  let config = {
        plugins: [vue(),qiankun('sub-app1', {useDevMode })],
        resolve: {
          extensions: ['.js', '.vue', '.json'],
            alias: {
              '@': resolve('src'),
              },
          },
        server: {
        host: '0.0.0.0' ,// 暴露内网ip
        port: 7200,
        cors: true,
        },
      define: {
        'process.env': env
      }
    }
  return config
})

APP.vue


<template>
  <img alt="Vue logo"
       :src="imgSrc" />
  <router-link to="/">Home</router-link> |
  <router-link to="/about">About</router-link>
  <router-view />
  <HelloWorld msg="Hello Vue 3 + Vite" />
  <el-button>123</el-button>
</template>
<script lang="ts">
import HelloWorld from './components/HelloWorld.vue'
  export default {
    name: 'About',
    components: {
      HelloWorld
    },
    computed:{
      imgSrc(){
        return  window.ORIGIN+"/src/assets/timg.jpeg"
      }
    }
  }
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

子项目加载图片会出现问题,大佬给的办法是使用window.ORIGIN。

Home.vue

<template>
  <h1>This is home page111</h1>
  <el-button type="primary" @click="goBack">返回首页</el-button>
</template>

<script setup>
const goBack = () => {
  history.pushState(null, '/home','/home');
}
</script>

<style>
</style>

打包配置部署

部署遇到了好多问题:

1.vite不识别process.env,网上说的import.meta.env也不能用,还有说dotenv.config()可以直接导入,我这里试了也没用,最后用dotenv和fs手动导入。

2.webpack的是要用webpack-publicpath,这里vite是在base里面改。

3.部署后,主应用里的子应用刷新后会空白,实际上是nginx配置的问题,点击主应用里的子应用是用项目里的路由,但是刷新后走的是nginx里的路由配置。location/sub-app1要指向父应用,所以不需要额外的location/sub-app1。

4.主应用进入子应用,刷新报错Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.问题是找不到文件,解决:把主应用的vite.config.ts的base从'./'改成'/'。

解决:在vite中配置

子应用vite.config.ts

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import qiankun from "vite-plugin-qiankun";
import { resolve } from "path";
import { loadEnv } from "vite";
import * as dotenv from 'dotenv'
import * as fs from 'fs'

// useDevMode 开启时与热更新插件冲突
const useDevMode = true; // 如果是在主应用中加载子应用vite,必须打开这个,否则vite加载不成功, 单独运行没影响

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd());
  const isEnvProduction = mode === 'production';
    // 利用dot 加载.env对应的文件
  // vite无法直接使用process.env,需要手动加载
  const envFiles = [
    /** mode file */ `.env.${mode}`
  ]
  for (const file of envFiles) {
    const envConfig = dotenv.parse(fs.readFileSync(file))
    for (const k in envConfig) {
      process.env[k] = envConfig[k]
    }
  }
  let config = {
    base:   mode === 'production' ? process.env.VITE_PUBLIC_PATH: '/',
    plugins: [vue(), qiankun("sub-app1", { useDevMode })],
    resolve: {
      extensions: [".js", ".vue", ".json"],
      alias: {
        "@": resolve("src"),
      },
    },

    server: {
      host: "0.0.0.0", // 暴露内网ip
      port: 7200,
      cors: true,
      headers: {
        "Access-Control-Allow-Origin": "*",
      },
    },
    build: {
      target: "es2015",
      outDir: "dist",
      assetsDir: "assets",
      assetsInlineLimit: 2048,
      cssCodeSplit: true,
      sourcemap: false,
      brotliSize: false,
      minify: !isEnvProduction ? "esbuild" : "terser",
      terserOptions: {
        compress: {
          drop_console: true,
          drop_debugger: true,
        },
      },
      chunkSizeWarningLimit: 1000, // 将警告限制调整为 1000KB
      rollupOptions: {
        output: {
          manualChunks: (id) => {
            // 在这里手动配置模块的拆分规则
            if (id.includes("node_modules")) {
              return id
                .toString()
                .split("node_modules/")[1]
                .split("/")[0]
                .toString();
            }
          },
        },
      },
    },
    define: {
      "process.env": env,
    },
  };
  return config;
});

nginx配置文件:

server {
        listen       9010;
        server_name  localhost;

        location / {
           # 允许跨域的请求,可以自定义变量$http_origin,*表示所有
    	   add_header 'Access-Control-Allow-Origin' *;
    	   # 允许携带cookie请求
    	   add_header 'Access-Control-Allow-Credentials' 'true';
    	   # 允许跨域请求的方法:GET,POST,OPTIONS,PUT
    	   add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT';
    	   # 允许请求时携带的头部信息,*表示所有
   	   add_header 'Access-Control-Allow-Headers' *;
  	   # 允许发送按段获取资源的请求
   	   add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    	   # 一定要有!!!否则Post请求无法进行跨域!
    	

           root   /usr/local/nginx/myproject/main/html;
           index  index.html;      
	   try_files $uri $uri/ /index.html;    
         }


        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }


    }

server {
        listen       9011;
	server_name  localhost;

        location / {
           # add_header 'Access-Control-Allow-Origin' 'http://192.168.16.86:9010';
           #允许跨域访问
           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';
           root   /usr/local/nginx/myproject/sub-app1/html;
           index  index.html;      
	   try_files $uri $uri/ /index.html;    
        }
    }