基于微前端qiankun的tab切换

news2025/1/15 19:39:03

文章目录

    • 背景
    • 主应用要做的
      • 1、新建tab组件
      • 2、引入组件
      • 3、tabs.js核心
      • 4、开始使用
    • 子应用要做的
      • 1、将父应用传给子应用的props挂载在Vue对象上
      • 2、创建核心逻辑
      • 3、将核心逻辑混入到App.vue
    • 注意事项
    • 分析@zy/qiankun-tabs源码
      • index.js
      • actions.js
      • tabs.js
    • 最终效果

背景

我们的平时做后台,如果项目越来越大,就会使用一些微前端框架来满足我们的需求,拆分为一个一个的小型应用。今天我们来讨论的就是基于微前端框架 qiankun,如何做一个全局的 tab 切换的功能。

首先一个基本的 tab 切换功能是很简单的,甚至直接使用 element-ui 等组件库的组件就可以完成,但是基于微前端的 tab 切换需要考虑以下几点:

  1. 当切换到的页面是子应用时候,需要去加载对应的子应用页面
  2. 列表页打开不同的详情页要打开多个详情页tab
  3. tab 切换时要保存页面状态
  4. 每个 tab 标签可以自定义标题
  5. 列表页跳转详情页改变状态后,再返回列表页自动更新列表状态

基于以上几个问题,我们一点一点的来完善一个 tab 切换功能。
首先我们希望,主应用只是一个壳子,最好是没有页面的,或者只有一两个页面,这样我们可以尽可能的把精力放在子应用。

主应用要做的

假设我们的微前端现在有一个主应用main、一个微应用app1

1、新建tab组件

在主应用 main 新建文件 components/tabs-switch.vue,这里把组件具体样式的制作留给业务来做(这里以elementUI的tab切换为例)

<template>
  <!-- 使用elementUI 的tab组件 -->
  <div class="ele-tab-main">
    <el-tabs
      :value="activeTab.fullPath"
      type="card"
      :closable="tabsList.length > 1"
      @tab-remove="removeElementTab"
      @tab-click="changeElementTab"
    >
      <el-tab-pane
        v-for="(item) in tabsList"
        :key="item.fullPath"
        :label="item.title"
        :name="item.fullPath"
      />
    </el-tabs>
  </div>
</template>

<script>

export default {
  props: {
    tabsList: {
      type: Array,
      default: () => []
    },
    activeTab: {
      type: Object,
      default: () => {}
    }
  },
  methods: {
    // 使用elementUI 的tab组件
    changeElementTab (tab) {
      if (tab.name === this.activeTab.fullPath) {
        return
      }
      this.$router.push(tab.name)
    },
    removeElementTab (path) {
      if (this.tabsList.length === 1) {
        return
      }
      const index = this.tabsList.findIndex(item => item.fullPath.startsWith(path))
      this.$emit('removeTab', this.tabsList[index], index)
    }
  }
}
</script>

<style lang="scss" scoped>
.ele-tab-main {
  background-color: #fff;
}

</style>

这里注意我们很多地方使用的 fullPath 来判断,而不是使用 path,是因为我们想实现上面说的第 2 点(列表页打开不同的详情页要打开多个详情页tab),这样我们的 url 只要稍有变化,就会新打开一个 tab 页,但是如果你想要列表页打开详情页只打开一个标签,那么你可以用 path 来进行判断。

可以看到,这里的组件是一个纯 ui 组件,其逻辑和参数都由外部来控制,而外部就是我们 tab 核心。

2、引入组件

在主应用 main 的 App.vue 中加上如下代码,其中具体的分析我都写在了代码的注释中

<template>
  <el-main id="mainBox">
  	// 引入tab切换组件
    <TabsSwitch :tabsList="tabsList" :activeTab="activeTab" @removeTab="removeTab"></TabsSwitch>
    // 主应用缓存,如果主应用没有页面,那么则不需要加keep-alive
    <keep-alive :include="loadedRouteNames">
      <router-view v-show="$route.name" :key="key"></router-view>
    </keep-alive>
    // 子应用挂载的节点,最终是根据这个id来挂载的
    <div v-show="!$route.name">
      <div
        v-for="item in copyAppsList"
        v-show="(routerBase + $route.path).startsWith(item.activeRule)"
        :key="item.name"
        :id="item.container.slice(1)"
      ></div>
    </div>
  </el-main>
