为iframe正名,你可能并不需要微前端

news2024/11/28 23:46:56

作者:刘显安(码怪)

任何新技术、新产品都是有一定适用场景的,它可能在当下很流行,但它不一定在任何时候都是最优解。

前言

最近几年微前端很火,火到有时候项目里面用到了iframe还要偷偷摸摸地藏起来生怕被别人知道了,因为担心被人质疑:你为什么不用微前端方案?直到最近笔者接手一个项目,需要将现有的一个系统整体嵌入到另外一个系统(一共20多个页面),在被微前端坑了几次之后,回过头发现,iframe真香!

qiankun的作者有一篇《Why Not Iframe》 介绍了iframe的优缺点(不过作者还有一篇《你可能并不需要微前端》给微前端降降火),诚然iframe确实存在很多缺点,但是在选择一个方案的时候还是要具体场景具体分析,它可能在当下很流行,但它不一定在任何时候都是最优解:iframe的这些缺点对我来说是否能够接受?它的缺点是否有其它方法可以弥补?使用它到底是利大于弊还是弊大于利?我们需要在优缺点之间找到一个平衡。

优缺点分析

iframe适合的场景

由于iframe的一些限制,部分场景并不适合用iframe,比如像下面这种iframe只占据页面中间部分区域,由于父页面已经有一个滚动条了,为了避免出现双滚动条,只能动态计算iframe的内容高度赋值给iframe,使得iframe高度完全撑满,但这样带来的问题是弹窗很难处理,如果居中的话一般弹窗都相对的是iframe内容高度而不是屏幕高度,从而导致弹窗可能看不见,如果固定弹窗top又会导致弹窗跟随页面滚动,而且稍有不慎iframe内容高度计算有一点点偏差就会出现双滚动条。

所以:

  • 如果页面本身比较简单,是一个没有弹窗、浮层、高度也是固定的纯信息展示页的话,用iframe一般没什么问题;
  • 如果页面是包含弹窗、信息提示、或者高度不是固定的话,需要看iframe是否占据了全部的内容区域,如果是像下图这种经典的导航+菜单+内容结构、并且整个内容区域都是iframe,那么可以放心大胆地尝试iframe,否则,需要慎重考虑方案选型。

为什么一定要满足“iframe占据全部内容区域”这个条件呢?可以想象一下下面这种场景,滚动条出现在页面中间应该大部分人都无法接受:

实战:A系统接入B系统

满足“iframe占据全部内容区域”条件的场景,iframe的几个缺点都比较好解决。下面通过一个实际案例来详细介绍将一个线上在运行的系统接入到另外一个系统的全过程。以笔者前段时间刚完成的ACP(全称Alibaba.com Pay,阿里巴巴国际站旗下一站式全球收款平台,下称A系统)接入生意贷(下称B系统)为例,已知:

  • ACP和生意贷都是MPA页面;
  • ACP系统在此之前没有接入其他系统的先例,生意贷是第一个;
  • 生意贷作为被接入系统,本次需要接入的一共有20多个页面,且服务端包含大量业务逻辑以及跳转控制,有些页面想看看长什么样子都非常困难,需要在Node层mock大量接口;
  • 接入时需要做功能删减,部分接口入参需要调整;
  • 生意贷除了接入到ACP系统中,之前还接入过AMES系统,本次接入需要兼容这部分历史逻辑;

我们希望的效果:

假设我们新增一个页面 /fin/base.html?entry=xxx 作为我们A系统承接B系统的地址,A系统有类似如下代码:

class App extends React.Component {
    state = {
        currentEntry: decodeURIComponent(iutil.getParam('entry') || '') || '',
    };
    render() {
        return <div>
            <iframe id="microFrontIframe" src={this.state.currentEntry}/>
        </div>;
    }
}

隐藏原系统导航菜单

因为是接入到另外一个系统,所以需要将原系统的菜单和导航等都通过一个类似“hideLayout”的参数去隐藏。

前进后退处理

需要特别注意的是,iframe页面内部的跳转虽然不会让浏览器地址栏发生变化,但是却会产生一个看不见的“history记录”,也就是点击前进或后退按钮(history.forward()history.back())可以让iframe页面也前进后退,但是地址栏无任何变化。

所以准确来说前进后退无需我们做任何处理,我们要做的就是让浏览器地址栏同步更新即可。

如果要禁用浏览器的上述默认行为,一般只能在iframe跳转时通知父页面更新整个<iframe />DOM节点。

URL的同步更新

