背景:随着业务功能的扩展,原有开发模式已无法满足需求。上线后出现问题时,排查过程变得异常复杂,新开发人员也难以迅速理解现有代码。同时,系统间界面风格和交互差异较大,导致跨系统办理业务时工作量增加。因此,引入微前端架构,以支持团队协作、实现独立部署,并提升开发效率。
微前端 qiankun:基于 single-spa 实现的微前端框架,允许多个子应用在一个主应用中独立运行且互不干扰,适用于大型应用或多团队协作场景。其优点包括:与技术栈无关,支持子应用独立开发和部署,提供开箱即用的 API,易于上手,且社区活跃,支持良好。
微前端框架 qiankun,支持不同技术栈的子应用,提供沙箱隔离、独立部署、生命周期管理、应用间通信等功能,能够动态加载和注册子应用,兼容 Hash 和 History 路由模式,灵活且易于集成。
Qiankun 和 iframe 都可以用来实现微前端架构,但它们的实现方式和应用场景有所不同。
qiankun 是基于 JavaScript
的微前端框架,允许子应用共享主应用环境,并通过全局状态管理和路由共享实现协调和通信,增强灵活性和可维护性。
iframe 通过独立窗口隔离子应用,部署简单且子应用完全独立,互不影响,适合嵌套简单页面。由于隔离性强,导致路由刷新丢失、状态和 DOM 不共享,交互复杂。每次加载需重建上下文和资源,性能开销大。
安装依赖:npm i qiankun -S
主应用入口文件中注册子应用信息。
// 定义全局消息传递对象,存储主应用的状态和 Vuex
const msg = {
data: store.getters, // 从主应用仓库读取的数据
channelVueX: store, // 传递 Vuex 实例给子应用
}
async function fetchSystemTreeAndInit() {
try {
const res = await API.getSystemTree() // 异步请求系统树数据
// 动态生成子应用列表
const apps = appList.map((item) => ({
name: item.name,
entry: getAppEntry(item), // 获取子应用的入口 URL
render,
activeRule: genActiveRule(item.code), // 生成子应用激活规则
props: { ...msg, permissibleMenu: res.data.treeMenuList }, // 传递给子应用的属性
}))
// 注册子应用信息...
} catch (error) {
console.error('获取系统树失败:', error)
}
}
注意:在注册子应用后,上述的 props 字段,传递给子应用的属性。
执行子应用的注册 API,并启动微前端框架。
registerMicroApps(apps)
// 第一个子应用加载完毕回调
runAfterFirstMounted(() => {})
// 启动微前端框架
start({
sandbox: false, // 关闭沙盒模式,确保子应用的 window 对象正常
prefetch: 'all', // 启用所有子应用的预加载
})
// 设置全局未捕获异常处理器
addGlobalUncaughtErrorHandler((event) => console.log(event))
主应用的渲染逻辑:只在首次渲染时创建 Vue 实例,从而避免不必要的重复创建。初始化函数 init(),在所有子应用成功注册后启动微前端框架。
let app = null
// 创建并渲染 Vue 实例
function createVueApp() {
return new Vue({
el: '#container', // 挂载根元素
router,
store,
created: bootstrap, // 应用启动时执行
render: (h) => h(App), // 渲染根组件
})
}
// 主应用渲染函数
export function render() {
if (!app) {
app = createVueApp() // 只在首次渲染时创建 Vue 实例
}
}
// 初始化函数,获取系统树并初始化子应用
export async function init() {
await fetchSystemTreeAndInit() // 使用 await 等待系统树获取并初始化子应用
}
子应用的入口 URL 通常根据环境的不同(如开发、测试、生产等)动态配置。通过window.location 或 process.env 来判断当前的环境,从而选择正确的子应用 URL。
// 获取环境对应的应用入口 URL
const getAppEntry = (item) => {
const envUrls = {
local: item.devUrl,
test: item.testUrl,
uat: item.uatUrl,
prod: item.proUrl
}
const env = window.location.href.includes('localhost') || process.env.NODE_ENV === 'development' ? 'local' :
process.env.NODE_ENV === 'test' ? 'test' :
process.env.VUE_APP_IS_UAT ? 'uat' : 'prod'
return envUrls[env]
}
每个子应用有唯一的 code 值,genActiveRule 根据路由前缀判断当前 URL 是否激活对应的子应用,适用于微前端架构中的子应用加载与路由控制。
// 获取环境对应的应用入口 URL
const getAppEntry = (item) => {
const envUrls = {
local: item.devUrl,
test: item.testUrl,
uat: item.uatUrl,
prod: item.proUrl
}
const env = window.location.href.includes('localhost') || process.env.NODE_ENV === 'development' ? 'local' :
process.env.NODE_ENV === 'test' ? 'test' :
process.env.VUE_APP_IS_UAT ? 'uat' : 'prod'
return envUrls[env]
}
在微前端架构中,子应用需要做一些配置,与主应用进行良好的集成。子应用配置如下:
在 src/public-path.js 文件中,添加如下信息,确保子应用正确加载资源路径:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
若当前运行在微前端环境中,webpack_public_path 会被动态设置为 qiankun 注入的公共路径,确保子应用在qiankun环境下能正确加载资源路径。
子应用的生命周期钩子函数分别为 bootstrap、mount 和 unmount。它们在子应用初始化、挂载、卸载时执行:
import './public-path'; // 引入 public-path.js
let instance = null
let router = null
// 初始化
export async function bootstrap(props) {
console.log(props)
}
// 挂载
export async function mount(props) {
// 这里可以进行子应用的初始化、路由配置等操作
}
// 卸载
export async function unmount() {
instance.$destroy()
instance = null
router = null
}
在 vue.config.js 中配置打包成 UMD 格式,以支持微前端架构:
const { name } = require('./package');
module.exports = {
// 其他配置项...
configureWebpack: {
output: {
library: `${name}`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
在子应用的 router.js 文件中引入 Vue 和 Vue Router,子应用在微前端环境下,则根据subAppCode作为路由前缀,否则使用默认的基础路径。
import Vue from 'vue'
import Router from 'vue-router'
import { constantRouterMap } from '@/router/router.config'
export default new Router({
// 根据子系统编码区分路径,动态设置 base 路径
base: window.__POWERED_BY_QIANKUN__ ? '/033' : process.env.BASE_URL,
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap,
})
在 Vue 项目中,路由守卫(router.beforeEach)是负责全局控制页面跳转、权限校验和用户状态管理等内容。主应用的路由守卫逻辑:包括子应用信息的设置、用户登录状态的判断、权限加载以及子系统菜单的动态加载。
每次路由跳转时,在beforeEach 路由守卫先触发,根据不同的条件来执行不同的操作,包括设置页面标题、判断用户是否已登录、加载用户权限、处理子系统菜单等,控制整个路由过程的流向。
router.beforeEach((to, from, next) => {
NProgress.start() // Start the progress bar
// 设置子应用信息
updateMicroAppInfo(to)
// 设置页面标题
if (to.meta?.title) {
setDocumentTitle(`${domTitle} - ${to.meta.title}`)
}
// 主页未设置市场,执行退出登录
if (to.name === 'home' && !store.getters.currentMarket) {
logoutAndRedirect(next, to)
return
}
// 检查用户是否已登录
const isLoggedIn = Cookies.get(ACCESS_TOKEN);
isLoggedIn ? handleLoggedInUser(to, next) : handleGuestUser(to, next);
})
router.afterEach(() => {
NProgress.done() // 结束进度条
})
在每次路由跳转前,检查页面路由配置,确保在非登录页面(passport)下,根据当前路径设置对应的子应用名称和菜单,更新子应用的相关信息:
function updateMicroAppInfo(to) {
if (to.matched.length > 0 && to.name !== 'passport') {
// 更新子应用信息和活动标签
store.dispatch('SetMicroApp', { name: to.meta.title, url: to.fullPath })
store.dispatch('SetActiveTab', to.fullPath)
// 设置当前菜单
const menuKey = to.path === '/home' ? to.path : to.path.slice(0, -5);
store.commit('SET_MENU_KEY', menuKey);
}
}
未登录用根据白名单判断是否允许访问,否则重定向到登录页并带上当前页面的跳转路径。
const whiteList = ['passport']
function handleGuestUser(to, next) {
if (whiteList.includes(to.name)) {
next() // 白名单页面直接进入
} else {
next({ path: '/passport', query: { redirect: to.fullPath } })
NProgress.done()
}
}
对于已登录的用户,加载权限、子系统菜单和按钮权限等。下面通过loadMicroAppMenu函数异步加载子系统菜单,并根据页面配置加载对应的按钮权限。
function handleLoggedInUser(to, next) {
loadUserPermissions(to, next) // 加载用户权限
loadMicroAppMenu(to, next) // 加载子系统菜单
loadActionPermissions(to, next) // 获取按钮权限
}
如果权限为空,请求并生成权限路由。
async function loadUserPermissions(to, next) {
store.dispatch('GetSystemMenu', store.state.passport.menuName); // 获取系统菜单
if (!store.getters.permissibleMenu.length) {
try {
const permissionMenu = await store.dispatch('GetPermission'); // 请求权限数据
await store.dispatch('GenerateRoutes', permissionMenu); // 生成路由
router.addRoutes(store.getters.addRouters); // 动态添加路由
} catch (err) {
handleError({ message: '错误', description: '请求用户信息失败,请重试' }, next, to);
}
}
}
对于多子系统应用,每次路由跳转时,根据当前路径检查当前菜单是否已经加载。如果未加载则向后端请求菜单数据,加载对应的子系统菜单。
async function loadMicroAppMenu(to, next) {
if (fetchMenuFlag) return; // 防止重复请求
const app = findAppByPath(to.path); // 查找当前子应用
if (!store.getters.microAppMenuList.length) {
fetchMenuFlag = true; // 请求标识
try {
await store.dispatch('GetMicroAppMenuList'); // 请求子系统菜单列表
} catch (err) {
console.error('Failed to fetch menu list:', err);
handleError({
message: '错误',
description: '您暂未拥有此页面权限,或者此页面已关闭。',
})
fetchMenuFlag = false;
next({ path: '/home' })
return;
} finally {
fetchMenuFlag = false; // 重置请求标识
}
}
// 菜单加载后,检查应用并加载
if (app) {
try {
await store.dispatch('LoadedApp', { code: app.code, store });
} catch (err) {
console.error('Error loading app:', err);
}
}
}
function findAppByPath(path) {
const appCode = path.split('/')[1]; // 提取路径中的应用码
return store.getters.microAppMenuList.find((i) => i.code === appCode);
}
注意:查找当前子应用后,调用 store.dispatch 中的 LoadedApp 方法加载子应用。
根据页面的meta.action属性,加载并保存该页面按钮权限。若没有按钮权限的页面直接进入。
async function loadActionPermissions(to, next) {
if (to.meta?.action) {
try {
const data = await store.dispatch('GetAction', to.meta.menuId); // 获取按钮权限
const btnList = data.reduce((acc, item) => {
acc[item.menuCode] = true;
acc[item.menuName] = item.menuName;
acc['formId_' + item.menuCode] = item.formId;
return acc;
}, {});
store.commit('SET_BUTTON_LIST', btnList); // 更新按钮权限
next(); // 跳转
} catch (err) {
console.error('Failed to load action permissions:', err);
next(); // 继续跳转
}
} else {
next(); // 没有按钮权限控制直接跳转
}
}
当发生错误时(如请求失败或用户没有访问权限),统一处理错误并重定向到登录页面。
// 统一处理错误提示
function handleError({ message, description }, next, to) {
notification.error({ message, description })
next && logoutAndRedirect(next, to) // 调用统一的登出和重定向方法,传递 to 参数
}
// 统一登出并重定向
function logoutAndRedirect(next, to = null) {
store.dispatch('Logout').then(() => {
next({ path: '/passport', query: { redirect: to ? to.fullPath : '/' } })
NProgress.done()
}).catch(() => {
NProgress.done()
})
}
子应用通过 mount
方法挂载,根据主应用权限菜单生成路由配置,并创建路由实例设置 base
路径。
export async function mount(props) {
const { container } = props;
let tempPermissibleMenu = props.permissibleMenu || []; // 获取权限菜单
// 根据子系统编码过滤菜单
if (props.channelVueX && props.channelVueX.getters.appCode === '118') {
tempPermissibleMenu = tempPermissibleMenu.filter(i => i.code === subAppCode)[0]?.list || [];
}
await store.dispatch('GenerateRoutes', tempPermissibleMenu); // 动态生成路由
// 配置路由
router = new Router({
base: window.__POWERED_BY_QIANKUN__ ? `/${subAppCode}` : process.env.BASE_URL,
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: window.__POWERED_BY_QIANKUN__ ? store.getters.addRouters : constantRouterMap,
});
setupRouterHooks(props); // 配置路由守卫
// 创建 Vue 实例并挂载
instance = new Vue({
router,
store,
created: bootstraps,
render: h => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
配置 beforeEach 和 afterEach 守卫,处理权限校验、页面标题设置及进度条管理:
function setupRouterHooks(props) {
router.beforeEach((to, from, next) => {
NProgress.start();
// 设置页面标题
if (to.meta?.title) {
setDocumentTitle(`${domTitle} - ${to.meta.title}`)
}
if (Cookies.get(ACCESS_TOKEN)) {
handleLoggedInUser(to, next, props);
} else {
handleGuestUser(to, next);
}
});
router.afterEach(() => {
NProgress.done();
});
}
未登录用户可以直接访问免登录白名单中的页面,否则会被重定向到登录页:
function handleGuestUser(to, next) {
if (whiteList.includes(to.name)) {
next();
} else {
next({ path: '/passport', query: { redirect: to.fullPath } });
NProgress.done();
}
}
已登录用户进行动态路由和按钮权限的初始化:
async function handleLoggedInUser(to, next) {
try {
// 动态路由和权限初始化
await ensureDynamicRoutes();
// 按钮权限初始化
if (to.meta.action) {
await initializeButtonPermissions(to.meta.menuId);
}
} catch (err) {
console.error('权限处理出错:', err);
notification.error({
message: '错误',
description: '请求用户信息失败,请重试',
});
await store.dispatch('Logout');
redirectToLogin(to, next);
}
}
只有在权限菜单为空时,才会请求后端接口获取权限数据并生成路由,避免重复请求。
/**
* 确保动态路由已加载
*/
async function ensureDynamicRoutes() {
// 如果没有权限菜单,加载权限并生成路由
if (store.getters.permissibleMenu.length === 0) {
const permissibleMenu = await store.dispatch('GetPermission');
if (!isCollaborationCenter()) {
await store.dispatch('GenerateRoutes', permissibleMenu);
router.addRoutes(store.getters.addRouters);
}
}
}
/**
* 判断是否为主应用
*/
function isCollaborationCenter() {
return props.channelVueX && props.channelVueX.getters.appCode === '118';
}
加载指定菜单的按钮权限并将其保存到 Vuex store 中,以便在页面中进行按钮权限的控制。
async function initializeButtonPermissions(menuId) {
const actions = await store.dispatch('GetAction', menuId);
const btnList = actions.reduce((btns, { menuCode, menuName, formId }) => ({
...btns,
[menuCode]: true,
[menuName]: menuName,
[`formId_${menuCode}`]: formId, // 自定义表单兼容
}), {});
store.commit('SET_BUTTON_LIST', btnList);
}
在 Qiankun 微前端框架中,Props 传递数据 和 全局状态管理 常用的应用间通信方式,用于主应用与子应用之间,或子应用之间的数据传递和事件触发,具备简单易用、与框架高度集成的特点。
在 Qiankun 框架中,将主应用传递的props注入子应用。子应用通过props获取主应用的数据和方法。Qiankun 支持主应用传递 props 注入子应用,在子应用的mount方法中接受并使用props,实现主应用与子应用之间的通信。实现步骤如下:
registerMicroApps([
{
name: 'childApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/child',
props: {
userInfo: { name: 'John Doe', role: 'admin' },
globalState: { theme: 'dark' },
setGlobalState: (state) => { console.log('Update state:', state); },
},
},
]);
export async function mount(props) {
console.log('Props from main app:', props);
const { userInfo, globalState, setGlobalState } = props;
// 调用主应用方法
setGlobalState({ theme: 'light' });
}
适用于主应用向子应用单向传递初始化数据,静态数据传递,简单高效,如:子应用初始化配置。
Qiankun 提供了initGlobalState 方法(全局状态管理工具),用于共享和同步主应用与子应用的状态。它支持双向通信,并且易于集成。实现步骤如下:
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ user: 'admin', theme: 'dark' });
// 监听全局状态变化
actions.onGlobalStateChange((state, prev) => {
console.log('Global state changed:', state, prev);
});
// 更新全局状态
actions.setGlobalState({ theme: 'light' });
// 获取全局状态
console.log(actions.getGlobalState());
export async function mount(props) {
const { onGlobalStateChange, setGlobalState } = props;
// 监听全局状态变化
onGlobalStateChange((state, prev) => {
console.log('State changed:', state, prev);
});
// 更新全局状态
setGlobalState({ user: 'guest' });
}
适用于多子应用共享状态,且支持双向同步,能够应对复杂的跨应用通信需求,如登录状态共享、子应用联动和跨应用动态更新。
主应用和子应用通过共享的 localStorage 或 sessionStorage 存储数据,但需要注意跨域限制。
待补充:单点登录。
在基于微前端架构的开发中,主应用与子应用之间的通信、状态管理是核心问题之一。微前端架构需要主应用满足:
通过 qiankun 提供的 loadMicroApp 动态加载子应用。加载逻辑如下:
LoadedApp({ dispatch, commit, state }, param) {
const app = appList.find(i => i.code.slice(1) === param.code)
if (!app) return // 如果找不到子应用,直接返回
const microApp = {
name: app.name,
entry: getAppUrl(app),
container: `#${app.name}App`,
props: { channelVueX: param.store, permissibleMenu: state.microAppMenuList },
}
if (state.loadedAppsList.length >= 5) {
dispatch('UnLoadedApp') // 缓存满时卸载最早的子应用
}
if (!state.loadedAppsMap[microApp.name]) {
const appInstance = loadMicroApp(microApp)
commit('SET_LOADED_APPS_MAP', { appName: microApp.name, loadedApp: appInstance })
}
}
根据用户权限菜单筛选出符合权限的子应用,通过qiankun使用 prefetchApps 提前加载这些子应用的资源,提升切换速度。
// 获取并设置子应用权限菜单列表
async GetMicroAppMenuList({ commit }) {
try {
const { data } = await API.getSystemTree() // 获取系统树数据
commit('SET_SUB_MENU', data.treeMenuList)
// 筛选出有权限的子应用进行预加载
const hasMenuCode = new Set(data.treeMenuList.map(item => item.code))
const appsToPrefetch = appList
.filter(item => hasMenuCode.has(item.code.slice(1)))
.map(item => ({
name: item.name,
entry: getAppUrl(item), // 根据环境选择子应用URL
}))
prefetchApps(appsToPrefetch) // 预加载子应用
} catch (error) {
console.error("获取子应用菜单失败:", error)
throw error // 重新抛出错误以便上层处理
}
},
为了优化内存使用,主应用限制了最多保留 5 个子应用。卸载最先加载的子应用,同时更新状态,确保映射表 loadedAppsMap 和 缓存列表 loadedAppsList 同步,实现子应用的有序缓存与管理。
UnLoadedApp({ commit, state }) {
if (state.loadedAppsList.length > 0) {
const firstAppName = state.loadedAppsList[0]
const appInstance = state.loadedAppsMap[firstAppName]
appInstance && appInstance.unmount() // 卸载子应用
commit('SET_LOADED_APPS_MAP', { appName: firstAppName, loadedApp: null })
}
}
在 mutation 中,维护已加载子应用的映射 loadedAppsMap 和缓存列表 loadedAppsList,确保主应用在缓存子应用时能够有效地管理子应用的加载状态。
SET_LOADED_APPS_MAP: (state, { appName, loadedApp }) => {
state.loadedAppsMap[appName] = loadedApp
// 更新已加载子应用列表
if (loadedApp && !state.loadedAppsList.includes(appName)) {
state.loadedAppsList.push(appName)
} else if (!loadedApp && state.loadedAppsList.includes(appName)) {
// 只移除存在的子应用
state.loadedAppsList = state.loadedAppsList.filter(app => app !== appName)
}
},
通过状态管理,确保子应用的动态加载和卸载过程能高效进行,同时管理子应用的缓存,避免内存溢出。
qiankun规定:若主应用history模式,则子应用可以是hash或history模式;若主应用hash模式,则子应用必须为hash模式。
前面我们通过当前的环境,加载对应子应用的入口 URL(如开发、测试、生产等)动态配置。因此,主应用与子应用分别运行在本地开发环境中,主应用通过 子应用本地入口地址加载子应用。
优点:子应用支持独立运行,便于快速调试子应用逻辑,但本地可能同时启动多个服务会占用系统资源。
registerMicroApps([
{
name: 'subApp1',
entry: '//localhost:8081', // 子应用1的本地地址
container: '#subApp1',
activeRule: '/subapp1',
},
{
name: 'subApp2',
entry: '//localhost:8082', // 子应用2的本地地址
container: '#subApp2',
activeRule: '/subapp2',
},
]);
在前面子应用路由配置中,子应用通过环境变量来动态设置 base 路径,实现子系统独立运行,快速验证功能。
export default new Router({
// 根据子系统编码区分路径,动态设置 base 路径
base: window.__POWERED_BY_QIANKUN__ ? '/033' : process.env.BASE_URL,
mode: 'history',
...
})
主应用通过代理(proxy)访问子应用本地服务,解决跨域问题。配置复杂,多个子应用需维护代理规则。
在使用 qiankun 微前端框架时,子应用通过内部的 keep-alive 特性来实现页面或组件的缓存功能,从而优化页面性能和用户体验。
// 子应用中
<keep-alive>
<router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<router-view v-else></router-view>
子应用内的实现与主应用解耦,符合单一职责的设计理念,控制灵活,但子应用需要自己处理状态管理逻辑。
微前端支持不同框架的子应用,通过监听页面 URL 变化来切换不同的子应用。
popstate
和hashchange
事件,触发时加载或卸载子应用。重新方法和事件监听:const originalPushState = window.history.pushState
window.history.pushState = function (state, title, url) {
const result = originalPushState.call(this, state, title, url)
loadApps() // 根据当前 url 加载或卸载 app
return result
}
window.addEventListener('popstate', () => loadApps(), true)
window.addEventListener('hashchange', () => loadApps(), true)
loadApps()
方法根据当前 URL 和子应用的触发规则加载或卸载子应用。export async function loadApps() {
// 获取所有需要处理的子应用状态
const toUnMountApp = getAppsWithStatus(AppStatus.MOUNTED);
const toLoadApp = getAppsWithStatus(AppStatus.BEFORE_BOOTSTRAP);
const toMountApp = [
...getAppsWithStatus(AppStatus.BOOTSTRAPPED),
...getAppsWithStatus(AppStatus.UNMOUNTED)
];
// 执行卸载、初始化和加载子应用的操作
await Promise.all([
...toUnMountApp.map(unMountApp), // 卸载失活的子应用
...toLoadApp.map(bootstrapApp), // 初始化新注册的子应用
...toMountApp.map(mountApp) // 加载符合条件的子应用
]);
}
根据子应用状态,卸载、初始化和加载子应用,并通过 Promise.all() 并行执行,确保生命周期管理与 URL 变化同步。
bootstrap()
、mount()
、unmount()
三个方法。bootstrap() 初始化,仅触发一次、mount() 每次加载时触发子应用渲染、unmount() 每次卸载时触发。registerApplication()
用于注册子应用,start()
方法启动微前端框架,执行 loadApps() 去加载子应用。let vueApp
// 注册 Vue 子应用
registerApplication({
name: 'vue',
loadApp() {
return Promise.resolve({
bootstrap() { console.log('vue bootstrap') }, // 初始化
mount() {
console.log('vue mount') // 挂载
vueApp = Vue.createApp({ data: () => ({ text: 'Vue App' }), render() { return Vue.h('div', this.text) } })
vueApp.mount('#app')
},
unmount() {
console.log('vue unmount') // 卸载
vueApp.unmount()
},
})
},
activeRule: (location) => location.hash === '#/vue', // 激活规则
})
使用 entry 参数,配置子应用HTML入口,自动加载资源文件。解析 HTML 并提取
通过 AJAX 获取子应用入口文件的 HTML 内容。
export function loadSourceText(url: string): Promise<string> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 请求成功时解析响应内容
xhr.onload = (res: any) => { resolve(res.target.response) }
// 请求失败或中止时处理错误
xhr.onerror = () => reject(new Error('Network error'));
xhr.onabort = () => reject(new Error('Request aborted'));
// 初始化并发送请求
xhr.open('GET', url);
xhr.send();
});
}
解析 HTML 中
export const globalLoadedURLs: string[] = [];
function extractScriptsAndStyles(node: Element, app: Application) {
if (!node.children.length) return { scripts: [], styles: [] };
const styles: Source[] = [];
const scripts: Source[] = [];
for (const child of Array.from(node.children)) {
const tagName = child.tagName;
const isGlobal = !!child.getAttribute('global');
const url = child.getAttribute(tagName === 'SCRIPT' ? 'src' : 'href') || '';
// 跳过重复加载的资源
if (url && (app.loadedURLs.includes(url) || globalLoadedURLs.includes(url))) continue;
if (tagName === 'STYLE') { // 提取