</template>

<script>
import TabsSwitch from '@/components/tabs-switch.vue'
// tab切换核心逻辑
import tabs from '@/utils/tabs'
// [
//   {
//     name: "App1MicroApp",
//     entry: '//localhost:9001',
//     container: "#app1",
//     activeRule: "/app1",
//     props
//   }
// ];
import appsList from '@/micro/apps.js'
import router from '@/router'

const copyAppsList = appsList.map(item => ({...item}))
export default {
  components: {
    TabsSwitch,
  },
  watch: {
    $route: {
      // <div id="app1"><div id="app"></div></div>
      // <div id="app2"><div id="app"></div></div>
      // 如上两个dom节点,如果先渲染了app1,这时候再点击app2,那么app2中的渲染会被渲染到app1的id="app"里,为了避免这种情况所以需要动态调换dom的顺序
      handler(newValue) {
        const index = appsList.findIndex(item => {
          return (this.routerBase + newValue.path).startsWith(item.activeRule)
        })
        let copyAppsList = appsList
        const spliceArr = copyAppsList.splice(index, 1)
        copyAppsList.unshift(spliceArr[0])
        this.copyAppsList = copyAppsList
      },
      immediate: true,
    }
  },
  data() {
    return {
      tabsList: [],
      activeTab: {},
      loadedRouteNames: [],
      copyAppsList: copyAppsList
    }
  },
  computed: {
    // 加key的目的是:如果不加,跳转/detail?code=0和/detail?code=1,是不会重新触发vue的mounted
    key () {
      return this.$route.fullPath
    },
    routerBase () {
      const routerBase = this.$router.options.base
      if (routerBase === '/' || !routerBase) {
        return ''
      } else {
        return routerBase
      }
    }
  },
  methods: {
    removeTab (item, index) {
      tabs.closeTab(item, index, router)
    },
    getQiankunTabsData () {
      tabs.onStorageChange(state => {
        const { tabsList, activeTab } = state
        this.tabsList = tabsList
        this.activeTab = activeTab
      })
      tabs.onStorageChangeMain(routeNameList => {
        this.loadedRouteNames = routeNameList
      });
    }
  },
  created () {
    this.getQiankunTabsData()
  }
}
</script>

3、tabs.js核心

上面看到我们引入了 @/utils/tabs,作为我们切换的核心逻辑
新建文件utils/tabs.js

// @zy/qiankun-tabs这里的内容在最下面会讲到
import { Tabs } from '@zy/qiankun-tabs'
import appsList from '@/micro/apps'
import router from '@/router/index'

export default new Tabs({
  apps: appsList,
  router: router,
  defaultTitle: '详情',
  routerPush: (url) => {
    router.push(url)
  },
  routerBase: router.options.base
})

4、开始使用

以上基本就接入完成了,我们开始使用的话首先预加载
在main.js加入代码,主要是为了预加载应用

import { prefetchQiankunApps } from "@zy/qiankun-tabs";
import apps from "./micro/apps";

prefetchQiankunApps(apps);

路由守卫进入时候打开tab,在 router/index.js 加入

import tabs from '@/utils/tabs'
// 子应用的路由跳转也可以在主应用这里监听到
router.beforeEach((to, from, next) => {
  const { fullPath, name, path, query, meta } = to
  const data = {
    fullPath,
    name,
    path,
    query,
    meta
  }
  if (to.fullPath === from.fullPath) {
    return
  }
  // 路由进入之前要打开tab
  tabs.openTab(data)
  next()
})

子应用要做的

1、将父应用传给子应用的props挂载在Vue对象上

将父应用传给子应用的props挂载在Vue对象上
main.js

export async function mount(props) {
  // 新增
  Vue.prototype.parentProps = props;
  render(props);
}

2、创建核心逻辑

新建文件 mixin/tab.js