让URL同步更新需要处理2个问题,一个是什么时候去触发更新的动作,一个是URL更新的规律,即父页面的URL地址(A系统)与iframe的URL地址(B系统)映射关系的维护。

保证URL同步更新功能正常需要满足这3种情况:

  • case1: 页面刷新,iframe能够加载正确页面;
  • case2: 页面跳转,浏览器地址栏能够正确更新;
  • case3: 点击浏览器的前进或后退,地址栏和iframe都能够同步变化;

什么时候更新URL地址

首先想到的肯定是在iframe加载完发送一个通知给父页面,父页面通过history.replaceState去更新URL。

为什么不是history.pushState呢?因为前面提到过,浏览器默认会产生一条历史记录,我们只需要更新地址即可,如果用pushState会产生2条记录。

B系统:

<script>
var postMessage = function(type, data) {
    if (window.parent !== window) {
        window.parent.postMessage({
            type: type,
            data: data,
        }, '*');
    }
}
// 为了让URL地址尽早地更新,这段代码需要尽可能前置,例如可以直接放在document.head中
postMessage('afterHistoryChange', { url: location.href });
</script>

A系统:

window.addEventListener('message', e => {
    const { data, type } = e.data || {};
    if (type === 'afterHistoryChange' && data?.url) {
        // 这里先采用一个兜底的URL承接任意地址
        const entry = `/fin/base.html?entry=${encodeURIComponent(data.url)}`;
        // 地址不一样才需要更新
        if (location.pathname + location.search !== entry) {
            window.history.replaceState(null, '', entry);
        }
    }
});

优化URL的更新速度

按照上面的方法实现后可以发现,URL虽然可以更新但是速度有点慢,点击跳转后一般需要等待7-800毫秒地址栏才会更新,有点美中不足。可以把地址栏的更新在“跳转后”基础之上再加一个“跳转前”。为此我们必须有一个全局的beforeRedirect钩子,先不考虑它的具体实现:

B系统:

function beforeRedirect(href) {
    postMessage('beforeHistoryChange', { url: href });
}

A系统:

window.addEventListener('message', e => {
    const { data, type } = e.data || {};
    if ((type === 'beforeHistoryChange' || type === 'afterHistoryChange') && data?.url) {
        // 这里先采用一个兜底的URL承接任意地址
        const entry = `/fin/base.html?entry=${encodeURIComponent(data.url)}`;
        // 地址不一样才需要更新
        if (location.pathname + location.search !== entry) {
            window.history.replaceState(null, '', entry);
        }
    }
});

加上上述代码之后,点击iframe中的跳转链接,URL会实时更新,浏览器的前进后退功能也正常。

为什么需要同时保留跳转前和跳转后呢?因为如果只保留跳转前,只能满足前面的case1和case2,case3无法满足,也就是点击后退按钮只有iframe会后退,URL地址不会更新。

美化URL地址

简单的使用/fin/base.html?entry=xxx这样的通用地址虽然能用,但是不太美观,而且很容易被人看出来是iframe实现的,比较没有诚意,所以如果被接入系统的页面数量在可枚举范围内,建议给每个地址维护一个新的短地址。

