微前端架构 qiankun
背景:随着业务功能的扩展,原有开发模式已无法满足需求。上线后出现问题时,排查过程变得异常复杂,新开发人员也难以迅速理解现有代码。同时,系统间界面风格和交互差异较大,导致跨系统办理业务时工作量增加。因此,引入微前端架构,以支持团队协作、实现独立部署,并提升开发效率。
微前端
微前端 qiankun:基于 single-spa 实现的微前端框架,允许多个子应用在一个主应用中独立运行且互不干扰,适用于大型应用或多团队协作场景。其优点包括:与技术栈无关,支持子应用独立开发和部署,提供开箱即用的 API,易于上手,且社区活跃,支持良好。
qiankun 的特点
- 技术栈无关:支持不同技术栈的主子应用(如 React、Vue、Angular 等),集成简单。
- 动态加载静态资源:通过 HTML
解析动态加载子应用 JS 和 CSS,无需强耦合。 - 沙箱隔离:提供 Proxy 或快照沙箱,避免全局变量和样式污染。
- 独立部署:子应用可单独开发和部署。
- 应用间通信:支持props或全局状态管理initGlobalState,实现主子应用及子应用间数据交互。
- 动态加载:支持动态注册和加载子应用。 生命周期管理:提供bootstrap、mount、unmount等钩子。
- 多路由模式:兼容
Hash 和 History 模式,支持嵌套路由配置。
微前端框架 qiankun,支持不同技术栈的子应用,提供沙箱隔离、独立部署、生命周期管理、应用间通信等功能,能够动态加载和注册子应用,兼容 Hash 和 History 路由模式,灵活且易于集成。
qiankun vs ifream
Qiankun 和 iframe 都可以用来实现微前端架构,但它们的实现方式和应用场景有所不同。
-
qiankun 是基于 JavaScript
的微前端框架,允许子应用共享主应用环境,并通过全局状态管理和路由共享实现协调和通信,增强灵活性和可维护性。 -
iframe 通过独立窗口隔离子应用,部署简单且子应用完全独立,互不影响,适合嵌套简单页面。由于隔离性强,导致路由刷新丢失、状态和 DOM 不共享,交互复杂。每次加载需重建上下文和资源,性能开销大。
实现方案
安装依赖:npm i qiankun -S
主应用基础配置
主应用入口文件中注册子应用信息。
1. 异步请求系统树数据
// 定义全局消息传递对象,存储主应用的状态和 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), // 获取子应用的入口 URLrender,activeRule: genActiveRule(item.code), // 生成子应用激活规则props: { ...msg, permissibleMenu: res.data.treeMenuList }, // 传递给子应用的属性}))// 注册子应用信息...} catch (error) {console.error('获取系统树失败:', error)}
}
注意:在注册子应用后,上述的 props 字段,传递给子应用的属性。
2. 子应用的生命周期管理
执行子应用的注册 API,并启动微前端框架。
registerMicroApps(apps)// 第一个子应用加载完毕回调
runAfterFirstMounted(() => {})// 启动微前端框架
start({sandbox: false, // 关闭沙盒模式,确保子应用的 window 对象正常prefetch: 'all', // 启用所有子应用的预加载
})// 设置全局未捕获异常处理器
addGlobalUncaughtErrorHandler((event) => console.log(event))
3. 主应用渲染函数
主应用的渲染逻辑:只在首次渲染时创建 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 等待系统树获取并初始化子应用
}
4. 环境配置与子应用入口 URL 获取
子应用的入口 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]
}
子应用基础配置
在微前端架构中,子应用需要做一些配置,与主应用进行良好的集成。子应用配置如下:
1. 配置信息
在 src/public-path.js 文件中,添加如下信息,确保子应用正确加载资源路径:
if (window.__POWERED_BY_QIANKUN__) {__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
若当前运行在微前端环境中,webpack_public_path 会被动态设置为 qiankun 注入的公共路径,确保子应用在qiankun环境下能正确加载资源路径。
2. 入口文件
子应用的生命周期钩子函数分别为 bootstrap、mount 和 unmount。它们在子应用初始化、挂载、卸载时执行:
import './public-path'; // 引入 public-path.jslet 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 = nullrouter = null
}
3. 打包配置
在 vue.config.js 中配置打包成 UMD 格式,以支持微前端架构:
const { name } = require('./package');module.exports = {// 其他配置项...configureWebpack: {output: {library: `${name}`,libraryTarget: 'umd',jsonpFunction: `webpackJsonp_${name}`,},},
};
4. 子应用路由配置
在子应用的 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() // 结束进度条})
1. 设置子应用信息
在每次路由跳转前,检查页面路由配置,确保在非登录页面(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);}
}
2. 处理未登录用户
未登录用根据白名单判断是否允许访问,否则重定向到登录页并带上当前页面的跳转路径。
const whiteList = ['passport']function handleGuestUser(to, next) {if (whiteList.includes(to.name)) {next() // 白名单页面直接进入} else {next({ path: '/passport', query: { redirect: to.fullPath } })NProgress.done()}
}
3. 处理已登录用户
对于已登录的用户,加载权限、子系统菜单和按钮权限等。下面通过loadMicroAppMenu函数异步加载子系统菜单,并根据页面配置加载对应的按钮权限。
function handleLoggedInUser(to, next) {loadUserPermissions(to, next) // 加载用户权限loadMicroAppMenu(to, next) // 加载子系统菜单loadActionPermissions(to, next) // 获取按钮权限
}
3.1 加载用户权限
如果权限为空,请求并生成权限路由。
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);}}
}
3.2 加载子系统菜单
对于多子系统应用,每次路由跳转时,根据当前路径检查当前菜单是否已经加载。如果未加载则向后端请求菜单数据,加载对应的子系统菜单。
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 方法加载子应用。
3.3 获取按钮权限
根据页面的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(); // 没有按钮权限控制直接跳转}
}
4. 异常处理和重定向逻辑
当发生错误时(如请求失败或用户没有访问权限),统一处理错误并重定向到登录页面。
// 统一处理错误提示
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()})
}
子应用路由守卫
1. 子应用 mount 挂载
子应用通过 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');
}
2. 配置路由守卫
配置 beforeEach 和 afterEach 守卫,处理权限校验、页面标题设置及进度条管理:
- 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();});
}
3. 未登录用户
未登录用户可以直接访问免登录白名单中的页面,否则会被重定向到登录页:
function handleGuestUser(to, next) {if (whiteList.includes(to.name)) {next();} else {next({ path: '/passport', query: { redirect: to.fullPath } });NProgress.done();}
}
4. 已登录用户
已登录用户进行动态路由和按钮权限的初始化:
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);}
}
4.1 加载动态路由
只有在权限菜单为空时,才会请求后端接口获取权限数据并生成路由,避免重复请求。
/*** 确保动态路由已加载*/
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';
}
4.2 初始化按钮权限
加载指定菜单的按钮权限并将其保存到 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 传递数据 和 全局状态管理 常用的应用间通信方式,用于主应用与子应用之间,或子应用之间的数据传递和事件触发,具备简单易用、与框架高度集成的特点。
1. Props 传递数据
在 Qiankun 框架中,将主应用传递的props注入子应用。子应用通过props获取主应用的数据和方法。Qiankun 支持主应用传递 props 注入子应用,在子应用的mount方法中接受并使用props,实现主应用与子应用之间的通信。实现步骤如下:
- 主应用: 在注册子应用时,通过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); },},},
]);
- 子应用: 在 mount方法中接收并使用props。
export async function mount(props) {console.log('Props from main app:', props);const { userInfo, globalState, setGlobalState } = props;// 调用主应用方法setGlobalState({ theme: 'light' });
}
适用于主应用向子应用单向传递初始化数据,静态数据传递,简单高效,如:子应用初始化配置。
2. 全局状态管理
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());
- 子应用:通过 props 获取全局状态,并监听状态变化或更新全局状态。
export async function mount(props) {const { onGlobalStateChange, setGlobalState } = props;// 监听全局状态变化onGlobalStateChange((state, prev) => {console.log('State changed:', state, prev);});// 更新全局状态setGlobalState({ user: 'guest' });
}
适用于多子应用共享状态,且支持双向同步,能够应对复杂的跨应用通信需求,如登录状态共享、子应用联动和跨应用动态更新。
3. 基于浏览器 localStorage 或 sessionStorage
主应用和子应用通过共享的 localStorage 或 sessionStorage 存储数据,但需要注意跨域限制。
待补充:单点登录。
主子应用通信频道
在基于微前端架构的开发中,主应用与子应用之间的通信、状态管理是核心问题之一。微前端架构需要主应用满足:
- 动态加载子应用。
- 管理子应用的权限菜单。
- 控制加载状态,提供缓存机制,避免重复加载。
- 保持已加载子应用数量在合理范围内,卸载超出部分的子应用以节省资源。
1. 加载子应用
通过 qiankun 提供的 loadMicroApp 动态加载子应用。加载逻辑如下:
- 在加载子应用之前,检查是否已达到缓存上限(5个),如超过则卸载最早加载的子应用。
- 检查子应用是否已加载,避免重复加载。
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 })}
}
2. 预加载
根据用户权限菜单筛选出符合权限的子应用,通过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 // 重新抛出错误以便上层处理}
},
3. 缓存与卸载优化
为了优化内存使用,主应用限制了最多保留 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)}
},
- 添加未加载的子应用:子应用加载成功,且未加载过,则将其添加到 loadedAppsList 列表中。
- 移除卸载的子应用:子应用实例为 null,且子应用加载过, 从 loadedAppsList 中移除该子应用。
通过状态管理,确保子应用的动态加载和卸载过程能高效进行,同时管理子应用的缓存,避免内存溢出。
qiankun 使用问题集
1. 主应用与子应用路由选择
qiankun规定:若主应用history模式,则子应用可以是hash或history模式;若主应用hash模式,则子应用必须为hash模式。
- 主应用和子应用都设置为 history 模式,主应用通过动态路由前缀区分子应用。主子应用路由结构一致,URL 美观且规范,易于维护。
- 主应用和子应用都设置为 hash 模式,主应用通过activeRule配置匹配子应用的hash路由前缀。子应用可以独立运行,URL
可读性差,不利于 SEO。 - 主应用 history模式,子应用hash模式。主应用支持 SEO和URL 美观,子应用也保持独立运行,存在调试和维护稍复杂。
2. 如何调试多个子项目?
2.1 主子应用同时本地运行
前面我们通过当前的环境,加载对应子应用的入口 URL(如开发、测试、生产等)动态配置。因此,主应用与子应用分别运行在本地开发环境中,主应用通过 子应用本地入口地址加载子应用。
优点:子应用支持独立运行,便于快速调试子应用逻辑,但本地可能同时启动多个服务会占用系统资源。
registerMicroApps([{name: 'subApp1',entry: '//localhost:8081', // 子应用1的本地地址container: '#subApp1',activeRule: '/subapp1',},{name: 'subApp2',entry: '//localhost:8082', // 子应用2的本地地址container: '#subApp2',activeRule: '/subapp2',},
]);
2.2 子应用支持独立运行
在前面子应用路由配置中,子应用通过环境变量来动态设置 base 路径,实现子系统独立运行,快速验证功能。
export default new Router({// 根据子系统编码区分路径,动态设置 base 路径base: window.__POWERED_BY_QIANKUN__ ? '/033' : process.env.BASE_URL,mode: 'history',...
})
2.3 配置代理解决跨越
主应用通过代理(proxy)访问子应用本地服务,解决跨域问题。配置复杂,多个子应用需维护代理规则。
3. 如何实现 keep-alive 的需求吗?
3.1 子应用内部实现 keep-alive
在使用 qiankun 微前端框架时,子应用通过内部的 keep-alive 特性来实现页面或组件的缓存功能,从而优化页面性能和用户体验。
// 子应用中
<keep-alive><router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<router-view v-else></router-view>
子应用内的实现与主应用解耦,符合单一职责的设计理念,控制灵活,但子应用需要自己处理状态管理逻辑。
实现一个微前端框架
1. 核心原理
微前端支持不同框架的子应用,通过监听页面 URL 变化来切换不同的子应用。
- 重写 pushState() 和 replaceState() 方法,根据 URL 变化加载或卸载子应用。监听
popstate
和hashchange
事件,触发时加载或卸载子应用。重新方法和事件监听:
const originalPushState = window.history.pushStatewindow.history.pushState = function (state, title, url) {const result = originalPushState.call(this, state, title, url)loadApps() // 根据当前 url 加载或卸载 appreturn 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 变化同步。
2. 子应用的生命周期管理
- 子应用必须暴露
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', // 激活规则
})
3. 加载子应用
使用 entry 参数,配置子应用HTML入口,自动加载资源文件。解析 HTML 并提取
3.1 加载 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();});
}
3.2 提取资源
解析 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') { // 提取 <style> 标签内容styles.push({ isGlobal, value: child.textContent || '' });} else if (tagName === 'SCRIPT') { // 提取 <script> 标签内容和属性scripts.push({ isGlobal, type: child.getAttribute('type'), value: child.textContent || '', url: url || undefined });} else if (tagName === 'LINK' && child.getAttribute('rel') === 'stylesheet' && url) {// 提取 <link rel="stylesheet"> 标签styles.push({ isGlobal, value: '', url });} else {// 递归处理子节点const result = extractScriptsAndStyles(child, app);scripts.push(...result.scripts);styles.push(...result.styles);}// 更新已加载资源列表并移除节点if (url) (isGlobal ? globalLoadedURLs : app.loadedURLs).push(url);removeNode(child);}return { scripts, styles };
}
3.3 加载样式和逻辑
将样式插入主应用页面,将脚本执行后加载子应用逻辑。
export function addStyles(styles: (string | HTMLStyleElement)[]) {styles.forEach(item => {// 如果是字符串,则创建 <style> 标签;否则直接使用现有的 HTMLStyleElementconst node = typeof item === 'string'? Object.assign(document.createElement('style'), { type: 'text/css', textContent: item }): item;// 将样式节点添加到 <head>document.head.appendChild(node);});
}
3.4 挂载子应用内容
保存子应用的 HTML 内容,并在挂载前赋值给容器。通过调用 mount()
渲染子应用,即:子应用的body内容渲染到指定的 DOM 节点。
// 保存 HTML 代码
app.pageBody = doc.body.innerHTML// 加载子应用前赋值给挂载的 DOM
app.container.innerHTML = app.pageBody
app.mount()
4. 沙箱机制
4.1 Proxy 代理 window
主应用和子应用共享一个 window 对象,导致属性互相覆盖。引入Proxy代理子应用的window对象,避免与父应用共享。
app.window = new Proxy({}, {get(target, key) {// 如果代理对象有该属性,直接返回if (Reflect.has(target, key)) return Reflect.get(target, key);const result = originalWindow[key]; // 否则从父应用的 window 获取return (isFunction(result) && needToBindOriginalWindow(result)) ? result.bind(window) // 如果是函数,绑定 this 到 window: result;},set: (target, key, value) => {this.injectKeySet.add(key); // 记录修改的属性return Reflect.set(target, key, value); // 修改代理对象的属性}
});
通过Proxy代理拦截对子应用 window 对象的操作,实现子应用与父应用的作用域隔离,避免子应用对父应用的 window 产生影响。
让子应用代码读取和修改 window 时,访问的是子应用的代理window对象,而不是父应用的window。前面说到微前端框架通过 entry 拉取子应用 JS 资源并执行,在执行之前,使用 with 语句包裹子应用的代码,将全局 window 指向代理 window。
export function executeScripts(scripts: string[], app: Application) {try {scripts.forEach(code => {if (isFunction(app.loader)) {code = app.loader(code); // 处理代码}// 使用 with 语句将 window 指向代理 windowconst warpCode = `;(function(proxyWindow){with (proxyWindow) {(function(window){${code}\n}).call(proxyWindow, proxyWindow)}})(this);`;new Function(warpCode).call(app.sandbox.proxyWindow); // 执行包裹后的代码});} catch (error) {throw error;}
}
4.2 卸载时清除子应用
在子应用卸载时,需要清除其 window 代理对象、绑定的全局事件和定时器,防止数据残留影响下一次加载。
4.2.1 清除 window 对象
injectKeySet 存储了所有新增的属性,卸载时需要删除对应的属性。
for (const key of injectKeySet) {Reflect.deleteProperty(microAppWindow, key);
}
4.2.2 清除事件和定时器
记录定时器和事件,卸载时清除:
- 定时器:在 setTimeout 和 clearTimeout 中记录和清除定时器。
- 事件监听器:记录事件并在卸载时移除。
// 清除所有定时器
for (const timer of timeoutSet) {originalWindow.clearTimeout(timer);
}// 移除所有事件监听
for (const [type, arr] of windowEventMap) {for (const item of arr) {originalWindowRemoveEventListener.call(originalWindow, type, item.listener, item.options);}
}
4.3 缓存子应用快照
在微前端中,子应用的 JS 文件只加载一次,mount() 方法每次执行前的初始化逻辑不会重复。为解决这个问题,可以通过快照机制记录和恢复子应用的状态。
- 生成快照:在卸载子应用时,保存其 window 状态和事件。
const { windowSnapshot, microAppWindow } = this
const recordAttrs = windowSnapshot.get('attrs')!
const recordWindowEvents = windowSnapshot.get('windowEvents')!// 保存 window 属性
this.injectKeySet.forEach(key => {windowSnapshot.get('attrs')!.set(key, deepCopy(microAppWindow[key]))
})// 保存 window 事件
this.windowEventMap.forEach((arr, type) => {windowSnapshot.get('windowEvents')!.set(type, deepCopy(arr))
})
- 恢复快照:在子应用重新加载时,还原之前记录的 window 状态和事件。
const { windowSnapshot, injectKeySet, microAppWindow, windowEventMap } = this;// 恢复 window 属性
windowSnapshot.get('attrs')?.forEach((value, key) => {injectKeySet.add(key); // 记录属性到 injectKeySetmicroAppWindow[key] = deepCopy(value); // 恢复属性到代理的 window 对象
});// 恢复 window 事件
windowSnapshot.get('windowEvents')?.forEach((events, type) => {windowEventMap.set(type, deepCopy(events)); // 更新事件到当前的事件映射events.forEach(({ listener, options }) => {// 绑定事件到原生 windoworiginalWindowAddEventListener.call(originalWindow, type, listener, options);});
});
4.4 隔离子应用元素作用域
避免查询到子应用范围外的 DOM 元素,重写查询 DOM API,将查询范围限制在子应用的挂载容器内,确保只在子应用容器内查询 DOM。
// 重写 querySelector,将查询范围限制在子应用容器内
Document.prototype.querySelector = function(selector) {const app = getCurrentApp();if (!app || !selector || isUniqueElement(selector)) {return originalQuerySelector.call(this, selector);}return app.container.querySelector(selector); // 限制查询范围
}// 恢复原始 querySelector API
Document.prototype.querySelector = originalQuerySelector;
Document.prototype.querySelectorAll = originalQuerySelectorAll;
同时限制样式作用域,将子应用样式限制在子应用挂载容器内,调整样式作用域,将body改为子应用容器 ID,避免污染全局。
const re = /^(\s|,)?(body|html)\b/g;
cssText.replace(re, `#${app.container.id}`);
5. 子应用样式隔离
为了防止子应用的样式相互干扰,通过标识 DOM 元素、移除样式标签和修改 CSS 规则,实现样式的隔离与独立。
5.1 添加子应用标识
为创建的 DOM 元素添加 single-spa-name 属性,标识所属子应用。
Document.prototype.createElement = function (tagName, options) {const appName = getCurrentAppName(); // 获取当前子应用名称const element = originalCreateElement.call(this, tagName, options); // 调用原生 createElement 方法创建元素if (appName) element.setAttribute('single-spa-name', appName); // 如果有子应用名称,则添加 'single-spa-name' 属性return element; // 返回创建的元素
};
5.2 卸载时移除样式
在子应用卸载时,移除对应的 style 标签。
export function removeStyles(name) {document.querySelectorAll(`style[single-spa-name=${name}]`) // 查询所有对应子应用名称的 style 标签.forEach(style => removeNode(style)); // 遍历并移除这些 style 标签
}
5.3 样式作用域隔离
将样式选择器添加子应用标识,限定样式作用范围。
- 原始样式:div { color: red; }
- 隔离后:div[single-spa-name=vue] { color: red; }
5.4 核心代码
- 遍历 CSS 规则 (cssRules)。
- 替换选择器,添加 [single-spa-name=子应用名]。
- 替换 body 和 html 为子应用挂载容器 ID。
function handleCSSRules(cssRules, app) {// 获取子应用容器的 ID,如果没有则生成一个唯一 IDconst id = app.container.id || `single-spa-id-${count++}`;app.container.id = id; // 设置容器的 ID// 遍历 CSS 规则并为每个选择器添加作用域return Array.from(cssRules).reduce((result, cssRule) => {const { selectorText } = cssRule; // 获取当前 CSS 规则的选择器文本// 将选择器添加子应用名称作为属性,并替换 body 和 htmlconst scopedSelector = selectorText.split(',') // 分割多个选择器.map(text => `${text.trim()}[single-spa-name=${app.name}]`) // 给每个选择器加上单独的作用域.join(',') // 合并多个选择器.replace(/^(\s|,)?(body|html)\b/g, `#${id}`); // 替换 body 和 html 为子应用容器的 IDreturn result + cssRule.cssText.replace(selectorText, scopedSelector); // 将修改后的选择器替换回原 CSS 规则}, '');
}
6. 各应用间通信
通过window.spaGlobalState允许多个应用共享,监听和修改全局状态,同时支持事件订阅/发布。
- 全局状态共享:通过 window.spaGlobalState,各应用共享和修改数据。修改时触发 change 事件,其他应用可以监听。
export default class GlobalState extends EventBus {private state: AnyObject = {} // 存储全局状态的对象private stateChangeCallbacksMap: Map<string, Array<Callback>> = new Map() // 存储每个应用的状态变更回调函数// 设置全局状态,并触发状态变更set(key: string, value: any) {this.state[key] = valuethis.emitChange('set', key) // 触发状态变更事件}// 获取全局状态get(key: string) {return this.state[key]}// 注册状态变化回调,监听状态变更onChange(callback: Callback) {const appName = getCurrentAppName() // 获取当前应用名称if (!appName) return // 如果没有获取到应用名,退出// 如果当前应用没有对应的回调列表,初始化if (!this.stateChangeCallbacksMap.get(appName)) {this.stateChangeCallbacksMap.set(appName, [])}// 将回调添加到当前应用的回调列表中this.stateChangeCallbacksMap.get(appName)?.push(callback)}// 触发状态变更事件,通知所有应用emitChange(operator: string, key?: string) {// 遍历所有注册的应用,调用其回调函数this.stateChangeCallbacksMap.forEach((callbacks, appName) => {const app = getApp(appName) as Applicationif (isActive(app) && app.status === AppStatus.MOUNTED) {// 仅在应用已挂载时触发回调callbacks.forEach(callback => callback(this.state, operator, key))}})}
}
- 事件通信:通过 EventBus 实现应用间的事件订阅和发布功能,支持应用间的通知与交互。
export default class EventBus {private eventsMap: Map<string, Record<string, Array<Callback>>> = new Map() // 存储各应用的事件及回调// 注册事件回调on(event: string, callback: Callback) {if (!isFunction(callback)) { // 确保回调是函数throw Error(`The second param ${typeof callback} is not a function`)}const appName = getCurrentAppName() || 'parent' // 获取当前应用名,默认为父应用// 如果当前应用没有事件列表,初始化const events = this.eventsMap.get(appName) || {}this.eventsMap.set(appName, {...events, [event]: [...(events[event] || []), callback]})}// 触发事件emit(event: string, ...args: any) {// 遍历所有应用的事件,调用相应的回调this.eventsMap.forEach((events, appName) => {const app = getApp(appName) as Application// 仅在应用已挂载或为父应用时触发事件if (appName === 'parent' || (isActive(app) && app.status === AppStatus.MOUNTED)) {events[event]?.forEach(callback => callback(...args)) // 执行事件回调}})}
}
相关文章:
微前端架构 qiankun
背景:随着业务功能的扩展,原有开发模式已无法满足需求。上线后出现问题时,排查过程变得异常复杂,新开发人员也难以迅速理解现有代码。同时,系统间界面风格和交互差异较大,导致跨系统办理业务时工作量增加。…...
RAT:融合RAG和CoT的高效多步推理任务策略
今天分享的是由北京大学、加州大学洛杉矶分校和北京通用人工智能研究院合作发表的一篇文章 论文题目:RAT: Retrieval Augmented Thoughts Elicit Context-Aware Reasoning in Long-Horizon Generation 论文链接:https://arxiv.org/pdf/2403.05313 代码地址:https://githu…...
C++之虚基类
虚基类(Virtual Base Class)是 C 中的一个特性,用于解决菱形继承问题,避免因为多重继承而导致的重复继承和冗余问题。 菱形继承问题 假设有如下的类结构: 一个基类 Base。两个类 Derived1 和 Derived2 继承自 Base。…...
大小写转换
描述 将下面的字符串中的大小写进行转换。 输入描述 输入一行仅包含字母的字符串(字符串长度 ≤100)。 输出描述 将其中的大写转换为小写,小写转换为大写。 abcD ABCd #include<iostream> #include<string> using namespace std; int main() { …...
Flink 热存储维表 使用 Guava Cache 减轻访问压力
目录 背景 Guava Cache 简介 实现方案 1. 项目依赖 2. Guava Cache 集成到 Flink (1) 定义 Cache (2) 使用 Cache 优化维表查询 3. 应用运行效果 (1) 维表查询逻辑优化 (2) 减少存储压力 Guava Cache 配置优化 总结 背景 在实时计算场景中,Flink 应用中…...
09.ES13 10.ES14
9.1、class扩展 9.1.1、类成员声明 在ES13之前,我们只能在构造函数里面声明类的成员,而不能像其他大多数语言一样在类的最外层作用域里面声明成员。不过ES13出来之后,这都不算什么事儿了。现在我们终于可以突破这个限制,写下面这…...
Day 30 贪心算法 part04
今天的三道题目,都算是 重叠区间 问题,大家可以好好感受一下。 都属于那种看起来好复杂,但一看贪心解法,惊呼:这么巧妙! 这种题还是属于那种,做过了也就会了,没做过就很难想出来。 不过大家把如下三题做了之后, 重叠区间 基本上差不多了 452. 用最少数量的箭引爆气球…...
ProtonBase 教育行业解决方案
01/方案概述 当前,大数据、云计算等技术正加速教育行业的数字化转型,教学模式从线下转向线上,传统教育企业向具有互联网性质的新型教育企业转变。在此背景下,教育企业亟需探索多源数据的融合扩展,以应对复杂的业务场景…...
mimic插件使用
最近搞机械臂的末端夹具,本来想用吸盘的插件的,不知道为什么吸盘吸不起来可乐瓶,后面就换成夹爪了。 因为原厂的urdf文件中提供夹爪是用mimic标签控制剩下的5个joint关节的,网上参考的资料太少了,也是废了好多力 气&am…...
Docker+Jenkinsg+Springboot流水式构建-实用篇
最近无聊想自己玩一玩devpos,方便以后接私活,或者学习,O(∩_∩)O, 以后直接安装这篇文档,傻瓜式安装,哈哈 废话不多说,直接进入实战,完成简单的搭建 1.初始化CentOS环境 1.1 关闭防…...
华为小米苹果三星移动设备访问windows共享文件夹windows11
如果移动设备和windows电脑都在同一个局域网内,可以用移动设备访问windows11的共享文件夹 1、设置共享文件夹 2、添加everyone用户即可 3、查看ip地址 4、在华为手机上点击文件管理,里面有个网上邻居 5、正常情况下,华为手机会扫描到同一局域…...
程序执行堆栈执行模拟
所有的文件都是在硬盘(磁盘)上,调用时先调用javac指令的jdk编译成.class然后被java指令的jre送到内存中,java在内存中有自己的一片区域叫JVM,编译进来的文件首先进入方法区。 staitc的属性就是在进入内存的时候开辟了一…...
【AIGC】ChatGPT提示词Prompt助力高效文献处理、公文撰写、会议纪要与视频总结
博客主页: [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: AIGC | 提示词Prompt应用实例 文章目录 💯前言💯高效英文文献阅读提示词使用方法 💯高效公文写作提示词使用方法 💯高效会议纪要提示词使用方法 💯高效视频内…...
电脑文件自动提取器介绍
1. 背景 电脑文件自动提取器是基于元神操作系统开发的一款应用级产品,其初衷是应对当下Windows系统频繁强制升级导致的系统崩溃问题,使得即使电脑系统瘫痪也能轻易提取硬盘中的文件,以免耽误工作和学习等。 2. 介绍 (1…...
Git命令大全(超详细)
Git 是一个分布式版本控制系统,用于跟踪计算机文件的更改,并协调多个用户之间的工作。下面是一份较为详细的 Git 命令大全,涵盖了从初始化仓库到日常使用中常见的操作。 1. 初始化与配置 设置用户信息: git config --global user.name &quo…...
Vue 3 中实现页面特定功能控制
在开发 Vue 应用时,我们经常会遇到需要在特定页面启用或禁用某些功能的情况。本文将以 A父.vue 页面为例,探讨如何在点击汇总菜单时仅在该页面生效,而在其他页面不生效的问题。 1. 利用 Vue 3 的 provide 和 inject 实现状态传递 Vue 3 提供…...
JavaScript原生深拷贝方法 structuredClone使用
structuredClone 简介 structuredClone 是现代浏览器提供的原生 JavaScript 方法,用于深拷贝对象。它可以处理各种复杂数据结构,包括嵌套对象、数组、Date、Map、Set 等,且支持循环引用。 语法 const clone structuredClone(value);value:…...
Wireshark常用功能使用说明
此处用于记录下本人所使用 wireshark 所可能用到的小技巧。Wireshark是一款强大的数据包分析工具,此处仅介绍常用功能。 Wireshark常用功能使用说明 1.相关介绍1.1.工具栏功能介绍1.1.1.时间戳/分组列表概况等设置 1.2.Windows抓包 2.wireshark过滤器规则2.1.wiresh…...
深度学习:自然语言处理
一、引言 自然语言处理作为人工智能领域的关键分支,致力于使计算机能够理解、分析和生成人类语言。近年来,随着深度学习技术的迅猛发展,自然语言处理取得了前所未有的突破,一系列创新技术和应用不断涌现,极大地推动了…...
C底层 函数栈帧
文章目录 一,什么是寄存器 二,栈和帧 前言 我们在学习c语言程序的时候,是不是有很多的疑问,如 1,为什么形参不可以改变实参 2,为什么我们编写程序的时候会出现烫烫烫......这个乱码 3,那些局…...
Linux系统编程——进程替换
目录 前言 二、进程程序替换的概念 三、进程程序替换的原理 编辑 四、为什么需要进行进程程序替换 五、如何进行进程程序替换 1、进程替换函数: 1)execl()函数 2)execv()函数 3) execlp()函数 4) execvp()函数 5)execle函数 6)ex…...
PVE中VLAN的设置要点
使用这个拓扑进行连接无法直接访问PVE PVE 设置如下: 核心重点:PVE 的 vmbr0 接口直接绑定了 enp2s0,这会导致 VLAN 流量无法正确处理,因为 PVE 没有专门为 VLAN 3 配置接口。 1.vmbr0 和 vmbr0.3 都是绑定在物理接口 enp2s0 上&…...
零基础Python学习
1.环境搭建 1.1 安装运行环境python3.13 Welcome to Python.org 1.2 安装集成开发环境PyCharm PyCharm: the Python IDE for data science and web development 1.3 创建项目 && 设置字体 2.基础语法 2.1 常量与表达式 在python中整数除整数不会优化,所…...
命令提示符窗口(CMD)控制windows操作系统
一、关于进程 1. 通过进程ID结束进程: taskkill /PID 进程ID 2. 通过进程名称结束进程 taskkill /IM 进程名称.exe 3. 强制结束进程 taskkill /F /IM 进程名称.exe 4. 结束包含特定字符串的全部进程 taskkill /IM 包含字符串* /T 5. 启动一个新的命令行窗口来运行指…...
虚幻引擎5(Unreal Engine 5)高级教程
虚幻引擎5(Unreal Engine 5)高级教程 引言 虚幻引擎5(Unreal Engine 5,简称UE5)是Epic Games推出的一款功能强大的游戏引擎,广泛应用于游戏开发、影视制作和虚拟现实等领域。UE5以其先进的图形渲染技术、…...
3DMAX星空图像生成器插件使用方法详解
3DMAX星空图像生成器插件,一键生成星空或夜空的二维图像。它可用于创建天空盒子或空间场景,或作为2D艺术的天空背景。 【主要特点】 -单击即可创建星空图像或夜空。 -星数、亮度、大小、形状等参数。 -支持任何图像大小(方形)。…...
【QNX+Android虚拟化方案】129 - USB眼图参数配置
【QNX+Android虚拟化方案】129 - USB眼图参数配置 1. 软件侧dts如何配置眼图参数 及 其对应关系2. 硬件 QNX 侧调试眼图命令2.1 High Speed USB2.0 Host2.2 Super Speed USB3.0 Host3. 硬件 Android 侧调试眼图命令基于原生纯净代码,自学总结 纯技术分享,不会也不敢涉项目、不…...
Linux内核4.14版本——ccf时钟子系统(3)——ccf一些核心结构体
目录 1. struct clk_hw 2. struct clk_ops 3. struct clk_core 4. struct clk_notifier 5. struct clk 6. struct clk_gate 7. struct clk_divider 8. struct clk_mux 9. struct clk_fixed_factor 10. struct clk_fractional_divider 11. struct clk_multiplier 12…...
服务器遭受DDoS攻击后如何恢复运行?
当服务器遭受 DDoS(分布式拒绝服务)攻击 后,恢复运行需要快速采取应急措施来缓解攻击影响,并在恢复后加强防护以减少未来攻击的风险。以下是详细的分步指南: 一、应急处理步骤 1. 确认服务器是否正在遭受 DDoS 攻击 …...
js原型、原型链和继承
文章目录 一、原型1、prototype2、constructor 二、原型链1、字面量原型链2、字面量继承3、构造函数的原型链4、Object.create5、Object.setPrototypeOf 三、继承1、构造函数继承2、原型链继承3、组合继承 四、常见链条1、Function2、Object.prototype 继承是指将特性从父代传递…...
看不见的彼方:交换空间——小菜一碟
有个蓝色的链接,先去看看两年前的题目的write up (https://github.com/USTC-Hackergame/hackergame2022-writeups/blob/master/official/%E7%9C%8B%E4%B8%8D%E8%A7%81%E7%9A%84%E5%BD%BC%E6%96%B9/README.md) 从别人的write up中了解到&…...
传奇996_38——称号系统
记住: 称号是装备,加属性的 特效是顶戴,加特效的 需要两个命令分开设置,称号和特效不关联 角色-称号栏显示的图标:由装备表字段,背包显示Looks控制,图片位置在:stab\res\private\t…...
C++:异常
---什么是异常? 异常是面向对象语法处理错误的一种方式。 ---C语言传统的处理错误的方式有哪些呢? 1.返回错误码,有些API接口都是把错误码放到errno中。 2.终止程序,比如发生越界等严重问题时,我们也可以主动调用exit…...
winScp连接Ubantu系统,访问拒绝的解决方式
一、原理分析 win10系统能够通过WinScp连接到Ubantu系统的前提是Ubantu系统开启ssh服务 二、解决步骤 1、Ubantu系统开启ssh服务 更新软件列表 sudo apt update安装OpenSSH服务器 sudo apt install openssh-server开启SSH服务 service sshd start到此,winScp…...
Oracle 建表的存储过程
建表的存储过程 下面是建表的存储过程,用途:通过不同的表,根据不同过滤条件,得到某个字段,例如neid,然后创建一个新表T,表T的表名为拼接XXXX_XXX_neid,表T的字段自行添加 xxx&…...
芯科科技率先支持Matter 1.4,推动智能家居迈向新高度
Matter 1.4引入核心增强功能、支持新设备类型,持续推进智能家居互联互通 近日,连接标准联盟(Connectivity Standard Alliance,CSA)发布了Matter 1.4标准版本。作为连接标准联盟的重要成员之一,以及Matter标…...
pandas快速解决空列表问题
在使用 Pandas 处理数据时,我们经常会遇到包含空列表(即空值或缺失值)的问题。Pandas 提供了一些非常有效的方法来处理这些空列表,使得数据清理和预处理变得更加简单和高效。 以下是一个示例,展示如何使用 Pandas 快速…...
sentinel使用手册
1.引入依赖 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency>2.yaml spring:cloud:sentinel:transport:dashboard: localhost:8090 #sentinel控制台地址…...
【继承】—— 我与C++的不解之缘(十九)
前言: 面向对象编程语言的三大特性:封装、继承和多态 本篇博客来学习C中的继承,加油! 一、什么是继承? 继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类…...
腾讯微众银行大数据面试题(包含数据分析/挖掘方向)面试题及参考答案
为什么喜欢使用 XGBoost,XGBoost 的主要优势有哪些? XGBoost 是一个优化的分布式梯度增强库,在数据科学和机器学习领域应用广泛,深受喜爱,原因主要在于其众多突出优势。 首先,它的精度高,在许多机器学习竞赛和实际应用中,XGBoost 都展现出卓越的预测准确性。其基于决策…...
组合数练习题——c++
题目设置: 现在有x个相同的小球,分给y个人,每个人至少分k个,请问有多少种可能的分发方法,由于结果可能较大,答案对10^97取模。 输入格式: 一行3个整数:x,y, k…...
Java:JPMS模块化开发
JPMS(Java Platform Module System)简介 为什么用JPMS? JPMS(Java 平台模块系统)是 Java 9 引入的模块化系统,也称为 Jigsaw 项目。它为 Java 提供了更精细的模块化机制,用于组织和管理代码&a…...
Spring Boot中配置Flink的资源管理
在 Spring Boot 中配置 Flink 的资源管理,需要遵循以下步骤: 添加 Flink 依赖项 在你的 pom.xml 文件中,添加 Flink 和 Flink-connector-kafka 的依赖项。这里以 Flink 1.14 版本为例: <!-- Flink dependencies --><de…...
【ruby on rails】dup、deep_dup、clone的区别
一、区别 dup 浅复制:dup 方法创建对象的浅复制。 不复制冻结状态:dup 不会复制对象的冻结状态。 不复制单例方法:dup 不会复制对象的单例方法。 deep_dup 深复制:deep_dup 方法创建对象的深复制,递归复制嵌套的对象。…...
鸿蒙开发-HMS Kit能力集(应用内支付、推送服务)
1 应用内支付 开发步骤 步骤一:判断当前登录的华为账号所在服务地是否支持应用内支付 在使用应用内支付之前,您的应用需要向IAP Kit发送queryEnvironmentStatus请求,以此判断用户当前登录的华为帐号所在的服务地是否在IAP Kit支持结算的国…...
springboot中使用mongodb完成评论功能
pom文件中引入 <!-- mongodb --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> yml中配置连接 data:mongodb:uri: mongodb://admin:1234561…...
南京仁品耳鼻喉专科医院:12月启动公益义诊月
专业医疗资源送至“家门口”!南京仁品耳鼻喉专科医院启动公益义诊月 随着2024年即将步入尾声,南京仁品耳鼻喉医院为回馈社会,提升公众健康福祉,将于12月隆重推出“三甲专家公益义诊月”活动。此次活动旨在通过汇聚众多耳鼻喉领域…...
微信小程序首页搜索框的实现教程
微信小程序首页搜索框的实现教程 前言 在现代移动应用中,搜索功能是用户获取信息的主要方式之一。对于购物小程序而言,提供一个美观且高效的搜索框,可以显著提升用户体验,帮助用户快速找到他们想要的商品。本文将详细介绍如何在微信小程序中实现一个样式优美的搜索框,包…...
Educational Codeforces Round 151 (Rated for Div. 2)
题目链接 B. Come Together 题意 输入 输出 思路 可以将B、C坐标作A的变换,将A平移至原点,然后分情况讨论: B、C两点都在轴上,具体分为同向轴和其他情况B、C两点都在象限中,具体分为相同象限、对角象限和相邻象限分别位于象限…...
第二十一天 深度学习简介
深度学习(Deep Learning,简称DL)是机器学习的一个分支,它通过构建和训练深层神经网络模型,从数据中学习和提取特征,以实现复杂任务的自动化处理和决策。以下是对深度学习的详细介绍: 一、起源与…...