export default function ({ appName='', whiteList=[] }) {
  return {
    computed: {
      // 加key的目的是:如果不加,跳转/detail?code=0和/detail?code=1,是不会重新触发vue的mounted
      key () {
        return this.$route.fullPath
      }
    },
    data() {
      return {
        loadedRouteNames: []
      }
    },
    methods: {
      getLoadedRouteNames (name, fun) {
        if (window.__POWERED_BY_QIANKUN__) {
          this.parentProps.onGlobalStateChange(state => {
            const microApp = state.loadedApp[name];
            if (microApp) {
              const { childRoute } = microApp
              const loadedRoutes = childRoute.map(item => this.$router.resolve(item));
              const loadedRouteNames = loadedRoutes.map(item => item.route.name);
              fun(loadedRouteNames)
            }
          }, true);
        } else {
          fun()
        }
      }
    },
    created () {
      this.getLoadedRouteNames(appName, res => {
        this.loadedRouteNames = window.__POWERED_BY_QIANKUN__ ? res : whiteList;
      })
    }
  }
}

3、将核心逻辑混入到App.vue

App.vue改造

<template>
  <div>
    <keep-alive :include="loadedRouteNames">
      <router-view :key="key" />
    </keep-alive>
  </div>
</template>

<script>
import myTabMixin from './mixin/tab'
const tabMixin = myTabMixin({
  // 必填:该应用的名称,同接入微前端主应用使用的name值
  appName: 'App1MicroApp',
  // 若需要下面的功能3,则将你要刷新的列表组件的 name 填到该数组里,否则置空数组就行
  whiteList: []
})
export default {
  mixins: [tabMixin],
}
</script>

以上三步,子应用必须接入

上面 功能 中说到这三个功能需要子应用接入
1、tab切换时保存状态(需子应用接入)
2、每个tab标签都可以自定义标题,若不填则默认为“详情”(需子应用接入)
3、列表页跳转详情页改变状态后,再返回列表页自动更新列表状态(需子应用接入)

我们看看如何接入:

功能1只需要满足下面的注意事项1就可以
功能2只需要满足下面注意事项2就可以
功能3需要两步骤:
1、上面 App.vue 中有个 whiteList 参数,将你要刷新的列表组件的 name 填到该数组里
2、将你要刷新的列表组件的生命周期函数created或者mounted,换成activated。

注意事项

1、路由名称问题:不管是主应用还是子应用,路由名称需要与组件名称一致,否则tab切换时将无法缓存,举例:

以下路由名称和组件名称应该是相同的
router.js

import HomeView from '@/views/HomeView.vue'
const routes = [
  {
    path: '/home',
    // 路由名称
    name: 'HomeView',
    component: HomeView
  }
]

views/HomeView.vue

<script>
export default {
  // 组件名称
  name: 'HomeView'
}
</script>

2、标题问题:不管子应用还是主应用,路由跳转的时候需要带上参数tabTitle,不带的话默认tab标题为’标题’,例如:

this.$router.push({
  path: '/detail',
  query: {
    tabTitle: '详情页'
  }
})

分析@zy/qiankun-tabs源码

包含三部分:

index.js

import Vue from 'vue'
import {
  loadMicroApp,
  prefetchApps,
  addGlobalUncaughtErrorHandler
} from "qiankun";
import _Tabs from './tabs'
import _actions from './actions'

/**
 * 添加全局的未捕获异常处理器
 */
addGlobalUncaughtErrorHandler((event) => {
  console.error(event);
});

export const prefetchQiankunApps = (apps) => {
  prefetchApps(apps)
}

export const manualLoadMicroApp = (app) => {
  return loadMicroApp(app)
}

export const Tabs = _Tabs

export const actions = _actions


actions.js

import { initGlobalState } from 'qiankun';

const state = {
  loadedApp: {}
};

// 初始化 state,返回值默认有三个函数
// onGlobalStateChange
// setGlobalState
// offGlobalStateChange
const actions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log('主应用检测到state变更:', state, prev);
});

actions.getGlobalStateThroughKey = (key) => {
  return key ? state[key] : state
}

actions.setGlobalStateThroughKey = (key, value) => {
  if (key) {
    actions.setGlobalState({
      ...state,
      ...{
        [key]: value,
      },
    });
  }
}

export default actions;

tabs.js

import { manualLoadMicroApp } from "./index";
// 一个进度条插件
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import actions from "./actions";

//  判断当前页签是否是微应用下的页面
const isMicroApp = function (list, path) {
  return !!list.some((item) => {
    return path.startsWith(item.activeRule);
  });
};