首先,新增一个SPA页面/fin/*.html,和前面的/fin/base.html指向同一个页面,然后维护一个URL地址的映射,类似这样:

// A系统地址到B系统地址映射
const entryMap = {
    '/fin/home.html': 'https://fs.alibaba.com/xxx/home.htm?hideLayout=1',
    '/fin/apply.html': 'https://fs.alibaba.com/xxx/apply?hideLayout=1',
    '/fin/failed.html': 'https://fs.aibaba.com/xxx/failed?hideLayout=1',
    // 省略
};
const iframeMap = {}; // 同时再维护一个子页面 -> 父页面URL映射
for (const entry in entryMap) {
    iframeMap[entryMap[entry].split('?')[0]] = entry;
}
class App extends React.Component {
    state = {
        currentEntry: decodeURIComponent(iutil.getParam('entry') || '') || entryMap[location.pathname] || '',
    };
    render() {
        return <div>
            <iframe id="microFrontIframe" src={this.state.currentEntry}/>
        </div>;
    }
}

同时完善一下更新URL地址部分:

// base.html继续用作兜底
let entry = `/fin/base.html?entry=${encodeURIComponent(data.url)}`;
const [path, search] = data.url.split('?');
if (iframeMap[path]) {
    entry = `${iframeMap[path]}?${search || ''}`;
}
// 地址不一样才需要更新
if (location.pathname + location.search !== entry) {
    window.history.replaceState(null, '', entry);
}

省略参数透传部分代码。

全局跳转拦截

为什么一定要做全局跳转拦截呢?一个因为我们需要把hideLayout参数一直透传下去,否则就会点着点着突然出现下面这种双菜单的情况:

另一个是有些页面在被嵌入前是当前页面打开的,但是被嵌入后不能继续在当前iframe打开,比如支付宝付款这种第三方页面,想象一下下面这种情况会不会觉得很怪?所以这类页面一定要做特殊处理让它跳出去而不是当前页面打开。

URL跳转可以分为服务端跳转和浏览器跳转,浏览器跳转又包括A标签跳转、location.href跳转、window.open跳转、historyAPI跳转等;

而根据是否新标签打开又可以分为以下4种场景:

  1. 继续当前iframe打开,需要隐藏原系统的所有layout;
  2. 当前父页面打开第三方页面,不需要任何layout;
  3. 新开标签打开第三方页面(如支付宝页面),不需要做特殊处理;
  4. 新开标签打开宿主页面,需要把原系统layout替换成新layout;

为此,先定义好一个beforeRedirect方法,由于新标签打开有target="_blank"window.open等方式,父页面打开有target="_parent"window.parent.location.href等方式,为了更好的统一封装,我们把特殊情况的跳转统一在beforeRedirect处理好,并约定只有有返回值的情况才需要后续继续处理跳转:

// 维护一个需要做特殊处理的第三方页面列表
const thirdPageList = [
    'https://service.alibaba.com/',
    'https://sale.alibaba.com/xxx/',
    'https://alipay.com/xxx/',
    // ...
];
/**
 * 封装统一的跳转拦截钩子,处理参数透传和一些特殊情况
 * @param {*} href 要跳转的地址,允许传入相对路径
 * @param {*} isNewTab 是否要新标签打开
 * @param {*} isParentOpen 是否要在父页面打开
 * @returns 返回处理好的跳转地址,如果没有返回值则表示不需要继续处理跳转
 */
function beforeRedirect(href, isNewTab) {
    if (!href) {
        return;
    }
    // 传过来的href可能是相对路径,为了做统一判断需要转成绝对路径
    if (href.indexOf('http') !== 0) {
        var a = document.createElement('a');
        a.href = href;
        href = a.href;
    }
    // 如果命中白名单
    if (thirdPageList.some(item => href.indexOf(item) === 0)) {
        if (isNewTab) {
            // _rawOpen参见后面 window.open 拦截
            window._rawOpen(href);
        } else {
            // 第三方页面如果不是新标签打开就一定是父页面打开
            window.parent.location.href = href;
        }
        return;
    }
    // 需要从当前URL继续往下透传的参数
    var params = ['hideLayout', 'tracelog'];
    for (var i = 0; i < params.length; i++) {
        var value = getParam(params[i], location.href);
        if (value) {
            href = setParam(params[i], value, href);
        }
    }
    if (isNewTab) {
        let entry = `/fin/base.html?entry=${encodeURIComponent(href)}`;
        const [path, search] = href.split('?');
        if (iframeMap[path]) {
            entry = `${iframeMap[path]}?${search || ''}`;
        }
        href = `https://payment.alibaba.com${entry}`;
        window._rawOpen(href);
        return;
    }
    // 如果是以iframe方式嵌入,向父页面发送通知
    postMessage('beforeHistoryChange', { url: href });
    return href;
}

服务端跳转拦截

服务端主要是对301或302重定向跳转进行拦截,以Egg为例,只要重写 ctx.redirect 方法即可。

A标签跳转拦截

document.addEventListener('click', function (e) {
    var target = e.target || {};
    // A标签可能包含子元素,点击目标可能不是A标签本身,这里只简单判断2层
    if (target.tagName === 'A' || (target.parentNode && target.parentNode.tagName === 'A')) {
        target = target.tagName === 'A' ? target : target.parentNode;
        var href = target.href;
        // 不处理没有配置href或者指向JS代码的A标签
        if (!href || href.indexOf('javascript') === 0) {
            return;
        }
        var newHref = beforeRedirect(href, target.target === '_blank');
        // 没有返回值一般是已经处理了跳转,需要禁用当前A标签的跳转
        if (!newHref) {
            target.target = '_self';
            target.href = 'javascript:;';
        } else if (newHref !== href) {
            target.href = newHref;
        }
    }
}, true);

location.href拦截

