概述:
公司的旧项目采用vue2+element-ui,项目重,性能较差,考虑到vue3逐渐成熟,而且很好的支持了tree shaking优化,开始起步。新的模块单独开个vue3项目,构建工具使用vite(启动速度更快,热更新),将vue3集成在vue2中使用微前端qiankun(很多坑,慢慢填)
1. 旧项目改造,使vue2支持vue3语法
npm install @vue/composition-api
npm install unplugin-vue2-script-setup //支持vue3 setup语法糖
// main.js
import VueCompositionApi from '@vue/composition-api'
Vue.use(VueCompositionApi)
const ScriptSetup = require('unplugin-vue2-script-setup/webpack').default
module.exports = { configureWebpack: (config)=>{ config.plugins.push(ScriptSetup({})); }, }
以上3步完成后就可以这样写代码啦
<script setup>
</script>
<template>
<div>
</div>
</template>
<style scoped>
</style>
目前遗留问题:打包到生产时有时会报语法错误,未解决... (已更换为原vue2语法)
2.将vue2项目设为基座(父应用)
1.引入qiankun
npm i qiankun -S
2.给定一个容器来承载这个子应用
<div id="subAPP"></div>
此容器可写在app.vue页面,也可写在layout页面,具体情况根据项目位置定。
//由于此项目要作为子菜单渲染(包含layot),我是写在了layout router-view下面占位。
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews" v-if="isReload">
<router-view :key="key" />
</keep-alive>
</transition>
<div id="subAPP" class="subAPP"></div>
</section>
3.main.js注册
import { registerMicroApps, start } from "qiankun";
registerMicroApps([
{
name: "subAPP",
entry: process.env.VUE_APP_SECRET || 'http://127.0.0.1:9000/',
container: "#subAPP",
activeRule: "/#/subAPP",
props: {
parentActions: actions
}
}
]);
// 注册微前端名称为subAPP container容器的id是subAPP
// entry为微应用的入口,此处是子应用抛出的地址
// activeRule是校验规则 当匹配到路由时渲染子应用,类似路由
start();
上述逻辑要写在new Vue的render前面,否则进入子应用页面时会闪动一下(先渲染了子应用)
new Vue({
el: "#app",
router,
store,
render: h => h(App)
});
4.router js添加路由匹配
{
path: "/subAPP/:chapters*", path修改为'/subAPP/*' 前者会导致子路由刷新地址栏/变为编码导致路由丢失
name: "subAPP",
component: Layout
}
// 浏览器输入以/subAPP开始的路由都可以正常进入页面,内容由微应用自己渲染,layout页面带有菜单栏
3.搭建新项目,将其作为父应用的子应用。
1.构建vite基础项目
由于vite本身不支持qiankun,安装插件vite-plugin-qiankun。如使用webpack构建可直接参考微前端qiankun官网qiankun.umijs.org/zh/guide/ge…
npm create vite@latest (vite官网:https://cn.vitejs.dev/guide/)
npm install vite-plugin-qiankun
2.viteconfig.js文件
1.从插件中引入qiankun,在plugins添加 qiankun('subAPP', {useDevMode: true})
2.修改serve的host和父应用相同,同时配置headers允许跨域
下面贴出全部代码
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import path from 'path';
import qiankun from "vite-plugin-qiankun"
const optimizeDepsElementPlusIncludes = ["element-plus/es"]
fs.readdirSync("node_modules/element-plus/es/components").map((dirname) => {
fs.access(
`node_modules/element-plus/es/components/${dirname}/style/css.mjs`,
(err) => {
if (!err) {
optimizeDepsElementPlusIncludes.push(
`element-plus/es/components/${dirname}/style/css`
)
}
}
)
})
export default ({ command, mode }: any) => {
return defineConfig({
// env文件配置的环境地址,
base: loadEnv(mode, process.cwd()).VITE_APP_BASE_PATH,
// 强制预加载 微应用首次打开会很慢,
optimizeDeps: {
include: optimizeDepsElementPlusIncludes
},
plugins: [
vue(),
vueJsx(),
qiankun('subAPP', {
useDevMode: true
}),
],
server: {
host: '127.0.0.1',
port: Number(loadEnv(mode, process.cwd()).VITE_APP_PORT) || 3000,
headers: {
'Access-Control-Allow-Origin': '*'
},
https: false,
open: true,
proxy: {
'/api': {
target: 'http://172.19.128.97:8086',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
},
hmr: {
overlay: true
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
}
3.main.ts文件变更
1.引入qiankunWindow和renderWithQiankun
2.重写render方法
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import "@/utils/auth.js";
import {
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
let app: any;
function render(props: any) {
const { container, parentActions } = props;
app = createApp(App);
app.use(router).mount(container instanceof Element
? (container.querySelector("#app"))
: (container)
);
}
如果qiankun实例不存在__POWERED_BY_QIANKUN__属性说明是子应用独立运行,此时直接将子应用挂载到子应用的app根节点
如果存在说明有父应用,直接挂载父应用传递下来的container容器(父容器设置的承载容器)
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render({ container: "#app" });
} else {
renderWithQiankun({
mount(props) {
每次挂载都重新执行render方法
render(props)
},
bootstrap() {
},
update() {
},
unmount() {
卸载时同时卸载子应用实例
app.unmount();
}
});
}
4.子应用路由文件
const router = createRouter({
history: createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? '/#/subAPP2/' : '/'),
routes
})
//后经验证 上面createWebHistory的参数有大坑,他会作为base 将/#/subAPP拼接至浏览器地址栏,影响浏览器history.pushState的调用(其实是影响了浏览器回车时候的url hash变更 导致路由错误)
// 所以改成下面这种写法
// 路由跳转需加 /subApp_cockpit 前缀 批量给路由加上前缀,而不去改base的值,经测试 浏览器回车跳转正常
const prefix = qiankunWindow.__POWERED_BY_QIANKUN__ ? '/subApp_cockpit' : ''
const addAlias = (routes: any): any => {
if (!qiankunWindow.__POWERED_BY_QIANKUN__) return
for (let route of routes) {
let path = route.path
if (path && path.startsWith('/') && !path.startsWith(prefix)) {
route.alias = prefix + path
}
if (route.children) {
addAlias(route.children)
}
}
}
addAlias(routes)
const router = createRouter({
history: createWebHashHistory(),
routes
})
// 路由跳转前将路由信息传递给父应用, 作菜单显示
router.beforeEach((to: any, from, next) => {
actions.setGlobalState({ subRoute: to })
next()
})
export default router
5.子应用actions文件封装(不进行父子交互的话可不封装)
action.ts文件
function emptyAction() {
}
class Actions {
actions = {
onGlobalStateChange: emptyAction,
setGlobalState: emptyAction,
};
setActions(actions) {
this.actions = actions;
}
onGlobalStateChange() {
return this.actions.onGlobalStateChange(...arguments);
}
setGlobalState() {
return this.actions.setGlobalState(...arguments);
}
}
const actions = new Actions();
export default actions;
main.ts
import actions from './utils/action'
const { container, parentActions } = props;
parentStoreData.value = props.parentStore
if (parentActions) {
actions.setActions(parentActions)
}
4.微应用一系列问题处理
上述操作做完之后,可以看到浏览器把配置子应用路由的菜单渲染出来,但是敏锐的你会发现引发了一系列问题。
1.样式污染
子应用渲染出来后对父应用样式产生了影响,其他非微应用页面样式全部错乱,通过排查,发现影响的样式都来自于子应用的ui样式文件。 (父应用的样式也有了子应用的element plus的样式) 首先能想到的肯定是沙箱隔离,在qiankun官网提供了sandbox,可以start({strictStyleIsolation: true, experimentalStyleIsolation: true})开启沙箱模式,遗憾的是问题并没有解决,子应用页面样式失效。 去翻element plus发现有一个命名空间
1.app.vue采用 elment-plus组件包裹
<el-config-provider namespace="ep">
<router-view />
</el-config-provider>
此时打开f12可以看到子应用的ui样式前缀全部变成了ep开头,但同时样式也失效了
2.设置 SCSS 和 CSS 变量
创建 styles/element/index.scss
@forward 'element-plus/theme-chalk/src/mixins/config.scss' with (
$namespace: 'ep'
);
// 固定写法
3.在 vite.config.ts 中导入 styles/element/index.scss:
import { defineConfig } from 'vite'
export default defineConfig({
// ...
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "~/styles/element/index.scss" as *;`,
// 路径和上述新建文件保持一致
},
},
},
// ...
})
//此时独立打开子应用发现样式已经有了,前缀也是ep开头,但是在父应用中样式还是有污染
4.将element-plus全局导入改为按需导入
1.安装unplugin-vue-components 和 unplugin-auto-import这两款插件
npm install -D unplugin-vue-components unplugin-auto-import
2.然后把下列代码插入到 vite 的配置文件中,viteconfig.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
// ...
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
extensions: ['vue', 'md'],
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
resolvers: [
ElementPlusResolver({
importStyle: 'sass',
}),
],
}),
],
})
此时再看子组件样式已经生效,父应用的样式也不会受到影响
5.此时发现其他ui样式均生效,但是ElMessage的相关类名上去了,样式却丢失(按需导入引起)
// main.js引入
import "element-plus/theme-chalk/src/message.scss"
2.与父应用数据交互
写到3-5了
3.刷新后报错 未找到容器
application 'subAPP' died in status LOADING_SOURCE_CODE: [qiankun]
将start调用时机从main.js放到layout页,由于是子路由页面渲染,需在加载layout后再调用start钩子
import { start } from "qiankun"
mounted () {
start()
},
5.使用svg
1.安装svg vite插件
npm install vite-plugin-svg-icons
2.viteconfig文件配置
1.import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
2.plugins添加
plugins: [
vue(),
// svg文件存放目录
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
symbolId: 'icon-[name]'
})
],
3. svg组件代码 index.vue
// packages/svgIcon/src/index.vue
<template>
<svg aria-hidden="true" width="20px" height="20px">
<use :class="fillClass" :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps({
name: {
type: String,
required: true
},
color: {
type: String
},
fillClass: {
type: String
},
width: {
type: [String, Number],
default: 20
},
height: {
type: [String, Number],
default: 20
},
})
const symbolId = computed(() => `#icon-${props.name}`)
</script>
4.svg组件注册
// packages/svgIcon/index.ts 将svg文件导出
import SvgIcon from './src/index.vue'
import { withInstall } from '../withInstall'
const LbSvgIcon = withInstall(SvgIcon)
export default LbSvgIcon
// packages/withInstall.ts
import { App, Plugin } from 'vue'
type SFCWithInstall<T> = T & Plugin
export const withInstall = <T, E extends Record<string, any>>(
main: T,
extra?: E
) => {
; (main as SFCWithInstall<T>).install = (app: App) => {
for (const comp of [main, ...Object.values(extra ?? {})]) {
app.component(comp.name, comp)
}
}
if (extra) {
for (const [compName, comp] of Object.entries(extra)) {
; (main as Record<string, any>)[compName] = comp
}
}
return main as SFCWithInstall<T> & E
}
// packages/index.ts文件 将组件导出
import LbSvgIcon from './svgIcon'
const components: {
[propName: string]: Component
} = {
LbSvgIcon,
}
const installComponents: any = (app: App) => {
for (const key in components) {
app.component(key, components[key])
}
}
const install: any = (app: any, router?: any) => {
installComponents(app)
}
export {
LbSvgIcon,
}
export default {
install,
}
// packages/components.d.ts 声明文件,组件可以高亮
import LbSvgIcon from './svgIcon/src/index.vue'
declare module 'vue' {
export interface GlobalComponents {
LbSvgIcon: typeof LbSvgIcon
}
}
5.main.ts导入
import LbUi from './packages'
app = createApp(App);
app.use(LbUi)
6.main.ts完整代码
import { createApp, ref } from 'vue'
import 'tailwindcss/tailwind.css'
import '@/assets/common.css'
import App from './App.vue'
import router from './router'
import {
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import 'ant-design-vue/dist/reset.css';
import actions from './utils/action'
import { createPinia } from 'pinia'
import adaptive from './directive/adaptive'
import { clear } from './utils/tools'
import LbUi from './packages'
let app: any;
const parentStoreData: any = ref();
router.afterEach(() => {
if (!parentStoreData.value) return
parentStoreData.value.dispatch("settings/changeSetting", {
key: "subAppLoading",
value: false
})
})
function render(props: any) {
const { container, parentActions } = props;
parentStoreData.value = props.parentStore
if (parentActions) {
actions.setActions(parentActions)
}
app = createApp(App);
app.config.devtools = true
app.config.globalProperties.$parentActions = props.parentActions
app.config.globalProperties.$parentRouter = props.parentRouter
app.config.globalProperties.$clear = clear
app.directive('adaptive', adaptive)
app.use(createPinia())
app.use(LbUi)
app.use(router).mount(container instanceof Element
? (container.querySelector("#app"))
: (container)
);
}
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render({ container: "#app" });
} else {
renderWithQiankun({
mount(props) {
render(props)
},
bootstrap() {
},
update() {
},
unmount() {
app.unmount();
}
});
}
export { parentStoreData }