const getIsExistItem = (list, currentItem, code) => {
  const isExistItem = list.findIndex((item) => {
    return item[code] === currentItem[code];
  });
  return isExistItem;
};

// 给当前的原始数据添加一些新参数,变成新的item,作为原始数据
const getCurrentItemForMain = ({ routes, defaultTitle }) => {
  const { query } = routes;
  const obj = {
    appName: "MainApp",
    title: (query && query.tabTitle) || defaultTitle,
    id: "",
  };
  return { ...routes, ...obj };
};
const getCurrentItemForMicro = ({ routes, appConfig, defaultTitle }) => {
  const { query } = routes;
  const obj = {
    appName: appConfig.name,
    title: (query && query.tabTitle) || defaultTitle,
    id: appConfig.container.slice(1),
  };
  return { ...routes, ...obj };
};

class Tabs {
  constructor({ apps, defaultTitle, routerPush, routerBase }) {
    this.appsList = apps;
    this.defaultTitle = defaultTitle || '标题'
    this.routerPush = routerPush
    this.routerBase = routerBase
  }
  // 已加载的微应用
  loadedApp = {};
  loadedMainRoute = [];
  state = {
    tabsList: [],
    activeTab: {}
  }
  callBackFun = null

  getFullUrl (url) {
    const routerBase = this.routerBase
    if (routerBase === '/' || !routerBase) {
      return '' + url
    } else {
      return routerBase + url
    }
  }
  onStorageChange (fun) {
    if (Object.prototype.toString.call(fun) === '[object Function]') {
      this.callBackFun = fun
    }
    if(this.callBackFun) {
      this.callBackFun(this.state)
    }
  }
  onStorageChangeMain (fun) {
    if (Object.prototype.toString.call(fun) === '[object Function]') {
      this.callBackFunMain = fun
    }
    if(this.callBackFunMain) {
      this.callBackFunMain(this.loadedMainRoute)
    }
  }

  getTabsState () {
    return this.state;
  }

  pushTabsList(data, index) {
    this.state.tabsList.splice(index, 0, data)
    this.state.activeTab = data
    this.onStorageChange()
  }

  changeActiveTab(data) {
    this.state.activeTab = data
    this.onStorageChange()
  }

  changeTabsList(data) {
    this.state.tabsList = data
    this.onStorageChange()
  }

  pushMainRoute (routeName) {
    this.loadedMainRoute.push(routeName)
    this.onStorageChangeMain()
  }

  deleteMainRoute (index) {
    this.loadedMainRoute.splice(index, 1);
    this.onStorageChangeMain()
  }


  loadedAppAndGetList(routes, appConfig) {
    const { fullPath } = routes;
    const loadedApp = this.loadedApp;
    try {
      // 判断目前有没有加载过该子应用,如果没有的话,才去加载应用
      // 如果没有加这个判断,所以从主应用跳到app1-home,在从app1-home跳到app1-about就会重新加载app1
      if (!loadedApp[appConfig.name]) {
        NProgress.start();
        const app = manualLoadMicroApp(appConfig);
        NProgress.done();
        loadedApp[appConfig.name] = {
          app: app,
          // 这里为什么要缓存一个子应用路由的数组?因为删除标签的时候,需要判断当前微应用的页面是否全部关闭才能卸载应用
          // 这里缓存主要是提供给子应用,让子应用使用keep-alive

          // 子应用为什么不全部使用keep-alive呢?如果全部使用,会出现这种情况:
          // 打开app1的home页和about页两个标签,在home页做一些操作,在about页面做一些操作,这时候关闭about页面,再重新打开about页,会出现之前的操作还保留的情况(因为由于没有关闭app1所有的页面,所以不会销毁app1,而且全局keep-alive),但是如果这时候是动态的keep-alive就不会有这种情况了
          childRoute: [],
        };
      }
      // '/app-vue-history/about'.replace('/app-vue-history', '')
      // '/about'
      const childRoutePath = this.getFullUrl(fullPath).replace(appConfig.activeRule, "");
      loadedApp[appConfig.name].childRoute.push(childRoutePath);
      return loadedApp;
    } catch (error) {
      console.error(error);
      const host = location.host;
      location.href = `https://${host}/admin/404`;
    }
  }