location.href拦截至今是一个困扰前端界的难题,这里只能采用一个折中的方法:

// 由于 location.href 无法重写,只能实现一个 location2.href = ''
if (Object.defineProperty) {
    window.location2 = {};
    Object.defineProperty(window.location2, 'href', {
        get: function() {
            return location.href;
        },
        set: function(href) {
            var newHref = beforeRedirect(href);
            if (newHref) {
                location.href = newHref;
            }
        },
    });
}

因为我们不仅实现了location.href的写,location.href的读也一起实现了,所以可以放心大胆的进行全局替换。找到对应前端工程,首先全局搜索window.location.href,批量替换成(window.location2 || window.location).href,然后再全局搜索location.href,批量替换成(window.location2 || window.location).href(思考一下为什么一定是这个顺序呢)。

另外需要注意,有些跳转可能是写在npm包里面的,这种情况只能npm也跟着替换一下了,并没有其它更好办法。

window.open拦截

var tempOpenName = '_rawOpen';
if (!window[tempOpenName]) {
    window[tempOpenName] = window.open;
    window.open = function(url, name, features) {
        url = beforeRedirect(url, true);
        if (url) {
            window[tempOpenName](url, name, features);
        }
    }
}

history.pushState拦截

var tempName = '_rawPushState';
if (!window.history[tempName]) {
    window.history[tempName] = window.history.pushState;
    window.history.pushState = function(state, title, url) {
        url = beforeRedirect(url);
        if (url) {
            window.history[tempName](state, title, url);
        }
    }
}

history.replaceState拦截

var tempName = '_rawReplaceState';
if (!window.history[tempName]) {
    window.history[tempName] = window.history.replaceState;
    window.history.replaceState = function(state, title, url) {
        url = beforeRedirect(url);
        if (url) {
            window.history[tempName](state, title, url);
        }
    }
}

全局loading处理

完成上述步骤后,基本上已经看不出来是iframe了,但是跳转的时候中间有短暂的白屏会有一点顿挫感,体验不算很流畅,这时候可以给iframe加一个全局的loading,开始跳转前显示,页面加载完再隐藏:

B系统:

document.addEventListener('DOMContentLoaded', function (e) {
    postMessage('iframeDOMContentLoaded', { url: location.href });
});

A系统:

window.addEventListener('message', (e) => {
    const { data, type } = e.data || {};
    // iframe 加载完毕
    if (type === 'iframeDOMContentLoaded') {
        this.setState({loading: false});
    }
    if (type === 'beforeHistoryChange') {
        // 此时页面并没有立即跳转,需要再稍微等待一下再显示loading
        setTimeout(() => this.setState({loading: true}), 100);
    }
});

除此之外还需要利用iframe自带的onload加一个兜底,防止iframe页面没有上报 iframeDOMContentLoaded 事件导致loading不消失:

// iframe自带的onload做兜底
iframeOnLoad = () => {
    this.setState({loading: false});
}
render() {
    return <div>
        <Loading visible={this.state.loading} tip="正在加载..." inline={false}>
            <iframe id="microFrontIframe" src={this.state.currentEntry} onLoad={this.iframeOnLoad}/>
        </Loading>
    </div>;
}

还需要注意,当新标签页打开页面时并不需要显示loading,需要注意区分。

弹窗居中问题

当前场景下弹窗个人觉得并不需要处理,因为菜单的宽度有限,不仔细看的话甚至都没注意到弹窗没有居中:

如果非要处理的话也不麻烦,覆盖一下原来页面弹窗的样式,当包含hideLayout参数时,让弹窗的位置分别向左移动menuWidth/2、向上移动navbarHeight/2即可(遮罩位置不能动、也动不了)。

添加了marginLeft=-120pxmarginTop=-30px 后的弹窗效果:

最终效果

其实不难看出,最终效果和SPA几乎无异,而且菜单和导航本来就是无刷新的,页面跳转没有割裂感:

结语

上述方案有几个没有提到的点:

  • 方案成立的前提是建立在2个系统共用一套用户体系,否则需要对2个系统的登录体系进行打通,一般包括账号绑定、A系统默认免登B系统,等等,这需要一定额外的工作量;
  • 参数的透传与删除,例如我希望除了hideLayout参数之外其它URL参数全部在父子页面之间透传;
  • 埋点,数据上报的时候需要增加一个额外参数来标识流量来自另外一个系统;

