新项目需要主应用包含多个子系统,我使用的是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();
我的项目是从登录页进入,到主页面,主页面有子系统的入口,进入子系统。
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;
}
}