  // routes是要打开的路由信息
  openTab(routes) {
    // const {
    //   path, // 普通路径
    //   fullPath, // 带参路径
    //   query, // query参数
    //   params, // params参数
    //   meta, // 其他参数
    //   name, // 路由name
    // } = routes;
    const {
      path,
      fullPath
    } = routes;
    const { activeTab, tabsList } = this.getTabsState();

    if (activeTab.fullPath === fullPath) {
      return;
    }

    // ----------------主应用逻辑start--------------------
    if (!isMicroApp(this.appsList, this.getFullUrl(path))) {
      const defaultTitle = this.defaultTitle
      const currentItem = getCurrentItemForMain({ routes, defaultTitle });
      // 当前item在tabList是否存在
      const existItemIndex = getIsExistItem(tabsList, currentItem, "fullPath");
      const existItem = tabsList[existItemIndex]
      // 如果当前tabList已经有了这一条,那么直接打开这一条
      if (existItem) {
        this.changeActiveTab(existItem)
      } else {
        const rightTabIndex = getIsExistItem(tabsList, activeTab, "fullPath");
        this.pushTabsList(currentItem, rightTabIndex + 1)
        this.pushMainRoute(currentItem.name)
      }
      return false;
    }

    // ----------------微应用逻辑start--------------------
    // 获取微应用在apps中的配置
    const appConfig = this.appsList.find((item) =>
      this.getFullUrl(routes.fullPath).startsWith(item.activeRule)
    );
    // 获取当前初始化后的item
    const defaultTitle = this.defaultTitle
    const currentItem = getCurrentItemForMicro({ routes, appConfig, defaultTitle });
    // 当前item在tabList是否存在
    const existItemIndex = getIsExistItem(tabsList, currentItem, "fullPath");
    const existItem = tabsList[existItemIndex]
    // 如果当前tabList已经有了这一条,那么直接打开这一条
    if (existItem) {
      this.changeActiveTab(existItem)
    } else {
      const rightTabIndex = getIsExistItem(tabsList, activeTab, "fullPath");
      this.pushTabsList(currentItem, rightTabIndex + 1)
      this.loadedApp = this.loadedAppAndGetList(currentItem, appConfig);
      // 设置全局变量:已经加载的app列表
      actions.setGlobalStateThroughKey("loadedApp", this.loadedApp);
    }
  }

  // 移除子应用已缓存的应用
  unmountAppAndGetList (routes, appConfig, tabsList) {
    const { fullPath } = routes;
    const loadedApp = this.loadedApp;
    // '/app-vue-history/about'.replace('/app-vue-history', '')
    // '/about'
    const childRoutePath = fullPath.replace(appConfig.activeRule, "");
    const childRouteIndex =
    loadedApp[appConfig.name].childRoute.indexOf(childRoutePath);
    // 删除childRoute中对应的路由
    loadedApp[appConfig.name].childRoute.splice(childRouteIndex, 1);
    // 比如app1的所有标签是否都已经关闭
    const microTabsAllClose = tabsList.every(
      (item) => !this.getFullUrl(item.fullPath).startsWith(appConfig.activeRule)
    );
    if (microTabsAllClose) {
      loadedApp[appConfig.name].app.unmount();
      loadedApp[appConfig.name] = null;
    }

    return loadedApp;
  }

  closeTab(routes, index) {
    const { activeTab, tabsList } = this.getTabsState()

    const { path } = routes
    // 删除后当前选中的
    tabsList.splice(index, 1);
    this.changeTabsList(tabsList)

    // 以下是显示哪个activeTab的逻辑,现在的tabList是删除掉以后的tabList
    // 如果点击叉号的routes是activeTab,那么activeTab右边的重置为最新的activeTab
    if (routes.fullPath === activeTab.fullPath) {
      // 这里的index是原来tabsList的后面的一位
      const rightTab = tabsList[index];
      // 如果点击了最后一位tab,那么activeTab就是当前列表的最后一个,否则就是右边的一个
      if (index === tabsList.length) {
        // 通过路由跳转,直接走router中的beforeEach逻辑了,所以这里的数据就不用管了
        // this.changeActiveTab(tabsList[tabsList.length - 1].fullPath)
        this.routerPush(tabsList[tabsList.length - 1].fullPath);
      } else {
        // this.changeActiveTab(rightTab.fullPath)
        this.routerPush(rightTab.fullPath);
      }
    }

    // 卸载子应用
    if (isMicroApp(this.appsList, this.getFullUrl(path))) {
      // 获取微应用在apps中的配置
      const appConfig = this.appsList.find((item) =>
        this.getFullUrl(routes.fullPath).startsWith(item.activeRule)
      );
      this.loadedApp = this.unmountAppAndGetList(routes, appConfig, tabsList)
      actions.setGlobalStateThroughKey("loadedApp", this.loadedApp);
    } else {
      this.deleteMainRoute(index)
    }
  }
}