在第一次摸索方案时可能需要花费一些时间,但是在熟悉之后,如果后续还有类似把B系统接入A系统的需求,在没有特殊情况且顺利的前提下可能花费1-2天时间即可完成,最重要的是大部分工作都是全局生效的,不会随着页面的增多而导致工作量增加,测试回归的成本也非常低,只需要验证所有页面跳转、展示等是否正常,功能本身一般不会有太大问题,而如果是微前端方案的话需要从头到尾全部仔仔细细测试一遍,开发和测试的成本都不可估量。

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

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

相关文章

Linux学习笔记——Tomcat安装部署

5.2、Tomcat安装部署 5.2.1、简介 Tomcat是由Apache开发的一个Servlet容器&#xff0c;实现了对Servlet和JSP的支持&#xff0c;并提供了作为Web服务器的一些特有功能&#xff0c;如Tomcat管理和控制平台、安全域管理和Tomcat阀等。 简单来说&#xff0c;Tomcat是一个WEB应用…

内核解读之内存管理(3)内存管理三级架构之内存区域zone

文章目录1、zone类型2、zone结构体3、zone的初始化流程1、zone类型 NUMA结构下, 每个处理器CPU与一个本地内存直接相连, 而不同处理器之前则通过总线进行进一步的连接, 因此相对于任何一个CPU访问本地内存的速度比访问远程内存的速度要快, 而Linux为了兼容NUMA结构, 把物理内存…

Flink数据流类型之间的转换(WindowedStream、DataStream、KeyedStream、AllWindowStream之间的转换)

Flink提供了一些流API&#xff0c;其中包括WindowedStream、DataStream、KeyedStream和AllWindowStream。 &#x1f34a;WindowedStream是一种特殊的流&#xff0c;其中数据已按时间或数据元素的键进行分组&#xff0c;并且每个分组的数据都在窗口中按时间划分。这意味着&…

2023年出入境政策-喜忧参半

2023年已经到来&#xff0c;随着卫健委公布中国防控新冠措施调整优化以后&#xff0c;出入境政策相应也有了很大变化&#xff0c;知识人网小编概括为喜忧参半。喜的是从国外入境中国不再需要集中隔离&#xff1b;忧的是有些国家对于中国人入境增加了核酸检测要求。下面我们就这…

第一章 Java入门开发

第一章 Java入门开发 目录一&#xff0e; 概述二&#xff0e; JDK1. 概述2. 安装3. JDK目录一&#xff0e; 概述 Java是一门高级程序设计语言&#xff0c;是支持跨平台和完成面向对象的程序设计语言。针对不同的开发市场&#xff0c;sun公司将Java分为Java SE&#xff08;标准版…

关于clip通信架构设计的调研

网络上大部分关于clip-as-service的描述都是关于它如何使用&#xff0c;基于它的编码功能上去计算文本相似度&#xff0c;根据文字推荐图片等等&#xff0c;只有作者的创作思路里面提及通信架构的设计。 作者博客&#xff1a; 链接: link 如何解决多个客户端同时请求服务端的场…

STS4中MVC项目中把log4j从1.x升级到2.x中遇到的两个问题

文章目录问题一 升级后看Maven Dependencies中还是有依赖1.x的log4j问题二 web.xml配置不对项目原来的log4j版本是1.2.14&#xff0c;有漏洞需要升级到2.18.0.问题一 升级后看Maven Dependencies中还是有依赖1.x的log4j 原因是有关联依赖&#xff0c; 项目中别的jar库有依赖低…

【算法笔记】【专题】RMQ 问题:ST表/树状数组/线段树

0. 前言 好久没更算法笔记专栏了&#xff0c;正好学了新算法来更新…… 这也是本专栏的第一个专题问题&#xff0c;涉及到三种数据结构&#xff0c;如果写得有问题请各位大佬多多指教&#xff0c;谢谢&#xff01; 1. 关于 RMQ 问题 RMQ 的全称是 Range Minimum/Maximum Que…

《Linux运维实战:Centos7.6基于docker-compose一键离线部署单节点redis6.2.8 》

一、部署背景 由于业务系统的特殊性&#xff0c;我们需要面向不通的客户安装我们的业务系统&#xff0c;而作为基础组件中的redis针对不同的客户环境需要多次部署&#xff0c;作为一个运维工程师&#xff0c;提升工作效率也是工作中的重要一环。所以我觉得有必要针对redis6.2.8…

使用 .NET 标记游戏地图关键坐标点

