perf: 优化微应用

This commit is contained in:
kuaifan 2025-08-06 16:51:21 +08:00
parent 27b64df870
commit b73ab76bfb
5 changed files with 187 additions and 84 deletions

View File

@ -196,10 +196,19 @@ export default {
this.$store.commit('microApps/update', { this.$store.commit('microApps/update', {
name: this.name, name: this.name,
data: { data: {
postMessage: (message) => {
if (!this.$refs.iframe || !this.$refs.iframe.contentWindow) {
return
}
this.$refs.iframe.contentWindow.postMessage(message, '*')
},
onBeforeClose: () => { onBeforeClose: () => {
if (this.hearTbeatLastTime && Date.now() - this.hearTbeatLastTime > 5000) { if (this.hearTbeatLastTime && Date.now() - this.hearTbeatLastTime > 5000) {
return true // return true //
} }
if (!this.$refs.iframe || !this.$refs.iframe.contentWindow) {
return true // iframe
}
return new Promise(resolve => { return new Promise(resolve => {
const message = { const message = {
id: $A.randomString(16), id: $A.randomString(16),

View File

@ -3,16 +3,14 @@
<MicroModal <MicroModal
v-for="(app, key) in microApps" v-for="(app, key) in microApps"
:key="key" :key="key"
v-model="app.isOpen" :open="app.isOpen"
:ref="`ref-${app.name}`" :ref="`ref-${app.name}`"
:size="1200" :size="1200"
:background="app.background" :options="app"
:transparent="app.transparent" :beforeClose="onBeforeClose"
:autoDarkTheme="app.auto_dark_theme" @on-capsule-more="onCapsuleMore"
:keepAlive="app.keep_alive" @on-popout-window="onPopoutWindow"
:beforeClose="async (isClick) => { await onBeforeClose(app.name, isClick) }" @on-close="closeMicroApp">
@on-restart-app="onRestartApp(app.name)"
@on-popout-window="onPopoutWindow(app.name)">
<MicroIFrame <MicroIFrame
v-if="shouldRenderIFrame(app)" v-if="shouldRenderIFrame(app)"
:name="app.name" :name="app.name"
@ -22,7 +20,7 @@
@mounted="mounted" @mounted="mounted"
@error="error"/> @error="error"/>
<micro-app <micro-app
v-else-if="app.isOpen && app.url" v-else-if="shouldRenderMicro(app)"
:name="app.name" :name="app.name"
:url="app.url" :url="app.url"
:keep-alive="app.keep_alive" :keep-alive="app.keep_alive"
@ -30,10 +28,14 @@
:data="appData(app.name)" :data="appData(app.name)"
@mounted="mounted" @mounted="mounted"
@error="error"/> @error="error"/>
<div v-if="app.isLoading" class="micro-app-loader"> </MicroModal>
<!--加载中-->
<transition name="fade">
<div v-if="loadings.length > 0" class="micro-app-loader">
<Loading/> <Loading/>
</div> </div>
</MicroModal> </transition>
<!--选择用户--> <!--选择用户-->
<UserSelect <UserSelect
@ -58,6 +60,7 @@
<style lang="scss"> <style lang="scss">
.micro-app-loader { .micro-app-loader {
position: absolute; position: absolute;
z-index: 9999;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
@ -113,6 +116,9 @@ export default {
return { return {
assistShow: false, assistShow: false,
userSelectOptions: {value: [], config: {}}, userSelectOptions: {value: [], config: {}},
loadings: [],
closings: [],
} }
}, },
@ -122,8 +128,8 @@ export default {
// //
microApp.start({ microApp.start({
'iframe': true,
'router-mode': 'state', 'router-mode': 'state',
'iframe': true,
'iframeSrc': window.location.origin + '/assets/empty.html', 'iframeSrc': window.location.origin + '/assets/empty.html',
}) })
}, },
@ -193,7 +199,7 @@ export default {
// //
finish(name) { finish(name) {
this.$store.commit('microApps/update', {name, data: {isLoading: false}}) this.loadings = this.loadings.filter(item => item !== name);
}, },
/** /**
@ -359,9 +365,9 @@ export default {
} }
// //
if (app.url != config.url) { if (app.url != config.url || !app.keep_alive) {
this.unmountMicroApp(app) this.unmountMicroApp(app)
app.isLoading = true this.loadings.push(app.name)
} }
Object.assign(app, config) Object.assign(app, config)
requestAnimationFrame(_ => { requestAnimationFrame(_ => {
@ -371,10 +377,11 @@ export default {
}) })
} else { } else {
// //
config.isLoading = true
config.isOpen = false config.isOpen = false
config.postMessage = () => {}
config.onBeforeClose = () => true config.onBeforeClose = () => true
this.$store.commit('microApps/push', config) this.$store.commit('microApps/push', config)
this.loadings.push(config.name)
requestAnimationFrame(_ => { requestAnimationFrame(_ => {
config.isOpen = true config.isOpen = true
config.lastOpenAt = Date.now() config.lastOpenAt = Date.now()
@ -401,7 +408,7 @@ export default {
appConfig.url = windowConfig.url; appConfig.url = windowConfig.url;
delete windowConfig.url; delete windowConfig.url;
} }
//
const path = `/single/apps/${appConfig.name}` const path = `/single/apps/${appConfig.name}`
const apps = (await $A.IDBArray("cacheMicroApps")).filter(item => item.name != appConfig.name); const apps = (await $A.IDBArray("cacheMicroApps")).filter(item => item.name != appConfig.name);
apps.length > 50 && apps.splice(0, 10) apps.length > 50 && apps.splice(0, 10)
@ -466,6 +473,19 @@ export default {
} }
}, },
/**
* 关闭微应用状态
* @param {Object} app 微应用对象
* @param app
*/
closeAppState(app) {
this.closings.push(app.name);
app.isOpen = false;
setTimeout(() => {
this.closings = this.closings.filter(item => item !== app.name);
}, 300);
},
/** /**
* 关闭微应用关闭前执行beforeClose * 关闭微应用关闭前执行beforeClose
* @param name * @param name
@ -483,28 +503,18 @@ export default {
* @param name * @param name
* @param destroy * @param destroy
*/ */
closeMicroApp(name, destroy) { closeMicroApp(name, destroy = false) {
const app = this.microApps.find(item => item.name == name); const app = this.microApps.find(item => item.name == name);
if (!app) { if (!app) {
return; return;
} }
app.isOpen = false this.closeAppState(app)
if (destroy) { if (destroy === true) {
this.unmountMicroApp(app) this.unmountMicroApp(app)
} }
}, },
/**
* 卸载所有微应用
*/
unmountAllMicroApp() {
this.microApps.forEach(app => {
app.isOpen = false
this.unmountMicroApp(app)
});
},
/** /**
* 卸载微应用 * 卸载微应用
* @param app * @param app
@ -517,6 +527,16 @@ export default {
microApp.unmountApp(app.name, {destroy: true}) microApp.unmountApp(app.name, {destroy: true})
}, },
/**
* 卸载所有微应用
*/
unmountAllMicroApp() {
this.microApps.forEach(app => {
this.closeAppState(app)
this.unmountMicroApp(app)
});
},
/** /**
* 关闭之前判断 * 关闭之前判断
* @param name * @param name
@ -545,7 +565,7 @@ export default {
return return
} }
if (/^iframe/i.test(app.url_type)) { if (this.isIframe(app.url_type)) {
const before = app.onBeforeClose(); const before = app.onBeforeClose();
if (before && before.then) { if (before && before.then) {
before.then(() => { before.then(() => {
@ -583,6 +603,30 @@ export default {
}) })
}, },
/**
* 点击更多操作
* @param name
* @param action
*/
onCapsuleMore(name, action) {
if (action === 'restart') {
this.onRestartApp(name)
return
}
const app = this.microApps.find(item => item.name == name);
if (!app) {
return
}
if (this.isIframe(app.url_type)) {
app.postMessage({
type: 'MICRO_APP_MENU_CLICK',
message: action
});
return
}
microApp.forceSetData(name, {type: 'menuClick', message: action})
},
/** /**
* 重启应用 * 重启应用
* @param name * @param name
@ -595,7 +639,7 @@ export default {
if (!app) { if (!app) {
$A.modalError("应用不存在"); $A.modalError("应用不存在");
} }
app.isLoading = true; this.loadings.push(app.name)
requestAnimationFrame(_ => { requestAnimationFrame(_ => {
app.isOpen = true app.isOpen = true
app.lastOpenAt = Date.now() app.lastOpenAt = Date.now()
@ -618,13 +662,31 @@ export default {
this.closeMicroApp(name, true) this.closeMicroApp(name, true)
}, },
/**
* 是否 iframe 类型
* @param type
* @returns {boolean}
*/
isIframe(type) {
return /^iframe/i.test(type)
},
/** /**
* 是否渲染 iframe * 是否渲染 iframe
* @param app * @param app
* @returns {boolean} * @returns {boolean}
*/ */
shouldRenderIFrame(app) { shouldRenderIFrame(app) {
return app.url_type === 'iframe' && (app.isOpen || app.keep_alive) && app.url; return app.url && this.isIframe(app.url_type) && (app.isOpen || app.keep_alive);
},
/**
* 是否渲染 micro
* @param app
* @returns {boolean}
*/
shouldRenderMicro(app) {
return app.url && !this.isIframe(app.url_type) && (app.isOpen || this.closings.includes(app.name));
}, },
/** /**

View File

@ -2,11 +2,12 @@
<div v-transfer-dom :data-transfer="true"> <div v-transfer-dom :data-transfer="true">
<div :class="className"> <div :class="className">
<transition :name="transitions[0]"> <transition :name="transitions[0]">
<div v-if="shouldRenderInDom" v-show="value" class="micro-modal-mask" @click="onClose(false)" :style="maskStyle"></div> <div v-if="shouldRenderInDom" v-show="open" class="micro-modal-mask" @click="onClose(false)" :style="maskStyle"></div>
</transition> </transition>
<transition :name="transitions[1]"> <transition :name="transitions[1]">
<div v-if="shouldRenderInDom" v-show="value" class="micro-modal-content" :style="contentStyle"> <div v-if="shouldRenderInDom" v-show="open" class="micro-modal-content" :style="contentStyle">
<!-- 工具栏移动端 --> <!-- 工具栏移动端 -->
<div v-if="capsuleMenuShow" class="micro-modal-capsule-mask"></div>
<div class="micro-modal-capsule" :style="capsuleStyle"> <div class="micro-modal-capsule" :style="capsuleStyle">
<div class="micro-modal-capsule-item" @click="onCapsuleMore"> <div class="micro-modal-capsule-item" @click="onCapsuleMore">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -23,6 +24,7 @@
</svg> </svg>
</div> </div>
</div> </div>
<!-- 工具栏桌面端 --> <!-- 工具栏桌面端 -->
<div class="micro-modal-tools" :class="{expanded: $A.isMainElectron}"> <div class="micro-modal-tools" :class="{expanded: $A.isMainElectron}">
<div class="tool-close" @click="onClose(true)"> <div class="tool-close" @click="onClose(true)">
@ -30,13 +32,14 @@
<path d="M8.28596 6.51819C7.7978 6.03003 7.00634 6.03003 6.51819 6.51819C6.03003 7.00634 6.03003 7.7978 6.51819 8.28596L11.2322 13L6.51819 17.714C6.03003 18.2022 6.03003 18.9937 6.51819 19.4818C7.00634 19.97 7.7978 19.97 8.28596 19.4818L13 14.7678L17.714 19.4818C18.2022 19.97 18.9937 19.97 19.4818 19.4818C19.97 18.9937 19.97 18.2022 19.4818 17.714L14.7678 13L19.4818 8.28596C19.97 7.7978 19.97 7.00634 19.4818 6.51819C18.9937 6.03003 18.2022 6.03003 17.714 6.51819L13 11.2322L8.28596 6.51819Z" fill="currentColor"></path> <path d="M8.28596 6.51819C7.7978 6.03003 7.00634 6.03003 6.51819 6.51819C6.03003 7.00634 6.03003 7.7978 6.51819 8.28596L11.2322 13L6.51819 17.714C6.03003 18.2022 6.03003 18.9937 6.51819 19.4818C7.00634 19.97 7.7978 19.97 8.28596 19.4818L13 14.7678L17.714 19.4818C18.2022 19.97 18.9937 19.97 19.4818 19.4818C19.97 18.9937 19.97 18.2022 19.4818 17.714L14.7678 13L19.4818 8.28596C19.97 7.7978 19.97 7.00634 19.4818 6.51819C18.9937 6.03003 18.2022 6.03003 17.714 6.51819L13 11.2322L8.28596 6.51819Z" fill="currentColor"></path>
</svg> </svg>
</div> </div>
<div class="tool-fullscreen" @click="$emit('on-popout-window')"> <div class="tool-fullscreen" @click="$emit('on-popout-window', options.name)">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M682.666667 298.666667H170.666667c-47.061333 0-85.333333 38.272-85.333334 85.333333v426.666667c0 47.061333 38.272 85.333333 85.333334 85.333333h512c47.061333 0 85.333333-38.272 85.333333-85.333333V384c0-47.061333-38.272-85.333333-85.333333-85.333333zM170.666667 810.666667v-341.333334h512V384l0.085333 426.666667H170.666667z" fill="currentColor"></path> <path d="M682.666667 298.666667H170.666667c-47.061333 0-85.333333 38.272-85.333334 85.333333v426.666667c0 47.061333 38.272 85.333333 85.333334 85.333333h512c47.061333 0 85.333333-38.272 85.333333-85.333333V384c0-47.061333-38.272-85.333333-85.333333-85.333333zM170.666667 810.666667v-341.333334h512V384l0.085333 426.666667H170.666667z" fill="currentColor"></path>
<path d="M938.666667 213.333333c0-47.061333-38.272-85.333333-85.333334-85.333333H298.666667c-47.061333 0-85.333333 38.272-85.333334 85.333333h554.709334c46.976 0 85.162667 38.186667 85.290666 85.077334L853.418667 640H853.333333v85.333333c47.061333 0 85.333333-38.272 85.333334-85.333333V341.632L938.709333 341.333333V256L938.666667 255.573333V213.333333z" fill="currentColor"></path> <path d="M938.666667 213.333333c0-47.061333-38.272-85.333333-85.333334-85.333333H298.666667c-47.061333 0-85.333333 38.272-85.333334 85.333333h554.709334c46.976 0 85.162667 38.186667 85.290666 85.077334L853.418667 640H853.333333v85.333333c47.061333 0 85.333333-38.272 85.333334-85.333333V341.632L938.709333 341.333333V256L938.666667 255.573333V213.333333z" fill="currentColor"></path>
</svg> </svg>
</div> </div>
</div> </div>
<!-- 窗口大小调整桌面端 --> <!-- 窗口大小调整桌面端 -->
<ResizeLine <ResizeLine
class="micro-modal-resize" class="micro-modal-resize"
@ -47,8 +50,9 @@
:reverse="true" :reverse="true"
:beforeResize="beforeResize" :beforeResize="beforeResize"
@on-change="onChangeResize"/> @on-change="onChangeResize"/>
<!-- 窗口内容 --> <!-- 窗口内容 -->
<div ref="body" class="micro-modal-body" :style="bodyStyle"> <div ref="body" class="micro-modal-body" :class="bodyClass" :style="bodyStyle">
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
@ -67,7 +71,7 @@ export default {
components: {ResizeLine}, components: {ResizeLine},
directives: {TransferDom}, directives: {TransferDom},
props: { props: {
value: { open: {
type: Boolean, type: Boolean,
default: false default: false
}, },
@ -79,55 +83,49 @@ export default {
type: Number, type: Number,
default: 300 default: 300
}, },
background: { options: {
default: null type: Object,
}, default: () => ({})
transparent: {
type: Boolean,
default: false
},
autoDarkTheme: {
type: Boolean,
default: true
},
keepAlive: {
type: Boolean,
default: true
}, },
beforeClose: Function beforeClose: Function
}, },
data() { data() {
return { return {
dynamicSize: 0, dynamicSize: 0,
zIndex: 1000 zIndex: 1000,
capsuleMenuShow: false,
} }
}, },
computed: { computed: {
...mapState(['windowIsMobileLayout']), ...mapState(['windowIsMobileLayout']),
shouldRenderInDom() { shouldRenderInDom() {
return this.value || this.keepAlive; return this.open || !!this.options.keep_alive;
}, },
className({value, autoDarkTheme, transparent, windowIsMobileLayout}) { className() {
return { return {
'micro-modal': true, 'micro-modal': true,
'micro-modal-hidden': !value, 'micro-modal-hidden': !this.open,
'no-dark-content': !autoDarkTheme, 'transparent-mode': !!this.options.transparent,
'transparent-mode': transparent, 'capsule-mode': this.windowIsMobileLayout,
'capsule-mode': windowIsMobileLayout,
} }
}, },
transitions({transparent}) { transitions() {
if (transparent) { if (!!this.options.transparent) {
return ['', ''] return ['', '']
} }
return ['micro-modal-fade', 'micro-modal-slide'] return ['micro-modal-fade', 'micro-modal-slide']
}, },
bodyClass() {
return {
'no-dark-content': !this.options.auto_dark_theme,
}
},
bodyStyle() { bodyStyle() {
const styleObject = {} const styleObject = {}
if ($A.isJson(this.background)) { if ($A.isJson(this.options.background)) {
styleObject.background = this.background styleObject.background = this.options.background
} else if (this.background) { } else if (this.options.background) {
styleObject.backgroundColor = this.background; styleObject.backgroundColor = this.options.background;
} }
return styleObject; return styleObject;
}, },
@ -139,11 +137,24 @@ export default {
return {width, zIndex} return {width, zIndex}
}, },
capsuleStyle({zIndex}) { capsuleStyle({zIndex}) {
return {zIndex} const styleObject = {zIndex}
const {capsule} = this.options
if ($A.isJson(capsule)) {
if (capsule.visible === false) {
styleObject.display = 'none';
}
if (typeof capsule.top === 'number') {
styleObject.top = `${capsule.top}px`;
}
if (typeof capsule.right === 'number') {
styleObject.right = `${capsule.right}px`;
}
}
return styleObject
}, },
}, },
watch: { watch: {
value: { open: {
handler(val) { handler(val) {
if (val) { if (val) {
this.zIndex = typeof window.modalTransferIndex === 'number' ? window.modalTransferIndex++ : 1000; this.zIndex = typeof window.modalTransferIndex === 'number' ? window.modalTransferIndex++ : 1000;
@ -181,19 +192,31 @@ export default {
}, },
onCapsuleMore(event) { onCapsuleMore(event) {
const list = [ const list = [];
{label: '重启应用', value: 'restart'}, const {capsule} = this.options;
{label: '关闭应用', value: 'close'}, if ($A.isJson(capsule) && $A.isArray(capsule.more_menus)) {
]; capsule.more_menus.forEach(item => {
if (item.label && item.value) {
list.push(item);
}
});
}
list.push(...[
{label: this.$L('重启应用'), value: 'restart', divided: list.length > 0},
{label: this.$L('关闭应用'), value: 'close'},
])
this.$store.commit('menu/operation', { this.$store.commit('menu/operation', {
event, event,
list, list,
size: 'large', size: 'large',
onVisibleChange: (visible) => {
this.capsuleMenuShow = visible;
},
onUpdate: (value) => { onUpdate: (value) => {
if (value === 'restart') { if (value === 'close') {
this.$emit('on-restart-app');
} else if (value === 'close') {
this.onClose(true); this.onClose(true);
} else {
this.$emit('on-capsule-more', this.options.name, value);
} }
} }
}) })
@ -203,7 +226,7 @@ export default {
if (!this.beforeClose) { if (!this.beforeClose) {
return this.handleClose(); return this.handleClose();
} }
const before = this.beforeClose(isClick); const before = this.beforeClose(this.options.name, isClick);
if (before && before.then) { if (before && before.then) {
before.then(() => { before.then(() => {
this.handleClose(); this.handleClose();
@ -214,7 +237,7 @@ export default {
}, },
handleClose() { handleClose() {
this.$emit('input', false) this.$emit('on-close', this.options.name);
} }
} }
} }
@ -280,10 +303,20 @@ export default {
background-color: var(--modal-mask-bg, rgba(0, 0, 0, .4)); background-color: var(--modal-mask-bg, rgba(0, 0, 0, .4));
} }
&-capsule-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
z-index: 1;
}
&-capsule { &-capsule {
position: absolute; position: absolute;
top: 8px; top: 10px;
right: 8px; right: 10px;
z-index: 2; z-index: 2;
transform: translateY(var(--status-bar-height, 0)); transform: translateY(var(--status-bar-height, 0));
display: var(--modal-capsule-display, none); display: var(--modal-capsule-display, none);
@ -466,13 +499,10 @@ export default {
body.dark-mode-reverse { body.dark-mode-reverse {
.micro-modal { .micro-modal {
&:not(.transparent-mode) { &:not(.transparent-mode) {
&:not(.no-dark-content) { --modal-mask-bg: rgba(230, 230, 230, 0.6);
--modal-mask-bg: rgba(230, 230, 230, 0.6); --modal-close-color: #323232;
--modal-close-color: #323232;
}
&.no-dark-content { .no-dark-content {
--modal-mask-bg: rgba(20, 20, 20, 0.6);
--modal-body-background-color: #000000; --modal-body-background-color: #000000;
} }
} }

View File

@ -2642,6 +2642,7 @@ export default {
name: 'okr_details', name: 'okr_details',
url: 'apps/okr/#details', url: 'apps/okr/#details',
props: {type: 'details', id}, props: {type: 'details', id},
keep_alive: false,
transparent: true, transparent: true,
}); });
}, },

View File

@ -4745,6 +4745,7 @@ export default {
url: $A.mainUrl(data.url), url: $A.mainUrl(data.url),
url_type: data.url_type || 'inline', url_type: data.url_type || 'inline',
background: data.background || null, background: data.background || null,
capsule: $A.isJson(data.capsule) ? data.capsule : {},
transparent: typeof data.transparent == 'boolean' ? data.transparent : false, transparent: typeof data.transparent == 'boolean' ? data.transparent : false,
disable_scope_css: typeof data.disable_scope_css == 'boolean' ? data.disable_scope_css : false, disable_scope_css: typeof data.disable_scope_css == 'boolean' ? data.disable_scope_css : false,
auto_dark_theme: typeof data.auto_dark_theme == 'boolean' ? data.auto_dark_theme : true, auto_dark_theme: typeof data.auto_dark_theme == 'boolean' ? data.auto_dark_theme : true,