export default Tabs;

最终效果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/398895.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1.1 误差的来源

不难发现&#xff0c;考察用计算机解决科学计算问题时所经历的几个环节&#xff08;如图1-1所示&#xff09;&#xff0c;其中每一步都可能产生误差&#xff0c;首先,数学模型是通过对实际问题进行抽象与简化得到的&#xff0c;它与实际问题之间有误差&#xff0e;数学模型与实…

Nginx 是如何进化到 kong 的

Nginx 是如何进化到 kong 的 在传统的互联网服务中,对网关的主要诉求就是反向代理、负载均衡、路由等基础功能。 一个经典的业务的架构图一般是采用四层 LVS 做 对外 IP 收敛,在七层采用 Nginx 来负责七层 HTTPS 协议接入,反向代理、负载均衡、路由。 Nginx 的每个 Worker…

DevOps工具集合

简介 DevOps&#xff08;Development和Operations的组合词&#xff09;是一组过程、方法与系统的统称&#xff0c;用于促进开发&#xff08;应用程序/软件工程&#xff09;、技术运营和质量保障&#xff08;QA&#xff09;部门之间的沟通、协作与整合。 它是一种重视“软件开…

近红外染料IR-825,IR-825 Maleimide,IR-825 Mal,IR-825马来酰亚胺

中文名称&#xff1a;荧光染料IR 825马来酰亚胺&#xff0c;近红外染料IR-825马来酰亚胺英文名称&#xff1a;IR825 Maleimide&#xff0c;IR-825 Maleimide&#xff0c;IR 825 Maleimide&#xff0c;IR825 Mal&#xff0c;IR-825 Mal试剂规格&#xff1a;10mg&#xff0c;25mg…

算法小抄5-原地哈希

书接上回,学会了数组中重复数字的解法三,相信接下来的题也难不倒你 找到数组中消失的数字 题目链接 题意 对于一个大小为n的数组,数组中所有的数都在[1,n]内,其中有些数字重复了,由于有些数字重复了,另一些数字就一定会确实,这次需要找到所有缺少的数字并且返回结果 有没有发…

反转链表相关的练习(上)

目录 一、反转链表 二、反转链表 || 三、两两交换链表中的结点 四、K 个一组翻转链表 一、反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,…

ip-guardip-guard如何通过准入网关对指定的服务器进行通讯加密保护?

1、准入网关在高级配置设置受保护服务器; WEB管理界面【系统工具】,点击【配置管理】,点击参数设置,进入高级配置界面,输入配置内容即可。 [ControlServer]

使用阈值图修改角色脸部阴影

大家好&#xff0c;我是阿赵。 之前介绍了通过修改模型顶点法线的方法&#xff0c;来解决角色模型脸部光影问题。这里再顺便介绍一下第二种方法&#xff0c;使用阈值图修改角色脸部阴影 一、角色脸部光影的问题 这个问题之前讨论过&#xff0c;由于角色脸部法线复杂&#xff0…

2023年最新交通航线(飞机、高铁)信息数据合集(含经纬度匹配)

中国高铁航线数据库Chinese High-speed Rail and Airline Database&#xff0c;CRAD&#xff09;是一个专门收集和管理航空公司和高铁公司交通航线信息的数据仓库。它包含了航线的起始点、终止点、中转点、飞行时间、票价、座位数、乘客数量、货物数量等信息。 该数据仓库可以运…

Fortinet 发布《2022下半年度全球威胁态势研究报告》,七大发现值得关注

全球网络与安全融合领域领导者Fortinet&#xff08;NASDAQ&#xff1a;FTNT&#xff09;&#xff0c;近日发布《2022 下半年度全球威胁态势研究报告》。报告指出&#xff0c;相对于组织攻击面的不断扩大以及全球威胁态势的持续演进&#xff0c;网络犯罪分子设计、优化技术与战术…