本文以天涯明月刀 OL 游戏的云上之城探索玩法为例&#xff0c;介绍如何使用 .NET 在游戏地图中标记大量关键坐标点。 1. 背景 大概很多程序员都是喜欢玩游戏的吧&#xff0c;我也不例外。我们经常会看到电视剧中的各路游戏大神&#xff0c;要么是有只有他一个人会的骚操作&…

Linux--信号--信号的产生方式--核心转储--0104

1. 什么是信号 生活中的信号&#xff1a;红绿灯&#xff0c;狼烟&#xff0c;撤退、集合...。 我们认识这些信号&#xff0c;首先是因为自己记住了对应场景下的信号后续需要执行的动作。如果信号没有产生&#xff0c;我们依旧知道如何处理这个信号。收到信号&#xff0c;我们…

springboot学习(七十八) springboot中通过自定义注解实现数据脱敏的功能

文章目录前言一、引入hutools工具类二、定义常用需要脱敏的数据类型的枚举三、定义脱敏方式枚举四、自定义脱敏的注解五、自定义Jackson的序列化方式六、使用七、脱敏效果前言 对于某些接口返回的信息&#xff0c;涉及到敏感数据的必须进行脱敏操作&#xff0c;例如银行卡号、…

带你了解ssh服务过程

远程连接服务 1、什么是远程连接服务器 远程连接服务器通过文字或图形接口方式来远程登录系统&#xff0c;让你在远程终端前登录linux主机以取得可操作主机接口&#xff08;shell&#xff09;&#xff0c;而登录后的操作感觉就像是坐在系统前面一样。 2、远程连接服务器的功…

【C++】函数重载的使用及原理

概述 在学校里&#xff0c;我们都会有班里同学被起外号的经历&#xff0c;而且同一个人可能还会有好几个外号。 在自然语言中&#xff0c;一个词可以有多重含义&#xff0c;人们可以通过上下文来判断该词真实的含义&#xff0c;即该词被重载了。 目录 概述 什么是函数重载 …

项目管理:如何制作项目进度计划表?

项目进度管理是根据项目目标&#xff0c;编制合理的进度计划&#xff0c;并在项目推进过程中随时检查项目执行情况。 项目进度管理的目的就是为了实现最优工期&#xff0c;多快好省地完成任务。 而甘特图&#xff0c;就是用表格图形的方式来展示项目的进展&#xff0c;是一个比…

赛狐ERP:优秀的亚马逊运营具备的五项能力!

我们都知道&#xff0c;亚马逊运营是整个店铺的主导&#xff0c;很大程度上会影响着一个店铺经营的好坏&#xff0c;那么一个好的亚马逊运营&#xff0c;应该具备哪些能力呢&#xff1f;今天赛狐ERP就来给和大家聊一聊&#xff0c;希望对各位亚马逊运营们会有启发&#xff01;1…

ORB-SLAM2 --- LocalMapping::Run 局部建图线程解析

目录 一、线程作用 二、局部建图线程主要流程 三、局部建图线程主函数 四、调用函数解析 4.1 设置"允许接受关键帧"的状态标志LocalMapping::SetAcceptKeyFrames函数解析 4.2 查看列表中是否有等待被插入的关键帧LocalMapping::CheckNewKeyFrames函数 4.3 …

十分钟学会在linux上部署chrony服务器(再见 NTP,是时候拥抱下一代时间同步服务 Chrony 了)

chrony服务器 Chrony 相较于 NTPD 服务的优势 安装与配置&#xff08;Chrony的配置文件是/etc/chrony.conf&#xff09; 同步网络时间服务器 设置开机启动&#xff0c;重启服务 chronyc sources 输出结果解析 练习 实验模型图如下 实验a如下 实验b如下 再见 NTP&#x…

中国手机市场全面衰退,连苹果也未能幸免,大跌近三成

CINNO公布了11月份国内手机市场的数据&#xff0c;数据显示2022年11月份中国市场的手机出货量同比下滑21.7%&#xff0c;在整体大环境出现销量下滑的情况下&#xff0c;此前曾持续逆势增长的苹果也顶不住了&#xff0c;苹果在中国市场的出货量也出现了下滑的势头。数据显示2022…

06-Alibaba Nacos注册中心源码剖析

Nacos&Ribbon&Feign核心微服务架构图 架构原理 1、微服务系统在启动时将自己注册到服务注册中心&#xff0c;同时外发布 Http 接口供其它系统调用(一般都是基于SpringMVC) 2、服务消费者基于 Feign 调用服务提供者对外发布的接口&#xff0c;先对调用的本地接口加上注…