设计模式---单例模式

目录 1 简介 2 实现 3 单例模式的几种实习方式 1. 饿汉式 2. 懒汉式&#xff0c;线程不安全 3. 懒汉式&#xff0c;线程安全 4. 双检锁/双重校验锁(DCL, double-check locking) 5. 登记式/静态内部类 4 单例模式的优缺点 1 简介 单例模式(Singleton Pattern) 是 Java…

React 中五种常见的样式

React 中五种常见的样式策略 React中的样式策略主要有以下几种&#xff1a; 内联样式&#xff1a; 内联样式就是在JSX元素中&#xff0c;直接定义行内的样式&#xff1b;CSS样式表&#xff1a; 这也是我们最常用的样式策略&#xff0c;使用单独的样式表&#xff0c;使用CSS或…

lvgl 笔记 按钮部件 (lv_btn) 和 开关部件 (lv_switch)

按钮基础使用方法&#xff1a; lv_btn 和 lb_obj 使用方法一样&#xff0c;只是外表并不相同&#xff0c;基础创建方法只需一行代码。 lv_obj_t* btn lv_btn_create(lv_scr_act()); 添加大小和位置&#xff1a; lv_obj_t* btn lv_btn_create(lv_scr_act()); lv_obj_set_s…

一个小故障:vTaskGenericNotifyGiveFromISR卡死的解决

平台&#xff1a;gd32f103 freertos V10.4.3 LTS Patch 2 调试的时候发现一个问题&#xff1a; 在中断中使用 vTaskNotifyGiveFromISR(TaskHandle_ToCpu_IIC,NULL); //唤醒任务 但是程序却出现卡死现象&#xff1a; 在vTaskGenericNotifyGiveFromISR函数中。 用调试器看到…

C++ STL:string类的概述及常用接口说明

目录 一. 什么是STL 二. string类的概述 三. string类的常用接口说明 3.1 字符串对象创建相关接口&#xff08;构造函数&#xff09; 3.2 字符串长度和容量相关接口 3.3 字符访问相关接口函数 3.4 字符串删改相关接口函数 3.5 字符查找和子串相关接口函数 3.6 迭代器相…

c++11右值引发的概念

右值引用右值&&左值c11增加了一个新的类型&#xff0c;右值引用&#xff0c;记作&#xff1a;&&左值是指在内存中有明确的地址&#xff0c;我们可以找到这块地址的数据&#xff08;可取地址&#xff09;右值是只提供数据&#xff0c;无法找到地址&#xff08;不…

跨时钟域CDC

https://www.cnblogs.com/icparadigm/p/12794483.html https://www.cnblogs.com/icparadigm/p/12794422.html 亚稳态 是什么 时序逻辑在跳变时&#xff0c;由于异步信号、跨时钟域等原因&#xff0c;不满足setup或hold条件&#xff0c;输出在0和1之间产生振荡。 原因 D触发…

Canny算法原理和应用

Canny算法的原理使用高斯滤波器滤波使用 Sobel 滤波器滤波获得在 x 和 y 方向上的输出&#xff0c;在此基础上求出梯度的强度和梯度的角度edge为边缘强度&#xff0c;tan为梯度方向上图表示的是中心点的梯度向量、方位角以及边缘方向&#xff08;任一点的边缘与梯度向量正交&am…

如何在MySQL 8中实现数据迁移?这里有一个简单易用的方案

文章目录前言一. 致敬IT领域的那些女性二. 进制方式安装MySQL2.1 下载软件包2.2 配置环境&#xff1a;2.2.1 配置yum环境2.2.2 配置安全前的系统环境2.3 开始安装2.4 初始化MySQL2.5 修改配置文件2.6 将MySQL设为服务并启动测试三. MySQL数据迁移总结前言 正好赶上IT女神节&am…

《Linux运维实战:ansible中的变量定义及以及变量的优先级》

一、配置文件优先级 Ansible配置以ini格式存储配置数据&#xff0c;在Ansible中⼏乎所有配置都可以通过Ansible的Playbook或环境变量来重新赋值。在运⾏Ansible命令时&#xff0c;命令将会按照以下顺序查找配置⽂件。 # ⾸先&#xff0c;Ansible命令会检查环境变量&#xff0c…